session-orchestrator 3.22.0 → 3.23.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 (268) 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/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  74. package/.cursor/skills/session-end/SKILL.md +13 -0
  75. package/.cursor/skills/session-plan/SKILL.md +13 -0
  76. package/.cursor/skills/session-start/SKILL.md +13 -0
  77. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  78. package/.cursor/skills/spinout/SKILL.md +12 -0
  79. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  80. package/.cursor/skills/test-runner/SKILL.md +13 -0
  81. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  82. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  83. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  84. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  85. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  86. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  87. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  88. package/.mcp.json +4 -1
  89. package/CHANGELOG.md +168 -0
  90. package/README.md +18 -15
  91. package/agents/AGENTS.md +23 -4
  92. package/agents/code-implementer.md +2 -1
  93. package/agents/db-specialist.md +2 -1
  94. package/agents/docs-writer.md +3 -1
  95. package/agents/eval-judge.md +1 -1
  96. package/agents/session-reviewer.md +7 -1
  97. package/agents/test-writer.md +2 -1
  98. package/agents/ui-developer.md +2 -1
  99. package/commands/bootstrap.md +2 -2
  100. package/commands/close.md +3 -1
  101. package/commands/go.md +1 -1
  102. package/commands/journey-audit.md +43 -0
  103. package/docs/USER-GUIDE.md +2 -2
  104. package/docs/ci-setup.md +14 -0
  105. package/docs/codex-setup.md +64 -0
  106. package/docs/components.md +6 -6
  107. package/docs/cursor-setup.md +26 -47
  108. package/docs/events-schema.md +76 -4
  109. package/docs/github-mirror-protection.md +197 -0
  110. package/docs/pi-setup.md +2 -0
  111. package/docs/rule-authoring.md +3 -1
  112. package/docs/scope-collision-guard.md +49 -2
  113. package/docs/session-config-reference.md +26 -4
  114. package/docs/session-config-template.md +4 -3
  115. package/docs/telemetry.md +22 -0
  116. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  117. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  118. package/hooks/enforce-scope.mjs +64 -0
  119. package/hooks/hooks-codex.json +1 -1
  120. package/hooks/hooks-cursor.json +201 -20
  121. package/hooks/hooks-pi.json +1 -1
  122. package/hooks/hooks.json +2 -2
  123. package/hooks/on-session-end.mjs +211 -10
  124. package/hooks/on-session-start.mjs +214 -11
  125. package/hooks/on-stop.mjs +48 -9
  126. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  127. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  128. package/hooks/pre-bash-issue-budget.mjs +117 -4
  129. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  130. package/hooks/pre-bash-staging-fence.mjs +4 -0
  131. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  132. package/hooks/skill-invocation-telemetry.mjs +21 -10
  133. package/monitors/monitors.json +6 -0
  134. package/package.json +1 -1
  135. package/pi/prompts/journey-audit.md +12 -0
  136. package/rules/_index.md +9 -1
  137. package/rules/always-on/ask-via-tool.md +62 -0
  138. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  139. package/rules/always-on/build-value.md +47 -0
  140. package/rules/always-on/cross-session-messaging.md +59 -0
  141. package/rules/always-on/loop-and-monitor.md +221 -0
  142. package/rules/always-on/parallel-sessions.md +142 -12
  143. package/rules/always-on/receiving-review.md +108 -0
  144. package/rules/always-on/test-value.md +40 -0
  145. package/rules/always-on/verification-before-completion.md +77 -0
  146. package/scripts/archive-closed-prds.mjs +258 -18
  147. package/scripts/autopilot.mjs +5 -0
  148. package/scripts/backfill-evidence-digest.mjs +376 -0
  149. package/scripts/cursor-install.mjs +89 -48
  150. package/scripts/export-hw-learnings.mjs +143 -2
  151. package/scripts/express-path.mjs +299 -0
  152. package/scripts/generate-cursor-adapter.mjs +253 -0
  153. package/scripts/github-protection-audit.mjs +358 -0
  154. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  155. package/scripts/lib/build-live-signals.mjs +24 -5
  156. package/scripts/lib/ci-status-banner.mjs +158 -11
  157. package/scripts/lib/command-blocker.mjs +70 -0
  158. package/scripts/lib/config/reconcile.mjs +79 -4
  159. package/scripts/lib/config/section-extractor.mjs +235 -36
  160. package/scripts/lib/config-schema.mjs +9 -1
  161. package/scripts/lib/config.mjs +57 -6
  162. package/scripts/lib/convergence-monitor.mjs +13 -2
  163. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  164. package/scripts/lib/dispatcher/cli.mjs +2 -2
  165. package/scripts/lib/express-path.mjs +327 -0
  166. package/scripts/lib/file-lock.mjs +22 -4
  167. package/scripts/lib/gates/gate-full.mjs +81 -8
  168. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  169. package/scripts/lib/git-config-drift.mjs +134 -5
  170. package/scripts/lib/host-identity.mjs +247 -2
  171. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  172. package/scripts/lib/issue-budget.mjs +229 -30
  173. package/scripts/lib/learnings/io.mjs +55 -10
  174. package/scripts/lib/learnings/schema.mjs +95 -28
  175. package/scripts/lib/lock-reaper.mjs +7 -1
  176. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  177. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  178. package/scripts/lib/memory-banner.mjs +5 -2
  179. package/scripts/lib/memory-paths.mjs +15 -6
  180. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  181. package/scripts/lib/platform.mjs +72 -9
  182. package/scripts/lib/plugin-root.mjs +143 -19
  183. package/scripts/lib/project-hygiene.mjs +43 -3
  184. package/scripts/lib/quality-gate.mjs +271 -13
  185. package/scripts/lib/reconcile/emitter.mjs +87 -19
  186. package/scripts/lib/reconcile/engine.mjs +281 -13
  187. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  188. package/scripts/lib/reconcile/renderer.mjs +148 -3
  189. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  190. package/scripts/lib/reconcile/writer.mjs +415 -84
  191. package/scripts/lib/rule-loader.mjs +37 -2
  192. package/scripts/lib/rules-sync.mjs +51 -8
  193. package/scripts/lib/scope-gate.mjs +90 -0
  194. package/scripts/lib/session-close-backfill.mjs +369 -28
  195. package/scripts/lib/session-discovery.mjs +13 -3
  196. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  197. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  198. package/scripts/lib/session-id.mjs +30 -14
  199. package/scripts/lib/session-identity/own-session.mjs +159 -0
  200. package/scripts/lib/session-lock.mjs +85 -30
  201. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  202. package/scripts/lib/session-schema/validator.mjs +40 -0
  203. package/scripts/lib/session-start-probes.mjs +608 -0
  204. package/scripts/lib/session-transition.mjs +277 -0
  205. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  206. package/scripts/lib/spiral-carryover.mjs +90 -9
  207. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  208. package/scripts/lib/state-md/mission-status.mjs +350 -52
  209. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  210. package/scripts/lib/state-md.mjs +12 -2
  211. package/scripts/lib/telemetry/sync.mjs +46 -8
  212. package/scripts/lib/validate/check-agents.mjs +66 -0
  213. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  214. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  215. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  216. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  217. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  218. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  219. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  220. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  221. package/scripts/lib/validate/repo-files.mjs +275 -0
  222. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  223. package/scripts/lib/vault-mirror/process.mjs +99 -43
  224. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  225. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  226. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  227. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  228. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  229. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  230. package/scripts/materialize-wave-scope.mjs +209 -12
  231. package/scripts/mcp-server.sh +11 -2
  232. package/scripts/parse-config.mjs +65 -0
  233. package/scripts/token-audit.sh +9 -2
  234. package/scripts/validate-plugin.mjs +3 -0
  235. package/scripts/validate-wave-scope.mjs +67 -0
  236. package/scripts/vault-mirror.mjs +203 -34
  237. package/skills/_shared/monitor-patterns.md +31 -5
  238. package/skills/_shared/parallel-aware-auq.md +1 -1
  239. package/skills/_shared/parallel-aware-preamble.md +4 -2
  240. package/skills/_shared/platform-tools.md +11 -5
  241. package/skills/_shared/state-ownership.md +29 -2
  242. package/skills/autopilot/SKILL.md +5 -1
  243. package/skills/bootstrap/SKILL.md +3 -3
  244. package/skills/bootstrap/_shared-template.md +18 -10
  245. package/skills/bootstrap/deep-template.md +10 -6
  246. package/skills/bootstrap/fast-template.md +15 -8
  247. package/skills/bootstrap/standard-template.md +10 -6
  248. package/skills/claude-md-drift-check/checker.mjs +39 -11
  249. package/skills/dispatcher/SKILL.md +1 -1
  250. package/skills/journey-audit/SKILL.md +269 -0
  251. package/skills/peekaboo-driver/SKILL.md +15 -3
  252. package/skills/persona-panel/SKILL.md +1 -1
  253. package/skills/reconcile/SKILL.md +41 -1
  254. package/skills/session-end/SKILL.md +17 -4
  255. package/skills/session-end/metrics-collection.md +7 -4
  256. package/skills/session-end/phase-3-6-tail.md +11 -3
  257. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  258. package/skills/session-plan/SKILL.md +6 -1
  259. package/skills/session-plan/wave-template.md +1 -0
  260. package/skills/session-start/SKILL.md +30 -16
  261. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  262. package/skills/session-start/phase-8-5-express-path.md +77 -12
  263. package/skills/vault-sync/validator.mjs +31 -0
  264. package/skills/wave-executor/SKILL.md +4 -2
  265. package/skills/wave-executor/circuit-breaker.md +34 -9
  266. package/skills/wave-executor/wave-loop.md +102 -19
  267. package/templates/_shared/journey-manifest.md +110 -0
  268. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -6,7 +6,8 @@
