muse-crew 0.7.19 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,17 +2,22 @@
2
2
  // verify-publish.js — deterministic parent publish verifier.
3
3
  //
4
4
  // The parent never eyeballs an inspection report. This script takes the
5
- // async read-back inspection's result JSON and MECHANICALLY decides the
6
- // verdict: it parses the inspector's machine-readable findings block,
7
- // compares every added/removed diff line against the reported
8
- // present/absent verdicts (with a mechanically computed collision
9
- // exemption for removed lines that also occur in untouched code),
10
- // checks supersession via git, and only then stamps provenance, logs
11
- // the terminal event, and re-queues the task.
5
+ // read-back inspection's result JSON and MECHANICALLY decides the verdict:
6
+ // it parses the machine-readable findings block, compares every
7
+ // added/removed diff line against the reported present/absent verdicts
8
+ // (with mechanically computed collision exemptions for lines that also
9
+ // occur in untouched code and therefore have zero discriminating power),
10
+ // checks supersession via git, and only then stamps provenance, logs the
11
+ // terminal event, and re-queues the task.
12
12
  //
13
- // The LLM is the sensor (it reads the artifact source); this code is the
14
- // judge. Unparseable findings, content mismatches, supersession, and stamp
15
- // failures all fail CLOSED with a terminal parent verdict — never a stamp.
13
+ // The sensor is the deterministic lib/readback-disk.js (it reads the
14
+ // on-disk tree the artifact is built/served from); this code is the judge.
15
+ // The LLM inspector driven by lib/build-readback-request.js is retained
16
+ // only as the manual fallback for environments without disk access.
17
+ // Unparseable findings, content mismatches, unverifiable changes (binary
18
+ // files, mode-only changes, renames, fully-colliding hunks), supersession,
19
+ // and stamp failures all fail CLOSED with a terminal parent verdict —
20
+ // never a stamp.
16
21
  //
17
22
  // Usage:
18
23
  // node verify-publish.js --crew-home <path> --task-id <uuid>
@@ -177,13 +182,15 @@ try {
177
182
  } catch (e) {
178
183
  terminal("read-back-unavailable", `git diff failed: ${e.message}`);
179
184
  }
180
- const expected = new Map(); // path -> { added: [], removed: [] }
185
+ const expected = new Map(); // path -> { added: [], removed: [], isBinary: bool }
181
186
  let curFile = null;
