session-orchestrator 3.24.0 → 4.0.1

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 (435) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +3 -2
  47. package/.codex-plugin/skills/architecture/SKILL.md +20 -0
  48. package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
  49. package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
  50. package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
  51. package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
  52. package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
  53. package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
  54. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
  55. package/.codex-plugin/skills/close/SKILL.md +21 -0
  56. package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
  57. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
  58. package/.codex-plugin/skills/debug/SKILL.md +21 -0
  59. package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
  60. package/.codex-plugin/skills/discovery/SKILL.md +21 -0
  61. package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
  62. package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
  63. package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
  64. package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
  65. package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
  66. package/.codex-plugin/skills/eli5/SKILL.md +21 -0
  67. package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
  68. package/.codex-plugin/skills/eval/SKILL.md +21 -0
  69. package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
  70. package/.codex-plugin/skills/evolve/SKILL.md +21 -0
  71. package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
  72. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
  73. package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
  74. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
  75. package/.codex-plugin/skills/go/SKILL.md +22 -0
  76. package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
  77. package/.codex-plugin/skills/grill/SKILL.md +21 -0
  78. package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
  79. package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
  80. package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
  81. package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
  82. package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
  83. package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
  84. package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
  85. package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
  86. package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
  87. package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
  88. package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
  89. package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
  90. package/.codex-plugin/skills/plan/SKILL.md +22 -0
  91. package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
  92. package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
  93. package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
  94. package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
  95. package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
  96. package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
  97. package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
  98. package/.codex-plugin/skills/release/SKILL.md +22 -0
  99. package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
  100. package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
  101. package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
  102. package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
  103. package/.codex-plugin/skills/session/SKILL.md +21 -0
  104. package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
  105. package/.codex-plugin/skills/session-end/SKILL.md +22 -0
  106. package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
  107. package/.codex-plugin/skills/session-start/SKILL.md +22 -0
  108. package/.codex-plugin/skills/spinout/SKILL.md +21 -0
  109. package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
  110. package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
  111. package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
  112. package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
  113. package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
  114. package/.codex-plugin/skills/test/SKILL.md +21 -0
  115. package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
  116. package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
  117. package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
  118. package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
  119. package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
  120. package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
  121. package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
  122. package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
  123. package/.cursor/commands/autopilot.md +2 -2
  124. package/.cursor/commands/bootstrap.md +1 -1
  125. package/.cursor/commands/brainstorm.md +1 -1
  126. package/.cursor/commands/debug.md +1 -1
  127. package/.cursor/commands/discovery.md +1 -1
  128. package/.cursor/commands/dispatcher.md +2 -2
  129. package/.cursor/commands/eli5.md +2 -2
  130. package/.cursor/commands/eval.md +2 -2
  131. package/.cursor/commands/evolve.md +1 -1
  132. package/.cursor/commands/go.md +1 -1
  133. package/.cursor/commands/grill.md +2 -2
  134. package/.cursor/commands/memory-cleanup.md +2 -2
  135. package/.cursor/commands/persona-panel.md +1 -1
  136. package/.cursor/commands/plan.md +1 -1
  137. package/.cursor/commands/portfolio.md +1 -1
  138. package/.cursor/commands/reconcile.md +2 -2
  139. package/.cursor/commands/release.md +2 -2
  140. package/.cursor/commands/session.md +2 -2
  141. package/.cursor/commands/spinout.md +2 -2
  142. package/.cursor/commands/sunset-review.md +2 -2
  143. package/.cursor/commands/templates-ack.md +2 -2
  144. package/.cursor/commands/test.md +2 -2
  145. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  146. package/.cursor/skills/eval/SKILL.md +1 -1
  147. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  148. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  149. package/.cursor-plugin/plugin.json +30 -0
  150. package/.orchestrator/policy/blocked-commands.json +121 -0
  151. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  152. package/.orchestrator/policy/quality-gates.example.json +16 -0
  153. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  154. package/.orchestrator/policy/templates-policy.json +27 -0
  155. package/.orchestrator/policy/test-profiles.json +47 -0
  156. package/AGENTS.md +225 -0
  157. package/CHANGELOG.md +1314 -2
  158. package/NOTICE +11 -6
  159. package/README.md +135 -94
  160. package/agents/eval-judge.md +1 -1
  161. package/agents/skill-applied-judge.md +1 -1
  162. package/assets/wave-lifecycle.svg +98 -0
  163. package/commands/release.md +6 -3
  164. package/commands/session.md +18 -3
  165. package/docs/README.md +4 -0
  166. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  167. package/docs/baseline.md +67 -0
  168. package/docs/ci-setup.md +108 -62
  169. package/docs/codex-setup.md +107 -29
  170. package/docs/components.md +38 -16
  171. package/docs/cursor-setup.md +6 -2
  172. package/docs/events-schema.md +9 -6
  173. package/docs/instruction-delivery.md +69 -0
  174. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  175. package/docs/migration-v4.md +365 -0
  176. package/docs/pi-setup.md +6 -1
  177. package/docs/plugin-architecture-v3.md +1 -1
  178. package/docs/rule-authoring.md +85 -19
  179. package/docs/scope-collision-guard.md +5 -5
  180. package/docs/session-config-reference.md +57 -56
  181. package/docs/session-config-template.md +6 -29
  182. package/docs/telemetry.md +157 -3
  183. package/docs/vault-docs-architecture.md +50 -11
  184. package/hooks/_lib/hook-import-set.json +1488 -0
  185. package/hooks/_lib/subagent-transcript.mjs +562 -0
  186. package/hooks/config-protection.mjs +2 -2
  187. package/hooks/cwd-change-restore.mjs +2 -2
  188. package/hooks/enforce-commands.mjs +69 -0
  189. package/hooks/hooks-codex.json +1 -1
  190. package/hooks/hooks-cursor.json +10 -0
  191. package/hooks/hooks-pi.json +5 -0
  192. package/hooks/hooks.json +6 -1
  193. package/hooks/loop-guard.mjs +3 -3
  194. package/hooks/on-session-end.mjs +2 -2
  195. package/hooks/on-session-start.mjs +103 -2
  196. package/hooks/on-stop.mjs +60 -14
  197. package/hooks/operator-steer.mjs +2 -2
  198. package/hooks/post-bash-write-verify.mjs +85 -0
  199. package/hooks/post-edit-import-probe.mjs +344 -0
  200. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  201. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  202. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  203. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  204. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  205. package/hooks/skill-invocation-telemetry.mjs +17 -5
  206. package/hooks/subagent-telemetry.mjs +13 -4
  207. package/monitors/monitors.json +3 -3
  208. package/package.json +9 -1
  209. package/pi/prompts/session.md +2 -2
  210. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  211. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  212. package/scripts/dialectic-deriver.mjs +73 -8
  213. package/scripts/export-hw-learnings.mjs +113 -1
  214. package/scripts/generate-agents-skills.mjs +378 -0
  215. package/scripts/generate-codex-skills.mjs +246 -0
  216. package/scripts/generate-cursor-adapter.mjs +45 -8
  217. package/scripts/generate-hook-import-set.mjs +292 -0
  218. package/scripts/lib/agent-status.mjs +13 -2
  219. package/scripts/lib/auto-dream.mjs +38 -36
  220. package/scripts/lib/autonomy/suitability.mjs +6 -0
  221. package/scripts/lib/autopilot/loop.mjs +2 -2
  222. package/scripts/lib/ci-status-banner.mjs +220 -75
  223. package/scripts/lib/codex/plugin-contract.mjs +88 -6
  224. package/scripts/lib/config/auto-dream.mjs +2 -1
  225. package/scripts/lib/config/block-header.mjs +8 -0
  226. package/scripts/lib/config/block-preprocess.mjs +177 -0
  227. package/scripts/lib/config/broken-window.mjs +2 -1
  228. package/scripts/lib/config/cold-start.mjs +2 -1
  229. package/scripts/lib/config/config-protection.mjs +22 -2
  230. package/scripts/lib/config/context-coverage.mjs +2 -1
  231. package/scripts/lib/config/cross-repo.mjs +2 -1
  232. package/scripts/lib/config/custom-phases.mjs +2 -1
  233. package/scripts/lib/config/dialectic.mjs +2 -1
  234. package/scripts/lib/config/discovery-validator.mjs +2 -1
  235. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  236. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  237. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  238. package/scripts/lib/config/docs-staleness.mjs +2 -1
  239. package/scripts/lib/config/drift-check.mjs +2 -1
  240. package/scripts/lib/config/eval.mjs +2 -1
  241. package/scripts/lib/config/events-rotation.mjs +2 -1
  242. package/scripts/lib/config/evolve.mjs +8 -2
  243. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  244. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  245. package/scripts/lib/config/handover-gate.mjs +2 -1
  246. package/scripts/lib/config/health-endpoints.mjs +7 -2
  247. package/scripts/lib/config/host-paths.mjs +20 -4
  248. package/scripts/lib/config/issue-budget.mjs +2 -1
  249. package/scripts/lib/config/loop-guard.mjs +2 -1
  250. package/scripts/lib/config/memory.mjs +2 -1
  251. package/scripts/lib/config/moc-staleness.mjs +2 -1
  252. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  253. package/scripts/lib/config/private-config-dir.mjs +67 -0
  254. package/scripts/lib/config/reconcile.mjs +2 -1
  255. package/scripts/lib/config/remote-hosts.mjs +2 -1
  256. package/scripts/lib/config/section-extractor.mjs +7 -1
  257. package/scripts/lib/config/skill-evolution.mjs +2 -1
  258. package/scripts/lib/config/slopcheck.mjs +2 -1
  259. package/scripts/lib/config/state-md-lock.mjs +2 -1
  260. package/scripts/lib/config/templates-first.mjs +2 -1
  261. package/scripts/lib/config/test.mjs +2 -1
  262. package/scripts/lib/config/vault-integration.mjs +7 -1
  263. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  264. package/scripts/lib/config/vault-staleness.mjs +2 -1
  265. package/scripts/lib/config/vault-sync.mjs +2 -1
  266. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  267. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  268. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  269. package/scripts/lib/convergence-monitor.mjs +82 -16
  270. package/scripts/lib/dispatcher/rank.mjs +124 -48
  271. package/scripts/lib/ecosystem-health.mjs +16 -2
  272. package/scripts/lib/eval/engine.mjs +9 -1
  273. package/scripts/lib/eval/session-resolve.mjs +23 -4
  274. package/scripts/lib/events.mjs +22 -6
  275. package/scripts/lib/frontmatter-guard.mjs +131 -13
  276. package/scripts/lib/gates/gate-full.mjs +30 -0
  277. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  278. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  279. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  280. package/scripts/lib/host-identity.mjs +50 -11
  281. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  282. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  283. package/scripts/lib/learnings/io.mjs +60 -6
  284. package/scripts/lib/memory-proposals/store.mjs +30 -22
  285. package/scripts/lib/owner-config-banner.mjs +41 -6
  286. package/scripts/lib/owner-config-loader.mjs +21 -10
  287. package/scripts/lib/owner-interview.mjs +3 -3
  288. package/scripts/lib/owner-yaml.mjs +215 -15
  289. package/scripts/lib/platform.mjs +108 -15
  290. package/scripts/lib/plugin-update-banner.mjs +414 -0
  291. package/scripts/lib/project-hygiene.mjs +38 -2
  292. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  293. package/scripts/lib/quality-gate.mjs +133 -44
  294. package/scripts/lib/reconcile/emitter.mjs +68 -6
  295. package/scripts/lib/reconcile/engine.mjs +51 -11
  296. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  297. package/scripts/lib/reconcile/writer.mjs +40 -18
  298. package/scripts/lib/session-close-backfill.mjs +67 -9
  299. package/scripts/lib/session-id.mjs +12 -23
  300. package/scripts/lib/session-identity/own-session.mjs +125 -10
  301. package/scripts/lib/session-lock-shape.mjs +43 -0
  302. package/scripts/lib/session-lock.mjs +5 -10
  303. package/scripts/lib/session-registry.mjs +25 -9
  304. package/scripts/lib/session-schema/constants.mjs +64 -3
  305. package/scripts/lib/session-schema/validator.mjs +38 -4
  306. package/scripts/lib/session-start-probes.mjs +30 -1
  307. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  308. package/scripts/lib/skill-health/join.mjs +17 -4
  309. package/scripts/lib/state-md.mjs +78 -0
  310. package/scripts/lib/sunset/walker.mjs +6 -0
  311. package/scripts/lib/telemetry/schema.mjs +202 -9
  312. package/scripts/lib/telemetry/sync.mjs +368 -12
  313. package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
  314. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  315. package/scripts/lib/validate/check-agents.mjs +3 -3
  316. package/scripts/lib/validate/check-codex-skills.mjs +191 -0
  317. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  318. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  319. package/scripts/lib/validate/check-owner-leakage.mjs +319 -22
  320. package/scripts/lib/validate/check-skill-links.mjs +193 -0
  321. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  322. package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
  323. package/scripts/lib/validate/check-unwired-features.mjs +163 -15
  324. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  325. package/scripts/lib/validate/confidential-names.mjs +95 -30
  326. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  327. package/scripts/lib/validate/repo-files.mjs +48 -14
  328. package/scripts/lib/vault-backfill/template.mjs +63 -6
  329. package/scripts/lib/vault-mirror/process.mjs +165 -42
  330. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  331. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  332. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  333. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  334. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  335. package/scripts/lib/wave-resource-gate.mjs +8 -2
  336. package/scripts/lib/wave-sizing.mjs +4 -1
  337. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  338. package/scripts/materialize-wave-scope.mjs +12 -5
  339. package/scripts/memory-propose.mjs +19 -5
  340. package/scripts/migrate-cold-start-seed.mjs +4 -1
  341. package/scripts/parse-config.mjs +60 -3
  342. package/scripts/release.mjs +430 -31
  343. package/scripts/repair-invalid-sessions.mjs +3 -3
  344. package/scripts/run-quality-gate.mjs +128 -11
  345. package/scripts/site-numbers.mjs +344 -8
  346. package/scripts/sweep-expired-learnings.mjs +90 -0
  347. package/scripts/sync-vault-schema.mjs +3 -1
  348. package/scripts/telemetry.mjs +2 -2
  349. package/scripts/validate-plugin.mjs +164 -0
  350. package/scripts/validate-wave-scope.mjs +28 -8
  351. package/scripts/wave-scope-binding.mjs +215 -0
  352. package/skills/_shared/instruction-file-resolution.md +10 -0
  353. package/skills/_shared/parallel-aware-preamble.md +1 -0
  354. package/skills/_shared/platform-tools.md +1 -1
  355. package/skills/_shared/state-ownership.md +1 -1
  356. package/skills/architecture/SKILL.md +7 -5
  357. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  358. package/skills/autopilot/SKILL.md +4 -18
  359. package/skills/claude-md-drift-check/SKILL.md +5 -1
  360. package/skills/claude-md-drift-check/checker.mjs +62 -2
  361. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  362. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  363. package/skills/discovery/probes-arch.md +20 -18
  364. package/skills/dispatcher/SKILL.md +3 -2
  365. package/skills/evolve/SKILL.md +65 -26
  366. package/skills/frontmatter-guard/SKILL.md +11 -5
  367. package/skills/npm-publish/SKILL.md +1 -1
  368. package/skills/reconcile/SKILL.md +33 -0
  369. package/skills/remote-offload/SKILL.md +1 -1
  370. package/skills/session-end/SKILL.md +18 -905
  371. package/skills/session-end/phase-3-6-tail.md +10 -3
  372. package/skills/session-end/plan-verification.md +221 -155
  373. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  374. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  375. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  376. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  377. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  378. package/skills/session-end/references/session-summary-template.md +62 -0
  379. package/skills/session-plan/SKILL.md +49 -0
  380. package/skills/session-start/SKILL.md +22 -904
  381. package/skills/session-start/phase-8-5-express-path.md +1 -1
  382. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  383. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  384. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  385. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  386. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  387. package/skills/session-start/references/phase-4-ssot-environment-check.md +160 -0
  388. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  389. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  390. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  391. package/skills/vault-sync/SKILL.md +10 -0
  392. package/skills/vault-sync/validator.mjs +21 -27
  393. package/skills/wave-executor/SKILL.md +15 -1
  394. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  395. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  396. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  397. package/skills/wave-executor/wave-loop.md +14 -1309
  398. package/templates/_shared/journey-manifest.md +10 -6
  399. package/.cursor/commands/autopilot-multi.md +0 -14
  400. package/.cursor/commands/contract-version-bump.md +0 -14
  401. package/.cursor/commands/journey-audit.md +0 -14
  402. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  403. package/.cursor/skills/daily/SKILL.md +0 -12
  404. package/.cursor/skills/domain-model/SKILL.md +0 -13
  405. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  406. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  407. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  408. package/commands/autopilot-multi.md +0 -74
  409. package/commands/contract-version-bump.md +0 -28
  410. package/commands/journey-audit.md +0 -43
  411. package/pi/prompts/autopilot-multi.md +0 -12
  412. package/pi/prompts/contract-version-bump.md +0 -12
  413. package/pi/prompts/journey-audit.md +0 -12
  414. package/scripts/autopilot-multi.mjs +0 -885
  415. package/scripts/backfill-learnings-expires.mjs +0 -196
  416. package/scripts/backfill-learnings.mjs +0 -203
  417. package/scripts/fleet-instruction-scan.mjs +0 -141
  418. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  419. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  420. package/scripts/lib/webhook-url.mjs +0 -105
  421. package/scripts/lifecycle-sim-v6.mjs +0 -347
  422. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  423. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  424. package/scripts/upload-social-preview.mjs +0 -316
  425. package/skills/_shared/model-selection.md +0 -64
  426. package/skills/contract-version-bump/SKILL.md +0 -219
  427. package/skills/daily/SKILL.md +0 -222
  428. package/skills/daily/generate.sh +0 -92
  429. package/skills/daily/templates/daily.md.tpl +0 -36
  430. package/skills/journey-audit/SKILL.md +0 -270
  431. package/skills/skill-creator/SKILL.md +0 -168
  432. package/skills/ubiquitous-language/SKILL.md +0 -97
  433. package/skills/vault-sync/package-lock.json +0 -40
  434. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  435. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -213,6 +213,37 @@ function learningContentMatches(existingContent, renderedContent) {
213
213
  );
