session-orchestrator 5.12.0 → 5.13.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 (385) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +162 -4
  11. package/README.md +126 -127
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +1 -1
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +90 -17
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +49 -15
  47. package/docs/session-config-template.md +30 -22
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +139 -49
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -2
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +39 -27
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/section-extractor.mjs +65 -11
  143. package/scripts/lib/config/template-keys.mjs +153 -0
  144. package/scripts/lib/config/vault-integration.mjs +1 -0
  145. package/scripts/lib/config.mjs +2 -2
  146. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  147. package/scripts/lib/discovery/triage-state.mjs +2 -1
  148. package/scripts/lib/dispatcher/rank.mjs +5 -7
  149. package/scripts/lib/ecosystem-health.mjs +2 -2
  150. package/scripts/lib/eval/engine.mjs +3 -2
  151. package/scripts/lib/eval/session-resolve.mjs +6 -2
  152. package/scripts/lib/eval/sink.mjs +4 -3
  153. package/scripts/lib/events-retention-banner.mjs +3 -2
  154. package/scripts/lib/events-schema.mjs +10 -0
  155. package/scripts/lib/events.mjs +23 -12
  156. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  157. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  158. package/scripts/lib/fleet-protocol.mjs +4 -0
  159. package/scripts/lib/gates/gate-full.mjs +172 -11
  160. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  161. package/scripts/lib/git-config-drift.mjs +141 -7
  162. package/scripts/lib/github-export.mjs +447 -0
  163. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  164. package/scripts/lib/handover-queue-marks.mjs +159 -0
  165. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  166. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  167. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  168. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  169. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  170. package/scripts/lib/host-identity.mjs +9 -5
  171. package/scripts/lib/http-request.mjs +64 -0
  172. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  173. package/scripts/lib/io.mjs +6 -3
  174. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  175. package/scripts/lib/issue-budget.mjs +36 -7
  176. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  177. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  178. package/scripts/lib/learnings/io.mjs +289 -15
  179. package/scripts/lib/learnings/select.mjs +53 -4
  180. package/scripts/lib/learnings/store-role.mjs +209 -0
  181. package/scripts/lib/learnings/surface.mjs +46 -7
  182. package/scripts/lib/ledger-path.mjs +113 -0
  183. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  184. package/scripts/lib/memory-banner.mjs +5 -6
  185. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  186. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  187. package/scripts/lib/memory-proposals/store.mjs +22 -5
  188. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  189. package/scripts/lib/no-follow-open.mjs +57 -0
  190. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  191. package/scripts/lib/own-lock-root.mjs +95 -0
  192. package/scripts/lib/owner-interview.mjs +2 -1
  193. package/scripts/lib/owner-yaml.mjs +70 -7
  194. package/scripts/lib/platform.mjs +161 -9
  195. package/scripts/lib/plugin-update-banner.mjs +15 -11
  196. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  197. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  198. package/scripts/lib/process-group.mjs +270 -34
  199. package/scripts/lib/project-hygiene.mjs +28 -0
  200. package/scripts/lib/quality-gate.mjs +33 -9
  201. package/scripts/lib/reconcile/backlog.mjs +29 -15
  202. package/scripts/lib/reconcile/engine.mjs +26 -2
  203. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  204. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  205. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  206. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  207. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  208. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  209. package/scripts/lib/rule-loader.mjs +20 -10
  210. package/scripts/lib/scope-baseline.mjs +123 -8
  211. package/scripts/lib/scope-echo.mjs +5 -2
  212. package/scripts/lib/scope-gate.mjs +55 -20
  213. package/scripts/lib/session-close-backfill.mjs +75 -7
  214. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  215. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  216. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  217. package/scripts/lib/session-id.mjs +9 -1
  218. package/scripts/lib/session-identity/own-session.mjs +63 -0
  219. package/scripts/lib/session-lock.mjs +114 -0
  220. package/scripts/lib/session-record-repair.mjs +245 -35
  221. package/scripts/lib/session-registry.mjs +72 -9
  222. package/scripts/lib/session-schema/constants.mjs +9 -0
  223. package/scripts/lib/session-schema/filters.mjs +56 -1
  224. package/scripts/lib/session-schema/validator.mjs +15 -5
  225. package/scripts/lib/session-start-probes.mjs +149 -22
  226. package/scripts/lib/session-token-rollup.mjs +2 -1
  227. package/scripts/lib/session-work-trees.mjs +166 -0
  228. package/scripts/lib/sessions-canonical.mjs +61 -3
  229. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  230. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  231. package/scripts/lib/sh-quote.mjs +1 -0
  232. package/scripts/lib/skill-evidence-window.mjs +5 -1
  233. package/scripts/lib/skill-health/join.mjs +10 -9
  234. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  235. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  236. package/scripts/lib/snapshot-probe.mjs +414 -0
  237. package/scripts/lib/so-hud/collect.mjs +49 -5
  238. package/scripts/lib/so-hud/runtime.mjs +15 -4
  239. package/scripts/lib/spiral-carryover.mjs +8 -1
  240. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  241. package/scripts/lib/state-md/idle-reset.mjs +14 -5
  242. package/scripts/lib/state-md/mission-status.mjs +5 -1
  243. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  244. package/scripts/lib/state-md.mjs +31 -8
  245. package/scripts/lib/sunset/walker.mjs +4 -3
  246. package/scripts/lib/telemetry/consent.mjs +4 -2
  247. package/scripts/lib/telemetry/queue.mjs +3 -3
  248. package/scripts/lib/telemetry/sync.mjs +45 -9
  249. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  250. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  251. package/scripts/lib/ux-grill/paths.mjs +5 -1
  252. package/scripts/lib/validate/check-agents.mjs +37 -6
  253. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  254. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  255. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  256. package/scripts/lib/validate/check-json-files.mjs +3 -1
  257. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  258. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  259. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  260. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  261. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  262. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  263. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  264. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  265. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  266. package/scripts/lib/validate/confidential-names.mjs +3 -1
  267. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  268. package/scripts/lib/vault-mirror/namespace.mjs +19 -7
  269. package/scripts/lib/vault-mirror/process.mjs +56 -27
  270. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  271. package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
  272. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  273. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  274. package/scripts/lib/vault-mirror/utils.mjs +79 -0
  275. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  276. package/scripts/lib/vault-status/board-writer.mjs +2 -1
  277. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  278. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  279. package/scripts/lib/win-process-tree.mjs +137 -0
  280. package/scripts/lib/workspace.mjs +1 -1
  281. package/scripts/lib/worktree/constants.mjs +1 -1
  282. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  283. package/scripts/lib/worktree/listing.mjs +1 -1
  284. package/scripts/lib/zx.mjs +50 -0
  285. package/scripts/mcp-server.mjs +16 -7
  286. package/scripts/mcp-server.sh +13 -83
  287. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  288. package/scripts/memory-propose.mjs +10 -3
  289. package/scripts/migrate-cold-start-seed.mjs +7 -0
  290. package/scripts/migrate-legacy-learnings.sh +22 -0
  291. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  292. package/scripts/migrate-vault-paths.mjs +29 -13
  293. package/scripts/parse-config.mjs +71 -14
  294. package/scripts/print-learnings-index.mjs +25 -2
  295. package/scripts/release-session-lock.mjs +94 -7
  296. package/scripts/release.mjs +451 -24
  297. package/scripts/relocate-vault-corpus.mjs +11 -7
  298. package/scripts/repair-invalid-sessions.mjs +68 -3
  299. package/scripts/resolve-session-invocation.mjs +10 -17
  300. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  301. package/scripts/run-quality-gate.mjs +33 -7
  302. package/scripts/self-update.mjs +56 -7
  303. package/scripts/session-commit-files.mjs +133 -0
  304. package/scripts/site-numbers.mjs +52 -9
  305. package/scripts/snapshot-probe.mjs +156 -0
  306. package/scripts/so-hud.mjs +29 -6
  307. package/scripts/state-md-idle-reset.mjs +2 -1
  308. package/scripts/sweep-expired-learnings.mjs +54 -6
  309. package/scripts/sweep-expired-rules.mjs +4 -1
  310. package/scripts/validate-plugin.mjs +22 -0
  311. package/scripts/vault-integration-watcher.mjs +54 -6
  312. package/scripts/vault-mirror.mjs +9 -0
  313. package/skills/_shared/fleet-protocol.md +142 -1
  314. package/skills/_shared/instruction-file-resolution.md +1 -1
  315. package/skills/_shared/monitor-patterns.md +44 -25
  316. package/skills/_shared/platform-tools.md +7 -0
  317. package/skills/_shared/state-ownership.md +3 -2
  318. package/skills/architecture/references/domain-model.md +1 -1
  319. package/skills/bootstrap/SKILL.md +3 -1
  320. package/skills/bootstrap/_shared-template.md +4 -4
  321. package/skills/bootstrap/fast-template.md +1 -1
  322. package/skills/bootstrap/private-contract.md +1 -1
  323. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  324. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  325. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  326. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  327. package/skills/claude-md-drift-check/SKILL.md +3 -3
  328. package/skills/claude-md-drift-check/checker.mjs +61 -101
  329. package/skills/close/SKILL.md +20 -7
  330. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  331. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  332. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  333. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  334. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  335. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  336. package/skills/discovery/probes-session.md +3 -0
  337. package/skills/discovery/probes-vault.md +2 -0
  338. package/skills/dispatcher/SKILL.md +1 -1
  339. package/skills/eval/SKILL.md +3 -1
  340. package/skills/evolve/SKILL.md +2 -0
  341. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  342. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  343. package/skills/gitlab-ops/SKILL.md +7 -6
  344. package/skills/gitlab-portfolio/SKILL.md +2 -2
  345. package/skills/go/SKILL.md +13 -1
  346. package/skills/npm-publish/SKILL.md +5 -2
  347. package/skills/plan/SKILL.md +23 -10
  348. package/skills/plan/mode-feature.md +13 -8
  349. package/skills/plan/mode-new.md +6 -14
  350. package/skills/plan/prd-feature-template.md +19 -0
  351. package/skills/plan/prd-full-template.md +12 -5
  352. package/skills/plan/prd-premortem-panel.md +48 -0
  353. package/skills/plan/prd-reviewer-prompt.md +11 -3
  354. package/skills/plan/soul.md +1 -1
  355. package/skills/portfolio/SKILL.md +5 -6
  356. package/skills/quality-gates/SKILL.md +2 -4
  357. package/skills/reconcile/SKILL.md +27 -4
  358. package/skills/release/SKILL.md +16 -8
  359. package/skills/session-end/SKILL.md +94 -11
  360. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  361. package/skills/session-end/phase-3-6-tail.md +11 -7
  362. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  363. package/skills/session-end/plan-verification.md +16 -1
  364. package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
  365. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  366. package/skills/session-end/references/session-summary-template.md +4 -0
  367. package/skills/session-end/session-metrics-write.md +7 -4
  368. package/skills/session-plan/SKILL.md +5 -3
  369. package/skills/session-start/SKILL.md +9 -3
  370. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  371. package/skills/session-start/phase-8-5-express-path.md +4 -3
  372. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  373. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
  374. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  375. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  376. package/skills/tmux-layout/SKILL.md +2 -2
  377. package/skills/vault-sync/validator.mjs +3 -2
  378. package/skills/wave-executor/SKILL.md +5 -4
  379. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  380. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  381. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  382. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  383. package/assets/og-card.png +0 -0
  384. package/assets/og-card.svg +0 -47
  385. package/assets/wave-lifecycle.svg +0 -98
