session-orchestrator 3.23.0 → 3.24.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.
Files changed (102) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  5. package/CHANGELOG.md +278 -0
  6. package/README.md +16 -14
  7. package/agents/db-specialist.md +0 -1
  8. package/docs/ci-setup.md +180 -25
  9. package/docs/codex-setup.md +1 -1
  10. package/docs/components.md +3 -3
  11. package/docs/events-schema.md +46 -8
  12. package/docs/scope-collision-guard.md +4 -4
  13. package/docs/session-config-reference.md +64 -6
  14. package/docs/session-config-template.md +34 -4
  15. package/docs/telemetry/telemetry-claims.md +11 -10
  16. package/docs/telemetry.md +30 -1
  17. package/hooks/_lib/atomic-json.mjs +111 -0
  18. package/hooks/_lib/subagent-paths.mjs +143 -0
  19. package/hooks/cwd-change-restore.mjs +9 -29
  20. package/hooks/enforce-scope.mjs +35 -6
  21. package/hooks/hooks-codex.json +1 -1
  22. package/hooks/hooks.json +1 -1
  23. package/hooks/on-session-end.mjs +278 -12
  24. package/hooks/on-session-start.mjs +50 -2
  25. package/hooks/on-stop.mjs +349 -20
  26. package/hooks/post-bash-write-verify.mjs +104 -4
  27. package/hooks/post-subagent-discovery-validator.mjs +148 -18
  28. package/hooks/post-tool-batch-wave-signal.mjs +154 -40
  29. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  30. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  31. package/hooks/subagent-telemetry.mjs +11 -26
  32. package/package.json +1 -1
  33. package/scripts/autopilot.mjs +26 -12
  34. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  35. package/scripts/emit-event.mjs +10 -2
  36. package/scripts/lib/auq/parse.mjs +5 -29
  37. package/scripts/lib/auto-dialectic.mjs +68 -0
  38. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  39. package/scripts/lib/build-live-signals.mjs +25 -22
  40. package/scripts/lib/cold-start-detector.mjs +23 -14
  41. package/scripts/lib/config/block-header.mjs +55 -0
  42. package/scripts/lib/config/discovery-validator.mjs +7 -2
  43. package/scripts/lib/config/health-endpoints.mjs +383 -0
  44. package/scripts/lib/config/remote-hosts.mjs +233 -0
  45. package/scripts/lib/config.mjs +31 -3
  46. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  47. package/scripts/lib/events-schema.mjs +48 -0
  48. package/scripts/lib/events.mjs +238 -5
  49. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  50. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  51. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  52. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  53. package/scripts/lib/memory-banner.mjs +20 -8
  54. package/scripts/lib/peer-discovery.mjs +20 -2
  55. package/scripts/lib/reconcile/engine.mjs +236 -5
  56. package/scripts/lib/scope-gate.mjs +36 -0
  57. package/scripts/lib/session-close-backfill.mjs +59 -10
  58. package/scripts/lib/session-discovery.mjs +57 -3
  59. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  60. package/scripts/lib/session-identity/own-session.mjs +62 -1
  61. package/scripts/lib/session-transition.mjs +1 -1
  62. package/scripts/lib/sessions-canonical.mjs +446 -0
  63. package/scripts/lib/telemetry/schema.mjs +74 -8
  64. package/scripts/lib/telemetry/sync.mjs +49 -12
  65. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  66. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  67. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  68. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  69. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  70. package/scripts/lib/validate/check-unwired-features.mjs +0 -7
  71. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  72. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  73. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  74. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  75. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  76. package/scripts/lib/vault-status/narrative-mirror.mjs +2 -19
  77. package/scripts/lib/wave-executor/foreign-dispatch.mjs +2 -2
  78. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  79. package/scripts/lib/wave-resource-gate.mjs +127 -7
  80. package/scripts/lib/wave-transcript-tail.mjs +24 -4
  81. package/scripts/materialize-wave-scope.mjs +20 -4
  82. package/scripts/memory-propose.mjs +132 -8
  83. package/scripts/promote-vault-strict.mjs +4 -15
  84. package/scripts/site-numbers.mjs +36 -4
  85. package/scripts/validate-plugin.mjs +26 -0
  86. package/scripts/vault-consolidate.mjs +3 -11
  87. package/scripts/vault-integration-watcher.mjs +2 -4
  88. package/scripts/vault-mirror.mjs +111 -26
  89. package/skills/_shared/parallel-aware-auq.md +31 -2
  90. package/skills/_shared/parallel-aware-preamble.md +17 -4
  91. package/skills/_shared/state-ownership.md +1 -1
  92. package/skills/contract-version-bump/SKILL.md +1 -1
  93. package/skills/ecosystem-health/SKILL.md +4 -1
  94. package/skills/ecosystem-health/wizard.md +5 -0
  95. package/skills/evolve/SKILL.md +38 -1
  96. package/skills/journey-audit/SKILL.md +6 -5
  97. package/skills/reconcile/SKILL.md +5 -2
  98. package/skills/remote-offload/SKILL.md +89 -0
  99. package/skills/session-end/phase-3-6-tail.md +9 -6
  100. package/skills/session-start/SKILL.md +26 -3
  101. package/skills/wave-executor/SKILL.md +1 -1
  102. package/skills/wave-executor/wave-loop.md +43 -5
