session-orchestrator 3.16.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 (762) hide show
  1. package/.claude-plugin/marketplace.json +29 -0
  2. package/.claude-plugin/plugin.json +18 -0
  3. package/.codex-plugin/agents/explorer.toml +14 -0
  4. package/.codex-plugin/agents/session-reviewer.toml +23 -0
  5. package/.codex-plugin/agents/wave-worker.toml +15 -0
  6. package/.codex-plugin/config.toml +20 -0
  7. package/.codex-plugin/plugin.json +37 -0
  8. package/.cursor/rules/000-session-orchestrator.mdc +73 -0
  9. package/.cursor/rules/010-session-workflow.mdc +170 -0
  10. package/.cursor/rules/020-quality-gates.mdc +128 -0
  11. package/.cursor/rules/030-wave-execution.mdc +216 -0
  12. package/.cursor/rules/040-discovery.mdc +242 -0
  13. package/.cursor/rules/050-plan.mdc +235 -0
  14. package/.cursor/rules/060-evolve.mdc +232 -0
  15. package/.cursor/rules/070-gitlab-ops.mdc +246 -0
  16. package/.cursor/rules/080-ecosystem-health.mdc +145 -0
  17. package/.mcp.json +8 -0
  18. package/CHANGELOG.md +1544 -0
  19. package/LICENSE +21 -0
  20. package/NOTICE +64 -0
  21. package/README.md +242 -0
  22. package/SECURITY.md +90 -0
  23. package/agents/AGENTS.md +136 -0
  24. package/agents/analyst.md +99 -0
  25. package/agents/architect-reviewer.md +93 -0
  26. package/agents/code-implementer.md +106 -0
  27. package/agents/db-specialist.md +104 -0
  28. package/agents/dialectic-deriver.md +139 -0
  29. package/agents/docs-writer.md +113 -0
  30. package/agents/eval-judge.md +146 -0
  31. package/agents/memory-proposal-collector.md +297 -0
  32. package/agents/qa-strategist.md +102 -0
  33. package/agents/schemas/analyst.schema.json +46 -0
  34. package/agents/schemas/architect-reviewer.schema.json +50 -0
  35. package/agents/schemas/code-implementer.schema.json +61 -0
  36. package/agents/schemas/db-specialist.schema.json +80 -0
  37. package/agents/schemas/docs-writer.schema.json +56 -0
  38. package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
  39. package/agents/schemas/qa-strategist.schema.json +46 -0
  40. package/agents/schemas/security-reviewer.schema.json +86 -0
  41. package/agents/schemas/session-reviewer.schema.json +69 -0
  42. package/agents/schemas/test-writer.schema.json +69 -0
  43. package/agents/schemas/ui-developer.schema.json +90 -0
  44. package/agents/schemas/ux-evaluator.schema.json +51 -0
  45. package/agents/security-reviewer.md +236 -0
  46. package/agents/session-reviewer.md +201 -0
  47. package/agents/skill-applied-judge.md +122 -0
  48. package/agents/test-writer.md +123 -0
  49. package/agents/ui-developer.md +109 -0
  50. package/agents/ux-evaluator.md +161 -0
  51. package/assets/icon.svg +11 -0
  52. package/assets/og-card.png +0 -0
  53. package/assets/og-card.svg +47 -0
  54. package/commands/autopilot-multi.md +74 -0
  55. package/commands/autopilot.md +80 -0
  56. package/commands/bootstrap.md +56 -0
  57. package/commands/brainstorm.md +48 -0
  58. package/commands/close.md +24 -0
  59. package/commands/debug.md +36 -0
  60. package/commands/discovery.md +32 -0
  61. package/commands/dispatcher.md +59 -0
  62. package/commands/eval.md +28 -0
  63. package/commands/evolve.md +10 -0
  64. package/commands/go.md +41 -0
  65. package/commands/grill.md +45 -0
  66. package/commands/harness-audit.md +26 -0
  67. package/commands/memory-cleanup.md +25 -0
  68. package/commands/persona-panel.md +121 -0
  69. package/commands/plan.md +15 -0
  70. package/commands/portfolio.md +97 -0
  71. package/commands/reconcile.md +23 -0
  72. package/commands/repo-audit.md +24 -0
  73. package/commands/session.md +30 -0
  74. package/commands/spinout.md +15 -0
  75. package/commands/sunset-review.md +27 -0
  76. package/commands/templates-ack.md +96 -0
  77. package/commands/test.md +97 -0
  78. package/docs/README.md +105 -0
  79. package/docs/USER-GUIDE.md +1403 -0
  80. package/docs/ci-setup.md +81 -0
  81. package/docs/codex-setup.md +142 -0
  82. package/docs/components.md +74 -0
  83. package/docs/cursor-setup.md +104 -0
  84. package/docs/events-schema.md +81 -0
  85. package/docs/migration-v3.md +148 -0
  86. package/docs/owner-config-schema.md +154 -0
  87. package/docs/persona-panel.md +433 -0
  88. package/docs/pi-setup.md +115 -0
  89. package/docs/plugin-architecture-v3.md +296 -0
  90. package/docs/pm-skills-marketplace.md +114 -0
  91. package/docs/policy-cache-validation-2026-04-28.md +118 -0
  92. package/docs/rule-authoring.md +316 -0
  93. package/docs/session-config-reference.md +1439 -0
  94. package/docs/session-config-template.md +961 -0
  95. package/docs/vault-docs-architecture.md +297 -0
  96. package/hooks/_lib/lock-bootstrap.mjs +272 -0
  97. package/hooks/_lib/lock-reconcile.mjs +93 -0
  98. package/hooks/_lib/profile-gate.mjs +95 -0
  99. package/hooks/_lib/transcript-history.mjs +211 -0
  100. package/hooks/agent-teams-h3-test.sh +362 -0
  101. package/hooks/config-protection.mjs +0 -0
  102. package/hooks/cwd-change-restore.mjs +131 -0
  103. package/hooks/enforce-commands.mjs +179 -0
  104. package/hooks/enforce-scope.mjs +273 -0
  105. package/hooks/hooks-codex.json +60 -0
  106. package/hooks/hooks-cursor.json +15 -0
  107. package/hooks/hooks-pi.json +115 -0
  108. package/hooks/hooks.json +215 -0
  109. package/hooks/loop-guard.mjs +260 -0
  110. package/hooks/on-session-end.mjs +217 -0
  111. package/hooks/on-session-start.mjs +660 -0
  112. package/hooks/on-stop.mjs +294 -0
  113. package/hooks/operator-steer.mjs +64 -0
  114. package/hooks/post-edit-validate.mjs +225 -0
  115. package/hooks/post-subagent-discovery-validator.mjs +398 -0
  116. package/hooks/post-tool-batch-wave-signal.mjs +328 -0
  117. package/hooks/post-tool-failure-corrective-context.mjs +248 -0
  118. package/hooks/post-tooluse-frontend-slop.mjs +184 -0
  119. package/hooks/pre-bash-destructive-guard.mjs +515 -0
  120. package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
  121. package/hooks/pre-bash-staging-fence.mjs +223 -0
  122. package/hooks/pre-bash-templates-first.mjs +404 -0
  123. package/hooks/run-node.sh +72 -0
  124. package/hooks/skill-invocation-telemetry.mjs +99 -0
  125. package/hooks/subagent-telemetry.mjs +249 -0
  126. package/hooks/wave-scope-commit-guard.mjs +191 -0
  127. package/monitors/monitors.json +14 -0
  128. package/output-styles/finding-report.md +48 -0
  129. package/output-styles/session-report.md +53 -0
  130. package/output-styles/wave-summary.md +38 -0
  131. package/package.json +94 -0
  132. package/pi/extensions/session-orchestrator.ts +25 -0
  133. package/pi/prompts/autopilot-multi.md +12 -0
  134. package/pi/prompts/autopilot.md +12 -0
  135. package/pi/prompts/bootstrap.md +12 -0
  136. package/pi/prompts/brainstorm.md +12 -0
  137. package/pi/prompts/close.md +11 -0
  138. package/pi/prompts/debug.md +12 -0
  139. package/pi/prompts/discovery.md +12 -0
  140. package/pi/prompts/dispatcher.md +12 -0
  141. package/pi/prompts/eval.md +12 -0
  142. package/pi/prompts/evolve.md +12 -0
  143. package/pi/prompts/go.md +12 -0
  144. package/pi/prompts/grill.md +12 -0
  145. package/pi/prompts/harness-audit.md +12 -0
  146. package/pi/prompts/memory-cleanup.md +12 -0
  147. package/pi/prompts/persona-panel.md +12 -0
  148. package/pi/prompts/plan.md +12 -0
  149. package/pi/prompts/portfolio.md +12 -0
  150. package/pi/prompts/reconcile.md +12 -0
  151. package/pi/prompts/repo-audit.md +12 -0
  152. package/pi/prompts/session.md +12 -0
  153. package/pi/prompts/spinout.md +12 -0
  154. package/pi/prompts/sunset-review.md +12 -0
  155. package/pi/prompts/templates-ack.md +12 -0
  156. package/pi/prompts/test.md +12 -0
  157. package/rules/_index.md +51 -0
  158. package/rules/always-on/commit-discipline.md +26 -0
  159. package/rules/always-on/npm-quality-gates.md +26 -0
  160. package/rules/always-on/parallel-sessions.md +43 -0
  161. package/rules/opt-in-domain/prompt-caching.md +270 -0
  162. package/rules/opt-in-stack/backend-data.md +188 -0
  163. package/rules/opt-in-stack/backend.md +390 -0
  164. package/rules/opt-in-stack/frontend.md +98 -0
  165. package/rules/opt-in-stack/security-web.md +194 -0
  166. package/rules/opt-in-stack/swift.md +65 -0
  167. package/scripts/archive-closed-prds.mjs +416 -0
  168. package/scripts/autopilot-multi.mjs +802 -0
  169. package/scripts/autopilot.mjs +383 -0
  170. package/scripts/backfill-abandoned-sessions.mjs +265 -0
  171. package/scripts/backfill-learnings-expires.mjs +196 -0
  172. package/scripts/backfill-learnings.mjs +203 -0
  173. package/scripts/backfill-sessions.mjs +282 -0
  174. package/scripts/check-doc-consistency.sh +279 -0
  175. package/scripts/check-package-manager.mjs +445 -0
  176. package/scripts/ci/assert-vitest-green.mjs +267 -0
  177. package/scripts/codex-install.mjs +435 -0
  178. package/scripts/compute-grounding-injection.sh +186 -0
  179. package/scripts/cursor-install.mjs +113 -0
  180. package/scripts/dialectic-deriver.mjs +573 -0
  181. package/scripts/emit-event.mjs +160 -0
  182. package/scripts/emit-session.mjs +212 -0
  183. package/scripts/eval-session.mjs +262 -0
  184. package/scripts/export-hw-learnings.mjs +437 -0
  185. package/scripts/gc-stale-worktrees.mjs +666 -0
  186. package/scripts/generate-pi-prompts.mjs +127 -0
  187. package/scripts/harness-audit.mjs +287 -0
  188. package/scripts/lib/agent-frontmatter.mjs +266 -0
  189. package/scripts/lib/agent-output-schema.mjs +166 -0
  190. package/scripts/lib/agent-status.mjs +303 -0
  191. package/scripts/lib/ajv-loader.mjs +34 -0
  192. package/scripts/lib/auto-dialectic.mjs +382 -0
  193. package/scripts/lib/auto-dream.mjs +471 -0
  194. package/scripts/lib/autonomy/suitability.mjs +212 -0
  195. package/scripts/lib/autopilot/dep-graph.mjs +417 -0
  196. package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
  197. package/scripts/lib/autopilot/flags.mjs +104 -0
  198. package/scripts/lib/autopilot/kill-switches.mjs +174 -0
  199. package/scripts/lib/autopilot/loop.mjs +320 -0
  200. package/scripts/lib/autopilot/mr-draft.mjs +520 -0
  201. package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
  202. package/scripts/lib/autopilot/recent-runs.mjs +106 -0
  203. package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
  204. package/scripts/lib/autopilot/telemetry.mjs +224 -0
  205. package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
  206. package/scripts/lib/autopilot-telemetry.mjs +11 -0
  207. package/scripts/lib/autopilot.mjs +39 -0
  208. package/scripts/lib/backlog-scan.mjs +179 -0
  209. package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
  210. package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
  211. package/scripts/lib/build-live-signals.mjs +150 -0
  212. package/scripts/lib/ci-status-banner.mjs +425 -0
  213. package/scripts/lib/claude-md-budget-lint.mjs +246 -0
  214. package/scripts/lib/cli-flags.mjs +158 -0
  215. package/scripts/lib/codex/plugin-contract.mjs +610 -0
  216. package/scripts/lib/cold-start-detector.mjs +240 -0
  217. package/scripts/lib/command-blocker.mjs +458 -0
  218. package/scripts/lib/common.mjs +333 -0
  219. package/scripts/lib/config/auto-dream.mjs +77 -0
  220. package/scripts/lib/config/block-header.mjs +94 -0
  221. package/scripts/lib/config/broken-window.mjs +114 -0
  222. package/scripts/lib/config/coercers.mjs +248 -0
  223. package/scripts/lib/config/cold-start.mjs +92 -0
  224. package/scripts/lib/config/config-protection.mjs +120 -0
  225. package/scripts/lib/config/cross-repo.mjs +104 -0
  226. package/scripts/lib/config/custom-phases.mjs +213 -0
  227. package/scripts/lib/config/dialectic.mjs +92 -0
  228. package/scripts/lib/config/discovery-validator.mjs +75 -0
  229. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
  230. package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
  231. package/scripts/lib/config/docs-orchestrator.mjs +90 -0
  232. package/scripts/lib/config/docs-staleness.mjs +96 -0
  233. package/scripts/lib/config/drift-check.mjs +155 -0
  234. package/scripts/lib/config/eval.mjs +130 -0
  235. package/scripts/lib/config/events-rotation.mjs +74 -0
  236. package/scripts/lib/config/evolve.mjs +308 -0
  237. package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
  238. package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
  239. package/scripts/lib/config/handover-gate.mjs +106 -0
  240. package/scripts/lib/config/host-paths.mjs +76 -0
  241. package/scripts/lib/config/io.mjs +54 -0
  242. package/scripts/lib/config/loop-guard.mjs +117 -0
  243. package/scripts/lib/config/memory.mjs +150 -0
  244. package/scripts/lib/config/persona-gate-wave.mjs +258 -0
  245. package/scripts/lib/config/reconcile.mjs +205 -0
  246. package/scripts/lib/config/section-extractor.mjs +100 -0
  247. package/scripts/lib/config/skill-evolution.mjs +112 -0
  248. package/scripts/lib/config/slopcheck.mjs +99 -0
  249. package/scripts/lib/config/state-md-lock.mjs +83 -0
  250. package/scripts/lib/config/templates-first.mjs +94 -0
  251. package/scripts/lib/config/test.mjs +113 -0
  252. package/scripts/lib/config/vault-integration.mjs +201 -0
  253. package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
  254. package/scripts/lib/config/vault-staleness.mjs +84 -0
  255. package/scripts/lib/config/vault-sync.mjs +96 -0
  256. package/scripts/lib/config/verification-auto-fix.mjs +84 -0
  257. package/scripts/lib/config/wave-reviewers.mjs +133 -0
  258. package/scripts/lib/config-schema.mjs +345 -0
  259. package/scripts/lib/config.mjs +474 -0
  260. package/scripts/lib/convergence-monitor.mjs +389 -0
  261. package/scripts/lib/coordinator-snapshot.mjs +371 -0
  262. package/scripts/lib/crypto-digest-utils.mjs +91 -0
  263. package/scripts/lib/discovery/helpers.mjs +127 -0
  264. package/scripts/lib/discovery/triage-state.mjs +279 -0
  265. package/scripts/lib/dispatcher/cli.mjs +257 -0
  266. package/scripts/lib/dispatcher/enumerate.mjs +243 -0
  267. package/scripts/lib/dispatcher/rank.mjs +363 -0
  268. package/scripts/lib/ecosystem-health.mjs +224 -0
  269. package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
  270. package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
  271. package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
  272. package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
  273. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
  274. package/scripts/lib/ecosystem-wizard.mjs +48 -0
  275. package/scripts/lib/env-check.mjs +89 -0
  276. package/scripts/lib/eval/engine.mjs +605 -0
  277. package/scripts/lib/eval/judge.mjs +433 -0
  278. package/scripts/lib/eval/report.mjs +367 -0
  279. package/scripts/lib/eval/schema.mjs +618 -0
  280. package/scripts/lib/eval/session-resolve.mjs +137 -0
  281. package/scripts/lib/eval/sink.mjs +77 -0
  282. package/scripts/lib/events-rotation.mjs +86 -0
  283. package/scripts/lib/events-schema.mjs +81 -0
  284. package/scripts/lib/events.mjs +80 -0
  285. package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
  286. package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
  287. package/scripts/lib/exclusivity-matrix.mjs +68 -0
  288. package/scripts/lib/fetch-baseline.mjs +311 -0
  289. package/scripts/lib/file-lock.mjs +512 -0
  290. package/scripts/lib/frontend-detect/detect.mjs +138 -0
  291. package/scripts/lib/frontend-detect/rules.mjs +295 -0
  292. package/scripts/lib/frontmatter-guard.mjs +241 -0
  293. package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
  294. package/scripts/lib/gates/gate-baseline.mjs +42 -0
  295. package/scripts/lib/gates/gate-full.mjs +85 -0
  296. package/scripts/lib/gates/gate-helpers.mjs +231 -0
  297. package/scripts/lib/gates/gate-incremental.mjs +76 -0
  298. package/scripts/lib/gates/gate-per-file.mjs +55 -0
  299. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
  300. package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
  301. package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
  302. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
  303. package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
  304. package/scripts/lib/handover-gate.mjs +222 -0
  305. package/scripts/lib/hardening.mjs +43 -0
  306. package/scripts/lib/hardware-pattern-detector.mjs +238 -0
  307. package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
  308. package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
  309. package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
  310. package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
  311. package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
  312. package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
  313. package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
  314. package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
  315. package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
  316. package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
  317. package/scripts/lib/harness-audit/categories.mjs +19 -0
  318. package/scripts/lib/historical-guard.mjs +15 -0
  319. package/scripts/lib/host-identity.mjs +262 -0
  320. package/scripts/lib/instruction-budget-guard.mjs +332 -0
  321. package/scripts/lib/io.mjs +304 -0
  322. package/scripts/lib/issue-close-strip-labels.mjs +161 -0
  323. package/scripts/lib/language-mappers/README.md +57 -0
  324. package/scripts/lib/language-mappers/index.mjs +165 -0
  325. package/scripts/lib/language-mappers/markdown.mjs +149 -0
  326. package/scripts/lib/language-mappers/python.mjs +249 -0
  327. package/scripts/lib/language-mappers/swift.mjs +201 -0
  328. package/scripts/lib/language-mappers/typescript.mjs +433 -0
  329. package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
  330. package/scripts/lib/learnings/filters.mjs +43 -0
  331. package/scripts/lib/learnings/io.mjs +255 -0
  332. package/scripts/lib/learnings/schema.mjs +518 -0
  333. package/scripts/lib/learnings/surface.mjs +207 -0
  334. package/scripts/lib/learnings.mjs +42 -0
  335. package/scripts/lib/lock-reaper.mjs +648 -0
  336. package/scripts/lib/locks/index.mjs +31 -0
  337. package/scripts/lib/locks/lock-body.mjs +62 -0
  338. package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
  339. package/scripts/lib/locks/state-md-lock.mjs +351 -0
  340. package/scripts/lib/loop-readiness-banner.mjs +144 -0
  341. package/scripts/lib/memory-banner.mjs +478 -0
  342. package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
  343. package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
  344. package/scripts/lib/memory-paths.mjs +31 -0
  345. package/scripts/lib/memory-proposals/collector.mjs +334 -0
  346. package/scripts/lib/memory-proposals/schema.mjs +289 -0
  347. package/scripts/lib/memory-proposals/sink.mjs +507 -0
  348. package/scripts/lib/memory-proposals/store.mjs +441 -0
  349. package/scripts/lib/mission-status-schema.mjs +114 -0
  350. package/scripts/lib/mode-selector/alternatives.mjs +64 -0
  351. package/scripts/lib/mode-selector/constants.mjs +29 -0
  352. package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
  353. package/scripts/lib/mode-selector/rationale.mjs +55 -0
  354. package/scripts/lib/mode-selector/scoring.mjs +221 -0
  355. package/scripts/lib/mode-selector-accuracy.mjs +121 -0
  356. package/scripts/lib/mode-selector.mjs +160 -0
  357. package/scripts/lib/multi-provider-build/providers.mjs +64 -0
  358. package/scripts/lib/multi-provider-build/templating.mjs +130 -0
  359. package/scripts/lib/named-baseline-resolver.mjs +233 -0
  360. package/scripts/lib/named-vault-resolver.mjs +433 -0
  361. package/scripts/lib/owner-config/coerce.mjs +29 -0
  362. package/scripts/lib/owner-config/constants.mjs +21 -0
  363. package/scripts/lib/owner-config/defaults.mjs +50 -0
  364. package/scripts/lib/owner-config/error.mjs +19 -0
  365. package/scripts/lib/owner-config/index.mjs +13 -0
  366. package/scripts/lib/owner-config/merge.mjs +52 -0
  367. package/scripts/lib/owner-config/validate.mjs +259 -0
  368. package/scripts/lib/owner-config-banner.mjs +126 -0
  369. package/scripts/lib/owner-config-loader.mjs +159 -0
  370. package/scripts/lib/owner-config.example.yaml +72 -0
  371. package/scripts/lib/owner-config.mjs +28 -0
  372. package/scripts/lib/owner-interview.mjs +243 -0
  373. package/scripts/lib/owner-yaml.mjs +571 -0
  374. package/scripts/lib/package-manager.mjs +160 -0
  375. package/scripts/lib/path-utils.mjs +217 -0
  376. package/scripts/lib/peer-cards/merger.mjs +310 -0
  377. package/scripts/lib/peer-cards/reader.mjs +125 -0
  378. package/scripts/lib/peer-cards/schema.mjs +230 -0
  379. package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
  380. package/scripts/lib/peer-cards/writer.mjs +138 -0
  381. package/scripts/lib/peer-discovery.mjs +200 -0
  382. package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
  383. package/scripts/lib/persona-panel/consolidator.mjs +370 -0
  384. package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
  385. package/scripts/lib/persona-panel/threshold.mjs +130 -0
  386. package/scripts/lib/pi-hook-bridge.mjs +328 -0
  387. package/scripts/lib/platform.mjs +266 -0
  388. package/scripts/lib/playwright-driver/runner.mjs +297 -0
  389. package/scripts/lib/plugin-root.mjs +210 -0
  390. package/scripts/lib/pre-dispatch-check.mjs +126 -0
  391. package/scripts/lib/product-repo-detect.mjs +121 -0
  392. package/scripts/lib/profiles/registry.mjs +176 -0
  393. package/scripts/lib/profiles/schema.mjs +209 -0
  394. package/scripts/lib/qg-command-drift-banner.mjs +88 -0
  395. package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
  396. package/scripts/lib/quality-gate.mjs +536 -0
  397. package/scripts/lib/quality-gates-cache.mjs +228 -0
  398. package/scripts/lib/quality-gates-policy.mjs +95 -0
  399. package/scripts/lib/recommendations-v0.mjs +156 -0
  400. package/scripts/lib/reconcile/eligibility.mjs +203 -0
  401. package/scripts/lib/reconcile/emitter.mjs +244 -0
  402. package/scripts/lib/reconcile/engine.mjs +412 -0
  403. package/scripts/lib/reconcile/idempotency.mjs +239 -0
  404. package/scripts/lib/reconcile/renderer.mjs +211 -0
  405. package/scripts/lib/reconcile/writer.mjs +293 -0
  406. package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
  407. package/scripts/lib/resource-probe/evaluate.mjs +190 -0
  408. package/scripts/lib/resource-probe/parsers.mjs +181 -0
  409. package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
  410. package/scripts/lib/resource-probe.mjs +95 -0
  411. package/scripts/lib/rule-loader.mjs +552 -0
  412. package/scripts/lib/rules-sync.mjs +439 -0
  413. package/scripts/lib/scope-gate.mjs +496 -0
  414. package/scripts/lib/session-close-backfill.mjs +539 -0
  415. package/scripts/lib/session-discovery.mjs +256 -0
  416. package/scripts/lib/session-end/phase-skip.mjs +357 -0
  417. package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
  418. package/scripts/lib/session-id.mjs +362 -0
  419. package/scripts/lib/session-lock.mjs +703 -0
  420. package/scripts/lib/session-registry.mjs +355 -0
  421. package/scripts/lib/session-schema/aliases.mjs +71 -0
  422. package/scripts/lib/session-schema/constants.mjs +115 -0
  423. package/scripts/lib/session-schema/normalizer.mjs +66 -0
  424. package/scripts/lib/session-schema/timestamps.mjs +64 -0
  425. package/scripts/lib/session-schema/validator.mjs +453 -0
  426. package/scripts/lib/session-schema.mjs +71 -0
  427. package/scripts/lib/session-token-rollup.mjs +137 -0
  428. package/scripts/lib/sessions-staleness-banner.mjs +247 -0
  429. package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
  430. package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
  431. package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
  432. package/scripts/lib/skill-evolution/engine.mjs +719 -0
  433. package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
  434. package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
  435. package/scripts/lib/skill-health/join.mjs +181 -0
  436. package/scripts/lib/skill-health/score.mjs +123 -0
  437. package/scripts/lib/skill-invocations-schema.mjs +214 -0
  438. package/scripts/lib/skill-judge.mjs +348 -0
  439. package/scripts/lib/skill-judgments-schema.mjs +264 -0
  440. package/scripts/lib/slopcheck.mjs +501 -0
  441. package/scripts/lib/soul-resolve.mjs +118 -0
  442. package/scripts/lib/spiral-carryover.mjs +495 -0
  443. package/scripts/lib/state-md/body-sections.mjs +851 -0
  444. package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
  445. package/scripts/lib/state-md/mission-status.mjs +247 -0
  446. package/scripts/lib/state-md/recommendations.mjs +57 -0
  447. package/scripts/lib/state-md/yaml-parser.mjs +234 -0
  448. package/scripts/lib/state-md-peer-guard.mjs +232 -0
  449. package/scripts/lib/state-md.mjs +53 -0
  450. package/scripts/lib/subagents-schema.mjs +309 -0
  451. package/scripts/lib/sunset/walker.mjs +1192 -0
  452. package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
  453. package/scripts/lib/test-runner/fingerprint.mjs +33 -0
  454. package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
  455. package/scripts/lib/tmux-layout/layouts.mjs +224 -0
  456. package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
  457. package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
  458. package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
  459. package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
  460. package/scripts/lib/validate/check-agents.mjs +457 -0
  461. package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
  462. package/scripts/lib/validate/check-commands.mjs +148 -0
  463. package/scripts/lib/validate/check-component-paths.mjs +112 -0
  464. package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
  465. package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
  466. package/scripts/lib/validate/check-json-files.mjs +116 -0
  467. package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
  468. package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
  469. package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
  470. package/scripts/lib/validate/check-pi-package.mjs +110 -0
  471. package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
  472. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
  473. package/scripts/lib/validate/check-plugin-json.mjs +96 -0
  474. package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
  475. package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
  476. package/scripts/lib/validate/check-rules.mjs +143 -0
  477. package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
  478. package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
  479. package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
  480. package/scripts/lib/validate/confidential-names.mjs +169 -0
  481. package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
  482. package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
  483. package/scripts/lib/validate/tier-inference.mjs +100 -0
  484. package/scripts/lib/validate-vendored-rules.mjs +519 -0
  485. package/scripts/lib/vault-archive.mjs +404 -0
  486. package/scripts/lib/vault-backfill/glab.mjs +164 -0
  487. package/scripts/lib/vault-backfill/manifest.mjs +75 -0
  488. package/scripts/lib/vault-backfill/template.mjs +130 -0
  489. package/scripts/lib/vault-consolidate-fs.mjs +331 -0
  490. package/scripts/lib/vault-migration-rules.mjs +155 -0
  491. package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
  492. package/scripts/lib/vault-mirror/namespace.mjs +152 -0
  493. package/scripts/lib/vault-mirror/process.mjs +567 -0
  494. package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
  495. package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
  496. package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
  497. package/scripts/lib/vault-mirror/render.mjs +8 -0
  498. package/scripts/lib/vault-mirror/utils.mjs +217 -0
  499. package/scripts/lib/vault-relocation-rules.mjs +555 -0
  500. package/scripts/lib/vault-repo-backfill.mjs +235 -0
  501. package/scripts/lib/vault-staleness-banner.mjs +142 -0
  502. package/scripts/lib/vault-status/board-writer.mjs +769 -0
  503. package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
  504. package/scripts/lib/vault-sync-baseline.mjs +152 -0
  505. package/scripts/lib/wave-context.mjs +29 -0
  506. package/scripts/lib/wave-executor/pool.mjs +248 -0
  507. package/scripts/lib/wave-resource-gate.mjs +204 -0
  508. package/scripts/lib/wave-sizing.mjs +75 -0
  509. package/scripts/lib/webhook-url.mjs +105 -0
  510. package/scripts/lib/workspace.mjs +198 -0
  511. package/scripts/lib/worktree/constants.mjs +35 -0
  512. package/scripts/lib/worktree/index.mjs +17 -0
  513. package/scripts/lib/worktree/lifecycle.mjs +287 -0
  514. package/scripts/lib/worktree/listing.mjs +118 -0
  515. package/scripts/lib/worktree/meta.mjs +64 -0
  516. package/scripts/lib/worktree-freshness.mjs +313 -0
  517. package/scripts/lib/worktree.mjs +15 -0
  518. package/scripts/lifecycle-sim-v6.mjs +347 -0
  519. package/scripts/lock-reaper.mjs +185 -0
  520. package/scripts/mcp-server.sh +241 -0
  521. package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
  522. package/scripts/memory-propose.mjs +464 -0
  523. package/scripts/migrate-cold-start-seed.mjs +404 -0
  524. package/scripts/migrate-learnings-jsonl.mjs +189 -0
  525. package/scripts/migrate-legacy-learnings.sh +61 -0
  526. package/scripts/migrate-sessions-jsonl.mjs +448 -0
  527. package/scripts/migrate-subagents-jsonl.mjs +196 -0
  528. package/scripts/migrate-vault-paths.mjs +796 -0
  529. package/scripts/parse-config.mjs +149 -0
  530. package/scripts/pi-install.mjs +117 -0
  531. package/scripts/print-applicable-rules.mjs +247 -0
  532. package/scripts/promote-vault-strict.mjs +496 -0
  533. package/scripts/relocate-vault-corpus.mjs +1178 -0
  534. package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
  535. package/scripts/run-quality-gate.mjs +216 -0
  536. package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
  537. package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
  538. package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
  539. package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
  540. package/scripts/sweep-expired-learnings.mjs +135 -0
  541. package/scripts/sync-vault-schema.mjs +376 -0
  542. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
  543. package/scripts/tmux-layout.mjs +245 -0
  544. package/scripts/token-audit.sh +191 -0
  545. package/scripts/typecheck.mjs +42 -0
  546. package/scripts/upload-social-preview.mjs +316 -0
  547. package/scripts/validate-config.mjs +46 -0
  548. package/scripts/validate-plugin-manifests.mjs +163 -0
  549. package/scripts/validate-plugin.mjs +264 -0
  550. package/scripts/validate-wave-scope.mjs +289 -0
  551. package/scripts/vault-backfill.mjs +404 -0
  552. package/scripts/vault-consolidate.mjs +596 -0
  553. package/scripts/vault-integration-watcher.mjs +394 -0
  554. package/scripts/vault-mirror.mjs +430 -0
  555. package/skills/_shared/bootstrap-gate.md +111 -0
  556. package/skills/_shared/config-reading.md +226 -0
  557. package/skills/_shared/instruction-file-resolution.md +79 -0
  558. package/skills/_shared/model-selection.md +64 -0
  559. package/skills/_shared/monitor-patterns.md +300 -0
  560. package/skills/_shared/parallel-aware-auq.md +121 -0
  561. package/skills/_shared/parallel-aware-preamble.md +185 -0
  562. package/skills/_shared/platform-tools.md +96 -0
  563. package/skills/_shared/state-ownership.md +221 -0
  564. package/skills/architecture/DEEPENING.md +37 -0
  565. package/skills/architecture/INTERFACE-DESIGN.md +44 -0
  566. package/skills/architecture/LANGUAGE.md +53 -0
  567. package/skills/architecture/SKILL.md +92 -0
  568. package/skills/autopilot/SKILL.md +419 -0
  569. package/skills/bootstrap/SKILL.md +592 -0
  570. package/skills/bootstrap/STATE.md.template +24 -0
  571. package/skills/bootstrap/_shared-template.md +243 -0
  572. package/skills/bootstrap/deep-template.md +659 -0
  573. package/skills/bootstrap/fast-template.md +251 -0
  574. package/skills/bootstrap/intensity-heuristic.md +80 -0
  575. package/skills/bootstrap/public-fallback.md +342 -0
  576. package/skills/bootstrap/standard-template.md +736 -0
  577. package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
  578. package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
  579. package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
  580. package/skills/brainstorm/SKILL.md +268 -0
  581. package/skills/brainstorm/soul.md +49 -0
  582. package/skills/claude-md-drift-check/SKILL.md +186 -0
  583. package/skills/claude-md-drift-check/checker.mjs +1380 -0
  584. package/skills/claude-md-drift-check/checker.sh +37 -0
  585. package/skills/claude-md-drift-check/package.json +16 -0
  586. package/skills/convergence-monitoring/README.md +39 -0
  587. package/skills/convergence-monitoring/SIGNALS.md +246 -0
  588. package/skills/convergence-monitoring/SKILL.md +285 -0
  589. package/skills/daily/SKILL.md +222 -0
  590. package/skills/daily/generate.sh +92 -0
  591. package/skills/daily/templates/daily.md.tpl +36 -0
  592. package/skills/debug/SKILL.md +188 -0
  593. package/skills/debug/soul.md +35 -0
  594. package/skills/discovery/SKILL.md +567 -0
  595. package/skills/discovery/issue-templates.md +237 -0
  596. package/skills/discovery/probes/docs-staleness.mjs +195 -0
  597. package/skills/discovery/probes/frontend-slop.mjs +186 -0
  598. package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
  599. package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
  600. package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
  601. package/skills/discovery/probes/vault-staleness.mjs +272 -0
  602. package/skills/discovery/probes-arch.md +252 -0
  603. package/skills/discovery/probes-audit.md +95 -0
  604. package/skills/discovery/probes-code.md +329 -0
  605. package/skills/discovery/probes-docs.md +76 -0
  606. package/skills/discovery/probes-feature.md +150 -0
  607. package/skills/discovery/probes-infra.md +138 -0
  608. package/skills/discovery/probes-intro.md +25 -0
  609. package/skills/discovery/probes-session.md +495 -0
  610. package/skills/discovery/probes-supply-chain.md +94 -0
  611. package/skills/discovery/probes-ui.md +147 -0
  612. package/skills/discovery/probes-vault.md +64 -0
  613. package/skills/discovery/slop-patterns.md +115 -0
  614. package/skills/dispatcher/SKILL.md +173 -0
  615. package/skills/docs-orchestrator/SKILL.md +362 -0
  616. package/skills/docs-orchestrator/audience-mapping.md +140 -0
  617. package/skills/domain-model/ADR-FORMAT.md +47 -0
  618. package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
  619. package/skills/domain-model/SKILL.md +85 -0
  620. package/skills/ecosystem-health/SKILL.md +119 -0
  621. package/skills/ecosystem-health/wizard.md +193 -0
  622. package/skills/eval/SKILL.md +293 -0
  623. package/skills/eval/rubric-v1.md +218 -0
  624. package/skills/evolve/SKILL.md +546 -0
  625. package/skills/frontmatter-guard/SKILL.md +126 -0
  626. package/skills/gitlab-ops/SKILL.md +368 -0
  627. package/skills/gitlab-portfolio/SKILL.md +196 -0
  628. package/skills/grill/SKILL.md +185 -0
  629. package/skills/grill/soul.md +55 -0
  630. package/skills/hook-development/SKILL.md +413 -0
  631. package/skills/mcp-builder/SKILL.md +260 -0
  632. package/skills/memory-cleanup/SKILL.md +310 -0
  633. package/skills/mode-selector/SKILL.md +226 -0
  634. package/skills/peekaboo-driver/SKILL.md +237 -0
  635. package/skills/peekaboo-driver/soul.md +32 -0
  636. package/skills/persona-panel/SKILL.md +365 -0
  637. package/skills/persona-panel/persona-format.md +205 -0
  638. package/skills/persona-panel/presets/designer-lens.md +87 -0
  639. package/skills/persona-panel/presets/engineer-lens.md +88 -0
  640. package/skills/persona-panel/presets/pm-lens.md +86 -0
  641. package/skills/plan/SKILL.md +496 -0
  642. package/skills/plan/mode-feature.md +141 -0
  643. package/skills/plan/mode-new.md +297 -0
  644. package/skills/plan/mode-retro.md +271 -0
  645. package/skills/plan/prd-feature-template.md +132 -0
  646. package/skills/plan/prd-full-template.md +151 -0
  647. package/skills/plan/prd-reviewer-prompt.md +103 -0
  648. package/skills/plan/retro-template.md +75 -0
  649. package/skills/plan/soul.md +62 -0
  650. package/skills/playwright-driver/SKILL.md +226 -0
  651. package/skills/playwright-driver/soul.md +30 -0
  652. package/skills/quality-gates/SKILL.md +212 -0
  653. package/skills/reconcile/SKILL.md +324 -0
  654. package/skills/repo-audit/SKILL.md +272 -0
  655. package/skills/session-end/SKILL.md +1044 -0
  656. package/skills/session-end/discovery-scan.md +37 -0
  657. package/skills/session-end/drift-operations.md +97 -0
  658. package/skills/session-end/learning-patterns.md +78 -0
  659. package/skills/session-end/metrics-collection.md +175 -0
  660. package/skills/session-end/phase-3-2-docs-verification.md +148 -0
  661. package/skills/session-end/phase-3-6-tail.md +344 -0
  662. package/skills/session-end/phase-3-7a-recommendations.md +86 -0
  663. package/skills/session-end/plan-verification.md +288 -0
  664. package/skills/session-end/session-metrics-write.md +223 -0
  665. package/skills/session-end/vault-operations.md +50 -0
  666. package/skills/session-end/verification-checklist.md +20 -0
  667. package/skills/session-plan/SKILL.md +554 -0
  668. package/skills/session-plan/wave-template.md +37 -0
  669. package/skills/session-start/SKILL.md +1043 -0
  670. package/skills/session-start/phase-2-5-docs-planning.md +119 -0
  671. package/skills/session-start/phase-4-5-resource-health.md +49 -0
  672. package/skills/session-start/phase-7-1-premise-check.md +47 -0
  673. package/skills/session-start/phase-7-5-mode-selector.md +237 -0
  674. package/skills/session-start/phase-8-5-express-path.md +61 -0
  675. package/skills/session-start/presentation-format.md +81 -0
  676. package/skills/session-start/soul.md +57 -0
  677. package/skills/skill-creator/SKILL.md +168 -0
  678. package/skills/spinout/SKILL.md +76 -0
  679. package/skills/sunset-review/SKILL.md +96 -0
  680. package/skills/test-runner/SKILL.md +362 -0
  681. package/skills/test-runner/rubric-v1.md +388 -0
  682. package/skills/test-runner/soul.md +46 -0
  683. package/skills/tmux-layout/SKILL.md +104 -0
  684. package/skills/ubiquitous-language/SKILL.md +97 -0
  685. package/skills/using-orchestrator/SKILL.md +144 -0
  686. package/skills/vault-mirror/SKILL.md +234 -0
  687. package/skills/vault-sync/SKILL.md +319 -0
  688. package/skills/vault-sync/package-lock.json +40 -0
  689. package/skills/vault-sync/package.json +11 -0
  690. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
  691. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  692. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
  693. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  694. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
  695. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
  696. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
  697. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
  698. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
  699. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
  700. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
  701. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  702. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
  703. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
  704. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  705. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  706. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
  707. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
  708. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  709. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
  710. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
  711. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
  712. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  713. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
  714. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  715. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
  716. package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
  717. package/skills/vault-sync/validator.mjs +658 -0
  718. package/skills/vault-sync/validator.sh +55 -0
  719. package/skills/wave-executor/SKILL.md +496 -0
  720. package/skills/wave-executor/circuit-breaker.md +169 -0
  721. package/skills/wave-executor/wave-loop.md +1043 -0
  722. package/skills/write-executable-plan/SKILL.md +237 -0
  723. package/skills/write-executable-plan/plan-template.md +154 -0
  724. package/templates/_minimal/CLAUDE.md.tmpl +41 -0
  725. package/templates/_minimal/README.md.tmpl +15 -0
  726. package/templates/_minimal/gitignore.tmpl +47 -0
  727. package/templates/_shared/harte-regeln.md +16 -0
  728. package/templates/_shared/loop.md +90 -0
  729. package/templates/_shared/rules/parallel-sessions.md +77 -0
  730. package/templates/nextjs-minimal/README.md +30 -0
  731. package/templates/nextjs-minimal/app/layout.tsx +18 -0
  732. package/templates/nextjs-minimal/app/page.tsx +7 -0
  733. package/templates/nextjs-minimal/eslint.config.mjs +16 -0
  734. package/templates/nextjs-minimal/next.config.mjs +4 -0
  735. package/templates/nextjs-minimal/package.json +27 -0
  736. package/templates/nextjs-minimal/tsconfig.json +23 -0
  737. package/templates/node-minimal/README.md +33 -0
  738. package/templates/node-minimal/eslint.config.mjs +10 -0
  739. package/templates/node-minimal/package.json +21 -0
  740. package/templates/node-minimal/src/index.ts +1 -0
  741. package/templates/node-minimal/tests/sanity.test.ts +5 -0
  742. package/templates/node-minimal/tsconfig.json +17 -0
  743. package/templates/personas/README.md +150 -0
  744. package/templates/personas/accounting-compliance.v1.md +120 -0
  745. package/templates/personas/accounting-tax-advisor.v1.md +116 -0
  746. package/templates/personas/buyer-p1-cto.v1.md +125 -0
  747. package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
  748. package/templates/personas/buyer-p3-build.v1.md +130 -0
  749. package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
  750. package/templates/personas/buyer-p5-solo.v1.md +132 -0
  751. package/templates/personas/buyer-p6-ld.v1.md +130 -0
  752. package/templates/personas/klima-ai-expert.v1.md +114 -0
  753. package/templates/personas/klima-physicist.v1.md +117 -0
  754. package/templates/python-uv/README.md +28 -0
  755. package/templates/python-uv/pyproject.toml +32 -0
  756. package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
  757. package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
  758. package/templates/python-uv/tests/test_sanity.py +2 -0
  759. package/templates/static-html/README.md +19 -0
  760. package/templates/static-html/index.html +15 -0
  761. package/templates/static-html/script.js +1 -0
  762. package/templates/static-html/styles.css +26 -0
