@event4u/agent-config 14.6.0 → 14.8.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 (258) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +255 -145
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  5. package/dist/agent-src/commands/analyze.md +1 -1
  6. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  7. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  8. package/dist/agent-src/commands/pr/create.md +14 -2
  9. package/dist/agent-src/commands/pr/merge.md +379 -0
  10. package/dist/agent-src/commands/roadmap/process-full.md +272 -32
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
  15. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
  16. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  17. package/dist/agent-src/rules/context-hygiene.md +2 -0
  18. package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
  19. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  21. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  22. package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
  23. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  24. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  25. package/dist/agent-src/rules/skill-quality.md +0 -2
  26. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  27. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  28. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  29. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  30. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  31. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  32. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  33. package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
  34. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  35. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  36. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  37. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  38. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  39. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  40. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  41. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  42. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  43. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  44. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  45. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  46. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  47. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  48. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  49. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  50. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  51. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  52. package/dist/agent-src/skills/security/SKILL.md +8 -0
  53. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  54. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  55. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  56. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  57. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  58. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  59. package/dist/agent-src/templates/roadmaps.md +65 -4
  60. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  61. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  62. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  63. package/dist/cli/registry.js +2 -1
  64. package/dist/cli/registry.js.map +1 -1
  65. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  66. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
  67. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  68. package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
  69. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
  70. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  71. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
  72. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  73. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
  74. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  75. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
  76. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
  77. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  78. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  79. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  80. package/dist/cli-delegate/cmd_conformance.js +466 -716
  81. package/dist/cli-delegate/cmd_converge.js +3 -3
  82. package/dist/cli-delegate/cmd_doctor.js +10 -10
  83. package/dist/cli-delegate/cmd_explain.js +1 -1
  84. package/dist/cli-delegate/cmd_export.js +10 -9
  85. package/dist/cli-delegate/cmd_handoff.js +10 -9
  86. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  89. package/dist/cli-delegate/cmd_preflight.js +5 -5
  90. package/dist/cli-delegate/cmd_prune.js +11 -10
  91. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  92. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  93. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  94. package/dist/cli-delegate/cmd_settings_get.js +3 -3
  95. package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
  96. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  97. package/dist/cli-delegate/cmd_sync.js +11 -10
  98. package/dist/cli-delegate/cmd_uninstall.js +13 -11
  99. package/dist/cli-delegate/cmd_update.js +4 -4
  100. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  101. package/dist/cli-delegate/cmd_validate.js +11 -10
  102. package/dist/cli-delegate/cmd_versions.js +3 -3
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +111 -37
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +5 -5
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +15 -12
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +11 -8
  112. package/dist/hooks/dispatch.js +3165 -2192
  113. package/dist/install/claudePathsPlan.js +187 -0
  114. package/dist/install/claudePathsPlan.js.map +1 -0
  115. package/dist/install/globalRuleLayers.js +169 -0
  116. package/dist/install/globalRuleLayers.js.map +1 -0
  117. package/dist/install/hostLayerFingerprint.js +136 -0
  118. package/dist/install/hostLayerFingerprint.js.map +1 -0
  119. package/dist/install/install.mjs +553 -425
  120. package/dist/install/partitionEligibility.js +432 -0
  121. package/dist/install/partitionEligibility.js.map +1 -0
  122. package/dist/install/ruleLayerPartition.js +157 -0
  123. package/dist/install/ruleLayerPartition.js.map +1 -0
  124. package/dist/mcp/registry-manifest.json +3 -3
  125. package/dist/mcp/server.json +2 -2
  126. package/dist/mcp/server.mjs +1863 -1234
  127. package/dist/router.json +1 -1
  128. package/dist/scripts/_lib/agent_src.js +41 -0
  129. package/dist/scripts/_lib/agent_src.js.map +1 -1
  130. package/dist/scripts/_lib/install_layout.js +83 -0
  131. package/dist/scripts/_lib/install_layout.js.map +1 -0
  132. package/dist/scripts/_lib/installed_lock.js +444 -0
  133. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  134. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  135. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  136. package/dist/scripts/condense.js +63 -251
  137. package/dist/scripts/condense.js.map +1 -1
  138. package/dist/server/schemas/settings.js +4 -0
  139. package/dist/server/schemas/settings.js.map +1 -1
  140. package/docs/contracts/settings-classes.md +12 -6
  141. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  142. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  143. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  144. package/docs/guidelines/agent-infra/false-green.md +47 -1
  145. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  146. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  147. package/package.json +1 -1
  148. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  149. package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  150. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  151. package/src/agent-src/scripts/gate_budget.ts +282 -0
  152. package/src/agent-src/scripts/gate_execute.ts +72 -31
  153. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  154. package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
  155. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  156. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  157. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  158. package/src/config/agent-settings.template.yml +27 -2
  159. package/src/config/ci-local-parity.yml +39 -0
  160. package/src/config/estate-count-budget.json +612 -3
  161. package/src/config/evaluator-budgets.json +3 -3
  162. package/src/config/gate-coverage.yml +220 -12
  163. package/src/config/gate-violation-baselines.json +73 -63
  164. package/src/config/gitignore-block.txt +15 -2
  165. package/src/config/hook-latency-budget.json +5 -0
  166. package/src/config/rule-activation-census.json +14 -6
  167. package/src/config/rule-stub-ceilings.json +34 -7
  168. package/src/install/claudePathsPlan.ts +196 -0
  169. package/src/install/globalRuleLayers.ts +197 -0
  170. package/src/install/hostLayerFingerprint.ts +145 -0
  171. package/src/install/partitionEligibility.ts +502 -0
  172. package/src/install/ruleLayerPartition.ts +212 -0
  173. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  174. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  175. package/src/scripts/_dispatch.bash +17 -1
  176. package/src/scripts/_lib/ac_heading.ts +30 -0
  177. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  178. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  179. package/src/scripts/_lib/agent_src.ts +42 -0
  180. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  181. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  182. package/src/scripts/_lib/envelope_grounding.ts +49 -9
  183. package/src/scripts/_lib/installed_lock.ts +50 -5
  184. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  185. package/src/scripts/_lib/md_section.ts +129 -0
  186. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  187. package/src/scripts/_lib/scope_guard.sh +26 -3
  188. package/src/scripts/_lib/self_repair.ts +19 -0
  189. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  190. package/src/scripts/adr/evidence_census.ts +899 -0
  191. package/src/scripts/adr/regenerate_index.ts +12 -32
  192. package/src/scripts/adr_cite_check.ts +540 -39
  193. package/src/scripts/ai_council/cli_help.ts +55 -0
  194. package/src/scripts/audit_adr_coverage.ts +103 -17
  195. package/src/scripts/before_complete_hook.ts +153 -28
  196. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  197. package/src/scripts/bench_hook_latency.ts +204 -0
  198. package/src/scripts/build_archive_index.ts +34 -0
  199. package/src/scripts/check_adr_frontmatter.ts +574 -39
  200. package/src/scripts/check_hook_bundle_content.ts +219 -0
  201. package/src/scripts/check_new_adr_evidence.ts +529 -0
  202. package/src/scripts/check_references.ts +78 -0
  203. package/src/scripts/check_release_published.ts +32 -11
  204. package/src/scripts/check_rule_layer_partition.ts +531 -0
  205. package/src/scripts/check_single_delivery.ts +194 -17
  206. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  207. package/src/scripts/ci_settle.ts +170 -0
  208. package/src/scripts/condense.ts +79 -259
  209. package/src/scripts/council_cli.ts +3 -3
  210. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  211. package/src/scripts/external_sources_denylist.json +4 -1
  212. package/src/scripts/hook_manifest.json +1 -1
  213. package/src/scripts/hook_manifest.yaml +77 -14
  214. package/src/scripts/hooks/block_no_verify.ts +61 -10
  215. package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
  216. package/src/scripts/hooks/concern_registry.ts +4 -0
  217. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  218. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  219. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  220. package/src/scripts/hooks/host_semantics.ts +21 -0
  221. package/src/scripts/hooks/injection_budget.ts +13 -3
  222. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  223. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  224. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  225. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  226. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  227. package/src/scripts/hooks/state_io.ts +870 -21
  228. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  229. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  230. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  231. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  232. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  233. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  234. package/src/scripts/injection_scan_hook.ts +137 -5
  235. package/src/scripts/install.ts +16 -11
  236. package/src/scripts/language_mirror_hook.ts +561 -25
  237. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  238. package/src/scripts/lint_hook_manifest.ts +49 -0
  239. package/src/scripts/lint_plan_risk_register.ts +4 -3
  240. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  241. package/src/scripts/lint_skill_router_head.ts +4 -4
  242. package/src/scripts/mcp_server/tools.ts +2 -2
  243. package/src/scripts/new_roadmap.ts +136 -0
  244. package/src/scripts/pack_mcp_content.ts +20 -5
  245. package/src/scripts/release.ts +37 -38
  246. package/src/scripts/report_layer_overlap.ts +6 -0
  247. package/src/scripts/routing_doctor.ts +5 -1
  248. package/src/scripts/self_repair_hook.ts +37 -5
  249. package/src/scripts/session_register_hook.ts +11 -0
  250. package/src/scripts/skill_usage_collect.ts +11 -1
  251. package/src/scripts/skill_usage_report.ts +278 -8
  252. package/src/scripts/sync_pr_branch.ts +170 -20
  253. package/src/server/schemas/settings.ts +8 -0
  254. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  255. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  256. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  257. package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
  258. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