214
214
  }
215
215
 
216
+ /**
217
+ * Would the process-wide masker (see `ensureMasker` below) still change `text`
218
+ * if it ran again right now? (#1028 residue 1)
219
+ *
220
+ * Used as a LAST guard before returning `skipped-noop` in BOTH `processLearning`
221
+ * and `processSession` — the session channel's two skip sites were date-only
222
+ * until the W4 review of #1028 (see the module note above).
223
+ * `learningContentMatches` above compares only five canonical fields, so it
224
+ * cannot see a raw secret sitting in a NON-canonical field (`evidence` is the
225
+ * realistic one — see the KNOWN-RESIDUAL note on `maskEntrySecrets` below): a
226
+ * candidate that matches on those five fields can still be an on-disk leak.
227
+ * When this returns `true`, the caller must NOT skip — it falls through to the
228
+ * write path so the note is re-rendered (and re-masked) from the current entry.
229
+ *
230
+ * NAMED CEILING: this only heals a needle that is still present in the CURRENT
231
+ * process env. A needle that has since been rotated out of the env no longer
232
+ * masks to anything different, so `mask(text) === text` and this returns
233
+ * `false` — that needle's leak is healed only by an explicit `force: true`
234
+ * re-mirror (which bypasses the comparison entirely), never by a plain re-run.
235
+ *
236
+ * COST: one extra `mask()` call per candidate no-op — cheap relative to the
237
+ * read/parse/render already done to reach this check, and only paid on the
238
+ * no-op path (a real content change already writes without reaching here).
239
+ *
240
+ * @param {string} text — on-disk note content to probe
241
+ * @returns {boolean} true when masking `text` again would produce different output
242
+ */
243
+ function maskerWouldChange(text) {
244
+ return ensureMasker().mask(text) !== text;
245
+ }
246
+
216
247
  // ── repo derivation ───────────────────────────────────────────────────────────