@@ -41,8 +41,9 @@ This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's
41
41
 
42
42
  **Outcome handling:**
43
43
  - `PASS_THROUGH` → continue to Phase 1
44
+ - `PASS_THROUGH` with a non-empty `advisory` array (GH#67) → a `discovered` peer with `lockSuperseded: true` never fires the Promotion AUQ (it stays visible, per the #1085 advisory-lock contract — it is not filtered). Print ONE advisory line per entry: `parallel-aware: registry entry <sessionId> (last heartbeat <N> min ago) is superseded by this root's live lock <lockOwnerId> — likely a finished task on a platform without SessionEnd (GH#67); still counted for PSA-001 awareness`, then continue to Phase 1. It remains PSA-002-relevant if the same id also shows up in STATE.md (`source: 'state-md'` is handled by Phase 1.2.1 unchanged).
44
45
  - `EXCLUSIVE_BLOCKED` → exit Phase 0 cleanly per the AUQ outcome (`Warten` / `Andere Session beenden` / `Abbrechen` — all three return without initializing STATE.md)
45
- - `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId() **for the worktree-name attribution label only**, `branch` from current HEAD, `repoRoot = process.cwd()`. It is not a lock/registry ownership key; the new worktree's Phase 1.2 obtains its own physical raw `session_id`. Because `branch` is the current HEAD it is normally checked out by `repoRoot` already, so `enterWorktree` treats it as a start point only and lands the promoted worktree on a fresh `so/<sessionId>` branch, returning `{ branch: 'so/<sessionId>', promotedFrom: '<branch>' }` (#1067) — the new worktree's STATE.md `branch` MUST record `so/<sessionId>` and note `promoted from <branch>@<repoRoot>`, never the source branch alone. On success, and BEFORE exiting Phase 0, call `leaveSourceRoot({ repoRoot, sessionId: <the raw physical session_id that owns this root's lock/registry entry>, semanticSessionId, reason: 'worktree-promotion' })` from `scripts/lib/session-transition.mjs`. **`sessionId` is the RAW physical `session_id` read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })` — never the semantic label, and never the id in `current-session.json`, which may describe a peer session (#863); a wrong id now aborts the teardown with `ok: false, reason: 'lock-session-mismatch:<owner>'` and removes nothing.** the promotion is a PROCESS BOUNDARY, not a live migration (#1069): the old root is deregistered and its `session.lock` released BEFORE the new worktree's own Phase 1.2 acquires, so the two roots never both own a live claim at once. It never throws; report a `{ ok: false }` result as a stderr WARN (`parallel-aware: leaveSourceRoot: <reason>`) and continue — the destination worktree already exists, so aborting here would leave exactly the two-live-roots state the call prevents. Then exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit stderr WARN `parallel-aware: enterWorktree failed: <err>; falling back to Manuell` and proceed via the Manuell path.
46
+ - `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot, rawSessionId, reason: 'worktree-promotion' })` from `scripts/lib/autopilot/worktree-pipeline.mjs` — since #1170 this ONE call does both jobs: it creates the destination worktree AND, because `rawSessionId` is supplied, releases the source root internally (see below), so no separate `leaveSourceRoot` call is made at this site. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId() **for the worktree-name attribution label only**, `branch` from current HEAD, `repoRoot = process.cwd()`, `rawSessionId` from `readLock({ repoRoot }).session_id`. `sessionId` (the semantic label) is not a lock/registry ownership key; the new worktree's Phase 1.2 obtains its own physical raw `session_id`. Because `branch` is the current HEAD it is normally checked out by `repoRoot` already, so `enterWorktree` treats it as a start point only and lands the promoted worktree on a fresh `so/<sessionId>` branch, returning `{ branch: 'so/<sessionId>', promotedFrom: '<branch>' }` (#1067) — the new worktree's STATE.md `branch` MUST record `so/<sessionId>` and note `promoted from <branch>@<repoRoot>`, never the source branch alone. **`rawSessionId` is the RAW physical `session_id` read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })` — never the semantic label, and never the id in `current-session.json`, which may describe a peer session (#863); a wrong id aborts the internal `leaveSourceRoot()` teardown with `left.ok: false, reason: 'lock-session-mismatch:<owner>'` and removes nothing.** The promotion is a PROCESS BOUNDARY, not a live migration (#1069): the old root is deregistered and its `session.lock` released BEFORE the new worktree's own Phase 1.2 acquires, so the two roots never both own a live claim at once. `enterWorktree()`'s return value carries the outcome as `left: { ok, steps, reason? }`; `leaveSourceRoot()` never throws, so on `left.ok !== true` `enterWorktree` itself emits the stderr WARN `enterWorktree: leaveSourceRoot: <reason>` and the promotion continues regardless — the destination worktree already exists by the time this runs, so aborting here would leave exactly the two-live-roots state the call prevents. Then exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit stderr WARN `parallel-aware: enterWorktree failed: <err>; falling back to Manuell` and proceed via the Manuell path.
46
47
  - `PROMOTION_OFFER` with user picking "Manuell — in-place daneben" → append Deviation, continue to Phase 1
47
48
  - `PROMOTION_OFFER` with user picking "Abbrechen" → exit cleanly
48
49
 
@@ -53,6 +54,25 @@ This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's
53
54
 
54
55
  Read and parse Session Config per `skills/_shared/config-reading.md`. Store result as `$CONFIG`.
55
56
 
57
+ ## Phase 1.05: Skill-Invocation Self-Report (#1199)
58
+
59
+ > Emit an L1 skill-invocation record for `session-start` itself. The PreToolUse `Skill`-matcher hook only captures skills dispatched via the `Skill` tool — a **prose-invoked** skill like this one is invisible to it (verified gap: external users show 0/20 sessions with a `session-start` row in `skill-invocations.jsonl`, vs. 93/93 for the operator). This self-report closes that gap so L2/L3 skill-health has a `session-start` selection signal. Best-effort, try/catch-silent — it never blocks Phase 1.1.
60
+
61
+ ```javascript
62
+ try {
63
+ const { appendSkillInvocation, DEFAULT_SKILL_INVOCATIONS_PATH } =
64
+ await import('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs');
65
+ const nodePath = await import('node:path');
66
+ await appendSkillInvocation(nodePath.join(process.cwd(), DEFAULT_SKILL_INVOCATIONS_PATH), {
67
+ timestamp: new Date().toISOString(),
68
+ event: 'selected',
69
+ skill: 'session-orchestrator:session-start',
70
+ session_id: null, // no session.lock is bound yet at Phase 1 — the raw id is acquired at Phase 1.2 (#1199)
71
+ phase: 'session-start',
72
+ });
73
+ } catch { /* self-report is advisory — never blocks Phase 1.1 */ }
74
+ ```
75
+
56
76
  ## Phase 1.1: Dispatcher-Autonomy Migration Capture (one-time, per-repo)
57
77
 
58
78
  > Closes session-orchestrator issue #681 (Epic #673 P3 — one-time per-repo dispatcher-autonomy capture). Migration trigger: the first session-start after this feature ships on a repo whose committed `dispatcher-autonomy:` block is still absent. Cross-reference `.claude/rules/ask-via-tool.md` (AUQ via tool, not prose).
@@ -222,11 +242,13 @@ if (peer) {
222
242
  }
223
243
  ```
224
244
 
245
+ GH#67 note: the `lockSuperseded` advisory-downgrade described in Phase 0.5's outcome handling applies only to the `discovered` peer subset — this phase's `peer` is always `source: 'state-md'`, so a `discovered`-side `lockSuperseded: true` never suppresses this guard; the Worktree-Promotion AUQ still fires exactly as below whenever a live STATE.md peer is found.
246
+
225
247
  ### Decision flow
226
248
 
227
249
  1. **`peer === null`** → no active peer owns STATE.md. Continue to Phase 1.5.
228
250
  2. **`peer !== null`** → STATE.md is owned by a live peer session. **Do NOT proceed with the default Phase 1.5/1b STATE.md overwrite.** Fire the Worktree-Promotion AUQ from `skills/_shared/parallel-aware-auq.md` (same options the Phase 0.5 preamble would emit on `PROMOTION_OFFER`).
229
- - User picks "Worktree anlegen + starten" → call `enterWorktree(...)`, thenBEFORE exiting Phase 1 `leaveSourceRoot({ repoRoot, sessionId, semanticSessionId, reason: 'worktree-promotion' })` from `scripts/lib/session-transition.mjs` `sessionId` is the RAW physical `session_id` read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })`, never the semantic label and never `current-session.json` (which may describe a peer, #863) — (#1069 process boundary: this site runs AFTER Phase 1.2 already acquired the lock, so the old root MUST be deregistered and its lock released here, or the new worktree's own Phase 1.2 finds a phantom owner). It never throws; on `{ ok: false }` emit a stderr WARN `parallel-aware: leaveSourceRoot: <reason>` and continue — the destination worktree already exists, so aborting here would leave exactly the two-live-roots state the call prevents. Then exit Phase 1 immediately (the new worktree's own session-start runs from scratch).
251
+ - User picks "Worktree anlegen + starten" → call `enterWorktree({ ..., rawSessionId, reason: 'worktree-promotion' })` from `scripts/lib/autopilot/worktree-pipeline.mjs` since #1170 this ONE call also releases the source root: it calls `leaveSourceRoot({ repoRoot, sessionId: rawSessionId, semanticSessionId, reason })` from `scripts/lib/session-transition.mjs` internally, on BOTH success exits, so no separate `leaveSourceRoot` call is made at this site. `rawSessionId` is the RAW physical `session_id` read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })`, never the semantic label and never `current-session.json` (which may describe a peer, #863) — (#1069 process boundary: this site runs AFTER Phase 1.2 already acquired the lock, so the old root MUST be deregistered and its lock released here, or the new worktree's own Phase 1.2 finds a phantom owner). The return value's `left` field carries `leaveSourceRoot()`'s result; it never throws, so on `left.ok !== true` `enterWorktree` itself emits the stderr WARN `enterWorktree: leaveSourceRoot: <reason>` and the promotion continues regardless — the destination worktree already exists, so aborting here would leave exactly the two-live-roots state the call prevents. Then exit Phase 1 immediately (the new worktree's own session-start runs from scratch).
230
252
  - User picks "Manuell — in-place daneben" → append a Deviation describing the missed peer detection, continue to Phase 1.5. STATE.md WILL be overwritten — the user has explicitly accepted that risk.
231
253
  - User picks "Abbrechen" → exit cleanly.
232
254
 
@@ -524,7 +546,7 @@ This single call does three things:
524
546
 
525
547
  - **Generator-marked + idempotent.** The board carries the `_generator: session-orchestrator-active-sessions@1` frontmatter sentinel; repeated writes that produce identical content are no-ops, so re-running this phase never churns the file.
526
548
  - **Host-local + git-ignorable.** The board lives under the operator's vault tree (under `$HOME`), never inside any repo — it is never committed.
527
- - **NEVER touches the sven-owned `_overview.md`.** The writer hard-refuses any path whose basename is `_overview.md` (returns `{ action: 'skipped-handwritten' }`), and only ever overwrites files it owns (frontmatter `_generator` matches the marker). The handwritten overview is structurally safe.
549
+ - **NEVER touches the human-authored `_overview.md`.** The writer hard-refuses any path whose basename is `_overview.md` (returns `{ action: 'skipped-handwritten' }`), and only ever overwrites files it owns (frontmatter `_generator` matches the marker). The handwritten overview is structurally safe.
528
550
 
529
551
  ### Non-blocking behavior
530
552
 
@@ -1233,6 +1255,7 @@ After user alignment:
1233
1255
  | File | Purpose |
1234
1256
  |------|---------|
1235
1257
  | `soul.md` | Identity and communication principles |
1258
+ | (inline) Phase 1.05 | Skill-Invocation Self-Report (#1199) — mirrors session-end Phase 0.6 |
1236
1259
  | (inline) Phase 1.2 | Session Lock Acquire — `acquire()` call, active/stale/cross-host AUQ flows, `forceAcquire()` on user consent, deviation note wiring |
1237
1260
  | (inline) Phase 1.7 | Vault Live-Status Board (#674/#716) — `sweepBoard()` from `scripts/lib/vault-status/board-writer.mjs`; gated on `vault-integration.enabled: true`; marks this repo `in-progress` + host-wide staleness sweep via `enumerateCandidates()` (`scripts/lib/dispatcher/enumerate.mjs`), so a crashed session in ANY repo renders `force-closed` from any repo's session-start; generator-marked + idempotent; never touches `_overview.md`; non-blocking (falls back to single-repo `mirrorBoard()` on enumeration failure) |
1238
1261
  | `presentation-format.md` | Phase 8 output templates and AskUserQuestion examples |
@@ -249,7 +249,7 @@ Each agent prompt MUST include:
249
249
  3. **Acceptance criteria**: measurable definition of done
250
250
  4. **Rule references**: the wave's applicable rules are injected automatically as the `<APPLICABLE-RULES>` block produced by `scripts/print-applicable-rules.mjs` (see `wave-loop.md` § "Pre-Dispatch: Glob-Scoped Rule Injection (#336/#694)"). The block is computed once per wave from the wave's `allowedPaths` and prepended to every agent prompt — do not hand-copy rule paths into the prompt. Past **learnings** arrive separately as the `<LEARNINGS-INDEX>` block from `scripts/print-learnings-index.mjs` (see `wave-loop.md` § "Pre-Dispatch: Learnings-Index Injection (#1014)"), computed **per agent** from its own file scope rather than once per wave.
251
251
  5. **Testing expectation** (need-gated): "Before writing any test, name the concrete bug a NEW test would catch that the existing suite does not. No nameable bug → write NO test and report `no-tests-needed: <reason>` — that is a SUCCESS outcome, not a gap. With a nameable bug: exactly one test for it. Running existing tests is always mandatory."
252
- 6. **Commit instruction**: "Do NOT commit. The coordinator handles commits."
252
+ 6. **Commit instruction**: "Do NOT commit. The coordinator handles commits. Never `git stash`, `git add`, `git checkout --` or `git reset` either (PSA-007) — to compare against the pre-change state, read `git show HEAD:<path>` (or `git show <sha>:<path>`); it never touches the shared index." Measured 2026-09-02: two agents in one wave reached for `git stash` to build a baseline; both recovered, both were the same shape.
253
253
  7. **Turn limit**: Include the maxTurns instruction from `circuit-breaker.md`
254
254
  8. **Verification before completion**: Before claiming any task done, run the verification command and quote the evidence inline. See `.claude/rules/verification-before-completion.md`.
255
255
 
@@ -78,6 +78,14 @@ const gate = await evaluateWaveResourceGate({
78
78
  | `proceed` | Dispatch at `gate.agents` (= `plannedAgents`). Include `gate.reasons` in the wave progress update (informational). |
79
79
  | `reduce` | Dispatch at `gate.agents` (< `plannedAgents`). Log the reduction as a deviation in STATE.md. Include `gate.reasons` in the wave progress update. |
80
80
  | `coordinator-direct` | Do NOT dispatch subagents. Coordinator executes the wave's tasks directly. Log as a deviation in STATE.md. Continue to `### 1. Dispatch Agents` only for stagnation-pattern detection wording — the section's execution is skipped. |
81
+ | `offload` (#1160) | The envelope carries `host` — `{ decision: 'offload', agents, host, reasons }`. Dispatch the offloadable roles to that alias via `dispatchRemote()` (`### Agent-Type Resolution`, `ssh:<alias>` branch) and the remaining roles locally at `gate.agents`. Placement, not reduction: log it in the wave progress update with the host named, since a wave that silently ran elsewhere is unreadable afterwards. |
82
+
83
+ **The offload witness is supplied, never probed (#1160).** The gate's own JSDoc states it: `@param {Record<string, boolean>} [opts.remoteReady] — #1160 readiness witness per declared host alias. The gate never probes the network itself; without a witness no host counts as ready and the decision stays local.` So an `offload` decision is impossible unless the coordinator passes one of two things alongside `config`/`plannedAgents`/`waveRole`:
84
+
85
+ - `remoteReady: { '<alias>': true }` — built from the SessionStart banner line `Offload <alias>: ready=yes`, which was measured at session start and needs no new network call here; or
86
+ - `probeFn: remoteReadyProbe` from `scripts/lib/wave-executor/remote-dispatch.mjs` — `@param {(alias: string) => Promise<boolean>} [opts.probeFn] — optional async witness, consulted only for aliases absent from `remoteReady`. Default null.` Use it when the banner is stale or absent; it costs a live probe per alias.
87
+
88
+ Passing neither is a valid choice, not a bug: the gate then behaves exactly as it did before #1160 and reduces locally.
81
89
 
82
90
  Reasons MUST appear in the wave's progress update under a "Resource gate:" bullet. Measurements (RAM free GB, CPU %, concurrent sessions) appear verbatim so the user can trust the decision.
83
91
 
@@ -208,7 +216,7 @@ After running this detection block, call `resolveIsolation({ agentCount, session
208
216
 
209
217
  #### Pre-Dispatch: Path-Cousin-Guard Injection (#730.3)
210
218
 
211
- Before dispatching each agent whose fileScope contains a NEW (non-existent) file target, check for existing "cousin" files with a similar basename elsewhere in the repo — prevents the framing-wrong class where an agent creates `scripts/lib/foo/bar.mjs` while `scripts/lib/bar.mjs` already exists and serves the same purpose.
219
+ Before dispatching each agent whose fileScope contains a NEW (non-existent) file target, check for existing "cousin" files with a similar basename elsewhere in the repo — prevents the framing-wrong class where an agent creates `scripts/lib/foo/bar.mjs` while `scripts/lib/bar.mjs` already exists and serves the same purpose. <!-- path-check: example -->
212
220
 
213
221
  **Detection (mechanical, reuses the new-file scan from #243 above):** for each not-yet-existing file target `<newPath>` in an agent's fileScope, take `basename(<newPath>)` minus extension; skip generic basenames (`index`, `utils`, `main`, `config`, or length ≤ 3 chars — false-positive control). Then:
214
222
 
@@ -294,7 +302,7 @@ For each agent in this wave:
294
302
 
295
303
  The colon heuristic (§ "How to detect project agents") has **three** readings, not two: no colon = project agent, `session-orchestrator:<agent>` = plugin agent, and `cursor:<model>` = **foreign channel**. When the session plan or `agent-mapping` resolves an agent to `cursor:<model>` (e.g. `impl: cursor:composer-2.5`), do NOT call the Agent tool. Dispatch **coordinator-direct** via `dispatchForeign({ model, prompt, repoRoot, role, runId, timeoutSec })` from `scripts/lib/wave-executor/foreign-dispatch.mjs`. Contract, in the order it binds:
296
304
 
297
- 1. **`never_foreign` gate first.** `isNeverForeignRole(role)` returns `{ok:false, reason:'never-foreign-role'}` before any worktree or spawn. `NEVER_FOREIGN_ROLES` (impl-core, security-review, migration, release, secrets, incident, refactor-crosscut) is hand-copied from the account-switch routing SSOT; `dispatch-cursor.sh` enforces none of it, so **this adapter is the only gate** — never route around it with a shell call.
305
+ 1. **`never_foreign` gate first.** `isNeverForeignRole(role)` returns `{ok:false, reason:'never-foreign-role'}` before any worktree or spawn. `NEVER_FOREIGN_ROLES` (impl-core, security-review, migration, release, secrets, incident, refactor-crosscut) is hand-copied from the operator's model-routing SSOT (ADR-002); `dispatch-cursor.sh` enforces none of it, so **this adapter is the only gate** — never route around it with a shell call.
298
306
  2. **Detached worktree**, `<tmpdir>/so-foreign/<runId>` by default, `git worktree add --detach`. Never under `.claude/worktrees/` — eight readdir scanners in this repo read that path. The stream log is KEPT beside it (`<runId>.log.jsonl`), not trap-deleted.
299
307
  3. **Verdict measured at the filesystem, never from the model's prose.** `ok` is true only on exit 0 + no timeout + a non-empty changed set (`git diff --name-only` ∪ `ls-files --others`, since a diff alone is blind to new files). **An empty diff is a failure regardless of what the report says.**
300
308
  4. **MANDATORY Claude diff-review gate.** Read `result.diff` and judge it SEMANTICALLY before any merge-back — test-green is not the bar (measured counterexample: the foreign #1149 solution was test-green and semantically wrong). The **coordinator** applies (`git apply`) and commits; the foreign model never touches this repo's index or worktree (PSA-007). On review failure, discard the diff. **`result.hookTampering === true` invalidates the run regardless of `ok`**: the child repointed or rewrote the shared `.git` hooks path (a linked worktree isolates the tree, not `core.hooksPath`), and that write is invisible in `result.diff` — discard and do not merge. `null` means the fingerprint could not be read (not measured), never "clean".
@@ -304,10 +312,34 @@ The colon heuristic (§ "How to detect project agents") has **three** readings,
304
312
 
305
313
  **Composition.** A foreign run is a background Bash task, so it composes with background `Agent()` dispatch in the same wave. It is **NOT** part of § Started-Set Verification: no `meta.json` sidecar exists for it and no task-notification arrives. Its lifecycle is the adapter's return value — do not count it as planned/started/completed, and do not read its absence from the started set as a drop.
306
314
 
307
- **Model selection** is not decided here: the account-switch routing SSOT (ADR-002 / `tools/routing/routing.yaml`) owns it. Working defaults: `composer-2.5` for foreign impl; `cursor-grok-4.6-high` for review / test-writing / judgment roles, at `timeoutSec ≥ 900`. Dogfood evidence (2026-08-25): `composer-2.5` on issue #1105 — 165 s, 2 files, +93 lines, mandatory review passed, merged only after independent test verification.
315
+ **Model selection** is not decided here: the operator's model-routing SSOT (ADR-002) owns it. Working defaults: `composer-2.5` for foreign impl; `cursor-grok-4.6-high` for review / test-writing / judgment roles, at `timeoutSec ≥ 900`. Dogfood evidence (2026-08-25): `composer-2.5` on issue #1105 — 165 s, 2 files, +93 lines, mandatory review passed, merged only after independent test verification.
308
316
 
309
317
  **Timeout.** `maxTurns` does not exist on this channel — see the § Platform-Specific Dispatch timeout note. The wall-clock SIGTERM (`DEFAULT_TIMEOUT_SEC = 900`, a floor) is the only circuit breaker.
310
318
 
319
+ ##### Fourth branch: remote-host dispatch (`ssh:<alias>` — #1160)
320
+
321
+ A fourth colon reading, beside the `cursor:<model>` branch above: `ssh:<alias>` routes the role to a declared offload host. Reached either from an `offload` resource-gate decision (§ 0.5) or from an explicit `agent-mapping` entry. Do NOT call the Agent tool — dispatch coordinator-direct via `dispatchRemote()` from `scripts/lib/wave-executor/remote-dispatch.mjs`:
322
+
323
+ ```js
324
+ import { dispatchRemote } from "scripts/lib/wave-executor/remote-dispatch.mjs";
325
+ // task fields, per the adapter's own JSDoc:
326
+ // host (offload alias, `-H`), repo, prompt (STDIN, never argv), role,
327
+ // runId (becomes `--job`), timeoutSec?, model?, patchPath?
328
+ const result = await dispatchRemote(
329
+ { host, repo, prompt, role, runId, timeoutSec, model, patchPath },
330
+ { repoRoot },
331
+ );
332
+ ```
333
+
334
+ The contract mirrors the `cursor:` branch and differs in exactly one place — the artefact is a PATCH, not a worktree:
335
+
336
+ 1. **`never_foreign` gate first**, same `NEVER_FOREIGN_ROLES` lock, refused before any spawn; a refusal is a ledger record, not a silence.
337
+ 2. **The prompt goes on STDIN**, never into argv.
338
+ 3. **Verdict = exit 0 AND a non-empty patch.** The adapter's own words: *"`ok` is false unless the child exited 0 AND left a non-empty patch."* A green-sounding remote report with an empty patch is a failure.
339
+ 4. **The coordinator reads and reviews the patch, then applies it with `git apply`.** The remote never touches this repo — same PSA-007 boundary as the foreign branch, and the same semantic-review bar (test-green is not the bar).
340
+ 5. **Telemetry:** `orchestrator.remote_dispatch.completed`, emitted on refusals too.
341
+ 6. **Not part of § Started-Set Verification** — no `meta.json` sidecar, no task notification, exactly as for the `cursor:` branch. Its lifecycle is the adapter's return value.
342
+
311
343
  #### Pre-Dispatch Grounding Injection (#85)
312
344
 
313
345
  Before dispatching each agent, prepend a line-numbered GROUNDING block to its prompt for any file in the agent's scope that has recent edit-format-friction history. This helps the agent reference edits by line number instead of re-matching exact character spans, reducing Edit-tool retry loops.
@@ -830,7 +862,7 @@ If the commit itself fails (e.g., nothing to commit, pre-commit hook rejects), d
830
862
 
831
863
  **Mission-status transition:** after a successful auto-commit, transition the mission status for all tasks in this wave from `in-dev` → `testing` using `setMissionStatus(stateContent, taskId, 'testing')` from `scripts/lib/state-md.mjs`. This matches the coordinator-level rule in `SKILL.md § Mission-Status Updates`: "in-dev → testing: Quality wave begins and this item's implementation wave completed without failure." The auto-commit checkpoint fires at the same logical moment — after implementation completes and Quality-Lite passes.
832
864
 
833
- **Implementation deferred:** This subsection documents the contract. The procedural body (git add/commit sequence + error handling) will land in a future release as `scripts/lib/auto-commit.mjs` (tracked in GitLab #214; not yet implemented as of v3.10.0). Until then, this section is a no-op stub when `auto-commit-per-wave: true` is set; the coordinator MUST warn the user at session-start that auto-commits are not yet active (emit: "auto-commit-per-wave is set but the implementation (scripts/lib/auto-commit.mjs) is not yet available — commits will occur at session-end via /close as normal").
865
+ **Implementation deferred:** This subsection documents the contract. The procedural body (git add/commit sequence + error handling) will land in a future release as `scripts/lib/auto-commit.mjs` (tracked in GitLab #214; not yet implemented as of v3.10.0). Until then, this section is a no-op stub when `auto-commit-per-wave: true` is set; the coordinator MUST warn the user at session-start that auto-commits are not yet active (emit: "auto-commit-per-wave is set but the implementation (scripts/lib/auto-commit.mjs) is not yet available — commits will occur at session-end via /close as normal"). <!-- path-check: planned #214 -->
834
866
 
835
867
  ---
836
868
 
@@ -974,7 +1006,7 @@ After reviewing wave results, adjust the next wave's agent count based on perfor
974
1006
 
975
1007
  After each wave completes and before the progress update, update `<state-dir>/STATE.md`:
976
1008
 
977
- 1. **Frontmatter**: set `current-wave` to the just-completed wave number; set `status` to `active` (or `paused` if waiting on user input)
1009
+ 1. **Frontmatter**: set `current-wave` to the just-completed wave number; set `status` to `active` (or `paused` if waiting on user input). Readers that need the RUNNING wave (e.g. `scripts/memory-propose.mjs`) read `<state-dir>/wave-scope.json` `wave` (only when the manifest is bound to this session via `semantic_session`; an unbound manifest is ignored — it may be a peer's, #1123) and fall back to `current-wave + 1` (#1166) — do not change this field's meaning.
978
1010
  2. **`## Current Wave`**: replace contents with next wave info — wave number, role, agents to dispatch and count
979
1011
  3. **`## Wave History`**: append an entry for the completed wave (the `(planned … → actual …, over-delivery …)` parenthetical is omitted when `grounding-check: false`, since the counts are unavailable):
980
1012
  > **Record the SUITE COUNT, not just "gates green" — and name the platform (#944).** The wave line MUST carry the full-suite pass/fail count from the gate that just ran (`<passed>/<failed>`), not merely that typecheck and lint were clean. A deep session on 2026-07-30 logged typecheck/lint/validate-plugin for every wave and no suite count; a test that had been vacuous for its entire life sat red on HEAD through three waves and was found only by the review panel — in a session whose own premise was turning CI from red to green.
@@ -1195,6 +1227,8 @@ Before each wave dispatch:
1195
1227
  ```
1196
1228
  The `gates` field (optional) mirrors `enforcement-gates` from Session Config (#77). When present, hooks check each gate individually via `gate_enabled()`. Missing gate entries default to enabled, preserving default behavior.
1197
1229
 
1230
+ The `wave` field also doubles as the RUNNING-wave signal for readers outside this step's STATE.md `current-wave` (which records the just-completed wave, per step 1 of `3a. Post-Wave: Update STATE.md` above): `scripts/memory-propose.mjs` reads it directly for proposal attribution only when the manifest is bound to the calling session (`semantic_session` matches STATE.md `session`); an unbound or foreign manifest is ignored and the reader falls back to `current-wave + 1` (#1166, #1123).
1231
+
1198
1232
  **What the binding means to a reader.** Three states, and the disposition differs for each. **Absent** = legacy = ENFORCE: a manifest written before #1123 (or by a stale skill body) binds nobody, so it must keep constraining everyone exactly as it did before — this is the only state that preserves the pre-#1123 contract, and `validate-wave-scope.mjs` marks it with one advisory stderr line rather than an error, because § 3.3's pre-union skeleton is itself an unbound manifest. **Own session** = ENFORCE, unchanged. **Foreign session** — `session` present and not this session's id — = ALLOW: `hooks/enforce-scope.mjs` lets the write through and emits `orchestrator.scope.foreign_session_ignored` so the skip is counted rather than silent. A foreign manifest is somebody else's wave plan; it never had authority over this session's writes, and the event is what keeps that visible instead of leaving an allow nothing recorded. (The reader half lives in `hooks/enforce-scope.mjs` — the writer's only obligation is to name itself honestly here.)
1199
1233
  2. Validate by piping through `node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs"` (where `$PLUGIN_ROOT` is `$CLAUDE_PLUGIN_ROOT`, `$CODEX_PLUGIN_ROOT`, or `$CURSOR_RULES_DIR` per platform — see `skills/_shared/config-reading.md`). If validation fails (exit 1), fix the JSON based on stderr errors and retry.
1200
1234
  3. **`allowedPaths` is COMPUTED from one canonical declaration array — never hand-transcribed (#1020/#1083).** Transcribing either declaration shape or the union by hand produced scope divergences. Globs stay verbatim (`scripts/*.sh`) — the enforcement hook resolves them at check time.
@@ -1223,6 +1257,10 @@ Before each wave dispatch:
1223
1257
 
1224
1258
  > **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs before the union exists, so writing these files reports `bash-write-verify: N file(s) changed by a Bash call OUTSIDE the wave's allowedPaths` naming `filescopes/wave-<N>/*.json`. Expected once per wave rollover at this step; it is information, not a scope violation. Never widen `allowedPaths` to silence it — that would grant agents write access to the deconfliction record itself.
1225
1259
 
1260
+ **3.1a — a peer session's declared paths are their OWN record (#1195).** When a reachable peer session runs in this same checkout and has sent a Peer-Scope-Union request (`skills/_shared/parallel-aware-auq.md` § Peer-Scope-Union), carry its complete path list into the record array as exactly ONE record `{"id":"peer-session-<id>","files":[…]}` — `<id>` being the peer's semantic session id. Never merge peer paths into an agent's record: an agent record is a territory ONE agent may write, and a peer's paths are a territory NO agent of this wave may write. Keeping them separate is also what makes the disjointness check meaningful — a peer path colliding with an agent's scope is a real collision that must surface at 3.2, not be laundered by living in that agent's own record. Mechanically, a peer record takes part in `--assert-disjoint` and is EXCLUDED from `--union` (`unionFileScopes`, `scripts/lib/scope-gate.mjs`), so `allowedPaths` never grants a peer's paths to this wave's agents — which is precisely what keeps them a territory no agent may write. It also gets NO per-agent shape-(a) file: `materialize-wave-scope.mjs` writes peer records into the aggregate only, because `$AGENT_FILESCOPE_JSON` is addressed by an agent id at dispatch and no agent is dispatched for a peer session. `hooks/post-bash-write-verify.mjs` reads the same aggregate sidecar and reports a change under a `peer-session-*` record as a peer write instead of a violation — reachable only BECAUSE the union excludes it (the hook sees a path only while it is outside `allowedPaths`).
1261
+
1262
+ The record's lifecycle is the coordinator's: every wave rollover re-materializes it (3.1 rebuilds the whole array, so an omitted peer record silently revokes the union mid-session), and session-end removes it with the rest of `filescopes/` — a peer record outliving its peer grants paths nothing is watching.
1263
+
1226
1264
  **3.2 — assert disjointness BEFORE computing the union.** The materialized aggregate is an ARRAY of `{id, files}` records (never an object map: a duplicated agent id must stay visible), including `coordinator.json`. Run:
1227
1265
 
1228
1266
  ```bash