muse-crew 0.7.19 → 0.8.0
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/docs/guide.md +179 -7
- package/docs/publish-verification.md +4 -2
- package/lib/AGENTS.md +6 -2
- package/lib/build-registry.js +3 -2
- package/lib/compose-evidence-caption.js +57 -14
- package/lib/publish-npm.sh +33 -0
- package/lib/test-publish-preflight.sh +210 -0
- package/lib/test-worktree-backend.sh +51 -1
- package/lib/update-watch.js +633 -0
- package/lib/verify-publish.js +51 -13
- package/lib/worktree-lifecycle.sh +68 -10
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-update-watch.md +13 -0
- package/seed/crons.json +14 -1
- package/seed/workflows/upgrade.md +21 -0
- package/workflows/AGENTS.md +1 -1
- package/workflows/bugfix.js +416 -74
- package/workflows/chore.js +223 -72
- package/workflows/crew-dispatch.js +43 -6
- package/workflows/crew-init.js +173 -10
- package/workflows/standard.js +231 -79
- package/workflows/upgrade.js +794 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-publish-preflight.sh — regression tests for the npm publish
|
|
3
|
+
# capability preflight (bugfix 2026-09-17): publish-npm.sh assumed the
|
|
4
|
+
# author-machine publish helper path and failed cryptically AFTER the
|
|
5
|
+
# version-bump release commit when npm publish was not configured.
|
|
6
|
+
#
|
|
7
|
+
# Covers:
|
|
8
|
+
# - structural: the PREFLIGHT block exists in lib/publish-npm.sh
|
|
9
|
+
# (extracted by anchor), sits after env validation and before the
|
|
10
|
+
# DRY_RUN block, checks the helper with -f/-r only (never -x — the
|
|
11
|
+
# helper is invoked as `python3 <path>`), and reads no wall clock or
|
|
12
|
+
# randomness.
|
|
13
|
+
# - full-script runs against a scratch repo:
|
|
14
|
+
# * NPM_PUBLISH_PY missing -> PUBLISH_SKIPPED=no-npm-publish + the
|
|
15
|
+
# actionable line, exit 0, HEAD unchanged, status clean, no tarball
|
|
16
|
+
# * fake helper whose --check reports CREDENTIAL_MISSING=custom.npm
|
|
17
|
+
# (exit 1) -> same skip outcome, exit 0, repo untouched
|
|
18
|
+
# * fake helper whose --check reports CREDENTIAL_OK=custom.npm
|
|
19
|
+
# (exit 0) with DRY_RUN=1 -> preflight passes through to
|
|
20
|
+
# PUBLISH_DRY_RUN=1 (asserts pass-through, not the mutation)
|
|
21
|
+
# * old helper that does not know --check (usage to stderr, exit 1,
|
|
22
|
+
# no CREDENTIAL markers) with DRY_RUN=1 -> no skip (fall-through
|
|
23
|
+
# preserved: unknown is not absent)
|
|
24
|
+
# - --check probe unit test against the real helper: on the author
|
|
25
|
+
# machine this expects exit 0 and exactly CREDENTIAL_OK=custom.npm.
|
|
26
|
+
# NOTE: this case depends on the `custom.npm` credential existing in
|
|
27
|
+
# the secure vault — on a machine without it the probe correctly
|
|
28
|
+
# reports CREDENTIAL_MISSING=custom.npm (exit 1) and this case fails.
|
|
29
|
+
# That is expected outside the author machine, not a regression.
|
|
30
|
+
# - bash -n syntax check on publish-npm.sh
|
|
31
|
+
#
|
|
32
|
+
# Anchors: `# PREFLIGHT-ANCHOR` ... `# PREFLIGHT-END` in lib/publish-npm.sh.
|
|
33
|
+
|
|
34
|
+
set -uo pipefail
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
|
+
PUBLISH="$SCRIPT_DIR/publish-npm.sh"
|
|
38
|
+
REAL_HELPER="$HOME/workspace/skills/npm/bin/npm-publish.py"
|
|
39
|
+
|
|
40
|
+
pass=0
|
|
41
|
+
fail_count=0
|
|
42
|
+
ok() { echo "PASS: $1"; pass=$((pass + 1)); }
|
|
43
|
+
no() { echo "FAIL: $1"; fail_count=$((fail_count + 1)); }
|
|
44
|
+
|
|
45
|
+
TMPBASE="$(mktemp -d)"
|
|
46
|
+
trap 'rm -rf "$TMPBASE"' EXIT
|
|
47
|
+
|
|
48
|
+
# --- Scratch repo (Publish's REPO_PATH must be a git checkout) ---
|
|
49
|
+
SCRATCH="$TMPBASE/repo"
|
|
50
|
+
git init -q -b main "$SCRATCH"
|
|
51
|
+
git -C "$SCRATCH" config user.email "test@crew"
|
|
52
|
+
git -C "$SCRATCH" config user.name "crew-test"
|
|
53
|
+
echo x > "$SCRATCH/f.txt"
|
|
54
|
+
git -C "$SCRATCH" add -A && git -C "$SCRATCH" commit -qm init
|
|
55
|
+
|
|
56
|
+
# --- Fake helpers ---
|
|
57
|
+
cat > "$TMPBASE/helper-ok.py" <<'EOF'
|
|
58
|
+
#!/usr/bin/env python3
|
|
59
|
+
import sys
|
|
60
|
+
if len(sys.argv) == 2 and sys.argv[1] == "--check":
|
|
61
|
+
print("CREDENTIAL_OK=custom.npm")
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
EOF
|
|
65
|
+
cat > "$TMPBASE/helper-missing-cred.py" <<'EOF'
|
|
66
|
+
#!/usr/bin/env python3
|
|
67
|
+
import sys
|
|
68
|
+
if len(sys.argv) == 2 and sys.argv[1] == "--check":
|
|
69
|
+
print("CREDENTIAL_MISSING=custom.npm")
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
EOF
|
|
73
|
+
# An old helper that predates --check: usage to stderr, exit 1, no markers.
|
|
74
|
+
cat > "$TMPBASE/helper-old.py" <<'EOF'
|
|
75
|
+
#!/usr/bin/env python3
|
|
76
|
+
import sys
|
|
77
|
+
print("Usage: helper-old.py <tarball-path>", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
EOF
|
|
80
|
+
|
|
81
|
+
ACTIONABLE="npm publish not configured on this machine — see docs/guide.md#npm-releases; code changes are merged, nothing was versioned or published"
|
|
82
|
+
|
|
83
|
+
# --- Structural: extract the shipped preflight block by anchor ---
|
|
84
|
+
BLOCK="$TMPBASE/preflight.sh"
|
|
85
|
+
sed -n '/# PREFLIGHT-ANCHOR/,/# PREFLIGHT-END/p' "$PUBLISH" > "$BLOCK"
|
|
86
|
+
if grep -q 'PUBLISH_SKIPPED=no-npm-publish' "$BLOCK" \
|
|
87
|
+
&& grep -q 'docs/guide.md#npm-releases' "$BLOCK" \
|
|
88
|
+
&& grep -q 'exit 0' "$BLOCK"; then
|
|
89
|
+
ok "extracted PREFLIGHT block from publish-npm.sh"
|
|
90
|
+
else
|
|
91
|
+
no "PREFLIGHT extraction failed (anchors missing)"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# The helper is invoked as `python3 <path>`: -f/-r, never -x.
|
|
95
|
+
if grep -q '\[ -f "\$NPM_PUBLISH_PY" \]' "$BLOCK" \
|
|
96
|
+
&& grep -q '\[ -r "\$NPM_PUBLISH_PY" \]' "$BLOCK" \
|
|
97
|
+
&& ! grep -q '\[ -x "\$NPM_PUBLISH_PY" \]' "$BLOCK"; then
|
|
98
|
+
ok "preflight checks -f/-r on the helper, never -x"
|
|
99
|
+
else
|
|
100
|
+
no "preflight helper checks wrong (want -f/-r, no -x)"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# No wall-clock or randomness in the preflight block.
|
|
104
|
+
if ! grep -Eq '\$\(date|date \+|\$RANDOM' "$BLOCK"; then
|
|
105
|
+
ok "preflight block reads no wall clock and no randomness"
|
|
106
|
+
else
|
|
107
|
+
no "preflight block contains clock/randomness"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Placement: after env validation, before the DRY_RUN block.
|
|
111
|
+
VALIDATE_LINE="$(grep -n 'rev-parse --git-dir' "$PUBLISH" | head -1 | cut -d: -f1)"
|
|
112
|
+
ANCHOR_LINE="$(grep -n '# PREFLIGHT-ANCHOR' "$PUBLISH" | head -1 | cut -d: -f1)"
|
|
113
|
+
END_LINE="$(grep -n '# PREFLIGHT-END' "$PUBLISH" | head -1 | cut -d: -f1)"
|
|
114
|
+
DRYRUN_LINE="$(grep -n 'DRY_RUN:-0}' "$PUBLISH" | head -1 | cut -d: -f1)"
|
|
115
|
+
if [ -n "$VALIDATE_LINE" ] && [ -n "$ANCHOR_LINE" ] && [ -n "$END_LINE" ] \
|
|
116
|
+
&& [ -n "$DRYRUN_LINE" ] \
|
|
117
|
+
&& [ "$VALIDATE_LINE" -lt "$ANCHOR_LINE" ] \
|
|
118
|
+
&& [ "$END_LINE" -lt "$DRYRUN_LINE" ]; then
|
|
119
|
+
ok "PREFLIGHT block sits after env validation, before DRY_RUN"
|
|
120
|
+
else
|
|
121
|
+
no "PREFLIGHT placement wrong (validate=$VALIDATE_LINE anchor=$ANCHOR_LINE end=$END_LINE dryrun=$DRYRUN_LINE)"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Runner: full publish-npm.sh against the scratch repo. The preflight exits
|
|
125
|
+
# before any mutation, so the repo needs no package.json.
|
|
126
|
+
run_full() { # run_full <helper_path> [extra_env...]
|
|
127
|
+
local helper="$1"; shift
|
|
128
|
+
local run="$TMPBASE/run-$RANDOM"
|
|
129
|
+
mkdir -p "$run"
|
|
130
|
+
(
|
|
131
|
+
export TASK_ID="preflight-test" REPO_PATH="$SCRATCH" PKG="muse-crew" TARGET_VERSION="9.9.9"
|
|
132
|
+
export NPM_PUBLISH_PY="$helper"
|
|
133
|
+
# A bare `export` (empty "$@") would dump the environment to stdout and
|
|
134
|
+
# corrupt the captured run path — only export when extras are given.
|
|
135
|
+
if [ $# -gt 0 ]; then export "$@"; fi
|
|
136
|
+
bash "$PUBLISH" > "$run/out" 2>&1
|
|
137
|
+
)
|
|
138
|
+
local code=$?
|
|
139
|
+
echo "$code" > "$run/code"
|
|
140
|
+
echo "$run"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
repo_state() { # prints "HEAD=<sha> STATUS=<porcelain-lines> TGZ=<count>"
|
|
144
|
+
local head status tgz
|
|
145
|
+
head="$(git -C "$SCRATCH" rev-parse HEAD)"
|
|
146
|
+
status="$(git -C "$SCRATCH" status --porcelain | wc -l)"
|
|
147
|
+
tgz="$(find "$SCRATCH" -maxdepth 1 -name '*.tgz' | wc -l)"
|
|
148
|
+
echo "HEAD=$head STATUS=$status TGZ=$tgz"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# --- Case 1: helper missing -> clean skip, exit 0, repo untouched ---
|
|
152
|
+
BEFORE="$(repo_state)"
|
|
153
|
+
RUN="$(run_full /nonexistent/path.py)"
|
|
154
|
+
CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"; AFTER="$(repo_state)"
|
|
155
|
+
if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_SKIPPED=no-npm-publish"* ]] \
|
|
156
|
+
&& [[ "$OUT" == *"$ACTIONABLE"* ]] \
|
|
157
|
+
&& [[ "$OUT" != *"PUBLISH_FAILED"* ]] && [[ "$OUT" != *"PUBLISH_COMPLETE"* ]] \
|
|
158
|
+
&& [ "$BEFORE" = "$AFTER" ]; then
|
|
159
|
+
ok "helper missing: skips with marker + actionable line, exit 0, repo untouched"
|
|
160
|
+
else
|
|
161
|
+
no "helper missing: code=$CODE state=$BEFORE/$AFTER out='$OUT'"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# --- Case 2: helper present, credential missing -> same skip, repo untouched ---
|
|
165
|
+
BEFORE="$(repo_state)"
|
|
166
|
+
RUN="$(run_full "$TMPBASE/helper-missing-cred.py")"
|
|
167
|
+
CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"; AFTER="$(repo_state)"
|
|
168
|
+
if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_SKIPPED=no-npm-publish"* ]] \
|
|
169
|
+
&& [[ "$OUT" == *"$ACTIONABLE"* ]] \
|
|
170
|
+
&& [[ "$OUT" != *"PUBLISH_FAILED"* ]] && [[ "$OUT" != *"PUBLISH_COMPLETE"* ]] \
|
|
171
|
+
&& [ "$BEFORE" = "$AFTER" ]; then
|
|
172
|
+
ok "credential missing: skips with marker + actionable line, exit 0, repo untouched"
|
|
173
|
+
else
|
|
174
|
+
no "credential missing: code=$CODE state=$BEFORE/$AFTER out='$OUT'"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# --- Case 3: credential present + DRY_RUN=1 -> preflight passes through ---
|
|
178
|
+
RUN="$(run_full "$TMPBASE/helper-ok.py" DRY_RUN=1)"
|
|
179
|
+
CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"
|
|
180
|
+
if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_DRY_RUN=1"* ]] \
|
|
181
|
+
&& [[ "$OUT" != *"PUBLISH_SKIPPED"* ]]; then
|
|
182
|
+
ok "credential present: preflight passes through to PUBLISH_DRY_RUN=1"
|
|
183
|
+
else
|
|
184
|
+
no "credential present: code=$CODE out='$OUT'"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# --- Case 4: old helper without --check -> no skip (fall-through) ---
|
|
188
|
+
RUN="$(run_full "$TMPBASE/helper-old.py" DRY_RUN=1)"
|
|
189
|
+
CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"
|
|
190
|
+
if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_DRY_RUN=1"* ]] \
|
|
191
|
+
&& [[ "$OUT" != *"PUBLISH_SKIPPED"* ]]; then
|
|
192
|
+
ok "old helper (no --check): unknown, not absent — falls through, no skip"
|
|
193
|
+
else
|
|
194
|
+
no "old helper: code=$CODE out='$OUT'"
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# --- Case 5: real helper --check probe (author machine: credential present) ---
|
|
198
|
+
CHECK_OUT="$(python3 "$REAL_HELPER" --check 2>&1)"
|
|
199
|
+
CHECK_CODE=$?
|
|
200
|
+
if [ "$CHECK_CODE" -eq 0 ] && [ "$CHECK_OUT" = "CREDENTIAL_OK=custom.npm" ]; then
|
|
201
|
+
ok "real helper --check: CREDENTIAL_OK=custom.npm, exit 0, nothing else printed"
|
|
202
|
+
else
|
|
203
|
+
no "real helper --check: code=$CHECK_CODE out='$CHECK_OUT' (expected CREDENTIAL_OK=custom.npm — this case is credential-dependent; see header)"
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
bash -n "$PUBLISH" && ok "publish-npm.sh syntax OK" || no "publish-npm.sh syntax"
|
|
207
|
+
|
|
208
|
+
echo ""
|
|
209
|
+
echo "$pass passed, $fail_count failed"
|
|
210
|
+
exit $((fail_count > 0))
|
|
@@ -147,7 +147,57 @@ echo "$out" | grep -q '^MERGED: ' || fail "integrate-test: no MERGED line (no re
|
|
|
147
147
|
echo "$out" | grep -q '^NO_REMOTE: ' || fail "integrate-test: no NO_REMOTE line: $out"
|
|
148
148
|
"$CREW_LIB/merge-lock.sh" release "int2" >/dev/null || fail "integrate-test: lock release (no remote) failed"
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
# Prefix resolution (task 4e1a1bba, 2026-09-17): a Build agent created the
|
|
151
|
+
# branch with raw git as task/<8-char id>, bypassing prepare, so the
|
|
152
|
+
# registry is empty. resolve-branch must find it by id-prefix enumeration;
|
|
153
|
+
# inspect/integrate/cleanup must operate on the real branch, never on a
|
|
154
|
+
# reconstructed task/<full-id>.
|
|
155
|
+
T4=/tmp/crew-git-prefix-test
|
|
156
|
+
rm -rf "$T4"
|
|
157
|
+
mkdir -p "$T4"
|
|
158
|
+
git init -q -b main "$T4/repo"
|
|
159
|
+
git -C "$T4/repo" config user.email test@test.t
|
|
160
|
+
git -C "$T4/repo" config user.name test
|
|
161
|
+
echo base > "$T4/repo/f.txt"
|
|
162
|
+
echo ".worktrees/" > "$T4/repo/.gitignore"
|
|
163
|
+
git -C "$T4/repo" add -A
|
|
164
|
+
git -C "$T4/repo" commit -qm base
|
|
165
|
+
export CREW_REPO="$T4/repo"
|
|
166
|
+
|
|
167
|
+
fullid="51aaa9fe-9cb5-4a4f-acfc-72092fcf0899"
|
|
168
|
+
shortid="51aaa9fe"
|
|
169
|
+
# Raw-git branch creation, like the Build agent: no registry entry.
|
|
170
|
+
git -C "$T4/repo" branch "task/$shortid"
|
|
171
|
+
[ -f "$T4/repo/.worktrees/.registry/$fullid" ] && fail "prefix-test: registry should be empty for this fixture"
|
|
172
|
+
out=$(bash "$LIFECYCLE" resolve-branch "$fullid") || fail "prefix-test: resolve-branch failed: $out"
|
|
173
|
+
[ "$out" = "task/$shortid" ] || fail "prefix-test: resolve-branch returned '$out', expected task/$shortid"
|
|
174
|
+
|
|
175
|
+
# inspect operates on the real branch (no "branch ... does not exist")
|
|
176
|
+
out=$(bash "$LIFECYCLE" inspect "$fullid") || fail "prefix-test: inspect failed on abbreviated branch: $out"
|
|
177
|
+
echo "$out" | grep -q "=== Branch: task/$shortid ===" || fail "prefix-test: inspect used the wrong branch"
|
|
178
|
+
|
|
179
|
+
# verify-merge vacuous-pass on the merged-prefix branch tip == main
|
|
180
|
+
out=$(bash "$LIFECYCLE" verify-merge "$fullid") || fail "prefix-test: verify-merge failed on abbreviated branch: $out"
|
|
181
|
+
|
|
182
|
+
# cleanup deletes the real abbreviated branch, not a reconstructed one
|
|
183
|
+
out=$(bash "$LIFECYCLE" cleanup "$fullid") || fail "prefix-test: cleanup failed: $out"
|
|
184
|
+
git -C "$T4/repo" rev-parse --verify "task/$shortid" >/dev/null 2>&1 && fail "prefix-test: cleanup left the abbreviated branch behind"
|
|
185
|
+
|
|
186
|
+
# Ambiguous prefixes fail closed (exit non-zero), never guess
|
|
187
|
+
fullid2="deadbeef-1111-2222-3333-444455556666"
|
|
188
|
+
git -C "$T4/repo" branch "task/deadbeef"
|
|
189
|
+
git -C "$T4/repo" branch "task/deadbeef-1111"
|
|
190
|
+
if bash "$LIFECYCLE" resolve-branch "$fullid2" >/dev/null 2>&1; then
|
|
191
|
+
fail "prefix-test: resolve-branch guessed on ambiguous prefix"
|
|
192
|
+
fi
|
|
193
|
+
git -C "$T4/repo" branch -D "task/deadbeef" "task/deadbeef-1111" >/dev/null
|
|
194
|
+
|
|
195
|
+
# No branch at all: resolve-branch falls back to the canonical name so
|
|
196
|
+
# callers keep their "branch X does not exist" failure
|
|
197
|
+
out=$(bash "$LIFECYCLE" resolve-branch "$fullid") || fail "prefix-test: resolve-branch failed with no branch present"
|
|
198
|
+
[ "$out" = "task/$fullid" ] || fail "prefix-test: resolve-branch returned '$out', expected canonical fallback task/$fullid"
|
|
199
|
+
|
|
200
|
+
rm -rf "$T2" "$T3" "$T4"
|
|
151
201
|
export CREW_REPO="$T"
|
|
152
202
|
rm -rf "$T"
|
|
153
203
|
echo "worktree-backend: OK"
|