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.
@@ -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
- # sweep [report|clean] find/clean orphans + stale locks
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
- REPO="${CREW_REPO:-$HOME/workspace/ts-spaces/orchestra-dashboard}"
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:-$HOME/workspace/.jarvis/lib}"
53
+ LIB_DIR="${CREW_LIB:-$CREW_HOME/lib}"
39
54
  MERGE_LOCK="$LIB_DIR/merge-lock.sh"
40
- ORPHAN_SWEEP="$LIB_DIR/orphan-sweep.sh"
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
- echo "ERROR: branch $branch does not exist"
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 || true
311
- echo "WORKTREE: removed"
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
- sweep) cmd_sweep "${1:-report}" ;;
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, sweep"
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.6",
4
- "description": "Opinionated orchestration for Muse \u2014 workflows, identities, and tooling for autonomous software development.",
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
@@ -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
- - `crew-init.js` — sets up a new crew instance: orchestration folders, dashboard, cron, sample project
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)