@@ -339,7 +339,8 @@ async function pushRecord(record, opts) {
339
339
  if (!acquired.ok) return acquired;
340
340
 
341
341
  try {
342
- const map = readCurrentMap(currentFile);
342
+ // Retention trim (#1633) — the reader drops the same entries, see LIFECYCLE_RETENTION_MS.
343
+ const map = withoutExpired(readCurrentMap(currentFile), typeof opts?.now === 'number' ? opts.now : Date.now());
343
344
  map[record.agentId] = record;
344
345
  const written = writeJsonAtomicSync(currentFile, map, { tmpPrefix: '.agent-status-current' });
345
346
  if (!written.ok) return { ok: false, reason: 'fs-error', error: written.error };
@@ -415,6 +416,113 @@ export async function setProgress(agentId, progress = {}, opts = {}) {
415
416
  return pushRecord(record, opts);
416
417
  }
417
418
 
419
+ // Lifecycle entries (#1580) carry a fresh harness agent_id per subagent, so —
420
+ // unlike the wave-executor's reused ids — the cache grew by one ~200-byte entry
421
+ // per subagent, and the lock wait of a write grew with it (#1633: 18 parallel
422
+ // writers, 10,000 entries → p50 468 ms / max 910 ms against a 1 s timeout).
423
+ // Pruning the cache ALONE is wrong: readCurrentStatus() folds cache ∪ ledger
424
+ // tail, and an id present only in the ledger tail flips the whole read to
425
+ // `rebuilt-log` (cache-behind-ledger). So the retention bound below is applied
426
+ // on BOTH sides with one predicate: every write drops expired lifecycle entries
427
+ // from the cache, and every read drops them from the cache view AND the ledger
428
+ // view before the fold. Expiry is monotone in time (expired at write time ⇒
429
+ // expired at any later read), so a trimmed cache reads exactly like the
430
+ // untrimmed one. The ledger itself is never rewritten.
431
+ // CEILING: 14 days of subagents stay in the cache (#1633 measured 725 starts in
432
+ // 31 days here ≈ 330 entries per 14 days); a repo with no subagent for 14 days
433
+ // reads as "no lifecycle record" (HUD "?") until the next start.
434
+ // REVISIT-TRIGGER: a session that legitimately lives longer than 14 days, or a
435
+ // repo starting more than ~5,000 subagents per 14 days (the cache is back in the
436
+ // measured slow range) — then shorten the window or trim per session.
437
+ const LIFECYCLE_STATES = new Set(['running', 'done']);
438
+ export const LIFECYCLE_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
439
+
440
+ // A `running` lifecycle entry without a SubagentStop (#1633: 2 of 725 agents
441
+ // since 2026-09-15, 0.28 %) would otherwise count as running forever. Bound
442
+ // measured 2026-10-16 on this repo's `.orchestrator/metrics/subagents.jsonl`
443
+ // (start→stop wall clock per agent_id): since 2026-09-15 n=810, p50 5.7 min,
444
+ // p99 47 min, max 715.5 min; all time n=3017, p99 55.1 min, max 1042.9 min
445
+ // (17.4 h — a batch of agents stopped together at a session resume; the hook's
446
+ // own duration_ms max is 132 min). 24 h sits above every observed pair.
447
+ // CEILING: a subagent genuinely running longer than 24 h is reported as decayed,
448
+ // not running. REVISIT-TRIGGER: any start→stop pair above ~20 h in subagents.jsonl.
449
+ export const LIFECYCLE_DECAY_MS = 24 * 60 * 60 * 1000;
450
+
451
+ /**
452
+ * Whether a record is a lifecycle entry older than the retention window
453
+ * (`LIFECYCLE_RETENTION_MS`). Records without a parsable `ts` never expire.
454
+ * @param {object} rec
455
+ * @param {number} now Epoch millis.
456
+ * @returns {boolean}
457
+ */
458
+ function isLifecycleExpired(rec, now) {
459
+ const ms = rec?.kind === 'lifecycle' ? recordTsMs(rec) : null;
460
+ return ms !== null && now - ms > LIFECYCLE_RETENTION_MS;
461
+ }
462
+
463
+ /**
464
+ * Whether a lifecycle entry still says `running` but is older than
465
+ * `LIFECYCLE_DECAY_MS` — its SubagentStop most likely never arrived (#1633).
466
+ * Such an agent is neither running nor done: report it as decayed.
467
+ * @param {object} rec
468
+ * @param {number} now Epoch millis.
469
+ * @returns {boolean}
470
+ */
471
+ export function isLifecycleDecayed(rec, now) {
472
+ if (rec?.kind !== 'lifecycle' || rec.status !== 'running') return false;
473
+ const ms = recordTsMs(rec);
474
+ return ms !== null && now - ms > LIFECYCLE_DECAY_MS;
475
+ }
476
+
477
+ /**
478
+ * Copy of a map without its expired lifecycle entries (null-prototype).
479
+ * @param {Record<string, object>} entries
480
+ * @param {number} now
481
+ * @returns {Record<string, object>}
482
+ */
483
+ function withoutExpired(entries, now) {
484
+ const out = Object.create(null);
485
+ for (const [id, rec] of Object.entries(entries)) {
486
+ if (!isLifecycleExpired(rec, now)) out[id] = rec;
487
+ }
488
+ return out;
489
+ }
490
+
491
+ /**
492
+ * Record a subagent lifecycle transition from the SubagentStart/SubagentStop
493
+ * hooks (#1580): `running` on start, `done` on stop, bound to the parent
494
+ * session so the HUD counts only its own session's agents. No-throw,
495
+ * best-effort — same result contract as `setStatus`.
496
+ *
497
+ * @param {string} agentId Harness agent id (LWW map key).
498
+ * @param {{ status: 'running'|'done', sessionId: string, agentType?: string|null }} lifecycle
499
+ * @param {{ repoRoot?: string, timeoutMs?: number, now?: number }} [opts]
500
+ * @returns {Promise<{ ok: true } | { ok: false, reason: 'invalid-input'|'timeout'|'fs-error', error?: string }>}
501
+ */
502
+ export async function setLifecycle(agentId, lifecycle = {}, opts = {}) {
503
+ if (typeof agentId !== 'string' || agentId.trim().length === 0 || UNSAFE_MAP_KEYS.has(agentId)) {
504
+ return { ok: false, reason: 'invalid-input', error: 'agentId must be a non-empty, safe string' };
505
+ }
506
+ const { status, sessionId, agentType } = lifecycle ?? {};
507
+ if (!LIFECYCLE_STATES.has(status)) {
508
+ return { ok: false, reason: 'invalid-input', error: 'status must be running or done' };
509
+ }
510
+ if (typeof sessionId !== 'string' || sessionId.trim().length === 0) {
511
+ return { ok: false, reason: 'invalid-input', error: 'sessionId must be a non-empty string' };
512
+ }
513
+ const now = typeof opts?.now === 'number' ? opts.now : Date.now();
514
+ const record = {
515
+ agentId,
516
+ kind: 'lifecycle',
517
+ status,
518
+ session_id: sessionId,
519
+ ts: new Date(now).toISOString(),
520
+ };
521
+ if (typeof agentType === 'string' && agentType.trim().length > 0) record.agent_type = truncate(agentType);
522
+
523
+ return pushRecord(record, opts);
524
+ }
525
+
418
526
  /**
419
527
  * Rebuild the current-status map from a bounded TAIL of the append-only ledger
420
528
  * (#1342). The ledger is the source of truth; this is how a consumer reads it
@@ -599,14 +707,24 @@ export function rebuildCurrentFromLedger(opts = {}) {
599
707
  * per-agentId, a `rebuilt-log` view can hold BOTH kinds. A consumer must treat
600
708
  * `binding` as optional — its ABSENCE means "from the cache", never "bound".
601
709
  *
602
- * @param {{ repoRoot?: string, maxBytes?: number }} [opts]
710
+ * Retention (#1633): lifecycle entries older than `LIFECYCLE_RETENTION_MS`
711
+ * (relative to `opts.now`, default `Date.now()`) are absent from the view.
712
+ *
713
+ * @param {{ repoRoot?: string, maxBytes?: number, now?: number }} [opts]
603
714
  * @returns {{ entries: Record<string, object>, source: 'live-map'|'rebuilt-log'|'stale-cache'|'absent', at: string|null, degraded?: object }}
604
715
  */
605
716
  export function readCurrentStatus(opts = {}) {
606
- const cache = readCurrentMapDetailed(currentPathFor(opts?.repoRoot));
717
+ const now = typeof opts?.now === 'number' ? opts.now : Date.now();
718
+ const cacheRead = readCurrentMapDetailed(currentPathFor(opts?.repoRoot));
607
719
  const rebuilt = rebuildCurrentFromLedger(opts);
720
+ // Both views lose their expired lifecycle entries BEFORE the fold (#1633), so a
721
+ // cache trimmed at write time reads exactly like the untrimmed one.
722
+ const cache = { ...cacheRead, entries: withoutExpired(cacheRead.entries, now) };
723
+ const ledgerEntries = withoutExpired(rebuilt.entries, now);
608
724
 
609
725
  const cacheNewest = newestTsMs(cache.entries);
726
+ // Provenance checks read the UNTRIMMED ledger: trimming can only lower the
727
+ // cache's newest stamp, so comparing against the full ledger stays exact.
610
728
  const ledgerNewest = rebuilt.at === null ? null : Date.parse(rebuilt.at);
611
729
  const ledgerUnusable =
612
730
  rebuilt.degraded !== null &&
@@ -626,7 +744,7 @@ export function readCurrentStatus(opts = {}) {
626
744
  let at;
627
745
 
628
746
  const cacheIds = Object.keys(cache.entries);
629
- const ledgerIds = Object.keys(rebuilt.entries);
747
+ const ledgerIds = Object.keys(ledgerEntries);
630
748
 
631
749
  if (ledgerUnusable && cacheIds.length === 0) {
632
750
  // Nothing on disk at all — no ledger to verify against AND no cached entry
@@ -648,8 +766,8 @@ export function readCurrentStatus(opts = {}) {
648
766
  const cached = Object.prototype.hasOwnProperty.call(cache.entries, id)
649
767
  ? cache.entries[id]
650
768
  : undefined;
651
- const logged = Object.prototype.hasOwnProperty.call(rebuilt.entries, id)
652
- ? rebuilt.entries[id]
769
+ const logged = Object.prototype.hasOwnProperty.call(ledgerEntries, id)
770
+ ? ledgerEntries[id]
653
771
  : undefined;
654
772
 
655
773
  if (cached === undefined) {
@@ -682,7 +800,7 @@ export function readCurrentStatus(opts = {}) {
682
800
  }
683
801
 
684
802
  at = newestTsMs(entries);
685
- if (tookFromLedger || (!cache.ok && ledgerIds.length > 0)) {
803
+ if (tookFromLedger || (!cache.ok && Object.keys(rebuilt.entries).length > 0)) {
686
804
  source = 'rebuilt-log';
687
805
  if (cache.reason !== 'ok') reasons.unshift(cache.reason);
688
806
  else reasons.unshift('cache-behind-ledger');
@@ -0,0 +1,394 @@
1
+ /**
2
+ * agent-usage-census.mjs — which agent types ran, on which model, at what cost
3
+ * shape (#1617). Read-only, stdlib only.
4
+ *
5
+ * ## Why the transcripts and not the ledger
6
+ *
7
+ * `.orchestrator/metrics/subagents.jsonl` sees only dispatches from sessions in
8
+ * THIS repo, carries no observed model on most records and no effort at all, and
9
+ * ~87% of its stop records are the #939 phantom class with no type. The fleet
10
+ * dispatches from 20+ repos. The Claude Code transcripts are the source that
11
+ * holds all of it:
12
+ *
13
+ * <root>/<project>/<session>/subagents/**\/agent-<id>.meta.json → agentType
14
+ * <root>/<project>/<session>/subagents/**\/agent-<id>.jsonl → message.model + usage
15
+ *
16
+ * (`**` because Workflow agents nest one level deeper, under
17
+ * `subagents/workflows/<wf-id>/`.)
18
+ *
19
+ * ## Definitions (the numbers are only comparable if these hold)
20
+ *
21
+ * run one `agent-<id>.jsonl` whose FIRST turn falls inside the window.
22
+ * Window membership by first turn, not by file mtime, so a run is
23
+ * never split across two weekly censuses.
24
+ * turn one API response. Streaming writes several assistant lines per
25
+ * response; they are collapsed on `requestId`, else `message.id`, last
26
+ * usage block wins — the same recipe as hooks/subagent-telemetry.mjs
27
+ * extractTranscriptUsage() (#950/#963). A line carrying neither key is
28
+ * its own turn. The turn's timestamp is its FIRST line (request time).
29
+ * Only lines with a `message.model` count as turns. The raw line
30
+ * count is reported beside it as `assistant_lines` /
31
+ * `models_by_lines` — the unit the #1617 discovery measured.
32
+ * rebuild a cache re-creation after an idle gap: the gap to the previous turn
33
+ * exceeds GAP_SECONDS AND this turn's cache_creation exceeds
34
+ * REBUILD_MIN_CREATION AND its cache_read is below its cache_creation.
35
+ * start input + cache_creation + cache_read of the run's first turn.
36
+ *
37
+ * ## Unreadable lines
38
+ *
39
+ * Every line is parsed; a line that does not parse is skipped AND counted in
40
+ * `malformed_lines` (measurement-discipline.md § Unlesbare JSONL-Zeilen). A
41
+ * meta.json that is missing or unreadable puts the run under `(no-meta)` /
42
+ * `(unreadable-meta)` rather than dropping it.
43
+ *
44
+ * Memory: transcripts are streamed line by line (readline). Per file only the
45
+ * turn map of that one run is held, so a multi-GB window stays bounded by the
46
+ * largest single run.
47
+ */
48
+
49
+ import fs from 'node:fs';
50
+ import path from 'node:path';
51
+ import readline from 'node:readline';
52
+
53
+ export const SCHEMA = 'agent-usage-census/1';
54
+ export const GAP_SECONDS = 300;
55
+ export const REBUILD_MIN_CREATION = 20_000;
56
+ export const DEFAULT_DAYS = 14;
57
+
58
+ const AGENT_FILE_RE = /^agent-[A-Za-z0-9_-]+\.jsonl$/;
59
+
60
+ const clamp = (v) => (Number.isInteger(v) && v >= 0 ? v : 0);
61
+
62
+ /** Median of a numeric array; null for an empty one. */
63
+ export function median(values) {
64
+ if (values.length === 0) return null;
65
+ const s = [...values].sort((a, b) => a - b);
66
+ const mid = s.length >> 1;
67
+ return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
68
+ }
69
+
70
+ /**
71
+ * List every subagent transcript under `root` whose mtime is at or after
72
+ * `cutoffMs` (a cheap pre-filter: a file untouched since the cutoff cannot hold
73
+ * a first turn inside the window). Unreadable directories are skipped and
74
+ * counted.
75
+ *
76
+ * @param {string} root
77
+ * @param {number} cutoffMs
78
+ * @returns {{ files: string[], unreadable_dirs: number }}
79
+ */
80
+ export function findSubagentTranscripts(root, cutoffMs) {
81
+ const files = [];
82
+ let unreadableDirs = 0;
83
+ const list = (dir) => {
84
+ try {
85
+ return fs.readdirSync(dir, { withFileTypes: true });
86
+ } catch {
87
+ unreadableDirs++;
88
+ return [];
89
+ }
90
+ };
91
+ const walkSubagents = (dir) => {
92
+ for (const e of list(dir)) {
93
+ const p = path.join(dir, e.name);
94
+ if (e.isDirectory()) walkSubagents(p);
95
+ else if (e.isFile() && AGENT_FILE_RE.test(e.name)) {
96
+ try {
97
+ if (fs.statSync(p).mtimeMs >= cutoffMs) files.push(p);
98
+ } catch {
99
+ // vanished between readdir and stat — not part of the window
100
+ }
101
+ }
102
+ }
103
+ };
104
+ for (const project of list(root)) {
105
+ if (!project.isDirectory()) continue;
106
+ for (const session of list(path.join(root, project.name))) {
107
+ if (!session.isDirectory()) continue;
108
+ const sub = path.join(root, project.name, session.name, 'subagents');
109
+ // lstat, not existsSync: a `subagents` symlink is skipped like every
110
+ // other symlink in this walk (Dirent types do not follow links).
111
+ let subStat;
112
+ try {
113
+ subStat = fs.lstatSync(sub);
114
+ } catch {
115
+ continue;
116
+ }
117
+ if (subStat.isDirectory()) walkSubagents(sub);
118
+ }
119
+ }
120
+ files.sort();
121
+ return { files, unreadable_dirs: unreadableDirs };
122
+ }
123
+
124
+ /**
125
+ * Agent type from the sibling meta.json.
126
+ * @param {string} jsonlPath
127
+ * @returns {string} agentType, or '(no-meta)' / '(unreadable-meta)'
128
+ */
129
+ export function readAgentType(jsonlPath) {
130
+ const metaPath = jsonlPath.replace(/\.jsonl$/, '.meta.json');
131
+ let raw;
132
+ try {
133
+ raw = fs.readFileSync(metaPath, 'utf8');
134
+ } catch {
135
+ return '(no-meta)';
136
+ }
137
+ try {
138
+ const t = JSON.parse(raw)?.agentType;
139
+ return typeof t === 'string' && t.trim() ? t : '(no-meta)';
140
+ } catch {
141
+ return '(unreadable-meta)';
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Stream one subagent transcript into a run summary.
147
+ *
148
+ * @param {string} jsonlPath
149
+ * @returns {Promise<{ turns: Array<{ model: string, ts: number|null, input: number,
150
+ * cacheCreation: number, cacheRead: number, output: number }>,
151
+ * lines: Record<string, number>, malformed: number }>}
152
+ */
153
+ export async function readRunTurns(jsonlPath) {
154
+ const byKey = new Map();
155
+ const unkeyed = [];
156
+ /** Raw assistant lines per model, BEFORE the streaming dedup. */
157
+ const lines = {};
158
+ let malformed = 0;
159
+ const rl = readline.createInterface({
160
+ input: fs.createReadStream(jsonlPath, { encoding: 'utf8' }),
161
+ crlfDelay: Infinity,
162
+ });
163
+ for await (const line of rl) {
164
+ if (!line.trim()) continue;
165
+ let obj;
166
+ try {
167
+ obj = JSON.parse(line);
168
+ } catch {
169
+ malformed++;
170
+ continue;
171
+ }
172
+ if (obj?.type !== 'assistant') continue;
173
+ const msg = obj.message;
174
+ const model = typeof msg?.model === 'string' && msg.model ? msg.model : null;
175
+ if (model === null) continue;
176
+ lines[model] = (lines[model] ?? 0) + 1;
177
+ const u = msg.usage && typeof msg.usage === 'object' ? msg.usage : {};
178
+ const ts = Date.parse(obj.timestamp);
179
+ const snap = {
180
+ model,
181
+ input: clamp(u.input_tokens),
182
+ cacheCreation: clamp(u.cache_creation_input_tokens),
183
+ cacheRead: clamp(u.cache_read_input_tokens),
184
+ output: clamp(u.output_tokens),
185
+ };
186
+ const key =
187
+ typeof obj.requestId === 'string' && obj.requestId
188
+ ? `r:${obj.requestId}`
189
+ : typeof msg.id === 'string' && msg.id
190
+ ? `m:${msg.id}`
191
+ : null;
192
+ if (key === null) {
193
+ unkeyed.push({ ...snap, ts: Number.isFinite(ts) ? ts : null });
194
+ continue;
195
+ }
196
+ const prev = byKey.get(key);
197
+ // Last usage block wins; the timestamp stays the FIRST one (request time).
198
+ byKey.set(key, { ...snap, ts: prev ? prev.ts : Number.isFinite(ts) ? ts : null });
199
+ }
200
+ return { turns: [...byKey.values(), ...unkeyed], lines, malformed };
201
+ }
202
+
203
+ /**
204
+ * Derive the per-run metrics from its turns (in file order).
205
+ * @param {Array<{ model: string, ts: number|null, input: number, cacheCreation: number,
206
+ * cacheRead: number, output: number }>} turns
207
+ */
208
+ export function summarizeRun(turns) {
209
+ const ordered = [...turns].sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
210
+ const models = {};
211
+ let output = 0;
212
+ let rebuilds = 0;
213
+ for (let i = 0; i < ordered.length; i++) {
214
+ const t = ordered[i];
215
+ models[t.model] = (models[t.model] ?? 0) + 1;
216
+ output += t.output;
217
+ const prev = ordered[i - 1];
218
+ if (prev && prev.ts !== null && t.ts !== null && (t.ts - prev.ts) / 1000 > GAP_SECONDS
219
+ && t.cacheCreation > REBUILD_MIN_CREATION && t.cacheRead < t.cacheCreation) {
220
+ rebuilds++;
221
+ }
222
+ }
223
+ const first = ordered[0];
224
+ return {
225
+ firstTs: first?.ts ?? null,
226
+ turns: ordered.length,
227
+ models,
228
+ output,
229
+ rebuilds,
230
+ startContext: first ? first.input + first.cacheCreation + first.cacheRead : null,
231
+ };
232
+ }
233
+
234
+ const shareMap = (counts, total, unit = 'turns') =>
235
+ Object.fromEntries(
236
+ Object.entries(counts)
237
+ .sort((a, b) => b[1] - a[1])
238
+ .map(([m, n]) => [m, { [unit]: n, share: total ? +(n / total).toFixed(4) : 0 }]),
239
+ );
240
+
241
+ /**
242
+ * Fold run summaries into the per-type census.
243
+ * @param {Array<{ agentType: string } & ReturnType<typeof summarizeRun>>} runs
244
+ */
245
+ export function aggregate(runs) {
246
+ const groups = new Map();
247
+ const allModels = {};
248
+ let totalTurns = 0;
249
+ for (const r of runs) {
250
+ let g = groups.get(r.agentType);
251
+ if (!g) {
252
+ g = { runs: [], models: {} };
253
+ groups.set(r.agentType, g);
254
+ }
255
+ g.runs.push(r);
256
+ for (const [m, n] of Object.entries(r.models)) {
257
+ g.models[m] = (g.models[m] ?? 0) + n;
258
+ allModels[m] = (allModels[m] ?? 0) + n;
259
+ totalTurns += n;
260
+ }
261
+ }
262
+ const byType = [...groups.entries()].map(([agentType, g]) => {
263
+ const turns = g.runs.reduce((s, r) => s + r.turns, 0);
264
+ return {
265
+ agent_type: agentType,
266
+ dispatches: g.runs.length,
267
+ turns,
268
+ models: shareMap(g.models, turns),
269
+ turns_per_run_median: median(g.runs.map((r) => r.turns)),
270
+ output_tokens_per_run_median: median(g.runs.map((r) => r.output)),
271
+ cache_rebuilds: g.runs.reduce((s, r) => s + r.rebuilds, 0),
272
+ runs_with_cache_rebuild: g.runs.filter((r) => r.rebuilds > 0).length,
273
+ start_context_median: median(g.runs.map((r) => r.startContext).filter((v) => v !== null)),
274
+ };
275
+ });
276
+ byType.sort((a, b) => b.turns - a.turns || a.agent_type.localeCompare(b.agent_type));
277
+ return { turns: totalTurns, models: shareMap(allModels, totalTurns), by_agent_type: byType };
278
+ }
279
+
280
+ /**
281
+ * Run the census over a transcript root.
282
+ *
283
+ * @param {{ root: string, days?: number, now?: number, concurrency?: number }} opts
284
+ */
285
+ export async function runCensus({ root, days = DEFAULT_DAYS, now = Date.now(), concurrency = 4 }) {
286
+ const cutoffMs = now - days * 86_400_000;
287
+ const { files, unreadable_dirs } = findSubagentTranscripts(root, cutoffMs);
288
+ const runs = [];
289
+ let malformed = 0;
290
+ let unreadableFiles = 0;
291
+ let outsideWindow = 0;
292
+ let emptyRuns = 0;
293
+ const lineModels = {};
294
+ let next = 0;
295
+ const worker = async () => {
296
+ while (next < files.length) {
297
+ const file = files[next++];
298
+ let res;
299
+ try {
300
+ res = await readRunTurns(file);
301
+ } catch {
302
+ unreadableFiles++;
303
+ continue;
304
+ }
305
+ malformed += res.malformed;
306
+ if (res.turns.length === 0) {
307
+ emptyRuns++;
308
+ continue;
309
+ }
310
+ const s = summarizeRun(res.turns);
311
+ if (s.firstTs === null || s.firstTs < cutoffMs || s.firstTs > now) {
312
+ outsideWindow++;
313
+ continue;
314
+ }
315
+ runs.push({ agentType: readAgentType(file), ...s });
316
+ for (const [m, n] of Object.entries(res.lines)) lineModels[m] = (lineModels[m] ?? 0) + n;
317
+ }
318
+ };
319
+ await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker));
320
+ return {
321
+ schema: SCHEMA,
322
+ measured_at: new Date(now).toISOString(),
323
+ root,
324
+ window_days: days,
325
+ cutoff: new Date(cutoffMs).toISOString(),
326
+ definition: {
327
+ turn: 'assistant response with message.model, deduped on requestId else message.id (last usage wins)',
328
+ run_in_window: 'first turn timestamp >= cutoff',
329
+ cache_rebuild: `gap > ${GAP_SECONDS}s, cache_creation > ${REBUILD_MIN_CREATION}, cache_read < cache_creation`,
330
+ start_context: 'input + cache_creation + cache_read of the first turn',
331
+ },
332
+ files_scanned: files.length,
333
+ runs: runs.length,
334
+ runs_outside_window: outsideWindow,
335
+ runs_without_turns: emptyRuns,
336
+ unreadable_files: unreadableFiles,
337
+ unreadable_dirs,
338
+ malformed_lines: malformed,
339
+ ...aggregate(runs),
340
+ // Raw streaming lines, not API turns: the #1617 discovery counted these
341
+ // (349,519 in 14 days); one response is ~1.9 lines. Kept for comparability.
342
+ assistant_lines: Object.values(lineModels).reduce((a, b) => a + b, 0),
343
+ models_by_lines: shareMap(lineModels, Object.values(lineModels).reduce((a, b) => a + b, 0), 'lines'),
344
+ };
345
+ }
346
+
347
+ const pct = (s) => `${(s * 100).toFixed(1)}%`;
348
+ const fmt = (v) => (v === null ? '-' : Number.isInteger(v) ? String(v) : v.toFixed(1));
349
+
350
+ /** Human-readable table of a census result. */
351
+ export function formatTable(result, { top = 25 } = {}) {
352
+ const out = [];
353
+ out.push(
354
+ `agent-usage-census: ${result.runs} runs / ${result.turns} turns, last ${result.window_days} days ` +
355
+ `(${result.files_scanned} files, malformed_lines ${result.malformed_lines}, ` +
356
+ `unreadable_files ${result.unreadable_files})`,
357
+ );
358
+ out.push(
359
+ 'models: ' +
360
+ Object.entries(result.models)
361
+ .map(([m, v]) => `${m} ${pct(v.share)} (${v.turns})`)
362
+ .join(' · '),
363
+ );
364
+ out.push(
365
+ `raw assistant lines ${result.assistant_lines}: ` +
366
+ Object.entries(result.models_by_lines)
367
+ .slice(0, 3)
368
+ .map(([m, v]) => `${m} ${pct(v.share)}`)
369
+ .join(' · '),
370
+ );
371
+ out.push('');
372
+ const head = ['agent_type', 'runs', 'turns', 'models', 'turns/run', 'out/run', 'rebuilds', 'start_ctx'];
373
+ const rows = result.by_agent_type.slice(0, top).map((t) => [
374
+ t.agent_type,
375
+ String(t.dispatches),
376
+ String(t.turns),
377
+ Object.entries(t.models)
378
+ .slice(0, 3)
379
+ .map(([m, v]) => `${m.replace(/^claude-/, '')} ${pct(v.share)}`)
380
+ .join(', '),
381
+ fmt(t.turns_per_run_median),
382
+ fmt(t.output_tokens_per_run_median),
383
+ `${t.cache_rebuilds}/${t.runs_with_cache_rebuild}`,
384
+ fmt(t.start_context_median),
385
+ ]);
386
+ const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
387
+ const line = (r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
388
+ out.push(line(head));
389
+ for (const r of rows) out.push(line(r));
390
+ if (result.by_agent_type.length > top) out.push(`… ${result.by_agent_type.length - top} more types (--json for all)`);
391
+ out.push('');
392
+ out.push('rebuilds = cache rebuild events / runs with ≥1; medians per run.');
393
+ return out.join('\n');
394
+ }
@@ -36,7 +36,11 @@ import { randomUUID } from 'node:crypto';
36
36
  import path from 'node:path';
37
37
  import { atomicWriteText, envelopeToError } from './io.mjs';
38
38
 
39
- import { filterRealSessions } from './session-schema.mjs';
39
+ import { canonicalizeSessions, isMeasuredSession } from './sessions-canonical.mjs';
40
+ import { resolveEventsLedgerRoot } from './platform.mjs';
41
+ import { resolveLedgerPath } from './ledger-path.mjs';
42
+ import { withVisibleInboxText } from './learnings/surface.mjs';
43
+ import { inboxPathFor } from './learnings/store-role.mjs';
40
44
 
41
45
  // ---------------------------------------------------------------------------
42
46
  // Constants
@@ -82,11 +86,11 @@ function consumedDirPath(repoRoot) {
82
86
  }
83
87
 
84
88
  function sessionsJsonlPath(repoRoot) {
85
- return path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
89
+ return path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl');
86
90
  }
87
91
 
88
92
  function learningsJsonlPath(repoRoot) {
89
- return path.join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
93
+ return resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot);
90
94
  }
91
95
 
92
96
  // ---------------------------------------------------------------------------
@@ -163,11 +167,10 @@ export async function readDialecticSignals({ repoRoot } = {}) {
163
167
  continue; // malformed line — skip silently
164
168
  }
165
169
  }
166
- // Abandoned-session filter (#834): mirrors auto-dream.mjs
167
- // readDreamSignals() — phantom `status: 'abandoned'` stubs are
168
- // legitimate DATA but not legitimate SIGNAL, and must not fire the
169
- // /evolve --dialectic cadence off zero real work.
170
- const realEntries = filterRealSessions(entries);
170
+ // Canonical identities precede the shared measured-work predicate.
171
+ // Keep ID-less legacy rows: this cadence reader has always accepted them.
172
+ const realEntries = canonicalizeSessions(entries, { keepUnidentified: true })
173
+ .filter(isMeasuredSession);
171
174
  for (const entry of realEntries) {
172
175
  const startedAt = entry.started_at;
173
176
  if (typeof startedAt !== 'string' || startedAt.length === 0) continue;
@@ -181,10 +184,14 @@ export async function readDialecticSignals({ repoRoot } = {}) {
181
184
  }
182
185
 
183
186
  // Learnings
187
+ // The visible inbox records count too (#1516, #1685): shared visibility rule,
188
+ // also when the live store is absent (inbox host); an unreadable inbox never
189
+ // costs the live count (`withVisibleInboxText` swallows its own errors).
184
190
  const learningsPath = learningsJsonlPath(repoRoot);
185
- if (existsSync(learningsPath)) {
191
+ if (existsSync(learningsPath) || existsSync(inboxPathFor(learningsPath))) {
186
192
  try {
187
- const raw = await readFile(learningsPath, 'utf8');
193
+ const live = existsSync(learningsPath) ? await readFile(learningsPath, 'utf8') : '';
194
+ const raw = await withVisibleInboxText(learningsPath, live);
188
195
  const lines = raw.split('\n').filter((l) => l.length > 0);
189
196
  for (const line of lines) {
190
197
  let entry;