session-orchestrator 5.1.0 → 5.3.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 (484) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +6 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +7 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +8 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +21 -0
  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 +7 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +7 -2
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +7 -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 +7 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +7 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +8 -3
  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 +22 -0
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +7 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +20 -0
  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 +6 -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/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +6 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +8 -2
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +21 -0
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +6 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +22 -0
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +6 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +6 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +7 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +21 -0
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +7 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  83. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  84. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  85. package/.codex-plugin/skills/close/SKILL.md +3 -3
  86. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
  87. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  88. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  89. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  90. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  91. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  92. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  93. package/.codex-plugin/skills/go/SKILL.md +3 -3
  94. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  95. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  96. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  97. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
  98. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  99. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  100. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  101. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  102. package/.codex-plugin/skills/release/SKILL.md +3 -3
  103. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  104. package/.codex-plugin/skills/session/SKILL.md +1 -1
  105. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  106. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  107. package/.codex-plugin/skills/test/SKILL.md +3 -3
  108. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  109. package/.cursor/commands/autopilot.md +4 -4
  110. package/.cursor/commands/bootstrap.md +5 -4
  111. package/.cursor/commands/brainstorm.md +5 -4
  112. package/.cursor/commands/close.md +4 -3
  113. package/.cursor/commands/debug.md +4 -4
  114. package/.cursor/commands/discovery.md +4 -4
  115. package/.cursor/commands/dispatcher.md +4 -4
  116. package/.cursor/commands/eli5.md +4 -4
  117. package/.cursor/commands/eval.md +4 -4
  118. package/.cursor/commands/evolve.md +4 -4
  119. package/.cursor/commands/go.md +4 -3
  120. package/.cursor/commands/grill.md +4 -4
  121. package/.cursor/commands/harness-audit.md +3 -3
  122. package/.cursor/commands/memory-cleanup.md +4 -4
  123. package/.cursor/commands/persona-panel.md +4 -4
  124. package/.cursor/commands/plan.md +5 -4
  125. package/.cursor/commands/portfolio.md +3 -3
  126. package/.cursor/commands/reconcile.md +4 -4
  127. package/.cursor/commands/release.md +4 -3
  128. package/.cursor/commands/repo-audit.md +4 -4
  129. package/.cursor/commands/session.md +1 -1
  130. package/.cursor/commands/spinout.md +4 -4
  131. package/.cursor/commands/sunset-review.md +4 -4
  132. package/.cursor/commands/test.md +3 -3
  133. package/.cursor/commands/ux-grill.md +4 -4
  134. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  135. package/.cursor/rules/010-session-workflow.mdc +2 -2
  136. package/.cursor/rules/050-plan.mdc +1 -1
  137. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  138. package/.cursor/skills/close/SKILL.md +13 -0
  139. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  140. package/.cursor/skills/debug/SKILL.md +0 -1
  141. package/.cursor/skills/discovery/SKILL.md +0 -1
  142. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  143. package/.cursor/skills/eli5/SKILL.md +0 -1
  144. package/.cursor/skills/eval/SKILL.md +1 -1
  145. package/.cursor/skills/evolve/SKILL.md +0 -1
  146. package/.cursor/skills/go/SKILL.md +13 -0
  147. package/.cursor/skills/grill/SKILL.md +0 -1
  148. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  149. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  150. package/.cursor/skills/portfolio/SKILL.md +12 -0
  151. package/.cursor/skills/release/SKILL.md +13 -0
  152. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  153. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  154. package/.cursor/skills/test/SKILL.md +12 -0
  155. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  156. package/.cursor-plugin/plugin.json +1 -1
  157. package/.orchestrator/policy/blocked-commands.json +13 -4
  158. package/AGENTS.md +3 -2
  159. package/CHANGELOG.md +197 -0
  160. package/README.md +11 -9
  161. package/SECURITY.md +12 -0
  162. package/agents/dialectic-deriver.md +13 -10
  163. package/agents/eval-judge.md +67 -45
  164. package/agents/skill-applied-judge.md +34 -19
  165. package/commands/session.md +17 -3
  166. package/docs/baseline.md +12 -6
  167. package/docs/ci-setup.md +53 -0
  168. package/docs/codex-setup.md +15 -3
  169. package/docs/components.md +13 -6
  170. package/docs/events-schema.md +59 -9
  171. package/docs/install.md +16 -0
  172. package/docs/persona-panel.md +1 -1
  173. package/docs/pi-setup.md +1 -1
  174. package/docs/rule-authoring.md +135 -14
  175. package/docs/scope-collision-guard.md +2 -0
  176. package/docs/session-config-reference.md +106 -11
  177. package/docs/session-config-template.md +31 -2
  178. package/docs/telemetry.md +2 -0
  179. package/hooks/_lib/hook-import-set.json +125 -8
  180. package/hooks/_lib/subagent-paths.mjs +15 -0
  181. package/hooks/_lib/subagent-transcript.mjs +582 -31
  182. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  183. package/hooks/config-protection.mjs +11 -3
  184. package/hooks/cwd-change-restore.mjs +11 -3
  185. package/hooks/enforce-commands.mjs +70 -23
  186. package/hooks/enforce-scope.mjs +143 -33
  187. package/hooks/hooks-codex.json +1 -1
  188. package/hooks/hooks.json +1 -1
  189. package/hooks/loop-guard.mjs +11 -3
  190. package/hooks/on-session-end.mjs +72 -25
  191. package/hooks/on-session-start.mjs +48 -11
  192. package/hooks/on-stop.mjs +211 -23
  193. package/hooks/operator-steer.mjs +11 -3
  194. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  195. package/hooks/post-bash-write-verify.mjs +6 -2
  196. package/hooks/post-edit-import-probe.mjs +17 -9
  197. package/hooks/post-edit-validate.mjs +13 -5
  198. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  199. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  200. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  201. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  202. package/hooks/pre-auq-clarity.mjs +18 -2
  203. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  204. package/hooks/pre-bash-issue-budget.mjs +119 -28
  205. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  206. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  207. package/hooks/pre-bash-staging-fence.mjs +335 -31
  208. package/hooks/pre-bash-templates-first.mjs +19 -14
  209. package/hooks/pre-task-scope-disjoint.mjs +385 -5
  210. package/hooks/skill-invocation-telemetry.mjs +2 -1
  211. package/hooks/subagent-telemetry.mjs +15 -19
  212. package/hooks/wave-scope-commit-guard.mjs +197 -100
  213. package/monitors/monitors.json +1 -1
  214. package/output-styles/wave-summary.md +1 -1
  215. package/package.json +2 -1
  216. package/pi/prompts/autopilot.md +3 -3
  217. package/pi/prompts/bootstrap.md +3 -3
  218. package/pi/prompts/brainstorm.md +3 -3
  219. package/pi/prompts/close.md +2 -2
  220. package/pi/prompts/debug.md +3 -3
  221. package/pi/prompts/discovery.md +3 -3
  222. package/pi/prompts/dispatcher.md +3 -3
  223. package/pi/prompts/eli5.md +3 -3
  224. package/pi/prompts/eval.md +3 -3
  225. package/pi/prompts/evolve.md +3 -3
  226. package/pi/prompts/go.md +2 -2
  227. package/pi/prompts/grill.md +3 -3
  228. package/pi/prompts/harness-audit.md +2 -3
  229. package/pi/prompts/memory-cleanup.md +3 -3
  230. package/pi/prompts/persona-panel.md +3 -3
  231. package/pi/prompts/plan.md +3 -3
  232. package/pi/prompts/portfolio.md +2 -2
  233. package/pi/prompts/reconcile.md +3 -3
  234. package/pi/prompts/release.md +3 -3
  235. package/pi/prompts/repo-audit.md +3 -4
  236. package/pi/prompts/session.md +2 -2
  237. package/pi/prompts/spinout.md +3 -3
  238. package/pi/prompts/sunset-review.md +3 -3
  239. package/pi/prompts/templates-ack.md +1 -1
  240. package/pi/prompts/test.md +3 -3
  241. package/pi/prompts/ux-grill.md +3 -3
  242. package/rules/README.md +1 -1
  243. package/rules/opt-in-domain/prompt-caching.md +1 -1
  244. package/rules/opt-in-stack/backend-data.md +1 -1
  245. package/rules/opt-in-stack/backend.md +3 -3
  246. package/rules/opt-in-stack/frontend.md +1 -1
  247. package/rules/opt-in-stack/security-web.md +3 -3
  248. package/rules/opt-in-stack/swift.md +1 -1
  249. package/scripts/archive-closed-prds.mjs +2 -2
  250. package/scripts/auq-audit.mjs +2 -3
  251. package/scripts/autopilot.mjs +23 -2
  252. package/scripts/backfill-abandoned-sessions.mjs +171 -15
  253. package/scripts/backfill-evidence-digest.mjs +2 -1
  254. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  255. package/scripts/check-package-manager.mjs +2 -2
  256. package/scripts/check-sessions-integrity.mjs +300 -0
  257. package/scripts/ci/assert-vitest-green.mjs +2 -1
  258. package/scripts/dialectic-deriver.mjs +50 -13
  259. package/scripts/emit-session.mjs +77 -32
  260. package/scripts/eval-session.mjs +65 -3
  261. package/scripts/export-hw-learnings.mjs +2 -1
  262. package/scripts/express-path.mjs +1 -1
  263. package/scripts/gc-stale-worktrees.mjs +2 -1
  264. package/scripts/generate-agents-skills.mjs +102 -29
  265. package/scripts/generate-codex-skills.mjs +48 -4
  266. package/scripts/generate-cursor-adapter.mjs +220 -11
  267. package/scripts/generate-hook-import-set.mjs +12 -27
  268. package/scripts/generate-pi-prompts.mjs +183 -13
  269. package/scripts/github-protection-audit.mjs +2 -3
  270. package/scripts/lib/agent-frontmatter.mjs +23 -1
  271. package/scripts/lib/agent-status.mjs +2 -31
  272. package/scripts/lib/auq/clarity.mjs +10 -2
  273. package/scripts/lib/auq/parse.mjs +12 -31
  274. package/scripts/lib/auq/schema.mjs +56 -41
  275. package/scripts/lib/auto-dialectic.mjs +304 -15
  276. package/scripts/lib/autopilot/flags.mjs +12 -1
  277. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  278. package/scripts/lib/autopilot/loop.mjs +14 -1
  279. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  280. package/scripts/lib/ci-status-banner.mjs +376 -16
  281. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  282. package/scripts/lib/command-blocker.mjs +408 -33
  283. package/scripts/lib/config/dialectic.mjs +12 -3
  284. package/scripts/lib/config/drift-check.mjs +19 -0
  285. package/scripts/lib/config/gate.mjs +74 -0
  286. package/scripts/lib/config/reaper.mjs +162 -0
  287. package/scripts/lib/config.mjs +14 -0
  288. package/scripts/lib/convergence-monitor.mjs +76 -13
  289. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  290. package/scripts/lib/description-surface.mjs +2 -5
  291. package/scripts/lib/dispatcher/cli.mjs +2 -1
  292. package/scripts/lib/ecosystem-health.mjs +11 -0
  293. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  294. package/scripts/lib/eval/engine.mjs +421 -53
  295. package/scripts/lib/eval/judge.mjs +463 -40
  296. package/scripts/lib/eval/schema.mjs +10 -1
  297. package/scripts/lib/events-rotation.mjs +221 -25
  298. package/scripts/lib/events-schema.mjs +114 -0
  299. package/scripts/lib/events.mjs +524 -5
  300. package/scripts/lib/fetch-baseline.mjs +3 -8
  301. package/scripts/lib/frontmatter-guard.mjs +21 -10
  302. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  303. package/scripts/lib/gates/gate-full.mjs +28 -3
  304. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  305. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  306. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  307. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  308. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  309. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  310. package/scripts/lib/instruction-budget-guard.mjs +332 -50
  311. package/scripts/lib/io.mjs +42 -8
  312. package/scripts/lib/is-main-module.mjs +82 -0
  313. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  314. package/scripts/lib/js-mask.mjs +197 -0
  315. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  316. package/scripts/lib/locks/index.mjs +32 -25
  317. package/scripts/lib/maintenance-due-banner.mjs +122 -91
  318. package/scripts/lib/orphan-reaper.mjs +1588 -0
  319. package/scripts/lib/peer-cards/merger.mjs +48 -10
  320. package/scripts/lib/peer-cards/reader.mjs +78 -2
  321. package/scripts/lib/peer-discovery.mjs +2 -5
  322. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  323. package/scripts/lib/process-group.mjs +899 -0
  324. package/scripts/lib/quality-gate.mjs +107 -28
  325. package/scripts/lib/reconcile/backlog.mjs +368 -0
  326. package/scripts/lib/reconcile/engine.mjs +55 -188
  327. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
  328. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  329. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  330. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  331. package/scripts/lib/rule-loader.mjs +41 -12
  332. package/scripts/lib/rules-sync.mjs +2 -5
  333. package/scripts/lib/scope-echo.mjs +429 -7
  334. package/scripts/lib/scope-gate.mjs +605 -1
  335. package/scripts/lib/session-close-backfill.mjs +91 -12
  336. package/scripts/lib/session-id.mjs +9 -20
  337. package/scripts/lib/session-invocation.mjs +20 -0
  338. package/scripts/lib/session-schema/constants.mjs +30 -2
  339. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  340. package/scripts/lib/session-schema.mjs +8 -3
  341. package/scripts/lib/session-start-probes.mjs +95 -10
  342. package/scripts/lib/sessions-canonical.mjs +23 -0
  343. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  344. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  345. package/scripts/lib/skill-evidence-window.mjs +891 -0
  346. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  347. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  348. package/scripts/lib/skill-judge.mjs +45 -3
  349. package/scripts/lib/state-md.mjs +84 -3
  350. package/scripts/lib/sunset/walker.mjs +31 -4
  351. package/scripts/lib/tail-window.mjs +56 -0
  352. package/scripts/lib/telemetry/schema.mjs +30 -0
  353. package/scripts/lib/telemetry/sync.mjs +61 -6
  354. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  355. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  356. package/scripts/lib/tests-src-ratio.mjs +2 -6
  357. package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
  358. package/scripts/lib/user-invocable-skills.mjs +205 -0
  359. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  360. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  361. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  362. package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
  363. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  364. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  365. package/scripts/lib/validate/check-entry-guard.mjs +329 -0
  366. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  367. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  368. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  369. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  370. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  371. package/scripts/lib/validate/check-rules.mjs +7 -5
  372. package/scripts/lib/validate/check-skill-links.mjs +35 -6
  373. package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
  374. package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
  375. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  376. package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
  377. package/scripts/lib/validate/check-unwired-features.mjs +220 -33
  378. package/scripts/lib/validate/check-validator-registration.mjs +36 -12
  379. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  380. package/scripts/lib/validate/confidential-names.mjs +10 -0
  381. package/scripts/lib/validate-vendored-rules.mjs +39 -12
  382. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  383. package/scripts/lib/vault-mirror/process.mjs +10 -3
  384. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  385. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  386. package/scripts/lib/vault-yaml.mjs +118 -0
  387. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  388. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  389. package/scripts/lock-reaper.mjs +2 -1
  390. package/scripts/materialize-wave-scope.mjs +87 -4
  391. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  392. package/scripts/migrate-vault-paths.mjs +2 -3
  393. package/scripts/release-session-lock.mjs +305 -0
  394. package/scripts/release.mjs +109 -39
  395. package/scripts/relocate-vault-corpus.mjs +2 -3
  396. package/scripts/repair-invalid-sessions.mjs +2 -2
  397. package/scripts/resolve-session-invocation.mjs +59 -0
  398. package/scripts/run-quality-gate.mjs +156 -17
  399. package/scripts/session-shape.mjs +2 -2
  400. package/scripts/site-numbers.mjs +35 -11
  401. package/scripts/sweep-expired-rules.mjs +227 -0
  402. package/scripts/validate-plugin.mjs +21 -0
  403. package/scripts/validate-wave-scope.mjs +32 -105
  404. package/scripts/vault-consolidate.mjs +2 -2
  405. package/scripts/vault-mirror.mjs +11 -4
  406. package/scripts/wave-scope-binding.mjs +2 -3
  407. package/skills/_shared/bootstrap-gate.md +1 -1
  408. package/skills/_shared/monitor-patterns.md +1 -1
  409. package/skills/_shared/platform-tools.md +23 -11
  410. package/skills/_shared/research-evidence.md +53 -0
  411. package/skills/_shared/state-ownership.md +3 -0
  412. package/skills/autopilot/SKILL.md +80 -11
  413. package/skills/bootstrap/SKILL.md +51 -1
  414. package/skills/brainstorm/SKILL.md +16 -0
  415. package/skills/claude-md-drift-check/SKILL.md +1 -1
  416. package/skills/claude-md-drift-check/checker.mjs +49 -11
  417. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  418. package/skills/convergence-monitoring/README.md +8 -1
  419. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  420. package/skills/convergence-monitoring/SKILL.md +15 -6
  421. package/skills/debug/SKILL.md +10 -0
  422. package/skills/discovery/SKILL.md +24 -1
  423. package/skills/discovery/probes-session.md +2 -2
  424. package/skills/dispatcher/SKILL.md +38 -7
  425. package/skills/eli5/SKILL.md +11 -0
  426. package/skills/eval/SKILL.md +52 -23
  427. package/skills/eval/rubric-v1.md +1 -0
  428. package/skills/eval/rubric-v2.md +457 -0
  429. package/skills/evolve/SKILL.md +9 -2
  430. package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
  431. package/skills/gitlab-ops/SKILL.md +3 -2
  432. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  433. package/skills/grill/SKILL.md +19 -0
  434. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  435. package/skills/hook-development/SKILL.md +46 -41
  436. package/skills/memory-cleanup/SKILL.md +7 -0
  437. package/skills/npm-publish/SKILL.md +2 -2
  438. package/skills/persona-panel/SKILL.md +56 -1
  439. package/skills/persona-panel/persona-format.md +1 -1
  440. package/skills/plan/SKILL.md +28 -1
  441. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  442. package/skills/reconcile/SKILL.md +21 -0
  443. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  444. package/skills/repo-audit/SKILL.md +7 -0
  445. package/skills/session-end/SKILL.md +13 -16
  446. package/skills/session-end/discovery-scan.md +1 -1
  447. package/skills/session-end/phase-3-6-tail.md +55 -9
  448. package/skills/session-end/plan-verification.md +2 -2
  449. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  450. package/skills/session-end/session-metrics-write.md +10 -0
  451. package/skills/session-plan/SKILL.md +18 -6
  452. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  453. package/skills/session-start/SKILL.md +5 -4
  454. package/skills/session-start/phase-8-5-express-path.md +6 -6
  455. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  456. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  457. package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
  458. package/skills/spinout/SKILL.md +12 -1
  459. package/skills/sunset-review/SKILL.md +13 -0
  460. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  461. package/skills/ux-grill/SKILL.md +20 -2
  462. package/skills/wave-executor/SKILL.md +14 -7
  463. package/skills/wave-executor/circuit-breaker.md +2 -0
  464. package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
  465. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  466. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  467. package/commands/autopilot.md +0 -80
  468. package/commands/bootstrap.md +0 -56
  469. package/commands/brainstorm.md +0 -48
  470. package/commands/debug.md +0 -36
  471. package/commands/discovery.md +0 -32
  472. package/commands/dispatcher.md +0 -59
  473. package/commands/eli5.md +0 -33
  474. package/commands/eval.md +0 -28
  475. package/commands/evolve.md +0 -10
  476. package/commands/grill.md +0 -45
  477. package/commands/memory-cleanup.md +0 -26
  478. package/commands/persona-panel.md +0 -121
  479. package/commands/plan.md +0 -15
  480. package/commands/reconcile.md +0 -23
  481. package/commands/repo-audit.md +0 -24
  482. package/commands/spinout.md +0 -15
  483. package/commands/sunset-review.md +0 -27
  484. package/commands/ux-grill.md +0 -51
