muse-crew 0.4.6 → 0.4.7
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 +3 -2
- package/lib/publish-npm.sh +133 -105
- package/lib/test-publish-skip.sh +192 -0
- package/lib/worktree-lifecycle.sh +13 -1
- package/package.json +1 -1
- package/workflows/bugfix.js +89 -26
- package/workflows/chore.js +86 -25
- package/workflows/standard.js +89 -26
package/lib/AGENTS.md
CHANGED
|
@@ -5,10 +5,11 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
5
5
|
- `build-registry.js` — deterministic extractor that generates `workflows/registry.json` (workflow step registry) from the workflow files' `meta` blocks at release time; invoked by `crew-release.sh` deploy
|
|
6
6
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
7
7
|
- `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
|
|
8
|
-
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/status/post-deploy over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving.
|
|
8
|
+
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/status/post-deploy/refresh-lock/lock-status over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving. `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or `LOCKED by <holder> since <ts> (pid <pid>)`, always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure.
|
|
9
9
|
- `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight) on a scratch repo
|
|
10
|
-
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the
|
|
10
|
+
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the \\u2014 escape; extracts the shipped block by anchor)
|
|
11
11
|
- `test-publish-verify.sh` — regression tests for the retry-tolerant step-12 verification in publish-npm.sh (canary 5a027278): extracts the shipped block by anchor and runs it against a fake npm whose read replica lags (non-zero exits, then the old version, then the target) — requires convergence on success, fail-closed `PUBLISH_FAILED=verify` on exhaustion, and `--prefer-online` on every read
|
|
12
|
+
- `test-publish-skip.sh` — regression tests for the no-lock graceful publish skip (park 2026-09-11): `lock-status` returns exact `UNLOCKED`/`LOCKED by <holder> since <ts> (pid <pid>)`; a fake empty-diff integrate takes the MERGED_EMPTY branch and never takes the lock; the shipped step-5 block from publish-npm.sh (extracted by anchor) skips gracefully on UNLOCKED (no park, no `PUBLISH_COMPLETE`), follows the refresh path when a lock is held, and fails closed on a `lock-status` query failure; the gate sits inside the ALREADY_PUBLISHED=0 branch and mutation steps are gated on NO_LOCK_HELD=0
|
|
12
13
|
- `orphan-sweep.sh` — find and clean stale worktrees and merge locks: parses `merge-lock.sh status` key=value fields (never a PID); a lock whose task has a running session is untouched regardless of lease age; a lock with no running session is broken only when its lease is expired (via merge-lock.sh release so the break lands in the audit log)
|
|
13
14
|
- `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.
|
|
14
15
|
- `compose-evidence.py` — deterministic visual-evidence compositor (Pillow): pairs identical PNG stems from baseline/ and postchange/ dirs, emits `<stem>-sidebyside.png` and amplified-difference `<stem>-overlay.png` into composites/, prints `PAIR`/`SKIP` manifests. Byte-deterministic; nonzero exit on errors.
|
package/lib/publish-npm.sh
CHANGED
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
# On success: print the markers below, exit 0. The final marker is
|
|
24
24
|
# PUBLISH_COMPLETE=<version> — the Publish agent pastes it verbatim into its
|
|
25
25
|
# summary as the script's completion marker.
|
|
26
|
+
# When no merge lock is held (empty-diff Integrate), the publish path is
|
|
27
|
+
# skipped gracefully: PUBLISH_SKIPPED=no-lock-held is printed, no version
|
|
28
|
+
# is written or published, and PUBLISH_COMPLETE is omitted.
|
|
26
29
|
|
|
27
30
|
set -euo pipefail
|
|
28
31
|
|
|
@@ -65,113 +68,133 @@ if { [ "$HEAD_MSG" = "release: $PKG@$TARGET_VERSION ($TASK_ID)" ] \
|
|
|
65
68
|
echo "PUBLISH_VERIFIED=$TARGET_VERSION"
|
|
66
69
|
fi
|
|
67
70
|
|
|
71
|
+
NO_LOCK_HELD=0
|
|
68
72
|
if [ "$ALREADY_PUBLISHED" = "0" ]; then
|
|
69
|
-
# 5
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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) via
|
|
88
|
-
# byte-preserving text substitution. A JSON.parse/stringify round-trip is
|
|
89
|
-
# forbidden here: it expands unicode escapes (e.g. the \u2014 escape in
|
|
90
|
-
# the description field) into literal characters, producing a two-line
|
|
91
|
-
# diff that trips the preservation check below (canary 5a027278). The
|
|
92
|
-
# substitution touches exactly one line; a missed substitution yields an
|
|
93
|
-
# empty diff and fails closed at the numstat check.
|
|
94
|
-
# STEP-8-ANCHOR: version write (escape-preserving)
|
|
95
|
-
PKG_JSON="$REPO_PATH/package.json"
|
|
96
|
-
ESCAPED_VER="$(printf '%s' "$TARGET_VERSION" | sed 's/[&\\]/\\&/g')"
|
|
97
|
-
sed -i -E 's/^([[:space:]]*"version"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"'"$ESCAPED_VER"'"/' "$PKG_JSON" \
|
|
98
|
-
|| fail "version-write" "sed could not write $TARGET_VERSION into package.json"
|
|
99
|
-
# Read-only verification: parse package.json and confirm .version equals
|
|
100
|
-
# TARGET_VERSION. (Parse only — no rewrite, so no clock/randomness.)
|
|
101
|
-
NEW_VER="$(PKG_JSON="$PKG_JSON" node -p "JSON.parse(require('fs').readFileSync(process.env.PKG_JSON,'utf8')).version")" \
|
|
102
|
-
|| fail "version-write" "could not parse package.json after write"
|
|
103
|
-
[ "$NEW_VER" = "$TARGET_VERSION" ] \
|
|
104
|
-
|| fail "version-write" "package.json version is $NEW_VER, expected $TARGET_VERSION"
|
|
105
|
-
echo "PUBLISH_DIFF:"
|
|
106
|
-
git -C "$REPO_PATH" diff -- package.json
|
|
107
|
-
ADD_DEL="$(git -C "$REPO_PATH" diff --numstat -- package.json | tr '\t' ' ')"
|
|
108
|
-
[ "$ADD_DEL" = "1 1 package.json" ] \
|
|
109
|
-
|| fail "version-diff" "expected a single-line version change, got: $ADD_DEL"
|
|
110
|
-
# STEP-8-END
|
|
111
|
-
|
|
112
|
-
# 9. Commit only if changed (retried run leaves the commit in place).
|
|
113
|
-
if git -C "$REPO_PATH" diff --quiet -- package.json; then
|
|
114
|
-
echo "PUBLISH_COMMIT_EXISTS"
|
|
73
|
+
# STEP-5-ANCHOR: lock-state gate (empty-diff Integrate takes no lock).
|
|
74
|
+
# An empty-diff Integrate (MERGED_EMPTY) intentionally takes no merge
|
|
75
|
+
# lock: nothing merged, nothing to ship. When no lock is held, Publish
|
|
76
|
+
# skips the publish path gracefully instead of failing closed on
|
|
77
|
+
# refresh-lock (park 2026-09-11). UNLOCKED is explicit state, not prose.
|
|
78
|
+
# The anchor sits inside the ALREADY_PUBLISHED=0 branch, so the skip
|
|
79
|
+
# marker prints only on a genuinely unheld lock — a resumed run whose
|
|
80
|
+
# post-deploy already released the lock prints nothing here; its
|
|
81
|
+
# PUBLISHED_ALREADY/PUBLISH_VERIFIED markers already say it all.
|
|
82
|
+
LOCK_STATE="$(CREW_REPO="$REPO_PATH" bash "${LIFECYCLE:?LIFECYCLE is required}" lock-status "$TASK_ID" 2>&1)" \
|
|
83
|
+
|| fail "lock-status" "$LOCK_STATE"
|
|
84
|
+
if [ "$LOCK_STATE" = "UNLOCKED" ]; then
|
|
85
|
+
NO_LOCK_HELD=1
|
|
86
|
+
echo "PUBLISH_SKIPPED=no-lock-held"
|
|
115
87
|
else
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|| fail "commit" "git commit failed"
|
|
120
|
-
echo "PUBLISH_COMMIT=$(git -C "$REPO_PATH" rev-parse HEAD)"
|
|
88
|
+
# 5b. Refresh the merge lock (held from Integrate through post-deploy).
|
|
89
|
+
LOCK_OUT="$(CREW_REPO="$REPO_PATH" bash "${LIFECYCLE:?LIFECYCLE is required}" refresh-lock "$TASK_ID" 2>&1)" \
|
|
90
|
+
|| fail "lock-refresh" "$LOCK_OUT"
|
|
121
91
|
fi
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
[ "$TGZ_VER" = "$TARGET_VERSION" ] \
|
|
132
|
-
|| fail "pack-version-mismatch" "tarball has $TGZ_VER, expected $TARGET_VERSION"
|
|
133
|
-
|
|
134
|
-
# 11. Publish. "previously published versions" means a retried Publish
|
|
135
|
-
# already landed this version (the merge lock guarantees no other task
|
|
136
|
-
# picked it) — continue. Any other failure is fatal.
|
|
137
|
-
if PUB_OUT="$(python3 "$NPM_PUBLISH_PY" "$TGZ" 2>&1)"; then
|
|
138
|
-
echo "PUBLISHED=$TARGET_VERSION"
|
|
139
|
-
else
|
|
140
|
-
case "$PUB_OUT" in
|
|
141
|
-
*"previously published versions"*)
|
|
142
|
-
echo "PUBLISHED_ALREADY=$TARGET_VERSION"
|
|
143
|
-
;;
|
|
144
|
-
*)
|
|
145
|
-
rm -f "$TGZ"
|
|
146
|
-
fail "publish" "$(printf '%s\n' "$PUB_OUT" | tail -5)"
|
|
147
|
-
;;
|
|
92
|
+
# STEP-5-END
|
|
93
|
+
|
|
94
|
+
if [ "$NO_LOCK_HELD" = "0" ]; then
|
|
95
|
+
# 6. Install/activate the immutable release.
|
|
96
|
+
REL_OUT="$(bash "${RELEASE_SCRIPT:?RELEASE_SCRIPT is required}" deploy "$REPO_PATH" 2>&1)" \
|
|
97
|
+
|| fail "release-deploy" "$REL_OUT"
|
|
98
|
+
case "$REL_OUT" in
|
|
99
|
+
*INSTALLED*ACTIVATED*|*EXISTS*ACTIVATED*) ;;
|
|
100
|
+
*) fail "release-deploy" "unexpected output: $REL_OUT" ;;
|
|
148
101
|
esac
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
102
|
+
|
|
103
|
+
# 7. Read the old version (audit only — TARGET_VERSION stays authoritative).
|
|
104
|
+
OLD="$(git -C "$REPO_PATH" show "HEAD:package.json" \
|
|
105
|
+
| node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version")" \
|
|
106
|
+
|| fail "read-base" "could not read version from HEAD:package.json"
|
|
107
|
+
echo "PUBLISH_BASE=$OLD"
|
|
108
|
+
|
|
109
|
+
# 8. Write TARGET_VERSION into package.json (`version` field only) via
|
|
110
|
+
# byte-preserving text substitution. A JSON.parse/stringify round-trip is
|
|
111
|
+
# forbidden here: it expands unicode escapes (e.g. the \u2014 escape in
|
|
112
|
+
# the description field) into literal characters, producing a two-line
|
|
113
|
+
# diff that trips the preservation check below (canary 5a027278). The
|
|
114
|
+
# substitution touches exactly one line; a missed substitution yields an
|
|
115
|
+
# empty diff and fails closed at the numstat check.
|
|
116
|
+
# STEP-8-ANCHOR: version write (escape-preserving)
|
|
117
|
+
PKG_JSON="$REPO_PATH/package.json"
|
|
118
|
+
ESCAPED_VER="$(printf '%s' "$TARGET_VERSION" | sed 's/[&\\]/\\&/g')"
|
|
119
|
+
sed -i -E 's/^([[:space:]]*"version"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"'"$ESCAPED_VER"'"/' "$PKG_JSON" \
|
|
120
|
+
|| fail "version-write" "sed could not write $TARGET_VERSION into package.json"
|
|
121
|
+
# Read-only verification: parse package.json and confirm .version equals
|
|
122
|
+
# TARGET_VERSION. (Parse only — no rewrite, so no clock/randomness.)
|
|
123
|
+
NEW_VER="$(PKG_JSON="$PKG_JSON" node -p "JSON.parse(require('fs').readFileSync(process.env.PKG_JSON,'utf8')).version")" \
|
|
124
|
+
|| fail "version-write" "could not parse package.json after write"
|
|
125
|
+
[ "$NEW_VER" = "$TARGET_VERSION" ] \
|
|
126
|
+
|| fail "version-write" "package.json version is $NEW_VER, expected $TARGET_VERSION"
|
|
127
|
+
echo "PUBLISH_DIFF:"
|
|
128
|
+
git -C "$REPO_PATH" diff -- package.json
|
|
129
|
+
ADD_DEL="$(git -C "$REPO_PATH" diff --numstat -- package.json | tr '\t' ' ')"
|
|
130
|
+
[ "$ADD_DEL" = "1 1 package.json" ] \
|
|
131
|
+
|| fail "version-diff" "expected a single-line version change, got: $ADD_DEL"
|
|
132
|
+
# STEP-8-END
|
|
133
|
+
|
|
134
|
+
# 9. Commit only if changed (retried run leaves the commit in place).
|
|
135
|
+
if git -C "$REPO_PATH" diff --quiet -- package.json; then
|
|
136
|
+
echo "PUBLISH_COMMIT_EXISTS"
|
|
137
|
+
else
|
|
138
|
+
git -C "$REPO_PATH" add package.json \
|
|
139
|
+
|| fail "commit" "git add package.json failed"
|
|
140
|
+
git -C "$REPO_PATH" commit -m "release: $PKG@$TARGET_VERSION ($TASK_ID)" >/dev/null \
|
|
141
|
+
|| fail "commit" "git commit failed"
|
|
142
|
+
echo "PUBLISH_COMMIT=$(git -C "$REPO_PATH" rev-parse HEAD)"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# 10. Pack and sanity-check the tarball's embedded version.
|
|
146
|
+
cd "$REPO_PATH" || fail "pack" "cannot cd to $REPO_PATH"
|
|
147
|
+
npm pack >/dev/null 2>&1 || fail "pack" "npm pack failed"
|
|
148
|
+
TGZ="$REPO_PATH/$PKG-$TARGET_VERSION.tgz"
|
|
149
|
+
[ -f "$TGZ" ] || fail "pack" "expected tarball $TGZ not produced"
|
|
150
|
+
TGZ_VER="$(tar -xzOf "$TGZ" package/package.json \
|
|
151
|
+
| node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version")" \
|
|
152
|
+
|| fail "pack-version-mismatch" "could not read version from tarball $TGZ"
|
|
153
|
+
[ "$TGZ_VER" = "$TARGET_VERSION" ] \
|
|
154
|
+
|| fail "pack-version-mismatch" "tarball has $TGZ_VER, expected $TARGET_VERSION"
|
|
155
|
+
|
|
156
|
+
# 11. Publish. "previously published versions" means a retried Publish
|
|
157
|
+
# already landed this version (the merge lock guarantees no other task
|
|
158
|
+
# picked it) — continue. Any other failure is fatal.
|
|
159
|
+
if PUB_OUT="$(python3 "$NPM_PUBLISH_PY" "$TGZ" 2>&1)"; then
|
|
160
|
+
echo "PUBLISHED=$TARGET_VERSION"
|
|
161
|
+
else
|
|
162
|
+
case "$PUB_OUT" in
|
|
163
|
+
*"previously published versions"*)
|
|
164
|
+
echo "PUBLISHED_ALREADY=$TARGET_VERSION"
|
|
165
|
+
;;
|
|
166
|
+
*)
|
|
167
|
+
rm -f "$TGZ"
|
|
168
|
+
fail "publish" "$(printf '%s\n' "$PUB_OUT" | tail -5)"
|
|
169
|
+
;;
|
|
170
|
+
esac
|
|
171
|
+
fi
|
|
172
|
+
rm -f "$TGZ"
|
|
173
|
+
|
|
174
|
+
# 12. Verify: ground truth is the registry, not any agent's summary.
|
|
175
|
+
# The registry is eventually consistent: a publish followed by an
|
|
176
|
+
# immediate read can observe the pre-publish version (canary 5a027278
|
|
177
|
+
# hit a stale read replica ~6s after publish). A single stale read must
|
|
178
|
+
# never park a landed publish, so this block retries with backoff and
|
|
179
|
+
# client cache-busting instead of failing on the first mismatch.
|
|
180
|
+
# STEP-12-ANCHOR: publish verification (retry-tolerant)
|
|
181
|
+
VERIFY_ATTEMPTS="${VERIFY_ATTEMPTS:-12}"
|
|
182
|
+
VERIFY_SLEEP_SECS="${VERIFY_SLEEP_SECS:-10}"
|
|
183
|
+
REG=""
|
|
184
|
+
for attempt in $(seq 1 "$VERIFY_ATTEMPTS"); do
|
|
185
|
+
# --prefer-online busts npm's client-side packument cache; the retry
|
|
186
|
+
# loop absorbs server-side replica lag. A non-zero exit is a miss,
|
|
187
|
+
# not an immediate failure.
|
|
188
|
+
REG="$(npm view "$PKG" version --prefer-online 2>/dev/null || true)"
|
|
189
|
+
[ "$REG" = "$TARGET_VERSION" ] && break
|
|
190
|
+
echo "PUBLISH_VERIFY_RETRY=$attempt registry=$REG"
|
|
191
|
+
[ "$attempt" -lt "$VERIFY_ATTEMPTS" ] && sleep "$VERIFY_SLEEP_SECS"
|
|
192
|
+
done
|
|
193
|
+
[ "$REG" = "$TARGET_VERSION" ] \
|
|
194
|
+
|| fail "verify" "registry=$REG after $VERIFY_ATTEMPTS attempts"
|
|
195
|
+
echo "PUBLISH_VERIFIED=$TARGET_VERSION"
|
|
196
|
+
# STEP-12-END
|
|
197
|
+
fi # NO_LOCK_HELD=0: skip the mutation path when no lock is held
|
|
175
198
|
fi
|
|
176
199
|
|
|
177
200
|
# 12b. Refresh the merge lock again — release-deploy, pack, publish, and the
|
|
@@ -195,5 +218,10 @@ case "$FIN_OUT" in
|
|
|
195
218
|
*) fail "post-deploy" "missing DEPLOYED marker: $FIN_OUT" ;;
|
|
196
219
|
esac
|
|
197
220
|
|
|
198
|
-
# Final marker
|
|
199
|
-
|
|
221
|
+
# Final marker. On the skip path (no lock held) there was nothing to
|
|
222
|
+
# publish: PUBLISH_SKIPPED=no-lock-held is the completion marker and
|
|
223
|
+
# PUBLISH_COMPLETE is deliberately not printed — the phase completed,
|
|
224
|
+
# the publish did not happen.
|
|
225
|
+
if [ "$NO_LOCK_HELD" = "0" ]; then
|
|
226
|
+
echo "PUBLISH_COMPLETE=$TARGET_VERSION"
|
|
227
|
+
fi
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-publish-skip.sh — regression test for the graceful no-lock publish
|
|
3
|
+
# skip (park 2026-09-11: empty-diff Integrate takes no merge lock, but
|
|
4
|
+
# Publish ran refresh-lock unconditionally and failed closed).
|
|
5
|
+
#
|
|
6
|
+
# Covers:
|
|
7
|
+
# - worktree-lifecycle.sh lock-status: UNLOCKED (exit 0) with no lock;
|
|
8
|
+
# LOCKED by <holder> since <ts> (pid <pid>) when held (exit 0).
|
|
9
|
+
# - fake empty-diff integrate -> lock-status reports UNLOCKED (the lock
|
|
10
|
+
# is intentionally never taken on the MERGED_EMPTY path).
|
|
11
|
+
# - the shipped step-5 block of publish-npm.sh (extracted by anchor):
|
|
12
|
+
# * UNLOCKED -> NO_LOCK_HELD=1, PUBLISH_SKIPPED=no-lock-held, exit 0
|
|
13
|
+
# * LOCKED (self) -> refresh path runs, NO_LOCK_HELD=0, exit 0
|
|
14
|
+
# * lock-status query failure -> fail "lock-status", exit 1 (fail-closed)
|
|
15
|
+
# - structural: the step-5 anchor sits inside the ALREADY_PUBLISHED=0
|
|
16
|
+
# branch (a resumed run whose post-deploy released the lock prints
|
|
17
|
+
# nothing), and the mutation steps are gated on NO_LOCK_HELD=0.
|
|
18
|
+
#
|
|
19
|
+
# Anchors: `# STEP-5-ANCHOR` ... `# STEP-5-END` in lib/publish-npm.sh.
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
LIFECYCLE="$SCRIPT_DIR/worktree-lifecycle.sh"
|
|
25
|
+
MERGE_LOCK="$SCRIPT_DIR/merge-lock.sh"
|
|
26
|
+
PUBLISH="$SCRIPT_DIR/publish-npm.sh"
|
|
27
|
+
|
|
28
|
+
pass=0
|
|
29
|
+
fail_count=0
|
|
30
|
+
ok() { echo "PASS: $1"; pass=$((pass + 1)); }
|
|
31
|
+
no() { echo "FAIL: $1"; fail_count=$((fail_count + 1)); }
|
|
32
|
+
|
|
33
|
+
TMPBASE="$(mktemp -d)"
|
|
34
|
+
trap 'rm -rf "$TMPBASE"' EXIT
|
|
35
|
+
|
|
36
|
+
# --- Scratch repo ---
|
|
37
|
+
SCRATCH="$TMPBASE/repo"
|
|
38
|
+
git init -q -b main "$SCRATCH"
|
|
39
|
+
git -C "$SCRATCH" config user.email "test@crew"
|
|
40
|
+
git -C "$SCRATCH" config user.name "crew-test"
|
|
41
|
+
echo ".worktrees/" > "$SCRATCH/.gitignore"
|
|
42
|
+
echo x > "$SCRATCH/f.txt"
|
|
43
|
+
git -C "$SCRATCH" add -A && git -C "$SCRATCH" commit -qm init
|
|
44
|
+
|
|
45
|
+
export CREW_REPO="$SCRATCH" CREW_LIB="$SCRIPT_DIR"
|
|
46
|
+
|
|
47
|
+
TASK="skiptest-task-1"
|
|
48
|
+
|
|
49
|
+
# --- lock-status with no lock held ---
|
|
50
|
+
OUT="$(bash "$LIFECYCLE" lock-status "$TASK" 2>&1)"
|
|
51
|
+
CODE=$?
|
|
52
|
+
if [ $CODE -eq 0 ] && [ "$OUT" = "UNLOCKED" ]; then
|
|
53
|
+
ok "lock-status reports UNLOCKED with no lock held (exit 0)"
|
|
54
|
+
else
|
|
55
|
+
no "lock-status UNLOCKED: code=$CODE out='$OUT'"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# --- lock-status with a lock held ---
|
|
59
|
+
bash "$MERGE_LOCK" acquire "$TASK" "$$" >/dev/null
|
|
60
|
+
OUT="$(bash "$LIFECYCLE" lock-status "$TASK" 2>&1)"
|
|
61
|
+
CODE=$?
|
|
62
|
+
if [ $CODE -eq 0 ] && printf '%s' "$OUT" | grep -Eq "^LOCKED by $TASK since .+ \(pid [0-9]+\)$"; then
|
|
63
|
+
ok "lock-status reports LOCKED by <holder> since <ts> (pid <pid>) (exit 0)"
|
|
64
|
+
else
|
|
65
|
+
no "lock-status LOCKED: code=$CODE out='$OUT'"
|
|
66
|
+
fi
|
|
67
|
+
bash "$MERGE_LOCK" release "$TASK" >/dev/null
|
|
68
|
+
|
|
69
|
+
# --- fake empty-diff integrate: MERGED_EMPTY takes no lock ---
|
|
70
|
+
bash "$LIFECYCLE" prepare "$TASK" >/dev/null 2>&1
|
|
71
|
+
INTEG="$(bash "$LIFECYCLE" integrate "$TASK" "merge: $TASK" 2>&1)"
|
|
72
|
+
if [[ "$INTEG" == MERGED_EMPTY* ]]; then
|
|
73
|
+
ok "fake empty-diff integrate takes the MERGED_EMPTY branch"
|
|
74
|
+
else
|
|
75
|
+
no "empty-diff integrate: '$INTEG'"
|
|
76
|
+
fi
|
|
77
|
+
OUT="$(bash "$LIFECYCLE" lock-status "$TASK" 2>&1)"
|
|
78
|
+
if [ "$OUT" = "UNLOCKED" ]; then
|
|
79
|
+
ok "after MERGED_EMPTY integrate the lock is still UNLOCKED (never taken)"
|
|
80
|
+
else
|
|
81
|
+
no "lock held after empty-diff integrate: '$OUT'"
|
|
82
|
+
fi
|
|
83
|
+
bash "$LIFECYCLE" cleanup "$TASK" >/dev/null 2>&1
|
|
84
|
+
|
|
85
|
+
# --- Extract the shipped step-5 block by anchor ---
|
|
86
|
+
BLOCK="$TMPBASE/step5.sh"
|
|
87
|
+
sed -n '/# STEP-5-ANCHOR/,/# STEP-5-END/p' "$PUBLISH" > "$BLOCK"
|
|
88
|
+
if grep -q 'lock-status' "$BLOCK" && grep -q 'PUBLISH_SKIPPED=no-lock-held' "$BLOCK" \
|
|
89
|
+
&& grep -q 'fail "lock-status"' "$BLOCK" && grep -q 'NO_LOCK_HELD=1' "$BLOCK"; then
|
|
90
|
+
ok "extracted step-5 block from publish-npm.sh"
|
|
91
|
+
else
|
|
92
|
+
no "step-5 extraction failed (anchors missing)"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Runner: provides fail(), env, sources the extracted block, then reports
|
|
96
|
+
# NO_LOCK_HELD and the exit code. Never calls fail = never parks.
|
|
97
|
+
run_block() { # run_block <lock_setup: none|held|broken>
|
|
98
|
+
local mode="$1"
|
|
99
|
+
local run="$TMPBASE/run-$mode"
|
|
100
|
+
mkdir -p "$run"
|
|
101
|
+
case "$mode" in
|
|
102
|
+
none) : ;;
|
|
103
|
+
held) bash "$MERGE_LOCK" acquire "$TASK" "$$" >/dev/null ;;
|
|
104
|
+
broken) : ;;
|
|
105
|
+
esac
|
|
106
|
+
(
|
|
107
|
+
set -euo pipefail
|
|
108
|
+
export CREW_REPO="$SCRATCH" TASK_ID="$TASK" REPO_PATH="$SCRATCH"
|
|
109
|
+
if [ "$mode" = "broken" ]; then
|
|
110
|
+
export LIFECYCLE="/nonexistent/lifecycle.sh"
|
|
111
|
+
else
|
|
112
|
+
export LIFECYCLE="$LIFECYCLE"
|
|
113
|
+
fi
|
|
114
|
+
fail() { echo "PUBLISH_FAILED=$1"; [ $# -ge 2 ] && echo "$2"; exit 1; }
|
|
115
|
+
NO_LOCK_HELD=0
|
|
116
|
+
# shellcheck disable=SC1090
|
|
117
|
+
source "$BLOCK"
|
|
118
|
+
echo "RESULT_NO_LOCK_HELD=$NO_LOCK_HELD"
|
|
119
|
+
) > "$run/out" 2>&1
|
|
120
|
+
local code=$?
|
|
121
|
+
echo "$code" > "$run/code"
|
|
122
|
+
case "$mode" in
|
|
123
|
+
held) bash "$MERGE_LOCK" release "$TASK" >/dev/null 2>&1 || true ;;
|
|
124
|
+
esac
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# --- UNLOCKED: skip gracefully, never park ---
|
|
128
|
+
run_block none
|
|
129
|
+
CODE="$(cat "$TMPBASE/run-none/code")"
|
|
130
|
+
OUT="$(cat "$TMPBASE/run-none/out")"
|
|
131
|
+
if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_SKIPPED=no-lock-held"* ]] \
|
|
132
|
+
&& [[ "$OUT" == *"RESULT_NO_LOCK_HELD=1"* ]] && [[ "$OUT" != *"PUBLISH_FAILED"* ]]; then
|
|
133
|
+
ok "step-5 on UNLOCKED: skips gracefully (NO_LOCK_HELD=1, marker printed, exit 0)"
|
|
134
|
+
else
|
|
135
|
+
no "step-5 UNLOCKED: code=$CODE out='$OUT'"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# --- LOCKED by self: refresh path runs, no skip ---
|
|
139
|
+
run_block held
|
|
140
|
+
CODE="$(cat "$TMPBASE/run-held/code")"
|
|
141
|
+
OUT="$(cat "$TMPBASE/run-held/out")"
|
|
142
|
+
if [ "$CODE" -eq 0 ] && [[ "$OUT" != *"PUBLISH_SKIPPED"* ]] \
|
|
143
|
+
&& [[ "$OUT" == *"RESULT_NO_LOCK_HELD=0"* ]] && [[ "$OUT" != *"PUBLISH_FAILED"* ]]; then
|
|
144
|
+
ok "step-5 on LOCKED (self): refresh path runs, no skip marker, exit 0"
|
|
145
|
+
else
|
|
146
|
+
no "step-5 LOCKED: code=$CODE out='$OUT'"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# --- lock-status query failure: fail closed ---
|
|
150
|
+
run_block broken
|
|
151
|
+
CODE="$(cat "$TMPBASE/run-broken/code")"
|
|
152
|
+
OUT="$(cat "$TMPBASE/run-broken/out")"
|
|
153
|
+
if [ "$CODE" -ne 0 ] && [[ "$OUT" == *"PUBLISH_FAILED=lock-status"* ]]; then
|
|
154
|
+
ok "step-5 on lock-status failure: fail-closed PUBLISH_FAILED=lock-status"
|
|
155
|
+
else
|
|
156
|
+
no "step-5 broken query: code=$CODE out='$OUT'"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# --- Structural: the gate sits inside the ALREADY_PUBLISHED=0 branch ---
|
|
160
|
+
# (a resumed run whose post-deploy already released the lock must print
|
|
161
|
+
# nothing — its PUBLISHED_ALREADY/PUBLISH_VERIFIED markers say it all).
|
|
162
|
+
AP_LINE="$(grep -n 'if \[ "\$ALREADY_PUBLISHED" = "0" \]; then' "$PUBLISH" | head -1 | cut -d: -f1)"
|
|
163
|
+
ANCHOR_LINE="$(grep -n '# STEP-5-ANCHOR' "$PUBLISH" | head -1 | cut -d: -f1)"
|
|
164
|
+
END_LINE="$(grep -n '# STEP-5-END' "$PUBLISH" | head -1 | cut -d: -f1)"
|
|
165
|
+
if [ -n "$AP_LINE" ] && [ -n "$ANCHOR_LINE" ] && [ -n "$END_LINE" ] \
|
|
166
|
+
&& [ "$AP_LINE" -lt "$ANCHOR_LINE" ] && [ "$ANCHOR_LINE" -lt "$END_LINE" ]; then
|
|
167
|
+
ok "step-5 anchor sits inside the ALREADY_PUBLISHED=0 branch"
|
|
168
|
+
else
|
|
169
|
+
no "step-5 anchor placement wrong (ap=$AP_LINE anchor=$ANCHOR_LINE end=$END_LINE)"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# --- Structural: mutation steps are gated on NO_LOCK_HELD=0 ---
|
|
173
|
+
if grep -q 'if \[ "\$NO_LOCK_HELD" = "0" \]; then' "$PUBLISH" \
|
|
174
|
+
&& grep -q 'fi # NO_LOCK_HELD=0' "$PUBLISH"; then
|
|
175
|
+
ok "mutation path is gated on NO_LOCK_HELD=0"
|
|
176
|
+
else
|
|
177
|
+
no "NO_LOCK_HELD gate missing in publish-npm.sh"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# --- Structural: PUBLISH_COMPLETE is omitted on the skip path ---
|
|
181
|
+
if grep -q 'PUBLISH_COMPLETE is deliberately not printed' "$PUBLISH"; then
|
|
182
|
+
ok "skip path omits PUBLISH_COMPLETE (no false publish claim)"
|
|
183
|
+
else
|
|
184
|
+
no "PUBLISH_COMPLETE skip-path handling missing"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
bash -n "$LIFECYCLE" && ok "worktree-lifecycle.sh syntax OK" || no "worktree-lifecycle.sh syntax"
|
|
188
|
+
bash -n "$PUBLISH" && ok "publish-npm.sh syntax OK" || no "publish-npm.sh syntax"
|
|
189
|
+
|
|
190
|
+
echo ""
|
|
191
|
+
echo "$pass passed, $fail_count failed"
|
|
192
|
+
exit $((fail_count > 0))
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
# cleanup <task_id> — remove worktree + branch (no lock)
|
|
20
20
|
# status <task_id> — report worktree state
|
|
21
21
|
# refresh-lock <task_id> — refresh merge lock timestamp (resets staleness clock)
|
|
22
|
+
# lock-status <task_id> — report merge lock state: UNLOCKED or LOCKED by <holder> since <ts> (pid <pid>)
|
|
22
23
|
# sweep [report|clean] — find/clean orphans + stale locks
|
|
23
24
|
#
|
|
24
25
|
# Exit codes:
|
|
@@ -367,6 +368,16 @@ cmd_refresh_lock() {
|
|
|
367
368
|
"$MERGE_LOCK" refresh "$task_id"
|
|
368
369
|
}
|
|
369
370
|
|
|
371
|
+
cmd_lock_status() {
|
|
372
|
+
local task_id="$1"
|
|
373
|
+
validate_task_id "$task_id"
|
|
374
|
+
# Passthrough to the lock's own status readout. Scripts never call
|
|
375
|
+
# merge-lock.sh directly — this keeps the seam. Always exits 0:
|
|
376
|
+
# UNLOCKED is explicit state (an empty-diff Integrate intentionally
|
|
377
|
+
# takes no lock), never an error.
|
|
378
|
+
"$MERGE_LOCK" status
|
|
379
|
+
}
|
|
380
|
+
|
|
370
381
|
# --- dispatch ---
|
|
371
382
|
|
|
372
383
|
cmd="${1:-}"
|
|
@@ -381,10 +392,11 @@ case "$cmd" in
|
|
|
381
392
|
cleanup) cmd_cleanup "${1:?task_id required}" ;;
|
|
382
393
|
status) cmd_status "${1:?task_id required}" ;;
|
|
383
394
|
refresh-lock) cmd_refresh_lock "${1:?task_id required}" ;;
|
|
395
|
+
lock-status) cmd_lock_status "${1:?task_id required}" ;;
|
|
384
396
|
sweep) cmd_sweep "${1:-report}" ;;
|
|
385
397
|
*)
|
|
386
398
|
echo "Usage: worktree-lifecycle.sh <command> [args]"
|
|
387
|
-
echo "Commands: validate, prepare, inspect, integrate, post-deploy, cleanup, status, refresh-lock, sweep"
|
|
399
|
+
echo "Commands: validate, prepare, inspect, integrate, post-deploy, cleanup, status, refresh-lock, lock-status, sweep"
|
|
388
400
|
exit 1
|
|
389
401
|
;;
|
|
390
402
|
esac
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -916,13 +916,17 @@ while (i < STEPS.length) {
|
|
|
916
916
|
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
917
917
|
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
918
918
|
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
919
|
-
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make
|
|
919
|
+
" → " + 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" +
|
|
920
920
|
"Run exactly this command and no other publish-related commands:\n" +
|
|
921
921
|
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
922
922
|
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
923
923
|
" bash " + PUBLISH_NPM + "\n\n" +
|
|
924
924
|
"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" +
|
|
925
925
|
"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" +
|
|
926
|
+
"If the script's output contains PUBLISH_SKIPPED=no-lock-held, the publish was skipped gracefully: Integrate reported MERGED_EMPTY (no commits ahead of main), 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" +
|
|
927
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
928
|
+
"skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
|
|
929
|
+
"VERDICT: PASS\n\n" +
|
|
926
930
|
"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" +
|
|
927
931
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
928
932
|
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
@@ -941,20 +945,34 @@ while (i < STEPS.length) {
|
|
|
941
945
|
// getprovenance-vs-HEAD verification below stays as the final gate.
|
|
942
946
|
var artifactPublish = null;
|
|
943
947
|
var publishLockRefreshed = false;
|
|
948
|
+
var publishSkippedNoLock = false;
|
|
944
949
|
try {
|
|
945
|
-
// STEP 0 (mechanical):
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
950
|
+
// STEP 0 (mechanical): read the merge-lock state explicitly — never
|
|
951
|
+
// infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
|
|
952
|
+
// intentionally takes no lock: nothing merged, nothing to ship. When
|
|
953
|
+
// no lock is held, Publish skips the publish path gracefully instead
|
|
954
|
+
// of failing closed on refresh-lock (park 2026-09-11).
|
|
955
|
+
var lockState = await agent(
|
|
956
|
+
"Run: "+ LIFECYCLE_ENV + " lock-status " + taskId + "\n" +
|
|
957
|
+
"If the output is exactly UNLOCKED, return JSON { \"state\": \"UNLOCKED\" } and run nothing else.\n" +
|
|
958
|
+
"Otherwise the lock is held: run "+ LIFECYCLE_ENV + " refresh-lock " + taskId + " (a long build must not silently lose the lock to the orphan sweep), then return JSON { \"state\": \"LOCKED\", \"refreshed\": <true if the refresh exited zero, false otherwise>, \"output\": \"<trimmed refresh stdout>\" } and nothing else.",
|
|
959
|
+
{ key: attemptKey("publish-lock-refresh-" + taskId, totalReworkCount), label: "Reading merge lock state for publish",
|
|
960
|
+
schema: { type: "object", properties: { state: { type: "string" }, refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["state"] } }
|
|
952
961
|
);
|
|
953
|
-
if (
|
|
954
|
-
|
|
962
|
+
if ((lockState.state || "").trim() === "UNLOCKED") {
|
|
963
|
+
publishSkippedNoLock = true;
|
|
964
|
+
log("Publish skipped for task " + taskId + ": no merge lock held (empty-diff Integrate) — nothing to ship");
|
|
965
|
+
} else {
|
|
966
|
+
if (!lockState.refreshed) {
|
|
967
|
+
return await parkTask("Publish lock refresh failed: " + (lockState.output || "no output") + ". Fail-closed — lock state unknown.");
|
|
968
|
+
}
|
|
969
|
+
publishLockRefreshed = true;
|
|
955
970
|
}
|
|
956
|
-
|
|
957
|
-
//
|
|
971
|
+
// STEP 1 (mechanical): trigger the rebuild with one narrow call.
|
|
972
|
+
// Skipped entirely when no lock was held — nothing merged, nothing
|
|
973
|
+
// to ship.
|
|
974
|
+
if (!publishSkippedNoLock) {
|
|
975
|
+
// (below) the rebuild trigger, bounded poll, and provenance stamp
|
|
958
976
|
// agent only makes the artifact_edit call and reports whether it was
|
|
959
977
|
// accepted — no prose claim to trust. If the artifact tool namespace
|
|
960
978
|
// is missing from this child it reports honestly and the workflow
|
|
@@ -1038,25 +1056,34 @@ while (i < STEPS.length) {
|
|
|
1038
1056
|
} else {
|
|
1039
1057
|
publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
|
|
1040
1058
|
}
|
|
1041
|
-
//
|
|
1059
|
+
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
1060
|
+
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
1042
1061
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
1043
|
-
// the merge lock — even when the publish
|
|
1044
|
-
// or failed publish can never leave the
|
|
1062
|
+
// the merge lock (forgiving when none is held) — even when the publish
|
|
1063
|
+
// itself failed, so a skipped or failed publish can never leave the
|
|
1064
|
+
// lock held (canary b5efd1b1).
|
|
1045
1065
|
var postDeploy = await agent(
|
|
1046
1066
|
"Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
|
|
1047
1067
|
"Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
|
|
1048
1068
|
{ key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
|
|
1049
1069
|
schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
|
|
1050
1070
|
);
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1071
|
+
if (publishSkippedNoLock) {
|
|
1072
|
+
if (!postDeploy.deployed) {
|
|
1073
|
+
return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
1074
|
+
}
|
|
1075
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy finalized cleanup");
|
|
1076
|
+
} else {
|
|
1077
|
+
if (publishFailure) {
|
|
1078
|
+
return await parkTask(publishFailure + (postDeploy.deployed
|
|
1079
|
+
? " Post-deploy finalized cleanup."
|
|
1080
|
+
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
1081
|
+
}
|
|
1082
|
+
if (!postDeploy.deployed) {
|
|
1083
|
+
return await parkTask("Post-deploy failed after a stamped publish: " + (postDeploy.output || "no output") + ". The publish landed but worktree cleanup and lock release are unknown — human attention needed.");
|
|
1084
|
+
}
|
|
1085
|
+
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
1058
1086
|
}
|
|
1059
|
-
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
1060
1087
|
} catch (pubErr) {
|
|
1061
1088
|
// Best-effort cleanup: if the lock was refreshed, try to release it
|
|
1062
1089
|
// before parking so the failure cannot wedge the next run.
|
|
@@ -1077,6 +1104,12 @@ while (i < STEPS.length) {
|
|
|
1077
1104
|
// The work agent no longer performs the publish — it reports on the
|
|
1078
1105
|
// mechanical outcome above. It must not rebuild or re-stamp: a second
|
|
1079
1106
|
// artifact_edit would trigger a duplicate build.
|
|
1107
|
+
if (publishSkippedNoLock) {
|
|
1108
|
+
instructions = "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" +
|
|
1109
|
+
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship. The workflow finalized cleanup via post-deploy.\n\n" +
|
|
1110
|
+
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
1111
|
+
"The VERDICT line must be the last line of your report.";
|
|
1112
|
+
} else {
|
|
1080
1113
|
instructions = "Publish the merged code to the live artifact.\n\n" +
|
|
1081
1114
|
"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" +
|
|
1082
1115
|
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
@@ -1089,6 +1122,7 @@ while (i < STEPS.length) {
|
|
|
1089
1122
|
"The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
|
|
1090
1123
|
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
1091
1124
|
"The VERDICT line must be the last line of your report.";
|
|
1125
|
+
}
|
|
1092
1126
|
} else if (PUBLISH_TYPE === "vercel") {
|
|
1093
1127
|
// vercel publish is not yet implemented — block without inventing behavior
|
|
1094
1128
|
instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
|
|
@@ -1100,7 +1134,9 @@ while (i < STEPS.length) {
|
|
|
1100
1134
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
1101
1135
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
1102
1136
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
1103
|
-
? "NPM PUBLISH CHECK: the accepted Build report 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" +
|
|
1137
|
+
? "NPM PUBLISH CHECK: the accepted Build report declared release: yes, so this run's Publish phase must have published — UNLESS it was skipped deterministically on an empty-diff Integrate. 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" +
|
|
1138
|
+
"CHECK THE SKIP PATH FIRST: if the notes contain a line matching skipped: no-lock-held, Publish was skipped deterministically — Integrate reported MERGED_EMPTY (no commits ahead of main), so no merge lock was taken and there was nothing to ship. Verify the notes contain that skip-marker line and do NOT contain a line matching published: muse-crew@. Do NOT run npm view and do NOT demand registry movement — nothing was supposed to ship. Report 'npm publish check: Publish skipped deterministically (empty-diff Integrate — nothing to ship)' and PASS this check.\n" +
|
|
1139
|
+
"Only when the notes contain no skip marker must the publish have landed — run the full verification below.\n" +
|
|
1104
1140
|
"Extract the line matching TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version> (the workflow appends it to the Publish notes, so it is always present). If the line is missing, report 'npm publish verification failed: Publish notes did not carry the computed target version', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
1105
1141
|
"Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if it differs, report the mismatch, then end your report with exactly this line: VERDICT: 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, report it, then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
1106
1142
|
"Extract the published version from the notes line matching published: muse-crew@<version>. 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, report 'npm publish verification failed: [details]', then end your report with exactly this line: VERDICT: FAIL.\n"
|
|
@@ -1295,8 +1331,21 @@ while (i < STEPS.length) {
|
|
|
1295
1331
|
const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
|
|
1296
1332
|
|
|
1297
1333
|
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
1334
|
+
// Skip-aware (park 2026-09-11): when the deterministic publish script found
|
|
1335
|
+
// no merge lock held (empty-diff Integrate), it skips the publish path
|
|
1336
|
+
// gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
|
|
1337
|
+
// marker. Verification is then vacuous — nothing was shipped, and the
|
|
1338
|
+
// registry must NOT have moved. The marker is script-emitted explicit state
|
|
1339
|
+
// (pasted verbatim per the Publish agent instructions), not agent prose; a
|
|
1340
|
+
// report without the marker still runs the full verification fail-closed.
|
|
1298
1341
|
var publishVerified = false;
|
|
1342
|
+
var npmPublishSkipped = false;
|
|
1299
1343
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
1344
|
+
if (/^PUBLISH_SKIPPED=no-lock-held$/m.test(workerText || "")) {
|
|
1345
|
+
npmPublishSkipped = true;
|
|
1346
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
|
|
1347
|
+
}
|
|
1348
|
+
if (!npmPublishSkipped) {
|
|
1300
1349
|
try {
|
|
1301
1350
|
var verifyResult = await agent(
|
|
1302
1351
|
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1313,6 +1362,7 @@ while (i < STEPS.length) {
|
|
|
1313
1362
|
} catch (e) {
|
|
1314
1363
|
return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1315
1364
|
}
|
|
1365
|
+
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
1316
1366
|
}
|
|
1317
1367
|
|
|
1318
1368
|
// Artifact publish verification: the worker cannot self-certify a deploy.
|
|
@@ -1320,6 +1370,14 @@ while (i < STEPS.length) {
|
|
|
1320
1370
|
// integrated commit. A stale or missing provenance means the publish did not
|
|
1321
1371
|
// land — fail closed, do not trust the worker's prose.
|
|
1322
1372
|
if (step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG) {
|
|
1373
|
+
// Skip-aware (park 2026-09-11): an empty-diff Integrate takes no merge
|
|
1374
|
+
// lock, and the deterministic publish path skips rebuild/stamp entirely —
|
|
1375
|
+
// there is no new provenance to compare against HEAD, so verification is
|
|
1376
|
+
// vacuous. publishSkippedNoLock is workflow-computed state from the
|
|
1377
|
+
// explicit lock-status read in STEP 0, not agent prose.
|
|
1378
|
+
if (publishSkippedNoLock) {
|
|
1379
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): artifact verification vacuous, nothing was shipped");
|
|
1380
|
+
} else {
|
|
1323
1381
|
try {
|
|
1324
1382
|
var headResult = await agent(
|
|
1325
1383
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1343,6 +1401,7 @@ while (i < STEPS.length) {
|
|
|
1343
1401
|
} catch (e) {
|
|
1344
1402
|
return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1345
1403
|
}
|
|
1404
|
+
} // end: !publishSkippedNoLock — a skipped publish has nothing to verify
|
|
1346
1405
|
}
|
|
1347
1406
|
|
|
1348
1407
|
// Session notes. Machine-readable marker lines are extracted from the full
|
|
@@ -1351,9 +1410,13 @@ while (i < STEPS.length) {
|
|
|
1351
1410
|
// reading TARGET_VERSION=) depend on them.
|
|
1352
1411
|
let summary;
|
|
1353
1412
|
var workerMarkers = extractMarkerLines(workerText);
|
|
1354
|
-
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && publishVerified) {
|
|
1413
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
|
1414
|
+
// The skip path records the computed target and the explicit skip — never
|
|
1415
|
+
// "published:", which would be a false claim (nothing was shipped).
|
|
1355
1416
|
const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1356
|
-
|
|
1417
|
+
(npmPublishSkipped
|
|
1418
|
+
? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
|
|
1419
|
+
: "published: muse-crew@" + publishTarget.target);
|
|
1357
1420
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
|
|
1358
1421
|
} else {
|
|
1359
1422
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
|
package/workflows/chore.js
CHANGED
|
@@ -853,13 +853,17 @@ while (i < STEPS.length) {
|
|
|
853
853
|
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
854
854
|
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
855
855
|
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
856
|
-
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make
|
|
856
|
+
" → " + 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" +
|
|
857
857
|
"Run exactly this command and no other publish-related commands:\n" +
|
|
858
858
|
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
859
859
|
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
860
860
|
" bash " + PUBLISH_NPM + "\n\n" +
|
|
861
861
|
"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" +
|
|
862
862
|
"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" +
|
|
863
|
+
"If the script's output contains PUBLISH_SKIPPED=no-lock-held, the publish was skipped gracefully: Integrate reported MERGED_EMPTY (no commits ahead of main), 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" +
|
|
864
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
865
|
+
"skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
|
|
866
|
+
"VERDICT: PASS\n\n" +
|
|
863
867
|
"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" +
|
|
864
868
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
865
869
|
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
@@ -878,20 +882,34 @@ while (i < STEPS.length) {
|
|
|
878
882
|
// getprovenance-vs-HEAD verification below stays as the final gate.
|
|
879
883
|
var artifactPublish = null;
|
|
880
884
|
var publishLockRefreshed = false;
|
|
885
|
+
var publishSkippedNoLock = false;
|
|
881
886
|
try {
|
|
882
|
-
// STEP 0 (mechanical):
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
887
|
+
// STEP 0 (mechanical): read the merge-lock state explicitly — never
|
|
888
|
+
// infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
|
|
889
|
+
// intentionally takes no lock: nothing merged, nothing to ship. When
|
|
890
|
+
// no lock is held, Publish skips the publish path gracefully instead
|
|
891
|
+
// of failing closed on refresh-lock (park 2026-09-11).
|
|
892
|
+
var lockState = await agent(
|
|
893
|
+
"Run: "+ LIFECYCLE_ENV + " lock-status " + taskId + "\n" +
|
|
894
|
+
"If the output is exactly UNLOCKED, return JSON { \"state\": \"UNLOCKED\" } and run nothing else.\n" +
|
|
895
|
+
"Otherwise the lock is held: run "+ LIFECYCLE_ENV + " refresh-lock " + taskId + " (a long build must not silently lose the lock to the orphan sweep), then return JSON { \"state\": \"LOCKED\", \"refreshed\": <true if the refresh exited zero, false otherwise>, \"output\": \"<trimmed refresh stdout>\" } and nothing else.",
|
|
896
|
+
{ key: attemptKey("publish-lock-refresh-" + taskId, reworkCount), label: "Reading merge lock state for publish",
|
|
897
|
+
schema: { type: "object", properties: { state: { type: "string" }, refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["state"] } }
|
|
889
898
|
);
|
|
890
|
-
if (
|
|
891
|
-
|
|
899
|
+
if ((lockState.state || "").trim() === "UNLOCKED") {
|
|
900
|
+
publishSkippedNoLock = true;
|
|
901
|
+
log("Publish skipped for task " + taskId + ": no merge lock held (empty-diff Integrate) — nothing to ship");
|
|
902
|
+
} else {
|
|
903
|
+
if (!lockState.refreshed) {
|
|
904
|
+
return await parkTask("Publish lock refresh failed: " + (lockState.output || "no output") + ". Fail-closed — lock state unknown.");
|
|
905
|
+
}
|
|
906
|
+
publishLockRefreshed = true;
|
|
892
907
|
}
|
|
893
|
-
|
|
894
|
-
//
|
|
908
|
+
// STEP 1 (mechanical): trigger the rebuild with one narrow call.
|
|
909
|
+
// Skipped entirely when no lock was held — nothing merged, nothing
|
|
910
|
+
// to ship.
|
|
911
|
+
if (!publishSkippedNoLock) {
|
|
912
|
+
// (below) the rebuild trigger, bounded poll, and provenance stamp
|
|
895
913
|
// agent only makes the artifact_edit call and reports whether it was
|
|
896
914
|
// accepted — no prose claim to trust. If the artifact tool namespace
|
|
897
915
|
// is missing from this child it reports honestly and the workflow
|
|
@@ -975,25 +993,34 @@ while (i < STEPS.length) {
|
|
|
975
993
|
} else {
|
|
976
994
|
publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
|
|
977
995
|
}
|
|
978
|
-
//
|
|
996
|
+
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
997
|
+
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
979
998
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
980
|
-
// the merge lock — even when the publish
|
|
981
|
-
// or failed publish can never leave the
|
|
999
|
+
// the merge lock (forgiving when none is held) — even when the publish
|
|
1000
|
+
// itself failed, so a skipped or failed publish can never leave the
|
|
1001
|
+
// lock held (canary b5efd1b1).
|
|
982
1002
|
var postDeploy = await agent(
|
|
983
1003
|
"Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
|
|
984
1004
|
"Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
|
|
985
1005
|
{ key: attemptKey("publish-postdeploy-" + taskId, reworkCount), label: "Finalizing publish (post-deploy)",
|
|
986
1006
|
schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
|
|
987
1007
|
);
|
|
988
|
-
if (
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1008
|
+
if (publishSkippedNoLock) {
|
|
1009
|
+
if (!postDeploy.deployed) {
|
|
1010
|
+
return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
1011
|
+
}
|
|
1012
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy finalized cleanup");
|
|
1013
|
+
} else {
|
|
1014
|
+
if (publishFailure) {
|
|
1015
|
+
return await parkTask(publishFailure + (postDeploy.deployed
|
|
1016
|
+
? " Post-deploy finalized cleanup."
|
|
1017
|
+
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
1018
|
+
}
|
|
1019
|
+
if (!postDeploy.deployed) {
|
|
1020
|
+
return await parkTask("Post-deploy failed after a stamped publish: " + (postDeploy.output || "no output") + ". The publish landed but worktree cleanup and lock release are unknown — human attention needed.");
|
|
1021
|
+
}
|
|
1022
|
+
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
995
1023
|
}
|
|
996
|
-
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
997
1024
|
} catch (pubErr) {
|
|
998
1025
|
// Best-effort cleanup: if the lock was refreshed, try to release it
|
|
999
1026
|
// before parking so the failure cannot wedge the next run.
|
|
@@ -1014,6 +1041,12 @@ while (i < STEPS.length) {
|
|
|
1014
1041
|
// The work agent no longer performs the publish — it reports on the
|
|
1015
1042
|
// mechanical outcome above. It must not rebuild or re-stamp: a second
|
|
1016
1043
|
// artifact_edit would trigger a duplicate build.
|
|
1044
|
+
if (publishSkippedNoLock) {
|
|
1045
|
+
instructions = "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" +
|
|
1046
|
+
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship. The workflow finalized cleanup via post-deploy.\n\n" +
|
|
1047
|
+
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
1048
|
+
"The VERDICT line must be the last line of your report.";
|
|
1049
|
+
} else {
|
|
1017
1050
|
instructions = "Publish the merged code to the live artifact.\n\n" +
|
|
1018
1051
|
"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" +
|
|
1019
1052
|
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
@@ -1026,6 +1059,7 @@ while (i < STEPS.length) {
|
|
|
1026
1059
|
"The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
|
|
1027
1060
|
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
1028
1061
|
"The VERDICT line must be the last line of your report.";
|
|
1062
|
+
}
|
|
1029
1063
|
} else if (PUBLISH_TYPE === "vercel") {
|
|
1030
1064
|
// vercel publish is not yet implemented — block without inventing behavior
|
|
1031
1065
|
instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
|
|
@@ -1172,8 +1206,21 @@ while (i < STEPS.length) {
|
|
|
1172
1206
|
const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
|
|
1173
1207
|
|
|
1174
1208
|
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
1209
|
+
// Skip-aware (park 2026-09-11): when the deterministic publish script found
|
|
1210
|
+
// no merge lock held (empty-diff Integrate), it skips the publish path
|
|
1211
|
+
// gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
|
|
1212
|
+
// marker. Verification is then vacuous — nothing was shipped, and the
|
|
1213
|
+
// registry must NOT have moved. The marker is script-emitted explicit state
|
|
1214
|
+
// (pasted verbatim per the Publish agent instructions), not agent prose; a
|
|
1215
|
+
// report without the marker still runs the full verification fail-closed.
|
|
1175
1216
|
var publishVerified = false;
|
|
1217
|
+
var npmPublishSkipped = false;
|
|
1176
1218
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
1219
|
+
if (/^PUBLISH_SKIPPED=no-lock-held$/m.test(workerText || "")) {
|
|
1220
|
+
npmPublishSkipped = true;
|
|
1221
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
|
|
1222
|
+
}
|
|
1223
|
+
if (!npmPublishSkipped) {
|
|
1177
1224
|
try {
|
|
1178
1225
|
var verifyResult = await agent(
|
|
1179
1226
|
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1190,6 +1237,7 @@ while (i < STEPS.length) {
|
|
|
1190
1237
|
} catch (e) {
|
|
1191
1238
|
return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1192
1239
|
}
|
|
1240
|
+
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
1193
1241
|
}
|
|
1194
1242
|
|
|
1195
1243
|
// Artifact publish verification: the worker cannot self-certify a deploy.
|
|
@@ -1197,6 +1245,14 @@ while (i < STEPS.length) {
|
|
|
1197
1245
|
// integrated commit. A stale or missing provenance means the publish did not
|
|
1198
1246
|
// land — fail closed, do not trust the worker's prose.
|
|
1199
1247
|
if (step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG) {
|
|
1248
|
+
// Skip-aware (park 2026-09-11): an empty-diff Integrate takes no merge
|
|
1249
|
+
// lock, and the deterministic publish path skips rebuild/stamp entirely —
|
|
1250
|
+
// there is no new provenance to compare against HEAD, so verification is
|
|
1251
|
+
// vacuous. publishSkippedNoLock is workflow-computed state from the
|
|
1252
|
+
// explicit lock-status read in STEP 0, not agent prose.
|
|
1253
|
+
if (publishSkippedNoLock) {
|
|
1254
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): artifact verification vacuous, nothing was shipped");
|
|
1255
|
+
} else {
|
|
1200
1256
|
try {
|
|
1201
1257
|
var headResult = await agent(
|
|
1202
1258
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1220,6 +1276,7 @@ while (i < STEPS.length) {
|
|
|
1220
1276
|
} catch (e) {
|
|
1221
1277
|
return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1222
1278
|
}
|
|
1279
|
+
} // end: !publishSkippedNoLock — a skipped publish has nothing to verify
|
|
1223
1280
|
}
|
|
1224
1281
|
|
|
1225
1282
|
// Session notes. Machine-readable marker lines are extracted from the full
|
|
@@ -1228,9 +1285,13 @@ while (i < STEPS.length) {
|
|
|
1228
1285
|
// reading TARGET_VERSION=) depend on them.
|
|
1229
1286
|
let summary;
|
|
1230
1287
|
var workerMarkers = extractMarkerLines(workerText);
|
|
1231
|
-
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && publishVerified) {
|
|
1288
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
|
1289
|
+
// The skip path records the computed target and the explicit skip — never
|
|
1290
|
+
// "published:", which would be a false claim (nothing was shipped).
|
|
1232
1291
|
const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1233
|
-
|
|
1292
|
+
(npmPublishSkipped
|
|
1293
|
+
? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
|
|
1294
|
+
: "published: muse-crew@" + publishTarget.target);
|
|
1234
1295
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
|
|
1235
1296
|
} else {
|
|
1236
1297
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
|
package/workflows/standard.js
CHANGED
|
@@ -906,13 +906,17 @@ while (i < STEPS.length) {
|
|
|
906
906
|
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
907
907
|
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
908
908
|
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
909
|
-
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make
|
|
909
|
+
" → " + 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" +
|
|
910
910
|
"Run exactly this command and no other publish-related commands:\n" +
|
|
911
911
|
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
912
912
|
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
913
913
|
" bash " + PUBLISH_NPM + "\n\n" +
|
|
914
914
|
"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" +
|
|
915
915
|
"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" +
|
|
916
|
+
"If the script's output contains PUBLISH_SKIPPED=no-lock-held, the publish was skipped gracefully: Integrate reported MERGED_EMPTY (no commits ahead of main), 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" +
|
|
917
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
918
|
+
"skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
|
|
919
|
+
"VERDICT: PASS\n\n" +
|
|
916
920
|
"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" +
|
|
917
921
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
918
922
|
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
@@ -931,20 +935,34 @@ while (i < STEPS.length) {
|
|
|
931
935
|
// getprovenance-vs-HEAD verification below stays as the final gate.
|
|
932
936
|
var artifactPublish = null;
|
|
933
937
|
var publishLockRefreshed = false;
|
|
938
|
+
var publishSkippedNoLock = false;
|
|
934
939
|
try {
|
|
935
|
-
// STEP 0 (mechanical):
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
940
|
+
// STEP 0 (mechanical): read the merge-lock state explicitly — never
|
|
941
|
+
// infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
|
|
942
|
+
// intentionally takes no lock: nothing merged, nothing to ship. When
|
|
943
|
+
// no lock is held, Publish skips the publish path gracefully instead
|
|
944
|
+
// of failing closed on refresh-lock (park 2026-09-11).
|
|
945
|
+
var lockState = await agent(
|
|
946
|
+
"Run: "+ LIFECYCLE_ENV + " lock-status " + taskId + "\n" +
|
|
947
|
+
"If the output is exactly UNLOCKED, return JSON { \"state\": \"UNLOCKED\" } and run nothing else.\n" +
|
|
948
|
+
"Otherwise the lock is held: run "+ LIFECYCLE_ENV + " refresh-lock " + taskId + " (a long build must not silently lose the lock to the orphan sweep), then return JSON { \"state\": \"LOCKED\", \"refreshed\": <true if the refresh exited zero, false otherwise>, \"output\": \"<trimmed refresh stdout>\" } and nothing else.",
|
|
949
|
+
{ key: attemptKey("publish-lock-refresh-" + taskId, totalReworkCount), label: "Reading merge lock state for publish",
|
|
950
|
+
schema: { type: "object", properties: { state: { type: "string" }, refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["state"] } }
|
|
942
951
|
);
|
|
943
|
-
if (
|
|
944
|
-
|
|
952
|
+
if ((lockState.state || "").trim() === "UNLOCKED") {
|
|
953
|
+
publishSkippedNoLock = true;
|
|
954
|
+
log("Publish skipped for task " + taskId + ": no merge lock held (empty-diff Integrate) — nothing to ship");
|
|
955
|
+
} else {
|
|
956
|
+
if (!lockState.refreshed) {
|
|
957
|
+
return await parkTask("Publish lock refresh failed: " + (lockState.output || "no output") + ". Fail-closed — lock state unknown.");
|
|
958
|
+
}
|
|
959
|
+
publishLockRefreshed = true;
|
|
945
960
|
}
|
|
946
|
-
|
|
947
|
-
//
|
|
961
|
+
// STEP 1 (mechanical): trigger the rebuild with one narrow call.
|
|
962
|
+
// Skipped entirely when no lock was held — nothing merged, nothing
|
|
963
|
+
// to ship.
|
|
964
|
+
if (!publishSkippedNoLock) {
|
|
965
|
+
// (below) the rebuild trigger, bounded poll, and provenance stamp
|
|
948
966
|
// agent only makes the artifact_edit call and reports whether it was
|
|
949
967
|
// accepted — no prose claim to trust. If the artifact tool namespace
|
|
950
968
|
// is missing from this child it reports honestly and the workflow
|
|
@@ -1028,25 +1046,34 @@ while (i < STEPS.length) {
|
|
|
1028
1046
|
} else {
|
|
1029
1047
|
publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
|
|
1030
1048
|
}
|
|
1031
|
-
//
|
|
1049
|
+
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
1050
|
+
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
1032
1051
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
1033
|
-
// the merge lock — even when the publish
|
|
1034
|
-
// or failed publish can never leave the
|
|
1052
|
+
// the merge lock (forgiving when none is held) — even when the publish
|
|
1053
|
+
// itself failed, so a skipped or failed publish can never leave the
|
|
1054
|
+
// lock held (canary b5efd1b1).
|
|
1035
1055
|
var postDeploy = await agent(
|
|
1036
1056
|
"Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
|
|
1037
1057
|
"Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
|
|
1038
1058
|
{ key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
|
|
1039
1059
|
schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
|
|
1040
1060
|
);
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1061
|
+
if (publishSkippedNoLock) {
|
|
1062
|
+
if (!postDeploy.deployed) {
|
|
1063
|
+
return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
1064
|
+
}
|
|
1065
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy finalized cleanup");
|
|
1066
|
+
} else {
|
|
1067
|
+
if (publishFailure) {
|
|
1068
|
+
return await parkTask(publishFailure + (postDeploy.deployed
|
|
1069
|
+
? " Post-deploy finalized cleanup."
|
|
1070
|
+
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
1071
|
+
}
|
|
1072
|
+
if (!postDeploy.deployed) {
|
|
1073
|
+
return await parkTask("Post-deploy failed after a stamped publish: " + (postDeploy.output || "no output") + ". The publish landed but worktree cleanup and lock release are unknown — human attention needed.");
|
|
1074
|
+
}
|
|
1075
|
+
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
1048
1076
|
}
|
|
1049
|
-
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
1050
1077
|
} catch (pubErr) {
|
|
1051
1078
|
// Best-effort cleanup: if the lock was refreshed, try to release it
|
|
1052
1079
|
// before parking so the failure cannot wedge the next run.
|
|
@@ -1067,6 +1094,12 @@ while (i < STEPS.length) {
|
|
|
1067
1094
|
// The work agent no longer performs the publish — it reports on the
|
|
1068
1095
|
// mechanical outcome above. It must not rebuild or re-stamp: a second
|
|
1069
1096
|
// artifact_edit would trigger a duplicate build.
|
|
1097
|
+
if (publishSkippedNoLock) {
|
|
1098
|
+
instructions = "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" +
|
|
1099
|
+
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship. The workflow finalized cleanup via post-deploy.\n\n" +
|
|
1100
|
+
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
1101
|
+
"The VERDICT line must be the last line of your report.";
|
|
1102
|
+
} else {
|
|
1070
1103
|
instructions = "Publish the merged code to the live artifact.\n\n" +
|
|
1071
1104
|
"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" +
|
|
1072
1105
|
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
@@ -1079,6 +1112,7 @@ while (i < STEPS.length) {
|
|
|
1079
1112
|
"The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
|
|
1080
1113
|
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
1081
1114
|
"The VERDICT line must be the last line of your report.";
|
|
1115
|
+
}
|
|
1082
1116
|
} else if (PUBLISH_TYPE === "vercel") {
|
|
1083
1117
|
// vercel publish is not yet implemented — block without inventing behavior
|
|
1084
1118
|
instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
|
|
@@ -1090,7 +1124,9 @@ while (i < STEPS.length) {
|
|
|
1090
1124
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
1091
1125
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
1092
1126
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
1093
|
-
? "NPM PUBLISH CHECK: the accepted Build report 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" +
|
|
1127
|
+
? "NPM PUBLISH CHECK: the accepted Build report declared release: yes, so this run's Publish phase must have published — UNLESS it was skipped deterministically on an empty-diff Integrate. 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" +
|
|
1128
|
+
"CHECK THE SKIP PATH FIRST: if the notes contain a line matching skipped: no-lock-held, Publish was skipped deterministically — Integrate reported MERGED_EMPTY (no commits ahead of main), so no merge lock was taken and there was nothing to ship. Verify the notes contain that skip-marker line and do NOT contain a line matching published: muse-crew@. Do NOT run npm view and do NOT demand registry movement — nothing was supposed to ship. Report 'npm publish check: Publish skipped deterministically (empty-diff Integrate — nothing to ship)' and PASS this check.\n" +
|
|
1129
|
+
"Only when the notes contain no skip marker must the publish have landed — run the full verification below.\n" +
|
|
1094
1130
|
"Extract the line matching TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version> (the workflow appends it to the Publish notes, so it is always present). If the line is missing, report 'npm publish verification failed: Publish notes did not carry the computed target version', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
1095
1131
|
"Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if it differs, report the mismatch, then end your report with exactly this line: VERDICT: 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, report it, then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
1096
1132
|
"Extract the published version from the notes line matching published: muse-crew@<version>. 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, report 'npm publish verification failed: [details]', then end your report with exactly this line: VERDICT: FAIL.\n"
|
|
@@ -1305,8 +1341,21 @@ while (i < STEPS.length) {
|
|
|
1305
1341
|
const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
|
|
1306
1342
|
|
|
1307
1343
|
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
1344
|
+
// Skip-aware (park 2026-09-11): when the deterministic publish script found
|
|
1345
|
+
// no merge lock held (empty-diff Integrate), it skips the publish path
|
|
1346
|
+
// gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
|
|
1347
|
+
// marker. Verification is then vacuous — nothing was shipped, and the
|
|
1348
|
+
// registry must NOT have moved. The marker is script-emitted explicit state
|
|
1349
|
+
// (pasted verbatim per the Publish agent instructions), not agent prose; a
|
|
1350
|
+
// report without the marker still runs the full verification fail-closed.
|
|
1308
1351
|
var publishVerified = false;
|
|
1352
|
+
var npmPublishSkipped = false;
|
|
1309
1353
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
1354
|
+
if (/^PUBLISH_SKIPPED=no-lock-held$/m.test(workerText || "")) {
|
|
1355
|
+
npmPublishSkipped = true;
|
|
1356
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
|
|
1357
|
+
}
|
|
1358
|
+
if (!npmPublishSkipped) {
|
|
1310
1359
|
try {
|
|
1311
1360
|
var verifyResult = await agent(
|
|
1312
1361
|
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1323,6 +1372,7 @@ while (i < STEPS.length) {
|
|
|
1323
1372
|
} catch (e) {
|
|
1324
1373
|
return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1325
1374
|
}
|
|
1375
|
+
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
1326
1376
|
}
|
|
1327
1377
|
|
|
1328
1378
|
// Artifact publish verification: the worker cannot self-certify a deploy.
|
|
@@ -1330,6 +1380,14 @@ while (i < STEPS.length) {
|
|
|
1330
1380
|
// integrated commit. A stale or missing provenance means the publish did not
|
|
1331
1381
|
// land — fail closed, do not trust the worker's prose.
|
|
1332
1382
|
if (step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG) {
|
|
1383
|
+
// Skip-aware (park 2026-09-11): an empty-diff Integrate takes no merge
|
|
1384
|
+
// lock, and the deterministic publish path skips rebuild/stamp entirely —
|
|
1385
|
+
// there is no new provenance to compare against HEAD, so verification is
|
|
1386
|
+
// vacuous. publishSkippedNoLock is workflow-computed state from the
|
|
1387
|
+
// explicit lock-status read in STEP 0, not agent prose.
|
|
1388
|
+
if (publishSkippedNoLock) {
|
|
1389
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): artifact verification vacuous, nothing was shipped");
|
|
1390
|
+
} else {
|
|
1333
1391
|
try {
|
|
1334
1392
|
var headResult = await agent(
|
|
1335
1393
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1353,6 +1411,7 @@ while (i < STEPS.length) {
|
|
|
1353
1411
|
} catch (e) {
|
|
1354
1412
|
return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1355
1413
|
}
|
|
1414
|
+
} // end: !publishSkippedNoLock — a skipped publish has nothing to verify
|
|
1356
1415
|
}
|
|
1357
1416
|
|
|
1358
1417
|
// Session notes. Machine-readable marker lines are extracted from the full
|
|
@@ -1361,9 +1420,13 @@ while (i < STEPS.length) {
|
|
|
1361
1420
|
// reading TARGET_VERSION=) depend on them.
|
|
1362
1421
|
let summary;
|
|
1363
1422
|
var workerMarkers = extractMarkerLines(workerText);
|
|
1364
|
-
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && publishVerified) {
|
|
1423
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
|
1424
|
+
// The skip path records the computed target and the explicit skip — never
|
|
1425
|
+
// "published:", which would be a false claim (nothing was shipped).
|
|
1365
1426
|
const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1366
|
-
|
|
1427
|
+
(npmPublishSkipped
|
|
1428
|
+
? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
|
|
1429
|
+
: "published: muse-crew@" + publishTarget.target);
|
|
1367
1430
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
|
|
1368
1431
|
} else {
|
|
1369
1432
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
|