passalong 0.18.0 → 0.20.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 +3 -3
- package/bin/passalong +19 -12
- package/package.json +1 -1
- package/skill/SKILL.md +1 -2
- package/src/api.js +2 -0
- package/src/guide.js +7 -6
- package/src/hooks.js +49 -23
- package/src/mcp.js +33 -31
- package/src/passalong.js +18 -14
- package/src/update.js +27 -3
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ codex mcp add passalong -- passalong mcp # Codex: the same MCP server
|
|
|
28
28
|
## Commands
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
passalong share [file] [--to team[/handle]] publish a guide
|
|
31
|
+
passalong share [file] [--to team[/handle]] publish a guide → id + link, Ready; --to addresses it
|
|
32
32
|
passalong pull <id|link> print a guide; --save also writes ./.passalong/<id>.md (git-ignored)
|
|
33
33
|
passalong inbox guides handed to you that you have not pulled yet
|
|
34
34
|
passalong board waiting on you, in flight, landed, worth keeping
|
|
@@ -73,8 +73,8 @@ passalong inbox # on Ada's side: what was handed to you
|
|
|
73
73
|
passalong activity # back on your side: it landed, and who pulled it
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
Every guide, whatever its kind, moves the same way: it is
|
|
77
|
-
|
|
76
|
+
Every guide, whatever its kind, moves the same way: it is Ready when shared (or a draft, with
|
|
77
|
+
`status: draft`, until a person runs `passalong ready <id>`), then whoever takes it (`passalong take <id>`) holds it, hands it in with
|
|
78
78
|
the evidence, and a person approves it or sends it back with the reason (`passalong approve` /
|
|
79
79
|
`passalong reject`).
|
|
80
80
|
|
package/bin/passalong
CHANGED
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
// handed to whoever pulls or starts that guide
|
|
7
7
|
// TARGET is a team, team/@handle for one person, or "team/#group" for the people who do a
|
|
8
8
|
// thing — quote that one, since a shell may read the # as a comment
|
|
9
|
-
// passalong task "<what needs doing>" write a task for an agent;
|
|
9
|
+
// passalong task "<what needs doing>" write a task for an agent; Ready at once
|
|
10
10
|
// passalong ready <id...|--all> guides you have read are ready — whoever they are for may take them now
|
|
11
11
|
// passalong tasks every task and where it is: draft, ready, claimed, stalled, review
|
|
12
12
|
// passalong work [--once] [--agent CMD] run an agent on each ready task here, one after another
|
|
13
|
-
// passalong
|
|
14
|
-
// passalong reject <id> <why> not done: back to Ready, and whoever takes it next reads why
|
|
13
|
+
// passalong reopen <id> <why> finished work isn't right: back to To do, and whoever takes it next reads why
|
|
15
14
|
// passalong release <id> take work back from whoever holds it, of any kind
|
|
16
15
|
// passalong attach <file> upload a picture or a file and print the markdown that points at it
|
|
17
16
|
// passalong reply <id> <text> write to whoever holds it (--file PATH to attach); or leave a note
|
|
@@ -337,9 +336,8 @@ const commands = {
|
|
|
337
336
|
const id = guide.meta.id;
|
|
338
337
|
console.log(id);
|
|
339
338
|
const target = guide.meta.target_context;
|
|
340
|
-
console.error(`task "${guide.meta.title}" is
|
|
339
|
+
console.error(`task "${guide.meta.title}" is Ready${target ? ` for ${target}` : " (no repo)"}`);
|
|
341
340
|
if (guide.meta.team) console.error(`handed to ${guide.meta.team} (team inbox)`);
|
|
342
|
-
console.error(`when an agent may take it: passalong ready ${id}`);
|
|
343
341
|
},
|
|
344
342
|
|
|
345
343
|
async tasks() {
|
|
@@ -384,16 +382,25 @@ const commands = {
|
|
|
384
382
|
if (n) console.error("review them with: passalong tasks");
|
|
385
383
|
},
|
|
386
384
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
console.error(
|
|
385
|
+
// Nothing waits for approval now (docs/REVIEW.md): finishing is final. Kept so a script that
|
|
386
|
+
// calls it is told, not refused.
|
|
387
|
+
async approve() {
|
|
388
|
+
console.error(
|
|
389
|
+
"nothing to approve: finished work is already Done. If it isn't right: passalong reopen <id> <why>",
|
|
390
|
+
);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/** Finished work back to To do, with why at its top; whoever finished it is told. */
|
|
394
|
+
async reopen(f) {
|
|
395
|
+
const id = f._[0] || die("usage: passalong reopen <id> <why>");
|
|
396
|
+
const why = f._.slice(1).join(" ");
|
|
397
|
+
if (!why) die("say why: passalong reopen <id> <what is wrong>");
|
|
398
|
+
await passalong.reopen(id, why).catch((e) => die(e.message));
|
|
399
|
+
console.error(`${id} is back in To do — whoever takes it next reads why`);
|
|
391
400
|
},
|
|
392
401
|
|
|
393
402
|
async reject(f) {
|
|
394
|
-
|
|
395
|
-
await passalong.rejectTask(id, f._.slice(1).join(" ")).catch((e) => die(e.message));
|
|
396
|
-
console.error(`${id} is back in Ready — the next agent reads why`);
|
|
403
|
+
return commands.reopen(f);
|
|
397
404
|
},
|
|
398
405
|
|
|
399
406
|
async release(f) {
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -67,8 +67,7 @@ machine, another agent session, or a teammate without your branch.
|
|
|
67
67
|
follow-up gets the guide it came out of, and where that guide has got to — so write it as a
|
|
68
68
|
note on that work rather than as a standalone brief. What you did or found while working on a
|
|
69
69
|
guide belongs in its `hand_in`, not a follow-up.
|
|
70
|
-
6. **Report** the id (and link) and
|
|
71
|
-
it and marks it Ready (`passalong ready <id>`, or Ready in the hub). Then one line on how to use
|
|
70
|
+
6. **Report** the id (and link). It is Ready and has reached whoever it is for. Then one line on how to use
|
|
72
71
|
it on the other side: `passalong take <id>` in the target repo, or "take passalong <id>" to an
|
|
73
72
|
agent with the Passalong MCP server. `take` rather than `pull` on purpose: it holds the work as
|
|
74
73
|
well as fetching it, and until somebody takes it the sender cannot tell a guide nobody noticed
|
package/src/api.js
CHANGED
|
@@ -429,6 +429,8 @@ export const approveTask = (id) =>
|
|
|
429
429
|
call(`/v1/tasks/${encodeURIComponent(id)}/approve`, { method: "POST", body: {} });
|
|
430
430
|
export const rejectTask = (id, why) =>
|
|
431
431
|
call(`/v1/tasks/${encodeURIComponent(id)}/reject`, { method: "POST", body: { why } });
|
|
432
|
+
export const reopen = (id, why) =>
|
|
433
|
+
call(`/v1/guides/${encodeURIComponent(id)}/reopen`, { method: "POST", body: { why } });
|
|
432
434
|
/** A handoff's or bug's approve and send-back. No `place` names the only hand-in there is. */
|
|
433
435
|
export const closeGuide = (id) =>
|
|
434
436
|
call(`/v1/guides/${encodeURIComponent(id)}/close`, { method: "POST", body: {} });
|
package/src/guide.js
CHANGED
|
@@ -497,7 +497,9 @@ export function stamp(guide, defaults = {}) {
|
|
|
497
497
|
const meta = { ...guide.meta };
|
|
498
498
|
if (!meta.id) meta.id = newId();
|
|
499
499
|
if (!meta.created) meta.created = new Date().toISOString();
|
|
500
|
-
|
|
500
|
+
// Ready unless whoever wrote it said draft: a guide sent to someone is meant to reach them, and
|
|
501
|
+
// drafts were where addressed work sat forgotten (every draft in production on 2026-10-15).
|
|
502
|
+
if (!meta.status) meta.status = "published";
|
|
501
503
|
for (const [k, v] of Object.entries(defaults))
|
|
502
504
|
if (meta[k] === undefined || meta[k] === "") meta[k] = v;
|
|
503
505
|
return { meta, body: guide.body };
|
|
@@ -525,8 +527,7 @@ export function bugGuide({
|
|
|
525
527
|
area = "",
|
|
526
528
|
report = "",
|
|
527
529
|
environment = "",
|
|
528
|
-
|
|
529
|
-
status = "draft",
|
|
530
|
+
status = "published",
|
|
530
531
|
evidence = [],
|
|
531
532
|
} = {}) {
|
|
532
533
|
/**
|
|
@@ -617,7 +618,7 @@ export function template(meta = {}) {
|
|
|
617
618
|
summary: "",
|
|
618
619
|
author: "",
|
|
619
620
|
source_context: "",
|
|
620
|
-
status: "
|
|
621
|
+
status: "published",
|
|
621
622
|
stack_assumptions: [],
|
|
622
623
|
tags: [],
|
|
623
624
|
...meta,
|
|
@@ -655,7 +656,7 @@ function bugTemplate(meta = {}) {
|
|
|
655
656
|
kind: "bug",
|
|
656
657
|
author: "",
|
|
657
658
|
source_context: "",
|
|
658
|
-
status: "
|
|
659
|
+
status: "published",
|
|
659
660
|
severity: "s3",
|
|
660
661
|
stack_assumptions: [],
|
|
661
662
|
tags: ["bug"],
|
|
@@ -697,7 +698,7 @@ function taskTemplate(meta = {}) {
|
|
|
697
698
|
author: "",
|
|
698
699
|
source_context: "",
|
|
699
700
|
target_context: "",
|
|
700
|
-
status: "
|
|
701
|
+
status: "published",
|
|
701
702
|
stack_assumptions: [],
|
|
702
703
|
tags: [],
|
|
703
704
|
...meta,
|
package/src/hooks.js
CHANGED
|
@@ -21,28 +21,47 @@ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
|
21
21
|
/**
|
|
22
22
|
* What this worktree's agent holds and what is waiting, as a few lines for an agent to read.
|
|
23
23
|
* Empty when there is nothing to say, so a session in an unrelated repo is not told anything.
|
|
24
|
+
*
|
|
25
|
+
* `held` is a list: an agent may hold several things, and several sessions in one worktree are one
|
|
26
|
+
* agent to the queue. So a session is told what the worktree holds, and to carry on only with what
|
|
27
|
+
* was its own before a restart — never to resume another session's work.
|
|
24
28
|
*/
|
|
25
|
-
export function nowText({ held, waiting = {} }) {
|
|
26
|
-
if (held) {
|
|
29
|
+
export function nowText({ held = [], waiting = {} }) {
|
|
30
|
+
if (held.length === 1) {
|
|
31
|
+
const [h] = held;
|
|
27
32
|
const lines = [
|
|
28
33
|
// `kind` is required on what /v1/working sends. The `|| "task"` that used to be here was
|
|
29
34
|
// both dead and wrong — absent has meant transfer since kinds existed, never task.
|
|
30
|
-
`Passalong:
|
|
35
|
+
`Passalong: This worktree holds ${h.id} (${h.kind}): ${h.title || h.id}.`,
|
|
31
36
|
];
|
|
32
|
-
if (
|
|
37
|
+
if (h.note) lines.push(`Last note: "${h.note}".`);
|
|
33
38
|
// The one thing here that is new information rather than a reminder: the person has written, and
|
|
34
39
|
// nothing else in the session will say so until the agent next checks in.
|
|
35
|
-
if (
|
|
40
|
+
if (h.replies)
|
|
36
41
|
lines.push(
|
|
37
|
-
`${plural(
|
|
38
|
-
`waiting
|
|
42
|
+
`${plural(h.replies, "message", "messages")} from the person ${h.replies === 1 ? "is" : "are"} ` +
|
|
43
|
+
`waiting: call take ${h.id} to read ${h.replies === 1 ? "it" : "them"} before anything else.`,
|
|
39
44
|
);
|
|
40
|
-
if (
|
|
45
|
+
if (h.state === "stalled")
|
|
41
46
|
lines.push("It is marked stalled: nobody has heard from its agent in 30 minutes.");
|
|
42
47
|
lines.push(
|
|
43
|
-
`
|
|
44
|
-
"milestone. When it is done, hand_in with evidence — what you ran and
|
|
45
|
-
"so keep it as you go; if it is not yours or you are stuck, pass with
|
|
48
|
+
`If it was yours before this session restarted, call take ${h.id} to carry on, then ` +
|
|
49
|
+
"progress at each milestone. When it is done, hand_in with evidence — what you ran and " +
|
|
50
|
+
"what came back — so keep it as you go; if it is not yours or you are stuck, pass with " +
|
|
51
|
+
"the reason. If another session here is on it, leave it to that session.",
|
|
52
|
+
);
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
if (held.length > 1) {
|
|
56
|
+
const lines = [`Passalong: This worktree holds ${held.length} things:`];
|
|
57
|
+
for (const h of held)
|
|
58
|
+
lines.push(
|
|
59
|
+
`- ${h.id} (${h.kind}): ${h.title || h.id}${h.state === "stalled" ? " — stalled" : ""}` +
|
|
60
|
+
`${h.replies ? ` — ${plural(h.replies, "message", "messages")} waiting` : ""}`,
|
|
61
|
+
);
|
|
62
|
+
lines.push(
|
|
63
|
+
"Several sessions may be working here. Carry on only with what was yours before this " +
|
|
64
|
+
"session restarted (take <id> resumes it) and leave the rest to the sessions on them.",
|
|
46
65
|
);
|
|
47
66
|
return lines.join("\n");
|
|
48
67
|
}
|
|
@@ -59,19 +78,22 @@ export function nowText({ held, waiting = {} }) {
|
|
|
59
78
|
* Whether a session may stop. `input` is what Claude Code sends the Stop hook on stdin. Null lets
|
|
60
79
|
* it stop; an object is the hook's answer, blocking once with what to call. `stop_hook_active`
|
|
61
80
|
* means this stop is already the session continuing because of this hook, and blocking again
|
|
62
|
-
* would leave it no way to end.
|
|
81
|
+
* would leave it no way to end. The hook cannot tell which session took what, so it names
|
|
82
|
+
* everything still open here and lets the session settle its own.
|
|
63
83
|
*/
|
|
64
|
-
export function stopVerdict({ held, input = {} }) {
|
|
84
|
+
export function stopVerdict({ held = [], input = {} }) {
|
|
65
85
|
// An agent that asked a question was told to stop and wait, so it must be let: blocking it would
|
|
66
86
|
// make the instruction and the hook contradict each other, and the agent would pick one.
|
|
67
|
-
|
|
87
|
+
const open = held.filter((h) => !h.asking);
|
|
88
|
+
if (!open.length || input.stop_hook_active) return null;
|
|
89
|
+
const ids = open.map((h) => h.id).join(", ");
|
|
68
90
|
return {
|
|
69
91
|
decision: "block",
|
|
70
92
|
reason:
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
93
|
+
`This worktree still holds ${ids}. For anything there you worked on in this session, do ` +
|
|
94
|
+
"one of before ending: hand_in it with your evidence if it is done; pass it with the reason " +
|
|
95
|
+
"if it is not yours or you are stuck; or, if the user is stopping mid-way, progress it with " +
|
|
96
|
+
"a note saying what is left. Leave what other sessions are on — then end.",
|
|
75
97
|
};
|
|
76
98
|
}
|
|
77
99
|
|
|
@@ -112,15 +134,19 @@ const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
|
112
134
|
* `needs` counts everything waiting on the person: work handed in for review, agents stuck on
|
|
113
135
|
* them, and guides handed to them.
|
|
114
136
|
*/
|
|
115
|
-
export function statusText({ held, needs = 0, ready = 0 }) {
|
|
137
|
+
export function statusText({ held = [], needs = 0, ready = 0 }) {
|
|
116
138
|
const parts = [];
|
|
117
|
-
if (held) {
|
|
118
|
-
const
|
|
119
|
-
|
|
139
|
+
if (held.length === 1) {
|
|
140
|
+
const [h] = held;
|
|
141
|
+
const said = h.note ? ` "${clip(h.note, 32)}"` : "";
|
|
142
|
+
parts.push(`▸ ${h.id}${h.state === "stalled" ? " (stalled)" : ""}${said}`);
|
|
143
|
+
} else if (held.length > 1) {
|
|
144
|
+
const stalled = held.filter((h) => h.state === "stalled").length;
|
|
145
|
+
parts.push(`▸ ${held.length} held${stalled ? ` (${stalled} stalled)` : ""}`);
|
|
120
146
|
} else parts.push("passalong");
|
|
121
147
|
if (needs) parts.push(`${needs} need${needs === 1 ? "s" : ""} you`);
|
|
122
148
|
if (ready) parts.push(`${ready} ready`);
|
|
123
|
-
if (parts.length === 1 && !held) parts.push("clear");
|
|
149
|
+
if (parts.length === 1 && !held.length) parts.push("clear");
|
|
124
150
|
return parts.join(" · ");
|
|
125
151
|
}
|
|
126
152
|
|
package/src/mcp.js
CHANGED
|
@@ -297,15 +297,16 @@ export function buildServer() {
|
|
|
297
297
|
"to act. It is what the person sees first in their hub; the document is behind it. A guide " +
|
|
298
298
|
"with no summary is refused.\n" +
|
|
299
299
|
"SAY WHICH KIND IT IS. There is no default: a guide with no `kind:` line is refused, and so " +
|
|
300
|
-
"is a spelling that is not one of the three.
|
|
301
|
-
"
|
|
302
|
-
"
|
|
300
|
+
"is a spelling that is not one of the three. A NEW GUIDE IS READY: it reaches whoever it is " +
|
|
301
|
+
"for at once. Say `status: draft` only when the person should read it first — something you " +
|
|
302
|
+
"are unsure of — and tell them; plan_tasks drafts for you. Every kind moves the same way — " +
|
|
303
|
+
"Ready, taken, handed in, approved. " +
|
|
303
304
|
`kind: task is work nobody has done yet. Sections: ${TASK_SECTIONS.join(", ")}. ` +
|
|
304
305
|
"It has no Steps: work out how to reach Goal within Constraints, leave Out of scope " +
|
|
305
306
|
"alone, and treat Acceptance as the definition of done. A proposal awaiting a team decision " +
|
|
306
|
-
"stays Draft until a person makes it Ready. hand_in
|
|
307
|
-
"
|
|
308
|
-
"
|
|
307
|
+
"stays Draft until a person makes it Ready. hand_in when every Acceptance line holds: it is " +
|
|
308
|
+
"final — the task goes to Done with what you showed, and nobody reviews it after you — so " +
|
|
309
|
+
"show it, and say anything you had to adapt in `writeup` (500 characters).\n" +
|
|
309
310
|
`kind: transfer (or no kind: line) is a finished implementation to repeat here. Sections: ${SECTIONS.join(", ")}. ` +
|
|
310
311
|
"Follow its Steps, adapting anything marked ASSUMES to this codebase; run its " +
|
|
311
312
|
"Verification, then hand_in with ok and, when it failed, a note saying what went wrong.\n" +
|
|
@@ -333,8 +334,8 @@ export function buildServer() {
|
|
|
333
334
|
"session into a guide (guide_template kind transfer shows the shape) and call " +
|
|
334
335
|
"publish_guide, with `to` as team, team/@handle for one teammate, or team/#group for the " +
|
|
335
336
|
"people who do a thing. " +
|
|
336
|
-
"BEFORE PUBLISHING ANYTHING, CHECK WHAT IS ALREADY OPEN: call
|
|
337
|
-
"
|
|
337
|
+
"BEFORE PUBLISHING ANYTHING, CHECK WHAT IS ALREADY OPEN: call board (it lists what this " +
|
|
338
|
+
"worktree holds) or read the inbox. If this session's work answers something you hold, hand_in " +
|
|
338
339
|
"that — never publish a second guide about it. A transfer guide is for work that has to " +
|
|
339
340
|
"cross a boundary: another repo, another machine, a teammate without your branch. Work you " +
|
|
340
341
|
"committed and pushed where the team can already see it has crossed no boundary, and a " +
|
|
@@ -943,12 +944,17 @@ export function buildServer() {
|
|
|
943
944
|
"The state of this user's transfers as queues: waiting on you (handed to you, not " +
|
|
944
945
|
"pulled), not working (someone gave it a failing verdict — the most urgent), in flight " +
|
|
945
946
|
"(handed over, nobody has taken it — `stale` means it has sat over a week), and landed " +
|
|
946
|
-
"(someone else has it). Use it to answer
|
|
947
|
+
"(someone else has it), and `held`: what this worktree's agents hold now. Use it to answer " +
|
|
948
|
+
"'what is outstanding?' and to see what you hold, without taking anything.",
|
|
947
949
|
inputSchema: {},
|
|
948
950
|
},
|
|
949
951
|
async () => {
|
|
950
952
|
try {
|
|
951
|
-
|
|
953
|
+
const [board, now] = await Promise.all([
|
|
954
|
+
passalong.board(),
|
|
955
|
+
passalong.now().catch(() => ({ held: [] })),
|
|
956
|
+
]);
|
|
957
|
+
return json({ ...board, held: now.held });
|
|
952
958
|
} catch (err) {
|
|
953
959
|
return fail(err);
|
|
954
960
|
}
|
|
@@ -1084,7 +1090,7 @@ export function buildServer() {
|
|
|
1084
1090
|
const ids = await passalong.planTasks(steps, { cwd: cwd || process.cwd() });
|
|
1085
1091
|
return json({
|
|
1086
1092
|
drafts: ids.map((id, i) => ({ id, title: steps[i].title, after: steps[i].after || [] })),
|
|
1087
|
-
next:
|
|
1093
|
+
next: `all in Draft: ask the user to read them and mark them Ready on the board at ${api.baseUrl()}/hub/work, or run \`passalong ready --all\``,
|
|
1088
1094
|
});
|
|
1089
1095
|
} catch (err) {
|
|
1090
1096
|
return fail(err);
|
|
@@ -1099,9 +1105,9 @@ export function buildServer() {
|
|
|
1099
1105
|
annotations: ADDS,
|
|
1100
1106
|
description:
|
|
1101
1107
|
"Say you are doing it, and get it. With `id`, that guide — any kind: a task, a bug, a " +
|
|
1102
|
-
"handoff. With no id, the next thing waiting
|
|
1103
|
-
"no other agent can take it here, and its sender sees you are on it.
|
|
1104
|
-
"
|
|
1108
|
+
"handoff. With no id, the next thing waiting — always something new, never what you hold. " +
|
|
1109
|
+
"While you hold it no other agent can take it here, and its sender sees you are on it. You " +
|
|
1110
|
+
"may hold several at once; the person watches them on the board. If somebody else has it, the " +
|
|
1105
1111
|
"answer says who — tell the user rather than doing the work twice. Every answer ends with " +
|
|
1106
1112
|
"what to call next.",
|
|
1107
1113
|
inputSchema: {
|
|
@@ -1157,7 +1163,7 @@ export function buildServer() {
|
|
|
1157
1163
|
outputSchema: handInOut,
|
|
1158
1164
|
annotations: ADDS,
|
|
1159
1165
|
description:
|
|
1160
|
-
"
|
|
1166
|
+
"Finished here, with proof. IT IS FINAL: the work goes to Done with what you showed, and nobody reviews it after you (anyone can reopen it with a reason). So SHOW IT: a screenshot for anything with a screen — before and after, for a bug — or, for backend work, the one command and the line of its output that shows it works. Send `checks`: one entry per " +
|
|
1161
1167
|
"line the guide asks for — `## Acceptance` on a task, `## Verification` on a handoff or a " +
|
|
1162
1168
|
"bug — each with that line and the evidence for it. That is what its author reads, line " +
|
|
1163
1169
|
"against line, instead of hunting through a wall of output for the part that answers each " +
|
|
@@ -1165,8 +1171,7 @@ export function buildServer() {
|
|
|
1165
1171
|
"command proves it: the command is run here before the hand-in lands, a non-zero exit " +
|
|
1166
1172
|
"refuses it, and what it printed is recorded instead of your account of it. Use `ran` for " +
|
|
1167
1173
|
"a check no command can settle. `evidence` as one block is the older shape and still " +
|
|
1168
|
-
"works. A task
|
|
1169
|
-
"this publishes it and attaches it. A handoff or a bug takes `ok`, whether its " +
|
|
1174
|
+
"works. A task needs no write-up guide any more. A handoff or a bug takes `ok`, whether its " +
|
|
1170
1175
|
"Verification held here, and `note` saying what went wrong when it did not. DO NOT " +
|
|
1171
1176
|
"publish a guide to carry your evidence: it belongs on this call, and a second document " +
|
|
1172
1177
|
'titled "Hand-in evidence" is the thing `checks` exists to replace. If it worked but you ' +
|
|
@@ -1242,10 +1247,10 @@ export function buildServer() {
|
|
|
1242
1247
|
.string()
|
|
1243
1248
|
.optional()
|
|
1244
1249
|
.describe(
|
|
1245
|
-
"
|
|
1246
|
-
"
|
|
1247
|
-
"it out when it worked as written. The next
|
|
1248
|
-
"
|
|
1250
|
+
"what you had to adapt to make it work here — a version, a name, a step that needed " +
|
|
1251
|
+
"something the guide does not mention. 500 characters at most; anything longer " +
|
|
1252
|
+
"belongs in the change. Optional: leave it out when it worked as written. The next " +
|
|
1253
|
+
"person to open the guide is shown it",
|
|
1249
1254
|
),
|
|
1250
1255
|
risk: z
|
|
1251
1256
|
.string()
|
|
@@ -1259,11 +1264,11 @@ export function buildServer() {
|
|
|
1259
1264
|
markdown: z
|
|
1260
1265
|
.string()
|
|
1261
1266
|
.optional()
|
|
1262
|
-
.describe("
|
|
1267
|
+
.describe("older shape, not needed: say what you adapted in `writeup` instead"),
|
|
1263
1268
|
report: z
|
|
1264
1269
|
.string()
|
|
1265
1270
|
.optional()
|
|
1266
|
-
.describe("
|
|
1271
|
+
.describe("older shape, not needed: say what you adapted in `writeup` instead"),
|
|
1267
1272
|
pr: z.string().optional().describe("task: PR or branch link, when there is one"),
|
|
1268
1273
|
cwd: z
|
|
1269
1274
|
.string()
|
|
@@ -1525,8 +1530,8 @@ export function buildServer() {
|
|
|
1525
1530
|
title: "Publish guide",
|
|
1526
1531
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
1527
1532
|
description:
|
|
1528
|
-
"Publish a guide from markdown (frontmatter + sections). Check what you hold first
|
|
1529
|
-
"
|
|
1533
|
+
"Publish a guide from markdown (frontmatter + sections). Check what you hold first " +
|
|
1534
|
+
"(board): if this session answers something you hold, hand_in that instead, and if the " +
|
|
1530
1535
|
"work never left a branch the team can see, publish what is still open as a bug or a task " +
|
|
1531
1536
|
"rather than a write-up of the fix. A transfer guide by default, or " +
|
|
1532
1537
|
"a single bug with `kind: bug`; use file_bugs for more than one; or a task for work nobody " +
|
|
@@ -1579,7 +1584,8 @@ export function buildServer() {
|
|
|
1579
1584
|
...(guide.meta.status === "draft"
|
|
1580
1585
|
? {
|
|
1581
1586
|
status: "draft",
|
|
1582
|
-
|
|
1587
|
+
// The hub page, never the share link: the share page has no Ready button.
|
|
1588
|
+
next: `in Draft: ask the user to read it and mark it Ready at ${api.baseUrl()}/hub/g/${guide.meta.id}, or run \`passalong ready ${guide.meta.id}\``,
|
|
1583
1589
|
}
|
|
1584
1590
|
: {}),
|
|
1585
1591
|
});
|
|
@@ -1677,11 +1683,7 @@ export function buildServer() {
|
|
|
1677
1683
|
{ title: title || "", environment: environment || "", issues },
|
|
1678
1684
|
{ cwd: cwd || process.cwd(), to },
|
|
1679
1685
|
);
|
|
1680
|
-
return json(
|
|
1681
|
-
...filed,
|
|
1682
|
-
// Filed is not sent: every bug is a draft until a person reads it.
|
|
1683
|
-
next: "all in Draft: ask the user to read them, then run `passalong ready --all`",
|
|
1684
|
-
});
|
|
1686
|
+
return json(filed);
|
|
1685
1687
|
} catch (err) {
|
|
1686
1688
|
return fail(err);
|
|
1687
1689
|
}
|
package/src/passalong.js
CHANGED
|
@@ -78,12 +78,6 @@ export async function share(markdown, { cwd = process.cwd(), to, follows } = {})
|
|
|
78
78
|
];
|
|
79
79
|
if (errors.length)
|
|
80
80
|
throw new PassalongError(`guide is not ready to share:\n - ${errors.join("\n - ")}`);
|
|
81
|
-
// Every guide is a draft when it is first shared, whatever its kind: Draft is where it waits
|
|
82
|
-
// until a person says it is ready — `ready()` — and an agent that writes one over MCP comes
|
|
83
|
-
// through here too, so publishing on share would let it reach people with nobody reading it.
|
|
84
|
-
// Sharing *is* putting it in Draft, so a new guide that names no status says so here; one already
|
|
85
|
-
// published keeps what its document says.
|
|
86
|
-
if (!guide.meta.status) guide = { ...guide, meta: { ...guide.meta, status: "draft" } };
|
|
87
81
|
|
|
88
82
|
// The server is where a guide lives; nothing is kept on this machine.
|
|
89
83
|
needsSync("sharing a guide");
|
|
@@ -270,7 +264,7 @@ export async function tasks() {
|
|
|
270
264
|
}
|
|
271
265
|
|
|
272
266
|
/**
|
|
273
|
-
* Take the next task for this worktree's repo
|
|
267
|
+
* Take the next task for this worktree's repo — always a new one, since an agent may hold several.
|
|
274
268
|
* `null` when there is nothing to do. Written to `.passalong/<id>.md` only when `save` asks.
|
|
275
269
|
*/
|
|
276
270
|
export async function nextTask({ cwd = process.cwd(), any = false, save = false } = {}) {
|
|
@@ -548,7 +542,7 @@ export async function ask(id, question, { cwd = process.cwd() } = {}) {
|
|
|
548
542
|
|
|
549
543
|
/**
|
|
550
544
|
* What this worktree's agent holds right now, and what is waiting for it: what the session-start
|
|
551
|
-
* and stop hooks read (src/hooks.js). `held`
|
|
545
|
+
* and stop hooks read (src/hooks.js). `held` lists everything it holds, empty when nothing. Counts are best effort
|
|
552
546
|
* — a hook must never fail a session over a count.
|
|
553
547
|
*/
|
|
554
548
|
export async function now({ cwd = process.cwd() } = {}) {
|
|
@@ -563,8 +557,7 @@ export async function now({ cwd = process.cwd() } = {}) {
|
|
|
563
557
|
api.handedIn(),
|
|
564
558
|
]);
|
|
565
559
|
if (w.status === "rejected") throw w.reason;
|
|
566
|
-
const held =
|
|
567
|
-
(who.agent && w.value.working.find((x) => x.agent === who.agent && x.by?.you)) || null;
|
|
560
|
+
const held = who.agent ? w.value.working.filter((x) => x.agent === who.agent && x.by?.you) : [];
|
|
568
561
|
const tasks = t.status === "fulfilled" ? t.value.tasks : [];
|
|
569
562
|
const waiting = {
|
|
570
563
|
ready: tasks.filter((x) => x.state === "ready" && x.target === who.repo).length,
|
|
@@ -583,6 +576,14 @@ export async function now({ cwd = process.cwd() } = {}) {
|
|
|
583
576
|
return { held, waiting, needs: review + handed + waiting.inbox, agent: who.agent };
|
|
584
577
|
}
|
|
585
578
|
|
|
579
|
+
/** Finished work back to To do, with why (docs/REVIEW.md §4). */
|
|
580
|
+
export async function reopen(id, why) {
|
|
581
|
+
needsSync("reopening work");
|
|
582
|
+
if (!String(why || "").trim())
|
|
583
|
+
throw new PassalongError("say why: whoever takes it next reads it");
|
|
584
|
+
return api.reopen(await resolveId(id), why);
|
|
585
|
+
}
|
|
586
|
+
|
|
586
587
|
/**
|
|
587
588
|
* The gate, a person's three answers to a task. Approve: the work in review is done. Reject: it
|
|
588
589
|
* goes back to Ready with `why` on it for the next agent. Release: a task an agent holds, live or
|
|
@@ -1046,10 +1047,13 @@ export async function planTasks(steps, { cwd = process.cwd() } = {}) {
|
|
|
1046
1047
|
].join("\n");
|
|
1047
1048
|
// The summary is the step's own, said by whoever planned it: it is what a person reads about the
|
|
1048
1049
|
// task, and this builds the document without guessing one (the server refuses a task without).
|
|
1050
|
+
// A plan is the case Draft is for: several tasks at once, which a person reads before any of
|
|
1051
|
+
// them runs. Stated here, by the code that knows it is building one.
|
|
1049
1052
|
const meta = {
|
|
1050
1053
|
title: step.title,
|
|
1051
1054
|
summary: step.summary,
|
|
1052
1055
|
kind: "task",
|
|
1056
|
+
status: "draft",
|
|
1053
1057
|
target_context: step.target_context ?? repo,
|
|
1054
1058
|
};
|
|
1055
1059
|
const after = (step.after || []).map((j) => ids[j]);
|
|
@@ -1094,9 +1098,8 @@ export function workPrompt(t) {
|
|
|
1094
1098
|
* hand_in land on the claim this took. `agent` is a command the prompt is appended to;
|
|
1095
1099
|
* `claude -p` by default, or whatever PASSALONG_AGENT says.
|
|
1096
1100
|
*
|
|
1097
|
-
* An agent that exits without finishing stops the loop
|
|
1098
|
-
*
|
|
1099
|
-
* forever. The task stays claimed here, for the next run to resume or a person to release.
|
|
1101
|
+
* An agent that exits without finishing stops the loop, rather than piling up half-done work. The
|
|
1102
|
+
* task stays claimed here, for a person to see on the board and resume (take it) or release.
|
|
1100
1103
|
*/
|
|
1101
1104
|
export async function work({
|
|
1102
1105
|
cwd = process.cwd(),
|
|
@@ -1117,8 +1120,9 @@ export async function work({
|
|
|
1117
1120
|
stdio: "inherit",
|
|
1118
1121
|
env: { ...process.env, PASSALONG_TASK: t.id },
|
|
1119
1122
|
});
|
|
1123
|
+
// Finishing is final, so a finished task is Done; "review" is a server from before that.
|
|
1120
1124
|
const state = (await tasks()).find((x) => x.id === t.id)?.state;
|
|
1121
|
-
if (state !== "review")
|
|
1125
|
+
if (state !== "done" && state !== "review")
|
|
1122
1126
|
return {
|
|
1123
1127
|
finished,
|
|
1124
1128
|
stopped: "unfinished",
|
package/src/update.js
CHANGED
|
@@ -15,7 +15,31 @@
|
|
|
15
15
|
// feature would have shipped dead while looking fine in review. So the line is read from a file and
|
|
16
16
|
// the file is refreshed by a process nobody waits for. What a person sees is a day old at worst,
|
|
17
17
|
// which is the right age for "there is a new version".
|
|
18
|
-
|
|
18
|
+
//
|
|
19
|
+
// The answer is kept in its own file, never in config.json. The refresh runs beside whatever
|
|
20
|
+
// command started it, and config.json is read, patched and written whole: a refresh that read it
|
|
21
|
+
// just before `logout` or `use` wrote it, and wrote it back after, put the old accounts and the old
|
|
22
|
+
// default back. This file has one writer, so there is nothing to lose.
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { HOME } from "./store.js";
|
|
26
|
+
|
|
27
|
+
const FILE = join(HOME, "update.json");
|
|
28
|
+
|
|
29
|
+
function readUpdate() {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(FILE, "utf8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeUpdate(held) {
|
|
38
|
+
if (!existsSync(HOME)) mkdirSync(HOME, { recursive: true, mode: 0o700 });
|
|
39
|
+
const tmp = `${FILE}.${process.pid}.tmp`;
|
|
40
|
+
writeFileSync(tmp, `${JSON.stringify(held)}\n`, { mode: 0o600 });
|
|
41
|
+
renameSync(tmp, FILE);
|
|
42
|
+
}
|
|
19
43
|
|
|
20
44
|
/** How long a cached answer lasts before a refresh is worth starting. */
|
|
21
45
|
export const CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -70,7 +94,7 @@ export function cachedNotice({
|
|
|
70
94
|
current,
|
|
71
95
|
now = Date.now(),
|
|
72
96
|
env = process.env,
|
|
73
|
-
config =
|
|
97
|
+
config = readUpdate,
|
|
74
98
|
}) {
|
|
75
99
|
if (env.PASSALONG_NO_UPDATE_CHECK === "1") return { notice: "", stale: false };
|
|
76
100
|
const held = config();
|
|
@@ -90,7 +114,7 @@ export function cachedNotice({
|
|
|
90
114
|
* trying again — `cachedNotice` reads an empty `update_latest` as a failure and uses the shorter
|
|
91
115
|
* gap.
|
|
92
116
|
*/
|
|
93
|
-
export async function refresh({ now = Date.now(), fetchImpl = fetch, save =
|
|
117
|
+
export async function refresh({ now = Date.now(), fetchImpl = fetch, save = writeUpdate } = {}) {
|
|
94
118
|
let latest = "";
|
|
95
119
|
try {
|
|
96
120
|
const res = await fetchImpl(REGISTRY, {
|