217
248
 
218
249
  /**
@@ -451,36 +482,54 @@ export function getMaskerStats() {
451
482
  * as a wildcard (see that function) so this direction resolves to
452
483
  * `skipped-noop` again.
453
484
  *
454
- * KNOWN RESIDUAL — and NOT the one an earlier revision of this note named.
455
- * That revision claimed a COLD-START FREEZE over the CANONICAL fields: first
456
- * run without the env writes the raw value, later runs render `[REDACTED]`,
457
- * and the note freezes. Measured, that direction HEALS: with no marker on the
458
- * on-disk side `matchesModuloRedaction` returns false at its first line, the
459
- * canonical fields differ, and the run writes the masked content. The
460
- * asymmetry is still deliberate (an on-disk redaction is evidence a mask ran;
461
- * an on-disk raw value is evidence of nothing) — it simply does not freeze
462
- * anything the field comparison can see.
463
- *
464
- * What DOES freeze is the half the field comparison cannot see.
465
- * `learningContentMatches` compares exactly five canonical fields — `status`,
466
- * `expires`, `confidence`, `insight`, `source_session`. A raw secret sitting
467
- * in any OTHER field (`evidence` is the realistic one; it is agent-authored
468
- * free text and it is rendered into the note) leaves all five identical
469
- * between the raw on-disk note and the masked candidate. The comparison
470
- * reports a match, the run emits `skipped-noop`, and the plaintext stays in
471
- * the tracked, pushed file permanently — no later run rewrites it, because no
472
- * later run ever sees a difference.
473
- *
474
- * THE ESCAPE HATCH EXISTS AND IS UNDOCUMENTED ELSEWHERE, which is the real
475
- * defect: `processLearning(entry, n, { ...ctx, force: true })` skips the
476
- * date/content comparison entirely and re-renders from the (masked) entry, so
477
- * a single forced re-mirror with the env populated heals every such note. It
478
- * covers the same-id and legacy-flat paths; the disambiguated-collision
479
- * branch below does not read `force` and is not healed by it.
480
- * Revisit-Trigger: widen the canonical field set (or diff the whole rendered
481
- * body) the first time a mirror run is observed leaving a raw needle in a
482
- * non-canonical field — a test written TODAY would only pin the leak as
483
- * expected behaviour.
485
+ * RESIDUAL (#1028), NOW HEALED BY A MASK RE-PROBE — and NOT the residual an
486
+ * earlier revision of this note named. That revision claimed a COLD-START
487
+ * FREEZE over the CANONICAL fields: first run without the env writes the raw
488
+ * value, later runs render `[REDACTED]`, and the note freezes. Measured, that
489
+ * direction HEALS: with no marker on the on-disk side `matchesModuloRedaction`
490
+ * returns false at its first line, the canonical fields differ, and the run
491
+ * writes the masked content. The asymmetry is still deliberate (an on-disk
492
+ * redaction is evidence a mask ran; an on-disk raw value is evidence of
493
+ * nothing) — it simply does not freeze anything the field comparison can see.
494
+ *
495
+ * What the field comparison ALONE cannot see: `learningContentMatches`
496
+ * compares exactly five canonical fields — `status`, `expires`, `confidence`,
497
+ * `insight`, `source_session`. A raw secret sitting in any OTHER field
498
+ * (`evidence` is the realistic one; it is agent-authored free text and it is
499
+ * rendered into the note) leaves all five identical between the raw on-disk
500
+ * note and the masked candidate — the comparison alone reports a match.
501
+ *
502
+ * THE FIX — IN BOTH GENERATORS, which is the half an earlier revision of
503
+ * this note got wrong: it described the guard as a `processLearning` matter,
504
+ * and `processSession` shipped with two purely DATE-based `skipped-noop`
505
+ * returns beside it. The session note's narrative body is the LARGER
506
+ * free-text surface of the two, so that omission was the bigger half of the
507
+ * leak. Every `skipped-noop` return in BOTH `processLearning` (legacy-flat,
508
+ * disambig-collision, same-id) and `processSession` (legacy-flat, same-id)
509
+ * is now gated by `maskerWouldChange` (see that function above). When the
510
+ * on-disk content would still be changed by the CURRENT masker, the run
511
+ * falls through to the write path and re-renders (masked) instead of
512
+ * skipping — so a plain re-mirror heals the leak on its own; `force: true`
513
+ * is no longer the only escape hatch. The disambig-collision branch's
514
+ * `!force` guard now also matches the same-id and legacy-flat branches (it
515
+ * previously lacked one, so `force: true` was silently ignored there).
516
+ *
517
+ * THE LEGACY-FLAT BRANCH NEEDS ONE MORE STEP (#1028 residue 2). Falling
518
+ * through there writes the NAMESPACED path, and from the next run on
519
+ * `existsSync(targetPath)` is true — the flat file is never read again. A
520
+ * guard alone would therefore leave the plaintext on disk and merely make it
521
+ * unreachable to the heal, with the ledger reporting a clean write. Both
522
+ * generators now re-render the legacy flat note IN PLACE (masked) before
523
+ * falling through, and mark the resulting action `healed_legacy_flat: true`.
524
+ * Rewriting rather than deleting is what the deferred-migration decision
525
+ * above asks for: that decision is about not MOVING the note, which a masked
526
+ * re-render preserves and a deletion would silently undo.
527
+ * Revisit-Trigger: `maskerWouldChange` only detects needles present in the
528
+ * CURRENT process env — a needle since rotated out of the env is not seen as
529
+ * still-leaking and is healed only by an explicit `force: true` re-mirror.
530
+ * Widen this (e.g. persist a needle-shape fingerprint, or diff the whole
531
+ * rendered body) the first time a rotated-out secret is observed staying on
532
+ * disk after a plain re-run.
484
533
  *
485
534
  * FRONTMATTER AND BODY ARE TREATED IDENTICALLY. A credential is exactly as
486
535
  * published in `title:` as it is under `## Insight` — both live in the same
@@ -636,22 +685,48 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
636
685
  // duplicating the note. The deferred-migration decision means we only skip;
637
686
  // we do NOT move the flat note into the namespaced dir here.
638
687
  const legacyFlatPath = join(resolve(vaultDir), '40-learnings', `${slug}.md`);
688
+ // Set when a still-leaking legacy flat note was healed in place below, so the
689
+ // action emitted for THIS entry can say so (see the write sites at the end).
690
+ let healedLegacyFlat = false;
639
691
  if (!existsSync(targetPath) && existsSync(legacyFlatPath)) {
640
692
  const legacyContent = readFileSync(legacyFlatPath, 'utf8');
641
693
  const legacyFm = parseFrontmatter(legacyContent);
642
694
  // Only skip if the flat note is ours (has our generator marker and matching id).
643
695
  if (legacyFm && legacyFm['_generator'] === GENERATOR_MARKER && legacyFm['id'] === slug) {
644
696
  const entryUpdated = toDate(dateSource);
697
+ // #1028 residue 2 (W4 review HIGH-2): the leak probe must run on EVERY
698
+ // path that leaves this branch, not only the date-not-advanced one — every
699
+ // one of them falls through to the NAMESPACED path, after which
700
+ // `existsSync(targetPath)` is true forever and this legacy file is never
701
+ // read again. A raw secret left in it would become unreachable to the
702
+ // self-heal while staying fully published in the tracked vault.
703
+ const legacyStillLeaks = maskerWouldChange(legacyContent);
645
704
  if (!force && legacyFm['updated'] && legacyFm['updated'] >= entryUpdated) {
646
705
  // Date has not advanced — but content may have changed (confidence, insight, etc.).
647
706
  // Render the candidate and compare canonical fields before deciding to skip.
648
707
  const candidateContent = generator(entry, slug, generatorOpts);
649
- if (learningContentMatches(legacyContent, candidateContent)) {
708
+ if (learningContentMatches(legacyContent, candidateContent) && !legacyStillLeaks) {
709
+ // #1028 residue 1: the five-field compare cannot see a raw secret
710
+ // sitting in a non-canonical field (e.g. `evidence`) — the
711
+ // `legacyStillLeaks` probe above is what makes this match trustworthy.
650
712
  return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: legacyFlatPath, id: slug });
651
713
  }
652
- // Content differs — fall through to write into the namespaced path.
714
+ // Content differs, or the on-disk note still leaks under the current
715
+ // env — fall through to write into the namespaced path.
653
716
  }
654
717
  // Updated date would advance or content changed — fall through to write into the namespaced path.
718
+ //
719
+ // Heal the leaking legacy note IN PLACE first. Rewriting is the option
720
+ // consistent with the deferred-migration rationale above: that decision is
721
+ // about not MOVING the note (the flat path stays authoritative for older
722
+ // vaults and for hand-made links into it), which a masked re-render
723
+ // preserves and a deletion would silently undo. Deleting an operator-visible
724
+ // vault file to fix a leak also trades one irreversible loss for another —
725
+ // the content here is already ours (generator marker + id checked above).
726
+ if (legacyStillLeaks) {
727
+ if (!dryRun) writeFileSync(legacyFlatPath, generator(entry, slug, generatorOpts), 'utf8');
728
+ healedLegacyFlat = true;
729
+ }
655
730
  }
656
731
  }
657
732
 
@@ -686,13 +761,22 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
686
761
  return emitEntryAction(_lineNum, ctx, { action: 'skipped-handwritten', path: targetPath, id: entryId });
687
762
  }
688
763
  // Check updated advancement; if date has not advanced, also diff content.
764
+ // #1028 residue 1: this guard previously lacked the `!force &&` that the
765
+ // legacy-flat and same-id branches carry, so `force: true` was silently
766
+ // ignored on this one path — byte-identical guard shape to those two now.
689
767
  const entryUpdated = toDate(dateSource);
690
- if (disambigFm['updated'] && disambigFm['updated'] >= entryUpdated) {
768
+ if (!force && disambigFm['updated'] && disambigFm['updated'] >= entryUpdated) {
691
769
  const candidateContent = generator(entry, slug, generatorOpts);
692
770
  if (learningContentMatches(disambigContent, candidateContent)) {
693
- return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: disambigSlug });
771
+ // #1028 residue 1: probe the on-disk content against the CURRENT
772
+ // masker before trusting the five-field match — see maskerWouldChange.
773
+ const stillLeaks = maskerWouldChange(disambigContent);
774
+ if (!stillLeaks) {
775
+ return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: disambigSlug });
776
+ }
694
777
  }
695
- // Content differs — fall through to write.
778
+ // Content differs, or the on-disk note still leaks under the current
779
+ // env — fall through to write.
696
780
  }
697
781
  }
698
782
 
@@ -708,9 +792,15 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
708
792
  if (!force && fm['updated'] && fm['updated'] >= entryUpdated) {
709
793
  const candidateContent = generator(entry, slug, generatorOpts);
710
794
  if (learningContentMatches(existingContent, candidateContent)) {
711
- return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: slug });
795
+ // #1028 residue 1: probe the on-disk content against the CURRENT masker
796
+ // before trusting the five-field match — see maskerWouldChange above.
797
+ const stillLeaks = maskerWouldChange(existingContent);
798
+ if (!stillLeaks) {
799
+ return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: slug });
800
+ }
712
801
  }
713
- // Content differs — fall through to overwrite (same path as date-advance branch).
802
+ // Content differs, or the on-disk note still leaks under the current env —
803
+ // fall through to overwrite (same path as date-advance branch).
714
804
  }
715
805
 
716
806
  // Overwrite with advanced updated date (or forced re-render)
@@ -722,7 +812,15 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
722
812
  // File does not exist — create
723
813
  const content = generator(entry, slug, generatorOpts);
724
814
  if (!dryRun) writeFileSync(targetPath, content, 'utf8');
725
- return emitEntryAction(_lineNum, ctx, { action: 'created', path: targetPath, id: slug });
815
+ return emitEntryAction(_lineNum, ctx, {
816
+ action: 'created',
817
+ path: targetPath,
818
+ id: slug,
819
+ // No PATH in the meta: the stdout payload's `path` is vault-relative on
820
+ // purpose (emitAction relativises it), a raw absolute path here would put
821
+ // the operator's home dir on stdout and into the ledger record.
822
+ ...(healedLegacyFlat ? { meta: { healed_legacy_flat: true } } : {}),
823
+ });
726
824
  }
727
825
 
728
826
  export async function processSession(rawEntry, _lineNum, ctx) {
@@ -841,15 +939,30 @@ export async function processSession(rawEntry, _lineNum, ctx) {
841
939
  // the namespaced path as absent. If a session note already exists flat
842
940
  // (pre-namespace migration), skip creating a duplicate.
843
941
  const legacyFlatPath = join(resolve(vaultDir), '50-sessions', `${session_id}.md`);
942
+ let healedLegacyFlat = false;
844
943
  if (!existsSync(targetPath) && existsSync(legacyFlatPath)) {
845
944
  const legacyContent = readFileSync(legacyFlatPath, 'utf8');
846
945
  const legacyFm = parseFrontmatter(legacyContent);
847
946
  if (legacyFm && legacyFm['_generator'] === GENERATOR_MARKER && legacyFm['id'] === session_id) {
848
947
  const entryUpdated = toDate(entry.completed_at);
849
- if (!force && legacyFm['updated'] && legacyFm['updated'] >= entryUpdated) {
948
+ // #1028 residue 1, session channel (W4 review HIGH-1): the skip decision
949
+ // here is DATE-ONLY — it never looks at the note's content, so a raw
950
+ // secret written into the narrative before the env carried the needle
951
+ // would stay published forever. Same `maskerWouldChange` probe the
952
+ // learning channel carries; see that function above.
953
+ const legacyStillLeaks = maskerWouldChange(legacyContent);
954
+ if (!force && !legacyStillLeaks && legacyFm['updated'] && legacyFm['updated'] >= entryUpdated) {
850
955
  return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: legacyFlatPath, id: session_id });
851
956
  }
852
- // Updated date would advance — fall through to write into the namespaced path.
957
+ // Updated date would advance, or the on-disk note still leaks — fall
958
+ // through to write into the namespaced path. Heal the legacy note in place
959
+ // first (HIGH-2, same rationale as processLearning's legacy-flat branch):
960
+ // after this run `existsSync(targetPath)` is true, so the flat file is
961
+ // never read again and its plaintext would be unreachable to the heal.
962
+ if (legacyStillLeaks) {
963
+ if (!dryRun) writeFileSync(legacyFlatPath, renderedBody, 'utf8');
964
+ healedLegacyFlat = true;
965
+ }
853
966
  }
854
967
  }
855
968
 
@@ -871,7 +984,11 @@ export async function processSession(rawEntry, _lineNum, ctx) {
871
984
  // Same generator: check id and updated
872
985
  if (fm['id'] === session_id) {
873
986
  const entryUpdated = toDate(entry.completed_at);
874
- if (!force && fm['updated'] && fm['updated'] >= entryUpdated) {
987
+ // #1028 residue 1, session channel (W4 review HIGH-1): probe the on-disk
988
+ // content against the CURRENT masker before trusting the date-only skip —
989
+ // the session note's narrative body is the larger free-text leak surface
990
+ // of the two channels. See maskerWouldChange above.
991
+ if (!force && !maskerWouldChange(existingContent) && fm['updated'] && fm['updated'] >= entryUpdated) {
875
992
  return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: session_id });
876
993
  }
877
994
  if (!dryRun) writeFileSync(targetPath, renderedBody, 'utf8');
@@ -882,5 +999,11 @@ export async function processSession(rawEntry, _lineNum, ctx) {
882
999
  // File does not exist — create. Reuse the rendered body computed during the
883
1000
  // quality-gate check (avoids a second generator invocation).
884
1001
  if (!dryRun) writeFileSync(targetPath, renderedBody, 'utf8');
885
- return emitEntryAction(_lineNum, ctx, { action: 'created', path: targetPath, id: session_id });
1002
+ return emitEntryAction(_lineNum, ctx, {
1003
+ action: 'created',
1004
+ path: targetPath,
1005
+ id: session_id,
1006
+ // Boolean only — never a path; see the same note in processLearning.
1007
+ ...(healedLegacyFlat ? { meta: { healed_legacy_flat: true } } : {}),
1008
+ });
886
1009
  }
@@ -38,7 +38,7 @@
38
38
  */
