muse-crew 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/docs/decisions/AGENTS.md +3 -0
- package/docs/decisions/publish-path.md +195 -6
- package/docs/decisions/workflow-core.md +1 -1
- package/docs/guide.md +3 -3
- package/docs/publish-verification.md +5 -2
- package/docs/reviews/critic-0143-redo.md +94 -0
- package/docs/reviews/critic-0143.md +108 -0
- package/lib/AGENTS.md +5 -3
- package/lib/crew-api.js +8 -6
- package/lib/publish-npm.sh +39 -2
- package/lib/retry-publish.js +5 -2
- package/lib/test-detached-integrate.sh +416 -0
- package/lib/test-publish-preflight.sh +94 -5
- package/lib/verify-publish.js +3 -4
- package/lib/worktree-lifecycle.sh +380 -94
- package/package.json +1 -1
- package/workflows/bugfix.js +67 -70
- package/workflows/chore.js +65 -68
- package/workflows/standard.js +67 -70
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
# * old helper that does not know --check (usage to stderr, exit 1,
|
|
22
22
|
# no CREDENTIAL markers) with DRY_RUN=1 -> no skip (fall-through
|
|
23
23
|
# preserved: unknown is not absent)
|
|
24
|
+
# - detached fixtures (detached-HEAD audit REDO, 2026-09-19): a detached
|
|
25
|
+
# checkout with a local origin. Case 6 (origin/HEAD set, DRY_RUN=1) ->
|
|
26
|
+
# preflight passes through to PUBLISH_DRY_RUN=1 (no refusal — detached
|
|
27
|
+
# publication is legitimate). Case 7 (origin/HEAD deleted, DRY_RUN=1) ->
|
|
28
|
+
# PUBLISH_FAILED=env before any mutation, repo untouched (the genuinely
|
|
29
|
+
# unknowable destination fails in the preflight, ahead of the DRY_RUN
|
|
30
|
+
# block).
|
|
31
|
+
# - Case 8: attached checkout with no origin (DRY_RUN=1) -> PUBLISH_FAILED=env
|
|
32
|
+
# pre-mutation, repo untouched (2026-09-19 critic F-A1: the anchor used to
|
|
33
|
+
# check no-origin only on the detached branch, so attached+no-origin
|
|
34
|
+
# failed post-mutation after the version-bump commit and registry publish).
|
|
24
35
|
# - --check probe unit test against the real helper: on the author
|
|
25
36
|
# machine this expects exit 0 and exactly CREDENTIAL_OK=custom.npm.
|
|
26
37
|
# NOTE: this case depends on the `custom.npm` credential existing in
|
|
@@ -52,6 +63,13 @@ git -C "$SCRATCH" config user.email "test@crew"
|
|
|
52
63
|
git -C "$SCRATCH" config user.name "crew-test"
|
|
53
64
|
echo x > "$SCRATCH/f.txt"
|
|
54
65
|
git -C "$SCRATCH" add -A && git -C "$SCRATCH" commit -qm init
|
|
66
|
+
# The scratch repo needs an origin for the DRY_RUN pass-through cases
|
|
67
|
+
# (2026-09-19 critic F-A1: the push-destination anchor fails closed
|
|
68
|
+
# pre-mutation when origin is absent, attached or detached).
|
|
69
|
+
git init -q --bare "$TMPBASE/origin.git"
|
|
70
|
+
git -C "$SCRATCH" remote add origin "$TMPBASE/origin.git"
|
|
71
|
+
git -C "$SCRATCH" push -q origin main
|
|
72
|
+
git -C "$SCRATCH" remote set-head origin -a
|
|
55
73
|
|
|
56
74
|
# --- Fake helpers ---
|
|
57
75
|
cat > "$TMPBASE/helper-ok.py" <<'EOF'
|
|
@@ -78,6 +96,32 @@ print("Usage: helper-old.py <tarball-path>", file=sys.stderr)
|
|
|
78
96
|
sys.exit(1)
|
|
79
97
|
EOF
|
|
80
98
|
|
|
99
|
+
# --- Detached fixtures (detached-HEAD audit REDO, 2026-09-19): a detached
|
|
100
|
+
# checkout with a local origin. Case 6 runs with origin/HEAD set; Case 7
|
|
101
|
+
# deletes origin/HEAD first, exercising push-destination's genuinely-
|
|
102
|
+
# unknowable branch. ---
|
|
103
|
+
ORIGIN="$TMPBASE/origin-det.git"
|
|
104
|
+
git init -q --bare "$ORIGIN"
|
|
105
|
+
SCRATCH_DET="$TMPBASE/repo-detached"
|
|
106
|
+
git init -q -b main "$SCRATCH_DET"
|
|
107
|
+
git -C "$SCRATCH_DET" config user.email "test@crew"
|
|
108
|
+
git -C "$SCRATCH_DET" config user.name "crew-test"
|
|
109
|
+
echo x > "$SCRATCH_DET/f.txt"
|
|
110
|
+
git -C "$SCRATCH_DET" add -A && git -C "$SCRATCH_DET" commit -qm init
|
|
111
|
+
git -C "$SCRATCH_DET" remote add origin "$ORIGIN"
|
|
112
|
+
git -C "$SCRATCH_DET" push -q origin main
|
|
113
|
+
git -C "$SCRATCH_DET" remote set-head origin -a
|
|
114
|
+
git -C "$SCRATCH_DET" checkout -q --detach HEAD
|
|
115
|
+
|
|
116
|
+
# --- Attached no-origin fixture (F-A1): an attached checkout with no origin
|
|
117
|
+
# fails closed in the anchor, attached or detached. ---
|
|
118
|
+
SCRATCH_NOORIGIN="$TMPBASE/repo-no-origin"
|
|
119
|
+
git init -q -b main "$SCRATCH_NOORIGIN"
|
|
120
|
+
git -C "$SCRATCH_NOORIGIN" config user.email "test@crew"
|
|
121
|
+
git -C "$SCRATCH_NOORIGIN" config user.name "crew-test"
|
|
122
|
+
echo x > "$SCRATCH_NOORIGIN/f.txt"
|
|
123
|
+
git -C "$SCRATCH_NOORIGIN" add -A && git -C "$SCRATCH_NOORIGIN" commit -qm init
|
|
124
|
+
|
|
81
125
|
ACTIONABLE="npm publish not configured on this machine — see docs/guide.md#npm-releases; code changes are merged, nothing was versioned or published"
|
|
82
126
|
|
|
83
127
|
# --- Structural: extract the shipped preflight block by anchor ---
|
|
@@ -123,13 +167,23 @@ fi
|
|
|
123
167
|
|
|
124
168
|
# Runner: full publish-npm.sh against the scratch repo. The preflight exits
|
|
125
169
|
# before any mutation, so the repo needs no package.json.
|
|
170
|
+
# LIFECYCLE + CREW_HOME are exported because the preflight now resolves the
|
|
171
|
+
# push destination up front (2026-09-19: the push-destination resolution moved
|
|
172
|
+
# from step 13 to the preflight — fail before mutation, never after the
|
|
173
|
+
# registry publish; 2026-09-19 detached-HEAD audit REDO: a detached checkout
|
|
174
|
+
# resolves to HEAD:<destination>, refused only when genuinely unknowable).
|
|
175
|
+
# The scratch repo sits on branch `main`, so integration-target
|
|
176
|
+
# resolves cleanly and the npm-credential assertions are unaffected.
|
|
126
177
|
run_full() { # run_full <helper_path> [extra_env...]
|
|
127
178
|
local helper="$1"; shift
|
|
128
179
|
local run="$TMPBASE/run-$RANDOM"
|
|
129
180
|
mkdir -p "$run"
|
|
181
|
+
local crew_home="$TMPBASE/crew-home"
|
|
182
|
+
mkdir -p "$crew_home"
|
|
130
183
|
(
|
|
131
184
|
export TASK_ID="preflight-test" REPO_PATH="$SCRATCH" PKG="muse-crew" TARGET_VERSION="9.9.9"
|
|
132
185
|
export NPM_PUBLISH_PY="$helper"
|
|
186
|
+
export LIFECYCLE="$SCRIPT_DIR/worktree-lifecycle.sh" CREW_HOME="$crew_home" CREW_REPO="$SCRATCH"
|
|
133
187
|
# A bare `export` (empty "$@") would dump the environment to stdout and
|
|
134
188
|
# corrupt the captured run path — only export when extras are given.
|
|
135
189
|
if [ $# -gt 0 ]; then export "$@"; fi
|
|
@@ -140,11 +194,11 @@ run_full() { # run_full <helper_path> [extra_env...]
|
|
|
140
194
|
echo "$run"
|
|
141
195
|
}
|
|
142
196
|
|
|
143
|
-
repo_state() { #
|
|
144
|
-
local head status tgz
|
|
145
|
-
head="$(git -C "$
|
|
146
|
-
status="$(git -C "$
|
|
147
|
-
tgz="$(find "$
|
|
197
|
+
repo_state() { # repo_state [repo]: "HEAD=<sha> STATUS=<porcelain-lines> TGZ=<count>"
|
|
198
|
+
local head status tgz repo="${1:-$SCRATCH}"
|
|
199
|
+
head="$(git -C "$repo" rev-parse HEAD)"
|
|
200
|
+
status="$(git -C "$repo" status --porcelain | wc -l)"
|
|
201
|
+
tgz="$(find "$repo" -maxdepth 1 -name '*.tgz' | wc -l)"
|
|
148
202
|
echo "HEAD=$head STATUS=$status TGZ=$tgz"
|
|
149
203
|
}
|
|
150
204
|
|
|
@@ -203,6 +257,41 @@ else
|
|
|
203
257
|
no "real helper --check: code=$CHECK_CODE out='$CHECK_OUT' (expected CREDENTIAL_OK=custom.npm — this case is credential-dependent; see header)"
|
|
204
258
|
fi
|
|
205
259
|
|
|
260
|
+
# --- Case 6: detached + resolvable destination + DRY_RUN=1 -> pass-through, no refusal ---
|
|
261
|
+
RUN="$(run_full "$TMPBASE/helper-ok.py" DRY_RUN=1 REPO_PATH="$SCRATCH_DET")"
|
|
262
|
+
CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"
|
|
263
|
+
if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_DRY_RUN=1"* ]] \
|
|
264
|
+
&& [[ "$OUT" != *"PUBLISH_SKIPPED"* ]] && [[ "$OUT" != *"PUBLISH_FAILED"* ]]; then
|
|
265
|
+
ok "detached + resolvable destination: preflight passes through to PUBLISH_DRY_RUN=1 (no refusal)"
|
|
266
|
+
else
|
|
267
|
+
no "detached resolvable: code=$CODE out='$OUT'"
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# --- Case 7: detached + origin/HEAD unset -> PUBLISH_FAILED=env before any mutation ---
|
|
271
|
+
git -C "$SCRATCH_DET" remote set-head origin -d
|
|
272
|
+
BEFORE="$(repo_state "$SCRATCH_DET")"
|
|
273
|
+
RUN="$(run_full "$TMPBASE/helper-ok.py" DRY_RUN=1 REPO_PATH="$SCRATCH_DET")"
|
|
274
|
+
CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"; AFTER="$(repo_state "$SCRATCH_DET")"
|
|
275
|
+
if [ "$CODE" -ne 0 ] && [[ "$OUT" == *"PUBLISH_FAILED=env"* ]] \
|
|
276
|
+
&& [[ "$OUT" != *"PUBLISH_SKIPPED"* ]] && [[ "$OUT" != *"PUBLISH_DRY_RUN=1"* ]] \
|
|
277
|
+
&& [ "$BEFORE" = "$AFTER" ]; then
|
|
278
|
+
ok "detached + unknowable destination: fails closed pre-mutation, repo untouched"
|
|
279
|
+
else
|
|
280
|
+
no "detached unknowable: code=$CODE state=$BEFORE/$AFTER out='$OUT'"
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
# --- Case 8: attached + no origin -> PUBLISH_FAILED=env before any mutation ---
|
|
284
|
+
BEFORE="$(repo_state "$SCRATCH_NOORIGIN")"
|
|
285
|
+
RUN="$(run_full "$TMPBASE/helper-ok.py" DRY_RUN=1 REPO_PATH="$SCRATCH_NOORIGIN")"
|
|
286
|
+
CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"; AFTER="$(repo_state "$SCRATCH_NOORIGIN")"
|
|
287
|
+
if [ "$CODE" -ne 0 ] && [[ "$OUT" == *"PUBLISH_FAILED=env"* ]] \
|
|
288
|
+
&& [[ "$OUT" != *"PUBLISH_SKIPPED"* ]] && [[ "$OUT" != *"PUBLISH_DRY_RUN=1"* ]] \
|
|
289
|
+
&& [ "$BEFORE" = "$AFTER" ]; then
|
|
290
|
+
ok "attached + no origin: fails closed pre-mutation, repo untouched"
|
|
291
|
+
else
|
|
292
|
+
no "attached no-origin: code=$CODE state=$BEFORE/$AFTER out='$OUT'"
|
|
293
|
+
fi
|
|
294
|
+
|
|
206
295
|
bash -n "$PUBLISH" && ok "publish-npm.sh syntax OK" || no "publish-npm.sh syntax"
|
|
207
296
|
|
|
208
297
|
echo ""
|
package/lib/verify-publish.js
CHANGED
|
@@ -298,8 +298,9 @@ if (head !== commit) {
|
|
|
298
298
|
// 2026-09-18, blocker 4: bind by exact (task_id, commit, attempt) when the
|
|
299
299
|
// attempt is known — the original and retry attempts share a commit, and
|
|
300
300
|
// positional selection is luck. Within one attempt, select the OLDEST
|
|
301
|
-
// submitted entry (the trigger issuance;
|
|
302
|
-
//
|
|
301
|
+
// submitted entry (the trigger issuance; a receipt-bearing second "submitted"
|
|
302
|
+
// may follow it — the oldest binds the trigger instant, the latest carries
|
|
303
|
+
// the observed agent_id).
|
|
303
304
|
let triggerTs = null;
|
|
304
305
|
let manifestBefore = null;
|
|
305
306
|
try {
|
|
@@ -313,8 +314,6 @@ try {
|
|
|
313
314
|
(attempt == null || e.attempt === attempt)
|
|
314
315
|
);
|
|
315
316
|
if (candidates.length > 0) {
|
|
316
|
-
// Oldest first: the trigger issuance is the first submitted entry for
|
|
317
|
-
// the attempt (observations are "build-observed" per blocker 2).
|
|
318
317
|
candidates.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
319
318
|
const e = candidates[0];
|
|
320
319
|
triggerTs = e.ts || null;
|