@event4u/agent-config 15.0.0 → 16.1.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 (283) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +301 -347
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +77 -29
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  24. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
  25. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  26. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
  27. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  28. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  29. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  30. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  31. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  32. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  33. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  34. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  35. package/dist/agent-src/rules/delegation-policy.md +23 -45
  36. package/dist/agent-src/rules/design-fidelity.md +32 -37
  37. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  38. package/dist/agent-src/rules/test-first.md +41 -0
  39. package/dist/agent-src/rules/user-interaction.md +31 -30
  40. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  41. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  42. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  44. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  45. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  46. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
  47. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  48. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  51. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  52. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  53. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  54. package/dist/agent-src/templates/agent-settings.md +1 -1
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +188 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  58. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  59. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  60. package/dist/cli-delegate/check_update_banner.js +40 -2
  61. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  62. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  63. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  64. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  65. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  66. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  67. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  68. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  69. package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
  70. package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
  71. package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
  72. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  73. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  74. package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
  75. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  76. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  77. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  78. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  79. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  80. package/dist/cli-delegate/cmd_conformance.js +918 -325
  81. package/dist/cli-delegate/cmd_doctor.js +7 -5
  82. package/dist/cli-delegate/cmd_explain.js +5 -3
  83. package/dist/cli-delegate/cmd_export.js +9 -7
  84. package/dist/cli-delegate/cmd_fleet.js +4 -2
  85. package/dist/cli-delegate/cmd_handoff.js +6 -6
  86. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +3 -2
  89. package/dist/cli-delegate/cmd_preflight.js +4 -2
  90. package/dist/cli-delegate/cmd_prune.js +10 -8
  91. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  92. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  93. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  94. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  95. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  96. package/dist/cli-delegate/cmd_settings_set.js +14 -1
  97. package/dist/cli-delegate/cmd_sync.js +10 -8
  98. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  99. package/dist/cli-delegate/cmd_update.js +1 -1
  100. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  101. package/dist/cli-delegate/cmd_validate.js +12 -10
  102. package/dist/cli-delegate/cmd_versions.js +1 -1
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +87 -40
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +4 -4
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +10 -8
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +8 -6
  112. package/dist/hooks/dispatch.js +2601 -1073
  113. package/dist/install/claudeRuleRewrite.js +14 -3
  114. package/dist/install/conflict.js +60 -10
  115. package/dist/install/conflictTracker.js +218 -0
  116. package/dist/install/fsPrimitives.js +54 -0
  117. package/dist/install/install.mjs +2008 -1481
  118. package/dist/install/manifestFiles.js +259 -0
  119. package/dist/install/preflight.js +14 -2
  120. package/dist/install/preserve.js +157 -0
  121. package/dist/install/recordedOwnership.js +132 -0
  122. package/dist/install/txlog.js +11 -2
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/mcp/server.json +2 -2
  125. package/dist/mcp/server.mjs +1612 -667
  126. package/dist/router.json +1 -1
  127. package/dist/scripts/_lib/agent_settings.js +3 -2
  128. package/dist/scripts/_lib/fs_atomic.js +167 -0
  129. package/dist/scripts/_lib/installed_lock.js +1 -1
  130. package/dist/scripts/_lib/installed_tools.js +460 -0
  131. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  132. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  133. package/dist/server/io/yamlIO.js +50 -0
  134. package/dist/server/routes/install.js +8 -0
  135. package/dist/server/routes/wizard.js +9 -3
  136. package/dist/server/schemas/settings.js +17 -5
  137. package/dist/shared/missionExecution.js +97 -0
  138. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  139. package/dist/ui/index.html +1 -1
  140. package/docs/MIGRATION.md +103 -0
  141. package/docs/contracts/settings-classes.md +26 -7
  142. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  143. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  144. package/docs/guidelines/design-fidelity-routing.md +29 -0
  145. package/package.json +1 -1
  146. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  147. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  148. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  149. package/src/config/agent-settings.template.yml +139 -27
  150. package/src/config/auto-rule-budget.json +16 -0
  151. package/src/config/ci-local-parity.yml +18 -5
  152. package/src/config/continuity-surface.json +28 -0
  153. package/src/config/gate-coverage.yml +146 -13
  154. package/src/config/gate-violation-baselines.json +6 -6
  155. package/src/config/hook-token-budget.json +2 -0
  156. package/src/config/host-injection-effect.json +1 -1
  157. package/src/config/loop-surfaces.yaml +215 -0
  158. package/src/config/platform-anchor.json +3 -2
  159. package/src/config/preamble-payload-budget.json +12 -7
  160. package/src/config/preamble-payload-exceptions.json +28 -0
  161. package/src/config/release-gate-locality.yml +11 -0
  162. package/src/config/routing-coverage-seed.json +18 -4
  163. package/src/config/rule-enforcement-baseline.json +0 -1
  164. package/src/install/claudeRuleRewrite.ts +16 -2
  165. package/src/install/conflict.ts +65 -9
  166. package/src/install/conflictTracker.ts +299 -0
  167. package/src/install/fsPrimitives.ts +54 -0
  168. package/src/install/manifestFiles.ts +312 -0
  169. package/src/install/preflight.ts +16 -2
  170. package/src/install/preserve.ts +205 -0
  171. package/src/install/recordedOwnership.ts +149 -0
  172. package/src/install/txlog.ts +11 -2
  173. package/src/install/types.ts +19 -4
  174. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  175. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  176. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  177. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  178. package/src/scripts/_cli/doctor_execution.ts +90 -0
  179. package/src/scripts/_dispatch.bash +3 -1
  180. package/src/scripts/_lib/activation_payload.ts +247 -0
  181. package/src/scripts/_lib/agent_settings.ts +4 -2
  182. package/src/scripts/_lib/authority_path.ts +102 -0
  183. package/src/scripts/_lib/base_ref_payload.ts +315 -0
  184. package/src/scripts/_lib/cascade_base.ts +94 -0
  185. package/src/scripts/_lib/changelog_eras.ts +7 -2
  186. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  187. package/src/scripts/_lib/council_transport.ts +129 -0
  188. package/src/scripts/_lib/delivery_ready.ts +112 -0
  189. package/src/scripts/_lib/dropped_decision.ts +142 -0
  190. package/src/scripts/_lib/forge_protection.ts +267 -0
  191. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  192. package/src/scripts/_lib/installed_lock.ts +1 -1
  193. package/src/scripts/_lib/json_python_parity.ts +127 -0
  194. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  195. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  196. package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
  197. package/src/scripts/_lib/mission_record.ts +190 -0
  198. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  199. package/src/scripts/_lib/obligations.ts +402 -0
  200. package/src/scripts/_lib/orchestration_record.ts +11 -20
  201. package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
  202. package/src/scripts/_lib/release_findings_ingest.ts +324 -0
  203. package/src/scripts/_lib/release_highlights.ts +199 -10
  204. package/src/scripts/_lib/release_holds.ts +493 -0
  205. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  206. package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
  207. package/src/scripts/_lib/review_baseline.ts +254 -0
  208. package/src/scripts/_lib/review_skipped_record.ts +61 -1
  209. package/src/scripts/_lib/rides_along.ts +102 -0
  210. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  211. package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
  212. package/src/scripts/_lib/test_delta.ts +166 -0
  213. package/src/scripts/_lib/test_provenance.ts +171 -0
  214. package/src/scripts/_lib/turn_end_refusals.ts +51 -4
  215. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  216. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  217. package/src/scripts/ai_council/config.ts +8 -11
  218. package/src/scripts/ai_council/fallback_config.ts +5 -3
  219. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  220. package/src/scripts/ask_block_census.ts +285 -15
  221. package/src/scripts/check_beta_review_markers.ts +35 -4
  222. package/src/scripts/check_enforcement_matrix.ts +624 -0
  223. package/src/scripts/check_finding_dispositions.ts +167 -16
  224. package/src/scripts/check_gate_reachability.ts +173 -10
  225. package/src/scripts/check_generator_sync.ts +954 -0
  226. package/src/scripts/check_held_object_arrivals.ts +381 -0
  227. package/src/scripts/check_preamble_payload_budget.ts +474 -124
  228. package/src/scripts/check_release_highlights.ts +33 -1
  229. package/src/scripts/check_release_holds.ts +567 -0
  230. package/src/scripts/check_release_published.ts +1 -1
  231. package/src/scripts/check_reply_consistency.ts +86 -17
  232. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  233. package/src/scripts/check_test_delta.ts +294 -0
  234. package/src/scripts/check_test_weakening.ts +299 -0
  235. package/src/scripts/closure_scan.ts +333 -0
  236. package/src/scripts/compile_hook_manifest.ts +43 -3
  237. package/src/scripts/council_record_shape.ts +209 -0
  238. package/src/scripts/hook_manifest.json +1 -1
  239. package/src/scripts/hook_manifest.yaml +127 -12
  240. package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
  241. package/src/scripts/hooks/concern_registry.ts +6 -0
  242. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  243. package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
  244. package/src/scripts/hooks/evidence_independence.ts +96 -0
  245. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  246. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  247. package/src/scripts/hooks/review_baseline_hook.ts +130 -0
  248. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  249. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  250. package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
  251. package/src/scripts/hooks_doctor.ts +89 -1
  252. package/src/scripts/hooks_status.ts +19 -0
  253. package/src/scripts/install.ts +107 -207
  254. package/src/scripts/interruption_report.ts +8 -2
  255. package/src/scripts/janitor.ts +15 -1
  256. package/src/scripts/lint_decision_classes.ts +464 -0
  257. package/src/scripts/lint_hook_manifest.ts +11 -0
  258. package/src/scripts/lint_major_migration_sections.ts +450 -0
  259. package/src/scripts/lint_plan_risk_register.ts +47 -3
  260. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  261. package/src/scripts/lint_roadmap_producers.ts +202 -0
  262. package/src/scripts/measure_rule_budget.ts +101 -5
  263. package/src/scripts/measure_turn_end_gate.ts +302 -5
  264. package/src/scripts/new_roadmap.ts +37 -0
  265. package/src/scripts/orchestration_record.ts +0 -5
  266. package/src/scripts/release.ts +66 -162
  267. package/src/scripts/release_drill.ts +460 -15
  268. package/src/scripts/release_env.ts +1 -1
  269. package/src/scripts/release_publication.ts +52 -1
  270. package/src/scripts/report_enforcement_drift.ts +566 -0
  271. package/src/scripts/report_held_object_arrivals.ts +456 -0
  272. package/src/scripts/report_host_injection_effect.ts +3 -11
  273. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  274. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  275. package/src/scripts/schemas/command.schema.json +4 -0
  276. package/src/scripts/scope_growth.ts +219 -0
  277. package/src/scripts/sync_agent_settings.ts +140 -18
  278. package/src/scripts/trigger_eval_grandfather.json +0 -1
  279. package/src/scripts/ui_conformance_probe.ts +622 -0
  280. package/src/server/io/yamlIO.ts +48 -0
  281. package/src/server/schemas/settings.ts +31 -7
  282. package/src/shared/missionExecution.ts +126 -0
  283. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -82,6 +82,10 @@ function _strip_codespans(line: string): string {
82
82
  }
