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,110 @@
1
+ // lib/chore/phases/build.js — Build (Wren) phase module (sandbox exit,
2
+ // Piece 2). Import-safe: no side effects on import, bare `node` exits 0.
3
+ //
4
+ // Reads: mapper spec (re-derived from the Map session notes), rejection
5
+ // notes (re-derived from the rejected Review session notes),
6
+ // worktree/project/surface facts. Writes: repository/worktree changes
7
+ // (via the work agent) and marker lines (worktree:, release:,
8
+ // version_bump:, repo_diff:, VERDICT). The worktree-confinement check and
9
+ // the repo_diff:none claim capture are mechanical closeout gates.
10
+
11
+ import {
12
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
13
+ closeoutPassed, ensureClaimed, latestSessionNotes,
14
+ lifecycleEnvPrefix, log,
15
+ } from "../phase-lib.js";
16
+ import { extractWorktree, extractRepoDiffNone } from "../extract.js";
17
+
18
+ export const PHASE = { name: "Build", identity: "wren" };
19
+
20
+ // buildInstructions — pure function of ctx (+ derived handoffs). Verbatim
21
+ // from workflows/chore.js (Build branch).
22
+ export function buildInstructions(ctx, handoffs) {
23
+ var env = ctx.env;
24
+ var h = handoffs || { mapperSpec: "", rejectionNotes: "" };
25
+ return "STEP 1: Prepare your worktree.\n" +
26
+ "Run: " + lifecycleEnvPrefix(env) + env.lifecycle + " prepare " + env.taskId + "\n" +
27
+ "If the output says CREATED or REUSED, proceed. If it says ERROR, stop and report the failure clearly.\n\n" +
28
+ "HEARTBEAT: Start a background heartbeat loop NOW (before STEP 2) to signal you are still alive during this build. Run this once:\n" +
29
+ "(while node " + env.crewApiPinned + " --crew-home " + env.crewHome + " heartbeat-session --json '{\"id\": \"" + ctx.state.activeSessionId + "\"}' >/dev/null 2>&1; do sleep 900; done) &\n" +
30
+ "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" +
31
+ "STEP 2: Edit source files to implement the mapper's spec below.\n" +
32
+ (h.mapperSpec ? "MAPPER'S SPEC (implement exactly this):\n" + h.mapperSpec + "\n\n" : "") +
33
+ "Your working directory: " + env.worktreeHint + "/\n" +
34
+ "This is the project source: " + env.projectDesc + "\n" +
35
+ "Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
36
+ "Do not add unrequested features.\n" +
37
+ (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" : "") +
38
+ (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" : "") +
39
+ "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" +
40
+ (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" +
41
+ "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" +
42
+ "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" +
43
+ "Example: release: yes\\nversion_bump: minor\n\n" : "") +
44
+ "STEP 3: Commit your changes.\n" +
45
+ "cd " + env.worktreeHint + "\n" +
46
+ "git add -A\n" +
47
+ "git commit -m \"chore: " + env.safeTitle + "\"\n\n" +
48
+ "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" +
49
+ (h.rejectionNotes ? "REWORK after rejection. Address:\n" + h.rejectionNotes + "\n\n" : "") +
50
+ "Report back in plain prose: what you built and the outcome." +
51
+ (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.");
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
+ // Cross-phase handoffs, re-derived from durable session notes so the run
61
+ // survives tick boundaries (source: in-process mapperSpec/rejectionNotes).
62
+ var handoffs = {
63
+ mapperSpec: await latestSessionNotes(env, "Map", "completed"),
64
+ rejectionNotes: await latestSessionNotes(env, "Review", "rejected"),
65
+ };
66
+
67
+ var boundary = await runWorkBoundary(env, state, {
68
+ phase: PHASE.name, identity: PHASE.identity,
69
+ instructions: buildInstructions(ctx, handoffs),
70
+ eventPreamble: buildEventPreamble(env, PHASE.name),
71
+ crewApiLine: true, verdictStep: true,
72
+ });
73
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
74
+
75
+ var passed = closeoutPassed(boundary);
76
+ var workerText = boundary.workerText;
77
+
78
+ // Worktree confinement (Build only): the declared worktree path must
79
+ // match the hint exactly. A builder that worked in any other checkout
80
+ // fails the phase here — the dispatcher retries Build under its
81
+ // consecutive-failure cap, and the retry re-runs prepare against the
82
+ // configured repo. Missing or mismatched lines fail closed.
83
+ if (passed) {
84
+ var wt = extractWorktree(workerText);
85
+ if (!wt.ok || wt.path !== env.worktreeHint) {
86
+ log("Build worktree confinement failed — declared: " + (wt.ok ? wt.path : "<none>") + ", expected: " + env.worktreeHint + " — marking failed for retry");
87
+ await recordPhase(env, {
88
+ task_id: taskId,
89
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: "Build declared worktree " + (wt.ok ? wt.path : "<none>") + " — expected " + env.worktreeHint + ". The builder worked outside the configured repo checkout; phase failed for retry" },
90
+ event: { task_id: taskId, type: "failed", message: "Build worktree confinement failed — builder worked outside " + env.worktreeHint + ", phase failed, dispatcher will retry" },
91
+ });
92
+ return { type: "FAILED", reason: "Build worked outside the configured repo checkout", detail: "The builder declared worktree " + (wt.ok ? wt.path : "<none>") + " but the task's worktree is " + env.worktreeHint + ". The phase is marked failed; the dispatcher will retry Build against the configured repo." };
93
+ }
94
+ log("Build worktree confinement passed: " + wt.path);
95
+ }
96
+
97
+ var summary = summarizeReport(workerText);
98
+ var status = passed ? "completed" : "rejected";
99
+ await recordPhase(env, {
100
+ task_id: taskId,
101
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: summary },
102
+ event: { task_id: taskId, type: status, identity: PHASE.identity, message: PHASE.name + " " + status + " by " + PHASE.identity },
103
+ });
104
+ // The plain `repo_diff: none` claim is captured from the full Build
105
+ // report (fresh each round); Review re-derives it from the session
106
+ // notes. Returned for the driver's observability.
107
+ var claimedNoDiff = extractRepoDiffNone(workerText);
108
+ log("Build " + status + " for task " + taskId);
109
+ return { type: "ADVANCE", next: "Review", buildClaimedNoDiff: claimedNoDiff };
110
+ }
@@ -0,0 +1,82 @@
1
+ // lib/chore/phases/capture.js — Capture (Hazel) phase module (sandbox
2
+ // exit, Piece 2). Import-safe: no side effects on import, bare `node`
3
+ // exits 0.
4
+ //
5
+ // Fully mechanical — no creative spawn. Reads the experiential status and
6
+ // baseline notes; writes exact-prefix baseline notes and phase/session
7
+ // state; advances to Map, or requests/carries evidence.
8
+
9
+ import {
10
+ resolveExperiential, baselineStatus, recordPhase, logEvent, parkTask,
11
+ crewApi, ensureClaimed, log,
12
+ } from "../phase-lib.js";
13
+
14
+ export const PHASE = { name: "Capture", identity: "hazel" };
15
+
16
+ export async function runPhase(ctx) {
17
+ var env = ctx.env, state = ctx.state;
18
+ var taskId = env.taskId;
19
+ var claimed = await ensureClaimed(env, state, PHASE);
20
+ if (claimed.type !== "CLAIMED") return claimed;
21
+
22
+ var capExp = await resolveExperiential(env, state);
23
+ // Chore has no QA phase, so Capture stays artifact-only: terminal
24
+ // baselines would be unused evidence.
25
+ if (capExp !== "yes" || !env.surfaceArtifact) {
26
+ log("Capture skipped for task " + taskId + " — " + (capExp !== "yes" ? "not experiential" : "no QA phase to consume terminal-surface baselines"));
27
+ await recordPhase(env, {
28
+ task_id: taskId,
29
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: "Capture skipped — not an experiential artifact task" },
30
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
31
+ });
32
+ return { type: "ADVANCE", next: "Map" };
33
+ }
34
+
35
+ var capStatus = await baselineStatus(env);
36
+ // See docs/decisions/qa-reproduce.md#stale-decision-guard: a stale baseline decision parks fail-closed.
37
+ var baselineLatestMessage = ("baseline: " + capStatus.baseline_kind + capStatus.baseline_refs).trim();
38
+ var baselineStale = env.visualProtocolAvailable && baselineLatestMessage === "baseline: none (visual protocol unavailable)";
39
+ if (capStatus.baseline_found && !baselineStale) {
40
+ var evidenceN = capStatus.evidence_count + 1;
41
+ var carryNote = "baseline: " + capStatus.baseline_kind + " (#" + evidenceN + " carries forward prior:" + capStatus.baseline_refs + ")";
42
+ log("Capture: baseline evidence already recorded for task " + taskId + " (" + capStatus.baseline_kind + ") — logging per-attempt carry-forward note (evidence #" + evidenceN + ")");
43
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: carryNote });
44
+ await recordPhase(env, {
45
+ task_id: taskId,
46
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: "Baseline evidence already recorded: " + capStatus.baseline_kind + " " + capStatus.baseline_refs + " (evidence #" + evidenceN + ")" },
47
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
48
+ });
49
+ return { type: "ADVANCE", next: "Map" };
50
+ }
51
+
52
+ var attemptN = capStatus.requested_count + 1;
53
+ if (capStatus.requested_count >= 2) {
54
+ log("Capture: baseline capture unavailable after 2 requests for task " + taskId + " — recording baseline:none");
55
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: "baseline: none (capture unavailable after 2 attempts)" });
56
+ await recordPhase(env, {
57
+ task_id: taskId,
58
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: "baseline: none — continuing without baseline comparison" },
59
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
60
+ });
61
+ return { type: "ADVANCE", next: "Map" };
62
+ }
63
+
64
+ log("Capture: requesting baseline capture (attempt " + attemptN + ") for task " + taskId);
65
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: "baseline: requested (attempt " + attemptN + ")" });
66
+ await crewApi(env, "upsert-session", {
67
+ id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed",
68
+ 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.",
69
+ });
70
+ if (!env.visualProtocolAvailable) {
71
+ log("Capture: visual protocol not available (VISUAL_PROTOCOL_AVAILABLE=false) — recording baseline:none instead of parking for task " + taskId);
72
+ await logEvent(env, { task_id: taskId, type: "note", identity: PHASE.identity, message: "baseline: none (visual protocol unavailable)" });
73
+ await recordPhase(env, {
74
+ task_id: taskId,
75
+ 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" },
76
+ event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: "Capture completed by " + PHASE.identity },
77
+ });
78
+ return { type: "ADVANCE", next: "Map" };
79
+ }
80
+ 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");
81
+ return parked;
82
+ }
@@ -0,0 +1,109 @@
1
+ // lib/chore/phases/integrate.js — Integrate (Wren) phase module (sandbox
2
+ // exit, Piece 2). Import-safe: no side effects on import, bare `node`
3
+ // exits 0.
4
+ //
5
+ // The work agent runs the lifecycle's `integrate` command (merge,
6
+ // reconcile, record, push under the merge lock) and reports VERDICT.
7
+ // The workflow then verifies mechanically: `verify-merge <taskId>` must
8
+ // print a VERIFIED: line — the agent cannot verify integrate itself.
9
+
10
+ import {
11
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
12
+ closeoutPassed, ensureClaimed, lifecycle, crewCmdString,
13
+ lifecycleEnvPrefix, log, runCmd,
14
+ } from "../phase-lib.js";
15
+
16
+ export const PHASE = { name: "Integrate", identity: "wren" };
17
+
18
+ // buildInstructions — pure function of ctx. Verbatim from
19
+ // workflows/chore.js (Integrate branch).
20
+ export function buildInstructions(ctx) {
21
+ var env = ctx.env;
22
+ var taskId = env.taskId;
23
+ var lockHolder = taskId + "/" + ctx.state.activeSessionId;
24
+ return "Merge the approved task branch into the integration target and push the result.\n" +
25
+ "The integration target is the repo's current checkout — a branch name, or the literal word HEAD when detached (no branch name exists when detached). Print the exact token with: "+ lifecycleEnvPrefix(env) + env.lifecycle + " integration-target\n" +
26
+ "NEVER run git checkout — the lifecycle merges into the current checkout; moving it abandons the line Publish verifies.\n\n" +
27
+ "Read the staged publish base (return its stdout verbatim):\n" + crewCmdString(env, "get-provenance", { project_id: env.projectId }) + "\n" +
28
+ "Then run: "+ lifecycleEnvPrefix(env) + "WORKFLOW_RUN_ID=" + lockHolder + " CREW_STAGED_BASE=<provenance.source_commit, empty when null> integrate " + taskId + " \"merge: chore: " + env.safeTitle + "\\\"\\n" +
29
+ "The script merges, reconciles, records, and pushes under the merge lock. Informational only: RECONCILED, NO_REMOTE, NO_REMOTE_RECONCILE. Read the output:\n" +
30
+ "- STAGED_BASE_MISMATCH: the line left the staged base (stray checkout). VERDICT: FAIL.\n" +
31
+ "- 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" +
32
+ "- 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" +
33
+ "- STALE_MERGE: the recorded merge no longer matches the task branch (rework moved it after the merge) — the script refused to push stale state. Report the STALE_MERGE line, VERDICT: FAIL.\n" +
34
+ "- LOCK_HELD: 10-minute backoff exhausted. VERDICT: FAIL.\n" +
35
+ "- CONFLICT: the plain merge failed — aborted, the target is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
36
+ "RESOLUTION:\n" +
37
+ "R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): "+ lifecycleEnvPrefix(env) + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (the integration target is checked out in the repo, so create the scratch worktree from HEAD — never check out another branch in the repo checkout): cd " + env.repoPath + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " HEAD. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge " + env.taskBranch + ". This reproduces the exact conflict (the integration target has not moved — the lock was held throughout). The task branch " + env.taskBranch + " is never modified.\n" +
38
+ "R2. For each conflicted file, read the three sides: git show :1:<file> (base), git show :2:<file> (ours = the integration target), git show :3:<file> (theirs = task branch). Resolve each hunk by keeping both sides' changes when they do not semantically overlap. Version-only hunks resolve to the higher semver (safety net). Never invent new behavior. Leave no markers.\n" +
39
+ "R3. Verify in the scratch worktree, in this order: (a) git diff --check is clean; (b) git grep -n '^<<<<<<<' -- . returns nothing; (c) node --check every changed .js file (conflicted files plus everything listed by git diff --name-only). If any check fails, refresh the merge lock ("+ lifecycleEnvPrefix(env) + " refresh-lock " + taskId + "), then retry using the failure output as context — max 3 attempts total.\n" +
40
+ "R4. Commit the resolution on resolve/" + taskId + ": git add -A && git commit -m \"resolve conflicts: " + taskId + "\".\n" +
41
+ "R5. Back in " + env.repoPath + ": git merge --ff-only resolve/" + taskId + " — merges the resolution into the current checkout (the integration target; no git checkout anywhere). It fast-forwards — the target has not moved under the lock. Report the merged hash.\n" +
42
+ "R6. Clean up: cd " + env.repoPath + " && git worktree remove --force /tmp/crew-resolve-" + taskId + " && git branch -D resolve/" + taskId + ".\n" +
43
+ "ESCALATE — 'conflict needs human resolution', VERDICT: FAIL — when: 3 attempts are exhausted; the conflict touches generated files, migrations, or public API contracts; or 'looks right + checks pass' is not sufficient for any other reason. On escalation, RELEASE THE LOCK so the task can be reworked later: run CREW_HOME=" + env.crewHome + " CREW_REPO=" + env.repoPath + " " + env.mergeLock + " release " + taskId + " (release is keyed on task id; no PID needed). Do NOT run post-deploy on escalation — it would delete the untouched task branch the human still needs.\n" +
44
+ "- ERROR: report it. VERDICT: FAIL.\n\n" +
45
+ "R5-PUSH (manual R5 resolution only — the normal path pushed inline). Run: " + lifecycleEnvPrefix(env) + env.lifecycle + " push-target " + taskId + "\n" +
46
+ "PUSHED — report the merged hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; PUSH_SKIPPED — no push attempted (no record + no lock); NO_REMOTE_PUSH — no remote; VERDICT: PASS; ERROR or CONFLICT — report it, VERDICT: FAIL.\n" +
47
+ "NEVER force-push.\n\n" +
48
+ "Report what happened at each step, ending with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
49
+ }
50
+
51
+ export async function runPhase(ctx) {
52
+ var env = ctx.env, state = ctx.state;
53
+ var taskId = env.taskId;
54
+ var claimed = await ensureClaimed(env, state, PHASE);
55
+ if (claimed.type !== "CLAIMED") return claimed;
56
+
57
+ var boundary = await runWorkBoundary(env, state, {
58
+ phase: PHASE.name, identity: PHASE.identity,
59
+ instructions: buildInstructions(ctx),
60
+ eventPreamble: buildEventPreamble(env, PHASE.name),
61
+ crewApiLine: true, verdictStep: true,
62
+ });
63
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
64
+
65
+ var passed = closeoutPassed(boundary);
66
+ var workerText = boundary.workerText;
67
+
68
+ // See docs/decisions/publish-path.md#integrate-verify: the agent cannot
69
+ // verify integrate mechanically; the workflow checks the diff. The
70
+ // command's stdout is captured even on non-zero exit (the source's
71
+ // `2>&1 || true`); runCmd surfaces it on the thrown error.
72
+ var summary = workerText;
73
+ if (passed) {
74
+ var integrateVerifyOut = "";
75
+ try {
76
+ var vr = await lifecycle(env, "verify-merge", [taskId]);
77
+ integrateVerifyOut = String(vr.stdout || "").trim();
78
+ } catch (e) {
79
+ integrateVerifyOut = String((e && e.stdout) || "").trim();
80
+ }
81
+ if (/^VERIFIED:/m.test(integrateVerifyOut)) {
82
+ log("Integrate verified for task " + taskId + ": task branch tip is an ancestor of the integration target");
83
+ } else {
84
+ var integrateVerifyReason = integrateVerifyOut
85
+ ? integrateVerifyOut.split("\n")[0].slice(0, 200)
86
+ : "verify-merge produced no usable output";
87
+ log("Integrate verification failed for task " + taskId + ": " + integrateVerifyReason + " — marking failed for retry");
88
+ summary = (summary || "") + "\nintegrate_verify: FAILED — " + integrateVerifyReason;
89
+ passed = false;
90
+ }
91
+ }
92
+
93
+ // "rejected" is an explicit phase verdict routed through rework (Review).
94
+ // Integrate work that did not finish is operational — "failed",
95
+ // retryable under the dispatcher's consecutive-failure cap.
96
+ var status = passed ? "completed" : "failed";
97
+ var finalSummary = summarizeReport(summary);
98
+ await recordPhase(env, {
99
+ task_id: taskId,
100
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: finalSummary },
101
+ event: { task_id: taskId, type: status, identity: PHASE.identity, message: PHASE.name + " " + status + " by " + PHASE.identity },
102
+ });
103
+ if (!passed) {
104
+ log("Integration failed for task " + taskId + ": " + finalSummary);
105
+ return { type: "FAILED", reason: "Integration failed: " + finalSummary, detail: "The integrate step failed. The dispatcher will retry under its consecutive-failure cap." };
106
+ }
107
+ log("Integrate completed for task " + taskId);
108
+ return { type: "ADVANCE", next: "Publish" };
109
+ }
@@ -0,0 +1,81 @@
1
+ // lib/chore/phases/map.js — Map (Mara) phase module (sandbox exit,
2
+ // Piece 2). Import-safe: no side effects on import, bare `node` exits 0.
3
+ //
4
+ // Reads: experiential/baseline gate, task context, prior events, rejection
5
+ // notes. Writes: mapper specification (durable cross-run handoff in the
6
+ // Map session notes). A missing baseline bounces back to Capture — never
7
+ // to Build, never a task failure. captureTargets is dead/write-only in
8
+ // chore.js and is NOT ported.
9
+
10
+ import {
11
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
12
+ closeoutPassed, ensureClaimed, resolveExperiential, baselineStatus, log,
13
+ } from "../phase-lib.js";
14
+
15
+ export const PHASE = { name: "Map", identity: "mara" };
16
+
17
+ // buildInstructions — pure function of ctx. Verbatim from
18
+ // workflows/chore.js (Map branch). mapGate: {refs, none} from the gate.
19
+ export function buildInstructions(ctx, mapGate) {
20
+ var env = ctx.env;
21
+ var gate = mapGate || { refs: "", none: false };
22
+ var mapGatePara = "";
23
+ if (gate.refs || gate.none) {
24
+ mapGatePara = "\nBASELINE GATE (experiential task): " +
25
+ (gate.none
26
+ ? "no baseline was capturable (baseline: none recorded) — write the spec without baseline comparison and note it."
27
+ : "pre-change baseline captures: " + gate.refs + " — consult them when writing the spec.") +
28
+ " 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" +
29
+ "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).";
30
+ }
31
+ return "Research options, pick the path, write a clear spec for the builder.\n" +
32
+ (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 — build and review against this spec and the shared bar at " + env.uxDoctrinePath + ".\n" : "") +
33
+ (env.surfaceArtifact ? "ARTIFACT SPEC: this project's surface is a rendered artifact. Specify the exact screens, flows, and visual states the change affects — build and review against this spec and the shared bar at " + env.uxDoctrinePath + ".\n" : "") +
34
+ "The builder will edit source files in a git worktree of the project at " + env.repoPath + ".\nProject: " + env.projectDesc + "\nTo understand the current code, read source files directly using the read tool. Do NOT use artifact_inspect — it was removed by the platform (2026-09-14) and does not exist; do not substitute artifact.inspect (malfunction diagnosis, not an inspection tool).\nIdentify the exact files and changes needed. Be specific: file paths, what to add or change.\nReport back in plain prose — what you specified." + mapGatePara;
35
+ }
36
+
37
+ export async function runPhase(ctx) {
38
+ var env = ctx.env, state = ctx.state;
39
+ var taskId = env.taskId;
40
+ var claimed = await ensureClaimed(env, state, PHASE);
41
+ if (claimed.type !== "CLAIMED") return claimed;
42
+
43
+ // Map gate: experiential tasks need baseline evidence. Must match
44
+ // Capture's run condition (experiential + artifact surface): when
45
+ // Capture skips, no baseline notes exist, so the gate must not apply.
46
+ var mapGate = { refs: "", none: false };
47
+ if ((await resolveExperiential(env, state)) === "yes" && env.surfaceArtifact) {
48
+ var gateStatus = await baselineStatus(env);
49
+ if (!gateStatus.baseline_found) {
50
+ log("Map gate: no baseline evidence for experiential task " + taskId + " — bouncing to Capture");
51
+ await recordPhase(env, {
52
+ task_id: taskId,
53
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "failed", notes: "Map gate bounce: no baseline evidence recorded for this experiential task — no spec was written. Baseline evidence must be captured before the spec." },
54
+ event: { task_id: taskId, type: "failed", identity: PHASE.identity, message: "Map gate bounce — baseline evidence missing, returning to Capture" },
55
+ });
56
+ return { type: "REWIND", next: "Capture", reason: "Map gate bounce — baseline evidence missing" };
57
+ }
58
+ mapGate = { refs: gateStatus.baseline_refs, none: gateStatus.baseline_kind === "none" };
59
+ }
60
+
61
+ var boundary = await runWorkBoundary(env, state, {
62
+ phase: PHASE.name, identity: PHASE.identity,
63
+ instructions: buildInstructions(ctx, mapGate),
64
+ eventPreamble: buildEventPreamble(env, PHASE.name),
65
+ crewApiLine: true, verdictStep: false,
66
+ });
67
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
68
+
69
+ var passed = closeoutPassed(boundary);
70
+ var workerText = boundary.workerText;
71
+ var summary = summarizeReport(workerText);
72
+ var status = passed ? "completed" : "rejected";
73
+ await recordPhase(env, {
74
+ task_id: taskId,
75
+ session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: summary },
76
+ event: { task_id: taskId, type: status, identity: PHASE.identity, message: PHASE.name + " " + status + " by " + PHASE.identity },
77
+ });
78
+ // captureTargets is dead/write-only in the source — not ported.
79
+ log("Map " + status + " for task " + taskId);
80
+ return { type: "ADVANCE", next: "Build" };
81
+ }