passalong 0.20.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp.js CHANGED
@@ -1,33 +1,22 @@
1
- // The Passalong MCP server (stdio). This is the rail that makes Passalong tool-agnostic: any
2
- // MCP-capable agent can search, pull, and publish guides without leaving its session. With a
3
- // team, the same tools see the team's guides and the user's inbox.
1
+ // The Passalong MCP server (stdio). Any MCP-capable agent can take work, hand it in, write work
2
+ // down, and read and write a project's documents without leaving its session.
4
3
  //
5
4
  // claude mcp add passalong -- passalong mcp
5
+ //
6
+ // Seventeen tools, four nouns: work, project, document, file (docs/WORK.md). The same tools are
7
+ // served over HTTP by apps/api/src/mcp-http.ts; the two say the same things in the same words.
6
8
  import { readFileSync } from "node:fs";
7
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
11
  import { z } from "zod";
10
12
  import * as api from "./api.js";
11
13
  import { refusal, runChecks } from "./checks.js";
12
- import {
13
- AREAS,
14
- BUG_SECTIONS,
15
- GUIDE_WARN,
16
- outlineText,
17
- parse,
18
- SECTIONS,
19
- section as sectionOf,
20
- sizeWarning,
21
- TASK_SECTIONS,
22
- template,
23
- } from "./guide.js";
24
14
  import * as passalong from "./passalong.js";
15
+ import { outlineText, parse, section as sectionOf, serialize, WORK_WARN } from "./work.js";
25
16
 
26
17
  /**
27
18
  * What the server calls itself in the MCP handshake — read from package.json rather than written
28
- * here, because a literal drifts. This one said 0.2.0 for two releases while the package was
29
- * 0.2.2, and the one thing a version string in a handshake is for is telling a client which build
30
- * it is talking to. `pnpm release` bumps package.json, so there is only one number to bump.
19
+ * here, because a literal drifts. `pnpm release` bumps package.json, so there is one number.
31
20
  */
