muse-crew 0.4.5 → 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 +5 -4
- package/lib/merge-lock.sh +149 -68
- package/lib/orphan-sweep.sh +38 -34
- package/lib/publish-npm.sh +141 -105
- package/lib/test-merge-lock.sh +77 -34
- package/lib/test-orphan-sweep.sh +19 -5
- package/lib/test-publish-skip.sh +192 -0
- package/lib/worktree-lifecycle.sh +46 -8
- package/package.json +1 -1
- package/workflows/bugfix.js +144 -36
- package/workflows/chore.js +141 -35
- package/workflows/standard.js +144 -36
|
@@ -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:
|
|
@@ -202,13 +203,39 @@ cmd_integrate() {
|
|
|
202
203
|
return 0
|
|
203
204
|
fi
|
|
204
205
|
|
|
205
|
-
# Acquire merge lock
|
|
206
|
+
# Acquire the merge lock (serializes version assignment across concurrent
|
|
207
|
+
# runs). The holder is the opaque task+run identity from WORKFLOW_RUN_ID —
|
|
208
|
+
# never a PID (bug 2fc8f52f: short-lived agent PIDs made every concurrent
|
|
209
|
+
# acquire take the stale path). Another task mid Integrate/Publish is
|
|
210
|
+
# normal under simultaneity>1: bounded backoff (30s sleeps, 10min cap,
|
|
211
|
+
# overridable via MERGE_LOCK_RETRY_SECS / MERGE_LOCK_RETRY_CAP_SECS), then
|
|
212
|
+
# give up with LOCK_HELD so the workflow records the failure with a reason
|
|
213
|
+
# (dispatcher retries under its cap, parks after the cap).
|
|
214
|
+
local holder="${WORKFLOW_RUN_ID:-$task_id}"
|
|
215
|
+
local retry_secs="${MERGE_LOCK_RETRY_SECS:-30}"
|
|
216
|
+
local retry_cap="${MERGE_LOCK_RETRY_CAP_SECS:-600}"
|
|
217
|
+
local waited=0
|
|
206
218
|
local lock_result
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
while true; do
|
|
220
|
+
if lock_result=$("$MERGE_LOCK" acquire "$task_id" "$holder"); then
|
|
221
|
+
echo "$lock_result"
|
|
222
|
+
break
|
|
223
|
+
fi
|
|
224
|
+
case "$lock_result" in
|
|
225
|
+
HELD*)
|
|
226
|
+
if [ "$waited" -ge "$retry_cap" ]; then
|
|
227
|
+
echo "LOCK_HELD: $lock_result (waited ${waited}s of ${retry_cap}s backoff)"
|
|
228
|
+
exit 2
|
|
229
|
+
fi
|
|
230
|
+
sleep "$retry_secs"
|
|
231
|
+
waited=$((waited + retry_secs))
|
|
232
|
+
;;
|
|
233
|
+
*)
|
|
234
|
+
echo "LOCK_ERROR: $lock_result"
|
|
235
|
+
exit 2
|
|
236
|
+
;;
|
|
237
|
+
esac
|
|
238
|
+
done
|
|
212
239
|
|
|
213
240
|
# Merge
|
|
214
241
|
if ! git merge --no-ff "$branch" -m "$commit_msg" 2>&1; then
|
|
@@ -338,7 +365,17 @@ cmd_sweep() {
|
|
|
338
365
|
cmd_refresh_lock() {
|
|
339
366
|
local task_id="$1"
|
|
340
367
|
validate_task_id "$task_id"
|
|
341
|
-
"$MERGE_LOCK" refresh "$task_id"
|
|
368
|
+
"$MERGE_LOCK" refresh "$task_id"
|
|
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
|
|
342
379
|
}
|
|
343
380
|
|
|
344
381
|
# --- dispatch ---
|
|
@@ -355,10 +392,11 @@ case "$cmd" in
|
|
|
355
392
|
cleanup) cmd_cleanup "${1:?task_id required}" ;;
|
|
356
393
|
status) cmd_status "${1:?task_id required}" ;;
|
|
357
394
|
refresh-lock) cmd_refresh_lock "${1:?task_id required}" ;;
|
|
395
|
+
lock-status) cmd_lock_status "${1:?task_id required}" ;;
|
|
358
396
|
sweep) cmd_sweep "${1:-report}" ;;
|
|
359
397
|
*)
|
|
360
398
|
echo "Usage: worktree-lifecycle.sh <command> [args]"
|
|
361
|
-
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"
|
|
362
400
|
exit 1
|
|
363
401
|
;;
|
|
364
402
|
esac
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -510,6 +510,12 @@ if (missingInitialPins.length > 0) {
|
|
|
510
510
|
}
|
|
511
511
|
log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
512
512
|
|
|
513
|
+
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
514
|
+
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
515
|
+
// made every concurrent acquire take the stale path). Set exactly once on
|
|
516
|
+
// the first claim; stable for the rest of the run's lifetime.
|
|
517
|
+
let lockHolder = taskId;
|
|
518
|
+
|
|
513
519
|
while (i < STEPS.length) {
|
|
514
520
|
const step = STEPS[i];
|
|
515
521
|
const isFirstClaim = (i === startStepIndex && totalReworkCount === 0);
|
|
@@ -693,6 +699,13 @@ while (i < STEPS.length) {
|
|
|
693
699
|
activeSessionId = claimResult.session_id;
|
|
694
700
|
}
|
|
695
701
|
|
|
702
|
+
// Mint the merge-lock holder identity once: task+session of this run's
|
|
703
|
+
// first claim. The task ID alone cannot distinguish two runs of the same
|
|
704
|
+
// task (re-dispatch after park), and a PID is not a valid holder at all.
|
|
705
|
+
if (lockHolder === taskId && activeSessionId) {
|
|
706
|
+
lockHolder = taskId + "/" + activeSessionId;
|
|
707
|
+
}
|
|
708
|
+
|
|
696
709
|
// ── Capture: baseline evidence for experiential tasks ─────────────
|
|
697
710
|
// Hazel's QA capture pass runs right after Triage, before Map, for tasks
|
|
698
711
|
// Sage flagged experiential. The capture itself is parent-driven (the
|
|
@@ -870,11 +883,11 @@ while (i < STEPS.length) {
|
|
|
870
883
|
|
|
871
884
|
} else if (step.name === "Integrate") {
|
|
872
885
|
instructions = "Merge the approved task branch into main.\n\n" +
|
|
873
|
-
"Run: "+ LIFECYCLE_ENV + " integrate " + taskId + " \"merge: fix: " + safeTitle + "\"\n\n" +
|
|
886
|
+
"Run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " integrate " + taskId + " \"merge: fix: " + safeTitle + "\"\n\n" +
|
|
874
887
|
"Read the output:\n" +
|
|
875
888
|
"- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
|
|
876
889
|
"- If it contains MERGED_EMPTY, the branch had no commits ahead of main (a runtime-state deliverable, declared by Build as repo_diff: none). Integration succeeded vacuously: the merge lock was NOT taken and there is no new commit. Report 'merged empty: no repo changes — deliverable was runtime state', then end your report with exactly this line: VERDICT: PASS. SKIP STEP 2 (push): there is no new commit to push.\n" +
|
|
877
|
-
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Report 'merge lock held', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
890
|
+
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish) and the 10-minute bounded backoff is exhausted. Report 'merge lock held after bounded backoff', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
878
891
|
"- If it contains CONFLICT, the plain merge failed — the merge was aborted, main is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
879
892
|
"RESOLUTION:\n" +
|
|
880
893
|
"R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): "+ LIFECYCLE_ENV + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (main is already checked out in the repo checkout, so git forbids checking it out a second time): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " main. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge " + TASK_BRANCH + ". This reproduces the exact conflict (main has not moved — the lock was held throughout). The task branch " + TASK_BRANCH + " is never modified.\n" +
|
|
@@ -903,13 +916,17 @@ while (i < STEPS.length) {
|
|
|
903
916
|
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
904
917
|
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
905
918
|
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
906
|
-
" → " + 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" +
|
|
907
920
|
"Run exactly this command and no other publish-related commands:\n" +
|
|
908
921
|
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
909
922
|
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
910
923
|
" bash " + PUBLISH_NPM + "\n\n" +
|
|
911
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" +
|
|
912
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" +
|
|
913
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" +
|
|
914
931
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
915
932
|
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
@@ -928,20 +945,34 @@ while (i < STEPS.length) {
|
|
|
928
945
|
// getprovenance-vs-HEAD verification below stays as the final gate.
|
|
929
946
|
var artifactPublish = null;
|
|
930
947
|
var publishLockRefreshed = false;
|
|
948
|
+
var publishSkippedNoLock = false;
|
|
931
949
|
try {
|
|
932
|
-
// STEP 0 (mechanical):
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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"] } }
|
|
939
961
|
);
|
|
940
|
-
if (
|
|
941
|
-
|
|
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;
|
|
942
970
|
}
|
|
943
|
-
|
|
944
|
-
//
|
|
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
|
|
945
976
|
// agent only makes the artifact_edit call and reports whether it was
|
|
946
977
|
// accepted — no prose claim to trust. If the artifact tool namespace
|
|
947
978
|
// is missing from this child it reports honestly and the workflow
|
|
@@ -962,14 +993,46 @@ while (i < STEPS.length) {
|
|
|
962
993
|
}
|
|
963
994
|
var publishFailure = null;
|
|
964
995
|
if (rebuildTrigger.edit_started) {
|
|
965
|
-
// STEP 1b (mechanical): bounded poll for build completion
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
996
|
+
// STEP 1b (mechanical): bounded poll for build completion, chunked so
|
|
997
|
+
// the merge-lock lease is refreshed before it can expire. The 600s
|
|
998
|
+
// lease is shorter than the worst-case 10-minute build poll, so the
|
|
999
|
+
// poll runs in three chunks (7 checks x 30s ~= 3.5 min each) with a
|
|
1000
|
+
// holder-only lease refresh between chunks. If a refresh fails, the
|
|
1001
|
+
// lock was lost: stop the run and park the task — never continue to
|
|
1002
|
+
// a provenance stamp or version assignment without holding the lock.
|
|
1003
|
+
var buildPoll = null;
|
|
1004
|
+
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
1005
|
+
if (chunk > 1) {
|
|
1006
|
+
var refreshPoll = await agent(
|
|
1007
|
+
"Refresh the merge lock for task " + taskId + ".\n" +
|
|
1008
|
+
"Run: " + LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
|
|
1009
|
+
"If the output contains REFRESHED, return JSON { \"held\": true } and nothing else. Otherwise return JSON { \"held\": false, \"output\": \"<verbatim output>\" } and nothing else.",
|
|
1010
|
+
{ key: attemptKey("publish-lock-refresh-poll-" + taskId + "-c" + chunk, totalReworkCount), label: "Refreshing merge lock during build poll",
|
|
1011
|
+
schema: { type: "object", properties: { held: { type: "boolean" }, output: { type: "string" } }, required: ["held"] },
|
|
1012
|
+
timeoutMs: 60000 }
|
|
1013
|
+
);
|
|
1014
|
+
if (!refreshPoll || refreshPoll.held !== true) {
|
|
1015
|
+
return await parkTask("Merge-lock lease lost during the artifact build poll (refresh before chunk " + chunk + " of 3 failed: " + ((refreshPoll && refreshPoll.output) || "no output") + "). Fail-closed: stopping before any provenance stamp or version assignment.");
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Chunk 1 keeps the original stable key; later chunks use -c<N>
|
|
1019
|
+
// suffixed keys. All are attemptKey-scoped so rework passes stay
|
|
1020
|
+
// disjoint (replay-key contract).
|
|
1021
|
+
var pollKey = (chunk === 1)
|
|
1022
|
+
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
1023
|
+
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
1024
|
+
buildPoll = await agent(
|
|
1025
|
+
"Poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 7 checks (3.5 minutes max).\n" +
|
|
1026
|
+
"Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
|
|
1027
|
+
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1028
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
|
|
1029
|
+
timeoutMs: 270000 }
|
|
1030
|
+
);
|
|
1031
|
+
if (buildPoll && buildPoll.build_done) { break; }
|
|
1032
|
+
}
|
|
1033
|
+
if (!buildPoll || !buildPoll.build_done) {
|
|
1034
|
+
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
1035
|
+
}
|
|
973
1036
|
if (buildPoll.build_done) {
|
|
974
1037
|
// STEP 1c (mechanical): stamp provenance from workflow-computed values.
|
|
975
1038
|
var provStamp = await agent(
|
|
@@ -993,25 +1056,34 @@ while (i < STEPS.length) {
|
|
|
993
1056
|
} else {
|
|
994
1057
|
publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
|
|
995
1058
|
}
|
|
996
|
-
//
|
|
1059
|
+
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
1060
|
+
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
997
1061
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
998
|
-
// the merge lock — even when the publish
|
|
999
|
-
// 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).
|
|
1000
1065
|
var postDeploy = await agent(
|
|
1001
1066
|
"Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
|
|
1002
1067
|
"Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
|
|
1003
1068
|
{ key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
|
|
1004
1069
|
schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
|
|
1005
1070
|
);
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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);
|
|
1013
1086
|
}
|
|
1014
|
-
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
1015
1087
|
} catch (pubErr) {
|
|
1016
1088
|
// Best-effort cleanup: if the lock was refreshed, try to release it
|
|
1017
1089
|
// before parking so the failure cannot wedge the next run.
|
|
@@ -1032,6 +1104,12 @@ while (i < STEPS.length) {
|
|
|
1032
1104
|
// The work agent no longer performs the publish — it reports on the
|
|
1033
1105
|
// mechanical outcome above. It must not rebuild or re-stamp: a second
|
|
1034
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 {
|
|
1035
1113
|
instructions = "Publish the merged code to the live artifact.\n\n" +
|
|
1036
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" +
|
|
1037
1115
|
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
@@ -1044,6 +1122,7 @@ while (i < STEPS.length) {
|
|
|
1044
1122
|
"The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
|
|
1045
1123
|
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
1046
1124
|
"The VERDICT line must be the last line of your report.";
|
|
1125
|
+
}
|
|
1047
1126
|
} else if (PUBLISH_TYPE === "vercel") {
|
|
1048
1127
|
// vercel publish is not yet implemented — block without inventing behavior
|
|
1049
1128
|
instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
|
|
@@ -1055,7 +1134,9 @@ while (i < STEPS.length) {
|
|
|
1055
1134
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
1056
1135
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
1057
1136
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
1058
|
-
? "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" +
|
|
1059
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" +
|
|
1060
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" +
|
|
1061
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"
|
|
@@ -1250,8 +1331,21 @@ while (i < STEPS.length) {
|
|
|
1250
1331
|
const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
|
|
1251
1332
|
|
|
1252
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.
|
|
1253
1341
|
var publishVerified = false;
|
|
1342
|
+
var npmPublishSkipped = false;
|
|
1254
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) {
|
|
1255
1349
|
try {
|
|
1256
1350
|
var verifyResult = await agent(
|
|
1257
1351
|
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1268,6 +1362,7 @@ while (i < STEPS.length) {
|
|
|
1268
1362
|
} catch (e) {
|
|
1269
1363
|
return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1270
1364
|
}
|
|
1365
|
+
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
1271
1366
|
}
|
|
1272
1367
|
|
|
1273
1368
|
// Artifact publish verification: the worker cannot self-certify a deploy.
|
|
@@ -1275,6 +1370,14 @@ while (i < STEPS.length) {
|
|
|
1275
1370
|
// integrated commit. A stale or missing provenance means the publish did not
|
|
1276
1371
|
// land — fail closed, do not trust the worker's prose.
|
|
1277
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 {
|
|
1278
1381
|
try {
|
|
1279
1382
|
var headResult = await agent(
|
|
1280
1383
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1298,6 +1401,7 @@ while (i < STEPS.length) {
|
|
|
1298
1401
|
} catch (e) {
|
|
1299
1402
|
return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1300
1403
|
}
|
|
1404
|
+
} // end: !publishSkippedNoLock — a skipped publish has nothing to verify
|
|
1301
1405
|
}
|
|
1302
1406
|
|
|
1303
1407
|
// Session notes. Machine-readable marker lines are extracted from the full
|
|
@@ -1306,9 +1410,13 @@ while (i < STEPS.length) {
|
|
|
1306
1410
|
// reading TARGET_VERSION=) depend on them.
|
|
1307
1411
|
let summary;
|
|
1308
1412
|
var workerMarkers = extractMarkerLines(workerText);
|
|
1309
|
-
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).
|
|
1310
1416
|
const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1311
|
-
|
|
1417
|
+
(npmPublishSkipped
|
|
1418
|
+
? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
|
|
1419
|
+
: "published: muse-crew@" + publishTarget.target);
|
|
1312
1420
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
|
|
1313
1421
|
} else {
|
|
1314
1422
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
|