6
6
  * aliases) or parent barrel.
7
7
  *
8
8
  * Exports: normalizeSession
9
- * Module-private: _warnedMissingSchemaVersion (Set, per-process dedupe)
9
+ * Module-private: _warnedMissingSchemaVersion (Set, per-process dedupe),
10
+ * isPlainObject, _canonicalizeExpressPath
10
11
  */
11
12
 
12
13
  import { SESSION_KEY_ALIASES } from './constants.mjs';
@@ -19,19 +20,82 @@ import { SESSION_KEY_ALIASES } from './constants.mjs';
19
20
  // per process, preventing log-spam on large sessions.jsonl files.
20
21
  const _warnedMissingSchemaVersion = new Set();
21
22
 
23
+ // ---------------------------------------------------------------------------
24
+ // Internal helper (intentional duplication — Option 1 submodule isolation,
25
+ // mirroring aliases.mjs / validator.mjs rather than adding a cross-import)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function isPlainObject(v) {
29
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
30
+ }
31
+
32
+ /**
33
+ * Collapse the legacy OBJECT form of `express_path` onto the canonical
34
+ * BOOLEAN form, in place on the caller's already-copied `next` object.
35
+ *
36
+ * WHY BOOLEAN IS CANONICAL. The ledger carried the field in two forms with no
37
+ * arbiter: 20 boolean records (14 `false`, 6 `true`) and exactly ONE object,
38
+ * `{activated, tasks, notes}` on `main-2026-05-01-housekeeping-2` (measured
39
+ * 2026-08-23 over 271 records). That single record is a hand-composition
40
+ * artefact, not a richer designed variant: it is the only one of the 21 with
41
+ * no top-level `notes`, and its `express_path.notes` holds session-level
42
+ * narrative that every other record puts in the canonical top-level `notes`.
43
+ * It also carries the same era's ad-hoc vocabulary elsewhere
44
+ * (`waves_completed`, `issues_new`, `cross_repo_writes`), so the object form
45
+ * is that composition freedom, not a schema. The question the field exists to
46
+ * answer — "greift der Express Path?" (docs/prd/2026-08-22-framework-
47
+ * verschlankung.md § VS-1) — is a boolean question whose denominator is the
48
+ * 14 `false` records.
49
+ *
50
+ * NOTHING IS DISCARDED. The original object is preserved verbatim under
51
+ * `_express_path_detail`, following the module's own "original keys are
52
+ * preserved alongside their canonical alias" convention and the
53
+ * `_completed_at_conflict` forensics tag in aliases.mjs. The conversion is
54
+ * therefore deterministic AND reversible.
55
+ *
56
+ * A shape this function cannot recognise (string, number, array, or an object
57
+ * without a boolean `activated`) is passed through UNTOUCHED — normalizeSession
58
+ * never throws. Refusing such a shape is the write path's job; validator.mjs
59
+ * rejects it there.
60
+ *
61
+ * BOUNDED TOLERANCE — REVISIT TRIGGER: the legacy object form is accepted only
62
+ * because one historical record uses it. When that record leaves the ledger (or
63
+ * is rewritten by a migration), delete this function and the matching
64
+ * `express_path` object clause in validator.mjs — canonical-boolean-only.
65
+ *
66
+ * @param {Record<string, any>} next — mutable copy of the entry being normalized
67
+ */
68
+ function _canonicalizeExpressPath(next) {
69
+ if (!('express_path' in next)) return;
70
+ const raw = next.express_path;
71
+ if (!isPlainObject(raw)) return; // boolean (canonical), null, or unrecognised
72
+ if (typeof raw.activated !== 'boolean') return; // not the legacy shape
73
+
74
+ // Non-clobber, matching the alias rule above: never overwrite a sidecar that
75
+ // a previous normalization pass (or a producer) already wrote.
76
+ if (!('_express_path_detail' in next)) {
77
+ next._express_path_detail = raw;
78
+ }
79
+ next.express_path = raw.activated;
80
+ }
81
+
22
82
  // ---------------------------------------------------------------------------