@@ -4,6 +4,25 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
4
4
 
5
5
  ---
6
6
 
7
+ ## Start from the skeleton, not from this page
8
+
9
+ ```bash
10
+ ./scripts-run src/scripts/new_roadmap <slug> # lightweight
11
+ ./scripts-run src/scripts/new_roadmap <slug> --structural
12
+ ./scripts-run src/scripts/new_roadmap <slug> --stdout # print, do not write
13
+ ```
14
+
15
+ It emits a file that passes every roadmap gate **unedited** — the complexity
16
+ enum, the acceptance heading in the exact form the extractor matches, the
17
+ risk-review marker, a legal risk type, and an anchor that resolves. Those four
18
+ conventions live in four different gates, and each one costs a full gate round
19
+ to discover by failing it. Measured 2026-08-20 across 88 roadmaps: **six
20
+ different invented `complexity:` values** were in the tree, and **10 of 22**
21
+ roadmaps wrote an acceptance heading the extractor cannot see.
22
+
23
+ The rules below still govern the content. The skeleton only removes the part
24
+ that was never a judgement call.
25
+
7
26
  ## Rules for Roadmaps
8
27
 
9
28
  1. **Be precise and concise.** Lightweight default: ≤ 600 lines (see rule 15). Only a declared `complexity: structural` roadmap may exceed that, capped at 1000 lines — if larger, split into multiple files.
