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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/CHANGELOG.md +278 -0
- package/README.md +16 -14
- package/agents/db-specialist.md +0 -1
- package/docs/ci-setup.md +180 -25
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +3 -3
- package/docs/events-schema.md +46 -8
- package/docs/scope-collision-guard.md +4 -4
- package/docs/session-config-reference.md +64 -6
- package/docs/session-config-template.md +34 -4
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +30 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-end.mjs +278 -12
- package/hooks/on-session-start.mjs +50 -2
- package/hooks/on-stop.mjs +349 -20
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +148 -18
- package/hooks/post-tool-batch-wave-signal.mjs +154 -40
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/subagent-telemetry.mjs +11 -26
- package/package.json +1 -1
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/emit-event.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/reconcile/engine.mjs +236 -5
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +59 -10
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-identity/own-session.mjs +62 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +49 -12
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -7
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +2 -19
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +2 -2
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +24 -4
- package/scripts/materialize-wave-scope.mjs +20 -4
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/validate-plugin.mjs +26 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +17 -4
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +6 -5
- package/skills/reconcile/SKILL.md +5 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/phase-3-6-tail.md +9 -6
- package/skills/session-start/SKILL.md +26 -3
- package/skills/wave-executor/SKILL.md +1 -1
- 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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|