passalong 0.6.0 → 0.8.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,30 @@ 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, 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,
228
349
  markdown,
229
350
  report,
230
351
  pr: pr || "",
231
- cwd: cwd || process.cwd(),
352
+ cwd: at,
232
353
  });
233
354
  return answer(r, id);
234
355
  } catch (err) {
@@ -244,10 +365,35 @@ export async function serve() {
244
365
  }
245
366
  }
246
367
 
368
+ server.registerTool(
369
+ "assign",
370
+ {
371
+ title: "Give it to someone else",
372
+ annotations: ADDS,
373
+ description:
374
+ "Reassign a guide or task the user wrote, or one assigned to them, to someone else in its team, when the user asks: " +
375
+ '`to` is @handle for one person, #group for the people who do a thing, or "team" for ' +
376
+ "everyone. Whoever held it and is left out has it taken back and is told; a task then only " +
377
+ "goes to the new assignee's agents. Its author, or whoever it is assigned to, can do this.",
378
+ inputSchema: {
379
+ id: z.string().describe("passalong id"),
380
+ to: z.string().describe('"@handle", "#group", or "team"'),
381
+ },
382
+ },
383
+ async ({ id, to }) => {
384
+ try {
385
+ return json(await passalong.assign(id, to));
386
+ } catch (err) {
387
+ return fail(err);
388
+ }
389
+ },
390
+ );
391
+
247
392
  server.registerTool(
248
393
  "search_guides",
249
394
  {
250
395
  title: "Search guides",
396
+ annotations: READS,
251
397
  description:
252
398
  "Search transfer guides by words in the title, tags, stack, or body: the user's own " +
253
399
  "(local and synced) plus every team they belong to. Empty query lists everything, newest first.",
@@ -270,6 +416,7 @@ export async function serve() {
270
416
  "inbox",
271
417
  {
272
418
  title: "Inbox",
419
+ annotations: READS,
273
420
  description:
274
421
  "Guides handed to this user (or to their teams) that they have not pulled yet. Call it " +
275
422
  "when starting work so handoffs are not missed. Needs sync (passalong login).",
@@ -288,6 +435,7 @@ export async function serve() {
288
435
  "board",
289
436
  {
290
437
  title: "Board",
438
+ annotations: READS,
291
439
  description:
292
440
  "The state of this user's transfers as queues: waiting on you (handed to you, not " +
293
441
  "pulled), not working (someone gave it a failing verdict — the most urgent), in flight " +
@@ -308,6 +456,7 @@ export async function serve() {
308
456
  "log",
309
457
  {
310
458
  title: "What this user did",
459
+ annotations: READS,
311
460
  description:
312
461
  "This user's own acts on guides, newest first: what they published, what they took " +
313
462
  "delivery of, and every verdict and ack they gave. Each item has a rendered `text` line " +
@@ -334,91 +483,62 @@ export async function serve() {
334
483
  },
335
484
  );
336
485
 
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
486
  server.registerTool(
377
487
  "activity",
378
488
  {
379
489
  title: "Activity",
490
+ annotations: READS,
380
491
  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.",
492
+ "What has happened to this user's guides and handoffs: who pulled one, who archived one, " +
493
+ "who was handed what, who joined a team. Each item has a ready-made `text` line. This only " +
494
+ "reads; clear_activity is what marks the feed seen.",
384
495
  inputSchema: {
385
496
  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
497
  },
388
498
  },
389
- async ({ all, mark_read }) => {
499
+ async ({ all }) => {
390
500
  try {
391
- const res = await passalong.activity({ all });
392
- if (mark_read && res.unread) await passalong.seen();
393
- return json(res);
501
+ return json(await passalong.activity({ all }));
394
502
  } catch (err) {
395
503
  return fail(err);
396
504
  }
397
505
  },
398
506
  );
399
507
 
508
+ /**
509
+ * Split out of `activity`, which took `mark_read` and so read on one call and wrote on the next.
510
+ *
511
+ * An annotation cannot say "sometimes": `readOnlyHint` was false for a tool that almost always
512
+ * only reads, which is a client being told the cautious lie on every call rather than the truth
513
+ * on the one that matters. Two tools, each true about itself.
514
+ */
400
515
  server.registerTool(
401
- "start_guide",
516
+ "clear_activity",
402
517
  {
403
- title: "Start work on a guide (same as take with an id)",
518
+ title: "Mark activity seen",
519
+ annotations: ADDS,
404
520
  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
- },
521
+ "Clear the user's unread feed, after they have been shown what is in it. Read it with " +
522
+ "activity first — clearing what nobody was told about loses it.",
523
+ inputSchema: {},
524
+ },
525
+ async () => {
526
+ try {
527
+ const res = await passalong.activity({ all: false });
528
+ if (!res.unread) return json({ cleared: 0, unread: 0 });
529
+ await passalong.seen();
530
+ return json({ cleared: res.unread, unread: 0 });
531
+ } catch (err) {
532
+ return fail(err);
533
+ }
414
534
  },
415
- async ({ ref, cwd }) => doTake({ id: ref, cwd }),
416
535
  );
417
536
 
418
537
  server.registerTool(
419
538
  "plan_tasks",
420
539
  {
421
540
  title: "Plan a goal as tasks",
541
+ annotations: ADDS,
422
542
  description:
423
543
  "Write tasks for agents: one step for a single task, or a larger goal broken into steps " +
424
544
  "an agent can each finish and a person can each check, written as drafts in order. The " +
@@ -462,68 +582,11 @@ export async function serve() {
462
582
  },
463
583
  );
464
584
 
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
585
  server.registerTool(
524
586
  "take",
525
587
  {
526
588
  title: "Take work",
589
+ annotations: ADDS,
527
590
  description:
528
591
  "Say you are doing it, and get it. With `id`, that guide — any kind: a task, a bug, a " +
529
592
  "handoff. With no id, the next thing waiting for this worktree's agent. While you hold it " +
@@ -553,6 +616,8 @@ export async function serve() {
553
616
  "progress",
554
617
  {
555
618
  title: "Report progress",
619
+ outputSchema: progressOut,
620
+ annotations: ADDS,
556
621
  description:
557
622
  "Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
558
623
  "without one marks it stalled. If the answer says you no longer hold it, stop.",
@@ -572,14 +637,51 @@ export async function serve() {
572
637
  "hand_in",
573
638
  {
574
639
  title: "Hand it in",
640
+ outputSchema: handInOut,
641
+ annotations: ADDS,
575
642
  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.",
643
+ "Done here, with proof. Every hand-in carries what you ran and what came back. On a TASK " +
644
+ "send `checks`: one entry per Acceptance line, each with that line and the evidence for " +
645
+ "it — that is what its author reads, line against line, instead of hunting through a wall " +
646
+ "of output for the part that answers each one. Give a check `cmd` when a command proves " +
647
+ "it: the command is run here before the hand-in lands, a non-zero exit refuses it, and " +
648
+ "what it printed is recorded instead of your account of it. Use `ran` for a check no " +
649
+ "command can settle. `evidence` is the same thing as one block, " +
650
+ "for a handoff or a bug, which have no Acceptance lines. A task also takes `markdown`, a " +
651
+ "transfer guide about what you did and decided — this publishes it and attaches it. A " +
652
+ "handoff or a bug takes `ok`, whether its Verification held here, and `note` saying what " +
653
+ "went wrong when it did not. More context than a note holds goes in a follow-up: " +
654
+ "publish_guide with `parent` set to this id.",
581
655
  inputSchema: {
582
656
  id: z.string().describe("passalong id"),
657
+ evidence: z
658
+ .string()
659
+ .optional()
660
+ .describe(
661
+ "what you ran and what came back: the command and the lines that decided it, a test " +
662
+ 'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
663
+ ),
664
+ checks: z
665
+ .array(
666
+ z.object({
667
+ check: z.string().describe("the Acceptance line this answers, in the task's words"),
668
+ ran: z
669
+ .string()
670
+ .optional()
671
+ .describe("what you ran for it, and what came back; not needed when `cmd` is set"),
672
+ cmd: z
673
+ .string()
674
+ .optional()
675
+ .describe(
676
+ "the shell command that proves this line, run here before the hand-in lands — " +
677
+ "its exit code decides the check and its output is recorded as the evidence. " +
678
+ "Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
679
+ '"the badge reads 3", and write `ran` instead',
680
+ ),
681
+ }),
682
+ )
683
+ .optional()
684
+ .describe("task: one entry per Acceptance line, in the order you worked them"),
583
685
  ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
584
686
  note: z.string().optional().describe("one line; required when ok is false"),
585
687
  markdown: z
@@ -604,6 +706,8 @@ export async function serve() {
604
706
  "pass",
605
707
  {
606
708
  title: "Pass it",
709
+ outputSchema: passOut,
710
+ annotations: ADDS,
607
711
  description:
608
712
  "Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
609
713
  "the next agent, and the reason goes to whoever is next — say why, or they start where " +
@@ -624,6 +728,7 @@ export async function serve() {
624
728
  "get_guide",
625
729
  {
626
730
  title: "Get guide",
731
+ annotations: READS,
627
732
  description:
628
733
  "READ a transfer guide by passalong id or share link and return its full markdown. Also " +
629
734
  "writes it to .passalong/<id>.md in the working directory so it survives the session. " +
@@ -648,13 +753,16 @@ export async function serve() {
648
753
  // Follow-ups are more context for this guide, so they come with it — after the document,
649
754
  // never inside it, so the guide an agent writes back out is still only the guide.
650
755
  const context = await passalong.followUps(meta);
756
+ // The guide this one came out of, in front of the document: a follow-up read on its own
757
+ // looks like a small piece of work and is a note on a bigger one.
758
+ const follows = await passalong.parentGuide(meta);
651
759
  // After the document, with the other trailing comments, not in front of it. An
652
760
  // instruction that arrives with the payload is what gets read — but anything before the
653
761
  // opening `---` stops the frontmatter being frontmatter, and this fires on every guide
654
762
  // anyone was handed rather than only on bugs. The bug lead stays where it is: it is a
655
763
  // warning against executing the document, so being read first is its whole job.
656
764
  return text(
657
- `${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
765
+ `${follows}${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
658
766
  `\n\n<!-- passalong: ${from}; written to ${path} -->` +
659
767
  `\n${passalong.handoffNudge(meta)}` +
660
768
  `\n${passalong.followUpNote(meta)}`,
@@ -669,6 +777,7 @@ export async function serve() {
669
777
  "attach_screenshot",
670
778
  {
671
779
  title: "Attach a screenshot",
780
+ annotations: { ...ADDS, openWorldHint: true },
672
781
  description:
673
782
  "Upload an image from this machine as evidence, and get back the markdown line that " +
674
783
  "points at it. Put that line in the guide body — a guide travels as markdown to whoever " +
@@ -697,8 +806,12 @@ export async function serve() {
697
806
  "publish_guide",
698
807
  {
699
808
  title: "Publish guide",
809
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
700
810
  description:
701
- "Publish a guide from markdown (frontmatter + sections) — a transfer guide by default, or " +
811
+ "Publish a guide from markdown (frontmatter + sections). Check what you hold first (take " +
812
+ "with no id): if this session answers something you hold, hand_in that instead, and if the " +
813
+ "work never left a branch the team can see, publish what is still open as a bug or a task " +
814
+ "rather than a write-up of the fix. A transfer guide by default, or " +
702
815
  "a single bug with `kind: bug`; use file_bugs for more than one; or a task for work nobody " +
703
816
  "has done yet with `kind: task`. Missing id, created, " +
704
817
  "author, and source_context are filled in. A screenshot belongs in the markdown: attach it " +
@@ -761,6 +874,7 @@ export async function serve() {
761
874
  "file_bugs",
762
875
  {
763
876
  title: "File bugs",
877
+ annotations: ADDS,
764
878
  description:
765
879
  "File one or more bugs you found but are not fixing, as a set. Opens a report and " +
766
880
  "publishes each issue as its own guide — its own id, share link, and verdict — so a " +
@@ -846,6 +960,7 @@ export async function serve() {
846
960
  "guide_template",
847
961
  {
848
962
  title: "Guide template",
963
+ annotations: READS,
849
964
  description:
850
965
  "The empty guide skeleton with guidance comments for each section. By default a task " +
851
966
  "brief: Goal, Context, Constraints, Acceptance, Out of scope. `kind: transfer` gives the " +
@@ -867,6 +982,7 @@ export async function serve() {
867
982
  "set_guide_status",
868
983
  {
869
984
  title: "Set guide status",
985
+ annotations: ADDS,
870
986
  description:
871
987
  "Archive a guide (`consumed`) or put it back on the board (`published`). Archiving is the " +
872
988
  "author's shelf: off the board, out of the free tier's count, reversible — it is not a " +
@@ -884,5 +1000,10 @@ export async function serve() {
884
1000
  },
885
1001
  );
886
1002
 
887
- await server.connect(new StdioServerTransport());
1003
+ return server;
1004
+ }
1005
+
1006
+ /** The stdio server: `passalong mcp`, and what `passalong setup` registers with a client. */
1007
+ export async function serve() {
1008
+ await buildServer().connect(new StdioServerTransport());
888
1009
  }