session-orchestrator 5.2.0 → 5.4.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 (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -8,14 +8,18 @@
8
8
  import {
9
9
  readFile,
10
10
  writeFile,
11
+ appendFile,
11
12
  mkdir,
12
13
  rename,
13
14
  copyFile,
14
15
  readdir,
15
16
  unlink,
16
17
  } from 'node:fs/promises';
17
- import { existsSync } from 'node:fs';
18
+ import { existsSync, realpathSync } from 'node:fs';
19
+ import { AsyncLocalStorage } from 'node:async_hooks';
20
+ import { randomUUID } from 'node:crypto';
18
21
  import path from 'node:path';
22
+ import { withFileLock } from '../file-lock.mjs';
19
23
  import {
20
24
  validateLearning,
21
25
  normalizeLearning,
@@ -24,6 +28,117 @@ import {
24
28
  ValidationError,
25
29
  } from './schema.mjs';
26
30
 
31
+ // ---------------------------------------------------------------------------
32
+ // Store lock (GitLab #1447 point 8)
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * How long a writer waits for the store lock before giving up — the
37
+ * `withFileLock` default. Every critical section here is one read plus one
38
+ * append or rewrite of a JSONL file (milliseconds), so ten seconds of
39
+ * contention means a stuck holder, not a slow one.
40
+ */
41
+ export const LEARNINGS_LOCK_TIMEOUT_MS = 10000;
42
+
43
+ /**
44
+ * Thrown when the learnings store lock cannot be acquired. Every writer takes
45
+ * the lock BEFORE it touches the store, so this error always means nothing was
46
+ * written. The message is one line and names the lock file.
47
+ */
48
+ export class LearningsLockError extends Error {
49
+ /**
50
+ * @param {string} lockPath
51
+ * @param {'timeout'|'fs-error'} reason
52
+ * @param {number} timeoutMs
53
+ * @param {string} [detail]
54
+ */
55
+ constructor(lockPath, reason, timeoutMs, detail) {
56
+ const why =
57
+ reason === 'timeout'
58
+ ? `held by another writer, waited ${timeoutMs} ms`
59
+ : `${reason}${detail ? `: ${detail}` : ''}`;
60
+ super(`learnings store lock ${lockPath} not acquired (${why}) — nothing written`);
61
+ this.name = 'LearningsLockError';
62
+ this.lockPath = lockPath;
63
+ this.reason = reason;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Store locks held by the CURRENT async call chain, so a nested writer inside a
69
+ * critical section (apply → prune → rewrite, sweep → rewrite, promote →
70
+ * rewrite) runs straight through instead of waiting on its own lock until the
71
+ * timeout. Keyed per async context, deliberately NOT per process: a concurrent
72
+ * call chain in the same process does not inherit the entry and waits like a
73
+ * second process would — which is exactly the interleaving the lock exists for.
74
+ * @type {AsyncLocalStorage<Map<string, {live: boolean}>>}
75
+ */
76
+ const heldStoreLocks = new AsyncLocalStorage();
77
+
78
+ /**
79
+ * `<store>.lock` beside the store. The directory is canonicalised so two
80
+ * spellings of one store (`/var` vs `/private/var`, a realpath'd repo root as
81
+ * `memory-proposals/sink.mjs` passes it) contend on ONE lock file. Creates the
82
+ * directory: the lock file needs it, and every caller is about to write there.
83
+ *
84
+ * @param {string} filePath
85
+ * @returns {Promise<string>}
86
+ */
87
+ async function resolveStoreLockPath(filePath) {
88
+ const dir = path.dirname(path.resolve(filePath));
89
+ await mkdir(dir, { recursive: true });
90
+ return path.join(realpathSync(dir), `${path.basename(filePath)}.lock`);
91
+ }
92
+
93
+ /**
94
+ * Run `fn` while holding the exclusive lock of the learnings store at
95
+ * `filePath`. Every read-modify-write of the store must run its WHOLE
96
+ * read → write sequence inside one call: a write outside the lock is silently
97
+ * lost when a rewriter that read before it renames its next generation over
98
+ * the file. `appendLearning()` and `rewriteLearnings()` take the lock
99
+ * themselves.
100
+ *
101
+ * Reentrant within one async call chain (see `heldStoreLocks`). Reentrancy
102
+ * prevents a deadlock, not a lost write: an `appendLearning()` issued INSIDE a
103
+ * read → rewrite section lands in the file that section is about to replace —
104
+ * fold such a record into the rewrite batch instead.
105
+ *
106
+ * Crash safety comes from `withFileLock`'s default `staleCheck: 'pid'`: a lock
107
+ * whose holder process is gone is overridden (WARN on stderr); a live holder or
108
+ * one on another host is waited for until `timeoutMs`.
109
+ *
110
+ * @template T
111
+ * @param {string} filePath — the learnings store (the lock file sits beside it)
112
+ * @param {() => (T | Promise<T>)} fn
113
+ * @param {{ timeoutMs?: number }} [opts]
114
+ * @returns {Promise<T>}
115
+ * @throws {LearningsLockError} when the lock is not acquired — `fn` never ran
116
+ */
117
+ export async function withLearningsLock(filePath, fn, { timeoutMs = LEARNINGS_LOCK_TIMEOUT_MS } = {}) {
118
+ const lockPath = await resolveStoreLockPath(filePath);
119
+ const held = heldStoreLocks.getStore();
120
+ if (held?.get(lockPath)?.live === true) return fn();
121
+
122
+ const token = { live: true };
123
+ const scope = new Map(held ?? []);
124
+ scope.set(lockPath, token);
125
+ let result;
126
+ try {
127
+ result = await withFileLock(lockPath, () => heldStoreLocks.run(scope, fn), {
128
+ timeoutMs,
129
+ // A per-acquisition holder makes the owner-guarded release delete only
130
+ // THIS acquisition's lock, never a successor's (#1285).
131
+ holder: `learnings-store:${process.pid}:${randomUUID()}`,
132
+ });
133
+ } finally {
134
+ // A continuation `fn` left running past its own return must not keep
135
+ // treating the released lock as held.
136
+ token.live = false;
137
+ }
138
+ if (!result.ok) throw new LearningsLockError(lockPath, result.reason, timeoutMs, result.error);
139
+ return result.value;
140
+ }
141
+
27
142
  // ---------------------------------------------------------------------------
28
143
  // Pre-write self-validation seam (issue #662)
29
144
  // ---------------------------------------------------------------------------
@@ -42,7 +157,7 @@ import {
42
157
  * append touches disk, so a bad write can never reach the file.
43
158
  *
44
159
  * @param {object} validated — already validated+normalized learning entry
45
- * @param {{ legacyTolerant?: boolean }} [opts] — GitLab #386. When `true`,
160
+ * @param {{ legacyTolerant?: boolean }} [opts] — EventDrop #386. When `true`,
46
161
  * a field that was ALREADY ABSENT on `validated` (e.g. a legacy record with
47
162
  * no `source_session`, tolerated by `readLearnings()`) stays tolerated after
48
163
  * the round-trip too — the re-validation call below runs in the same
@@ -80,8 +195,9 @@ function serializeLearningLineChecked(validated, { legacyTolerant = false } = {}
80
195
  // A key that existed on `validated` (present, even as `undefined`) but
81
196
  // vanished from `reparsed` was DROPPED by JSON.stringify — the exact
82
197
  // undefined/NaN/etc. corruption #662 exists to catch. A key that was
83
- // never on `validated` in the first place (the #386 legacy-field case)
84
- // cannot appear here, because we only iterate `validated`'s own keys.
198
+ // never on `validated` in the first place (the EventDrop #386
199
+ // legacy-field case) cannot appear here, because we only iterate
200
+ // `validated`'s own keys.
85
201
  const droppedKeys = Object.keys(validated).filter((k) => !(k in reparsed));
86
202
  if (droppedKeys.length > 0) {
87
203
  throw new ValidationError(
@@ -143,11 +259,18 @@ export async function readLearnings(filePath) {
143
259
  * atomic on POSIX append. For very large insight/evidence fields that
144
260
  * might exceed that boundary, use rewriteLearnings() instead.
145
261
  *
262
+ * The append runs under the store lock ({@link withLearningsLock}): an append
263
+ * that landed between a rewriter's read and its rename was silently lost.
264
+ * Validation runs first, outside the lock — a rejected record never waits.
265
+ *
146
266
  * @param {string} filePath
147
267
  * @param {object} entry
268
+ * @param {{ lockTimeoutMs?: number }} [opts] — store-lock wait, default
269
+ * {@link LEARNINGS_LOCK_TIMEOUT_MS}
148
270
  * @returns {Promise<object>} validated entry
271
+ * @throws {LearningsLockError} when the store lock is not acquired (nothing appended)
149
272
  */
150
- export async function appendLearning(filePath, entry) {
273
+ export async function appendLearning(filePath, entry, { lockTimeoutMs } = {}) {
151
274
  // Ensure created_at is set first — many writers omit it, and expires_at
152
275
  // derivation depends on it. Use ISO 8601 UTC.
153
276
  const createdAt =
@@ -173,9 +296,10 @@ export async function appendLearning(filePath, entry) {
173
296
  // parses back AND re-validates before any append touches disk. Throws
174
297
  // ValidationError on a non-round-tripping record — file is left untouched.
175
298
  const line = serializeLearningLineChecked(validated);
176
- await mkdir(path.dirname(filePath), { recursive: true });
177
- const { appendFile } = await import('node:fs/promises');
178
- await appendFile(filePath, line, 'utf8');
299
+ // The lock path resolution creates the parent directory.
300
+ await withLearningsLock(filePath, () => appendFile(filePath, line, 'utf8'), {
301
+ timeoutMs: lockTimeoutMs,
302
+ });
179
303
  return validated;
180
304
  }
181
305
 
@@ -281,7 +405,7 @@ async function rotateBackups(dir, baseName, keep = BACKUP_KEEP) {
281
405
  * - `backup` (default `true`): before the destructive rename, copy the current
282
406
  * file to `${filePath}.bak-<ISO>`, then rotate to keep only the newest
283
407
  * {@link BACKUP_KEEP}. Rotation is best-effort and never blocks the rewrite.
284
- * - `legacyTolerant` (default `true`, GitLab #386): this function is a
408
+ * - `legacyTolerant` (default `true`, EventDrop #386): this function is a
285
409
  * ROUND-TRIP writer — its usual caller (`sweepExpiredLearnings` /
286
410
  * `pruneLearnings` in `expiry-sweep.mjs`) reads the store with
287
411
  * `readLearnings()` first, and that reader already tolerates a legacy
@@ -298,7 +422,7 @@ async function rotateBackups(dir, baseName, keep = BACKUP_KEEP) {
298
422
  * round-trip (a value JSON.stringify drops or coerces, e.g. `undefined`/
299
423
  * `NaN`) is still caught by the #662 checked serializer regardless of this
300
424
  * flag — see {@link serializeLearningLineChecked}. Pass `false` to restore
301
- * the pre-#386 fully-strict behaviour.
425
+ * the fully-strict behaviour from before EventDrop #386.
302
426
  *
303
427
  * @param {string} filePath
304
428
  * @param {object[]} entries
@@ -331,24 +455,28 @@ export async function rewriteLearnings(
331
455
  if (dryRun) return validated;
332
456
 
333
457
  const body = lines.join('');
334
- await mkdir(path.dirname(filePath), { recursive: true });
335
458
 
336
- // Backup-before-rewrite (#721): snapshot the current store to a timestamped
337
- // sidecar BEFORE the destructive rename, then prune to keep-N. Only meaningful
338
- // when the target already exists (a first-time write has nothing to lose).
339
- if (backup && existsSync(filePath)) {
340
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
341
- await copyFile(filePath, `${filePath}.bak-${ts}`);
342
- try {
343
- await rotateBackups(path.dirname(filePath), path.basename(filePath));
344
- } catch {
345
- // Rotation is best-effort — a stale/undeletable sibling must never abort
346
- // the rewrite. The fresh backup above is already safely on disk.
459
+ // Under the store lock (#1447 point 8) so no append lands between the backup
460
+ // and the rename. The CALLER's read must sit inside the same lock for a
461
+ // read-modify-write to be safe — this call joins it reentrantly.
462
+ await withLearningsLock(filePath, async () => {
463
+ // Backup-before-rewrite (#721): snapshot the current store to a timestamped
464
+ // sidecar BEFORE the destructive rename, then prune to keep-N. Only meaningful
465
+ // when the target already exists (a first-time write has nothing to lose).
466
+ if (backup && existsSync(filePath)) {
467
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
468
+ await copyFile(filePath, `${filePath}.bak-${ts}`);
469
+ try {
470
+ await rotateBackups(path.dirname(filePath), path.basename(filePath));
471
+ } catch {
472
+ // Rotation is best-effort — a stale/undeletable sibling must never abort
473
+ // the rewrite. The fresh backup above is already safely on disk.
474
+ }
347
475
  }
348
- }
349
476
 
350
- const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
351
- await writeFile(tmp, body, 'utf8');
352
- await rename(tmp, filePath);
477
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
478
+ await writeFile(tmp, body, 'utf8');
479
+ await rename(tmp, filePath);
480
+ });
353
481
  return validated;
354
482
  }
@@ -230,17 +230,19 @@ export class ValidationError extends Error {
230
230
  * Throws ValidationError on contract violations. Does NOT mutate input.
231
231
  *
232
232
  * @param {object} entry — candidate learning
233
- * @param {{ legacyTolerant?: boolean }} [opts] — GitLab #386. When `true`, a
233
+ * @param {{ legacyTolerant?: boolean }} [opts] — EventDrop #386. When `true`, a
234
234
  * value `normalizeLearning()` already passes through UNCHECKED (or merely
235
235
  * DEFAULTED, never validated) on read is no longer rejected here either:
236
236
  * an out-of-enum `schema_version` (measured against the real EventDrop.at
237
237
  * store: 3 live records carry `schema_version: 2`), the
238
238
  * LEGACY_REQUIRED_FIELDS presence check, the `confidence` range/type check
239
239
  * when NO `confidence` key is present, and the `scope`/`host_class`/
240
- * `anonymized` shape checks (measured live: a record with `scope:
241
- * "src/components/landing/mobile-sticky-cta.tsx"` — a producer bug that
242
- * wrote a file path into the scope column, which `normalizeLearning` never
243
- * validates and therefore never rejects). A field that IS present keeps
240
+ * `anonymized`/`file_paths` shape checks (measured live: a record whose `scope` held a
241
+ * file path — e.g. a repo-relative component path such as
242
+ * `src/components/sample/sticky-cta.tsx` (the measured value is not
243
+ * reproduced here) — a producer bug that wrote a file path into the scope
244
+ * column, which `normalizeLearning` never validates and therefore never
245
+ * rejects). A field that IS present keeps
244
246
  * being validated regardless when its DATA TYPE can be silently corrupted
245
247
  * by a JSON round-trip (a present-but-malformed `confidence` still throws
246
248
  * — see io.mjs #662): this option relaxes "you must HAVE a valid value",
@@ -296,9 +298,11 @@ export function validateLearning(entry, { legacyTolerant = false } = {}) {
296
298
  // scope/host_class/anonymized: `normalizeLearning()` only DEFAULTS an
297
299
  // absent value (`d.scope ?? 'local'`, etc.) on read — it never validates a
298
300
  // PRESENT one, so a corrupt legacy value round-trips silently today
299
- // (measured live in production: a record with `scope:
300
- // "src/components/landing/mobile-sticky-cta.tsx"` — evidently a producer
301
- // bug that wrote a file path into the scope column). All three checks are
301
+ // (measured live in production: a record whose `scope` held a file path —
302
+ // e.g. a repo-relative component path such as
303
+ // `src/components/sample/sticky-cta.tsx` (the measured value is not
304
+ // reproduced here) — evidently a producer bug that wrote a file path into
305
+ // the scope column). All three checks are
302
306
  // therefore skipped under legacyTolerant. Unlike `confidence` (a number,
303
307
  // where `NaN`/`Infinity` are non-JSON-safe and silently become `null`
304
308
  // across a JSON round-trip — the #662 concern), a string/string-or-null/
@@ -319,6 +323,24 @@ export function validateLearning(entry, { legacyTolerant = false } = {}) {
319
323
  throw new ValidationError(`anonymized must be boolean, got: ${typeof anonymized}`);
320
324
  }
321
325
 
326
+ // file_paths (GitLab #1447): when present, an array of repo-relative path
327
+ // strings — a bare string (`file_paths: "src/x.ts"`) is the producer slip
328
+ // this rejects; every reader iterates it as a list. `null` reads as absent,
329
+ // as in migrateLegacyLearning. Skipped under legacyTolerant like the shape
330
+ // checks above: the read funnel passes the value through unchecked.
331
+ const filePaths = entry.file_paths;
332
+ if (!legacyTolerant && filePaths !== undefined && filePaths !== null) {
333
+ if (!Array.isArray(filePaths)) {
334
+ throw new ValidationError(`file_paths must be an array of repo-relative paths, got: ${typeof filePaths}`);
335
+ }
336
+ const badIndex = filePaths.findIndex((p) => !isRepoRelativePathEntry(p));
337
+ if (badIndex !== -1) {
338
+ throw new ValidationError(
339
+ `file_paths[${badIndex}] must be a non-empty repo-relative path (not absolute, no '..' segment), got: ${JSON.stringify(filePaths[badIndex])}`
340
+ );
341
+ }
342
+ }
343
+
322
344
  // Privacy contract — NEVER relaxed, legacyTolerant or not: an out-of-enum
323
345
  // `scope` (garbage or otherwise) is by definition not `'public'`, so this
324
346
  // pair only ever fires for a record that genuinely claims `scope: 'public'`
@@ -476,6 +498,85 @@ export function normalizeDialects(entry, { reserializeTimestamps = true } = {})
476
498
  return out;
477
499
  }
478
500
 
501
+ /**
502
+ * File extensions a path-like `scope` value must end in to be treated as a
503
+ * repo-relative file path (GitHub #69 / GitLab #1446). Deliberately a closed
504
+ * list: a looser "contains a slash and a dot" test would also swallow values
505
+ * such as `infrastructure/docker-compose` or `node.js`.
506
+ */
507
+ const PATH_SCOPE_EXT = /\.(mjs|cjs|js|jsx|ts|tsx|mts|cts|json|jsonl|md|mdx|ya?ml|toml|css|scss|html|vue|svelte|astro|py|rb|go|rs|swift|kt|java|sh|sql|prisma)$/i;
508
+
509
+ /**
510
+ * Strict predicate: is `s` a repo-relative file path (at least one directory
511
+ * segment, no `..`, no empty segment, a known source/doc extension)? Rejects
512
+ * canonical scopes, absolute and home-relative paths, Windows drive paths,
513
+ * URLs, and anything containing whitespace.
514
+ *
515
+ * @param {unknown} s — candidate `scope` value
516
+ * @returns {boolean}
517
+ */
518
+ function looksLikeRepoRelativePath(s) {
519
+ if (typeof s !== 'string' || VALID_SCOPES.includes(s) || s.length > 512) return false;
520
+ if (/\s/.test(s) || /^[/~]/.test(s) || /^[A-Za-z]:[\\/]/.test(s) || s.includes('://')) return false;
521
+ const segs = s.split('/');
522
+ if (segs.length < 2 || segs.includes('..') || segs.includes('')) return false;
523
+ return PATH_SCOPE_EXT.test(segs[segs.length - 1]);
524
+ }
525
+
526
+ /**
527
+ * Loose predicate for ONE `file_paths` entry (GitLab #1447): a non-empty string
528
+ * that is not absolute (`/`, `\`, `~`, a Windows drive), not a URL, and has no
529
+ * `..` segment. Deliberately looser than {@link looksLikeRepoRelativePath}:
530
+ * root-level files (`package.json`), directories (`docs/prd`) and globs
531
+ * (`skills/**`) are legitimate `file_paths` values.
532
+ *
533
+ * Exported because `scripts/memory-propose.mjs` gates `--file-paths` on the same
534
+ * rule: a proposal this predicate rejects would be queued, approved, and then
535
+ * refused by {@link validateLearning} at write time.
536
+ *
537
+ * @param {unknown} s — candidate entry
538
+ * @returns {boolean}
539
+ */
540
+ export function isRepoRelativePathEntry(s) {
541
+ if (typeof s !== 'string' || s.trim() === '') return false;
542
+ if (/^[/\\~]/.test(s) || /^[A-Za-z]:[\\/]/.test(s) || s.includes('://')) return false;
543
+ return !s.split(/[\\/]/).includes('..');
544
+ }
545
+
546
+ const LEADING_DOT_SLASH = /^(?:\.\/)+/;
547
+
548
+ /** Non-canonical STRING scopes {@link migrateLegacyLearning} coerces to `'local'`. */
549
+ const LOCAL_ALIAS_SCOPES = new Set(['vault-tools', 'deep-sessions', 'wave-executor', 'coordinator']);
550
+
551
+ /** Non-canonical STRING scopes {@link migrateLegacyLearning} coerces to `'private'`. */
552
+ const PRIVATE_ALIAS_SCOPES = new Set(['project', 'repo']);
553
+
554
+ /**
555
+ * Every word the migration reads as scope vocabulary. An array-scope entry
556
+ * equal to one of these is a scope word, never a path — the same reading the
557
+ * string branch gives it, so `['project']` is not turned into
558
+ * `file_paths: ['project']`.
559
+ */
560
+ const SCOPE_WORDS = new Set([...VALID_SCOPES, ...LOCAL_ALIAS_SCOPES, ...PRIVATE_ALIAS_SCOPES]);
561
+
562
+ /**
563
+ * The repo-relative paths a broken `scope` value carries, or `null` when it is
564
+ * not a path scope. A single string must pass the strict
565
+ * {@link looksLikeRepoRelativePath} (a lone string may be a scope word); an
566
+ * ARRAY is never scope vocabulary, so the loose per-entry predicate suffices
567
+ * there — an empty array yields `[]`. A leading `./` run is stripped from each
568
+ * path; case is kept (git paths are case-sensitive).
569
+ *
570
+ * @param {unknown} scope
571
+ * @returns {string[] | null}
572
+ */
573
+ function pathScopeEntries(scope) {
574
+ if (looksLikeRepoRelativePath(scope)) return [scope.replace(LEADING_DOT_SLASH, '')];
575
+ if (!Array.isArray(scope)) return null;
576
+ const entries = scope.map((e) => (typeof e === 'string' ? e.replace(LEADING_DOT_SLASH, '') : e));
577
+ return entries.every((e) => isRepoRelativePathEntry(e) && !SCOPE_WORDS.has(e)) ? entries : null;
578
+ }
579
+
479
580
  /**
480
581
  * Migrate a legacy learning record to the canonical schema_version:1 shape.
481
582
  * Idempotent — calling it on an already-canonical record is a safe no-op.
@@ -483,9 +584,32 @@ export function normalizeDialects(entry, { reserializeTimestamps = true } = {})
483
584
  * Alias precedence for insight: insight > description > recommendation > observation > lesson
484
585
  *
485
586
  * Producer dialects (`files`→`file_paths`, duplicate `session_id`, `last_seen`,
486
- * `next_review: null`) are normalized by {@link normalizeDialects} as the final
487
- * step. Timestamp FORMAT is deliberately NOT reformatted here (byte-exact
488
- * contract preserved); the read funnel + backfill canonicalize timestamps.
587
+ * `next_review: null`) are normalized by {@link normalizeDialects}. Timestamp
588
+ * FORMAT is deliberately NOT reformatted here (byte-exact contract preserved);
589
+ * the read funnel + backfill canonicalize timestamps.
590
+ *
591
+ * Broken-consumer-record coercions (GitHub #69 / GitLab #1446 — measured in a
592
+ * consumer store: 124 of 307 records failed strict validateLearning, 76 on
593
+ * `scope`, 48 on `schema_version`). Applied AFTER normalizeDialects, because
594
+ * coercing a path-like scope before the `files`→`file_paths` move would lose
595
+ * the legacy `files` data. Only this MIGRATION path coerces — the read funnel
596
+ * (normalizeLearning / normalizeDialects) stays pass-through by design:
597
+ * - path-like `scope` (a repo-relative file path a producer wrote into the
598
+ * scope column, see {@link looksLikeRepoRelativePath}) → appended to
599
+ * `file_paths` (copied, never mutated in place; created when absent/null;
600
+ * no duplicate) and `scope: 'private'`. A present non-array `file_paths`
601
+ * leaves the record untouched — it stays invalid and is counted. A
602
+ * leading `./` run is stripped first (GitLab #1447).
603
+ * - array-typed `scope` (GitLab #1447) → the same move when every entry is a
604
+ * repo-relative path ({@link isRepoRelativePathEntry}: root files, dirs and
605
+ * globs included); an empty array → `scope: 'private'`. An array holding an
606
+ * absolute path, a non-string or a scope word — canonical or one of the
607
+ * aliases below, e.g. `['project']` — stays untouched (invalid, counted);
608
+ * nothing is ever dropped.
609
+ * - `scope: 'project'` / `scope: 'repo'` → `'private'`.
610
+ * - `schema_version: '1'` (string) or `2` (number) → `1`.
611
+ * Each rule's output never re-matches that rule, so the whole function stays
612
+ * idempotent.
489
613
  *
490
614
  * The caller MUST still run validateLearning() on the result to confirm the
491
615
  * migrated record passes the full schema gate before writing.
@@ -534,8 +658,7 @@ export function migrateLegacyLearning(entry) {
534
658
  out.evidence = '';
535
659
  }
536
660
 
537
- const COERCIBLE_SCOPES = new Set(['vault-tools', 'deep-sessions', 'wave-executor', 'coordinator']);
538
- if (out.scope && !VALID_SCOPES.includes(out.scope) && COERCIBLE_SCOPES.has(out.scope)) {
661
+ if (out.scope && !VALID_SCOPES.includes(out.scope) && LOCAL_ALIAS_SCOPES.has(out.scope)) {
539
662
  out.scope = 'local';
540
663
  }
541
664
 
@@ -560,12 +683,37 @@ export function migrateLegacyLearning(entry) {
560
683
  out.schema_version = CURRENT_SCHEMA_VERSION;
561
684
  }
562
685
 
563
- // Normalize producer dialects last (files→file_paths, session_id/last_seen/
686
+ // Normalize producer dialects (files→file_paths, session_id/last_seen/
564
687
  // next_review reconciliation) — schema_version is preserved by normalizeDialects.
565
688
  // reserializeTimestamps:false keeps migrateLegacyLearning's byte-exact timestamp
566
689
  // contract (it does schema/alias migration, not timestamp reformatting). The
567
690
  // READ funnel (normalizeLearning) and the backfill canonicalize timestamp format.
568
- return normalizeDialects(out, { reserializeTimestamps: false });
691
+ const migrated = normalizeDialects(out, { reserializeTimestamps: false });
692
+
693
+ // Broken-consumer-record coercions (GitHub #69 / GitLab #1446, #1447) — AFTER
694
+ // normalizeDialects so a legacy `files` list has already become `file_paths`.
695
+ const pathEntries = pathScopeEntries(migrated.scope);
696
+ if (pathEntries !== null) {
697
+ const existing = migrated.file_paths;
698
+ if (existing === undefined || existing === null || Array.isArray(existing)) {
699
+ // Copy: normalizeDialects' shallow copy still shares the caller's array.
700
+ const filePaths = Array.isArray(existing) ? [...existing] : [];
701
+ for (const p of pathEntries) {
702
+ if (!filePaths.includes(p)) filePaths.push(p);
703
+ }
704
+ if (filePaths.length > 0) migrated.file_paths = filePaths;
705
+ migrated.scope = 'private';
706
+ }
707
+ // Present but not an array → leave untouched; the record stays invalid.
708
+ } else if (PRIVATE_ALIAS_SCOPES.has(migrated.scope)) {
709
+ migrated.scope = 'private';
710
+ }
711
+
712
+ if (migrated.schema_version === '1' || migrated.schema_version === 2) {
713
+ migrated.schema_version = CURRENT_SCHEMA_VERSION;
714
+ }
715
+
716
+ return migrated;
569
717
  }
570
718
 
571
719
  // Module-level dedupe sets for warnings (per-process).
@@ -9,8 +9,8 @@
9
9
  * 4-5. Before this module, the `effective-sizing` analyzer
10
10
  * (`skills/evolve/references/evolve-analyze-mode.md`) keyed its subject on
11
11
  * `session_type` alone (e.g. `deep-session-sizing`), so a 7-wave ultradeep
12
- * session and a 5-wave deep session landed on the SAME row — the over-delivery
13
- * median for one profile silently absorbed the other's.
12
+ * session and a 5-wave deep session landed on the SAME row — the agents-per-wave
13
+ * average for one profile silently absorbed the other's.
14
14
  *
15
15
  * Pure, stdlib-only, no imports, no clock, no fs.
16
16
  *