39
39
 
40
40
  import { emitEvent, sessionAttribution } from '../events.mjs';
41
- import { SO_PROJECT_DIR } from '../platform.mjs';
41
+ import { getProjectDir } from '../platform.mjs';
42
42
 
43
43
  /** Canonical event name for a single vault-mirror JSONL entry. */
44
44
  export const MIRROR_EVENT = 'orchestrator.vault.mirror_completed';
@@ -81,7 +81,7 @@ const present = (v) => v !== undefined && v !== null;
81
81
  * @returns {{session_id?: string, semantic_session_id?: string}}
82
82
  */
83
83
  function mirrorSessionAttribution() {
84
- return sessionAttribution(SO_PROJECT_DIR);
84
+ return sessionAttribution(getProjectDir());
85
85
  }
86
86
 
87
87
  /**
@@ -218,15 +218,27 @@ export function extractNarrative(stateMdContents) {
218
218
  * REFERENCE (byte-identical downstream), and `mask` passes non-strings through —
219
219
  * masking must never be the reason a mirror run dies.
220
220
  *
221
+ * Takes the ALREADY-BUILT masker (rather than building its own) so
222
+ * `runNarrativeMirror` can build it exactly ONCE per run and reuse it for both
223
+ * the actual masking below AND the `orchestrator.secret_masker.applied`
224
+ * telemetry (#1028) — the masker's `needleCount` is measured once, not
225
+ * potentially twice with process.env read between the two reads.
226
+ *
221
227
  * @param {{ waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }} narrative
222
- * @returns {{ waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }}
228
+ * @param {{ mask: (text: string) => string, needleCount: number }} masker
229
+ * @returns {{ narrative: { waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }, hits: number }}
223
230
  */
