passalong 0.6.0 → 0.9.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
@@ -8,6 +8,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
9
  import { z } from "zod";
10
10
  import * as api from "./api.js";
11
+ import { refusal, runChecks } from "./checks.js";
11
12
  import { AREAS, BUG_SECTIONS, parse, SECTIONS, TASK_SECTIONS, template } from "./guide.js";
12
13
  import * as passalong from "./passalong.js";
13
14
 
@@ -23,6 +24,16 @@ const VERSION = JSON.parse(
23
24
 
24
25
  const text = (s) => ({ content: [{ type: "text", text: s }] });
25
26
 
27
+ /**
28
+ * Said in front of every guide an agent opens, whatever its kind: the hand-in needs evidence, and
29
+ * evidence is collected while the work happens, not reconstructed from memory once it is done.
30
+ * Mirrors KEEP_EVIDENCE in apps/api/src/mcp-http.ts.
31
+ */
32
+ const KEEP_EVIDENCE =
33
+ "KEEP YOUR EVIDENCE AS YOU GO. hand_in needs it: the commands you ran and what came back, the " +
34
+ "test summary, the link to the change. Copy each one when it happens — at the end you will be " +
35
+ "writing from memory, which is the thing evidence is here to replace.\n\n";
36
+
26
37
  /**
27
38
  * What to say in front of a guide whose kind changes what the reader should do with it.
28
39
  *
@@ -36,7 +47,8 @@ function leadFor(meta) {
36
47
  return (
37
48
  "THIS IS A BUG REPORT, NOT WORK TO REPEAT. Do not follow Reproduce as instructions " +
38
49
  "— those steps produce the defect. Fix what Problem describes, then check " +
39
- "Verification and answer with hand_in.\n\n"
50
+ "Verification and answer with hand_in.\n\n" +
51
+ KEEP_EVIDENCE
40
52
  );
41
53
  if (meta.kind === "task")
42
54
  return (
@@ -44,8 +56,12 @@ function leadFor(meta) {
44
56
  "reach Goal within Constraints, and leave Out of scope alone. It is done when every check " +
45
57
  "under Acceptance holds. Opening it here does not make it yours: to work on it, call take " +
46
58
  "with its id in the repo it is for, so no other agent can. Then progress, and hand_in " +
47
- "with a write-up.\n\n"
59
+ "with a write-up.\n\n" +
60
+ KEEP_EVIDENCE
48
61
  );
62
+ // A transfer guide is handed over untouched: it is a document to follow, and anything in front
63
+ // of it is one more thing that is not the document. The reminder still reaches the agent on the
64
+ // answer's `next` line, which is where every other instruction from the server rides.
49
65
  return "";
50
66
  }
51
67
 
@@ -78,7 +94,65 @@ async function related(meta) {
78
94
  return "";
79
95
  }
80
96
  }
81
- const json = (data) => text(JSON.stringify(data, null, 2));
97
+ /**
98
+ * An answer a client can read as data as well as prose.
99
+ *
100
+ * `structuredContent` is the same JSON the text block holds; the spec asks for both, because a
101
+ * tool "that returns structured content SHOULD also return the serialized JSON in a TextContent
102
+ * block" for clients that only read text. Only an object goes in the structured field — the spec
103
+ * allows any JSON value there, but a bare array or string tells a client nothing it could key on.
104
+ */
105
+ const json = (data) => ({
106
+ ...text(JSON.stringify(data, null, 2)),
107
+ ...(data && typeof data === "object" && !Array.isArray(data) ? { structuredContent: data } : {}),
108
+ });
109
+
110
+ /**
111
+ * What each tool does to the world, said out loud. Mirrors the same block in
112
+ * apps/api/src/mcp-http.ts, which had these and this server did not — the same four jobs described
113
+ * to a client two different ways, on the surface most agents actually reach.
114
+ *
115
+ * MCP's defaults for a tool that says nothing are the worst case — it writes, it may destroy, it
116
+ * reaches outside — and a client acts on them: over HTTP, ChatGPT badged every tool here, `inbox`
117
+ * included, as a destructive public write. The spec is equally clear that these are hints and
118
+ * nothing more: "clients MUST consider tool annotations to be untrusted unless they come from
119
+ * trusted servers". They are a description, not a permission.
120
+ *
121
+ * `openWorldHint` is false for everything that stays inside Passalong. Only attach_screenshot
122
+ * reaches outside it, for a file on this machine that Passalong did not put there.
123
+ */
124
+ /**
125
+ * What the three verbs that answer with JSON return. Declared only for those: `outputSchema` puts
126
+ * the obligation on the server — "Servers MUST provide structured results that conform to this
127
+ * schema", while a client only SHOULD check — so it is a promise, and promising a shape this
128
+ * server does not control would be worse than staying quiet. `take` is the reason for the rule
129
+ * rather than an exception to it: it answers with the guide's markdown and a lead-in to read, not
130
+ * with a record.
131
+ *
132
+ * Every field is optional and the objects are open. The four calls serve three kinds of guide and
133
+ * the routes add fields freely, so a required key here would be a promise broken by a kind that
134
+ * does not carry it.
135
+ */
136
+ // The object itself is handed to registerTool, not its `.shape`: a shape is rebuilt into a closed
137
+ // object and advertised as `additionalProperties: false`, which would be this server promising that
138
+ // an answer carries nothing else — and then breaking that promise the first time a route adds a
139
+ // field. Passing the object through keeps it open.
140
+ const openObject = (shape) => z.object(shape).partial().passthrough();
141
+ const progressOut = openObject({
142
+ id: z.string(),
143
+ lease_until: z.string(),
144
+ note: z.string(),
145
+ });
146
+ const handInOut = openObject({
147
+ id: z.string(),
148
+ state: z.string(),
149
+ report: z.string(),
150
+ ok: z.boolean(),
151
+ });
152
+ const passOut = openObject({ id: z.string(), passed: z.boolean() });
153
+
154
+ const READS = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
155
+ const ADDS = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
82
156
  const fail = (err) => ({ content: [{ type: "text", text: err.message }], isError: true });
83
157
 
84
158
  /**
@@ -87,7 +161,11 @@ const fail = (err) => ({ content: [{ type: "text", text: err.message }], isError
87
161
  */
88
162
  export function nextNote({ next = [], say = "" } = {}, id = "") {
89
163
  const lines = next.map(
90
- (s) => ` ${s.tool}${id && s.tool !== "take" ? ` ${id}` : ""} — when ${s.when} (${s.why})`,
164
+ (s) =>
165
+ ` ${s.tool}${id && s.tool !== "take" ? ` ${id}` : ""} — when ${s.when} (${s.why})` +
166
+ // What the call has to carry, under the call it belongs to: an agent that reads this at the
167
+ // end of a long session should not have to remember hand_in takes evidence.
168
+ (s.with ? `\n with ${s.with}` : ""),
91
169
  );
92
170
  if (say) lines.unshift(` ${say}`);
93
171
  return lines.length ? `<!-- passalong: next:\n${lines.join("\n")}\n-->` : "";
@@ -102,7 +180,12 @@ const failWith = (err) => {
102
180
  };
103
181
  };
104
182
 
105
- export async function serve() {
183
+ /**
184
+ * The server, built but not connected. Split out so the tool surface can be read without a
185
+ * transport — `test/mcp-surface.test.js` lists the tools over an in-memory pair and checks what
186
+ * each one says about itself. apps/api/src/mcp-http.ts is shaped the same way.
187
+ */
188
+ export function buildServer() {
106
189
  const server = new McpServer(
107
190
  { name: "passalong", version: VERSION },
108
191
  {
@@ -118,6 +201,11 @@ export async function serve() {
118
201
  "stop, stop. OPEN A GUIDE YOU MEAN TO ACT ON WITH take, NOT get_guide: get_guide only " +
119
202
  "reads, and the sender learns nothing. If take says somebody else has it, tell the user " +
120
203
  "instead of doing the work twice.\n" +
204
+ "EVERY HAND-IN CARRIES EVIDENCE: what you ran and what came back — the command and the " +
205
+ "lines that decided it, a test summary, a link to the change, or a screenshot url. " +
206
+ "Collect it as you work rather than writing it from memory at the end. hand_in without " +
207
+ "it is refused, because the write-up and the verdict are both your word for your own " +
208
+ "work and evidence is the part the person reviewing it can check.\n" +
121
209
  `kind: task (the default) is work nobody has done yet. Sections: ${TASK_SECTIONS.join(", ")}. ` +
122
210
  "It has no Steps: work out how to reach Goal within Constraints, leave Out of scope " +
123
211
  "alone, and treat Acceptance as the definition of done. hand_in with `markdown`: a " +
@@ -150,6 +238,16 @@ export async function serve() {
150
238
  "session into a guide (guide_template kind transfer shows the shape) and call " +
151
239
  "publish_guide, with `to` as team, team/@handle for one teammate, or team/#group for the " +
152
240
  "people who do a thing. " +
241
+ "BEFORE PUBLISHING ANYTHING, CHECK WHAT IS ALREADY OPEN: call take with no id, or read the " +
242
+ "inbox, and see what you hold. If this session's work answers something you hold, hand_in " +
243
+ "that — never publish a second guide about it. A transfer guide is for work that has to " +
244
+ "cross a boundary: another repo, another machine, a teammate without your branch. Work you " +
245
+ "committed and pushed where the team can already see it has crossed no boundary, and a " +
246
+ "guide about it is one more thing for somebody to read and review; what is worth " +
247
+ "publishing from a session like that is what is still open, as a bug or a task. " +
248
+ '"Update the passalong", "add this to passalong" and the like are ambiguous — hand in ' +
249
+ "what you hold, add a follow-up, or publish something new? Ask which, in one line, rather " +
250
+ "than publishing and leaving the user to undo it. " +
153
251
  "At the start of work, inbox shows guides teammates have handed to this user — take one " +
154
252
  "to start it — and activity shows whether the guides they handed off have landed. When " +
155
253
  "the user asks what they have been working on, or wants a standup or a summary of a " +
@@ -160,7 +258,9 @@ export async function serve() {
160
258
  "you found doing it — publish that context with publish_guide `parent` set to the guide's " +
161
259
  "id. It is listed under the original, and anyone who opens the original, person or agent, " +
162
260
  "gets it too. get_guide and take return a guide's follow-ups after it; read them " +
163
- "before acting.",
261
+ "before acting. They also return the guide it follows, in front of it: a follow-up is a " +
262
+ "note on a bigger piece of work, not the work. If it needs that guide done and it is " +
263
+ "not, say so rather than starting.",
164
264
  },
165
265
  );
166
266
 
@@ -172,7 +272,12 @@ export async function serve() {
172
272
  const answer = (r, id) => {
173
273
  const { next, say, ...rest } = r || {};
174
274
  const note = nextNote({ next, say }, id);
175
- return text(`${JSON.stringify(rest, null, 2)}${note ? `\n${note}` : ""}`);
275
+ // The note is what the agent acts on and it is prose, so it stays in the text half only. The
276
+ // structured half is the answer itself, which is what `outputSchema` below describes.
277
+ return {
278
+ ...text(`${JSON.stringify(rest, null, 2)}${note ? `\n${note}` : ""}`),
279
+ structuredContent: rest,
280
+ };
176
281
  };
177
282
 
178
283
  async function doTake({ id, cwd, any }) {
@@ -192,12 +297,13 @@ export async function serve() {
192
297
  const meta = parse(r.guide.markdown).meta;
193
298
  const siblings = await related(meta);
194
299
  const context = await passalong.followUps(meta);
300
+ const from = await passalong.parentGuide(meta);
195
301
  const held =
196
302
  `<!-- passalong: ${r.guide.id} is yours` +
197
303
  `${r.guide.resumed ? " (you already held it — carry on from where it was left)" : ""}; ` +
198
304
  `written to ${r.path}. -->`;
199
305
  return text(
200
- `${leadFor(meta)}${r.guide.markdown}${siblings}${context ? `\n\n${context}` : ""}` +
306
+ `${from}${leadFor(meta)}${r.guide.markdown}${siblings}${context ? `\n\n${context}` : ""}` +
201
307
  `\n\n${held}\n${nextNote(r, r.guide.id)}\n${passalong.followUpNote(meta)}`,
202
308
  );
203
309
  } catch (err) {
@@ -220,15 +326,31 @@ export async function serve() {
220
326
  }
221
327
  }
222
328
 
223
- async function doHandIn({ id, ok, note, markdown, report, pr, cwd }) {
329
+ async function doHandIn({ id, ok, note, evidence, checks, writeup, markdown, report, pr, cwd }) {
224
330
  try {
331
+ // A check that names a command is run here, before anything is recorded, and its exit code
332
+ // decides it rather than the agent's account of it. A failure is refused with the command's
333
+ // own output: the claim is still held, nothing is written, and the agent has what it needs
334
+ // to fix. Checks with no `cmd` pass straight through to the prose rule.
335
+ const at = cwd || process.cwd();
336
+ const done = runChecks(checks || [], { cwd: at });
337
+ if (done.failed)
338
+ return {
339
+ content: [
340
+ { type: "text", text: refusal(done.failed, { ran: done.ran, total: checks.length }) },
341
+ ],
342
+ isError: true,
343
+ };
225
344
  const r = await passalong.handIn(id, {
226
345
  ok,
227
346
  note: note || "",
347
+ evidence: evidence || "",
348
+ checks: done.checks,
349
+ writeup: writeup || "",
228
350
  markdown,
229
351
  report,
230
352
  pr: pr || "",
231
- cwd: cwd || process.cwd(),
353
+ cwd: at,
232
354
  });
233
355
  return answer(r, id);
234
356
  } catch (err) {
@@ -244,10 +366,35 @@ export async function serve() {
244
366
  }
245
367
  }
246
368
 
369
+ server.registerTool(
370
+ "assign",
371
+ {
372
+ title: "Give it to someone else",
373
+ annotations: ADDS,
374
+ description:
375
+ "Reassign a guide or task the user wrote, or one assigned to them, to someone else in its team, when the user asks: " +
376
+ '`to` is @handle for one person, #group for the people who do a thing, or "team" for ' +
377
+ "everyone. Whoever held it and is left out has it taken back and is told; a task then only " +
378
+ "goes to the new assignee's agents. Its author, or whoever it is assigned to, can do this.",
379
+ inputSchema: {
380
+ id: z.string().describe("passalong id"),
381
+ to: z.string().describe('"@handle", "#group", or "team"'),
382
+ },
383
+ },
384
+ async ({ id, to }) => {
385
+ try {
386
+ return json(await passalong.assign(id, to));
387
+ } catch (err) {
388
+ return fail(err);
389
+ }
390
+ },
391
+ );
392
+
247
393
  server.registerTool(
248
394
  "search_guides",
249
395
  {
250
396
  title: "Search guides",
397
+ annotations: READS,
251
398
  description:
252
399
  "Search transfer guides by words in the title, tags, stack, or body: the user's own " +
253
400
  "(local and synced) plus every team they belong to. Empty query lists everything, newest first.",
@@ -270,6 +417,7 @@ export async function serve() {
270
417
  "inbox",
271
418
  {
272
419
  title: "Inbox",
420
+ annotations: READS,
273
421
  description:
274
422
  "Guides handed to this user (or to their teams) that they have not pulled yet. Call it " +
275
423
  "when starting work so handoffs are not missed. Needs sync (passalong login).",
@@ -288,6 +436,7 @@ export async function serve() {
288
436
  "board",
289
437
  {
290
438
  title: "Board",
439
+ annotations: READS,
291
440
  description:
292
441
  "The state of this user's transfers as queues: waiting on you (handed to you, not " +
293
442
  "pulled), not working (someone gave it a failing verdict — the most urgent), in flight " +
@@ -308,6 +457,7 @@ export async function serve() {
308
457
  "log",
309
458
  {
310
459
  title: "What this user did",
460
+ annotations: READS,
311
461
  description:
312
462
  "This user's own acts on guides, newest first: what they published, what they took " +
313
463
  "delivery of, and every verdict and ack they gave. Each item has a rendered `text` line " +
@@ -334,91 +484,62 @@ export async function serve() {
334
484
  },
335
485
  );
336
486
 
337
- server.registerTool(
338
- "ack_guide",
339
- {
340
- title: "Take it or pass it (same as take / pass)",
341
- description:
342
- "Kept for older prompts: taken=true is take with this id, taken=false is pass with `note` " +
343
- "as the reason.",
344
- inputSchema: {
345
- id: z.string().describe("passalong id"),
346
- taken: z.boolean().describe("true if you are doing it; false hands it back"),
347
- note: z.string().default("").describe("why it is not yours (required when taken is false)"),
348
- cwd: z
349
- .string()
350
- .optional()
351
- .describe("the worktree you are working in; default is the server's cwd"),
352
- },
353
- },
354
- async ({ id, taken, note, cwd }) =>
355
- taken ? doTake({ id, cwd }) : doPass({ id, why: note || "", cwd }),
356
- );
357
-
358
- server.registerTool(
359
- "verify_guide",
360
- {
361
- title: "Report whether it works (same as hand_in)",
362
- description: "Kept for older prompts: exactly hand_in with `ok` and `note`.",
363
- inputSchema: {
364
- id: z.string().describe("passalong id"),
365
- ok: z.boolean().describe("true if the Verification steps passed"),
366
- note: z.string().default("").describe("what went wrong (required when ok is false)"),
367
- cwd: z
368
- .string()
369
- .optional()
370
- .describe("the worktree you are working in; default is the server's cwd"),
371
- },
372
- },
373
- async (args) => doHandIn(args),
374
- );
375
-
376
487
  server.registerTool(
377
488
  "activity",
378
489
  {
379
490
  title: "Activity",
491
+ annotations: READS,
380
492
  description:
381
- "What has happened to this user's guides and handoffs: who pulled one, who marked one " +
382
- "consumed, who was handed what, who joined a team. Each item has a ready-made `text` " +
383
- "line. Read-only by default — it does not clear the user's unread feed unless asked.",
493
+ "What has happened to this user's guides and handoffs: who pulled one, who archived one, " +
494
+ "who was handed what, who joined a team. Each item has a ready-made `text` line. This only " +
495
+ "reads; clear_activity is what marks the feed seen.",
384
496
  inputSchema: {
385
497
  all: z.boolean().default(false).describe("include what the user has already seen"),
386
- mark_read: z.boolean().default(false).describe("clear the unread feed after reading"),
387
498
  },
388
499
  },
389
- async ({ all, mark_read }) => {
500
+ async ({ all }) => {
390
501
  try {
391
- const res = await passalong.activity({ all });
392
- if (mark_read && res.unread) await passalong.seen();
393
- return json(res);
502
+ return json(await passalong.activity({ all }));
394
503
  } catch (err) {
395
504
  return fail(err);
396
505
  }
397
506
  },
398
507
  );
399
508
 
509
+ /**
510
+ * Split out of `activity`, which took `mark_read` and so read on one call and wrote on the next.
511
+ *
512
+ * An annotation cannot say "sometimes": `readOnlyHint` was false for a tool that almost always
513
+ * only reads, which is a client being told the cautious lie on every call rather than the truth
514
+ * on the one that matters. Two tools, each true about itself.
515
+ */
400
516
  server.registerTool(
401
- "start_guide",
517
+ "clear_activity",
402
518
  {
403
- title: "Start work on a guide (same as take with an id)",
519
+ title: "Mark activity seen",
520
+ annotations: ADDS,
404
521
  description:
405
- "Kept for older prompts: exactly take with this id — say you are doing it and get it in " +
406
- "one call. Use it the moment you are going to do the work.",
407
- inputSchema: {
408
- ref: z.string().describe("passalong id (e.g. k3mq2xa7) or share URL"),
409
- cwd: z
410
- .string()
411
- .optional()
412
- .describe("the worktree you are working in; default is the server's cwd"),
413
- },
522
+ "Clear the user's unread feed, after they have been shown what is in it. Read it with " +
523
+ "activity first — clearing what nobody was told about loses it.",
524
+ inputSchema: {},
525
+ },
526
+ async () => {
527
+ try {
528
+ const res = await passalong.activity({ all: false });
529
+ if (!res.unread) return json({ cleared: 0, unread: 0 });
530
+ await passalong.seen();
531
+ return json({ cleared: res.unread, unread: 0 });
532
+ } catch (err) {
533
+ return fail(err);
534
+ }
414
535
  },
415
- async ({ ref, cwd }) => doTake({ id: ref, cwd }),
416
536
  );
417
537
 
418
538
  server.registerTool(
419
539
  "plan_tasks",
420
540
  {
421
541
  title: "Plan a goal as tasks",
542
+ annotations: ADDS,
422
543
  description:
423
544
  "Write tasks for agents: one step for a single task, or a larger goal broken into steps " +
424
545
  "an agent can each finish and a person can each check, written as drafts in order. The " +
@@ -462,68 +583,11 @@ export async function serve() {
462
583
  },
463
584
  );
464
585
 
465
- server.registerTool(
466
- "next_task",
467
- {
468
- title: "Take the next task (same as take with no id)",
469
- description:
470
- "Kept for older prompts: exactly take with no id. Takes the next guide waiting for this " +
471
- "worktree's agent, or gives back the one it already holds.",
472
- inputSchema: {
473
- cwd: z
474
- .string()
475
- .optional()
476
- .describe("the worktree you are working in; default is the server's cwd"),
477
- any: z
478
- .boolean()
479
- .optional()
480
- .describe("any repo, or none — only when the user asks for that"),
481
- },
482
- },
483
- async ({ cwd, any }) => doTake({ cwd, any }),
484
- );
485
-
486
- server.registerTool(
487
- "task_progress",
488
- {
489
- title: "Report progress (same as progress)",
490
- description: "Kept for older prompts: exactly progress.",
491
- inputSchema: {
492
- id: z.string().describe("the id of what you hold"),
493
- note: z.string().optional().describe("one line"),
494
- cwd: z
495
- .string()
496
- .optional()
497
- .describe("the worktree you are working in; default is the server's cwd"),
498
- },
499
- },
500
- async (args) => doProgress(args),
501
- );
502
-
503
- server.registerTool(
504
- "finish_task",
505
- {
506
- title: "Finish a task (same as hand_in)",
507
- description: "Kept for older prompts: exactly hand_in, for a task.",
508
- inputSchema: {
509
- id: z.string().describe("the task's id"),
510
- markdown: z.string().optional().describe("the transfer guide about this work"),
511
- report: z.string().optional().describe("instead of markdown: id of one already published"),
512
- pr: z.string().optional().describe("PR or branch link, when there is one"),
513
- note: z.string().optional().describe("one line for the hub"),
514
- cwd: z
515
- .string()
516
- .optional()
517
- .describe("the worktree you are working in; default is the server's cwd"),
518
- },
519
- },
520
- async (args) => doHandIn(args),
521
- );
522
-
523
586
  server.registerTool(
524
587
  "take",
525
588
  {
526
589
  title: "Take work",
590
+ annotations: ADDS,
527
591
  description:
528
592
  "Say you are doing it, and get it. With `id`, that guide — any kind: a task, a bug, a " +
529
593
  "handoff. With no id, the next thing waiting for this worktree's agent. While you hold it " +
@@ -553,6 +617,8 @@ export async function serve() {
553
617
  "progress",
554
618
  {
555
619
  title: "Report progress",
620
+ outputSchema: progressOut,
621
+ annotations: ADDS,
556
622
  description:
557
623
  "Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
558
624
  "without one marks it stalled. If the answer says you no longer hold it, stop.",
@@ -572,16 +638,74 @@ export async function serve() {
572
638
  "hand_in",
573
639
  {
574
640
  title: "Hand it in",
641
+ outputSchema: handInOut,
642
+ annotations: ADDS,
575
643
  description:
576
- "Done here. A task: `markdown`, a transfer guide about what you did, decided and how you " +
577
- "checked each Acceptance line — this publishes it and attaches it; its author reviews it. " +
578
- "A handoff or a bug: `ok`, whether its Verification held here, and `note` saying what went " +
579
- "wrong when it did not. More context than a note holds goes in a follow-up: publish_guide " +
580
- "with `parent` set to this id.",
644
+ "Done here, with proof, sorted against the line it answers. Send `checks`: one entry per " +
645
+ "line the guide asks for — `## Acceptance` on a task, `## Verification` on a handoff or a " +
646
+ "bug — each with that line and the evidence for it. That is what its author reads, line " +
647
+ "against line, instead of hunting through a wall of output for the part that answers each " +
648
+ "one, AND IT IS HOW YOU KNOW YOU ARE DONE: every line has one. Give a check `cmd` when a " +
649
+ "command proves it: the command is run here before the hand-in lands, a non-zero exit " +
650
+ "refuses it, and what it printed is recorded instead of your account of it. Use `ran` for " +
651
+ "a check no command can settle. `evidence` as one block is the older shape and still " +
652
+ "works. A task also takes `markdown`, a transfer guide about what you did and decided — " +
653
+ "this publishes it and attaches it. A handoff or a bug takes `ok`, whether its " +
654
+ "Verification held here, and `note` saying what went wrong when it did not. DO NOT " +
655
+ "publish a guide to carry your evidence: it belongs on this call, and a second document " +
656
+ 'titled "Hand-in evidence" is the thing `checks` exists to replace. If it worked but you ' +
657
+ "had to adapt something, that goes in `writeup` and the next reader of the guide is shown " +
658
+ "it — that is its home. A follow-up guide is for work somebody else should now do, not " +
659
+ "for answering.",
581
660
  inputSchema: {
582
661
  id: z.string().describe("passalong id"),
662
+ evidence: z
663
+ .string()
664
+ .optional()
665
+ .describe(
666
+ "what you ran and what came back: the command and the lines that decided it, a test " +
667
+ 'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
668
+ ),
669
+ checks: z
670
+ .array(
671
+ z.object({
672
+ check: z
673
+ .string()
674
+ .describe(
675
+ "the line this answers, in the guide's own words: an Acceptance line on a task, " +
676
+ "a Verification line on a handoff or a bug",
677
+ ),
678
+ ran: z
679
+ .string()
680
+ .optional()
681
+ .describe("what you ran for it, and what came back; not needed when `cmd` is set"),
682
+ cmd: z
683
+ .string()
684
+ .optional()
685
+ .describe(
686
+ "the shell command that proves this line, run here before the hand-in lands — " +
687
+ "its exit code decides the check and its output is recorded as the evidence. " +
688
+ "Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
689
+ '"the badge reads 3", and write `ran` instead',
690
+ ),
691
+ }),
692
+ )
693
+ .optional()
694
+ .describe(
695
+ "one entry per line the guide asks for — Acceptance on a task, Verification on a " +
696
+ "handoff or a bug — in the order you worked them",
697
+ ),
583
698
  ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
584
699
  note: z.string().optional().describe("one line; required when ok is false"),
700
+ writeup: z
701
+ .string()
702
+ .optional()
703
+ .describe(
704
+ "handoff or bug: what you had to adapt to make it work here — a version, a name, a " +
705
+ "step that needed something the guide does not mention. Prose, and optional: leave " +
706
+ "it out when it worked as written. The next person to open the guide is shown it, " +
707
+ "which is why it does not need to be a guide of its own",
708
+ ),
585
709
  markdown: z
586
710
  .string()
587
711
  .optional()
@@ -604,6 +728,8 @@ export async function serve() {
604
728
  "pass",
605
729
  {
606
730
  title: "Pass it",
731
+ outputSchema: passOut,
732
+ annotations: ADDS,
607
733
  description:
608
734
  "Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
609
735
  "the next agent, and the reason goes to whoever is next — say why, or they start where " +
@@ -624,6 +750,7 @@ export async function serve() {
624
750
  "get_guide",
625
751
  {
626
752
  title: "Get guide",
753
+ annotations: READS,
627
754
  description:
628
755
  "READ a transfer guide by passalong id or share link and return its full markdown. Also " +
629
756
  "writes it to .passalong/<id>.md in the working directory so it survives the session. " +
@@ -648,13 +775,16 @@ export async function serve() {
648
775
  // Follow-ups are more context for this guide, so they come with it — after the document,
649
776
  // never inside it, so the guide an agent writes back out is still only the guide.
650
777
  const context = await passalong.followUps(meta);
778
+ // The guide this one came out of, in front of the document: a follow-up read on its own
779
+ // looks like a small piece of work and is a note on a bigger one.
780
+ const follows = await passalong.parentGuide(meta);
651
781
  // After the document, with the other trailing comments, not in front of it. An
652
782
  // instruction that arrives with the payload is what gets read — but anything before the
653
783
  // opening `---` stops the frontmatter being frontmatter, and this fires on every guide
654
784
  // anyone was handed rather than only on bugs. The bug lead stays where it is: it is a
655
785
  // warning against executing the document, so being read first is its whole job.
656
786
  return text(
657
- `${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
787
+ `${follows}${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
658
788
  `\n\n<!-- passalong: ${from}; written to ${path} -->` +
659
789
  `\n${passalong.handoffNudge(meta)}` +
660
790
  `\n${passalong.followUpNote(meta)}`,
@@ -669,6 +799,7 @@ export async function serve() {
669
799
  "attach_screenshot",
670
800
  {
671
801
  title: "Attach a screenshot",
802
+ annotations: { ...ADDS, openWorldHint: true },
672
803
  description:
673
804
  "Upload an image from this machine as evidence, and get back the markdown line that " +
674
805
  "points at it. Put that line in the guide body — a guide travels as markdown to whoever " +
@@ -697,8 +828,12 @@ export async function serve() {
697
828
  "publish_guide",
698
829
  {
699
830
  title: "Publish guide",
831
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
700
832
  description:
701
- "Publish a guide from markdown (frontmatter + sections) — a transfer guide by default, or " +
833
+ "Publish a guide from markdown (frontmatter + sections). Check what you hold first (take " +
834
+ "with no id): if this session answers something you hold, hand_in that instead, and if the " +
835
+ "work never left a branch the team can see, publish what is still open as a bug or a task " +
836
+ "rather than a write-up of the fix. A transfer guide by default, or " +
702
837
  "a single bug with `kind: bug`; use file_bugs for more than one; or a task for work nobody " +
703
838
  "has done yet with `kind: task`. Missing id, created, " +
704
839
  "author, and source_context are filled in. A screenshot belongs in the markdown: attach it " +
@@ -761,6 +896,7 @@ export async function serve() {
761
896
  "file_bugs",
762
897
  {
763
898
  title: "File bugs",
899
+ annotations: ADDS,
764
900
  description:
765
901
  "File one or more bugs you found but are not fixing, as a set. Opens a report and " +
766
902
  "publishes each issue as its own guide — its own id, share link, and verdict — so a " +
@@ -846,6 +982,7 @@ export async function serve() {
846
982
  "guide_template",
847
983
  {
848
984
  title: "Guide template",
985
+ annotations: READS,
849
986
  description:
850
987
  "The empty guide skeleton with guidance comments for each section. By default a task " +
851
988
  "brief: Goal, Context, Constraints, Acceptance, Out of scope. `kind: transfer` gives the " +
@@ -867,6 +1004,7 @@ export async function serve() {
867
1004
  "set_guide_status",
868
1005
  {
869
1006
  title: "Set guide status",
1007
+ annotations: ADDS,
870
1008
  description:
871
1009
  "Archive a guide (`consumed`) or put it back on the board (`published`). Archiving is the " +
872
1010
  "author's shelf: off the board, out of the free tier's count, reversible — it is not a " +
@@ -884,5 +1022,10 @@ export async function serve() {
884
1022
  },
885
1023
  );
886
1024
 
887
- await server.connect(new StdioServerTransport());
1025
+ return server;
1026
+ }
1027
+
1028
+ /** The stdio server: `passalong mcp`, and what `passalong setup` registers with a client. */
1029
+ export async function serve() {
1030
+ await buildServer().connect(new StdioServerTransport());
888
1031
  }