passalong 1.0.0 → 1.2.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 CHANGED
@@ -51,7 +51,9 @@ passalong archive <id> off the board and out of the free tier's count
51
51
  passalong rm <id> delete it
52
52
  passalong attach <file> upload a picture or file and print the markdown that points at it
53
53
  passalong work [--once] [--agent CMD] run an agent on each piece of ready work here, in turn
54
- passalong work --watch the runner: keep taking work a person marked safe to run alone
54
+ passalong work --watch the runner, in a repo or a folder of repos: work a person marked safe
55
+ to run alone, up to 3 at once and 10 a day, each in its own worktree,
56
+ finished as a pull request; each agent's steps show in the hub
55
57
  passalong now what this worktree holds and what is waiting
56
58
  passalong hub open the hub in the browser
57
59
  passalong export [dir] every piece of work as plain markdown
package/bin/passalong CHANGED
@@ -22,7 +22,8 @@
22
22
  // passalong rm <id> delete it
23
23
  // passalong attach <file> upload a picture or a file and print the markdown that points at it
24
24
  // passalong work [--once] [--agent CMD] run an agent on each piece of ready work here, one after another
25
- // passalong work --watch [--every SECS] the runner: keep taking work marked safe to run alone
25
+ // passalong work --watch [--every SECS] the runner, in a repo or a folder of repos: keep taking work marked safe to run alone,
26
+ // up to 3 at once and 10 a day, each in its own worktree and branch, finished as a pull request
26
27
  // passalong hub open the hub in the browser
27
28
  // passalong export [dir] dump every piece of work as plain markdown (default ./passalong-export)
28
29
  // passalong admin create <email> make an account that runs Passalong (owner only)
@@ -250,6 +251,59 @@ async function statusLine() {
250
251
  }
251
252
  }
252
253
 