@@ -93,6 +93,7 @@ export const USAGE_PING_OPTIONAL_FIELDS = Object.freeze([
93
93
  'fleet_self_declared',
94
94
  'session_record',
95
95
  'session_profile',
96
+ 'ledger_complete',
96
97
  ]);
97
98
 
98
99
  /**
@@ -117,6 +118,29 @@ const USAGE_PING_PROJECTED_FIELDS = Object.freeze([...USAGE_PING_FIELDS, ...USAG
117
118
  */
118
119
  export const SESSION_RECORD_SOURCES = Object.freeze(['ledger', 'derived', 'absent']);
119
120
 
121
+ /*
122
+ * `ledger_complete` — a SECOND, ORTHOGONAL axis beside `session_record`
123
+ * (GitLab #1416), deliberately NOT a fourth provenance token.
124
+ *
125
+ * `deriveSessionFromEvents` (telemetry/sync.mjs) returns a three-state verdict on
126
+ * the events ledger it read FROM: `true` (every rotation archive present),
127
+ * `false` (at least one measured gap — a deleted/missing archive), and `null`
128
+ * (no verdict was produced at all: the read threw, or there was no source). That
129
+ * verdict decided nothing on the wire, so a derived record reconstructed from a
130
+ * holey ledger was indistinguishable from one reconstructed from a whole one.
131
+ *
132
+ * Why a boolean and not a `derived-partial` token: `SESSION_RECORD_SOURCES` is a
133
+ * frozen enum mirrored in prose and in the server's `raw_json` consumers, and
134
+ * completeness is a property of the SOURCE READ, not of WHICH source was used —
135
+ * widening the provenance enum would make the two facts inseparable and force
136
+ * every reader of `session_record === 'derived'` to learn a second spelling.
137
+ *
138
+ * THE THIRD STATE IS OMISSION: only a real boolean is written. `null` (nothing
139
+ * measured) omits the key, exactly like `session_profile` — "we did not look" is
140
+ * never sent as a measured `false`. On the `ledger` branch the field is not set
141
+ * at ALL: that path never reads the events ledger, so it has no verdict to give.
142
+ */
143
+
120
144
  /** Exact duration-bucket tokens (ASCII, stable wire values). */
