session-orchestrator 3.24.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +3 -2
  47. package/.codex-plugin/skills/architecture/SKILL.md +20 -0
  48. package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
  49. package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
  50. package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
  51. package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
  52. package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
  53. package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
  54. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
  55. package/.codex-plugin/skills/close/SKILL.md +21 -0
  56. package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
  57. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
  58. package/.codex-plugin/skills/debug/SKILL.md +21 -0
  59. package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
  60. package/.codex-plugin/skills/discovery/SKILL.md +21 -0
  61. package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
  62. package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
  63. package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
  64. package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
  65. package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
  66. package/.codex-plugin/skills/eli5/SKILL.md +21 -0
  67. package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
  68. package/.codex-plugin/skills/eval/SKILL.md +21 -0
  69. package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
  70. package/.codex-plugin/skills/evolve/SKILL.md +21 -0
  71. package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
  72. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
  73. package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
  74. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
  75. package/.codex-plugin/skills/go/SKILL.md +22 -0
  76. package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
  77. package/.codex-plugin/skills/grill/SKILL.md +21 -0
  78. package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
  79. package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
  80. package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
  81. package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
  82. package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
  83. package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
  84. package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
  85. package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
  86. package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
  87. package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
  88. package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
  89. package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
  90. package/.codex-plugin/skills/plan/SKILL.md +22 -0
  91. package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
  92. package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
  93. package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
  94. package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
  95. package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
  96. package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
  97. package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
  98. package/.codex-plugin/skills/release/SKILL.md +22 -0
  99. package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
  100. package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
  101. package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
  102. package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
  103. package/.codex-plugin/skills/session/SKILL.md +21 -0
  104. package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
  105. package/.codex-plugin/skills/session-end/SKILL.md +22 -0
  106. package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
  107. package/.codex-plugin/skills/session-start/SKILL.md +22 -0
  108. package/.codex-plugin/skills/spinout/SKILL.md +21 -0
  109. package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
  110. package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
  111. package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
  112. package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
  113. package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
  114. package/.codex-plugin/skills/test/SKILL.md +21 -0
  115. package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
  116. package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
  117. package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
  118. package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
  119. package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
  120. package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
  121. package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
  122. package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
  123. package/.cursor/commands/autopilot.md +2 -2
  124. package/.cursor/commands/bootstrap.md +1 -1
  125. package/.cursor/commands/brainstorm.md +1 -1
  126. package/.cursor/commands/debug.md +1 -1
  127. package/.cursor/commands/discovery.md +1 -1
  128. package/.cursor/commands/dispatcher.md +2 -2
  129. package/.cursor/commands/eli5.md +2 -2
  130. package/.cursor/commands/eval.md +2 -2
  131. package/.cursor/commands/evolve.md +1 -1
  132. package/.cursor/commands/go.md +1 -1
  133. package/.cursor/commands/grill.md +2 -2
  134. package/.cursor/commands/memory-cleanup.md +2 -2
  135. package/.cursor/commands/persona-panel.md +1 -1
  136. package/.cursor/commands/plan.md +1 -1
  137. package/.cursor/commands/portfolio.md +1 -1
  138. package/.cursor/commands/reconcile.md +2 -2
  139. package/.cursor/commands/release.md +2 -2
  140. package/.cursor/commands/session.md +2 -2
  141. package/.cursor/commands/spinout.md +2 -2
  142. package/.cursor/commands/sunset-review.md +2 -2
  143. package/.cursor/commands/templates-ack.md +2 -2
  144. package/.cursor/commands/test.md +2 -2
  145. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  146. package/.cursor/skills/eval/SKILL.md +1 -1
  147. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  148. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  149. package/.cursor-plugin/plugin.json +30 -0
  150. package/.orchestrator/policy/blocked-commands.json +121 -0
  151. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  152. package/.orchestrator/policy/quality-gates.example.json +16 -0
  153. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  154. package/.orchestrator/policy/templates-policy.json +27 -0
  155. package/.orchestrator/policy/test-profiles.json +47 -0
  156. package/AGENTS.md +225 -0
  157. package/CHANGELOG.md +1314 -2
  158. package/NOTICE +11 -6
  159. package/README.md +135 -94
  160. package/agents/eval-judge.md +1 -1
  161. package/agents/skill-applied-judge.md +1 -1
  162. package/assets/wave-lifecycle.svg +98 -0
  163. package/commands/release.md +6 -3
  164. package/commands/session.md +18 -3
  165. package/docs/README.md +4 -0
  166. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  167. package/docs/baseline.md +67 -0
  168. package/docs/ci-setup.md +108 -62
  169. package/docs/codex-setup.md +107 -29
  170. package/docs/components.md +38 -16
  171. package/docs/cursor-setup.md +6 -2
  172. package/docs/events-schema.md +9 -6
  173. package/docs/instruction-delivery.md +69 -0
  174. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  175. package/docs/migration-v4.md +365 -0
  176. package/docs/pi-setup.md +6 -1
  177. package/docs/plugin-architecture-v3.md +1 -1
  178. package/docs/rule-authoring.md +85 -19
  179. package/docs/scope-collision-guard.md +5 -5
  180. package/docs/session-config-reference.md +57 -56
  181. package/docs/session-config-template.md +6 -29
  182. package/docs/telemetry.md +157 -3
  183. package/docs/vault-docs-architecture.md +50 -11
  184. package/hooks/_lib/hook-import-set.json +1488 -0
  185. package/hooks/_lib/subagent-transcript.mjs +562 -0
  186. package/hooks/config-protection.mjs +2 -2
  187. package/hooks/cwd-change-restore.mjs +2 -2
  188. package/hooks/enforce-commands.mjs +69 -0
  189. package/hooks/hooks-codex.json +1 -1
  190. package/hooks/hooks-cursor.json +10 -0
  191. package/hooks/hooks-pi.json +5 -0
  192. package/hooks/hooks.json +6 -1
  193. package/hooks/loop-guard.mjs +3 -3
  194. package/hooks/on-session-end.mjs +2 -2
  195. package/hooks/on-session-start.mjs +103 -2
  196. package/hooks/on-stop.mjs +60 -14
  197. package/hooks/operator-steer.mjs +2 -2
  198. package/hooks/post-bash-write-verify.mjs +85 -0
  199. package/hooks/post-edit-import-probe.mjs +344 -0
  200. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  201. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  202. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  203. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  204. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  205. package/hooks/skill-invocation-telemetry.mjs +17 -5
  206. package/hooks/subagent-telemetry.mjs +13 -4
  207. package/monitors/monitors.json +3 -3
  208. package/package.json +9 -1
  209. package/pi/prompts/session.md +2 -2
  210. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  211. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  212. package/scripts/dialectic-deriver.mjs +73 -8
  213. package/scripts/export-hw-learnings.mjs +113 -1
  214. package/scripts/generate-agents-skills.mjs +378 -0
  215. package/scripts/generate-codex-skills.mjs +246 -0
  216. package/scripts/generate-cursor-adapter.mjs +45 -8
  217. package/scripts/generate-hook-import-set.mjs +292 -0
  218. package/scripts/lib/agent-status.mjs +13 -2
  219. package/scripts/lib/auto-dream.mjs +38 -36
  220. package/scripts/lib/autonomy/suitability.mjs +6 -0
  221. package/scripts/lib/autopilot/loop.mjs +2 -2
  222. package/scripts/lib/ci-status-banner.mjs +220 -75
  223. package/scripts/lib/codex/plugin-contract.mjs +88 -6
  224. package/scripts/lib/config/auto-dream.mjs +2 -1
  225. package/scripts/lib/config/block-header.mjs +8 -0
  226. package/scripts/lib/config/block-preprocess.mjs +177 -0
  227. package/scripts/lib/config/broken-window.mjs +2 -1
  228. package/scripts/lib/config/cold-start.mjs +2 -1
  229. package/scripts/lib/config/config-protection.mjs +22 -2
  230. package/scripts/lib/config/context-coverage.mjs +2 -1
  231. package/scripts/lib/config/cross-repo.mjs +2 -1
  232. package/scripts/lib/config/custom-phases.mjs +2 -1
  233. package/scripts/lib/config/dialectic.mjs +2 -1
  234. package/scripts/lib/config/discovery-validator.mjs +2 -1
  235. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  236. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  237. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  238. package/scripts/lib/config/docs-staleness.mjs +2 -1
  239. package/scripts/lib/config/drift-check.mjs +2 -1
  240. package/scripts/lib/config/eval.mjs +2 -1
  241. package/scripts/lib/config/events-rotation.mjs +2 -1
  242. package/scripts/lib/config/evolve.mjs +8 -2
  243. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  244. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  245. package/scripts/lib/config/handover-gate.mjs +2 -1
  246. package/scripts/lib/config/health-endpoints.mjs +7 -2
  247. package/scripts/lib/config/host-paths.mjs +20 -4
  248. package/scripts/lib/config/issue-budget.mjs +2 -1
  249. package/scripts/lib/config/loop-guard.mjs +2 -1
  250. package/scripts/lib/config/memory.mjs +2 -1
  251. package/scripts/lib/config/moc-staleness.mjs +2 -1
  252. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  253. package/scripts/lib/config/private-config-dir.mjs +67 -0
  254. package/scripts/lib/config/reconcile.mjs +2 -1
  255. package/scripts/lib/config/remote-hosts.mjs +2 -1
  256. package/scripts/lib/config/section-extractor.mjs +7 -1
  257. package/scripts/lib/config/skill-evolution.mjs +2 -1
  258. package/scripts/lib/config/slopcheck.mjs +2 -1
  259. package/scripts/lib/config/state-md-lock.mjs +2 -1
  260. package/scripts/lib/config/templates-first.mjs +2 -1
  261. package/scripts/lib/config/test.mjs +2 -1
  262. package/scripts/lib/config/vault-integration.mjs +7 -1
  263. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  264. package/scripts/lib/config/vault-staleness.mjs +2 -1
  265. package/scripts/lib/config/vault-sync.mjs +2 -1
  266. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  267. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  268. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  269. package/scripts/lib/convergence-monitor.mjs +82 -16
  270. package/scripts/lib/dispatcher/rank.mjs +124 -48
  271. package/scripts/lib/ecosystem-health.mjs +16 -2
  272. package/scripts/lib/eval/engine.mjs +9 -1
  273. package/scripts/lib/eval/session-resolve.mjs +23 -4
  274. package/scripts/lib/events.mjs +22 -6
  275. package/scripts/lib/frontmatter-guard.mjs +131 -13
  276. package/scripts/lib/gates/gate-full.mjs +30 -0
  277. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  278. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  279. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  280. package/scripts/lib/host-identity.mjs +50 -11
  281. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  282. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  283. package/scripts/lib/learnings/io.mjs +60 -6
  284. package/scripts/lib/memory-proposals/store.mjs +30 -22
  285. package/scripts/lib/owner-config-banner.mjs +41 -6
  286. package/scripts/lib/owner-config-loader.mjs +21 -10
  287. package/scripts/lib/owner-interview.mjs +3 -3
  288. package/scripts/lib/owner-yaml.mjs +215 -15
  289. package/scripts/lib/platform.mjs +108 -15
  290. package/scripts/lib/plugin-update-banner.mjs +414 -0
  291. package/scripts/lib/project-hygiene.mjs +38 -2
  292. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  293. package/scripts/lib/quality-gate.mjs +133 -44
  294. package/scripts/lib/reconcile/emitter.mjs +68 -6
  295. package/scripts/lib/reconcile/engine.mjs +51 -11
  296. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  297. package/scripts/lib/reconcile/writer.mjs +40 -18
  298. package/scripts/lib/session-close-backfill.mjs +67 -9
  299. package/scripts/lib/session-id.mjs +12 -23
  300. package/scripts/lib/session-identity/own-session.mjs +125 -10
  301. package/scripts/lib/session-lock-shape.mjs +43 -0
  302. package/scripts/lib/session-lock.mjs +5 -10
  303. package/scripts/lib/session-registry.mjs +25 -9
  304. package/scripts/lib/session-schema/constants.mjs +64 -3
  305. package/scripts/lib/session-schema/validator.mjs +38 -4
  306. package/scripts/lib/session-start-probes.mjs +30 -1
  307. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  308. package/scripts/lib/skill-health/join.mjs +17 -4
  309. package/scripts/lib/state-md.mjs +78 -0
  310. package/scripts/lib/sunset/walker.mjs +6 -0
  311. package/scripts/lib/telemetry/schema.mjs +202 -9
  312. package/scripts/lib/telemetry/sync.mjs +368 -12
  313. package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
  314. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  315. package/scripts/lib/validate/check-agents.mjs +3 -3
  316. package/scripts/lib/validate/check-codex-skills.mjs +191 -0
  317. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  318. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  319. package/scripts/lib/validate/check-owner-leakage.mjs +319 -22
  320. package/scripts/lib/validate/check-skill-links.mjs +193 -0
  321. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  322. package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
  323. package/scripts/lib/validate/check-unwired-features.mjs +163 -15
  324. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  325. package/scripts/lib/validate/confidential-names.mjs +95 -30
  326. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  327. package/scripts/lib/validate/repo-files.mjs +48 -14
  328. package/scripts/lib/vault-backfill/template.mjs +63 -6
  329. package/scripts/lib/vault-mirror/process.mjs +165 -42
  330. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  331. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  332. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  333. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  334. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  335. package/scripts/lib/wave-resource-gate.mjs +8 -2
  336. package/scripts/lib/wave-sizing.mjs +4 -1
  337. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  338. package/scripts/materialize-wave-scope.mjs +12 -5
  339. package/scripts/memory-propose.mjs +19 -5
  340. package/scripts/migrate-cold-start-seed.mjs +4 -1
  341. package/scripts/parse-config.mjs +60 -3
  342. package/scripts/release.mjs +430 -31
  343. package/scripts/repair-invalid-sessions.mjs +3 -3
  344. package/scripts/run-quality-gate.mjs +128 -11
  345. package/scripts/site-numbers.mjs +344 -8
  346. package/scripts/sweep-expired-learnings.mjs +90 -0
  347. package/scripts/sync-vault-schema.mjs +3 -1
  348. package/scripts/telemetry.mjs +2 -2
  349. package/scripts/validate-plugin.mjs +164 -0
  350. package/scripts/validate-wave-scope.mjs +28 -8
  351. package/scripts/wave-scope-binding.mjs +215 -0
  352. package/skills/_shared/instruction-file-resolution.md +10 -0
  353. package/skills/_shared/parallel-aware-preamble.md +1 -0
  354. package/skills/_shared/platform-tools.md +1 -1
  355. package/skills/_shared/state-ownership.md +1 -1
  356. package/skills/architecture/SKILL.md +7 -5
  357. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  358. package/skills/autopilot/SKILL.md +4 -18
  359. package/skills/claude-md-drift-check/SKILL.md +5 -1
  360. package/skills/claude-md-drift-check/checker.mjs +62 -2
  361. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  362. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  363. package/skills/discovery/probes-arch.md +20 -18
  364. package/skills/dispatcher/SKILL.md +3 -2
  365. package/skills/evolve/SKILL.md +65 -26
  366. package/skills/frontmatter-guard/SKILL.md +11 -5
  367. package/skills/npm-publish/SKILL.md +1 -1
  368. package/skills/reconcile/SKILL.md +33 -0
  369. package/skills/remote-offload/SKILL.md +1 -1
  370. package/skills/session-end/SKILL.md +18 -905
  371. package/skills/session-end/phase-3-6-tail.md +10 -3
  372. package/skills/session-end/plan-verification.md +221 -155
  373. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  374. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  375. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  376. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  377. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  378. package/skills/session-end/references/session-summary-template.md +62 -0
  379. package/skills/session-plan/SKILL.md +49 -0
  380. package/skills/session-start/SKILL.md +22 -904
  381. package/skills/session-start/phase-8-5-express-path.md +1 -1
  382. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  383. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  384. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  385. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  386. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  387. package/skills/session-start/references/phase-4-ssot-environment-check.md +160 -0
  388. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  389. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  390. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  391. package/skills/vault-sync/SKILL.md +10 -0
  392. package/skills/vault-sync/validator.mjs +21 -27
  393. package/skills/wave-executor/SKILL.md +15 -1
  394. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  395. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  396. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  397. package/skills/wave-executor/wave-loop.md +14 -1309
  398. package/templates/_shared/journey-manifest.md +10 -6
  399. package/.cursor/commands/autopilot-multi.md +0 -14
  400. package/.cursor/commands/contract-version-bump.md +0 -14
  401. package/.cursor/commands/journey-audit.md +0 -14
  402. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  403. package/.cursor/skills/daily/SKILL.md +0 -12
  404. package/.cursor/skills/domain-model/SKILL.md +0 -13
  405. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  406. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  407. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  408. package/commands/autopilot-multi.md +0 -74
  409. package/commands/contract-version-bump.md +0 -28
  410. package/commands/journey-audit.md +0 -43
  411. package/pi/prompts/autopilot-multi.md +0 -12
  412. package/pi/prompts/contract-version-bump.md +0 -12
  413. package/pi/prompts/journey-audit.md +0 -12
  414. package/scripts/autopilot-multi.mjs +0 -885
  415. package/scripts/backfill-learnings-expires.mjs +0 -196
  416. package/scripts/backfill-learnings.mjs +0 -203
  417. package/scripts/fleet-instruction-scan.mjs +0 -141
  418. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  419. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  420. package/scripts/lib/webhook-url.mjs +0 -105
  421. package/scripts/lifecycle-sim-v6.mjs +0 -347
  422. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  423. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  424. package/scripts/upload-social-preview.mjs +0 -316
  425. package/skills/_shared/model-selection.md +0 -64
  426. package/skills/contract-version-bump/SKILL.md +0 -219
  427. package/skills/daily/SKILL.md +0 -222
  428. package/skills/daily/generate.sh +0 -92
  429. package/skills/daily/templates/daily.md.tpl +0 -36
  430. package/skills/journey-audit/SKILL.md +0 -270
  431. package/skills/skill-creator/SKILL.md +0 -168
  432. package/skills/ubiquitous-language/SKILL.md +0 -97
  433. package/skills/vault-sync/package-lock.json +0 -40
  434. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  435. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -14,6 +14,9 @@