224
- function maskNarrative(narrative) {
225
- const { mask, needleCount } = createSecretValueMasker(process.env);
226
- if (needleCount === 0) return narrative;
231
+ function maskNarrative(narrative, masker) {
232
+ const { mask, needleCount } = masker;
233
+ if (needleCount === 0) return { narrative, hits: 0 };
227
234
 
235
+ let hits = 0;
228
236
  const walk = (value) => {
229
- if (typeof value === 'string') return mask(value);
237
+ if (typeof value === 'string') {
238
+ const masked = mask(value);
239
+ if (masked !== value) hits++;
240
+ return masked;
241
+ }
230
242
  if (Array.isArray(value)) return value.map(walk);
231
243
  if (value && typeof value === 'object') {
232
244
  const out = {};
@@ -236,7 +248,7 @@ function maskNarrative(narrative) {
236
248
  return value;
237
249
  };
238
250
 
239
- return walk(narrative);
251
+ return { narrative: walk(narrative), hits };
240
252
  }
241
253
 
242
254
  // ── Pure render ────────────────────────────────────────────────────────────────
@@ -404,6 +416,30 @@ function normalizeUpdated(content) {
404
416
  * between runs: run 1 (env set) → `skipped-noop`, run 2 (env absent) → `written`
405
417
  * with the raw value on disk.
406
418
  *
419
+ * WHY THERE IS NO `maskerWouldChange` RE-PROBE HERE (#1214). The two sibling
420
+ * sinks in `vault-mirror/process.mjs` (processLearning, processSession) guard
421
+ * all five of their `skipped-noop` returns with `maskerWouldChange(existing)`,
422
+ * because their five-field / date comparisons can report a match while the
423
+ * on-disk note still carries a raw needle. This sink compares the WHOLE
424
+ * document, which makes that residue unreachable: `matchesModuloRedaction`
425
+ * only fires when the EXISTING side carries a `[REDACTED]` marker, and every
426
+ * literal segment around such a marker must appear verbatim in the candidate —
427
+ * but the candidate comes through `maskNarrative` with the CURRENT masker, so
428
+ * it can never contain a value that masker would redact. A raw needle on disk
429
+ * therefore always lands in a literal segment that fails to match, and the run
430
+ * writes. Measured 2026-09-04 at HEAD cd785003 in both directions (no marker on
431
+ * disk; marker already on disk plus a second needle entering the env): both
432
+ * returned `written` with the raw value gone. The two tests named `(#1214)` in
433
+ * tests/lib/vault-status/narrative-mirror.test.mjs pin that invariant — an
434
+ * argument-order slip on the call below makes both return `skipped-noop` and
435
+ * republish the raw value, which is how they were verified to bite.
436
+ *
437
+ * NAMED CEILING: the invariant rests on `maskNarrative` walking EVERY string
438
+ * that reaches the render. `repo` is fed to `renderNarrative` outside that walk
439
+ * (it is a directory basename, not STATE.md content), so a future rendered
440
+ * field added outside the walk would reopen this. Revisit trigger: any new
441
+ * `renderNarrative` input that does not pass through `maskNarrative`.
442
+ *
407
443
  * `matchesModuloRedaction` is IMPORTED from `../vault-mirror/process.mjs`, not
408
444
  * re-derived here: `secret-masker.mjs`'s header states this compensation as a
409
445
  * contract binding on every consumer that diffs a written artifact against a fresh
@@ -664,6 +700,56 @@ async function emitNarrativeEvent({ repoRoot, action, path: outputPath, chars, e
664
700
  }
665
701
  }
666
702
 
703
+ /**
704
+ * Emit this channel's `orchestrator.secret_masker.applied` record (#1028
705
+ * residue 2). Until now the narrative mirror was the ONLY masker channel of
706
+ * the three (`vault-mirror`, `narrative-mirror`, `export-hw-learnings`) that
707
+ * never emitted this event — `maskNarrative` has masked unconditionally since
708
+ * #1025, but nothing told the ledger. A census grepping event NAMES for
709
+ * `narrative` therefore read as "this channel masks nothing", which was never
710
+ * true; it just never SAID so.
711
+ *
712
+ * Emitted UNCONDITIONALLY once per `mirrorNarrative` run whenever a `repoRoot`
713
+ * was resolvable — including every skip outcome, not only `written` — so
714
+ * `needle_count: 0` is a REAL measured "the env carries no secret-shaped
715
+ * value" rather than a stand-in for "the masker never ran" (same
716
+ * force-build-don't-default posture as `getMaskerStats()` in
717
+ * `scripts/lib/vault-mirror/process.mjs`). `records` is the literal `1`: one
718
+ * `mirrorNarrative` run always masks (or attempts to mask) exactly one
719
+ * STATE.md, unlike the CLI channels that fan out over many JSONL lines.
720
+ *
721
+ * Same field set as the `vault-mirror` channel's own masker emit
722
+ * (`channel`, `needle_count`, `records`, `hits`, `dry_run`), PLUS
723
+ * `session_id`/`semantic_session_id` via `sessionAttribution` — matching this
724
+ * module's own `emitNarrativeEvent` above rather than the CLI's 2-arg
725
+ * `emitEvent` call, because `mirrorNarrative` (unlike the CLI) already always
726
+ * carries a `repoRoot` to attribute against.
727
+ *
728
+ * Best-effort: never throws, never alters the mirror result.
729
+ *
730
+ * @param {{ repoRoot?: string, needleCount: number, hits: number, dryRun: boolean }} opts
731
+ * @returns {Promise<void>}
732
+ */
733
+ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
734
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
735
+ try {
736
+ await emitEvent(
737
+ 'orchestrator.secret_masker.applied',
738
+ {
739
+ channel: 'narrative-mirror',
740
+ needle_count: needleCount,
741
+ records: 1,
742
+ hits,
743
+ dry_run: dryRun,
744
+ ...sessionAttribution(repoRoot),
745
+ },
746
+ { repoRoot },
747
+ );
748
+ } catch {
749
+ /* Best-effort telemetry — see emitNarrativeEvent above for why. */
750
+ }
751
+ }
752
+
667
753
  // ── Convenience orchestration ────────────────────────────────────────────────────
668
754
 
669
755
  /**
@@ -723,42 +809,62 @@ export async function mirrorNarrative(opts) {
723
809
  chars: outcome.chars,
724
810
  });
725
811
 
812
+ // #1028 residue 2: the masker-telemetry sibling of the narrative event
813
+ // above, on every non-throwing outcome (including skips) — see
814
+ // emitMaskerEvent for why this fires unconditionally.
815
+ await emitMaskerEvent({
816
+ repoRoot: opts?.repoRoot,
817
+ needleCount: outcome.needleCount,
818
+ hits: outcome.hits,
819
+ dryRun: outcome.dryRun,
820
+ });
821
+
726
822
  return outcome.result;
727
823
  }
728
824
 
729
825
  /**
730
826
  * The mirror itself — every early return of {@link mirrorNarrative} lives here.
731
827
  *
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
828
+ * Split out so that the two emit sites in `mirrorNarrative` (the narrative
829
+ * event AND, since #1028, the masker event) each cover EVERY outcome from
830
+ * ONE call: a future early return added inside this function is telemetered
831
+ * by construction, whereas hand-placing an emit beside each of the seven
832
+ * `return`s makes "forgot the new one" the default failure. The `chars` companion travels beside the result
736
833
  * rather than inside it because the returned object is a PUBLIC shape that
737
834
  * callers (and tests) compare with `toEqual` — adding a key there would be an
738
835
  * observable contract change for a purely internal measurement.
739
836
  *
740
837
  * @param {Parameters<typeof mirrorNarrative>[0]} opts
741
- * @returns {Promise<{ result: { action: string, path?: string }, chars?: number }>}
838
+ * @returns {Promise<{ result: { action: string, path?: string }, chars?: number, needleCount: number, hits: number, dryRun: boolean }>}
742
839
  */
743
840
  async function runNarrativeMirror(opts) {
744
841
  const { repoRoot, repo, now = new Date(), dryRun = false, fs: injectedFs, hostPaths } = opts;
745
842
 
746
843
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
747
- return { result: { action: 'skipped-vault-disabled' } };
844
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: 0, hits: 0, dryRun };
748
845
  }
749
846
 
847
+ // #1028: the masker is built ONCE per run, right here — before every
848
+ // remaining early-return below — so `mirrorNarrative`'s
849
+ // `orchestrator.secret_masker.applied` emit always carries a REAL measured
850
+ // `needle_count` on every outcome (including a skip), rather than a
851
+ // defaulted 0 that would be indistinguishable from "the masker never ran".
852
+ // Same "force-build, never default" posture as `getMaskerStats()` in
853
+ // `scripts/lib/vault-mirror/process.mjs`.
854
+ const masker = createSecretValueMasker(process.env);
855
+
750
856
  // Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
751
857
  let config;
752
858
  try {
753
859
  const configText = await readConfigFile(repoRoot);
754
860
  config = parseSessionConfig(configText, { hostPaths });
755
861
  } catch {
756
- return { result: { action: 'skipped-vault-disabled' } };
862
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
757
863
  }
758
864
 
759
865
  const vaultIntegration = config?.['vault-integration'];
760
866
  if (!vaultIntegration || vaultIntegration.enabled !== true) {
761
- return { result: { action: 'skipped-vault-disabled' } };
867
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
762
868
  }
763
869
 
764
870
  // Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
@@ -775,7 +881,7 @@ async function runNarrativeMirror(opts) {
775
881
 
776
882
  const rawVaultDir = vaultIntegration['vault-dir'];
777
883
  if (!rawVaultDir || typeof rawVaultDir !== 'string') {
778
- return { result: { action: 'skipped-vault-disabled' } };
884
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
779
885
  }
780
886
 
781
887
  const vaultDir = path.resolve(expandTilde(rawVaultDir));
@@ -799,7 +905,7 @@ async function runNarrativeMirror(opts) {
799
905
  canonicalizeRoot: true,
800
906
  });
801
907
  if (!inside.ok) {
802
- return { result: { action: 'skipped-invalid-path', path: outputPath } };
908
+ return { result: { action: 'skipped-invalid-path', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
803
909
  }
804
910
 
805
911
  // Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
@@ -808,13 +914,13 @@ async function runNarrativeMirror(opts) {
808
914
  try {
809
915
  stateContents = await readFile(stateMdPath, 'utf8');
810
916
  } catch {
811
- return { result: { action: 'skipped-no-statemd', path: outputPath } };
917
+ return { result: { action: 'skipped-no-statemd', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
812
918
  }
813
919
 
814
920
  // #1025: the ONE masking site for the narrative mirror — after the frontmatter
815
921
  // parse (so `[REDACTED]` can never break it) and before the render, the
816
922
  // idempotency comparison and the write. See maskNarrative above.
817
- const narrative = maskNarrative(extractNarrative(stateContents));
923
+ const { narrative, hits } = maskNarrative(extractNarrative(stateContents), masker);
818
924
  const content = renderNarrative({ repo: repoName, narrative, now });
819
925
 
820
926
  // `chars` measures the document THIS call rendered — so it is present on
@@ -823,5 +929,8 @@ async function runNarrativeMirror(opts) {
823
929
  return {
824
930
  result: writeNarrative({ outputPath, content, dryRun, fs: injectedFs }),
825
931
  chars: content.length,
932
+ needleCount: masker.needleCount,
933
+ hits,
934
+ dryRun,
826
935
  };
827
936
  }