32
21
  const VERSION = JSON.parse(
33
22
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
@@ -36,42 +25,36 @@ const VERSION = JSON.parse(
36
25
  const text = (s) => ({ content: [{ type: "text", text: s }] });
37
26
 
38
27
  /**
39
- * Said in front of every guide an agent opens, whatever its kind: the hand-in needs evidence, and
40
- * evidence is collected while the work happens, not reconstructed from memory once it is done.
41
- * Mirrors KEEP_EVIDENCE in apps/api/src/mcp-http.ts.
28
+ * Where the line `add_file` returns goes, which is not always new work. Proof of a hand-in belongs
29
+ * on the hand-in: six screenshots of a delivered change were once published as a document with six
30
+ * sections wrapped round them, because the upload tool named only one destination.
31
+ * Mirrors WHERE_THE_LINE_GOES in apps/api/src/mcp-http.ts.
42
32
  */
43
33
  const WHERE_THE_LINE_GOES =
44
- "WHERE THE LINE GOES DEPENDS ON WHAT THE IMAGE IS EVIDENCE OF. Proof that work you are handing i" +
45
- "n holds — a screen that renders right, a total that matches — goes in that check's `ran` on han" +
46
- "d_in, and nothing is published: a check answered with a picture is shown, which is what the han" +
47
- "d-in asks for. Only an image that is part of a DOCUMENT goes in a guide body — the screenshot i" +
48
- "n a bug report, a diagram a guide is explaining — and then it must be in the markdown, because " +
49
- "a guide travels as markdown to whoever holds its link and evidence beside the document does not" +
50
- " travel at all; publishing claims whatever the markdown names, so attach first and publish afte" +
51
- "r. DO NOT PUBLISH A GUIDE TO CARRY SCREENSHOTS. Six images and a caption each is a hand-in, not" +
52
- " a document, and wrapping them in Problem / Solution shape / Decisions / Steps / Verification t" +
53
- "o make them publishable is how a set of pictures becomes fifteen hundred words nobody asked for" +
54
- ". png, jpg, webp or gif.";
34
+ "WHERE THE LINE GOES DEPENDS ON WHAT THE FILE IS EVIDENCE OF. Proof that work you are handing in " +
35
+ "holds — a screen that renders right, a total that matches — goes in that check's `ran` on " +
36
+ "hand_in, and nothing new is written: a check answered with a picture is shown, which is what " +
37
+ "the hand-in asks for. A picture that is part of the work itself — the screenshot of a bug, a " +
38
+ "diagram it explains — goes in its text (create_work, or update_work), because work travels as " +
39
+ "its text and a file beside it does not travel at all. DO NOT WRITE NEW WORK TO CARRY " +
40
+ "SCREENSHOTS: six images and a caption each is a hand-in. With `project`, the file is kept in " +
41
+ "that project instead. Pictures (png, jpg, webp, gif) are drawn where they are named; anything " +
42
+ "else is a file to download.";
55
43
 
56
44
  /**
57
- * What a screenshot has to be a screenshot OF.
58
- *
59
- * "A check is run or shown" made a picture mandatory, and an agent short of one built a page to
60
- * photograph: it added `src/app/notes-preview`, shot that, then `rm -rf`'d it and published the
61
- * shots as evidence that the real screen rendered. The rule was satisfied and almost nothing was
62
- * proved — the measure had become the target, and the page the picture showed no longer existed.
63
- *
45
+ * What a screenshot has to be a screenshot OF. An agent short of a picture once built a page to
46
+ * photograph, shot it, deleted it, and offered the shots as evidence the real screen rendered.
64
47
  * Mirrors SHOOT_THE_REAL_THING in apps/api/src/mcp-http.ts.
65
48
  */
66
49
  const SHOOT_THE_REAL_THING =
67
50
  "SHOOT THE RUNNING THING, NOT SOMETHING YOU BUILT TO SHOOT. A screenshot is worth what the sc" +
68
51
  "reen behind it is worth: the real route in the real app, reached the way a reader would reac" +
69
52
  "h it. A scratch page, a story, a harness or a component rendered on its own is a picture of " +
70
- "your scaffolding, and it can look right while the thing the guide is about does not. If the " +
53
+ "your scaffolding, and it can look right while the thing the work is about does not. If the " +
71
54
  "only way you could get a picture was to build something, that is not evidence the real scree" +
72
55
  "n works — say what you built and why in `writeup`, and leave the check to the commands that " +
73
56
  "do hold. NEVER DELETE WHAT YOUR EVIDENCE POINTS AT: a shot of a page you removed afterwards " +
74
- "is one nobody can take again, including you.";
57
+ "is one nobody can take again, including you.\n\n";
75
58
 
76
59
  const KEEP_EVIDENCE =
77
60
  "KEEP YOUR EVIDENCE AS YOU GO. hand_in needs it: the commands you ran and what came back, the " +
@@ -79,15 +62,9 @@ const KEEP_EVIDENCE =
79
62
  "writing from memory, which is the thing evidence is here to replace.\n\n";
80
63
 
81
64
  /**
82
- * Said in front of every guide an agent has to locate things in for itself — a bug and a task.
83
- *
84
- * The failure it is here to stop: an agent lists one directory, or opens the one file it expected,
85
- * and concludes from that. It then reports that a thing is absent, or that it found the only
86
- * occurrence, having never looked past the place it happened to start. The conclusion is confident
87
- * and wrong, and nothing downstream can tell, because the evidence it brings is real — it is just
88
- * evidence of a search that was too small.
89
- *
90
- * Mirrors SEARCH_WIDE in apps/api/src/mcp-http.ts.
65
+ * The failure this stops: an agent lists one directory, or opens the one file it expected, and
66
+ * reports that a thing is absent, or that it found the only occurrence. Confident and wrong, and
67
+ * nothing downstream can tell. Mirrors SEARCH_WIDE in apps/api/src/mcp-http.ts.
91
68
  */
92
69
  const SEARCH_WIDE =
93
70
  "SEARCH THE WHOLE TREE BEFORE YOU CONCLUDE. Listing a directory, or reading the one file you " +
@@ -96,77 +73,12 @@ const SEARCH_WIDE =
96
73
  'already handled, search across the repository and let the output be your reason. "I looked" ' +
97
74
  "is a claim; the search and what it printed is evidence.\n\n";
98
75
 
99
- /**
100
- * What to say in front of a guide whose kind changes what the reader should do with it.
101
- *
102
- * The heading text is what an agent keys on, and a bug's or a task's headings are close enough to
103
- * a transfer guide's to be treated as one by an agent that never opened the frontmatter. So the
104
- * kind is stated before the document, in the imperative. A transfer guide needs nothing: following
105
- * it is what an agent does with an unmarked document anyway.
106
- */
107
- function leadFor(meta) {
108
- if (meta.kind === "bug")
109
- return (
110
- "THIS IS A BUG REPORT, NOT WORK TO REPEAT. Do not follow Reproduce as instructions " +
111
- "— those steps produce the defect. Fix what Problem describes, then check " +
112
- "Verification and answer with hand_in.\n\n" +
113
- SEARCH_WIDE +
114
- SHOOT_THE_REAL_THING +
115
- KEEP_EVIDENCE
116
- );
117
- if (meta.kind === "task")
118
- return (
119
- "THIS IS A TASK: WORK NOBODY HAS DONE YET. There are no Steps to follow — work out how to " +
120
- "reach Goal within Constraints, and leave Out of scope alone. It is done when every check " +
121
- "under Acceptance holds. Opening it here does not make it yours: to work on it, call take " +
122
- "with its id in the repo it is for, so no other agent can. Then progress, and hand_in " +
123
- "with a write-up.\n\n" +
124
- SEARCH_WIDE +
125
- SHOOT_THE_REAL_THING +
126
- KEEP_EVIDENCE
127
- );
128
- // A transfer guide is handed over untouched: it is a document to follow, and anything in front
129
- // of it is one more thing that is not the document. The reminder still reaches the agent on the
130
- // answer's `next` line, which is where every other instruction from the server rides.
131
- return "";
132
- }
76
+ /** Said in front of every piece of work an agent takes. Mirrors LEAD in apps/api/src/mcp-http.ts. */
77
+ const LEAD = SEARCH_WIDE + SHOOT_THE_REAL_THING + KEEP_EVIDENCE;
133
78
 
134
79
  /**
135
- * The rest of the report this issue came from.
136
- *
137
- * A QA pass files six bugs at once, and they arrive in an inbox as six unrelated guides — which
138
- * is how the same root cause gets fixed three times, or how someone fixes one of a pair and calls
139
- * the sweep done. Naming the siblings costs one request and only happens for a guide that has a
140
- * parent. A failure here is not worth failing the pull over: the issue itself is what was asked
141
- * for, and it is already in hand.
142
- */
143
- async function related(meta) {
144
- if (!meta.report) return "";
145
- try {
146
- const { report } = await api.report(meta.report);
147
- const others = (report.areas || [])
148
- .flatMap((a) => a.issues.map((i) => ({ ...i, area: a.area })))
149
- .filter((i) => i.id !== meta.id);
150
- if (!others.length) return "";
151
- const lines = others.map(
152
- (i) => ` ${i.id} [${i.severity || "--"}] ${i.area || "no area"} — ${i.title}`,
153
- );
154
- return (
155
- `\n\n<!-- passalong: filed with ${others.length} other issue${others.length === 1 ? "" : "s"}` +
156
- `${report.title ? ` in "${report.title}"` : ""}. Fix only this one unless asked; the others are\n` +
157
- `${lines.join("\n")}\n-->`
158
- );
159
- } catch {
160
- return "";
161
- }
162
- }
163
- /**
164
- * An answer a client can read as data as well as prose.
165
- *
166
- * `structuredContent` is the same JSON the text block holds; the spec asks for both, because a
167
- * tool "that returns structured content SHOULD also return the serialized JSON in a TextContent
168
- * block" for clients that only read text. Only an object goes in the structured field — the spec
169
- * allows any JSON value there, but a bare array or string tells a client nothing it could key on.
80
+ * An answer a client can read as data as well as prose. The spec asks for both: structured content
81
+ * "SHOULD also return the serialized JSON in a TextContent block" for clients that only read text.
170
82
  */
171
83
  const json = (data) => ({
172
84
  ...text(JSON.stringify(data, null, 2)),
@@ -174,52 +86,25 @@ const json = (data) => ({
174
86
  });
175
87
 
176
88
  /**
177
- * What each tool does to the world, said out loud. Mirrors the same block in
178
- * apps/api/src/mcp-http.ts, which had these and this server did not — the same four jobs described
179
- * to a client two different ways, on the surface most agents actually reach.
180
- *
181
- * MCP's defaults for a tool that says nothing are the worst case — it writes, it may destroy, it
182
- * reaches outside — and a client acts on them: over HTTP, ChatGPT badged every tool here, `inbox`
183
- * included, as a destructive public write. The spec is equally clear that these are hints and
184
- * nothing more: "clients MUST consider tool annotations to be untrusted unless they come from
185
- * trusted servers". They are a description, not a permission.
186
- *
187
- * `openWorldHint` is false for everything that stays inside Passalong. Only attach_screenshot
188
- * reaches outside it, for a file on this machine that Passalong did not put there.
189
- */
190
- /**
191
- * What the three verbs that answer with JSON return. Declared only for those: `outputSchema` puts
192
- * the obligation on the server — "Servers MUST provide structured results that conform to this
193
- * schema", while a client only SHOULD check — so it is a promise, and promising a shape this
194
- * server does not control would be worse than staying quiet. `take` is the reason for the rule
195
- * rather than an exception to it: it answers with the guide's markdown and a lead-in to read, not
196
- * with a record.
197
- *
198
- * Every field is optional and the objects are open. The four calls serve three kinds of guide and
199
- * the routes add fields freely, so a required key here would be a promise broken by a kind that
200
- * does not carry it.
89
+ * What the four calls that answer with a record return. Declared only for those: `outputSchema`
90
+ * puts a MUST on the server, so it is a promise, and promising a shape this server does not
91
+ * control would be worse than staying quiet. Every field is optional and the objects are open —
92
+ * the object itself goes to registerTool, never its `.shape`, which is rebuilt closed and
93
+ * advertised as `additionalProperties: false`.
201
94
  */
202
- // The object itself is handed to registerTool, not its `.shape`: a shape is rebuilt into a closed
203
- // object and advertised as `additionalProperties: false`, which would be this server promising that
204
- // an answer carries nothing else — and then breaking that promise the first time a route adds a
205
- // field. Passing the object through keeps it open.
206
95
  const openObject = (shape) => z.object(shape).partial().passthrough();
207
- const progressOut = openObject({
208
- id: z.string(),
209
- lease_until: z.string(),
210
- note: z.string(),
211
- });
212
- const handInOut = openObject({
213
- id: z.string(),
214
- state: z.string(),
215
- report: z.string(),
216
- ok: z.boolean(),
217
- });
96
+ const progressOut = openObject({ id: z.string(), lease_until: z.string(), note: z.string() });
97
+ const handInOut = openObject({ id: z.string(), state: z.string() });
218
98
  const passOut = openObject({ id: z.string(), passed: z.boolean() });
99
+ const askOut = openObject({ id: z.string(), lease_until: z.string() });
219
100
 
101
+ /**
102
+ * What each tool does to the world, said out loud. MCP's defaults for a tool that says nothing are
103
+ * the worst case, and clients act on them. These are hints, not permissions.
104
+ */
220
105
  const READS = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
221
106
  const ADDS = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
222
- // Removes something that cannot be brought back. Only a person's request should lead here.
107
+ // Removes or replaces something that cannot be brought back. Only a person's request should lead here.
223
108
  const REMOVES = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
224
109
  const fail = (err) => ({ content: [{ type: "text", text: err.message }], isError: true });
225
110
 
@@ -231,8 +116,7 @@ export function nextNote({ next = [], say = "" } = {}, id = "") {
231
116
  const lines = next.map(
232
117
  (s) =>
233
118
  ` ${s.tool}${id && s.tool !== "take" ? ` ${id}` : ""} — when ${s.when} (${s.why})` +
234
- // What the call has to carry, under the call it belongs to: an agent that reads this at the
235
- // end of a long session should not have to remember hand_in takes evidence.
119
+ // What the call has to carry, under the call it belongs to.
236
120
  (s.with ? `\n with ${s.with}` : ""),
237
121
  );
238
122
  if (say) lines.unshift(` ${say}`);
@@ -241,8 +125,7 @@ export function nextNote({ next = [], say = "" } = {}, id = "") {
241
125
 
242
126
  /**
243
127
  * What the person said to this agent since it last heard, in front of everything else: a reply is
244
- * the thing the agent stopped for, and burying it under the guide it already knows is how it gets
245
- * missed. See claims.deliver() in apps/api/src/claims.ts.
128
+ * the thing the agent stopped for. See claims.deliver() in apps/api/src/claims.ts.
246
129
  */
247
130
  export function repliesNote(replies = []) {
248
131
  if (!Array.isArray(replies) || !replies.length) return "";
@@ -263,6 +146,22 @@ const failWith = (err) => {
263
146
  };
264
147
  };
265
148
 
149
+ /** One call, its answer as JSON, its refusal as an error. */
150
+ const run = (fn) => async (args) => {
151
+ try {
152
+ return json(await fn(args));
153
+ } catch (err) {
154
+ return fail(err);
155
+ }
156
+ };
157
+
158
+ const CWD = z
159
+ .string()
160
+ .optional()
161
+ .describe("the worktree you are working in; default is the server's cwd");
162
+
163
+ const STATUS = { ready: "ready", draft: "draft", archived: "done" };
164
+
266
165
  /**
267
166
  * The server, built but not connected. Split out so the tool surface can be read without a
268
167
  * transport — `test/mcp-surface.test.js` lists the tools over an in-memory pair and checks what
@@ -273,143 +172,75 @@ export function buildServer() {
273
172
  { name: "passalong", version: VERSION },
274
173
  {
275
174
  instructions:
276
- "Passalong hands work between contexts as guides: markdown with frontmatter, in three " +
277
- "kinds, and `kind:` in the frontmatter says which. READ IT BEFORE ACTING — each asks " +
278
- "for different behaviour. Every kind is worked with the same four calls:\n" +
279
- "take (with an id, or none for the next thing waiting for you) says you are doing it and " +
280
- "returns it; nobody else can take it here while you hold it, and its sender sees you are " +
281
- "on it. progress, with a one-line note at each milestone — 30 minutes of silence marks it " +
282
- "stalled. hand_in when done here. pass, with the reason, when it is not yours or you are " +
283
- "stuck. ask, with a question, when you need an answer from the person: you keep what you " +
284
- "hold, stop and wait, and take it again once they have replied. Every answer ends with " +
285
- "`next`: what to call now. Follow it, and when it says to " +
286
- "stop, stop. OPEN A GUIDE YOU MEAN TO ACT ON WITH take, NOT get_guide: get_guide only " +
287
- "reads, and the sender learns nothing. If take says somebody else has it, tell the user " +
288
- "instead of doing the work twice.\n" +
175
+ "Passalong keeps a team's work and what it knows, as four things. WORK is something " +
176
+ "someone must still do: one thing, with no kinds, sections or templates — a title, a " +
177
+ "summary for a person, and whatever text, pictures and files the next reader needs, in " +
178
+ "whatever shape fits. Read it and work out what done means; nothing tells you in advance. " +
179
+ "A PROJECT is a place for one product or body of work; its DOCUMENTS are what to know " +
180
+ "about it — a README saying what it is, who uses it and how it is built, a page per area, " +
181
+ "a Log of what changed. FILES are pictures and other files, on work or in a project.\n" +
182
+ "DOING WORK. take (with an id, or none for the next thing waiting here) says you are doing " +
183
+ "it and returns it; nobody else can take it while you hold it, and whoever sent it sees " +
184
+ "you are on it. progress, with a one-line note at each milestone — 30 minutes of silence " +
185
+ "marks it stalled. hand_in when it is done. pass, with the reason, when it is not yours or " +
186
+ "you are stuck. ask, with a question, when you need the person's answer: you keep it, stop " +
187
+ "and wait, and take it again once they have replied. Every answer ends with `next`: what " +
188
+ "to call now. Follow it, and when it says to stop, stop. OPEN WORK YOU MEAN TO DO WITH " +
189
+ "take, NOT find_work: find_work only reads, and nobody learns you are on it. If take says " +
190
+ "somebody else has it, tell the user instead of doing the work twice.\n" +
289
191
  "EVERY HAND-IN CARRIES EVIDENCE: what you ran and what came back — the command and the " +
290
192
  "lines that decided it, a test summary, a link to the change, or a screenshot url. " +
291
193
  "Collect it as you work rather than writing it from memory at the end. hand_in without " +
292
- "it is refused, because the write-up and the verdict are both your word for your own " +
293
- "work and evidence is the part the person reviewing it can check.\n" +
294
- "EVERY GUIDE SAYS WHAT IT IS TO A PERSON. The document is for agents: exact, dense, full of " +
295
- "ids and paths. Put `summary:` in its frontmatter — one or two plain sentences, 400 " +
296
- "characters at most, no ids, paths or jargon, saying what this is and whether anybody has " +
297
- "to act. It is what the person sees first in their hub; the document is behind it. A guide " +
298
- "with no summary is refused.\n" +
299
- "SAY WHICH KIND IT IS. There is no default: a guide with no `kind:` line is refused, and so " +
300
- "is a spelling that is not one of the three. A NEW GUIDE IS READY: it reaches whoever it is " +
301
- "for at once. Say `status: draft` only when the person should read it first — something you " +
302
- "are unsure of — and tell them; plan_tasks drafts for you. Every kind moves the same way — " +
303
- "Ready, taken, handed in, approved. " +
304
- `kind: task is work nobody has done yet. Sections: ${TASK_SECTIONS.join(", ")}. ` +
305
- "It has no Steps: work out how to reach Goal within Constraints, leave Out of scope " +
306
- "alone, and treat Acceptance as the definition of done. A proposal awaiting a team decision " +
307
- "stays Draft until a person makes it Ready. hand_in when every Acceptance line holds: it is " +
308
- "final — the task goes to Done with what you showed, and nobody reviews it after you — so " +
309
- "show it, and say anything you had to adapt in `writeup` (500 characters).\n" +
310
- `kind: transfer (or no kind: line) is a finished implementation to repeat here. Sections: ${SECTIONS.join(", ")}. ` +
311
- "Follow its Steps, adapting anything marked ASSUMES to this codebase; run its " +
312
- "Verification, then hand_in with ok and, when it failed, a note saying what went wrong.\n" +
313
- `kind: bug is a defect to FIX here. Sections: ${BUG_SECTIONS.join(", ")}. ` +
314
- "It has no Steps and nothing in it is a procedure to apply: Reproduce is how to see the " +
315
- "bug and running it produces the bug, Verification is the behaviour that should have " +
316
- "happened. Fix the defect, then check Verification and hand_in — ok true once the " +
317
- "behaviour it describes actually holds. A bug report is not broken because you " +
318
- "reproduced it. " +
319
- "When you find defects you are not fixing — a test run, a QA pass, a review — call " +
320
- "file_bugs with all of them at once; each becomes a guide someone can take on its own. " +
321
- "AN IMAGE THE USER SHOWED YOU IS EVIDENCE, NOT CONTEXT. Before filing or publishing, " +
322
- "attach it with attach_screenshot and pass what it returns as `evidence` — a screenshot " +
323
- "you described instead of attaching is the most useful thing in the report, thrown away. " +
324
- "A guide already filed without one is not stuck: get_guide it, add the markdown line to " +
325
- "the body, and publish_guide the same id — publishing claims whatever the markdown names. " +
326
- "\n" +
327
- "When the user asks you to write a task, or to queue work for an agent, call plan_tasks — " +
328
- "one step for one task — with every section filled from the conversation and the code, " +
329
- "Acceptance as checks a person can run; it fills in the repo you are in. When the user " +
330
- "asks you to plan or break down a larger goal, call plan_tasks with the steps in order, " +
331
- "`after` naming the earlier steps each needs. Tasks land in Draft, not the queue: tell the " +
332
- "user to read them and run `passalong ready <id>` or `passalong ready --all`.\n" +
333
- "When the user asks to pass along, hand off, or share what was just done, distill the " +
334
- "session into a guide (guide_template kind transfer shows the shape) and call " +
335
- "publish_guide, with `to` as team, team/@handle for one teammate, or team/#group for the " +
336
- "people who do a thing. " +
337
- "BEFORE PUBLISHING ANYTHING, CHECK WHAT IS ALREADY OPEN: call board (it lists what this " +
338
- "worktree holds) or read the inbox. If this session's work answers something you hold, hand_in " +
339
- "that — never publish a second guide about it. A transfer guide is for work that has to " +
340
- "cross a boundary: another repo, another machine, a teammate without your branch. Work you " +
341
- "committed and pushed where the team can already see it has crossed no boundary, and a " +
342
- "guide about it is one more thing for somebody to read and review; what is worth " +
343
- "publishing from a session like that is what is still open, as a bug or a task. " +
344
- '"Update the passalong", "add this to passalong" and the like are ambiguous — hand in ' +
345
- "what you hold, add a follow-up, or publish something new? Ask which, in one line, rather " +
346
- "than publishing and leaving the user to undo it. " +
347
- "At the start of work, inbox shows guides teammates have handed to this user — take one " +
348
- "to start it — and activity shows whether the guides they handed off have landed. When " +
349
- "the user asks what they have been working on, or wants a standup or a summary of a " +
350
- "period, call log — but say that it holds what they passed along and not everything they " +
351
- "did. Gotchas are the highest-value section: record what failed and why.\n" +
352
- "A FOLDER IS A PLACE; ITS DOCUMENTS ARE WHAT TO KNOW; GUIDES ARE WHAT TO DO. Sort every " +
353
- "item before writing it into a folder. If someone still has to act on it — a defect that " +
354
- "is still open, work not yet done, a check nobody has run — it is a guide: kind bug for " +
355
- "something broken, kind task for work to do. Publish it and link_folder_guide it into the " +
356
- "folder; a guide has an owner, a state and an answer. A document has none of those and " +
357
- "tells nobody, so open work recorded only as a document is lost. If it is what someone " +
358
- "needs to know — an overview, a decision, a finding, something already fixed with its " +
359
- "commit — it is a document. An incident is both: one overview document, a document per " +
360
- "resolved finding, and a bug or task guide per open item, all in the incident's folder. " +
361
- "WHERE A FOLDER LIVES is in the `space` field of create_folder and get_folder: private, " +
362
- "or a named team. Tell the person that, never what you meant to do; a folder made without " +
363
- "`team` is private, and a subfolder takes its parent's space.\n" +
364
- "FOLDERS ARE REUSABLE PROJECT CONTEXT, NOT GUIDES. When the user asks for a folder for a " +
365
- "script, brief, assets or work spanning several guides, call list_folders first to avoid " +
366
- "making a duplicate. If none fits, call create_folder with a title and short description. " +
367
- "Use its parent id to nest a subfolder at any depth; keep a related project together. " +
368
- "A folder's color can mark an area, and update_folder can recolor or move it later. " +
369
- "A short request is enough: name it from the person's purpose and draft the first Markdown " +
370
- "document with create_folder_document. Do not leave an empty folder or make them fill a form. " +
371
- "Pass `team` only when the user wants it shared with that team; use the slug they named " +
372
- "or one returned by list_folders, and omit it for a private folder. Use the returned " +
373
- "folder id with create_folder_document to add Markdown, get_folder to see its contents, " +
374
- "get_folder_document to read text and its version, and save_folder_document with that " +
375
- "version when editing. On a stale-version refusal, reread before retrying so another " +
376
- "person's work is not lost. Use add_folder_asset for a screenshot or file available on " +
377
- "this machine, and get_folder_asset to read one already there. " +
378
- "To put a guide in a folder — a task, bug or handoff, new or already published — publish it " +
379
- "as usual (for a team folder, to that folder's team, or its members cannot see it), then call " +
380
- "link_folder_guide with the folder id and the guide id. Linking leaves the guide unchanged; " +
381
- "unlink_folder_guide takes it out again. Delete a document or file with " +
382
- "delete_folder_document or delete_folder_asset only when the person asked for it to go: " +
383
- "neither can be undone. No tool deletes a whole folder; a person does that in the hub. " +
384
- "share_folder returns a link anyone can read the folder by, like a guide's; use it only " +
385
- "when the person asked to share the folder. " +
386
- "list_folders with query finds folders and documents by their words, and with guide the " +
387
- "folders a guide is in. rename_folder_document renames a document and move_folder_document " +
388
- "moves it to another folder in the same space, keeping its history; " +
389
- "get_folder_document_history reads earlier versions, so a bad edit can be put back.\n" +
390
- "A FOLLOW-UP IS MORE CONTEXT FOR A GUIDE, WRITTEN AS ITS OWN GUIDE. When a guide needs " +
391
- "more context — a missing detail, a step that needed explaining, what changed since, what " +
392
- "you found doing it — publish that context with publish_guide `parent` set to the guide's " +
393
- "id. It is listed under the original, and anyone who opens the original, person or agent, " +
394
- "gets it too. get_guide and take return a guide's follow-ups after it; read them " +
395
- "before acting. They also return the guide it follows, in front of it: a follow-up is a " +
396
- "note on a bigger piece of work, not the work. If it needs that guide done and it is " +
397
- "not, say so rather than starting.",
398
- },
399
- );
400
-
401
- // ---- the four verbs (docs/V2.md §11) --------------------------------------------------------
402
- // One implementation each. The older tool names above call these too, so an agent following an
403
- // old prompt gets exactly what one following a new prompt gets.
404
-
405
- /** An answer as JSON, with the server's next move after it. */
194
+ "it is refused, because your note is your word for your own work and evidence is the " +
195
+ "part a person can check.\n" +
196
+ "WRITING WORK DOWN. create_work takes one piece of work or several. Each says what it is " +
197
+ "to a person in `summary` — one or two plain sentences, 400 characters at most, no ids, " +
198
+ "paths or jargon, saying what this is and whether anybody has to act. It is what the " +
199
+ "person sees first; the text behind it is for agents: exact, dense, full of ids and " +
200
+ "paths. New work is Ready: it reaches whoever it is for at once. Pass `draft: true` when " +
201
+ "a person should read it first — a plan of several pieces, or something you are unsure " +
202
+ "of — and tell them. A tag such as bug, feature or question may describe it; nothing " +
203
+ "turns on one. BEFORE WRITING ANYTHING DOWN, CHECK WHAT IS ALREADY OPEN: find_work with " +
204
+ "nothing shows the board and what this worktree holds. If this session's work answers " +
205
+ "something you hold, hand_in that — never write a second piece of work about it. Defects " +
206
+ "you found and are not fixing go in one create_work call, each its own item, so each can " +
207
+ "be taken on its own. AN IMAGE THE USER SHOWED YOU IS EVIDENCE, NOT CONTEXT: add_file it " +
208
+ "and put the line it returns in the text — a screenshot described instead of attached is " +
209
+ "the most useful thing in the report, thrown away. update_work changes work that exists: " +
210
+ "Ready, Draft or archived, who it is for, or its text.\n" +
211
+ "WHAT YOU LEARN GOES INTO THE PROJECT'S DOCUMENTS. When the user asks you to pass this " +
212
+ "along, hand it off, capture it, or share what was just done, write what the next person " +
213
+ "needs to know into the project's documents: find_projects, read_document its README and " +
214
+ "the page for that area, and save_document to update the page that already covers it " +
215
+ "rather than adding a new one each time, with a line on its Log. Never hand knowledge to " +
216
+ 'one person as a piece of work. "Update the passalong", "add this to passalong" and the ' +
217
+ "like are ambiguous — hand in what you hold, update the documents, or write new work? Ask " +
218
+ "which, in one line, rather than writing and leaving the user to undo it.\n" +
219
+ "A PROJECT IS A PLACE; ITS DOCUMENTS ARE WHAT TO KNOW; WORK IS WHAT TO DO. Sort " +
220
+ "everything before writing it. If someone still has to act on it — a defect still open, " +
221
+ "work not yet done, a check nobody has run — it is work: it has an owner, a state and an " +
222
+ "answer. A document has none of those and tells nobody, so open work recorded only as a " +
223
+ "document is lost. If it is what someone needs to know — an overview, a decision, a " +
224
+ "finding, something already fixed with its commit — it is a document. WHERE A PROJECT " +
225
+ "LIVES is in the `space` field find_projects and save_project return: private, or a named " +
226
+ "team. Tell the person that, never what you meant to do; a project made without `team` " +
227
+ "is private, and one made inside another takes its space. Call find_projects before " +
228
+ "save_project, so you do not make a duplicate; a short request is enough to name one and " +
229
+ "write its first document. read_document gives a document's text and version; " +
230
+ "save_document with that version, and on a stale-version refusal read it again before " +
231
+ "retrying, so nobody's writing is lost. Every version is kept, and read_document with " +
232
+ "`history` lists them, so a bad edit can be put back. delete_document and delete_file " +
233
+ "only when the person asked for that one to go: neither can be undone, and no tool " +
234
+ "deletes a whole project.\n" +
235
+ "SEVERAL ACCOUNTS. When this machine has more than one signed in and none is chosen, " +
236
+ "every call is refused until accounts with `use` says which. Ask the person; never choose.",
237
+ },
238
+ );
239
+
240
+ /** An answer as JSON, with the server's next move after it, and a reply in front of it. */
406
241
  const answer = (r, id) => {
407
242
  const { next, say, ...rest } = r || {};
408
243
  const note = nextNote({ next, say }, id);
409
- // The note is what the agent acts on and it is prose, so it stays in the text half only. The
410
- // structured half is the answer itself, which is what `outputSchema` below describes.
411
- // A reply goes first, in prose: it is what the agent stopped for, and inside the JSON it reads
412
- // as one more field.
413
244
  const said = repliesNote(rest.replies);
414
245
  return {
415
246
  ...text(
@@ -418,1177 +249,664 @@ export function buildServer() {
418
249
  structuredContent: rest,
419
250
  };
420
251
  };
421
-
422
- async function doTake({ id, cwd, any }) {
423
- const dir = cwd || process.cwd();
424
- try {
425
- const r = await passalong.take(id, { cwd: dir, any: Boolean(any) });
426
- if (!r.guide) return text(nextNote(r) || "Nothing is waiting for this agent here.");
427
- const meta = parse(r.guide.markdown).meta;
428
- const siblings = await related(meta);
429
- const context = await passalong.followUps(meta);
430
- const from = await passalong.parentGuide(meta);
431
- const held =
432
- `<!-- passalong: ${r.guide.id} is yours` +
433
- `${r.guide.resumed ? " (you already held it — carry on from where it was left)" : ""}. -->`;
434
- const said = repliesNote(r.replies);
435
- return text(
436
- `${said ? `${said}\n\n` : ""}${from}${leadFor(meta)}${r.guide.markdown}${siblings}${context ? `\n\n${context}` : ""}` +
437
- `\n\n${held}\n${nextNote(r, r.guide.id)}\n${passalong.followUpNote(meta)}`,
438
- );
439
- } catch (err) {
440
- const h = err.body?.holder;
441
- if (h)
442
- return fail({
443
- message:
444
- `${err.message}. Somebody else is on it here${h.agent ? ` (agent ${h.agent}${h.host ? ` on ${h.host}` : ""})` : ""}${h.note ? ` (last said: "${h.note}")` : ""}. ` +
445
- "Tell the user before doing this work too; get_guide reads it without taking it.",
446
- });
447
- return failWith(err);
448
- }
449
- }
450
-
451
- async function doProgress({ id, note, cwd }) {
452
- try {
453
- return answer(await passalong.progress(id, note || "", { cwd: cwd || process.cwd() }), id);
454
- } catch (err) {
455
- return failWith(err);
456
- }
457
- }
458
-
459
- async function doHandIn({
460
- id,
461
- ok,
462
- note,
463
- evidence,
464
- checks,
465
- writeup,
466
- risk,
467
- markdown,
468
- report,
469
- pr,
470
- cwd,
471
- }) {
472
- try {
473
- // A check that names a command is run here, before anything is recorded, and its exit code
474
- // decides it rather than the agent's account of it. A failure is refused with the command's
475
- // own output: the claim is still held, nothing is written, and the agent has what it needs
476
- // to fix. Checks with no `cmd` pass straight through to the prose rule.
477
- const at = cwd || process.cwd();
478
- const done = runChecks(checks || [], { cwd: at });
479
- if (done.failed)
480
- return {
481
- content: [
482
- { type: "text", text: refusal(done.failed, { ran: done.ran, total: checks.length }) },
483
- ],
484
- isError: true,
485
- };
486
- const r = await passalong.handIn(id, {
487
- ok,
488
- note: note || "",
489
- evidence: evidence || "",
490
- checks: done.checks,
491
- writeup: writeup || "",
492
- risk: risk || "",
493
- markdown,
494
- report,
495
- pr: pr || "",
496
- cwd: at,
497
- });
498
- return answer(r, id);
499
- } catch (err) {
500
- return failWith(err);
501
- }
502
- }
503
-
504
- async function doAsk({ id, question, cwd }) {
252
+ const verb = (fn) => async (args) => {
505
253
  try {
506
- return answer(await passalong.ask(id, question, { cwd: cwd || process.cwd() }), id);
254
+ return answer(await fn(args), args.id);
507
255
  } catch (err) {
508
256
  return failWith(err);
509
257
  }
510
- }
258
+ };
511
259
 
512
- async function doPass({ id, why, cwd }) {
513
- try {
514
- return answer(await passalong.pass(id, why, { cwd: cwd || process.cwd() }), id);
515
- } catch (err) {
516
- return failWith(err);
517
- }
518
- }
260
+ // ---- work: take, progress, hand_in, pass, ask ---------------------------------------------
519
261
 
520
262
  server.registerTool(
521
- "assign",
263
+ "take",
522
264
  {
523
- title: "Give it to someone else",
265
+ title: "Take work",
524
266
  annotations: ADDS,
525
267
  description:
526
- "Reassign a guide or task the user wrote, or one assigned to them, to someone else in its team, when the user asks: " +
527
- '`to` is @handle for one person, "@ada,@bola" for several named people, #group for the people who do a thing, or "team" for ' +
528
- "everyone. Whoever held it and is left out has it taken back and is told; a task then only " +
529
- "goes to the new assignee's agents. Its author, or whoever it is assigned to, can do this.",
268
+ "Say you are doing it, and get it. With `id`, that piece of work. With no id, the next " +
269
+ "thing waiting here — always something new, never what you hold. While you hold it no " +
270
+ "other agent can take it, and whoever sent it sees you are on it. You may hold several; " +
271
+ "the person watches them on the board. If somebody else has it, the answer says who — " +
272
+ "tell the user rather than doing the work twice. Every answer ends with what to call next.",
530
273
  inputSchema: {
531
- id: z.string().describe("passalong id"),
532
- to: z.string().describe('"@handle", "#group", or "team"'),
274
+ id: z
275
+ .string()
276
+ .optional()
277
+ .describe("passalong id or share link; leave out for the next one"),
278
+ cwd: CWD,
279
+ any: z
280
+ .boolean()
281
+ .optional()
282
+ .describe("work for another repo, or none — only when the user asks"),
533
283
  },
534
284
  },
535
- async ({ id, to }) => {
285
+ async ({ id, cwd, any }) => {
536
286
  try {
537
- return json(await passalong.assign(id, to));
287
+ const r = await passalong.take(id, { cwd: cwd || process.cwd(), any: Boolean(any) });
288
+ if (!r.work) return text(nextNote(r) || "Nothing is waiting for this agent here.");
289
+ const from = await passalong.parentWork(parse(r.work.markdown).meta);
290
+ const held =
291
+ `<!-- passalong: ${r.work.id} is yours` +
292
+ `${r.work.resumed ? " (you already held it — carry on from where it was left)" : ""}. -->`;
293
+ const said = repliesNote(r.replies);
294
+ return text(
295
+ `${said ? `${said}\n\n` : ""}${from}${LEAD}${r.work.markdown}` +
296
+ `\n\n${held}\n${nextNote(r, r.work.id)}\n${passalong.AFTER}`,
297
+ );
538
298
  } catch (err) {
539
- return fail(err);
299
+ const h = err.body?.holder;
300
+ if (h)
301
+ return fail({
302
+ message:
303
+ `${err.message}. Somebody else is on it${h.agent ? ` (agent ${h.agent}${h.host ? ` on ${h.host}` : ""})` : ""}${h.note ? ` (last said: "${h.note}")` : ""}. ` +
304
+ "Tell the user before doing this work too; find_work reads it without taking it.",
305
+ });
306
+ return failWith(err);
540
307
  }
541
308
  },
542
309
  );
543
310
 
544
311
  server.registerTool(
545
- "list_folders",
312
+ "progress",
546
313
  {
547
- title: "List project folders",
548
- annotations: READS,
549
- description: `Find project folders of documents, assets and guides. Pass query to find the folders whose name, description, or any document's name or text mention those words; each then names its matching documents. Pass guide to find the folders a guide is in. Requires sync sign-in.`,
314
+ title: "Report progress",
315
+ outputSchema: progressOut,
316
+ annotations: ADDS,
317
+ description:
318
+ "Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
319
+ "without one marks it stalled. If the answer says you no longer hold it, stop.",
550
320
  inputSchema: {
551
- scope: z.string().optional().describe('"all", "mine", or a team slug'),
552
- query: z.string().optional().describe("Words to look for in folders and their documents"),
553
- guide: z.string().optional().describe("A guide id: only the folders it is linked in"),
321
+ id: z.string().describe("the id of what you hold"),
322
+ note: z
323
+ .string()
324
+ .optional()
325
+ .describe(
326
+ 'one plain sentence in the first person, as you would tell the person: "Reading ' +
327
+ 'the settings page to find where the toggle goes." Start it with BLOCKED: when ' +
328
+ "you are stopped and need an answer",
329
+ ),
330
+ cwd: CWD,
554
331
  },
555
332
  },
556
- async ({ scope, query, guide }) => {
557
- try {
558
- return json(await api.folders(scope || "all", { query, guide }));
559
- } catch (err) {
560
- return fail(err);
561
- }
562
- },
333
+ verb(({ id, note, cwd }) => passalong.progress(id, note || "", { cwd: cwd || process.cwd() })),
563
334
  );
335
+
564
336
  server.registerTool(
565
- "create_folder",
337
+ "hand_in",
566
338
  {
567
- title: "Create project folder",
339
+ title: "Hand it in",
340
+ outputSchema: handInOut,
568
341
  annotations: ADDS,
569
342
  description:
570
- "Create a folder or nested subfolder after checking list_folders. Pass parent to nest it; omit team for a private root, and pass a team slug only when sharing was requested. A subfolder inherits its parent's space. The answer's `space` says where it landed: report that to the person.",
343
+ "Finished here, with proof. IT IS FINAL: the work goes to Done with what you showed, and " +
344
+ "nobody reviews it after you (anyone can reopen it with a reason). So SHOW IT: a " +
345
+ "screenshot for anything with a screen — before and after, for a defect — or, for " +
346
+ "backend work, the one command and the line of its output that shows it works. Send " +
347
+ "`checks`: one entry per thing the work asked for, each with that line and the evidence " +
348
+ "for it. That is what a person reads, line against line, AND IT IS HOW YOU KNOW YOU ARE " +
349
+ "DONE: every line has one. Give a check `cmd` when a command proves it: the command is " +
350
+ "run here before the hand-in lands, a non-zero exit refuses it, and what it printed is " +
351
+ "recorded instead of your account of it. Use `ran` for a check no command can settle — " +
352
+ "add_file the screenshot and put the line it returns there. `evidence` as one block works " +
353
+ "too. DO NOT write new work to carry your evidence: it belongs on this call. What you had " +
354
+ "to adapt goes in `writeup`; what you learned about the project goes into its documents " +
355
+ "(save_document).",
571
356
  inputSchema: {
572
- title: z.string().describe("Name of the project or body of work"),
573
- description: z.string().optional().describe("Short purpose of the folder"),
574
- team: z.string().optional().describe("Team slug; omit for a private folder"),
575
- parent: z
357
+ id: z.string().describe("passalong id"),
358
+ evidence: z
359
+ .string()
360
+ .optional()
361
+ .describe(
362
+ "what you ran and what came back: the command and the lines that decided it, a test " +
363
+ 'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
364
+ ),
365
+ checks: z
366
+ .array(
367
+ // A bare string is a check line with nothing behind it. Let it through to the server's
368
+ // refusal, which says what to send, rather than a schema error that says only "expected
369
+ // object".
370
+ z.preprocess(
371
+ (v) => (typeof v === "string" ? { check: v } : v),
372
+ z.object({
373
+ check: z
374
+ .string()
375
+ .describe("what the work asked for that this answers, in its own words"),
376
+ says: z
377
+ .string()
378
+ .optional()
379
+ .describe(
380
+ 'what happened, in one plain sentence for a PERSON: "Swagger lists all six ' +
381
+ 'endpoints, each with request and response schemas." It is the row they read; ' +
382
+ "the evidence stays folded behind it",
383
+ ),
384
+ ran: z
385
+ .string()
386
+ .optional()
387
+ .describe(
388
+ "what you ran for it, and what came back; not needed when `cmd` is set",
389
+ ),
390
+ cmd: z
391
+ .string()
392
+ .optional()
393
+ .describe(
394
+ "the shell command that proves this line, run here before the hand-in lands — " +
395
+ "its exit code decides the check and its output is recorded as the evidence. " +
396
+ "Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
397
+ '"the badge reads 3" — then SHOW it with add_file and put the line it returns in `ran`; a check is run or shown, and describing what you saw is refused',
398
+ ),
399
+ }),
400
+ ),
401
+ )
402
+ .optional()
403
+ .describe(
404
+ "one entry per thing the work asked for, in the order you worked them. Each is RUN " +
405
+ "(`cmd`) or SHOWN (an add_file line in `ran`). Pasting a command you typed into " +
406
+ "`ran` is not running it and is refused",
407
+ ),
408
+ note: z
409
+ .string()
410
+ .describe(
411
+ "one plain sentence, in the first person, saying what you did and how it went: " +
412
+ '"Added the toggle to Settings; it survives a reload." It is the first thing the ' +
413
+ "person reads, and the evidence is behind it",
414
+ ),
415
+ writeup: z
576
416
  .string()
577
417
  .optional()
578
- .describe("Parent folder id for a nested subfolder; omit for a top-level folder"),
579
- color: z
580
- .enum(["coral", "amber", "green", "blue", "violet"])
418
+ .describe(
419
+ "what you had to adapt to make it work here — a version, a name, a step the work did " +
420
+ "not mention. 500 characters at most; anything longer belongs in the change. " +
421
+ "Optional: leave it out when it went as written",
422
+ ),
423
+ risk: z
424
+ .string()
581
425
  .optional()
582
- .describe("Optional grouping color"),
426
+ .describe(
427
+ "what this could break, in one line: a shared helper changed, a migration that " +
428
+ "cannot be undone, a route other clients call. Optional; leave it out when there is " +
429
+ 'nothing specific, rather than writing "low risk"',
430
+ ),
431
+ pr: z.string().optional().describe("PR, branch or commit link, when there is one"),
432
+ cwd: CWD,
583
433
  },
584
434
  },
585
- async ({ title, description, team, parent, color }) => {
586
- try {
587
- return json(
588
- await api.createFolder(title, description || "", team || "", parent || "", color || ""),
589
- );
435
+ async ({ id, note, evidence, checks, writeup, risk, pr, cwd }) => {
436
+ try {
437
+ // A check that names a command is run here, before anything is recorded, and its exit
438
+ // code decides it rather than the agent's account of it. A failure is refused with the
439
+ // command's own output: the claim is still held, nothing is written.
440
+ const at = cwd || process.cwd();
441
+ const done = runChecks(checks || [], { cwd: at });
442
+ if (done.failed)
443
+ return fail({
444
+ message: refusal(done.failed, { ran: done.ran, total: checks.length }),
445
+ });
446
+ const r = await passalong.handIn(id, {
447
+ note,
448
+ evidence: evidence || "",
449
+ checks: done.checks,
450
+ writeup: writeup || "",
451
+ risk: risk || "",
452
+ pr: pr || "",
453
+ cwd: at,
454
+ });
455
+ return answer(r, id);
590
456
  } catch (err) {
591
- return fail(err);
457
+ return failWith(err);
592
458
  }
593
459
  },
594
460
  );
461
+
595
462
  server.registerTool(
596
- "update_folder",
463
+ "pass",
597
464
  {
598
- title: "Organize project folder",
465
+ title: "Pass it",
466
+ outputSchema: passOut,
599
467
  annotations: ADDS,
600
468
  description:
601
- "Rename, describe, recolor or move a folder. Pass parent as an empty string to move it to the top level. A folder cannot move inside itself or a descendant, or into another team/private space.",
469
+ "Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
470
+ "the next agent, and the reason goes to whoever is next — say why, or they start where " +
471
+ "silence left them.",
602
472
  inputSchema: {
603
- folder: z.string(),
604
- title: z.string().optional(),
605
- description: z.string().optional(),
606
- parent: z
607
- .string()
608
- .optional()
609
- .describe("New parent folder id; empty string means top level"),
610
- color: z.enum(["", "coral", "amber", "green", "blue", "violet"]).optional(),
473
+ id: z.string().describe("passalong id"),
474
+ why: z.string().describe("one line: why it is not yours, or where you got stuck"),
475
+ cwd: CWD,
611
476
  },
612
477
  },
613
- async ({ folder, title, description, parent, color }) => {
614
- try {
615
- const changes = Object.fromEntries(
616
- Object.entries({ title, description, parent, color }).filter(
617
- ([, value]) => value !== undefined,
618
- ),
619
- );
620
- return json(await api.updateFolder(folder, changes));
621
- } catch (err) {
622
- return fail(err);
623
- }
624
- },
478
+ verb(({ id, why, cwd }) => passalong.pass(id, why, { cwd: cwd || process.cwd() })),
625
479
  );
480
+
626
481
  server.registerTool(
627
- "get_folder",
482
+ "ask",
628
483
  {
629
- title: "Open project folder",
630
- annotations: READS,
631
- description: "List a folder's documents, assets and linked guides.",
632
- inputSchema: { folder: z.string() },
633
- },
634
- async ({ folder }) => {
635
- try {
636
- return json(await api.folder(folder));
637
- } catch (err) {
638
- return fail(err);
639
- }
484
+ title: "Ask the person",
485
+ outputSchema: askOut,
486
+ annotations: ADDS,
487
+ description:
488
+ "You need the person's answer before you can go on. Ask one clear question, with the " +
489
+ "context it needs to be answered without opening your terminal. You keep what you hold " +
490
+ "and nobody else can take it. Then STOP and tell the user you are waiting: do not carry " +
491
+ "on guessing. When they have replied, call take with this id and their reply comes back " +
492
+ "with it. Use `pass` instead when the work is not yours.",
493
+ inputSchema: {
494
+ id: z.string().describe("passalong id"),
495
+ question: z
496
+ .string()
497
+ .describe(
498
+ "one clear question in the first person, a few sentences at most, plain text: " +
499
+ '"Should the dark-mode toggle live in Settings or the header? The header is ' +
500
+ 'crowded on mobile." What you were about to do, if they just say yes, helps',
501
+ ),
502
+ cwd: CWD,
503
+ },
640
504
  },
505
+ verb(({ id, question, cwd }) => passalong.ask(id, question, { cwd: cwd || process.cwd() })),
641
506
  );
507
+
508
+ // ---- work: create_work, find_work, update_work ---------------------------------------------
509
+
642
510
  server.registerTool(
643
- "get_folder_document",
511
+ "create_work",
644
512
  {
645
- title: "Read folder document",
646
- annotations: READS,
647
- description: "Read the full Markdown and version before editing it.",
648
- inputSchema: { folder: z.string(), document: z.string() },
513
+ title: "Write work down",
514
+ annotations: ADDS,
515
+ description:
516
+ "Write down work someone must still do: one piece, or several at once — a plan broken " +
517
+ "into steps, or every defect a test run turned up, each its own item so each can be " +
518
+ "taken on its own. Check what is open first (find_work): if this session answers " +
519
+ "something you hold, hand_in that instead. Each item is Ready at once and reaches whoever " +
520
+ "it is for; `draft: true` holds them all for a person to read first. A picture belongs " +
521
+ "in the text: add_file it and put the line it returns in `body`. Knowledge is not work: " +
522
+ "what someone needs to know goes into a project's documents (save_document).",
523
+ inputSchema: {
524
+ items: z
525
+ .array(
526
+ z.object({
527
+ title: z.string().describe("what it is, in a few words"),
528
+ summary: z
529
+ .string()
530
+ .describe(
531
+ "one or two plain sentences for a PERSON, no ids or paths: what this is and " +
532
+ "whether anybody has to act. 400 characters at most",
533
+ ),
534
+ body: z
535
+ .string()
536
+ .optional()
537
+ .describe(
538
+ "markdown for whoever does it: what is wrong or wanted, what you found, what " +
539
+ "done looks like if you know — in whatever shape fits",
540
+ ),
541
+ to: z
542
+ .string()
543
+ .optional()
544
+ .describe("who it is for, when not the same as the top-level `to`"),
545
+ parent: z
546
+ .string()
547
+ .optional()
548
+ .describe("id of the work this comes out of — a bigger piece, a customer request"),
549
+ after: z
550
+ .array(z.number().int())
551
+ .optional()
552
+ .describe("positions (from 0) of earlier items this one needs done first"),
553
+ repo: z
554
+ .string()
555
+ .optional()
556
+ .describe("owner/repo it is for; default is the repo you are in, '' for none"),
557
+ tags: z
558
+ .array(z.string())
559
+ .optional()
560
+ .describe(
561
+ "words that describe it, e.g. bug, feature, question; nothing turns on one",
562
+ ),
563
+ }),
564
+ )
565
+ .min(1)
566
+ .max(20),
567
+ to: z
568
+ .string()
569
+ .optional()
570
+ .describe(
571
+ "team slug, team/@handle for one teammate, team/@ada,@bola for several, or " +
572
+ "team/#group for the people who do a thing; default is your current team",
573
+ ),
574
+ draft: z
575
+ .boolean()
576
+ .optional()
577
+ .describe("hold it all in Draft for a person to read and make Ready"),
578
+ cwd: z.string().optional().describe("the repo you are writing from"),
579
+ },
649
580
  },
650
- async ({ folder, document }) => {
581
+ async ({ items, to, draft, cwd }) => {
651
582
  try {
652
- return json(await api.folderDocument(folder, document));
583
+ const made = await passalong.createWork(items, {
584
+ draft: Boolean(draft),
585
+ to,
586
+ cwd: cwd || process.cwd(),
587
+ });
588
+ return json({
589
+ work: made,
590
+ // Nothing in Draft reaches anybody, and an agent that reports "sent it" has told the
591
+ // user something untrue. Said in the result so it is said to them.
592
+ ...(draft
593
+ ? {
594
+ next: `in Draft: ask the user to read it and mark it Ready at ${api.baseUrl()}/hub/work, or run \`passalong ready --all\``,
595
+ }
596
+ : {}),
597
+ });
653
598
  } catch (err) {
654
599
  return fail(err);
655
600
  }
656
601
  },
657
602
  );
603
+
658
604
  server.registerTool(
659
- "get_folder_asset",
605
+ "find_work",
660
606
  {
661
- title: "Read folder asset",
607
+ title: "Find work",
662
608
  annotations: READS,
663
609
  description:
664
- "Read an image or small text file from a folder into this agent's context. Other file types stay private downloads.",
665
- inputSchema: { folder: z.string(), asset: z.string() },
666
- },
667
- async ({ folder, asset }) => {
668
- try {
669
- const file = await api.folderAsset(folder, asset);
670
- const { data, text: body, ...meta } = file;
671
- return {
672
- content: data
673
- ? [
674
- { type: "text", text: file.name },
675
- { type: "image", data, mimeType: file.type },
676
- ]
677
- : [{ type: "text", text: `${file.name}\n\n${body || ""}` }],
678
- structuredContent: meta,
679
- };
680
- } catch (err) {
681
- return fail(err);
682
- }
683
- },
684
- );
685
-
686
- server.registerTool(
687
- "add_folder_asset",
688
- {
689
- title: "Add file to project folder",
690
- annotations: { ...ADDS, openWorldHint: true },
691
- description:
692
- "Add a screenshot, reference file or other asset from this machine to a folder. Use a real file path you can read; the bytes are uploaded directly, not copied into a guide. Images, PDF, ZIP and text are accepted up to 10 MB.",
693
- inputSchema: {
694
- folder: z.string().describe("folder id returned by create_folder or list_folders"),
695
- file: z.string().describe("path to the file on this machine"),
696
- name: z.string().optional().describe("label in the folder; defaults to the filename"),
697
- },
698
- },
699
- async ({ folder, file, name }) => {
700
- try {
701
- return json(await api.addFolderAsset(folder, file, name || ""));
702
- } catch (err) {
703
- return fail(err);
704
- }
705
- },
706
- );
707
-
708
- server.registerTool(
709
- "create_folder_document",
710
- {
711
- title: "Write new folder document",
712
- annotations: ADDS,
713
- description:
714
- "Add a Markdown document to a folder: reference to read, such as a brief, script, overview or a finding already resolved. It has no owner and no state and notifies nobody, so anything someone must still act on (an open bug, work to do) belongs in a bug or task guide linked to the folder instead.",
715
- inputSchema: { folder: z.string(), name: z.string(), body: z.string() },
716
- },
717
- async ({ folder, name, body }) => {
718
- try {
719
- return json(await api.createFolderDocument(folder, name, body));
720
- } catch (err) {
721
- return fail(err);
722
- }
723
- },
724
- );
725
- server.registerTool(
726
- "save_folder_document",
727
- {
728
- title: "Save folder document",
729
- annotations: ADDS,
730
- description:
731
- "Save a new revision. Send the version returned when you read it; a newer edit is refused.",
732
- inputSchema: {
733
- folder: z.string(),
734
- document: z.string(),
735
- version: z.number().int().positive(),
736
- body: z.string(),
737
- },
738
- },
739
- async ({ folder, document, version, body }) => {
740
- try {
741
- return json(await api.saveFolderDocument(folder, document, version, body));
742
- } catch (err) {
743
- return fail(err);
744
- }
745
- },
746
- );
747
- server.registerTool(
748
- "rename_folder_document",
749
- {
750
- title: "Rename folder document",
751
- annotations: ADDS,
752
- description:
753
- "Rename a folder document. Its id, text and history stay; no version is needed, because a rename cannot overwrite anyone's text.",
754
- inputSchema: { folder: z.string(), document: z.string(), name: z.string() },
755
- },
756
- async ({ folder, document, name }) => {
757
- try {
758
- return json(await api.renameFolderDocument(folder, document, name));
759
- } catch (err) {
760
- return fail(err);
761
- }
762
- },
763
- );
764
- server.registerTool(
765
- "share_folder",
766
- {
767
- title: "Share folder link",
768
- annotations: ADDS,
769
- description:
770
- "Get the folder's share link, making one if it has none: like a guide's link, anyone holding it can read the folder and the folders inside it and download its files, with no account. Linked guides are not included. Call it only when the person asked to share the folder or for its link; stopping sharing is done in the hub.",
771
- inputSchema: { folder: z.string() },
772
- },
773
- async ({ folder }) => {
774
- try {
775
- return json(await api.shareFolder(folder));
776
- } catch (err) {
777
- return fail(err);
778
- }
779
- },
780
- );
781
- server.registerTool(
782
- "move_folder_document",
783
- {
784
- title: "Move folder document",
785
- annotations: ADDS,
786
- description:
787
- "Move a folder document into another folder in the same space, private or the same team. It keeps its id, text and history; pass name too to rename it on the way. Refused if the other folder already has a document by that name.",
788
- inputSchema: {
789
- folder: z.string().describe("The folder the document is in now"),
790
- document: z.string(),
791
- to: z.string().describe("The folder to move it into"),
792
- name: z.string().optional().describe("A new name, if it should change too"),
793
- },
794
- },
795
- async ({ folder, document, to, name }) => {
796
- try {
797
- return json(await api.moveFolderDocument(folder, document, to, name || ""));
798
- } catch (err) {
799
- return fail(err);
800
- }
801
- },
802
- );
803
- server.registerTool(
804
- "get_folder_document_history",
805
- {
806
- title: "Read folder document history",
807
- annotations: READS,
808
- description:
809
- "Read a folder document's earlier versions. Without version, lists them newest first; with version, returns that version's text. To bring one back, save it with save_folder_document over the current version.",
810
- inputSchema: {
811
- folder: z.string(),
812
- document: z.string(),
813
- version: z.number().int().positive().optional(),
814
- },
815
- },
816
- async ({ folder, document, version }) => {
817
- try {
818
- return json(await api.folderDocumentHistory(folder, document, version || 0));
819
- } catch (err) {
820
- return fail(err);
821
- }
822
- },
823
- );
824
- server.registerTool(
825
- "link_folder_guide",
826
- {
827
- title: "Link guide to folder",
828
- annotations: ADDS,
829
- description:
830
- "Put a task, bug or handoff guide in the folder, beside the documents and assets it uses: one just published or one that already existed.",
831
- inputSchema: { folder: z.string(), guide: z.string() },
832
- },
833
- async ({ folder, guide }) => {
834
- try {
835
- return json(await api.linkFolderGuide(folder, guide));
836
- } catch (err) {
837
- return fail(err);
838
- }
839
- },
840
- );
841
-
842
- server.registerTool(
843
- "unlink_folder_guide",
844
- {
845
- title: "Unlink guide from folder",
846
- annotations: ADDS,
847
- description:
848
- "Take a guide out of a folder. Only the link goes: the guide itself is unchanged, and link_folder_guide puts it back.",
849
- inputSchema: { folder: z.string(), guide: z.string() },
850
- },
851
- async ({ folder, guide }) => {
852
- try {
853
- return json(await api.unlinkFolderGuide(folder, guide));
854
- } catch (err) {
855
- return fail(err);
856
- }
857
- },
858
- );
859
-
860
- server.registerTool(
861
- "delete_folder_document",
862
- {
863
- title: "Delete folder document",
864
- annotations: REMOVES,
865
- description:
866
- "Delete one document from a folder, with its earlier versions. It cannot be brought back, so do it only when the person asked for that document to go; to change what it says, use save_folder_document instead.",
867
- inputSchema: { folder: z.string(), document: z.string() },
868
- },
869
- async ({ folder, document }) => {
870
- try {
871
- return json(await api.deleteFolderDocument(folder, document));
872
- } catch (err) {
873
- return fail(err);
874
- }
875
- },
876
- );
877
-
878
- server.registerTool(
879
- "delete_folder_asset",
880
- {
881
- title: "Delete folder file",
882
- annotations: REMOVES,
883
- description:
884
- "Delete one file from a folder. It cannot be brought back, so do it only when the person asked for that file to go.",
885
- inputSchema: { folder: z.string(), asset: z.string() },
886
- },
887
- async ({ folder, asset }) => {
888
- try {
889
- return json(await api.deleteFolderAsset(folder, asset));
890
- } catch (err) {
891
- return fail(err);
892
- }
893
- },
894
- );
895
-
896
- server.registerTool(
897
- "search_guides",
898
- {
899
- title: "Search guides",
900
- annotations: READS,
901
- description:
902
- "Search transfer guides by words in the title, tags, stack, or body: the user's own " +
903
- "plus every team they belong to. Empty query lists everything, newest first.",
904
- inputSchema: {
905
- query: z.string().default(""),
906
- scope: z.string().optional().describe('"mine", "all" (default), or a team slug'),
907
- },
908
- },
909
- async ({ query, scope }) => {
910
- try {
911
- const rows = await passalong.list(query, { scope: scope || "" });
912
- return json({ guides: rows });
913
- } catch (err) {
914
- return fail(err);
915
- }
916
- },
917
- );
918
-
919
- server.registerTool(
920
- "inbox",
921
- {
922
- title: "Inbox",
923
- annotations: READS,
924
- description:
925
- "Guides handed to this user (or to their teams) that they have not pulled yet. Call it " +
926
- "when starting work so handoffs are not missed. Needs sync (passalong login).",
927
- inputSchema: {},
928
- },
929
- async () => {
930
- try {
931
- return json({ guides: await passalong.inbox() });
932
- } catch (err) {
933
- return fail(err);
934
- }
935
- },
936
- );
937
-
938
- server.registerTool(
939
- "board",
940
- {
941
- title: "Board",
942
- annotations: READS,
943
- description:
944
- "The state of this user's transfers as queues: waiting on you (handed to you, not " +
945
- "pulled), not working (someone gave it a failing verdict — the most urgent), in flight " +
946
- "(handed over, nobody has taken it — `stale` means it has sat over a week), and landed " +
947
- "(someone else has it), and `held`: what this worktree's agents hold now. Use it to answer " +
948
- "'what is outstanding?' and to see what you hold, without taking anything.",
949
- inputSchema: {},
950
- },
951
- async () => {
952
- try {
953
- const [board, now] = await Promise.all([
954
- passalong.board(),
955
- passalong.now().catch(() => ({ held: [] })),
956
- ]);
957
- return json({ ...board, held: now.held });
958
- } catch (err) {
959
- return fail(err);
960
- }
961
- },
962
- );
963
-
964
- server.registerTool(
965
- "log",
966
- {
967
- title: "What this user did",
968
- annotations: READS,
969
- description:
970
- "This user's own acts on guides, newest first: what they published, what they took " +
971
- "delivery of, and every verdict and ack they gave. Each item has a rendered `text` line " +
972
- "and the guide's repo. Use it to answer 'what have I been working on', to write a " +
973
- "standup or a weekly summary, or to find work from a repo by when it happened rather " +
974
- "than by what it was called. The hub calls this page Activities, so 'my activities' means " +
975
- "this tool. It is the opposite of `activity`, which is what other people did. IMPORTANT: it records what was passed along, not what was worked on — work " +
976
- "that never became a guide has no entry, so never present it as a complete record of " +
977
- "this user's work, and never infer that a quiet period was an idle one.",
978
- inputSchema: {
979
- repo: z.string().default("").describe("narrow to guides whose source repo matches this"),
980
- since: z
981
- .string()
982
- .default("")
983
- .describe("only what happened on or after this date: 2026, 2026-09, or 2026-09-11"),
984
- },
985
- },
986
- async ({ repo, since }) => {
987
- try {
988
- return json({ log: await passalong.log({ repo: repo || "", since: since || "" }) });
989
- } catch (err) {
990
- return fail(err);
991
- }
992
- },
993
- );
994
-
995
- server.registerTool(
996
- "activity",
997
- {
998
- title: "Activity",
999
- annotations: READS,
1000
- description:
1001
- "What has happened to this user's guides and handoffs: who pulled one, who archived one, " +
1002
- "who was handed what, who joined a team. Each item has a ready-made `text` line. This is " +
1003
- "the hub's notifications, not its Activities page — for the user's own acts ('my " +
1004
- "activities'), use log. This only reads; clear_activity is what marks the feed seen.",
1005
- inputSchema: {
1006
- all: z.boolean().default(false).describe("include what the user has already seen"),
1007
- },
1008
- },
1009
- async ({ all }) => {
1010
- try {
1011
- return json(await passalong.activity({ all }));
1012
- } catch (err) {
1013
- return fail(err);
1014
- }
1015
- },
1016
- );
1017
-
1018
- /**
1019
- * Split out of `activity`, which took `mark_read` and so read on one call and wrote on the next.
1020
- *
1021
- * An annotation cannot say "sometimes": `readOnlyHint` was false for a tool that almost always
1022
- * only reads, which is a client being told the cautious lie on every call rather than the truth
1023
- * on the one that matters. Two tools, each true about itself.
1024
- */
1025
- server.registerTool(
1026
- "clear_activity",
1027
- {
1028
- title: "Mark activity seen",
1029
- annotations: ADDS,
1030
- description:
1031
- "Clear the user's unread feed, after they have been shown what is in it. Read it with " +
1032
- "activity first — clearing what nobody was told about loses it.",
1033
- inputSchema: {},
1034
- },
1035
- async () => {
1036
- try {
1037
- const res = await passalong.activity({ all: false });
1038
- if (!res.unread) return json({ cleared: 0, unread: 0 });
1039
- await passalong.seen();
1040
- return json({ cleared: res.unread, unread: 0 });
1041
- } catch (err) {
1042
- return fail(err);
1043
- }
1044
- },
1045
- );
1046
-
1047
- server.registerTool(
1048
- "plan_tasks",
1049
- {
1050
- title: "Plan a goal as tasks",
1051
- annotations: ADDS,
1052
- description:
1053
- "Write tasks for agents: one step for a single task, or a larger goal broken into steps " +
1054
- "an agent can each finish and a person can each check, written as drafts in order. The " +
1055
- "tool for any task, rather than publish_guide. Give each step a Goal and an Acceptance " +
1056
- "a person can run. " +
1057
- "`after` names the earlier steps (by position, from 0) a step needs; it waits for them " +
1058
- "to be approved, and steps that need nothing of each other can run side by side. Keep a " +
1059
- "step to one sitting of work in one repo. Nothing runs until the user makes them ready.",
1060
- inputSchema: {
1061
- steps: z
1062
- .array(
1063
- z.object({
1064
- title: z.string(),
1065
- summary: z
1066
- .string()
1067
- .describe(
1068
- "one or two plain sentences for a PERSON, no ids or paths: what this task is and " +
1069
- "why it matters. The goal below is for the agent; this is what a person reads",
1070
- ),
1071
- goal: z.string().describe("what is true when this step is done"),
1072
- acceptance: z.string().describe("checks a person can run, one per line"),
1073
- context: z.string().optional(),
1074
- constraints: z.string().optional(),
1075
- out_of_scope: z.string().optional(),
1076
- target_context: z
1077
- .string()
1078
- .optional()
1079
- .describe("owner/repo it is for; default is the repo you are in, '' for none"),
1080
- after: z.array(z.number().int()).optional().describe("earlier steps this one needs"),
1081
- }),
1082
- )
1083
- .min(1)
1084
- .max(20),
1085
- cwd: z.string().optional().describe("the repo you are planning from"),
1086
- },
1087
- },
1088
- async ({ steps, cwd }) => {
1089
- try {
1090
- const ids = await passalong.planTasks(steps, { cwd: cwd || process.cwd() });
1091
- return json({
1092
- drafts: ids.map((id, i) => ({ id, title: steps[i].title, after: steps[i].after || [] })),
1093
- next: `all in Draft: ask the user to read them and mark them Ready on the board at ${api.baseUrl()}/hub/work, or run \`passalong ready --all\``,
1094
- });
1095
- } catch (err) {
1096
- return fail(err);
1097
- }
1098
- },
1099
- );
1100
-
1101
- server.registerTool(
1102
- "take",
1103
- {
1104
- title: "Take work",
1105
- annotations: ADDS,
1106
- description:
1107
- "Say you are doing it, and get it. With `id`, that guide — any kind: a task, a bug, a " +
1108
- "handoff. With no id, the next thing waiting — always something new, never what you hold. " +
1109
- "While you hold it no other agent can take it here, and its sender sees you are on it. You " +
1110
- "may hold several at once; the person watches them on the board. If somebody else has it, the " +
1111
- "answer says who — tell the user rather than doing the work twice. Every answer ends with " +
1112
- "what to call next.",
1113
- inputSchema: {
1114
- id: z
1115
- .string()
1116
- .optional()
1117
- .describe("passalong id or share link; leave out for the next one"),
1118
- cwd: z
1119
- .string()
1120
- .optional()
1121
- .describe("the worktree you are working in; default is the server's cwd"),
1122
- any: z
1123
- .boolean()
1124
- .optional()
1125
- .describe("a task for another repo, or none — only when the user asks"),
1126
- },
1127
- },
1128
- async (args) => doTake(args),
1129
- );
1130
-
1131
- server.registerTool(
1132
- "progress",
1133
- {
1134
- title: "Report progress",
1135
- outputSchema: progressOut,
1136
- annotations: ADDS,
1137
- description:
1138
- "Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
1139
- "without one marks it stalled. If the answer says you no longer hold it, stop.",
1140
- inputSchema: {
1141
- id: z.string().describe("the id of what you hold"),
1142
- note: z
1143
- .string()
1144
- .optional()
1145
- .describe(
1146
- 'one plain sentence in the first person, as you would tell the person: "Reading ' +
1147
- 'the settings page to find where the toggle goes." Start it with BLOCKED: when ' +
1148
- "you are stopped and need an answer",
1149
- ),
1150
- cwd: z
1151
- .string()
1152
- .optional()
1153
- .describe("the worktree you are working in; default is the server's cwd"),
1154
- },
1155
- },
1156
- async (args) => doProgress(args),
1157
- );
1158
-
1159
- server.registerTool(
1160
- "hand_in",
1161
- {
1162
- title: "Hand it in",
1163
- outputSchema: handInOut,
1164
- annotations: ADDS,
1165
- description:
1166
- "Finished here, with proof. IT IS FINAL: the work goes to Done with what you showed, and nobody reviews it after you (anyone can reopen it with a reason). So SHOW IT: a screenshot for anything with a screen — before and after, for a bug — or, for backend work, the one command and the line of its output that shows it works. Send `checks`: one entry per " +
1167
- "line the guide asks for — `## Acceptance` on a task, `## Verification` on a handoff or a " +
1168
- "bug — each with that line and the evidence for it. That is what its author reads, line " +
1169
- "against line, instead of hunting through a wall of output for the part that answers each " +
1170
- "one, AND IT IS HOW YOU KNOW YOU ARE DONE: every line has one. Give a check `cmd` when a " +
1171
- "command proves it: the command is run here before the hand-in lands, a non-zero exit " +
1172
- "refuses it, and what it printed is recorded instead of your account of it. Use `ran` for " +
1173
- "a check no command can settle. `evidence` as one block is the older shape and still " +
1174
- "works. A task needs no write-up guide any more. A handoff or a bug takes `ok`, whether its " +
1175
- "Verification held here, and `note` saying what went wrong when it did not. DO NOT " +
1176
- "publish a guide to carry your evidence: it belongs on this call, and a second document " +
1177
- 'titled "Hand-in evidence" is the thing `checks` exists to replace. If it worked but you ' +
1178
- "had to adapt something, that goes in `writeup` and the next reader of the guide is shown " +
1179
- "it — that is its home. A follow-up guide is for work somebody else should now do, not " +
1180
- "for answering.",
1181
- inputSchema: {
1182
- id: z.string().describe("passalong id"),
1183
- evidence: z
1184
- .string()
1185
- .optional()
1186
- .describe(
1187
- "what you ran and what came back: the command and the lines that decided it, a test " +
1188
- 'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
1189
- ),
1190
- checks: z
1191
- .array(
1192
- // A bare string is a check line with nothing behind it. Let it through to the server's
1193
- // refusal, which says what to send, rather than a schema error that says only "expected
1194
- // object".
1195
- z.preprocess(
1196
- (v) => (typeof v === "string" ? { check: v } : v),
1197
- z.object({
1198
- check: z
1199
- .string()
1200
- .describe(
1201
- "the line this answers, in the guide's own words: an Acceptance line on a task, " +
1202
- "a Verification line on a handoff or a bug",
1203
- ),
1204
- says: z
1205
- .string()
1206
- .optional()
1207
- .describe(
1208
- 'what happened, in one plain sentence for a PERSON: "Swagger lists all six ' +
1209
- 'endpoints, each with request and response schemas." It is the row they read ' +
1210
- "in the review table; the evidence stays folded behind it. Strongly " +
1211
- "recommended: a check without one shows only the line it answers",
1212
- ),
1213
- ran: z
1214
- .string()
1215
- .optional()
1216
- .describe(
1217
- "what you ran for it, and what came back; not needed when `cmd` is set",
1218
- ),
1219
- cmd: z
1220
- .string()
1221
- .optional()
1222
- .describe(
1223
- "the shell command that proves this line, run here before the hand-in lands — " +
1224
- "its exit code decides the check and its output is recorded as the evidence. " +
1225
- "Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
1226
- '"the badge reads 3" — then SHOW it with attach_screenshot and put the line it returns in `ran`; a check is run or shown, and describing what you saw is refused',
1227
- ),
1228
- }),
1229
- ),
1230
- )
1231
- .optional()
1232
- .describe(
1233
- "one entry per line the guide asks for — Acceptance on a task, Verification on a " +
1234
- "handoff or a bug — in the order you worked them. Each is RUN (`cmd`) or SHOWN (an " +
1235
- "attach_screenshot line in `ran`). Pasting a command you typed into `ran` is not " +
1236
- "running it and is refused",
1237
- ),
1238
- ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
1239
- note: z
1240
- .string()
1241
- .describe(
1242
- "one plain sentence, in the first person, saying what you did and how it went: " +
1243
- '"Added the toggle to Settings; it survives a reload." It is the first thing the ' +
1244
- "person reads, and the evidence is behind it. Say what went wrong when ok is false",
1245
- ),
1246
- writeup: z
1247
- .string()
1248
- .optional()
1249
- .describe(
1250
- "what you had to adapt to make it work here — a version, a name, a step that needed " +
1251
- "something the guide does not mention. 500 characters at most; anything longer " +
1252
- "belongs in the change. Optional: leave it out when it worked as written. The next " +
1253
- "person to open the guide is shown it",
1254
- ),
1255
- risk: z
1256
- .string()
1257
- .optional()
1258
- .describe(
1259
- "what this could break, in one line — the part of a PR template's Risk that is " +
1260
- "worth reading: a shared helper changed, a migration that cannot be undone, a " +
1261
- "route other clients call. Optional; leave it out when there is nothing specific, " +
1262
- 'rather than writing "low risk". Its author reads it before accepting the work',
1263
- ),
1264
- markdown: z
1265
- .string()
1266
- .optional()
1267
- .describe("older shape, not needed: say what you adapted in `writeup` instead"),
1268
- report: z
610
+ "READ work without taking it. With `id`, that piece of work — its markdown, read only; " +
611
+ "work over 20,000 characters comes back as an outline unless you ask for `section` or " +
612
+ "`full`. With `query`, the work whose words match. With neither, the board: what needs " +
613
+ "the person, who is on what, what is ready, and what this worktree holds. To DO the work, " +
614
+ "call take instead: it is the only way anyone learns somebody picked it up.",
615
+ inputSchema: {
616
+ id: z.string().optional().describe("passalong id (e.g. k3mq2xa7) or share link"),
617
+ query: z.string().optional().describe("words to look for in titles, tags and text"),
618
+ scope: z
1269
619
  .string()
1270
620
  .optional()
1271
- .describe("older shape, not needed: say what you adapted in `writeup` instead"),
1272
- pr: z.string().optional().describe("task: PR or branch link, when there is one"),
1273
- cwd: z
621
+ .describe('with query: "mine", "all" (default), or a team slug'),
622
+ section: z
1274
623
  .string()
1275
624
  .optional()
1276
- .describe("the worktree you are working in; default is the server's cwd"),
625
+ .describe("with id: only the part under this heading; see the outline"),
626
+ full: z.boolean().optional().describe("with id: the whole text, however long"),
1277
627
  },
1278
628
  },
1279
- async (args) => doHandIn(args),
629
+ async ({ id, query, scope, section, full }) => {
630
+ try {
631
+ if (id) {
632
+ const { work } = await passalong.resolve(id, { peek: true });
633
+ const markdown = serialize(work);
634
+ const ref = work.meta.id || id;
635
+ if (section) {
636
+ const part = sectionOf(markdown, section);
637
+ return text(
638
+ part
639
+ ? `${part.text}\n\n<!-- passalong: ${ref}, "${part.heading}" only; ask with full: true for all of it -->`
640
+ : `No heading matching "${section}".\n\n${outlineText(markdown, { id: ref })}`,
641
+ );
642
+ }
643
+ if (!full && markdown.length > WORK_WARN) return text(outlineText(markdown, { id: ref }));
644
+ return text(
645
+ `${await passalong.parentWork(work.meta)}${markdown}\n\n` +
646
+ `<!-- passalong: reading it does not make it yours: to work on it, call take ${ref}. -->`,
647
+ );
648
+ }
649
+ if (query !== undefined && query !== "")
650
+ return json({ work: await passalong.list(query, { scope: scope || "" }) });
651
+ const [board, now] = await Promise.all([
652
+ passalong.board(),
653
+ passalong.now().catch(() => ({ held: [] })),
654
+ ]);
655
+ return json({ ...board, held: now.held });
656
+ } catch (err) {
657
+ return fail(err);
658
+ }
659
+ },
1280
660
  );
1281
661
 
1282
662
  server.registerTool(
1283
- "ask",
663
+ "update_work",
1284
664
  {
1285
- title: "Ask the person",
1286
- outputSchema: openObject({ id: z.string(), lease_until: z.string() }),
1287
- annotations: ADDS,
665
+ title: "Change work",
666
+ // Destructive because `markdown` replaces the text that was there.
667
+ annotations: REMOVES,
1288
668
  description:
1289
- "You need the person's answer before you can go on. Ask one clear question, with the " +
1290
- "context it needs to be answered without opening your terminal. You keep what you hold " +
1291
- "and nobody else can take it. Then STOP and tell the user you are waiting: do not carry " +
1292
- "on guessing. When they have replied, call take with this id and their reply comes back " +
1293
- "with it. Use `pass` instead when the work is not yours; a progress note starting " +
1294
- "BLOCKED: is the older way of saying this, and is not heard as a question.",
669
+ "Change work that exists, when the person asks or when you wrote it. `status`: ready " +
670
+ "(whoever it is for may take it), draft (held for a person to read), or archived (off the " +
671
+ "board; it comes back to Draft). `to`: who it is for — @handle, several as @ada,@bola, " +
672
+ '#group, or "" for the whole team; whoever held it and is left out has it taken back and ' +
673
+ "is told. `markdown`: its whole text, frontmatter first, as find_work gave it — say " +
674
+ "what changed, keep what did not.",
1295
675
  inputSchema: {
1296
676
  id: z.string().describe("passalong id"),
1297
- question: z
1298
- .string()
1299
- .describe(
1300
- "one clear question in the first person, a few sentences at most, plain text: " +
1301
- '"Should the dark-mode toggle live in Settings or the header? The header is ' +
1302
- 'crowded on mobile." What you were about to do, if they just say yes, helps',
1303
- ),
1304
- cwd: z
677
+ status: z.enum(["ready", "draft", "archived"]).optional(),
678
+ to: z.string().optional().describe('"@handle", "@ada,@bola", "#group", or "" for the team'),
679
+ markdown: z
1305
680
  .string()
1306
681
  .optional()
1307
- .describe("the worktree you are working in; default is the server's cwd"),
682
+ .describe("the whole document, frontmatter first, to replace its text"),
683
+ cwd: z.string().optional().describe("the repo you are working in"),
1308
684
  },
1309
685
  },
1310
- async (args) => doAsk(args),
686
+ async ({ id, status, to, markdown, cwd }) => {
687
+ try {
688
+ if (!markdown && !status && to === undefined)
689
+ throw new Error("say what to change: status, to, or markdown");
690
+ const done = { id };
691
+ if (markdown)
692
+ done.url = (await passalong.updateText(id, markdown, { cwd: cwd || process.cwd() })).url;
693
+ if (to !== undefined) done.to = (await passalong.assign(id, to)).to ?? to;
694
+ if (status) {
695
+ await passalong.setStatus(id, STATUS[status]);
696
+ done.status = status;
697
+ }
698
+ return json(done);
699
+ } catch (err) {
700
+ return fail(err);
701
+ }
702
+ },
1311
703
  );
1312
704
 
705
+ // ---- projects and documents -----------------------------------------------------------------
706
+
1313
707
  server.registerTool(
1314
- "pass",
708
+ "find_projects",
1315
709
  {
1316
- title: "Pass it",
1317
- outputSchema: passOut,
1318
- annotations: ADDS,
710
+ title: "Find projects",
711
+ annotations: READS,
1319
712
  description:
1320
- "Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
1321
- "the next agent, and the reason goes to whoever is next — say why, or they start where " +
1322
- "silence left them.",
713
+ "With `id`, one project: its documents, files and the projects inside it. With `query`, " +
714
+ "the projects whose name, description or any document mentions those words, each naming " +
715
+ "its matching documents. With neither, every project you can see. Each answer's `space` " +
716
+ 'says where it lives: private, or a named team; `repo` is the repo it is built from, "" ' +
717
+ "when it has none. Read that repo when you write or check its documents.",
1323
718
  inputSchema: {
1324
- id: z.string().describe("passalong id"),
1325
- why: z.string().describe("one line: why it is not yours, or where you got stuck"),
1326
- cwd: z
1327
- .string()
1328
- .optional()
1329
- .describe("the worktree you are working in; default is the server's cwd"),
719
+ id: z.string().optional().describe("project id"),
720
+ query: z.string().optional().describe("words to look for in projects and their documents"),
721
+ scope: z.string().optional().describe('"all" (default), "mine", or a team slug'),
1330
722
  },
1331
723
  },
1332
- async (args) => doPass(args),
724
+ run(({ id, query, scope }) =>
725
+ id ? api.project(id) : api.projects(scope || "all", query || ""),
726
+ ),
1333
727
  );
1334
728
 
1335
729
  server.registerTool(
1336
- "get_guide",
730
+ "save_project",
1337
731
  {
1338
- title: "Get guide",
1339
- annotations: READS,
732
+ title: "Save a project",
733
+ annotations: ADDS,
1340
734
  description:
1341
- "READ a transfer guide by passalong id or share link and return its markdown. A guide over " +
1342
- "20,000 characters comes back as an outline (its headings and their sizes) unless you ask " +
1343
- "for `section`, for one part, or `full`, for all of it. Nothing is written to disk. " +
1344
- "Pulling a teammate's guide tells them the transfer landed. Use this to look at a guide " +
1345
- "you have not committed to. If you are about to DO the work, call take instead: " +
1346
- "it does this and takes it in one call, which is the only way the sender learns " +
1347
- "somebody picked it up.",
735
+ "With no `id`, make a project, after find_projects shows none fits: a `title` and a short " +
736
+ "`description`, and its `repo` when it has one. Leave out `team` for a private one, and " +
737
+ "pass a team slug only when the person wants it shared; `parent` puts it inside another, " +
738
+ 'in that one\'s space. With `id`, change its title, description, repo ("" for none), ' +
739
+ 'colour or place (`parent` "" for the top level); ' +
740
+ "its space never changes. The answer's `space` says where it is: tell the person.",
1348
741
  inputSchema: {
1349
- ref: z.string().describe("passalong id (e.g. k3mq2xa7) or share URL"),
1350
- section: z
742
+ id: z.string().optional().describe("the project to change; leave out to make one"),
743
+ title: z.string().optional().describe("name of the product or body of work"),
744
+ description: z.string().optional().describe("what it is for, in a line"),
745
+ team: z.string().optional().describe("new project only: team slug; omit for private"),
746
+ parent: z.string().optional().describe('project to put it inside; "" for the top level'),
747
+ color: z.enum(["", "coral", "amber", "green", "blue", "violet"]).optional(),
748
+ repo: z
1351
749
  .string()
1352
750
  .optional()
1353
- .describe("read only the part under this heading, e.g. Acceptance; see the outline"),
1354
- outline: z
1355
- .boolean()
1356
- .optional()
1357
- .describe("just the headings and their sizes, whatever the guide's length"),
1358
- full: z
1359
- .boolean()
1360
- .optional()
1361
- .describe("the whole document, even one over 20,000 characters"),
751
+ .describe('the repo it is built from, e.g. github.com/acme/billing; "" for none'),
1362
752
  },
1363
753
  },
1364
- async ({ ref, section, outline, full }) => {
1365
- try {
1366
- const { markdown, from } = await passalong.pull(ref);
1367
- const meta = parse(markdown).meta;
1368
- if (section) {
1369
- const part = sectionOf(markdown, section);
1370
- if (!part)
1371
- return text(
1372
- `No heading matching "${section}".\n\n${outlineText(markdown, { id: meta.id || ref })}`,
1373
- );
1374
- return text(
1375
- `${part.text}\n\n<!-- passalong: ${meta.id || ref}, "${part.heading}" only; ask with full: true for the whole guide -->`,
1376
- );
1377
- }
1378
- if (outline || (!full && markdown.length > GUIDE_WARN))
1379
- return text(
1380
- `${outlineText(markdown, { id: meta.id || ref })}\n\n<!-- passalong: ${from} -->`,
1381
- );
1382
- const lead = leadFor(meta);
1383
- const siblings = await related(meta);
1384
- // Follow-ups are more context for this guide, so they come with it — after the document,
1385
- // never inside it, so the guide an agent writes back out is still only the guide.
1386
- const context = await passalong.followUps(meta);
1387
- // The guide this one came out of, in front of the document: a follow-up read on its own
1388
- // looks like a small piece of work and is a note on a bigger one.
1389
- const follows = await passalong.parentGuide(meta);
1390
- // After the document, with the other trailing comments, not in front of it. An
1391
- // instruction that arrives with the payload is what gets read — but anything before the
1392
- // opening `---` stops the frontmatter being frontmatter, and this fires on every guide
1393
- // anyone was handed rather than only on bugs. The bug lead stays where it is: it is a
1394
- // warning against executing the document, so being read first is its whole job.
1395
- return text(
1396
- `${follows}${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
1397
- `\n\n<!-- passalong: ${from} -->` +
1398
- `\n${passalong.handoffNudge(meta)}` +
1399
- `\n${passalong.followUpNote(meta)}`,
754
+ run(({ id, title, description, team, parent, color, repo }) => {
755
+ if (!id) {
756
+ if (!title) throw new Error("a new project needs a `title`");
757
+ return api.createProject(
758
+ title,
759
+ description || "",
760
+ team || "",
761
+ parent || "",
762
+ color || "",
763
+ repo || "",
1400
764
  );
1401
- } catch (err) {
1402
- return fail(err);
1403
765
  }
1404
- },
766
+ if (team) throw new Error("a project's space does not change; leave out `team`");
767
+ const changes = Object.fromEntries(
768
+ Object.entries({ title, description, parent, color, repo }).filter(
769
+ ([, v]) => v !== undefined,
770
+ ),
771
+ );
772
+ return api.updateProject(id, changes);
773
+ }),
1405
774
  );
1406
775
 
1407
776
  server.registerTool(
1408
- "attach_screenshot",
777
+ "read_document",
1409
778
  {
1410
- title: "Attach a screenshot",
1411
- annotations: { ...ADDS, openWorldHint: true },
779
+ title: "Read a document",
780
+ annotations: READS,
1412
781
  description:
1413
- "Upload an image as evidence — a path in `file`, or the bytes in `data` when you have " +
1414
- "the image and no file — and get back the markdown line that points at it. " +
1415
- WHERE_THE_LINE_GOES,
782
+ "One document's Markdown and its `version`, which save_document needs. `history: true` " +
783
+ "lists its earlier versions, newest first; `version` returns that version's text. To " +
784
+ "bring one back, save it over the current version.",
1416
785
  inputSchema: {
1417
- file: z
1418
- .string()
1419
- .default("")
1420
- .describe("path to the image on this machine; leave out when sending `data`"),
1421
- data: z
1422
- .string()
1423
- .default("")
1424
- .describe(
1425
- "the image itself, base64 (a data: URL is fine), for when you have the bytes and no " +
1426
- "file — a browser tool that hands back an image inline and never writes to disk. " +
1427
- "5MB; larger belongs in create_upload",
1428
- ),
1429
- type: z
1430
- .string()
1431
- .default("image/png")
1432
- .describe("media type of `data`: image/png, image/jpeg, image/webp or image/gif"),
1433
- name: z.string().default("").describe("label for the image; defaults to its filename"),
786
+ project: z.string().describe("project id"),
787
+ document: z.string().describe("document id"),
788
+ history: z.boolean().optional().describe("list its earlier versions"),
789
+ version: z.number().int().positive().optional().describe("read this earlier version"),
1434
790
  },
1435
791
  },
1436
- async ({ file, data, type, name }) => {
1437
- try {
1438
- if (!file && !data) {
1439
- return fail(
1440
- new Error(
1441
- "send `file` (a path on this machine) or `data` (the image, base64). If your " +
1442
- "screenshot is a tool result you cannot write out, capture it to a file instead " +
1443
- "— a headless browser's page.screenshot({ path }) or your platform's capture " +
1444
- "command — and pass that path.",
1445
- ),
1446
- );
1447
- }
1448
- const shot = data
1449
- ? await passalong.attachBytes(data, { name: name || "", type: type || "image/png" })
1450
- : await passalong.attach(file, { name: name || "" });
1451
- return text(
1452
- `${JSON.stringify({ id: shot.id, url: shot.url, bytes: shot.bytes }, null, 2)}\n\n` +
1453
- `Put this in the guide body:\n${shot.markdown}`,
1454
- );
1455
- } catch (err) {
1456
- return fail(err);
1457
- }
1458
- },
792
+ run(({ project, document, history, version }) =>
793
+ history || version
794
+ ? api.projectDocumentHistory(project, document, version || 0)
795
+ : api.projectDocument(project, document),
796
+ ),
1459
797
  );
1460
798
 
1461
799
  server.registerTool(
1462
- "attach_file",
800
+ "save_document",
1463
801
  {
1464
- title: "Attach a file",
1465
- annotations: { ...ADDS, openWorldHint: true },
802
+ title: "Save a document",
803
+ annotations: ADDS,
1466
804
  description:
1467
- "Upload a file that is not a picture — a log, a PDF, a CSV, a zip, up to 10MB — and get " +
1468
- "back the markdown line that points at it. For a guide body or a bug report: put the " +
1469
- "line there and publishing claims the file. It is private, so only people who can read " +
1470
- "that guide can download it; for something a reviewer must SEE, use attach_screenshot. " +
1471
- "Pictures belong to attach_screenshot, not here.",
805
+ "Write what someone needs to know into a project. With no `document`, a new one: `name` " +
806
+ "and `body`. With `document`, change it: `body` with the `version` read_document gave you " +
807
+ "— a newer edit is refused, so nobody's writing is lost — and/or `name` to rename it, " +
808
+ "and/or `move_to` another project in the same space. Its id and history stay. Update the " +
809
+ "page that already covers something rather than adding another. A document has no owner " +
810
+ "and no state and tells nobody: anything someone must still act on is work (create_work).",
1472
811
  inputSchema: {
1473
- file: z.string().describe("path to the file on this machine"),
1474
- name: z.string().default("").describe("label for the file; defaults to its filename"),
812
+ project: z.string().describe("project id"),
813
+ document: z.string().optional().describe("the document to change; leave out for a new one"),
814
+ name: z
815
+ .string()
816
+ .optional()
817
+ .describe("its name: required for a new one, a rename otherwise"),
818
+ body: z.string().optional().describe("the whole Markdown text"),
819
+ version: z
820
+ .number()
821
+ .int()
822
+ .positive()
823
+ .optional()
824
+ .describe("with `body` on an existing document: the version you read"),
825
+ move_to: z.string().optional().describe("a project id in the same space to move it into"),
1475
826
  },
1476
827
  },
1477
- async ({ file, name }) => {
1478
- try {
1479
- const got = await passalong.attachFile(file, { name: name || "" });
1480
- return text(
1481
- `${JSON.stringify({ id: got.id, url: got.url, bytes: got.bytes }, null, 2)}\n\n` +
1482
- `Put this in the guide body:\n${got.markdown}`,
1483
- );
1484
- } catch (err) {
1485
- return fail(err);
828
+ run(async ({ project, document, name, body, version, move_to }) => {
829
+ if (!document) {
830
+ if (!name || body === undefined) throw new Error("a new document needs `name` and `body`");
831
+ return api.createProjectDocument(project, name, body);
1486
832
  }
1487
- },
833
+ let saved;
834
+ if (body !== undefined) {
835
+ if (!version) throw new Error("send the `version` read_document gave you with `body`");
836
+ saved = await api.saveProjectDocument(project, document, version, body);
837
+ }
838
+ if (name || move_to)
839
+ saved = await api.patchProjectDocument(project, document, {
840
+ ...(name ? { name } : {}),
841
+ ...(move_to ? { project: move_to } : {}),
842
+ });
843
+ if (!saved)
844
+ throw new Error("say what to change: `body` with `version`, `name`, or `move_to`");
845
+ return saved;
846
+ }),
1488
847
  );
1489
848
 
1490
849
  server.registerTool(
1491
- "reply",
850
+ "delete_document",
1492
851
  {
1493
- title: "Write to the agent",
1494
- annotations: ADDS,
852
+ title: "Delete a document",
853
+ annotations: REMOVES,
1495
854
  description:
1496
- "A person's tool, for someone driving their work from an assistant: write to whoever " +
1497
- "holds a guide — an answer to a question it asked, or something you thought of since — " +
1498
- "or, when nobody holds it yet and it is yours, leave a note for whoever takes it. Plain " +
1499
- "text, 1000 characters. `files` are paths on this machine: a picture is drawn in the " +
1500
- "thread and anything else is a file to download. Only its author, whoever it is " +
1501
- "assigned to, or whoever holds it can write; the agent reads it the next time it " +
1502
- "checks in. Agents answering their own questions do not use this: they use ask and take.",
1503
- inputSchema: {
1504
- id: z.string().describe("passalong id"),
1505
- text: z.string().default("").describe("what to say; may be empty when attaching a file"),
1506
- files: z
1507
- .array(z.string())
1508
- .max(3)
1509
- .default([])
1510
- .describe("paths to up to three files or pictures to attach"),
1511
- },
1512
- },
1513
- async ({ id, text: said, files }) => {
1514
- try {
1515
- const r = await passalong.reply(id, said, { files });
1516
- return text(
1517
- r?.noted
1518
- ? `Nobody holds ${id} yet, so this is a note: whoever takes it is handed it first.`
1519
- : `Sent to whoever holds ${id}. It reads this the next time it checks in.`,
1520
- );
1521
- } catch (err) {
1522
- return failWith(err);
1523
- }
855
+ "Delete one document, with its earlier versions. It cannot be brought back, so do it only " +
856
+ "when the person asked for that document to go; to change what it says, use save_document.",
857
+ inputSchema: { project: z.string(), document: z.string() },
1524
858
  },
859
+ run(({ project, document }) => api.deleteProjectDocument(project, document)),
1525
860
  );
1526
861
 
862
+ // ---- files ----------------------------------------------------------------------------------
863
+
1527
864
  server.registerTool(
1528
- "publish_guide",
865
+ "add_file",
1529
866
  {
1530
- title: "Publish guide",
1531
- annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
867
+ title: "Add a file",
868
+ annotations: { ...ADDS, openWorldHint: true },
1532
869
  description:
1533
- "Publish a guide from markdown (frontmatter + sections). Check what you hold first " +
1534
- "(board): if this session answers something you hold, hand_in that instead, and if the " +
1535
- "work never left a branch the team can see, publish what is still open as a bug or a task " +
1536
- "rather than a write-up of the fix. A transfer guide by default, or " +
1537
- "a single bug with `kind: bug`; use file_bugs for more than one; or a task for work nobody " +
1538
- "has done yet with `kind: task`. Missing id, created, " +
1539
- "author, and source_context are filled in. A screenshot belongs in the markdown: attach it " +
1540
- "with attach_screenshot and put the line it returns in the body, because publishing claims " +
1541
- "whatever the markdown names. " +
1542
- '`to` addresses it to a team ("khaime") or a ' +
1543
- 'teammate ("khaime/lukman"), who is notified. Returns the id and share link.',
870
+ "Upload a picture or any other file — a path in `file`, or a picture's bytes in `data` " +
871
+ "when you have the image and no file — and get back the markdown line that points at it. " +
872
+ "Files up to 10MB, pictures up to 5MB. " +
873
+ WHERE_THE_LINE_GOES,
1544
874
  inputSchema: {
1545
- markdown: z.string().describe("full guide markdown; start from guide_template"),
1546
- to: z
875
+ file: z.string().optional().describe("path to the file on this machine"),
876
+ data: z
1547
877
  .string()
1548
878
  .optional()
1549
879
  .describe(
1550
- "team slug, team/@handle for one teammate, team/@ada,@bola for several named teammates, or team/#group for a set of them",
880
+ "a picture itself, base64 (a data: URL is fine), for when a tool handed you the image " +
881
+ "and never wrote it to disk",
1551
882
  ),
1552
- cwd: z
1553
- .string()
1554
- .optional()
1555
- .describe("directory the work happened in, used to infer source_context"),
1556
- parent: z
883
+ type: z
1557
884
  .string()
1558
885
  .optional()
1559
- .describe(
1560
- "id or share link of the guide this one adds context to — set it and this is " +
1561
- "published as that guide's follow-up: listed under it, and read by whoever opens it",
1562
- ),
886
+ .describe("media type of `data`: image/png, image/jpeg, image/webp or image/gif"),
887
+ name: z.string().optional().describe("label for it; defaults to its filename"),
888
+ project: z.string().optional().describe("keep it in this project instead"),
1563
889
  },
1564
890
  },
1565
- async ({ markdown, to, cwd, parent }) => {
891
+ async ({ file, data, type, name, project }) => {
1566
892
  try {
1567
- const { guide, url, synced, notified } = await passalong.share(markdown, {
1568
- cwd: cwd || process.cwd(),
1569
- to,
1570
- follows: parent,
1571
- });
1572
- return json({
1573
- id: guide.meta.id,
1574
- title: guide.meta.title,
1575
- url,
1576
- synced,
1577
- team: guide.meta.team || "",
1578
- to: guide.meta.to || "",
1579
- parent: guide.meta.parent || "",
1580
- notified,
1581
- ...(sizeWarning(markdown.length) ? { warning: sizeWarning(markdown.length) } : {}),
1582
- // Nothing goes live on publish, and an agent that reports "sent it" or "queued it" has
1583
- // told the user something untrue. Said in the result so it is said to them.
1584
- ...(guide.meta.status === "draft"
1585
- ? {
1586
- status: "draft",
1587
- // The hub page, never the share link: the share page has no Ready button.
1588
- next: `in Draft: ask the user to read it and mark it Ready at ${api.baseUrl()}/hub/g/${guide.meta.id}, or run \`passalong ready ${guide.meta.id}\``,
1589
- }
1590
- : {}),
1591
- });
893
+ if (project) {
894
+ if (!file) throw new Error("a file for a project needs `file`, a path on this machine");
895
+ return json(await api.addProjectFile(project, file, name || ""));
896
+ }
897
+ if (!file && !data)
898
+ throw new Error(
899
+ "send `file` (a path on this machine) or `data` (a picture, base64). If your " +
900
+ "screenshot is a tool result you cannot write out, capture it to a file instead — a " +
901
+ "headless browser's page.screenshot({ path }) or your platform's capture command.",
902
+ );
903
+ const got = data
904
+ ? await passalong.attachBytes(data, { name: name || "", type: type || "image/png" })
905
+ : await passalong.attachAny(file, { name: name || "" });
906
+ return text(
907
+ `${JSON.stringify({ id: got.id, url: got.url, bytes: got.bytes }, null, 2)}\n\n` +
908
+ `The line that points at it:\n${got.markdown}`,
909
+ );
1592
910
  } catch (err) {
1593
911
  return fail(err);
1594
912
  }
@@ -1596,94 +914,28 @@ export function buildServer() {
1596
914
  );
1597
915
 
1598
916
  server.registerTool(
1599
- "file_bugs",
917
+ "read_file",
1600
918
  {
1601
- title: "File bugs",
1602
- annotations: ADDS,
919
+ title: "Read a file",
920
+ annotations: READS,
1603
921
  description:
1604
- "File one or more bugs you found but are not fixing, as a set. Opens a report and " +
1605
- "publishes each issue as its own guide — its own id, share link, and verdict — so a " +
1606
- "reviewer can hand any one of them to whoever fixes it. Use this after a test run, a QA " +
1607
- "pass, or a review that turned up defects; use publish_guide instead for work you " +
1608
- "finished and want repeated elsewhere. If there is a screenshot of any of this, it is " +
1609
- "evidence: call attach_screenshot first and pass what it returns as that issue's " +
1610
- "`evidence`. Describing a screenshot instead of attaching it throws away the most useful " +
1611
- "thing in the report. Needs sync (`passalong login`).",
1612
- inputSchema: {
1613
- title: z
1614
- .string()
1615
- .optional()
1616
- .describe('what the sweep was, e.g. "Checkout regression pass, 8 Sep build"'),
1617
- environment: z
1618
- .string()
1619
- .optional()
1620
- .describe("production, staging or development — where you saw these"),
1621
- to: z
1622
- .string()
1623
- .optional()
1624
- .describe(
1625
- "team slug, team/@handle for one teammate, team/@ada,@bola for several named teammates, or team/#group for a set of them",
1626
- ),
1627
- issues: z
1628
- .array(
1629
- z.object({
1630
- evidence: z
1631
- .array(z.string())
1632
- .default([])
1633
- .describe(
1634
- "screenshot URLs from attach_screenshot, or the markdown lines it returned; " +
1635
- "they go under Problem, where a reader looks first",
1636
- ),
1637
- title: z.string().describe("what is broken, in one line"),
1638
- summary: z
1639
- .string()
1640
- .optional()
1641
- .describe(
1642
- "one or two plain sentences for a PERSON, no ids or paths: what is broken and " +
1643
- "whether anyone needs to act. Defaults to the title",
1644
- ),
1645
- problem: z
1646
- .string()
1647
- .describe(
1648
- "what is broken and what it stops someone doing; the error if there is one",
1649
- ),
1650
- reproduce: z
1651
- .string()
1652
- .describe(
1653
- "numbered steps that show the bug — these produce it, they are not a fix",
1654
- ),
1655
- verification: z
1656
- .string()
1657
- .optional()
1658
- .describe(
1659
- "the behaviour that should have happened, as something a fixer can check",
1660
- ),
1661
- gotchas: z
1662
- .string()
1663
- .optional()
1664
- .describe("anything already ruled out, or that made it hard to pin down"),
1665
- area: z
1666
- .string()
1667
- .optional()
1668
- .describe(`which surface it is on — known: ${AREAS.map((a) => a.slug).join(", ")}`),
1669
- severity: z
1670
- .string()
1671
- .optional()
1672
- .describe("s1 blocker, s2 major, s3 minor, s4 cosmetic (default s3)"),
1673
- }),
1674
- )
1675
- .min(1)
1676
- .describe("one entry per defect; file them together rather than one call each"),
1677
- cwd: z.string().optional().describe("directory the bugs were found in"),
1678
- },
922
+ "Read a picture or a small text file kept in a project into this conversation. Other " +
923
+ "files stay downloads in the hub.",
924
+ inputSchema: { project: z.string(), file: z.string().describe("file id") },
1679
925
  },
1680
- async ({ title, environment, to, issues, cwd }) => {
926
+ async ({ project, file }) => {
1681
927
  try {
1682
- const filed = await passalong.fileBugs(
1683
- { title: title || "", environment: environment || "", issues },
1684
- { cwd: cwd || process.cwd(), to },
1685
- );
1686
- return json(filed);
928
+ const got = await api.projectFile(project, file);
929
+ const { data, text: body, ...meta } = got;
930
+ return {
931
+ content: data
932
+ ? [
933
+ { type: "text", text: got.name },
934
+ { type: "image", data, mimeType: got.type },
935
+ ]
936
+ : [{ type: "text", text: `${got.name}\n\n${body || ""}` }],
937
+ structuredContent: meta,
938
+ };
1687
939
  } catch (err) {
1688
940
  return fail(err);
1689
941
  }
@@ -1691,100 +943,55 @@ export function buildServer() {
1691
943
  );
1692
944
 
1693
945
  server.registerTool(
1694
- "guide_template",
946
+ "delete_file",
1695
947
  {
1696
- title: "Guide template",
1697
- annotations: READS,
948
+ title: "Delete a file",
949
+ annotations: REMOVES,
1698
950
  description:
1699
- "The empty guide skeleton with guidance comments for each section. By default a task " +
1700
- "brief: Goal, Context, Constraints, Acceptance, Out of scope. `kind: transfer` gives the " +
1701
- "skeleton for finished work to repeat; `kind: bug` a bug report, with Reproduce and no Steps.",
1702
- inputSchema: {
1703
- kind: z
1704
- .enum(["transfer", "bug", "task"])
1705
- .optional()
1706
- .describe(
1707
- "task (default) for work nobody has done yet; transfer for finished work to repeat; " +
1708
- "bug for a defect to fix",
1709
- ),
1710
- },
951
+ "Delete one file from a project. It cannot be brought back, so do it only when the person " +
952
+ "asked for that file to go.",
953
+ inputSchema: { project: z.string(), file: z.string().describe("file id") },
1711
954
  },
1712
- async ({ kind }) => text(template(kind ? { kind } : {})),
955
+ run(({ project, file }) => api.deleteProjectFile(project, file)),
1713
956
  );
1714
957
 
958
+ // ---- account ----------------------------------------------------------------------------------
959
+
1715
960
  server.registerTool(
1716
961
  "accounts",
1717
962
  {
1718
963
  title: "Accounts on this machine",
1719
- annotations: READS,
964
+ // Read-only to the world: `use` is this process remembering a name until it exits, and
965
+ // nothing on disk or on the server changes, so the machine's default stays where it was.
966
+ annotations: { ...READS, idempotentHint: true },
1720
967
  description:
1721
968
  "The Passalong accounts signed in on this machine, which one this session is using, and " +
1722
- "whether it still has to be chosen. With two or more and nothing chosen, every call that " +
1723
- "needs an account is refused until use_account says which. Read-only.",
1724
- inputSchema: {},
1725
- },
1726
- async () => {
1727
- const list = api.accountList();
1728
- return json({
1729
- accounts: list,
1730
- in_use: api.accountInUse(),
1731
- must_choose: api.needsChoice(),
1732
- ...(api.needsChoice() ? { next: api.choiceMessage() } : {}),
1733
- });
1734
- },
1735
- );
1736
-
1737
- server.registerTool(
1738
- "use_account",
1739
- {
1740
- title: "Use an account for this session",
1741
- annotations: { ...ADDS, idempotentHint: true },
1742
- description:
1743
- "Say which signed-in account this session acts as. Call it only with a name the PERSON " +
1744
- "gave you: when a machine has more than one account and none is chosen, ask them which, " +
1745
- "in plain words, then pass their answer. It lasts until this session ends and changes " +
1746
- "nothing on disk, so it never moves the machine's default.",
969
+ "whether it still has to be chosen. With two or more and nothing chosen, every other call " +
970
+ "is refused until `use` says which: ask the PERSON, in plain words, and pass their " +
971
+ "answer. It lasts until this session ends and never moves the machine's default.",
1747
972
  inputSchema: {
1748
- name: z.string().describe("an account name from the accounts tool, as the person said it"),
973
+ use: z.string().optional().describe("an account name from the list, as the person said it"),
1749
974
  },
1750
975
  },
1751
- async ({ name }) => {
1752
- try {
1753
- api.chooseAccount(name);
976
+ run(async ({ use }) => {
977
+ if (use) {
978
+ api.chooseAccount(use);
1754
979
  const who = await api.me();
1755
- return json({
1756
- using: name,
980
+ return {
981
+ using: use,
1757
982
  account: who.account,
1758
983
  handle: who.handle || "",
1759
984
  email: who.email || "",
1760
985
  teams: (who.teams || []).map((t) => t.slug),
1761
- });
1762
- } catch (err) {
1763
- return fail(err);
1764
- }
1765
- },
1766
- );
1767
-
1768
- server.registerTool(
1769
- "set_guide_status",
1770
- {
1771
- title: "Set guide status",
1772
- annotations: ADDS,
1773
- description:
1774
- "Archive a guide (`consumed`) or put it back on the board (`published`). Archiving is the " +
1775
- "author's shelf: off the board, out of the free tier's count, reversible — it is not a " +
1776
- "judgement that the work landed, which is what hand_in reports. `promoted` was " +
1777
- "retired and the server refuses it.",
1778
- inputSchema: { id: z.string(), status: z.enum(["published", "consumed"]) },
1779
- },
1780
- async ({ id, status }) => {
1781
- try {
1782
- const g = await passalong.setStatus(id, status);
1783
- return json(passalong.summary(g));
1784
- } catch (err) {
1785
- return fail(err);
986
+ };
1786
987
  }
1787
- },
988
+ return {
989
+ accounts: api.accountList(),
990
+ in_use: api.accountInUse(),
991
+ must_choose: api.needsChoice(),
992
+ ...(api.needsChoice() ? { next: api.choiceMessage() } : {}),
993
+ };
994
+ }),
1788
995
  );
1789
996
 
1790
997
  return server;