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
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
10
- "version": "3.23.0"
10
+ "version": "3.24.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "session-orchestrator",
4
- "version": "3.23.0",
4
+ "version": "3.24.0",
5
5
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
6
6
  "author": {
7
7
  "name": "Bernhard Goetzendorfer",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "3.23.0+codex.20260828183221",
3
+ "version": "3.24.0+codex.20260902192928",
4
4
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
5
5
  "keywords": [
6
6
  "session",
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: remote-offload
3
+ description: Use when local resource pressure would shrink or coordinator-direct a wave, a wave plan carries heavy build/test/audit roles (test, ui, perf), or the operator says offload, remote host, or auslagern — reference for routing that wave role to a declared SSH-reachable host instead of reducing agent count
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # remote-offload
8
+
9
+ Canonical skill: `skills/remote-offload/SKILL.md`
10
+
11
+ Read that file and follow it exactly. Resolve relative links against `skills/remote-offload/`, not this wrapper.
12
+
13
+ Cursor has no Skill tool. Treat "invoke the remote-offload skill" as: Read `skills/remote-offload/SKILL.md`.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,284 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.24.0] - 2026-09-02
11
+
12
+ Five commits since v3.23.0 (4 `feat`, 1 `docs`; 152 files, +14,596/−942), no
13
+ `BREAKING CHANGE:` footer and no `!` subject.
14
+
15
+ One `docs` commit (`c3ab4801`) adds ADR-0013, naming the ownership-check-before-deregister
16
+ ordering that closed Wave 4 of the previous session. One `feat` commit (`a019d5a4`, deep
17
+ session, 5 waves, 37 agents + 3 reviewers + 3 panel) closes the GH#67 peer-filter class
18
+ across every affected site, plus #1170, #1176, #1177, #1166, #1167, #1174, #1180 and #1175.
19
+ One `feat` commit (`936dae8a`) closes a Fleet-Mining v2 instrument audit across 18 repos
20
+ (1,587 learnings, 144k events, 347 telemetry records) with five repairs to the fleet's own
21
+ measurement instruments (#1189, #1190, #1191, #1192, #1193).
22
+
23
+ Two further `feat` commits (`2ccea0f2`, `3b352d78`) are Waves 2 and 3 of a new session (11
24
+ agents each; Opus for guards/identity/protocol, Sonnet for consolidation/polish). The
25
+ through-line: identity and scope boundaries are now drawn at the PROCESS, not the working
26
+ copy — a peer session's manifest in the same checkout is `foreign`, a dispatched subagent's
27
+ raw session id belongs to its coordinator, and four duplicated helper implementations
28
+ collapse to one canonical each.
29
+
30
+ ### Added
31
+
32
+ - **`orchestrator.reconcile.completed` (#1192).** Wrapper around `runReconcile` covering all
33
+ three return paths (empty corpus, normal, engine-error) with `trigger`
34
+ (`skill|session-end|phase-skip|unknown`), `targets` (allowlisted `repo-local|baseline`),
35
+ counters including measured zeros, `duration_ms`, and `aborted`+`reason` on the catch path.
36
+ Skipped with a stderr WARN when no `repoRoot` is passed, so test runs never write the fleet
37
+ ledger. First live record: `eligible 102 / proposals 10 / rejected 154 / capped 72 /
38
+ already_materialized 20`.
39
+ - **`docs/audits/2026-09-02-fleet-instruments.md`** — the public scrubbed extract of the
40
+ Fleet-Mining v2 instrument audit: 18 repos, 1,587 learnings, 143,967 events, 347 telemetry
41
+ records (6 `anon_id`), 13 instrument verdicts and 12 orchestrator candidates (K1–K12).
42
+ - **A new blocking validator catches dead script paths cited in skill/command/agent prose
43
+ (#1176).** `scripts/lib/validate/check-skill-script-paths.mjs` scans `skills/`, `commands/`
44
+ and `agents/` for `scripts/**.mjs` citations that neither exist nor carry a same-line/
45
+ line-above `<!-- path-check: planned #<iid> | historical | example -->` marker; fenced code
46
+ blocks are skipped as illustrative shape, not a claim. Measured 2026-09-02 @ `c3ab480`: 237
47
+ distinct citations, 7 dead, 4 of the 7 inside fences.
48
+ - **`events.jsonl` records are now schema-versioned and validated before they are written
49
+ (#1177).** `emitEvent()` stamps every record `schema_version: 1` (never overwriting a
50
+ caller-supplied value) and runs `validateEventRecord()` BEFORE the append and BEFORE any
51
+ webhook POST — an invalid record throws `EventValidationError` and produces neither.
52
+ `scripts/emit-event.mjs` maps that to exit `1` (I/O failures keep exit `2`);
53
+ `scripts/lib/tmux-layout/telemetry.mjs` stamps + validates synchronously and drops invalid
54
+ lines. The read path stays lenient — the 33k+ pre-#1177 records without the field remain
55
+ valid.
56
+ - **A canonical reader collapses `sessions.jsonl`'s append-only duplication (#1167).**
57
+ `scripts/lib/sessions-canonical.mjs` applies newest-wins per `session_id`, drops any record a
58
+ later `supersedes` pointer refutes, and narrowly collapses the systemic double-stub class
59
+ (two `abandoned` records sharing an exact `started_at`/`completed_at` pair, one synthetic).
60
+ The startup/CLI backfill path (`scripts/backfill-abandoned-sessions.mjs`) now also emits
61
+ `orchestrator.session.backfill_completed` per record — previously only the SessionEnd hook
62
+ did — and stamps `raw_session_id`; `hooks/on-session-end.mjs`'s backfill gained a second
63
+ UUID→semantic bridge via the `session.ended` event for sessions that lost the
64
+ lock-acquire race.
65
+ - **A vault-scoped lock protects the shared live-status board's read-modify-write (#1180).**
66
+ `scripts/lib/vault-status/board-lock.mjs` wraps `sweepBoard()`'s merge in a cross-repo mutex
67
+ (mtime-based staleness — the vault dir can be synced cross-host, so a recorded pid isn't
68
+ probeable); fail-open on acquire timeout or fs-error (one stderr WARN, then runs unlocked)
69
+ since a board update is best-effort telemetry. `.gitignore` gained `state.lock` and
70
+ `rules.lock`.
71
+ - **A GitLab Project Access Token recipe for the schema-drift CI job (#1175).**
72
+ `docs/ci-setup.md` documents Option A (Project Access Token, recommended) and Option B
73
+ (personal PAT) for `SCHEMA_DRIFT_TOKEN`. Activation is blocked on the documented
74
+ vendor-ahead drift tracked in #531: the token is provisioned but the CI/CD variable is not
75
+ yet set upstream.
76
+ - **A `remote-hosts:` config block routes wave work to another machine over SSH (#1160).**
77
+ `scripts/lib/config/remote-hosts.mjs` adds the block (opt-in template + reference doc) and
78
+ an `ssh:<alias>` channel in `agent-mapping`; the wave-resource gate adds an `offload`
79
+ decision AFTER the HR-004 heavy-repo cap, gated on an injected readiness witness
80
+ (`remoteReady` map or async `probeFn`) — the gate never probes the network itself, so with
81
+ no witness the decision stays local. `dispatchRemote()`/`remoteDoctor()`/
82
+ `remoteReadyProbe()` wrap the host-side `offload` CLI (prompt travels on stdin, never argv;
83
+ exit codes 1–8 mapped to typed refusal reasons; an empty returned patch counts as failure,
84
+ not success). Emits `orchestrator.remote_dispatch.completed` on every attempt AND every
85
+ refusal (`ok:false`, `exit_code: null`) — the payload carries the configured alias, never a
86
+ hostname or IP. `skills/remote-offload/SKILL.md` documents the channel generically for any
87
+ consuming repo (skill count measured 48 → 49); wave-3 added the fourth wave-executor
88
+ dispatch branch (`ssh:<alias>` via `dispatchRemote()`) to `wave-loop.md` alongside the
89
+ existing three.
90
+ - **A Peer-Scope-Union protocol lets two coordinator sessions in the same working copy share
91
+ scope without racing (#1195).** `skills/_shared/parallel-aware-auq.md` documents the
92
+ four-step handshake plus a message template; `skills/wave-executor/wave-loop.md` §
93
+ Scope Manifest 3.1a adds a `peer-session-<id>` record (rolled over each wave, cleared at
94
+ session-end) that is included in the disjoint check but excluded from the coordinator's own
95
+ file-scope union; a new branch in the `parallel-sessions.md` decision tree routes to it; and
96
+ `hooks/post-bash-write-verify.mjs` now partitions a peer's writes from genuine scope
97
+ violations instead of flagging both alike.
98
+ - **Session-start now writes its own identity before anything can read it wrong (#1199).**
99
+ Phase 1.05 adds a self-report block (`session_id: null` before Phase 1.2 resolves it) so
100
+ downstream consumers see an explicit unresolved state instead of inferring one.
101
+ - **`/evolve` and the auto-dialectic nudge now emit their own completion events (#1200).**
102
+ `orchestrator.evolve.completed` and `dialectic.completed` are emitted via `emit-event.mjs`
103
+ from inside the skill; `decideAndRecordAutoDialectic()` emits `dialectic.nudge_decided`
104
+ mechanically rather than leaving the decision only in skill prose, consumed by
105
+ `phase-3-6-tail.md`.
106
+
107
+ ### Changed
108
+
109
+ - **BREAKING (behavior): `discovery-validator.enabled` defaults to `false` again (#1191).**
110
+ Reverts the #690 flip of 2026-06-25. Fleet measurement (2026-09-02, 18 repos): 6,946
111
+ `discovery_validator_violation` events had accumulated in repos that never declared the
112
+ block, and a scope-adjusted n=60 sample put real precision at 0/60 — the hook was scanning
113
+ the MAIN transcript on `SubagentStop`, not the subagent's own. It now reads the subagent
114
+ transcript (`agent_transcript_path` when the harness sends it, else
115
+ `<dir>/<base>/subagents/agent-<agent_id>.jsonl` — no fallback to the main transcript), and
116
+ the violation event now carries `agent_id`. Repos that relied on the silent default lose the
117
+ signal until they set `enabled: true` explicitly.
118
+ - **Scope enforcement now treats every identity as process-local, including your own
119
+ (#1194).** `enforce-scope.mjs` reads session ids via a new `readProcessLocalSessionIds()`
120
+ instead of `readOwnSessionIds()` — a peer's manifest sitting in the SAME working copy now
121
+ classifies as `foreign`, not `own`. Two existing tests had asserted the inverted behaviour
122
+ and were rewritten with a named ceiling (a harness with no session id, e.g. bare
123
+ Codex/Cursor, resolves to `unknown` → enforce, never skip); `scope-collision-guard.md` and
124
+ `state-ownership.md` were updated to match.
125
+ - **`memory.propose`'s lock check is now a raw→semantic lookup, not a trust boundary
126
+ (#1188).** The lock is consulted only to translate the caller's raw id into its semantic
127
+ session id, authorised by a process-local match against the RAW id — measured: a dispatched
128
+ subagent carries the COORDINATOR's raw UUID in `CLAUDE_CODE_SESSION_ID`, never its own.
129
+ `sessions-canonical.mjs` and the backfill path now distinguish silent `ENOENT` from loud
130
+ `EACCES`/`EISDIR` in two separate ledger readers instead of treating every read failure the
131
+ same.
132
+ - **Four duplicated helper implementations collapse to one each.**
133
+ `scripts/lib/validate/markdown-fences.mjs` (#1181) replaces 4 line-start-only fence
134
+ scanners with one line-start-AND-end-anchored automaton shared by all 4 callers
135
+ (`validate-plugin` output byte-identical before/after, 203/0). `expandTilde` (#1182)
136
+ replaces 8 copies of home-directory expansion with the existing `common.mjs`
137
+ implementation — one of the deleted copies (`gitlab-portfolio/cli.mjs`) mis-expanded
138
+ `~user` (a different user's home, not the caller's). `hooks/_lib/subagent-paths.mjs`
139
+ (#1196) replaces 4 divergent sidecar-path derivations with one carrying the strictest rules
140
+ from any of them (agent-id length `{1,64}`, `'unknown'` rejected,
141
+ `agent_transcript_path` confined inside the transcript directory).
142
+ `hooks/_lib/atomic-json.mjs` (#1197) replaces 4 byte-identical copies with one whose
143
+ read-modify-write default only replaces a genuinely MISSING file (`ENOENT`) — an
144
+ unparsable or unreadable file is no longer silently overwritten with the default.
145
+ - **A block-header matcher gained an indent+inline-value form, additively (#1185).**
146
+ `matchBlockHeaderDetailed` is now shared so `health-endpoints.mjs` no longer maintains its
147
+ own `HEADER_RE`.
148
+ - **Five readers now go through the canonical `sessions.jsonl` collapse instead of counting
149
+ raw lines (#1186).** `telemetry/sync.mjs` and `build-live-signals.mjs` (wave 2), plus
150
+ `site-numbers.mjs` and `vault-mirror.mjs --kind session` (wave 3), now read
151
+ newest-per-`session_id`; on this repo the session count measured 289 lines → 278 sessions
152
+ (11 duplicate lines collapsed). Two tests pinning the old raw-line count were removed as no
153
+ longer testing real behaviour.
154
+ - **Wave-completion refusals are now named events, not silent skips (#1201).**
155
+ `KNOWN_TRIGGERS` plus three explicit wrapper functions (`runReconcileFromSkill`/
156
+ `AtSessionEnd`/`FromPhaseSkip`) replace an implicit trigger string — an unrecognised
157
+ trigger now throws instead of silently defaulting. `emitFinalWaveCompleted` emits
158
+ `orchestrator.wave.final_refused` for all six refusal paths instead of leaving them
159
+ unrecorded — closing the same class of instrument gap named in
160
+ `.claude/rules/host-resources.md` § HR-105.
161
+
162
+ ### Fixed
163
+
164
+ - **`orchestrator.agent.stopped` carried `agent: ""` in 86.7% of fleet records (#1190).**
165
+ 89,991 of 103,763 historical records carried an empty `agent` because
166
+ `input?.agent_type ?? 'unknown'` never fires on an empty string — the harness's actual
167
+ shape. `agent` is now omitted when empty (never `'unknown'`, never a fabricated `0`/`false`).
168
+ Seven optional sidecar-derived fields were added — `agent_id`, `transcript_found`,
169
+ `tool_use_id`, `agent_type_meta`, `duration_ms` + `duration_source` (`meta-birthtime`), and
170
+ `status` (`done|partial|blocked|failed|no-tests-needed`, from the last line-anchored
171
+ `STATUS:` marker in the final 64 KiB of the agent transcript) — each independently omitted
172
+ when unmeasured, each wrapped in its own try/catch, with a charset guard on `agent_id`
173
+ before path interpolation. `CURRENT_SCHEMA_VERSION` is unchanged (additive). Measured
174
+ `status` coverage: 61.7% (71 done / 3 partial / 46 absent) over 120 completed sidecars.
175
+ - **`commands[]` was empty in every usage ping (#1189).** `classifyInvocationName` routes
176
+ plugin-prefixed command names (`session-orchestrator:session`) into `commands[]` under the
177
+ bare name — the prefix is required, so a bare foreign name never becomes one of our
178
+ commands — and `.command`-field names are prefixed before classification, since that field
179
+ is itself the "this is one of ours" provenance signal a bare `.skill` arrival lacks.
180
+ - **The final `orchestrator.wave.completed` of every session was never emitted (#1193).**
181
+ Fleet-wide: 1,018 `started` vs. 722 `completed`, a gap of exactly 296 — one per wave run.
182
+ `hooks/on-session-end.mjs` now emits it at SessionEnd, idempotent via a
183
+ `last_wave_completed` high-water mark written monotonically by both emitters, gated on
184
+ ownership of `current-session.json` via the RAW stdin session id (not the resolved one), and
185
+ skipped on `reason: clear`. `hooks/post-tool-batch-wave-signal.mjs` gained the same
186
+ raw-stdin-id ownership check, so it can no longer write wave keys into a peer session's
187
+ record either.
188
+ - **`orchestrator.session.stopped.duration_ms` was a literal `0` in 8,127 of 8,127 fleet
189
+ records.** The harness never sends `start_ms`, so the old expression fell back to a
190
+ fabricated zero — indistinguishable from a real instant turn. It is now derived from an
191
+ OWNED `.orchestrator/session.lock` (`duration_source: 'session-lock'`, session-elapsed
192
+ measured at turn end) or omitted entirely, never `0`.
193
+ - **`resolveSession()`'s ownership predicate was self-fulfilling** when the stdin `session_id`
194
+ was absent or non-UUID: it fell back to the recorded id and then compared the fallback
195
+ against itself. Fixed at the root (`rawStdinId !== null && rawStdinId === recordedId`) in
196
+ `hooks/on-session-end.mjs` for `duration_ms`, `semantic_session_id` and the new wave-completed
197
+ emit, plus the sibling site `resolveSessionId()` in `hooks/on-stop.mjs`.
198
+ - **A live local `session.lock` no longer hides a same-working-copy registry peer (GH#67).**
199
+ Registry-sourced entries in `scripts/lib/session-discovery.mjs` / `peer-discovery.mjs` now
200
+ carry additive `registryOnly` / `lockSuperseded` / `lockOwnerId` annotations instead of
201
+ being filtered out — `lockSuperseded: true` is a HINT (the lock is advisory, so the entry
202
+ may still be a live session that lost the acquire race), never a verdict. Only
203
+ PROMOTION_OFFER decisions downgrade such a peer to advisory; peer-count and display
204
+ consumers keep it. Lock-sourced sessions carry none of the three fields, so their shape
205
+ stays byte-identical to pre-GH#67.
206
+ - **Worktree-promotion teardown is now mechanical, not skill prose (#1170).**
207
+ `enterWorktree()` takes an optional `rawSessionId`; when given, it calls `leaveSourceRoot()`
208
+ on the OLD root after the destination provably exists and records the outcome on
209
+ `result.left`. Before #1170, four promotion call sites relied on coordinator prose to
210
+ release the source root — measured zero `deregisterSelf`/`release(` call sites across the
211
+ affected skill docs, so the abandoned registry entry advertised a phantom peer for up to an
212
+ hour.
213
+ - **`memory.propose` filed wave-N+1 proposals into the wave-N quota bucket (#1166).**
214
+ `scripts/memory-propose.mjs` now resolves the RUNNING wave from `<state-dir>/wave-scope.json`
215
+ first (when present and unbound or bound to this session), falling back to `STATE.md
216
+ current-wave + 1` only when no usable manifest exists — `current-wave` itself always
217
+ records the JUST-COMPLETED wave.
218
+ - **`health-endpoints` silently dropped the wizard's own nested-block output (#1174).** The
219
+ key was read off the flat KV map via `_coerceList`, which bails to `null` on any `{` and
220
+ cannot see a nested YAML block at all. `scripts/lib/config/health-endpoints.mjs` now parses
221
+ it content-scoped, accepting inline object arrays, the nested block form (top-level or
222
+ under `ecosystem-health:`), and a bare URL list; `ecosystem-health`'s own valueless
223
+ block-header form is now read as a fallback when the scalar key is absent.
224
+ - **`emitEvent()`'s automatic session/semantic-session fill now requires a process-local
225
+ witness (#1177).** `CLAUDE_CODE_SESSION_ID` (or the hook-input session id) must exactly
226
+ equal the lock's raw `session_id` before `session_id`/`semantic_session_id` are
227
+ auto-filled — STATE.md is never treated as a witness. `wave` is filled the same way, only
228
+ from a `wave-scope.json` manifest bound to this same session, and is coerced to an integer.
229
+ See `docs/events-schema.md` § Correlation keys.
230
+ - **The memory banner's "sessions ever" stat now counts distinct sessions, not JSONL lines
231
+ (#1167).** `sessions.jsonl` is append-only, so one physical session can occupy two lines;
232
+ on this repo the stat drops from 286 lines to 275 sessions. Five readers now go through
233
+ the canonical collapse: cold-start detection, the memory banner, harness-audit Category 1,
234
+ evolve's autopilot-effectiveness/verdict, and autopilot's id-set guard.
235
+ - **`check-skill-script-paths` no longer goes blind past an unbalanced code fence (#1176).**
236
+ An unclosed/unbalanced fence now reports as its own `unbalanced-fence` finding instead of
237
+ silently absorbing the rest of the file as "inside a fence"; a fence opened inside a
238
+ blockquote is now recognised as a fence too.
239
+ - **`sessions-canonical.mjs`'s `supersedes` collapse now requires an attestable join key
240
+ (#1167).** A `supersedes` pointer on an abandoned record is honoured only when it can be
241
+ matched to the record it claims to supersede; rule ordering was fixed so a superseded
242
+ record's synthetic double-stub twin is collapsed too. A new `keepUnidentified` option lets
243
+ callers retain records the canonicalizer could not confidently classify instead of
244
+ dropping them.
245
+ - **`memory.propose` no longer trusts an unbound `wave-scope.json` (#1177).** A manifest
246
+ without a `semantic_session` binding is a peer's or a stale artefact, not this session's
247
+ own — it is now ignored with a stderr note instead of adopted; an unreadable/malformed
248
+ manifest also WARNs on stderr before falling back to `current-wave + 1`, rather than
249
+ failing silently.
250
+ - **The vault-status board lock's stale-override is now surfaced, not silent (#1180).**
251
+ `onLockOutcome` reports a `staleOverride` reason when the lock was force-acquired past a
252
+ stale holder, and `sweepBoard()`'s board event now records it too — distinguishing "the
253
+ mutex worked as designed" from "a slow host looked like a crash".
254
+ - **`check-hooks-emit-event-guard` and `check-validator-registration` both went live in
255
+ `validate-plugin` (#1183, #1184).** `check-hooks-emit-event-guard.mjs` (#1183) parses hooks
256
+ via `@babel/parser` (AST, not regex) to catch a hook that mutates state without emitting a
257
+ corresponding event; 5 pre-existing sites are grandfathered into an explicit WARN baseline
258
+ with a reason and stale-detection instead of silently passing. `check-validator-registration.mjs`
259
+ (#1184) confirms every validator module is actually wired into `validate-plugin.mjs`
260
+ (measured 33 of 33 wired, using comment-stripped source so a commented-out registration
261
+ doesn't count as live).
262
+ - **`check-skill-script-paths`'s new `--strict-sh` mode found its own premise refuted
263
+ (#1187).** Extending the dead-path scan to shell-script citations, only 1 of 27 `.sh`
264
+ references sits inside the tool's own scan directories (`skills/`, `commands/`, `agents/`)
265
+ — the other 26 are in `docs/`, alongside 7 dead `.mjs` citations inside `docs/adr/` the same
266
+ run surfaced. Shipped as a WARN-only opt-in flag; the `docs/` sweep is a follow-up.
267
+ - **The discovery-validator's dedupe key, gate-summary skip, and masking order all landed
268
+ together (#1198).** A dedupe key (`session_id`, `agent_id`, `sha256(claim)`) collapses
269
+ repeated flags of the identical claim; gate-summary recap lines (the coordinator's own
270
+ restatement of a subagent's findings, not a fresh claim) are now skipped entirely; claim
271
+ masking now runs before every pattern is checked, not a subset. Measured against the
272
+ flagged corpus: 186 of 400 flagged claims were false positives. Two claims from this
273
+ session's own discovery briefing were checked against the fix and refuted.
274
+ - **A pre-push scratch-vs-publish check now reads the two arguments git already passes it.**
275
+ `.husky/pre-push` had never read `$1` (remote name/URL) or `$2` (remote URL) — confirmed by
276
+ `rg -n '\$1|\$2' .husky/pre-push` finding only a local function's own arguments — so every
277
+ push ran the full quality gate regardless of destination. It now skips the gate for a raw,
278
+ unconfigured scratch URL (e.g. the remote-offload sync target) while continuing to gate
279
+ real publish remotes; a named ceiling covers a real but unlisted third publish host, which
280
+ still reads as scratch.
281
+ - **`check-untracked-test-deps` misclassified `.git` as an untracked file inside a linked
282
+ worktree.** In a normal checkout `.git` is a directory and was already excluded; in a
283
+ `git worktree add` checkout `.git` is a FILE containing `gitdir: <path>`, which satisfied
284
+ the same `isFile()` check the untracked-candidate branch uses. `.git` is now explicitly
285
+ exempted regardless of which shape it takes — measured 62 of 62 tests passing across the 5
286
+ affected test files inside a linked worktree checkout.
287
+
10
288
  ## [3.23.0] - 2026-08-28
11
289
 
12
290
  Two commits today (1 `feat`, 1 `fix`; 64 files, +4,971/−184) close Waves 3 and 4 of the
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Session Orchestrator
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![Version](https://img.shields.io/badge/version-3.23.0-blue.svg)](CHANGELOG.md)
4
+ [![Version](https://img.shields.io/badge/version-3.24.0-blue.svg)](CHANGELOG.md)
5
5
  [![npm](https://img.shields.io/npm/v/session-orchestrator.svg)](https://www.npmjs.com/package/session-orchestrator)
6
- [![Tests](https://img.shields.io/badge/tests-12%2C000%2B-brightgreen.svg)](docs/telemetry/telemetry-claims.md)
6
+ [![Tests](https://img.shields.io/badge/tests-15%2C000%2B-brightgreen.svg)](docs/telemetry/telemetry-claims.md)
7
7
 
8
8
  Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on **Claude Code, Codex CLI, Cursor, and [Pi](docs/pi-setup.md)**.
9
9
 
@@ -72,11 +72,11 @@ Everything else is opt-in. See [`docs/session-config-template.md`](docs/session-
72
72
 
73
73
  ## What you get
74
74
 
75
- - **48 skills** for the session lifecycle (start, plan, execute, close, evolve), discovery, vault sync, MCP authoring, debugging, brainstorming, plan grilling, persona panels, cross-repo dispatch, learning→rule reconciliation, session-process eval, audits, and more
75
+ - **49 skills** for the session lifecycle (start, plan, execute, close, evolve), discovery, vault sync, MCP authoring, debugging, brainstorming, plan grilling, persona panels, cross-repo dispatch, learning→rule reconciliation, session-process eval, audits, and more
76
76
  - **28 slash commands** (`/session`, `/go`, `/close`, `/discovery`, `/plan`, `/grill`, `/evolve`, `/autopilot`, `/dispatcher`, `/reconcile`, `/eval`, `/test`, `/debug`, …)
77
77
  - **15 typed subagents** (code-implementer, test-writer, security-reviewer, session-reviewer, qa-strategist, architect-reviewer, …)
78
78
  - **10 hook event types** enforcing scope, blocking destructive commands, gating templates-first, capturing telemetry — full on Claude Code; experimental, post-hoc, or bridged on the other platforms ([Platform support](#platform-support))
79
- - **10,000+ vitest tests** run on every commit ([telemetry methodology](docs/telemetry/telemetry-claims.md))
79
+ - **15,000+ vitest tests** run on every commit ([telemetry methodology](docs/telemetry/telemetry-claims.md))
80
80
 
81
81
  Full component inventory: [`docs/components.md`](docs/components.md).
82
82
 
@@ -133,19 +133,21 @@ The system is markdown-driven config plus a thin Node runtime — skills, comman
133
133
  - **Cross-session learning is opt-in and inspectable.** Every session writes a record; after 5+ sessions `/evolve analyze` extracts confidence-scored patterns you can read and prune. Nothing is hidden.
134
134
  - **VCS dual support, no lock-in.** Auto-detects GitLab or GitHub from your remote and drives the full lifecycle for both.
135
135
 
136
- ## Recent highlights (v3.23.0)
136
+ ## Recent highlights (v3.24.0)
137
137
 
138
- Every release is additive and backward-compatible. Highlights of the v3.23.0 line: the first shaped by three external bug reports on the public mirror (Kanevry#64, #65, #66), all three reproduced, fixed and live-verified:
138
+ Every release is additive and backward-compatible. Highlights of the v3.24.0 line: parallel-session identity moves from repo-scoped to process-local, remote work gets a declared host channel, and four duplicated-copy classes collapse to one module each:
139
139
 
140
- - **Codex CLI mints UUIDv7 session ids; every reader accepted only v4 (#66 / #1091)** — each SessionStart minted a fresh v4, so a resumed or compacted thread read its own lock as a foreign session. `parseSessionId` now accepts RFC 9562 versions 1–8 and the stop/end hooks apply the writer's rule, so one id owns the lock from start through release. The `UUID_V4_RE` alias is gone: zero importers, and a name that said v4 while matching v1–8.
141
- - **Every `/close` wrote 0 of 5 recommendation fields (#65 / #1036)** the documented Phase 3.7a call passed `undefined` where a repo root is required, and the fail-open catch hid it on every run. The snippet binds the root; the catch now names the cause. A second defect found while verifying the fix: backticks in a comment inside a `node -e "…"` string made bash execute `undefined` on each close.
142
- - **Codex copies a marketplace plugin and starts the MCP child with no plugin-root variable (#64)** — measured: the copy lives under `~/.codex/plugins/cache/<marketplace>/session-orchestrator/<version>/`, and from a non-git cwd the launcher resolved to `/scripts/mcp-server.sh`. `.mcp.json` and `plugin-root.mjs` gained a cache-scan tier with a name-matched `package.json`, and `.mcp.json` now mirrors the module's tier order under two drift tests. Existing installs need a reinstall — Codex snapshots `.mcp.json` at install time.
143
- - **Worktree-Auto-Promotion is a process boundary, not a live migration (#1069, ADR-0013)** — the source session deregisters and releases its lock before the new worktree's session acquires (`leaveSourceRoot()`), which removes the phantom peer that stayed visible for up to 60 minutes. Because the new session's id never equals the worktree suffix, Phase 4a cleanup keys on a promotion marker written at creation time; the review panel found that key dead before any user did.
144
- - **The host registry contributed nothing to session numbering (#1066)** — the census projected only raw UUIDs, which the n-increment discards. It now counts `semantic_session_id`, so two sessions on one host cannot mint the same label. The semantic id stays a best-effort label; ownership remains the raw id plus owner proof.
145
- - **The mode selector scored a field no record carries (#1071)** `completion_rate` sits under `effectiveness` in all 281 ledger records; the flat read was always `undefined`, so the high-completion bonus was unreachable and the fixtures pinned a shape production never writes. Fixed with a nested-first read and a divisor test for the 99 records that carry no rate at all.
146
- - **Semgrep regained two rules a path filter had dropped (#1129)** — re-aimed at this repo's real trust boundary (hook stdin, child-process stdout), taint-mode; the first true positives were three unguarded `JSON.parse` calls on `glab`/`gh` output in the CI banner. A proposed spread-sink was refused with a measurement: object spread cannot pollute a prototype.
140
+ - **Parallel-session identity moves from repo-scoped to process-local (#1194 / #1188)** — `enforce-scope` now reads only process-local session ids (`readProcessLocalSessionIds`), so a peer's manifest in the same working copy is treated as foreign, not as your own; `memory-propose` authorizes off a process-local match of the raw session id rather than trusting the lock file alone. A peer's declared scope also gets its own place in the wave manifest now, as a `peer-session-<id>` record instead of a silent gap (#1195).
141
+ - **Remote-offload: declare hosts, route heavy roles to a ready host instead of halving the wave (#1160)** a `remote-hosts:` config block, an `ssh:<alias>` dispatch channel, and an offload decision wired into the resource gate right after the HR-004 cap (readiness is injected, never measured inside the gate itself). `skills/remote-offload/SKILL.md` is now the generic entry point, with adapters around the `offload` CLI.
142
+ - **Four copy-classes collapsed to one module each (#1181 / #1182 / #1196 / #1197)** — atomic-JSON handling, subagent sidecar-path derivation, `expandHome`, and markdown-fence parsing each had 4–8 divergent copies; consolidating `atomic-json.mjs` also closed a data-loss path where a read-modify-write default silently replaced an unparsable or unreadable file.
143
+ - **Refusals are now events, not silence (#1200 / #1201)** — `emitFinalWaveCompleted` writes `orchestrator.wave.final_refused` for six refusal paths, evolve and dialectic completions emit their own events, and reconcile's trigger set now lives in code (`KNOWN_TRIGGERS`) instead of being asserted in prose.
144
+ - **Canonical session counting, fleet-wide (#1186)** — `site-numbers` and `vault-mirror --kind session` now count the same way everywhere: 289 raw lines in this repo's own ledger collapsed to 278 real sessions once double-counts were removed.
145
+ - **Discovery-validator false positives cut 186 of 400 measured hits were the harness's own gate-summary lines (#1198)** a dedupe key and a gate-summary skip removed the noise; two brief claims about the masking order were also measured and refuted along the way.
146
+ - **Three new mechanical guards land in `validate-plugin` (#1183 / #1184 / #1187)** — `emitEvent` try/catch coverage, validator-registration parity (every check actually wired in), and dead `.sh` citations in docs.
147
+ - **The pre-push gate distinguishes a publish push from a scratch push; linked-worktree gates run green (15,829/0 on the remote host)** — a `.git` file (not a directory) in a linked worktree was previously misread as an untracked candidate, flagging 13 tracked files as missing.
148
+ - **The AUQ clarity guard's delivery is proven live, not asserted (#1122)** — `check-auq-clarity` is wired into `validate-plugin` and blocks on the two hard hurdles at a measured 0% false-positive rate; its own wiring closes with a fake-regression proof, not a claim.
147
149
 
148
- Previous line (v3.22.0): instruments that confidently measured the wrong quantity the 99%-firing resource warning, the AUQ audit, and the lock-release identity split.
150
+ Previous line (v3.23.0): three external bug reports fixed end-to-end (Codex session-id versions, missing `/close` fields, plugin-root resolution), the worktree-promotion process boundary, and three measurement fixes (host-registry numbering, the mode-selector's mis-shaped read, two regained Semgrep rules).
149
151
 
150
152
  Full version history: [CHANGELOG.md](CHANGELOG.md).
151
153
 
@@ -93,7 +93,6 @@ Append a fenced ```json block at the end of your response per `agents/schemas/db
93
93
  ```
94
94
 
95
95
  Required: `status`, `task_id`, `files_changed`, `blockers`. Optional: `verdict`. **Emit `verdict` alongside `status` (status→verdict mapping: done→PROCEED, partial→PROCEED_WITH_FOLLOWUPS, blocked→BLOCKED). `status` is deprecated and will be removed in v4.0 (#472).** The coordinator parses the LAST fenced ```json block.
96
- ```
97
96
 
98
97
  ## Edge Cases
99
98