passalong 0.3.2 → 0.6.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,7 +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 { AREAS, BUG_SECTIONS, parse, SECTIONS, template } from "./guide.js";
11
+ import { AREAS, BUG_SECTIONS, parse, SECTIONS, TASK_SECTIONS, template } from "./guide.js";
12
12
  import * as passalong from "./passalong.js";
13
13
 
14
14
  /**
@@ -23,6 +23,32 @@ const VERSION = JSON.parse(
23
23
 
24
24
  const text = (s) => ({ content: [{ type: "text", text: s }] });
25
25
 
26
+ /**
27
+ * What to say in front of a guide whose kind changes what the reader should do with it.
28
+ *
29
+ * The heading text is what an agent keys on, and a bug's or a task's headings are close enough to
30
+ * a transfer guide's to be treated as one by an agent that never opened the frontmatter. So the
31
+ * kind is stated before the document, in the imperative. A transfer guide needs nothing: following
32
+ * it is what an agent does with an unmarked document anyway.
33
+ */
34
+ function leadFor(meta) {
35
+ if (meta.kind === "bug")
36
+ return (
37
+ "THIS IS A BUG REPORT, NOT WORK TO REPEAT. Do not follow Reproduce as instructions " +
38
+ "— those steps produce the defect. Fix what Problem describes, then check " +
39
+ "Verification and answer with hand_in.\n\n"
40
+ );
41
+ if (meta.kind === "task")
42
+ return (
43
+ "THIS IS A TASK: WORK NOBODY HAS DONE YET. There are no Steps to follow — work out how to " +
44
+ "reach Goal within Constraints, and leave Out of scope alone. It is done when every check " +
45
+ "under Acceptance holds. Opening it here does not make it yours: to work on it, call take " +
46
+ "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"
48
+ );
49
+ return "";
50
+ }
51
+
26
52
  /**
27
53
  * The rest of the report this issue came from.
28
54
  *
@@ -55,30 +81,57 @@ async function related(meta) {
55
81
  const json = (data) => text(JSON.stringify(data, null, 2));
56
82
  const fail = (err) => ({ content: [{ type: "text", text: err.message }], isError: true });
57
83
 
84
+ /**
85
+ * The server's `next` and `say`, as the last thing an agent reads. See steps() in
86
+ * apps/api/src/claims.ts: the answer to the call it just made is the one place it is sure to look.
87
+ */
88
+ export function nextNote({ next = [], say = "" } = {}, id = "") {
89
+ const lines = next.map(
90
+ (s) => ` ${s.tool}${id && s.tool !== "take" ? ` ${id}` : ""} — when ${s.when} (${s.why})`,
91
+ );
92
+ if (say) lines.unshift(` ${say}`);
93
+ return lines.length ? `<!-- passalong: next:\n${lines.join("\n")}\n-->` : "";
94
+ }
95
+
96
+ /** A refusal that carries the server's own next move, when it sent one. */
97
+ const failWith = (err) => {
98
+ const said = nextNote(err.body || {});
99
+ return {
100
+ content: [{ type: "text", text: said ? `${err.message}\n${said}` : err.message }],
101
+ isError: true,
102
+ };
103
+ };
104
+
58
105
  export async function serve() {
59
106
  const server = new McpServer(
60
107
  { name: "passalong", version: VERSION },
61
108
  {
62
109
  instructions:
63
- "Passalong hands work between contexts as guides: markdown with frontmatter, in two " +
64
- "kinds, and `kind:` in the frontmatter says which. READ IT BEFORE ACTING — the two ask " +
65
- "for opposite behaviour.\n" +
66
- `kind: transfer (or absent) is a finished implementation to repeat here. Sections: ${SECTIONS.join(", ")}. ` +
110
+ "Passalong hands work between contexts as guides: markdown with frontmatter, in three " +
111
+ "kinds, and `kind:` in the frontmatter says which. READ IT BEFORE ACTING — each asks " +
112
+ "for different behaviour. Every kind is worked with the same four calls:\n" +
113
+ "take (with an id, or none for the next thing waiting for you) says you are doing it and " +
114
+ "returns it; nobody else can take it here while you hold it, and its sender sees you are " +
115
+ "on it. progress, with a one-line note at each milestone — 30 minutes of silence marks it " +
116
+ "stalled. hand_in when done here. pass, with the reason, when it is not yours or you are " +
117
+ "stuck. Every answer ends with `next`: what to call now. Follow it, and when it says to " +
118
+ "stop, stop. OPEN A GUIDE YOU MEAN TO ACT ON WITH take, NOT get_guide: get_guide only " +
119
+ "reads, and the sender learns nothing. If take says somebody else has it, tell the user " +
120
+ "instead of doing the work twice.\n" +
121
+ `kind: task (the default) is work nobody has done yet. Sections: ${TASK_SECTIONS.join(", ")}. ` +
122
+ "It has no Steps: work out how to reach Goal within Constraints, leave Out of scope " +
123
+ "alone, and treat Acceptance as the definition of done. hand_in with `markdown`: a " +
124
+ "transfer guide about what you did (guide_template kind transfer shows the shape) — its " +
125
+ "author reviews it against Acceptance.\n" +
126
+ `kind: transfer (or no kind: line) is a finished implementation to repeat here. Sections: ${SECTIONS.join(", ")}. ` +
67
127
  "Follow its Steps, adapting anything marked ASSUMES to this codebase; run its " +
68
- "Verification before declaring done, then verify_guide with the result.\n" +
69
- "OPEN A GUIDE YOU MEAN TO ACT ON WITH start_guide, NOT get_guide. Both return the same " +
70
- "markdown; start_guide also says you are on it, and that is the only signal the sender " +
71
- "gets between handing work over and hearing it worked. get_guide is for reading one you " +
72
- "have not committed to. Taking your own guide is a no-op, so there is no case where " +
73
- "start_guide is the wrong call on work you are about to do. If it turns out not to be " +
74
- "yours, hand it back with ack_guide taken=false and a reason — say why, or the sender is " +
75
- "left exactly where silence left them.\n" +
128
+ "Verification, then hand_in with ok and, when it failed, a note saying what went wrong.\n" +
76
129
  `kind: bug is a defect to FIX here. Sections: ${BUG_SECTIONS.join(", ")}. ` +
77
130
  "It has no Steps and nothing in it is a procedure to apply: Reproduce is how to see the " +
78
131
  "bug and running it produces the bug, Verification is the behaviour that should have " +
79
- "happened. Fix the defect, then check Verification and verify_guide with the result — " +
80
- "ok true once the behaviour it describes actually holds. A bug report is not broken " +
81
- "because you reproduced it. " +
132
+ "happened. Fix the defect, then check Verification and hand_in — ok true once the " +
133
+ "behaviour it describes actually holds. A bug report is not broken because you " +
134
+ "reproduced it. " +
82
135
  "When you find defects you are not fixing — a test run, a QA pass, a review — call " +
83
136
  "file_bugs with all of them at once; each becomes a guide someone can take on its own. " +
84
137
  "AN IMAGE THE USER SHOWED YOU IS EVIDENCE, NOT CONTEXT. Before filing or publishing, " +
@@ -86,25 +139,111 @@ export async function serve() {
86
139
  "you described instead of attaching is the most useful thing in the report, thrown away. " +
87
140
  "A guide already filed without one is not stuck: get_guide it, add the markdown line to " +
88
141
  "the body, and publish_guide the same id — publishing claims whatever the markdown names. " +
89
- "When the user asks to pass along, hand off, or " +
90
- "share what was just done, distill the session into a guide (guide_template shows the " +
91
- "shape) and call publish_guide, with `to` as team, team/@handle for one teammate, or " +
92
- "team/#group for the people who do a thing. " +
93
- "At the start of work, inbox shows guides teammates have handed to this user — open one " +
94
- "with start_guide — and activity " +
95
- "shows whether the guides they handed off have landed. When the user asks what they have " +
96
- "been working on, or wants a standup or a summary of a period, call log — but say that it " +
97
- "holds what they passed along and not everything they did. Gotchas are " +
98
- "the highest-value section: record what failed and why.\n" +
99
- "A FOLLOW-UP IS A GUIDE, NOT A NOTE. When you worked from a guide and departed from it — " +
100
- "changed or skipped a Step, adapted an ASSUMES for this stack, found the fix a failing " +
101
- "guide did not have, or hit a Gotcha it does not list — publish what you learned as its " +
102
- "own guide with publish_guide `parent` set to that guide's id; the original then lists it " +
103
- "as a follow-up. If it worked exactly as written, do not: answer with verify_guide, or " +
104
- "every guide collects copies that say nothing new.",
142
+ "\n" +
143
+ "When the user asks you to write a task, or to queue work for an agent, call plan_tasks — " +
144
+ "one step for one task — with every section filled from the conversation and the code, " +
145
+ "Acceptance as checks a person can run; it fills in the repo you are in. When the user " +
146
+ "asks you to plan or break down a larger goal, call plan_tasks with the steps in order, " +
147
+ "`after` naming the earlier steps each needs. Tasks land in Draft, not the queue: tell the " +
148
+ "user to read them and run `passalong ready <id>` or `passalong ready --all`.\n" +
149
+ "When the user asks to pass along, hand off, or share what was just done, distill the " +
150
+ "session into a guide (guide_template kind transfer shows the shape) and call " +
151
+ "publish_guide, with `to` as team, team/@handle for one teammate, or team/#group for the " +
152
+ "people who do a thing. " +
153
+ "At the start of work, inbox shows guides teammates have handed to this user — take one " +
154
+ "to start it — and activity shows whether the guides they handed off have landed. When " +
155
+ "the user asks what they have been working on, or wants a standup or a summary of a " +
156
+ "period, call log — but say that it holds what they passed along and not everything they " +
157
+ "did. Gotchas are the highest-value section: record what failed and why.\n" +
158
+ "A FOLLOW-UP IS MORE CONTEXT FOR A GUIDE, WRITTEN AS ITS OWN GUIDE. When a guide needs " +
159
+ "more context — a missing detail, a step that needed explaining, what changed since, what " +
160
+ "you found doing it — publish that context with publish_guide `parent` set to the guide's " +
161
+ "id. It is listed under the original, and anyone who opens the original, person or agent, " +
162
+ "gets it too. get_guide and take return a guide's follow-ups after it; read them " +
163
+ "before acting.",
105
164
  },
106
165
  );
107
166
 
167
+ // ---- the four verbs (docs/V2.md §11) --------------------------------------------------------
168
+ // One implementation each. The older tool names above call these too, so an agent following an
169
+ // old prompt gets exactly what one following a new prompt gets.
170
+
171
+ /** An answer as JSON, with the server's next move after it. */
172
+ const answer = (r, id) => {
173
+ const { next, say, ...rest } = r || {};
174
+ const note = nextNote({ next, say }, id);
175
+ return text(`${JSON.stringify(rest, null, 2)}${note ? `\n${note}` : ""}`);
176
+ };
177
+
178
+ async function doTake({ id, cwd, any }) {
179
+ const dir = cwd || process.cwd();
180
+ try {
181
+ // Without sync there is no claim to make; a guide by id can still be read and worked from.
182
+ if (!api.loggedIn() && id) {
183
+ const r = await passalong.pull(id, { cwd: dir });
184
+ return text(
185
+ `${leadFor(parse(r.markdown).meta)}${r.markdown}\n\n<!-- passalong: not logged in, so ` +
186
+ "nobody was told you took it; written to " +
187
+ `${r.path} -->`,
188
+ );
189
+ }
190
+ const r = await passalong.take(id, { cwd: dir, any: Boolean(any) });
191
+ if (!r.guide) return text(nextNote(r) || "Nothing is waiting for this agent here.");
192
+ const meta = parse(r.guide.markdown).meta;
193
+ const siblings = await related(meta);
194
+ const context = await passalong.followUps(meta);
195
+ const held =
196
+ `<!-- passalong: ${r.guide.id} is yours` +
197
+ `${r.guide.resumed ? " (you already held it — carry on from where it was left)" : ""}; ` +
198
+ `written to ${r.path}. -->`;
199
+ return text(
200
+ `${leadFor(meta)}${r.guide.markdown}${siblings}${context ? `\n\n${context}` : ""}` +
201
+ `\n\n${held}\n${nextNote(r, r.guide.id)}\n${passalong.followUpNote(meta)}`,
202
+ );
203
+ } catch (err) {
204
+ const h = err.body?.holder;
205
+ if (h)
206
+ return fail({
207
+ message:
208
+ `${err.message}. Somebody else is on it here${h.note ? ` (last said: "${h.note}")` : ""}. ` +
209
+ "Tell the user before doing this work too; get_guide reads it without taking it.",
210
+ });
211
+ return failWith(err);
212
+ }
213
+ }
214
+
215
+ async function doProgress({ id, note, cwd }) {
216
+ try {
217
+ return answer(await passalong.progress(id, note || "", { cwd: cwd || process.cwd() }), id);
218
+ } catch (err) {
219
+ return failWith(err);
220
+ }
221
+ }
222
+
223
+ async function doHandIn({ id, ok, note, markdown, report, pr, cwd }) {
224
+ try {
225
+ const r = await passalong.handIn(id, {
226
+ ok,
227
+ note: note || "",
228
+ markdown,
229
+ report,
230
+ pr: pr || "",
231
+ cwd: cwd || process.cwd(),
232
+ });
233
+ return answer(r, id);
234
+ } catch (err) {
235
+ return failWith(err);
236
+ }
237
+ }
238
+
239
+ async function doPass({ id, why, cwd }) {
240
+ try {
241
+ return answer(await passalong.pass(id, why, { cwd: cwd || process.cwd() }), id);
242
+ } catch (err) {
243
+ return failWith(err);
244
+ }
245
+ }
246
+
108
247
  server.registerTool(
109
248
  "search_guides",
110
249
  {
@@ -198,57 +337,40 @@ export async function serve() {
198
337
  server.registerTool(
199
338
  "ack_guide",
200
339
  {
201
- title: "Say whether you are taking it",
340
+ title: "Take it or pass it (same as take / pass)",
202
341
  description:
203
- "The first word back on a guide handed to you, before any work: take it, or pass it " +
204
- "back. Passing must say why — an unanswered handoff is indistinguishable from one nobody " +
205
- "has noticed, and the sender finds out in a week instead of a minute. Answer this when " +
206
- "you pick up an inbox; verify_guide comes later, once you have actually run it.",
342
+ "Kept for older prompts: taken=true is take with this id, taken=false is pass with `note` " +
343
+ "as the reason.",
207
344
  inputSchema: {
208
345
  id: z.string().describe("passalong id"),
209
346
  taken: z.boolean().describe("true if you are doing it; false hands it back"),
210
- note: z
347
+ note: z.string().default("").describe("why it is not yours (required when taken is false)"),
348
+ cwd: z
211
349
  .string()
212
- .default("")
213
- .describe("why it is not yours (required when taken is false); one line, max 280 chars"),
350
+ .optional()
351
+ .describe("the worktree you are working in; default is the server's cwd"),
214
352
  },
215
353
  },
216
- async ({ id, taken, note }) => {
217
- try {
218
- return json(await passalong.ack(id, taken, note || ""));
219
- } catch (err) {
220
- return fail(err);
221
- }
222
- },
354
+ async ({ id, taken, note, cwd }) =>
355
+ taken ? doTake({ id, cwd }) : doPass({ id, why: note || "", cwd }),
223
356
  );
224
357
 
225
358
  server.registerTool(
226
359
  "verify_guide",
227
360
  {
228
- title: "Report whether a guide works",
229
- description:
230
- "After following a guide's Verification section, report the result. This is the only way " +
231
- "the author learns their handoff did not land — `set_guide_status consumed` says it was " +
232
- "implemented, this says it actually works. A failing verdict must say what went wrong. " +
233
- "If doing it taught you something the guide did not say — a flag it needed, a step that " +
234
- "was wrong for this stack — publish that as its own guide with publish_guide `parent` set " +
235
- "to this id. The author sees it as a follow-up; a verdict note is one line and gets lost.",
361
+ title: "Report whether it works (same as hand_in)",
362
+ description: "Kept for older prompts: exactly hand_in with `ok` and `note`.",
236
363
  inputSchema: {
237
364
  id: z.string().describe("passalong id"),
238
365
  ok: z.boolean().describe("true if the Verification steps passed"),
239
- note: z
366
+ note: z.string().default("").describe("what went wrong (required when ok is false)"),
367
+ cwd: z
240
368
  .string()
241
- .default("")
242
- .describe("what went wrong (required when ok is false); one line, max 280 chars"),
369
+ .optional()
370
+ .describe("the worktree you are working in; default is the server's cwd"),
243
371
  },
244
372
  },
245
- async ({ id, ok, note }) => {
246
- try {
247
- return json(await passalong.verdict(id, ok, note || ""));
248
- } catch (err) {
249
- return fail(err);
250
- }
251
- },
373
+ async (args) => doHandIn(args),
252
374
  );
253
375
 
254
376
  server.registerTool(
@@ -278,53 +400,226 @@ export async function serve() {
278
400
  server.registerTool(
279
401
  "start_guide",
280
402
  {
281
- title: "Start work on a guide",
403
+ title: "Start work on a guide (same as take with an id)",
282
404
  description:
283
- "Take a guide handed to you AND fetch it, in one call — use this the moment you are going " +
284
- "to do the work. Same markdown as get_guide, plus the handoff is answered: until somebody " +
285
- "says they are on it, a guide nobody has noticed and a guide somebody is deep in look " +
286
- "identical to the sender, who finds out in a week instead of a minute. Taking your own " +
287
- "guide is a no-op, not an error, so this is always safe to call. If the work turns out " +
288
- "not to be yours after reading, hand it back with ack_guide taken=false and a reason.",
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.",
289
407
  inputSchema: {
290
408
  ref: z.string().describe("passalong id (e.g. k3mq2xa7) or share URL"),
291
409
  cwd: z
292
410
  .string()
293
411
  .optional()
294
- .describe("directory to write .passalong/<id>.md into; default is the server's cwd"),
412
+ .describe("the worktree you are working in; default is the server's cwd"),
295
413
  },
296
414
  },
297
- async ({ ref, cwd }) => {
415
+ async ({ ref, cwd }) => doTake({ id: ref, cwd }),
416
+ );
417
+
418
+ server.registerTool(
419
+ "plan_tasks",
420
+ {
421
+ title: "Plan a goal as tasks",
422
+ description:
423
+ "Write tasks for agents: one step for a single task, or a larger goal broken into steps " +
424
+ "an agent can each finish and a person can each check, written as drafts in order. The " +
425
+ "tool for any task, rather than publish_guide. Give each step a Goal and an Acceptance " +
426
+ "a person can run. " +
427
+ "`after` names the earlier steps (by position, from 0) a step needs; it waits for them " +
428
+ "to be approved, and steps that need nothing of each other can run side by side. Keep a " +
429
+ "step to one sitting of work in one repo. Nothing runs until the user makes them ready.",
430
+ inputSchema: {
431
+ steps: z
432
+ .array(
433
+ z.object({
434
+ title: z.string(),
435
+ goal: z.string().describe("what is true when this step is done"),
436
+ acceptance: z.string().describe("checks a person can run, one per line"),
437
+ context: z.string().optional(),
438
+ constraints: z.string().optional(),
439
+ out_of_scope: z.string().optional(),
440
+ target_context: z
441
+ .string()
442
+ .optional()
443
+ .describe("owner/repo it is for; default is the repo you are in, '' for none"),
444
+ after: z.array(z.number().int()).optional().describe("earlier steps this one needs"),
445
+ }),
446
+ )
447
+ .min(1)
448
+ .max(20),
449
+ cwd: z.string().optional().describe("the repo you are planning from"),
450
+ },
451
+ },
452
+ async ({ steps, cwd }) => {
298
453
  try {
299
- const r = await passalong.start(ref, { cwd: cwd || process.cwd() });
300
- const meta = parse(r.markdown).meta;
301
- const lead =
302
- meta.kind === "bug"
303
- ? "THIS IS A BUG REPORT, NOT WORK TO REPEAT. Do not follow Reproduce as instructions " +
304
- "— those steps produce the defect. Fix what Problem describes, then check " +
305
- "Verification and answer with verify_guide.\n\n"
306
- : "";
307
- // What actually happened to the ack, said plainly. An agent that reports "took it" when
308
- // nothing was sent is the failure this tool exists to prevent, one step further along.
309
- const took = r.took
310
- ? "handoff taken; the sender has been told"
311
- : r.own
312
- ? "your own guide — nothing to take, and nobody to tell"
313
- : r.ack_error
314
- ? `NOT taken (${r.ack_error}) — retry with ack_guide before reporting that you have it`
315
- : "not logged in, so no handoff was taken";
316
- const siblings = await related(meta);
317
- return text(
318
- `${lead}${r.markdown}${siblings}` +
319
- `\n\n<!-- passalong: ${r.from}; written to ${r.path}; ${took} -->` +
320
- `\n${passalong.followUpNote(meta)}`,
321
- );
454
+ const ids = await passalong.planTasks(steps, { cwd: cwd || process.cwd() });
455
+ return json({
456
+ drafts: ids.map((id, i) => ({ id, title: steps[i].title, after: steps[i].after || [] })),
457
+ next: "all in Draft: ask the user to read them, then run `passalong ready --all`",
458
+ });
322
459
  } catch (err) {
323
460
  return fail(err);
324
461
  }
325
462
  },
326
463
  );
327
464
 
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
+ server.registerTool(
524
+ "take",
525
+ {
526
+ title: "Take work",
527
+ description:
528
+ "Say you are doing it, and get it. With `id`, that guide — any kind: a task, a bug, a " +
529
+ "handoff. With no id, the next thing waiting for this worktree's agent. While you hold it " +
530
+ "no other agent can take it here, and its sender sees you are on it. One thing at a time: " +
531
+ "if you already hold something, finish or pass that first. If somebody else has it, the " +
532
+ "answer says who — tell the user rather than doing the work twice. Every answer ends with " +
533
+ "what to call next.",
534
+ inputSchema: {
535
+ id: z
536
+ .string()
537
+ .optional()
538
+ .describe("passalong id or share link; leave out for the next one"),
539
+ cwd: z
540
+ .string()
541
+ .optional()
542
+ .describe("the worktree you are working in; default is the server's cwd"),
543
+ any: z
544
+ .boolean()
545
+ .optional()
546
+ .describe("a task for another repo, or none — only when the user asks"),
547
+ },
548
+ },
549
+ async (args) => doTake(args),
550
+ );
551
+
552
+ server.registerTool(
553
+ "progress",
554
+ {
555
+ title: "Report progress",
556
+ description:
557
+ "Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
558
+ "without one marks it stalled. If the answer says you no longer hold it, stop.",
559
+ inputSchema: {
560
+ id: z.string().describe("the id of what you hold"),
561
+ note: z.string().optional().describe('one line, e.g. "migrating schema, 2 of 5 steps"'),
562
+ cwd: z
563
+ .string()
564
+ .optional()
565
+ .describe("the worktree you are working in; default is the server's cwd"),
566
+ },
567
+ },
568
+ async (args) => doProgress(args),
569
+ );
570
+
571
+ server.registerTool(
572
+ "hand_in",
573
+ {
574
+ title: "Hand it in",
575
+ 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.",
581
+ inputSchema: {
582
+ id: z.string().describe("passalong id"),
583
+ ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
584
+ note: z.string().optional().describe("one line; required when ok is false"),
585
+ markdown: z
586
+ .string()
587
+ .optional()
588
+ .describe("task: the write-up; start from guide_template kind transfer"),
589
+ report: z
590
+ .string()
591
+ .optional()
592
+ .describe("task, instead of markdown: id of a write-up already published"),
593
+ pr: z.string().optional().describe("task: PR or branch link, when there is one"),
594
+ cwd: z
595
+ .string()
596
+ .optional()
597
+ .describe("the worktree you are working in; default is the server's cwd"),
598
+ },
599
+ },
600
+ async (args) => doHandIn(args),
601
+ );
602
+
603
+ server.registerTool(
604
+ "pass",
605
+ {
606
+ title: "Pass it",
607
+ description:
608
+ "Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
609
+ "the next agent, and the reason goes to whoever is next — say why, or they start where " +
610
+ "silence left them.",
611
+ inputSchema: {
612
+ id: z.string().describe("passalong id"),
613
+ why: z.string().describe("one line: why it is not yours, or where you got stuck"),
614
+ cwd: z
615
+ .string()
616
+ .optional()
617
+ .describe("the worktree you are working in; default is the server's cwd"),
618
+ },
619
+ },
620
+ async (args) => doPass(args),
621
+ );
622
+
328
623
  server.registerTool(
329
624
  "get_guide",
330
625
  {
@@ -333,8 +628,8 @@ export async function serve() {
333
628
  "READ a transfer guide by passalong id or share link and return its full markdown. Also " +
334
629
  "writes it to .passalong/<id>.md in the working directory so it survives the session. " +
335
630
  "Pulling a teammate's guide tells them the transfer landed. Use this to look at a guide " +
336
- "you have not committed to. If you are about to DO the work, call start_guide instead: " +
337
- "it does this and takes the handoff in one call, which is the only way the sender learns " +
631
+ "you have not committed to. If you are about to DO the work, call take instead: " +
632
+ "it does this and takes it in one call, which is the only way the sender learns " +
338
633
  "somebody picked it up.",
339
634
  inputSchema: {
340
635
  ref: z.string().describe("passalong id (e.g. k3mq2xa7) or share URL"),
@@ -347,24 +642,19 @@ export async function serve() {
347
642
  async ({ ref, cwd }) => {
348
643
  try {
349
644
  const { markdown, path, from } = await passalong.pull(ref, { cwd: cwd || process.cwd() });
350
- // The heading text is what an agent keys on, and a bug's headings look enough like a
351
- // transfer guide's to be followed by one that never opened the frontmatter. So the kind
352
- // is stated in front of the document, in the imperative, every time.
353
645
  const meta = parse(markdown).meta;
354
- const lead =
355
- meta.kind === "bug"
356
- ? "THIS IS A BUG REPORT, NOT WORK TO REPEAT. Do not follow Reproduce as instructions " +
357
- "— those steps produce the defect. Fix what Problem describes, then check " +
358
- "Verification and answer with verify_guide.\n\n"
359
- : "";
646
+ const lead = leadFor(meta);
360
647
  const siblings = await related(meta);
648
+ // Follow-ups are more context for this guide, so they come with it — after the document,
649
+ // never inside it, so the guide an agent writes back out is still only the guide.
650
+ const context = await passalong.followUps(meta);
361
651
  // After the document, with the other trailing comments, not in front of it. An
362
652
  // instruction that arrives with the payload is what gets read — but anything before the
363
653
  // opening `---` stops the frontmatter being frontmatter, and this fires on every guide
364
654
  // anyone was handed rather than only on bugs. The bug lead stays where it is: it is a
365
655
  // warning against executing the document, so being read first is its whole job.
366
656
  return text(
367
- `${lead}${markdown}${siblings}` +
657
+ `${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
368
658
  `\n\n<!-- passalong: ${from}; written to ${path} -->` +
369
659
  `\n${passalong.handoffNudge(meta)}` +
370
660
  `\n${passalong.followUpNote(meta)}`,
@@ -409,7 +699,8 @@ export async function serve() {
409
699
  title: "Publish guide",
410
700
  description:
411
701
  "Publish a guide from markdown (frontmatter + sections) — a transfer guide by default, or " +
412
- "a single bug with `kind: bug`; use file_bugs for more than one. Missing id, created, " +
702
+ "a single bug with `kind: bug`; use file_bugs for more than one; or a task for work nobody " +
703
+ "has done yet with `kind: task`. Missing id, created, " +
413
704
  "author, and source_context are filled in. A screenshot belongs in the markdown: attach it " +
414
705
  "with attach_screenshot and put the line it returns in the body, because publishing claims " +
415
706
  "whatever the markdown names. " +
@@ -429,8 +720,8 @@ export async function serve() {
429
720
  .string()
430
721
  .optional()
431
722
  .describe(
432
- "id or share link of the guide this one came out of — set it when this is what you " +
433
- "learned doing someone else's guide, so theirs lists it as a follow-up",
723
+ "id or share link of the guide this one adds context to — set it and this is " +
724
+ "published as that guide's follow-up: listed under it, and read by whoever opens it",
434
725
  ),
435
726
  },
436
727
  },
@@ -451,6 +742,14 @@ export async function serve() {
451
742
  parent: guide.meta.parent || "",
452
743
  notified,
453
744
  path,
745
+ // A task does not go live on publish, and an agent that reports "queued it" has told
746
+ // the user something untrue. Said in the result so it is said to them.
747
+ ...(guide.meta.kind === "task" && guide.meta.status === "draft"
748
+ ? {
749
+ status: "draft",
750
+ next: `in Draft: ask the user to read it, then run \`passalong ready ${guide.meta.id}\``,
751
+ }
752
+ : {}),
454
753
  });
455
754
  } catch (err) {
456
755
  return fail(err);
@@ -548,16 +847,20 @@ export async function serve() {
548
847
  {
549
848
  title: "Guide template",
550
849
  description:
551
- "The empty guide skeleton with guidance comments for each section. `kind: bug` gives the " +
552
- "bug report skeleton instead, which has a Reproduce section and no Steps.",
850
+ "The empty guide skeleton with guidance comments for each section. By default a task " +
851
+ "brief: Goal, Context, Constraints, Acceptance, Out of scope. `kind: transfer` gives the " +
852
+ "skeleton for finished work to repeat; `kind: bug` a bug report, with Reproduce and no Steps.",
553
853
  inputSchema: {
554
854
  kind: z
555
- .enum(["transfer", "bug"])
855
+ .enum(["transfer", "bug", "task"])
556
856
  .optional()
557
- .describe("transfer (default) for finished work to repeat; bug for a defect to fix"),
857
+ .describe(
858
+ "task (default) for work nobody has done yet; transfer for finished work to repeat; " +
859
+ "bug for a defect to fix",
860
+ ),
558
861
  },
559
862
  },
560
- async ({ kind }) => text(template(kind === "bug" ? { kind: "bug" } : {})),
863
+ async ({ kind }) => text(template(kind ? { kind } : {})),
561
864
  );
562
865
 
563
866
  server.registerTool(
@@ -567,7 +870,7 @@ export async function serve() {
567
870
  description:
568
871
  "Archive a guide (`consumed`) or put it back on the board (`published`). Archiving is the " +
569
872
  "author's shelf: off the board, out of the free tier's count, reversible — it is not a " +
570
- "judgement that the work landed, which is what verify_guide reports. `promoted` was " +
873
+ "judgement that the work landed, which is what hand_in reports. `promoted` was " +
571
874
  "retired and the server refuses it.",
572
875
  inputSchema: { id: z.string(), status: z.enum(["published", "consumed"]) },
573
876
  },