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,120 @@
1
+ // lib/standard/phases/build.js — Build (wren) phase module (sandbox exit,
2
+ // Phase C Piece 1). Import-safe: no side effects on import, bare `node`
3
+ // exits 0.
4
+ //
5
+ // Reads: Map session notes (the mapper spec), latest rejected Review/QA
6
+ // session notes (rework), claim/session context.
7
+ // Writes: completed/rejected session + event (notes = summarized report
8
+ // incl. the machine-read `release:`/`version_bump:`/`repo_diff:`/`worktree:`
9
+ // marker lines).
10
+ // Transitions: ADVANCE → Review on pass. A VERDICT: FAIL records a
11
+ // "rejected" session and still advances to Review (source fall-through:
12
+ // Review's mechanical branch gate turns an empty failed build into a
13
+ // rework bounce; only Review/QA rejections spend the shared budget).
14
+
15
+ import {
16
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
17
+ closeoutPassed, ensureClaimed, crewApi, latestSessionNotes,
18
+ lifecycleEnvPrefix, log,
19
+ } from "../../workflow-lib.js";
20
+ import { extractReleaseDecision } from "../../extract.js";
21
+
22
+ export const PHASE = { name: "Build", identity: "wren" };
23
+
24
+ // latestRejectionNotes — the latest "rejected" session notes among the
25
+ // verdict steps that bounce to Build (Review, QA). Cross-run durable:
26
+ // re-derived from session notes so a run survives tick boundaries
27
+ // (source: run-local rejectionNotes, set on every Review/QA rejection).
28
+ async function latestRejectionNotes(env) {
29
+ var st = await crewApi(env, "get-state", { events_limit: 1 });
30
+ var sessions = (st && st.sessions) || [];
31
+ var best = null;
32
+ for (var i = 0; i < sessions.length; i++) {
33
+ var s = sessions[i];
34
+ if (s.task_id === env.taskId && (s.step === "Review" || s.step === "QA") && s.status === "rejected") {
35
+ if (!best || String(s.started_at || "") > String(best.started_at || "")) best = s;
36
+ }
37
+ }
38
+ return best ? (best.notes || "") : "";
39
+ }
40
+
41
+ // buildInstructions — pure function of ctx. The mapper spec and rejection
42
+ // notes are stashed in ctx.state.memo.buildHandoffs by runPhase before the
43
+ // call (keeps the signature pure). Instruction text verbatim from
44
+ // workflows/standard.js (Build branch).
45
+ export function buildInstructions(ctx) {
46
+ var env = ctx.env;
47
+ var h = (ctx.state.memo && ctx.state.memo.buildHandoffs) || { mapperSpec: "", rejectionNotes: "" };
48
+ return "STEP 1: Prepare your worktree.\n" +
49
+ "Run: " + lifecycleEnvPrefix(env) + " prepare " + env.taskId + "\n" +
50
+ "If the output says CREATED or REUSED, proceed. If it says ERROR, stop and report the failure clearly.\n\n" +
51
+ "HEARTBEAT: Start a background heartbeat loop NOW (before STEP 2) to signal you are still alive during this build. Run this once:\n" +
52
+ "(while node " + env.crewApiPinned + " --crew-home " + env.crewHome + " heartbeat-session --json '{\"id\": \"" + ctx.state.activeSessionId + "\"}' >/dev/null 2>&1; do sleep 900; done) &\n" +
53
+ "The loop heartbeats every 15 minutes and exits on its own when the session ends. This prevents the dispatcher from mistaking a long build for a dead session and launching a duplicate.\n\n" +
54
+ "STEP 2: Edit source files to implement the mapper's spec below.\n" +
55
+ (h.mapperSpec ? "MAPPER'S SPEC (implement exactly this):\n" + h.mapperSpec + "\n\n" : "") +
56
+ "Your working directory: " + env.worktreeHint + "/\n" +
57
+ "This is the project source: " + env.projectDesc + "\n" +
58
+ "Edit source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
59
+ "Do not add unrequested features. Build exactly what the spec calls for.\n" +
60
+ (env.surfaceTerminal ? "TERMINAL UX: build to the shared bar at " + env.uxDoctrinePath + " — --help text, error messages, and exit codes are user-facing and ship in this commit.\n" : "") +
61
+ (env.surfaceArtifact ? "ARTIFACT UX: build to the shared bar at " + env.uxDoctrinePath + " — the rendered result is what the user sees; it ships in this commit.\n" : "") +
62
+ "PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
63
+ (env.publishType === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Versions are assigned at PUBLISH time — never in your branch. Do NOT touch the `version` field in package.json (or package-lock). Instead, end your report with exactly these two lines:\n" +
64
+ "release: yes|no — 'yes' if this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API); 'no' if internal-only.\n" +
65
+ "version_bump: patch|minor|major — patch for fixes (default), minor for new behavior, major for breaking changes. Omit this line only when release is no.\n" +
66
+ "Example: release: yes\\nversion_bump: minor\n\n" : "") +
67
+ "STEP 3: Commit your changes.\n" +
68
+ "cd " + env.worktreeHint + "\n" +
69
+ "git add -A\n" +
70
+ "git commit -m \"" + env.safeTitle + "\"\n\n" +
71
+ "If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of the integration target and declare `repo_diff: none` in your report, naming the runtime-state deliverable. If you verified the deliverable is already on the integration target (a prior merge landed it — do NOT re-implement working code), make no commit and declare `repo_diff: none` in your report. The workflow verifies the claim mechanically from git — the task's own merge records, never your declaration, are the proof of delivery. Otherwise commit your changes normally.\n\n" +
72
+ (h.rejectionNotes ? "This is REWORK after rejection. Address these specific issues:\n" + h.rejectionNotes + "\n\n" : "") +
73
+ "Report back in plain prose: what you built and the outcome." +
74
+ (env.publishType === "npm" ? " End your report with the release: and version_bump: lines exactly as specified above — keep them on their own lines, lowercase, unrephrased — then a line `worktree: ` followed by the exact working directory path from above (copy it verbatim — it must match character-for-character), then a final line with exactly: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not." : " End your report with a line `worktree: ` followed by the exact working directory path from above (copy it verbatim — it must match character-for-character), then exactly one line: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not.");
75
+ }
76
+
77
+ export async function runPhase(ctx) {
78
+ var env = ctx.env, state = ctx.state;
79
+ var taskId = env.taskId;
80
+ var claimed = await ensureClaimed(env, state, PHASE);
81
+ if (claimed.type !== "CLAIMED") return claimed;
82
+
83
+ // Cross-run handoffs, re-derived from durable session notes (source:
84
+ // run-local mapperSpec/rejectionNotes).
85
+ state.memo.buildHandoffs = {
86
+ mapperSpec: await latestSessionNotes(env, "Map", "completed"),
87
+ rejectionNotes: await latestRejectionNotes(env),
88
+ };
89
+
90
+ var boundary = await runWorkBoundary(env, state, {
91
+ phase: PHASE.name, identity: PHASE.identity,
92
+ instructions: buildInstructions(ctx),
93
+ eventPreamble: buildEventPreamble(env, PHASE.name),
94
+ crewApiLine: true, verdictStep: true,
95
+ });
96
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
97
+
98
+ // Capture the accepted Build report's machine-readable release decision.
99
+ // Deterministic extraction from the full worker report. A missing or
100
+ // malformed declaration fails closed to null and Review rejects.
101
+ var passed = closeoutPassed(boundary);
102
+ var workerText = boundary.workerText;
103
+ var releaseDecision = passed ? extractReleaseDecision(workerText) : null;
104
+ state.memo.releaseDecision = releaseDecision;
105
+ var summary = summarizeReport(workerText);
106
+ var status = passed ? "completed" : "rejected";
107
+ await recordPhase(env, {
108
+ task_id: taskId,
109
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: summary },
110
+ event: { task_id: taskId, type: status, identity: PHASE.identity, message: PHASE.name + " " + status + " by " + PHASE.identity },
111
+ });
112
+ if (!passed) {
113
+ // Source fall-through: a failed Build still advances — Review's
114
+ // mechanical branch gate owns the empty-branch bounce.
115
+ log("Build rejected for task " + taskId + " — advancing to Review (source fall-through)");
116
+ } else {
117
+ log("Build complete for task " + taskId);
118
+ }
119
+ return { type: "ADVANCE", next: "Review" };
120
+ }
@@ -0,0 +1,163 @@
1
+ // lib/standard/phases/capture.js — Capture (hazel) phase module (sandbox
2
+ // exit, Phase C Piece 1). Import-safe: no side effects on import, bare
3
+ // `node` exits 0.
4
+ //
5
+ // Reads: experiential status (durable Triage session notes), surface
6
+ // classification, baseline note events, terminal targets.
7
+ // Writes: exact-prefix `baseline:` note events, completed/failed session +
8
+ // event. The durable cross-run handoff is the `baseline:` notes themselves
9
+ // (Map's gate reads them).
10
+ // Transitions: ADVANCE → Map on every path except the park path (PARK
11
+ // with the baseline-request reason).
12
+ //
13
+ // Worker-layer shape: the artifact-surface paths are fully mechanical
14
+ // (direct Crew API calls). The terminal-surface baseline is creative
15
+ // (Hazel drives the CLI herself, reads transcripts) and crosses the work
16
+ // boundary via NEED_SPAWN; the phase owns the deterministic recording
17
+ // (log-event + record-phase) after the boundary resolves. Map-gate
18
+ // bounce re-visits pass state.gateBounceCount as the spawn visitSuffix so
19
+ // the second visit mints fresh keys.
20
+
21
+ import {
22
+ resolveExperiential, baselineStatus, recordPhase, logEvent, parkTask,
23
+ crewApi, runWorkBoundary, buildEventPreamble, closeoutPassed,
24
+ ensureClaimed, log,
25
+ } from "../../workflow-lib.js";
26
+
27
+ export const PHASE = { name: "Capture", identity: "hazel" };
28
+
29
+ // buildInstructions — the creative terminal-baseline prompt. Steps 1–5
30
+ // verbatim from workflows/standard.js (Capture branch); the recording tail
31
+ // is adapted: the worker layer owns log-event / record-phase
32
+ // deterministically, so the agent ends with a machine-read baseline
33
+ // declaration instead of running the recording commands itself.
34
+ export function buildInstructions(ctx) {
35
+ var env = ctx.env, state = ctx.state;
36
+ var termBaseDir = env.taskEvidenceDir + "/baseline";
37
+ var terminalTargets = state.memo.terminalTargets || "";
38
+ return "Run the pre-change terminal baseline yourself — there is no parent capture protocol for terminal-surface projects.\n" +
39
+ "1. The project's repo is at " + env.repoPath + " (pre-change state; the task branch does not exist yet). The CLI under test is the project's own command-line interface in that tree — start like a new user with --help. You are code-blind: you may RUN the CLI, never READ its source.\n" +
40
+ "2. Terminal targets for this task: " + (terminalTargets || "not declared — derive them from --help and the task description") + ".\n" +
41
+ "3. Run in shell: mkdir -p " + termBaseDir + "\n" +
42
+ "4. For each target, run the command with stdout AND stderr captured to " + termBaseDir + "/<nn>-<short-slug>.txt (number them 01, 02, ...), appending the exit code as the final line. Pattern: <cmd> > " + termBaseDir + "/01-<slug>.txt 2>&1; echo \"exit=$?\" >> " + termBaseDir + "/01-<slug>.txt\n" +
43
+ "5. READ every transcript file you wrote — an unread transcript is not evidence.\n" +
44
+ "End your report with exactly one line on its own: either `baseline: captured (terminal transcripts: <comma-separated filenames>)` listing the transcript files you archived, or `baseline: none (terminal targets not runnable: <reason>)` when the CLI cannot run from the pre-change tree — never fabricate a transcript. This line is machine-read.\n" +
45
+ "Report back in plain prose: which commands you ran and what the pre-change baseline looks like.";
46
+ }
47
+
48
+ // buildTerminalBaselineInstructions — legacy alias for buildInstructions
49
+ // (the creative terminal-baseline prompt).
50
+ export function buildTerminalBaselineInstructions(ctx) {
51
+ return buildInstructions(ctx);
52
+ }
53
+
54
+ export async function runPhase(ctx) {
55
+ var env = ctx.env, state = ctx.state;
56
+ var taskId = env.taskId;
57
+ var claimed = await ensureClaimed(env, state, PHASE);
58
+ if (claimed.type !== "CLAIMED") return claimed;
59
+
60
+ var capExp = await resolveExperiential(env, state);
61
+ var surfaceClassified = env.surfaceArtifact || env.surfaceTerminal;
62
+ if (capExp !== "yes" || !surfaceClassified) {
63
+ log("Capture skipped for task " + taskId + " — " + (capExp !== "yes" ? "not experiential" : "surface unclassified"));
64
+ await recordPhase(env, {
65
+ task_id: taskId,
66
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: "Capture skipped — not an experiential task on a classified surface" },
67
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
68
+ });
69
+ return { type: "ADVANCE", next: "Map" };
70
+ }
71
+
72
+ // Terminal-surface Capture: Hazel runs the CLI herself against the
73
+ // pre-change tree and archives attempt-scoped transcripts. No parent
74
+ // protocol exists for terminal projects (there is no see-act loop to
75
+ // drive), so the baseline is agent-driven in one shot — the Map gate
76
+ // reads the same "baseline: captured/none" note prefixes either way.
77
+ if (env.surfaceTerminal) {
78
+ log("Capture: terminal-surface baseline for task " + taskId + " — Hazel drives the CLI herself");
79
+ var visitSuffix = (state.gateBounceCount > 0) ? "-g" + state.gateBounceCount : "";
80
+ var boundary = await runWorkBoundary(env, state, {
81
+ phase: PHASE.name, identity: PHASE.identity,
82
+ instructions: buildInstructions(ctx),
83
+ eventPreamble: buildEventPreamble(env, PHASE.name),
84
+ crewApiLine: true, verdictStep: false,
85
+ visitSuffix: visitSuffix,
86
+ });
87
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
88
+ var workerText = boundary.workerText || "";
89
+ var decls = [];
90
+ var dm;
91
+ var dRe = /^baseline:\s*(captured|none)\b.*$/gim;
92
+ while ((dm = dRe.exec(workerText)) !== null) decls.push(dm[0].trim());
93
+ if (decls.length === 0 || !closeoutPassed(boundary)) {
94
+ var failNotes = "Terminal baseline agent produced no machine-read baseline declaration — cannot record evidence";
95
+ log("Capture: " + failNotes + " for task " + taskId);
96
+ await recordPhase(env, {
97
+ task_id: taskId,
98
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: failNotes },
99
+ event: { task_id: taskId, type: "failed", identity: PHASE.identity, message: "Capture failed for task " + taskId + " — no baseline declaration" },
100
+ });
101
+ return { type: "FAILED", reason: failNotes };
102
+ }
103
+ var declaration = decls[decls.length - 1];
104
+ var captured = /^baseline:\s*captured\b/i.test(declaration);
105
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: declaration });
106
+ await recordPhase(env, {
107
+ task_id: taskId,
108
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed",
109
+ notes: captured ? "Terminal baseline captured by the QA agent (transcripts archived)" : "baseline: none — terminal targets not runnable, final QA judges on rubric alone" },
110
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
111
+ });
112
+ return { type: "ADVANCE", next: "Map" };
113
+ }
114
+
115
+ var capStatus = await baselineStatus(env);
116
+ // See docs/decisions/qa-reproduce.md#stale-decision-guard: a stale baseline decision parks fail-closed.
117
+ var baselineLatestMessage = ("baseline: " + capStatus.baseline_kind + capStatus.baseline_refs).trim();
118
+ var baselineStale = env.visualProtocolAvailable && baselineLatestMessage === "baseline: none (visual protocol unavailable)";
119
+ if (capStatus.baseline_found && !baselineStale) {
120
+ var evidenceN = capStatus.evidence_count + 1;
121
+ var carryNote = "baseline: " + capStatus.baseline_kind + " (#" + evidenceN + " carries forward prior:" + capStatus.baseline_refs + ")";
122
+ log("Capture: baseline evidence already recorded for task " + taskId + " (" + capStatus.baseline_kind + ") — logging per-attempt carry-forward note (evidence #" + evidenceN + ")");
123
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: carryNote });
124
+ await recordPhase(env, {
125
+ task_id: taskId,
126
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed",
127
+ notes: "Baseline evidence already recorded: " + capStatus.baseline_kind + " " + capStatus.baseline_refs + " (evidence #" + evidenceN + ")" },
128
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
129
+ });
130
+ return { type: "ADVANCE", next: "Map" };
131
+ }
132
+
133
+ var attemptN = capStatus.requested_count + 1;
134
+ if (capStatus.requested_count >= 2) {
135
+ log("Capture: baseline capture unavailable after 2 requests for task " + taskId + " — recording baseline:none");
136
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: "baseline: none (capture unavailable after 2 attempts)" });
137
+ await recordPhase(env, {
138
+ task_id: taskId,
139
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: "baseline: none — final QA judges on rubric alone" },
140
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
141
+ });
142
+ return { type: "ADVANCE", next: "Map" };
143
+ }
144
+
145
+ log("Capture: requesting baseline capture (attempt " + attemptN + ") for task " + taskId);
146
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: "baseline: requested (attempt " + attemptN + ")" });
147
+ await crewApi(env, "upsert-session", {
148
+ id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed",
149
+ notes: "Baseline capture requested (attempt " + attemptN + ") — parent protocol: run the baseline capture protocol in docs/visual-verdict.md, then re-queue the task at Map. The Capture step re-checks baseline evidence when re-run.",
150
+ });
151
+ if (!env.visualProtocolAvailable) {
152
+ log("Capture: visual protocol not available (VISUAL_PROTOCOL_AVAILABLE=false) — recording baseline:none instead of parking for task " + taskId);
153
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: "baseline: none (visual protocol unavailable)" });
154
+ await recordPhase(env, {
155
+ task_id: taskId,
156
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: "baseline: none — visual protocol unavailable, final QA judges on rubric alone" },
157
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
158
+ });
159
+ return { type: "ADVANCE", next: "Map" };
160
+ }
161
+ var parked = await parkTask(env, "Baseline capture requested (attempt " + attemptN + ") — parent: run the baseline capture protocol in docs/visual-verdict.md, then re-queue the task at Map");
162
+ return parked;
163
+ }
@@ -0,0 +1,172 @@
1
+ // lib/standard/phases/integrate.js — Integrate (wren) phase module (sandbox
2
+ // exit, Phase C Piece 1). Import-safe: no side effects on import, bare
3
+ // `node` exits 0.
4
+ //
5
+ // Reads: claim/session context, merge outcome markers (MERGED /
6
+ // MERGED_EMPTY / PUSHED) from the pinned integrate script's verbatim
7
+ // output, mechanical verify-merge result, qa-deploy.mjs result.
8
+ // Writes: completed/failed session + event (notes carry deploy markers),
9
+ // a rebuilt QA bundle via qa-deploy.mjs (versioned artifact projects).
10
+ // Transitions: ADVANCE → Publish on verified PASS. FAILED on any
11
+ // operational failure (verdict FAIL, verify-merge failure, deploy
12
+ // failure) — the dispatcher retries under its consecutive-failure cap.
13
+ //
14
+ // 0.16.0 note: the BUILD_NUMBER bump and REQUIRED_BUILD marker were
15
+ // deliberately removed from the source (commit b7319ca). Freshness is
16
+ // constructive (rebuild via qa-deploy.mjs), not numeric.
17
+
18
+ import {
19
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
20
+ closeoutPassed, ensureClaimed, lifecycle, parkTask, runCmd,
21
+ lifecycleEnvPrefix, log,
22
+ } from "../../workflow-lib.js";
23
+ import { join } from "path";
24
+ import { readFileSync, writeFileSync } from "fs";
25
+
26
+ export const PHASE = { name: "Integrate", identity: "wren" };
27
+
28
+ // buildInstructions — pure function of ctx. Verbatim from
29
+ // workflows/standard.js (Integrate branch).
30
+ export function buildInstructions(ctx) {
31
+ var env = ctx.env;
32
+ return "Integrate the change into the main codebase.\\n" +
33
+ "Run the integration script:\\n" +
34
+ lifecycleEnvPrefix(env) + env.lifecycle + " integrate " + env.taskId + "\\n\\n" +
35
+ "The script merges, reconciles, records, and pushes under the merge lock — report what it prints, do not improvise git commands.\\n" +
36
+ "The script prints machine-readable marker lines:\\n" +
37
+ "- INTEGRATION_TARGET=<branch-or-ref> — the target you merged into\\n" +
38
+ "- MERGE_BASE=<sha> — the merge base the script reconciled against (verify: run `git -C " + env.repoPath + " merge-base HEAD <INTEGRATION_TARGET>` and confirm it prints the same sha; if it differs, report the discrepancy and VERDICT: FAIL — the script did not reconcile against the true base)\\n" +
39
+ "- MERGED_EMPTY — check FIRST (it contains the word MERGED): either a runtime-state deliverable or a task-attributed merge the workflow verified from the task's own merge records. No lock taken, no new commit. Report 'merged empty: no repo changes'. VERDICT: PASS.\\n" +
40
+ "- MERGED: the merge landed; the script pushed inline. Read the push line: PUSHED — report the hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; NO_REMOTE_PUSH — no remote configured, VERDICT: PASS; ERROR after MERGED — a retry recovers the push; report it, VERDICT: FAIL.\\n" +
41
+ "- CONFLICT — conflicts; resolve them per the script's conflict output, re-run the script, then report.\\n" +
42
+ "- ERROR — report the error line, VERDICT: FAIL.\\n" +
43
+ "Do not run your own git merge, rebase, or push commands. If the script's output is ambiguous, report the ambiguity and VERDICT: FAIL — never guess the merge state.\\n" +
44
+ "If you changed public-affecting behavior, the branch must carry the matching public doc updates (API.md or the published API contract) before you report PASS — Review already checked this, but a merge that drops the docs fails here too.\\n" +
45
+ "End your report with exactly one line: VERDICT: PASS if the merge landed (or MERGED_EMPTY), VERDICT: FAIL otherwise.";
46
+ }
47
+
48
+ // verifyMerge — mechanical post-PASS check (publish-path.md#integrate-verify:
49
+ // the agent cannot verify integrate mechanically; the workflow checks the
50
+ // diff). Direct lifecycle call, no agent ferry.
51
+ async function verifyMerge(env) {
52
+ var out = "";
53
+ try {
54
+ var res = await lifecycle(env, "verify-merge", [env.taskId]);
55
+ out = String((res && res.stdout) || "").trim();
56
+ } catch (e) {
57
+ out = "";
58
+ }
59
+ if (/^VERIFIED:/m.test(out)) {
60
+ log("Integrate verified for task " + env.taskId + ": task branch tip is an ancestor of the integration target");
61
+ return { ok: true };
62
+ }
63
+ var reason = out ? out.split("\n")[0].slice(0, 200) : "verify-merge produced no usable output";
64
+ log("Integrate verification failed for task " + env.taskId + ": " + reason + " — marking failed for retry");
65
+ return { ok: false, reason: reason };
66
+ }
67
+
68
+ // deployQaBundle — after a verified merge on a versioned artifact project,
69
+ // ensure the QA environment serves a bundle built from merged source
70
+ // (0.16.0: this REPLACES the old numeric version gate — freshness is
71
+ // constructive, not comparative). Direct qa-deploy.mjs call — no agent
72
+ // ferry. A failed deploy is an operational failure (retryable), never a
73
+ // park.
74
+ async function deployQaBundle(env) {
75
+ var qaDeploy = join(env.runLib, "qa-deploy.mjs");
76
+ var qaEnvDir = env.crewHome + "/qa-envs/" + env.projectId;
77
+ var serveArtifact = join(env.runLib, "serve-artifact.js");
78
+ var out = "";
79
+ try {
80
+ var res = await runCmd(
81
+ ["node", qaDeploy, "--repo", env.repoPath, "--qa-dir", qaEnvDir,
82
+ "--task", env.taskId, "--crew-home", env.crewHome, "--crew-api", env.crewApiPinned,
83
+ "--serve-artifact", serveArtifact],
84
+ {}
85
+ );
86
+ out = String((res && res.stdout) || "").trim();
87
+ } catch (e) {
88
+ out = "";
89
+ }
90
+ var m = /^DEPLOY_OK ([0-9a-f]{7})$/m.exec(out);
91
+ if (m) {
92
+ log("QA deploy ok for task " + env.taskId + ": bundle " + m[1]);
93
+ return { ok: true, hash: m[1] };
94
+ }
95
+ var f = /^DEPLOY_FAIL ([a-z-]+):(.{1,200})$/m.exec(out);
96
+ var reason = f ? f[1] + ": " + f[2].trim() : "unrecognized deploy output";
97
+ log("QA deploy failed for task " + env.taskId + ": " + reason + " — marking failed for retry");
98
+ return { ok: false, reason: reason };
99
+ }
100
+
101
+ export async function runPhase(ctx) {
102
+ var env = ctx.env, state = ctx.state;
103
+ var taskId = env.taskId;
104
+ var claimed = await ensureClaimed(env, state, PHASE);
105
+ if (claimed.type !== "CLAIMED") return claimed;
106
+
107
+ var boundary = await runWorkBoundary(env, state, {
108
+ phase: PHASE.name, identity: PHASE.identity,
109
+ instructions: buildInstructions(ctx),
110
+ eventPreamble: buildEventPreamble(env, PHASE.name),
111
+ crewApiLine: false, verdictStep: true,
112
+ });
113
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
114
+ var workerText = boundary.workerText;
115
+ var passed = closeoutPassed(boundary) === true;
116
+
117
+ var summary = summarizeReport(workerText);
118
+ var markers = [];
119
+
120
+ if (passed) {
121
+ // Mechanical merge verification (the agent cannot self-certify).
122
+ var mv = await verifyMerge(env);
123
+ if (!mv.ok) {
124
+ summary += "\nintegrate_verify: FAILED — " + mv.reason;
125
+ await recordPhase(env, {
126
+ task_id: taskId,
127
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: summary },
128
+ event: { task_id: taskId, type: "failed", identity: PHASE.identity, message: PHASE.name + " failed by " + PHASE.identity },
129
+ });
130
+ return { type: "FAILED", reason: "Integrate merge verification failed: " + mv.reason };
131
+ }
132
+
133
+ var mergedEmpty = /MERGED_EMPTY/.test(workerText || "");
134
+ // 0.16.0: freshness is constructive — rebuild the QA bundle from the
135
+ // merged source so the bundle QA tests is current. No version bump,
136
+ // no REQUIRED_BUILD marker, no numeric comparison.
137
+ if (env.versionedBuild) {
138
+ // Rebuild the QA bundle from the merged source.
139
+ if (env.surfaceArtifact) {
140
+ var dep = await deployQaBundle(env);
141
+ if (!dep.ok) {
142
+ summary += "\ndeploy: FAILED — " + dep.reason;
143
+ await recordPhase(env, {
144
+ task_id: taskId,
145
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: summary },
146
+ event: { task_id: taskId, type: "failed", identity: PHASE.identity, message: PHASE.name + " failed by " + PHASE.identity },
147
+ });
148
+ return { type: "FAILED", reason: "Integrate QA deploy failed: " + dep.reason };
149
+ }
150
+ markers.push("deploy: ok " + dep.hash);
151
+ }
152
+ }
153
+
154
+ var notes = (markers.length ? markers.join("\n") + "\n" : "") + summary;
155
+ await recordPhase(env, {
156
+ task_id: taskId,
157
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: notes },
158
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: PHASE.name + " completed by " + PHASE.identity },
159
+ });
160
+ log("Integrate complete for task " + taskId);
161
+ return { type: "ADVANCE", next: "Publish" };
162
+ }
163
+
164
+ // Integrate failure is operational (there is no rework loop — the
165
+ // failure is not a verdict on the change, it is the merge not landing).
166
+ await recordPhase(env, {
167
+ task_id: taskId,
168
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: summary },
169
+ event: { task_id: taskId, type: "failed", identity: PHASE.identity, message: PHASE.name + " failed by " + PHASE.identity },
170
+ });
171
+ return { type: "FAILED", reason: "Integration failed" };
172
+ }
@@ -0,0 +1,119 @@
1
+ // lib/standard/phases/map.js — Map (mara) phase module (sandbox exit,
2
+ // Phase C Piece 1). Import-safe: no side effects on import, bare `node`
3
+ // exits 0.
4
+ //
5
+ // Reads: experiential status (durable Triage session notes), `baseline:`
6
+ // note events, claim/session context.
7
+ // Writes: completed/failed session + event (notes = the mapper
8
+ // specification — the durable cross-run handoff Build reads).
9
+ // Transitions: REWIND → Capture (Map gate bounce — session `failed`,
10
+ // never a task failure); ADVANCE → Build. A Map spec verification failure
11
+ // is operational (FAILED — the dispatcher retries at Map under its
12
+ // consecutive-failure cap).
13
+
14
+ import { statSync } from "fs";
15
+ import {
16
+ resolveExperiential, baselineStatus, recordPhase, summarizeReport,
17
+ runWorkBoundary, buildEventPreamble,
18
+ ensureClaimed, log,
19
+ } from "../../workflow-lib.js";
20
+
21
+ export const PHASE = { name: "Map", identity: "mara" };
22
+
23
+ // buildInstructions — pure function of ctx. The baseline-gate paragraph
24
+ // inputs are stashed in ctx.state.memo.mapGate by runPhase before the
25
+ // call (keeps the signature pure). Instruction text verbatim from
26
+ // workflows/standard.js (Map branch).
27
+ export function buildInstructions(ctx) {
28
+ var env = ctx.env;
29
+ var gate = (ctx.state.memo && ctx.state.memo.mapGate) || { refs: "", none: false, applies: false };
30
+ var mapGatePara = "";
31
+ if (gate.applies) {
32
+ mapGatePara = "\nBASELINE GATE (experiential task): " +
33
+ (gate.none
34
+ ? "no baseline was capturable (baseline: none recorded) — write the spec without baseline comparison and note it."
35
+ : "pre-change baseline evidence: " + gate.refs + " — consult it when writing the spec.") +
36
+ " If the baseline evidence is missing with no baseline:none recorded, do not write the spec — report 'baseline evidence missing — Map gate bounce required' and stop.\n" +
37
+ (env.surfaceTerminal
38
+ ? "Declare the CLI surface to exercise: end your report with a line `terminal_targets: <comma-separated CLI commands/flags>` (machine-read; optional — falls back to the task description)."
39
+ : "Declare capture targets for the post-change visual capture: end your report with a line `capture_targets: <comma-separated views/controls this change affects>` (optional; falls back to the task description).");
40
+ }
41
+ return "Research the problem space, evaluate options, pick the shortest path.\nWrite a clear spec that a builder can execute without asking questions.\n" +
42
+ (env.surfaceTerminal ? "TERMINAL SPEC: this project's surface is a CLI. Specify the exact commands, their expected stdout/stderr, exit codes, --help text, and error messages — Hazel judges the build against this spec and the shared bar at " + env.uxDoctrinePath + ".\n" : "") +
43
+ (env.surfaceArtifact ? "ARTIFACT SPEC: this project's surface is a rendered artifact. Specify the exact screens, flows, and visual states the change affects — Hazel judges the build against this spec and the shared bar at " + env.uxDoctrinePath + ".\n" : "") +
44
+ "The builder will edit source files in a git worktree.\nProject: " + env.projectDesc + "\nSave the spec to exactly this file: " + env.specPath + " — run mkdir -p \"" + env.specDir + "\" first. Do not save the spec anywhere else; this exact path is fixed and will be checked mechanically after your step.\nReport back in plain prose — what you specified." + mapGatePara;
45
+ }
46
+
47
+ export async function runPhase(ctx) {
48
+ var env = ctx.env, state = ctx.state;
49
+ var taskId = env.taskId;
50
+ var claimed = await ensureClaimed(env, state, PHASE);
51
+ if (claimed.type !== "CLAIMED") return claimed;
52
+
53
+ // ── Map gate: experiential tasks need baseline evidence ────────────
54
+ // Mara must verify baseline evidence exists before writing the spec. If
55
+ // missing, bounce back to Capture — never to Build, never a task failure.
56
+ // Must match Capture's run condition (experiential + classified surface):
57
+ // when Capture skips, no baseline notes exist, so the gate must not apply.
58
+ var surfaceClassified = env.surfaceArtifact || env.surfaceTerminal;
59
+ var mapExp = await resolveExperiential(env, state);
60
+ var gateApplies = (mapExp === "yes" && surfaceClassified);
61
+ var gateRefs = "", gateNone = false;
62
+ if (gateApplies) {
63
+ var gateStatus = await baselineStatus(env);
64
+ var baselineLatestMessage = ("baseline: " + gateStatus.baseline_kind + gateStatus.baseline_refs).trim();
65
+ var baselineStale = env.visualProtocolAvailable && baselineLatestMessage === "baseline: none (visual protocol unavailable)";
66
+ if (!gateStatus.baseline_found || baselineStale) {
67
+ log("Map gate: no baseline evidence for task " + taskId + " — bouncing to Capture");
68
+ await recordPhase(env, {
69
+ task_id: taskId,
70
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: "Map gate bounce — no baseline evidence, re-running Capture" },
71
+ event: { task_id: taskId, type: "failed", identity: PHASE.identity, message: "Map gate bounce for task " + taskId + " — no baseline evidence" },
72
+ });
73
+ // The driver increments state.gateBounceCount on this REWIND so the
74
+ // re-visited Capture mints fresh spawn keys (visitSuffix "-g<N>").
75
+ return { type: "REWIND", next: "Capture", reason: "Map gate: no baseline evidence for experiential task — bounced to Capture" };
76
+ }
77
+ gateRefs = (gateStatus.baseline_refs || []).join(", ");
78
+ gateNone = gateStatus.baseline_kind === "none";
79
+ }
80
+ state.memo.mapGate = { applies: gateApplies, refs: gateRefs, none: gateNone };
81
+
82
+ var boundary = await runWorkBoundary(env, state, {
83
+ phase: PHASE.name, identity: PHASE.identity,
84
+ instructions: buildInstructions(ctx),
85
+ eventPreamble: buildEventPreamble(env, PHASE.name),
86
+ crewApiLine: true, verdictStep: false,
87
+ });
88
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
89
+
90
+ // Map spec failure is operational (the spec file did not land at the
91
+ // workflow-computed SPEC_PATH), not a verdict: the session is recorded
92
+ // "failed" and the dispatcher retries at Map under its
93
+ // consecutive-failure cap. The retry re-runs Map with the same
94
+ // exact-path instructions against the same SPEC_PATH.
95
+ var specExists = false;
96
+ try { specExists = statSync(env.specPath).isFile(); } catch (e) { specExists = false; }
97
+ if (!specExists) {
98
+ log("Map spec verification failed for task " + taskId + " — returning failed for dispatcher retry");
99
+ await recordPhase(env, {
100
+ task_id: taskId,
101
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: "Map spec verification failed: no spec file at " + env.specPath },
102
+ event: { task_id: taskId, type: "failed", identity: PHASE.identity, message: "Map spec verification failed for task " + taskId },
103
+ });
104
+ return { type: "FAILED", reason: "Map spec verification failed: no spec file at " + env.specPath };
105
+ }
106
+
107
+ // The session notes carry the mapper specification — the durable
108
+ // cross-run handoff Build re-derives via latestSessionNotes. Marker
109
+ // lines are re-attached after the slice (source: stepResult.summary
110
+ // construction).
111
+ var summary = summarizeReport(boundary.workerText);
112
+ await recordPhase(env, {
113
+ task_id: taskId,
114
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: summary },
115
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: PHASE.name + " completed by " + PHASE.identity },
116
+ });
117
+ log("Map complete for task " + taskId);
118
+ return { type: "ADVANCE", next: "Build" };
119
+ }