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