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.
Files changed (70) hide show
  1. package/API.md +11 -0
  2. package/docs/decisions/composition-machinery.md +180 -0
  3. package/docs/decisions/publish-path.md +6 -0
  4. package/docs/decisions/workflow-core.md +5 -4
  5. package/lib/AGENTS.md +2 -1
  6. package/lib/bugfix/phases/build.js +168 -0
  7. package/lib/bugfix/phases/capture.js +170 -0
  8. package/lib/bugfix/phases/integrate.js +165 -0
  9. package/lib/bugfix/phases/map.js +129 -0
  10. package/lib/bugfix/phases/publish.js +592 -0
  11. package/lib/bugfix/phases/qa.js +356 -0
  12. package/lib/bugfix/phases/reproduce.js +254 -0
  13. package/lib/bugfix/phases/review.js +292 -0
  14. package/lib/bugfix/phases/triage.js +69 -0
  15. package/lib/chore/CONTRACT.md +181 -0
  16. package/lib/chore/DISPOSITION.md +98 -0
  17. package/lib/chore/extract.js +204 -0
  18. package/lib/chore/phase-lib.js +885 -0
  19. package/lib/chore/phases/build.js +110 -0
  20. package/lib/chore/phases/capture.js +82 -0
  21. package/lib/chore/phases/integrate.js +109 -0
  22. package/lib/chore/phases/map.js +81 -0
  23. package/lib/chore/phases/publish.js +543 -0
  24. package/lib/chore/phases/review.js +255 -0
  25. package/lib/chore/phases/triage.js +57 -0
  26. package/lib/chore/prompts/evidence-gatherer.js +41 -0
  27. package/lib/chore/prompts/evidence-gatherer.schema.json +1 -0
  28. package/lib/chore/prompts/tool-check.js +15 -0
  29. package/lib/chore/prompts/trailers.js +56 -0
  30. package/lib/chore/prompts/verdict-reask.js +28 -0
  31. package/lib/chore/prompts/verdict-reask.schema.json +1 -0
  32. package/lib/chore/prompts/work-agent.js +52 -0
  33. package/lib/chore/prompts/work-agent.schema.json +1 -0
  34. package/lib/chore/spawn-keys.js +44 -0
  35. package/lib/chore/spawn-vocab.js +87 -0
  36. package/lib/chore-run.js +538 -0
  37. package/lib/chore-tick.js +289 -0
  38. package/lib/crew-api.js +273 -0
  39. package/lib/crew-dispatch-worker.js +27 -7
  40. package/lib/crew-release.sh +7 -2
  41. package/lib/extract.js +252 -0
  42. package/lib/prompts/tool-check.js +18 -0
  43. package/lib/prompts/trailers.js +59 -0
  44. package/lib/prompts/verdict-reask.js +31 -0
  45. package/lib/prompts/verdict-reask.schema.json +1 -0
  46. package/lib/prompts/work-agent.js +56 -0
  47. package/lib/prompts/work-agent.schema.json +1 -0
  48. package/lib/reap-spawns.js +407 -0
  49. package/lib/schema.sql +12 -1
  50. package/lib/spawn-keys.js +47 -0
  51. package/lib/spawn-step.js +572 -0
  52. package/lib/standard/phases/build.js +120 -0
  53. package/lib/standard/phases/capture.js +163 -0
  54. package/lib/standard/phases/integrate.js +172 -0
  55. package/lib/standard/phases/map.js +119 -0
  56. package/lib/standard/phases/publish.js +565 -0
  57. package/lib/standard/phases/qa.js +399 -0
  58. package/lib/standard/phases/review.js +281 -0
  59. package/lib/standard/phases/triage.js +64 -0
  60. package/lib/test-detached-integrate.sh +47 -0
  61. package/lib/workflow-driver.js +605 -0
  62. package/lib/workflow-lib.js +1012 -0
  63. package/lib/workflow-spec.js +187 -0
  64. package/lib/worktree-lifecycle.sh +55 -3
  65. package/package.json +1 -1
  66. package/seed/cron-body-template.md +61 -9
  67. package/workflows/bugfix.js +17 -17
  68. package/workflows/chore.js +16 -16
  69. package/workflows/docs.js +14 -11
  70. package/workflows/standard.js +16 -16
