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.
- 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/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,592 @@
|
|
|
1
|
+
// lib/bugfix/phases/publish.js — Publish (wren) phase module (sandbox
|
|
2
|
+
// exit, Phase D). Import-safe: no side effects on import, bare `node`
|
|
3
|
+
// exits 0.
|
|
4
|
+
//
|
|
5
|
+
// Bugfix's Publish is NOT the standard workflow's Publish: a rigorous
|
|
6
|
+
// source comparison (hidden_files/phase-d/extract-integrate-publish.md,
|
|
7
|
+
// 15 deltas D1–D15) found behavioral differences that change observable
|
|
8
|
+
// task outcomes, so this is a dedicated specialization sharing only the
|
|
9
|
+
// workflow-neutral helpers. Deliberate bugfix behaviors preserved here:
|
|
10
|
+
//
|
|
11
|
+
// - D1/D14: pin guard, empty-target skip, unknown-target park, and the
|
|
12
|
+
// npm release-decision/version computation all run BEFORE the
|
|
13
|
+
// self-claim. The empty-target and release:no skips hold no claim and
|
|
14
|
+
// record no session/event (D2/D3) — the phase advances silently.
|
|
15
|
+
// - D4: the release decision is re-derived from durable Build notes at
|
|
16
|
+
// every entry (the worker layer has no run-local variable to reuse; a
|
|
17
|
+
// dispatcher retry resumed at Publish must work).
|
|
18
|
+
// - D5: the npm package name is hardcoded to "muse-crew" (the source
|
|
19
|
+
// hardcodes it; there is no env.npmPackage indirection in bugfix).
|
|
20
|
+
// - D6: the skip note is truthful per reason (the source's closeout
|
|
21
|
+
// collapsed both skips into "skipped: no-lock-held"; the QA backstop
|
|
22
|
+
// is updated to recognize both markers).
|
|
23
|
+
// - D7: the verified re-entry guard runs BEFORE lock-status (a verified
|
|
24
|
+
// re-entry with no lock held takes the verified framing, never the
|
|
25
|
+
// empty-diff skip note).
|
|
26
|
+
// - D8: ledger entries carry attempt: rebuildAttemptKey (minted once per
|
|
27
|
+
// phase entry as attemptKey("publish-artifact-rebuild-"+taskId,
|
|
28
|
+
// reworkCount)); preflight entries record commit: null — the source's
|
|
29
|
+
// var-hoisting quirk (P4), preserved deliberately via the shared
|
|
30
|
+
// recordPublishLedger.
|
|
31
|
+
// - D9: the .files.json write parks fail-closed on error (strict).
|
|
32
|
+
// - D10: intent-anchor failure parks ("Deterministic artifact publish
|
|
33
|
+
// failed: ..."), it does not return FAILED.
|
|
34
|
+
// - D11: on the artifact no-lock skip path, post-deploy is strict — a
|
|
35
|
+
// missing DEPLOYED parks fail-closed.
|
|
36
|
+
// - D12: vercel parks honestly (the source's vercel branch is dead code
|
|
37
|
+
// that dispatched an agent with empty instructions).
|
|
38
|
+
// - D13: published_at is left null for the server default (no clock read
|
|
39
|
+
// in the module).
|
|
40
|
+
// - D15: the npm publish script's markers are classified mechanically
|
|
41
|
+
// (no creative agent runs the publish).
|
|
42
|
+
//
|
|
43
|
+
// Reads: claim/session context (only once publish work actually starts),
|
|
44
|
+
// the release decision re-derived from durable Build notes,
|
|
45
|
+
// env.publishType, merge-lock state, npm registry base version,
|
|
46
|
+
// dashboard provenance.
|
|
47
|
+
// Writes: completed/failed session + event (machine-readable marker lines
|
|
48
|
+
// appended after the summary slice — QA's npm backstop reads
|
|
49
|
+
// TARGET_VERSION=, published: muse-crew@, and the skipped: markers),
|
|
50
|
+
// ~/.publish-diffs/<taskId>.files.json (the complete file list of what
|
|
51
|
+
// the task changed — QA classifies content findings against it),
|
|
52
|
+
// publish-attempt ledger intent entries, post-deploy cleanup (lock
|
|
53
|
+
// release + worktree reclaim).
|
|
54
|
+
// Transitions: ADVANCE → QA on success or honest skip. FAILED on
|
|
55
|
+
// operational failure (retryable under the dispatcher's cap). PARK on:
|
|
56
|
+
// unknown publish target, missing/unparseable release decision, publish
|
|
57
|
+
// verification failure, artifact publish intent (one-party: issuance
|
|
58
|
+
// belongs to the parent layer — the phase parks at intent), vercel
|
|
59
|
+
// (declared but unimplemented — fails honestly), intent-anchor failure,
|
|
60
|
+
// strict post-deploy failure on the artifact skip path.
|
|
61
|
+
|
|
62
|
+
import {
|
|
63
|
+
runCmd, crewApi, recordPhase, ensureClaimed,
|
|
64
|
+
latestSessionNotes, lifecycle, parkTask, pinLifecycle, PIN_BASENAMES, log,
|
|
65
|
+
recordPublishLedger,
|
|
66
|
+
} from "../../workflow-lib.js";
|
|
67
|
+
import {
|
|
68
|
+
extractReleaseDecision, bumpVersion, extractMarkerLines,
|
|
69
|
+
} from "../../extract.js";
|
|
70
|
+
import { join, basename } from "path";
|
|
71
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, readlinkSync } from "fs";
|
|
72
|
+
|
|
73
|
+
export const PHASE = { name: "Publish", identity: "wren" };
|
|
74
|
+
const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
75
|
+
|
|
76
|
+
// buildInstructions — pure function of ctx. The deterministic publish
|
|
77
|
+
// procedure for the configured publish type (the worker layer runs the
|
|
78
|
+
// pinned scripts directly — no creative agent is dispatched by this
|
|
79
|
+
// phase; the text documents the mechanical procedure). The npm package is
|
|
80
|
+
// hardcoded to muse-crew (D5), matching the source.
|
|
81
|
+
export function buildInstructions(ctx) {
|
|
82
|
+
var env = ctx.env;
|
|
83
|
+
var taskId = env.taskId;
|
|
84
|
+
if (!env.publishType) {
|
|
85
|
+
return "Publish is skipped: no publish target is configured for this project (deploy_type empty). Run post-deploy cleanup (release the merge lock, reclaim the worktree) and advance to QA. Nothing is shipped. No session is recorded for this skip.";
|
|
86
|
+
}
|
|
87
|
+
if (env.publishType === "npm") {
|
|
88
|
+
return "Deterministic npm publish. The release decision was already made and recorded by the workflow — there is no decision to make.\n" +
|
|
89
|
+
"1. Read the registry base version (npm view muse-crew version, falling back to the repo's package.json), validate it is semver, and compute the target version as base + version_bump (the scope from the Build report's release decision).\n" +
|
|
90
|
+
"2. Run exactly one command — the pinned publish script at " + join(env.runLib, "publish-npm.sh") + " with TASK_ID=" + taskId + " REPO_PATH=" + env.repoPath + " PKG=muse-crew TARGET_VERSION=<target> CREW_HOME=" + env.crewHome + " LIFECYCLE=" + env.lifecycle + " RELEASE_SCRIPT=" + env.releaseScript + ".\n" +
|
|
91
|
+
"3. Parse the script's marker lines: PUBLISH_SKIPPED=no-lock-held (empty-diff Integrate — nothing merged, nothing to ship, an honest skip), PUBLISH_SKIPPED=no-npm-publish (npm publish not configured — honest skip), PUBLISH_FAILED=<reason> (operational failure), PUBLISH_COMPLETE=<version> (shipped).\n" +
|
|
92
|
+
"4. On PUBLISH_COMPLETE, verify mechanically: the registry version must equal the target. A mismatch parks fail-closed — the publish did not land.\n" +
|
|
93
|
+
"Do NOT run npm, npm pack, npm publish, or the python publish script directly. Do NOT compare local and registry versions by judgment. Do NOT decide whether to publish.";
|
|
94
|
+
}
|
|
95
|
+
if (env.publishType === "artifact") {
|
|
96
|
+
return "Deterministic artifact publish (one-party: this phase stages the intent; issuance belongs to the parent layer).\n" +
|
|
97
|
+
"1. Verified re-entry guard FIRST: if the dashboard provenance stamps this task's merge commit as the repo's current HEAD, the parent already published and verified it — skip issuance entirely and advance to QA.\n" +
|
|
98
|
+
"2. Read the merge-lock state explicitly (lock-status). UNLOCKED means an empty-diff Integrate — skip gracefully: nothing merged, nothing to ship; post-deploy must report DEPLOYED or the phase parks fail-closed. Otherwise refresh the lock; a refresh failure parks fail-closed.\n" +
|
|
99
|
+
"3. Preflight: the artifact target directory ~/workspace/ts-spaces/" + (env.publishSlug || "<deploy_slug>") + "/ must exist (conclusive). A missing target parks with a ledger 'rejected' entry; an inconclusive preflight parks with 'unknown'.\n" +
|
|
100
|
+
"4. Compute the publish diff deterministically (compute-publish-diff.js from the stamped provenance base), persist the file list to " + env.publishDiffsDir + "/" + taskId + ".files.json for QA attribution, anchor the intent in the publish-attempt ledger (attempt key minted once per phase entry), and park at publish intent. An unconfirmed intent anchor parks fail-closed — never an orphan intent.\n" +
|
|
101
|
+
"This phase never issues an artifact_edit and never writes an issuance ledger entry — the outcome is 'publish-intent', never 'submitted'.";
|
|
102
|
+
}
|
|
103
|
+
return "Publish target '" + env.publishType + "' is not a known publish type — the phase parks fail-closed. Expected 'npm', 'artifact', 'vercel', or empty (skip publish).";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// pinGuard — the Publish preflight from the source: all pinned lifecycle
|
|
107
|
+
// scripts must be present (re-pin once, then park honestly). Runs BEFORE
|
|
108
|
+
// the self-claim (D1): a pin failure parks without ever claiming.
|
|
109
|
+
async function pinGuard(env) {
|
|
110
|
+
var present = [];
|
|
111
|
+
try {
|
|
112
|
+
present = readdirSync(env.runLib);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
present = [];
|
|
115
|
+
}
|
|
116
|
+
var missing = PIN_BASENAMES.filter(function (b) { return present.indexOf(b) === -1; });
|
|
117
|
+
if (missing.length > 0) {
|
|
118
|
+
log("Pin guard at Publish: missing " + missing.join(", ") + " — re-pinning once");
|
|
119
|
+
try {
|
|
120
|
+
pinLifecycle(env);
|
|
121
|
+
present = readdirSync(env.runLib);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
present = [];
|
|
124
|
+
}
|
|
125
|
+
missing = PIN_BASENAMES.filter(function (b) { return present.indexOf(b) === -1; });
|
|
126
|
+
}
|
|
127
|
+
if (missing.length > 0) {
|
|
128
|
+
return { parked: await parkTask(env, "Pinned lifecycle scripts missing at Publish even after re-pin: " + missing.join(", ") + " in " + env.runLib + ".") };
|
|
129
|
+
}
|
|
130
|
+
log("Pin guard: all four lifecycle scripts present at Publish");
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// postDeploy — STEP 2 (mechanical, always — skip path included):
|
|
135
|
+
// post-deploy commits builder leftovers if any, removes the worktree, and
|
|
136
|
+
// releases the merge lock (forgiving when none is held) — even when the
|
|
137
|
+
// publish itself failed, so a skipped or failed publish can never leave the
|
|
138
|
+
// lock held. Best-effort hygiene: never a gate, never a park — EXCEPT on
|
|
139
|
+
// the artifact no-lock skip path, where the source gates on DEPLOYED
|
|
140
|
+
// (D11); that path uses postDeployStrict below.
|
|
141
|
+
async function postDeploy(env) {
|
|
142
|
+
try {
|
|
143
|
+
var res = await lifecycle(env, "post-deploy", [env.taskId]);
|
|
144
|
+
var out = String((res && res.stdout) || "");
|
|
145
|
+
log("Publish post-deploy for task " + env.taskId + ": " + (/DEPLOYED/.test(out) ? "DEPLOYED" : "output: " + out.slice(0, 120)));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log("Publish post-deploy threw for task " + env.taskId + " (" + ((e && e.message) || e) + ") — forgiving, continuing");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// postDeployStrict — the artifact no-lock skip path (D11): the source's
|
|
152
|
+
// skip-cleanup agent reported {deployed, output} and the workflow parked
|
|
153
|
+
// fail-closed when DEPLOYED was absent. Returns {deployed, output}.
|
|
154
|
+
async function postDeployStrict(env) {
|
|
155
|
+
var out = "";
|
|
156
|
+
try {
|
|
157
|
+
out = String((await lifecycle(env, "post-deploy", [env.taskId])).stdout || "");
|
|
158
|
+
} catch (e) {
|
|
159
|
+
out = String((e && e.stdout) || "");
|
|
160
|
+
log("Publish strict post-deploy threw for task " + env.taskId + " (" + ((e && e.message) || e) + ")");
|
|
161
|
+
}
|
|
162
|
+
var deployed = /DEPLOYED/.test(out);
|
|
163
|
+
log("Publish strict post-deploy for task " + env.taskId + ": " + (deployed ? "DEPLOYED" : "no DEPLOYED in output: " + out.slice(0, 120)));
|
|
164
|
+
return { deployed: deployed, output: out.trim() };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// computeNpmTarget — deterministic pre-publish: read the registry base,
|
|
168
|
+
// validate semver, compute target = base + scope. Fail-closed parks. The
|
|
169
|
+
// package name is hardcoded to muse-crew (D5), matching the source.
|
|
170
|
+
async function computeNpmTarget(env, releaseDecision) {
|
|
171
|
+
var pkg = "muse-crew";
|
|
172
|
+
var base = "";
|
|
173
|
+
try {
|
|
174
|
+
base = String((await runCmd(["npm", "view", pkg, "version"], {})).stdout || "").trim();
|
|
175
|
+
} catch (e) {
|
|
176
|
+
base = "";
|
|
177
|
+
}
|
|
178
|
+
if (!base) {
|
|
179
|
+
try {
|
|
180
|
+
base = String((await runCmd(["node", "-p", "require('" + join(env.repoPath, "package.json") + "').version"], {})).stdout || "").trim();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
base = "";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!/^\d+\.\d+\.\d+$/.test(base)) {
|
|
186
|
+
return { parked: await parkTask(env, "Publish base version unreadable: '" + base + "'. Fail-closed.") };
|
|
187
|
+
}
|
|
188
|
+
var scope = (releaseDecision && releaseDecision.version_bump) || "patch";
|
|
189
|
+
var target = { base: base, scope: scope, target: bumpVersion(base, scope) };
|
|
190
|
+
log("Publish target for task " + env.taskId + ": " + base + " + " + scope + " -> " + target.target);
|
|
191
|
+
return { target: target, pkg: pkg };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// runNpmPublish — the deterministic publish script, run directly (the
|
|
195
|
+
// source had the Publish agent run exactly this one command; the worker
|
|
196
|
+
// layer skips the agent and runs it mechanically). PKG is hardcoded to
|
|
197
|
+
// muse-crew (D5).
|
|
198
|
+
async function runNpmPublish(env, pkg, target) {
|
|
199
|
+
var script = join(env.runLib, "publish-npm.sh");
|
|
200
|
+
var scriptEnv = Object.assign({}, process.env, {
|
|
201
|
+
CREW_HOME: env.crewHome, CREW_REPO: env.repoPath,
|
|
202
|
+
TASK_ID: env.taskId, REPO_PATH: env.repoPath, PKG: pkg,
|
|
203
|
+
TARGET_VERSION: target.target, LIFECYCLE: env.lifecycle,
|
|
204
|
+
RELEASE_SCRIPT: env.releaseScript,
|
|
205
|
+
});
|
|
206
|
+
var out = "";
|
|
207
|
+
try {
|
|
208
|
+
out = String((await runCmd([script], { env: scriptEnv })).stdout || "");
|
|
209
|
+
} catch (e) {
|
|
210
|
+
out = String((e && e.stdout) || "");
|
|
211
|
+
var failLine = /^PUBLISH_FAILED=(.+)$/m.exec(out);
|
|
212
|
+
return { failed: "publish script failed: " + (failLine ? failLine[1].trim() : ((e && e.message) || e).slice(0, 200)), output: out };
|
|
213
|
+
}
|
|
214
|
+
var skip = /^PUBLISH_SKIPPED=(no-lock-held|no-npm-publish)$/m.exec(out);
|
|
215
|
+
if (skip) return { skipped: skip[1], output: out };
|
|
216
|
+
var complete = /^PUBLISH_COMPLETE=(.+)$/m.exec(out);
|
|
217
|
+
if (complete) return { complete: complete[1].trim(), output: out };
|
|
218
|
+
var failed = /^PUBLISH_FAILED=(.+)$/m.exec(out);
|
|
219
|
+
if (failed) return { failed: "publish script reported PUBLISH_FAILED=" + failed[1].trim(), output: out };
|
|
220
|
+
return { failed: "publish script produced no usable marker (no PUBLISH_COMPLETE/PUBLISH_FAILED/PUBLISH_SKIPPED)", output: out };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// verifyNpmPublish — the parent verifies the publish (publish-path.md:
|
|
224
|
+
// the agent cannot verify publish; the parent does). Registry version
|
|
225
|
+
// must equal the target, else park fail-closed.
|
|
226
|
+
async function verifyNpmPublish(env, pkg, target) {
|
|
227
|
+
var regVer = "";
|
|
228
|
+
try {
|
|
229
|
+
regVer = String((await runCmd(["npm", "view", pkg, "version"], {})).stdout || "").trim();
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return { parked: await parkTask(env, "Publish verification failed: could not read the registry version (" + ((e && e.message) || e).slice(0, 200) + "). Fail-closed.") };
|
|
232
|
+
}
|
|
233
|
+
if (regVer !== target.target) {
|
|
234
|
+
return { parked: await parkTask(env, "Publish verification failed: registry shows " + regVer + " but the release decision required " + target.target + " (" + target.base + " + " + target.scope + "). The publish did not land.") };
|
|
235
|
+
}
|
|
236
|
+
return { ok: true };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// refreshProvenance — provenance-refresh (publish-verification.md): refresh
|
|
240
|
+
// crew_release after a landed publish, preserving source_commit.
|
|
241
|
+
// published_at defaults server-side (stampProvenance: published_at ?? now())
|
|
242
|
+
// so this module never reads the clock (D13).
|
|
243
|
+
async function refreshProvenance(env) {
|
|
244
|
+
var taskId = env.taskId;
|
|
245
|
+
var prov;
|
|
246
|
+
try {
|
|
247
|
+
prov = await crewApi(env, "get-provenance", { project_id: env.projectId });
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return { parked: await parkTask(env, "Provenance refresh failed: could not read dashboard provenance after a verified npm publish (" + ((e && e.message) || e).slice(0, 200) + "). Fail-closed.") };
|
|
250
|
+
}
|
|
251
|
+
var existing = prov && prov.provenance;
|
|
252
|
+
if (!existing) {
|
|
253
|
+
log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
|
|
254
|
+
return { ok: true, skipped: true };
|
|
255
|
+
}
|
|
256
|
+
var rel;
|
|
257
|
+
try {
|
|
258
|
+
rel = basename(readlinkSync(env.crewHome + "/current"));
|
|
259
|
+
} catch (e) {
|
|
260
|
+
return { parked: await parkTask(env, "Provenance refresh failed: could not resolve the active release symlink (" + ((e && e.message) || e).slice(0, 200) + "). Fail-closed.") };
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
var res = await crewApi(env, "set-provenance", {
|
|
264
|
+
project_id: env.projectId, source_commit: existing.source_commit,
|
|
265
|
+
crew_release: rel, published_at: null, task_id: taskId,
|
|
266
|
+
});
|
|
267
|
+
var ok = res && (res.ok === true || /ok:\s*true/i.test(JSON.stringify(res)));
|
|
268
|
+
if (!ok) {
|
|
269
|
+
return { parked: await parkTask(env, "Provenance refresh failed after a verified npm publish (crew_release " + rel + "). The release is live but the dashboard record is stale — fail-closed.") };
|
|
270
|
+
}
|
|
271
|
+
log("Provenance refreshed for task " + taskId + ": crew_release " + rel);
|
|
272
|
+
return { ok: true };
|
|
273
|
+
} catch (e) {
|
|
274
|
+
return { parked: await parkTask(env, "Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + ((e && e.message) || e).slice(0, 200) + "). Fail-closed.") };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// checkArtifactReentry — the verified re-entry guard (blocker 22): if the
|
|
279
|
+
// stamped provenance names THIS task and its source_commit is the repo's
|
|
280
|
+
// current HEAD, the merge commit was already published and verified by the
|
|
281
|
+
// parent — issuance is skipped entirely. A guard failure never claims
|
|
282
|
+
// verification — the task proceeds down the normal path. Runs BEFORE
|
|
283
|
+
// lock-status (D7): a verified re-entry with no lock held (the normal
|
|
284
|
+
// case — the intent park's terminal cleanup released the lock) takes the
|
|
285
|
+
// verified framing, never the empty-diff skip note.
|
|
286
|
+
async function checkArtifactReentry(env) {
|
|
287
|
+
try {
|
|
288
|
+
var prov = await crewApi(env, "get-provenance", { project_id: env.projectId });
|
|
289
|
+
var head = String((await runCmd(["git", "-C", env.repoPath, "rev-parse", "HEAD"], {})).stdout || "").trim();
|
|
290
|
+
var p = prov && prov.provenance;
|
|
291
|
+
if (p && p.source_commit && p.source_commit === head && p.task_id === env.taskId) {
|
|
292
|
+
log("Publish verified re-entry for task " + env.taskId + ": provenance stamps " + p.source_commit + " == HEAD for this task — the parent already published and verified it; issuance skipped");
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
log("Publish re-entry guard threw for task " + env.taskId + " (" + ((e && e.message) || e).slice(0, 200) + ") — proceeding with the normal publish path; a guard failure never claims verification");
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// publishArtifact — the one-party artifact publish path. Everything through
|
|
302
|
+
// the intent anchor is read-only and deterministic (direct calls, no agent
|
|
303
|
+
// couriers — the worker layer has no ferry constraint). Parks at intent:
|
|
304
|
+
// issuance belongs to the parent layer, which issues the edit directly and
|
|
305
|
+
// whose acknowledgement scan owns the task from there.
|
|
306
|
+
//
|
|
307
|
+
// Ordering (D7): the verified re-entry guard runs BEFORE lock-status.
|
|
308
|
+
// Ledger entries carry attempt: rebuildAttemptKey, minted once per phase
|
|
309
|
+
// entry (D8). Preflight entries record commit: null — the source's
|
|
310
|
+
// var-hoisting quirk (P4), preserved deliberately by the shared
|
|
311
|
+
// recordPublishLedger.
|
|
312
|
+
async function publishArtifact(env, state, rebuildAttemptKey) {
|
|
313
|
+
var taskId = env.taskId;
|
|
314
|
+
var slug = env.publishSlug;
|
|
315
|
+
|
|
316
|
+
// Verified re-entry FIRST (D7): the intent park's terminal cleanup
|
|
317
|
+
// already released the lock and reclaimed the worktree. Post-deploy below
|
|
318
|
+
// is best-effort hygiene on this branch — never a gate, never a park.
|
|
319
|
+
if (await checkArtifactReentry(env)) {
|
|
320
|
+
await postDeploy(env);
|
|
321
|
+
await recordPhase(env, {
|
|
322
|
+
task_id: taskId,
|
|
323
|
+
session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed",
|
|
324
|
+
notes: "Publish verified re-entry: provenance stamps this task's merge commit as HEAD — the parent already published and verified it; issuance skipped." },
|
|
325
|
+
event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: PHASE.name + " completed by " + PHASE.identity },
|
|
326
|
+
});
|
|
327
|
+
return { type: "ADVANCE", next: "QA" };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// STEP 0 (mechanical): read the merge-lock state explicitly — never infer
|
|
331
|
+
// it from prose. An empty-diff Integrate (MERGED_EMPTY) intentionally
|
|
332
|
+
// takes no lock: nothing merged, nothing to ship. When no lock is held,
|
|
333
|
+
// Publish skips gracefully instead of failing closed on refresh-lock —
|
|
334
|
+
// but post-deploy is strict here (D11): a missing DEPLOYED parks
|
|
335
|
+
// fail-closed, because worktree cleanup and lock state would be unknown.
|
|
336
|
+
var lockOut = "";
|
|
337
|
+
try {
|
|
338
|
+
lockOut = String((await lifecycle(env, "lock-status", [taskId])).stdout || "").trim();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
return { parked: await parkTask(env, "Publish lock state unreadable: " + ((e && e.message) || e).slice(0, 200) + ". Fail-closed — lock state unknown.") };
|
|
341
|
+
}
|
|
342
|
+
if (lockOut === "UNLOCKED") {
|
|
343
|
+
var pd = await postDeployStrict(env);
|
|
344
|
+
if (!pd.deployed) {
|
|
345
|
+
return { parked: await parkTask(env, "Post-deploy failed after a skipped publish: " + (pd.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.") };
|
|
346
|
+
}
|
|
347
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy step finished");
|
|
348
|
+
await recordPhase(env, {
|
|
349
|
+
task_id: taskId,
|
|
350
|
+
session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed",
|
|
351
|
+
notes: "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)" },
|
|
352
|
+
event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: PHASE.name + " completed by " + PHASE.identity },
|
|
353
|
+
});
|
|
354
|
+
return { type: "ADVANCE", next: "QA" };
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
await lifecycle(env, "refresh-lock", [taskId]);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
return { parked: await parkTask(env, "Publish lock refresh failed: " + ((e && e.message) || e).slice(0, 200) + ". Fail-closed — lock state unknown.") };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Artifact preflight: the project is classified artifact but the target
|
|
363
|
+
// must provably exist. Conclusive missing -> ledger "rejected" + park;
|
|
364
|
+
// inconclusive -> ledger "unknown" + park. (Publish edits an existing
|
|
365
|
+
// artifact; it never creates one.)
|
|
366
|
+
if (!slug) {
|
|
367
|
+
await recordPublishLedger(env, { commit: null, attempt: rebuildAttemptKey, outcome: "rejected", 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." });
|
|
368
|
+
return { parked: 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.") };
|
|
369
|
+
}
|
|
370
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(slug)) {
|
|
371
|
+
await recordPublishLedger(env, { commit: null, attempt: rebuildAttemptKey, outcome: "unknown", detail: "artifact preflight inconclusive: PUBLISH_SLUG has an unsafe shape — fail-closed, no edit issued." });
|
|
372
|
+
return { parked: await parkTask(env, "Publish cannot proceed for task " + taskId + ": the artifact target preflight was inconclusive (deploy_slug has an unsafe shape). Target existence is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.") };
|
|
373
|
+
}
|
|
374
|
+
var artifactTargetDir = join(process.env.HOME || "", "workspace/ts-spaces", slug);
|
|
375
|
+
var targetPresent = false;
|
|
376
|
+
try {
|
|
377
|
+
targetPresent = readdirSync(artifactTargetDir).length >= 0;
|
|
378
|
+
} catch (e) {
|
|
379
|
+
targetPresent = false;
|
|
380
|
+
}
|
|
381
|
+
if (!targetPresent) {
|
|
382
|
+
await recordPublishLedger(env, { commit: null, attempt: rebuildAttemptKey, outcome: "rejected", detail: "artifact target directory missing (preflight): " + artifactTargetDir + " does not exist — setup never provisioned the artifact for deploy_slug " + slug + ". The edit provably did not go through: no trigger issued, no blind retry" });
|
|
383
|
+
return { parked: 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 " + slug + ") 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.") };
|
|
384
|
+
}
|
|
385
|
+
log("Publish artifact preflight for task " + taskId + ": target " + artifactTargetDir + " present");
|
|
386
|
+
|
|
387
|
+
// Stamped publish base (per-project provenance; empty tree = first publish).
|
|
388
|
+
var publishBase = "";
|
|
389
|
+
try {
|
|
390
|
+
var provResult = await crewApi(env, "get-provenance", { project_id: env.projectId });
|
|
391
|
+
publishBase = String((provResult && provResult.provenance && provResult.provenance.source_commit) || "").trim();
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return { parked: await parkTask(env, "Publish cannot proceed for task " + taskId + ": the stamped publish base is unreadable (" + ((e && e.message) || e).slice(0, 200) + "). Human attention needed.") };
|
|
394
|
+
}
|
|
395
|
+
if (!publishBase) {
|
|
396
|
+
publishBase = EMPTY_TREE_SHA;
|
|
397
|
+
log("Publish base for task " + taskId + ": no provenance stamped yet — using empty tree (first publish)");
|
|
398
|
+
} else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
|
|
399
|
+
return { parked: await parkTask(env, "Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.") };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Deterministic diff (diff travels via file, never through an LLM).
|
|
403
|
+
var diffFile = env.publishDiffsDir + "/" + taskId + (state.reworkCount > 0 ? "-r" + state.reworkCount : "") + ".diff";
|
|
404
|
+
var diffSummary = null;
|
|
405
|
+
try {
|
|
406
|
+
mkdirSync(env.publishDiffsDir, { recursive: true });
|
|
407
|
+
var diffOut = String((await runCmd(["node", join(env.runLib, "compute-publish-diff.js"),
|
|
408
|
+
"--repo-path", env.repoPath, "--base", publishBase,
|
|
409
|
+
"--task-id", taskId, "--attempt", "1", "--out", diffFile], {})).stdout || "").trim();
|
|
410
|
+
diffSummary = JSON.parse(diffOut);
|
|
411
|
+
} catch (e) {
|
|
412
|
+
return { parked: await parkTask(env, "Publish diff computation returned unparseable output — cannot verify application. Human attention needed.") };
|
|
413
|
+
}
|
|
414
|
+
if (!diffSummary || diffSummary.ok !== true) {
|
|
415
|
+
if (diffSummary && diffSummary.error === "not-ancestor") {
|
|
416
|
+
return { parked: 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.") };
|
|
417
|
+
}
|
|
418
|
+
return { parked: await parkTask(env, "Publish diff computation failed (" + ((diffSummary && diffSummary.error) || "unknown error") + ") — cannot verify application. Human attention needed.") };
|
|
419
|
+
}
|
|
420
|
+
if (diffSummary.base !== publishBase) {
|
|
421
|
+
return { parked: 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.") };
|
|
422
|
+
}
|
|
423
|
+
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
424
|
+
var publishVersion = String(diffSummary.version || "");
|
|
425
|
+
if (!/^[0-9a-f]{40}:[1-9][0-9]*$/.test(publishVersion)) {
|
|
426
|
+
return { parked: await parkTask(env, "Publish diff for commit " + (mergeCommitForPublish || "unknown") + " carries no usable publish version — cannot stage a verifiable intent. Human attention needed.") };
|
|
427
|
+
}
|
|
428
|
+
if (!diffSummary.bytes) {
|
|
429
|
+
return { parked: 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.") };
|
|
430
|
+
}
|
|
431
|
+
if (diffSummary.has_binary) {
|
|
432
|
+
return { parked: await parkTask(env, "Publish diff contains binary files — the diff transport cannot carry them. Human attention needed.") };
|
|
433
|
+
}
|
|
434
|
+
if (diffSummary.has_rename) {
|
|
435
|
+
return { parked: await parkTask(env, "Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.") };
|
|
436
|
+
}
|
|
437
|
+
// Persist the publish-diff file set for QA content-finding attribution
|
|
438
|
+
// (blocker 38): the complete record of what the task changed. Direct fs
|
|
439
|
+
// write — the worker layer needs no agent courier. A write failure parks
|
|
440
|
+
// fail-closed (D9): QA attribution would be blind.
|
|
441
|
+
var filesPath = env.publishDiffsDir + "/" + taskId + ".files.json";
|
|
442
|
+
try {
|
|
443
|
+
writeFileSync(filesPath, JSON.stringify(diffSummary.files || []) + "\n", "utf8");
|
|
444
|
+
} catch (e) {
|
|
445
|
+
return { parked: await parkTask(env, "Publish diff file list could not be persisted (" + ((e && e.message) || e).slice(0, 200) + ") — QA attribution would be blind. Human attention needed.") };
|
|
446
|
+
}
|
|
447
|
+
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
448
|
+
// output lines. The count comes from the deterministic script summary,
|
|
449
|
+
// never from an LLM.
|
|
450
|
+
var changedLines = diffSummary.changed_lines || 0;
|
|
451
|
+
if (changedLines > 200) {
|
|
452
|
+
return { parked: await parkTask(env, "Publish diff changes " + changedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.") };
|
|
453
|
+
}
|
|
454
|
+
if (!diffSummary.file_count) {
|
|
455
|
+
return { parked: await parkTask(env, "Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.") };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Anchor the intent. The entry is issuer-written BY THE WORKFLOW and
|
|
459
|
+
// names the intent: outcome "publish-intent", never "submitted" — the
|
|
460
|
+
// workflow issued nothing. The parent claims it via the park note below.
|
|
461
|
+
var intentAnchored = await recordPublishLedger(env, {
|
|
462
|
+
commit: mergeCommitForPublish,
|
|
463
|
+
attempt: rebuildAttemptKey,
|
|
464
|
+
publish_attempt: 1,
|
|
465
|
+
version: publishVersion,
|
|
466
|
+
agent_id: null,
|
|
467
|
+
applied_report: null,
|
|
468
|
+
outcome: "publish-intent",
|
|
469
|
+
issuer: "workflow",
|
|
470
|
+
diff_path: diffFile,
|
|
471
|
+
diff_sha256: String(diffSummary.sha256 || ""),
|
|
472
|
+
base: publishBase,
|
|
473
|
+
detail: "publish intent: preflight and checksummed diff are complete; issuance is owned by the parent layer. The workflow performed no artifact_edit and wrote no issuance.",
|
|
474
|
+
});
|
|
475
|
+
if (!intentAnchored) {
|
|
476
|
+
// Fail loudly by parking (D10) — never an orphan intent. Best-effort
|
|
477
|
+
// post-deploy first so the failure cannot wedge the next run.
|
|
478
|
+
await postDeploy(env);
|
|
479
|
+
return { parked: await parkTask(env, "Deterministic artifact publish failed: publish-intent ledger anchor write unconfirmed. Fail-closed.") };
|
|
480
|
+
}
|
|
481
|
+
return { parked: await parkTask(env, "publish: publish-requested " + mergeCommitForPublish + " attempt=1 — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + diffFile + ", sha256 " + String(diffSummary.sha256 || "") + ") carrying the per-attempt publish version for the parent layer to issue. The parent issues the edit directly and the acknowledgement scan owns the task from there; no action needed.") };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export async function runPhase(ctx) {
|
|
485
|
+
var env = ctx.env, state = ctx.state;
|
|
486
|
+
var taskId = env.taskId;
|
|
487
|
+
|
|
488
|
+
// D1/D14: the pin guard, the target checks, and the npm
|
|
489
|
+
// release-decision/version computation all run BEFORE the self-claim.
|
|
490
|
+
// The skip paths hold no claim and record no session (D2/D3).
|
|
491
|
+
var pg = await pinGuard(env);
|
|
492
|
+
if (pg.parked) return pg.parked;
|
|
493
|
+
|
|
494
|
+
// Publish is optional and target-based. Empty target = prototyping
|
|
495
|
+
// project: skip the phase (no record — D2). Unknown target (incl.
|
|
496
|
+
// legacy "repo") = config error: block.
|
|
497
|
+
if (!env.publishType) {
|
|
498
|
+
log("Publish skipped for task " + taskId + " — no publish target configured (deploy_type empty)");
|
|
499
|
+
await postDeploy(env);
|
|
500
|
+
return { type: "ADVANCE", next: "QA" };
|
|
501
|
+
}
|
|
502
|
+
if (env.publishType !== "npm" && env.publishType !== "artifact" && env.publishType !== "vercel") {
|
|
503
|
+
log("Unknown publish target for task " + taskId + ": " + env.publishType);
|
|
504
|
+
return { parked: await parkTask(env, "Unknown publish target '" + env.publishType + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish).") };
|
|
505
|
+
}
|
|
506
|
+
if (env.publishType === "vercel") {
|
|
507
|
+
return { parked: await parkTask(env, "Vercel publish is declared but has no implementation path. Human attention needed: implement the Vercel publish path or change the project's publish type.") };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Merge-time versioning, decided deterministically: the release decision
|
|
511
|
+
// is re-derived from the durable Build notes at every Publish entry (a
|
|
512
|
+
// dispatcher retry resumed at Publish never re-runs Build). release: no
|
|
513
|
+
// skips like the no-target case (no record — D3); an unparseable
|
|
514
|
+
// decision blocks fail-closed.
|
|
515
|
+
var releaseDecision = extractReleaseDecision(await latestSessionNotes(env, "Build", "completed"));
|
|
516
|
+
state.memo.releaseDecision = releaseDecision;
|
|
517
|
+
|
|
518
|
+
if (env.publishType === "npm") {
|
|
519
|
+
if (!releaseDecision) {
|
|
520
|
+
return { parked: await parkTask(env, "Build report has no machine-readable release:/version_bump: declaration — cannot assign version at publish time.") };
|
|
521
|
+
}
|
|
522
|
+
if (releaseDecision.release === "no") {
|
|
523
|
+
log("Publish skipped for task " + taskId + " — accepted Build report declared release: no");
|
|
524
|
+
await postDeploy(env);
|
|
525
|
+
return { type: "ADVANCE", next: "QA" };
|
|
526
|
+
}
|
|
527
|
+
var ct = await computeNpmTarget(env, releaseDecision);
|
|
528
|
+
if (ct.parked) return ct.parked;
|
|
529
|
+
var target = ct.target, pkg = ct.pkg;
|
|
530
|
+
|
|
531
|
+
// Self-claim: only now that publish work actually starts (D1).
|
|
532
|
+
var claimed = await ensureClaimed(env, state, PHASE);
|
|
533
|
+
if (claimed.type !== "CLAIMED") return claimed;
|
|
534
|
+
|
|
535
|
+
var pub = await runNpmPublish(env, pkg, target);
|
|
536
|
+
await postDeploy(env);
|
|
537
|
+
var markerLines, status, eventType;
|
|
538
|
+
if (pub.failed) {
|
|
539
|
+
markerLines = "TARGET_VERSION=" + target.target + " computed as " + target.base + " + " + target.scope + " → " + target.target + "\n" +
|
|
540
|
+
"publish: FAILED — " + pub.failed;
|
|
541
|
+
status = "failed"; eventType = "failed";
|
|
542
|
+
await recordPhase(env, {
|
|
543
|
+
task_id: taskId,
|
|
544
|
+
session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: status, notes: markerLines },
|
|
545
|
+
event: { task_id: taskId, type: eventType, identity: PHASE.identity, message: PHASE.name + " failed by " + PHASE.identity },
|
|
546
|
+
});
|
|
547
|
+
return { type: "FAILED", reason: "npm publish failed: " + pub.failed };
|
|
548
|
+
}
|
|
549
|
+
if (pub.skipped) {
|
|
550
|
+
// The skip path records the computed target and the explicit skip —
|
|
551
|
+
// never "published:", which would be a false claim. The skip note is
|
|
552
|
+
// truthful per reason (D6): QA's backstop recognizes both markers.
|
|
553
|
+
var skipNote = pub.skipped === "no-npm-publish"
|
|
554
|
+
? "skipped: no-npm-publish (npm publish not configured — helper or credential missing; nothing versioned or published)"
|
|
555
|
+
: "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)";
|
|
556
|
+
log("Publish skipped for task " + taskId + " (" + pub.skipped + "): publish verification vacuous, nothing was shipped");
|
|
557
|
+
markerLines = "TARGET_VERSION=" + target.target + " computed as " + target.base + " + " + target.scope + " → " + target.target + "\n" + skipNote;
|
|
558
|
+
await recordPhase(env, {
|
|
559
|
+
task_id: taskId,
|
|
560
|
+
session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed", notes: markerLines },
|
|
561
|
+
event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: PHASE.name + " completed by " + PHASE.identity },
|
|
562
|
+
});
|
|
563
|
+
return { type: "ADVANCE", next: "QA" };
|
|
564
|
+
}
|
|
565
|
+
// Landed — the parent verifies (the agent cannot verify publish).
|
|
566
|
+
var pv = await verifyNpmPublish(env, pkg, target);
|
|
567
|
+
if (pv.parked) return pv.parked;
|
|
568
|
+
var pr = await refreshProvenance(env);
|
|
569
|
+
if (pr.parked) return pr.parked;
|
|
570
|
+
markerLines = "TARGET_VERSION=" + target.target + " computed as " + target.base + " + " + target.scope + " → " + target.target + "\n" +
|
|
571
|
+
"published: " + pkg + "@" + target.target;
|
|
572
|
+
var workerMarkers = extractMarkerLines(pub.output || "");
|
|
573
|
+
await recordPhase(env, {
|
|
574
|
+
task_id: taskId,
|
|
575
|
+
session: { id: state.activeSessionId, task_id: taskId, identity: PHASE.identity, step: PHASE.name, status: "completed",
|
|
576
|
+
notes: markerLines + (workerMarkers ? "\n" + workerMarkers : "") },
|
|
577
|
+
event: { task_id: taskId, type: "completed", identity: PHASE.identity, message: PHASE.name + " completed by " + PHASE.identity },
|
|
578
|
+
});
|
|
579
|
+
log("Publish complete for task " + taskId + ": " + pkg + "@" + target.target);
|
|
580
|
+
return { type: "ADVANCE", next: "QA" };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Artifact path (one-party publish). The claim precedes the artifact
|
|
584
|
+
// flow (D1); the verified re-entry guard inside publishArtifact runs
|
|
585
|
+
// before lock-status (D7).
|
|
586
|
+
var claimedArtifact = await ensureClaimed(env, state, PHASE);
|
|
587
|
+
if (claimedArtifact.type !== "CLAIMED") return claimedArtifact;
|
|
588
|
+
// Mint the attempt key once per phase entry (D8): the exact key the
|
|
589
|
+
// ledger records at every site, preflight parks included.
|
|
590
|
+
var rebuildAttemptKey = "publish-artifact-rebuild-" + taskId + (state.reworkCount > 0 ? "-r" + state.reworkCount : "");
|
|
591
|
+
return await publishArtifact(env, state, rebuildAttemptKey);
|
|
592
|
+
}
|