session-orchestrator 3.23.0 → 4.0.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 (393) 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 +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * markdown-fences.mjs — the ONE fenced-code-block tracker shared by every
3
+ * Markdown-scanning validator/extractor in this repo (#1181).
4
+ *
5
+ * ## Why one module
6
+ *
7
+ * The same regex pair + open/close comparison had drifted into four separate
8
+ * copies — `check-doc-cli-commands.mjs`, `check-skill-script-paths.mjs`,
9
+ * `check-vcs-repo-flag.mjs` and `auq/parse.mjs` — each tracking fence depth
10
+ * and language independently. A drifted copy is a silent one: nothing fails
11
+ * when copy #3 diverges from copy #1, because each copy only has to agree
12
+ * with itself.
13
+ *
14
+ * ## The rule this module encodes
15
+ *
16
+ * A fence opens on a line beginning with 3+ backticks or 3+ tildes, carrying
17
+ * an optional info string (most commonly a language tag). It closes on a
18
+ * line whose marker CHARACTER matches, whose LENGTH is at least the
19
+ * opener's, and whose info string is empty — CommonMark reserves the info
20
+ * string for the OPENING fence only, so a fence-shaped line that still
21
+ * carries one is fence CONTENT (most often a nested fence one level in), not
22
+ * a closer.
23
+ *
24
+ * ## The one real divergence between the four original copies, preserved as a parameter
25
+ *
26
+ * Three of the four copies anchor the fence-line regex only at the START of
27
+ * the line — trailing text after the info string is simply not captured,
28
+ * never rejected. `auq/parse.mjs`'s copy anchors at BOTH ends: a line
29
+ * carrying anything past the info string besides trailing whitespace is not
30
+ * recognised as a fence line at all. This is `{ wholeLine: true }` below —
31
+ * a real behavioural difference, not stylistic, so it stays a caller-chosen
32
+ * option rather than being silently resolved one way. Every other
33
+ * consumer-specific behaviour (the shell-language predicate, blockquote
34
+ * stripping, unbalanced-fence reporting) likewise stays at the call site —
35
+ * this module owns only the fence-line grammar itself.
36
+ */
37
+
38
+ /** Fence languages whose body is shell. Both `check-doc-cli-commands.mjs` and
39
+ * `check-vcs-repo-flag.mjs` filtered on this identical literal set before
40
+ * extraction; centralised here rather than kept as two copies of the same
41
+ * five strings. */
42
+ export const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
43
+
44
+ /** Start-anchored: matches CommonMark, tolerates trailing info-string text. */
45
+ const FENCE_LINE_START_RE = /^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/;
46
+
47
+ /** Whole-line-anchored: nothing but whitespace may follow the info string. */
48
+ const FENCE_LINE_WHOLE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*([^\s`~]*)[ \t]*$/u;
49
+
50
+ /**
51
+ * Match a candidate fence-marker line.
52
+ *
53
+ * @param {string} line
54
+ * @param {{ wholeLine?: boolean }} [options] `wholeLine: true` requires the
55
+ * ENTIRE line (after the marker) to be nothing but the info string plus
56
+ * trailing whitespace — the stricter reading `auq/parse.mjs` needs.
57
+ * Default `false` only anchors the START of the line, matching
58
+ * CommonMark and the three `check-*.mjs` validators.
59
+ * @returns {{ marker: string, length: number, info: string } | null}
60
+ */
61
+ export function matchFenceLine(line, { wholeLine = false } = {}) {
62
+ const match = (wholeLine ? FENCE_LINE_WHOLE_RE : FENCE_LINE_START_RE).exec(line);
63
+ if (!match) return null;
64
+ return { marker: match[1][0], length: match[1].length, info: match[2] ?? '' };
65
+ }
66
+
67
+ /**
68
+ * Does `candidate` close the fence opened by `open`? Same marker character,
69
+ * at least as long, and carrying no info string of its own.
70
+ *
71
+ * @param {{ marker: string, length: number }} open
72
+ * @param {{ marker: string, length: number, info: string }} candidate
73
+ * @returns {boolean}
74
+ */
75
+ export function closesFence(open, candidate) {
76
+ return candidate.marker === open.marker && candidate.length >= open.length && candidate.info === '';
77
+ }
78
+
79
+ /**
80
+ * Normalise a fence's info string into a comparable language tag. Every
81
+ * caller that classified a fence by language did `.toLowerCase()` (never
82
+ * `.trim()`, since the capturing regex cannot include leading/trailing
83
+ * whitespace in the first place) before comparing against a language set —
84
+ * this makes that normalisation a single, explicit step instead of an
85
+ * implicit property of the extraction regex.
86
+ *
87
+ * @param {string} info
88
+ * @returns {string}
89
+ */
90
+ export function normalizeLang(info) {
91
+ return String(info ?? '').trim().toLowerCase();
92
+ }
93
+
94
+ /**
95
+ * Strip a leading blockquote `>` chain so a quoted fence (`> \`\`\``) is
96
+ * still recognised as a fence line. Only `check-skill-script-paths.mjs`
97
+ * needs this — a fence inside a blockquote is still a fence there, but the
98
+ * other callers never scan quoted content.
99
+ *
100
+ * @param {string} line
101
+ * @returns {string}
102
+ */
103
+ export function stripBlockquote(line) {
104
+ return line.replace(/^(?:\s*>)+\s?/, '');
105
+ }
106
+
107
+ /**
108
+ * Walk `text` line by line, tracking fence state, and call `onLine` for
109
+ * every CONTENT line. Fence-marker lines themselves (the opener and the
110
+ * closer) are consumed by the tracker and never handed to the callback —
111
+ * all four original copies treated marker lines as structural, never as
112
+ * scannable content. A fence-shaped line that neither opens nor validly
113
+ * closes (a nested fence one level in) IS still content and reaches the
114
+ * callback, exactly as in the original copies.
115
+ *
116
+ * @param {string} text
117
+ * @param {(line: string, state: { lineNumber: number, inFence: boolean, lang: string | null }) => void} onLine
118
+ * @param {{ wholeLine?: boolean, stripBlockquotes?: boolean }} [options]
119
+ * @returns {{ unbalancedFenceLine: number | null }} the 1-based line of a
120
+ * fence that opened and never closed by EOF, or `null` if every fence
121
+ * this walk saw was balanced.
122
+ */
123
+ export function forEachLine(text, onLine, options = {}) {
124
+ const { wholeLine = false, stripBlockquotes = false } = options;
125
+ const lines = text.split('\n');
126
+ /** @type {{ marker: string, length: number, openLine: number } | null} */
127
+ let fence = null;
128
+ let lang = null;
129
+
130
+ for (let index = 0; index < lines.length; index += 1) {
131
+ const raw = lines[index];
132
+ const probe = stripBlockquotes ? stripBlockquote(raw) : raw;
133
+ const candidate = matchFenceLine(probe, { wholeLine });
134
+ if (candidate) {
135
+ if (fence === null) {
136
+ fence = { marker: candidate.marker, length: candidate.length, openLine: index + 1 };
137
+ lang = normalizeLang(candidate.info);
138
+ continue;
139
+ }
140
+ if (closesFence(fence, candidate)) {
141
+ fence = null;
142
+ lang = null;
143
+ continue;
144
+ }
145
+ // Otherwise it is fence content (a nested fence inside a wider one) — falls through.
146
+ }
147
+ onLine(raw, { lineNumber: index + 1, inFence: fence !== null, lang });
148
+ }
149
+
150
+ return { unbalancedFenceLine: fence ? fence.openLine : null };
151
+ }
152
+
153
+ /**
154
+ * Extract fence BLOCKS (an open/close pair plus its body) rather than a
155
+ * per-line walk — the shape `auq/parse.mjs` needs, since it iterates fences
156
+ * as units, not lines.
157
+ *
158
+ * An unbalanced fence (opens, never closes) contributes NO block, mirroring
159
+ * the original `fencesOf()` contract: it silently drops the dangling opener
160
+ * rather than reporting it. Unlike `check-skill-script-paths.mjs`, whose
161
+ * unterminated fence is a doc DEFECT worth its own finding, `auq/parse.mjs`
162
+ * scans AskUserQuestion blocks in production code and prose, where an
163
+ * unterminated fence was never treated as a reportable condition in its own
164
+ * right — only as "no block found here".
165
+ *
166
+ * @param {string} text
167
+ * @param {{ wholeLine?: boolean }} [options]
168
+ * @returns {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>}
169
+ */
170
+ export function scanFenceBlocks(text, options = {}) {
171
+ const { wholeLine = false } = options;
172
+ const lines = text.split('\n');
173
+ /** @type {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>} */
174
+ const blocks = [];
175
+ /** @type {{ marker: string, length: number, info: string, startIdx: number } | null} */
176
+ let open = null;
177
+
178
+ for (let index = 0; index < lines.length; index += 1) {
179
+ const candidate = matchFenceLine(lines[index], { wholeLine });
180
+ if (!candidate) continue;
181
+ if (open === null) {
182
+ open = { ...candidate, startIdx: index };
183
+ continue;
184
+ }
185
+ if (!closesFence(open, candidate)) continue; // fence content (nested), not a close
186
+ blocks.push({
187
+ openLine: open.startIdx + 1,
188
+ closeLine: index + 1,
189
+ lang: open.info,
190
+ bodyLines: lines.slice(open.startIdx + 1, index),
191
+ bodyStartLine: open.startIdx + 2,
192
+ });
193
+ open = null;
194
+ }
195
+ return blocks;
196
+ }
@@ -1,17 +1,71 @@
1
1
  /**
2
2
  * template.mjs — Canonical .vault.yaml template renderer for vault-backfill.
3
3
  *
4
- * Reads the template once from projects-baseline; subsequent calls use cache.
4
+ * Reads the template once from a projects-baseline checkout; subsequent calls
5
+ * use the cache. The checkout is optional — see docs/baseline.md.
5
6
  * Part of scripts/vault-backfill.mjs (Issue #241).
6
7
  */
7
8
 
8
9
  import { readFileSync, existsSync } from 'node:fs';
9
10
  import { homedir } from 'node:os';
10
- import { resolve } from 'node:path';
11
+ import { dirname, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { resolveHostPath } from '../config/host-paths.mjs';
11
14
 
12
- export const TEMPLATE_PATH = process.env.PROJECTS_BASELINE_DIR
13
- ? resolve(process.env.PROJECTS_BASELINE_DIR, 'templates/shared/.vault.yaml.template')
14
- : resolve(homedir(), 'Projects/projects-baseline/templates/shared/.vault.yaml.template');
15
+ /** Path of the template RELATIVE to a projects-baseline checkout root. */
16
+ const TEMPLATE_REL_PATH = 'templates/shared/.vault.yaml.template';
17
+
18
+ /** This file lives at `<repoRoot>/scripts/lib/vault-backfill/`. */
19
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
20
+
21
+ /**
22
+ * Candidate projects-baseline checkout roots. The baseline is optional and
23
+ * private (`docs/baseline.md`), so no host-specific directory name may be
24
+ * committed here.
25
+ *
26
+ * Two tiers, and the split is load-bearing:
27
+ *
28
+ * EXPLICIT — `PROJECTS_BASELINE_DIR`, else `SO_BASELINE_PATH` / `owner.yaml`
29
+ * `paths.baseline-path`. When the operator has SAID where the baseline is,
30
+ * that answer is used ALONE. Probing past a wrong explicit value would resolve
31
+ * a DIFFERENT baseline than the one named and report success — silently using
32
+ * a corpus nobody asked for is worse than the abort, and it would hide the
33
+ * typo forever.
34
+ *
35
+ * CONVENTION — the sibling checkout `scripts/sync-vault-schema.mjs` already
36
+ * uses, then the legacy `~/Projects` default this module shipped with. These
37
+ * are guesses, so probing among them is exactly right.
38
+ *
39
+ * @returns {string[]} never empty
40
+ */
41
+ function baselineCandidates() {
42
+ const envDir = (process.env.PROJECTS_BASELINE_DIR || '').trim();
43
+ if (envDir) return [envDir];
44
+ const hostDir = resolveHostPath('baseline-path', null);
45
+ if (typeof hostDir === 'string' && hostDir.trim() !== '') return [hostDir.trim()];
46
+ return [
47
+ resolve(REPO_ROOT, '..', 'projects-baseline'),
48
+ resolve(homedir(), 'Projects/projects-baseline'),
49
+ ];
50
+ }
51
+
52
+ /**
53
+ * Resolved absolute path of the canonical template.
54
+ *
55
+ * Import-time resolution is retained deliberately: the export is a plain string
56
+ * that `scripts/vault-backfill.mjs` and the tests both read directly. Ceiling:
57
+ * at most two `existsSync` calls at import. When no candidate exists the FIRST
58
+ * candidate is exported anyway, so `loadTemplate`'s die message names the path
59
+ * the operator most likely meant rather than `undefined`.
60
+ */
61
+ export const TEMPLATE_PATH = (() => {
62
+ const candidates = baselineCandidates();
63
+ for (const base of candidates) {
64
+ const candidate = resolve(base, TEMPLATE_REL_PATH);
65
+ if (existsSync(candidate)) return candidate;
66
+ }
67
+ return resolve(candidates[0], TEMPLATE_REL_PATH);
68
+ })();
15
69
 
16
70
  const TODAY = new Date().toISOString().slice(0, 10);
17
71
 
@@ -27,7 +81,10 @@ export function loadTemplate(dieFn) {
27
81
  dieFn(
28
82
  2,
29
83
  `canonical template not found at ${TEMPLATE_PATH} — ` +
30
- `set PROJECTS_BASELINE_DIR env var or check projects-baseline checkout at $HOME/Projects/projects-baseline`,
84
+ `the projects-baseline checkout is optional and private (see docs/baseline.md). ` +
85
+ `Point at it with owner.yaml \`paths.baseline-path\` (host-local, never committed), ` +
86
+ `the SO_BASELINE_PATH env var, or PROJECTS_BASELINE_DIR; a sibling checkout at ` +
87
+ `../projects-baseline is picked up automatically.`,
31
88
  );
32
89
  }
33
90
 
@@ -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
  /**