muse-crew 0.17.1 → 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 (71) 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/compare-dispatch-shadow.js +35 -10
  39. package/lib/crew-api.js +273 -0
  40. package/lib/crew-dispatch-worker.js +27 -7
  41. package/lib/crew-release.sh +7 -2
  42. package/lib/extract.js +252 -0
  43. package/lib/prompts/tool-check.js +18 -0
  44. package/lib/prompts/trailers.js +59 -0
  45. package/lib/prompts/verdict-reask.js +31 -0
  46. package/lib/prompts/verdict-reask.schema.json +1 -0
  47. package/lib/prompts/work-agent.js +56 -0
  48. package/lib/prompts/work-agent.schema.json +1 -0
  49. package/lib/reap-spawns.js +407 -0
  50. package/lib/schema.sql +12 -1
  51. package/lib/spawn-keys.js +47 -0
  52. package/lib/spawn-step.js +572 -0
  53. package/lib/standard/phases/build.js +120 -0
  54. package/lib/standard/phases/capture.js +163 -0
  55. package/lib/standard/phases/integrate.js +172 -0
  56. package/lib/standard/phases/map.js +119 -0
  57. package/lib/standard/phases/publish.js +565 -0
  58. package/lib/standard/phases/qa.js +399 -0
  59. package/lib/standard/phases/review.js +281 -0
  60. package/lib/standard/phases/triage.js +64 -0
  61. package/lib/test-detached-integrate.sh +47 -0
  62. package/lib/workflow-driver.js +605 -0
  63. package/lib/workflow-lib.js +1012 -0
  64. package/lib/workflow-spec.js +187 -0
  65. package/lib/worktree-lifecycle.sh +55 -3
  66. package/package.json +1 -1
  67. package/seed/cron-body-template.md +61 -9
  68. package/workflows/bugfix.js +17 -17
  69. package/workflows/chore.js +16 -16
  70. package/workflows/docs.js +14 -11
  71. package/workflows/standard.js +16 -16