23
83
  // Public API
24
84
  // ---------------------------------------------------------------------------
25
85
 
26
86
  /**
27
- * Normalize a session entry read from disk. Applies SAFE key aliases, tags
87
+ * Normalize a session entry read from disk. Applies SAFE key aliases, collapses
88
+ * the legacy object form of `express_path` onto its canonical boolean, and tags
28
89
  * legacy entries without `schema_version` as 0 (distinct from
29
90
  * CURRENT_SESSION_SCHEMA_VERSION=2 which is stamped on new writes; bumped
30
91
  * 1 -> 2 via #372).
31
92
  *
32
93
  * Never throws. Malformed input (null, non-object, array) is passed through
33
94
  * unchanged. Original keys are preserved alongside their canonical alias for
34
- * debugging.
95
+ * debugging; the pre-collapse `express_path` object is preserved under
96
+ * `_express_path_detail` for the same reason.
97
+ *
98
+ * Idempotent: normalize(normalize(x)) deep-equals normalize(x).
35
99
  *
36
100
  * @param {any} entry
37
101
  * @returns {any} normalized entry (or original if non-object)
@@ -48,6 +112,9 @@ export function normalizeSession(entry) {
48
112
  }
49
113
  }
50
114
 
115
+ // express_path — same key, two shapes. Collapse onto the canonical boolean.
116
+ _canonicalizeExpressPath(next);
117
+
51
118
  // schema_version — legacy entries tagged as 0 (NOT CURRENT_SESSION_SCHEMA_VERSION).
52
119
  if ('schema_version' in next && next.schema_version !== undefined) {
53
120
  // Preserve existing version.
@@ -431,6 +431,46 @@ function _validateOptionalFields(entry) {
431
431
  }
432
432
  }
433
433
  }
434
+
435
+ // `express_path` — CANONICAL FORM IS BOOLEAN (`true` = the express path was
436
+ // taken, `false` = it was offered and declined). Until now the field had no
437
+ // arbiter at all: the ledger carried 20 booleans and 1 object
438
+ // (`{activated, tasks, notes}`) and nothing in the schema knew the key, so
439
+ // neither shape could be called wrong. See the rationale + the 2026-08-23
440
+ // census in normalizer.mjs `_canonicalizeExpressPath`.
441
+ //
442
+ // The legacy `{activated: boolean, ...}` object is a BOUNDED read tolerance,
443
+ // in the same spirit as `_validateSchemaVersion` accepting [0,1,2,3] while
444
+ // only CURRENT_SESSION_SCHEMA_VERSION is ever written. It is bounded in two
445
+ // ways: an object WITHOUT a boolean `activated` is refused (the tolerance
446
+ // cannot grow into "any object"), and `normalizeSession` collapses the
447
+ // legacy object to the canonical boolean on every read, so no consumer
448
+ // downstream of the read path ever observes two shapes. Rejecting the legacy
449
+ // object outright here would have cost exactly one historical record its
450
+ // clean bill of health in `checkSessionsIntegrity` for zero benefit —
451
+ // measured 2026-08-23: `validateSession` fails 0 of 271 records today.
452
+ //
453
+ // REVISIT TRIGGER: when no ledger record uses the object form any more, drop
454
+ // the `isPlainObject` branch below and require a boolean outright.
455
+ if (entry.express_path !== undefined && entry.express_path !== null) {
456
+ const ep = entry.express_path;
457
+ if (typeof ep !== 'boolean' && !(isPlainObject(ep) && typeof ep.activated === 'boolean')) {
458
+ const shape = Array.isArray(ep) ? 'array' : typeof ep;
459
+ throw new ValidationError(
460
+ `express_path must be a boolean (canonical), null, or the legacy {activated: boolean} object, got: ${shape}`
461
+ );
462
+ }
463
+ }
464
+
465
+ // `_express_path_detail` — forensic sidecar written by `normalizeSession`
466
+ // when it collapses a legacy object `express_path` onto its boolean. Holds
467
+ // the pre-collapse object verbatim so the conversion stays reversible.
468
+ // Same optional-object contract as `effectiveness` / `discovery_stats`.
469
+ if (entry._express_path_detail !== undefined && entry._express_path_detail !== null) {
470
+ if (!isPlainObject(entry._express_path_detail)) {
471
+ throw new ValidationError('_express_path_detail must be an object or null');
472
+ }
473
+ }
434
474
  }
435
475
 
436
476
  // ---------------------------------------------------------------------------