254
+ /**
255
+ * `passalong work --watch`: the runner (src/runner.js). Each piece of work marked safe to run alone
256
+ * gets its own worktree, branch and Claude Code; what it does arrives as a pull request.
257
+ */
258
+ async function runner(f) {
259
+ const every = Number(f.every) || 60;
260
+ let idle = false;
261
+ /** "a, b and 4 more": the repos a runner serves, short enough for one line. */
262
+ const some = (repos) =>
263
+ repos.length <= 3 ? repos.join(", ") : `${repos.slice(0, 3).join(", ")} and ${repos.length - 3} more`;
264
+ await passalong
265
+ .watch({
266
+ agent: typeof f.agent === "string" ? f.agent : undefined,
267
+ every,
268
+ cli: fileURLToPath(import.meta.url),
269
+ signals: true,
270
+ // Said only once the folder checked out: a runner that cannot run says why, not what it will do.
271
+ onStart: ({ repos, skipped }) => {
272
+ console.error(
273
+ `Runner for ${repos.length === 1 ? repos[0] : `${repos.length} repos: ${some(repos)}`}.\n` +
274
+ "It takes work marked safe to run alone, up to 3 at once and 10 a day, and finishes each " +
275
+ "as a pull request. You can watch each agent here and on the work's page in the hub. " +
276
+ "Stop it with Ctrl-C: agents running then stop too.",
277
+ );
278
+ if (skipped.length)
279
+ console.error(`Skipped ${skipped.length} checkout${skipped.length === 1 ? "" : "s"} with no origin remote: ${some(skipped)}`);
280
+ },
281
+ onTask: (t, { dir, branch, repo }) => {
282
+ idle = false;
283
+ console.error(`\n→ ${t.id} ${t.title}\n ${repo}, on ${branch} in ${dir}`);
284
+ },
285
+ onStep: (t, text) => console.error(` ${t.id} · ${text}`),
286
+ onIdle: (repos) => {
287
+ if (!idle)
288
+ console.error(
289
+ `waiting for work marked safe to run alone in ${some(repos)}. ` +
290
+ "Work with no repo needs one picked on its page in the hub.",
291
+ );
292
+ idle = true;
293
+ },
294
+ onCapped: (n) =>
295
+ console.error(`${n} runs today: that's the day's limit. Picking up again tomorrow.`),
296
+ onFinished: (t, { branch, log }) => console.error(`✓ ${t.id} finished on ${branch} (log: ${log})`),
297
+ onWaiting: (t, { question }) =>
298
+ console.error(`? ${t.id} is waiting on you: "${question.split("\n")[0]}" (answer it in the hub)`),
299
+ onResumed: (t) => console.error(`\n→ ${t.id} ${t.title} (answered: carrying on)`),
300
+ onPassed: (t, { dir, log }) =>
301
+ console.error(`✗ ${t.id} went back to its author, unfinished. Its worktree: ${dir} (log: ${log})`),
302
+ onTrouble: (err, t) => console.error(`! ${t ? `${t.id}: ` : ""}${err?.message || err}`),
303
+ })
304
+ .catch((e) => die(e.message));
305
+ }
306
+
253
307
  const commands = {
254
308
  /**
255
309
  * Write work down: a sentence becomes its title, summary and text, or a file is sent as it is. At
@@ -444,30 +498,14 @@ const commands = {
444
498
  * files without asking — which is yours to decide, not this command's.
445
499
  */
446
500
  async work(f) {
447
- // The runner: only work a person marked safe to run alone, waiting for more when there is none.
448
- const watch = Boolean(f.watch);
449
- let idle = false;
450
- if (watch)
451
- console.error("Runner: taking only work marked safe to run alone. Stop it with Ctrl-C.");
501
+ if (f.watch) return runner(f);
452
502
  const run = await passalong
453
503
  .work({
454
504
  agent: typeof f.agent === "string" ? f.agent : undefined,
455
505
  once: Boolean(f.once),
456
506
  any: Boolean(f.any),
457
- watch,
458
- every: Number(f.every) || 60,
459
- onTask: (t) => {
460
- idle = false;
461
- console.error(`\n→ ${t.id} ${t.title}${t.resumed ? " (resuming)" : ""}`);
462
- },
463
- onIdle: () => {
464
- if (!idle) console.error("waiting for work marked safe to run alone…");
465
- idle = true;
466
- },
467
- onUnfinished: (t, state) =>
468
- console.error(
469
- `✗ ${t.id} is still ${state || "open"}: left held here; the sweep puts it back if it goes quiet`,
470
- ),
507
+ onTask: (t) =>
508
+ console.error(`\n→ ${t.id} ${t.title}${t.resumed ? " (resuming)" : ""}`),
471
509
  })
472
510
  .catch((e) => die(e.message));
473
511
  for (const id of run.finished) console.log(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passalong",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Work, projects, documents and files for a team and its agents: take work, hand it in with evidence, and keep what the team knows in its projects.",
5
5
  "mcpName": "io.github.codellyson/passalong",
6
6
  "keywords": [
package/src/api.js CHANGED
@@ -168,7 +168,7 @@ async function call(
168
168
  if (auth) {
169
169
  if (!as && needsChoice()) throw new ApiError(409, choiceMessage());
170
170
  const t = as ? as.token : token();
171
- if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
171
+ if (!t) throw new ApiError(401, "not logged in: run `passalong login` to enable sync");
172
172
  headers.authorization = `Bearer ${t}`;
173
173
  }
174
174
  if (body !== undefined) {
@@ -357,6 +357,11 @@ export const setStatus = (id, status) =>
357
357
  export const remove = (id) => call(`/v1/work/${id}`, { method: "DELETE" });
358
358
  export const tasks = () => call("/v1/tasks");
359
359
  export const nextTask = (body) => call("/v1/tasks/next", { method: "POST", body });
360
+ /** The runner saying how it is, for the hub: what it is on, today's count, or that it stopped. */
361
+ export const runnerStatus = (body) => call("/v1/runner", { method: "PUT", body });
362
+ /** What a runner's agent did since the last report, a line each, for the hub to show. */
363
+ export const workSteps = (id, steps) =>
364
+ call(`/v1/work/${encodeURIComponent(id)}/steps`, { method: "POST", body: { steps } });
360
365
  export const reopen = (id, why) =>
361
366
  call(`/v1/work/${encodeURIComponent(id)}/reopen`, { method: "POST", body: { why } });
362
367
  /** Take work back from whoever holds it. */
@@ -384,7 +389,7 @@ export const pass = (id, body) =>
384
389
  export async function uploadFile(bytes, type, name = "") {
385
390
  if (needsChoice()) throw new ApiError(409, choiceMessage());
386
391
  const t = token();
387
- if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
392
+ if (!t) throw new ApiError(401, "not logged in: run `passalong login` to enable sync");
388
393
  const headers = {
389
394
  ...CLIENT,
390
395
  authorization: `Bearer ${t}`,
@@ -414,7 +419,7 @@ export async function uploadFile(bytes, type, name = "") {
414
419
  */
415
420
  export async function uploadShot(bytes, type, name = "") {
416
421
  const t = token();
417
- if (!t) throw new ApiError(401, "not logged in — run `passalong login` to enable sync");
422
+ if (!t) throw new ApiError(401, "not logged in: run `passalong login` to enable sync");
418
423
  const headers = { ...CLIENT, authorization: `Bearer ${t}`, "content-type": type };
419
424
  // A header is latin-1: a filename with an accent in it throws on the way out rather than at the
420
425
  // server, and the label is not worth failing an upload over.
package/src/hooks.js CHANGED
@@ -42,8 +42,8 @@ export function nowText({ held = [], ready = 0 }) {
42
42
  lines.push("It is marked stalled: nobody has heard from its agent in 30 minutes.");
43
43
  lines.push(
44
44
  `If it was yours before this session restarted, call take ${h.id} to carry on, then ` +
45
- "progress at each milestone. When it is done, hand_in with evidence — what you ran and " +
46
- "what came back — so keep it as you go; if it is not yours or you are stuck, pass with " +
45
+ "progress at each milestone. When it is done, hand_in with evidence (what you ran and " +
46
+ "what came back), so keep it as you go; if it is not yours or you are stuck, pass with " +
47
47
  "the reason. If another session here is on it, leave it to that session.",
48
48
  );
49
49
  return lines.join("\n");
@@ -52,8 +52,8 @@ export function nowText({ held = [], ready = 0 }) {
52
52
  const lines = [`Passalong: This worktree holds ${held.length} things:`];
53
53
  for (const h of held)
54
54
  lines.push(
55
- `- ${h.id}: ${h.title || h.id}${h.state === "stalled" ? " — stalled" : ""}` +
56
- `${h.replies ? ` — ${plural(h.replies, "message", "messages")} waiting` : ""}`,
55
+ `- ${h.id}: ${h.title || h.id}${h.state === "stalled" ? " (stalled)" : ""}` +
56
+ `${h.replies ? `, ${plural(h.replies, "message", "messages")} waiting` : ""}`,
57
57
  );
58
58
  lines.push(
59
59
  "Several sessions may be working here. Carry on only with what was yours before this " +
@@ -84,7 +84,7 @@ export function stopDecision({ held = [], input = {} }) {
84
84
  `This worktree still holds ${ids}. For anything there you worked on in this session, do ` +
85
85
  "one of before ending: hand_in it with your evidence if it is done; pass it with the reason " +
86
86
  "if it is not yours or you are stuck; or, if the user is stopping mid-way, progress it with " +
87
- "a note saying what is left. Leave what other sessions are on — then end.",
87
+ "a note saying what is left. Leave what other sessions are on, then end.",
88
88
  };
89
89
  }
90
90
 
package/src/mcp.js CHANGED
@@ -32,10 +32,10 @@ const text = (s) => ({ content: [{ type: "text", text: s }] });
32
32
  */
33
33
  const WHERE_THE_LINE_GOES =
34
34
  "WHERE THE LINE GOES DEPENDS ON WHAT THE FILE IS EVIDENCE OF. Proof that work you are handing in " +
35
- "holds — a screen that renders right, a total that matches — goes in that check's `ran` on " +
35
+ "holds (a screen that renders right, a total that matches) goes in that check's `ran` on " +
36
36
  "hand_in, and nothing new is written: a check answered with a picture is shown, which is what " +
37
- "the hand-in asks for. A picture that is part of the work itself — the screenshot of a bug, a " +
38
- "diagram it explains — goes in its text (create_work, or update_work), because work travels as " +
37
+ "the hand-in asks for. A picture that is part of the work itself (the screenshot of a bug, a " +
38
+ "diagram it explains) goes in its text (create_work, or update_work), because work travels as " +
39
39
  "its text and a file beside it does not travel at all. DO NOT WRITE NEW WORK TO CARRY " +
40
40
  "SCREENSHOTS: six images and a caption each is a hand-in. With `project`, the file is kept in " +
41
41
  "that project instead. Pictures (png, jpg, webp, gif) are drawn where they are named; anything " +
@@ -52,13 +52,13 @@ const SHOOT_THE_REAL_THING =
52
52
  "h it. A scratch page, a story, a harness or a component rendered on its own is a picture of " +
53
53
  "your scaffolding, and it can look right while the thing the work is about does not. If the " +
54
54
  "only way you could get a picture was to build something, that is not evidence the real scree" +
55
- "n works — say what you built and why in `writeup`, and leave the check to the commands that " +
55
+ "n works: say what you built and why in `writeup`, and leave the check to the commands that " +
56
56
  "do hold. NEVER DELETE WHAT YOUR EVIDENCE POINTS AT: a shot of a page you removed afterwards " +
57
57
  "is one nobody can take again, including you.\n\n";
58
58
 
59
59
  const KEEP_EVIDENCE =
60
60
  "KEEP YOUR EVIDENCE AS YOU GO. hand_in needs it: the commands you ran and what came back, the " +
61
- "test summary, the link to the change. Copy each one when it happens — at the end you will be " +
61
+ "test summary, the link to the change. Copy each one when it happens: at the end you will be " +
62
62
  "writing from memory, which is the thing evidence is here to replace.\n\n";
63
63
 
64
64
  /**
@@ -68,13 +68,24 @@ const KEEP_EVIDENCE =
68
68
  */
69
69
  const SEARCH_WIDE =
70
70
  "SEARCH THE WHOLE TREE BEFORE YOU CONCLUDE. Listing a directory, or reading the one file you " +
71
- "expected, tells you what is in that directory — not whether the thing exists, and not whether " +
71
+ "expected, tells you what is in that directory: not whether the thing exists, and not whether " +
72
72
  "you have found all of it. Before you report that something is absent, is the only one, or is " +
73
73
  'already handled, search across the repository and let the output be your reason. "I looked" ' +
74
74
  "is a claim; the search and what it printed is evidence.\n\n";
75
75
 
76
+ /**
77
+ * The failure this stops: an agent starts changing code from the first line of the work, before it
78
+ * has read what the people who know the product wrote down, or seen the problem for itself, or
79
+ * learned what done means — and builds the wrong thing well.
80
+ */
81
+ const READ_FIRST =
82
+ "READ BEFORE YOU CHANGE ANYTHING. Before you write code, read what you were given to know (the " +
83
+ "project's README, when the work arrives with one) and find the thing itself: reproduce the " +
84
+ "problem, or find where the change goes. If you still cannot say what done means, ask the person " +
85
+ "with `ask` and wait for the answer: a question now costs less than the wrong work.\n\n";
86
+
76
87
  /** Said in front of every piece of work an agent takes. Mirrors LEAD in apps/api/src/mcp-http.ts. */
77
- const LEAD = SEARCH_WIDE + SHOOT_THE_REAL_THING + KEEP_EVIDENCE;
88
+ const LEAD = READ_FIRST + SEARCH_WIDE + SHOOT_THE_REAL_THING + KEEP_EVIDENCE;
78
89
 
79
90
  /**
80
91
  * An answer a client can read as data as well as prose. The spec asks for both: structured content
@@ -115,7 +126,7 @@ const fail = (err) => ({ content: [{ type: "text", text: err.message }], isError
115
126
  export function nextNote({ next = [], say = "" } = {}, id = "") {
116
127
  const lines = next.map(
117
128
  (s) =>
118
- ` ${s.tool}${id && s.tool !== "take" ? ` ${id}` : ""} — when ${s.when} (${s.why})` +
129
+ ` ${s.tool}${id && s.tool !== "take" ? ` ${id}` : ""}: when ${s.when} (${s.why})` +
119
130
  // What the call has to carry, under the call it belongs to.
120
131
  (s.with ? `\n with ${s.with}` : ""),
121
132
  );
@@ -173,39 +184,39 @@ export function buildServer() {
173
184
  {
174
185
  instructions:
175
186
  "Passalong keeps a team's work and what it knows, as four things. WORK is something " +
176
- "someone must still do: one thing, with no kinds, sections or templates — a title, a " +
187
+ "someone must still do (one thing, with no kinds, sections or templates): a title, a " +
177
188
  "summary for a person, and whatever text, pictures and files the next reader needs, in " +
178
189
  "whatever shape fits. Read it and work out what done means; nothing tells you in advance. " +
179
190
  "A PROJECT is a place for one product or body of work; its DOCUMENTS are what to know " +
180
- "about it — a README saying what it is, who uses it and how it is built, a page per area, " +
191
+ "about it: a README saying what it is, who uses it and how it is built, a page per area, " +
181
192
  "a Log of what changed. FILES are pictures and other files, on work or in a project.\n" +
182
193
  "DOING WORK. take (with an id, or none for the next thing waiting here) says you are doing " +
183
194
  "it and returns it; nobody else can take it while you hold it, and whoever sent it sees " +
184
- "you are on it. progress, with a one-line note at each milestone — 30 minutes of silence " +
195
+ "you are on it. progress, with a one-line note at each milestone; 30 minutes of silence " +
185
196
  "marks it stalled. hand_in when it is done. pass, with the reason, when it is not yours or " +
186
197
  "you are stuck. ask, with a question, when you need the person's answer: you keep it, stop " +
187
198
  "and wait, and take it again once they have replied. Every answer ends with `next`: what " +
188
199
  "to call now. Follow it, and when it says to stop, stop. OPEN WORK YOU MEAN TO DO WITH " +
189
200
  "take, NOT find_work: find_work only reads, and nobody learns you are on it. If take says " +
190
201
  "somebody else has it, tell the user instead of doing the work twice.\n" +
191
- "EVERY HAND-IN CARRIES EVIDENCE: what you ran and what came back — the command and the " +
202
+ "EVERY HAND-IN CARRIES EVIDENCE: what you ran and what came back, with the command and the " +
192
203
  "lines that decided it, a test summary, a link to the change, or a screenshot url. " +
193
204
  "Collect it as you work rather than writing it from memory at the end. hand_in without " +
194
205
  "it is refused, because your note is your word for your own work and evidence is the " +
195
206
  "part a person can check.\n" +
196
207
  "WRITING WORK DOWN. create_work takes one piece of work or several. Each says what it is " +
197
- "to a person in `summary` — one or two plain sentences, 400 characters at most, no ids, " +
208
+ "to a person in `summary`: one or two plain sentences, 400 characters at most, no ids, " +
198
209
  "paths or jargon, saying what this is and whether anybody has to act. It is what the " +
199
210
  "person sees first; the text behind it is for agents: exact, dense, full of ids and " +
200
211
  "paths. New work is Ready: it reaches whoever it is for at once. Pass `draft: true` when " +
201
- "a person should read it first — a plan of several pieces, or something you are unsure " +
202
- "of — and tell them. A tag such as bug, feature or question may describe it; nothing " +
212
+ "a person should read it first (a plan of several pieces, or something you are unsure " +
213
+ "of) and tell them. A tag such as bug, feature or question may describe it; nothing " +
203
214
  "turns on one. BEFORE WRITING ANYTHING DOWN, CHECK WHAT IS ALREADY OPEN: find_work with " +
204
215
  "nothing shows the board and what this worktree holds. If this session's work answers " +
205
- "something you hold, hand_in that — never write a second piece of work about it. Defects " +
216
+ "something you hold, hand_in that: never write a second piece of work about it. Defects " +
206
217
  "you found and are not fixing go in one create_work call, each its own item, so each can " +
207
218
  "be taken on its own. AN IMAGE THE USER SHOWED YOU IS EVIDENCE, NOT CONTEXT: add_file it " +
208
- "and put the line it returns in the text — a screenshot described instead of attached is " +
219
+ "and put the line it returns in the text: a screenshot described instead of attached is " +
209
220
  "the most useful thing in the report, thrown away. update_work changes work that exists: " +
210
221
  "Ready, Draft or archived, who it is for, or its text.\n" +
211
222
  "WHAT YOU LEARN GOES INTO THE PROJECT'S DOCUMENTS. When the user asks you to pass this " +
@@ -214,14 +225,14 @@ export function buildServer() {
214
225
  "the page for that area, and save_document to update the page that already covers it " +
215
226
  "rather than adding a new one each time, with a line on its Log. Never hand knowledge to " +
216
227
  'one person as a piece of work. "Update the passalong", "add this to passalong" and the ' +
217
- "like are ambiguous — hand in what you hold, update the documents, or write new work? Ask " +
228
+ "like are ambiguous: hand in what you hold, update the documents, or write new work? Ask " +
218
229
  "which, in one line, rather than writing and leaving the user to undo it.\n" +
219
230
  "A PROJECT IS A PLACE; ITS DOCUMENTS ARE WHAT TO KNOW; WORK IS WHAT TO DO. Sort " +
220
- "everything before writing it. If someone still has to act on it — a defect still open, " +
221
- "work not yet done, a check nobody has run — it is work: it has an owner, a state and an " +
231
+ "everything before writing it. If someone still has to act on it (a defect still open, " +
232
+ "work not yet done, a check nobody has run), it is work: it has an owner, a state and an " +
222
233
  "answer. A document has none of those and tells nobody, so open work recorded only as a " +
223
- "document is lost. If it is what someone needs to know — an overview, a decision, a " +
224
- "finding, something already fixed with its commit — it is a document. WHERE A PROJECT " +
234
+ "document is lost. If it is what someone needs to know (an overview, a decision, a " +
235
+ "finding, something already fixed with its commit), it is a document. WHERE A PROJECT " +
225
236
  "LIVES is in the `space` field find_projects and save_project return: private, or a named " +
226
237
  "team. Tell the person that, never what you meant to do; a project made without `team` " +
227
238
  "is private, and one made inside another takes its space. Call find_projects before " +
@@ -266,9 +277,9 @@ export function buildServer() {
266
277
  annotations: ADDS,
267
278
  description:
268
279
  "Say you are doing it, and get it. With `id`, that piece of work. With no id, the next " +
269
- "thing waiting here — always something new, never what you hold. While you hold it no " +
280
+ "thing waiting here: always something new, never what you hold. While you hold it no " +
270
281
  "other agent can take it, and whoever sent it sees you are on it. You may hold several; " +
271
- "the person watches them on the board. If somebody else has it, the answer says who — " +
282
+ "the person watches them on the board. If somebody else has it, the answer says who: " +
272
283
  "tell the user rather than doing the work twice. Every answer ends with what to call next.",
273
284
  inputSchema: {
274
285
  id: z
@@ -279,7 +290,7 @@ export function buildServer() {
279
290
  any: z
280
291
  .boolean()
281
292
  .optional()
282
- .describe("work for another repo, or none — only when the user asks"),
293
+ .describe("work for another repo, or none: only when the user asks"),
283
294
  },
284
295
  },
285
296
  async ({ id, cwd, any }) => {
@@ -287,12 +298,13 @@ export function buildServer() {
287
298
  const r = await passalong.take(id, { cwd: cwd || process.cwd(), any: Boolean(any) });
288
299
  if (!r.work) return text(nextNote(r) || "Nothing is waiting for this agent here.");
289
300
  const from = await passalong.parentWork(parse(r.work.markdown).meta);
301
+ const known = passalong.projectsNote(r.projects);
290
302
  const held =
291
303
  `<!-- passalong: ${r.work.id} is yours` +
292
- `${r.work.resumed ? " (you already held it — carry on from where it was left)" : ""}. -->`;
304
+ `${r.work.resumed ? " (you already held it: carry on from where it was left)" : ""}. -->`;
293
305
  const said = repliesNote(r.replies);
294
306
  return text(
295
- `${said ? `${said}\n\n` : ""}${from}${LEAD}${r.work.markdown}` +
307
+ `${said ? `${said}\n\n` : ""}${from}${known}${LEAD}${r.work.markdown}` +
296
308
  `\n\n${held}\n${nextNote(r, r.work.id)}\n${passalong.AFTER}`,
297
309
  );
298
310
  } catch (err) {
@@ -342,13 +354,13 @@ export function buildServer() {
342
354
  description:
343
355
  "Finished here, with proof. IT IS FINAL: the work goes to Done with what you showed, and " +
344
356
  "nobody reviews it after you (anyone can reopen it with a reason). So SHOW IT: a " +
345
- "screenshot for anything with a screen — before and after, for a defect — or, for " +
357
+ "screenshot for anything with a screen (before and after, for a defect) or, for " +
346
358
  "backend work, the one command and the line of its output that shows it works. Send " +
347
359
  "`checks`: one entry per thing the work asked for, each with that line and the evidence " +
348
360
  "for it. That is what a person reads, line against line, AND IT IS HOW YOU KNOW YOU ARE " +
349
361
  "DONE: every line has one. Give a check `cmd` when a command proves it: the command is " +
350
362
  "run here before the hand-in lands, a non-zero exit refuses it, and what it printed is " +
351
- "recorded instead of your account of it. Use `ran` for a check no command can settle — " +
363
+ "recorded instead of your account of it. Use `ran` for a check no command can settle: " +
352
364
  "add_file the screenshot and put the line it returns there. `evidence` as one block works " +
353
365
  "too. DO NOT write new work to carry your evidence: it belongs on this call. What you had " +
354
366
  "to adapt goes in `writeup`; what you learned about the project goes into its documents " +
@@ -359,7 +371,7 @@ export function buildServer() {
359
371
  .string()
360
372
  .optional()
361
373
  .describe(
362
- "what you ran and what came back: the command and the lines that decided it, a test " +
374
+ "what you ran and what came back, with the command and the lines that decided it, a test " +
363
375
  'summary, a link to the change, or a screenshot url. "it works" is a claim, not evidence',
364
376
  ),
365
377
  checks: z
@@ -391,10 +403,10 @@ export function buildServer() {
391
403
  .string()
392
404
  .optional()
393
405
  .describe(
394
- "the shell command that proves this line, run here before the hand-in lands — " +
406
+ "the shell command that proves this line, run here before the hand-in lands: " +
395
407
  "its exit code decides the check and its output is recorded as the evidence. " +
396
408
  "Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
397
- '"the badge reads 3" — then SHOW it with add_file and put the line it returns in `ran`; a check is run or shown, and describing what you saw is refused',
409
+ '"the badge reads 3": then SHOW it with add_file and put the line it returns in `ran`; a check is run or shown, and describing what you saw is refused',
398
410
  ),
399
411
  }),
400
412
  ),
@@ -416,7 +428,7 @@ export function buildServer() {
416
428
  .string()
417
429
  .optional()
418
430
  .describe(
419
- "what you had to adapt to make it work here — a version, a name, a step the work did " +
431
+ "what you had to adapt to make it work here: a version, a name, a step the work did " +
420
432
  "not mention. 500 characters at most; anything longer belongs in the change. " +
421
433
  "Optional: leave it out when it went as written",
422
434
  ),
@@ -467,7 +479,7 @@ export function buildServer() {
467
479
  annotations: ADDS,
468
480
  description:
469
481
  "Not yours to do, or you are stuck: give it back with the reason. It is open again for " +
470
- "the next agent, and the reason goes to whoever is next — say why, or they start where " +
482
+ "the next agent, and the reason goes to whoever is next: say why, or they start where " +
471
483
  "silence left them.",
472
484
  inputSchema: {
473
485
  id: z.string().describe("passalong id"),
@@ -489,7 +501,8 @@ export function buildServer() {
489
501
  "context it needs to be answered without opening your terminal. You keep what you hold " +
490
502
  "and nobody else can take it. Then STOP and tell the user you are waiting: do not carry " +
491
503
  "on guessing. When they have replied, call take with this id and their reply comes back " +
492
- "with it. Use `pass` instead when the work is not yours.",
504
+ "with it. Use `pass` instead when the work is not yours. When the answer is a choice " +
505
+ "between options, offer them as `choices` so the person can pick one with a tap.",
493
506
  inputSchema: {
494
507
  id: z.string().describe("passalong id"),
495
508
  question: z
@@ -499,10 +512,31 @@ export function buildServer() {
499
512
  '"Should the dark-mode toggle live in Settings or the header? The header is ' +
500
513
  'crowded on mobile." What you were about to do, if they just say yes, helps',
501
514
  ),
515
+ choices: z
516
+ .array(
517
+ z.object({
518
+ label: z.string().describe("what this option is, in a few words"),
519
+ detail: z.string().optional().describe("one line more: what it means or costs"),
520
+ picture: z
521
+ .string()
522
+ .optional()
523
+ .describe("a screenshot of it, uploaded with add_file first: the /v1/shots/ link"),
524
+ }),
525
+ )
526
+ .min(2)
527
+ .max(4)
528
+ .optional()
529
+ .describe(
530
+ "when the answer is a pick between options (two layouts, three fixes): two to four, " +
531
+ "each with a screenshot when it is something to look at. The person picks one in " +
532
+ "the hub with a tap; their pick comes back as the reply",
533
+ ),
502
534
  cwd: CWD,
503
535
  },
504
536
  },
505
- verb(({ id, question, cwd }) => passalong.ask(id, question, { cwd: cwd || process.cwd() })),
537
+ verb(({ id, question, choices, cwd }) =>
538
+ passalong.ask(id, question, { cwd: cwd || process.cwd(), choices }),
539
+ ),
506
540
  );
507
541
 
508
542
  // ---- work: create_work, find_work, update_work ---------------------------------------------
@@ -513,7 +547,7 @@ export function buildServer() {
513
547
  title: "Write work down",
514
548
  annotations: ADDS,
515
549
  description:
516
- "Write down work someone must still do: one piece, or several at once — a plan broken " +
550
+ "Write down work someone must still do (one piece, or several at once): a plan broken " +
517
551
  "into steps, or every defect a test run turned up, each its own item so each can be " +
518
552
  "taken on its own. Check what is open first (find_work): if this session answers " +
519
553
  "something you hold, hand_in that instead. Each item is Ready at once and reaches whoever " +
@@ -536,7 +570,7 @@ export function buildServer() {
536
570
  .optional()
537
571
  .describe(
538
572
  "markdown for whoever does it: what is wrong or wanted, what you found, what " +
539
- "done looks like if you know — in whatever shape fits",
573
+ "done looks like if you know: in whatever shape fits",
540
574
  ),
541
575
  to: z
542
576
  .string()
@@ -545,7 +579,7 @@ export function buildServer() {
545
579
  parent: z
546
580
  .string()
547
581
  .optional()
548
- .describe("id of the work this comes out of — a bigger piece, a customer request"),
582
+ .describe("id of the work this comes out of: a bigger piece, a customer request"),
549
583
  after: z
550
584
  .array(z.number().int())
551
585
  .optional()
@@ -607,7 +641,7 @@ export function buildServer() {
607
641
  title: "Find work",
608
642
  annotations: READS,
609
643
  description:
610
- "READ work without taking it. With `id`, that piece of work — its markdown, read only; " +
644
+ "READ work without taking it. With `id`, that piece of work: its markdown, read only; " +
611
645
  "work over 20,000 characters comes back as an outline unless you ask for `section` or " +
612
646
  "`full`. With `query`, the work whose words match. With neither, the board: what needs " +
613
647
  "the person, who is on what, what is ready, and what this worktree holds. To DO the work, " +
@@ -668,9 +702,9 @@ export function buildServer() {
668
702
  description:
669
703
  "Change work that exists, when the person asks or when you wrote it. `status`: ready " +
670
704
  "(whoever it is for may take it), draft (held for a person to read), or archived (off the " +
671
- "board; it comes back to Draft). `to`: who it is for — @handle, several as @ada,@bola, " +
705
+ "board; it comes back to Draft). `to`: who it is for: @handle, several as @ada,@bola, " +
672
706
  '#group, or "" for the whole team; whoever held it and is left out has it taken back and ' +
673
- "is told. `markdown`: its whole text, frontmatter first, as find_work gave it — say " +
707
+ "is told. `markdown`: its whole text, frontmatter first, as find_work gave it: say " +
674
708
  "what changed, keep what did not.",
675
709
  inputSchema: {
676
710
  id: z.string().describe("passalong id"),
@@ -804,7 +838,7 @@ export function buildServer() {
804
838
  description:
805
839
  "Write what someone needs to know into a project. With no `document`, a new one: `name` " +
806
840
  "and `body`. With `document`, change it: `body` with the `version` read_document gave you " +
807
- "— a newer edit is refused, so nobody's writing is lost — and/or `name` to rename it, " +
841
+ "(a newer edit is refused, so nobody\'s writing is lost) and/or `name` to rename it, " +
808
842
  "and/or `move_to` another project in the same space. Its id and history stay. Update the " +
809
843
  "page that already covers something rather than adding another. A document has no owner " +
810
844
  "and no state and tells nobody: anything someone must still act on is work (create_work).",
@@ -867,8 +901,8 @@ export function buildServer() {
867
901
  title: "Add a file",
868
902
  annotations: { ...ADDS, openWorldHint: true },
869
903
  description:
870
- "Upload a picture or any other file — a path in `file`, or a picture's bytes in `data` " +
871
- "when you have the image and no file — and get back the markdown line that points at it. " +
904
+ "Upload a picture or any other file (a path in `file`, or a picture's bytes in `data` " +
905
+ "when you have the image and no file) and get back the markdown line that points at it. " +
872
906
  "Files up to 10MB, pictures up to 5MB. " +
873
907
  WHERE_THE_LINE_GOES,
874
908
  inputSchema: {
@@ -897,7 +931,7 @@ export function buildServer() {
897
931
  if (!file && !data)
898
932
  throw new Error(
899
933
  "send `file` (a path on this machine) or `data` (a picture, base64). If your " +
900
- "screenshot is a tool result you cannot write out, capture it to a file instead — a " +
934
+ "screenshot is a tool result you cannot write out, capture it to a file instead: a " +
901
935
  "headless browser's page.screenshot({ path }) or your platform's capture command.",
902
936
  );
903
937
  const got = data