muse-crew 0.4.3 → 0.4.5
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 +2 -0
- package/API.md +68 -21
- package/README.md +18 -2
- package/docs/guide.md +81 -11
- package/docs/visual-verdict.md +120 -0
- package/identities/hazel.md +22 -0
- package/lib/AGENTS.md +6 -1
- package/lib/build-registry.js +80 -0
- package/lib/compose-evidence.py +144 -0
- package/lib/crew-release.sh +9 -1
- package/lib/merge-lock.sh +18 -0
- package/lib/orphan-sweep.sh +70 -7
- package/lib/publish-npm.sh +38 -14
- package/lib/test-merge-lock.sh +69 -0
- package/lib/test-orphan-sweep.sh +74 -8
- package/lib/test-publish-verify.sh +157 -0
- package/lib/test-version-write.sh +102 -0
- package/lib/test-worktree-backend.sh +79 -0
- package/lib/worktree-lifecycle.sh +69 -28
- package/package.json +1 -1
- package/seed/cron-body-template.md +5 -3
- package/seed/workflows/bugfix.md +7 -2
- package/seed/workflows/chore.md +7 -3
- package/seed/workflows/standard.md +7 -2
- package/workflows/AGENTS.md +4 -4
- package/workflows/bugfix.js +1023 -173
- package/workflows/chore.js +888 -146
- package/workflows/crew-dispatch.js +405 -98
- package/workflows/crew-init.js +4 -35
- package/workflows/docs.js +342 -22
- package/workflows/standard.js +1024 -172
- package/workflows/tests/retry-cap.test.mjs +102 -0
- package/workflows/tests/work-agent-failure.test.mjs +122 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-publish-verify.sh — regression test for the retry-tolerant step-12
|
|
3
|
+
# publish verification in publish-npm.sh (canary 5a027278).
|
|
4
|
+
#
|
|
5
|
+
# Self-contained: extracts the shipped step-12 block from lib/publish-npm.sh
|
|
6
|
+
# by anchor and runs it against a fake `npm` on PATH whose read replica
|
|
7
|
+
# lags — non-zero exits, then the old version, then the target version —
|
|
8
|
+
# and requires the block to converge on success. Also asserts fail-closed
|
|
9
|
+
# exhaustion when the replica never converges.
|
|
10
|
+
#
|
|
11
|
+
# Anchors: `# STEP-12-ANCHOR` ... `# STEP-12-END` in lib/publish-npm.sh.
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
PUBLISH="$SCRIPT_DIR/publish-npm.sh"
|
|
17
|
+
|
|
18
|
+
PKG="muse-crew"
|
|
19
|
+
OLD_VERSION="9.9.8"
|
|
20
|
+
TARGET_VERSION="9.9.9"
|
|
21
|
+
|
|
22
|
+
pass=0
|
|
23
|
+
fail_count=0
|
|
24
|
+
ok() { echo "PASS: $1"; pass=$((pass + 1)); }
|
|
25
|
+
no() { echo "FAIL: $1"; fail_count=$((fail_count + 1)); }
|
|
26
|
+
|
|
27
|
+
TMPBASE="$(mktemp -d)"
|
|
28
|
+
trap 'rm -rf "$TMPBASE"' EXIT
|
|
29
|
+
|
|
30
|
+
# --- Extract the shipped step-12 block by anchor ---
|
|
31
|
+
BLOCK="$TMPBASE/step12.sh"
|
|
32
|
+
sed -n '/# STEP-12-ANCHOR/,/# STEP-12-END/p' "$PUBLISH" > "$BLOCK"
|
|
33
|
+
if grep -q 'PUBLISH_VERIFIED' "$BLOCK" && grep -q 'fail "verify"' "$BLOCK"; then
|
|
34
|
+
ok "extracted step-12 block from publish-npm.sh"
|
|
35
|
+
else
|
|
36
|
+
no "step-12 extraction failed (anchors missing)"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# --- Runner: fake npm first in PATH, behavior baked in at generation time ---
|
|
40
|
+
FAKEBIN="$TMPBASE/fakebin"
|
|
41
|
+
mkdir -p "$FAKEBIN"
|
|
42
|
+
|
|
43
|
+
# write_fake_npm <behavior>
|
|
44
|
+
# laggy: exit 1 twice (npm failure), old version twice (stale replica),
|
|
45
|
+
# then the target version (replica converged)
|
|
46
|
+
# stale: old version forever (replica never converges)
|
|
47
|
+
write_fake_npm() {
|
|
48
|
+
case "$1" in
|
|
49
|
+
laggy) BEHAVIOR='
|
|
50
|
+
if [ "$n" -le 2 ]; then exit 1; fi
|
|
51
|
+
if [ "$n" -le 4 ]; then echo "'"$OLD_VERSION"'"; exit 0; fi
|
|
52
|
+
echo "'"$TARGET_VERSION"'"; exit 0' ;;
|
|
53
|
+
stale) BEHAVIOR='echo "'"$OLD_VERSION"'"; exit 0' ;;
|
|
54
|
+
*) echo "unknown behavior: $1" >&2; exit 1 ;;
|
|
55
|
+
esac
|
|
56
|
+
cat > "$FAKEBIN/npm" <<EOF
|
|
57
|
+
#!/usr/bin/env bash
|
|
58
|
+
# Records its own argv (for the cache-busting assertion) and its call count.
|
|
59
|
+
echo "\$*" >> "$TMPBASE/argv"
|
|
60
|
+
n=\$(cat "$TMPBASE/calls" 2>/dev/null || echo 0)
|
|
61
|
+
n=\$((n + 1))
|
|
62
|
+
echo "\$n" > "$TMPBASE/calls"
|
|
63
|
+
$BEHAVIOR
|
|
64
|
+
EOF
|
|
65
|
+
chmod +x "$FAKEBIN/npm"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# run_step12: runs the shipped block; env: PKG TARGET_VERSION
|
|
69
|
+
# VERIFY_ATTEMPTS VERIFY_SLEEP_SECS; returns the block's exit code and
|
|
70
|
+
# captures its stdout in $OUT.
|
|
71
|
+
run_step12() {
|
|
72
|
+
local runner="$TMPBASE/run-$RANDOM.sh"
|
|
73
|
+
{
|
|
74
|
+
echo 'fail() { echo "PUBLISH_FAILED=$1"; shift; echo "$1"; exit 1; }'
|
|
75
|
+
cat "$BLOCK"
|
|
76
|
+
} > "$runner"
|
|
77
|
+
chmod +x "$runner"
|
|
78
|
+
rm -f "$TMPBASE/calls" "$TMPBASE/argv"
|
|
79
|
+
OUT="$(PATH="$FAKEBIN:$PATH" \
|
|
80
|
+
PKG="$PKG" TARGET_VERSION="$TARGET_VERSION" \
|
|
81
|
+
VERIFY_ATTEMPTS="${VERIFY_ATTEMPTS:-12}" \
|
|
82
|
+
VERIFY_SLEEP_SECS="${VERIFY_SLEEP_SECS:-0}" \
|
|
83
|
+
bash "$runner" 2>&1)"
|
|
84
|
+
local code=$?
|
|
85
|
+
echo "$OUT"
|
|
86
|
+
return "$code"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# --- Case 1: lagging replica converges — block must succeed ---
|
|
90
|
+
write_fake_npm laggy
|
|
91
|
+
OUT="$(run_step12)"
|
|
92
|
+
CODE=$?
|
|
93
|
+
if [ "$CODE" -eq 0 ]; then
|
|
94
|
+
ok "laggy replica converges: block exits 0"
|
|
95
|
+
else
|
|
96
|
+
no "laggy replica converges: block exited $CODE (output: $OUT)"
|
|
97
|
+
fi
|
|
98
|
+
case "$OUT" in
|
|
99
|
+
*"PUBLISH_VERIFIED=$TARGET_VERSION"*)
|
|
100
|
+
ok "prints PUBLISH_VERIFIED=$TARGET_VERSION on convergence" ;;
|
|
101
|
+
*) no "missing PUBLISH_VERIFIED=$TARGET_VERSION (output: $OUT)" ;;
|
|
102
|
+
esac
|
|
103
|
+
case "$OUT" in
|
|
104
|
+
*"PUBLISH_FAILED"*) no "PUBLISH_FAILED printed despite convergence" ;;
|
|
105
|
+
*) ok "no PUBLISH_FAILED on convergence" ;;
|
|
106
|
+
esac
|
|
107
|
+
CALLS="$(cat "$TMPBASE/calls" 2>/dev/null || echo 0)"
|
|
108
|
+
if [ "$CALLS" = "5" ]; then
|
|
109
|
+
ok "converged after exactly 5 reads (2 failures, 2 stale, 1 good)"
|
|
110
|
+
else
|
|
111
|
+
no "expected 5 npm calls on convergence, saw $CALLS"
|
|
112
|
+
fi
|
|
113
|
+
RETRIES="$(grep -c 'PUBLISH_VERIFY_RETRY=' <<< "$OUT" || true)"
|
|
114
|
+
if [ "$RETRIES" = "4" ]; then
|
|
115
|
+
ok "prints a PUBLISH_VERIFY_RETRY progress line per miss"
|
|
116
|
+
else
|
|
117
|
+
no "expected 4 PUBLISH_VERIFY_RETRY lines, saw $RETRIES"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# --- Case 2: replica never converges — fail closed with the verify marker ---
|
|
121
|
+
write_fake_npm stale
|
|
122
|
+
VERIFY_ATTEMPTS=3 OUT="$(run_step12)"
|
|
123
|
+
CODE=$?
|
|
124
|
+
if [ "$CODE" -ne 0 ]; then
|
|
125
|
+
ok "stale replica: block fails closed (non-zero exit)"
|
|
126
|
+
else
|
|
127
|
+
no "stale replica: block exited 0 despite persistent mismatch"
|
|
128
|
+
fi
|
|
129
|
+
case "$OUT" in
|
|
130
|
+
*"PUBLISH_FAILED=verify"*)
|
|
131
|
+
ok "exhaustion keeps the PUBLISH_FAILED=verify marker contract" ;;
|
|
132
|
+
*) no "missing PUBLISH_FAILED=verify (output: $OUT)" ;;
|
|
133
|
+
esac
|
|
134
|
+
case "$OUT" in
|
|
135
|
+
*"registry=$OLD_VERSION after 3 attempts"*)
|
|
136
|
+
ok "exhaustion detail names registry value and attempt count" ;;
|
|
137
|
+
*) no "missing exhaustion detail (output: $OUT)" ;;
|
|
138
|
+
esac
|
|
139
|
+
CALLS="$(cat "$TMPBASE/calls" 2>/dev/null || echo 0)"
|
|
140
|
+
if [ "$CALLS" = "3" ]; then
|
|
141
|
+
ok "bounded: stops after VERIFY_ATTEMPTS reads"
|
|
142
|
+
else
|
|
143
|
+
no "expected 3 npm calls on exhaustion, saw $CALLS"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# --- Case 3: every read goes through --prefer-online (client cache-bust) ---
|
|
147
|
+
write_fake_npm laggy
|
|
148
|
+
run_step12 >/dev/null
|
|
149
|
+
if grep -q -- '--prefer-online' "$TMPBASE/argv"; then
|
|
150
|
+
ok "every npm view read passes --prefer-online"
|
|
151
|
+
else
|
|
152
|
+
no "npm view reads missing --prefer-online"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo ""
|
|
156
|
+
echo "$pass passed, $fail_count failed."
|
|
157
|
+
[ "$fail_count" -eq 0 ]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-version-write.sh — regression test for the escape-preserving step-8
|
|
3
|
+
# version write in publish-npm.sh (canary 5a027278).
|
|
4
|
+
#
|
|
5
|
+
# Self-contained: seeds a temp git repo with the repo's current package.json
|
|
6
|
+
# (which carries the \u2014 escape in its description field), extracts the
|
|
7
|
+
# shipped step-8 block from lib/publish-npm.sh by anchor, and runs it.
|
|
8
|
+
# Exits 0 only if the write lands a true one-line diff, the version parses
|
|
9
|
+
# back to TARGET_VERSION, and the \u2014 escape survives as escape bytes.
|
|
10
|
+
#
|
|
11
|
+
# Anchors: `# STEP-8-ANCHOR` ... `# STEP-8-END` in lib/publish-npm.sh.
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
17
|
+
PUBLISH="$SCRIPT_DIR/publish-npm.sh"
|
|
18
|
+
|
|
19
|
+
# Target the next patch after the repo's current version. (A hardcoded target
|
|
20
|
+
# goes stale the moment the repo releases it: writing the version that's
|
|
21
|
+
# already there produces no diff and the preservation check fails closed.)
|
|
22
|
+
TARGET_VERSION="$(node -e 'const fs=require("fs");const v=JSON.parse(fs.readFileSync(process.argv[1],"utf8")).version.split(".");v[v.length-1]=String(Number(v[v.length-1])+1);console.log(v.join("."))' "$REPO_DIR/package.json")"
|
|
23
|
+
|
|
24
|
+
pass=0
|
|
25
|
+
fail_count=0
|
|
26
|
+
ok() { echo "PASS: $1"; pass=$((pass + 1)); }
|
|
27
|
+
no() { echo "FAIL: $1"; fail_count=$((fail_count + 1)); }
|
|
28
|
+
|
|
29
|
+
TMPBASE="$(mktemp -d)"
|
|
30
|
+
trap 'rm -rf "$TMPBASE"' EXIT
|
|
31
|
+
|
|
32
|
+
# --- Fixture: temp git repo seeded with the repo's current package.json ---
|
|
33
|
+
FIX="$TMPBASE/fixture"
|
|
34
|
+
mkdir -p "$FIX"
|
|
35
|
+
git init -q -b main "$FIX" >/dev/null
|
|
36
|
+
git -C "$FIX" config user.email "test@example.com"
|
|
37
|
+
git -C "$FIX" config user.name "test"
|
|
38
|
+
cp "$REPO_DIR/package.json" "$FIX/package.json"
|
|
39
|
+
git -C "$FIX" add package.json
|
|
40
|
+
git -C "$FIX" commit -qm "initial"
|
|
41
|
+
|
|
42
|
+
# Sanity: fixture actually carries the escape this regression is about.
|
|
43
|
+
if grep -q '\\u2014' "$FIX/package.json"; then
|
|
44
|
+
ok "fixture package.json carries the \\u2014 escape"
|
|
45
|
+
else
|
|
46
|
+
no "fixture package.json lacks the \\u2014 escape"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# --- Extract the shipped step-8 block by anchor ---
|
|
50
|
+
BLOCK="$TMPBASE/step8.sh"
|
|
51
|
+
sed -n '/# STEP-8-ANCHOR/,/# STEP-8-END/p' "$PUBLISH" > "$BLOCK"
|
|
52
|
+
if grep -q 'fail "version-diff"' "$BLOCK" && grep -q 'sed -i -E' "$BLOCK"; then
|
|
53
|
+
ok "extracted step-8 block from publish-npm.sh"
|
|
54
|
+
else
|
|
55
|
+
no "step-8 extraction failed (anchors missing)"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# --- Run the shipped block against the fixture ---
|
|
59
|
+
RUNNER="$TMPBASE/run.sh"
|
|
60
|
+
{
|
|
61
|
+
echo 'fail() { echo "STEP_FAIL=$1" >&2; exit 1; }'
|
|
62
|
+
cat "$BLOCK"
|
|
63
|
+
} > "$RUNNER"
|
|
64
|
+
chmod +x "$RUNNER"
|
|
65
|
+
|
|
66
|
+
if REPO_PATH="$FIX" TARGET_VERSION="$TARGET_VERSION" bash "$RUNNER" >/dev/null 2>&1; then
|
|
67
|
+
ok "shipped step-8 block exits 0 on escape-carrying package.json"
|
|
68
|
+
else
|
|
69
|
+
no "shipped step-8 block failed closed on escape-carrying package.json"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# --- Assertions on the result ---
|
|
73
|
+
VER="$(node -p "JSON.parse(require('fs').readFileSync('$FIX/package.json','utf8')).version")"
|
|
74
|
+
if [ "$VER" = "$TARGET_VERSION" ]; then
|
|
75
|
+
ok "package.json version is $TARGET_VERSION after write"
|
|
76
|
+
else
|
|
77
|
+
no "package.json version is $VER, expected $TARGET_VERSION"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
NUMSTAT="$(git -C "$FIX" diff --numstat -- package.json | tr '\t' ' ')"
|
|
81
|
+
if [ "$NUMSTAT" = "1 1 package.json" ]; then
|
|
82
|
+
ok "diff is exactly one line (preservation check passes)"
|
|
83
|
+
else
|
|
84
|
+
no "numstat is '$NUMSTAT', expected '1 1 package.json'"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
if grep -q '\\u2014' "$FIX/package.json"; then
|
|
88
|
+
ok "\\u2014 escape preserved as escape bytes (not expanded)"
|
|
89
|
+
else
|
|
90
|
+
no "\\u2014 escape was expanded by the write"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
DESC_DIFF="$(git -C "$FIX" diff -- package.json | grep -c '^[-+].*description' || true)"
|
|
94
|
+
if [ "$DESC_DIFF" = "0" ]; then
|
|
95
|
+
ok "description line untouched by the write"
|
|
96
|
+
else
|
|
97
|
+
no "description line was rewritten"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
echo ""
|
|
101
|
+
echo "$pass passed, $fail_count failed."
|
|
102
|
+
[ "$fail_count" -eq 0 ]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# test-worktree-backend.sh — regression tests for lib/worktree-lifecycle.sh
|
|
3
|
+
# (git worktree backend).
|
|
4
|
+
#
|
|
5
|
+
# Lifecycle on a scratch repo:
|
|
6
|
+
# validate → prepare (CREATED) → prepare (REUSED) → inspect → status →
|
|
7
|
+
# cleanup → cleanup (idempotent) → dirty-main preflight (exit 4)
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
|
+
LIFECYCLE="$REPO_DIR/lib/worktree-lifecycle.sh"
|
|
12
|
+
|
|
13
|
+
fail() { echo "FAIL: $1"; exit 1; }
|
|
14
|
+
|
|
15
|
+
T=/tmp/crew-git-backend-test
|
|
16
|
+
rm -rf "$T"
|
|
17
|
+
mkdir -p "$T"
|
|
18
|
+
cd "$T"
|
|
19
|
+
git init -q -b main .
|
|
20
|
+
git config user.email test@test.t
|
|
21
|
+
git config user.name test
|
|
22
|
+
echo x > f.txt
|
|
23
|
+
git add -A
|
|
24
|
+
git commit -qm init
|
|
25
|
+
|
|
26
|
+
export CREW_REPO="$T"
|
|
27
|
+
tid="backendtest1"
|
|
28
|
+
|
|
29
|
+
# validate: good id passes, bad id is rejected
|
|
30
|
+
bash "$LIFECYCLE" validate "$tid" >/dev/null || fail "validate rejected a good id"
|
|
31
|
+
bash "$LIFECYCLE" validate "bad id!" >/dev/null 2>&1 && fail "validate accepted a bad id"
|
|
32
|
+
|
|
33
|
+
# prepare: creates worktree + branch + registry entry
|
|
34
|
+
out=$(bash "$LIFECYCLE" prepare "$tid") || fail "prepare exited non-zero: $out"
|
|
35
|
+
echo "$out" | grep -q '^CREATED' || fail "prepare: expected CREATED, got: $out"
|
|
36
|
+
echo "$out" | grep -q '^BASE: ' || fail "prepare: no BASE: line"
|
|
37
|
+
[ -d "$T/.worktrees/$tid" ] || fail "prepare: .worktrees/$tid missing"
|
|
38
|
+
git -C "$T" rev-parse --verify "task/$tid" >/dev/null 2>&1 || fail "prepare: branch task/$tid missing"
|
|
39
|
+
[ -f "$T/.worktrees/.registry/$tid" ] || fail "prepare: registry entry missing"
|
|
40
|
+
grep -q '^branch=task/backendtest1$' "$T/.worktrees/.registry/$tid" || fail "prepare: registry branch wrong"
|
|
41
|
+
grep -q "^path=$T/.worktrees/$tid\$" "$T/.worktrees/.registry/$tid" || fail "prepare: registry path wrong"
|
|
42
|
+
|
|
43
|
+
# prepare again: reuses the existing worktree
|
|
44
|
+
out=$(bash "$LIFECYCLE" prepare "$tid") || fail "prepare (reuse) exited non-zero"
|
|
45
|
+
echo "$out" | grep -q '^REUSED' || fail "prepare: expected REUSED on second run: $out"
|
|
46
|
+
|
|
47
|
+
# inspect: diffs the task branch against main
|
|
48
|
+
echo change >> "$T/.worktrees/$tid/f.txt"
|
|
49
|
+
(cd "$T/.worktrees/$tid" && git commit -qam work)
|
|
50
|
+
out=$(bash "$LIFECYCLE" inspect "$tid") || fail "inspect exited non-zero"
|
|
51
|
+
echo "$out" | grep -q "=== Branch: task/$tid ===" || fail "inspect: wrong branch header"
|
|
52
|
+
echo "$out" | grep -q "change" || fail "inspect: diff missing the worktree change"
|
|
53
|
+
|
|
54
|
+
# status: reports registry, worktree, branch
|
|
55
|
+
out=$(bash "$LIFECYCLE" status "$tid") || fail "status exited non-zero"
|
|
56
|
+
echo "$out" | grep -q "=== Worktree: $tid ===" || fail "status: missing header"
|
|
57
|
+
echo "$out" | grep -q "task/$tid" || fail "status: missing branch info"
|
|
58
|
+
|
|
59
|
+
# cleanup: removes worktree + branch + registry entry
|
|
60
|
+
out=$(bash "$LIFECYCLE" cleanup "$tid") || fail "cleanup exited non-zero"
|
|
61
|
+
[ -d "$T/.worktrees/$tid" ] && fail "cleanup: worktree still exists"
|
|
62
|
+
git -C "$T" rev-parse --verify "task/$tid" >/dev/null 2>&1 && fail "cleanup: branch still exists"
|
|
63
|
+
[ -f "$T/.worktrees/.registry/$tid" ] && fail "cleanup: registry entry still exists"
|
|
64
|
+
|
|
65
|
+
# cleanup again: idempotent
|
|
66
|
+
out=$(bash "$LIFECYCLE" cleanup "$tid") || fail "cleanup (idempotent) exited non-zero"
|
|
67
|
+
echo "$out" | grep -q "already clean" || fail "cleanup: expected already-clean on second run"
|
|
68
|
+
|
|
69
|
+
# prepare fails closed when main is dirty (exit 4)
|
|
70
|
+
echo dirty >> "$T/f.txt"
|
|
71
|
+
if bash "$LIFECYCLE" prepare "backendtest2" >/dev/null 2>&1; then
|
|
72
|
+
fail "prepare succeeded with dirty main"
|
|
73
|
+
else
|
|
74
|
+
[ "$?" -eq 4 ] || fail "prepare with dirty main: expected exit 4"
|
|
75
|
+
fi
|
|
76
|
+
git -C "$T" checkout -q -- f.txt
|
|
77
|
+
|
|
78
|
+
rm -rf "$T"
|
|
79
|
+
echo "worktree-backend: OK"
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# worktree-lifecycle.sh —
|
|
2
|
+
# worktree-lifecycle.sh — worktree lifecycle manager for Muse Crew
|
|
3
|
+
#
|
|
4
|
+
# Git backend: task worktrees live in $REPO/.worktrees/<task_id>, on
|
|
5
|
+
# branches named task/<task_id>. This script is the single seam where the
|
|
6
|
+
# worktree strategy lives — workflows and agents never run raw git worktree
|
|
7
|
+
# commands; they call these lifecycle commands instead.
|
|
8
|
+
#
|
|
9
|
+
# The crew's registry ($REPO/.worktrees/.registry/<task_id>) is the source
|
|
10
|
+
# of truth for task→branch/path. Never reconstruct the mapping from git
|
|
11
|
+
# state alone.
|
|
3
12
|
#
|
|
4
13
|
# Commands:
|
|
5
14
|
# validate <task_id> — check task ID is safe for branches/paths
|
|
@@ -30,6 +39,28 @@ ORPHAN_SWEEP="$LIB_DIR/orphan-sweep.sh"
|
|
|
30
39
|
|
|
31
40
|
# --- helpers ---
|
|
32
41
|
|
|
42
|
+
# Task branch name: task/<id>. The registry wins when the task was
|
|
43
|
+
# prepared before.
|
|
44
|
+
resolve_branch() {
|
|
45
|
+
local task_id="$1" b
|
|
46
|
+
if [ -f "$REGISTRY_DIR/$task_id" ]; then
|
|
47
|
+
b=$(grep '^branch=' "$REGISTRY_DIR/$task_id" | cut -d= -f2-)
|
|
48
|
+
if [ -n "$b" ]; then printf '%s' "$b"; return 0; fi
|
|
49
|
+
fi
|
|
50
|
+
printf 'task/%s' "$task_id"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Worktree path: $REPO/.worktrees/<id>. The registry wins when the task
|
|
54
|
+
# was prepared before.
|
|
55
|
+
resolve_path() {
|
|
56
|
+
local task_id="$1" p
|
|
57
|
+
if [ -f "$REGISTRY_DIR/$task_id" ]; then
|
|
58
|
+
p=$(grep '^path=' "$REGISTRY_DIR/$task_id" | cut -d= -f2-)
|
|
59
|
+
if [ -n "$p" ]; then printf '%s' "$p"; return 0; fi
|
|
60
|
+
fi
|
|
61
|
+
printf '%s/%s' "$WORKTREE_DIR" "$task_id"
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
validate_task_id() {
|
|
34
65
|
local id="$1"
|
|
35
66
|
if [[ ! "$id" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
|
|
@@ -57,12 +88,12 @@ require_clean_main() {
|
|
|
57
88
|
}
|
|
58
89
|
|
|
59
90
|
register() {
|
|
60
|
-
local task_id="$1" base_commit="$2"
|
|
91
|
+
local task_id="$1" base_commit="$2" branch="$3" wt_path="$4"
|
|
61
92
|
mkdir -p "$REGISTRY_DIR"
|
|
62
93
|
cat > "$REGISTRY_DIR/$task_id" <<EOF
|
|
63
94
|
task_id=$task_id
|
|
64
|
-
branch
|
|
65
|
-
path=$
|
|
95
|
+
branch=$branch
|
|
96
|
+
path=$wt_path
|
|
66
97
|
base_commit=$base_commit
|
|
67
98
|
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
68
99
|
EOF
|
|
@@ -96,7 +127,7 @@ cmd_prepare() {
|
|
|
96
127
|
return 0
|
|
97
128
|
fi
|
|
98
129
|
|
|
99
|
-
# Preflight: main must be clean
|
|
130
|
+
# Preflight: main must be clean — prepare fails closed.
|
|
100
131
|
require_clean_main || exit 4
|
|
101
132
|
|
|
102
133
|
local base_commit
|
|
@@ -107,7 +138,7 @@ cmd_prepare() {
|
|
|
107
138
|
mkdir -p "$WORKTREE_DIR"
|
|
108
139
|
git worktree add "$WORKTREE_DIR/$task_id" -b "task/$task_id"
|
|
109
140
|
|
|
110
|
-
register "$task_id" "$base_commit"
|
|
141
|
+
register "$task_id" "$base_commit" "task/$task_id" "$WORKTREE_DIR/$task_id"
|
|
111
142
|
|
|
112
143
|
echo "CREATED: .worktrees/$task_id on branch task/$task_id"
|
|
113
144
|
echo "BASE: $base_commit"
|
|
@@ -117,26 +148,29 @@ cmd_inspect() {
|
|
|
117
148
|
local task_id="$1"
|
|
118
149
|
validate_task_id "$task_id"
|
|
119
150
|
|
|
151
|
+
local branch
|
|
152
|
+
branch=$(resolve_branch "$task_id")
|
|
153
|
+
|
|
120
154
|
cd "$REPO"
|
|
121
155
|
|
|
122
156
|
# Verify the branch exists
|
|
123
|
-
if ! git rev-parse --verify "
|
|
124
|
-
echo "ERROR: branch
|
|
157
|
+
if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
158
|
+
echo "ERROR: branch $branch does not exist"
|
|
125
159
|
return 1
|
|
126
160
|
fi
|
|
127
161
|
|
|
128
|
-
echo "=== Branch:
|
|
162
|
+
echo "=== Branch: $branch ==="
|
|
129
163
|
local tip
|
|
130
|
-
tip=$(git rev-parse "
|
|
164
|
+
tip=$(git rev-parse "$branch")
|
|
131
165
|
echo "TIP: $tip"
|
|
132
166
|
echo ""
|
|
133
167
|
echo "=== Commits ahead of main ==="
|
|
134
|
-
git log --oneline "main
|
|
168
|
+
git log --oneline "main..$branch"
|
|
135
169
|
echo ""
|
|
136
|
-
echo "=== Diff: main
|
|
137
|
-
git diff --stat "main
|
|
170
|
+
echo "=== Diff: main...$branch ==="
|
|
171
|
+
git diff --stat "main...$branch"
|
|
138
172
|
echo ""
|
|
139
|
-
git diff "main
|
|
173
|
+
git diff "main...$branch"
|
|
140
174
|
}
|
|
141
175
|
|
|
142
176
|
cmd_integrate() {
|
|
@@ -144,24 +178,27 @@ cmd_integrate() {
|
|
|
144
178
|
local commit_msg="${2:-merge: $task_id}"
|
|
145
179
|
validate_task_id "$task_id"
|
|
146
180
|
|
|
181
|
+
local branch
|
|
182
|
+
branch=$(resolve_branch "$task_id")
|
|
183
|
+
|
|
147
184
|
cd "$REPO"
|
|
148
185
|
|
|
149
186
|
# Preflight: main must be clean
|
|
150
187
|
require_clean_main || exit 4
|
|
151
188
|
|
|
152
189
|
# Verify task branch exists and has commits ahead of main
|
|
153
|
-
if ! git rev-parse --verify "
|
|
154
|
-
echo "ERROR: branch
|
|
190
|
+
if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
191
|
+
echo "ERROR: branch $branch does not exist"
|
|
155
192
|
return 1
|
|
156
193
|
fi
|
|
157
194
|
|
|
158
195
|
local ahead
|
|
159
|
-
ahead=$(git rev-list --count "main
|
|
196
|
+
ahead=$(git rev-list --count "main..$branch")
|
|
160
197
|
if [ "$ahead" -eq 0 ]; then
|
|
161
198
|
# Approved empty diff: the deliverable was runtime state (cron,
|
|
162
199
|
# scheduler, dashboard config), not a repo change. Nothing to merge,
|
|
163
200
|
# nothing to serialize — the merge lock is intentionally not taken.
|
|
164
|
-
echo "MERGED_EMPTY:
|
|
201
|
+
echo "MERGED_EMPTY: $branch has no commits ahead of main — runtime-state deliverable, nothing to merge"
|
|
165
202
|
return 0
|
|
166
203
|
fi
|
|
167
204
|
|
|
@@ -174,7 +211,7 @@ cmd_integrate() {
|
|
|
174
211
|
echo "$lock_result"
|
|
175
212
|
|
|
176
213
|
# Merge
|
|
177
|
-
if ! git merge --no-ff "
|
|
214
|
+
if ! git merge --no-ff "$branch" -m "$commit_msg" 2>&1; then
|
|
178
215
|
echo "CONFLICT: merge failed — aborting"
|
|
179
216
|
git merge --abort 2>/dev/null || true
|
|
180
217
|
exit 3
|
|
@@ -248,6 +285,10 @@ cmd_status() {
|
|
|
248
285
|
local task_id="$1"
|
|
249
286
|
validate_task_id "$task_id"
|
|
250
287
|
|
|
288
|
+
local branch wt_path
|
|
289
|
+
branch=$(resolve_branch "$task_id")
|
|
290
|
+
wt_path=$(resolve_path "$task_id")
|
|
291
|
+
|
|
251
292
|
cd "$REPO"
|
|
252
293
|
|
|
253
294
|
echo "=== Worktree: $task_id ==="
|
|
@@ -261,11 +302,11 @@ cmd_status() {
|
|
|
261
302
|
fi
|
|
262
303
|
|
|
263
304
|
# Worktree existence
|
|
264
|
-
if [ -d "$
|
|
265
|
-
echo "Worktree: exists at
|
|
305
|
+
if [ -n "$wt_path" ] && [ -d "$wt_path" ]; then
|
|
306
|
+
echo "Worktree: exists at $wt_path"
|
|
266
307
|
local head dirty
|
|
267
|
-
head=$(cd "$
|
|
268
|
-
dirty=$(cd "$
|
|
308
|
+
head=$(cd "$wt_path" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
309
|
+
dirty=$(cd "$wt_path" && git status --porcelain 2>/dev/null | wc -l)
|
|
269
310
|
echo "HEAD: $head"
|
|
270
311
|
[ "$dirty" -gt 0 ] && echo "Dirty: $dirty uncommitted changes" || echo "Dirty: clean"
|
|
271
312
|
else
|
|
@@ -273,13 +314,13 @@ cmd_status() {
|
|
|
273
314
|
fi
|
|
274
315
|
|
|
275
316
|
# Branch existence and merge status
|
|
276
|
-
if git rev-parse --verify "
|
|
317
|
+
if git rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
277
318
|
local ahead behind
|
|
278
|
-
ahead=$(git rev-list --count "main
|
|
279
|
-
behind=$(git rev-list --count "
|
|
280
|
-
echo "Branch:
|
|
319
|
+
ahead=$(git rev-list --count "main..$branch" 2>/dev/null || echo "?")
|
|
320
|
+
behind=$(git rev-list --count "$branch..main" 2>/dev/null || echo "?")
|
|
321
|
+
echo "Branch: $branch (${ahead} ahead, ${behind} behind main)"
|
|
281
322
|
local merged
|
|
282
|
-
merged=$(git branch --merged main 2>/dev/null | sed 's/^[* +]*//' | grep -Fx "
|
|
323
|
+
merged=$(git branch --merged main 2>/dev/null | sed 's/^[* +]*//' | grep -Fx "$branch" || true)
|
|
283
324
|
[ -n "$merged" ] && echo "Merged: yes" || echo "Merged: no"
|
|
284
325
|
else
|
|
285
326
|
echo "Branch: not found"
|
package/package.json
CHANGED
|
@@ -6,15 +6,17 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
6
6
|
|
|
7
7
|
1. **Load tools:** Call tool_search_load_tool_namespace with paths ["workflow_launch"].
|
|
8
8
|
|
|
9
|
-
2. **
|
|
9
|
+
2. **Load the workflow registry:** Read the file "{crewHome}/workflows/registry.json" with the read tool and parse it as JSON. If the file does not exist (the live release predates the registry), proceed without it — omit the `registry` arg and the dispatcher will load the registry the slow way and log a warning.
|
|
10
|
+
|
|
11
|
+
3. **Run the dispatcher:** Call workflow_launch with scriptPath "{crewHome}/workflows/crew-dispatch.js" and args {"dashboardSlug": "{dashboardSlug}", "crewHome": "{crewHome}", "registry": <parsed registry JSON, or omit the key when the file was missing>}.
|
|
10
12
|
|
|
11
13
|
Wait for it to complete. It reads the board, determines eligibility, claims tasks, acknowledges the poll, and returns structured results.
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
4. **Report claims:** Extract the `claims` array from the dispatcher result. For each claim, emit one line:
|
|
14
16
|
LAUNCH: script={claim.scriptPath} args={JSON.stringify(claim.args)}
|
|
15
17
|
|
|
16
18
|
If the dispatcher returned no claims or the claims array is empty, report: NO_DISPATCH
|
|
17
19
|
|
|
18
20
|
Do NOT call workflow_launch_async for any task workflow. The main chat agent does that.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
5. Exit silently.
|
package/seed/workflows/bugfix.md
CHANGED
|
@@ -6,7 +6,11 @@ The full software engineering workflow. Use for bug reports where reproduction m
|
|
|
6
6
|
|
|
7
7
|
### Triage
|
|
8
8
|
**Identity:** Sage
|
|
9
|
-
Validate, prioritize, connect dependencies, assign this workflow. If the task needs decomposition, break it into subtasks first.
|
|
9
|
+
Validate, prioritize, connect dependencies, assign this workflow. If the task needs decomposition, break it into subtasks first. Flags the task experiential when it changes anything rendered and visible in the artifact.
|
|
10
|
+
|
|
11
|
+
### Capture
|
|
12
|
+
**Identity:** Hazel
|
|
13
|
+
Baseline evidence for experiential tasks only — skipped otherwise. The capture itself is parent-driven; the workflow records a baseline request and parks for the parent protocol (`docs/visual-verdict.md`). After two requests the task continues with `baseline: none`.
|
|
10
14
|
|
|
11
15
|
### Reproduce
|
|
12
16
|
**Identity:** Hazel
|
|
@@ -15,7 +19,7 @@ Validate, prioritize, connect dependencies, assign this workflow. If the task ne
|
|
|
15
19
|
|
|
16
20
|
### Map
|
|
17
21
|
**Identity:** Mara
|
|
18
|
-
Update the task with a solution-oriented spec. Research options, pick the shortest path, write it clearly enough that the builder doesn't need to ask questions.
|
|
22
|
+
Update the task with a solution-oriented spec. Research options, pick the shortest path, write it clearly enough that the builder doesn't need to ask questions. For experiential tasks, the spec cannot be written without baseline evidence (or an explicit `baseline: none`) — without it Map bounces back to Capture.
|
|
19
23
|
|
|
20
24
|
### Build
|
|
21
25
|
**Identity:** Wren
|
|
@@ -40,3 +44,4 @@ Optional. Ships the merged code to the project's publish target (`deploy_type` o
|
|
|
40
44
|
**Personas:** Yes — Hazel wears a persona costume to test from that perspective.
|
|
41
45
|
**Constraint:** No code context. Same tools and process as reproduction. Pass or fail. File follow-up tasks for related issues found during testing.
|
|
42
46
|
Fail if public-affecting changes lack public docs. Public docs are not code — Hazel reads them as a user would.
|
|
47
|
+
**Visual verdict:** for experiential artifact tasks, the QA agent covers the mechanical checks only — the visual verdict is owned by the parent after the rendered post-change inspection results arrive (`docs/visual-verdict.md`). No experiential task completes without a recorded visual PASS.
|
package/seed/workflows/chore.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# Chore
|
|
2
2
|
|
|
3
|
-
For routine maintenance and simple tasks. Like Standard but without QA — the change is low-risk enough to ship after review.
|
|
3
|
+
For routine maintenance and simple tasks. Like Standard but without QA — the change is low-risk enough to ship after review. Capture keeps its baseline pass for experiential tasks, but there is no visual verdict and no QA gate here.
|
|
4
4
|
|
|
5
5
|
## Steps
|
|
6
6
|
|
|
7
7
|
### Triage
|
|
8
8
|
**Identity:** Sage
|
|
9
|
-
Validate, prioritize, connect dependencies, assign this workflow.
|
|
9
|
+
Validate, prioritize, connect dependencies, assign this workflow. Flags the task experiential when it changes anything rendered and visible in the artifact.
|
|
10
|
+
|
|
11
|
+
### Capture
|
|
12
|
+
**Identity:** Hazel
|
|
13
|
+
Baseline evidence for experiential tasks only — skipped otherwise. The capture itself is parent-driven; the workflow records a baseline request and parks for the parent protocol (`docs/visual-verdict.md`). After two requests the task continues with `baseline: none`.
|
|
10
14
|
|
|
11
15
|
### Map
|
|
12
16
|
**Identity:** Mara
|
|
13
|
-
Research options, pick the path, write the spec.
|
|
17
|
+
Research options, pick the path, write the spec. For experiential tasks, the spec cannot be written without baseline evidence (or an explicit `baseline: none`) — without it Map bounces back to Capture.
|
|
14
18
|
|
|
15
19
|
### Build
|
|
16
20
|
**Identity:** Wren
|
|
@@ -6,11 +6,15 @@ The default workflow for feature work and improvements. Like Bugfix but without
|
|
|
6
6
|
|
|
7
7
|
### Triage
|
|
8
8
|
**Identity:** Sage
|
|
9
|
-
Validate, prioritize, connect dependencies, assign this workflow.
|
|
9
|
+
Validate, prioritize, connect dependencies, assign this workflow. Flags the task experiential when it changes anything rendered and visible in the artifact.
|
|
10
|
+
|
|
11
|
+
### Capture
|
|
12
|
+
**Identity:** Hazel
|
|
13
|
+
Baseline evidence for experiential tasks only — skipped otherwise. The capture itself is parent-driven; the workflow records a baseline request and parks for the parent protocol (`docs/visual-verdict.md`). After two requests the task continues with `baseline: none` and final QA judges on the rubric alone.
|
|
10
14
|
|
|
11
15
|
### Map
|
|
12
16
|
**Identity:** Mara
|
|
13
|
-
Research options, pick the path, write the spec.
|
|
17
|
+
Research options, pick the path, write the spec. For experiential tasks, the spec cannot be written without baseline evidence (or an explicit `baseline: none`) — without it Map bounces back to Capture.
|
|
14
18
|
|
|
15
19
|
### Build
|
|
16
20
|
**Identity:** Wren
|
|
@@ -35,3 +39,4 @@ Optional. Ships the merged code to the project's publish target (`deploy_type` o
|
|
|
35
39
|
**Personas:** Yes — Hazel wears a persona costume to test from that perspective.
|
|
36
40
|
**Constraint:** No code context. Pass or fail. File follow-up tasks for related issues found during testing.
|
|
37
41
|
Fail if public-affecting changes lack public docs. Public docs are not code — Hazel reads them as a user would.
|
|
42
|
+
**Visual verdict:** for experiential artifact tasks, the QA agent covers the mechanical checks only — the visual verdict is owned by the parent after the rendered post-change inspection results arrive (`docs/visual-verdict.md`). No experiential task completes without a recorded visual PASS.
|
package/workflows/AGENTS.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Executable Muse workflow scripts (JavaScript). These are what the workflow runtime actually runs.
|
|
4
4
|
|
|
5
|
-
- `crew-dispatch.js` — reads the board,
|
|
5
|
+
- `crew-dispatch.js` — reads the board, recommends eligible tasks, returns structured launch records (the launched workflow self-claims; the dispatcher never writes claims)
|
|
6
6
|
- `crew-init.js` — sets up a new crew instance: orchestration folders, dashboard, cron, sample project
|
|
7
|
-
- `standard.js` — default task workflow: Triage → Map → Build → Review → Integrate → Publish → QA
|
|
8
|
-
- `bugfix.js` — adds
|
|
9
|
-
- `chore.js` — drops QA (low-risk)
|
|
7
|
+
- `standard.js` — default task workflow: Triage → Capture → Map → Build → Review → Integrate → Publish → QA
|
|
8
|
+
- `bugfix.js` — adds Capture after Triage, then Reproduce
|
|
9
|
+
- `chore.js` — adds Capture after Triage, drops QA (low-risk)
|
|
10
10
|
- `docs.js` — Tate writes, Cass reviews
|
|
11
11
|
|
|
12
12
|
The `.js` scripts here are distinct from the `.md` definitions in `seed/workflows/` and `.orchestration/workflows/`. The `.md` files describe phases and prompts; these `.js` files execute them.
|