@@ -0,0 +1,255 @@
1
+ // lib/chore/phases/review.js — Review (Cass) phase module (sandbox exit,
2
+ // Piece 2). Import-safe: no side effects on import, bare `node` exits 0.
3
+ //
4
+ // Hybrid phase: the branch classification and npm-version checks run
5
+ // mechanically (direct execFile, in-place instrument retries) BEFORE any
6
+ // creative review. A mechanical failure uses synthetic prose through the
7
+ // same extraction path and never dispatches Cass. FAIL increments the
8
+ // durable rework count (re-derived from rejected events), records
9
+ // rejection notes, and rewinds explicitly to Build.
10
+
11
+ import {
12
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
13
+ closeoutPassed, ensureClaimed, latestSessionNotes, lifecycle, parkTask,
14
+ lifecycleEnvPrefix, log,
15
+ } from "../phase-lib.js";
16
+ import {
17
+ parseClassifyFerry, parseVersionFerry, extractVerdict,
18
+ extractReleaseDecision, releaseDecisionText, extractRepoDiffNone,
19
+ } from "../extract.js";
20
+
21
+ export const PHASE = { name: "Review", identity: "cass" };
22
+ const MAX_REWORK = 2;
23
+
24
+ // runClassifyGate — the mechanical branch classifier (blocker 34/42).
25
+ // Direct execFile, no agent ferry. UNKNOWN retries in place (up to 2);
26
+ // exhaustion parks honestly — never asserts empty-no-work.
27
+ async function runClassifyGate(env) {
28
+ var taskId = env.taskId;
29
+ var branchState = null;
30
+ var branchStateErr = "";
31
+ var classifyFailedReturn = "";
32
+ for (var classifyAttempt = 0; classifyAttempt <= 2 && branchState === null; classifyAttempt++) {
33
+ var parsed;
34
+ try {
35
+ var out = await lifecycle(env, "classify-branch", [taskId]);
36
+ classifyFailedReturn = JSON.stringify({ stdout: out.stdout, stderr: out.stderr }).slice(0, 500);
37
+ parsed = parseClassifyFerry({ stdout: out.stdout, stderr: out.stderr });
38
+ } catch (e) {
39
+ var msg = String((e && e.message) || e);
40
+ classifyFailedReturn = msg.slice(0, 500);
41
+ parsed = { state: null, diagField: null, diagText: "", failReason: "classifier call failed: " + msg.slice(0, 120) };
42
+ }
43
+ if (parsed.state !== null) {
44
+ branchState = parsed.state;
45
+ branchStateErr = "";
46
+ if (parsed.diagField !== null) {
47
+ log("classify-branch DIAG pin (" + parsed.diagField + " field): " + parsed.diagText);
48
+ } else {
49
+ log("classify-branch: no DIAG: record-scan counter returned (scan unverifiable)");
50
+ }
51
+ } else {
52
+ branchStateErr = parsed.failReason;
53
+ if (classifyAttempt < 2) {
54
+ log("classify-branch unparseable (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
55
+ }
56
+ }
57
+ }
58
+ if (branchState === null) {
59
+ return { parked: await parkTask(env, "Branch state unclassifiable after 2 instrument retries (" + branchStateErr + "; last failure: " + classifyFailedReturn + ") — the classifier instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy branch classification was produced.") };
60
+ }
61
+ var alreadyMergedSha = null;
62
+ if (branchState.indexOf("already-merged:") === 0) {
63
+ alreadyMergedSha = branchState.slice("already-merged:".length);
64
+ log("Branch state already-merged: " + alreadyMergedSha + " (DIAG pin present — tripwire satisfied) — Cass reviews the frozen merge diff");
65
+ } else {
66
+ log("Branch state: " + branchState);
67
+ }
68
+ return { branchState: branchState, branchStateErr: branchStateErr, alreadyMergedSha: alreadyMergedSha };
69
+ }
70
+
71
+ // runVersionGate — the mechanical package.json `version` check (npm
72
+ // projects only). Same in-place retry shape as the classifier.
73
+ async function runVersionGate(env) {
74
+ var taskId = env.taskId;
75
+ var versionState = null;
76
+ var versionStateErr = "";
77
+ var versionFailedReturn = "";
78
+ for (var versionAttempt = 0; versionAttempt <= 2 && versionState === null; versionAttempt++) {
79
+ var parsed;
80
+ try {
81
+ var out = await lifecycle(env, "version-check", [taskId]);
82
+ versionFailedReturn = JSON.stringify({ stdout: out.stdout }).slice(0, 500);
83
+ parsed = parseVersionFerry({ stdout: out.stdout });
84
+ } catch (e) {
85
+ var msg = String((e && e.message) || e);
86
+ versionFailedReturn = msg.slice(0, 500);
87
+ parsed = { versionState: null, failReason: "version-check call failed: " + msg.slice(0, 120) };
88
+ }
89
+ if (parsed.versionState !== null) {
90
+ versionState = parsed.versionState;
91
+ versionStateErr = "";
92
+ } else {
93
+ versionStateErr = parsed.failReason;
94
+ if (versionAttempt < 2) {
95
+ log("version-check unparseable (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
96
+ }
97
+ }
98
+ }
99
+ if (versionState === null) {
100
+ return { parked: await parkTask(env, "package.json `version` state unclassifiable after 2 instrument retries (" + versionStateErr + "; last failure: " + versionFailedReturn + ") — the version-check instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy version classification was produced.") };
101
+ }
102
+ var touched = (versionState === "touched");
103
+ log("Review: package.json `version` " + (touched ? "touched by the branch — mechanical FAIL, Cass not dispatched" : "untouched — mechanical check clean"));
104
+ return { versionTouched: touched };
105
+ }
106
+
107
+ // buildInstructions — pure function of ctx (+ gate facts). Verbatim from
108
+ // workflows/chore.js (Review branch).
109
+ export function buildInstructions(ctx, gate) {
110
+ var env = ctx.env;
111
+ var reviewChangeExam, reviewBranchRule;
112
+ if (gate.branchState === "has-work") {
113
+ reviewChangeExam =
114
+ "Examine the code changes by running:\n" +
115
+ lifecycleEnvPrefix(env) + env.lifecycle + " inspect " + env.taskId + "\n\n" +
116
+ "The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of the integration target (inspect prints the target name). Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n";
117
+ reviewBranchRule =
118
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): branch state = has-work. The branch has commits ahead of the integration target. Review their content; branch emptiness is settled and is not yours to judge.\n";
119
+ } else if (gate.branchState.indexOf("already-merged:") === 0) {
120
+ reviewChangeExam =
121
+ "The change under review is the frozen merge " + gate.alreadyMergedSha + " — it is already on the integration target (see MECHANICAL FACT below). Examine it by running:\n" +
122
+ "cd " + env.repoPath + " && git diff " + gate.alreadyMergedSha + "^1 " + gate.alreadyMergedSha + "\n\n" +
123
+ "That first-parent diff is the frozen, attributable change. Base your review ONLY on this diff — do NOT run git log to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n";
124
+ reviewBranchRule =
125
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): branch state = already-merged:" + gate.alreadyMergedSha + ". The deliverable landed on the integration target via this task's own prior merge " + gate.alreadyMergedSha + " (verified ancestor of the integration target). The branch is empty by design — emptiness is settled fact, not a finding.\n";
126
+ } else if (gate.buildClaimedNoDiff) {
127
+ reviewChangeExam =
128
+ "The branch has no commits ahead of the integration target (see MECHANICAL FACT below) — there is no diff to examine. Judge the Build report's `repo_diff: none` claim on its plausibility.\n\n";
129
+ reviewBranchRule =
130
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the branch is empty and no task-attributed merge was verified', then end your report with exactly this line: VERDICT: FAIL.\n";
131
+ } else {
132
+ // Mechanical FAIL — Cass is never dispatched. Instructions unused.
133
+ reviewChangeExam = "";
134
+ reviewBranchRule = "";
135
+ }
136
+ return "Review independently and cold. No prior context from the builder.\nDo NOT access the task dashboard, event log, or any comments. Your review is based solely on the spec and the code.\n\n" +
137
+ (gate.mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + gate.mapperSpec + "\n\n" : "Read the spec from the task description.\n\n") +
138
+ reviewChangeExam +
139
+ "You can also read specific files in the worktree at:\n" +
140
+ env.worktreeHint + "/\n\n" +
141
+ "Check quality, correctness, spec compliance.\n" +
142
+ (env.surfaceTerminal ? "TERMINAL UX REVIEW: judge the CLI surface against " + env.uxDoctrinePath + " — help accuracy, error quality, exit codes, output clarity. Reject when the bar is not met.\n" : "") +
143
+ (env.surfaceArtifact ? "ARTIFACT UX REVIEW: judge the rendered surface against " + env.uxDoctrinePath + " — alignment, spacing, hierarchy, composition, balance, finish, correctness. Reject when the bar is not met.\n" : "") +
144
+ "Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, report what is stale, then end your report with exactly this line: VERDICT: FAIL.\n" +
145
+ reviewBranchRule +
146
+ (env.publishType === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches.\n" +
147
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the task branch did not change package.json's `version` field. A branch that touches `version` fails Review mechanically before any reviewer is dispatched, so this is settled — do not re-check it.\n" +
148
+ "The accepted Build report declares: " + releaseDecisionText(gate.releaseDecision) + ". " +
149
+ (gate.releaseDecision
150
+ ? "Validate this decision against the change: release must be 'yes' when the change is consumer-observable and 'no' when internal-only; the version_bump scope must fit the change (patch for fixes, minor for new behavior, major for breaking changes). If the decision is wrong or mis-scoped, report your notes, then end with exactly this line: VERDICT: FAIL."
151
+ : "The decision is missing or malformed — report 'Build report must end with release: yes|no and (when release is yes) version_bump: patch|minor|major lines', then end with exactly this line: VERDICT: FAIL.") + "\n" : "") +
152
+ "Write your review as plain prose — findings, then decision. End your report with exactly one line: VERDICT: PASS if it passes, VERDICT: FAIL if it fails.";
153
+ }
154
+
155
+ export async function runPhase(ctx) {
156
+ var env = ctx.env, state = ctx.state;
157
+ var taskId = env.taskId;
158
+ var claimed = await ensureClaimed(env, state, PHASE);
159
+ if (claimed.type !== "CLAIMED") return claimed;
160
+
161
+ // Mechanical gates run BEFORE Cass is dispatched. Re-classified on
162
+ // every Review entry: rework rewinds here and the branch changed.
163
+ var cg = await runClassifyGate(env);
164
+ if (cg.parked) return cg.parked;
165
+ var versionTouched = false;
166
+ if (env.publishType === "npm") {
167
+ var vg = await runVersionGate(env);
168
+ if (vg.parked) return vg.parked;
169
+ versionTouched = vg.versionTouched;
170
+ }
171
+
172
+ // Cross-phase handoffs re-derived from durable session notes.
173
+ var buildNotes = await latestSessionNotes(env, "Build", "completed");
174
+ var gate = {
175
+ branchState: cg.branchState,
176
+ alreadyMergedSha: cg.alreadyMergedSha,
177
+ buildClaimedNoDiff: extractRepoDiffNone(buildNotes),
178
+ releaseDecision: extractReleaseDecision(buildNotes),
179
+ mapperSpec: await latestSessionNotes(env, "Map", "completed"),
180
+ };
181
+
182
+ var mechanicalFailReason = (versionTouched ? "version-touched" : (cg.branchState === "empty-no-work" && !gate.buildClaimedNoDiff ? "empty-no-work" : null));
183
+ var mechanicalReviewFail = (mechanicalFailReason !== null);
184
+ var workerText, verdictPassed;
185
+ if (mechanicalReviewFail) {
186
+ log("Review: " + mechanicalFailReason + " — mechanical FAIL, Cass not dispatched");
187
+ workerText =
188
+ "MECHANICAL REVIEW VERDICT (written by the workflow — no reviewer was dispatched).\n" +
189
+ (mechanicalFailReason === "version-touched"
190
+ ? "Package.json `version` (checked by the workflow from git): the task branch changed the `version` field. Versions are assigned at publish time — never in branches. Remove the version change.\n"
191
+ : "Branch state (classified by the workflow from git): empty-no-work — the task branch has no commits ahead of the integration target, " +
192
+ "no task-attributed merge on the target was verified" + (cg.branchStateErr ? " (branch classification itself failed: " + cg.branchStateErr + ")" : "") + ", " +
193
+ "and the accepted Build report declared no `repo_diff: none` deliverable. " +
194
+ "No change was reviewed because there is no change to review. The branch is empty and no task-attributed merge was verified.\n") +
195
+ "Worktree: " + env.worktreeHint + "\n" +
196
+ "Recovery: commit the deliverable on the task branch in the worktree above and re-run Review. If the deliverable is genuinely runtime state outside the repo, declare `repo_diff: none` in the Build report instead of leaving the branch empty without a declaration.\n" +
197
+ "VERDICT: FAIL";
198
+ verdictPassed = false;
199
+ } else {
200
+ var boundary = await runWorkBoundary(env, state, {
201
+ phase: PHASE.name, identity: PHASE.identity,
202
+ instructions: buildInstructions(ctx, gate),
203
+ eventPreamble: buildEventPreamble(env, PHASE.name),
204
+ crewApiLine: false, verdictStep: true,
205
+ });
206
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
207
+ workerText = boundary.workerText;
208
+ verdictPassed = closeoutPassed(boundary);
209
+ }
210
+
211
+ var passed = verdictPassed === true;
212
+ var summary = summarizeReport(workerText);
213
+ // Machine-readable review basis, prepended to the session notes.
214
+ var reviewBasisNote;
215
+ if (mechanicalReviewFail) {
216
+ reviewBasisNote = "REVIEW_BASIS: none — mechanical FAIL (" + mechanicalFailReason + "), no reviewer dispatched";
217
+ } else if (cg.branchState.indexOf("already-merged:") === 0) {
218
+ reviewBasisNote = "REVIEW_BASIS: frozen merge " + cg.alreadyMergedSha + " — reviewed via git diff " + cg.alreadyMergedSha + "^1 " + cg.alreadyMergedSha;
219
+ } else if (cg.branchState === "has-work") {
220
+ reviewBasisNote = "REVIEW_BASIS: branch diff — reviewed via inspect output (task branch vs integration target)";
221
+ } else {
222
+ reviewBasisNote = "REVIEW_BASIS: runtime-state-none — no repo change; plausibility of Build's repo_diff: none claim";
223
+ }
224
+ summary = reviewBasisNote + "\n" + summary;
225
+ var reviewBasis =
226
+ mechanicalReviewFail ? "mechanical-fail" :
227
+ cg.branchState.indexOf("already-merged:") === 0 ? "frozen-merge:" + cg.alreadyMergedSha :
228
+ cg.branchState === "has-work" ? "branch-diff" : "runtime-state-none";
229
+ var status = passed ? "completed" : "rejected";
230
+ await recordPhase(env, {
231
+ task_id: taskId,
232
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: summary },
233
+ event: { task_id: taskId, type: status, identity: PHASE.identity, message: PHASE.name + " " + status + " by " + PHASE.identity },
234
+ verdict: {
235
+ step: "Review",
236
+ attempt: state.reworkCount,
237
+ reviewer: mechanicalReviewFail ? "workflow" : PHASE.identity,
238
+ verdict: passed ? "PASS" : "FAIL",
239
+ review_basis: reviewBasis,
240
+ grounds: workerText,
241
+ },
242
+ });
243
+
244
+ if (!passed) {
245
+ var newRework = state.reworkCount + 1;
246
+ if (newRework > MAX_REWORK) {
247
+ log("Max rework attempts reached for task " + taskId + " — worktree preserved at " + env.worktreePreservedHint + " for manual inspection");
248
+ return await parkTask(env, "Exceeded " + MAX_REWORK + " rework attempts after Review rejection. Worktree preserved.");
249
+ }
250
+ log("Review rejected — bouncing to Build (rework #" + newRework + ")");
251
+ return { type: "REWIND", next: "Build", reason: "Review rejected — bouncing to Build (rework #" + newRework + ")" };
252
+ }
253
+ log("Review completed for task " + taskId);
254
+ return { type: "ADVANCE", next: "Integrate" };
255
+ }
@@ -0,0 +1,57 @@
1
+ // lib/chore/phases/triage.js — Triage (Sage) phase module (sandbox exit,
2
+ // Piece 2). Import-safe: no side effects on import, bare `node` exits 0.
3
+ //
4
+ // Reads: task/project/surface facts, claim/session context.
5
+ // Writes: completed session + event; the machine-read `experiential: yes|no`
6
+ // marker line. The durable cross-run handoff is the Triage session notes.
7
+
8
+ import {
9
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
10
+ closeoutPassed, ensureClaimed, log,
11
+ } from "../phase-lib.js";
12
+ import { extractExperiential } from "../extract.js";
13
+
14
+ export const PHASE = { name: "Triage", identity: "sage" };
15
+
16
+ // buildInstructions — pure function of ctx. Verbatim from
17
+ // workflows/chore.js (Triage branch).
18
+ export function buildInstructions(ctx) {
19
+ var env = ctx.env;
20
+ return "Validate the task against the project's repo at " + env.repoPath + " — that exact checkout, not any other copy of the project on disk. If you run git commands, cd " + env.repoPath + " first.\nCheck clarity, note dependencies, confirm the chore workflow assignment.\nReport back in plain prose — what you found.\nEXPERIENTIAL FLAG: does this task change anything a user can directly observe in the project's user-facing surface? " + env.surfaceTriageDesc + " For an artifact surface that means rendered and visible — pages, components, styles, layout, copy, visual states. For a terminal surface it means the CLI experience — command output, help text, flags, error messages, defaults. The shared UX bar is " + (env.uxDoctrinePath ? env.uxDoctrinePath + " — flag experiential when the task touches anything it covers." : "not classified for this project — flag experiential when the task touches anything user-observable in the surface described above.") + " If yes it is experiential and gets baseline captures. End your report with exactly one line on its own, lowercase, unrephrased: experiential: yes — or experiential: no. This line is machine-read.";
21
+ }
22
+
23
+ export async function runPhase(ctx) {
24
+ var env = ctx.env, state = ctx.state;
25
+ var claimed = await ensureClaimed(env, state, PHASE);
26
+ if (claimed.type !== "CLAIMED") return claimed;
27
+
28
+ var boundary = await runWorkBoundary(env, state, {
29
+ phase: PHASE.name, identity: PHASE.identity,
30
+ instructions: buildInstructions(ctx),
31
+ eventPreamble: buildEventPreamble(env, PHASE.name),
32
+ crewApiLine: true, verdictStep: false,
33
+ });
34
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
35
+
36
+ // Triage is not a verdict step: producing output means passed.
37
+ var passed = closeoutPassed(boundary);
38
+ var workerText = boundary.workerText;
39
+ var summary = summarizeReport(workerText);
40
+ var status = passed ? "completed" : "rejected";
41
+ await recordPhase(env, {
42
+ task_id: env.taskId,
43
+ session: { id: state.activeSessionId, task_id: env.taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: summary },
44
+ event: { task_id: env.taskId, type: status, identity: PHASE.identity, message: PHASE.name + " " + status + " by " + PHASE.identity },
45
+ });
46
+ if (!passed) {
47
+ // A rejected Triage still advances (source: only Review, Integrate,
48
+ // and Publish have special failure paths); the rejection is recorded.
49
+ log("Triage rejected for task " + env.taskId + " — advancing");
50
+ return { type: "ADVANCE", next: "Capture", experiential: null };
51
+ }
52
+ log("Triage complete for task " + env.taskId);
53
+ return {
54
+ type: "ADVANCE", next: "Capture",
55
+ experiential: extractExperiential(workerText),
56
+ };
57
+ }
@@ -0,0 +1,41 @@
1
+ // lib/chore/prompts/evidence-gatherer.js — the evidence-gatherer prompt
2
+ // builder (sandbox exit, Piece 2).
3
+ //
4
+ // Minimal placeholder: the kind is in the frozen creative vocabulary but
5
+ // has no chore site (its only use is the standard workflow's QA phase,
6
+ // deferred to Piece 4). The bridge's kind→builder map is closed, so the
7
+ // kind needs a builder; the per-assignment content arrives as params and
8
+ // the Piece 4 contract will flesh out the reporting grammar. Pure
9
+ // function of declared params; no I/O, no clock, no randomness.
10
+
11
+ import { TOOL_CHECK_PREAMBLE } from "./tool-check.js";
12
+
13
+ function requiredString(params, name) {
14
+ var v = params[name];
15
+ if (typeof v !== "string" || v.length === 0) {
16
+ throw new Error("buildEvidenceGathererPrompt: missing required param '" + name + "'");
17
+ }
18
+ return v;
19
+ }
20
+
21
+ export function buildEvidenceGathererPrompt(params) {
22
+ var p = params || {};
23
+ var identity = requiredString(p, "identity");
24
+ var orchPath = requiredString(p, "orch_path");
25
+ var assignment = requiredString(p, "assignment");
26
+ var constraints = requiredString(p, "constraints");
27
+ var includeToolCheck = p.include_tool_check !== false;
28
+
29
+ return (
30
+ (includeToolCheck ? TOOL_CHECK_PREAMBLE : "") +
31
+ "Read the identity file at " + orchPath + "/identities/" + identity + ".md using the read tool, and embody that character fully.\n\n" +
32
+ "## Your Assignment\n\n" +
33
+ assignment + "\n\n" +
34
+ "CONSTRAINTS:\n" + constraints + "\n\n" +
35
+ "Stay in character. Do the work thoroughly.\n\n" +
36
+ "Your report is plain prose describing the evidence you gathered. " +
37
+ "Do NOT write a VERDICT line — this kind declares machine effects " +
38
+ "(note-event prefixes / OODA log entries), not a verdict; the exact " +
39
+ "reporting grammar is defined by the Piece 4 contract."
40
+ );
41
+ }
@@ -0,0 +1 @@
1
+ {"_note":"Extractor contract for the evidence-gatherer spawn kind — placeholder for the Piece 4 contract. The kind is in the frozen creative vocabulary but has no chore site; this file exists so the bridge's kind→builder/schema map stays closed.","contract":{"piece_4":"The full evidence grammar (OODA/verdict effects, permitted board writes under a declared identity) is deferred to Piece 4 — see DESIGN-chore-pilot.md §1.","verdict":{"function":"extractVerdict","grammar":"Exactly one line: 'VERDICT: PASS' or 'VERDICT: FAIL'. Read with extractVerdict; anything else → missing (null)."}},"extractor_module":"../extract.js","kind":"evidence-gatherer","prompt_builder":"./evidence-gatherer.js"}
@@ -0,0 +1,15 @@
1
+ // lib/chore/prompts/tool-check.js — the tool-check preamble for creative
2
+ // spawn prompts (sandbox exit, Piece 2).
3
+ //
4
+ // Import-safe: pure constant, no I/O. Verbatim from workflows/chore.js
5
+ // (byte-identical across standard/bugfix/chore/docs — pinned by
6
+ // tests/artifact-tools.test.js). The two signal lines are the ONLY
7
+ // machine-read tool-availability evidence — the extractor never guesses
8
+ // from English prose.
9
+
10
+ export const TOOL_CHECK_PREAMBLE =
11
+ "TOOL CHECK (do this first, before any other work):\n" +
12
+ "1. Call tool_search.load_tool_namespace with paths [\"artifact\"].\n" +
13
+ "2. Write exactly one line: artifact_tools: ok - or artifact_tools: missing if the call failed or the tool does not exist.\n" +
14
+ "3. Run the shell command: echo tool-probe-ok - then write exactly one line: shell_transport: ok - or shell_transport: unavailable if you cannot run shell commands.\n" +
15
+ "Then do the assignment below.\n\n";
@@ -0,0 +1,56 @@
1
+ // lib/chore/prompts/trailers.js — retry trailers for creative spawn prompts
2
+ // (sandbox exit, Piece 2).
3
+ //
4
+ // Import-safe: pure functions, no I/O, no clock, no randomness. Logic-
5
+ // verbatim from workflows/chore.js with the exact source signatures:
6
+ // buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason)
7
+ // classifyRetryTrailer(reason, failedReturn)
8
+ // versionRetryTrailer(reason, failedReturn)
9
+ // buildTransportRetryTrailer is byte-identical across standard/bugfix/chore/docs
10
+ // (pinned by tests/transport-retry.test.js); the classify/version trailers are
11
+ // chore-local.
12
+ //
13
+ // Note: the classify/version trailers are retained for contract fidelity but
14
+ // have no consumer in the worker layer — the mechanical gates they re-prompted
15
+ // (classify-branch, version-check) now run via direct execFile, so there is no
16
+ // agent ferry to re-prompt. Only buildTransportRetryTrailer is used, by the
17
+ // work-agent transport-retry path.
18
+
19
+ export function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
20
+ // reason: "discarded" (the runtime threw the output away — the JSON-candidate
21
+ // scan found {...}-shaped fragments it could not parse), "empty" (the worker returned without throwing but produced
22
+ // nothing usable), "no-tools" (the worker's TOOL CHECK reported
23
+ // artifact_tools: missing), or "no-transport" (the worker's TOOL CHECK
24
+ // reported shell_transport: unavailable).
25
+ // The trailer tells the retry what to expect, not just to try again.
26
+ var why = reason === "empty"
27
+ ? "your previous attempt returned no usable output"
28
+ : reason === "no-tools"
29
+ ? "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)"
30
+ : reason === "no-transport"
31
+ ? "your previous attempt's TOOL CHECK reported shell_transport: unavailable (this is a fresh launch, so run the TOOL CHECK's shell probe again before the work)"
32
+ : "your previous attempt's output was discarded by the transport because it contained {...}-shaped fragments the transport could not parse; write plain prose with no JSON-shaped fragments";
33
+ return "\n\nTRANSPORT RETRY (attempt " + attempt + " of 2): " + why + ". " +
34
+ "First check existing state (worktree/branch at " + repoPath + "/.worktrees/" + taskId + ", the task branch, dashboard sessions for this task) - " +
35
+ "if the " + stepName + " work is already complete, report on what was done rather than duplicating side effects. " +
36
+ "Then return your report in exactly the shape specified above.";
37
+ }
38
+
39
+ export function classifyRetryTrailer(reason, failedReturn) {
40
+ // Pass-2 simplification: the reason string already carries the truth
41
+ // (throw vs parse failure), so one neutral sentence replaces the
42
+ // throw/parse branch — the stringly-typed prefix contract between the
43
+ // call site and this function is deleted, not moved. Prompt accuracy,
44
+ // not prompt hardening.
45
+ return "\n\nCLASSIFY RETRY: the previous classifier attempt failed (" + reason + "). " +
46
+ "Return ONLY the two named fields: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }. " +
47
+ "Put each stream in its named field — do not add labels like \"stdout:\" in front of the content. " +
48
+ "Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
49
+ }
50
+
51
+ export function versionRetryTrailer(reason, failedReturn) {
52
+ return "\n\nVERSION-CHECK RETRY: the previous version-check attempt failed (" + reason + "). " +
53
+ "Return ONLY the named field: { \"stdout\": \"<the command's exact stdout>\" }. " +
54
+ "Put the command's stdout in the named field — do not add labels like \"stdout:\" in front of the content. " +
55
+ "Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
56
+ }
@@ -0,0 +1,28 @@
1
+ // lib/chore/prompts/verdict-reask.js — the verdict re-ask prompt builder
2
+ // (sandbox exit, Piece 2).
3
+ //
4
+ // A pure function of declared params (P1). Logic-verbatim from
5
+ // workflows/chore.js buildVerdictReaskPrompt: the re-ask is a fresh content
6
+ // judgment — it must NOT copy any VERDICT line from the report, it decides
7
+ // from the content. Bounded to 2 attempts by the spawn bridge
8
+ // (MAX_ATTEMPTS), 180s each.
9
+
10
+ export function buildVerdictReaskPrompt(params) {
11
+ var p = params || {};
12
+ var stepName = p.step_name;
13
+ var workerText = p.worker_text;
14
+ if (typeof stepName !== "string" || stepName.length === 0) {
15
+ throw new Error("buildVerdictReaskPrompt: missing required param 'step_name'");
16
+ }
17
+ if (typeof workerText !== "string") {
18
+ throw new Error("buildVerdictReaskPrompt: missing required param 'worker_text'");
19
+ }
20
+ return (
21
+ "Mechanical transcription task. Read the work report below and emit its verdict.\n\n" +
22
+ "WORK REPORT (verbatim):\n" + workerText + "\n\n" +
23
+ "Decide from the report's own content whether the " + stepName + " step clearly describes successful completion: " +
24
+ "if it does, the verdict is PASS; otherwise — failure, error, unfinished work, or unclear — the verdict is FAIL. " +
25
+ "Do NOT copy any VERDICT line from the report — decide from the content.\n\n" +
26
+ "The result must be exactly one line and nothing else: VERDICT: PASS or VERDICT: FAIL."
27
+ );
28
+ }
@@ -0,0 +1 @@
1
+ {"_note":"Extractor contract for the verdict-reask spawn kind — the exact line grammar lib/chore/extract.js requires. This is NOT JSON Schema; 'schema' here means the machine-read report contract the agent's prose must satisfy.","contract":{"bounds":"2 attempts max, 180s each (MAX_ATTEMPTS['verdict-reask']); attempts are separate ledger rows kind='verdict-reask'. Terminal reask failures count toward the per-task transport budget.","verdict":{"function":"extractVerdict","grammar":"Exactly one line and nothing else: 'VERDICT: PASS' or 'VERDICT: FAIL'. Read with extractVerdict (same grammar as work-agent); anything else → missing (null) and the re-ask attempt is spent."}},"extractor_module":"../extract.js","kind":"verdict-reask","prompt_builder":"./verdict-reask.js"}
@@ -0,0 +1,52 @@
1
+ // lib/chore/prompts/work-agent.js — the work-agent prompt builder for the
2
+ // worker-layer chore port (sandbox exit, Piece 2).
3
+ //
4
+ // A pure function of declared params (P1, 2026-09-26): prompts are NOT
5
+ // verbatim-lifted (only 1 of 44 chore prompts was liftable). The frame below
6
+ // reproduces the workPromptBase assembly from workflows/chore.js exactly;
7
+ // the per-step instructions arrive as a param (built by the phase modules),
8
+ // as do the conditional trailers.
9
+
10
+ import { TOOL_CHECK_PREAMBLE } from "./tool-check.js";
11
+
12
+ function requiredString(params, name) {
13
+ var v = params[name];
14
+ if (typeof v !== "string" || v.length === 0) {
15
+ throw new Error("buildWorkPrompt: missing required param '" + name + "'");
16
+ }
17
+ return v;
18
+ }
19
+
20
+ export function buildWorkPrompt(params) {
21
+ var p = params || {};
22
+ var identity = requiredString(p, "identity");
23
+ var orchPath = requiredString(p, "orch_path");
24
+ var taskId = requiredString(p, "task_id");
25
+ var stepName = requiredString(p, "step_name");
26
+ var instructions = requiredString(p, "instructions");
27
+ // Source equivalence (workflows/chore.js): task_title and task_description
28
+ // flow through `inputs.task_title || ""` — empty values are valid and must
29
+ // NOT be rejected. Only identity/orch_path/task_id/step_name/instructions
30
+ // are required.
31
+ var taskTitle = p.task_title || "";
32
+ var taskDescription = p.task_description || "";
33
+ var crewApi = typeof p.crew_api === "string" ? p.crew_api : "";
34
+ // Source equivalence: the "Crew API:" line is emitted for every step
35
+ // EXCEPT Review (Cass must not call the Crew API directly — the frame is
36
+ // `(step.name !== "Review" ? "Crew API: " + CREW_API + "\n" : "")`). The
37
+ // phase module passes the pinned crew-api path, or "" for Review.
38
+ var eventPreamble = typeof p.event_preamble === "string" ? p.event_preamble : "";
39
+ var retryTrailer = typeof p.transport_retry_trailer === "string" ? p.transport_retry_trailer : "";
40
+ var includeToolCheck = p.include_tool_check !== false;
41
+
42
+ return (
43
+ (includeToolCheck ? TOOL_CHECK_PREAMBLE : "") +
44
+ "Read the identity file at " + orchPath + "/identities/" + identity + ".md using the read tool, and embody that character fully.\n\n" +
45
+ "## Your Assignment\n\n" +
46
+ "Task: " + taskTitle + "\nTask ID: " + taskId + "\nDescription: " + taskDescription + "\nStep: " + stepName + "\n" +
47
+ (crewApi ? "Crew API: " + crewApi + "\n" : "") +
48
+ "\n## Instructions\n\n" + eventPreamble + instructions + "\n\nCONSTRAINT: Do NOT call logevent or upsertagentsession — the workflow handles all phase tracking after your step completes.\n\nStay in character. Do the work thoroughly.\n\n" +
49
+ "Your report is plain prose describing what you did and found. For verdict steps, end the report with exactly one line: VERDICT: PASS or VERDICT: FAIL." +
50
+ retryTrailer
51
+ );
52
+ }
@@ -0,0 +1 @@
1
+ {"_note":"Extractor contract for the work-agent spawn kind — the exact line grammars lib/chore/extract.js requires. This is NOT JSON Schema; 'schema' here means the machine-read report contract the agent's prose must satisfy.","contract":{"bump_version":{"function":"bumpVersion","grammar":"Deterministic semver bump (patch default); computed by the workflow, never by the agent."},"experiential":{"function":"extractExperiential","grammar":"'experiential: yes|no' → boolean; missing or malformed → null (opt-in flag, never parks on a garbled line)."},"marker_lines":{"function":"extractMarkerLines","grammar":"Known prefixes (repo_diff:, release:, version_bump:, VERDICT:, TARGET_VERSION=, published:, experiential:, layer:, capture_targets:, terminal_targets:, worktree:); identical lines deduped. Long reports cannot amputate them — they are re-appended after the summary slice."},"release_decision":{"function":"extractReleaseDecision","grammar":"'release: yes|no' → {release, version_bump}; missing release line → null; 'yes' without version_bump → null (fail closed)."},"repo_diff_none":{"function":"extractRepoDiffNone","grammar":"/repo_diff:\\s*none/im — the builder declared no repo change."},"tool_signals":{"function":"parseToolSignals","grammar":"Lines 'artifact_tools: ok|missing' and 'shell_transport: ok|unavailable' (anchored, first match each). Missing lines → 'unknown', never null."},"verdict":{"function":"extractVerdict","grammar":"The LAST 'VERDICT: PASS|FAIL' in the report; must be in the trailing 100 chars; conflicting verdicts in the trailing 200 chars fail closed. Returns {ok, passed} or {ok:false, count}. Word boundary prevents 'PASSING' matching."},"worktree":{"function":"extractWorktree","grammar":"Line 'worktree: <path>' (last wins); trailing slashes normalized. Missing → {ok:false}."}},"extractor_module":"../extract.js","kind":"work-agent","prompt_builder":"./work-agent.js"}
@@ -0,0 +1,44 @@
1
+ // lib/chore/spawn-keys.js — pure spawn-key builders for the worker-layer
2
+ // chore port (sandbox exit, Piece 2).
3
+ //
4
+ // Import-safe: pure functions, no I/O, no clock, no randomness.
5
+ // Key logic from workflows/chore.js:
6
+ // attemptKey(base, reworkCount)
7
+ // workRetryKey(stepName, reworkSuffix, attempt)
8
+ // verdictReaskKey(stepName, reworkSuffix, attempt)
9
+ // plus workKeyBase (the first-attempt work key, inline in chore.js as
10
+ // `"work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : "")`).
11
+ //
12
+ // Worker-layer difference (2026-09-26): the source's keys were implicitly
13
+ // scoped per workflow process — the runtime keyed agent() calls by explicit
14
+ // key per process, so `work-Triage` could never collide across tasks. The
15
+ // worker-layer ledger (worker_runs) is global per crew home, so every key
16
+ // carries the task id as a suffix. Without it, task B's driver would read
17
+ // task A's completed `work-Triage` row as its own, and the global
18
+ // duplicate-running guard in record-spawn-open would serialize unrelated
19
+ // tasks. The task suffix is the mechanical equivalent of the runtime's
20
+ // per-process scoping.
21
+
22
+ // attemptKey — replay-key scoping: on rework a re-executed phase mints fresh
23
+ // keys with the same -r<N> suffix as the phase loop. Pure function of inputs,
24
+ // no clock, no randomness (determinism contract).
25
+ export function attemptKey(base, reworkCount) {
26
+ return base + (reworkCount > 0 ? "-r" + reworkCount : "");
27
+ }
28
+
29
+ // workKeyBase — the first-attempt work-agent key for a phase visit.
30
+ export function workKeyBase(taskId, stepName, reworkCount) {
31
+ return "work-" + stepName + (reworkCount > 0 ? "-r" + reworkCount : "") + "-" + taskId;
32
+ }
33
+
34
+ // workRetryKey — transport-retry work-agent keys: work-<Step>[-r<N>]-t<attempt>-<taskId>
35
+ // (attempt is the 1-based retry number: first retry is -t1).
36
+ export function workRetryKey(taskId, stepName, reworkSuffix, attempt) {
37
+ return "work-" + stepName + reworkSuffix + "-t" + attempt + "-" + taskId;
38
+ }
39
+
40
+ // verdictReaskKey — verdict re-ask keys: verdict-reask-<Step>[-r<N>]-a<attempt>-<taskId>
41
+ // (note the -a- infix, not -t-).
42
+ export function verdictReaskKey(taskId, stepName, reworkSuffix, attempt) {
43
+ return "verdict-reask-" + stepName + reworkSuffix + "-a" + attempt + "-" + taskId;
44
+ }