session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -36,6 +36,16 @@
36
36
  * dispatcher: (optional; host-local cross-repo dispatcher autonomy override — #679)
37
37
  * autonomy: string ('' = no override; resolver enum off | advisory | autonomous-gated;
38
38
  * precedence env SO_DISPATCHER_AUTONOMY > this > committed > off)
39
+ * vault-integration: (optional; host-local switch for the vault-integration gate — SO#1448)
40
+ * enabled: boolean (false = off on this host; true is ignored — the host may not enable)
41
+ * mode: 'off' | 'warn' | 'strict' (applied only when weaker than the committed mode;
42
+ * precedence env SO_VAULT_INTEGRATION > this > committed.
43
+ * An invalid value drops ONLY this section, see loadOwnerConfig)
44
+ * vault-dirs: (optional list; per-directory vault-dir — agents/vault#319)
45
+ * - path: string (the vault directory)
46
+ * match: { path-prefix: string } (required; cwd directory prefix, first match wins)
47
+ * name: string (optional label, defaults to path)
48
+ * precedence env SO_VAULT_DIR > this > paths.vault-dir > committed
39
49
  *
40
50
  * ── Exports ───────────────────────────────────────────────────────────────────
41
51
  *
@@ -44,7 +54,8 @@
44
54
  * validateOwnerSections(obj) — pure validation, no I/O; bucketed per section (#820)
45
55
  * validateOwnerConfig(obj) — pure validation, no I/O; thin wrapper over validateOwnerSections
46
56
  * loadOwnerConfig({path?}) — reads file; per-section tolerance for OPTIONAL
47
- * sections (paths/dispatcher/vaults/baselines) — #820
57
+ * sections (paths/dispatcher/vault-integration/vaults/
58
+ * baselines/vault-dirs) — #820
48
59
  * writeOwnerConfig(config, {path?}) — validates, writes YAML, creates dir
49
60
  * getDefaults() — returns sensible default config object
50
61
  *
@@ -202,9 +213,12 @@ const REQUIRED_SECTIONS = /** @type {const} */ (['owner', 'tone', 'efficiency',
202
213
  * sections — currently `scripts/lib/owner-config-banner.mjs`, which derived the
203
214
  * #1244 "survived a whole-file discard" set from a hand-mirrored local copy.
204
215
  * Frozen so a consumer cannot mutate the shared array.
216
+ *
217
+ * `vault-integration` has no `getDefaults()` entry: absent means "no host
218
+ * override", so a dropped section simply leaves the committed gate in force.
205
219
  */
206
220
  export const OPTIONAL_OBJECT_SECTIONS = /** @type {const} */ (
207
- Object.freeze(['paths', 'dispatcher'])
221
+ Object.freeze(['paths', 'dispatcher', 'vault-integration'])
208
222
  );
209
223
 
210
224
  /**
@@ -213,7 +227,10 @@ export const OPTIONAL_OBJECT_SECTIONS = /** @type {const} */ (
213
227
  * `parseNamedVaults`/`parseBaselines` — that already drops bad entries with
214
228
  * its own WARN). Surfaced here only via `sectionWarnings`, never dropped.
215
229
  */
216
- const OPTIONAL_LIST_SECTIONS = /** @type {const} */ (['vaults', 'baselines']);
230
+ const OPTIONAL_LIST_SECTIONS = /** @type {const} */ (['vaults', 'baselines', 'vault-dirs']);
231
+
232
+ /** Host-settable vault-integration modes (mirrors VAULT_MODE_VALUES in config-schema.mjs). */
233
+ const VALID_VAULT_INTEGRATION_MODES = ['off', 'warn', 'strict'];
217
234
 
218
235
  /** Canonical validation-order — MUST match the pre-#820 inline validation order. */
219
236
  const SECTION_ORDER = /** @type {const} */ ([
@@ -421,6 +438,35 @@ export function validateOwnerSections(obj) {
421
438
  sections.dispatcher = { valid: errors.length === 0, errors };
422
439
  }
423
440
 
441
+ // ── vault-integration (optional; host-local gate switch — SO#1448) ──────────
442
+ // STRICT enum validation, unlike `dispatcher`: a typo like `mode: of` must not
443
+ // fall through to the committed `strict` silently while the operator believes
444
+ // the host is off. An invalid section is dropped (and reported) on its own —
445
+ // `paths` and every other section survive (#820 per-section tolerance).
446
+ {
447
+ const errors = [];
448
+ const vi = obj['vault-integration'];
449
+ if (vi !== undefined && vi !== null) {
450
+ if (!isPlainObject(vi)) {
451
+ errors.push('vault-integration must be an object when present');
452
+ } else {
453
+ if (vi.enabled !== undefined && vi.enabled !== null && typeof vi.enabled !== 'boolean') {
454
+ errors.push(`vault-integration.enabled must be a boolean, got: ${typeof vi.enabled}`);
455
+ }
456
+ if (
457
+ vi.mode !== undefined &&
458
+ vi.mode !== null &&
459
+ !VALID_VAULT_INTEGRATION_MODES.includes(vi.mode)
460
+ ) {
461
+ errors.push(
462
+ `vault-integration.mode must be one of ${VALID_VAULT_INTEGRATION_MODES.join(', ')}, got: ${JSON.stringify(vi.mode)}`,
463
+ );
464
+ }
465
+ }
466
+ }
467
+ sections['vault-integration'] = { valid: errors.length === 0, errors };
468
+ }
469
+
424
470
  // ── vaults (optional; N named vaults for walk-up resolution — #700) ──────────
425
471
  // Drop-and-WARN on malformed entries is handled by parseNamedVaults() in
426
472
  // named-vault-resolver.mjs. Here we only validate the container shape to
@@ -512,6 +558,47 @@ export function validateOwnerSections(obj) {
512
558
  sections.baselines = { valid: errors.length === 0, errors };
513
559
  }
514
560
 
561
+ // ── vault-dirs (optional; per-directory vault-dir — agents/vault#319) ────────
562
+ // Same entry shape as baselines: (and parsed by the same parseBaselines() with
563
+ // section 'vault-dirs'), except `name` is optional. List section: surfaced via
564
+ // sectionWarnings, never dropped — the resolver drops bad entries itself.
565
+ {
566
+ const errors = [];
567
+ const vaultDirs = obj['vault-dirs'];
568
+ if (vaultDirs !== undefined && vaultDirs !== null) {
569
+ if (!Array.isArray(vaultDirs)) {
570
+ errors.push('vault-dirs must be an array when present');
571
+ } else {
572
+ for (let i = 0; i < vaultDirs.length; i++) {
573
+ const entry = vaultDirs[i];
574
+ if (!isPlainObject(entry)) {
575
+ errors.push(`vault-dirs[${i}] must be an object`);
576
+ continue;
577
+ }
578
+ if (typeof entry.path !== 'string' || entry.path.trim() === '') {
579
+ errors.push(`vault-dirs[${i}].path must be a non-empty string`);
580
+ }
581
+ if (
582
+ entry.name !== undefined &&
583
+ entry.name !== null &&
584
+ (typeof entry.name !== 'string' || entry.name.trim() === '')
585
+ ) {
586
+ errors.push(`vault-dirs[${i}].name must be a non-empty string when present`);
587
+ }
588
+ if (!isPlainObject(entry.match)) {
589
+ errors.push(`vault-dirs[${i}].match must be an object`);
590
+ } else if (
591
+ typeof entry.match['path-prefix'] !== 'string' ||
592
+ entry.match['path-prefix'].trim() === ''
593
+ ) {
594
+ errors.push(`vault-dirs[${i}].match.path-prefix must be a non-empty string`);
595
+ }
596
+ }
597
+ }
598
+ }
599
+ sections['vault-dirs'] = { valid: errors.length === 0, errors };
600
+ }
601
+
515
602
  const errors = SECTION_ORDER.flatMap((name) => sections[name].errors);
516
603
  return { sections, errors };
517
604
  }
@@ -540,10 +627,10 @@ export function validateOwnerConfig(obj) {
540
627
  * Per-section tolerance (#820): an invalid REQUIRED section (owner, tone,
541
628
  * efficiency, hardware-sharing) still discards the whole file (legacy
542
629
  * behaviour, unchanged). An invalid OPTIONAL object section (paths,
543
- * dispatcher) is instead replaced by its default value — the rest of the
630
+ * dispatcher, vault-integration) is instead replaced by its default value — the rest of the
544
631
  * file survives, `source` becomes `'partial'`, and the drop is reported via
545
632
  * `droppedSections` + a stderr WARN. OPTIONAL list sections (vaults,
546
- * baselines) are passed through UNTOUCHED even when strict-invalid — their
633
+ * baselines, vault-dirs) are passed through UNTOUCHED even when strict-invalid — their
547
634
  * consumers already run a lenient parse-at-point-of-use pass — and are
548
635
  * surfaced only via `sectionWarnings` (never dropped, never counted towards
549
636
  * `'partial'`).
@@ -658,6 +745,14 @@ export function loadOwnerConfig(opts = {}) {
658
745
  discardDropped.push({ section: name, errors: sec.errors });
659
746
  }
660
747
  }
748
+ // `vault-dirs:` (agents/vault#319) survives the discard too, raw — exactly as
749
+ // on the required-valid branch below; parseBaselines() drops bad entries at
750
+ // point-of-use. Dropping it would hand a repo under a declared prefix the
751
+ // host-wide `paths.vault-dir` (which DOES survive above): the silent vault
752
+ // override #319 exists to prevent, triggered by an unrelated `tone:` typo.
753
+ if (parsed['vault-dirs'] !== undefined && parsed['vault-dirs'] !== null) {
754
+ discardConfig['vault-dirs'] = parsed['vault-dirs'];
755
+ }
661
756
  const discardResult = { config: discardConfig, source: 'defaults', errors: allErrors };
662
757
  if (discardDropped.length > 0) discardResult.droppedSections = discardDropped;
663
758
  return discardResult;
@@ -673,7 +768,10 @@ export function loadOwnerConfig(opts = {}) {
673
768
  for (const name of OPTIONAL_OBJECT_SECTIONS) {
674
769
  const sec = sections[name];
675
770
  if (sec && !sec.valid) {
676
- config[name] = defaults[name];
771
+ // A section without a default (vault-integration) is removed, not set to
772
+ // `undefined`: js-yaml's dump rejects undefined values on a write-back.
773
+ if (defaults[name] === undefined) delete config[name];
774
+ else config[name] = defaults[name];
677
775
  droppedSections.push({ section: name, errors: sec.errors });
678
776
  const firstError = sec.errors[0] ?? 'invalid section';
679
777
  console.warn(
@@ -261,7 +261,7 @@ export function validatePersonaSpec(rawSpec, sourcePath) {
261
261
  pushError(
262
262
  'model',
263
263
  'enum',
264
- `model must be one of ${[...ALLOWED_MODEL_ALIASES].join('|')} or a full model ID like "claude-opus-4-7" (got ${JSON.stringify(m)})`,
264
+ `model must be one of ${[...ALLOWED_MODEL_ALIASES].join('|')} or a full model ID like "claude-opus-5-5" (got ${JSON.stringify(m)})`,
265
265
  );
266
266
  }
267
267
  }
@@ -8,12 +8,12 @@
8
8
  * - plain constants (no filesystem work): SO_SHARED_DIR, SO_OS, SO_IS_WINDOWS,
9
9
  * SO_IS_WSL, SO_PATH_SEP
10
10
  * - pure resolvers: detectPlatform, resolvePluginRoot, resolveProjectDir,
11
- * resolveStateDir, resolveConfigFile
11
+ * resolveSessionRoot, resolveStateDir, resolveConfigFile
12
12
  *
13
13
  * NOTHING in this module touches the filesystem at import time.
14
14
  */
15
15
 
16
- import { existsSync, statSync } from 'node:fs';
16
+ import { existsSync, realpathSync, statSync } from 'node:fs';
17
17
  import os from 'node:os';
18
18
  import path from 'node:path';
19
19
  import { resolvePluginRoot as _resolvePluginRootRobust } from './plugin-root.mjs';
@@ -249,6 +249,210 @@ export function resolveProjectDir(platform) {
249
249
  return cwd;
250
250
  }
251
251
 
252
+ // ---------------------------------------------------------------------------
253
+ // resolveSessionRoot (#1492)
254
+ // ---------------------------------------------------------------------------
255
+
256
+ /**
257
+ * Basename of a worktree the HARNESS creates for an `isolation: "worktree"`
258
+ * subagent: `agent-` + the agent id. Measured 2026-10-02 over this host's
259
+ * transcripts: 13481 `.claude/worktrees/agent-<hex>` hits, every agent id of the
260
+ * form `a` + 16 hex (`agent-a00828908cdd83bd8`). A worktree a SESSION enters
261
+ * (`EnterWorktree`, `claude --worktree <name>`) lives in the same directory under
262
+ * a chosen name (`landing-produktschau`, `s71-politur`), so the name is the only
263
+ * discriminator the path offers.
264
+ */
265
+ const HARNESS_AGENT_WORKTREE_RE = /^agent-[0-9a-f]{8,}$/;
266
+
267
+ /**
268
+ * The first non-blank launch-dir env var, in `resolveProjectDir()` order, or `''`.
269
+ * Trimmed: a whitespace-only value is truthy and would otherwise win
270
+ * (`development.md` § Error Handling, env-var fallback whitespace trap).
271
+ *
272
+ * The ONE definition of "the launch dir the harness states": the clamp in
273
+ * {@link resolveSessionRoot} and the relocation trace in `hooks/enforce-scope.mjs`
274
+ * must read the same chain, so a second copy of it is a second definition.
275
+ *
276
+ * @returns {string}
277
+ */
278
+ export function launchDirFromEnv() {
279
+ for (const name of ['CLAUDE_PROJECT_DIR', 'CODEX_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'PI_PROJECT_DIR']) {
280
+ const value = (process.env[name] || '').trim();
281
+ if (value !== '') return value;
282
+ }
283
+ return '';
284
+ }
285
+
286
+ /**
287
+ * The nearest directory at or above `cwd` holding a `.git` entry — a directory,
288
+ * or the FILE a linked worktree carries — or `''` when none does. Git's own repo
289
+ * discovery walks up looking for exactly this entry, so where git answers, both
290
+ * agree (measured 2026-10-02: same answer, 0.017 ms here vs 4.57 ms for a
291
+ * `git rev-parse --show-toplevel` spawn). Existence only — the entry is not
292
+ * validated, which git would do.
293
+ *
294
+ * Not `walkUpFor(cwd, '.git', 'any')`: its home-directory boundary exists for
295
+ * PLATFORM markers (#1139); a repo whose root is `$HOME` is still a repo root.
296
+ *
297
+ * @param {string} cwd
298
+ * @returns {string}
299
+ */
300
+ export function dotGitAncestor(cwd) {
301
+ for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) {
302
+ if (existsSync(path.join(dir, '.git'))) return dir;
303
+ if (path.dirname(dir) === dir) return '';
304
+ }
305
+ }
306
+
307
+ /**
308
+ * `<P>` when `root` is a harness subagent worktree `<P>/.claude/worktrees/agent-<hex>`
309
+ * whose parent `<P>` is itself a working copy; otherwise `root` unchanged.
310
+ *
311
+ * @param {string} root
312
+ * @returns {string}
313
+ */
314
+ function _liftHarnessAgentWorktree(root) {
315
+ if (!HARNESS_AGENT_WORKTREE_RE.test(path.basename(root))) return root;
316
+ const worktreesDir = path.dirname(root);
317
+ if (path.basename(worktreesDir) !== 'worktrees') return root;
318
+ const stateDir = path.dirname(worktreesDir);
319
+ if (path.basename(stateDir) !== '.claude') return root;
320
+ const parent = path.dirname(stateDir);
321
+ return existsSync(path.join(parent, '.git')) ? parent : root;
322
+ }
323
+
324
+ /**
325
+ * The NATIVE realpath, or the resolved spelling when the path does not exist.
326
+ *
327
+ * `realpathSync.native`, not the JS `realpathSync`: on a case-insensitive
328
+ * volume (APFS default) the JS form resolves symlinks but keeps the CALLER's
329
+ * letter case, so `/users/me/repo` stayed `/users/…` beside the git-found
330
+ * `/Users/…`, `path.relative` answered `../../users/…` and the clamp was
331
+ * skipped (#1504 point 8, measured 2026-10-03 on a home dir typed lowercase:
332
+ * JS `/users/<me>`, native `/Users/<me>`). Every comparison in the clamp chain
333
+ * goes through this one function — mixing the two forms re-opens the split.
334
+ *
335
+ * @param {string} p
336
+ * @returns {string}
337
+ */
338
+ function _canonical(p) {
339
+ try { return realpathSync.native(p); } catch { return path.resolve(p); }
340
+ }
341
+
342
+ /**
343
+ * Is `child` a STRICT descendant of `parent` (never `parent` itself)? Both sides
344
+ * canonical, so macOS `/tmp` vs `/private/tmp` cannot split one directory in two.
345
+ *
346
+ * @param {string} child
347
+ * @param {string} parent
348
+ * @returns {boolean}
349
+ */
350
+ function _isStrictlyInside(child, parent) {
351
+ const rel = path.relative(_canonical(parent), _canonical(child));
352
+ return rel !== '' && rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
353
+ }
354
+
355
+ /**
356
+ * The SESSION ROOT (#1492) — the working copy whose control files belong to the
357
+ * session a hook fires in: `wave-scope.json`, the `.orchestrator/` ledgers, the
358
+ * event records. One resolver for every hook that reads them (enforce-scope,
359
+ * enforce-commands, post-bash-write-verify, pre-task-scope-disjoint).
360
+ *
361
+ * NOT the launch dir whenever the session works elsewhere: `$CLAUDE_PROJECT_DIR`
362
+ * stays on the dir the session was LAUNCHED in after it enters a worktree
363
+ * (docs/en/hooks § "Worktrees are different"), while the coordinator writes
364
+ * `wave-scope.json` into that worktree. Read from the launch dir, the manifest
365
+ * was absent and every scope gate allowed everything for the whole session,
366
+ * silently (#1492, measured in session main-2026-10-02-session-21); preferred
367
+ * for the dispatch ledger it also gave every worktree session of one launch dir
368
+ * ONE shared ledger and the wave key `w?` (measured on be6a2e3e).
369
+ *
370
+ * NOT the payload `cwd` itself: it follows the session's `cd` (docs/en/hooks
371
+ * § "cwd follows Claude"), and a `cd sub` must not move the session's state into
372
+ * `<root>/sub` — a second, empty ledger whose collisions were allowed (#1489 Pkt 6).
373
+ *
374
+ * NEVER ABOVE the launch dir: a session launched in a repo SUBDIRECTORY (a
375
+ * monorepo package, a project under a git-tracked `$HOME`) keeps its manifest
376
+ * there. The repo root above it holds none, so resolving to it switched every
377
+ * scope gate off (review HIGH-1 on 4fdbc469, measured DENY → ALLOW).
378
+ *
379
+ * Precedence:
380
+ * 1. no payload `cwd` → `resolveProjectDir()`, the pre-#1492 answer unchanged;
381
+ * 2. the repo root of `cwd` — `toplevel` when the caller already holds
382
+ * `git rev-parse --show-toplevel` of it, else the nearest `.git` ancestor
383
+ * (`dotGitAncestor`), which answers the same without a spawn, so neither a
384
+ * timed-out git (review MED on 63f35e8c) nor a hot-path budget can send a
385
+ * worktree session back to the launch dir — lifted to `<P>` when it is a
386
+ * harness subagent worktree `<P>/.claude/worktrees/agent-<hex>` (below),
387
+ * then replaced by the launch dir from env (`launchDirFromEnv`) when it
388
+ * lies strictly ABOVE it;
389
+ * 3. the launch dir from env (`CLAUDE_PROJECT_DIR` → `CODEX_PROJECT_DIR` →
390
+ * `CURSOR_PROJECT_DIR` → `PI_PROJECT_DIR`), only when `cwd` is in no repo;
391
+ * 4. `cwd`.
392
+ *
393
+ * The clamp reads the SAME launch-dir chain as rung 3, not `$CLAUDE_PROJECT_DIR`
394
+ * alone. The Cursor and Pi bridges delete `CLAUDE_PROJECT_DIR` and set their
395
+ * own `*_PROJECT_DIR` to the payload `cwd` on every call (`cursor-hook-bridge.mjs`,
396
+ * `pi-hook-bridge.mjs`); a clamp on `$CLAUDE_PROJECT_DIR` alone therefore never
397
+ * fired there, and a Cursor/Pi workspace in a repo SUBDIRECTORY climbed to the
398
+ * repo root, found no manifest and allowed every write (review HIGH-1 on
399
+ * b57e572c, measured end-to-end through `cursor-hook-bridge.mjs`: DENY at
400
+ * ff3e43b5 → ALLOW). For a Claude session nothing changes — `$CLAUDE_PROJECT_DIR`
401
+ * is first in the chain. For a bridge session the answer is the payload `cwd`
402
+ * whenever it sits in a repo, which is exactly what the bridges resolved before
403
+ * #1492 (`resolveProjectDir()` → their `*_PROJECT_DIR` → the payload `cwd`).
404
+ * The clamp does not ask whether `cwd` lies inside the launch dir: an
405
+ * `isolation: "worktree"` agent's `cwd` never does, and it must clamp too (see
406
+ * THE LIFT).
407
+ *
408
+ * THE LIFT. An `isolation: "worktree"` subagent's hook payload carries the
409
+ * worktree the harness made for it, `<P>/.claude/worktrees/agent-<hex>`. That
410
+ * worktree is a repo root of its own and holds no manifest, so rung 2 alone
411
+ * would disarm scope enforcement for every such agent. The harness puts it under
412
+ * the LAUNCH checkout, so `<P>` is what `$CLAUDE_PROJECT_DIR` resolved to before
413
+ * #1492 — the lift keeps exactly that, and the clamp keeps it for a launch in a
414
+ * subdirectory of `<P>`. It is keyed on the PATH SHAPE, never on which candidate
415
+ * holds a manifest: a root picked because a control file is readable there
416
+ * would let any directory that lacks one become the deciding root.
417
+ *
418
+ * Ceilings (BV-004):
419
+ * - An agent of a session that ENTERED a worktree is still unenforced, as before
420
+ * #1492: its worktree sits under the launch checkout (measured: session
421
+ * 0d5e4fc3, 2026-09-25 — cwd `<main>/.claude/worktrees/landing-produktschau`,
422
+ * agent worktreePath `<main>/.claude/worktrees/agent-acd5bfd0aa590e2e8`), so
423
+ * the lift lands on the launch root, where that session has no manifest.
424
+ * Fixing it needs a design (e.g. find the manifest bound to the payload
425
+ * `session_id` among the repo's worktrees). Revisit when an entered-worktree
426
+ * session dispatches `isolation: "worktree"` agents under a manifest.
427
+ * - Rung 2 is `cd`-relocatable: a session whose `cwd` sits in ANOTHER repo (a
428
+ * submodule, a nested or sibling checkout) is checked against that repo's
429
+ * manifest — usually none, so its gates allow — where the launch dir's
430
+ * manifest used to apply. Same relocation `pre-task-scope-disjoint` has
431
+ * keyed its ledger on since #1489. Revisit if an `orchestrator.scope.*`
432
+ * record or a review ever shows an out-of-scope write made after such a `cd`.
433
+ * - A session worktree NAMED like a harness one (`agent-` + 8 or more hex) is
434
+ * lifted to its parent — the pre-#1492 launch-dir behaviour for that one
435
+ * session, not a new failure. Revisit if the harness changes its naming.
436
+ * - A Cursor/Pi bridge session follows its payload `cwd`, as it did before
437
+ * #1492: the bridges hand over no launch dir distinct from that `cwd`, so a
438
+ * bridge session whose `cwd` moves into a subdirectory without a manifest is
439
+ * unenforced there. Not a new failure — the pre-#1492 bridge answer. Revisit
440
+ * when a bridge hands over a stable workspace root separately from the
441
+ * payload `cwd` — then clamp on that instead.
442
+ *
443
+ * @param {string|undefined} cwd the hook payload's `cwd`
444
+ * @param {string} [toplevel] `git rev-parse --show-toplevel` of `cwd`, `''` when unknown
445
+ * @returns {string} Absolute path
446
+ */
447
+ export function resolveSessionRoot(cwd, toplevel = '') {
448
+ if (typeof cwd !== 'string' || cwd.trim() === '') return resolveProjectDir();
449
+ const repoRoot = (typeof toplevel === 'string' ? toplevel : '') || dotGitAncestor(cwd);
450
+ if (!repoRoot) return launchDirFromEnv() || cwd;
451
+ const root = _liftHarnessAgentWorktree(repoRoot);
452
+ const launch = launchDirFromEnv();
453
+ return launch !== '' && _isStrictlyInside(launch, root) ? launch : root;
454
+ }
455
+
252
456
  // ---------------------------------------------------------------------------
253
457
  // resolveStateDir
254
458
  // ---------------------------------------------------------------------------
@@ -260,7 +260,9 @@ async function resolveLatestVersion({ cacheDir, now, fetchImpl }) {
260
260
  * @type {Record<"claude"|"codex"|"cursor"|"pi", string>}
261
261
  */
262
262
  const PLATFORM_UPDATE_INSTRUCTIONS = {
263
- claude: '/plugin update session-orchestrator@kanevry, then restart Claude Code',
263
+ claude:
264
+ '/plugin update session-orchestrator@kanevry, then restart Claude Code ' +
265
+ '(from a clone: `npm run update:local` also installs the deps Claude Code refuses — docs/install.md § Upgrade)',
264
266
  codex:
265
267
  'run `codex plugin marketplace upgrade kanevry && codex plugin add session-orchestrator@kanevry` ' +
266
268
  '(docs/codex-setup.md), then restart Codex',