passalong 0.3.2 → 0.5.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/bin/passalong +186 -8
- package/package.json +3 -2
- package/skill/SKILL.md +8 -7
- package/src/api.js +33 -2
- package/src/capture.js +41 -1
- package/src/guide.js +73 -5
- package/src/hooks.js +82 -0
- package/src/mcp.js +421 -118
- package/src/passalong.js +433 -26
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
|
|
64
|
-
"kinds, and `kind:` in the frontmatter says which. READ IT BEFORE ACTING —
|
|
65
|
-
"for
|
|
66
|
-
|
|
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
|
|
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
|
|
80
|
-
"
|
|
81
|
-
"
|
|
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
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
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: "
|
|
340
|
+
title: "Take it or pass it (same as take / pass)",
|
|
202
341
|
description:
|
|
203
|
-
"
|
|
204
|
-
"
|
|
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
|
-
.
|
|
213
|
-
.describe("
|
|
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
|
-
|
|
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
|
|
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
|
-
.
|
|
242
|
-
.describe("
|
|
369
|
+
.optional()
|
|
370
|
+
.describe("the worktree you are working in; default is the server's cwd"),
|
|
243
371
|
},
|
|
244
372
|
},
|
|
245
|
-
async (
|
|
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
|
-
"
|
|
284
|
-
"
|
|
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("
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
337
|
-
"it does this and takes
|
|
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
|
|
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
|
|
433
|
-
"
|
|
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.
|
|
552
|
-
"
|
|
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(
|
|
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
|
|
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
|
|
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
|
},
|