muse-crew 0.4.1 → 0.4.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/lib/AGENTS.md +1 -0
- package/lib/publish-npm.sh +167 -0
- package/package.json +1 -1
- package/workflows/bugfix.js +81 -31
- package/workflows/chore.js +77 -30
- package/workflows/standard.js +82 -31
package/lib/AGENTS.md
CHANGED
|
@@ -6,4 +6,5 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
6
6
|
- `merge-lock.sh` — serialized merge lock for concurrent agents; records owner PID
|
|
7
7
|
- `worktree-lifecycle.sh` — git worktree create/cleanup for isolated agent work
|
|
8
8
|
- `orphan-sweep.sh` — find and clean stale worktrees and merge locks
|
|
9
|
+
- `publish-npm.sh` — deterministic npm publish: lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
9
10
|
- `test-orphan-sweep.sh` — regression tests for orphan-sweep.sh (active-run guard, verified removal, fail-closed)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# publish-npm.sh — deterministic npm publish for Muse Crew.
|
|
3
|
+
#
|
|
4
|
+
# Takes the entire publish decision as input; the Publish agent makes none.
|
|
5
|
+
# TARGET_VERSION is authoritative — computed by the workflow, never here.
|
|
6
|
+
# Idempotent on retry/resume: if this task already published the target
|
|
7
|
+
# version (version-bump commit + registry both present), the mutation steps
|
|
8
|
+
# are skipped and the run goes straight to push (idempotent) + finalize.
|
|
9
|
+
#
|
|
10
|
+
# Env:
|
|
11
|
+
# TASK_ID task id (idempotent commit message + merge-lock ops)
|
|
12
|
+
# REPO_PATH repo checkout (Publish runs here, not in a worktree)
|
|
13
|
+
# PKG npm package name (default: muse-crew)
|
|
14
|
+
# TARGET_VERSION version to publish (workflow-computed, authoritative)
|
|
15
|
+
# CREW_HOME crew instance home (accepted for uniformity; unused here)
|
|
16
|
+
# LIFECYCLE pinned worktree-lifecycle.sh
|
|
17
|
+
# RELEASE_SCRIPT crew-release.sh (stable path)
|
|
18
|
+
# NPM_PUBLISH_PY publish helper (default: ~/workspace/skills/npm/bin/npm-publish.py)
|
|
19
|
+
# DRY_RUN=1 print markers and exit before any mutation
|
|
20
|
+
#
|
|
21
|
+
# Output markers (PUBLISH_* lines). On any failure: print
|
|
22
|
+
# PUBLISH_FAILED=<short-reason> plus one detail line, exit 1.
|
|
23
|
+
# On success: print the markers below, exit 0. The final marker is
|
|
24
|
+
# PUBLISH_COMPLETE=<version> — the Publish agent pastes it verbatim into its
|
|
25
|
+
# summary as the script's completion marker.
|
|
26
|
+
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
PKG="${PKG:-muse-crew}"
|
|
30
|
+
NPM_PUBLISH_PY="${NPM_PUBLISH_PY:-$HOME/workspace/skills/npm/bin/npm-publish.py}"
|
|
31
|
+
|
|
32
|
+
fail() { # fail <reason> [detail]
|
|
33
|
+
echo "PUBLISH_FAILED=$1"
|
|
34
|
+
if [ $# -ge 2 ]; then
|
|
35
|
+
echo "$2"
|
|
36
|
+
fi
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# 1-2. Validate env, then print the target.
|
|
41
|
+
[ -n "${TASK_ID:-}" ] || fail "env" "TASK_ID is required"
|
|
42
|
+
[ -n "${REPO_PATH:-}" ] || fail "env" "REPO_PATH is required"
|
|
43
|
+
[ -n "${TARGET_VERSION:-}" ] || fail "env" "TARGET_VERSION is required"
|
|
44
|
+
echo "PUBLISH_TARGET=$TARGET_VERSION"
|
|
45
|
+
git -C "$REPO_PATH" rev-parse --git-dir >/dev/null 2>&1 \
|
|
46
|
+
|| fail "env" "REPO_PATH is not a git checkout: $REPO_PATH"
|
|
47
|
+
|
|
48
|
+
# 3. Dry run: markers only, no lock, no deploy, no mutation.
|
|
49
|
+
if [ "${DRY_RUN:-0}" = "1" ]; then
|
|
50
|
+
echo "PUBLISH_DRY_RUN=1"
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# 4. Idempotency: this task's version-bump commit is HEAD and the registry
|
|
55
|
+
# already serves the target version — a prior Publish finished. Skip the
|
|
56
|
+
# mutation steps; push (idempotent) and finalize still run.
|
|
57
|
+
ALREADY_PUBLISHED=0
|
|
58
|
+
HEAD_MSG="$(git -C "$REPO_PATH" log -1 --format=%s)"
|
|
59
|
+
REG="$(npm view "$PKG" version 2>/dev/null || true)"
|
|
60
|
+
if { [ "$HEAD_MSG" = "release: $PKG@$TARGET_VERSION ($TASK_ID)" ] \
|
|
61
|
+
|| [ "$HEAD_MSG" = "release: $PKG@$TARGET_VERSION" ]; } \
|
|
62
|
+
&& [ "$REG" = "$TARGET_VERSION" ]; then
|
|
63
|
+
ALREADY_PUBLISHED=1
|
|
64
|
+
echo "PUBLISHED_ALREADY=$TARGET_VERSION"
|
|
65
|
+
echo "PUBLISH_VERIFIED=$TARGET_VERSION"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ "$ALREADY_PUBLISHED" = "0" ]; then
|
|
69
|
+
# 5. Refresh the merge lock (held from Integrate through post-deploy).
|
|
70
|
+
LOCK_OUT="$(CREW_REPO="$REPO_PATH" bash "${LIFECYCLE:?LIFECYCLE is required}" refresh-lock "$TASK_ID" 2>&1)" \
|
|
71
|
+
|| fail "lock-refresh" "$LOCK_OUT"
|
|
72
|
+
|
|
73
|
+
# 6. Install/activate the immutable release.
|
|
74
|
+
REL_OUT="$(bash "${RELEASE_SCRIPT:?RELEASE_SCRIPT is required}" deploy "$REPO_PATH" 2>&1)" \
|
|
75
|
+
|| fail "release-deploy" "$REL_OUT"
|
|
76
|
+
case "$REL_OUT" in
|
|
77
|
+
*INSTALLED*ACTIVATED*|*EXISTS*ACTIVATED*) ;;
|
|
78
|
+
*) fail "release-deploy" "unexpected output: $REL_OUT" ;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
# 7. Read the old version (audit only — TARGET_VERSION stays authoritative).
|
|
82
|
+
OLD="$(git -C "$REPO_PATH" show "HEAD:package.json" \
|
|
83
|
+
| node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version")" \
|
|
84
|
+
|| fail "read-base" "could not read version from HEAD:package.json"
|
|
85
|
+
echo "PUBLISH_BASE=$OLD"
|
|
86
|
+
|
|
87
|
+
# 8. Write TARGET_VERSION into package.json (`version` field only),
|
|
88
|
+
# preserving the file's existing formatting (2-space indent + trailing
|
|
89
|
+
# newline). The change must be exactly one line — anything else means the
|
|
90
|
+
# rewrite did not preserve the file.
|
|
91
|
+
PKG_JSON="$REPO_PATH/package.json"
|
|
92
|
+
PKG_JSON="$PKG_JSON" TARGET_VERSION="$TARGET_VERSION" node -e '
|
|
93
|
+
const fs = require("fs");
|
|
94
|
+
const f = process.env.PKG_JSON;
|
|
95
|
+
const j = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
96
|
+
j.version = process.env.TARGET_VERSION;
|
|
97
|
+
fs.writeFileSync(f, JSON.stringify(j, null, 2) + "\n");
|
|
98
|
+
' || fail "version-write" "node could not write $TARGET_VERSION into package.json"
|
|
99
|
+
echo "PUBLISH_DIFF:"
|
|
100
|
+
git -C "$REPO_PATH" diff -- package.json
|
|
101
|
+
ADD_DEL="$(git -C "$REPO_PATH" diff --numstat -- package.json | tr '\t' ' ')"
|
|
102
|
+
[ "$ADD_DEL" = "1 1 package.json" ] \
|
|
103
|
+
|| fail "version-diff" "expected a single-line version change, got: $ADD_DEL"
|
|
104
|
+
|
|
105
|
+
# 9. Commit only if changed (retried run leaves the commit in place).
|
|
106
|
+
if git -C "$REPO_PATH" diff --quiet -- package.json; then
|
|
107
|
+
echo "PUBLISH_COMMIT_EXISTS"
|
|
108
|
+
else
|
|
109
|
+
git -C "$REPO_PATH" add package.json \
|
|
110
|
+
|| fail "commit" "git add package.json failed"
|
|
111
|
+
git -C "$REPO_PATH" commit -m "release: $PKG@$TARGET_VERSION ($TASK_ID)" >/dev/null \
|
|
112
|
+
|| fail "commit" "git commit failed"
|
|
113
|
+
echo "PUBLISH_COMMIT=$(git -C "$REPO_PATH" rev-parse HEAD)"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# 10. Pack and sanity-check the tarball's embedded version.
|
|
117
|
+
cd "$REPO_PATH" || fail "pack" "cannot cd to $REPO_PATH"
|
|
118
|
+
npm pack >/dev/null 2>&1 || fail "pack" "npm pack failed"
|
|
119
|
+
TGZ="$REPO_PATH/$PKG-$TARGET_VERSION.tgz"
|
|
120
|
+
[ -f "$TGZ" ] || fail "pack" "expected tarball $TGZ not produced"
|
|
121
|
+
TGZ_VER="$(tar -xzOf "$TGZ" package/package.json \
|
|
122
|
+
| node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version")" \
|
|
123
|
+
|| fail "pack-version-mismatch" "could not read version from tarball $TGZ"
|
|
124
|
+
[ "$TGZ_VER" = "$TARGET_VERSION" ] \
|
|
125
|
+
|| fail "pack-version-mismatch" "tarball has $TGZ_VER, expected $TARGET_VERSION"
|
|
126
|
+
|
|
127
|
+
# 11. Publish. "previously published versions" means a retried Publish
|
|
128
|
+
# already landed this version (the merge lock guarantees no other task
|
|
129
|
+
# picked it) — continue. Any other failure is fatal.
|
|
130
|
+
if PUB_OUT="$(python3 "$NPM_PUBLISH_PY" "$TGZ" 2>&1)"; then
|
|
131
|
+
echo "PUBLISHED=$TARGET_VERSION"
|
|
132
|
+
else
|
|
133
|
+
case "$PUB_OUT" in
|
|
134
|
+
*"previously published versions"*)
|
|
135
|
+
echo "PUBLISHED_ALREADY=$TARGET_VERSION"
|
|
136
|
+
;;
|
|
137
|
+
*)
|
|
138
|
+
rm -f "$TGZ"
|
|
139
|
+
fail "publish" "$(printf '%s\n' "$PUB_OUT" | tail -5)"
|
|
140
|
+
;;
|
|
141
|
+
esac
|
|
142
|
+
fi
|
|
143
|
+
rm -f "$TGZ"
|
|
144
|
+
|
|
145
|
+
# 12. Verify: ground truth is the registry, not any agent's summary.
|
|
146
|
+
REG="$(npm view "$PKG" version 2>/dev/null)" \
|
|
147
|
+
|| fail "verify" "npm view $PKG failed"
|
|
148
|
+
[ "$REG" = "$TARGET_VERSION" ] \
|
|
149
|
+
|| fail "verify" "registry=$REG"
|
|
150
|
+
echo "PUBLISH_VERIFIED=$TARGET_VERSION"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# 13. Push the version-bump commit (idempotent: no-op if already pushed).
|
|
154
|
+
PUSH_OUT="$(git -C "$REPO_PATH" push origin main 2>&1)" \
|
|
155
|
+
|| fail "push" "$PUSH_OUT"
|
|
156
|
+
echo "PUBLISH_PUSHED=1"
|
|
157
|
+
|
|
158
|
+
# 14. Finalize: releases the merge lock and cleans up the worktree.
|
|
159
|
+
FIN_OUT="$(CREW_REPO="$REPO_PATH" bash "$LIFECYCLE" post-deploy "$TASK_ID" 2>&1)" \
|
|
160
|
+
|| fail "post-deploy" "$FIN_OUT"
|
|
161
|
+
case "$FIN_OUT" in
|
|
162
|
+
*DEPLOYED*) echo "PUBLISH_FINALIZED=1" ;;
|
|
163
|
+
*) fail "post-deploy" "missing DEPLOYED marker: $FIN_OUT" ;;
|
|
164
|
+
esac
|
|
165
|
+
|
|
166
|
+
# Final marker: the full publish path completed.
|
|
167
|
+
echo "PUBLISH_COMPLETE=$TARGET_VERSION"
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -32,6 +32,8 @@ const MERGE_LOCK_SRC = crewHome + "/lib/merge-lock.sh";
|
|
|
32
32
|
const RUN_LIB = "/tmp/crew-lib-" + taskId;
|
|
33
33
|
const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
|
|
34
34
|
const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
|
|
35
|
+
const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
36
|
+
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
35
37
|
const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
|
|
36
38
|
const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
|
|
37
39
|
|
|
@@ -84,6 +86,15 @@ let mapperSpec = "";
|
|
|
84
86
|
// rationalized a skip against explicit instruction text — text alone did not
|
|
85
87
|
// hold, so the decision now lives in workflow code, not agent judgment.
|
|
86
88
|
let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
|
|
89
|
+
// Deterministic publish target — computed by the workflow (registry base +
|
|
90
|
+
// bumpVersion), never by the Publish agent.
|
|
91
|
+
let publishTarget = null; // { base, scope, target }
|
|
92
|
+
function bumpVersion(base, scope) {
|
|
93
|
+
var p = String(base).trim().split(".").map(function (x) { return parseInt(x, 10) || 0; });
|
|
94
|
+
if (scope === "major") return (p[0] + 1) + ".0.0";
|
|
95
|
+
if (scope === "minor") return p[0] + "." + (p[1] + 1) + ".0";
|
|
96
|
+
return p[0] + "." + p[1] + "." + (p[2] + 1); // patch (default)
|
|
97
|
+
}
|
|
87
98
|
function extractReleaseDecision(text) {
|
|
88
99
|
const t = text || "";
|
|
89
100
|
const r = /^release:\s*(yes|no)\s*$/im.exec(t);
|
|
@@ -101,7 +112,7 @@ let i = startStepIndex;
|
|
|
101
112
|
// Pin lifecycle scripts
|
|
102
113
|
await agent(
|
|
103
114
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
104
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + ORPHAN_SWEEP,
|
|
115
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " " + ORPHAN_SWEEP,
|
|
105
116
|
{ key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
|
|
106
117
|
);
|
|
107
118
|
|
|
@@ -188,6 +199,32 @@ while (i < STEPS.length) {
|
|
|
188
199
|
i++;
|
|
189
200
|
continue;
|
|
190
201
|
}
|
|
202
|
+
|
|
203
|
+
// Deterministic pre-publish: the workflow reads the registry base and computes
|
|
204
|
+
// the target version. The Publish agent never does version math. Guarded by
|
|
205
|
+
// the enclosing Publish+npm branch: releaseDecision is guaranteed non-null
|
|
206
|
+
// and release !== "no" at this point.
|
|
207
|
+
try {
|
|
208
|
+
var baseResult = await agent(
|
|
209
|
+
"Read the npm registry base version.\n" +
|
|
210
|
+
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
211
|
+
"If the output is NOT_FOUND, run: node -p \"require('" + REPO_PATH + "/package.json').version\"\n" +
|
|
212
|
+
"Return JSON { \"base\": \"<the version string, trimmed>\" } and nothing else.",
|
|
213
|
+
{ key: "publish-base-" + taskId, label: "Reading registry base version",
|
|
214
|
+
schema: { type: "object", properties: { base: { type: "string" } }, required: ["base"] } }
|
|
215
|
+
);
|
|
216
|
+
var pubScope = releaseDecision.version_bump || "patch";
|
|
217
|
+
var pubBase = (baseResult.base || "").trim();
|
|
218
|
+
if (!/^\d+\.\d+\.\d+$/.test(pubBase)) {
|
|
219
|
+
return { status: "blocked", task_id: taskId,
|
|
220
|
+
reason: "Publish base version unreadable: '" + pubBase + "'. Fail-closed." };
|
|
221
|
+
}
|
|
222
|
+
publishTarget = { base: pubBase, scope: pubScope, target: bumpVersion(pubBase, pubScope) };
|
|
223
|
+
log("Publish target for task " + taskId + ": " + pubBase + " + " + pubScope + " -> " + publishTarget.target);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
return { status: "blocked", task_id: taskId,
|
|
226
|
+
reason: "Deterministic pre-publish failed: " + (e && e.message ? e.message : e) + ". Fail-closed." };
|
|
227
|
+
}
|
|
191
228
|
}
|
|
192
229
|
|
|
193
230
|
// Claim session
|
|
@@ -305,35 +342,24 @@ while (i < STEPS.length) {
|
|
|
305
342
|
|
|
306
343
|
} else if (step.name === "Publish") {
|
|
307
344
|
if (PUBLISH_TYPE === "npm") {
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"
|
|
317
|
-
"Run
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"
|
|
325
|
-
"
|
|
326
|
-
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-<new-version>.tgz\n" +
|
|
327
|
-
"If publish fails with 'You cannot publish over the previously published versions', <new-version> is already on the registry (a retried Publish — the merge lock guarantees no other task picked this version): continue to STEP 6 verification. Any other publish failure: set passed to false with the failure details.\n\n" +
|
|
328
|
-
"STEP 6: Verify.\n" +
|
|
329
|
-
"Run: npm view muse-crew version\n" +
|
|
330
|
-
"It must equal <new-version>. If not, set passed to false with the mismatch details.\n\n" +
|
|
331
|
-
"STEP 7: Push the version-bump commit: cd " + REPO_PATH + " && git push origin main. Never use --force.\n\n" +
|
|
332
|
-
"STEP 8: Finalize.\n" +
|
|
333
|
-
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
334
|
-
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
335
|
-
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
336
|
-
"End your summary with exactly this line: published: muse-crew@<new-version>\n\n" +
|
|
345
|
+
// Deterministic publish: the workflow computed the target version and the
|
|
346
|
+
// agent runs exactly one command — the pinned publish script. The script
|
|
347
|
+
// does lock refresh, release install, version write/commit, pack, registry
|
|
348
|
+
// publish, verify, push, and post-deploy. Text stays minimal by design;
|
|
349
|
+
// decisions live in code, not in agent judgment.
|
|
350
|
+
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
351
|
+
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
352
|
+
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
353
|
+
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make and no skip path.\n\n" +
|
|
354
|
+
"Run exactly this command and no other publish-related commands:\n" +
|
|
355
|
+
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
356
|
+
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
357
|
+
" bash " + PUBLISH_NPM + "\n\n" +
|
|
358
|
+
"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" +
|
|
359
|
+
"If the command exits nonzero, set passed to false and put the script's PUBLISH_FAILED line in your summary.\n" +
|
|
360
|
+
"If it exits zero, paste the script's COMPLETE marker block verbatim into your summary, then end your summary with exactly these two lines, in this order — lowercase, no trailing period, do not rephrase (the QA backstop extracts them by pattern):\n" +
|
|
361
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
362
|
+
"published: muse-crew@" + publishTarget.target + "\n\n" +
|
|
337
363
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
338
364
|
"No prose, no markdown, just the JSON object.";
|
|
339
365
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -374,7 +400,10 @@ while (i < STEPS.length) {
|
|
|
374
400
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
375
401
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
376
402
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
377
|
-
? "NPM PUBLISH CHECK: the accepted Build summary declared release: yes, so this run's Publish phase must have published. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and
|
|
403
|
+
? "NPM PUBLISH CHECK: the accepted Build summary declared release: yes, so this run's Publish phase must have published. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and read its session notes (the Publish agent's summary — event history does NOT carry it).\n" +
|
|
404
|
+
"Extract the line matching TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version>. If the line is missing, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: Publish summary did not echo its computed target version (STEP 3B)\" }.\n" +
|
|
405
|
+
"Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if the Publish agent applied a different scope, FAIL. Verify the ARITHMETIC: <base> + <scope> must equal <new-version> (patch increments the last segment only, e.g. 0.3.0 + patch → 0.3.1; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0) — if the math is wrong, FAIL.\n" +
|
|
406
|
+
"Extract the published version from the notes line matching published: muse-crew@<version> (match case-insensitively and ignore any trailing period — agents sometimes rephrase it). It MUST equal <new-version> from the TARGET_VERSION line. Then run: npm view muse-crew version. The registry version MUST equal <new-version>. If any of these checks fails, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: [details]\" }.\n"
|
|
378
407
|
: "";
|
|
379
408
|
instructions = "Final QA testing. You are CODE-BLIND — do NOT read source code.\n" +
|
|
380
409
|
"Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n" +
|
|
@@ -460,6 +489,27 @@ while (i < STEPS.length) {
|
|
|
460
489
|
const passed = stepResult.passed !== false;
|
|
461
490
|
const status = passed ? "completed" : "rejected";
|
|
462
491
|
|
|
492
|
+
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
493
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
494
|
+
try {
|
|
495
|
+
var verifyResult = await agent(
|
|
496
|
+
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
497
|
+
{ key: "verify-publish-" + taskId, label: "Verifying published version",
|
|
498
|
+
schema: { type: "object", properties: { registry_version: { type: "string" } }, required: ["registry_version"] } }
|
|
499
|
+
);
|
|
500
|
+
var regVer = (verifyResult.registry_version || "").trim();
|
|
501
|
+
if (regVer !== publishTarget.target) {
|
|
502
|
+
return { status: "blocked", task_id: taskId,
|
|
503
|
+
reason: "Publish verification failed: registry shows " + regVer + " but the release decision required " +
|
|
504
|
+
publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land." };
|
|
505
|
+
}
|
|
506
|
+
log("Publish verified for task " + taskId + ": registry at " + regVer);
|
|
507
|
+
} catch (e) {
|
|
508
|
+
return { status: "blocked", task_id: taskId,
|
|
509
|
+
reason: "Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed." };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
463
513
|
// Capture mapper's spec for Build and Review
|
|
464
514
|
if (step.name === "Map" && passed) {
|
|
465
515
|
mapperSpec = summary;
|
package/workflows/chore.js
CHANGED
|
@@ -30,6 +30,8 @@ const MERGE_LOCK_SRC = crewHome + "/lib/merge-lock.sh";
|
|
|
30
30
|
const RUN_LIB = "/tmp/crew-lib-" + taskId;
|
|
31
31
|
const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
|
|
32
32
|
const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
|
|
33
|
+
const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
34
|
+
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
33
35
|
const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
|
|
34
36
|
const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
|
|
35
37
|
|
|
@@ -78,6 +80,15 @@ let mapperSpec = "";
|
|
|
78
80
|
// rationalized a skip against explicit instruction text — text alone did not
|
|
79
81
|
// hold, so the decision now lives in workflow code, not agent judgment.
|
|
80
82
|
let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
|
|
83
|
+
// Deterministic publish target — computed by the workflow (registry base +
|
|
84
|
+
// bumpVersion), never by the Publish agent.
|
|
85
|
+
let publishTarget = null; // { base, scope, target }
|
|
86
|
+
function bumpVersion(base, scope) {
|
|
87
|
+
var p = String(base).trim().split(".").map(function (x) { return parseInt(x, 10) || 0; });
|
|
88
|
+
if (scope === "major") return (p[0] + 1) + ".0.0";
|
|
89
|
+
if (scope === "minor") return p[0] + "." + (p[1] + 1) + ".0";
|
|
90
|
+
return p[0] + "." + p[1] + "." + (p[2] + 1); // patch (default)
|
|
91
|
+
}
|
|
81
92
|
function extractReleaseDecision(text) {
|
|
82
93
|
const t = text || "";
|
|
83
94
|
const r = /^release:\s*(yes|no)\s*$/im.exec(t);
|
|
@@ -95,7 +106,7 @@ let i = startStepIndex;
|
|
|
95
106
|
// Pin lifecycle scripts
|
|
96
107
|
await agent(
|
|
97
108
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
98
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + ORPHAN_SWEEP,
|
|
109
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " " + ORPHAN_SWEEP,
|
|
99
110
|
{ key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
|
|
100
111
|
);
|
|
101
112
|
|
|
@@ -182,6 +193,32 @@ while (i < STEPS.length) {
|
|
|
182
193
|
i++;
|
|
183
194
|
continue;
|
|
184
195
|
}
|
|
196
|
+
|
|
197
|
+
// Deterministic pre-publish: the workflow reads the registry base and computes
|
|
198
|
+
// the target version. The Publish agent never does version math. Guarded by
|
|
199
|
+
// the enclosing Publish+npm branch: releaseDecision is guaranteed non-null
|
|
200
|
+
// and release !== "no" at this point.
|
|
201
|
+
try {
|
|
202
|
+
var baseResult = await agent(
|
|
203
|
+
"Read the npm registry base version.\n" +
|
|
204
|
+
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
205
|
+
"If the output is NOT_FOUND, run: node -p \"require('" + REPO_PATH + "/package.json').version\"\n" +
|
|
206
|
+
"Return JSON { \"base\": \"<the version string, trimmed>\" } and nothing else.",
|
|
207
|
+
{ key: "publish-base-" + taskId, label: "Reading registry base version",
|
|
208
|
+
schema: { type: "object", properties: { base: { type: "string" } }, required: ["base"] } }
|
|
209
|
+
);
|
|
210
|
+
var pubScope = releaseDecision.version_bump || "patch";
|
|
211
|
+
var pubBase = (baseResult.base || "").trim();
|
|
212
|
+
if (!/^\d+\.\d+\.\d+$/.test(pubBase)) {
|
|
213
|
+
return { status: "blocked", task_id: taskId,
|
|
214
|
+
reason: "Publish base version unreadable: '" + pubBase + "'. Fail-closed." };
|
|
215
|
+
}
|
|
216
|
+
publishTarget = { base: pubBase, scope: pubScope, target: bumpVersion(pubBase, pubScope) };
|
|
217
|
+
log("Publish target for task " + taskId + ": " + pubBase + " + " + pubScope + " -> " + publishTarget.target);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return { status: "blocked", task_id: taskId,
|
|
220
|
+
reason: "Deterministic pre-publish failed: " + (e && e.message ? e.message : e) + ". Fail-closed." };
|
|
221
|
+
}
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
let activeSessionId;
|
|
@@ -287,35 +324,24 @@ while (i < STEPS.length) {
|
|
|
287
324
|
|
|
288
325
|
} else if (step.name === "Publish") {
|
|
289
326
|
if (PUBLISH_TYPE === "npm") {
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"Run
|
|
300
|
-
"
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-<new-version>.tgz\n" +
|
|
309
|
-
"If publish fails with 'You cannot publish over the previously published versions', <new-version> is already on the registry (a retried Publish — the merge lock guarantees no other task picked this version): continue to STEP 6 verification. Any other publish failure: set passed to false with the failure details.\n\n" +
|
|
310
|
-
"STEP 6: Verify.\n" +
|
|
311
|
-
"Run: npm view muse-crew version\n" +
|
|
312
|
-
"It must equal <new-version>. If not, set passed to false with the mismatch details.\n\n" +
|
|
313
|
-
"STEP 7: Push the version-bump commit: cd " + REPO_PATH + " && git push origin main. Never use --force.\n\n" +
|
|
314
|
-
"STEP 8: Finalize.\n" +
|
|
315
|
-
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
316
|
-
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
317
|
-
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
318
|
-
"End your summary with exactly this line: published: muse-crew@<new-version>\n\n" +
|
|
327
|
+
// Deterministic publish: the workflow computed the target version and the
|
|
328
|
+
// agent runs exactly one command — the pinned publish script. The script
|
|
329
|
+
// does lock refresh, release install, version write/commit, pack, registry
|
|
330
|
+
// publish, verify, push, and post-deploy. Text stays minimal by design;
|
|
331
|
+
// decisions live in code, not in agent judgment.
|
|
332
|
+
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
333
|
+
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
334
|
+
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
335
|
+
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make and no skip path.\n\n" +
|
|
336
|
+
"Run exactly this command and no other publish-related commands:\n" +
|
|
337
|
+
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
338
|
+
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
339
|
+
" bash " + PUBLISH_NPM + "\n\n" +
|
|
340
|
+
"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" +
|
|
341
|
+
"If the command exits nonzero, set passed to false and put the script's PUBLISH_FAILED line in your summary.\n" +
|
|
342
|
+
"If it exits zero, paste the script's COMPLETE marker block verbatim into your summary, then end your summary with exactly these two lines, in this order — lowercase, no trailing period, do not rephrase (the QA backstop extracts them by pattern):\n" +
|
|
343
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
344
|
+
"published: muse-crew@" + publishTarget.target + "\n\n" +
|
|
319
345
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
320
346
|
"No prose, no markdown, just the JSON object.";
|
|
321
347
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -409,6 +435,27 @@ while (i < STEPS.length) {
|
|
|
409
435
|
const passed = stepResult.passed !== false;
|
|
410
436
|
const status = passed ? "completed" : "rejected";
|
|
411
437
|
|
|
438
|
+
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
439
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
440
|
+
try {
|
|
441
|
+
var verifyResult = await agent(
|
|
442
|
+
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
443
|
+
{ key: "verify-publish-" + taskId, label: "Verifying published version",
|
|
444
|
+
schema: { type: "object", properties: { registry_version: { type: "string" } }, required: ["registry_version"] } }
|
|
445
|
+
);
|
|
446
|
+
var regVer = (verifyResult.registry_version || "").trim();
|
|
447
|
+
if (regVer !== publishTarget.target) {
|
|
448
|
+
return { status: "blocked", task_id: taskId,
|
|
449
|
+
reason: "Publish verification failed: registry shows " + regVer + " but the release decision required " +
|
|
450
|
+
publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land." };
|
|
451
|
+
}
|
|
452
|
+
log("Publish verified for task " + taskId + ": registry at " + regVer);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
return { status: "blocked", task_id: taskId,
|
|
455
|
+
reason: "Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed." };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
412
459
|
// Capture mapper's spec for Build and Review
|
|
413
460
|
if (step.name === "Map" && passed) {
|
|
414
461
|
mapperSpec = summary;
|
package/workflows/standard.js
CHANGED
|
@@ -32,6 +32,8 @@ const MERGE_LOCK_SRC = crewHome + "/lib/merge-lock.sh";
|
|
|
32
32
|
const RUN_LIB = "/tmp/crew-lib-" + taskId;
|
|
33
33
|
const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
|
|
34
34
|
const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
|
|
35
|
+
const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
36
|
+
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
35
37
|
const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
|
|
36
38
|
const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
|
|
37
39
|
|
|
@@ -83,6 +85,15 @@ let mapperSpec = "";
|
|
|
83
85
|
// rationalized a skip against explicit instruction text — text alone did not
|
|
84
86
|
// hold, so the decision now lives in workflow code, not agent judgment.
|
|
85
87
|
let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
|
|
88
|
+
// Deterministic publish target — computed by the workflow (registry base +
|
|
89
|
+
// bumpVersion), never by the Publish agent.
|
|
90
|
+
let publishTarget = null; // { base, scope, target }
|
|
91
|
+
function bumpVersion(base, scope) {
|
|
92
|
+
var p = String(base).trim().split(".").map(function (x) { return parseInt(x, 10) || 0; });
|
|
93
|
+
if (scope === "major") return (p[0] + 1) + ".0.0";
|
|
94
|
+
if (scope === "minor") return p[0] + "." + (p[1] + 1) + ".0";
|
|
95
|
+
return p[0] + "." + p[1] + "." + (p[2] + 1); // patch (default)
|
|
96
|
+
}
|
|
86
97
|
function extractReleaseDecision(text) {
|
|
87
98
|
const t = text || "";
|
|
88
99
|
const r = /^release:\s*(yes|no)\s*$/im.exec(t);
|
|
@@ -106,8 +117,9 @@ await agent(
|
|
|
106
117
|
" mkdir -p " + RUN_LIB + "\n" +
|
|
107
118
|
" cp " + LIFECYCLE_SRC + " " + LIFECYCLE + "\n" +
|
|
108
119
|
" cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + "\n" +
|
|
120
|
+
" cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + "\n" +
|
|
109
121
|
" cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + "\n" +
|
|
110
|
-
" chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + ORPHAN_SWEEP + "\n" +
|
|
122
|
+
" chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " " + ORPHAN_SWEEP + "\n" +
|
|
111
123
|
"Confirm the files exist by listing " + RUN_LIB + ".",
|
|
112
124
|
{ key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
|
|
113
125
|
);
|
|
@@ -196,6 +208,32 @@ while (i < STEPS.length) {
|
|
|
196
208
|
i++;
|
|
197
209
|
continue;
|
|
198
210
|
}
|
|
211
|
+
|
|
212
|
+
// Deterministic pre-publish: the workflow reads the registry base and computes
|
|
213
|
+
// the target version. The Publish agent never does version math. Guarded by
|
|
214
|
+
// the enclosing Publish+npm branch: releaseDecision is guaranteed non-null
|
|
215
|
+
// and release !== "no" at this point.
|
|
216
|
+
try {
|
|
217
|
+
var baseResult = await agent(
|
|
218
|
+
"Read the npm registry base version.\n" +
|
|
219
|
+
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
220
|
+
"If the output is NOT_FOUND, run: node -p \"require('" + REPO_PATH + "/package.json').version\"\n" +
|
|
221
|
+
"Return JSON { \"base\": \"<the version string, trimmed>\" } and nothing else.",
|
|
222
|
+
{ key: "publish-base-" + taskId, label: "Reading registry base version",
|
|
223
|
+
schema: { type: "object", properties: { base: { type: "string" } }, required: ["base"] } }
|
|
224
|
+
);
|
|
225
|
+
var pubScope = releaseDecision.version_bump || "patch";
|
|
226
|
+
var pubBase = (baseResult.base || "").trim();
|
|
227
|
+
if (!/^\d+\.\d+\.\d+$/.test(pubBase)) {
|
|
228
|
+
return { status: "blocked", task_id: taskId,
|
|
229
|
+
reason: "Publish base version unreadable: '" + pubBase + "'. Fail-closed." };
|
|
230
|
+
}
|
|
231
|
+
publishTarget = { base: pubBase, scope: pubScope, target: bumpVersion(pubBase, pubScope) };
|
|
232
|
+
log("Publish target for task " + taskId + ": " + pubBase + " + " + pubScope + " -> " + publishTarget.target);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return { status: "blocked", task_id: taskId,
|
|
235
|
+
reason: "Deterministic pre-publish failed: " + (e && e.message ? e.message : e) + ". Fail-closed." };
|
|
236
|
+
}
|
|
199
237
|
}
|
|
200
238
|
|
|
201
239
|
// Claim session — reuse dispatcher's session for the very first step
|
|
@@ -303,35 +341,24 @@ while (i < STEPS.length) {
|
|
|
303
341
|
|
|
304
342
|
} else if (step.name === "Publish") {
|
|
305
343
|
if (PUBLISH_TYPE === "npm") {
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"Run
|
|
316
|
-
"
|
|
317
|
-
"
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-<new-version>.tgz\n" +
|
|
325
|
-
"If publish fails with 'You cannot publish over the previously published versions', <new-version> is already on the registry (a retried Publish — the merge lock guarantees no other task picked this version): continue to STEP 6 verification. Any other publish failure: set passed to false with the failure details.\n\n" +
|
|
326
|
-
"STEP 6: Verify.\n" +
|
|
327
|
-
"Run: npm view muse-crew version\n" +
|
|
328
|
-
"It must equal <new-version>. If not, set passed to false with the mismatch details.\n\n" +
|
|
329
|
-
"STEP 7: Push the version-bump commit: cd " + REPO_PATH + " && git push origin main. Never use --force.\n\n" +
|
|
330
|
-
"STEP 8: Finalize.\n" +
|
|
331
|
-
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
332
|
-
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
333
|
-
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
334
|
-
"End your summary with exactly this line: published: muse-crew@<new-version>\n\n" +
|
|
344
|
+
// Deterministic publish: the workflow computed the target version and the
|
|
345
|
+
// agent runs exactly one command — the pinned publish script. The script
|
|
346
|
+
// does lock refresh, release install, version write/commit, pack, registry
|
|
347
|
+
// publish, verify, push, and post-deploy. Text stays minimal by design;
|
|
348
|
+
// decisions live in code, not in agent judgment.
|
|
349
|
+
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
350
|
+
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
351
|
+
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
352
|
+
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make and no skip path.\n\n" +
|
|
353
|
+
"Run exactly this command and no other publish-related commands:\n" +
|
|
354
|
+
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
355
|
+
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
356
|
+
" bash " + PUBLISH_NPM + "\n\n" +
|
|
357
|
+
"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" +
|
|
358
|
+
"If the command exits nonzero, set passed to false and put the script's PUBLISH_FAILED line in your summary.\n" +
|
|
359
|
+
"If it exits zero, paste the script's COMPLETE marker block verbatim into your summary, then end your summary with exactly these two lines, in this order — lowercase, no trailing period, do not rephrase (the QA backstop extracts them by pattern):\n" +
|
|
360
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
361
|
+
"published: muse-crew@" + publishTarget.target + "\n\n" +
|
|
335
362
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
336
363
|
"No prose, no markdown, just the JSON object.";
|
|
337
364
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -372,7 +399,10 @@ while (i < STEPS.length) {
|
|
|
372
399
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
373
400
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
374
401
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
375
|
-
? "NPM PUBLISH CHECK: the accepted Build summary declared release: yes, so this run's Publish phase must have published. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and
|
|
402
|
+
? "NPM PUBLISH CHECK: the accepted Build summary declared release: yes, so this run's Publish phase must have published. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and read its session notes (the Publish agent's summary — event history does NOT carry it).\n" +
|
|
403
|
+
"Extract the line matching TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version>. If the line is missing, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: Publish summary did not echo its computed target version (STEP 3B)\" }.\n" +
|
|
404
|
+
"Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if the Publish agent applied a different scope, FAIL. Verify the ARITHMETIC: <base> + <scope> must equal <new-version> (patch increments the last segment only, e.g. 0.3.0 + patch → 0.3.1; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0) — if the math is wrong, FAIL.\n" +
|
|
405
|
+
"Extract the published version from the notes line matching published: muse-crew@<version> (match case-insensitively and ignore any trailing period — agents sometimes rephrase it). It MUST equal <new-version> from the TARGET_VERSION line. Then run: npm view muse-crew version. The registry version MUST equal <new-version>. If any of these checks fails, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: [details]\" }.\n"
|
|
376
406
|
: "";
|
|
377
407
|
if (PUBLISH_TYPE === "artifact") {
|
|
378
408
|
var safeDesc = taskDescription.replace(/"/g, "'").replace(/\\/g, "\\\\").slice(0, 500);
|
|
@@ -471,6 +501,27 @@ while (i < STEPS.length) {
|
|
|
471
501
|
const passed = stepResult.passed !== false;
|
|
472
502
|
const status = passed ? "completed" : "rejected";
|
|
473
503
|
|
|
504
|
+
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
505
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
506
|
+
try {
|
|
507
|
+
var verifyResult = await agent(
|
|
508
|
+
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
509
|
+
{ key: "verify-publish-" + taskId, label: "Verifying published version",
|
|
510
|
+
schema: { type: "object", properties: { registry_version: { type: "string" } }, required: ["registry_version"] } }
|
|
511
|
+
);
|
|
512
|
+
var regVer = (verifyResult.registry_version || "").trim();
|
|
513
|
+
if (regVer !== publishTarget.target) {
|
|
514
|
+
return { status: "blocked", task_id: taskId,
|
|
515
|
+
reason: "Publish verification failed: registry shows " + regVer + " but the release decision required " +
|
|
516
|
+
publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land." };
|
|
517
|
+
}
|
|
518
|
+
log("Publish verified for task " + taskId + ": registry at " + regVer);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
return { status: "blocked", task_id: taskId,
|
|
521
|
+
reason: "Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed." };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
474
525
|
// Capture mapper's spec for Build and Review
|
|
475
526
|
if (step.name === "Map" && passed) {
|
|
476
527
|
mapperSpec = summary;
|