182
187
  for (const line of diff.split("\n")) {
183
188
  if (line.startsWith("diff --git")) {
184
189
  const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
185
190
  curFile = m ? m[2] : "unknown";
186
- expected.set(curFile, { added: [], removed: [] });
191
+ expected.set(curFile, { added: [], removed: [], isBinary: false });
192
+ } else if (curFile && line.startsWith("Binary files ")) {
193
+ expected.get(curFile).isBinary = true;
187
194
  } else if (curFile && line.startsWith("+") && !line.startsWith("+++")) {
188
195
  expected.get(curFile).added.push(line.slice(1));
189
196
  } else if (curFile && line.startsWith("-") && !line.startsWith("---")) {
@@ -257,11 +264,42 @@ let exempted = 0;
257
264
  for (const [path, exp] of expected) {
258
265
  const found = findings.get(path);
259
266
  if (!found) terminal("content-mismatch", `no findings for changed file ${path}`);
267
+ // (2026-09-16, critic finding 1) Binary files, mode-only changes, and
268
+ // renames produce zero added/removed lines: the loops below would
269
+ // iterate over empty arrays and the stamp would issue with "all added
270
+ // lines PRESENT, all removed lines ABSENT" — vacuously true, content
271
+ // never read. The line-based judge cannot verify these changes, so they
272
+ // fail closed as unverifiable (never stamped); a human verifies.
273
+ if (exp.added.length === 0 && exp.removed.length === 0) {
274
+ terminal("unverifiable-content",
275
+ `${exp.isBinary ? "binary file" : "no content lines (mode-only change or rename)"} ${path}: ` +
276
+ "the diff carries no added/removed lines for this file, so the line-based read-back checked nothing — " +
277
+ "provenance NOT stamped; human verification needed");
278
+ }
279
+ // (2026-09-16, critic finding 5) Added-line collisions: an added line
280
+ // that already occurs verbatim in the old tree is reported PRESENT
281
+ // whether or not the new hunk actually landed — zero discriminating
282
+ // power (the sensor is membership-only, not count-sensitive). Exempt
283
+ // such lines from the pass criteria, symmetric to the removed-side
284
+ // exemption below — and require at least one discriminating added line
285
+ // per file, or the added-side check is vacuous (finding 1's class).
286
+ let discriminatingAdded = 0;
260
287
  for (const line of exp.added) {
261
288
  const v = found.added.get(line);
262
289
  if (v === undefined) terminal("unreadable-result", `no ADDED finding for line in ${path}: ${line.slice(0, 60)}`);
290
+ if (oldCount(path, line) > 0) {
291
+ exempted += 1; // colliding pre-existing line: zero signal, cannot fail a good publish
292
+ continue;
293
+ }
294
+ discriminatingAdded += 1;
263
295
  if (v !== "PRESENT") terminal("content-mismatch", `added line ABSENT in ${path}: ${line.slice(0, 80)}`);
264
296
  }
297
+ if (exp.added.length > 0 && discriminatingAdded === 0) {
298
+ terminal("unverifiable-content",
299
+ `all ${exp.added.length} added line(s) in ${path} already occur in the old tree: ` +
300
+ "their PRESENT findings cannot tell \"hunk landed\" from \"hunk dropped\" — " +
301
+ "provenance NOT stamped; human verification needed");
302
+ }
265
303
  // The diff may remove the same line more than once; the old tree must
266
304
  // account for every removal before a line counts as a collision.
267
305
  const removedBudget = new Map();
@@ -316,7 +354,7 @@ if (!p || p.source_commit !== commit || p.task_id !== taskId || p.crew_release !
316
354
  // --- 8. Terminal verified event + re-queue -----------------------------------
317
355
  const verifiedMsg =
318
356
  `publish: verified ${commit} (${inspectionId}) — read-back: all added lines PRESENT, all removed lines ABSENT` +
319
- (exempted > 0 ? ` (${exempted} colliding removed line${exempted === 1 ? "" : "s"} exempted)` : "") +
357
+ (exempted > 0 ? ` (${exempted} colliding line${exempted === 1 ? "" : "s"} exempted as non-discriminating)` : "") +
320
358
  `; ${buildNote}; no supersession (HEAD=${commit}).`;
321
359
  api("log-event", { task_id: taskId, type: "note", message: verifiedMsg.slice(0, 1000) });
322
360
  api("update-task", { id: taskId, state: "in_progress" });
@@ -6,13 +6,18 @@
6
6
  # worktree strategy lives — workflows and agents never run raw git worktree
7
7
  # commands; they call these lifecycle commands instead.
8
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.
9
+ # The crew's registry ($REPO/.worktrees/.registry/<task_id>) is the first
10
+ # source of truth for task→branch/path. When the registry is empty (a
11
+ # Build agent created the branch with raw git from an abbreviated id,
12
+ # bypassing prepare — task 4e1a1bba, 2026-09-17), resolve_branch() falls
13
+ # back to enumerating refs: the canonical task/<full-id> ref wins, then a
14
+ # task/<id-prefix> ref is tolerated (matched by strict prefix, never
15
+ # created). Canonical stays task/<full-id>; ambiguous prefixes fail closed.
12
16
  #
13
17
  # Commands:
14
18
  # validate <task_id> — check task ID is safe for branches/paths
15
19
  # prepare <task_id> — create worktree + branch, register ownership
20
+ # resolve-branch <task_id> — print the resolved task branch name
16
21
  # inspect <task_id> — diff against main for review
17
22
  # integrate <task_id> <commit_msg> — merge lock → merge into main → report commit
18
23
  # verify-merge <task_id> — confirm the task branch tip is an ancestor of main (post-step merge check)
@@ -63,15 +68,47 @@ MERGE_RECORDS_DIR="$CREW_HOME/.merge-records"
63
68
 
64
69
  # --- helpers ---
65
70
 
66
- # Task branch name: task/<id>. The registry wins when the task was
67
- # prepared before.
71
+ # Task branch name. Resolution order (task 4e1a1bba, 2026-09-17):
72
+ # 1. the registry — prepare writes the branch it created;
73
+ # 2. the canonical task/<full-id> ref, when it exists;
74
+ # 3. a task/<id-prefix> ref, matched by strict literal prefix over
75
+ # refs/heads/task/ — tolerated, never created. Build and Integrate
76
+ # therefore agree on the branch name even when Build bypassed
77
+ # prepare with an abbreviated id.
78
+ # When nothing matches, the canonical name is returned anyway so callers
79
+ # keep their "branch X does not exist" failure (and verify-merge's merge-
80
+ # record fallback) instead of failing on an empty name. Ambiguous prefixes
81
+ # fail closed (exit 1).
68
82
  resolve_branch() {
69
83
  local task_id="$1" b
70
84
  if [ -f "$REGISTRY_DIR/$task_id" ]; then
71
85
  b=$(grep '^branch=' "$REGISTRY_DIR/$task_id" | cut -d= -f2-)
72
86
  if [ -n "$b" ]; then printf '%s' "$b"; return 0; fi
73
87
  fi
74
- printf 'task/%s' "$task_id"
88
+ local canonical="task/$task_id"
89
+ if git -C "$REPO" rev-parse --verify "$canonical" >/dev/null 2>&1; then
90
+ printf '%s' "$canonical"; return 0
91
+ fi
92
+ local matches=() ref stripped
93
+ while IFS= read -r ref; do
94
+ [ -n "$ref" ] || continue
95
+ [ "$ref" = "$canonical" ] && continue
96
+ # Strict literal prefix: ref must be a proper prefix of the canonical
97
+ # name. The quoted expansion keeps the pattern literal (task ids are
98
+ # [a-zA-Z0-9_-], but literal is literal).
99
+ stripped="${canonical#"$ref"}"
100
+ if [ "$stripped" != "$canonical" ]; then
101
+ matches+=("$ref")
102
+ fi
103
+ done < <(git -C "$REPO" for-each-ref --format='%(refname:short)' 'refs/heads/task/')
104
+ if [ "${#matches[@]}" -eq 1 ]; then
105
+ printf '%s' "${matches[0]}"; return 0
106
+ fi
107
+ if [ "${#matches[@]}" -gt 1 ]; then
108
+ echo "ERROR: task id '$task_id' matches multiple task branches: ${matches[*]} — ambiguous, refusing to guess" >&2
109
+ return 1
110
+ fi
111
+ printf '%s' "$canonical"
75
112
  }
76
113
 
77
114
  # Worktree path: $REPO/.worktrees/<id>. The registry wins when the task
@@ -168,6 +205,17 @@ cmd_prepare() {
168
205
  echo "BASE: $base_commit"
169
206
  }
170
207
 
208
+ # Agent-side one-liner: $(worktree-lifecycle.sh resolve-branch <id>) prints
209
+ # the branch Build and Integrate must agree on. Mechanical — no agent call
210
+ # per phase needed (task 4e1a1bba). Exits 1 on an ambiguous prefix.
211
+ cmd_resolve_branch() {
212
+ local task_id="$1"
213
+ validate_task_id "$task_id"
214
+ local branch
215
+ branch=$(resolve_branch "$task_id")
216
+ echo "$branch"
217
+ }
218
+
171
219
  cmd_inspect() {
172
220
  local task_id="$1"
173
221
  validate_task_id "$task_id"
@@ -373,7 +421,12 @@ cmd_post_deploy() {
373
421
  final_commit=$(git rev-parse HEAD)
374
422
 
375
423
  # Cleanup worktree + branch — report honestly: a failed removal must
376
- # never print success (a lying "removed" hid real leftovers).
424
+ # never print success (a lying "removed" hid real leftovers). The branch
425
+ # is resolved, never reconstructed: a Build agent may have created it
426
+ # from an abbreviated id (task 4e1a1bba) and deleting "task/<full-id>"
427
+ # would leave the real branch behind.
428
+ local branch
429
+ branch=$(resolve_branch "$task_id")
377
430
  if [ -d "$WORKTREE_DIR/$task_id" ]; then
378
431
  if git worktree remove "$WORKTREE_DIR/$task_id" 2>/dev/null; then
379
432
  echo "WORKTREE: removed"
@@ -382,7 +435,7 @@ cmd_post_deploy() {
382
435
  fi
383
436
  fi
384
437
  git worktree prune 2>/dev/null
385
- git branch -d "task/$task_id" 2>/dev/null && echo "BRANCH: deleted" || true
438
+ git branch -d "$branch" 2>/dev/null && echo "BRANCH: deleted" || true
386
439
  unregister "$task_id"
387
440
 
388
441
  # Release merge lock
@@ -480,7 +533,11 @@ cmd_cleanup() {
480
533
  echo "WORKTREE: not found (already clean)"
481
534
  fi
482
535
  git worktree prune 2>/dev/null
483
- git branch -D "task/$task_id" 2>/dev/null && echo "BRANCH: deleted" || true
536
+ # Resolve, never reconstruct: the branch may live under an abbreviated
537
+ # id (task 4e1a1bba).
538
+ local branch
539
+ branch=$(resolve_branch "$task_id")
540
+ git branch -D "$branch" 2>/dev/null && echo "BRANCH: deleted" || true
484
541
  unregister "$task_id"
485
542
  echo "CLEANUP: done"
486
543
  }
@@ -569,6 +626,7 @@ shift || true
569
626
  case "$cmd" in
570
627
  validate) cmd_validate "${1:?task_id required}" ;;
571
628
  prepare) cmd_prepare "${1:?task_id required}" ;;
629
+ resolve-branch) cmd_resolve_branch "${1:?task_id required}" ;;
572
630
  inspect) cmd_inspect "${1:?task_id required}" ;;
573
631
  integrate) cmd_integrate "${1:?task_id required}" "${2:-}" ;;
574
632
  verify-merge) cmd_verify_merge "${1:?task_id required}" ;;
@@ -581,7 +639,7 @@ case "$cmd" in
581
639
  merge-record) cmd_merge_record "${1:?task_id required}" ;;