@@ -0,0 +1,165 @@
1
+ // lib/bugfix/phases/integrate.js — Bugfix Integrate phase (worker layer).
2
+ //
3
+ // Wren merges the task branch onto the integration target. Derived from
4
+ // workflows/bugfix.js (the Bugfix Integrate step).
5
+ //
6
+ // Contract:
7
+ // - Read: task branch (mechanical git facts); staged publish base
8
+ // (provenance); task title (for the merge message).
9
+ // - Wren merges in the current checkout via the lifecycle integrate
10
+ // command: `merge: fix: <safeTitle>`.
11
+ // - verify-merge is a direct lifecycle invocation (worker-layer
12
+ // equivalent of the source's agent ferry). Not VERIFIED →
13
+ // `integrate_verify: FAILED` appended, status failed (operational).
14
+ // - Optional QA deploy (versioned artifact builds only): failed deploy →
15
+ // `deploy: FAILED` appended, status failed (operational, never a park).
16
+ // - FAIL → status "failed" (operational, retried by the dispatcher at
17
+ // Integrate), NOT "rejected" (no rework routing).
18
+ // - PASS → ADVANCE to Publish.
19
+ //
20
+ // Phase I/O contract (Phase D):
21
+ // read: task branch, provenance (staged publish base), task title
22
+ // write: session (completed | failed), event (completed | failed)
23
+ // out: ADVANCE → Publish | FAILED (dispatcher retry) |
24
+ // NEED_SPAWN / STANDBY (boundary)
25
+
26
+ import {
27
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
28
+ ensureClaimed, log, lifecycle, lifecycleEnvPrefix, runCmd,
29
+ parseDeployResult, crewCmdString,
30
+ } from "../../workflow-lib.js";
31
+
32
+ export const PHASE = { name: "Integrate", identity: "wren" };
33
+
34
+ // buildInstructions — verbatim from workflows/bugfix.js (variable
35
+ // references remapped; logic and prose unchanged).
36
+ // ctx: { env, safeTitle, lockHolder }.
37
+ export function buildInstructions(ctx) {
38
+ var env = ctx.env;
39
+ var instructions = "Merge the approved task branch into the integration target and push the result.\n" +
40
+ "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" +
41
+ "NEVER run git checkout — the lifecycle merges into the current checkout; moving it abandons the line Publish verifies.\n\n" +
42
+ "Read the staged publish base (return its stdout verbatim):\n" + crewCmdString(env, "get-provenance", { project_id: env.projectId }) + "\n" +
43
+ "Then run: "+ lifecycleEnvPrefix(env) + "WORKFLOW_RUN_ID=" + ctx.lockHolder + " CREW_STAGED_BASE=<provenance.source_commit, empty when null> integrate " + env.taskId + " \"merge: fix: " + ctx.safeTitle + "\\\"\\n" +
44
+ "The script merges, reconciles, records, and pushes under the merge lock. Informational only: RECONCILED, NO_REMOTE, NO_REMOTE_RECONCILE. Read the output:\n" +
45
+ "- STAGED_BASE_MISMATCH: the line left the staged base (stray checkout). VERDICT: FAIL.\n" +
46
+ "- 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" +
47
+ "- 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" +
48
+ "- 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" +
49
+ "- LOCK_HELD: 10-minute backoff exhausted. VERDICT: FAIL.\n" +
50
+ "- 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" +
51
+ "RESOLUTION:\n" +
52
+ "R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): "+ lifecycleEnvPrefix(env) + " refresh-lock " + env.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/" + env.taskId + " /tmp/crew-resolve-" + env.taskId + " HEAD. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + env.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" +
53
+ "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" +
54
+ "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 " + env.taskId + "), then retry using the failure output as context — max 3 attempts total.\n" +
55
+ "R4. Commit the resolution on resolve/" + env.taskId + ": git add -A && git commit -m \"resolve conflicts: " + env.taskId + "\".\n" +
56
+ "R5. Back in " + env.repoPath + ": git merge --ff-only resolve/" + env.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" +
57
+ "R6. Clean up: cd " + env.repoPath + " && git worktree remove --force /tmp/crew-resolve-" + env.taskId + " && git branch -D resolve/" + env.taskId + ".\n" +
58
+ "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 " + env.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" +
59
+ "- ERROR: report it. VERDICT: FAIL.\n\n" +
60
+ "R5-PUSH (manual R5 resolution only — the normal path pushed inline). Run: " + lifecycleEnvPrefix(env) + env.lifecycle + " push-target " + env.taskId + "\n" +
61
+ "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" +
62
+ "NEVER force-push.\n\n" +
63
+ "Report what happened at each step, ending with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
64
+
65
+
66
+ return instructions;
67
+ }
68
+
69
+ // safeTitle — exact source transformation of the task title for the merge
70
+ // message.
71
+ function safeTitleOf(title) {
72
+ return String(title || "").replace(/"/g, "").replace(/\n/g, " ").slice(0, 72);
73
+ }
74
+
75
+ export async function runPhase(ctx) {
76
+ var env = ctx.env, state = ctx.state;
77
+
78
+ var claimed = await ensureClaimed(env, state, PHASE);
79
+ if (claimed.type !== "CLAIMED") return claimed;
80
+
81
+ ctx.safeTitle = safeTitleOf(env.taskTitle);
82
+ // lockHolder: taskId alone cannot distinguish two runs of the same task;
83
+ // the session id disambiguates (source: lockHolder = taskId/session).
84
+ ctx.lockHolder = env.taskId + "/" + state.activeSessionId;
85
+
86
+ var boundary = await runWorkBoundary(env, state, {
87
+ phase: PHASE.name,
88
+ identity: PHASE.identity,
89
+ instructions: buildInstructions(ctx),
90
+ eventPreamble: buildEventPreamble(env, PHASE.name),
91
+ crewApiLine: true,
92
+ });
93
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
94
+ var workerText = boundary.workerText;
95
+
96
+ // The agent cannot verify integrate mechanically; the workflow checks
97
+ // the diff (docs/decisions/publish-path.md#integrate-verify).
98
+ var passed = boundary.verdictPassed !== false;
99
+ var summary = workerText;
100
+ if (passed) {
101
+ var integrateVerifyOut = "";
102
+ try {
103
+ var vr = await lifecycle(env, "verify-merge", [env.taskId]);
104
+ integrateVerifyOut = String((vr.stdout || "") + "\n" + (vr.stderr || "")).trim();
105
+ } catch (e) {
106
+ integrateVerifyOut = "";
107
+ }
108
+ if (/^VERIFIED:/m.test(integrateVerifyOut)) {
109
+ log("Integrate verified for task " + env.taskId + ": task branch tip is an ancestor of the integration target");
110
+ // QA deploy: after verified merge, ensure QA env serves a bundle from
111
+ // merged source. Failed deploy = operational failure, never a park.
112
+ if (env.versionedBuild === true && env.surfaceArtifact) {
113
+ var deployOut = "";
114
+ try {
115
+ var dr = await runCmd(["node", env.runLib + "/qa-deploy.mjs",
116
+ "--repo", env.repoPath,
117
+ "--qa-dir", env.crewHome + "/qa-envs/" + env.projectId,
118
+ "--task", env.taskId,
119
+ "--crew-home", env.crewHome,
120
+ "--crew-api", env.crewApiPinned,
121
+ "--serve-artifact", env.crewHome + "/current/lib/serve-artifact.js",
122
+ ]);
123
+ deployOut = String(dr.stdout || "").trim();
124
+ } catch (e) {
125
+ deployOut = "";
126
+ }
127
+ var deployParsed = parseDeployResult(deployOut);
128
+ if (deployParsed.ok) {
129
+ log("QA deploy ok for task " + env.taskId + ": bundle " + deployParsed.hash);
130
+ summary = (summary || "") + "\ndeploy: ok " + deployParsed.hash;
131
+ } else {
132
+ log("QA deploy failed for task " + env.taskId + ": " + deployParsed.reason + " — marking failed for retry");
133
+ summary = (summary || "") + "\ndeploy: FAILED — " + deployParsed.reason;
134
+ passed = false;
135
+ }
136
+ }
137
+ } else {
138
+ var integrateVerifyReason = integrateVerifyOut
139
+ ? integrateVerifyOut.split("\n")[0].slice(0, 200)
140
+ : "verify-merge produced no usable output";
141
+ log("Integrate verification failed for task " + env.taskId + ": " + integrateVerifyReason + " — marking failed for retry");
142
+ summary = (summary || "") + "\nintegrate_verify: FAILED — " + integrateVerifyReason;
143
+ passed = false;
144
+ }
145
+ }
146
+
147
+ // Integrate work that did not finish is operational — "failed",
148
+ // retryable under the dispatcher's consecutive-failure cap (never
149
+ // "rejected": no rework routing).
150
+ var status = passed ? "completed" : "failed";
151
+ await recordPhase(env, {
152
+ task_id: env.taskId,
153
+ session: {
154
+ id: state.activeSessionId, task_id: env.taskId, identity: PHASE.identity,
155
+ step: PHASE.name, status: status, notes: summarizeReport(summary),
156
+ },
157
+ event: {
158
+ task_id: env.taskId, type: status, identity: PHASE.identity,
159
+ message: "Integrate " + status + " by " + PHASE.identity,
160
+ },
161
+ });
162
+
163
+ if (!passed) return { type: "FAILED" };
164
+ return { type: "ADVANCE", next: "Publish" };
165
+ }
@@ -0,0 +1,129 @@
1
+ // lib/bugfix/phases/map.js — Bugfix Map phase (worker layer).
2
+ //
3
+ // Mara researches the bug and writes a solution-oriented spec for Build.
4
+ // Derived from workflows/bugfix.js (the Bugfix Map step).
5
+ //
6
+ // Contract:
7
+ // - Read: Reproduce evidence (the completed Reproduce session notes are
8
+ // the handoff — Map does NOT inherit Standard's exact spec.md gate);
9
+ // task row (title, description); project surface and description.
10
+ // - Baseline gate (experiential + classified surface only — must match
11
+ // Capture's run condition): when no baseline evidence exists, record a
12
+ // "failed" session/event and REWIND to Capture (Map gate bounce). The
13
+ // driver's gateBounceCount gives Capture fresh spawn keys on the
14
+ // re-visit.
15
+ // - Instructions carry the baseline paragraph when baseline refs exist
16
+ // (or baseline:none was recorded), plus surface-specific spec guidance.
17
+ // - Closeout: standard session/event record; on PASS the truncated
18
+ // summary becomes Build's mapper spec (re-derived by Build from the
19
+ // durable Map session notes).
20
+ // - Map never rejects: a completed spec advances to Build.
21
+ //
22
+ // Phase I/O contract (Phase D):
23
+ // read: Reproduce session notes, task title/description, project
24
+ // surface/description, experiential flag, baseline status
25
+ // write: session (completed | failed [gate bounce]), event (completed |
26
+ // failed [gate bounce])
27
+ // out: ADVANCE → Build | REWIND → Capture (gate bounce) |
28
+ // NEED_SPAWN / STANDBY / FAILED (boundary)
29
+
30
+ import {
31
+ runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
32
+ ensureClaimed, log, resolveExperiential, baselineStatus,
33
+ } from "../../workflow-lib.js";
34
+
35
+ export const PHASE = { name: "Map", identity: "mara" };
36
+
37
+ // buildInstructions — verbatim from workflows/bugfix.js (variable
38
+ // references remapped to ctx.env; logic and prose unchanged).
39
+ // ctx: { env, state, mapBaselineRefs, mapBaselineNone }.
40
+ export function buildInstructions(ctx) {
41
+ var env = ctx.env;
42
+ var mapBaselineRefs = ctx.mapBaselineRefs || "";
43
+ var mapBaselineNone = !!ctx.mapBaselineNone;
44
+ var mapGatePara = "";
45
+ if (mapBaselineRefs || mapBaselineNone) {
46
+ mapGatePara = "\nBASELINE GATE (experiential task): " +
47
+ (mapBaselineNone
48
+ ? "no baseline was capturable (baseline: none recorded) — write the spec without baseline comparison and note it."
49
+ : "pre-change baseline evidence: " + mapBaselineRefs + " — consult it when writing the spec.") +
50
+ " 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" +
51
+ (env.surfaceTerminal
52
+ ? "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)."
53
+ : "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).");
54
+ }
55
+ return "Update the task with a solution-oriented spec. Research options, pick the shortest path.\n" +
56
+ (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" : "") +
57
+ (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" : "") +
58
+ "The builder will edit source files in a git worktree.\nProject: " + env.projectDesc + "\nWrite it clearly enough that the builder does not need to ask questions.\nReport back in plain prose — what you specified." + mapGatePara;
59
+ }
60
+
61
+ export async function runPhase(ctx) {
62
+ var env = ctx.env, state = ctx.state;
63
+
64
+ // Claim first: the baseline gate below records a "failed" session on the
65
+ // gate bounce, and that record needs the claimed session id (source
66
+ // parity: the source claims before instruction-building, which is where
67
+ // the gate lives).
68
+ var claimed = await ensureClaimed(env, state, PHASE);
69
+ if (claimed.type !== "CLAIMED") return claimed;
70
+
71
+ // Baseline gate — must match Capture's run condition (experiential +
72
+ // classified surface): when Capture skips, no baseline notes exist, so
73
+ // the gate must not apply.
74
+ var mapBaselineRefs = "";
75
+ var mapBaselineNone = false;
76
+ if ((await resolveExperiential(env, state)) === "yes" && (env.surfaceArtifact || env.surfaceTerminal)) {
77
+ var gateStatus = await baselineStatus(env);
78
+ if (!gateStatus.baseline_found) {
79
+ log("Map gate: no baseline evidence for experiential task " + env.taskId + " — bouncing to Capture");
80
+ await recordPhase(env, {
81
+ task_id: env.taskId,
82
+ session: {
83
+ id: state.activeSessionId, task_id: env.taskId, identity: PHASE.identity,
84
+ step: PHASE.name, status: "failed",
85
+ notes: "Map gate bounce: no baseline evidence recorded for this experiential task — no spec was written. Baseline evidence must be captured before the spec.",
86
+ },
87
+ event: {
88
+ task_id: env.taskId, type: "failed", identity: PHASE.identity,
89
+ message: "Map gate bounce — baseline evidence missing, returning to Capture",
90
+ },
91
+ });
92
+ // The driver's gateBounceCount increments on REWIND, giving Capture
93
+ // fresh spawn keys on the re-visit (worker-layer equivalent of the
94
+ // source's -g<N> bounce suffix).
95
+ return { type: "REWIND", next: "Capture" };
96
+ }
97
+ mapBaselineRefs = (gateStatus.baseline_refs || []).join(", ");
98
+ mapBaselineNone = (gateStatus.baseline_kind === "none");
99
+ }
100
+ ctx.mapBaselineRefs = mapBaselineRefs;
101
+ ctx.mapBaselineNone = mapBaselineNone;
102
+
103
+ var boundary = await runWorkBoundary(env, state, {
104
+ phase: PHASE.name,
105
+ identity: PHASE.identity,
106
+ instructions: buildInstructions(ctx),
107
+ eventPreamble: buildEventPreamble(env, PHASE.name),
108
+ crewApiLine: true,
109
+ visitSuffix: (state.gateBounceCount > 0) ? "-g" + state.gateBounceCount : "",
110
+ });
111
+ if (boundary.type !== "BOUNDARY_DONE") return boundary;
112
+
113
+ // Closeout: the truncated summary becomes Build's mapper spec. It is
114
+ // re-derived by Build from these durable session notes (the worker-layer
115
+ // equivalent of the source's in-memory mapperSpec).
116
+ var summary = summarizeReport(boundary.workerText);
117
+ await recordPhase(env, {
118
+ task_id: env.taskId,
119
+ session: {
120
+ id: state.activeSessionId, task_id: env.taskId, identity: PHASE.identity,
121
+ step: PHASE.name, status: "completed", notes: summary,
122
+ },
123
+ event: {
124
+ task_id: env.taskId, type: "completed", identity: PHASE.identity,
125
+ message: "Map completed by " + PHASE.identity,
126
+ },
127
+ });
128
+ return { type: "ADVANCE", next: "Build" };
129
+ }