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.
- package/API.md +11 -0
- package/docs/decisions/composition-machinery.md +180 -0
- package/docs/decisions/publish-path.md +6 -0
- package/docs/decisions/workflow-core.md +5 -4
- package/lib/AGENTS.md +2 -1
- package/lib/bugfix/phases/build.js +168 -0
- package/lib/bugfix/phases/capture.js +170 -0
- package/lib/bugfix/phases/integrate.js +165 -0
- package/lib/bugfix/phases/map.js +129 -0
- package/lib/bugfix/phases/publish.js +592 -0
- package/lib/bugfix/phases/qa.js +356 -0
- package/lib/bugfix/phases/reproduce.js +254 -0
- package/lib/bugfix/phases/review.js +292 -0
- package/lib/bugfix/phases/triage.js +69 -0
- package/lib/chore/CONTRACT.md +181 -0
- package/lib/chore/DISPOSITION.md +98 -0
- package/lib/chore/extract.js +204 -0
- package/lib/chore/phase-lib.js +885 -0
- package/lib/chore/phases/build.js +110 -0
- package/lib/chore/phases/capture.js +82 -0
- package/lib/chore/phases/integrate.js +109 -0
- package/lib/chore/phases/map.js +81 -0
- package/lib/chore/phases/publish.js +543 -0
- package/lib/chore/phases/review.js +255 -0
- package/lib/chore/phases/triage.js +57 -0
- package/lib/chore/prompts/evidence-gatherer.js +41 -0
- package/lib/chore/prompts/evidence-gatherer.schema.json +1 -0
- package/lib/chore/prompts/tool-check.js +15 -0
- package/lib/chore/prompts/trailers.js +56 -0
- package/lib/chore/prompts/verdict-reask.js +28 -0
- package/lib/chore/prompts/verdict-reask.schema.json +1 -0
- package/lib/chore/prompts/work-agent.js +52 -0
- package/lib/chore/prompts/work-agent.schema.json +1 -0
- package/lib/chore/spawn-keys.js +44 -0
- package/lib/chore/spawn-vocab.js +87 -0
- package/lib/chore-run.js +538 -0
- package/lib/chore-tick.js +289 -0
- package/lib/compare-dispatch-shadow.js +35 -10
- package/lib/crew-api.js +273 -0
- package/lib/crew-dispatch-worker.js +27 -7
- package/lib/crew-release.sh +7 -2
- package/lib/extract.js +252 -0
- package/lib/prompts/tool-check.js +18 -0
- package/lib/prompts/trailers.js +59 -0
- package/lib/prompts/verdict-reask.js +31 -0
- package/lib/prompts/verdict-reask.schema.json +1 -0
- package/lib/prompts/work-agent.js +56 -0
- package/lib/prompts/work-agent.schema.json +1 -0
- package/lib/reap-spawns.js +407 -0
- package/lib/schema.sql +12 -1
- package/lib/spawn-keys.js +47 -0
- package/lib/spawn-step.js +572 -0
- package/lib/standard/phases/build.js +120 -0
- package/lib/standard/phases/capture.js +163 -0
- package/lib/standard/phases/integrate.js +172 -0
- package/lib/standard/phases/map.js +119 -0
- package/lib/standard/phases/publish.js +565 -0
- package/lib/standard/phases/qa.js +399 -0
- package/lib/standard/phases/review.js +281 -0
- package/lib/standard/phases/triage.js +64 -0
- package/lib/test-detached-integrate.sh +47 -0
- package/lib/workflow-driver.js +605 -0
- package/lib/workflow-lib.js +1012 -0
- package/lib/workflow-spec.js +187 -0
- package/lib/worktree-lifecycle.sh +55 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +61 -9
- package/workflows/bugfix.js +17 -17
- package/workflows/chore.js +16 -16
- package/workflows/docs.js +14 -11
- package/workflows/standard.js +16 -16
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// lib/chore/phases/publish.js — Publish (Wren) phase module (sandbox
|
|
2
|
+
// exit, Piece 2). Import-safe: no side effects on import, bare `node`
|
|
3
|
+
// exits 0.
|
|
4
|
+
//
|
|
5
|
+
// P4 RESOLVED (2026-09-26, Eric's call): the artifact ledger `commit`
|
|
6
|
+
// field used to be null for the early (pre-diff) ledger entries — the
|
|
7
|
+
// source declared `var mergeCommitForPublish` AFTER those entries
|
|
8
|
+
// (hoisting), so the early references were `undefined` and
|
|
9
|
+
// recordPublishLedger wrote `commit: null`. That defect was preserved
|
|
10
|
+
// through the mechanical port; it is now fixed in the pilot: the merged
|
|
11
|
+
// commit is resolved up front from the durable merge record
|
|
12
|
+
// ($CREW_HOME/.merge-records/<task>, newest merge_commit= wins), so the
|
|
13
|
+
// early preflight ledger entries carry the real commit. The later
|
|
14
|
+
// diff-derived `mergeCommitForPublish` declaration is unchanged and still
|
|
15
|
+
// feeds everything downstream of the diff computation.
|
|
16
|
+
//
|
|
17
|
+
// npm path: the workflow computes the target version deterministically
|
|
18
|
+
// (registry base + bump); the work agent runs exactly one command (the
|
|
19
|
+
// pinned publish script); the workflow verifies the registry version
|
|
20
|
+
// and refreshes provenance mechanically.
|
|
21
|
+
// artifact path: fully mechanical before the work agent — re-entry guard,
|
|
22
|
+
// lock-status/refresh, artifact preflight, provenance base, checksummed
|
|
23
|
+
// diff, intent ledger anchor — then parks at publish intent for the tick
|
|
24
|
+
// worker. The work agent only reports on the mechanical outcome.
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
runWorkBoundary, recordPhase, buildEventPreamble, summarizeReport,
|
|
28
|
+
closeoutPassed, ensureClaimed, latestSessionNotes, verifyPin,
|
|
29
|
+
pinLifecycle, parkTask, lifecycle, crewApi, recordPublishLedger,
|
|
30
|
+
runCmd, log,
|
|
31
|
+
} from "../phase-lib.js";
|
|
32
|
+
import {
|
|
33
|
+
extractPublishSkipped, extractReleaseDecision, bumpVersion,
|
|
34
|
+
} from "../extract.js";
|
|
35
|
+
import { attemptKey } from "../spawn-keys.js";
|
|
36
|
+
import { homedir } from "os";
|
|
37
|
+
import { statSync } from "fs";
|
|
38
|
+
import { join } from "path";
|
|
39
|
+
|
|
40
|
+
export const PHASE = { name: "Publish", identity: "wren" };
|
|
41
|
+
const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
42
|
+
|
|
43
|
+
// buildNpmInstructions — verbatim from workflows/chore.js (Publish/npm).
|
|
44
|
+
export function buildNpmInstructions(ctx, publishTarget) {
|
|
45
|
+
var env = ctx.env;
|
|
46
|
+
var taskId = env.taskId;
|
|
47
|
+
return "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
48
|
+
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
49
|
+
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
50
|
+
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make; the script itself skips gracefully when no merge lock is held.\n\n" +
|
|
51
|
+
"Run exactly this command and no other publish-related commands:\n" +
|
|
52
|
+
"TASK_ID=" + taskId + " REPO_PATH=" + env.repoPath + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
53
|
+
" CREW_HOME=" + env.crewHome + " LIFECYCLE=" + env.lifecycle + " RELEASE_SCRIPT=" + env.releaseScript +
|
|
54
|
+
" bash " + env.publishNpm + "\n\n" +
|
|
55
|
+
"Do NOT run npm, npm pack, npm publish, or the python publish script yourself. Do NOT compare local and registry versions. Do NOT decide whether to publish.\n\n" +
|
|
56
|
+
"If the command exits nonzero, put the script's PUBLISH_FAILED line in your report, then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
57
|
+
"If the script's output contains PUBLISH_SKIPPED=no-lock-held, the publish was skipped gracefully: Integrate reported MERGED_EMPTY (no commits ahead of the integration target), so no merge lock was taken and there is nothing to ship. Paste the marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
58
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
59
|
+
"skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
|
|
60
|
+
"VERDICT: PASS\n\n" +
|
|
61
|
+
"If the script's output contains PUBLISH_SKIPPED=no-npm-publish, the publish was skipped gracefully: npm publish is not configured on this machine (helper or credential absent) — the merge stands, the version was not cut, nothing was shipped. Paste the marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
62
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
63
|
+
"skipped: no-npm-publish (npm publish not configured — helper or credential missing; nothing versioned or published)\n" +
|
|
64
|
+
"VERDICT: PASS\n\n" +
|
|
65
|
+
"If it exits zero, paste the script's COMPLETE marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
66
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
67
|
+
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
68
|
+
"VERDICT: PASS\n\n";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Artifact-path instruction framings (verbatim from the source).
|
|
72
|
+
export function buildArtifactInstructions(env, framing) {
|
|
73
|
+
var taskId = env.taskId;
|
|
74
|
+
if (framing === "verified") {
|
|
75
|
+
return "Publish was already completed and verified for this task — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
76
|
+
"Provenance stamps this task's merge commit as published (docs/publish-verification.md): the tick worker issued the edit, the ack scan observed the artifact's exact version acknowledgement — the sole positive completion criterion — and the publish was stamped. Already verified, nothing to issue and nothing to re-verify.\n\n" +
|
|
77
|
+
"For the change summary, run: cd " + env.repoPath + " && git log -1 --stat\n\n" +
|
|
78
|
+
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
79
|
+
"The VERDICT line must be the last line of your report.";
|
|
80
|
+
}
|
|
81
|
+
if (framing === "skipped") {
|
|
82
|
+
return "Publish was skipped deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
83
|
+
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of the integration target), so no merge lock was taken and there is nothing to ship. Post-deploy step finished per its return — do NOT run post-deploy yourself.\n\n" +
|
|
84
|
+
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
85
|
+
"The VERDICT line must be the last line of your report.";
|
|
86
|
+
}
|
|
87
|
+
return "Publish the merged code to the live artifact.\n\n" +
|
|
88
|
+
"The publish was performed deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would trigger a duplicate build or disturb the finalized state. You perform no publish actions.\n\n" +
|
|
89
|
+
"For the change summary, run: cd " + env.repoPath + " && git log -1 --stat\n\n" +
|
|
90
|
+
"Mechanical outcome (the workflow's mechanical steps; the ack scan's version-acknowledgement check is still pending):\n" +
|
|
91
|
+
"- merge lock refreshed: yes\n" +
|
|
92
|
+
"- artifact rebuild triggered and completed: yes\n" +
|
|
93
|
+
"- provenance stamped: NO — not yet, and you must NOT stamp it. The ack scan stamps it when it observes the artifact's exact on-disk acknowledgement of this attempt's version — version acknowledgement is the sole positive completion criterion (docs/publish-verification.md). A stamped-but-hollow build is exactly how canary run 8 (2026-09-11) went green on stale content.\n" +
|
|
94
|
+
"- post-deploy finalized: yes (worktree removed, merge lock released)\n\n" +
|
|
95
|
+
"POLICY: The live artifact is rebuilt only in this phase, from the repo. Never use artifact_edit to change the artifact directly — fixes go through the repo and the loop. A source fix is not done until the artifact is rebuilt from it here.\n\n" +
|
|
96
|
+
"The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
|
|
97
|
+
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
98
|
+
"The VERDICT line must be the last line of your report.";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// runArtifactPath — the deterministic artifact publish (source: the
|
|
102
|
+
// PUBLISH_TYPE === "artifact" instructions branch). Runs after the claim,
|
|
103
|
+
// before the work agent. Returns a PARK result, or {mechanical, framing}
|
|
104
|
+
// for the work-agent report step.
|
|
105
|
+
async function runArtifactPath(env, state) {
|
|
106
|
+
var taskId = env.taskId;
|
|
107
|
+
|
|
108
|
+
// Blocker 22 (one-party publish, room #24): verified re-entry guard.
|
|
109
|
+
var publishAlreadyVerified = false;
|
|
110
|
+
try {
|
|
111
|
+
var reentryProv = await crewApi(env, "get-provenance", { project_id: env.projectId });
|
|
112
|
+
var reentryP = (reentryProv && reentryProv.provenance) || null;
|
|
113
|
+
var reentrySC = String((reentryP && reentryP.source_commit) || "").trim();
|
|
114
|
+
var reentryTID = String((reentryP && reentryP.task_id) || "").trim();
|
|
115
|
+
var headOut = await runCmd(["git", "rev-parse", "HEAD"], { cwd: env.repoPath });
|
|
116
|
+
var reentryHEAD = String(headOut.stdout || "").trim();
|
|
117
|
+
if (reentrySC && reentryHEAD && reentrySC === reentryHEAD && reentryTID === taskId) {
|
|
118
|
+
publishAlreadyVerified = true;
|
|
119
|
+
log("Publish verified re-entry for task " + taskId + ": provenance stamps " + reentrySC + " == HEAD for this task — the parent already published and verified it; issuance skipped");
|
|
120
|
+
}
|
|
121
|
+
} catch (reentryErr) {
|
|
122
|
+
log("Publish re-entry guard threw for task " + taskId + " (" + (reentryErr && reentryErr.message ? reentryErr.message : reentryErr) + ") — proceeding with the normal publish path; a guard failure never claims verification");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
var publishLockRefreshed = false;
|
|
126
|
+
var publishSkippedNoLock = false;
|
|
127
|
+
try {
|
|
128
|
+
// STEP 0 (mechanical): read the merge-lock state explicitly — never
|
|
129
|
+
// infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
|
|
130
|
+
// intentionally takes no lock: nothing merged, nothing to ship.
|
|
131
|
+
var lockOut = await lifecycle(env, "lock-status", [taskId]);
|
|
132
|
+
if (String(lockOut.stdout || "").trim() === "UNLOCKED") {
|
|
133
|
+
publishSkippedNoLock = true;
|
|
134
|
+
log("Publish skipped for task " + taskId + ": no merge lock held (empty-diff Integrate) — nothing to ship");
|
|
135
|
+
} else {
|
|
136
|
+
var refreshOut = "";
|
|
137
|
+
var refreshed = false;
|
|
138
|
+
try {
|
|
139
|
+
var rr = await lifecycle(env, "refresh-lock", [taskId]);
|
|
140
|
+
refreshed = true;
|
|
141
|
+
refreshOut = String(rr.stdout || "").trim();
|
|
142
|
+
} catch (e) {
|
|
143
|
+
refreshed = false;
|
|
144
|
+
refreshOut = String((e && e.message) || e);
|
|
145
|
+
}
|
|
146
|
+
if (!refreshed) {
|
|
147
|
+
return await parkTask(env, "Publish lock refresh failed: " + (refreshOut || "no output") + ". Fail-closed — lock state unknown.");
|
|
148
|
+
}
|
|
149
|
+
publishLockRefreshed = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!publishSkippedNoLock && !publishAlreadyVerified) {
|
|
153
|
+
// The trigger key of the attempt that last ran, for the publish
|
|
154
|
+
// ledger. Minted once here (not re-minted per use site).
|
|
155
|
+
var rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId, state.reworkCount);
|
|
156
|
+
|
|
157
|
+
// The merged commit is knowable before the diff is computed: the
|
|
158
|
+
// Integrate phase recorded it durably in the merge record
|
|
159
|
+
// ($CREW_HOME/.merge-records/<task>). The early preflight ledger
|
|
160
|
+
// writes below used to carry commit: null (the P4 hoisted-var
|
|
161
|
+
// defect, preserved from the source through the mechanical port);
|
|
162
|
+
// resolving the commit here gives them the real value. Newest
|
|
163
|
+
// merge_commit= block wins, per the record's own convention. A
|
|
164
|
+
// missing/unreadable record falls back to "" — recordPublishLedger
|
|
165
|
+
// writes commit: null for it, exactly today's behavior.
|
|
166
|
+
var mergedCommitForPublish = "";
|
|
167
|
+
try {
|
|
168
|
+
var mrOut = await lifecycle(env, "merge-record", [taskId]);
|
|
169
|
+
var mrText = String((mrOut && mrOut.stdout) || "");
|
|
170
|
+
var mrMatch;
|
|
171
|
+
var mrRe = /^merge_commit=([0-9a-f]{40})$/gm;
|
|
172
|
+
while ((mrMatch = mrRe.exec(mrText)) !== null) mergedCommitForPublish = mrMatch[1];
|
|
173
|
+
} catch (e) {
|
|
174
|
+
log("Publish could not read the merge record for task " + taskId + " — early ledger entries will carry commit: null");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// STEP 0.5 (mechanical, room #16 blocker 10): assert the artifact
|
|
178
|
+
// target exists before any publish.
|
|
179
|
+
var artifactTargetDir = join(homedir(), "workspace", "ts-spaces", env.publishSlug) + "/";
|
|
180
|
+
var preflightSignal = "";
|
|
181
|
+
var preflightInconclusive = false;
|
|
182
|
+
if (!env.publishSlug) {
|
|
183
|
+
await recordPublishLedger(env, {
|
|
184
|
+
commit: mergedCommitForPublish,
|
|
185
|
+
attempt: rebuildAttemptKey,
|
|
186
|
+
agent_id: null,
|
|
187
|
+
applied_report: null,
|
|
188
|
+
outcome: "rejected",
|
|
189
|
+
detail: "artifact surface with empty deploy_slug (preflight): the project is classified as artifact but has no deploy_slug — misconfiguration, not a missing artifact. No edit was issued."
|
|
190
|
+
});
|
|
191
|
+
return await parkTask(env, "Publish cannot proceed for task " + taskId + ": the project is classified as an artifact surface but has no deploy_slug. This is a project configuration defect — set a deploy_slug for the project, then re-run Publish. Human attention needed.");
|
|
192
|
+
}
|
|
193
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(env.publishSlug)) {
|
|
194
|
+
preflightInconclusive = true;
|
|
195
|
+
log("Publish artifact preflight for task " + taskId + ": PUBLISH_SLUG has an unsafe shape — inconclusive, fail-closed");
|
|
196
|
+
} else {
|
|
197
|
+
try {
|
|
198
|
+
var st = statSync(join(homedir(), "workspace", "ts-spaces", env.publishSlug));
|
|
199
|
+
preflightSignal = st.isDirectory() ? "ARTIFACT_TARGET: present" : "ARTIFACT_TARGET: missing";
|
|
200
|
+
} catch (e) {
|
|
201
|
+
if (e && e.code === "ENOENT") {
|
|
202
|
+
preflightSignal = "ARTIFACT_TARGET: missing";
|
|
203
|
+
} else {
|
|
204
|
+
preflightInconclusive = true;
|
|
205
|
+
log("Publish artifact preflight for task " + taskId + " threw (" + (e && e.message ? e.message : e) + ") — inconclusive, fail-closed");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!preflightInconclusive && /ARTIFACT_TARGET:\s*missing/.test(preflightSignal)) {
|
|
210
|
+
await recordPublishLedger(env, {
|
|
211
|
+
commit: mergedCommitForPublish,
|
|
212
|
+
attempt: rebuildAttemptKey,
|
|
213
|
+
agent_id: null,
|
|
214
|
+
applied_report: null,
|
|
215
|
+
outcome: "rejected",
|
|
216
|
+
detail: "artifact target directory missing (preflight): " + artifactTargetDir + " does not exist — setup never provisioned the artifact for deploy_slug " + env.publishSlug + ". The edit provably did not go through: no trigger issued, no blind retry"
|
|
217
|
+
});
|
|
218
|
+
return await parkTask(env, "Publish cannot proceed for task " + taskId + ": the artifact target directory " + artifactTargetDir + " does not exist. The project is classified as an artifact surface (deploy_slug " + env.publishSlug + ") but setup never provisioned the artifact — this is conclusive (rejected, not unknown): no edit was issued. Create the artifact via the Muse UI (Publish edits an existing artifact; it never creates one), then re-run init and Publish. Human attention needed.");
|
|
219
|
+
}
|
|
220
|
+
if (preflightInconclusive || !/ARTIFACT_TARGET:\s*present/.test(preflightSignal)) {
|
|
221
|
+
await recordPublishLedger(env, {
|
|
222
|
+
commit: mergedCommitForPublish,
|
|
223
|
+
attempt: rebuildAttemptKey,
|
|
224
|
+
agent_id: null,
|
|
225
|
+
applied_report: null,
|
|
226
|
+
outcome: "unknown",
|
|
227
|
+
detail: "artifact preflight inconclusive (no parsable ARTIFACT_TARGET signal): target existence unproven, so the trigger was NOT issued; unknown parks fail closed with no blind retry"
|
|
228
|
+
});
|
|
229
|
+
return await parkTask(env, "Publish cannot proceed for task " + taskId + ": the artifact target preflight was inconclusive (no parsable signal). Target existence is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
|
|
230
|
+
}
|
|
231
|
+
log("Publish artifact preflight for task " + taskId + ": target " + artifactTargetDir + " present");
|
|
232
|
+
|
|
233
|
+
// Provenance base + checksummed diff (docs/decisions/publish-path.md).
|
|
234
|
+
var provResult = await crewApi(env, "get-provenance", { project_id: env.projectId });
|
|
235
|
+
var provP = (provResult && provResult.provenance) || null;
|
|
236
|
+
var publishBase = String((provP && provP.source_commit) || "").trim();
|
|
237
|
+
if (!publishBase) {
|
|
238
|
+
publishBase = EMPTY_TREE_SHA;
|
|
239
|
+
log("Publish base for task " + taskId + ": no provenance stamped yet — using empty tree (first publish)");
|
|
240
|
+
} else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
|
|
241
|
+
return await parkTask(env, "Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.");
|
|
242
|
+
}
|
|
243
|
+
var publishDiffFile = join(env.crewHome, ".publish-diffs", taskId + (state.reworkCount > 0 ? "-r" + state.reworkCount : "") + ".diff");
|
|
244
|
+
var diffOut;
|
|
245
|
+
try {
|
|
246
|
+
diffOut = await runCmd(["node", env.computeDiff, "--repo-path", env.repoPath, "--base", publishBase, "--task-id", taskId, "--attempt", "1", "--out", publishDiffFile]);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
throw new Error("publish diff computation failed: " + ((e && e.message) || e));
|
|
249
|
+
}
|
|
250
|
+
var diffSummary = null;
|
|
251
|
+
try {
|
|
252
|
+
diffSummary = JSON.parse(String(diffOut.stdout || "").trim());
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return await parkTask(env, "Publish diff computation returned unparseable output — cannot verify application. Human attention needed.");
|
|
255
|
+
}
|
|
256
|
+
if (!diffSummary || diffSummary.ok !== true) {
|
|
257
|
+
if (diffSummary && diffSummary.error === "not-ancestor") {
|
|
258
|
+
return await parkTask(env, "Publish base " + publishBase.slice(0, 12) + " is not an ancestor of HEAD " + String(diffSummary.commit || "").slice(0, 12) + " — the stamped provenance does not lead to the integrated commit. Human attention needed.");
|
|
259
|
+
}
|
|
260
|
+
return await parkTask(env, "Publish diff computation failed (" + ((diffSummary && diffSummary.error) || "unknown error") + ") — cannot verify application. Human attention needed.");
|
|
261
|
+
}
|
|
262
|
+
if (diffSummary.base !== publishBase) {
|
|
263
|
+
return await parkTask(env, "Publish diff base mismatch: script reported '" + String(diffSummary.base || "").slice(0, 12) + "' but the stamped base is '" + publishBase.slice(0, 12) + "'. Human attention needed.");
|
|
264
|
+
}
|
|
265
|
+
// The diff-derived commit, used for everything downstream of the
|
|
266
|
+
// diff computation. (The early preflight ledger writes above now
|
|
267
|
+
// carry the merge-record commit resolved at the top of this block.)
|
|
268
|
+
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
269
|
+
var mergeCommitShortForPublish = String(mergeCommitForPublish).substring(0, 7) || "unknown";
|
|
270
|
+
void mergeCommitShortForPublish;
|
|
271
|
+
var publishDiffSha256 = String(diffSummary.sha256 || "");
|
|
272
|
+
var publishVersion = String(diffSummary.version || "");
|
|
273
|
+
// Mirrors isPublishVersion() in lib/crew-api.js: <40-hex commit>:<attempt>.
|
|
274
|
+
if (!/^[0-9a-f]{40}:[1-9][0-9]*$/.test(publishVersion)) {
|
|
275
|
+
return await parkTask(env, "Publish diff for commit " + (mergeCommitForPublish || "unknown") + " carries no usable publish version — cannot stage a verifiable intent. Human attention needed.");
|
|
276
|
+
}
|
|
277
|
+
if (!diffSummary.bytes) {
|
|
278
|
+
return await parkTask(env, "Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
|
|
279
|
+
}
|
|
280
|
+
if (diffSummary.has_binary) {
|
|
281
|
+
return await parkTask(env, "Publish diff contains binary files — the diff transport cannot carry them. Human attention needed.");
|
|
282
|
+
}
|
|
283
|
+
if (diffSummary.has_rename) {
|
|
284
|
+
return await parkTask(env, "Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
|
|
285
|
+
}
|
|
286
|
+
var mergeDiffChangedLines = diffSummary.changed_lines || 0;
|
|
287
|
+
if (mergeDiffChangedLines > 200) {
|
|
288
|
+
return await parkTask(env, "Publish diff changes " + mergeDiffChangedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
289
|
+
}
|
|
290
|
+
if (!diffSummary.file_count) {
|
|
291
|
+
return await parkTask(env, "Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
|
|
292
|
+
}
|
|
293
|
+
// The intent ledger entry is the anchor the whole machine depends
|
|
294
|
+
// on. Fail loudly instead of parking an orphan intent.
|
|
295
|
+
var intentAnchored = await recordPublishLedger(env, {
|
|
296
|
+
commit: mergeCommitForPublish,
|
|
297
|
+
attempt: rebuildAttemptKey,
|
|
298
|
+
publish_attempt: 1,
|
|
299
|
+
version: publishVersion,
|
|
300
|
+
agent_id: null,
|
|
301
|
+
applied_report: null,
|
|
302
|
+
outcome: "publish-intent",
|
|
303
|
+
issuer: "workflow",
|
|
304
|
+
diff_path: publishDiffFile,
|
|
305
|
+
diff_sha256: publishDiffSha256,
|
|
306
|
+
base: publishBase,
|
|
307
|
+
detail: "publish intent: preflight and checksummed diff are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
|
|
308
|
+
});
|
|
309
|
+
if (!intentAnchored) {
|
|
310
|
+
throw new Error("publish-intent ledger anchor write unconfirmed for task " + taskId + " — refusing to park an unclaimable intent; the Publish phase fails loudly instead");
|
|
311
|
+
}
|
|
312
|
+
return await parkTask(env, "publish: publish-requested " + mergeCommitForPublish + " attempt=1 — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") carrying the per-attempt publish version for the crew's publish worker to issue. The worker issues the edit directly and the acknowledgement scan owns the task from there; no action needed.");
|
|
313
|
+
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
314
|
+
|
|
315
|
+
// STEP 2 (mechanical, always — skip path included): post-deploy.
|
|
316
|
+
var pdOut = await lifecycle(env, "post-deploy", [taskId]);
|
|
317
|
+
var postDeploy = { deployed: /DEPLOYED/.test(String(pdOut.stdout || "")), output: String(pdOut.stdout || "").trim() };
|
|
318
|
+
if (publishAlreadyVerified) {
|
|
319
|
+
log("Publish verified re-entry for task " + taskId + " — post-deploy ran forgivingly; continuing to the verified work-agent framing");
|
|
320
|
+
} else if (publishSkippedNoLock) {
|
|
321
|
+
if (!postDeploy.deployed) {
|
|
322
|
+
return await parkTask(env, "Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
323
|
+
}
|
|
324
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy step finished");
|
|
325
|
+
}
|
|
326
|
+
} catch (pubErr) {
|
|
327
|
+
// Best-effort cleanup: if the lock was refreshed, try to release it
|
|
328
|
+
// before parking so the failure cannot wedge the next run.
|
|
329
|
+
if (publishLockRefreshed) {
|
|
330
|
+
try {
|
|
331
|
+
await lifecycle(env, "post-deploy", [taskId]);
|
|
332
|
+
} catch (cleanupErr) {
|
|
333
|
+
log("Publish cleanup post-deploy also failed: " + (cleanupErr && cleanupErr.message ? cleanupErr.message : cleanupErr));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return await parkTask(env, "Deterministic artifact publish failed: " + (pubErr && pubErr.message ? pubErr.message : pubErr) + ". Fail-closed.");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
mechanical: true,
|
|
341
|
+
framing: publishAlreadyVerified ? "verified" : (publishSkippedNoLock ? "skipped" : "normal"),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function runPhase(ctx) {
|
|
346
|
+
var env = ctx.env, state = ctx.state;
|
|
347
|
+
var taskId = env.taskId;
|
|
348
|
+
|
|
349
|
+
// ── Pre-claim ──
|
|
350
|
+
var pinCheck = verifyPin(env);
|
|
351
|
+
var missingAtPublish = pinCheck.missing;
|
|
352
|
+
if (missingAtPublish.length > 0) {
|
|
353
|
+
log("Pin guard at Publish: missing " + missingAtPublish.join(", ") + " — re-pinning once");
|
|
354
|
+
pinLifecycle(env);
|
|
355
|
+
missingAtPublish = verifyPin(env).missing;
|
|
356
|
+
}
|
|
357
|
+
if (missingAtPublish.length > 0) {
|
|
358
|
+
return await parkTask(env, "Pinned lifecycle scripts missing at Publish even after re-pin: " + missingAtPublish.join(", ") + " in " + env.runLib + ".");
|
|
359
|
+
}
|
|
360
|
+
log("Pin guard: all four lifecycle scripts present at Publish");
|
|
361
|
+
|
|
362
|
+
// Publish is optional and target-based. Empty target = prototyping
|
|
363
|
+
// project: skip the phase. Unknown target (incl. legacy "repo") =
|
|
364
|
+
// config error: block.
|
|
365
|
+
if (!env.publishType) {
|
|
366
|
+
log("Publish skipped for task " + taskId + " — no publish target configured (deploy_type empty)");
|
|
367
|
+
await lifecycle(env, "post-deploy", [taskId]);
|
|
368
|
+
return { type: "ADVANCE", next: null };
|
|
369
|
+
}
|
|
370
|
+
if (env.publishType !== "npm" && env.publishType !== "artifact" && env.publishType !== "vercel") {
|
|
371
|
+
log("Unknown publish target for task " + taskId + ": " + env.publishType);
|
|
372
|
+
return await parkTask(env, "Unknown publish target '" + env.publishType + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish).");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Merge-time versioning, decided deterministically: the release decision
|
|
376
|
+
// was extracted from the accepted Build report (re-derived here from
|
|
377
|
+
// the durable Build session notes). release: no never reaches the
|
|
378
|
+
// Publish agent (skipped here, exactly like the no-target case); an
|
|
379
|
+
// unparseable decision blocks fail-closed.
|
|
380
|
+
var publishTarget = null;
|
|
381
|
+
if (env.publishType === "npm") {
|
|
382
|
+
var buildNotes = await latestSessionNotes(env, "Build", "completed");
|
|
383
|
+
var releaseDecision = extractReleaseDecision(buildNotes);
|
|
384
|
+
if (!releaseDecision) {
|
|
385
|
+
return await parkTask(env, "Build report has no machine-readable release:/version_bump: declaration — cannot assign version at publish time.");
|
|
386
|
+
}
|
|
387
|
+
if (releaseDecision.release === "no") {
|
|
388
|
+
log("Publish skipped for task " + taskId + " — accepted Build report declared release: no");
|
|
389
|
+
await lifecycle(env, "post-deploy", [taskId]);
|
|
390
|
+
return { type: "ADVANCE", next: null };
|
|
391
|
+
}
|
|
392
|
+
// Deterministic pre-publish: the workflow reads the registry base and
|
|
393
|
+
// computes the target version. The Publish agent never does version
|
|
394
|
+
// math.
|
|
395
|
+
try {
|
|
396
|
+
var baseResult;
|
|
397
|
+
try {
|
|
398
|
+
baseResult = String((await runCmd(["npm", "view", "muse-crew", "version"])).stdout || "").trim();
|
|
399
|
+
} catch (e) {
|
|
400
|
+
baseResult = "NOT_FOUND";
|
|
401
|
+
}
|
|
402
|
+
if (baseResult === "NOT_FOUND") {
|
|
403
|
+
try {
|
|
404
|
+
baseResult = String((await runCmd(["node", "-p", "require('" + env.repoPath + "/package.json').version"])).stdout || "").trim();
|
|
405
|
+
} catch (e2) {
|
|
406
|
+
baseResult = "";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
var pubScope = releaseDecision.version_bump || "patch";
|
|
410
|
+
var pubBase = (baseResult || "").trim();
|
|
411
|
+
if (!/^\d+\.\d+\.\d+$/.test(pubBase)) {
|
|
412
|
+
return await parkTask(env, "Publish base version unreadable: '" + pubBase + "'. Fail-closed.");
|
|
413
|
+
}
|
|
414
|
+
publishTarget = { base: pubBase, scope: pubScope, target: bumpVersion(pubBase, pubScope) };
|
|
415
|
+
log("Publish target for task " + taskId + ": " + pubBase + " + " + pubScope + " -> " + publishTarget.target);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
return await parkTask(env, "Deterministic pre-publish failed: " + (e && e.message ? e.message : e) + ". Fail-closed.");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
var claimed = await ensureClaimed(env, state, PHASE);
|
|
422
|
+
if (claimed.type !== "CLAIMED") return claimed;
|
|
423
|
+
|
|
424
|
+
// ── Instructions branch ──
|
|
425
|
+
var instructions;
|
|
426
|
+
var artifactFraming = null;
|
|
427
|
+
if (env.publishType === "npm") {
|
|
428
|
+
instructions = buildNpmInstructions(ctx, publishTarget);
|
|
429
|
+
} else if (env.publishType === "artifact") {
|
|
430
|
+
// See docs/decisions/publish-path.md#deterministic-artifact-publish:
|
|
431
|
+
// the work agent never publishes; the parent runs the deterministic
|
|
432
|
+
// publish script. The mechanical steps below run here, in the
|
|
433
|
+
// instructions branch — before the work agent.
|
|
434
|
+
var mech = await runArtifactPath(env, state);
|
|
435
|
+
if (mech.type) return mech; // PARK / PARK_FAILED
|
|
436
|
+
artifactFraming = mech.framing;
|
|
437
|
+
instructions = buildArtifactInstructions(env, artifactFraming);
|
|
438
|
+
} else {
|
|
439
|
+
// vercel publish is not yet implemented — block without inventing behavior
|
|
440
|
+
instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
|
|
441
|
+
"Do NOT invent publish behavior — do not guess CLI commands, APIs, or deployment steps.\n" +
|
|
442
|
+
"Report the situation in prose, then end your report with exactly this line: VERDICT: FAIL.";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
var boundary = await runWorkBoundary(env, state, {
|
|
446
|
+
phase: PHASE.name, identity: PHASE.identity,
|
|
447
|
+
instructions: instructions,
|
|
448
|
+
eventPreamble: buildEventPreamble(env, PHASE.name),
|
|
449
|
+
crewApiLine: true, verdictStep: true,
|
|
450
|
+
});
|
|
451
|
+
if (boundary.type !== "BOUNDARY_DONE") return boundary;
|
|
452
|
+
|
|
453
|
+
var passed = closeoutPassed(boundary);
|
|
454
|
+
var workerText = boundary.workerText;
|
|
455
|
+
|
|
456
|
+
// See docs/decisions/publish-path.md#publish-verify: the agent cannot
|
|
457
|
+
// verify publish; the parent does.
|
|
458
|
+
var publishVerified = false;
|
|
459
|
+
var npmPublishSkipped = false;
|
|
460
|
+
var summary;
|
|
461
|
+
if (env.publishType === "npm" && publishTarget) {
|
|
462
|
+
var publishSkipped = extractPublishSkipped(workerText);
|
|
463
|
+
if (publishSkipped.ok) {
|
|
464
|
+
npmPublishSkipped = true;
|
|
465
|
+
if (publishSkipped.reason === "no-npm-publish") {
|
|
466
|
+
log("Publish skipped for task " + taskId + " (no-npm-publish — npm publish not configured): publish verification vacuous, nothing was shipped");
|
|
467
|
+
} else {
|
|
468
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!npmPublishSkipped) {
|
|
472
|
+
try {
|
|
473
|
+
var regOut = await runCmd(["npm", "view", "muse-crew", "version"]);
|
|
474
|
+
var regVer = String(regOut.stdout || "").trim();
|
|
475
|
+
if (regVer !== publishTarget.target) {
|
|
476
|
+
return await parkTask(env, "Publish verification failed: registry shows " + regVer + " but the release decision required " +
|
|
477
|
+
publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
|
|
478
|
+
}
|
|
479
|
+
publishVerified = true;
|
|
480
|
+
// See docs/decisions/publish-path.md#provenance-refresh: refresh
|
|
481
|
+
// crew_release after landed publish, preserving source_commit.
|
|
482
|
+
try {
|
|
483
|
+
var provGet = await crewApi(env, "get-provenance", { project_id: env.projectId });
|
|
484
|
+
var provP = (provGet && provGet.provenance) || null;
|
|
485
|
+
if (provP) {
|
|
486
|
+
var relOut = await runCmd(["basename", (await runCmd(["readlink", join(env.crewHome, "current")])).stdout.trim()]);
|
|
487
|
+
var tsOut = await runCmd(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"]);
|
|
488
|
+
var rel = String(relOut.stdout || "").trim();
|
|
489
|
+
var ts = String(tsOut.stdout || "").trim();
|
|
490
|
+
var setRes = await crewApi(env, "set-provenance", {
|
|
491
|
+
project_id: env.projectId,
|
|
492
|
+
source_commit: provP.source_commit,
|
|
493
|
+
crew_release: rel,
|
|
494
|
+
published_at: ts,
|
|
495
|
+
task_id: taskId,
|
|
496
|
+
});
|
|
497
|
+
if (!(setRes && setRes.ok === true)) {
|
|
498
|
+
return await parkTask(env, "Provenance refresh failed after a verified npm publish (crew_release " + (rel || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// No provenance record (null) → refreshed:false, reason
|
|
502
|
+
// "no-record" → not a park, exactly like the source.
|
|
503
|
+
} catch (e) {
|
|
504
|
+
return await parkTask(env, "Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
505
|
+
}
|
|
506
|
+
} catch (e) {
|
|
507
|
+
return await parkTask(env, "Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Version acknowledgement: the exact per-attempt version on the
|
|
513
|
+
// artifact's disk is the sole positive completion criterion. A skipped
|
|
514
|
+
// or failed publish has nothing to ack.
|
|
515
|
+
if (env.publishType === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
|
516
|
+
// The skip path records the computed target and the explicit skip —
|
|
517
|
+
// never "published:", which would be a false claim (nothing was
|
|
518
|
+
// shipped).
|
|
519
|
+
var markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
520
|
+
(npmPublishSkipped
|
|
521
|
+
? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
|
|
522
|
+
: "published: muse-crew@" + publishTarget.target);
|
|
523
|
+
summary = summarizeReport(workerText, { markerLines: markerLines });
|
|
524
|
+
} else {
|
|
525
|
+
summary = summarizeReport(workerText);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// "rejected" is an explicit phase verdict routed through rework (Review).
|
|
529
|
+
// Publish work that did not finish is operational — "failed", retryable
|
|
530
|
+
// under the dispatcher's consecutive-failure cap.
|
|
531
|
+
var status = passed ? "completed" : "failed";
|
|
532
|
+
await recordPhase(env, {
|
|
533
|
+
task_id: taskId,
|
|
534
|
+
session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: summary },
|
|
535
|
+
event: { task_id: taskId, type: status, identity: PHASE.identity, message: PHASE.name + " " + status + " by " + PHASE.identity },
|
|
536
|
+
});
|
|
537
|
+
if (!passed) {
|
|
538
|
+
log("Publish failed for task " + taskId + ": " + summary);
|
|
539
|
+
return { type: "FAILED", reason: "Publish failed: " + summary, detail: "The publish step failed. The dispatcher will retry under its consecutive-failure cap." };
|
|
540
|
+
}
|
|
541
|
+
log("Publish completed for task " + taskId);
|
|
542
|
+
return { type: "ADVANCE", next: null, publishSuccess: true };
|
|
543
|
+
}
|