121
145
  export const DURATION_BUCKETS = Object.freeze(['<15m', '15-60m', '1-3h', '>3h']);
122
146
 
@@ -541,6 +565,7 @@ export function buildUsagePing({
541
565
  consentState,
542
566
  sessionRecordSource,
543
567
  sessionProfile,
568
+ ledgerComplete,
544
569
  } = {}) {
545
570
  const session = isPlainObject(sessionRecord) ? sessionRecord : {};
546
571
  const invocations = Array.isArray(skillInvocations) ? skillInvocations : [];
@@ -637,6 +662,11 @@ export function buildUsagePing({
637
662
  session_record: SESSION_RECORD_SOURCES.includes(sessionRecordSource)
638
663
  ? sessionRecordSource
639
664
  : 'absent',
665
+ // ONLY a real boolean travels (#1416). `null`/`undefined` — the read never
666
+ // happened, or this path never read the events ledger at all — OMITS the
667
+ // key, the same absent-is-not-empty contract `session_profile` uses above.
668
+ // Writing `false` there would turn "we did not look" into a measured gap.
669
+ ...(typeof ledgerComplete === 'boolean' ? { ledger_complete: ledgerComplete } : {}),
640
670
  skills: filterRosterNames(skillNames, rosterSkills),
641
671
  commands: filterRosterNames(commandNames, rosterCommands),
642
672
  };
@@ -41,6 +41,7 @@ import { ensureAnonId } from './anon-id.mjs';
41
41
  import { peekAll, enqueue, clear, queueStats } from './queue.mjs';
42
42
  import { loadOwnerConfig } from '../owner-yaml.mjs';
43
43
  import { readJsonlFile } from '../io.mjs';
44
+ import { readEventsWithRotations } from '../events.mjs';
44
45
  import { readCanonicalSessions } from '../sessions-canonical.mjs';
45
46
  import { resolvePrivateConfigDir } from '../config/private-config-dir.mjs';
46
47
  import { readSessionProfile } from '../state-md.mjs';
@@ -338,12 +339,54 @@ export function readSessionProfileForMetricsDir(metricsDir) {
338
339
  * Never throws.
339
340
  *
340
341
  * @param {string} metricsDir
341
- * @returns {{ session: object, source: 'derived'|'absent' }}
342
+ * @returns {{ session: object, source: 'derived'|'absent',
343
+ * ledger_complete: boolean|null, ledger_gaps: object[] }}
344
+ * `ledger_complete` is `null` — never `false` — when the read produced no
345
+ * verdict at all: an undetermined ledger is not a proven gap. TWO paths reach
346
+ * that third state (#1423): this function throwing, and the reader finding NO
347
+ * source (no active file, no archive, no legacy ring) — in the latter case
348
+ * `events: []` is the absence of a ledger, not a measured quiet window.
349
+ * `ledger_complete === false` additionally writes ONE stderr line naming the
350
+ * gap kinds, because a verdict nothing surfaces is a verdict nobody acts on.
342
351
  */
343
352
  export function deriveSessionFromEvents(metricsDir) {
344
353
  try {
345
- const events = readJsonlFile(path.join(metricsDir, 'events.jsonl'), { skipInvalid: true });
346
- if (!Array.isArray(events) || events.length === 0) return { session: {}, source: 'absent' };
354
+ // #1407: read ACROSS rotation boundaries. This reconstruction is the ONLY
355
+ // source of session facts whenever `sessions.jsonl` has no usable record,
356
+ // and its sibling window is `DAILY_FLUSH_MS` (24 h) — so a rotation between
357
+ // a session's start and its flush silently truncated it.
358
+ //
359
+ // CEILING (BV-004): the reader loads the active file AND every archive
360
+ // fully into memory — up to ~60 MB transient at the default
361
+ // `max-size-mb: 10` / `max-backups: 5`. Measured 2026-09-20 A/B in ONE
362
+ // process on exactly that worst case (60 MB / 6 sources / 293,994 records,
363
+ // host loadavg 11.2): median 25.5 ms → 215.8 ms, i.e. +190 ms, which is
364
+ // 6.4 % of this path's `POST_TIMEOUT_MS` (3000). Acceptable for a
365
+ // once-per-flush cold read. Revisit if `max-size-mb` is raised past ~100,
366
+ // if `max-backups` grows, or if the flush budget drops below ~1 s.
367
+ const ledger = readEventsWithRotations(undefined, {
368
+ filePath: path.join(metricsDir, 'events.jsonl'),
369
+ });
370
+ const events = ledger.events;
371
+ // A missing archive is a GAP, never an empty window (#1407 AC-3). Carried
372
+ // on EVERY return below, including the empty ones — that is precisely where
373
+ // a deleted archive and a quiet period are otherwise indistinguishable.
374
+ const ledgerVerdict = { ledger_complete: ledger.complete, ledger_gaps: ledger.gaps };
375
+ // Visibility (#1423): the field had ZERO production consumers — a measured
376
+ // gap reached nobody. ONE stderr line, at the single point the verdict is
377
+ // formed, so every return below is covered by construction. Diagnostics on
378
+ // stderr, never stdout (cli-design.md); no new event, because the gap is a
379
+ // property of the LEDGER and an event about it would land in the same file.
380
+ if (ledger.complete === false) {
381
+ const kinds = [...new Set((ledger.gaps ?? []).map((g) => g?.kind ?? 'unknown'))].sort();
382
+ process.stderr.write(
383
+ `⚠ telemetry: events ledger INCOMPLETE — ${ledger.gaps.length} gap(s) ` +
384
+ `[${kinds.join(', ')}]; the session facts below are reconstructed from a PARTIAL read\n`,
385
+ );
386
+ }
387
+ if (!Array.isArray(events) || events.length === 0) {
388
+ return { session: {}, source: 'absent', ...ledgerVerdict };
389
+ }
347
390
 
348
391
  let startedAt = null;
349
392
  let sessionType = null;
@@ -412,7 +455,9 @@ export function deriveSessionFromEvents(metricsDir) {
412
455
  profile = undatedShapeProfile;
413
456
  }
414
457
 
415
- if (startedAt === null && sessionType === null && profile === null) return { session: {}, source: 'absent' };
458
+ if (startedAt === null && sessionType === null && profile === null) {
459
+ return { session: {}, source: 'absent', ...ledgerVerdict };
460
+ }
416
461
 
417
462
  const session = {};
418
463
  if (sessionType !== null) session.session_type = sessionType;
@@ -422,9 +467,11 @@ export function deriveSessionFromEvents(metricsDir) {
422
467
  // omit-never-fabricate contract session-close-backfill.mjs uses (#914 R1).
423
468
  if (lastTs !== null && startedAt !== null && lastTs >= startedAt) session.completed_at = lastTs;
424
469
 
425
- return { session, source: 'derived' };
470
+ return { session, source: 'derived', ...ledgerVerdict };
426
471
  } catch {
427
- return { session: {}, source: 'absent' };
472
+ // No verdict was ever produced — `null`, not `false`: a measurement that
473
+ // did not happen is not a measured zero.
474
+ return { session: {}, source: 'absent', ledger_complete: null, ledger_gaps: [] };
428
475
  }
429
476
  }
430
477
 
@@ -483,6 +530,12 @@ export function buildBatch({
483
530
  let sessionForPing;
484
531
  /** @type {'ledger'|'derived'|'absent'} */
485
532
  let sessionRecordSource = 'ledger';
533
+ // #1416: the events-ledger completeness verdict, carried to the ping as its
534
+ // own optional boolean. Stays `null` on the LEDGER branch by construction —
535
+ // that branch never reads events.jsonl, so it has nothing to report, and
536
+ // `buildUsagePing` omits the key for anything that is not a real boolean.
537
+ /** @type {boolean|null} */
538
+ let ledgerComplete = null;
486
539
  if (sessionRecord && typeof sessionRecord.started_at === 'string' && !Number.isNaN(Date.parse(sessionRecord.started_at))) {
487
540
  const startMs = Date.parse(sessionRecord.started_at);
488
541
  windowInvocations = invocations.filter((rec) => {
@@ -500,6 +553,7 @@ export function buildBatch({
500
553
  const derived = deriveSessionFromEvents(dir);
501
554
  sessionForPing = derived.session;
502
555
  sessionRecordSource = derived.source;
556
+ ledgerComplete = derived.ledger_complete;
503
557
 
504
558
  const derivedStartMs = Date.parse(sessionForPing.started_at);
505
559
  const cutoff = (Number.isNaN(Date.parse(nowIso)) ? Date.now() : Date.parse(nowIso)) - DAILY_FLUSH_MS;
@@ -522,6 +576,7 @@ export function buildBatch({
522
576
  consentState,
523
577
  sessionRecordSource,
524
578
  sessionProfile: readSessionProfileForMetricsDir(dir),
579
+ ledgerComplete,
525
580
  });
526
581
 
527
582
  const target = statePath || TELEMETRY_JSON_PATH;
@@ -31,9 +31,11 @@
31
31
  * @module scripts/lib/telemetry-flush-health-banner
32
32
  */
33
33
 
34
- import { existsSync, openSync, readSync, fstatSync, closeSync } from 'node:fs';
34
+ import { existsSync } from 'node:fs';
35
35
  import path from 'node:path';
36
36
 
37
+ import { readTailWindow } from './tail-window.mjs';
38
+
37
39
  /**
38
40
  * Bytes of `events.jsonl` read from the END of the file.
39
41
  *
@@ -82,31 +84,11 @@ function sanitizeReason(reason) {
82
84
  * @returns {{text: string} | {missing: true} | {error: string}}
83
85
  */
84
86
  function readTail(file) {
85
- let fd;
86
87
  try {
87
- fd = openSync(file, 'r');
88
- const { size } = fstatSync(fd);
89
- const length = Math.min(size, TAIL_BYTES);
90
- const start = size - length;
91
- const buf = Buffer.allocUnsafe(length);
92
- let read = 0;
93
- while (read < length) {
94
- const n = readSync(fd, buf, read, length - read, start + read);
95
- if (n <= 0) break;
96
- read += n;
97
- }
98
- return { text: buf.subarray(0, read).toString('utf8') };
88
+ return { text: readTailWindow(file, TAIL_BYTES).text };
99
89
  } catch (err) {
100
90
  if (err?.code === 'ENOENT') return { missing: true };
101
91
  return { error: typeof err?.code === 'string' ? err.code : 'EUNKNOWN' };
102
- } finally {
103
- if (fd !== undefined) {
104
- try {
105
- closeSync(fd);
106
- } catch {
107
- /* best effort */
108
- }
109
- }
110
92
  }
111
93
  }
112
94
 
@@ -22,10 +22,17 @@
22
22
  * triageDecision(finding, candidates)
23
23
  * → {action: 'create'|'update'|'ignore', target?, reason: string, confidence: number}
24
24
  *
25
+ * buildIssueBody(finding, fp) → string
26
+ *
27
+ * neutralizeFingerprintSentinel(text) → string
28
+ * fingerprintSentinelLine(fp) → string
29
+ * The one shared sentinel writer + neutraliser, also used by
30
+ * ux-grill/reconcile.mjs (#1339 P6).
31
+ *
25
32
  * ReconcileError
26
33
  *
27
34
  * Security:
28
- * - #388 (SEC-IR-MED-1): sentinel injection hardening — sanitizeRecommendation()
35
+ * - #388 (SEC-IR-MED-1): sentinel injection hardening — neutralizeFingerprintSentinel()
29
36
  * strips **Fingerprint:** literals from free-text fields before they are
30
37
  * embedded in the body (before the authoritative sentinel line is appended).
31
38
  * - #389 (SEC-IR-LOW-1): maxBuffer set to 4 MB on every execFile call;
@@ -126,39 +133,64 @@ function validateFinding(finding) {
126
133
  // ---------------------------------------------------------------------------
127
134
 
128
135
  /**
129
- * Sanitize free-text recommendation fields before embedding in the issue body.
130
- * Replaces any literal `**Fingerprint:**` with `__Fingerprint__` to prevent
131
- * a crafted recommendation from spoofing the authoritative fingerprint sentinel
132
- * line that is appended by buildIssueBody().
136
+ * Neutralise every `**Fingerprint:**` literal in untrusted text by rewriting it
137
+ * to `__Fingerprint__`. The ONE copy of this neutralisation: this module's
138
+ * {@link buildIssueBody} and `ux-grill/reconcile.mjs` both route free text
139
+ * through it (#1339 P6), so a hardening added here reaches both producers.
133
140
  *
134
- * Context: glab parses issue bodies as Markdown. If `recommendation` contained
141
+ * Context: glab parses issue bodies as Markdown. If a free-text field contained
135
142
  * `**Fingerprint:** <attacker-hash>`, a grep for the fingerprint sentinel in
136
143
  * listExistingFindings() would match the spoofed value instead of the real one,
137
144
  * bypassing dedup. Replacing the literal prevents the attack without altering
138
- * the semantic meaning of the recommendation text.
145
+ * the semantic meaning of the text.
146
+ *
147
+ * @param {unknown} text - coerced with `String(text ?? '')`
148
+ * @returns {string}
149
+ */
150
+ export function neutralizeFingerprintSentinel(text) {
151
+ // gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
152
+ // are also neutralized. Sanitizer is intentionally broader than the case-sensitive
153
+ // extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
154
+ return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
155
+ }
156
+
157
+ /**
158
+ * The authoritative dedup sentinel line, in the exact shape
159
+ * {@link extractFingerprintFromBody} reads. The caller passes an
160
+ * already-trusted value; this function does not sanitise it.
161
+ *
162
+ * @param {string} fp - 16-char hex fingerprint
163
+ * @returns {string}
164
+ */
165
+ export function fingerprintSentinelLine(fp) {
166
+ return `**Fingerprint:** \`${fp}\``;
167
+ }
168
+
169
+ /**
170
+ * Sanitize free-text recommendation fields before embedding in the issue body
171
+ * (#388 SEC-IR-MED-1) — {@link neutralizeFingerprintSentinel} for a non-empty
172
+ * string, anything else returned unchanged (a `null` description is dropped by
173
+ * buildIssueBody's line filter rather than rendered as an empty line).
139
174
  *
140
175
  * @param {string|undefined|null} text
141
176
  * @returns {string|undefined|null}
142
177
  */
143
178
  function sanitizeRecommendation(text) {
144
179
  if (!text || typeof text !== 'string') return text;
145
- // gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
146
- // are also neutralized. Sanitizer is intentionally broader than the case-sensitive
147
- // extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
148
- return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
180
+ return neutralizeFingerprintSentinel(text);
149
181
  }
150
182
 
151
183
  /**
152
184
  * Render untrusted text as the content of ONE inline Markdown code span:
153
- * sentinel literals neutralised (sanitizeRecommendation), line breaks and NUL
154
- * folded to a space, backticks turned into `'` — so a page-controlled locator
155
- * can neither close its code span nor start a new body line.
185
+ * sentinel literals neutralised, line breaks and NUL folded to a space,
186
+ * backticks turned into `'` — so a page-controlled locator can neither close
187
+ * its code span nor start a new body line.
156
188
  *
157
189
  * @param {unknown} text
158
190
  * @returns {string}
159
191
  */
160
192
  function codeSpanContent(text) {
161
- return sanitizeRecommendation(String(text ?? ''))
193
+ return neutralizeFingerprintSentinel(text)
162
194
  .replace(/[\r\n\0]+/g, ' ')
163
195
  .replace(/`/g, "'");
164
196
  }
@@ -218,7 +250,7 @@ export function buildIssueBody(finding, fp) {
218
250
  '',
219
251
  safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
220
252
  '',
221
- `**Fingerprint:** \`${fp}\``,
253
+ fingerprintSentinelLine(fp),
222
254
  `**Severity:** ${finding.severity}`,
223
255
  `**Check:** ${finding.checkId}`,
224
256
  `**Locator:** \`${codeSpanContent(finding.locator)}\``,
@@ -76,9 +76,9 @@
76
76
  import { readFileSync, existsSync, statSync } from 'node:fs';
77
77
  import { join, isAbsolute, relative, resolve } from 'node:path';
78
78
  import { execFileSync } from 'node:child_process';
79
- import { fileURLToPath } from 'node:url';
80
79
 
81
80
  import { writeStdoutLineSync } from './io.mjs';
81
+ import { isMainModule } from './is-main-module.mjs';
82
82
 
83
83
  // ---------------------------------------------------------------------------
84
84
  // The definition — the part that must never be re-derived by hand
@@ -475,10 +475,6 @@ function main() {
475
475
  process.exit(args.check && !result.withinCorridor ? 1 : 0);
476
476
  }
477
477
 
478
- const isMain =
479
- typeof process !== 'undefined' &&
480
- process.argv[1] !== null &&
481
- process.argv[1] !== undefined &&
482
- resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
478
+ const isMain =isMainModule(import.meta.url);
483
479
 
484
480
  if (isMain) main();
@@ -2,7 +2,8 @@
2
2
  * scripts/lib/tmux-layout/telemetry-stats.mjs
3
3
  *
4
4
  * Aggregator stub for /tmux-layout telemetry promotion gate (#563).
5
- * Reads .orchestrator/metrics/events.jsonl and computes:
5
+ * Reads the events ledger — active file AND every rotated archive (#1407) —
6
+ * and computes:
6
7
  * - invocation count (total + per layout)
7
8
  * - completion rate (completed / invoked) — promotion gate threshold 80%
8
9
  * - top-K degradation reasons
@@ -13,25 +14,68 @@
13
14
  * - CLI: `node scripts/lib/tmux-layout/telemetry-stats.mjs` (emits JSON to stdout)
14
15
  */
15
16
 
16
- import { readFileSync, existsSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { readEventsWithRotations } from '../events.mjs';
19
+ import { isMainModule } from '../is-main-module.mjs';
17
20
 
18
21
  const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
19
22
 
23
+ /** Every event this module reports on carries this prefix. */
24
+ const TMUX_EVENT_PREFIX = 'tmux-layout.';
25
+
26
+ /**
27
+ * Read the tmux-layout events ACROSS rotation boundaries, together with the
28
+ * honesty verdict of that read.
29
+ *
30
+ * WHY THE ENVELOPE (#1407 acceptance criterion 3): the stats below are an
31
+ * ALL-TIME rate. Reading only the active file silently redefines "all time" as
32
+ * "since the last rotation", and a genuinely missing archive would then look
33
+ * identical to a quiet week. `complete === false` says which of the two it is;
34
+ * the CLI prints it and never computes over a partial set in silence.
35
+ *
36
+ * THREE states (#1423): `complete === null` is a THIRD reading — no source
37
+ * existed at all, so `invocations: 0` is UNMEASURED, not a measured zero, and
38
+ * `meetsPromotionGate: false` below rests on nothing. The CLI names that case
39
+ * separately; folding it into `false` would report a gap nobody found, folding
40
+ * it into `true` would report a measurement nobody took.
41
+ *
42
+ * CEILING (BV-004): `readEventsWithRotations` loads the active file and every
43
+ * archive fully into memory — up to ~60 MB transient at the default
44
+ * `max-size-mb: 10` / `max-backups: 5`. Acceptable here because this is a COLD
45
+ * ops path: a hand-run CLI / promotion-gate check, never a hook and never on
46
+ * the session path. Revisit if this module gains a hot-path caller or if
47
+ * `max-size-mb` is raised past ~100.
48
+ *
49
+ * @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
50
+ * @returns {{events: Array<object>, complete: boolean|null, gaps: Array<object>,
51
+ * notices: Array<object>}}
52
+ */
53
+ export function readTmuxEventsEnvelope(eventsPath = EVENTS_PATH) {
54
+ const { events, gaps, complete, notices } = readEventsWithRotations(undefined, {
55
+ filePath: eventsPath,
56
+ });
57
+ return {
58
+ events: events.filter(
59
+ (rec) => rec && typeof rec.event === 'string' && rec.event.startsWith(TMUX_EVENT_PREFIX),
60
+ ),
61
+ complete,
62
+ gaps,
63
+ notices: notices ?? [],
64
+ };
65
+ }
66
+
20
67
  /**
21
- * Read events.jsonl and return tmux-layout-related events.
68
+ * Read the events ledger and return tmux-layout-related events.
69
+ *
70
+ * Thin array-returning view of {@link readTmuxEventsEnvelope} — the shape every
71
+ * existing caller expects. Use the envelope when the completeness of the read
72
+ * matters to the answer.
22
73
  *
23
74
  * @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
24
75
  * @returns {Array<object>} parsed event records (filtered to tmux-layout.* events)
25
76
  */
26
77
  export function readTmuxEvents(eventsPath = EVENTS_PATH) {
27
- if (!existsSync(eventsPath)) return [];
28
- const raw = readFileSync(eventsPath, 'utf-8');
29
- const lines = raw.split('\n').filter((l) => l.trim().length > 0);
30
- return lines
31
- .map((l) => {
32
- try { return JSON.parse(l); } catch { return null; }
33
- })
34
- .filter((rec) => rec && typeof rec.event === 'string' && rec.event.startsWith('tmux-layout.'));
78
+ return readTmuxEventsEnvelope(eventsPath).events;
35
79
  }
36
80
 
37
81
  /**
@@ -93,8 +137,24 @@ export function computeStats(events) {
93
137
  }
94
138
 
95
139
  // CLI entry-point — emit stats as JSON to stdout when run directly
96
- if (import.meta.url === `file://${process.argv[1]}`) {
97
- const events = readTmuxEvents();
140
+ if (isMainModule(import.meta.url)) {
141
+ const { events, complete, gaps } = readTmuxEventsEnvelope();
98
142
  const stats = computeStats(events);
99
- console.log(JSON.stringify(stats, null, 2));
143
+ if (complete === null) {
144
+ // NOT MEASURED — a different sentence from "incomplete" (#1423): no active
145
+ // file and no archive means the zeros below were never observed.
146
+ console.error(
147
+ 'WARN: no events source was readable (no active file, no archive) — the stats below are UNMEASURED, not zero',
148
+ );
149
+ } else if (!complete) {
150
+ // A gap, never an empty window: diagnostics on stderr (cli-design.md),
151
+ // the machine-readable verdict in the JSON below.
152
+ const detail = gaps
153
+ .map((g) => `${g.kind}:${path.basename(String(g.archived_as ?? 'unknown'))}`)
154
+ .join(', ');
155
+ console.error(
156
+ `WARN: events ledger incomplete — the all-time rate below is computed over a PARTIAL set (${gaps.length} gap(s): ${detail})`,
157
+ );
158
+ }
159
+ console.log(JSON.stringify({ ...stats, ledgerComplete: complete, ledgerGaps: gaps }, null, 2));
100
160
  }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * scripts/lib/user-invocable-skills.mjs
3
+ *
4
+ * ONE definition of "this skill is an operator-facing slash command".
5
+ *
6
+ * Since the #1370 command→skill fold (2026-09-16) `commands/` holds only the two
7
+ * names that cannot be a skill (`session`, `templates-ack` — see
8
+ * `tests/commands/headless-bare-command-availability.test.mjs` for the measured
9
+ * mechanism). Every other slash command IS a skill, marked by an EXPLICIT
10
+ * `user-invocable: true` in its SKILL.md frontmatter. Consumers that used to
11
+ * count or link `commands/*.md` therefore need the union:
12
+ *
13
+ * commands/*.md ∪ skills with explicit `user-invocable: true`
14
+ *
15
+ * This module exists so that union has one implementation rather than one per
16
+ * consumer (site-numbers tile, sunset walker linkage, the guard tests). It is
17
+ * deliberately tiny and dependency-free: `scripts/lib/sunset/walker.mjs` imports it,
18
+ * and the walker sits in the hook import graph, which must load WITHOUT
19
+ * node_modules (GH#62/#63) — so no `js-yaml` here; the frontmatter parser is the
20
+ * repo's own `parseAgentFrontmatter`, already in that graph.
21
+ *
22
+ * PARSED, never line-matched: `.claude/rules/test-hygiene.md` records that a
23
+ * line-regex frontmatter reader is blind to unparseable YAML and mis-measures
24
+ * block scalars. A SKILL.md whose frontmatter does not parse is treated as NOT
25
+ * user-invocable — fail-closed, and loud where it matters because such a file
26
+ * already fails `check-skills`/`validate-plugin`.
27
+ */
28
+
29
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
30
+ import path from 'node:path';
31
+ import { parseAgentFrontmatter } from './agent-frontmatter.mjs';
32
+
33
+ /**
34
+ * YAML 1.1 boolean lookalikes that YAML 1.2 / js-yaml's CORE_SCHEMA (what
35
+ * `generate-codex-skills` parses with) reads as PLAIN STRINGS, not as `true`.
36
+ * They are therefore NOT user-invocable here either — but silently demoting a
37
+ * skill on one of them is the #1370 defect class, so each one gets a WARN.
38
+ */
39
+ const TRUTHY_LOOKALIKE = /^(?:yes|y|on|t|1)$/i;
40
+
41
+ /**
42
+ * Strip a trailing `#` comment, honouring quotes: `true # note` is the same
43
+ * declaration as `true`, but `"a # b"` carries the hash as content.
44
+ *
45
+ * @param {string} text
46
+ * @returns {string}
47
+ */
48
+ function stripTrailingComment(text) {
49
+ let quote = null;
50
+ for (let i = 0; i < text.length; i += 1) {
51
+ const ch = text[i];
52
+ if (quote) {
53
+ if (ch === quote) quote = null;
54
+ continue;
55
+ }
56
+ if (ch === '"' || ch === "'") {
57
+ quote = ch;
58
+ continue;
59
+ }
60
+ // A `#` opens a YAML comment only at the start or after whitespace.
61
+ if (ch === '#' && (i === 0 || /\s/.test(text[i - 1]))) return text.slice(0, i);
62
+ }
63
+ return text;
64
+ }
65
+
66
+ /**
67
+ * Normalise a raw frontmatter scalar to the token a YAML parser would see:
68
+ * trim, drop a trailing comment, drop one layer of matching surrounding quotes.
69
+ *
70
+ * @param {string} text
71
+ * @returns {string}
72
+ */
73
+ function normaliseScalar(text) {
74
+ let out = stripTrailingComment(text).trim();
75
+ if (out.length >= 2 && (out[0] === '"' || out[0] === "'") && out[out.length - 1] === out[0]) {
76
+ out = out.slice(1, -1).trim();
77
+ }
78
+ return out;
79
+ }
80
+
81
+ /**
82
+ * THE one normaliser for `user-invocable` — the marker "this skill is an
83
+ * operator-facing slash command". Every generator (Cursor, Pi, Codex) and every
84
+ * counter (site-numbers tile, drift-check `command-count`, the guard tests) must
85
+ * route through this function; four private re-implementations disagreeing on a
86
+ * quoted value is the defect this replaces.
87
+ *
88
+ * Explicit-true only. `undefined` (flag absent) is NOT user-invocable: the whole
89
+ * point of the marker is that it is written down, so a skill that forgot it is a
90
+ * defect to surface, not a default to guess.
91
+ *
92
+ * Accepted: the boolean `true`, and a string that normalises to `true`
93
+ * case-insensitively — so `"true"`, `'true'`, `True`, `true # note` and
94
+ * `true ` (trailing space, invisible in an editor) all read as the same
95
+ * declaration. Trailing-whitespace tolerance is load-bearing rather than
96
+ * cosmetic: without it a stray space silently demotes a skill out of the
97
+ * generated adapters with no diagnostic anywhere.
98
+ *
99
+ * REJECTED: `yes` / `y` / `on` / `t` / `1`. Those are YAML 1.1 booleans; under
100
+ * YAML 1.2 — which is what js-yaml's CORE_SCHEMA in `generate-codex-skills`
101
+ * applies — they are plain strings, and this predicate follows the parser rather
102
+ * than inventing a fifth dialect. Because the demotion is the surprising half,
103
+ * each one emits exactly one stderr WARN naming the file.
104
+ *
105
+ * The same normalisation governs the SIBLING boolean marker
106
+ * `disable-model-invocation` (the Cursor and Codex adapters read it with this
107
+ * predicate). The WARN therefore names the key it was asked about rather than a
108
+ * hard-coded `user-invocable`: measured 2026-09-18, a `disable-model-invocation:
109
+ * yes` demotion reported `user-invocable: "yes"`, sending the operator to a
110
+ * line that does not exist in that file.
111
+ *
112
+ * The NOUN follows the key too (#1388 P10). Naming the key while hard-coding
113
+ * "is NOT a slash-command marker" told a `disable-model-invocation: yes` author
114
+ * that his value failed to be something it never was — a correct key with a
115
+ * wrong subject reads as a broken diagnostic. The default phrase is unchanged
116
+ * for `user-invocable`, so the four existing call sites see identical text.
117
+ *
118
+ * @param {unknown} value the raw frontmatter value
119
+ * @param {string} [file] path named in the WARN when a truthy-looking value is demoted
120
+ * @param {string} [key] frontmatter key named in the WARN (default `user-invocable`)
121
+ * @returns {boolean}
122
+ */
123
+ const MARKER_NOUN = Object.freeze({
124
+ 'user-invocable': 'a slash-command marker',
125
+ 'disable-model-invocation': 'a model-invocation opt-out marker',
126
+ });
127
+ const DEFAULT_MARKER_NOUN = 'a boolean marker';
128
+
129
+ export function isUserInvocableValue(value, file, key = 'user-invocable') {
130
+ if (value === true) return true;
131
+ if (typeof value !== 'string') return false;
132
+ const token = normaliseScalar(value);
133
+ if (token.toLowerCase() === 'true') return true;
134
+ if (TRUTHY_LOOKALIKE.test(token)) {
135
+ const where = file ? `${file}: ` : '';
136
+ process.stderr.write(
137
+ `WARN ${where}${key}: ${JSON.stringify(value)} is NOT ${MARKER_NOUN[key] ?? DEFAULT_MARKER_NOUN} — `
138
+ + `only \`true\` is (YAML 1.2 reads yes/on/1 as strings). Write \`${key}: true\`.\n`,
139
+ );
140
+ }
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * Parse a SKILL.md's YAML frontmatter. Returns `null` when there is no
146
+ * frontmatter block or it does not parse.
147
+ *
148
+ * @param {string} content
149
+ * @returns {Record<string, unknown>|null}
150
+ */
151
+ export function parseSkillFrontmatter(content) {
152
+ if (typeof content !== 'string') return null;
153
+ const parsed = parseAgentFrontmatter(content);
154
+ return parsed.ok ? parsed.frontmatter : null;
155
+ }
156
+
157
+ /**
158
+ * Names of skills carrying an explicit `user-invocable: true`, sorted.
159
+ *
160
+ * @param {string} repoRoot plugin root (the directory holding `skills/`)
161
+ * @returns {string[]}
162
+ */
163
+ export function userInvocableSkills(repoRoot) {
164
+ const dir = path.join(repoRoot, 'skills');
165
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
166
+ const names = [];
167
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
168
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
169
+ const file = path.join(dir, entry.name, 'SKILL.md');
170
+ if (!existsSync(file) || !statSync(file).isFile()) continue;
171
+ let fm;
172
+ try {
173
+ fm = parseSkillFrontmatter(readFileSync(file, 'utf8'));
174
+ } catch {
175
+ continue;
176
+ }
177
+ if (fm && isUserInvocableValue(fm['user-invocable'], file)) names.push(entry.name);
178
+ }
179
+ return names.sort();
180
+ }
181
+
182
+ /** Names of `commands/*.md` files (without the extension), sorted. */
183
+ export function commandFileNames(repoRoot) {
184
+ const dir = path.join(repoRoot, 'commands');
185
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
186
+ return readdirSync(dir)
187
+ .filter((f) => f.endsWith('.md') && !f.startsWith('.'))
188
+ .map((f) => f.replace(/\.md$/, ''))
189
+ .sort();
190
+ }
191
+
192
+ /**
193
+ * The product's "slash commands" set: `commands/*.md` ∪ explicitly
194
+ * user-invocable skills, deduplicated by NAME.
195
+ *
196
+ * Dedup is load-bearing rather than defensive: a name present as BOTH is the
197
+ * picker-duplicate bug (the body runs, and the entry lists twice), so it must
198
+ * count once here and go red in the guard test — never inflate the number.
199
+ *
200
+ * @param {string} repoRoot
201
+ * @returns {string[]} sorted unique names
202
+ */
203
+ export function slashCommandNames(repoRoot) {
204
+ return [...new Set([...commandFileNames(repoRoot), ...userInvocableSkills(repoRoot)])].sort();
205
+ }