14
14
  * defaults from the policy file or built-in defaults apply.
15
15
  * --files <f1,f2,...> Comma-separated file list (incremental + per-file).
16
16
  * --session-start-ref <r> Git ref for diff base (incremental, to find changed files).
17
+ * --ledger-root <path> Repo root the telemetry event is pinned to (and the
18
+ * root session attribution is read from). Only the
19
+ * pre-push hook passes it; see the emission block below.
17
20
  * -h, --help Show this help and exit.
18
21
  *
19
22
  * Exit codes:
@@ -31,8 +34,8 @@
31
34
  * scripts/lib/gates/gate-{baseline,incremental,full,per-file}.mjs
32
35
  */
33
36
 
34
- import { existsSync, readFileSync } from 'node:fs';
35
- import { join, dirname } from 'node:path';
37
+ import { existsSync, readFileSync, statSync } from 'node:fs';
38
+ import { join, dirname, resolve } from 'node:path';
36
39
  import { fileURLToPath } from 'node:url';
37
40
  import { spawnSync } from 'node:child_process';
38
41
 
@@ -80,7 +83,7 @@ const argv = process.argv.slice(2);
80
83
  if (argv.includes('-h') || argv.includes('--help')) {
81
84
  process.stdout.write(
82
85
  'Usage: run-quality-gate.mjs --variant <variant> [--config <json-or-file>] ' +
83
- '[--files <file1,file2,...>] [--session-start-ref <ref>]\n\n' +
86
+ '[--files <file1,file2,...>] [--session-start-ref <ref>] [--ledger-root <path>]\n\n' +
84
87
  'Variants: baseline, incremental, full-gate, per-file\n\n' +
85
88
  'Exit codes:\n' +
86
89
  ' 0 — pass (non-blocking variants always exit 0)\n' +
@@ -94,6 +97,7 @@ let variant = '';
94
97
  let config = '';
95
98
  let files = '';
96
99
  let sessionStartRef = '';
100
+ let ledgerRootArg = '';
97
101
 
98
102
  for (let i = 0; i < argv.length; i++) {
99
103
  const arg = argv[i];
@@ -114,6 +118,10 @@ for (let i = 0; i < argv.length; i++) {
114
118
  if (i + 1 >= argv.length) die('Missing value for --session-start-ref');
115
119
  sessionStartRef = argv[++i];
116
120
  break;
121
+ case '--ledger-root':
122
+ if (i + 1 >= argv.length) die('Missing value for --ledger-root');
123
+ ledgerRootArg = argv[++i];
124
+ break;
117
125
  default:
118
126
  die(`Unknown argument: ${arg}`);
119
127
  }
@@ -208,6 +216,76 @@ function suiteCountsFromGateStdout(stdout) {
208
216
  return admitSuiteCounts(test);
209
217
  }
210
218
 
219
+ /**
220
+ * Lift `test.failed_files` out of a gate sub-script's JSON stdout envelope.
221
+ *
222
+ * Envelope adapter, same posture as {@link suiteCountsFromGateStdout}: it
223
+ * decides only whether a NAMED-FILE measurement exists, never what the names
224
+ * mean. `null` — never `[]` — for every non-measurement, so the caller OMITS
225
+ * the key instead of publishing an empty array that reads as "no file failed".
226
+ *
227
+ * Only `gate-full.mjs` publishes the key, and only when the runner printed a
228
+ * file-level summary; every other variant emits a bare status string.
229
+ *
230
+ * Never throws.
231
+ *
232
+ * @param {string} stdout — the gate sub-script's captured stdout.
233
+ * @returns {string[]|null}
234
+ */
235
+ function failedFilesFromGateStdout(stdout) {
236
+ if (typeof stdout !== 'string' || !stdout.trim()) return null;
237
+ let parsed;
238
+ try {
239
+ parsed = JSON.parse(stdout);
240
+ } catch {
241
+ return null;
242
+ }
243
+ const files = parsed?.test?.failed_files;
244
+ if (!Array.isArray(files) || files.length === 0) return null;
245
+ const named = files.filter((f) => typeof f === 'string' && f.trim());
246
+ return named.length > 0 ? named : null;
247
+ }
248
+
249
+ /**
250
+ * Validate and resolve the `--ledger-root` flag (see the telemetry block below).
251
+ *
252
+ * Only the pre-push hook passes this, and it hands over a path the gate then
253
+ * WRITES to — so a typo must not silently create an `.orchestrator/metrics/`
254
+ * tree somewhere arbitrary. The check is therefore two-part: the value must be
255
+ * an existing DIRECTORY, and it must already contain an `.orchestrator/`
256
+ * directory — the marker of a root this harness has already been initialised in.
257
+ *
258
+ * The alternative check (`git rev-parse --show-toplevel` with cwd = that path,
259
+ * compared against the value) is rejected on cost: it spawns a process on every
260
+ * gate run to prove a property two `statSync` calls already prove. The one case
261
+ * it accepts and this one rejects — a git root that has never run the
262
+ * orchestrator — is precisely the case with no ledger to pin to.
263
+ *
264
+ * A bad value NEVER crashes the gate: it warns once on stderr and returns
265
+ * `null`, which restores the previous resolution
266
+ * (`CLAUDE_PROJECT_DIR ?? CODEX_PROJECT_DIR ?? repoRoot`) at the call sites.
267
+ * The gate's exit code is the authoritative output; telemetry is best-effort.
268
+ *
269
+ * @param {string} value — raw flag value (`''` when the flag was not passed).
270
+ * @returns {string|null} absolute, validated root — or `null` to fall back.
271
+ */
272
+ function resolveLedgerRoot(value) {
273
+ const raw = (value || '').trim();
274
+ if (!raw) return null;
275
+ const abs = resolve(raw);
276
+ try {
277
+ if (statSync(abs).isDirectory() && statSync(join(abs, '.orchestrator')).isDirectory()) {
278
+ return abs;
279
+ }
280
+ } catch { /* falls through to the warn below */ }
281
+ warn(
282
+ `--ledger-root '${raw}' is not an initialised project root ` +
283
+ '(existing directory containing .orchestrator/) — falling back to the default ' +
284
+ 'telemetry destination.',
285
+ );
286
+ return null;
287
+ }
288
+
211
289
  /**
212
290
  * Resolve the active wave number from the wave-scope sidecar (#966 step 1).
213
291
  *
@@ -325,24 +403,63 @@ if (result.error && typeof result.status !== 'number') {
325
403
  // (single emission path). `sessionAttribution` is the shared helper in
326
404
  // events.mjs (#941); this CLI wrapper runs against the CWD `repoRoot`, so the
327
405
  // bare emitEvent destination (SO_PROJECT_DIR default) is correct here.
406
+ //
407
+ // EXCEPT under the pre-push hook, which is the one caller that runs the gate in
408
+ // a tree that is about to be DELETED. `.husky/pre-push` materialises the tracked
409
+ // tree into a temp dir and deliberately scrubs every `*PROJECT_DIR` name before
410
+ // invoking the gate there, so `getProjectDir()` resolves to that temp tree (it
411
+ // carries both a CLAUDE.md (or AGENTS.md) and a .git) and the record lands in
412
+ // `<tmp>/.orchestrator/metrics/events.jsonl`, which the hook's EXIT trap then
413
+ // removes. Measured 2026-09-06: a pre-push run that BLOCKED a push left no
414
+ // `orchestrator.quality_gate.failed` line in this repo's ledger at all — the
415
+ // gate failure was, by construction, the one event that could never be recorded.
416
+ //
417
+ // `--ledger-root` is that hook's channel for handing back the root it already
418
+ // knows (`git rev-parse --show-toplevel`, read BEFORE it cds). It pins ONLY the
419
+ // telemetry destination and the attribution root — every other path the gate
420
+ // resolves stays inside the tree actually under test, which is the whole point
421
+ // of the materialisation. Absent (every other caller) → unchanged behaviour:
422
+ // `emitEvent`'s own default resolution.
423
+ //
424
+ // It is an ARGV FLAG and not an env var, and that is load-bearing. Measured
425
+ // 2026-09-06 with the env-var form: `SO_GATE_LEDGER_ROOT=$tmp npx vitest run
426
+ // tests/scripts/run-quality-gate.test.mjs -t "telemetry emission"` → `8 failed |
427
+ // 1 passed`. The chain was: hook exports the var → `npm run quality-gate` →
428
+ // `gate-full.mjs` spawns `npm test` → every vitest worker inherits it → the
429
+ // suite's own gate spawns spread `...process.env`, so the pinned root outranked
430
+ // their per-test project dir and the gate's telemetry tests wrote to the hook's
431
+ // root. The gate that releases 4.0.0 would have blocked on itself. An env var is
432
+ // inherited by every descendant; a flag reaches exactly one process.
433
+ //
328
434
  // Best-effort: a telemetry failure must NEVER alter the gate's authoritative
329
435
  // exit code — which is why the counts parse also lives inside this try.
330
436
  const exitCode = result.status ?? 1;
437
+ const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
331
438
  try {
332
439
  const counts = suiteCountsFromGateStdout(gateStdout);
440
+ // The names behind `counts.failed`. Absent, never `[]` — see
441
+ // `failedFilesFromGateStdout`.
442
+ const failedFiles = failedFilesFromGateStdout(gateStdout);
333
443
  // Wave-scope sidecar is read from the SAME project dir the event lands in
334
444
  // (emitEvent's own destination precedence), so a tmp-scoped run cannot pick
335
445
  // up the host repo's live wave. Mirrors the hook's projectDir resolution.
336
446
  const waveNumber = resolveWaveNumber(
337
- process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot,
447
+ ledgerRoot ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot,
448
+ );
449
+ await emitEvent(
450
+ `orchestrator.quality_gate.${exitCode === 0 ? 'passed' : 'failed'}`,
451
+ {
452
+ variant,
453
+ exit_code: exitCode,
454
+ ...(counts ? { counts } : {}),
455
+ ...(failedFiles ? { failed_files: failedFiles } : {}),
456
+ ...(waveNumber !== null ? { wave_number: waveNumber } : {}),
457
+ ...sessionAttribution(ledgerRoot ?? repoRoot),
458
+ },
459
+ // `{}` is byte-identical to omitting the argument (`opts.repoRoot ??
460
+ // getProjectDir()`), so the default path is untouched.
461
+ ledgerRoot ? { repoRoot: ledgerRoot } : {},
338
462
  );
339
- await emitEvent(`orchestrator.quality_gate.${exitCode === 0 ? 'passed' : 'failed'}`, {
340
- variant,
341
- exit_code: exitCode,
342
- ...(counts ? { counts } : {}),
343
- ...(waveNumber !== null ? { wave_number: waveNumber } : {}),
344
- ...sessionAttribution(repoRoot),
345
- });
346
463
  } catch { /* best-effort telemetry — gate result is authoritative */ }
347
464
 
348
465
  process.exit(exitCode);
@@ -66,6 +66,31 @@
66
66
  * --write rewrite the span contents in place, and refresh `site/_census.json`
67
67
  * from the same measurement (only when --site is the repo's `site/`).
68
68
  *
69
+ * ## The two usage metrics (`npm-downloads-30d`, `github-stars`)
70
+ *
71
+ * These two are the only ones whose source is NOT this repository: they are
72
+ * fetched over the network, and only under `--write` (5 s timeout). `--check`
73
+ * never opens a socket — it compares the page against `site/_census.json`, which
74
+ * IS their truth between writes. That is what keeps the CI/build guard offline
75
+ * and deterministic; a `--check` that fetched would go red whenever npm's API
76
+ * was slow, which is a signal about npm and not about the page.
77
+ *
78
+ * On a fetch failure `--write` keeps the previous snapshot value and warns. It
79
+ * never writes a placeholder over a real number: the number on the page stays
80
+ * the last one that was actually measured, and the WARN names why it did not
81
+ * move. With no snapshot to keep, the metric simply has no value and the run
82
+ * fails loudly (the existing partial-census guard) rather than shipping "n/a".
83
+ *
84
+ * ## The census blocks in `site/llms-full.txt` and `site/llms.txt`
85
+ *
86
+ * The same measurement also fills a marker-bounded line in every file listed in
87
+ * `CENSUS_BLOCK_FILES` (`<!-- census:start -->` … `<!-- census:end -->`) — the
88
+ * two plain-text surfaces LLM crawlers read. Only the text BETWEEN the
89
+ * markers is rewritten; everything else in that file is hand-authored prose and
90
+ * is handed back byte-for-byte. A missing end marker is a hard error, never a
91
+ * silent skip — a generator that quietly stops filling a surface is the exact
92
+ * failure this file exists to end.
93
+ *
69
94
  * Exit codes (`.claude/rules/cli-design.md`):
70
95
  * 0 — no drift (--check) / files updated or already current (--write)
71
96
  * 1 — drift found (--check), or a contract violation in either mode
@@ -327,7 +352,7 @@ export function isDirty(root) {
327
352
  ['--no-optional-locks', 'status', '--porcelain', '--untracked-files=no'],
328
353
  { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] },
329
354
  );
330
- return out.split('\n').filter(Boolean).length > 0;
355
+ return out.split('\n').some(Boolean);
331
356
  } catch {
332
357
  return null;
333
358
  }
@@ -458,6 +483,37 @@ export const METRIC_DEFS = Object.freeze([
458
483
  source: 'jq length .orchestrator/policy/blocked-commands.json',
459
484
  compute: (root) => fmtCount(countBlockedCommands(root)),
460
485
  },
486
+ {
487
+ id: 'npm-downloads-30d',
488
+ provenance: false,
489
+ // Network-sourced: absent from EVERY offline run, which is most of them
490
+ // (--check never fetches). The snapshot is therefore not a fresh-clone
491
+ // convenience here but the metric's normal answer between two --write runs.
492
+ snapshotFallback: true,
493
+ network: true,
494
+ url: 'https://api.npmjs.org/downloads/point/last-month/session-orchestrator',
495
+ field: 'downloads',
496
+ source:
497
+ 'curl -s https://api.npmjs.org/downloads/point/last-month/session-orchestrator | jq .downloads (fetched only under --write)',
498
+ compute: (root, ctx) => fmtCount(ctx?.usage?.['npm-downloads-30d'] ?? null),
499
+ },
500
+ {
501
+ id: 'github-stars',
502
+ provenance: false,
503
+ snapshotFallback: true,
504
+ network: true,
505
+ url: 'https://api.github.com/repos/Kanevry/session-orchestrator',
506
+ field: 'stargazers_count',
507
+ // The API answers unauthenticated requests but rejects ones without a
508
+ // User-Agent, and the Accept header pins the response schema version.
509
+ headers: {
510
+ Accept: 'application/vnd.github+json',
511
+ 'User-Agent': 'session-orchestrator-site-numbers',
512
+ },
513
+ source:
514
+ 'curl -s https://api.github.com/repos/Kanevry/session-orchestrator | jq .stargazers_count (fetched only under --write)',
515
+ compute: (root, ctx) => fmtCount(ctx?.usage?.['github-stars'] ?? null),
516
+ },
461
517
  {
462
518
  id: 'counted-at',
463
519
  provenance: true,
@@ -606,7 +662,7 @@ export function writeCensusSnapshot(root, values) {
606
662
  * `fromSnapshot` lists the metrics answered by `site/_census.json` because
607
663
  * their live source was absent.
608
664
  */
609
- export function collect(root) {
665
+ export function collect(root, ctx = {}) {
610
666
  const values = {};
611
667
  const missing = [];
612
668
  const warnings = [];
@@ -619,8 +675,8 @@ export function collect(root) {
619
675
  // LIVE FIRST, always. The snapshot is a fallback, never a cache: reading it
620
676
  // first (or memoising a live value into it) would freeze the page on the
621
677
  // last release's numbers while the repository moved on.
622
- let v = def.compute(root);
623
- if ((v === null || v === undefined || v === '') && def.snapshotFallback === true) {
678
+ let v = def.compute(root, ctx);
679
+ if ((v === null || v === undefined || v === '') && def.snapshotFallback) {
624
680
  if (snapshot === undefined) snapshot = readCensusSnapshot(root);
625
681
  const s = snapshot?.[def.id];
626
682
  if (s !== undefined) {
@@ -632,9 +688,17 @@ export function collect(root) {
632
688
  else values[def.id] = String(v);
633
689
  }
634
690
 
635
- if (fromSnapshot.length > 0) {
691
+ // Only the REPOSITORY-sourced fallbacks are worth a warning. For the two
692
+ // network metrics the snapshot is not a degraded answer but the designed one
693
+ // between two `--write` runs — warning about them would fire on every single
694
+ // `--check`, and a warning that always fires is a broken instrument
695
+ // (`.claude/rules/host-resources.md` HR-101).
696
+ const localFallbacks = fromSnapshot.filter(
697
+ (id) => !METRIC_DEFS.find((d) => d.id === id)?.network,
698
+ );
699
+ if (localFallbacks.length > 0) {
636
700
  warnings.push(
637
- `${fromSnapshot.join(', ')} read from ${CENSUS_FILE.join('/')} — the live source is absent under ${root} ` +
701
+ `${localFallbacks.join(', ')} read from ${CENSUS_FILE.join('/')} — the live source is absent under ${root} ` +
638
702
  '(expected in a fresh clone / tarball build; the snapshot is only as current as the last --write)',
639
703
  );
640
704
  }
@@ -662,6 +726,215 @@ export function collect(root) {
662
726
  return { values, missing, warnings, fromSnapshot };
663
727
  }
664
728
 
729
+ // ---------------------------------------------------------------------------
730
+ // The two network metrics
731
+ // ---------------------------------------------------------------------------
732
+
733
+ /** Metrics whose source is an HTTP endpoint rather than this repository. */
734
+ export const NETWORK_METRIC_DEFS = Object.freeze(METRIC_DEFS.filter((d) => d.network));
735
+
736
+ /** Hard ceiling on a single usage fetch. Revisit if a source starts answering slower. */
737
+ export const FETCH_TIMEOUT_MS = 5000;
738
+
739
+ /**
740
+ * Fetch the usage metrics. CALLED ONLY UNDER `--write` — see the header for why
741
+ * `--check` must stay offline.
742
+ *
743
+ * Every failure mode (timeout, non-2xx, unparseable body, missing/non-numeric
744
+ * field) lands in the same place: a WARN naming the reason and the snapshot
745
+ * value that will be kept, and NO entry in the returned map. `collect()` then
746
+ * falls back to the snapshot exactly as it does for a fresh clone's ledgers —
747
+ * so a failed fetch degrades to "the last measured number", never to a
748
+ * placeholder written over a real one.
749
+ *
750
+ * @param {string} root repo root (its `site/_census.json` supplies the WARN's value)
751
+ * @param {{fetchImpl?: Function, timeoutMs?: number, warn?: (s:string)=>void}} [opts]
752
+ * @returns {Promise<Record<string, number>>} only the metrics that actually resolved
753
+ */
754
+ export async function fetchUsageMetrics(root, opts = {}) {
755
+ // Offline switch (tests, air-gapped CI): SO_SITE_NUMBERS_OFFLINE=1 skips every
756
+ // network call and lets the snapshot answer, exactly like a failed fetch would,
757
+ // minus the per-metric WARN noise. Measured 2026-09-07: two fixture tests that
758
+ // spawned --write went red on a GitHub 403 rate limit; a test must never depend
759
+ // on api.github.com being reachable.
760
+ if (process.env.SO_SITE_NUMBERS_OFFLINE === '1') {
761
+ (opts.warn ?? ((m) => process.stderr.write(`${m}\n`)))(
762
+ 'site-numbers: SO_SITE_NUMBERS_OFFLINE=1 — usage metrics answered from the snapshot',
763
+ );
764
+ return {};
765
+ }
766
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
767
+ const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
768
+ const warn = opts.warn ?? ((s) => process.stderr.write(`${s}\n`));
769
+ const out = {};
770
+ let snapshot;
771
+
772
+ for (const def of NETWORK_METRIC_DEFS) {
773
+ try {
774
+ const res = await doFetch(def.url, {
775
+ signal: AbortSignal.timeout(timeoutMs),
776
+ headers: def.headers ?? {},
777
+ });
778
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
779
+ const body = await res.json();
780
+ const v = body?.[def.field];
781
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
782
+ throw new Error(`no numeric "${def.field}" field in the response`);
783
+ }
784
+ out[def.id] = Math.trunc(v);
785
+ } catch (err) {
786
+ if (snapshot === undefined) snapshot = readCensusSnapshot(root);
787
+ const kept = snapshot?.[def.id];
788
+ warn(
789
+ `WARN site-numbers: ${def.id} fetch failed (${err?.message ?? String(err)}), keeping snapshot ` +
790
+ `${kept ?? '(none — this metric will have no value and the run will fail)'}`,
791
+ );
792
+ }
793
+ }
794
+ return out;
795
+ }
796
+
797
+ // ---------------------------------------------------------------------------
798
+ // The marker-bounded census line in site/llms-full.txt
799
+ // ---------------------------------------------------------------------------
800
+
801
+ /** Files the census line lives in, relative to the SITE directory. */
802
+ export const LLMS_FULL_FILE = 'llms-full.txt';
803
+ export const LLMS_FILE = 'llms.txt';
804
+
805
+ /**
806
+ * Every file carrying a marker-bounded census block.
807
+ *
808
+ * A CONSTANT LIST rather than a second code path per file: `site/llms.txt` used
809
+ * to hand-maintain its own "## Surface" line, and it drifted exactly as the page
810
+ * had — measured 2026-09-07 it claimed `661 vitest test files` while every other
811
+ * surface (and the repository) said 662, and `--check` exited 0 because the
812
+ * generator did not know that file existed. Adding a file to this array is the
813
+ * whole change needed to bring it under the guard.
814
+ */
815
+ export const CENSUS_BLOCK_FILES = Object.freeze([LLMS_FULL_FILE, LLMS_FILE]);
816
+
817
+ export const CENSUS_START = '<!-- census:start -->';
818
+ export const CENSUS_END = '<!-- census:end -->';
819
+
820
+ /**
821
+ * The metric ids the census line carries, with the label each one gets.
822
+ *
823
+ * A SUBSET of METRIC_IDS on purpose: the line is prose for a reader, not the
824
+ * machine receipt (`site/_census.json` is that, and carries all of them).
825
+ * `version`, `counted-at` and `counted-sha` are not in this list because they
826
+ * are rendered as the line's opening clause, not as `label value` pairs.
827
+ */
828
+ export const CENSUS_LINE_FIELDS = Object.freeze([
829
+ { id: 'skills', label: 'skills' },
830
+ { id: 'commands', label: 'commands' },
831
+ { id: 'agents', label: 'agents' },
832
+ { id: 'hooks', label: 'hooks' },
833
+ { id: 'tests', label: 'test files' },
834
+ { id: 'sessions', label: 'sessions' },
835
+ { id: 'learnings', label: 'learnings' },
836
+ { id: 'npm-downloads-30d', label: 'npm downloads (30d)' },
837
+ { id: 'github-stars', label: 'GitHub stars' },
838
+ ]);
839
+
840
+ /**
841
+ * Render the census line.
842
+ *
843
+ * The `Version X.Y.Z` opening is load-bearing beyond this file: `SURFACES` in
844
+ * `scripts/release.mjs` matches `/Version\s+(\d+\.\d+\.\d+)/g` against
845
+ * `site/llms-full.txt`, so the literal must keep exactly this shape or the
846
+ * release drift check stops seeing the surface it guards.
847
+ *
848
+ * @returns {{line: string, head: string, tail: string}}
849
+ * `head` is the provenance clause (warn-only, like the spans), `tail` the
850
+ * counts (real drift). Split so `--check` can tell the two apart.
851
+ */
852
+ export function renderCensusLine(values) {
853
+ const head = `Version ${values.version} · counted ${values['counted-at']} at ${values['counted-sha']}`;
854
+ const tail = CENSUS_LINE_FIELDS.map((f) => `${f.label} ${values[f.id]}`).join(' · ');
855
+ return { line: `${head} · ${tail}`, head, tail };
856
+ }
857
+
858
+ /**
859
+ * Replace the text between the two markers, and nothing else.
860
+ *
861
+ * A start marker without an end marker is a HARD ERROR rather than an append:
862
+ * guessing where the block ends would let one bad edit swallow the rest of a
863
+ * hand-authored file.
864
+ *
865
+ * @returns {{ok: true, text: string, changed: boolean, current: string}
866
+ * | {ok: false, reason: string}}
867
+ */
868
+ export function rewriteCensusBlock(text, line) {
869
+ const start = text.indexOf(CENSUS_START);
870
+ const end = text.indexOf(CENSUS_END);
871
+ if (start === -1 && end === -1) return { ok: false, reason: 'no census markers' };
872
+ if (start === -1) return { ok: false, reason: `"${CENSUS_END}" without "${CENSUS_START}"` };
873
+ if (end === -1) return { ok: false, reason: `"${CENSUS_START}" without "${CENSUS_END}"` };
874
+ if (end < start) return { ok: false, reason: 'census markers are in the wrong order' };
875
+
876
+ const inner = text.slice(start + CENSUS_START.length, end);
877
+ const current = inner.trim();
878
+ const next = `${text.slice(0, start + CENSUS_START.length)}\n${line}\n${text.slice(end)}`;
879
+ return { ok: true, text: next, changed: next !== text, current };
880
+ }
881
+
882
+ /**
883
+ * Judge (and optionally rewrite) the census block of ONE file under `siteDir`.
884
+ *
885
+ * Mirrors the span policy: a difference only in the provenance clause is
886
+ * `stale` (warn-only — a claim about the past does not become false when HEAD
887
+ * moves), a difference in the counts is `drift`.
888
+ *
889
+ * @param {{write?:boolean, file?:string}} [opts] `file` is relative to `siteDir`
890
+ * and defaults to `llms-full.txt`; `syncCensusBlocks` iterates CENSUS_BLOCK_FILES.
891
+ * @returns {{present:boolean, error?:string, drift?:boolean, stale?:boolean,
892
+ * written?:boolean, file:string}}
893
+ */
894
+ export function syncCensusBlock(siteDir, values, { write = false, file = LLMS_FULL_FILE } = {}) {
895
+ const abs = join(siteDir, file);
896
+ // Absent file: not this generator's business to create one. Every HTML
897
+ // fixture directory in the test suite is such a case.
898
+ if (!existsSync(abs) || !statSync(abs).isFile()) return { present: false, file: abs };
899
+
900
+ const { line, tail } = renderCensusLine(values);
901
+ const text = readFileSync(abs, 'utf8');
902
+ const res = rewriteCensusBlock(text, line);
903
+ if (!res.ok) return { present: true, file: abs, error: res.reason };
904
+
905
+ // Same split as the spans: `counted <date> at <sha>` lagging is a claim about
906
+ // the past, not a wrong number.
907
+ const provenanceOnly =
908
+ res.current !== line &&
909
+ res.current.startsWith(`Version ${values.version} · `) &&
910
+ res.current.endsWith(tail);
911
+ const out = {
912
+ present: true,
913
+ file: abs,
914
+ drift: res.current !== line && !provenanceOnly,
915
+ stale: provenanceOnly,
916
+ written: false,
917
+ };
918
+ if (write && res.changed) {
919
+ writeFileSync(abs, res.text, 'utf8');
920
+ out.written = true;
921
+ }
922
+ return out;
923
+ }
924
+
925
+ /**
926
+ * Judge (and optionally rewrite) EVERY file in `CENSUS_BLOCK_FILES`.
927
+ *
928
+ * One measurement, N surfaces — the same reason `writeCensusSnapshot` takes an
929
+ * already-computed `values` map: two census runs inside one invocation can
930
+ * disagree across midnight or a concurrent ledger append.
931
+ *
932
+ * @returns {Array<ReturnType<typeof syncCensusBlock>>} in CENSUS_BLOCK_FILES order
933
+ */
934
+ export function syncCensusBlocks(siteDir, values, { write = false } = {}) {
935
+ return CENSUS_BLOCK_FILES.map((file) => syncCensusBlock(siteDir, values, { write, file }));
936
+ }
937
+
665
938
  // ---------------------------------------------------------------------------
666
939
  // Markup
667
940
  // ---------------------------------------------------------------------------
@@ -881,7 +1154,7 @@ export function main(argv = process.argv.slice(2), env = {}) {
881
1154
  return 2;
882
1155
  }
883
1156
 
884
- const { values, missing, warnings, fromSnapshot } = collect(root);
1157
+ const { values, missing, warnings, fromSnapshot } = collect(root, { usage: env.usage });
885
1158
  if (missing.length > 0) {
886
1159
  stderr(
887
1160
  `Error: could not measure ${missing.join(', ')} under ${root} — ` +
@@ -961,6 +1234,38 @@ export function main(argv = process.argv.slice(2), env = {}) {
961
1234
  });
962
1235
  }
963
1236
 
1237
+ // The marker-bounded census line in site/llms-full.txt. Judged with the same
1238
+ // split as the spans (counts = drift, provenance = warn-only) and rewritten
1239
+ // BETWEEN the markers only.
1240
+ const censusDriftLines = [];
1241
+ const censusBlocks = syncCensusBlocks(siteDir, values, { write: args.write });
1242
+ for (const b of censusBlocks) {
1243
+ if (b.error) {
1244
+ stderr(
1245
+ `Error: ${b.file}: ${b.error} — the census block contract is broken ` +
1246
+ `(expected "${CENSUS_START}" … "${CENSUS_END}")`,
1247
+ );
1248
+ contractTotal += 1;
1249
+ } else if (b.present) {
1250
+ if (b.drift && !args.write) {
1251
+ // Held back rather than printed here: under --json, stdout carries the
1252
+ // envelope and NOTHING else (`cli-design.md` — data on stdout). Printing
1253
+ // it inline put a bare DRIFT line in front of the JSON and made the
1254
+ // envelope unparseable, which is the same class of silent failure as an
1255
+ // unjudged surface: a consumer sees a parse error, not a drift report.
1256
+ censusDriftLines.push(`DRIFT ${b.file}: the census block does not match this measurement`);
1257
+ driftTotal += 1;
1258
+ } else if (b.drift) {
1259
+ driftTotal += 1;
1260
+ } else if (b.stale) {
1261
+ stderr(`stale ${b.file}: only the counted date/sha of the census block lags`);
1262
+ }
1263
+ }
1264
+ }
1265
+ // Kept as its own name in the --json envelope for the consumers that already
1266
+ // read `llmsFull`; `censusBlocks` below carries all of them.
1267
+ const llms = censusBlocks[CENSUS_BLOCK_FILES.indexOf(LLMS_FULL_FILE)];
1268
+
964
1269
  // The named silent-failure class: a generator that matches nothing, changes
965
1270
  // nothing, and reports success. Zero spans means the markup contract is not in
966
1271
  // the page — that is a defect, in BOTH modes, never a no-op.
@@ -1040,6 +1345,23 @@ export function main(argv = process.argv.slice(2), env = {}) {
1040
1345
  written: writtenTotal,
1041
1346
  rejected: rejectedTotal,
1042
1347
  fromSnapshot,
1348
+ llmsFull: {
1349
+ file: llms.present ? relative(root, llms.file) : null,
1350
+ present: llms.present,
1351
+ error: llms.error ?? null,
1352
+ drift: llms.drift === true,
1353
+ stale: llms.stale === true,
1354
+ written: llms.written === true,
1355
+ },
1356
+ censusBlocks: censusBlocks.map((b, i) => ({
1357
+ name: CENSUS_BLOCK_FILES[i],
1358
+ file: b.present ? relative(root, b.file) : null,
1359
+ present: b.present,
1360
+ error: b.error ?? null,
1361
+ drift: b.drift === true,
1362
+ stale: b.stale === true,
1363
+ written: b.written === true,
1364
+ })),
1043
1365
  censusWritten,
1044
1366
  ok,
1045
1367
  },
@@ -1051,6 +1373,9 @@ export function main(argv = process.argv.slice(2), env = {}) {
1051
1373
  stdout(
1052
1374
  `site-numbers: wrote ${writtenTotal} value(s) across ${files.length} file(s) in ${siteDir}` +
1053
1375
  ` (${spanTotal} metric cells @ ${values['counted-sha']}${dirty ? '+dirty' : ''})` +
1376
+ censusBlocks
1377
+ .map((b, i) => (b.written ? ` + ${CENSUS_BLOCK_FILES[i]}` : ''))
1378
+ .join('') +
1054
1379
  (censusWritten ? ` + ${CENSUS_FILE.join('/')}` : ''),
1055
1380
  );
1056
1381
  } else {
@@ -1062,6 +1387,7 @@ export function main(argv = process.argv.slice(2), env = {}) {
1062
1387
  stderr(`stale ${f.file}:${s.line} ${s.metric}: "${s.actual}" → would become "${s.expected}" on --write`);
1063
1388
  }
1064
1389
  }
1390
+ for (const l of censusDriftLines) stdout(l);
1065
1391
  stdout(
1066
1392
  driftTotal === 0 && !noSpans && contractTotal === 0
1067
1393
  ? `site-numbers: ${spanTotal} metric cell(s) current across ${files.length} file(s)`
@@ -1078,4 +1404,14 @@ const isMain =
1078
1404
  process.argv[1] !== undefined &&
1079
1405
  resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
1080
1406
 
1081
- if (isMain) process.exit(main());
1407
+ if (isMain) {
1408
+ const argv = process.argv.slice(2);
1409
+ const parsed = parseArgs(argv);
1410
+ // The ONLY network call in this file, and only on the write path — see the
1411
+ // header. A `--check` (the CI/build guard) never reaches this branch.
1412
+ const usage =
1413
+ !parsed.error && parsed.write && !parsed.help && !parsed.version
1414
+ ? await fetchUsageMetrics(resolve(parsed.root ?? process.cwd()))
1415
+ : {};
1416
+ process.exit(main(argv, { usage }));
1417
+ }