muse-crew 0.4.0 → 0.4.2
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 +1 -1
- package/lib/AGENTS.md +1 -0
- package/lib/orphan-sweep.sh +98 -25
- package/lib/test-orphan-sweep.sh +109 -0
- package/package.json +2 -2
- package/workflows/bugfix.js +8 -2
- package/workflows/chore.js +4 -1
- package/workflows/crew-init.js +6 -4
- package/workflows/standard.js +8 -2
package/docs/guide.md
CHANGED
|
@@ -18,7 +18,7 @@ This is the full setup and operations reference. If you're new, start with the [
|
|
|
18
18
|
- An **`.orchestration/` directory** in the crew home with identities, personas, workflow docs, and feedback conventions.
|
|
19
19
|
- A **project registration** — the task service registered as its own first project.
|
|
20
20
|
- A **polling loop** — checks for work every 3 minutes via Muse's scheduling, even when nobody's in the conversation.
|
|
21
|
-
- An **orphan sweep** — runs every 30 minutes to clean merged worktrees and break stale merge locks.
|
|
21
|
+
- An **orphan sweep** — runs every 30 minutes to clean merged worktrees and break stale merge locks. It never touches worktrees belonging to tasks with a running dashboard session.
|
|
22
22
|
|
|
23
23
|
The agent running in the main chat receives the dispatcher's claims and launches each task workflow. Workflows can't launch workflows, so this handoff is structural.
|
|
24
24
|
|
package/lib/AGENTS.md
CHANGED
|
@@ -6,3 +6,4 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
6
6
|
- `merge-lock.sh` — serialized merge lock for concurrent agents; records owner PID
|
|
7
7
|
- `worktree-lifecycle.sh` — git worktree create/cleanup for isolated agent work
|
|
8
8
|
- `orphan-sweep.sh` — find and clean stale worktrees and merge locks
|
|
9
|
+
- `test-orphan-sweep.sh` — regression tests for orphan-sweep.sh (active-run guard, verified removal, fail-closed)
|
package/lib/orphan-sweep.sh
CHANGED
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
5
|
# orphan-sweep.sh report — list orphans (read-only for worktrees);
|
|
6
|
-
#
|
|
6
|
+
# stale locks with dead PIDs are released
|
|
7
7
|
# orphan-sweep.sh clean — remove safe-to-clean orphans (merged branches only)
|
|
8
|
-
# and
|
|
8
|
+
# and release stale merge locks
|
|
9
|
+
#
|
|
10
|
+
# Active-run knowledge is injected by the caller, never fetched here:
|
|
11
|
+
# CREW_ACTIVE_TASKS — space-separated task IDs with a running session
|
|
12
|
+
# CREW_ACTIVE_TASKS_FILE — path to a file with one task ID per line
|
|
13
|
+
# (blank lines and '#' comments are ignored)
|
|
14
|
+
# Both inputs feed one active set, matched exactly against worktree dir names.
|
|
15
|
+
# A task in the active set is never touched: its worktree and any merge lock
|
|
16
|
+
# it holds are skipped regardless of merge status, lock age, or PID liveness.
|
|
17
|
+
#
|
|
18
|
+
# Fail closed: clean mode without either input refuses to remove anything
|
|
19
|
+
# and exits 2. Report mode without either input still runs read-only but
|
|
20
|
+
# prints a banner noting the ACTIVE checks were skipped.
|
|
21
|
+
#
|
|
22
|
+
# Callers (the sweep cron body; worktree-lifecycle.sh cmd_sweep is a
|
|
23
|
+
# passthrough) must export CREW_ACTIVE_TASKS.
|
|
9
24
|
#
|
|
10
25
|
# "Safe to clean" means the task branch is fully merged into main.
|
|
11
26
|
# Dirty or unmerged worktrees are always preserved and reported.
|
|
@@ -19,33 +34,74 @@ STALE_LOCK_MIN=30
|
|
|
19
34
|
|
|
20
35
|
cmd="${1:-report}"
|
|
21
36
|
found=0
|
|
37
|
+
any_failed=0
|
|
38
|
+
|
|
39
|
+
# --- Active-run set (union of both inputs) ---
|
|
40
|
+
have_active_data=0
|
|
41
|
+
ACTIVE_TASKS=""
|
|
42
|
+
if [ -n "${CREW_ACTIVE_TASKS+set}" ]; then
|
|
43
|
+
have_active_data=1
|
|
44
|
+
ACTIVE_TASKS="${ACTIVE_TASKS} ${CREW_ACTIVE_TASKS}"
|
|
45
|
+
fi
|
|
46
|
+
if [ -n "${CREW_ACTIVE_TASKS_FILE+set}" ]; then
|
|
47
|
+
if [ -r "${CREW_ACTIVE_TASKS_FILE}" ]; then
|
|
48
|
+
have_active_data=1
|
|
49
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
50
|
+
case "$line" in ''|\#*) continue ;; esac
|
|
51
|
+
ACTIVE_TASKS="${ACTIVE_TASKS} ${line}"
|
|
52
|
+
done < "${CREW_ACTIVE_TASKS_FILE}"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
task_is_active() {
|
|
57
|
+
[ -n "${1:-}" ] || return 1
|
|
58
|
+
case " ${ACTIVE_TASKS} " in
|
|
59
|
+
*" $1 "*) return 0 ;;
|
|
60
|
+
*) return 1 ;;
|
|
61
|
+
esac
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if [ "$cmd" = "clean" ] && [ "$have_active_data" -eq 0 ]; then
|
|
65
|
+
echo "BLOCKED: clean mode requires the active-run list (set CREW_ACTIVE_TASKS or CREW_ACTIVE_TASKS_FILE) — refusing to remove anything"
|
|
66
|
+
exit 2
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [ "$cmd" != "clean" ] && [ "$have_active_data" -eq 0 ]; then
|
|
70
|
+
echo "NOTE: no active-run data provided; ACTIVE checks skipped"
|
|
71
|
+
fi
|
|
22
72
|
|
|
23
73
|
# --- Stale merge lock ---
|
|
24
74
|
if [ -f "$LOCK_FILE" ]; then
|
|
25
75
|
lock_holder=$(cut -d' ' -f1 "$LOCK_FILE" 2>/dev/null || echo "unknown")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
76
|
+
if task_is_active "$lock_holder"; then
|
|
77
|
+
echo "ACTIVE_LOCK: held by $lock_holder (active run on dashboard) — skipping"
|
|
78
|
+
found=1
|
|
79
|
+
else
|
|
80
|
+
lock_time=$(cut -d' ' -f2 "$LOCK_FILE" 2>/dev/null || echo "")
|
|
81
|
+
lock_pid=$(awk '{print $3}' "$LOCK_FILE" 2>/dev/null || echo "")
|
|
82
|
+
if [ -n "$lock_time" ]; then
|
|
83
|
+
lock_epoch=$(date -d "$lock_time" +%s 2>/dev/null || echo 0)
|
|
84
|
+
now_epoch=$(date -u +%s)
|
|
85
|
+
age_min=$(( (now_epoch - lock_epoch) / 60 ))
|
|
86
|
+
if [ "$age_min" -gt "$STALE_LOCK_MIN" ]; then
|
|
87
|
+
# Check if owner process is still alive
|
|
88
|
+
pid_alive=0
|
|
89
|
+
if [ -n "$lock_pid" ] && [ "$lock_pid" != "-" ] && kill -0 "$lock_pid" 2>/dev/null; then
|
|
90
|
+
pid_alive=1
|
|
91
|
+
fi
|
|
38
92
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
else
|
|
42
|
-
echo "STALE_LOCK: held ${age_min}m by $lock_holder (pid ${lock_pid:-none} dead) — threshold ${STALE_LOCK_MIN}m"
|
|
43
|
-
found=1
|
|
44
|
-
rm -f "$LOCK_FILE"
|
|
45
|
-
if [ ! -f "$LOCK_FILE" ]; then
|
|
46
|
-
echo " → released (dead PID)"
|
|
93
|
+
if [ "$pid_alive" -eq 1 ]; then
|
|
94
|
+
echo "ACTIVE_LOCK: held ${age_min}m by $lock_holder (pid $lock_pid alive) — skipping"
|
|
47
95
|
else
|
|
48
|
-
echo "
|
|
96
|
+
echo "STALE_LOCK: held ${age_min}m by $lock_holder (pid ${lock_pid:-none} dead) — threshold ${STALE_LOCK_MIN}m"
|
|
97
|
+
found=1
|
|
98
|
+
rm -f "$LOCK_FILE"
|
|
99
|
+
if [ ! -f "$LOCK_FILE" ]; then
|
|
100
|
+
echo " → released (dead PID)"
|
|
101
|
+
else
|
|
102
|
+
echo " → FAILED: lock file still present after rm"
|
|
103
|
+
any_failed=1
|
|
104
|
+
fi
|
|
49
105
|
fi
|
|
50
106
|
fi
|
|
51
107
|
fi
|
|
@@ -67,19 +123,31 @@ if [ -d "$WORKTREE_DIR" ]; then
|
|
|
67
123
|
|
|
68
124
|
branch="task/$task_id"
|
|
69
125
|
|
|
126
|
+
found=1
|
|
127
|
+
|
|
128
|
+
# Active runs are never touched, regardless of merge status.
|
|
129
|
+
if task_is_active "$task_id"; then
|
|
130
|
+
echo "ACTIVE: $task_id — dashboard shows a running session, skipped"
|
|
131
|
+
continue
|
|
132
|
+
fi
|
|
133
|
+
|
|
70
134
|
# Is the branch merged into main?
|
|
71
135
|
merged=$(git branch --merged main 2>/dev/null | sed 's/^[* +]*//' | grep -Fx "$branch" || true)
|
|
72
136
|
|
|
73
137
|
# Is the worktree dirty?
|
|
74
138
|
dirty=$(cd "$wt" && git status --porcelain 2>/dev/null | wc -l)
|
|
75
139
|
|
|
76
|
-
found=1
|
|
77
140
|
if [ -n "$merged" ]; then
|
|
78
141
|
echo "MERGED: $task_id — branch merged into main, safe to remove"
|
|
79
142
|
if [ "$cmd" = "clean" ]; then
|
|
80
143
|
git worktree remove "$wt" 2>/dev/null || true
|
|
81
144
|
git branch -d "$branch" 2>/dev/null || true
|
|
82
|
-
|
|
145
|
+
if [ ! -d "$wt" ] && ! git show-ref --verify --quiet "refs/heads/$branch"; then
|
|
146
|
+
echo " → removed"
|
|
147
|
+
else
|
|
148
|
+
echo " → FAILED: removal attempted but worktree/branch still present, left in place"
|
|
149
|
+
any_failed=1
|
|
150
|
+
fi
|
|
83
151
|
fi
|
|
84
152
|
elif [ "$dirty" -gt 0 ]; then
|
|
85
153
|
echo "DIRTY: $task_id — $dirty uncommitted changes, preserved"
|
|
@@ -92,3 +160,8 @@ fi
|
|
|
92
160
|
if [ "$found" -eq 0 ]; then
|
|
93
161
|
echo "CLEAN: no orphans, no stale locks"
|
|
94
162
|
fi
|
|
163
|
+
|
|
164
|
+
if [ "$any_failed" -eq 1 ]; then
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
exit 0
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-orphan-sweep.sh — regression tests for orphan-sweep.sh
|
|
3
|
+
#
|
|
4
|
+
# Self-contained: builds throwaway git fixtures in a temp dir and drives
|
|
5
|
+
# lib/orphan-sweep.sh through the CREW_ACTIVE_TASKS / CREW_ACTIVE_TASKS_FILE
|
|
6
|
+
# seam (no network, no dashboard). Exits 0 only if every case passes.
|
|
7
|
+
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
SWEEP="$SCRIPT_DIR/orphan-sweep.sh"
|
|
12
|
+
|
|
13
|
+
pass=0
|
|
14
|
+
fail=0
|
|
15
|
+
ok() { echo "PASS: $1"; pass=$((pass + 1)); }
|
|
16
|
+
no() { echo "FAIL: $1"; fail=$((fail + 1)); }
|
|
17
|
+
|
|
18
|
+
TMPBASE="$(mktemp -d)"
|
|
19
|
+
trap 'rm -rf "$TMPBASE"' EXIT
|
|
20
|
+
|
|
21
|
+
# new_fixture <name> — fresh git repo, one commit on main; exports CREW_REPO.
|
|
22
|
+
new_fixture() {
|
|
23
|
+
local dir="$TMPBASE/$1"
|
|
24
|
+
mkdir -p "$dir"
|
|
25
|
+
git init -q -b main "$dir" >/dev/null
|
|
26
|
+
git -C "$dir" config user.email "test@example.com"
|
|
27
|
+
git -C "$dir" config user.name "test"
|
|
28
|
+
echo base > "$dir/seed.txt"
|
|
29
|
+
git -C "$dir" add seed.txt
|
|
30
|
+
git -C "$dir" commit -qm "initial"
|
|
31
|
+
export CREW_REPO="$dir"
|
|
32
|
+
cd "$dir" || exit 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# merged_worktree <task_id> — worktree whose branch gets a real merge commit.
|
|
36
|
+
merged_worktree() {
|
|
37
|
+
local t="$1"
|
|
38
|
+
git worktree add -q ".worktrees/$t" -b "task/$t" >/dev/null
|
|
39
|
+
echo "$t" > ".worktrees/$t/work.txt"
|
|
40
|
+
git -C ".worktrees/$t" add work.txt
|
|
41
|
+
git -C ".worktrees/$t" commit -qm "work for $t"
|
|
42
|
+
git merge -q --no-ff "task/$t" -m "merge $t" >/dev/null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# --- Case 1: live run protected (the incident's exact case) ---
|
|
46
|
+
# T1's branch tip equals main (no commits — "merged" by equality), clean tree.
|
|
47
|
+
new_fixture case1
|
|
48
|
+
git worktree add -q .worktrees/T1 -b task/T1 >/dev/null
|
|
49
|
+
out=$(CREW_ACTIVE_TASKS="T1" "$SWEEP" clean 2>&1); code=$?
|
|
50
|
+
[ "$code" -eq 0 ] && ok "1: clean exits 0 with only an active run present" || no "1: exit $code, want 0"
|
|
51
|
+
[ -d .worktrees/T1 ] && ok "1: active worktree dir preserved" || no "1: active worktree dir removed"
|
|
52
|
+
git show-ref --verify --quiet refs/heads/task/T1 && ok "1: active branch preserved" || no "1: active branch deleted"
|
|
53
|
+
echo "$out" | grep -q "ACTIVE: T1" && ok "1: ACTIVE line printed" || no "1: no ACTIVE line in output"
|
|
54
|
+
|
|
55
|
+
# --- Case 2: dead run cleaned ---
|
|
56
|
+
new_fixture case2
|
|
57
|
+
merged_worktree T2
|
|
58
|
+
out=$(CREW_ACTIVE_TASKS="" "$SWEEP" clean 2>&1); code=$?
|
|
59
|
+
[ "$code" -eq 0 ] && ok "2: clean exits 0" || no "2: exit $code, want 0"
|
|
60
|
+
[ ! -d .worktrees/T2 ] && ok "2: merged worktree dir removed" || no "2: merged worktree dir still present"
|
|
61
|
+
git show-ref --verify --quiet refs/heads/task/T2 && no "2: merged branch still present" || ok "2: merged branch deleted"
|
|
62
|
+
echo "$out" | grep -q "→ removed" && ok "2: removal reported" || no "2: no removal line in output"
|
|
63
|
+
|
|
64
|
+
# --- Case 3: fail closed ---
|
|
65
|
+
new_fixture case3
|
|
66
|
+
merged_worktree T3
|
|
67
|
+
out=$(env -u CREW_ACTIVE_TASKS -u CREW_ACTIVE_TASKS_FILE "$SWEEP" clean 2>&1); code=$?
|
|
68
|
+
[ "$code" -eq 2 ] && ok "3: clean without active data exits 2" || no "3: exit $code, want 2"
|
|
69
|
+
echo "$out" | grep -q "BLOCKED" && ok "3: BLOCKED line printed" || no "3: no BLOCKED line in output"
|
|
70
|
+
[ -d .worktrees/T3 ] && ok "3: worktree left alone (dir)" || no "3: worktree removed despite BLOCKED"
|
|
71
|
+
git show-ref --verify --quiet refs/heads/task/T3 && ok "3: worktree left alone (branch)" || no "3: branch deleted despite BLOCKED"
|
|
72
|
+
|
|
73
|
+
# --- Case 4: false report impossible (forced removal failure via worktree lock) ---
|
|
74
|
+
new_fixture case4
|
|
75
|
+
merged_worktree T4
|
|
76
|
+
git worktree lock .worktrees/T4
|
|
77
|
+
out=$(CREW_ACTIVE_TASKS="" "$SWEEP" clean 2>&1); code=$?
|
|
78
|
+
[ "$code" -eq 1 ] && ok "4: clean with failed removal exits 1" || no "4: exit $code, want 1"
|
|
79
|
+
echo "$out" | grep -q "→ FAILED" && ok "4: FAILED line printed (no false 'removed')" || no "4: no FAILED line — false report!"
|
|
80
|
+
[ -d .worktrees/T4 ] && ok "4: failed worktree left in place" || no "4: worktree gone despite failed removal"
|
|
81
|
+
git worktree unlock .worktrees/T4 2>/dev/null || true
|
|
82
|
+
|
|
83
|
+
# --- Case 5: lock path ---
|
|
84
|
+
new_fixture case5
|
|
85
|
+
mkdir -p .worktrees
|
|
86
|
+
printf 'T5 2020-01-01T00:00:00Z 999999\n' > .worktrees/.merge-lock
|
|
87
|
+
out=$(CREW_ACTIVE_TASKS="" "$SWEEP" clean 2>&1); code=$?
|
|
88
|
+
[ ! -f .worktrees/.merge-lock ] && ok "5a: stale lock released" || no "5a: stale lock file still present"
|
|
89
|
+
echo "$out" | grep -q "released (dead PID)" && ok "5a: release reported" || no "5a: no release line in output"
|
|
90
|
+
[ "$code" -eq 0 ] && ok "5a: exit 0" || no "5a: exit $code, want 0"
|
|
91
|
+
|
|
92
|
+
printf 'T6 2020-01-01T00:00:00Z 999999\n' > .worktrees/.merge-lock
|
|
93
|
+
out=$(CREW_ACTIVE_TASKS="T6" "$SWEEP" clean 2>&1); code=$?
|
|
94
|
+
[ -f .worktrees/.merge-lock ] && ok "5b: active holder's lock not released" || no "5b: active holder's lock was released"
|
|
95
|
+
echo "$out" | grep -q "ACTIVE_LOCK" && ok "5b: ACTIVE_LOCK line printed" || no "5b: no ACTIVE_LOCK line in output"
|
|
96
|
+
[ "$code" -eq 0 ] && ok "5b: exit 0" || no "5b: exit $code, want 0"
|
|
97
|
+
|
|
98
|
+
# --- Case 6: report mode without active data ---
|
|
99
|
+
new_fixture case6
|
|
100
|
+
merged_worktree T7
|
|
101
|
+
out=$(env -u CREW_ACTIVE_TASKS -u CREW_ACTIVE_TASKS_FILE "$SWEEP" report 2>&1); code=$?
|
|
102
|
+
[ "$code" -eq 0 ] && ok "6: report exits 0" || no "6: exit $code, want 0"
|
|
103
|
+
echo "$out" | grep -q "NOTE: no active-run data provided; ACTIVE checks skipped" && ok "6: NOTE banner printed" || no "6: no NOTE banner in output"
|
|
104
|
+
[ -d .worktrees/T7 ] && ok "6: report mode removes nothing" || no "6: report mode removed a worktree"
|
|
105
|
+
echo "$out" | grep -q "MERGED: T7" && ok "6: MERGED line still listed" || no "6: MERGED line missing from report"
|
|
106
|
+
|
|
107
|
+
echo ""
|
|
108
|
+
echo "== $pass passed, $fail failed =="
|
|
109
|
+
[ "$fail" -eq 0 ]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muse-crew",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Opinionated orchestration for Muse
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Opinionated orchestration for Muse \u2014 workflows, identities, and tooling for autonomous software development.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"repository": {
|
package/workflows/bugfix.js
CHANGED
|
@@ -320,6 +320,7 @@ while (i < STEPS.length) {
|
|
|
320
320
|
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
321
321
|
"If NOT_FOUND, use the local package.json version as the base instead.\n\n" +
|
|
322
322
|
"STEP 3: Apply the version_bump scope (" + releaseDecision.version_bump + ") to the base version: patch increments the last segment; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0. Example: base 1.2.3 + minor → 1.3.0. Call the result <new-version>.\n" +
|
|
323
|
+
"STEP 3B: Make the version math auditable. State the computed target version explicitly — your summary MUST include the line: TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version> (example: TARGET_VERSION=0.3.1 computed as 0.3.0 + patch → 0.3.1). Every later step uses exactly this version. Do not improvise the arithmetic: 0.3.0 + patch is 0.3.1, never 0.4.0.\n" +
|
|
323
324
|
"STEP 4: Write <new-version> into package.json (only the `version` field), then commit it under the still-held merge lock: cd " + REPO_PATH + " && git add package.json && git commit -m \"release: muse-crew@<new-version>\". The lock serializes Publish per repo, so two tasks can never pick the same version.\n" +
|
|
324
325
|
"STEP 5: Pack and publish.\n" +
|
|
325
326
|
"Run: cd " + REPO_PATH + " && npm pack\n" +
|
|
@@ -333,7 +334,9 @@ while (i < STEPS.length) {
|
|
|
333
334
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
334
335
|
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
335
336
|
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
336
|
-
"End your summary with exactly this
|
|
337
|
+
"End your summary with exactly these two lines, in this order — lowercase, no trailing period, do not rephrase (the QA backstop extracts them by pattern):\n" +
|
|
338
|
+
"TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version>\n" +
|
|
339
|
+
"published: muse-crew@<new-version>\n\n" +
|
|
337
340
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
338
341
|
"No prose, no markdown, just the JSON object.";
|
|
339
342
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -374,7 +377,10 @@ while (i < STEPS.length) {
|
|
|
374
377
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
375
378
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
376
379
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
377
|
-
? "NPM PUBLISH CHECK: the accepted Build summary 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
|
|
380
|
+
? "NPM PUBLISH CHECK: the accepted Build summary 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" +
|
|
381
|
+
"Extract the line matching TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version>. If the line is missing, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: Publish summary did not echo its computed target version (STEP 3B)\" }.\n" +
|
|
382
|
+
"Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if the Publish agent applied a different scope, 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, FAIL.\n" +
|
|
383
|
+
"Extract the published version from the notes line matching published: muse-crew@<version> (match case-insensitively and ignore any trailing period — agents sometimes rephrase it). 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, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: [details]\" }.\n"
|
|
378
384
|
: "";
|
|
379
385
|
instructions = "Final QA testing. You are CODE-BLIND — do NOT read source code.\n" +
|
|
380
386
|
"Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n" +
|
package/workflows/chore.js
CHANGED
|
@@ -302,6 +302,7 @@ while (i < STEPS.length) {
|
|
|
302
302
|
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
303
303
|
"If NOT_FOUND, use the local package.json version as the base instead.\n\n" +
|
|
304
304
|
"STEP 3: Apply the version_bump scope (" + releaseDecision.version_bump + ") to the base version: patch increments the last segment; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0. Example: base 1.2.3 + minor → 1.3.0. Call the result <new-version>.\n" +
|
|
305
|
+
"STEP 3B: Make the version math auditable. State the computed target version explicitly — your summary MUST include the line: TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version> (example: TARGET_VERSION=0.3.1 computed as 0.3.0 + patch → 0.3.1). Every later step uses exactly this version. Do not improvise the arithmetic: 0.3.0 + patch is 0.3.1, never 0.4.0.\n" +
|
|
305
306
|
"STEP 4: Write <new-version> into package.json (only the `version` field), then commit it under the still-held merge lock: cd " + REPO_PATH + " && git add package.json && git commit -m \"release: muse-crew@<new-version>\". The lock serializes Publish per repo, so two tasks can never pick the same version.\n" +
|
|
306
307
|
"STEP 5: Pack and publish.\n" +
|
|
307
308
|
"Run: cd " + REPO_PATH + " && npm pack\n" +
|
|
@@ -315,7 +316,9 @@ while (i < STEPS.length) {
|
|
|
315
316
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
316
317
|
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
317
318
|
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
318
|
-
"End your summary with exactly this
|
|
319
|
+
"End your summary with exactly these two lines, in this order — lowercase, no trailing period, do not rephrase (the QA backstop extracts them by pattern):\n" +
|
|
320
|
+
"TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version>\n" +
|
|
321
|
+
"published: muse-crew@<new-version>\n\n" +
|
|
319
322
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
320
323
|
"No prose, no markdown, just the JSON object.";
|
|
321
324
|
} else if (PUBLISH_TYPE === "artifact") {
|
package/workflows/crew-init.js
CHANGED
|
@@ -251,10 +251,12 @@ try {
|
|
|
251
251
|
"Body text for the cron:\n" +
|
|
252
252
|
"---\n" +
|
|
253
253
|
"## Muse Crew Orphan Sweep\n\n" +
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
|
|
257
|
-
"
|
|
254
|
+
"1. Call artifact_invoke_action on slug \"" + dashboardSlug + "\", action \"getdispatchstate\", args {}.\n" +
|
|
255
|
+
"2. Collect the task IDs of every entry in ready_tasks whose latest_session.status is \"running\".\n" +
|
|
256
|
+
" (Sessions older than 1h are already reclassified as timed_out by the dashboard — no extra math.)\n" +
|
|
257
|
+
"3. Run: CREW_ACTIVE_TASKS=\"<space-separated ids>\" " + crewHome + "/lib/orphan-sweep.sh clean\n" +
|
|
258
|
+
" If the list is empty, pass an empty string — do NOT omit the variable (the sweep fails closed without it).\n" +
|
|
259
|
+
"4. Report the output. If it says CLEAN, no action was needed.\n" +
|
|
258
260
|
"---\n\n" +
|
|
259
261
|
"Return JSON with existed (boolean).",
|
|
260
262
|
{
|
package/workflows/standard.js
CHANGED
|
@@ -318,6 +318,7 @@ while (i < STEPS.length) {
|
|
|
318
318
|
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
319
319
|
"If NOT_FOUND, use the local package.json version as the base instead.\n\n" +
|
|
320
320
|
"STEP 3: Apply the version_bump scope (" + releaseDecision.version_bump + ") to the base version: patch increments the last segment; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0. Example: base 1.2.3 + minor → 1.3.0. Call the result <new-version>.\n" +
|
|
321
|
+
"STEP 3B: Make the version math auditable. State the computed target version explicitly — your summary MUST include the line: TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version> (example: TARGET_VERSION=0.3.1 computed as 0.3.0 + patch → 0.3.1). Every later step uses exactly this version. Do not improvise the arithmetic: 0.3.0 + patch is 0.3.1, never 0.4.0.\n" +
|
|
321
322
|
"STEP 4: Write <new-version> into package.json (only the `version` field), then commit it under the still-held merge lock: cd " + REPO_PATH + " && git add package.json && git commit -m \"release: muse-crew@<new-version>\". The lock serializes Publish per repo, so two tasks can never pick the same version.\n" +
|
|
322
323
|
"STEP 5: Pack and publish.\n" +
|
|
323
324
|
"Run: cd " + REPO_PATH + " && npm pack\n" +
|
|
@@ -331,7 +332,9 @@ while (i < STEPS.length) {
|
|
|
331
332
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
332
333
|
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
333
334
|
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
334
|
-
"End your summary with exactly this
|
|
335
|
+
"End your summary with exactly these two lines, in this order — lowercase, no trailing period, do not rephrase (the QA backstop extracts them by pattern):\n" +
|
|
336
|
+
"TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version>\n" +
|
|
337
|
+
"published: muse-crew@<new-version>\n\n" +
|
|
335
338
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
336
339
|
"No prose, no markdown, just the JSON object.";
|
|
337
340
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -372,7 +375,10 @@ while (i < STEPS.length) {
|
|
|
372
375
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
373
376
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
374
377
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
375
|
-
? "NPM PUBLISH CHECK: the accepted Build summary 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
|
|
378
|
+
? "NPM PUBLISH CHECK: the accepted Build summary 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" +
|
|
379
|
+
"Extract the line matching TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version>. If the line is missing, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: Publish summary did not echo its computed target version (STEP 3B)\" }.\n" +
|
|
380
|
+
"Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if the Publish agent applied a different scope, 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, FAIL.\n" +
|
|
381
|
+
"Extract the published version from the notes line matching published: muse-crew@<version> (match case-insensitively and ignore any trailing period — agents sometimes rephrase it). 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, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: [details]\" }.\n"
|
|
376
382
|
: "";
|
|
377
383
|
if (PUBLISH_TYPE === "artifact") {
|
|
378
384
|
var safeDesc = taskDescription.replace(/"/g, "'").replace(/\\/g, "\\\\").slice(0, 500);
|