muse-crew 0.17.2 → 0.17.3
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/API.md +11 -0
- package/docs/decisions/composition-machinery.md +180 -0
- package/docs/decisions/publish-path.md +6 -0
- package/docs/decisions/workflow-core.md +5 -4
- package/lib/AGENTS.md +2 -1
- package/lib/bugfix/phases/build.js +168 -0
- package/lib/bugfix/phases/capture.js +170 -0
- package/lib/bugfix/phases/integrate.js +165 -0
- package/lib/bugfix/phases/map.js +129 -0
- package/lib/bugfix/phases/publish.js +592 -0
- package/lib/bugfix/phases/qa.js +356 -0
- package/lib/bugfix/phases/reproduce.js +254 -0
- package/lib/bugfix/phases/review.js +292 -0
- package/lib/bugfix/phases/triage.js +69 -0
- package/lib/chore/CONTRACT.md +181 -0
- package/lib/chore/DISPOSITION.md +98 -0
- package/lib/chore/extract.js +204 -0
- package/lib/chore/phase-lib.js +885 -0
- package/lib/chore/phases/build.js +110 -0
- package/lib/chore/phases/capture.js +82 -0
- package/lib/chore/phases/integrate.js +109 -0
- package/lib/chore/phases/map.js +81 -0
- package/lib/chore/phases/publish.js +543 -0
- package/lib/chore/phases/review.js +255 -0
- package/lib/chore/phases/triage.js +57 -0
- package/lib/chore/prompts/evidence-gatherer.js +41 -0
- package/lib/chore/prompts/evidence-gatherer.schema.json +1 -0
- package/lib/chore/prompts/tool-check.js +15 -0
- package/lib/chore/prompts/trailers.js +56 -0
- package/lib/chore/prompts/verdict-reask.js +28 -0
- package/lib/chore/prompts/verdict-reask.schema.json +1 -0
- package/lib/chore/prompts/work-agent.js +52 -0
- package/lib/chore/prompts/work-agent.schema.json +1 -0
- package/lib/chore/spawn-keys.js +44 -0
- package/lib/chore/spawn-vocab.js +87 -0
- package/lib/chore-run.js +538 -0
- package/lib/chore-tick.js +289 -0
- package/lib/crew-api.js +273 -0
- package/lib/crew-dispatch-worker.js +27 -7
- package/lib/crew-release.sh +7 -2
- package/lib/extract.js +252 -0
- package/lib/prompts/tool-check.js +18 -0
- package/lib/prompts/trailers.js +59 -0
- package/lib/prompts/verdict-reask.js +31 -0
- package/lib/prompts/verdict-reask.schema.json +1 -0
- package/lib/prompts/work-agent.js +56 -0
- package/lib/prompts/work-agent.schema.json +1 -0
- package/lib/reap-spawns.js +407 -0
- package/lib/schema.sql +12 -1
- package/lib/spawn-keys.js +47 -0
- package/lib/spawn-step.js +572 -0
- package/lib/standard/phases/build.js +120 -0
- package/lib/standard/phases/capture.js +163 -0
- package/lib/standard/phases/integrate.js +172 -0
- package/lib/standard/phases/map.js +119 -0
- package/lib/standard/phases/publish.js +565 -0
- package/lib/standard/phases/qa.js +399 -0
- package/lib/standard/phases/review.js +281 -0
- package/lib/standard/phases/triage.js +64 -0
- package/lib/test-detached-integrate.sh +47 -0
- package/lib/workflow-driver.js +605 -0
- package/lib/workflow-lib.js +1012 -0
- package/lib/workflow-spec.js +187 -0
- package/lib/worktree-lifecycle.sh +55 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +61 -9
- package/workflows/bugfix.js +17 -17
- package/workflows/chore.js +16 -16
- package/workflows/docs.js +14 -11
- package/workflows/standard.js +16 -16
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// lib/workflow-spec.js — workflow spec loader/validator (composition
|
|
2
|
+
// machinery, Phase B, 2026-09-26). Import-safe ESM: no side effects on
|
|
3
|
+
// import, bare `node` exits 0.
|
|
4
|
+
//
|
|
5
|
+
// A workflow spec is JSON declaring COMPOSITION: the ordered phases, the
|
|
6
|
+
// named non-sequential transitions (rework loops), and guard references.
|
|
7
|
+
// Phases stay code — the spec names the module, never the implementation.
|
|
8
|
+
// The future composition UI edits specs; the generic driver
|
|
9
|
+
// (lib/workflow-driver.js) runs them.
|
|
10
|
+
//
|
|
11
|
+
// Spec shape:
|
|
12
|
+
// {
|
|
13
|
+
// "workflow": "chore",
|
|
14
|
+
// "phases": [
|
|
15
|
+
// { "name": "Triage",
|
|
16
|
+
// "module": "../lib/chore/phases/triage.js",
|
|
17
|
+
// "contract": "Reads: ... Writes: ..." }
|
|
18
|
+
// ],
|
|
19
|
+
// "transitions": [
|
|
20
|
+
// { "from": "Review", "on": "rework", "to": "Build" }
|
|
21
|
+
// ],
|
|
22
|
+
// "guards": [],
|
|
23
|
+
// "rejectedResume": "Build"
|
|
24
|
+
// }
|
|
25
|
+
//
|
|
26
|
+
// Module refs resolve against the spec file's own directory (so the spec
|
|
27
|
+
// is self-contained and relocatable). Validation fails closed: any
|
|
28
|
+
// malformed spec throws with a clear reason — the driver never runs a
|
|
29
|
+
// half-understood workflow. No wall-clock reads, no randomness.
|
|
30
|
+
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import { dirname, resolve } from "node:path";
|
|
33
|
+
import { pathToFileURL } from "node:url";
|
|
34
|
+
|
|
35
|
+
function fail(msg) { throw new Error("workflow spec: " + msg); }
|
|
36
|
+
|
|
37
|
+
function isObj(v) { return v !== null && typeof v === "object" && !Array.isArray(v); }
|
|
38
|
+
|
|
39
|
+
// Closed schema: unknown keys are almost certainly typos (a misspelled
|
|
40
|
+
// "rejectedResume" must not silently load). Fail closed on them.
|
|
41
|
+
function rejectUnknown(obj, known, where) {
|
|
42
|
+
for (var k in obj) {
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(obj, k) && known.indexOf(k) === -1) {
|
|
44
|
+
fail(where + ": unknown key \"" + k + "\" — must be one of: " + known.join(", ") + ".");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// validateSpec — pure: takes a parsed value, returns the normalized spec
|
|
50
|
+
// or throws. Normalized: { workflow, phases, phaseNames, phaseIndex,
|
|
51
|
+
// transitions, guards, rejectedResume, projectDescFallback, promptsDir }.
|
|
52
|
+
// projectDescFallback/promptsDir are optional workflow-declared runtime
|
|
53
|
+
// values: the generic driver threads them into the run environment so a
|
|
54
|
+
// workflow keeps its own project description fallback and prompt-template
|
|
55
|
+
// directory without workflow-shaped conditionals in the driver.
|
|
56
|
+
export function validateSpec(spec) {
|
|
57
|
+
if (!isObj(spec)) fail("top level must be an object.");
|
|
58
|
+
rejectUnknown(spec, ["workflow", "phases", "transitions", "guards", "rejectedResume", "projectDescFallback", "promptsDir", "reworkPhases"], "spec");
|
|
59
|
+
if (typeof spec.workflow !== "string" || spec.workflow.trim() === "")
|
|
60
|
+
fail("\"workflow\" must be a non-empty string.");
|
|
61
|
+
if (!Array.isArray(spec.phases) || spec.phases.length === 0)
|
|
62
|
+
fail("\"phases\" must be a non-empty array.");
|
|
63
|
+
var phases = [];
|
|
64
|
+
var seen = {};
|
|
65
|
+
for (var i = 0; i < spec.phases.length; i++) {
|
|
66
|
+
var p = spec.phases[i];
|
|
67
|
+
if (!isObj(p)) fail("phases[" + i + "] must be an object.");
|
|
68
|
+
rejectUnknown(p, ["name", "module", "contract"], "phases[" + i + "]");
|
|
69
|
+
if (typeof p.name !== "string" || p.name.trim() === "")
|
|
70
|
+
fail("phases[" + i + "].name must be a non-empty string.");
|
|
71
|
+
if (seen[p.name]) fail("duplicate phase name \"" + p.name + "\".");
|
|
72
|
+
seen[p.name] = true;
|
|
73
|
+
if (typeof p.module !== "string" || p.module.trim() === "")
|
|
74
|
+
fail("phases[" + i + "].module must be a non-empty string.");
|
|
75
|
+
phases.push({
|
|
76
|
+
name: p.name,
|
|
77
|
+
module: p.module,
|
|
78
|
+
contract: typeof p.contract === "string" ? p.contract : "",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
var transitions = [];
|
|
82
|
+
if (spec.transitions !== undefined) {
|
|
83
|
+
if (!Array.isArray(spec.transitions)) fail("\"transitions\" must be an array.");
|
|
84
|
+
for (var t = 0; t < spec.transitions.length; t++) {
|
|
85
|
+
var tr = spec.transitions[t];
|
|
86
|
+
if (!isObj(tr)) fail("transitions[" + t + "] must be an object.");
|
|
87
|
+
rejectUnknown(tr, ["from", "to", "on"], "transitions[" + t + "]");
|
|
88
|
+
if (typeof tr.from !== "string" || !seen[tr.from])
|
|
89
|
+
fail("transitions[" + t + "].from must name a declared phase.");
|
|
90
|
+
if (typeof tr.to !== "string" || !seen[tr.to])
|
|
91
|
+
fail("transitions[" + t + "].to must name a declared phase.");
|
|
92
|
+
if (tr.on !== undefined && (typeof tr.on !== "string" || tr.on.trim() === ""))
|
|
93
|
+
fail("transitions[" + t + "].on must be a non-empty string when present.");
|
|
94
|
+
transitions.push({ from: tr.from, to: tr.to, on: tr.on || null });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var guards = [];
|
|
98
|
+
if (spec.guards !== undefined) {
|
|
99
|
+
if (!Array.isArray(spec.guards)) fail("\"guards\" must be an array.");
|
|
100
|
+
for (var g = 0; g < spec.guards.length; g++) {
|
|
101
|
+
if (typeof spec.guards[g] !== "string" || spec.guards[g].trim() === "")
|
|
102
|
+
fail("guards[" + g + "] must be a non-empty string.");
|
|
103
|
+
guards.push(spec.guards[g]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
var phaseNames = phases.map(function (p) { return p.name; });
|
|
107
|
+
var phaseIndex = {};
|
|
108
|
+
for (var n = 0; n < phaseNames.length; n++) phaseIndex[phaseNames[n]] = n;
|
|
109
|
+
// rejectedResume — optional: the phase a rejected latest session resumes
|
|
110
|
+
// at (rework entry point; workflow knowledge, so spec-declared).
|
|
111
|
+
var rejectedResume = null;
|
|
112
|
+
if (spec.rejectedResume !== undefined) {
|
|
113
|
+
if (typeof spec.rejectedResume !== "string" || !seen[spec.rejectedResume])
|
|
114
|
+
fail("\"rejectedResume\" must name a declared phase.");
|
|
115
|
+
rejectedResume = spec.rejectedResume;
|
|
116
|
+
}
|
|
117
|
+
// projectDescFallback — optional: the project-description fallback the
|
|
118
|
+
// driver threads into deriveRunEnv (chore's dashboard description; other
|
|
119
|
+
// workflows leave it empty).
|
|
120
|
+
var projectDescFallback = "";
|
|
121
|
+
if (spec.projectDescFallback !== undefined) {
|
|
122
|
+
if (typeof spec.projectDescFallback !== "string")
|
|
123
|
+
fail("\"projectDescFallback\" must be a string when present.");
|
|
124
|
+
projectDescFallback = spec.projectDescFallback;
|
|
125
|
+
}
|
|
126
|
+
// promptsDir — optional: the prompt-template directory the driver threads
|
|
127
|
+
// into deriveRunEnv, resolved relative to the spec file's directory by the
|
|
128
|
+
// driver (phaseModuleUrl's rule). Unset keeps the run-lib default.
|
|
129
|
+
var promptsDir = null;
|
|
130
|
+
if (spec.promptsDir !== undefined) {
|
|
131
|
+
if (typeof spec.promptsDir !== "string" || spec.promptsDir.trim() === "")
|
|
132
|
+
fail("\"promptsDir\" must be a non-empty string when present.");
|
|
133
|
+
promptsDir = spec.promptsDir;
|
|
134
|
+
}
|
|
135
|
+
// reworkPhases — optional: the phases whose rejections consume the shared
|
|
136
|
+
// rework budget. When declared, deriveReworkCount counts rejected sessions
|
|
137
|
+
// for exactly these phases; when absent, it counts every rejected event
|
|
138
|
+
// (the pre-spec behavior Standard and Chore rely on). Bugfix declares
|
|
139
|
+
// ["Review", "QA"] — its Build and Reproduce failures record "rejected"
|
|
140
|
+
// but must not spend the shared Review/QA budget.
|
|
141
|
+
var reworkPhases = null;
|
|
142
|
+
if (spec.reworkPhases !== undefined) {
|
|
143
|
+
if (!Array.isArray(spec.reworkPhases) || spec.reworkPhases.length === 0)
|
|
144
|
+
fail("\"reworkPhases\" must be a non-empty array when present.");
|
|
145
|
+
for (var r = 0; r < spec.reworkPhases.length; r++) {
|
|
146
|
+
if (typeof spec.reworkPhases[r] !== "string" || !seen[spec.reworkPhases[r]])
|
|
147
|
+
fail("\"reworkPhases[" + r + "]\" must name a declared phase.");
|
|
148
|
+
}
|
|
149
|
+
reworkPhases = spec.reworkPhases.slice();
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
workflow: spec.workflow,
|
|
153
|
+
phases: phases,
|
|
154
|
+
phaseNames: phaseNames,
|
|
155
|
+
phaseIndex: phaseIndex,
|
|
156
|
+
transitions: transitions,
|
|
157
|
+
guards: guards,
|
|
158
|
+
rejectedResume: rejectedResume,
|
|
159
|
+
projectDescFallback: projectDescFallback,
|
|
160
|
+
promptsDir: promptsDir,
|
|
161
|
+
reworkPhases: reworkPhases,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// loadSpec — read + JSON.parse + validate. IO and syntax errors fail
|
|
166
|
+
// closed with the spec path named.
|
|
167
|
+
export function loadSpec(specPath) {
|
|
168
|
+
var text;
|
|
169
|
+
try {
|
|
170
|
+
text = readFileSync(specPath, "utf8");
|
|
171
|
+
} catch (e) {
|
|
172
|
+
fail("cannot read " + specPath + ": " + (e && e.message ? e.message : String(e)));
|
|
173
|
+
}
|
|
174
|
+
var raw;
|
|
175
|
+
try {
|
|
176
|
+
raw = JSON.parse(text);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
fail(specPath + " is not valid JSON: " + (e && e.message ? e.message : String(e)));
|
|
179
|
+
}
|
|
180
|
+
return validateSpec(raw);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// phaseModuleUrl — resolve a phase's module ref against the spec file's
|
|
184
|
+
// directory, as a file: URL string for dynamic import(). Pure path logic.
|
|
185
|
+
export function phaseModuleUrl(specPath, moduleRef) {
|
|
186
|
+
return pathToFileURL(resolve(dirname(resolve(specPath)), moduleRef)).href;
|
|
187
|
+
}
|
|
@@ -422,6 +422,26 @@ acquire_merge_lock() {
|
|
|
422
422
|
done
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
# ff_integrated(branch, target): print the branch's current tip when it is
|
|
426
|
+
# EXACTLY the integration target's tip — the branch was pushed by
|
|
427
|
+
# fast-forward (or a prior recovery pushed it), so no merge commit exists
|
|
428
|
+
# but the line carries precisely the branch's present state. Prints
|
|
429
|
+
# nothing and returns 1 otherwise. Exact tip equality is the only shape
|
|
430
|
+
# recognized: a branch reset backward after integration can remain an
|
|
431
|
+
# ancestor of the target, so ancestry alone would defeat blocker-35's
|
|
432
|
+
# fail-closed protection (fixture F3: backward rework after a recorded
|
|
433
|
+
# merge). Anything less certain than tip == target tip stays fail-closed.
|
|
434
|
+
ff_integrated() {
|
|
435
|
+
local branch="$1" target="$2" tip target_tip
|
|
436
|
+
tip=$(git rev-parse --verify "$branch" 2>/dev/null) || return 1
|
|
437
|
+
target_tip=$(git rev-parse --verify "$target" 2>/dev/null) || return 1
|
|
438
|
+
if [ "$tip" = "$target_tip" ]; then
|
|
439
|
+
printf '%s' "$tip"
|
|
440
|
+
return 0
|
|
441
|
+
fi
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
|
|
425
445
|
# current_branch_merge(branch): print the newest merge commit on live HEAD's
|
|
426
446
|
# first-parent chain whose branch-side parent (^2) is the branch's CURRENT
|
|
427
447
|
# tip — i.e. the merge that brought the branch's present state onto the
|
|
@@ -597,7 +617,28 @@ cmd_integrate() {
|
|
|
597
617
|
echo "LOCK: held by $task_id — release after deploy with post-deploy"
|
|
598
618
|
return 0
|
|
599
619
|
fi
|
|
620
|
+
# Fast-forward recovery (2026-09-27): no merge commit exists, but the
|
|
621
|
+
# branch tip IS the target tip (pushed by fast-forward in a prior
|
|
622
|
+
# attempt) — ff_integrated recognizes only exact tip equality, so
|
|
623
|
+
# blocker-35's fail-closed protection is preserved (a branch moved
|
|
624
|
+
# backward after integration is still an ancestor of the target, and
|
|
625
|
+
# ancestry alone is not accepted). No merge record is appended:
|
|
626
|
+
# scan_merge_records skips non-merge commits, and the ff state is
|
|
627
|
+
# self-describing (tip == target tip).
|
|
600
628
|
if [ -n "$recorded_commit" ]; then
|
|
629
|
+
local ff_tip
|
|
630
|
+
ff_tip=$(ff_integrated "$branch" "$target" 2>/dev/null) || ff_tip=""
|
|
631
|
+
if [ -n "$ff_tip" ]; then
|
|
632
|
+
acquire_merge_lock "$task_id" || exit 2
|
|
633
|
+
if do_push "$task_id"; then
|
|
634
|
+
echo "MERGED: $ff_tip (recovered: branch tip fast-forwarded onto $target in a prior attempt; push completed now)"
|
|
635
|
+
else
|
|
636
|
+
echo "ERROR: push recovery failed for fast-forwarded branch tip $ff_tip"
|
|
637
|
+
return 1
|
|
638
|
+
fi
|
|
639
|
+
echo "LOCK: held by $task_id — release after deploy with post-deploy"
|
|
640
|
+
return 0
|
|
641
|
+
fi
|
|
601
642
|
echo "STALE_MERGE: recorded merge $recorded_commit no longer corresponds to $branch (tip $(git rev-parse --verify "$branch" 2>/dev/null)) — no merge of the current tip is on the live line; refusing to push possibly-stale state"
|
|
602
643
|
return 1
|
|
603
644
|
fi
|
|
@@ -1221,12 +1262,19 @@ cmd_push_target() {
|
|
|
1221
1262
|
# branch's current tip was merged onto the live line before pushing.
|
|
1222
1263
|
# Branch gone (merge-lease reclaim): no rework could have moved it, so
|
|
1223
1264
|
# the recorded merge on the line is still the deliverable.
|
|
1224
|
-
local branch live_merge recorded_commit
|
|
1265
|
+
local branch live_merge recorded_commit ff_tip
|
|
1225
1266
|
recorded_commit=$(grep "^merge_commit=" "$record_file" | tail -1 | cut -d= -f2-)
|
|
1226
1267
|
live_merge=""
|
|
1268
|
+
ff_tip=""
|
|
1227
1269
|
if branch=$(resolve_branch "$task_id" 2>/dev/null); then
|
|
1228
1270
|
if git rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
1229
1271
|
live_merge=$(current_branch_merge "$branch") || live_merge=""
|
|
1272
|
+
if [ -z "$live_merge" ]; then
|
|
1273
|
+
# Fast-forward case (2026-09-27): no merge commit exists, but the
|
|
1274
|
+
# branch tip is exactly the target tip — ff_integrated accepts only
|
|
1275
|
+
# tip equality, so blocker-35's fail-closed protection is preserved.
|
|
1276
|
+
ff_tip=$(ff_integrated "$branch" "HEAD" 2>/dev/null) || ff_tip=""
|
|
1277
|
+
fi
|
|
1230
1278
|
elif [ -n "$recorded_commit" ] && git merge-base --is-ancestor "$recorded_commit" "$(git rev-parse HEAD 2>/dev/null)" 2>/dev/null; then
|
|
1231
1279
|
live_merge="$recorded_commit"
|
|
1232
1280
|
fi
|
|
@@ -1234,11 +1282,15 @@ cmd_push_target() {
|
|
|
1234
1282
|
echo "ERROR: cannot resolve task branch for $task_id — not pushing without a verifiable branch"
|
|
1235
1283
|
return 1
|
|
1236
1284
|
fi
|
|
1237
|
-
if [ -z "$live_merge" ]; then
|
|
1285
|
+
if [ -z "$live_merge" ] && [ -z "$ff_tip" ]; then
|
|
1238
1286
|
echo "STALE_MERGE: no merge of the current ${branch:-unknown} tip is on the live line (recorded: ${recorded_commit:-none}) — the task branch changed since the recorded merge; refusing to push possibly-stale state"
|
|
1239
1287
|
return 1
|
|
1240
1288
|
fi
|
|
1241
|
-
|
|
1289
|
+
if [ -n "$ff_tip" ]; then
|
|
1290
|
+
echo "PUSH_IDENTITY_OK: branch $branch tip $ff_tip is on the live line via fast-forward — pushing"
|
|
1291
|
+
else
|
|
1292
|
+
echo "PUSH_IDENTITY_OK: pushing merge $live_merge carrying the current $branch tip"
|
|
1293
|
+
fi
|
|
1242
1294
|
|
|
1243
1295
|
do_push "$task_id"
|
|
1244
1296
|
return $?
|
package/package.json
CHANGED
|
@@ -39,19 +39,38 @@ The failures below are settled and recorded in the Gate 1 OODA state. The author
|
|
|
39
39
|
- Route each completed-run failure through the same two commands — `record-platform-failure`, then `retry-platform-failure` — exactly as above.
|
|
40
40
|
- Log the results. This ensures transient platform failures don't strand tasks.
|
|
41
41
|
|
|
42
|
+
0b. **Reap worker-layer spawn rows (cutover piece):** the worker-layer
|
|
43
|
+
Chore driver leaves `worker_runs` rows `running` when its tick dies
|
|
44
|
+
mid-spawn. Reap them before dispatch so a dead tick never strands a
|
|
45
|
+
spawn past its timeout. Run in shell:
|
|
46
|
+
`node {crewHome}/lib/reap-spawns.js --crew-home {crewHome}`
|
|
47
|
+
The last stdout line is one JSON object
|
|
48
|
+
`{closed:[...], kill_instructions:[...], errors:[]}`; log it verbatim.
|
|
49
|
+
- `closed` entries were finished by the reaper itself — no action.
|
|
50
|
+
- For each entry in `kill_instructions`: attempt the runtime close of
|
|
51
|
+
the named child (subagent handle or OS pid, as the instruction
|
|
52
|
+
specifies), then record the attempt in shell:
|
|
53
|
+
`node {crewHome}/lib/reap-spawns.js --crew-home {crewHome} --record-close --id <row_id> --result <killed|not_found|refused> [--detail <text>]`
|
|
54
|
+
Log the `{recorded:...}` result verbatim. A `refused` close leaves
|
|
55
|
+
the row for the kill-and-retry path — never force it.
|
|
56
|
+
- `errors` = log only; a reaper error never aborts the tick.
|
|
57
|
+
(Inert until worker spawn rows exist — pre-cutover this always returns
|
|
58
|
+
empty lists.)
|
|
59
|
+
|
|
42
60
|
1. **Load tools:** Call tool_search_load_tool_namespace with paths ["workflow_launch"].
|
|
43
61
|
|
|
44
|
-
2. **
|
|
62
|
+
2. **Check the workflow registry:** Read the file "{crewHome}/workflows/registry.json" with the read tool and confirm it is valid JSON — do NOT hand-transcribe it anywhere (room #26: 15+ launch rejections 09-19→09-21 from garbled nested-object args, and one hallucinated registry that passed validation). The Step-3 dispatcher reads this file itself; you only verify it exists and parses. If the read FAILS on a file that exists (transient read error — observed 2026-09-13; the file itself was healthy and later ticks read it fine), retry the read once; if it still fails, write the error text into your run summary (observability — never silently swallow a failed read) and continue — the dispatcher will log its own registry warning.
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
`node {crewHome}/lib/crew-dispatch-worker.js --crew-home {crewHome} --
|
|
48
|
-
The last stdout line is
|
|
64
|
+
3. **Run the dispatcher (worker-layer, authoritative — cutover 2026-09-26):** the dispatcher is `lib/crew-dispatch-worker.js`, run in shell — NOT the sandboxed `workflows/crew-dispatch.js` (retired; two dispatchers would double-dispatch). Run:
|
|
65
|
+
`node {crewHome}/lib/crew-dispatch-worker.js --crew-home {crewHome} --authoritative`
|
|
66
|
+
The last stdout line is one JSON object (`{status, claims, ...}`); lines before it are the decision log. Read the `claims` array from the parsed JSON — do NOT transcribe claims by hand. (The dispatcher reads `{crewHome}/workflows/registry.json` itself; never pass the registry on the command line.)
|
|
49
67
|
|
|
50
|
-
|
|
68
|
+
It reads the crew's task state, determines eligibility, reserves tasks through the atomic `reserve-dispatch` (a live reservation wins; the loser stands down), acknowledges the poll, and writes the tick-release and dispatch-decision logs. Every claim carries `executor: "worker"` and the launch contract (`task_id`, `workflow`, `step`, `scriptPath`, `args`).
|
|
51
69
|
|
|
52
|
-
|
|
70
|
+
If the script exits non-zero, log the exit code and the stderr tail, then CONTINUE to Step 4.4 with zero claims — a failed dispatch means no launches this tick, but the intent scan, ack scan, and evidence steps still run. Do NOT fall back to the sandboxed dispatcher.
|
|
53
71
|
|
|
54
72
|
4. **Launch claims autonomously:** Extract the `claims` array from the dispatcher result. For each claim (up to 3 per tick — if more than 3, launch the first 3 and log the rest as deferred):
|
|
73
|
+
- **Routing (cutover piece):** if `claim.workflow === "chore"` AND `claim.executor === "worker"`, do NOT launch it here — follow Step 4w (worker-layer Chore) instead. Every other claim follows this step unchanged. (Every claim now carries `executor: "worker"` — the worker-layer dispatcher is the sole launch authority since the 2026-09-26 cutover.)
|
|
55
74
|
- Call workflow_launch_async with scriptPath={claim.scriptPath} and args={claim.args}. Record the returned run_id.
|
|
56
75
|
- Immediately acknowledge the dispatch reservation with the real run_id (the dispatcher already created a placeholder reservation — this updates it). Run in shell:
|
|
57
76
|
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} acknowledge-dispatch-run --json '{"task_id": "{claim.task_id}", "run_id": "<run_id>"}'`
|
|
@@ -60,6 +79,42 @@ The failures below are settled and recorded in the Gate 1 OODA state. The author
|
|
|
60
79
|
|
|
61
80
|
If the dispatcher returned no claims or the claims array is empty, log NO_DISPATCH and CONTINUE to Step 4.4 — do NOT exit. Intent scan (4.4), ack scan (4.5) and evidence (6) run independently of dispatch claims. A no-claims tick must still issue parked publish intents, scan for acknowledgements, and deliver evidence. (Fixed 2026-09-14: the old "exit on NO_DISPATCH" skipped publication permanently.)
|
|
62
81
|
|
|
82
|
+
4w. **Worker-layer Chore claim (cutover piece — inert until a dispatcher
|
|
83
|
+
tags a chore claim `executor: "worker"`):** the worker-layer driver
|
|
84
|
+
(`lib/chore-run.js`) replaces `workflow_launch_async` for this claim.
|
|
85
|
+
You are the ferry: the driver decides, the bridge posts, you spawn.
|
|
86
|
+
The driver claims the task itself through the atomic `claim-task`
|
|
87
|
+
(claim-as-gate) — you do not claim, and you do not check the claim
|
|
88
|
+
first. If the sandbox dispatcher (or another tick) won the race, the
|
|
89
|
+
ferry returns `done` with outcome `stood-down`: log it and move on;
|
|
90
|
+
the task is already owned.
|
|
91
|
+
- Tick sequence: the `tick_seq` of the dispatch decision from this
|
|
92
|
+
tick's Step 3 — the last line of
|
|
93
|
+
`{crewHome}/.dispatch-decisions.jsonl`. Use it verbatim (no
|
|
94
|
+
arithmetic); it must be a positive integer. It becomes the spawn
|
|
95
|
+
rows' `owner_tick_seq`. If the file is missing or the last line
|
|
96
|
+
carries no `tick_seq`, stop and log — do not invent a sequence.
|
|
97
|
+
- Invoke the ferry in shell:
|
|
98
|
+
`node {crewHome}/lib/chore-tick.js --crew-home {crewHome} --task-id "<claim.task_id>" --tick-seq <N> --project-id "<claim.args.project_id>" [--visual-protocol|--no-visual-protocol from claim.args.visual_protocol] [--resolved-workflow <claim.args.resolved_workflow>] [--workflow-was-null when claim.args.workflow_was_null]`
|
|
99
|
+
Copy flag values verbatim from the claim's `args` — never re-derive them.
|
|
100
|
+
- Read the ferry's single stdout JSON line (`action`):
|
|
101
|
+
- `"spawn"`: perform the spawn ritual —
|
|
102
|
+
1. Spawn ONE subagent with `spawn.prompt` as its full instructions.
|
|
103
|
+
Convey the working directory (`spawn.cwd`) and environment
|
|
104
|
+
(`spawn.env`) in the spawn — if your spawn mechanism takes only
|
|
105
|
+
a message, state them explicitly at the top of the instructions;
|
|
106
|
+
never let the agent guess where to work.
|
|
107
|
+
2. Immediately record `child: {"kind":"subagent","agent_id":"<id>"}` into the existing session file at `spawn.session_path` (read-modify-write: preserve its fields, add `child`).
|
|
108
|
+
3. Wait up to `spawn.timeout_ms` for the agent's final report (the
|
|
109
|
+
report arrives asynchronously — do not poll; if the timeout
|
|
110
|
+
elapses first, treat it as a timeout). Update the session file: `outcome: "returned"` + `text` = the final report verbatim. On timeout: stop the agent if possible (your agent-close mechanism), then `outcome: "timeout"` (+ `kill_evidence: {"verified_dead": true, "method": "<how>", "detail": "<what>"}` only when a machine liveness check proved it dead — an unconfirmed kill leaves the row running for the Step-0b reaper, by design). On spawn failure: `outcome: "spawn_error"` + `error`.
|
|
111
|
+
4. Re-invoke the ferry with `--session "<spawn.session_path>"` appended. Repeat from the action read.
|
|
112
|
+
- `"wait"`: log `reason`; stop driving this task this tick. The next tick resumes (re-invocation is safe — a running spawn yields WAIT again; a lost claim race yields done/stood-down).
|
|
113
|
+
- `"done"`: log `outcome` (+ `reason`); stop. The driver already wrote the terminal task state.
|
|
114
|
+
- `"error"`: log `reason`; stop. Do not retry in a tight loop — the next tick retries.
|
|
115
|
+
- Loop within the tick until `wait`/`done`/`error`. The ferry enforces its own frame budget (20 frames; exhaustion yields `wait` with reason `frame-budget-exhausted` — never done/failed). If the tick's 90-minute ceiling (Step 5) is nearly exhausted, stop looping — the next tick resumes. Budgets end the tick's participation, never the task.
|
|
116
|
+
- Never build prompts, never write the ledger except through the ferry, never interpret agent output — `spawn-step.js post` classifies it.
|
|
117
|
+
|
|
63
118
|
4.4. **Publish intent scan (0.14.6):** the workflow parks at Publish with a version-carrying publish-intent ledger entry; issuance belongs to the tick. Deterministic code owns every transition; you are the ferry (scan → issue → record). Run this BEFORE Step 4.5 so an issued edit's ack window is open to the acknowledgement scan on a later tick. Log every list the scan returns.
|
|
64
119
|
- Scan (code): `node {crewHome}/lib/crew-api.js --crew-home {crewHome} scan-publish-intent`
|
|
65
120
|
This atomically claims parked publish intents for this tick (1-hour lease, so a second tick cannot double-issue). It returns `{ intent: [...], skipped: [...], requeued: [...] }`. Each intent entry carries `task_id`, `commit`, `attempt`, `slug`, `repo_path`, `project_id`, `ledger_path`, `base`, `diff_path`, `diff_sha256`, `version` (`<commit>:<attempt>`), `files` (changed paths), `claimed_at`, `claim_expiry`, `reclaimed` (true when a previous tick died mid-issuance). Copy the entry's fields verbatim — never re-derive them.
|
|
@@ -111,9 +166,6 @@ The failures below are settled and recorded in the Gate 1 OODA state. The author
|
|
|
111
166
|
- `reissued` = fresh intents for the next attempt — a 30-minute window expired with no acknowledgement, so code re-issued with a fresh version (by derivation), immediately claimable (no backoff). This tick does NOT issue them; the NEXT tick's `scan-publish-intent` claims them. Log only.
|
|
112
167
|
- `skipped` = window still open or evidence absent — log only, take no action.
|
|
113
168
|
|
|
114
|
-
4.6. **Shadow verdict (Piece 1, 2026-09-26):** after the Step-3 dispatcher's decision is recorded, pair its decision with the Step-2.5 shadow evidence. Run in shell:
|
|
115
|
-
`node {crewHome}/lib/compare-dispatch-shadow.js --crew-home {crewHome}`
|
|
116
|
-
Log every `SHADOW_*` line verbatim. `SHADOW_MATCH` means the worker layer agreed with the sandbox on this tick's claims. `SHADOW_DIVERGE` names the differing claim triples (`task_id|workflow|step`) — log them; divergence is evidence for the cutover review, not a tick failure, so continue the tick. `SHADOW_UNPAIRED` means the authoritative decision line is missing (the Step-3 dispatcher died after the shadow ran) — log it loudly and continue. `SHADOW_PENDING` means the decision hasn't been written yet; the shadow stays pending and a later tick retries the pairing — log and continue. `SHADOW_NO_EVIDENCE` on the first shadowed tick is normal. Verdicts append to `{crewHome}/.dispatch-shadow-verdicts.jsonl`; the cutover decision after ~100-200 ticks is made from that file, never from a single tick.
|
|
117
169
|
- Never stamp provenance from prose. Never infer a verdict from an inspector's summary text. The exact version string is the sole positive signal.
|
|
118
170
|
|
|
119
171
|
5. **Monitor launched workflows until terminal (stay-alive — 2026-09-13):** The platform ties async workflow `agent()` authorization to the launcher's lifetime: if THIS tick ends while a workflow is still running, the workflow's next `agent()` call fails with "subagent bootstrap is no longer authorized" / "subagent reservation owner is terminal". Prevention beats recovery here, so this tick is configured with a 90-minute execution timeout (`timeout_secs: 5400` in seed/crons.json) and you MUST stay alive until every launched run reaches a terminal state. Do not exit early while a launched run is still `running` — your death is what kills it.
|
package/workflows/bugfix.js
CHANGED
|
@@ -300,13 +300,17 @@ var TOOL_CHECK_PREAMBLE =
|
|
|
300
300
|
"Then do the assignment below.\n\n";
|
|
301
301
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
302
302
|
// reason: "discarded" (the runtime threw the output away — the JSON-candidate
|
|
303
|
-
// scan found {...}-shaped fragments it could not parse), "
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
// reported
|
|
303
|
+
// scan found {...}-shaped fragments it could not parse), "errored" (the
|
|
304
|
+
// attempt errored — not a brace discard), "empty" (agent() returned
|
|
305
|
+
// without throwing but produced nothing usable), "no-tools" (the
|
|
306
|
+
// worker's TOOL CHECK reported artifact_tools: missing), or
|
|
307
|
+
// "no-transport" (the worker's TOOL CHECK reported shell_transport:
|
|
308
|
+
// unavailable).
|
|
307
309
|
// The trailer tells the retry what to expect, not just to try again.
|
|
308
310
|
var why = reason === "empty"
|
|
309
311
|
? "your previous attempt returned no usable output"
|
|
312
|
+
: reason === "errored"
|
|
313
|
+
? "your previous attempt errored before producing a usable report (not a brace discard — diagnose the error and report the true state in plain prose)"
|
|
310
314
|
: reason === "no-tools"
|
|
311
315
|
? "your previous attempt's TOOL CHECK reported artifact_tools: missing (this is a fresh launch, so run the TOOL CHECK's load step again before the work)"
|
|
312
316
|
: reason === "no-transport"
|
|
@@ -474,12 +478,11 @@ function describeWorkAgentFailure(stepName, identity, attempts) {
|
|
|
474
478
|
// Honest classification of a work-agent call that yielded no usable
|
|
475
479
|
// report, with the per-attempt evidence preserved in the session notes.
|
|
476
480
|
// Two distinct cases:
|
|
477
|
-
// - agent() THREW: the runtime discarded output it could not
|
|
478
|
-
// (
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
// and the claim is removed.)
|
|
481
|
+
// - agent() THREW: either the runtime discarded output it could not
|
|
482
|
+
// machine-read (prose tripping the JSON-candidate heuristic), or the
|
|
483
|
+
// agent errored before producing a report. The raw output is gone —
|
|
484
|
+
// the workflow never received it — so the surviving error text is
|
|
485
|
+
// recorded here instead.
|
|
483
486
|
// - agent() RETURNED EMPTY without throwing: the child produced nothing
|
|
484
487
|
// usable. Each attempt's outcome is the evidence.
|
|
485
488
|
// attempts: [{threw, error, outcome}, ...], in order.
|
|
@@ -943,7 +946,7 @@ let alreadyMergedSha = null;
|
|
|
943
946
|
// verified sha and no `repo_diff: none` claim fails mechanically without
|
|
944
947
|
// dispatching Cass; reviewer prose is never parsed for git identity.
|
|
945
948
|
let branchState = null; // "has-work" | "already-merged:<sha>" | "empty-no-work"
|
|
946
|
-
let buildClaimedNoDiff = false; // Build
|
|
949
|
+
let buildClaimedNoDiff = false; // Build's `repo_diff: none` claim, from the same-process Build report only (no cross-process hydration).
|
|
947
950
|
let versionTouched = false; // npm: branch changed package.json's `version` (mechanical git fact, blocker-34 follow-up)
|
|
948
951
|
let branchStateErr = ""; // classifier failure detail (retry trailer + park note grounds)
|
|
949
952
|
// Deterministic publish target — computed by the workflow (registry base +
|
|
@@ -2029,13 +2032,10 @@ while (i < STEPS.length) {
|
|
|
2029
2032
|
// state agent steps were deleted — their outputs fed only the
|
|
2030
2033
|
// retired content-verdict/receipt-attribution machinery, and the
|
|
2031
2034
|
// toolcheck ran in a workflow child that cannot reach artifact_edit.
|
|
2032
|
-
// Issuance belongs to the session-carrying tick worker, which
|
|
2033
|
-
// handles tool-unavailability as inconclusive per the tick protocol.)
|
|
2034
|
-
// Publish at intent. Everything above — preflight, provenance base,
|
|
2035
|
-
// checksummed diff, toolcheck, pre-trigger baselines — is read-only.
|
|
2036
2035
|
// Issuance belongs to the session-carrying tick worker
|
|
2037
2036
|
// (scan-publish-intent's intent bucket), the only caller class that
|
|
2038
|
-
// can reach artifact_edit
|
|
2037
|
+
// can reach artifact_edit; it handles tool-unavailability as
|
|
2038
|
+
// inconclusive per the tick protocol. This entry is issuer-written BY THE
|
|
2039
2039
|
// WORKFLOW and names the intent: outcome "publish-intent", never
|
|
2040
2040
|
// "submitted" — the workflow issued nothing. The worker claims it
|
|
2041
2041
|
// via the park note below.
|
|
@@ -2341,7 +2341,7 @@ while (i < STEPS.length) {
|
|
|
2341
2341
|
for (var workAttempt = 0; workAttempt <= 2 && !mechanicalReviewFail; workAttempt++) {
|
|
2342
2342
|
var workKey = workAttempt === 0 ? workKeyBase : workRetryKey(step.name, (totalReworkCount > 0 ? "-r" + totalReworkCount : ""), workAttempt);
|
|
2343
2343
|
var prevAttempt = workAttempt === 0 ? null : workAttempts[workAttempt - 1];
|
|
2344
|
-
var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? "discarded" : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
|
|
2344
|
+
var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? (/output was not JSON/.test(prevAttempt.error || "") ? "discarded" : "errored") : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
|
|
2345
2345
|
try {
|
|
2346
2346
|
workerResult = await agent(
|
|
2347
2347
|
workPromptBase + (workAttempt === 0 ? "" : buildTransportRetryTrailer(step.name, REPO_PATH, taskId, workAttempt, retryReason)),
|
package/workflows/chore.js
CHANGED
|
@@ -310,13 +310,17 @@ var TOOL_CHECK_PREAMBLE =
|
|
|
310
310
|
"Then do the assignment below.\n\n";
|
|
311
311
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
312
312
|
// reason: "discarded" (the runtime threw the output away — the JSON-candidate
|
|
313
|
-
// scan found {...}-shaped fragments it could not parse), "
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// reported
|
|
313
|
+
// scan found {...}-shaped fragments it could not parse), "errored" (the
|
|
314
|
+
// attempt errored — not a brace discard), "empty" (agent() returned
|
|
315
|
+
// without throwing but produced nothing usable), "no-tools" (the
|
|
316
|
+
// worker's TOOL CHECK reported artifact_tools: missing), or
|
|
317
|
+
// "no-transport" (the worker's TOOL CHECK reported shell_transport:
|
|
318
|
+
// unavailable).
|
|
317
319
|
// The trailer tells the retry what to expect, not just to try again.
|
|
318
320
|
var why = reason === "empty"
|
|
319
321
|
? "your previous attempt returned no usable output"
|
|
322
|
+
: reason === "errored"
|
|
323
|
+
? "your previous attempt errored before producing a usable report (not a brace discard — diagnose the error and report the true state in plain prose)"
|
|
320
324
|
: reason === "no-tools"
|
|
321
325
|
? "your previous attempt's TOOL CHECK reported artifact_tools: missing (this is a fresh launch, so run the TOOL CHECK's load step again before the work)"
|
|
322
326
|
: reason === "no-transport"
|
|
@@ -484,12 +488,11 @@ function describeWorkAgentFailure(stepName, identity, attempts) {
|
|
|
484
488
|
// Honest classification of a work-agent call that yielded no usable
|
|
485
489
|
// report, with the per-attempt evidence preserved in the session notes.
|
|
486
490
|
// Two distinct cases:
|
|
487
|
-
// - agent() THREW: the runtime discarded output it could not
|
|
488
|
-
// (
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
// and the claim is removed.)
|
|
491
|
+
// - agent() THREW: either the runtime discarded output it could not
|
|
492
|
+
// machine-read (prose tripping the JSON-candidate heuristic), or the
|
|
493
|
+
// agent errored before producing a report. The raw output is gone —
|
|
494
|
+
// the workflow never received it — so the surviving error text is
|
|
495
|
+
// recorded here instead.
|
|
493
496
|
// - agent() RETURNED EMPTY without throwing: the child produced nothing
|
|
494
497
|
// usable. Each attempt's outcome is the evidence.
|
|
495
498
|
// attempts: [{threw, error, outcome}, ...], in order.
|
|
@@ -1823,13 +1826,10 @@ while (i < STEPS.length) {
|
|
|
1823
1826
|
// state agent steps were deleted — their outputs fed only the
|
|
1824
1827
|
// retired content-verdict/receipt-attribution machinery, and the
|
|
1825
1828
|
// toolcheck ran in a workflow child that cannot reach artifact_edit.
|
|
1826
|
-
// Issuance belongs to the session-carrying tick worker, which
|
|
1827
|
-
// handles tool-unavailability as inconclusive per the tick protocol.)
|
|
1828
|
-
// Publish at intent. Everything above — preflight, provenance base,
|
|
1829
|
-
// checksummed diff, toolcheck, pre-trigger baselines — is read-only.
|
|
1830
1829
|
// Issuance belongs to the session-carrying tick worker
|
|
1831
1830
|
// (scan-publish-intent's intent bucket), the only caller class that
|
|
1832
|
-
// can reach artifact_edit
|
|
1831
|
+
// can reach artifact_edit; it handles tool-unavailability as
|
|
1832
|
+
// inconclusive per the tick protocol. This entry is issuer-written BY THE
|
|
1833
1833
|
// WORKFLOW and names the intent: outcome "publish-intent", never
|
|
1834
1834
|
// "submitted" — the workflow issued nothing. The worker claims it
|
|
1835
1835
|
// via the park note below.
|
|
@@ -1999,7 +1999,7 @@ while (i < STEPS.length) {
|
|
|
1999
1999
|
for (var workAttempt = 0; workAttempt <= 2 && !mechanicalReviewFail; workAttempt++) {
|
|
2000
2000
|
var workKey = workAttempt === 0 ? workKeyBase : workRetryKey(step.name, (reworkCount > 0 ? "-r" + reworkCount : ""), workAttempt);
|
|
2001
2001
|
var prevAttempt = workAttempt === 0 ? null : workAttempts[workAttempt - 1];
|
|
2002
|
-
var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? "discarded" : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
|
|
2002
|
+
var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? (/output was not JSON/.test(prevAttempt.error || "") ? "discarded" : "errored") : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
|
|
2003
2003
|
try {
|
|
2004
2004
|
workerResult = await agent(
|
|
2005
2005
|
workPromptBase + (workAttempt === 0 ? "" : buildTransportRetryTrailer(step.name, REPO_PATH, taskId, workAttempt, retryReason)),
|
package/workflows/docs.js
CHANGED
|
@@ -329,13 +329,17 @@ while (i < STEPS.length) {
|
|
|
329
329
|
"Then do the assignment below.\n\n";
|
|
330
330
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
331
331
|
// reason: "discarded" (the runtime threw the output away — the JSON-candidate
|
|
332
|
-
// scan found {...}-shaped fragments it could not parse), "
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
// reported
|
|
332
|
+
// scan found {...}-shaped fragments it could not parse), "errored" (the
|
|
333
|
+
// attempt errored — not a brace discard), "empty" (agent() returned
|
|
334
|
+
// without throwing but produced nothing usable), "no-tools" (the
|
|
335
|
+
// worker's TOOL CHECK reported artifact_tools: missing), or
|
|
336
|
+
// "no-transport" (the worker's TOOL CHECK reported shell_transport:
|
|
337
|
+
// unavailable).
|
|
336
338
|
// The trailer tells the retry what to expect, not just to try again.
|
|
337
339
|
var why = reason === "empty"
|
|
338
340
|
? "your previous attempt returned no usable output"
|
|
341
|
+
: reason === "errored"
|
|
342
|
+
? "your previous attempt errored before producing a usable report (not a brace discard — diagnose the error and report the true state in plain prose)"
|
|
339
343
|
: reason === "no-tools"
|
|
340
344
|
? "your previous attempt's TOOL CHECK reported artifact_tools: missing (this is a fresh launch, so run the TOOL CHECK's load step again before the work)"
|
|
341
345
|
: reason === "no-transport"
|
|
@@ -367,12 +371,11 @@ function describeWorkAgentFailure(stepName, identity, attempts) {
|
|
|
367
371
|
// Honest classification of a work-agent call that yielded no usable
|
|
368
372
|
// report, with the per-attempt evidence preserved in the session notes.
|
|
369
373
|
// Two distinct cases:
|
|
370
|
-
// - agent() THREW: the runtime discarded output it could not
|
|
371
|
-
// (
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// and the claim is removed.)
|
|
374
|
+
// - agent() THREW: either the runtime discarded output it could not
|
|
375
|
+
// machine-read (prose tripping the JSON-candidate heuristic), or the
|
|
376
|
+
// agent errored before producing a report. The raw output is gone —
|
|
377
|
+
// the workflow never received it — so the surviving error text is
|
|
378
|
+
// recorded here instead.
|
|
376
379
|
// - agent() RETURNED EMPTY without throwing: the child produced nothing
|
|
377
380
|
// usable. Each attempt's outcome is the evidence.
|
|
378
381
|
// attempts: [{threw, error, outcome}, ...], in order.
|
|
@@ -450,7 +453,7 @@ function describeWorkAgentFailure(stepName, identity, attempts) {
|
|
|
450
453
|
for (var workAttempt = 0; workAttempt <= 2; workAttempt++) {
|
|
451
454
|
var workKey = workAttempt === 0 ? workKeyBase : workRetryKey(step.name, (reworkCount > 0 ? "-r" + reworkCount : ""), workAttempt);
|
|
452
455
|
var prevAttempt = workAttempt === 0 ? null : workAttempts[workAttempt - 1];
|
|
453
|
-
var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? "discarded" : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
|
|
456
|
+
var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? (/output was not JSON/.test(prevAttempt.error || "") ? "discarded" : "errored") : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
|
|
454
457
|
try {
|
|
455
458
|
workerResult = await agent(
|
|
456
459
|
workPromptBase + (workAttempt === 0 ? "" : buildTransportRetryTrailer(step.name, REPO_PATH, taskId, workAttempt, retryReason)),
|