@@ -0,0 +1,769 @@
1
+ /**
2
+ * board-writer.mjs — Render + idempotently write the vault live-status board.
3
+ *
4
+ * Epic #673 Phase 1 (issue #674). Writes a host-local board at
5
+ * `<vault-dir>/01-projects/_active-sessions.md` listing, per repo, one row:
6
+ * status ∈ {frei, in-progress, closed, force-closed}
7
+ * semantic-session-id · branch · mode · last-heartbeat
8
+ *
9
+ * Source of truth: the per-repo `session.lock` v2 lease (heartbeat-based
10
+ * liveness via {@link isLockLive}) plus the host-wide session registry (the
11
+ * registry entry is the ONLY source of `branch` — the lock lacks that field).
12
+ *
13
+ * Exports:
14
+ * GENERATOR_MARKER — frontmatter sentinel that identifies generator-owned files
15
+ * resolveBoardPath — vaultDir → `<vaultDir>/01-projects/_active-sessions.md`
16
+ * collectRows — per-repo status derivation (readLock + readRegistry)
17
+ * renderBoard — pure render: rows[] → full markdown (frontmatter + table)
18
+ * normalizeUpdated — stabilise the `updated:` line for byte-equality noop compare
19
+ * writeBoard — idempotent write with skip-handwritten / skip-noop / dry-run
20
+ * mirrorBoard — thin convenience: config-read + resolve + write (no-ops when vault off)
21
+ * buildSweepRepos — pure helper: Candidate[] → sweep repo descriptors (busy ∪ thisRepo)
22
+ * sweepBoard — host-wide sweep: enumerateCandidates + mirrorBoard (issue #716)
23
+ *
24
+ * Idempotent merge: writeBoard's caller passes a fully-rendered board, but
25
+ * {@link collectRows} preserves rows for repos NOT in the current update by
26
+ * reading the EXISTING generator-owned board first, so repeated writes are stable.
27
+ * PRESERVED `in-progress` rows are additionally re-derived for TTL staleness
28
+ * (issue #829 Finding 2) inside {@link mirrorBoard} — a preserved row whose
29
+ * heartbeat has aged past {@link DEFAULT_TTL_HOURS} flips to `force-closed`
30
+ * instead of being copied forward unboundedly.
31
+ *
32
+ * CRITICAL SAFETY (Epic #673 #1 risk — never clobber hand-authored vault notes):
33
+ * 1. The `_generator` marker guard refuses any file we did not author.
34
+ * 2. Defense-in-depth: writeBoard hard-refuses to touch `_overview.md`
35
+ * (sven-owned, must NEVER be written by this generator).
36
+ *
37
+ * No console noise — library code. Plain Node ESM. No external deps.
38
+ */
39
+
40
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
41
+ import os from 'node:os';
42
+ import path from 'node:path';
43
+
44
+ import { isLockLive, readLock, DEFAULT_TTL_HOURS } from '../session-lock.mjs';
45
+ import { readRegistry, repoPathHash, isRegistryEntryFresh } from '../session-registry.mjs';
46
+ import { parseFrontmatter } from '../vault-mirror/utils.mjs';
47
+ import { readConfigFile, parseSessionConfig } from '../config.mjs';
48
+ import { validatePathInsideProject } from '../path-utils.mjs';
49
+ import { enumerateCandidates } from '../dispatcher/enumerate.mjs';
50
+
51
+ /** Frontmatter sentinel that identifies generator-owned board files. */
52
+ export const GENERATOR_MARKER = 'session-orchestrator-active-sessions@1';
53
+
54
+ /** Placeholder used for noop comparison (replaces the live `updated:` value). */
55
+ const UPDATED_PLACEHOLDER = '__UPDATED_PLACEHOLDER__';
56
+
57
+ /** The four board statuses, in display priority order. */
58
+ const STATUS_IN_PROGRESS = 'in-progress';
59
+ const STATUS_FORCE_CLOSED = 'force-closed';
60
+ const STATUS_CLOSED = 'closed';
61
+ const STATUS_FREI = 'frei';
62
+
63
+ // ── Key normalization ────────────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Fold a repo-name key to a case-insensitive form for merge/compare purposes
67
+ * (issue #719). On case-insensitive-preserving filesystems (APFS, the default
68
+ * for macOS Home volumes), `some-repo` and `Some-Repo` are the SAME
69
+ * physical directory — every site that keys rows by `repoName` (prior-status
70
+ * lookup, preserved-row map, merge upsert) must fold through this helper so
71
+ * the two casings collapse to one board row instead of rendering as
72
+ * duplicates. Row OBJECTS are left untouched — {@link renderBoard} still
73
+ * displays the row's original `repo` string (true on-disk casing); only the
74
+ * MAP KEY is folded.
75
+ *
76
+ * @param {string} s
77
+ * @returns {string}
78
+ */
79
+ const foldKey = (s) => String(s ?? '').toLowerCase();
80
+
81
+ // ── Path helpers ────────────────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Expand a leading `~` to the current user's home directory. Inlined here on
85
+ * purpose — the shared helper is private elsewhere, and a shared
86
+ * `vault-write-guard.mjs` extraction is deferred to a later epic (W2 forbids a
87
+ * new shared file in this slice).
88
+ *
89
+ * @param {string} p
90
+ * @returns {string}
91
+ */
92
+ function expandHome(p) {
93
+ if (typeof p !== 'string' || p.length === 0) return p;
94
+ if (p === '~') return os.homedir();
95
+ if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
96
+ return p;
97
+ }
98
+
99
+ /**
100
+ * Resolve the board file path from a vault directory.
101
+ *
102
+ * @param {string} vaultDir — absolute or `~`-prefixed vault root
103
+ * @returns {string} `<vaultDir>/01-projects/_active-sessions.md`
104
+ */
105
+ export function resolveBoardPath(vaultDir) {
106
+ return path.join(expandHome(vaultDir), '01-projects', '_active-sessions.md');
107
+ }
108
+
109
+ // ── Formatting helpers ───────────────────────────────────────────────────────────
110
+
111
+ /**
112
+ * Format a Date as ISO 8601 (for frontmatter fields).
113
+ * @param {Date} date
114
+ * @returns {string}
115
+ */
116
+ function toIso(date) {
117
+ return date.toISOString();
118
+ }
119
+
120
+ /**
121
+ * Format a last-heartbeat ISO string for table display. Returns '—' if absent.
122
+ * Kept verbatim (the ISO string) so operators can diff against lock files.
123
+ *
124
+ * @param {string|null|undefined} iso
125
+ * @returns {string}
126
+ */
127
+ function fmtHeartbeat(iso) {
128
+ if (!iso) return '—';
129
+ return String(iso);
130
+ }
131
+
132
+ /**
133
+ * Render a cell value, substituting '—' for empty/absent values and escaping
134
+ * the pipe character so a stray `|` cannot break the markdown table.
135
+ *
136
+ * @param {string|null|undefined} value
137
+ * @returns {string}
138
+ */
139
+ function cell(value) {
140
+ if (value === null || value === undefined || value === '') return '—';
141
+ return String(value).replace(/\|/g, '\\|');
142
+ }
143
+
144
+ // ── Row collection (status derivation) ───────────────────────────────────────────
145
+
146
+ /**
147
+ * Build the rows array from each repo's session.lock + the host-wide registry.
148
+ *
149
+ * Status derivation (this module derives ALL four states itself — no existing
150
+ * code does):
151
+ * - in-progress — readLock non-null AND isLockLive(lock) === true.
152
+ * - force-closed — readLock non-null AND isLockLive(lock) === false (dead
153
+ * lease: heartbeat older than ttl). Fields are read straight
154
+ * off the raw lock — the dead lock is never silently dropped.
155
+ * - closed — explicit per-repo `status: 'closed'` override (session-end
156
+ * passes this for the current repo), OR a prior generator-owned
157
+ * board row was in-progress/force-closed and there is now no lock.
158
+ * - frei — no lock AND no fresh registry entry AND not previously in-progress.
159
+ *
160
+ * branch/mode/semantic-session-id: prefer the lock's, then the matching registry
161
+ * entry. `branch` ONLY exists on the registry entry (the lock has no branch).
162
+ * semantic id = `semantic_session_id ?? session_id`.
163
+ *
164
+ * @param {object} opts
165
+ * @param {Array<{ repoRoot: string, repoName?: string, status?: string }>} opts.repos
166
+ * Repos to compute rows for. `repoRoot` is an absolute path; `repoName`
167
+ * defaults to its basename; optional `status: 'closed'` forces a closed row.
168
+ * @param {Date} [opts.now] — clock seam (defaults to new Date()).
169
+ * @param {Array<object>} [opts.registry] — pre-read registry (test seam); defaults
170
+ * to a fresh {@link readRegistry} call.
171
+ * @param {Map<string, string>} [opts.priorStatusByRepo] — {@link foldKey}-folded
172
+ * (case-insensitive) repoName → prior board status, used to derive `closed`
173
+ * when a once-active repo now has no lock. Callers MUST fold the key with
174
+ * {@link foldKey} before inserting (issue #719) — this function folds its
175
+ * own lookup key to match.
176
+ * @returns {Promise<Array<{ repo: string, status: string, session: string|null,
177
+ * branch: string|null, mode: string|null, heartbeat: string|null }>>}
178
+ */
179
+ export async function collectRows({ repos, now = new Date(), registry, priorStatusByRepo } = {}) {
180
+ if (!Array.isArray(repos)) {
181
+ throw new TypeError('collectRows: opts.repos must be an array');
182
+ }
183
+
184
+ const nowMs = now instanceof Date ? now.getTime() : Date.now();
185
+ const registryEntries = Array.isArray(registry) ? registry : await readRegistry();
186
+ const priorStatus = priorStatusByRepo instanceof Map ? priorStatusByRepo : new Map();
187
+
188
+ const rows = [];
189
+
190
+ for (const repo of repos) {
191
+ if (!repo || typeof repo.repoRoot !== 'string' || repo.repoRoot.length === 0) {
192
+ // Skip malformed repo descriptors rather than throwing — one bad entry
193
+ // must not abort the whole board render.
194
+ continue;
195
+ }
196
+
197
+ const repoName = typeof repo.repoName === 'string' && repo.repoName.length > 0
198
+ ? repo.repoName
199
+ : path.basename(path.resolve(repo.repoRoot));
200
+
201
+ const lock = readLock({ repoRoot: repo.repoRoot });
202
+
203
+ // Match the registry entry for this repo by path hash (branch lives here only).
204
+ let registryEntry;
205
+ try {
206
+ const hash = repoPathHash(repo.repoRoot);
207
+ registryEntry = registryEntries.find((e) => e && e.repo_path_hash === hash) ?? null;
208
+ } catch {
209
+ registryEntry = null;
210
+ }
211
+
212
+ // Field resolution: prefer the lock, then the registry entry.
213
+ const semanticFromLock = lock
214
+ ? (lock.semantic_session_id ?? lock.session_id ?? null)
215
+ : null;
216
+ const semanticFromRegistry = registryEntry
217
+ ? (registryEntry.semantic_session_id ?? registryEntry.session_id ?? null)
218
+ : null;
219
+ const session = semanticFromLock ?? semanticFromRegistry ?? null;
220
+
221
+ // branch ONLY exists on the registry entry — the lock has no branch field.
222
+ const branch = (registryEntry && registryEntry.branch) ? registryEntry.branch : null;
223
+
224
+ const mode = (lock && lock.mode)
225
+ ? lock.mode
226
+ : (registryEntry && registryEntry.mode ? registryEntry.mode : null);
227
+
228
+ const heartbeat = lock
229
+ ? (lock.last_heartbeat ?? lock.started_at ?? null)
230
+ : (registryEntry ? (registryEntry.last_heartbeat ?? null) : null);
231
+
232
+ // ── Status derivation ──
233
+ let status;
234
+ if (repo.status === STATUS_CLOSED) {
235
+ // Explicit per-repo override (session-end passes 'closed' for current repo).
236
+ status = STATUS_CLOSED;
237
+ } else if (lock && isLockLive(lock, nowMs)) {
238
+ status = STATUS_IN_PROGRESS;
239
+ } else if (lock) {
240
+ // Lock present but dead lease (heartbeat older than ttl).
241
+ status = STATUS_FORCE_CLOSED;
242
+ } else {
243
+ // No live lock. Derive status from the prior board state + registry freshness.
244
+ // Key is folded (issue #719) so `Some-Repo` and `some-repo` resolve
245
+ // to the same prior-status entry on case-insensitive-preserving filesystems.
246
+ const prior = priorStatus.get(foldKey(repoName));
247
+ if (prior === STATUS_CLOSED || prior === STATUS_FORCE_CLOSED) {
248
+ // Terminal prior state is STICKY absent a live lock. A still-fresh registry
249
+ // entry must NOT resurrect a cleanly-closed (or force-closed) repo to
250
+ // in-progress — only a real lock (handled above) re-asserts in-progress.
251
+ // Without this, a second bare refresh within the registry freshness window
252
+ // (default 15min) of a clean close would wrongly flip `closed` → `in-progress`
253
+ // (#674 review finding).
254
+ status = prior;
255
+ } else if (prior === STATUS_IN_PROGRESS) {
256
+ // Was live, lock now gone → the session ended → closed.
257
+ status = STATUS_CLOSED;
258
+ } else if (registryEntry && isRegistryEntryFresh(registryEntry, { now: nowMs })) {
259
+ // Never-seen repo with a fresh registry heartbeat but the lock momentarily
260
+ // absent — treat as in-progress (the session is heartbeating the registry).
261
+ status = STATUS_IN_PROGRESS;
262
+ } else {
263
+ status = STATUS_FREI;
264
+ }
265
+ }
266
+
267
+ rows.push({
268
+ repo: repoName,
269
+ status,
270
+ session: status === STATUS_FREI ? null : session,
271
+ branch: status === STATUS_FREI ? null : branch,
272
+ mode: status === STATUS_FREI ? null : mode,
273
+ heartbeat: status === STATUS_FREI ? null : heartbeat,
274
+ });
275
+ }
276
+
277
+ return rows;
278
+ }
279
+
280
+ // ── Render (pure) ────────────────────────────────────────────────────────────────
281
+
282
+ /**
283
+ * Render the board markdown from the rows array.
284
+ *
285
+ * Rows are sorted alphabetically by repo name for stable, diff-friendly output.
286
+ *
287
+ * @param {Array<{ repo: string, status: string, session?: string|null,
288
+ * branch?: string|null, mode?: string|null, heartbeat?: string|null }>} rows
289
+ * @param {{ now: Date, createdIso?: string, updatedPlaceholder?: string }} opts
290
+ * @returns {string} full markdown (frontmatter + table)
291
+ */
292
+ export function renderBoard(rows, opts = {}) {
293
+ const { now, createdIso, updatedPlaceholder } = opts;
294
+ const nowDate = now instanceof Date ? now : new Date();
295
+
296
+ const nowIso = toIso(nowDate);
297
+ const updatedValue = updatedPlaceholder ?? nowIso;
298
+ const createdValue = createdIso ?? nowIso;
299
+
300
+ const sortedRows = [...(Array.isArray(rows) ? rows : [])].sort((a, b) =>
301
+ String(a?.repo ?? '').localeCompare(String(b?.repo ?? '')),
302
+ );
303
+
304
+ const lines = [];
305
+
306
+ // Frontmatter
307
+ lines.push('---');
308
+ lines.push(`_generator: ${GENERATOR_MARKER}`);
309
+ lines.push('id: active-sessions');
310
+ lines.push('type: board');
311
+ lines.push(`created: ${createdValue}`);
312
+ lines.push(`updated: ${updatedValue}`);
313
+ lines.push('---');
314
+ lines.push('');
315
+
316
+ // Title + preamble
317
+ lines.push('# Active Sessions');
318
+ lines.push('');
319
+ lines.push('> Live session-status board. Generator-owned — do not hand-edit.');
320
+ lines.push('');
321
+
322
+ // Board table
323
+ lines.push('| Repo | Status | Session | Branch | Mode | Last heartbeat |');
324
+ lines.push('|---|---|---|---|---|---|');
325
+ for (const row of sortedRows) {
326
+ lines.push(
327
+ `| ${cell(row?.repo)} | ${cell(row?.status)} | ${cell(row?.session)} | ` +
328
+ `${cell(row?.branch)} | ${cell(row?.mode)} | ${cell(fmtHeartbeat(row?.heartbeat))} |`,
329
+ );
330
+ }
331
+ lines.push('');
332
+
333
+ return lines.join('\n');
334
+ }
335
+
336
+ // ── Write (idempotent) ───────────────────────────────────────────────────────────
337
+
338
+ /**
339
+ * Normalize a markdown string by replacing the `updated:` frontmatter line with
340
+ * a stable placeholder, enabling byte-for-byte noop comparison.
341
+ *
342
+ * @param {string} content
343
+ * @returns {string}
344
+ */
345
+ export function normalizeUpdated(content) {
346
+ return String(content ?? '').replace(/^(updated:\s*)(.+)$/m, `$1${UPDATED_PLACEHOLDER}`);
347
+ }
348
+
349
+ /**
350
+ * Parse a generator-owned board's table back into row objects. Used by the
351
+ * idempotent-merge path in {@link mirrorBoard} to recover prior per-repo rows
352
+ * (status carry-over + row preservation for repos not in the current update).
353
+ *
354
+ * Tolerant by design: skips the header + separator rows, ignores any line that
355
+ * is not a 6-column table row, and maps the literal '—' placeholder back to
356
+ * null. Unescapes the `\|` pipe-escaping applied by {@link renderBoard}.
357
+ *
358
+ * @param {string} content — full board markdown
359
+ * @returns {Array<{ repo: string, status: string, session: string|null,
360
+ * branch: string|null, mode: string|null, heartbeat: string|null }>}
361
+ */
362
+ export function parseBoardRows(content) {
363
+ const rows = [];
364
+ const text = String(content ?? '');
365
+ for (const rawLine of text.split(/\r?\n/)) {
366
+ const line = rawLine.trim();
367
+ if (!line.startsWith('|')) continue;
368
+ // Split on unescaped pipes, drop the leading/trailing empty fields.
369
+ const cells = line
370
+ .split(/(?<!\\)\|/)
371
+ .slice(1, -1)
372
+ .map((c) => c.trim());
373
+ if (cells.length !== 6) continue;
374
+ // Skip the header row and the |---|---| separator row.
375
+ if (cells[0] === 'Repo' || /^-+$/.test(cells[0])) continue;
376
+ const unesc = (v) => (v === '—' ? null : v.replace(/\\\|/g, '|'));
377
+ const repo = unesc(cells[0]);
378
+ if (repo === null) continue;
379
+ rows.push({
380
+ repo,
381
+ status: cells[1],
382
+ session: unesc(cells[2]),
383
+ branch: unesc(cells[3]),
384
+ mode: unesc(cells[4]),
385
+ heartbeat: unesc(cells[5]),
386
+ });
387
+ }
388
+ return rows;
389
+ }
390
+
391
+ /**
392
+ * Write the board file with idempotency + safety guards.
393
+ *
394
+ * Guard order:
395
+ * 1. dryRun → { action: 'dry-run' } (never touches disk).
396
+ * 2. SAFETY (Epic #673 #1 risk): basename === '_overview.md' →
397
+ * { action: 'skipped-handwritten' }. `_overview.md` is sven-owned and must
398
+ * NEVER be written by this generator, regardless of any marker.
399
+ * 3. file exists → parseFrontmatter:
400
+ * - !fm || !fm._generator → skipped-handwritten
401
+ * - fm._generator !== GENERATOR_MARKER → skipped-handwritten
402
+ * - normalizeUpdated(existing) === new → skipped-noop
403
+ * 4. else → mkdirSync(recursive) + writeFileSync → written.
404
+ *
405
+ * @param {{
406
+ * outputPath: string,
407
+ * content: string,
408
+ * dryRun?: boolean,
409
+ * fs?: { readFileSync?: Function, writeFileSync?: Function, mkdirSync?: Function, existsSync?: Function },
410
+ * }} opts
411
+ * @returns {{ action: 'written'|'skipped-handwritten'|'skipped-noop'|'dry-run', path: string }}
412
+ */
413
+ export function writeBoard(opts) {
414
+ const { outputPath, content, dryRun = false, fs: injectedFs } = opts;
415
+
416
+ const fsReadFile = injectedFs?.readFileSync ?? readFileSync;
417
+ const fsWriteFile = injectedFs?.writeFileSync ?? writeFileSync;
418
+ const fsMkdir = injectedFs?.mkdirSync ?? mkdirSync;
419
+ const fsExists = injectedFs?.existsSync ?? existsSync;
420
+
421
+ // 1. Dry-run: never write.
422
+ if (dryRun) {
423
+ return { action: 'dry-run', path: outputPath };
424
+ }
425
+
426
+ // 2. SAFETY (Epic #673 #1 risk): _overview.md is sven-owned — NEVER write it.
427
+ if (path.basename(outputPath) === '_overview.md') {
428
+ return { action: 'skipped-handwritten', path: outputPath };
429
+ }
430
+
431
+ // 3. Existing-file guards.
432
+ if (fsExists(outputPath)) {
433
+ let existingContent;
434
+ try {
435
+ existingContent = fsReadFile(outputPath, 'utf8');
436
+ } catch {
437
+ existingContent = null;
438
+ }
439
+
440
+ if (existingContent !== null && existingContent !== undefined) {
441
+ const fm = parseFrontmatter(existingContent);
442
+
443
+ // Skip-on-manual-edit: no _generator, or a different generator.
444
+ if (!fm || !fm['_generator']) {
445
+ return { action: 'skipped-handwritten', path: outputPath };
446
+ }
447
+ if (fm['_generator'] !== GENERATOR_MARKER) {
448
+ return { action: 'skipped-handwritten', path: outputPath };
449
+ }
450
+
451
+ // Skip-noop: identical modulo the live `updated:` timestamp.
452
+ if (normalizeUpdated(existingContent) === normalizeUpdated(content)) {
453
+ return { action: 'skipped-noop', path: outputPath };
454
+ }
455
+ }
456
+ }
457
+
458
+ // 4. Write.
459
+ fsMkdir(path.dirname(outputPath), { recursive: true });
460
+ fsWriteFile(outputPath, content, 'utf8');
461
+ return { action: 'written', path: outputPath };
462
+ }
463
+
464
+ // ── Convenience: config-read + resolve + write ───────────────────────────────────
465
+
466
+ /**
467
+ * Thin convenience that reads Session Config, resolves the vault board path,
468
+ * collects rows, renders, and writes. Silently no-ops when vault-integration is
469
+ * disabled or vault-dir is absent — returns `{ action: 'skipped-vault-disabled' }`.
470
+ *
471
+ * The caller (session-start / session-end) typically owns the higher-level
472
+ * decision and may call {@link collectRows} + {@link renderBoard} + {@link writeBoard}
473
+ * directly; this helper exists for the common single-repo update path.
474
+ *
475
+ * Vault path assertion (Epic #673 safety): the resolved vault dir MUST live
476
+ * under $HOME — a vault outside the home tree is refused as `skipped-vault-disabled`
477
+ * so a misconfigured path can never drive a write into an arbitrary location.
478
+ *
479
+ * @param {object} opts
480
+ * @param {string} opts.repoRoot — the repo whose row is being updated.
481
+ * @param {Array<{ repoRoot: string, repoName?: string, status?: string }>} [opts.repos]
482
+ * Full repo list; defaults to `[{ repoRoot, status: explicitStatus }]`.
483
+ * @param {string} [opts.explicitStatus] — per-repo status override ('closed' from session-end).
484
+ * @param {Date} [opts.now]
485
+ * @param {boolean} [opts.dryRun]
486
+ * @param {object} [opts.fs] — injectable fs for tests.
487
+ * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
488
+ * — forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653).
489
+ * Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig: undefined }`) when
490
+ * asserting a fixture's committed `vault-dir` — omitting it reads the REAL host
491
+ * `owner.yaml`, whose `paths.vault-dir` override (if set) wins over the fixture value
492
+ * and bleeds into the assertion (issue #783). Production callers omit this — the
493
+ * default (real owner.yaml resolution) is the correct host-local behavior there.
494
+ * @returns {Promise<{ action: string, path?: string }>}
495
+ */
496
+ export async function mirrorBoard({ repoRoot, repos, explicitStatus, now = new Date(), dryRun = false, fs, hostPaths } = {}) {
497
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
498
+ return { action: 'skipped-vault-disabled' };
499
+ }
500
+
501
+ // Read + parse Session Config. Any failure → silent no-op.
502
+ let config;
503
+ try {
504
+ const text = await readConfigFile(repoRoot);
505
+ config = parseSessionConfig(text, { hostPaths });
506
+ } catch {
507
+ return { action: 'skipped-vault-disabled' };
508
+ }
509
+
510
+ const vault = config?.['vault-integration'];
511
+ if (!vault || vault.enabled !== true) {
512
+ return { action: 'skipped-vault-disabled' };
513
+ }
514
+ const vaultDir = vault['vault-dir'];
515
+ if (typeof vaultDir !== 'string' || vaultDir.length === 0) {
516
+ return { action: 'skipped-vault-disabled' };
517
+ }
518
+
519
+ // Safety: the resolved vault dir must live under $HOME.
520
+ const expandedVault = expandHome(vaultDir);
521
+ const home = os.homedir();
522
+ const inHome = validatePathInsideProject(expandedVault, home);
523
+ if (!inHome.ok) {
524
+ return { action: 'skipped-vault-disabled' };
525
+ }
526
+
527
+ const repoList = Array.isArray(repos) && repos.length > 0
528
+ ? repos
529
+ : [{ repoRoot, status: explicitStatus }];
530
+
531
+ const outputPath = resolveBoardPath(vaultDir);
532
+
533
+ // Read the EXISTING generator-owned board (if any) to:
534
+ // 1. preserve its `created:` — otherwise every render differs on `created:`
535
+ // and the noop-skip in writeBoard would never fire.
536
+ // 2. recover the prior per-repo status — drives the `closed` derivation for
537
+ // repos NOT in this update (idempotent merge: their rows are re-derived).
538
+ // Both maps are keyed by {@link foldKey}(repo) — case-insensitively folded
539
+ // (issue #719) — so two prior rows differing only by case (e.g.
540
+ // `some-repo` vs `Some-Repo`, the same physical directory on a
541
+ // case-insensitive-preserving filesystem like APFS) collapse to ONE entry
542
+ // instead of coexisting as duplicates. The row OBJECTS keep their original
543
+ // `repo` string untouched, so `renderBoard` still displays true casing.
544
+ const fsReadFile = fs?.readFileSync ?? readFileSync;
545
+ const fsExists = fs?.existsSync ?? existsSync;
546
+ let createdIso;
547
+ const priorStatusByRepo = new Map();
548
+ const preservedRows = new Map(); // foldKey(repoName) → prior row (for merge)
549
+ if (fsExists(outputPath)) {
550
+ let existing;
551
+ try {
552
+ existing = fsReadFile(outputPath, 'utf8');
553
+ } catch {
554
+ existing = null;
555
+ }
556
+ if (existing) {
557
+ const fm = parseFrontmatter(existing);
558
+ if (fm && fm['_generator'] === GENERATOR_MARKER) {
559
+ if (fm['created']) createdIso = fm['created'];
560
+ for (const prior of parseBoardRows(existing)) {
561
+ const key = foldKey(prior.repo);
562
+ const collidingPrior = preservedRows.get(key);
563
+ if (collidingPrior) {
564
+ // Collision WITHIN parseBoardRows output — two prior rows fold to
565
+ // the same key with no fresh row in play yet (that upsert happens
566
+ // below). Prefer the row with the most-recent `heartbeat` rather
567
+ // than silently last-in-file-order. Guard: if either heartbeat is
568
+ // unparsable, fall through to last-written-wins (the pre-#719
569
+ // default) by NOT skipping the overwrite below.
570
+ const collidingTs = Date.parse(collidingPrior.heartbeat ?? '');
571
+ const priorTs = Date.parse(prior.heartbeat ?? '');
572
+ if (Number.isFinite(collidingTs) && Number.isFinite(priorTs) && collidingTs > priorTs) {
573
+ // The already-preserved row is strictly newer — keep it, skip
574
+ // this older colliding row entirely.
575
+ continue;
576
+ }
577
+ }
578
+ priorStatusByRepo.set(key, prior.status);
579
+ preservedRows.set(key, prior);
580
+ }
581
+ }
582
+ }
583
+ }
584
+
585
+ const rows = await collectRows({ repos: repoList, now, priorStatusByRepo });
586
+
587
+ // TTL-staleness re-derivation for PRESERVED rows (issue #829 Finding 2).
588
+ // Without this pass, a preserved `in-progress` row (a repo NOT in this
589
+ // update) is copied forward FOREVER — a crashed/never-closed session's row
590
+ // never flips even after its heartbeat has aged well past the lock's TTL,
591
+ // because `collectRows` only re-derives status for repos actually IN
592
+ // `repoList`. Re-derive staleness for every preserved row here, BEFORE the
593
+ // freshly-derived `rows` are upserted over it below (fresh data always
594
+ // wins regardless of this pass — a live lock or an explicit-closed update
595
+ // always takes precedence over the TTL flip).
596
+ //
597
+ // Board rows carry only a raw `heartbeat` string, never the lock's own
598
+ // `ttl_hours` (that field is not part of the rendered board) — so this
599
+ // reuses the shared {@link DEFAULT_TTL_HOURS} constant rather than the
600
+ // per-lock TTL {@link isLockLive} uses when a live lock object is in hand.
601
+ // Rows with an unparseable/absent heartbeat are left UNCHANGED (fail-open,
602
+ // never crash on a malformed prior board).
603
+ const nowMs = now instanceof Date ? now.getTime() : Date.now();
604
+ const ttlMs = DEFAULT_TTL_HOURS * 3600 * 1000;
605
+ const staleRederivedRows = new Map();
606
+ for (const [key, row] of preservedRows) {
607
+ if (row.status === STATUS_IN_PROGRESS) {
608
+ const heartbeatMs = Date.parse(row.heartbeat ?? '');
609
+ if (Number.isFinite(heartbeatMs) && (nowMs - heartbeatMs) >= ttlMs) {
610
+ staleRederivedRows.set(key, { ...row, status: STATUS_FORCE_CLOSED });
611
+ continue;
612
+ }
613
+ }
614
+ staleRederivedRows.set(key, row);
615
+ }
616
+
617
+ // Idempotent merge: keep prior (TTL-rederived) rows for repos NOT in this
618
+ // update, then upsert the freshly-derived rows over them so repeated
619
+ // writes stay stable. Both the seed (`staleRederivedRows`) and the upsert
620
+ // key below are folded via {@link foldKey} (issue #719) — a
621
+ // freshly-derived row ALWAYS wins over a preserved row sharing its folded
622
+ // key, which is what collapses a live `Some-Repo` row over a stale
623
+ // preserved `some-repo` row on the next board write.
624
+ const merged = new Map(staleRederivedRows);
625
+ for (const row of rows) merged.set(foldKey(row.repo), row);
626
+
627
+ const content = renderBoard([...merged.values()], { now, createdIso });
628
+
629
+ return writeBoard({ outputPath, content, dryRun, fs });
630
+ }
631
+
632
+ // ── Host-wide sweep (issue #716) ─────────────────────────────────────────────────
633
+
634
+ /**
635
+ * Pure helper: reduce an {@link enumerateCandidates} result down to the sweep
636
+ * repo descriptors {@link mirrorBoard} expects — every BUSY candidate
637
+ * (`free === false`, i.e. `in-progress` or `force-closed`), unioned with the
638
+ * calling repo (`thisRepoRoot`) so its own row is always re-derived even when
639
+ * `enumerateCandidates` did not surface it (e.g. `thisRepoRoot` sits outside
640
+ * `startDir`'s confinement root).
641
+ *
642
+ * Free (`frei`) candidates are intentionally EXCLUDED — re-deriving them would
643
+ * add board noise for repos with nothing to report; their prior rows (if any)
644
+ * are preserved by {@link mirrorBoard}'s idempotent merge.
645
+ *
646
+ * Pure — no fs access, no I/O. Dedupe is by `path.resolve()` so a candidate
647
+ * already covering `thisRepoRoot` is not duplicated.
648
+ *
649
+ * @param {import('../dispatcher/enumerate.mjs').Candidate[]} candidates
650
+ * @param {{ thisRepoRoot: string }} opts
651
+ * @returns {Array<{ repoRoot: string }>}
652
+ */
653
+ export function buildSweepRepos(candidates, { thisRepoRoot } = {}) {
654
+ const list = Array.isArray(candidates) ? candidates : [];
655
+ const seen = new Set();
656
+ const repos = [];
657
+
658
+ for (const candidate of list) {
659
+ if (!candidate || candidate.free !== false) continue;
660
+ if (typeof candidate.repoRoot !== 'string' || candidate.repoRoot.length === 0) continue;
661
+ const resolved = path.resolve(candidate.repoRoot);
662
+ if (seen.has(resolved)) continue;
663
+ seen.add(resolved);
664
+ repos.push({ repoRoot: candidate.repoRoot });
665
+ }
666
+
667
+ if (typeof thisRepoRoot === 'string' && thisRepoRoot.length > 0) {
668
+ const resolvedThis = path.resolve(thisRepoRoot);
669
+ if (!seen.has(resolvedThis)) {
670
+ seen.add(resolvedThis);
671
+ repos.push({ repoRoot: thisRepoRoot });
672
+ }
673
+ }
674
+
675
+ return repos;
676
+ }
677
+
678
+ /**
679
+ * Host-wide vault-board staleness sweep (issue #716). Extends {@link mirrorBoard}'s
680
+ * single-repo re-derivation to every BUSY repo on the host, so a crashed session
681
+ * in repo A renders as `force-closed` on the board from any repo B's
682
+ * session-start — not only from repo A's own next session-start/-end.
683
+ *
684
+ * Composition: {@link enumerateCandidates}(startDir) → {@link buildSweepRepos}
685
+ * (busy ∪ thisRepo) → {@link mirrorBoard}(repos).
686
+ *
687
+ * Notes:
688
+ * (a) THIS repo's `in-progress` row is rendered from ITS OWN live
689
+ * `session.lock` lease via {@link collectRows} — NOT via `explicitStatus`
690
+ * (that field is inert for `'in-progress'`; `collectRows` only honors an
691
+ * explicit `status: 'closed'` override). The lease is the one Phase 1.2
692
+ * writes/heartbeats for the calling session.
693
+ * (b) `frei` (lock-less) repos are excluded from re-derivation to avoid board
694
+ * noise — see {@link buildSweepRepos}. Prior rows of un-swept repos
695
+ * (busy-but-not-enumerated, or genuinely frei) are preserved by
696
+ * {@link mirrorBoard}'s idempotent merge, never dropped.
697
+ * (c) The enumerate + collectRows path is synchronous fs (readdirSync /
698
+ * existsSync / readLock per candidate) — O(repos) small reads, single-digit
699
+ * ms at host scale (~31 repos observed). No timeout is applied: a sync
700
+ * call cannot be preempted in-process, so a timeout would only convert a
701
+ * slow sweep into a thrown error, not a faster one.
702
+ * (d) Merge key is `repoName` (`path.basename`), case-insensitively folded via
703
+ * {@link foldKey} (issue #719) — two rows differing only by case (e.g.
704
+ * `some-repo` vs `Some-Repo`, the same physical directory on a
705
+ * case-insensitive-preserving filesystem like APFS) now collapse to ONE
706
+ * board row instead of rendering as duplicates. The survivor is whichever
707
+ * row is live/newest: a freshly-derived row (this sweep's own
708
+ * `collectRows` output) always wins over a preserved stale row; among two
709
+ * PRESERVED rows colliding on the folded key, the more-recent `heartbeat`
710
+ * wins (see the collision-resolution loop inside {@link mirrorBoard}).
711
+ * Two GENUINELY different, differently-rooted repos that happen to share
712
+ * a basename (case-insensitively) still collapse to one row — that
713
+ * remains a known limitation, inherited from {@link collectRows}/
714
+ * {@link mirrorBoard}; not addressed here.
715
+ *
716
+ * Best-effort contract: `sweepBoard` itself never throws for an enumeration
717
+ * failure — `enumerateCandidates` is wrapped in try/catch; on ANY failure the
718
+ * sweep degrades to the pre-#716 single-repo write
719
+ * (`mirrorBoard({ repoRoot, explicitStatus: 'in-progress' })`) so the board
720
+ * write still happens. `mirrorBoard`'s own internal guards (vault disabled,
721
+ * `_overview.md` refusal, noop-skip, …) are untouched and still apply.
722
+ *
723
+ * @param {object} [opts]
724
+ * @param {string} opts.repoRoot — the calling repo (always included in the sweep).
725
+ * @param {string} [opts.startDir] — enumeration root; omitted in production so
726
+ * {@link enumerateCandidates} defaults to `getConfinementRoot()` (~/Projects).
727
+ * Test seam only — do NOT compute `path.dirname(repoRoot)` here.
728
+ * @param {Date|number} [opts.now] — clock seam. A `Date` is used as-is; a finite
729
+ * number is treated as epoch-ms (matching {@link collectRows}'s own
730
+ * `now instanceof Date ? … : Date.now()` convention); anything else falls
731
+ * back to `Date.now()`. Always forwarded to {@link mirrorBoard} as a `Date`
732
+ * (see body) — {@link renderBoard} only special-cases `now instanceof Date`,
733
+ * so a bare number would otherwise be silently discarded there.
734
+ * @param {boolean} [opts.dryRun]
735
+ * @param {object} [opts.fs] — injectable fs for {@link mirrorBoard}/{@link writeBoard}.
736
+ * @param {object} [opts.deps] — injectable deps for {@link enumerateCandidates}
737
+ * (test seam: `readdirSync`, `existsSync`, `readLock`, `isLockLive`,
738
+ * `getCrossRepoProjects`, `validatePathInsideProject`, `now`).
739
+ * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
740
+ * — forwarded verbatim to every {@link mirrorBoard} call below (both the happy-path
741
+ * and the enumeration-failure-fallback path), which in turn forwards it to
742
+ * {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653/#783). See
743
+ * {@link mirrorBoard}'s own `hostPaths` doc for the hermetic-test rationale.
744
+ * @returns {Promise<{ action: string, path?: string }>}
745
+ */
746
+ export async function sweepBoard({ repoRoot, startDir, now = new Date(), dryRun = false, fs, deps, hostPaths } = {}) {
747
+ // Accept both a Date and a caller-passed epoch-ms number (previously the
748
+ // numeric case was silently discarded by `now instanceof Date ? … : Date.now()`).
749
+ const nowMs = now instanceof Date
750
+ ? now.getTime()
751
+ : (typeof now === 'number' && Number.isFinite(now) ? now : Date.now());
752
+
753
+ // mirrorBoard → renderBoard only special-case `now instanceof Date` (a bare
754
+ // number falls through to `new Date()` inside renderBoard, breaking
755
+ // determinism). Forward a real Date built from `nowMs` so a numeric `now`
756
+ // stays deterministic end-to-end.
757
+ const nowForMirror = now instanceof Date ? now : new Date(nowMs);
758
+
759
+ try {
760
+ const candidates = await enumerateCandidates({ startDir, now: nowMs, deps });
761
+ const repos = buildSweepRepos(candidates, { thisRepoRoot: repoRoot });
762
+ return await mirrorBoard({ repoRoot, repos, now: nowForMirror, dryRun, fs, hostPaths });
763
+ } catch (err) {
764
+ console.warn('[sweepBoard] host-wide enumeration failed — degraded to single-repo board write:', err?.message ?? err);
765
+ // Best-effort fallback: enumeration failed for any reason — degrade to the
766
+ // pre-#716 single-repo write so the board is still updated for THIS repo.
767
+ return mirrorBoard({ repoRoot, explicitStatus: 'in-progress', now: nowForMirror, dryRun, fs, hostPaths });
768
+ }
769
+ }