@@ -89,8 +108,8 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
89
108
  - `interactive` — every gate fires as authored by its owning rule.
90
109
  **Not what an absent field means.** When the field is absent the mode is
91
110
  DERIVED from the invocation form (`roadmap-process-loop § 3a`):
92
- `process-full` and `/roadmap:next` derive `autonomous`, `process-phase`
93
- derives `phase-checkpoints`, `process-step` runs without a contract. The
111
+ `process-full` derives `autonomous`; `/roadmap:next` and `process-phase`
112
+ derive `phase-checkpoints`; `process-step` runs without a contract. The
94
113
  contract screen is still shown once and still carries "run interactive
95
114
  instead", so declaring nothing costs a keystroke, never control.
96
115
  - `phase-checkpoints` — the run halts at each phase boundary with a
@@ -110,6 +129,24 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
110
129
  lint. Follow-up roadmaps (rule 17) inherit the parent's mode as the
111
130
  *suggested* option during creation but the question is always asked
112
131
  again.
132
+
133
+ **Companion field, same block — `depends:`.** Optional, and only
134
+ meaningful for a set run:
135
+
136
+ ```yaml
137
+ depends: [road-to-other-thing] # slugs, not paths
138
+ ```
139
+
140
+ A declared edge is authoritative and is never overridden by the
141
+ file-overlap heuristic the set contract also computes
142
+ ([`roadmap-process-loop § 3d`](../contexts/execution/roadmap-process-loop.md)).
143
+ The union of declared and inferred edges feeds exactly two decisions —
144
+ the order members run in, and whether two members may share a lane —
145
+ and nothing else. Declaring an edge that is not real costs serial
146
+ execution; missing a real one costs a collision, so the heuristic
147
+ resolves toward serial and a declaration is the cheap way to be right.
148
+ Absent means no declared dependency, never “independent”: the overlap
149
+ heuristic still runs.
113
150
  19. **Source-derived roadmaps carry a gap-table + provenance; internally
114
151
  originated roadmaps carry neither.** This rule fires **only** when a
115
152
  roadmap originates from an external input (a suggestion, a competitive
@@ -203,16 +240,40 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
203
240
  | `0` | auto-run — deterministic, free, reversible | the agent runs `Run:`; the output IS the unblock |
204
241
  | `1` | budget-preauthorized — billable but reversible | the agent runs `Run:` under a standing budget and logs a receipt |
205
242
  | `2` | consent-once — a real preference call, reversible | one line with the recommendation and a default |
206
- | `3` | human-only — irreversible, legal, or Hard Floor | unchanged: the human IS the content |
243
+ | `3` | human-only — the human IS the content: externally impossible for the agent, or an explicitly excluded action | a person, because nobody else can |
244
+
245
+ **CAPABILITY BEFORE ROLE (ADR-237).** `3` means *a human is the
246
+ content of this gate*, never *a human usually does this*. Before
247
+ authoring `3`, ask one question: **can the agent execute this at all**
248
+ — through the filesystem, git, `gh`, an API, a CLI, a tool, a model, a
249
+ council? If yes, it is **not** class 3, whoever conventionally
250
+ performs it. A branch to create, a push, a PR to open, a repository or
251
+ branch setting the agent can change, a workflow to start, CI to
252
+ re-run, a merge base to update, conflicts, failing tests, local
253
+ configuration, a paid call inside the run's budget — all of these are
254
+ **work**, and authoring `3` on any of them is a defect a
255
+ `process-full` run repairs rather than obeys.
256
+
257
+ What legitimately remains `3`: a credential that does not exist and
258
+ the agent cannot create · a purchase beyond the delegated budget ·
259
+ physical hardware access · another person or organisation must act · a
260
+ wait that is factually mandatory and cannot be simulated or verified ·
261
+ an action on the Hard Floor's EXCLUDED list (production-trunk merge,
262
+ deploy, prod data / secrets / IAM / DNS, an irreversible external
263
+ action).
207
264
 
208
265
  Three properties are load-bearing. **`Class:` is optional and its
209
266
  absence means 3**, so nothing becomes executable because an author
210
- forgot a field. **A class-0 or class-1 entry MUST carry `Run:`** —
267
+ forgot a field — but an absent class is a *default*, not a finding,
268
+ and the capability question above still decides whether the step is
269
+ really blocked. **A class-0 or class-1 entry MUST carry `Run:`** —
211
270
  `lint_roadmap_blockers` fails otherwise, because a gate that claims
212
271
  to be runnable without naming the command reads as actionable and is
213
272
  not. And **class is authored, never inferred**: promoting a gate is
214
273
  a reviewed edit, which is what keeps a Hard-Floor gate
215
274
  (`non-destructive-by-default`) out of reach of a runtime judgment.
275
+ Repairing a `3` that fails the capability test is such an edit — it is
276
+ recorded at the blocker with the reason, not applied silently.
216
277
 
217
278
  Only the leading token of `Class:` is read, so
