Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The most useful technical references are:
- [Repository channels](docs/channels.md)
- [Storage and persistence](docs/storage.md)
- [Hosted agent (Planner)](docs/hosted-agent.md)
- [Planner link wire format](docs/planner-link.md)
- [Background jobs and workers](docs/background-jobs.md)
- [Experimental implementation lifecycle](docs/implementation-lifecycle.md)
- [Self-hosting](docs/self-hosting.md)
Expand Down Expand Up @@ -62,20 +63,21 @@ Before opening a PR or repairing CI, use the repository's

## Repository map

| Area | Responsibility | Internal workspace dependencies |
| --------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `packages/dialect` | Restricted MDX, MDAST, and Lexical schema | none |
| `packages/protocol` | WebSocket declarations and addressing helper | none |
| `packages/question` | Questionnaire definitions and shared drafts | `protocol` |
| `packages/draft` | Bounded collaborative plain-text drafts | none |
| `packages/viewport` | Browser geometry and subscriptions | none |
| `packages/diagrams` | Bounded diagram rendering and scoped React viewing | `icons` (React peer) |
| `packages/experiment` | Investigation result schemas, selections and native views | `diagrams` (React peer) |
| `apps/connector` | Local ACP client and run-scoped MCP bridge | `experiment`, `protocol` |
| `packages/editor` | Collaborative editor, decisions, comments, and widgets | `diagrams`, `dialect`, `experiment`, `question`, `protocol`, `viewport` |
| `apps/server` | Auth, channels, rooms, storage, Planner, MCP, tasks | `diagrams`, `dialect`, `draft`, `experiment`, `question`, `protocol` |
| `apps/web` | Repository picker, navigation, conversation, workspace | `dialect`, `diagrams`, `draft`, `editor`, `experiment`, `protocol`, `viewport` |
| `e2e` | Browser and system integration harness | may import server internals as fixtures |
| Area | Responsibility | Internal workspace dependencies |
| ----------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `packages/dialect` | Restricted MDX, MDAST, and Lexical schema | none |
| `packages/protocol` | WebSocket declarations and addressing helper | none |
| `packages/planner-link` | Remote Planner link messages, schemas and test client | none |
| `packages/question` | Questionnaire definitions and shared drafts | `protocol` |
| `packages/draft` | Bounded collaborative plain-text drafts | none |
| `packages/viewport` | Browser geometry and subscriptions | none |
| `packages/diagrams` | Bounded diagram rendering and scoped React viewing | `icons` (React peer) |
| `packages/experiment` | Investigation result schemas, selections and native views | `diagrams` (React peer) |
| `apps/connector` | Local ACP client and run-scoped MCP bridge | `experiment`, `protocol` |
| `packages/editor` | Collaborative editor, decisions, comments, and widgets | `diagrams`, `dialect`, `experiment`, `question`, `protocol`, `viewport` |
| `apps/server` | Auth, channels, rooms, storage, Planner, MCP, tasks | `diagrams`, `dialect`, `draft`, `experiment`, `planner-link`, `question`, `protocol` |
| `apps/web` | Repository picker, navigation, conversation, workspace | `dialect`, `diagrams`, `draft`, `editor`, `experiment`, `protocol`, `viewport` |
| `e2e` | Browser and system integration harness | may import server internals as fixtures |

Runtime workspace packages do not depend on an application. E2E and skill
contract tests may deliberately import server internals; do not treat those test
Expand Down Expand Up @@ -135,6 +137,15 @@ collaborative state and external implementation runs are durable.
remembered per channel in memory, or else an empty per-channel directory. Its
summary and research workers stay isolated. There is no flag for this; the
harness is the choice.
- **`HARNESS=remote` moves the model, not the authority.** Turns run in a client
attached at `/planner-link` with a GitHub bearer and push access; Chopin's
host tools, Decisions, and room writes still run in the server. The attached
client sees what a turn sees and steers its document writes. Attaching makes
the account's live browser login the document's Planner owner, one client per
document; nothing else claims ownership. `@chopin` and `invoke_planner` on a
document without one fail with `planner-not-attached`. Signing out the
owner's login, its expiry, or a Planner reset closes the link. See
[Hosted agent](docs/hosted-agent.md#remote-planner).
- **Repository node IDs are authoritative.** Owner and repository names resolve
GitHub requests but never replace the stored node identity.
- **Persistence should precede publication.** Do not acknowledge or broadcast a
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ COPY packages/draft/package.json ./packages/draft/package.json
COPY packages/editor/package.json ./packages/editor/package.json
COPY packages/experiment/package.json ./packages/experiment/package.json
COPY packages/icons/package.json ./packages/icons/package.json
COPY packages/planner-link/package.json ./packages/planner-link/package.json
COPY packages/protocol/package.json ./packages/protocol/package.json
COPY packages/question/package.json ./packages/question/package.json
COPY packages/viewport/package.json ./packages/viewport/package.json
Expand Down Expand Up @@ -49,6 +50,7 @@ COPY --from=production-dependencies --chown=bun:bun /app/apps/server/node_module
COPY --from=production-dependencies --chown=bun:bun /app/packages/dialect/node_modules ./packages/dialect/node_modules
COPY --from=production-dependencies --chown=bun:bun /app/packages/draft/node_modules ./packages/draft/node_modules
COPY --from=production-dependencies --chown=bun:bun /app/packages/experiment/node_modules ./packages/experiment/node_modules
COPY --from=production-dependencies --chown=bun:bun /app/packages/planner-link/node_modules ./packages/planner-link/node_modules
COPY --from=production-dependencies --chown=bun:bun /app/packages/question/node_modules ./packages/question/node_modules
COPY --chown=bun:bun package.json bun.lock ./
COPY --chown=bun:bun apps/server ./apps/server
Expand Down
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@chopin/diagrams": "workspace:*",
"@chopin/draft": "workspace:*",
"@chopin/experiment": "workspace:*",
"@chopin/planner-link": "workspace:*",
"@chopin/protocol": "workspace:*",
"@chopin/question": "workspace:*",
"@github/copilot": "catalog:copilot",
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/agent/planner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ test("ordinary and Atomic guidance prefers native diagrams with a bounded Mermai
}
});

