session-orchestrator 3.22.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 (316) 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/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -18,6 +18,7 @@
18
18
  *
19
19
  * Exports:
20
20
  * GENERATOR_MARKER — frontmatter sentinel identifying generator-owned files
21
+ * NARRATIVE_EVENT — canonical telemetry event name for a mirror attempt
21
22
  * extractNarrative — pure: STATE.md contents → { waveHistory, deviations, whatNotToRetry, missionStatus }
22
23
  * renderNarrative — pure: narrative + repo + now → full markdown (frontmatter + body)
23
24
  * writeNarrative — idempotent write with skip-handwritten / skip-noop / dry-run + _overview refusal
@@ -28,8 +29,8 @@
28
29
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
29
30
  import { readFile } from 'node:fs/promises';
30
31
  import path from 'node:path';
31
- import os from 'node:os';
32
32
 
33
+ import { emitEvent, sessionAttribution } from '../events.mjs';
33
34
  import { parseStateMd, parseMissionStatus } from '../state-md.mjs';
34
35
  import {
35
36
  parseFrontmatter,
@@ -41,6 +42,7 @@ import { matchesModuloRedaction } from '../vault-mirror/process.mjs';
41
42
  import { readConfigFile, parseSessionConfig } from '../config.mjs';
42
43
  import { validatePathInsideProject } from '../path-utils.mjs';
43
44
  import { createSecretValueMasker } from '../secret-masker.mjs';
45
+ import { expandTilde } from '../common.mjs';
44
46
 
45
47
  /** Frontmatter sentinel that identifies generator-owned narrative files. */
46
48
  export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
@@ -61,23 +63,6 @@ const SECTION_TITLES = {
61
63
 
62
64
  // ── Raw section extraction ──────────────────────────────────────────────────────
63
65
 
64
- /**
65
- * Expand a leading `~` to the current user's home directory.
66
- *
67
- * NOTE: deferred shared-helper extraction. The same `expandHome` pattern lives in
68
- * other vault-status modules; W2 forbids introducing a shared new file, so this is
69
- * inlined here. Consolidate into a shared util in a follow-up wave.
70
- *
71
- * @param {string} p
72
- * @returns {string}
73
- */
74
- function expandHome(p) {
75
- if (typeof p !== 'string' || p.length === 0) return p;
76
- if (p === '~') return os.homedir();
77
- if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
78
- return p;
79
- }
80
-
81
66
  /**
82
67
  * Match an ATX markdown heading line. Returns `{ level, text }` or null.
83
68
  *
@@ -557,6 +542,128 @@ function resolveLooseSlug(vaultDir, candidateSlug, fsSeam = {}) {
557
542
  return matches.length === 1 ? matches[0] : candidateSlug;
558
543
  }
559
544
 
545
+ // ── Telemetry ────────────────────────────────────────────────────────────────────
546
+
547
+ /**
548
+ * Canonical event name for a narrative-mirror attempt (issue #1129).
549
+ *
550
+ * ONE event per {@link mirrorNarrative} call — the REJECTION and NO-OP paths
551
+ * included, because those are the point. Before this existed, a vault-disabled
552
+ * config, a missing STATE.md, a hand-authored target file and a healthy write
553
+ * were all indistinguishable from the ledger's side: measured 2026-08-23 over
554
+ * 28 387 records in `.orchestrator/metrics/events.jsonl`, ZERO board/mirror
555
+ * events, because this module did not import {@link emitEvent} at all. Its only
556
+ * caller is shell prose in `skills/session-end/session-metrics-write.md`, whose
557
+ * `mode: warn` degradation prints a WARNING and closes the session anyway — so
558
+ * an outage of this writer left no durable trace whatsoever.
559
+ *
560
+ * Name deliberately NOT minted here: it is the one the wave assigned, so the
561
+ * fact "a narrative mirror ran" has exactly one address in the ledger.
562
+ */
563
+ export const NARRATIVE_EVENT = 'orchestrator.vault.narrative_mirrored';
564
+
565
+ /**
566
+ * Emit the narrative-mirror telemetry record. Best-effort: never throws, never
567
+ * alters the mirror result.
568
+ *
569
+ * ABSENT IS NOT ZERO (`docs/events-schema.md`): every optional field is spread
570
+ * conditionally, so an UNMEASURED field is MISSING from the record rather than
571
+ * written as `0`. `chars: 0` would then honestly mean "the narrative rendered to
572
+ * an empty document"; an absent `chars` means "no render was reached on this
573
+ * path" — reading the missing key as `0` conflates the two in both directions.
574
+ *
575
+ * NAMED CEILING (build-value BV-004): when `repoRoot` is absent or empty this
576
+ * emits NOTHING. `emitEvent` without `opts.repoRoot` falls back to
577
+ * `SO_PROJECT_DIR` (#941), so the only destination left for a rootless call is
578
+ * whichever repo the process happens to sit in — i.e. a caller that named no
579
+ * repo would silently append to a FOREIGN ledger, and the two call shapes that
580
+ * reach this branch today are the `repoRoot: ''` / omitted-`repoRoot` unit
581
+ * tests, which would then write synthetic records into this repo's real fleet
582
+ * telemetry on every suite run. REVISIT TRIGGER: if a production caller ever
583
+ * legitimately invokes `mirrorNarrative` without a `repoRoot`, that call needs a
584
+ * destination decision of its own — not this silent skip.
585
+ *
586
+ * @param {object} opts
587
+ * @param {string} [opts.repoRoot] — pins the ledger to THIS repo's
588
+ * `.orchestrator/metrics/events.jsonl` (#941), and supplies session attribution.
589
+ * @param {string} opts.action — the `action` the mirror returned, or `'error'`
590
+ * when the writer threw. ALWAYS present.
591
+ * @param {string} [opts.path] — resolved narrative path, when one was resolved.
592
+ * @param {number} [opts.chars] — length of the rendered narrative document this
593
+ * call produced. Present whenever the render was reached (so also on
594
+ * `skipped-noop` / `dry-run`, where the document was built but not written —
595
+ * `action` is what says whether it landed); absent on the earlier no-op paths,
596
+ * which return before anything is rendered.
597
+ * @param {string} [opts.errorCode] — `err.code` on the throw path (a bounded
598
+ * token such as `EACCES`). The error MESSAGE is deliberately NOT recorded: it
599
+ * can quote a path or STATE.md content, and this module's whole reason for
600
+ * masking (#1025) is that such prose carries secrets.
601
+ * @returns {Promise<void>}
602
+ */
603
+ /**
604
+ * Reduce an absolute vault path to its LAST TWO segments for telemetry.
605
+ *
606
+ * The full path is the module's public return contract and stays untouched.
607
+ * What must not travel is the path in the EMITTED payload: on a real host it
608
+ * reads `/Users/<name>/Projects/<vault>/01-projects/<private-slug>/…`, i.e. an
609
+ * OS username plus a private project slug. Those are exactly the two shapes
610
+ * `scripts/lib/validate/check-owner-leakage.mjs` blocks as CP1 and CP6 — and
611
+ * that scanner structurally cannot see this one, because it walks `git ls-files`
612
+ * and `.orchestrator/metrics/*.jsonl` is gitignored (`.gitignore:40`).
613
+ * The record is invisible to the pre-commit guard and visible to the optional
614
+ * Clank webhook (`scripts/lib/events.mjs`, `CLANK_EVENT_URL`), which posts the
615
+ * payload verbatim with no redaction.
616
+ *
617
+ * The BASENAME is the deliberate ceiling — one segment, not two. Two segments
618
+ * would keep the parent directory, and under `01-projects/` that directory IS
619
+ * the private project slug, i.e. exactly the CP6 shape this is meant to drop.
620
+ * The diagnostic value lives in the filename alone: it says WHICH writer ran
621
+ * (`_session-narrative.md` vs `_active-sessions.md`), which is the question the
622
+ * event exists to answer. Which project it was is already answerable from the
623
+ * record's own `session_id` / repo-scoped ledger location.
624
+ * Revisit trigger: a consumer that needs more than the filename — then it
625
+ * belongs in the RETURN value, which already carries the absolute path, never
626
+ * in the event.
627
+ *
628
+ * @param {unknown} outputPath
629
+ * @returns {string|undefined} `undefined` when there is nothing measured to report.
630
+ */
631
+ function telemetrySafePath(outputPath) {
632
+ if (typeof outputPath !== 'string' || outputPath.length === 0) return undefined;
633
+ const base = path.basename(outputPath);
634
+ return base.length > 0 ? base : undefined;
635
+ }
636
+
637
+ async function emitNarrativeEvent({ repoRoot, action, path: outputPath, chars, errorCode }) {
638
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
639
+ try {
640
+ await emitEvent(
641
+ NARRATIVE_EVENT,
642
+ {
643
+ action,
644
+ ...(telemetrySafePath(outputPath) !== undefined ? { path_tail: telemetrySafePath(outputPath) } : {}),
645
+ // `typeof … === 'number'` rather than `!= null`: the repo's eslint
646
+ // `eqeqeq: always` forbids the loose form, and this shape additionally
647
+ // refuses a non-numeric `chars` outright. A measured `0` still lands.
648
+ ...(typeof chars === 'number' ? { chars } : {}),
649
+ // `typeof === 'string'`, not truthiness: an `err.code` of '' is a measured
650
+ // empty code, and the sibling `chars` field four lines up already states
651
+ // why this file rejects the loose form.
652
+ ...(typeof errorCode === 'string' && errorCode.length > 0 ? { error_code: errorCode } : {}),
653
+ // session_id / semantic_session_id — omitted entirely when no session
654
+ // lock is readable (CI, ad-hoc runs). See sessionAttribution's contract:
655
+ // a fabricated id would read as a real session.
656
+ ...sessionAttribution(repoRoot),
657
+ },
658
+ { repoRoot },
659
+ );
660
+ } catch {
661
+ /* Best-effort telemetry. emitEvent does real file I/O (mkdir + append), so a
662
+ read-only or occupied ledger path WILL throw — and a broken ledger must
663
+ never fail a narrative write. The mirror result is authoritative. */
664
+ }
665
+ }
666
+
560
667
  // ── Convenience orchestration ────────────────────────────────────────────────────
561
668
 
562
669
  /**
@@ -594,10 +701,50 @@ function resolveLooseSlug(vaultDir, candidateSlug, fsSeam = {}) {
594
701
  * @returns {Promise<{ action: string, path?: string }>}
595
702
  */
596
703
  export async function mirrorNarrative(opts) {
704
+ let outcome;
705
+ try {
706
+ outcome = await runNarrativeMirror(opts);
707
+ } catch (err) {
708
+ // The harshest silent-failure case: `session-metrics-write.md` catches this
709
+ // throw, prints a WARNING under `mode: warn` and closes the session anyway.
710
+ // Record it, then re-throw — telemetry observes, it never swallows.
711
+ await emitNarrativeEvent({
712
+ repoRoot: opts?.repoRoot,
713
+ action: 'error',
714
+ errorCode: typeof err?.code === 'string' ? err.code : undefined,
715
+ });
716
+ throw err;
717
+ }
718
+
719
+ await emitNarrativeEvent({
720
+ repoRoot: opts?.repoRoot,
721
+ action: outcome.result.action,
722
+ path: outcome.result.path,
723
+ chars: outcome.chars,
724
+ });
725
+
726
+ return outcome.result;
727
+ }
728
+
729
+ /**
730
+ * The mirror itself — every early return of {@link mirrorNarrative} lives here.
731
+ *
732
+ * Split out so that exactly ONE emit site covers EVERY outcome: a future early
733
+ * return added inside this function is telemetered by construction, whereas
734
+ * hand-placing an emit beside each of the seven `return`s makes "forgot the new
735
+ * one" the default failure. The `chars` companion travels beside the result
736
+ * rather than inside it because the returned object is a PUBLIC shape that
737
+ * callers (and tests) compare with `toEqual` — adding a key there would be an
738
+ * observable contract change for a purely internal measurement.
739
+ *
740
+ * @param {Parameters<typeof mirrorNarrative>[0]} opts
741
+ * @returns {Promise<{ result: { action: string, path?: string }, chars?: number }>}
742
+ */
743
+ async function runNarrativeMirror(opts) {
597
744
  const { repoRoot, repo, now = new Date(), dryRun = false, fs: injectedFs, hostPaths } = opts;
598
745
 
599
746
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
600
- return { action: 'skipped-vault-disabled' };
747
+ return { result: { action: 'skipped-vault-disabled' } };
601
748
  }
602
749
 
603
750
  // Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
@@ -606,12 +753,12 @@ export async function mirrorNarrative(opts) {
606
753
  const configText = await readConfigFile(repoRoot);
607
754
  config = parseSessionConfig(configText, { hostPaths });
608
755
  } catch {
609
- return { action: 'skipped-vault-disabled' };
756
+ return { result: { action: 'skipped-vault-disabled' } };
610
757
  }
611
758
 
612
759
  const vaultIntegration = config?.['vault-integration'];
613
760
  if (!vaultIntegration || vaultIntegration.enabled !== true) {
614
- return { action: 'skipped-vault-disabled' };
761
+ return { result: { action: 'skipped-vault-disabled' } };
615
762
  }
616
763
 
617
764
  // Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
@@ -628,10 +775,10 @@ export async function mirrorNarrative(opts) {
628
775
 
629
776
  const rawVaultDir = vaultIntegration['vault-dir'];
630
777
  if (!rawVaultDir || typeof rawVaultDir !== 'string') {
631
- return { action: 'skipped-vault-disabled' };
778
+ return { result: { action: 'skipped-vault-disabled' } };
632
779
  }
633
780
 
634
- const vaultDir = path.resolve(expandHome(rawVaultDir));
781
+ const vaultDir = path.resolve(expandTilde(rawVaultDir));
635
782
  const candidateSlug = subjectToSlug(repoName) || 'unknown';
636
783
  // Loose-match against existing 01-projects/ folders before minting a new
637
784
  // slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
@@ -640,9 +787,19 @@ export async function mirrorNarrative(opts) {
640
787
  const outputPath = resolveNarrativePath(vaultDir, repoSlug);
641
788
 
642
789
  // Defense-in-depth: ensure the resolved file stays inside the vault root.
643
- const inside = validatePathInsideProject(path.relative(vaultDir, outputPath), vaultDir);
790
+ //
791
+ // `canonicalizeRoot: true` (#1033) is load-bearing, not hygiene. The guard's
792
+ // realpath phase is skipped while the target is ABSENT (ENOENT) and fires once
793
+ // it EXISTS — so on a vault root reached through a symlink, run 1 writes and
794
+ // run 2 resolves the file to the canonical path, reads it as outside the
795
+ // LEXICAL root, and returns `skipped-invalid-path` forever after. Canonicalizing
796
+ // the root makes both sides of the comparison the same kind of path. Same call
797
+ // shape as scripts/lib/reconcile/writer.mjs and scripts/lib/memory-proposals/store.mjs.
798
+ const inside = validatePathInsideProject(path.relative(vaultDir, outputPath), vaultDir, {
799
+ canonicalizeRoot: true,
800
+ });
644
801
  if (!inside.ok) {
645
- return { action: 'skipped-invalid-path', path: outputPath };
802
+ return { result: { action: 'skipped-invalid-path', path: outputPath } };
646
803
  }
647
804
 
648
805
  // Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
@@ -651,7 +808,7 @@ export async function mirrorNarrative(opts) {
651
808
  try {
652
809
  stateContents = await readFile(stateMdPath, 'utf8');
653
810
  } catch {
654
- return { action: 'skipped-no-statemd', path: outputPath };
811
+ return { result: { action: 'skipped-no-statemd', path: outputPath } };
655
812
  }
656
813
 
657
814
  // #1025: the ONE masking site for the narrative mirror — after the frontmatter
@@ -660,5 +817,11 @@ export async function mirrorNarrative(opts) {
660
817
  const narrative = maskNarrative(extractNarrative(stateContents));
661
818
  const content = renderNarrative({ repo: repoName, narrative, now });
662
819
 
663
- return writeNarrative({ outputPath, content, dryRun, fs: injectedFs });
820
+ // `chars` measures the document THIS call rendered — so it is present on
821
+ // `skipped-noop` and `dry-run` too, where the render happened but nothing
822
+ // landed. `action` is the field that says whether it landed.
823
+ return {
824
+ result: writeNarrative({ outputPath, content, dryRun, fs: injectedFs }),
825
+ chars: content.length,
826
+ };
664
827
  }