passalong 0.3.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/passalong +250 -9
- package/package.json +3 -2
- package/skill/SKILL.md +8 -7
- package/src/api.js +34 -2
- package/src/capture.js +41 -1
- package/src/guide.js +73 -5
- package/src/hooks.js +126 -0
- package/src/mcp.js +421 -118
- package/src/passalong.js +446 -26
package/src/hooks.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// The Claude Code hooks `passalong setup` installs. See docs/V2.md §11.
|
|
2
|
+
//
|
|
3
|
+
// Two moments an agent forgets what it holds: when a session starts (a restart, a /clear, a
|
|
4
|
+
// compaction), and when it decides it is done. SessionStart runs `passalong now --hook session`
|
|
5
|
+
// and puts what this worktree's agent holds into the session's context. Stop runs
|
|
6
|
+
// `passalong now --hook stop` and refuses, once, to end a session that still holds work nobody
|
|
7
|
+
// has handed in or passed — the "done!" that leaves a task to stall thirty minutes later.
|
|
8
|
+
//
|
|
9
|
+
// Pure functions here; the command in bin/passalong fetches the state and prints what these say.
|
|
10
|
+
// Neither hook may ever get in the way when something is wrong: offline, signed out, a server
|
|
11
|
+
// error — each says nothing and lets the session carry on.
|
|
12
|
+
|
|
13
|
+
/** The commands, as written into settings. Matched on when merging, so setup is idempotent. */
|
|
14
|
+
export const HOOK = {
|
|
15
|
+
session: "passalong now --hook session",
|
|
16
|
+
stop: "passalong now --hook stop",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What this worktree's agent holds and what is waiting, as a few lines for an agent to read.
|
|
23
|
+
* Empty when there is nothing to say, so a session in an unrelated repo is not told anything.
|
|
24
|
+
*/
|
|
25
|
+
export function nowText({ held, waiting = {} }) {
|
|
26
|
+
if (held) {
|
|
27
|
+
const lines = [
|
|
28
|
+
`Passalong: You hold ${held.id} (${held.kind || "task"}): ${held.title || held.id}.`,
|
|
29
|
+
];
|
|
30
|
+
if (held.note) lines.push(`Last note: "${held.note}".`);
|
|
31
|
+
if (held.state === "stalled")
|
|
32
|
+
lines.push("It is marked stalled: nobody has heard from its agent in 30 minutes.");
|
|
33
|
+
lines.push(
|
|
34
|
+
`To carry on, call take ${held.id} (it resumes what you hold), then progress at each ` +
|
|
35
|
+
"milestone. When it is done, hand_in; if it is not yours or you are stuck, pass with the reason.",
|
|
36
|
+
);
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (waiting.ready)
|
|
41
|
+
parts.push(plural(waiting.ready, "ready task for this repo", "ready tasks for this repo"));
|
|
42
|
+
if (waiting.inbox)
|
|
43
|
+
parts.push(plural(waiting.inbox, "guide handed to you", "guides handed to you"));
|
|
44
|
+
if (!parts.length) return "";
|
|
45
|
+
return `Passalong: ${parts.join(" and ")}. Call take with no id to start the next one, when the user wants that.`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a session may stop. `input` is what Claude Code sends the Stop hook on stdin. Null lets
|
|
50
|
+
* it stop; an object is the hook's answer, blocking once with what to call. `stop_hook_active`
|
|
51
|
+
* means this stop is already the session continuing because of this hook, and blocking again
|
|
52
|
+
* would leave it no way to end.
|
|
53
|
+
*/
|
|
54
|
+
export function stopVerdict({ held, input = {} }) {
|
|
55
|
+
if (!held || input.stop_hook_active) return null;
|
|
56
|
+
return {
|
|
57
|
+
decision: "block",
|
|
58
|
+
reason:
|
|
59
|
+
`You still hold ${held.id}: ${held.title || held.id}. Before ending, do one of: ` +
|
|
60
|
+
`hand_in ${held.id} if it is done; pass ${held.id} with the reason if it is not yours or ` +
|
|
61
|
+
`you are stuck; or, if the user is stopping mid-way, progress ${held.id} with a note ` +
|
|
62
|
+
"saying what is left — then end.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Settings with both hooks added, and nothing else changed. A hook already there — matched on its
|
|
68
|
+
* command — is left alone, so running setup twice adds nothing. Returns a new object.
|
|
69
|
+
*/
|
|
70
|
+
export function withHooks(settings = {}) {
|
|
71
|
+
const out = structuredClone(settings);
|
|
72
|
+
out.hooks ??= {};
|
|
73
|
+
for (const [event, command] of [
|
|
74
|
+
["SessionStart", HOOK.session],
|
|
75
|
+
["Stop", HOOK.stop],
|
|
76
|
+
]) {
|
|
77
|
+
const list = (out.hooks[event] ??= []);
|
|
78
|
+
const has = list.some((m) => (m.hooks || []).some((h) => h.command === command));
|
|
79
|
+
if (!has) list.push({ hooks: [{ type: "command", command }] });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- the status line -------------------------------------------------------------------------------
|
|
85
|
+
//
|
|
86
|
+
// Claude Code draws a status line from a command's output, under the prompt, all session long. It is
|
|
87
|
+
// the one place a person sees what their agent holds without asking. The command is
|
|
88
|
+
// `passalong now --statusline`, which prints from a local cache and refreshes it in the background:
|
|
89
|
+
// Claude Code cancels a status line that is still running when the next update arrives, so a
|
|
90
|
+
// network call in the foreground would leave the line blank.
|
|
91
|
+
|
|
92
|
+
/** The status line command, as written into settings. */
|
|
93
|
+
export const STATUS_LINE = "passalong now --statusline";
|
|
94
|
+
|
|
95
|
+
/** Keep a one-line note to what fits beside everything else in a terminal footer. */
|
|
96
|
+
const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One line: what this worktree's agent holds, then what needs you and what is ready here.
|
|
100
|
+
* `needs` counts everything waiting on the person: work handed in for review, agents stuck on
|
|
101
|
+
* them, and guides handed to them.
|
|
102
|
+
*/
|
|
103
|
+
export function statusText({ held, needs = 0, ready = 0 }) {
|
|
104
|
+
const parts = [];
|
|
105
|
+
if (held) {
|
|
106
|
+
const said = held.note ? ` "${clip(held.note, 32)}"` : "";
|
|
107
|
+
parts.push(`▸ ${held.id}${held.state === "stalled" ? " (stalled)" : ""}${said}`);
|
|
108
|
+
} else parts.push("passalong");
|
|
109
|
+
if (needs) parts.push(`${needs} need${needs === 1 ? "s" : ""} you`);
|
|
110
|
+
if (ready) parts.push(`${ready} ready`);
|
|
111
|
+
if (parts.length === 1 && !held) parts.push("clear");
|
|
112
|
+
return parts.join(" · ");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Settings with the status line added, when there is none. A status line someone already has is
|
|
117
|
+
* theirs — a script with their git branch or their context meter — and setup never replaces it.
|
|
118
|
+
* `refreshInterval` keeps it current while the session sits idle.
|
|
119
|
+
*/
|
|
120
|
+
export function withStatusLine(settings = {}) {
|
|
121
|
+
if (settings.statusLine) return settings;
|
|
122
|
+
return {
|
|
123
|
+
...structuredClone(settings),
|
|
124
|
+
statusLine: { type: "command", command: STATUS_LINE, refreshInterval: 30 },
|
|
125
|
+
};
|
|
126
|
+
}
|