218
279
  `- **Class:** 1 — budget-preauthorized` is valid; the taxonomy name
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The detached Class-A sender (road-to-org-telemetry Phase 2, step 2.1).
4
+ *
5
+ * Spawned by `spawn_detached_sender` and never by a human. It is the ONLY
6
+ * code in this tree that makes an outbound telemetry call, which is why it is
7
+ * one small file with no imports beyond node builtins: the egress surface
8
+ * should be readable end to end in one screen by anyone auditing what leaves
9
+ * a machine.
10
+ *
11
+ * PLAIN JS, DELIBERATELY. Its siblings here are TypeScript resolved by `tsx`
12
+ * at import time. A detached child inherits no such resolution, so a `.ts`
13
+ * sender would depend on finding a `tsx` binary in a consumer install at
14
+ * session teardown. `process.execPath` — plain node — exists by construction.
15
+ * The cost of that choice is that this file cannot import the sibling
16
+ * modules, so the drain algorithm lives HERE and nowhere else; the TypeScript
17
+ * side owns enqueue and spawn only, and there is no second copy to drift.
18
+ *
19
+ * CLAIM-BY-RENAME, NOT READ-THEN-TRUNCATE. `rename` is atomic within a
20
+ * filesystem, so a session that appends a record while this process is
21
+ * sending writes to a fresh spool rather than into bytes about to be deleted.
22
+ * Read-then-truncate loses exactly those records, silently, and only under
23
+ * concurrency — the failure shape that never shows up in a single-session
24
+ * test.
25
+ *
26
+ * FAILURE KEEPS THE DATA. A non-2xx, a timeout, a DNS failure, a thrown
27
+ * anything: the claimed lines go back in front of the spool and the next
28
+ * session's flush tries again. "Local retention for the next flush" is the
29
+ * step's own wording and this is it. Exit code is always 0 — nothing reads it
30
+ * (`stdio: 'ignore'`), and a non-zero from a detached child is a log line
31
+ * nobody will ever see.
32
+ */
33
+ import * as fs from 'node:fs';
34
+
35
+ const DEFAULT_TIMEOUT_MS = 1000;
36
+
37
+ function parse_args(argv) {
38
+ const out = { spool: '', endpoint: '', timeout_ms: DEFAULT_TIMEOUT_MS };
39
+ for (let i = 0; i < argv.length; i += 1) {
40
+ const a = argv[i];
41
+ if (a === '--spool') {
42
+ out.spool = argv[i + 1] ?? '';
43
+ i += 1;
44
+ } else if (a === '--endpoint') {
45
+ out.endpoint = argv[i + 1] ?? '';
46
+ i += 1;
47
+ } else if (a === '--timeout') {
48
+ const n = Number.parseInt(argv[i + 1] ?? '', 10);
49
+ if (Number.isFinite(n) && n > 0) out.timeout_ms = n;
50
+ i += 1;
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /**
57
+ * Take the spool by rename. Returns the claim path, or `null` when there is
58
+ * nothing to send — including the race where a sibling sender claimed it
59
+ * first, which is a no-op rather than an error.
60
+ */
61
+ export function claim_spool(spool_path) {
62
+ let size = 0;
63
+ try {
64
+ size = fs.statSync(spool_path).size;
65
+ } catch {
66
+ return null;
67
+ }
68
+ if (size === 0) return null;
69
+ const claim = `${spool_path}.sending.${process.pid}.${Date.now()}`;
70
+ try {
71
+ fs.renameSync(spool_path, claim);
72
+ } catch {
73
+ return null;
74
+ }
75
+ return claim;
76
+ }
77
+
78
+ /** Put the claimed lines back in FRONT of whatever arrived meanwhile. */
79
+ export function restore_claim(claim_path, spool_path) {
80
+ try {
81
+ const claimed = fs.readFileSync(claim_path, 'utf-8');
82
+ let current = '';
83
+ try {
84
+ current = fs.readFileSync(spool_path, 'utf-8');
85
+ } catch {
86
+ current = '';
87
+ }
88
+ fs.writeFileSync(spool_path, claimed + current, { encoding: 'utf-8' });
89
+ fs.unlinkSync(claim_path);
90
+ return true;
91
+ } catch {
92
+ // The claim file survives under its own name; the next sender's
93
+ // `claim_spool` will not see it, so it is recoverable by hand rather
94
+ // than lost. Better than a partial write over the live spool.
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * POST the batch as NDJSON. Returns true only on a 2xx.
101
+ *
102
+ * The body is the spooled bytes verbatim — every line is a Class-A record
103
+ * whose fields are all structural (see `remote.ts`), so there is no
104
+ * serialisation step here that could introduce a field the record type does
105
+ * not have.
106
+ */
107
+ export async function post_batch(endpoint, body, timeout_ms) {
108
+ try {
109
+ const res = await fetch(endpoint, {
110
+ method: 'POST',
111
+ headers: { 'content-type': 'application/x-ndjson' },
112
+ body,
113
+ signal: AbortSignal.timeout(timeout_ms),
114
+ });
115
+ return res.ok === true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ export async function run(args) {
122
+ if (!args.spool || !args.endpoint) return 'no-op';
123
+ const claim = claim_spool(args.spool);
124
+ if (claim === null) return 'no-op';
125
+
126
+ let body = '';
127
+ try {
128
+ body = fs.readFileSync(claim, 'utf-8');
129
+ } catch {
130
+ return 'read-failed';
131
+ }
132
+ if (body.length === 0) {
133
+ try {
134
+ fs.unlinkSync(claim);
135
+ } catch {
136
+ /* nothing to clean */
137
+ }
138
+ return 'no-op';
139
+ }
140
+
141
+ const ok = await post_batch(args.endpoint, body, args.timeout_ms);
142
+ if (ok) {
143
+ try {
144
+ fs.unlinkSync(claim);
145
+ } catch {
146
+ /* sent; a stale claim is harmless */
147
+ }
148
+ return 'sent';
149
+ }
150
+ restore_claim(claim, args.spool);
151
+ return 'retained';
152
+ }
153
+
154
+ const _entry = process.argv[1] ?? '';
155
+ const _invoked_directly = _entry !== '' && import.meta.url === new URL(`file://${_entry}`).href;
156
+
157
+ if (_invoked_directly) {
158
+ run(parse_args(process.argv.slice(2)))
159
+ .then(() => process.exit(0))
160
+ .catch(() => process.exit(0));
161
+ }
162
+
163
+ export { parse_args };
@@ -35,8 +35,14 @@ import { check_id_redaction, py_json_dumps_compact_sorted } from './engagement.j
35
35
 
36
36
  export const CLASS_A_SCHEMA_VERSION = 1;
37
37
 
38
- /** Usage classes. Phase 5 adds the self-repair class; Phase 1 emits one. */
39
- export const ALLOWED_RECORD_CLASSES = ['usage'] as const;
38
+ /**
39
+ * Record classes. `usage` is Phase 1; `self-repair` is Phase 5 step 5.1.
40
+ *
41
+ * Both are Class A — structural fields only — and both travel the same log,
42
+ * spool and transport, discriminated by this field. `skill_usage_report`
43
+ * filters on it so a defect report can never be counted as an invocation.
44
+ */
45
+ export const ALLOWED_RECORD_CLASSES = ['usage', 'self-repair'] as const;
40
46
 
41
47
  /**
42
48
  * Discipline-profile values, the knob that actually decides which rule
@@ -279,6 +285,122 @@ export function build_class_a_record(input: BuildClassAInput): ClassARecord {
279
285
  };
280
286
  }
281
287
 
288
+ /**
289
+ * One Class-A SELF-REPAIR record (Phase 5, step 5.1).
290
+ *
291
+ * The shadow of a queued defect: it says a defect of a named class occurred,
292
+ * on which host, at which package version, under which declared profile, for
293
+ * which pseudonymous user. It says nothing about WHAT happened.
294
+ *
295
+ * A SEPARATE TYPE, NOT A WIDENED `ClassARecord`, AND THAT IS THE WHOLE POINT.
296
+ * `DefectFinding` carries `evidence` (a quoted span of the offending text) and
297
+ * `suggested_surface` (a free sentence). Those are the Class-B payload and
298
+ * they ship only on explicit per-case approval. Adding either to a
299
+ * telemetry record type — even optionally, even "sanitized" — would give this
300
+ * type a field capable of holding project content, and the privacy property
301
+ * this whole surface rests on is that no such field exists. There is then no
302
+ * scrubber to fail. Keeping the automatic class in its own interface is how
303
+ * that stays checkable by reading the type instead of auditing every writer.
304
+ *
305
+ * WHAT IS DELIBERATELY ABSENT: an enumerated active-rule / active-skill list.
306
+ * The roadmap Context asks Class A to carry "the active rule and skill
307
+ * snapshot" so that "this rule was loaded in six of seven reports of this
308
+ * class" localizes an artefact. No producer for that exists on a hook path:
309
+ * `match_prompt` needs the compiled router, which nothing in the hook surface
310
+ * loads today and which is not established as present in a consumer install.
311
+ * `discipline_profile` is recorded instead — the knob that decides which rule
312
+ * surfaces load at all — and it is a profile-level snapshot, NOT the
313
+ * per-artefact attribution key. That attribution claim stays unavailable, and
314
+ * saying so here is cheaper than a reader inferring it from a field name.
315
+ */
316
+ export interface ClassADefectRecord {
317
+ schema_version: number;
318
+ record_class: string;
319
+ ts_bucket: string;
320
+ /** Detector / intake class — a closed vocabulary owned by the caller. */
321
+ defect_class: string;
322
+ /** Which intake produced it — also a closed vocabulary. */
323
+ defect_source: string;
324
+ /** How many times this fingerprint has been seen. A counter, not content. */
325
+ occurrences: number;
326
+ host: string | null;
327
+ package_version: string | null;
328
+ discipline_profile: string | null;
329
+ org_id: string;
330
+ user_hash: string;
331
+ session_hash: string | null;
332
+ }
333
+
334
+ export interface BuildClassADefectInput {
335
+ defect_class: string;
336
+ defect_source: string;
337
+ occurrences: number;
338
+ host: string | null;
339
+ org_id: string;
340
+ salt: string;
341
+ hostname: string;
342
+ username: string;
343
+ session_id: string;
344
+ package_version?: string | null;
345
+ discipline_profile?: string | null;
346
+ now?: Date;
347
+ }
348
+
349
+ /**
350
+ * Build one validated Class-A self-repair record. Pure, and throws rather
351
+ * than returning a partial record — same contract as its usage sibling.
352
+ *
353
+ * `defect_class` and `defect_source` go through the same id-redaction floor as
354
+ * every other id here, so a caller that ever passed a path, an extension or
355
+ * an over-long free string gets an exception instead of an egress.
356
+ */
357
+ export function build_class_a_defect_record(
358
+ input: BuildClassADefectInput,
359
+ ): ClassADefectRecord {
360
+ check_id_redaction('defect_class', input.defect_class);
361
+ check_id_redaction('defect_source', input.defect_source);
362
+ check_id_redaction('org_id', input.org_id);
363
+
364
+ const host = input.host ?? null;
365
+ if (host !== null) {
366
+ check_id_redaction('host', host);
367
+ }
368
+
369
+ const discipline_profile = input.discipline_profile ?? null;
370
+ if (
371
+ discipline_profile !== null
372
+ && !(ALLOWED_DISCIPLINE_PROFILES as readonly string[]).includes(discipline_profile)
373
+ ) {
374
+ throw new ClassARecordError(
375
+ `discipline_profile must be one of ${ALLOWED_DISCIPLINE_PROFILES.join(', ')} or null`,
376
+ );
377
+ }
378
+
379
+ const package_version = input.package_version ?? null;
380
+ if (package_version !== null) {
381
+ check_id_redaction('package_version', package_version);
382
+ }
383
+
384
+ if (!Number.isInteger(input.occurrences) || input.occurrences < 1) {
385
+ throw new ClassARecordError('occurrences must be a positive integer');
386
+ }
387
+
388
+ return {
389
+ schema_version: CLASS_A_SCHEMA_VERSION,
390
+ record_class: 'self-repair',
391
+ ts_bucket: hour_bucket(input.now ?? new Date()),
392
+ defect_class: input.defect_class,
393
+ defect_source: input.defect_source,
394
+ occurrences: input.occurrences,
395
+ host,
396
+ package_version,
397
+ discipline_profile,
398
+ org_id: input.org_id,
399
+ user_hash: derive_user_hash(input.salt, input.hostname, input.username),
400
+ session_hash: derive_session_hash(input.salt, input.session_id),
401
+ };
402
+ }
403
+
282
404
  // ── Retention — the growth budget for the local record log ──────────────
283
405
  //
284
406
  // Phase 1 shipped this file as append-only with no cap and no pruning, and
@@ -544,6 +666,20 @@ export function enforce_retention(
544
666
  return { kept: kept_lines.length, dropped_by_age, dropped_by_size };
545
667
  }
546
668
 
669
+ /** Append one line to one bounded JSONL file. Both targets share it. */
670
+ function _append_and_bound(
671
+ target: string,
672
+ payload: string,
673
+ policy: RetentionPolicy,
674
+ now: Date,
675
+ ): void {
676
+ fs.mkdirSync(path.dirname(target) || '.', { recursive: true });
677
+ fs.appendFileSync(target, payload, { encoding: 'utf-8' });
678
+ if (retention_due(target, policy, now)) {
679
+ enforce_retention(target, policy, now);
680
+ }
681
+ }
682
+
547
683
  /**
548
684
  * Append one record as a JSONL line, then enforce the retention policy.
549
685
  *
@@ -555,17 +691,34 @@ export function enforce_retention(
555
691
  * which makes the common append one extra `stat` plus one bounded read. It
556
692
  * lives here rather than in a separate sweep because this is the only writer:
557
693
  * a policy a caller has to remember to run is a policy that stops running.
694
+ *
695
+ * THE SPOOL IS WRITTEN HERE, IN THE SAME CALL (Phase 2, step 2.1). When
696
+ * `spool_path` is given, the SAME bytes are appended to the outbound spool
697
+ * under the SAME retention policy, and the session-end flush then only has
698
+ * to drain a file. The alternative — a flush that works out for itself which
699
+ * logged records are still unsent — needs a byte watermark into a file that
700
+ * `enforce_retention` rewrites in place, and a watermark over a compacting
701
+ * file is a silent-corruption pair: the offset survives the prune, the
702
+ * records it points at do not. Enqueue-at-write has no watermark to
703
+ * invalidate and spools each record exactly once, by the only writer.
704
+ *
705
+ * The spool inherits the log's growth budget rather than getting its own,
706
+ * which is what answers Phase 0's unmeasured "queue growth bound across a
707
+ * multi-day outage": a sink that is down for days cannot push the spool past
708
+ * `max_bytes`. The cost of that bound is that the oldest UNSENT records are
709
+ * dropped rather than kept — the same trade the log already makes, stated
710
+ * here rather than discovered later in a truncated file.
558
711
  */
559
712
  export function append_class_a_record(
560
713
  log_path: string,
561
- record: ClassARecord,
714
+ record: ClassARecord | ClassADefectRecord,
562
715
  policy: RetentionPolicy = DEFAULT_RETENTION_POLICY,
563
716
  now: Date = new Date(),
717
+ spool_path: string | null = null,
564
718
  ): void {
565
719
  const payload = `${py_json_dumps_compact_sorted(record as unknown as Record<string, unknown>)}\n`;
566
- fs.mkdirSync(path.dirname(log_path) || '.', { recursive: true });
567
- fs.appendFileSync(log_path, payload, { encoding: 'utf-8' });
568
- if (retention_due(log_path, policy, now)) {
569
- enforce_retention(log_path, policy, now);
720
+ _append_and_bound(log_path, payload, policy, now);
721
+ if (spool_path !== null && spool_path !== '') {
722
+ _append_and_bound(spool_path, payload, policy, now);
570
723
  }
571
724
  }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Class-A transport — the enqueue half (road-to-org-telemetry Phase 2, step 2.1).
3
+ *
4
+ * WHY THIS IS NOT AN INLINE `fetch` AT SESSION END. Phase 0's second spike
5
+ * pre-registered "added latency at or below one second at p95" for an inline
6
+ * flush and measured it FAILING: a healthy sink costs 0.4 ms p95 and a
7
+ * refused connection 0.3 ms, but a BLACKHOLE — a socket that accepts and
8
+ * never answers, the shape a wedged sink actually takes — costs 1002 ms p95
9
+ * against a 1000 ms bar, because the timeout IS the cost. The step's own text
10
+ * defers to that result ("per the second spike's result"), and the result is:
11
+ * session end ENQUEUES ONLY, a detached sender flushes, and local retention
12
+ * carries what did not go out. The same spike measured that fallback at
13
+ * 20.5 ms p95 against the same blackhole — dominated by process spawn — and
14
+ * that is the number this module must not regress.
15
+ *
16
+ * SO THE SPOOL IS WRITTEN BY THE APPENDER, NOT BY THE FLUSH. A session-end
17
+ * flush that had to work out which records were already sent would need a
18
+ * byte watermark into the log, and the log is rewritten in place by
19
+ * `enforce_retention` — a watermark and a compacting file are a
20
+ * silent-corruption pair (offsets survive the prune; the records they point
21
+ * at do not). Enqueue-at-write has no watermark to invalidate: a record is
22
+ * spooled exactly once, by the only writer, in the same call that logs it.
23
+ *
24
+ * TWO PROPERTIES PHASE 0 SAID PHASE 2 MUST MEASURE, AND WHERE THEY LANDED.
25
+ * (1) Does a detached child survive teardown of the session process group —
26
+ * measured, see `agents/evidence/eval-findings/org-telemetry-p2-transport.md`.
27
+ * (2) The queue's growth bound across a multi-day outage — answered by
28
+ * construction here: the spool carries the SAME `RetentionPolicy` as the log,
29
+ * enforced by the same code, so a sink that is down for days cannot grow it
30
+ * past `max_bytes`. That bound is a decision to drop the oldest unsent
31
+ * records rather than to keep all of them, and it is stated rather than left
32
+ * for an operator to find in a truncated file.
33
+ *
34
+ * NO SINK EXISTS YET. `sink-choice` is transferred (a private repository is
35
+ * the council's preferred architecture, but creating it is an org-admin
36
+ * action outside this repository), so nothing in this tree can name a real
37
+ * endpoint. What ships here is the transport an org's own endpoint value
38
+ * activates; with no endpoint the settings never resolve `active` and not a
39
+ * byte moves.
40
+ */
41
+ import { spawn } from 'node:child_process';
42
+ import * as fs from 'node:fs';
43
+ import * as path from 'node:path';
44
+
45
+ import {
46
+ DEFAULT_RETENTION_POLICY,
47
+ enforce_retention,
48
+ retention_due,
49
+ type RetentionPolicy,
50
+ } from './remote.js';
51
+
52
+ /**
53
+ * The outbound-flush timeout, in ms.
54
+ *
55
+ * The step says "at or below one second"; this is the bar itself rather than
56
+ * a value under it, because the spike measured the timeout as the dominant
57
+ * cost only for an INLINE flush. In the detached child the wait costs the
58
+ * session nothing, so shortening it would only turn recoverable slow sinks
59
+ * into dropped batches.
60
+ */
61
+ export const DEFAULT_FLUSH_TIMEOUT_MS = 1000;
62
+
63
+ /** `never` means no transport at all — no spool is written. */
64
+ export const FLUSH_NEVER = 'never';
65
+
66
+ /** The one flush policy that spools and sends. */
67
+ export const FLUSH_SESSION_END = 'session-end';
68
+
69
+ /**
70
+ * The spool that sits beside a record log.
71
+ *
72
+ * Derived rather than configured: a second path key would let an install
73
+ * point the spool somewhere the appender does not write, and the failure —
74
+ * records logged locally, nothing ever sent — is invisible. `.jsonl` is
75
+ * replaced rather than appended to so the spool is not itself picked up by a
76
+ * reader globbing `*.jsonl` for records.
77
+ */
78
+ export function spool_path_for(log_path: string): string {
79
+ const dir = path.dirname(log_path);
80
+ const base = path.basename(log_path).replace(/\.jsonl$/u, '');
81
+ return path.join(dir, `${base}.spool.jsonl`);
82
+ }
83
+
84
+ /**
85
+ * Append one already-serialised record line to the spool and bound it.
86
+ *
87
+ * The line is passed in rather than re-serialised so the spooled bytes are
88
+ * byte-identical to the logged bytes: a sink and a local inspection of the
89
+ * same record must not be able to disagree.
90
+ */
91
+ export function enqueue_line(
92
+ spool_path: string,
93
+ line: string,
94
+ policy: RetentionPolicy = DEFAULT_RETENTION_POLICY,
95
+ now: Date = new Date(),
96
+ ): void {
97
+ fs.mkdirSync(path.dirname(spool_path) || '.', { recursive: true });
98
+ fs.appendFileSync(spool_path, line, { encoding: 'utf-8' });
99
+ if (retention_due(spool_path, policy, now)) {
100
+ enforce_retention(spool_path, policy, now);
101
+ }
102
+ }
103
+
104
+ export interface SpawnSenderInput {
105
+ /** Absolute path to `flush_sender.mjs`. */
106
+ readonly sender_script: string;
107
+ readonly spool_path: string;
108
+ readonly endpoint: string;
109
+ readonly timeout_ms?: number;
110
+ /** Node binary. Defaults to the one running this process. */
111
+ readonly node_path?: string;
112
+ }
113
+
114
+ /**
115
+ * Spawn the detached sender and return immediately.
116
+ *
117
+ * `detached: true` + `stdio: 'ignore'` + `unref()` is the exact shape the
118
+ * spike measured at 20.5 ms p95, and all three are load-bearing: without
119
+ * `detached` the child dies with the session's process group, without
120
+ * `ignore` the parent holds pipe fds open, and without `unref` the parent's
121
+ * event loop waits for the child — which would reintroduce the very latency
122
+ * the spool exists to remove.
123
+ *
124
+ * The sender is a PLAIN-NODE `.mjs` on purpose. Its siblings in this
125
+ * directory are TypeScript resolved by `tsx` at import time; a detached child
126
+ * has no such resolution to inherit, so a `.ts` sender would depend on a
127
+ * `tsx` binary being findable in a consumer install at teardown — the least
128
+ * observable moment there is. `process.execPath` is the node already running
129
+ * this hook, so it exists by construction.
130
+ *
131
+ * Returns `true` when the child was spawned. Never throws: a transport that
132
+ * can fail a session is worse than a transport that loses a batch, and the
133
+ * batch is not lost — it stays in the spool for the next flush.
134
+ */
135
+ export function spawn_detached_sender(input: SpawnSenderInput): boolean {
136
+ try {
137
+ const child = spawn(
138
+ input.node_path ?? process.execPath,
139
+ [
140
+ input.sender_script,
141
+ '--spool',
142
+ input.spool_path,
143
+ '--endpoint',
144
+ input.endpoint,
145
+ '--timeout',
146
+ String(input.timeout_ms ?? DEFAULT_FLUSH_TIMEOUT_MS),
147
+ ],
148
+ { detached: true, stdio: 'ignore' },
149
+ );
150
+ child.unref();
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Is there anything to send?
159
+ *
160
+ * Checked before the spawn so an idle session pays a `stat` rather than a
161
+ * process. Most sessions invoke no skill at all.
162
+ */
163
+ export function spool_has_work(spool_path: string): boolean {
164
+ try {
165
+ return fs.statSync(spool_path).size > 0;
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
@@ -59,11 +59,12 @@ export const REGISTRY = [
59
59
  { name: 'sessions:list', disposition: 'delegate', synopsis: 'List live agent sessions plus unmerged branches held by other worktrees; --json, --branches.' },
60
60
  { name: 'sessions:claim', disposition: 'delegate', synopsis: 'Claim a roadmap for this session so other sessions skip it; --release clears it.' },
61
61
  { name: 'session:recycle', disposition: 'delegate', synopsis: 'Validate + write the main-session recycle envelope (--verify validates only, --project names the repo); the successor resumes from it after /clear.' },
62
- { name: 'roadmap:progress', disposition: 'delegate', synopsis: 'Regenerate agents/roadmaps-progress.md from open roadmaps.' },
62
+ { name: 'roadmap:progress', disposition: 'delegate', synopsis: 'Regenerate agents/roadmaps-progress.md from open roadmaps; archives completed ones (--no-archive to skip).' },
63
63
  { name: 'roadmap:progress-check', disposition: 'delegate', synopsis: 'Fail if agents/roadmaps-progress.md is stale (for CI).' },
64
64
  { name: 'roadmap:archive', disposition: 'delegate', synopsis: 'Archive completed roadmaps (PR-gate sweep).' },
65
65
  { name: 'gates', disposition: 'delegate', synopsis: 'Open decisions that need you, as actions — owner-filtered roadmap blockers; --all, --json, --reply, --pending (staged requires_confirmation actions).' },
66
66
  { name: 'capabilities:index', disposition: 'delegate', synopsis: 'Regenerate CAPABILITIES.yaml (the package coverage index); --check for CI.' },
67
+ { name: 'adr:effective', disposition: 'delegate', synopsis: "Effective state of one ADR: status, Decision verbatim, clauses its own amendments superseded, active amendments, axes, trigger state. Authorizes nothing." },
67
68
  { name: 'affected', disposition: 'delegate', synopsis: 'Artefacts related to <artefact> via the discovery relation-graph (BFS).' },
68
69
  { name: 'graph-explain', disposition: 'delegate', synopsis: 'Seed on a <concept>, expand 2 hops over the discovery relation-graph with a node budget.' },
69
70
  { name: 'benchmark', disposition: 'delegate', synopsis: 'Report context-token reduction vs the full always-loaded projection.' },