83
83
 
84
84
  /** Split on \n / \r\n / \r, dropping a single trailing empty element. */
85
+ export function splitAndMask(text: string): string[] {
86
+ return mask_fences(_splitlines(text));
87
+ }
88
+
85
89
  function _splitlines(text: string): string[] {
86
90
  if (text === '') return [];
87
91
  const parts = text.split(/\r\n|\r|\n/);
@@ -143,6 +147,64 @@ export function find_option_blocks(text: string): OptionBlock[] {
143
147
  return blocks;
144
148
  }
145
149
 
150
+ /** A recommendation line found in a block's adjacency window. */
151
+ export interface RecommendationLine {
152
+ /** 1-based line of the recommendation. */
153
+ line: number;
154
+ /** The number it names. Not necessarily one of the block's options. */
155
+ num: number;
156
+ }
157
+
158
+ /**
159
+ * The recommendation line(s) sitting in ONE block's adjacency window.
160
+ *
161
+ * Extracted from `check_reply` rather than copied, because it has a second
162
+ * caller: `turn_end_gate_hook`'s `pending-decision` detector needs the same
163
+ * question this answers — is this numbered block an ASK, or is it a narrative
164
+ * list? Iron Law 1 says the recommendation line IS the ask, so a block without
165
+ * one was never a decision put to the user.
166
+ *
167
+ * That distinction is not decoration. Replayed over this repository's own 592
168
+ * assistant turns, a detector keyed on the block alone fired 29 times and 8 of
169
+ * those blocks carried no recommendation line anywhere near them — plans,
170
+ * findings lists, ordinary enumerations. Requiring the line removes that whole
171
+ * class, and it removes it by applying the rule the gate already cites rather
172
+ * than by adding a heuristic beside it.
173
+ *
174
+ * Returns every recommendation in the window, not the first: `check_reply`
175
+ * treats two as its own finding, and a caller that only wanted existence can
176
+ * read `.length > 0`.
177
+ */
178
+ export function recommendationsUnder(
179
+ text: string,
180
+ block: OptionBlock,
181
+ maskedLines?: readonly string[],
182
+ ): RecommendationLine[] {
183
+ // `maskedLines` lets a caller with several blocks over one text mask once
184
+ // instead of once per block. It is the SAME array `check_reply` and
185
+ // `find_option_blocks` compute; passing anything else silently changes the
186
+ // line numbers this returns, which is why it is an optimisation parameter
187
+ // and not part of the contract.
188
+ const lines = maskedLines ?? mask_fences(_splitlines(text));
189
+ const recs: RecommendationLine[] = [];
190
+ let inspected = 0;
191
+ for (let idx = block.endLine; idx < lines.length && inspected < REC_ADJACENCY_WINDOW; idx++) {
192
+ const line = lines[idx]!;
193
+ if (line.trim() === '') {
194
+ continue; // blank lines do not consume the adjacency window
195
+ }
196
+ if (HEADING_RE.test(line) || OPTION_LINE_RE.test(line)) {
197
+ break; // a new heading or a new options block ends this block's window
198
+ }
199
+ inspected += 1;
200
+ const m = REC_LINE_RE.exec(line);
201
+ if (m) {
202
+ recs.push({ line: idx + 1, num: parseInt(m[1]!, 10) });
203
+ }
204
+ }
205
+ return recs;
206
+ }
207
+
146
208
  /**
147
209
  * Core check — the exported function tests drive. Returns all findings for
148
210
  * one reply draft; empty array = the draft is consistent.
@@ -166,22 +228,7 @@ export function check_reply(text: string): Finding[] {
166
228
  // Per-block: exactly one adjacent recommendation line, number in range.
167
229
  const blocks = find_option_blocks(text);
168
230
  for (const block of blocks) {
169
- const recs: Array<{ line: number; num: number }> = [];
170
- let inspected = 0;
171
- for (let idx = block.endLine; idx < lines.length && inspected < REC_ADJACENCY_WINDOW; idx++) {
172
- const line = lines[idx]!;
173
- if (line.trim() === '') {
174
- continue; // blank lines do not consume the adjacency window
175
- }
176
- if (HEADING_RE.test(line) || OPTION_LINE_RE.test(line)) {
177
- break; // a new heading or a new options block ends this block's window
178
- }
179
- inspected += 1;
180
- const m = REC_LINE_RE.exec(line);
181
- if (m) {
182
- recs.push({ line: idx + 1, num: parseInt(m[1]!, 10) });
183
- }
184
- }
231
+ const recs = recommendationsUnder(text, block);
185
232
  if (recs.length === 0) {
186
233
  findings.push({
187
234
  line: block.startLine,
@@ -421,7 +468,24 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
421
468
  return 2;
422
469
  }
423
470
 
471
+ // Bundle-safety: never auto-run when inlined into an esbuild bundle, where
472
+ // every module shares the bundle's `import.meta.url`.
473
+ //
474
+ // This module is not a hook, and until `turn_end_gate_hook` began importing
475
+ // `find_option_blocks` / `recommendationsUnder` from it, it was never inside
476
+ // one. It is now pulled into `dist/hooks/dispatch.js`, where without this guard
477
+ // the `import.meta.url === argvUrl` comparison below is TRUE for every bundled
478
+ // module — so importing this file would call `process.exit(main())` and take the
479
+ // whole dispatch down at import time. The build banner's `.__direct__` argv
480
+ // rewrite also prevents it, and a comment in the gate says so; a guard the other
481
+ // bundled entries all carry is the load-bearing half, and depending on the
482
+ // banner alone is depending on something no test pins.
483
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
484
+
424
485
  function _isCliEntry(): boolean {
486
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
487
+ return false;
488
+ }
425
489
  if (process.argv[1] === undefined) {
426
490
  return false;
427
491
  }
@@ -442,7 +506,12 @@ function _isCliEntry(): boolean {
442
506
  }
443
507
  }
444
508
 
445
- if (_isCliEntry() || process.argv[1] === _HERE) {
509
+ // The `|| process.argv[1] === _HERE` arm is the reason the bundle check lives in
510
+ // BOTH places: inside a bundle `_HERE` resolves to the bundle's own path and so
511
+ // does `process.argv[1]`, so this arm would be true and would route straight past
512
+ // a guard that only sat in `_isCliEntry`.
513
+ const _IN_BUNDLE = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
514
+ if (!_IN_BUNDLE && (_isCliEntry() || process.argv[1] === _HERE)) {
446
515
  process.exit(main());
447
516
  }
448
517
 
@@ -6,22 +6,32 @@
6
6
  * WHY THIS GATE EXISTS (road-to-conformance-round5 Phase 1, measured 2026-08-07)
7
7
  * ---------------------------------------------------------------------------
8
8
  * `docs/contracts/rule-router.md` (§ "The router earns its place as a
9
- * **compile-time** artifact") states that under every shipped default nothing
10
- * loads `dist/router.json` at runtime, so **projection is the only reach
11
- * mechanism there is**:
9
+ * **compile-time** artifact") used to state that under every shipped default
10
+ * nothing loads `dist/router.json` at runtime, so **projection is the only
11
+ * reach mechanism there is**. Both halves of that have since been corrected,
12
+ * and the gate outlived both:
12
13
  *
13
14
  * CORRECTED 2026-08-26. This docblock used to cite that section's "measured:
14
15
  * zero consumers under `src/scripts/hooks/`", and that measurement is stale: the
15
16
  * `rule-inject` concern reads the router via `_lib/rule_injection.ts:76-79` and
16
- * is bound on three slots. The gate's rationale survives the correction because
17
- * the concern is DEFAULT-OFF and returns before reading the router unless
18
- * `lean_projection.mode: delivery` is set — so on a shipped default, projection
19
- * IS still the only reach mechanism. What no longer holds is the stronger claim
20
- * that no such mechanism exists. Stated here rather than quietly re-worded,
21
- * because a gate whose stated reason rests on a refuted premise is exactly the
22
- * decoration ADR-127 rejects: a non-kernel rule activates by the model's judgment over text
23
- * already in context, and the per-tool rule tree is what puts it there. That
24
- * makes the state of the projection load-bearing, and it was broken:
17
+ * is bound on three slots. What no longer holds is the stronger claim that no
18
+ * such mechanism exists.
19
+ *
20
+ * CORRECTED AGAIN 2026-09-12. The 2026-08-26 note rescued the rationale with
21
+ * "the concern is DEFAULT-OFF … so on a shipped default, projection IS still
22
+ * the only reach mechanism". That rescue is now false and the clause is gone:
23
+ * since ADR-267 the shipped template carries `lean_projection.mode: delivery`
24
+ * with `hosts: [claude-code]`, so on the shipped default that one host does
25
+ * reach rule bodies at runtime. The gate never depended on the rescue — its two
26
+ * assertions below are about the emit plan and the entry mtimes, and a thinned
27
+ * tree of stubs is exactly as load-bearing as a tree of bodies: a missing entry
28
+ * is a rule the model neither reads nor has a pointer to, in either mode.
29
+ *
30
+ * Both are stated rather than quietly re-worded, because a gate whose stated
31
+ * reason rests on a refuted premise is exactly the decoration ADR-127 rejects:
32
+ * a non-kernel rule activates by the model's judgment over text already in
33
+ * context, and the per-tool rule tree is what puts it there. That makes the
34
+ * state of the projection load-bearing, and it was broken:
25
35
  *
26
36
  * - `.claude/rules/` in the working checkout held **92** entries. A
27
37
  * regeneration in a clean worktree produces **108**. The tree was last
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * check_test_delta — a code change with no credible test delta is a finding.
4
+ *
5
+ * `road-to-adversarial-verification-and-long-runs` Phase 2.4, first of two. The
6
+ * rule `test-first` states that a behaviour change gets a failing test first;
7
+ * nothing could see whether one arrived. This gate sees the cheap half: a diff
8
+ * that touches production code and touches no test at all.
9
+ *
10
+ * **What it deliberately cannot see.** Whether the test came FIRST, and whether
11
+ * it tests the thing that changed. Both are judgements about intent and content
12
+ * that no diff carries, and claiming them here would be the coverage inflation
13
+ * `evaluator-independence` exists over. This gate proves an ABSENCE, which is
14
+ * the one thing about a diff that is mechanically decidable.
15
+ *
16
+ * **The exemption is owner-set, and that is Risk 4's mitigation.** A gate that
17
+ * reds a legitimate PR invites the cheapest repair — widening the exemption until
18
+ * the gate finds nothing. So the escape is a PR LABEL a human applies, mirroring
19
+ * `check_kernel_rule_bundle`'s `bundled-always-rules-acknowledged`: it is visible
20
+ * on the PR, it is one click for the owner, and no agent can apply it by editing
21
+ * a file in the diff under review.
22
+ *
23
+ * Usage:
24
+ * check_test_delta [--files F …] [--base-ref REF] [--root DIR]
25
+ * [--label NAME] [--event-path P] [--quiet] [--self-test]
26
+ *
27
+ * Exit codes: 0 = pass (or exempted) · 1 = fail · 2 = usage error / dead scope.
28
+ */
29
+
30
+ import { spawnSync } from 'node:child_process';
31
+ import * as fs from 'node:fs';
32
+ import * as os from 'node:os';
33
+ import * as path from 'node:path';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
+
36
+ import { GateLedger } from './_lib/gate_ledger.js';
37
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
38
+ import { reportScanned } from './_lib/scan_scope.js';
39
+ import { isCodePath, isTestPath } from './_lib/test_delta.js';
40
+
41
+ const _HERE = fileURLToPath(import.meta.url);
42
+ const REPO = path.resolve(path.dirname(_HERE), '..', '..');
43
+
44
+ /** The label an owner applies to accept a code change with no test delta. */
45
+ export const DEFAULT_LABEL = 'test-delta-acknowledged';
46
+
47
+ export interface DeltaVerdict {
48
+ readonly code: string[];
49
+ readonly tests: string[];
50
+ readonly scanned: number;
51
+ }
52
+
53
+ /** Classify a changed-file list. Pure — the whole decision, without I/O. */
54
+ export function classify(files: readonly string[]): DeltaVerdict {
55
+ return {
56
+ code: files.filter(isCodePath),
57
+ tests: files.filter(isTestPath),
58
+ scanned: files.length,
59
+ };
60
+ }
61
+
62
+ /** The finding, or `null`. Separated from rendering so a test reads the decision. */
63
+ export function findingFor(v: DeltaVerdict): string | null {
64
+ if (v.code.length === 0) return null;
65
+ if (v.tests.length > 0) return null;
66
+ const shown = v.code.slice(0, 5).join(', ');
67
+ const more = v.code.length > 5 ? `, +${String(v.code.length - 5)} more` : '';
68
+ return (
69
+ `${String(v.code.length)} code path(s) changed and no test path did: ${shown}${more}. ` +
70
+ `A behaviour change gets a test (src/rules/test-first.md). If this change genuinely ` +
71
+ `has no testable behaviour, apply the \`${DEFAULT_LABEL}\` label to the PR — that is ` +
72
+ `an owner decision, deliberately not a file an agent can edit inside this diff.`
73
+ );
74
+ }
75
+
76
+ function gitChangedFiles(baseRef: string, root: string): string[] | null {
77
+ const r = spawnSync('git', ['diff', '--name-only', `${baseRef}...HEAD`], {
78
+ cwd: root,
79
+ encoding: 'utf8',
80
+ });
81
+ if (r.status !== 0) return null;
82
+ return r.stdout
83
+ .split('\n')
84
+ .map((s) => s.trim())
85
+ .filter((s) => s !== '');
86
+ }
87
+
88
+ function resolveBaseRef(explicit: string | null, root: string): string {
89
+ if (explicit !== null) return explicit;
90
+ for (const ref of ['origin/main', 'origin/master', 'main', 'master']) {
91
+ const r = spawnSync('git', ['rev-parse', '--verify', '--quiet', ref], {
92
+ cwd: root,
93
+ encoding: 'utf8',
94
+ });
95
+ if (r.status === 0) return ref;
96
+ }
97
+ return 'HEAD~1';
98
+ }
99
+
100
+ /** PR labels from the GitHub event JSON, or `[]` when there is none to read. */
101
+ export function labelsFrom(eventPath: string | null): string[] {
102
+ if (eventPath === null || eventPath === '') return [];
103
+ try {
104
+ const raw = JSON.parse(fs.readFileSync(eventPath, 'utf8')) as Record<string, unknown>;
105
+ const pr = raw['pull_request'] as { labels?: unknown } | undefined;
106
+ const labels = Array.isArray(pr?.labels) ? pr.labels : [];
107
+ return labels
108
+ .map((l) => (l as { name?: unknown }).name)
109
+ .filter((n): n is string => typeof n === 'string');
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ function usage(): string {
116
+ return (
117
+ 'usage: check_test_delta [--files F …] [--base-ref REF] [--root DIR]\n' +
118
+ ' [--label NAME] [--event-path P] [--quiet] [--self-test]\n'
119
+ );
120
+ }
121
+
122
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
123
+ if (argv.includes('--help') || argv.includes('-h')) {
124
+ process.stdout.write(usage());
125
+ return 0;
126
+ }
127
+ if (argv.includes('--self-test')) return selfTest();
128
+
129
+ const quiet = argv.includes('--quiet');
130
+ const at = (flag: string): string | null => {
131
+ const i = argv.indexOf(flag);
132
+ return i >= 0 ? (argv[i + 1] ?? null) : null;
133
+ };
134
+ const root = at('--root') ?? REPO;
135
+ const label = at('--label') ?? DEFAULT_LABEL;
136
+ const eventPath = at('--event-path') ?? process.env['GITHUB_EVENT_PATH'] ?? null;
137
+
138
+ let files: string[];
139
+ const fi = argv.indexOf('--files');
140
+ if (fi >= 0) {
141
+ files = argv.slice(fi + 1).filter((a) => !a.startsWith('--'));
142
+ } else {
143
+ const fromGit = gitChangedFiles(resolveBaseRef(at('--base-ref'), root), root);
144
+ if (fromGit === null) {
145
+ process.stderr.write('check_test_delta: git diff failed\n');
146
+ return 2;
147
+ }
148
+ files = fromGit;
149
+ }
150
+
151
+ // Per-path accounting, so "no code changed" and "code changed and nothing
152
+ // was looked at" stop printing the same green line. Every changed path is a
153
+ // planned target: a code path completes or fails, and everything else is out
154
+ // of scope by classification rather than silently skipped.
155
+ const ledger = new GateLedger('check_test_delta');
156
+ ledger.plan(files.map((f) => `path:${f}`));
157
+
158
+ const verdict = classify(files);
159
+ const finding0 = findingFor(verdict);
160
+ for (const f of files) {
161
+ const target = `path:${f}`;
162
+ if (!isCodePath(f)) {
163
+ ledger.outOfScope(target, 'not_applicable_kind');
164
+ continue;
165
+ }
166
+ if (finding0 === null) ledger.complete(target);
167
+ else ledger.fail(target, `${f}: no test path changed alongside it`);
168
+ }
169
+ ledger.report();
170
+
171
+ reportScanned({
172
+ gate: 'check_test_delta',
173
+ scanned: verdict.scanned,
174
+ units: 'changed path(s)',
175
+ roots: ['<the diff against the base ref>'],
176
+ // A diff with nothing in it is a real state — a docs-only branch, or a
177
+ // re-run against an already-merged base — and it is not this gate's job
178
+ // to red on it. Declared rather than silently passing a zero.
179
+ allowEmpty: 'EMPTY_VALID: a diff with no changed files carries no code change to gate',
180
+ });
181
+
182
+ const finding = finding0;
183
+ if (finding === null) {
184
+ if (!quiet) {
185
+ process.stdout.write(
186
+ `✅ check_test_delta: ${String(verdict.code.length)} code path(s), ` +
187
+ `${String(verdict.tests.length)} test path(s)\n`,
188
+ );
189
+ }
190
+ return 0;
191
+ }
192
+
193
+ if (labelsFrom(eventPath).includes(label)) {
194
+ process.stdout.write(
195
+ `✅ check_test_delta: exempted by the \`${label}\` label — ` +
196
+ 'an owner accepted a code change with no test delta.\n',
197
+ );
198
+ return 0;
199
+ }
200
+
201
+ process.stderr.write(`❌ check_test_delta: ${finding}\n`);
202
+ return 1;
203
+ }
204
+
205
+ function selfTest(): number {
206
+ const run = (files: Record<string, string>, args: readonly string[] = []): number => {
207
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ctd-'));
208
+ try {
209
+ return runGateCli(
210
+ REPO,
211
+ 'src/scripts/check_test_delta.ts',
212
+ ['--root', dir, '--quiet', ...args],
213
+ dir,
214
+ );
215
+ } finally {
216
+ fs.rmSync(dir, { recursive: true, force: true });
217
+ void files;
218
+ }
219
+ };
220
+ const eventWith = (labels: string[]): string => {
221
+ const f = path.join(
222
+ fs.mkdtempSync(path.join(os.tmpdir(), 'ctd-ev-')),
223
+ 'event.json',
224
+ );
225
+ fs.writeFileSync(f, JSON.stringify({ pull_request: { labels: labels.map((name) => ({ name })) } }));
226
+ return f;
227
+ };
228
+
229
+ return runSelfTest({
230
+ gate: 'check_test_delta',
231
+ minCases: 6,
232
+ minRejectCases: 2,
233
+ cases: [
234
+ {
235
+ name: 'code change with no test delta is REJECTED',
236
+ expect: 'reject',
237
+ run: () => run({}, ['--files', 'src/scripts/thing.ts']),
238
+ },
239
+ {
240
+ name: 'several code changes with no test delta are REJECTED',
241
+ expect: 'reject',
242
+ run: () => run({}, ['--files', 'src/a.ts', 'src/b.ts', 'docs/x.md']),
243
+ },
244
+ {
245
+ name: 'code change WITH a test delta passes',
246
+ expect: 'accept',
247
+ run: () => run({}, ['--files', 'src/scripts/thing.ts', 'tests/scripts/thing.test.ts']),
248
+ },
249
+ {
250
+ name: 'a co-located *.test.ts counts as the test delta',
251
+ expect: 'accept',
252
+ run: () => run({}, ['--files', 'src/a.ts', 'src/a.test.ts']),
253
+ },
254
+ {
255
+ name: 'docs-only change is not a code change',
256
+ expect: 'accept',
257
+ run: () => run({}, ['--files', 'docs/x.md', 'README.md']),
258
+ },
259
+ {
260
+ name: 'a generated projection is not an independent code change',
261
+ expect: 'accept',
262
+ run: () => run({}, ['--files', 'dist/agent-src/scripts/thing.ts']),
263
+ },
264
+ {
265
+ name: 'the owner label exempts a real finding',
266
+ expect: 'accept',
267
+ run: () =>
268
+ run({}, [
269
+ '--files',
270
+ 'src/scripts/thing.ts',
271
+ '--event-path',
272
+ eventWith([DEFAULT_LABEL]),
273
+ ]),
274
+ },
275
+ {
276
+ name: 'an unrelated label does NOT exempt it',
277
+ expect: 'reject',
278
+ run: () =>
279
+ run({}, [
280
+ '--files',
281
+ 'src/scripts/thing.ts',
282
+ '--event-path',
283
+ eventWith(['documentation']),
284
+ ]),
285
+ },
286
+ ],
287
+ });
288
+ }
289
+
290
+ /* c8 ignore start — CLI entry, exercised by the self-test rather than in-process. */
291
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1] ?? '')).href) {
292
+ process.exit(main());
293
+ }
294
+ /* c8 ignore stop */