passalong 1.2.0 → 1.3.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 -2
- package/bin/passalong +14 -5
- package/package.json +1 -1
- package/src/passalong.js +70 -29
- package/src/runner.js +83 -7
package/README.md
CHANGED
|
@@ -52,8 +52,9 @@ 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
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
|
|
56
|
-
|
|
55
|
+
to run alone, up to 3 at once, each in its own worktree, finished
|
|
56
|
+
as a pull request; each agent's steps show in the hub. When Claude's
|
|
57
|
+
usage runs out it pauses, and carries on once it is back
|
|
57
58
|
passalong now what this worktree holds and what is waiting
|
|
58
59
|
passalong hub open the hub in the browser
|
|
59
60
|
passalong export [dir] every piece of work as plain markdown
|
package/bin/passalong
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
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
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
|
|
26
|
+
// up to 3 at once, each in its own worktree and branch, finished as a pull request
|
|
27
27
|
// passalong hub open the hub in the browser
|
|
28
28
|
// passalong export [dir] dump every piece of work as plain markdown (default ./passalong-export)
|
|
29
29
|
// passalong admin create <email> make an account that runs Passalong (owner only)
|
|
@@ -271,7 +271,7 @@ async function runner(f) {
|
|
|
271
271
|
onStart: ({ repos, skipped }) => {
|
|
272
272
|
console.error(
|
|
273
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
|
|
274
|
+
"It takes work marked safe to run alone, up to 3 at once, and finishes each " +
|
|
275
275
|
"as a pull request. You can watch each agent here and on the work's page in the hub. " +
|
|
276
276
|
"Stop it with Ctrl-C: agents running then stop too.",
|
|
277
277
|
);
|
|
@@ -291,12 +291,21 @@ async function runner(f) {
|
|
|
291
291
|
);
|
|
292
292
|
idle = true;
|
|
293
293
|
},
|
|
294
|
-
|
|
295
|
-
console.error(
|
|
294
|
+
onPaused: (t, { until }) =>
|
|
295
|
+
console.error(
|
|
296
|
+
`‖ ${t.id} paused: Claude's usage ran out. ` +
|
|
297
|
+
(until
|
|
298
|
+
? `It carries on at ${new Date(until).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`
|
|
299
|
+
: "It tries again every 15 minutes") +
|
|
300
|
+
", and takes nothing new until then.",
|
|
301
|
+
),
|
|
296
302
|
onFinished: (t, { branch, log }) => console.error(`✓ ${t.id} finished on ${branch} (log: ${log})`),
|
|
297
303
|
onWaiting: (t, { question }) =>
|
|
298
304
|
console.error(`? ${t.id} is waiting on you: "${question.split("\n")[0]}" (answer it in the hub)`),
|
|
299
|
-
onResumed: (t
|
|
305
|
+
onResumed: (t, { afterPause }) =>
|
|
306
|
+
console.error(
|
|
307
|
+
`\n→ ${t.id} ${t.title} (${afterPause ? "carrying on where it stopped" : "answered: carrying on"})`,
|
|
308
|
+
),
|
|
300
309
|
onPassed: (t, { dir, log }) =>
|
|
301
310
|
console.error(`✗ ${t.id} went back to its author, unfinished. Its worktree: ${dir} (log: ${log})`),
|
|
302
311
|
onTrouble: (err, t) => console.error(`! ${t ? `${t.id}: ` : ""}${err?.message || err}`),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "passalong",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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/passalong.js
CHANGED
|
@@ -776,8 +776,12 @@ export async function work({
|
|
|
776
776
|
/**
|
|
777
777
|
* The runner (src/runner.js, docs/LOOP.md §1a): work a person ticked Safe to run alone, each piece in
|
|
778
778
|
* a git worktree of its own beside its checkout, on its own branch, by an agent that finishes with
|
|
779
|
-
* a pull request. Up to `atOnce` at a time
|
|
780
|
-
* passes it back to its author. Runs until stopped; `sleep` is how the tests stop it.
|
|
779
|
+
* a pull request. Up to `atOnce` at a time, with no count a day; a run that ends without the work
|
|
780
|
+
* done passes it back to its author. Runs until stopped; `sleep` is how the tests stop it.
|
|
781
|
+
*
|
|
782
|
+
* When the agent's usage runs out (runner.usageOut) its work is not given back: it stays held here
|
|
783
|
+
* with its worktree, the runner takes nothing new, and once the usage is back (when the agent said,
|
|
784
|
+
* or every `USAGE_RETRY_MS`) it carries on where it stopped.
|
|
781
785
|
*
|
|
782
786
|
* Started inside a checkout it serves that repo. Started in a folder of checkouts it serves every
|
|
783
787
|
* one with an `origin` remote, so one terminal does what six used to. The folder is checked before
|
|
@@ -793,7 +797,6 @@ export async function watch({
|
|
|
793
797
|
agent: command,
|
|
794
798
|
every = 60,
|
|
795
799
|
atOnce = runner.LIMITS.atOnce,
|
|
796
|
-
perDay = runner.LIMITS.perDay,
|
|
797
800
|
minutes = runner.LIMITS.minutes,
|
|
798
801
|
cli,
|
|
799
802
|
git = runner.git,
|
|
@@ -805,7 +808,7 @@ export async function watch({
|
|
|
805
808
|
onTask = () => {},
|
|
806
809
|
onStep = () => {},
|
|
807
810
|
onIdle = () => {},
|
|
808
|
-
|
|
811
|
+
onPaused = () => {},
|
|
809
812
|
onFinished = () => {},
|
|
810
813
|
onPassed = () => {},
|
|
811
814
|
onWaiting = () => {},
|
|
@@ -850,7 +853,7 @@ export async function watch({
|
|
|
850
853
|
repos,
|
|
851
854
|
running: [...onNow].map(([id, title]) => ({ id, title })),
|
|
852
855
|
runs_today: runner.runsToday(countFile, runner.dayOf(now())),
|
|
853
|
-
per_day:
|
|
856
|
+
per_day: 0,
|
|
854
857
|
at_once: atOnce,
|
|
855
858
|
stopped,
|
|
856
859
|
}),
|
|
@@ -937,7 +940,8 @@ export async function watch({
|
|
|
937
940
|
say();
|
|
938
941
|
return {
|
|
939
942
|
run: finish(t, k, dir, branch).finally(() => {
|
|
940
|
-
|
|
943
|
+
// A paused run is still on: the hub keeps showing it, with its last step saying why.
|
|
944
|
+
if (!paused.has(t.id)) onNow.delete(t.id);
|
|
941
945
|
say();
|
|
942
946
|
}),
|
|
943
947
|
};
|
|
@@ -957,16 +961,21 @@ export async function watch({
|
|
|
957
961
|
onStep(t, text);
|
|
958
962
|
};
|
|
959
963
|
step(
|
|
960
|
-
resumed
|
|
961
|
-
? `Carrying on
|
|
962
|
-
:
|
|
964
|
+
resumed === "usage"
|
|
965
|
+
? `Carrying on now that Claude's usage is back, on ${name}`
|
|
966
|
+
: resumed
|
|
967
|
+
? `Carrying on after your answer, on ${name}`
|
|
968
|
+
: `Started on ${name} in ${k.repo}, on branch ${branch}`,
|
|
963
969
|
);
|
|
964
970
|
const timer = setInterval(flush, stepEvery * 1000);
|
|
965
971
|
const ran = await runAgent({
|
|
966
972
|
...agentRun,
|
|
967
|
-
prompt:
|
|
968
|
-
|
|
969
|
-
|
|
973
|
+
prompt:
|
|
974
|
+
resumed === "usage"
|
|
975
|
+
? runner.pausedPrompt(t, branch, { forge: k.forge })
|
|
976
|
+
: resumed
|
|
977
|
+
? runner.resumePrompt(t, branch, { forge: k.forge })
|
|
978
|
+
: runner.runnerPrompt(t, branch, { forge: k.forge }),
|
|
970
979
|
cwd: dir,
|
|
971
980
|
log,
|
|
972
981
|
minutes,
|
|
@@ -988,6 +997,16 @@ export async function watch({
|
|
|
988
997
|
}
|
|
989
998
|
// The last step says how the run ended, while the work is still held here: once it is passed
|
|
990
999
|
// back, its steps are no longer this runner's to write.
|
|
1000
|
+
// Its usage ran out: not given up. It stays held here, and carries on once the usage is back.
|
|
1001
|
+
if (ran.usage && held.state !== "done" && !held.claim?.asking) {
|
|
1002
|
+
pauseUntil = Math.max(pauseUntil, ran.usage.until || now() + runner.USAGE_RETRY_MS);
|
|
1003
|
+
const when = ran.usage.until ? `at ${runner.clockOf(pauseUntil)}` : "when it's back";
|
|
1004
|
+
step(`Paused: Claude's usage ran out. It carries on here ${when}, with nothing lost.`);
|
|
1005
|
+
await flush();
|
|
1006
|
+
paused.set(t.id, { t, k, dir, branch });
|
|
1007
|
+
onPaused(t, { until: ran.usage.until ? pauseUntil : 0 });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
991
1010
|
if (held.state !== "done" && !held.claim?.asking)
|
|
992
1011
|
step("Gave it back unfinished, with the reason on the work. Nothing it changed is lost.");
|
|
993
1012
|
if (held.claim?.asking) step("Waiting for your answer, then it carries on");
|
|
@@ -1025,16 +1044,19 @@ export async function watch({
|
|
|
1025
1044
|
const running = new Set();
|
|
1026
1045
|
/** Runs stopped on a question, by work id: their checkout, worktree and branch, to resume in. */
|
|
1027
1046
|
const waiting = new Map();
|
|
1047
|
+
/** Runs stopped because the agent's usage ran out, the same way: they carry on at `pauseUntil`. */
|
|
1048
|
+
const paused = new Map();
|
|
1049
|
+
let pauseUntil = 0;
|
|
1028
1050
|
/** Start an agent again where it stopped: the same worktree, branch and claim. */
|
|
1029
|
-
function resume(t, k, dir, branch) {
|
|
1051
|
+
function resume(t, k, dir, branch, { afterPause = false } = {}) {
|
|
1030
1052
|
runner.countRun(countFile, runner.dayOf(now()));
|
|
1031
1053
|
onNow.set(t.id, t.title || t.id);
|
|
1032
|
-
onResumed(t, { dir, branch });
|
|
1054
|
+
onResumed(t, { dir, branch, afterPause });
|
|
1033
1055
|
say();
|
|
1034
|
-
const run = finish(t, k, dir, branch, { resumed: true })
|
|
1056
|
+
const run = finish(t, k, dir, branch, { resumed: afterPause ? "usage" : true })
|
|
1035
1057
|
.catch((err) => onTrouble(err, t))
|
|
1036
1058
|
.finally(() => {
|
|
1037
|
-
onNow.delete(t.id);
|
|
1059
|
+
if (!paused.has(t.id)) onNow.delete(t.id);
|
|
1038
1060
|
running.delete(run);
|
|
1039
1061
|
say();
|
|
1040
1062
|
});
|
|
@@ -1069,15 +1091,15 @@ export async function watch({
|
|
|
1069
1091
|
continue;
|
|
1070
1092
|
}
|
|
1071
1093
|
if (row.claim.asking) continue;
|
|
1072
|
-
if (running.size >= atOnce) return;
|
|
1073
|
-
if (runner.runsToday(countFile, runner.dayOf(now())) >= perDay) return;
|
|
1094
|
+
if (running.size >= atOnce || paused.size) return;
|
|
1074
1095
|
waiting.delete(id);
|
|
1075
1096
|
resume(w.t, w.k, w.dir, w.branch);
|
|
1076
1097
|
}
|
|
1077
1098
|
}
|
|
1078
1099
|
|
|
1079
|
-
// A runner started again finds
|
|
1080
|
-
//
|
|
1100
|
+
// A runner started again finds what its earlier runs left held: their worktrees are still beside
|
|
1101
|
+
// their checkouts. One waiting on a question waits on; any other (paused when the usage ran out,
|
|
1102
|
+
// or stopped with the runner) carries on straight away.
|
|
1081
1103
|
try {
|
|
1082
1104
|
for (const row of (await api.tasks()).tasks) {
|
|
1083
1105
|
const leaf = basename(row.claim?.worktree || "");
|
|
@@ -1089,21 +1111,40 @@ export async function watch({
|
|
|
1089
1111
|
try {
|
|
1090
1112
|
branch = git(["-C", dir, "branch", "--show-current"]) || branch;
|
|
1091
1113
|
} catch {}
|
|
1092
|
-
|
|
1114
|
+
(row.claim?.asking ? waiting : paused).set(row.id, {
|
|
1115
|
+
t: { id: row.id, title: row.title, markdown: "" },
|
|
1116
|
+
k,
|
|
1117
|
+
dir,
|
|
1118
|
+
branch,
|
|
1119
|
+
});
|
|
1093
1120
|
}
|
|
1094
1121
|
} catch {}
|
|
1095
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* While the agent's usage is out: keep each paused run's hold alive (an empty report of steps
|
|
1125
|
+
* renews it, so the hub doesn't call it gone quiet), and once the time comes, carry them on. If
|
|
1126
|
+
* the usage is still out they pause again at once, which costs nothing.
|
|
1127
|
+
*/
|
|
1128
|
+
async function carryOnPaused() {
|
|
1129
|
+
if (!paused.size) return;
|
|
1130
|
+
if (now() < pauseUntil) {
|
|
1131
|
+
for (const id of paused.keys()) await postSteps(id, []).catch(() => {});
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
pauseUntil = 0;
|
|
1135
|
+
for (const [id, p] of [...paused]) {
|
|
1136
|
+
if (running.size >= atOnce) break;
|
|
1137
|
+
paused.delete(id);
|
|
1138
|
+
resume(p.t, p.k, p.dir, p.branch, { afterPause: true });
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1096
1142
|
let n = 0;
|
|
1097
|
-
let cappedOn = "";
|
|
1098
1143
|
for (;;) {
|
|
1144
|
+
await carryOnPaused();
|
|
1099
1145
|
await resumeAnswered();
|
|
1100
|
-
while
|
|
1101
|
-
|
|
1102
|
-
if (runner.runsToday(countFile, day) >= perDay) {
|
|
1103
|
-
if (cappedOn !== day) onCapped(perDay);
|
|
1104
|
-
cappedOn = day;
|
|
1105
|
-
break;
|
|
1106
|
-
}
|
|
1146
|
+
// Nothing new while the usage is out: it would only start and stop at once.
|
|
1147
|
+
while (running.size < atOnce && !paused.size && now() >= pauseUntil) {
|
|
1107
1148
|
let slot;
|
|
1108
1149
|
try {
|
|
1109
1150
|
slot = await startOne(++n);
|
package/src/runner.js
CHANGED
|
@@ -23,8 +23,16 @@ import {
|
|
|
23
23
|
import { hostname, platform } from "node:os";
|
|
24
24
|
import { basename, dirname, join, relative } from "node:path";
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* How many agents at once, and how long one run may take. There is no count of runs a day: it was
|
|
28
|
+
* ten, and a loop that gave the same work back over and over spent all ten on nothing, so the
|
|
29
|
+
* runner sat idle with work waiting (2026-11-06). What limits a runner now is the agent's own usage
|
|
30
|
+
* (`usageOut`: it pauses until that comes back) and the work the Passalong plan lets you keep.
|
|
31
|
+
*/
|
|
32
|
+
export const LIMITS = { atOnce: 3, minutes: 60 };
|
|
33
|
+
|
|
34
|
+
/** How long a runner waits to try again when its agent's usage ran out and it didn't say until when. */
|
|
35
|
+
export const USAGE_RETRY_MS = 15 * 60 * 1000;
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
38
|
* What an unattended agent may do. `dontAsk` refuses anything not listed here, since there is
|
|
@@ -174,13 +182,31 @@ export function resumePrompt(t, branch = branchOf(t.id), opts = {}) {
|
|
|
174
182
|
].join("\n");
|
|
175
183
|
}
|
|
176
184
|
|
|
185
|
+
/**
|
|
186
|
+
* What an agent is told when its run starts again after it stopped part-way, because Claude's usage
|
|
187
|
+
* ran out or the runner was stopped: the work is still held, its changes are in this worktree.
|
|
188
|
+
*/
|
|
189
|
+
export function pausedPrompt(t, branch = branchOf(t.id), opts = {}) {
|
|
190
|
+
return [
|
|
191
|
+
`You hold passalong work ${t.id} (${t.title || t.id}). An earlier run on it stopped before it`,
|
|
192
|
+
"finished, because Claude's usage ran out or the runner was stopped. Call the passalong MCP tool",
|
|
193
|
+
`take with id ${t.id} first to read the work again. Then look at what is already done in this`,
|
|
194
|
+
"worktree (git status, git log), carry on from there, and finish as before:",
|
|
195
|
+
"",
|
|
196
|
+
runnerPrompt({ ...t, markdown: "" }, branch, opts)
|
|
197
|
+
.split("\n")
|
|
198
|
+
.slice(4)
|
|
199
|
+
.join("\n"),
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
177
203
|
/** Today, as the runner counts days: the machine's own date. */
|
|
178
204
|
export const dayOf = (now) => {
|
|
179
205
|
const d = new Date(now);
|
|
180
206
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
181
207
|
};
|
|
182
208
|
|
|
183
|
-
/** How many runs have started today, kept in `file` beside the rest of the CLI's state. */
|
|
209
|
+
/** How many runs have started today, kept in `file` beside the rest of the CLI's state: shown, not a limit. */
|
|
184
210
|
export function runsToday(file, day) {
|
|
185
211
|
try {
|
|
186
212
|
const s = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -238,6 +264,42 @@ export function sentences(text, max) {
|
|
|
238
264
|
return `${(space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
239
265
|
}
|
|
240
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Whether what an agent said means its usage ran out (Claude Code's plan limit, or the API refusing
|
|
269
|
+
* for rate or credit), and when it comes back: `{ until }` in ms, 0 when it didn't say. `null` when
|
|
270
|
+
* it means something else. Only Claude Code's own phrasings, in short messages: an agent that ran
|
|
271
|
+
* out says so in a line, and one that added "a daily limit" to someone's app must not pause.
|
|
272
|
+
*/
|
|
273
|
+
export function usageOut(text, now = Date.now()) {
|
|
274
|
+
const s = String(text ?? "").trim();
|
|
275
|
+
if (!s || s.length > 400) return null;
|
|
276
|
+
if (
|
|
277
|
+
!/usage limit reached|hit your (usage |session |weekly )?limit|(session|weekly|5-hour|five-hour|opus|sonnet) limit reached|out of (extra )?usage|rate_limit_error|credit balance is too low/i.test(
|
|
278
|
+
s,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
return null;
|
|
282
|
+
// "Claude AI usage limit reached|1736789000": when it resets, in seconds.
|
|
283
|
+
const epoch = s.match(/\|(\d{10})\b/);
|
|
284
|
+
if (epoch) return { until: Number(epoch[1]) * 1000 };
|
|
285
|
+
// "resets 3pm", "resets at 5:30 pm": the next time the clock says that, on this machine.
|
|
286
|
+
const at = s.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
|
|
287
|
+
if (at) {
|
|
288
|
+
let h = Number(at[1]) % 12;
|
|
289
|
+
if (!at[3]) h = Number(at[1]) % 24;
|
|
290
|
+
else if (at[3].toLowerCase() === "pm") h += 12;
|
|
291
|
+
const d = new Date(now);
|
|
292
|
+
d.setHours(h, Number(at[2] || 0), 0, 0);
|
|
293
|
+
if (d.getTime() <= now) d.setDate(d.getDate() + 1);
|
|
294
|
+
return { until: d.getTime() };
|
|
295
|
+
}
|
|
296
|
+
return { until: 0 };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** "3:00 pm", for saying when a paused runner carries on. */
|
|
300
|
+
export const clockOf = (ms) =>
|
|
301
|
+
new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
302
|
+
|
|
241
303
|
/** The last line an agent printed, from its log: often the reason it stopped. "" when none. */
|
|
242
304
|
export function lastLine(log) {
|
|
243
305
|
try {
|
|
@@ -257,7 +319,7 @@ export function git(args) {
|
|
|
257
319
|
* Start an agent and wait for it. What it prints is read a line at a time: each becomes a line of
|
|
258
320
|
* `log` a person can read (Claude Code's JSON events made into sentences, anything else as it came),
|
|
259
321
|
* and each thing it does is handed to `onStep`. Killed after `minutes`. Resolves
|
|
260
|
-
* `{ exit, timedOut }`, never rejects: an agent that would not start is a run that ended without
|
|
322
|
+
* `{ exit, timedOut, said, usage }`, never rejects: an agent that would not start is a run that ended without
|
|
261
323
|
* the work done.
|
|
262
324
|
*/
|
|
263
325
|
export function runAgent({
|
|
@@ -276,7 +338,9 @@ export function runAgent({
|
|
|
276
338
|
let child;
|
|
277
339
|
/** What the agent said at the end (Claude Code's result), for why it gave the work back. */
|
|
278
340
|
let said = "";
|
|
279
|
-
|
|
341
|
+
/** Set when it said its usage ran out (usageOut): the run is paused, not given up. */
|
|
342
|
+
let usage = null;
|
|
343
|
+
const done = (r) => resolve({ ...r, said, usage });
|
|
280
344
|
const write = (text) => {
|
|
281
345
|
try {
|
|
282
346
|
appendFileSync(log, `${clock()} ${text}\n`);
|
|
@@ -302,6 +366,7 @@ export function runAgent({
|
|
|
302
366
|
if (!line.trim()) continue;
|
|
303
367
|
const d = describe(line, cwd);
|
|
304
368
|
if (d.said) said = d.said;
|
|
369
|
+
usage = d.usage || usage;
|
|
305
370
|
if (d.log) write(d.log);
|
|
306
371
|
for (const step of d.step ? d.step.split("\n") : []) onStep(step);
|
|
307
372
|
}
|
|
@@ -406,12 +471,18 @@ export function describe(line, cwd = "") {
|
|
|
406
471
|
e = JSON.parse(line);
|
|
407
472
|
} catch {
|
|
408
473
|
const text = hideSecrets(one(line, 300));
|
|
409
|
-
return { log: text, step: text };
|
|
474
|
+
return { log: text, step: text, usage: usageOut(line) };
|
|
410
475
|
}
|
|
411
476
|
if (!e || typeof e !== "object") return { log: hideSecrets(one(line, 300)), step: "" };
|
|
412
477
|
if (e.type === "system" && e.subtype === "init")
|
|
413
478
|
return { log: `started${e.model ? ` (${e.model})` : ""}`, step: "" };
|
|
414
479
|
if (e.type === "assistant") {
|
|
480
|
+
// Claude Code writes the refusal as a message of its own, marked with what went wrong.
|
|
481
|
+
if (["rate_limit", "billing_error"].includes(e.error)) {
|
|
482
|
+
const said = (e.message?.content ?? []).map((c) => c.text || "").join(" ");
|
|
483
|
+
const text = hideSecrets(one(said || "Claude's usage ran out", 200));
|
|
484
|
+
return { log: text, step: "", usage: usageOut(said) || { until: 0 } };
|
|
485
|
+
}
|
|
415
486
|
const logs = [];
|
|
416
487
|
const steps = [];
|
|
417
488
|
for (const c of e.message?.content ?? []) {
|
|
@@ -439,7 +510,12 @@ export function describe(line, cwd = "") {
|
|
|
439
510
|
const how = e.is_error ? "Stopped with an error" : "Finished";
|
|
440
511
|
const text = hideSecrets(`${how}${e.result ? `: ${one(e.result, 200)}` : ""}`);
|
|
441
512
|
// Its whole last message, too: when the work comes back unfinished, this is why.
|
|
442
|
-
return {
|
|
513
|
+
return {
|
|
514
|
+
log: text,
|
|
515
|
+
step: text,
|
|
516
|
+
said: hideSecrets(String(e.result || "").trim()),
|
|
517
|
+
usage: usageOut(e.result),
|
|
518
|
+
};
|
|
443
519
|
}
|
|
444
520
|
return { log: "", step: "" };
|
|
445
521
|
}
|