582
640
  *)
583
641
  echo "Usage: worktree-lifecycle.sh <command> [args]"
584
- echo "Commands: validate, prepare, inspect, integrate, verify-merge, post-deploy, terminal-cleanup, cleanup, status, refresh-lock, lock-status, merge-record"
642
+ echo "Commands: validate, prepare, resolve-branch, inspect, integrate, verify-merge, post-deploy, terminal-cleanup, cleanup, status, refresh-lock, lock-status, merge-record"
585
643
  exit 1
586
644
  ;;
587
645
  esac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.7.19",
3
+ "version": "0.8.0",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
package/seed/AGENTS.md CHANGED
@@ -4,6 +4,7 @@ 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-body-update-watch.md` — template for the daily `crew-update-watch` cron job: runs `lib/update-watch.js --crew-home {crewHome}` verbatim and reports stdout
7
8
  - `posture.md` — PM posture that shapes the user's assistant for crew interaction
8
9
  - `feedback/` — feedback feature placeholder
9
10
  - `workflows/` — human-readable workflow definitions, copied to `$CREW_HOME/.orchestration/workflows/` during init
@@ -0,0 +1,13 @@
1
+ ## Muse Crew update watch
2
+
3
+ You are the update watcher. Run the deterministic check and report its output.
4
+
5
+ ### Steps
6
+
7
+ 1. Run verbatim:
8
+ node {crewHome}/current/lib/update-watch.js --crew-home {crewHome}
9
+ The script exits 0 on every path. Check failures are logged to
10
+ {crewHome}/update-watch.log and reported on stdout — never thrown.
11
+ 2. Report the script's stdout verbatim. Do not summarize, do not judge, do
12
+ not file tasks yourself — the watcher files through the Crew API when
13
+ policy and idempotency allow.
package/seed/crons.json CHANGED
@@ -5,13 +5,26 @@
5
5
  "enabled": true,
6
6
  "id": "crew-poll",
7
7
  "mode": "task",
8
- "owner": "space:{dashboardSlug}",
8
+ "owner": "cli:{instanceId}",
9
9
  "schedule": {
10
10
  "every": "15m",
11
11
  "kind": "interval"
12
12
  },
13
13
  "timeout_secs": 5400,
14
14
  "title": "Muse Crew polling loop"
15
+ },
16
+ {
17
+ "body_template": "cron-body-update-watch.md",
18
+ "enabled": true,
19
+ "id": "crew-update-watch",
20
+ "mode": "task",
21
+ "owner": "cli:{instanceId}",
22
+ "schedule": {
23
+ "every": "24h",
24
+ "kind": "interval"
25
+ },
26
+ "timeout_secs": 600,
27
+ "title": "Muse Crew automatic update watcher"
15
28
  }
16
29
  ],
17
30
  "version": 1
@@ -0,0 +1,21 @@
1
+ # Upgrade
2
+
3
+ Upgrade the crew itself through its own dispatch loop. The task description carries a `source:` line naming the upgrade source — `repo` (default, the task project's repo HEAD) or `npm@<x.y.z>` (the published npm package). The stable `crew-release.sh` deploys, the workflow verifies mechanically, and handover to the new release is automatic on the next tick.
4
+
5
+ ## Steps
6
+
7
+ ### Triage
8
+ **Identity:** Sage
9
+ Validate the `source:` line (exactly `repo`, or `npm@<x.y.z>` — anything else parks fail-closed), verify the repo checkout and resolve `git rev-parse HEAD` for repo source, read the live release via `crew-release.sh current`, and compute the target. If the crew is already on the target, record the idempotent no-op and skip Deploy.
10
+
11
+ ### Deploy
12
+ **Identity:** Wren
13
+ Run the stable `$CREW_HOME/crew-release.sh deploy` — against the repo HEAD for repo source, or against a staging dir under `$CREW_HOME/.upgrade-staging/<version>` (public npm install only) for npm source. Single command, no merge lock. Any non-zero exit parks fail-closed with the exact output; no retry, no auto-rollback — `crew-release.sh rollback` is the human recovery path.
14
+
15
+ ### Verify
16
+ **Identity:** Wren
17
+ Mechanical, no LLM judgment: (1) `crew-release.sh current` equals the target; (2) `<crewHome>/workflows/registry.json` parses as JSON — and for repo source carries the `upgrade` key (npm releases predating the upgrade workflow are still valid upgrades); (3) `crew-api get-state` works via the pinned old-release CLI. Handover is automatic: the next poll tick launches the dispatcher through the `current` symlink, i.e. the new release. In-flight runs finish on the old release via per-task lib pinning.
18
+
19
+ ## Safety
20
+
21
+ Never mutate the crew repo — repo source always means the repo's current HEAD. Never publish to npm — the npm source only installs the published package. Never touch the scheduler. Fail closed everywhere.
@@ -5,7 +5,7 @@ Executable Muse workflow scripts (JavaScript). These are what the workflow runti
5
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
6
 
7
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, and (in dashboard mode) validates dashboardRepoPath is workspace-contained and an existing git repo (never auto-created); bootstraps the release system; scaffolds orchestration folders; registers the first project with repo_path set to the validated dashboard repo — skipped in CLI-only mode (no dashboardSlug/dashboardRepoPath), where projects are created later via crew-api.js; creates/converges cron jobs from the declarative manifest (owner `space:<slug>` in dashboard mode, `cli:<crew-home-basename>` in CLI-only mode). Idempotent. The crew owns its state via the Crew API (lib/crew-api.js); the dashboard is an optional client and is never a dependency.
8
+ - `crew-init.js` — sets up a new crew instance: Gate 0 validates crewHome is workspace-contained and a valid git repo, and (in dashboard mode) validates dashboardRepoPath is workspace-contained and an existing git repo (never auto-created); bootstraps the release system; scaffolds orchestration folders; registers the first project with repo_path set to the validated dashboard repo — skipped in CLI-only mode (no dashboardSlug/dashboardRepoPath), where projects are created later via crew-api.js; creates/converges cron jobs from the declarative manifest (scheduler identity is dashboard-independent: first init uses the crew-home basename, re-runs keep the instanceId recorded in `.cron-registry.json`; owner `cli:<instanceId>` — deleting a dashboard artifact never stops the crew). Idempotent. The crew owns its state via the Crew API (lib/crew-api.js); the dashboard is an optional client and is never a dependency.
9
9
  - `standard.js` — default task workflow: Triage → Capture → Map → Build → Review → Integrate → Publish → QA
10
10
  - `bugfix.js` — adds Capture after Triage, then Reproduce
11
11
  - `chore.js` — adds Capture after Triage, drops QA (low-risk)