test("a remote Planner learns where it runs and that its questions become Decisions", () => {
let prompt = plannerInstructions("octo-org/score", "Earlier context.", { remote: true })
.replace(/\s+/g, " ");
expect(prompt).toContain("You run in a member's own session on their machine");
expect(prompt).toContain("questions appear to the document's members as Decisions");
expect(prompt).toContain("**Stop Planner** pauses it");
expect(prompt).not.toContain("You have no shell");
expect(prompt).not.toContain("Your working directory");
expect(prompt.endsWith("Earlier context.")).toBe(true);
});

test("ordinary guidance supplies an executable numerical chart example", () => {
let examples = [...DIAGRAM_AUTHORING.matchAll(/```seecode\n([^`]+)\n```/g)]
.map(match => JSON.parse(match[1]!));
Expand Down
27 changes: 19 additions & 8 deletions apps/server/src/agent/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,13 @@ the plan, use the \`detach_question\` operation rather than deleting the block.`
const ROUTED_PROMPT = PROMPT.replace(DIAGRAM_INTRO, ROUTED_DIAGRAM_INTRO)
.replace(DIAGRAM_AUTHORING, ROUTED_DIAGRAM_AUTHORING);

/** Where a remote Planner's model and own tools run: the attached member's machine. */
export type RemoteWorkspace = { remote: true };

export function plannerInstructions(
repository: string,
bootstrap?: string,
workspace?: PlannerWorkspace,
workspace?: PlannerWorkspace | RemoteWorkspace,
visualRouting = false,
): string {
let visual = visualRouting
Expand Down Expand Up @@ -337,13 +340,6 @@ add a table or diagram without its route.`
and cannot change GitHub. Ground the plan in what those reading tools return.`;
return [prompt, visual, reading, isolated, bootstrap].filter(Boolean).join("\n\n");
}
let place = workspace.checkout
? `Your working directory, ${workspace.cwd}, is a local checkout of ${repository}
verified against its origin. Its branch and working tree may differ from what the
repository tools read.`
: `Your working directory, ${workspace.cwd}, is a scratch directory Chopin keeps for
this document. It is not a checkout and holds no repository files, so read
${repository} through the repository tools.`;
let questions =
`\`ask_user_question\` and \`workflow\` questions appear to the document's members as
Decisions. If one expires unanswered, proceed on your best judgement and say what you assumed.`;
Expand All @@ -354,6 +350,21 @@ tool result suggests it. When you start a workflow, say in plain words what it w
where to follow it: Chat shows a card for each run with its stages and status, its questions
appear under Decisions, **Stop Planner** pauses it, and **Resume Planner** resumes it. Offer to
check on or steer a run yourself with your \`workflow\` and \`intercom\` tools when someone asks.`;
if ("remote" in workspace) {
let remote = `You run in a member's own session on their machine, attached to this document.
Your own tools act on that machine and are not visible to the document's other members. The
document changes only through this document's tools, and ${repository} is read through the
repository tools, whatever that machine holds.`;
return [prompt, visual, reading, remote, questions, surface, bootstrap].filter(Boolean)
.join("\n\n");
}
let place = workspace.checkout
? `Your working directory, ${workspace.cwd}, is a local checkout of ${repository}
verified against its origin. Its branch and working tree may differ from what the
repository tools read.`
: `Your working directory, ${workspace.cwd}, is a scratch directory Chopin keeps for
this document. It is not a checkout and holds no repository files, so read
${repository} through the repository tools.`;
let implementing =
`Your own tools read files and the web but cannot edit files or run commands, so
you do not implement the plan yourself. If a member asks to implement it
Expand Down
14 changes: 14 additions & 0 deletions apps/server/src/chat/invoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,20 @@ test("a run that ends blocked during the Planner's own turn starts no follow-up
expect(spoken(context, "plan-review ended blocked after 10 min.")).toBe(1);
});

test("a remote Planner's run that ends blocked between turns starts no turn nobody asked for", async () => {
let { context, user } = await setup(configured(ATOMIC));
let planner = runningSession();
let remote = Object.assign(planner.session, { followsUpRuns: false });
context.openPlannerSession = async () => ({ ok: true, value: remote });
await Chat.invoke(context, user, "Run the workflow");
await context.chat.running;
expect(context.chat.retained).toBeDefined();
planner.set({ active: [], paused: [], cards: [card("blocked")] });
while (!planner.calls.includes("destroy")) await tick();
expect(followUps(planner.calls)).toHaveLength(0);
expect(spoken(context, "plan-review ended blocked after 10 min.")).toBe(1);
});

test("the last job finishing while a reply is still being saved still lets the Planner go afterwards", async () => {
let { context, user } = await setup(configured(ATOMIC));
let planner = runningSession();
Expand Down
Loading
Loading