muse-crew 0.6.6 → 0.6.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/AGENTS.md +1 -1
- package/API.md +53 -9
- package/docs/guide.md +9 -6
- package/docs/publish-verification.md +205 -0
- package/lib/AGENTS.md +5 -5
- package/lib/crew-api.js +1101 -0
- package/lib/crew-release.sh +19 -0
- package/lib/merge-lock.sh +18 -5
- package/lib/publish-npm.sh +6 -5
- package/lib/schema.sql +101 -0
- package/lib/test-merge-lock.sh +13 -0
- package/lib/test-publish-skip.sh +2 -1
- package/lib/test-terminal-cleanup.sh +131 -0
- package/lib/test-verify-merge.sh +1 -0
- package/lib/test-worktree-backend.sh +74 -0
- package/lib/worktree-lifecycle.sh +164 -16
- package/package.json +2 -2
- package/seed/AGENTS.md +0 -1
- package/seed/crons.json +0 -13
- package/workflows/AGENTS.md +4 -2
- package/workflows/bugfix.js +987 -193
- package/workflows/chore.js +936 -177
- package/workflows/crew-dispatch.js +201 -37
- package/workflows/crew-init.js +82 -54
- package/workflows/docs.js +117 -48
- package/workflows/standard.js +970 -195
- package/workflows/tests/read-board-parse.test.mjs +162 -0
- package/lib/orphan-sweep.sh +0 -287
- package/lib/test-orphan-sweep.sh +0 -276
- package/seed/cron-sweep-body-template.md +0 -11
|
@@ -17,27 +17,49 @@
|
|
|
17
17
|
# integrate <task_id> <commit_msg> — merge lock → merge into main → report commit
|
|
18
18
|
# verify-merge <task_id> — confirm the task branch tip is an ancestor of main (post-step merge check)
|
|
19
19
|
# post-deploy <task_id> — commit builder artifacts, cleanup, release lock
|
|
20
|
+
# terminal-cleanup <task_id> — park/fail boundary: release lock (always), reclaim worktree+branch only when fully merged
|
|
20
21
|
# cleanup <task_id> — remove worktree + branch (no lock)
|
|
21
22
|
# status <task_id> — report worktree state
|
|
22
23
|
# refresh-lock <task_id> — refresh merge lock timestamp (resets staleness clock)
|
|
23
24
|
# lock-status <task_id> — report merge lock state: UNLOCKED, or key=value lock status (locked=true, task_id, holder, acquired_at, lease_seconds, age_seconds, remaining_seconds)
|
|
24
|
-
#
|
|
25
|
+
# merge-record <task_id> — print $CREW_HOME/.merge-records/<task_id>, or NO_RECORD
|
|
25
26
|
#
|
|
26
27
|
# Exit codes:
|
|
27
28
|
# 0 — success
|
|
28
29
|
# 1 — validation/usage error
|
|
29
|
-
# 2 — merge lock held by another task
|
|
30
|
+
# 2 — merge lock held by another task; also BLOCKED on unset
|
|
31
|
+
# CREW_HOME/CREW_REPO (prints a BLOCKED line, never proceeds silently)
|
|
30
32
|
# 3 — merge conflict
|
|
31
33
|
# 4 — dirty main (cannot merge)
|
|
32
34
|
|
|
33
35
|
set -euo pipefail
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
# Fail closed: no stale dev-path defaults. Every caller (workflows thread
|
|
38
|
+
# both through LIFECYCLE_ENV) provides them. An unset CREW_REPO would
|
|
39
|
+
# prepare/integrate in the wrong checkout; an unset CREW_HOME would read
|
|
40
|
+
# the wrong instance's pins and audit log.
|
|
41
|
+
if [ -z "${CREW_REPO:-}" ]; then
|
|
42
|
+
echo "BLOCKED: CREW_REPO is not set — refusing to run (a default path would operate on the wrong repo)"
|
|
43
|
+
exit 2
|
|
44
|
+
fi
|
|
45
|
+
if [ -z "${CREW_HOME:-}" ]; then
|
|
46
|
+
echo "BLOCKED: CREW_HOME is not set — refusing to run (a default path would read the wrong instance's state)"
|
|
47
|
+
exit 2
|
|
48
|
+
fi
|
|
49
|
+
REPO="$CREW_REPO"
|
|
50
|
+
export CREW_REPO # internal merge-lock.sh calls inherit the repo being worked on
|
|
36
51
|
WORKTREE_DIR="$REPO/.worktrees"
|
|
37
52
|
REGISTRY_DIR="$WORKTREE_DIR/.registry"
|
|
38
|
-
LIB_DIR="${CREW_LIB:-$
|
|
53
|
+
LIB_DIR="${CREW_LIB:-$CREW_HOME/lib}"
|
|
39
54
|
MERGE_LOCK="$LIB_DIR/merge-lock.sh"
|
|
40
|
-
|
|
55
|
+
|
|
56
|
+
# Merge records: $CREW_HOME/.merge-records/<task_id> (key=value, append-only,
|
|
57
|
+
# on persistent disk — same durability class as $CREW_HOME/.pins/<task>).
|
|
58
|
+
# Written by cmd_integrate (post-reconcile HEAD). Read by cmd_verify_merge
|
|
59
|
+
# (branch-reclamation fallback) and the workflow. Consumers trust a record
|
|
60
|
+
# only after verifying `git merge-base --is-ancestor <merge_commit> main` —
|
|
61
|
+
# a stale record is inert. Removed by cmd_post_deploy (consumed).
|
|
62
|
+
MERGE_RECORDS_DIR="$CREW_HOME/.merge-records"
|
|
41
63
|
|
|
42
64
|
# --- helpers ---
|
|
43
65
|
|
|
@@ -245,9 +267,41 @@ cmd_integrate() {
|
|
|
245
267
|
exit 3
|
|
246
268
|
fi
|
|
247
269
|
|
|
270
|
+
# Reconcile with the remote while the merge lock is still held. Another
|
|
271
|
+
# writer (a second crew instance, direct pushes) can land commits on
|
|
272
|
+
# origin/main outside this lock; pushing a stale main then fails
|
|
273
|
+
# non-fast-forward and the Integrate phase fails closed. Fetch and merge
|
|
274
|
+
# origin/main now so the agent's subsequent push is a fast-forward.
|
|
275
|
+
# Fail-soft when there is no remote; fail closed on a real conflict.
|
|
276
|
+
if git rev-parse --verify "origin/main" >/dev/null 2>&1; then
|
|
277
|
+
if git fetch origin main 2>&1; then
|
|
278
|
+
if ! git merge --no-edit "origin/main" -m "merge: reconcile with origin/main before push ($task_id)" 2>&1; then
|
|
279
|
+
echo "CONFLICT: origin/main diverged with conflicting changes — aborting"
|
|
280
|
+
git merge --abort 2>/dev/null || true
|
|
281
|
+
exit 3
|
|
282
|
+
fi
|
|
283
|
+
echo "RECONCILED: merged origin/main into main"
|
|
284
|
+
else
|
|
285
|
+
echo "WARNING: git fetch origin failed — pushing without remote reconcile; the push step may report non-fast-forward"
|
|
286
|
+
fi
|
|
287
|
+
else
|
|
288
|
+
echo "NO_REMOTE: no origin/main — skipping remote reconcile"
|
|
289
|
+
fi
|
|
290
|
+
|
|
248
291
|
local merged_commit
|
|
249
292
|
merged_commit=$(git rev-parse HEAD)
|
|
250
293
|
echo "MERGED: $merged_commit"
|
|
294
|
+
|
|
295
|
+
# Record the merge commit (merge-lease fix, 2026-09-12): if the branch is
|
|
296
|
+
# later reclaimed (e.g. under an expired lease), verify-merge can still
|
|
297
|
+
# confirm the merge landed from this record. Append-only, on persistent disk.
|
|
298
|
+
mkdir -p "$MERGE_RECORDS_DIR"
|
|
299
|
+
{
|
|
300
|
+
echo "merge_commit=$merged_commit"
|
|
301
|
+
echo "merged_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
302
|
+
} >> "$MERGE_RECORDS_DIR/$task_id"
|
|
303
|
+
echo "RECORDED: merge commit $merged_commit in $MERGE_RECORDS_DIR/$task_id"
|
|
304
|
+
|
|
251
305
|
echo "LOCK: held by $task_id — release after deploy with post-deploy"
|
|
252
306
|
}
|
|
253
307
|
|
|
@@ -264,8 +318,21 @@ cmd_verify_merge() {
|
|
|
264
318
|
|
|
265
319
|
cd "$REPO"
|
|
266
320
|
|
|
321
|
+
# If the branch is gone, fall back to the merge record (merge-lease fix,
|
|
322
|
+
# 2026-09-12): the branch may have been reclaimed under an expired lease
|
|
323
|
+
# after the merge landed. The record is trusted only if the recorded
|
|
324
|
+
# commit is actually an ancestor of main.
|
|
267
325
|
if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
268
|
-
|
|
326
|
+
local record_file="$MERGE_RECORDS_DIR/$task_id"
|
|
327
|
+
if [ -f "$record_file" ]; then
|
|
328
|
+
local recorded_commit
|
|
329
|
+
recorded_commit=$(grep "^merge_commit=" "$record_file" | tail -1 | cut -d= -f2)
|
|
330
|
+
if [ -n "$recorded_commit" ] && git merge-base --is-ancestor "$recorded_commit" main 2>/dev/null; then
|
|
331
|
+
echo "VERIFIED: branch $branch gone but recorded merge commit $recorded_commit is an ancestor of main — merge landed"
|
|
332
|
+
return 0
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
echo "ERROR: branch $branch does not exist (and no valid merge record)"
|
|
269
336
|
return 1
|
|
270
337
|
fi
|
|
271
338
|
|
|
@@ -305,10 +372,14 @@ cmd_post_deploy() {
|
|
|
305
372
|
local final_commit
|
|
306
373
|
final_commit=$(git rev-parse HEAD)
|
|
307
374
|
|
|
308
|
-
# Cleanup worktree + branch
|
|
375
|
+
# Cleanup worktree + branch — report honestly: a failed removal must
|
|
376
|
+
# never print success (a lying "removed" hid real leftovers).
|
|
309
377
|
if [ -d "$WORKTREE_DIR/$task_id" ]; then
|
|
310
|
-
git worktree remove "$WORKTREE_DIR/$task_id" 2>/dev/null
|
|
311
|
-
|
|
378
|
+
if git worktree remove "$WORKTREE_DIR/$task_id" 2>/dev/null; then
|
|
379
|
+
echo "WORKTREE: removed"
|
|
380
|
+
else
|
|
381
|
+
echo "WORKTREE: remove-failed"
|
|
382
|
+
fi
|
|
312
383
|
fi
|
|
313
384
|
git worktree prune 2>/dev/null
|
|
314
385
|
git branch -d "task/$task_id" 2>/dev/null && echo "BRANCH: deleted" || true
|
|
@@ -317,9 +388,79 @@ cmd_post_deploy() {
|
|
|
317
388
|
# Release merge lock
|
|
318
389
|
"$MERGE_LOCK" release "$task_id" 2>/dev/null && echo "LOCK: released" || true
|
|
319
390
|
|
|
391
|
+
# Consume the merge record — the merge is now deployed
|
|
392
|
+
rm -f "$MERGE_RECORDS_DIR/$task_id"
|
|
393
|
+
|
|
320
394
|
echo "DEPLOYED: $final_commit"
|
|
321
395
|
}
|
|
322
396
|
|
|
397
|
+
# Terminal cleanup: the run's last act at every park/fail boundary. The
|
|
398
|
+
# merge lock is a shared resource, so its release is unconditional and
|
|
399
|
+
# forgiving (only the holder can release; anyone else is a no-op). The
|
|
400
|
+
# worktree and branch are reclaimed ONLY when the task branch is fully
|
|
401
|
+
# merged into main — then they are redundant and safe to drop. Anything
|
|
402
|
+
# else is preserved for the human by design: parked work awaiting
|
|
403
|
+
# judgment must survive its run. Never force-removes a dirty worktree:
|
|
404
|
+
# uncommitted work is reported loudly, never silently destroyed.
|
|
405
|
+
cmd_terminal_cleanup() {
|
|
406
|
+
local task_id="$1"
|
|
407
|
+
validate_task_id "$task_id"
|
|
408
|
+
|
|
409
|
+
cd "$REPO"
|
|
410
|
+
|
|
411
|
+
if "$MERGE_LOCK" release "$task_id" 2>/dev/null; then
|
|
412
|
+
echo "LOCK: released"
|
|
413
|
+
else
|
|
414
|
+
echo "LOCK: not-held"
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
local branch
|
|
418
|
+
branch=$(resolve_branch "$task_id")
|
|
419
|
+
if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
420
|
+
echo "BRANCH: absent — nothing to reclaim"
|
|
421
|
+
echo "CLEANED: false"
|
|
422
|
+
return 0
|
|
423
|
+
fi
|
|
424
|
+
if ! git merge-base --is-ancestor "$branch" main 2>/dev/null; then
|
|
425
|
+
local ahead
|
|
426
|
+
ahead=$(git rev-list --count "main..$branch" 2>/dev/null || echo "?")
|
|
427
|
+
echo "PRESERVED: $branch holds $ahead unmerged commit(s) — left for human"
|
|
428
|
+
echo "CLEANED: false"
|
|
429
|
+
return 0
|
|
430
|
+
fi
|
|
431
|
+
local wt_path
|
|
432
|
+
wt_path=$(resolve_path "$task_id")
|
|
433
|
+
local cleaned=true
|
|
434
|
+
if [ -d "$wt_path" ]; then
|
|
435
|
+
if git worktree remove "$wt_path" 2>/dev/null; then
|
|
436
|
+
echo "WORKTREE: removed"
|
|
437
|
+
else
|
|
438
|
+
echo "WORKTREE: remove-failed (left in place)"
|
|
439
|
+
cleaned=false
|
|
440
|
+
fi
|
|
441
|
+
else
|
|
442
|
+
echo "WORKTREE: not-found"
|
|
443
|
+
fi
|
|
444
|
+
git worktree prune 2>/dev/null || true
|
|
445
|
+
if git branch -d "$branch" 2>/dev/null; then
|
|
446
|
+
echo "BRANCH: deleted"
|
|
447
|
+
else
|
|
448
|
+
echo "BRANCH: delete-failed"
|
|
449
|
+
cleaned=false
|
|
450
|
+
fi
|
|
451
|
+
# The registry is the source of truth for task→branch/path: it is
|
|
452
|
+
# unregistered only when the reclamation actually completed. A partial
|
|
453
|
+
# cleanup keeps the mapping so the leftovers stay visible.
|
|
454
|
+
if [ "$cleaned" = true ]; then
|
|
455
|
+
unregister "$task_id"
|
|
456
|
+
echo "REGISTRY: unregistered"
|
|
457
|
+
echo "CLEANED: true"
|
|
458
|
+
else
|
|
459
|
+
echo "REGISTRY: kept (cleanup incomplete)"
|
|
460
|
+
echo "CLEANED: false"
|
|
461
|
+
fi
|
|
462
|
+
}
|
|
463
|
+
|
|
323
464
|
cmd_cleanup() {
|
|
324
465
|
local task_id="$1"
|
|
325
466
|
validate_task_id "$task_id"
|
|
@@ -393,11 +534,6 @@ cmd_status() {
|
|
|
393
534
|
"$MERGE_LOCK" status 2>/dev/null || true
|
|
394
535
|
}
|
|
395
536
|
|
|
396
|
-
cmd_sweep() {
|
|
397
|
-
local mode="${1:-report}"
|
|
398
|
-
"$ORPHAN_SWEEP" "$mode"
|
|
399
|
-
}
|
|
400
|
-
|
|
401
537
|
cmd_refresh_lock() {
|
|
402
538
|
local task_id="$1"
|
|
403
539
|
validate_task_id "$task_id"
|
|
@@ -414,6 +550,17 @@ cmd_lock_status() {
|
|
|
414
550
|
"$MERGE_LOCK" status
|
|
415
551
|
}
|
|
416
552
|
|
|
553
|
+
cmd_merge_record() {
|
|
554
|
+
local task_id="$1"
|
|
555
|
+
validate_task_id "$task_id"
|
|
556
|
+
local record_file="$MERGE_RECORDS_DIR/$task_id"
|
|
557
|
+
if [ -f "$record_file" ]; then
|
|
558
|
+
cat "$record_file"
|
|
559
|
+
else
|
|
560
|
+
echo "NO_RECORD"
|
|
561
|
+
fi
|
|
562
|
+
}
|
|
563
|
+
|
|
417
564
|
# --- dispatch ---
|
|
418
565
|
|
|
419
566
|
cmd="${1:-}"
|
|
@@ -426,14 +573,15 @@ case "$cmd" in
|
|
|
426
573
|
integrate) cmd_integrate "${1:?task_id required}" "${2:-}" ;;
|
|
427
574
|
verify-merge) cmd_verify_merge "${1:?task_id required}" ;;
|
|
428
575
|
post-deploy) cmd_post_deploy "${1:?task_id required}" ;;
|
|
576
|
+
terminal-cleanup) cmd_terminal_cleanup "${1:?task_id required}" ;;
|
|
429
577
|
cleanup) cmd_cleanup "${1:?task_id required}" ;;
|
|
430
578
|
status) cmd_status "${1:?task_id required}" ;;
|
|
431
579
|
refresh-lock) cmd_refresh_lock "${1:?task_id required}" ;;
|
|
432
580
|
lock-status) cmd_lock_status "${1:?task_id required}" ;;
|
|
433
|
-
|
|
581
|
+
merge-record) cmd_merge_record "${1:?task_id required}" ;;
|
|
434
582
|
*)
|
|
435
583
|
echo "Usage: worktree-lifecycle.sh <command> [args]"
|
|
436
|
-
echo "Commands: validate, prepare, inspect, integrate, verify-merge, post-deploy, cleanup, status, refresh-lock, lock-status,
|
|
584
|
+
echo "Commands: validate, prepare, inspect, integrate, verify-merge, post-deploy, terminal-cleanup, cleanup, status, refresh-lock, lock-status, merge-record"
|
|
437
585
|
exit 1
|
|
438
586
|
;;
|
|
439
587
|
esac
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muse-crew",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Opinionated orchestration for Muse
|
|
3
|
+
"version": "0.6.7",
|
|
4
|
+
"description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"repository": {
|
package/seed/AGENTS.md
CHANGED
|
@@ -4,7 +4,6 @@ Init source data. Everything `crew-init.js` reads when setting up a new crew ins
|
|
|
4
4
|
|
|
5
5
|
- `crons.json` — the declarative cron manifest; crew-init creates/updates each entry idempotently; `enabled` is a creation-time default only
|
|
6
6
|
- `cron-body-template.md` — template for creating the dispatch cron job
|
|
7
|
-
- `cron-sweep-body-template.md` — template for creating the orphan-sweep cron job
|
|
8
7
|
- `posture.md` — PM posture that shapes the user's assistant for crew interaction
|
|
9
8
|
- `feedback/` — feedback feature placeholder
|
|
10
9
|
- `workflows/` — human-readable workflow definitions, copied to `$CREW_HOME/.orchestration/workflows/` during init
|
package/seed/crons.json
CHANGED
|
@@ -12,19 +12,6 @@
|
|
|
12
12
|
},
|
|
13
13
|
"timeout_secs": 120,
|
|
14
14
|
"title": "Muse Crew polling loop"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"body_template": "cron-sweep-body-template.md",
|
|
18
|
-
"enabled": false,
|
|
19
|
-
"id": "crew-orphan-sweep",
|
|
20
|
-
"mode": "task",
|
|
21
|
-
"owner": "space:{dashboardSlug}",
|
|
22
|
-
"schedule": {
|
|
23
|
-
"every": "30m",
|
|
24
|
-
"kind": "interval"
|
|
25
|
-
},
|
|
26
|
-
"timeout_secs": 120,
|
|
27
|
-
"title": "Muse Crew orphan sweep"
|
|
28
15
|
}
|
|
29
16
|
],
|
|
30
17
|
"version": 1
|
package/workflows/AGENTS.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
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, recommends eligible tasks, returns structured launch records (the launched workflow self-claims; the dispatcher never writes claims)
|
|
6
|
-
|
|
5
|
+
- `crew-dispatch.js` — reads the board, recommends eligible tasks, returns structured launch records (the launched workflow self-claims; the dispatcher never writes claims). Skips tasks on quiesced projects and on projects with no repo_path configured.
|
|
6
|
+
|
|
7
|
+
All four workflow scripts share byte-identical transport helpers (`workRetryKey`, `buildTransportRetryTrailer`, `describeWorkAgentFailure`, `workerMissingArtifactTools` — pinned by `tests/closeout.test.js`). The transport-retry loop treats a worker report naming the missing artifact tool namespace (bug 3472bf36, a per-launch platform flake) as a retryable attempt with a fresh launch rather than accepting a useless report.
|
|
8
|
+
- `crew-init.js` — sets up a new crew instance: Gate 0 validates crewHome is workspace-contained and a valid git repo; bootstraps the release system; scaffolds orchestration folders; creates/converges cron jobs from the declarative manifest. Idempotent. The crew owns its state via the Crew API (lib/crew-api.js); the dashboard is a pure client and is never a dependency.
|
|
7
9
|
- `standard.js` — default task workflow: Triage → Capture → Map → Build → Review → Integrate → Publish → QA
|
|
8
10
|
- `bugfix.js` — adds Capture after Triage, then Reproduce
|
|
9
11
|
- `chore.js` — adds Capture after Triage, drops QA (low-risk)
|