passalong 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/passalong +227 -18
- package/package.json +1 -1
- package/skill/SKILL.md +28 -1
- package/src/api.js +82 -5
- package/src/checks.js +127 -0
- package/src/guide.js +27 -5
- package/src/hooks.js +8 -5
- package/src/mcp.js +278 -135
- package/src/passalong.js +183 -14
- package/src/update.js +116 -0
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
275
|
+
// The note is what the agent acts on and it is prose, so it stays in the text half only. The
|
|
276
|
+
// structured half is the answer itself, which is what `outputSchema` below describes.
|
|
277
|
+
return {
|
|
278
|
+
...text(`${JSON.stringify(rest, null, 2)}${note ? `\n${note}` : ""}`),
|
|
279
|
+
structuredContent: rest,
|
|
280
|
+
};
|
|
176
281
|
};
|
|
177
282
|
|
|
178
283
|
async function doTake({ id, cwd, any }) {
|
|
@@ -192,12 +297,13 @@ export async function serve() {
|
|
|
192
297
|
const meta = parse(r.guide.markdown).meta;
|
|
193
298
|
const siblings = await related(meta);
|
|
194
299
|
const context = await passalong.followUps(meta);
|
|
300
|
+
const from = await passalong.parentGuide(meta);
|
|
195
301
|
const held =
|
|
196
302
|
`<!-- passalong: ${r.guide.id} is yours` +
|
|
197
303
|
`${r.guide.resumed ? " (you already held it — carry on from where it was left)" : ""}; ` +
|
|
198
304
|
`written to ${r.path}. -->`;
|
|
199
305
|
return text(
|
|
200
|
-
`${leadFor(meta)}${r.guide.markdown}${siblings}${context ? `\n\n${context}` : ""}` +
|
|
306
|
+
`${from}${leadFor(meta)}${r.guide.markdown}${siblings}${context ? `\n\n${context}` : ""}` +
|
|
201
307
|
`\n\n${held}\n${nextNote(r, r.guide.id)}\n${passalong.followUpNote(meta)}`,
|
|
202
308
|
);
|
|
203
309
|
} catch (err) {
|
|
@@ -220,15 +326,31 @@ export async function serve() {
|
|
|
220
326
|
}
|
|
221
327
|
}
|
|
222
328
|
|
|
223
|
-
async function doHandIn({ id, ok, note, markdown, report, pr, cwd }) {
|
|
329
|
+
async function doHandIn({ id, ok, note, evidence, checks, writeup, markdown, report, pr, cwd }) {
|
|
224
330
|
try {
|
|
331
|
+
// A check that names a command is run here, before anything is recorded, and its exit code
|
|
332
|
+
// decides it rather than the agent's account of it. A failure is refused with the command's
|
|
333
|
+
// own output: the claim is still held, nothing is written, and the agent has what it needs
|
|
334
|
+
// to fix. Checks with no `cmd` pass straight through to the prose rule.
|
|
335
|
+
const at = cwd || process.cwd();
|
|
336
|
+
const done = runChecks(checks || [], { cwd: at });
|
|
337
|
+
if (done.failed)
|
|
338
|
+
return {
|
|
339
|
+
content: [
|
|
340
|
+
{ type: "text", text: refusal(done.failed, { ran: done.ran, total: checks.length }) },
|
|
341
|
+
],
|
|
342
|
+
isError: true,
|
|
343
|
+
};
|
|
225
344
|
const r = await passalong.handIn(id, {
|
|
226
345
|
ok,
|
|
227
346
|
note: note || "",
|
|
347
|
+
evidence: evidence || "",
|
|
348
|
+
checks: done.checks,
|
|
349
|
+
writeup: writeup || "",
|
|
228
350
|
markdown,
|
|
229
351
|
report,
|
|
230
352
|
pr: pr || "",
|
|
231
|
-
cwd:
|
|
353
|
+
cwd: at,
|
|
232
354
|
});
|
|
233
355
|
return answer(r, id);
|
|
234
356
|
} catch (err) {
|
|
@@ -244,10 +366,35 @@ export async function serve() {
|
|
|
244
366
|
}
|
|
245
367
|
}
|
|
246
368
|
|
|
369
|
+
server.registerTool(
|
|
370
|
+
"assign",
|
|
371
|
+
{
|
|
372
|
+
title: "Give it to someone else",
|
|
373
|
+
annotations: ADDS,
|
|
374
|
+
description:
|
|
375
|
+
"Reassign a guide or task the user wrote, or one assigned to them, to someone else in its team, when the user asks: " +
|
|
376
|
+
'`to` is @handle for one person, #group for the people who do a thing, or "team" for ' +
|
|
377
|
+
"everyone. Whoever held it and is left out has it taken back and is told; a task then only " +
|
|
378
|
+
"goes to the new assignee's agents. Its author, or whoever it is assigned to, can do this.",
|
|
379
|
+
inputSchema: {
|
|
380
|
+
id: z.string().describe("passalong id"),
|
|
381
|
+
to: z.string().describe('"@handle", "#group", or "team"'),
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
async ({ id, to }) => {
|
|
385
|
+
try {
|
|
386
|
+
return json(await passalong.assign(id, to));
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return fail(err);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
|
|
247
393
|
server.registerTool(
|
|
248
394
|
"search_guides",
|
|
249
395
|
{
|
|
250
396
|
title: "Search guides",
|
|
397
|
+
annotations: READS,
|
|
251
398
|
description:
|
|
252
399
|
"Search transfer guides by words in the title, tags, stack, or body: the user's own " +
|
|
253
400
|
"(local and synced) plus every team they belong to. Empty query lists everything, newest first.",
|
|
@@ -270,6 +417,7 @@ export async function serve() {
|
|
|
270
417
|
"inbox",
|
|
271
418
|
{
|
|
272
419
|
title: "Inbox",
|
|
420
|
+
annotations: READS,
|
|
273
421
|
description:
|
|
274
422
|
"Guides handed to this user (or to their teams) that they have not pulled yet. Call it " +
|
|
275
423
|
"when starting work so handoffs are not missed. Needs sync (passalong login).",
|
|
@@ -288,6 +436,7 @@ export async function serve() {
|
|
|
288
436
|
"board",
|
|
289
437
|
{
|
|
290
438
|
title: "Board",
|
|
439
|
+
annotations: READS,
|
|
291
440
|
description:
|
|
292
441
|
"The state of this user's transfers as queues: waiting on you (handed to you, not " +
|
|
293
442
|
"pulled), not working (someone gave it a failing verdict — the most urgent), in flight " +
|
|
@@ -308,6 +457,7 @@ export async function serve() {
|
|
|
308
457
|
"log",
|
|
309
458
|
{
|
|
310
459
|
title: "What this user did",
|
|
460
|
+
annotations: READS,
|
|
311
461
|
description:
|
|
312
462
|
"This user's own acts on guides, newest first: what they published, what they took " +
|
|
313
463
|
"delivery of, and every verdict and ack they gave. Each item has a rendered `text` line " +
|
|
@@ -334,91 +484,62 @@ export async function serve() {
|
|
|
334
484
|
},
|
|
335
485
|
);
|
|
336
486
|
|
|
337
|
-
server.registerTool(
|
|
338
|
-
"ack_guide",
|
|
339
|
-
{
|
|
340
|
-
title: "Take it or pass it (same as take / pass)",
|
|
341
|
-
description:
|
|
342
|
-
"Kept for older prompts: taken=true is take with this id, taken=false is pass with `note` " +
|
|
343
|
-
"as the reason.",
|
|
344
|
-
inputSchema: {
|
|
345
|
-
id: z.string().describe("passalong id"),
|
|
346
|
-
taken: z.boolean().describe("true if you are doing it; false hands it back"),
|
|
347
|
-
note: z.string().default("").describe("why it is not yours (required when taken is false)"),
|
|
348
|
-
cwd: z
|
|
349
|
-
.string()
|
|
350
|
-
.optional()
|
|
351
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
352
|
-
},
|
|
353
|
-
},
|
|
354
|
-
async ({ id, taken, note, cwd }) =>
|
|
355
|
-
taken ? doTake({ id, cwd }) : doPass({ id, why: note || "", cwd }),
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
server.registerTool(
|
|
359
|
-
"verify_guide",
|
|
360
|
-
{
|
|
361
|
-
title: "Report whether it works (same as hand_in)",
|
|
362
|
-
description: "Kept for older prompts: exactly hand_in with `ok` and `note`.",
|
|
363
|
-
inputSchema: {
|
|
364
|
-
id: z.string().describe("passalong id"),
|
|
365
|
-
ok: z.boolean().describe("true if the Verification steps passed"),
|
|
366
|
-
note: z.string().default("").describe("what went wrong (required when ok is false)"),
|
|
367
|
-
cwd: z
|
|
368
|
-
.string()
|
|
369
|
-
.optional()
|
|
370
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
async (args) => doHandIn(args),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
487
|
server.registerTool(
|
|
377
488
|
"activity",
|
|
378
489
|
{
|
|
379
490
|
title: "Activity",
|
|
491
|
+
annotations: READS,
|
|
380
492
|
description:
|
|
381
|
-
"What has happened to this user's guides and handoffs: who pulled one, who
|
|
382
|
-
"
|
|
383
|
-
"
|
|
493
|
+
"What has happened to this user's guides and handoffs: who pulled one, who archived one, " +
|
|
494
|
+
"who was handed what, who joined a team. Each item has a ready-made `text` line. This only " +
|
|
495
|
+
"reads; clear_activity is what marks the feed seen.",
|
|
384
496
|
inputSchema: {
|
|
385
497
|
all: z.boolean().default(false).describe("include what the user has already seen"),
|
|
386
|
-
mark_read: z.boolean().default(false).describe("clear the unread feed after reading"),
|
|
387
498
|
},
|
|
388
499
|
},
|
|
389
|
-
async ({ all
|
|
500
|
+
async ({ all }) => {
|
|
390
501
|
try {
|
|
391
|
-
|
|
392
|
-
if (mark_read && res.unread) await passalong.seen();
|
|
393
|
-
return json(res);
|
|
502
|
+
return json(await passalong.activity({ all }));
|
|
394
503
|
} catch (err) {
|
|
395
504
|
return fail(err);
|
|
396
505
|
}
|
|
397
506
|
},
|
|
398
507
|
);
|
|
399
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Split out of `activity`, which took `mark_read` and so read on one call and wrote on the next.
|
|
511
|
+
*
|
|
512
|
+
* An annotation cannot say "sometimes": `readOnlyHint` was false for a tool that almost always
|
|
513
|
+
* only reads, which is a client being told the cautious lie on every call rather than the truth
|
|
514
|
+
* on the one that matters. Two tools, each true about itself.
|
|
515
|
+
*/
|
|
400
516
|
server.registerTool(
|
|
401
|
-
"
|
|
517
|
+
"clear_activity",
|
|
402
518
|
{
|
|
403
|
-
title: "
|
|
519
|
+
title: "Mark activity seen",
|
|
520
|
+
annotations: ADDS,
|
|
404
521
|
description:
|
|
405
|
-
"
|
|
406
|
-
"
|
|
407
|
-
inputSchema: {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
522
|
+
"Clear the user's unread feed, after they have been shown what is in it. Read it with " +
|
|
523
|
+
"activity first — clearing what nobody was told about loses it.",
|
|
524
|
+
inputSchema: {},
|
|
525
|
+
},
|
|
526
|
+
async () => {
|
|
527
|
+
try {
|
|
528
|
+
const res = await passalong.activity({ all: false });
|
|
529
|
+
if (!res.unread) return json({ cleared: 0, unread: 0 });
|
|
530
|
+
await passalong.seen();
|
|
531
|
+
return json({ cleared: res.unread, unread: 0 });
|
|
532
|
+
} catch (err) {
|
|
533
|
+
return fail(err);
|
|
534
|
+
}
|
|
414
535
|
},
|
|
415
|
-
async ({ ref, cwd }) => doTake({ id: ref, cwd }),
|
|
416
536
|
);
|
|
417
537
|
|
|
418
538
|
server.registerTool(
|
|
419
539
|
"plan_tasks",
|
|
420
540
|
{
|
|
421
541
|
title: "Plan a goal as tasks",
|
|
542
|
+
annotations: ADDS,
|
|
422
543
|
description:
|
|
423
544
|
"Write tasks for agents: one step for a single task, or a larger goal broken into steps " +
|
|
424
545
|
"an agent can each finish and a person can each check, written as drafts in order. The " +
|
|
@@ -462,68 +583,11 @@ export async function serve() {
|
|
|
462
583
|
},
|
|
463
584
|
);
|
|
464
585
|
|
|
465
|
-
server.registerTool(
|
|
466
|
-
"next_task",
|
|
467
|
-
{
|
|
468
|
-
title: "Take the next task (same as take with no id)",
|
|
469
|
-
description:
|
|
470
|
-
"Kept for older prompts: exactly take with no id. Takes the next guide waiting for this " +
|
|
471
|
-
"worktree's agent, or gives back the one it already holds.",
|
|
472
|
-
inputSchema: {
|
|
473
|
-
cwd: z
|
|
474
|
-
.string()
|
|
475
|
-
.optional()
|
|
476
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
477
|
-
any: z
|
|
478
|
-
.boolean()
|
|
479
|
-
.optional()
|
|
480
|
-
.describe("any repo, or none — only when the user asks for that"),
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
async ({ cwd, any }) => doTake({ cwd, any }),
|
|
484
|
-
);
|
|
485
|
-
|
|
486
|
-
server.registerTool(
|
|
487
|
-
"task_progress",
|
|
488
|
-
{
|
|
489
|
-
title: "Report progress (same as progress)",
|
|
490
|
-
description: "Kept for older prompts: exactly progress.",
|
|
491
|
-
inputSchema: {
|
|
492
|
-
id: z.string().describe("the id of what you hold"),
|
|
493
|
-
note: z.string().optional().describe("one line"),
|
|
494
|
-
cwd: z
|
|
495
|
-
.string()
|
|
496
|
-
.optional()
|
|
497
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
498
|
-
},
|
|
499
|
-
},
|
|
500
|
-
async (args) => doProgress(args),
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
server.registerTool(
|
|
504
|
-
"finish_task",
|
|
505
|
-
{
|
|
506
|
-
title: "Finish a task (same as hand_in)",
|
|
507
|
-
description: "Kept for older prompts: exactly hand_in, for a task.",
|
|
508
|
-
inputSchema: {
|
|
509
|
-
id: z.string().describe("the task's id"),
|
|
510
|
-
markdown: z.string().optional().describe("the transfer guide about this work"),
|
|
511
|
-
report: z.string().optional().describe("instead of markdown: id of one already published"),
|
|
512
|
-
pr: z.string().optional().describe("PR or branch link, when there is one"),
|
|
513
|
-
note: z.string().optional().describe("one line for the hub"),
|
|
514
|
-
cwd: z
|
|
515
|
-
.string()
|
|
516
|
-
.optional()
|
|
517
|
-
.describe("the worktree you are working in; default is the server's cwd"),
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
async (args) => doHandIn(args),
|
|
521
|
-
);
|
|
522
|
-
|
|
523
586
|
server.registerTool(
|
|
524
587
|
"take",
|
|
525
588
|
{
|
|
526
589
|
title: "Take work",
|
|
590
|
+
annotations: ADDS,
|
|
527
591
|
description:
|
|
528
592
|
"Say you are doing it, and get it. With `id`, that guide — any kind: a task, a bug, a " +
|
|
529
593
|
"handoff. With no id, the next thing waiting for this worktree's agent. While you hold it " +
|
|
@@ -553,6 +617,8 @@ export async function serve() {
|
|
|
553
617
|
"progress",
|
|
554
618
|
{
|
|
555
619
|
title: "Report progress",
|
|
620
|
+
outputSchema: progressOut,
|
|
621
|
+
annotations: ADDS,
|
|
556
622
|
description:
|
|
557
623
|
"Say you are still on what you hold, with a one-line note the hub shows. 30 minutes " +
|
|
558
624
|
"without one marks it stalled. If the answer says you no longer hold it, stop.",
|
|
@@ -572,16 +638,74 @@ export async function serve() {
|
|
|
572
638
|
"hand_in",
|
|
573
639
|
{
|
|
574
640
|
title: "Hand it in",
|
|
641
|
+
outputSchema: handInOut,
|
|
642
|
+
annotations: ADDS,
|
|
575
643
|
description:
|
|
576
|
-
"Done here
|
|
577
|
-
"
|
|
578
|
-
"
|
|
579
|
-
"
|
|
580
|
-
"
|
|
644
|
+
"Done here, with proof, sorted against the line it answers. Send `checks`: one entry per " +
|
|
645
|
+
"line the guide asks for — `## Acceptance` on a task, `## Verification` on a handoff or a " +
|
|
646
|
+
"bug — each with that line and the evidence for it. That is what its author reads, line " +
|
|
647
|
+
"against line, instead of hunting through a wall of output for the part that answers each " +
|
|
648
|
+
"one, AND IT IS HOW YOU KNOW YOU ARE DONE: every line has one. Give a check `cmd` when a " +
|
|
649
|
+
"command proves it: the command is run here before the hand-in lands, a non-zero exit " +
|
|
650
|
+
"refuses it, and what it printed is recorded instead of your account of it. Use `ran` for " +
|
|
651
|
+
"a check no command can settle. `evidence` as one block is the older shape and still " +
|
|
652
|
+
"works. A task also takes `markdown`, a transfer guide about what you did and decided — " +
|
|
653
|
+
"this publishes it and attaches it. A handoff or a bug takes `ok`, whether its " +
|
|
654
|
+
"Verification held here, and `note` saying what went wrong when it did not. DO NOT " +
|
|
655
|
+
"publish a guide to carry your evidence: it belongs on this call, and a second document " +
|
|
656
|
+
'titled "Hand-in evidence" is the thing `checks` exists to replace. If it worked but you ' +
|
|
657
|
+
"had to adapt something, that goes in `writeup` and the next reader of the guide is shown " +
|
|
658
|
+
"it — that is its home. A follow-up guide is for work somebody else should now do, not " +
|
|
659
|
+
"for answering.",
|
|
581
660
|
inputSchema: {
|
|
582
661
|
id: z.string().describe("passalong id"),
|
|
662
|
+
evidence: z
|
|
663
|
+
.string()
|
|
664
|
+
.optional()
|
|
665
|
+
.describe(
|
|
666
|
+
"what you ran and what came back: the command and the lines that decided it, a test " +
|
|
667
|
+
'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
|
|
668
|
+
),
|
|
669
|
+
checks: z
|
|
670
|
+
.array(
|
|
671
|
+
z.object({
|
|
672
|
+
check: z
|
|
673
|
+
.string()
|
|
674
|
+
.describe(
|
|
675
|
+
"the line this answers, in the guide's own words: an Acceptance line on a task, " +
|
|
676
|
+
"a Verification line on a handoff or a bug",
|
|
677
|
+
),
|
|
678
|
+
ran: z
|
|
679
|
+
.string()
|
|
680
|
+
.optional()
|
|
681
|
+
.describe("what you ran for it, and what came back; not needed when `cmd` is set"),
|
|
682
|
+
cmd: z
|
|
683
|
+
.string()
|
|
684
|
+
.optional()
|
|
685
|
+
.describe(
|
|
686
|
+
"the shell command that proves this line, run here before the hand-in lands — " +
|
|
687
|
+
"its exit code decides the check and its output is recorded as the evidence. " +
|
|
688
|
+
"Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
|
|
689
|
+
'"the badge reads 3", and write `ran` instead',
|
|
690
|
+
),
|
|
691
|
+
}),
|
|
692
|
+
)
|
|
693
|
+
.optional()
|
|
694
|
+
.describe(
|
|
695
|
+
"one entry per line the guide asks for — Acceptance on a task, Verification on a " +
|
|
696
|
+
"handoff or a bug — in the order you worked them",
|
|
697
|
+
),
|
|
583
698
|
ok: z.boolean().optional().describe("handoff or bug: did its Verification hold"),
|
|
584
699
|
note: z.string().optional().describe("one line; required when ok is false"),
|
|
700
|
+
writeup: z
|
|
701
|
+
.string()
|
|
702
|
+
.optional()
|
|
703
|
+
.describe(
|
|
704
|
+
"handoff or bug: what you had to adapt to make it work here — a version, a name, a " +
|
|
705
|
+
"step that needed something the guide does not mention. Prose, and optional: leave " +
|
|
706
|
+
"it out when it worked as written. The next person to open the guide is shown it, " +
|
|
707
|
+
"which is why it does not need to be a guide of its own",
|
|
708
|
+
),
|
|
585
709
|
markdown: z
|
|
586
710
|
.string()
|
|
587
711
|
.optional()
|
|
@@ -604,6 +728,8 @@ export async function serve() {
|
|
|
604
728
|
"pass",
|
|
605
729
|
{
|
|
606
730
|
title: "Pass it",
|
|
731
|
+
outputSchema: passOut,
|
|
732
|
+
annotations: ADDS,
|
|
607
733
|
description:
|
|
608
734
|
"Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
|
|
609
735
|
"the next agent, and the reason goes to whoever is next — say why, or they start where " +
|
|
@@ -624,6 +750,7 @@ export async function serve() {
|
|
|
624
750
|
"get_guide",
|
|
625
751
|
{
|
|
626
752
|
title: "Get guide",
|
|
753
|
+
annotations: READS,
|
|
627
754
|
description:
|
|
628
755
|
"READ a transfer guide by passalong id or share link and return its full markdown. Also " +
|
|
629
756
|
"writes it to .passalong/<id>.md in the working directory so it survives the session. " +
|
|
@@ -648,13 +775,16 @@ export async function serve() {
|
|
|
648
775
|
// Follow-ups are more context for this guide, so they come with it — after the document,
|
|
649
776
|
// never inside it, so the guide an agent writes back out is still only the guide.
|
|
650
777
|
const context = await passalong.followUps(meta);
|
|
778
|
+
// The guide this one came out of, in front of the document: a follow-up read on its own
|
|
779
|
+
// looks like a small piece of work and is a note on a bigger one.
|
|
780
|
+
const follows = await passalong.parentGuide(meta);
|
|
651
781
|
// After the document, with the other trailing comments, not in front of it. An
|
|
652
782
|
// instruction that arrives with the payload is what gets read — but anything before the
|
|
653
783
|
// opening `---` stops the frontmatter being frontmatter, and this fires on every guide
|
|
654
784
|
// anyone was handed rather than only on bugs. The bug lead stays where it is: it is a
|
|
655
785
|
// warning against executing the document, so being read first is its whole job.
|
|
656
786
|
return text(
|
|
657
|
-
`${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
|
|
787
|
+
`${follows}${lead}${markdown}${siblings}${context ? `\n\n${context}` : ""}` +
|
|
658
788
|
`\n\n<!-- passalong: ${from}; written to ${path} -->` +
|
|
659
789
|
`\n${passalong.handoffNudge(meta)}` +
|
|
660
790
|
`\n${passalong.followUpNote(meta)}`,
|
|
@@ -669,6 +799,7 @@ export async function serve() {
|
|
|
669
799
|
"attach_screenshot",
|
|
670
800
|
{
|
|
671
801
|
title: "Attach a screenshot",
|
|
802
|
+
annotations: { ...ADDS, openWorldHint: true },
|
|
672
803
|
description:
|
|
673
804
|
"Upload an image from this machine as evidence, and get back the markdown line that " +
|
|
674
805
|
"points at it. Put that line in the guide body — a guide travels as markdown to whoever " +
|
|
@@ -697,8 +828,12 @@ export async function serve() {
|
|
|
697
828
|
"publish_guide",
|
|
698
829
|
{
|
|
699
830
|
title: "Publish guide",
|
|
831
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
700
832
|
description:
|
|
701
|
-
"Publish a guide from markdown (frontmatter + sections)
|
|
833
|
+
"Publish a guide from markdown (frontmatter + sections). Check what you hold first (take " +
|
|
834
|
+
"with no id): if this session answers something you hold, hand_in that instead, and if the " +
|
|
835
|
+
"work never left a branch the team can see, publish what is still open as a bug or a task " +
|
|
836
|
+
"rather than a write-up of the fix. A transfer guide by default, or " +
|
|
702
837
|
"a single bug with `kind: bug`; use file_bugs for more than one; or a task for work nobody " +
|
|
703
838
|
"has done yet with `kind: task`. Missing id, created, " +
|
|
704
839
|
"author, and source_context are filled in. A screenshot belongs in the markdown: attach it " +
|
|
@@ -761,6 +896,7 @@ export async function serve() {
|
|
|
761
896
|
"file_bugs",
|
|
762
897
|
{
|
|
763
898
|
title: "File bugs",
|
|
899
|
+
annotations: ADDS,
|
|
764
900
|
description:
|
|
765
901
|
"File one or more bugs you found but are not fixing, as a set. Opens a report and " +
|
|
766
902
|
"publishes each issue as its own guide — its own id, share link, and verdict — so a " +
|
|
@@ -846,6 +982,7 @@ export async function serve() {
|
|
|
846
982
|
"guide_template",
|
|
847
983
|
{
|
|
848
984
|
title: "Guide template",
|
|
985
|
+
annotations: READS,
|
|
849
986
|
description:
|
|
850
987
|
"The empty guide skeleton with guidance comments for each section. By default a task " +
|
|
851
988
|
"brief: Goal, Context, Constraints, Acceptance, Out of scope. `kind: transfer` gives the " +
|
|
@@ -867,6 +1004,7 @@ export async function serve() {
|
|
|
867
1004
|
"set_guide_status",
|
|
868
1005
|
{
|
|
869
1006
|
title: "Set guide status",
|
|
1007
|
+
annotations: ADDS,
|
|
870
1008
|
description:
|
|
871
1009
|
"Archive a guide (`consumed`) or put it back on the board (`published`). Archiving is the " +
|
|
872
1010
|
"author's shelf: off the board, out of the free tier's count, reversible — it is not a " +
|
|
@@ -884,5 +1022,10 @@ export async function serve() {
|
|
|
884
1022
|
},
|
|
885
1023
|
);
|
|
886
1024
|
|
|
887
|
-
|
|
1025
|
+
return server;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** The stdio server: `passalong mcp`, and what `passalong setup` registers with a client. */
|
|
1029
|
+
export async function serve() {
|
|
1030
|
+
await buildServer().connect(new StdioServerTransport());
|
|
888
1031
|
}
|