@event4u/agent-config 16.1.0 → 16.2.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 (217) 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 +237 -248
  5. package/dist/agent-src/commands/roadmap/process-full.md +1 -1
  6. package/dist/agent-src/commands/work.md +4 -0
  7. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  8. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  11. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  12. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  13. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
  14. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  15. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  16. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  17. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/agent-src/templates/roadmaps.md +30 -0
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  24. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  25. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  26. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  27. package/dist/cli/python/knowledge_ingest.js +1 -1
  28. package/dist/cli/python/workspace_drive.js +1 -1
  29. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  30. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
  31. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
  32. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
  33. package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
  34. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  35. package/dist/cli-delegate/cmd_conformance.js +902 -782
  36. package/dist/cli-delegate/cmd_doctor.js +2 -2
  37. package/dist/cli-delegate/cmd_export.js +2 -2
  38. package/dist/cli-delegate/cmd_handoff.js +4 -4
  39. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  42. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  44. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  45. package/dist/cli-delegate/cmd_sync.js +2 -2
  46. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  47. package/dist/cli-delegate/cmd_validate.js +2 -2
  48. package/dist/discovery/deprecation-report.md +1 -1
  49. package/dist/discovery/discovery-manifest.json +10 -10
  50. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  51. package/dist/discovery/discovery-manifest.summary.md +1 -1
  52. package/dist/discovery/dormancy-report.md +1 -1
  53. package/dist/discovery/orphan-report.md +1 -1
  54. package/dist/discovery/packs.json +2 -2
  55. package/dist/discovery/trust-report.md +1 -1
  56. package/dist/discovery/workspaces.json +2 -2
  57. package/dist/hooks/dispatch.js +6666 -2581
  58. package/dist/hooks/dispatch.sha256 +1 -0
  59. package/dist/install/install.mjs +848 -733
  60. package/dist/mcp/registry-manifest.json +2 -2
  61. package/dist/mcp/server.json +2 -2
  62. package/dist/mcp/server.mjs +2747 -1510
  63. package/dist/server/schemas/settings.js +23 -0
  64. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  65. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  66. package/dist/ui/index.html +2 -2
  67. package/docs/contracts/settings-classes.md +6 -4
  68. package/package.json +4 -4
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  73. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  74. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  75. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  76. package/src/config/agent-settings.template.yml +29 -0
  77. package/src/config/ci-local-parity.yml +18 -0
  78. package/src/config/continuity-surface.json +2 -2
  79. package/src/config/gate-coverage.yml +234 -3
  80. package/src/config/gate-violation-baselines.json +7 -3
  81. package/src/config/hook-latency-budget.json +21 -0
  82. package/src/config/hook-token-budget.json +2 -2
  83. package/src/config/metric-registry.yml +114 -0
  84. package/src/config/placeholder-drift-budget.json +10 -0
  85. package/src/config/routing-coverage-seed.json +18 -4
  86. package/src/scripts/_cli/cmd_doctor.ts +5 -19
  87. package/src/scripts/_cli/doctor_execution.ts +100 -17
  88. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  89. package/src/scripts/_lib/body_portable.ts +102 -0
  90. package/src/scripts/_lib/cc_transcript.ts +132 -1
  91. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  92. package/src/scripts/_lib/forge_protection.ts +114 -0
  93. package/src/scripts/_lib/forge_reader.ts +558 -0
  94. package/src/scripts/_lib/host_env_write.ts +111 -0
  95. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  96. package/src/scripts/_lib/obligations.ts +119 -8
  97. package/src/scripts/_lib/probe_inputs.ts +271 -0
  98. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  99. package/src/scripts/_lib/release_highlights.ts +2 -1
  100. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  101. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  102. package/src/scripts/_lib/structural_hiding.ts +622 -0
  103. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  104. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  105. package/src/scripts/_lib/verification_command.ts +324 -0
  106. package/src/scripts/_lib/verification_evidence.ts +647 -0
  107. package/src/scripts/_lib/verify_clause.ts +280 -0
  108. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  109. package/src/scripts/before_complete_hook.ts +340 -23
  110. package/src/scripts/bench_hook_latency.ts +412 -5
  111. package/src/scripts/cache_realization_report.ts +61 -1
  112. package/src/scripts/check_argument_hint.ts +287 -0
  113. package/src/scripts/check_claims.ts +125 -1
  114. package/src/scripts/check_enforcement_coverage.ts +38 -9
  115. package/src/scripts/check_enforcement_matrix.ts +245 -3
  116. package/src/scripts/check_finding_dispositions.ts +22 -3
  117. package/src/scripts/check_generator_sync.ts +71 -0
  118. package/src/scripts/check_host_docs_digest.ts +786 -0
  119. package/src/scripts/check_host_format_column.ts +559 -0
  120. package/src/scripts/check_input_references.ts +228 -0
  121. package/src/scripts/check_inputs_parity.ts +235 -0
  122. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  123. package/src/scripts/check_kill_switch_table.ts +294 -0
  124. package/src/scripts/check_placeholder_drift.ts +221 -0
  125. package/src/scripts/check_read_surface_coverage.ts +515 -0
  126. package/src/scripts/check_release_holds.ts +2 -1
  127. package/src/scripts/check_routing_coverage.ts +378 -11
  128. package/src/scripts/check_static_parity.ts +17 -0
  129. package/src/scripts/ci_settle.ts +145 -19
  130. package/src/scripts/closure_scan.ts +115 -1
  131. package/src/scripts/compile_hook_manifest.ts +60 -16
  132. package/src/scripts/external_sources_denylist.json +29 -4
  133. package/src/scripts/hook_manifest.json +1 -1
  134. package/src/scripts/hook_manifest.yaml +42 -5
  135. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  136. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  137. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  138. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  139. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  140. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  141. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  142. package/src/scripts/hooks/concern_registry.ts +2 -0
  143. package/src/scripts/hooks/concern_timings.ts +94 -0
  144. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  145. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  146. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  147. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  148. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  149. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  150. package/src/scripts/hooks/evidence_independence.ts +1 -2
  151. package/src/scripts/hooks/exit_codes.ts +128 -0
  152. package/src/scripts/hooks/host_lowering.json +1 -0
  153. package/src/scripts/hooks/host_lowering.ts +128 -3
  154. package/src/scripts/hooks/host_lowering.yaml +262 -46
  155. package/src/scripts/hooks/host_semantics.ts +30 -2
  156. package/src/scripts/hooks/injection_budget.ts +9 -2
  157. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  158. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  159. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  160. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  161. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  162. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  163. package/src/scripts/hooks/permission_gate.ts +1 -2
  164. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  165. package/src/scripts/hooks/replay_hook.ts +2 -1
  166. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  167. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  168. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  169. package/src/scripts/hooks/rule_inject_hook.ts +8 -5
  170. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  171. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  172. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  173. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  174. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  175. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  176. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  178. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  179. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  181. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  182. package/src/scripts/install.ts +3 -4
  183. package/src/scripts/lint_deny_text.ts +419 -0
  184. package/src/scripts/lint_exit_codes.ts +262 -0
  185. package/src/scripts/lint_hook_manifest.ts +55 -0
  186. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  187. package/src/scripts/lint_skill_descriptions.ts +1 -1
  188. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  189. package/src/scripts/mcp_server/prompts.ts +142 -1
  190. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  191. package/src/scripts/measure_turn_end_gate.ts +193 -0
  192. package/src/scripts/memory_lookup.ts +1 -1
  193. package/src/scripts/prepack-check.mjs +53 -0
  194. package/src/scripts/release_publication.ts +13 -1
  195. package/src/scripts/report_invocation_surface.ts +383 -0
  196. package/src/scripts/report_obligation_carriers.ts +699 -0
  197. package/src/scripts/report_obligation_writer_split.ts +236 -0
  198. package/src/scripts/report_skill_menu_census.ts +208 -0
  199. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  200. package/src/scripts/roadmap_verify_share.ts +213 -0
  201. package/src/scripts/schemas/command.schema.json +44 -0
  202. package/src/scripts/schemas/review-findings.schema.json +4 -0
  203. package/src/scripts/schemas/skill.schema.json +44 -0
  204. package/src/scripts/self_review_gate.ts +359 -10
  205. package/src/scripts/semantic_parity_census.ts +314 -0
  206. package/src/scripts/session_eol_report.ts +54 -1
  207. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  208. package/src/scripts/trigger_eval_grandfather.json +0 -2
  209. package/src/scripts/trigger_eval_rotation.ts +122 -27
  210. package/src/scripts/ui_conformance_probe.ts +156 -4
  211. package/src/scripts/update_prices.ts +85 -3
  212. package/src/scripts/write_bundle_digest.mjs +74 -0
  213. package/src/server/schemas/settings.ts +31 -0
  214. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  215. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  216. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  217. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -8,6 +8,50 @@
8
8
  "additionalProperties": false,
9
9
  "required": ["name", "description", "disable-model-invocation"],
10
10
  "properties": {
11
+ "inputs": {
12
+ "type": "array",
13
+ "minItems": 1,
14
+ "description": "Structured declaration of the parameters this artifact takes. OPTIONAL on every artifact: one with no block validates unchanged, so nothing existing breaks and no migration is owed. Where the block IS present it is the single authority — the argument-hint and the MCP `arguments` set are derived from it, and a hand-written hint on the same artifact is a conflict rather than a merge (road-to-an-invocation-contract-that-reaches-the-wire).",
15
+ "items": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": ["name", "type"],
19
+ "properties": {
20
+ "name": {
21
+ "type": "string",
22
+ "pattern": "^[a-z][a-z0-9_]*$",
23
+ "description": "Parameter name. Lowercase snake_case, so it is safe both as a wire key and as an in-body ${name} reference."
24
+ },
25
+ "type": {
26
+ "enum": ["string", "number", "boolean", "enum", "path"],
27
+ "description": "Closed set on purpose: an unknown type must FAIL validation rather than reach a host that cannot interpret it."
28
+ },
29
+ "required": {
30
+ "type": "boolean",
31
+ "description": "Absent means optional."
32
+ },
33
+ "default": {
34
+ "description": "Value used when the caller supplies none. Any JSON type — it is checked against `type` by the consuming generator, not here."
35
+ },
36
+ "enum": {
37
+ "type": "array",
38
+ "minItems": 1,
39
+ "items": {
40
+ "type": "string",
41
+ "minLength": 1,
42
+ "pattern": "^[^|\\]>]+$",
43
+ "description": "A value. `|`, `]` and `>` are excluded because they are the delimiters of the derived argument-hint — permitting them would make the hint ambiguous and break the round-trip the hint gate asserts."
44
+ },
45
+ "description": "Allowed values. Only meaningful when `type` is `enum`."
46
+ },
47
+ "description": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "description": "One line, shown to a caller choosing a value."
51
+ }
52
+ }
53
+ }
54
+ },
11
55
  "name": {
12
56
  "type": "string",
13
57
  "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
@@ -127,6 +127,10 @@
127
127
  "type": "string",
128
128
  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
129
129
  "description": "ISO calendar date of the disposition."
130
+ },
131
+ "contradicted": {
132
+ "type": "string",
133
+ "description": "The tree's mechanical disproof of the finding's premise, written by self_review_gate.annotateContradicted when a deletion claim names an artifact the range does not delete and the working tree still holds. A finding carrying this is NOT merge-blocking in either gate (self_review_gate.classifyBlocking and check_finding_dispositions.isBlocking both read it), and it is still reported so a human sees what was claimed and why it was set aside."
130
134
  }
131
135
  }
132
136
  }
@@ -13,6 +13,50 @@
13
13
  "domain"
14
14
  ],
15
15
  "properties": {
16
+ "inputs": {
17
+ "type": "array",
18
+ "minItems": 1,
19
+ "description": "Structured declaration of the parameters this artifact takes. OPTIONAL on every artifact: one with no block validates unchanged, so nothing existing breaks and no migration is owed. Where the block IS present it is the single authority — the argument-hint and the MCP `arguments` set are derived from it, and a hand-written hint on the same artifact is a conflict rather than a merge (road-to-an-invocation-contract-that-reaches-the-wire).",
20
+ "items": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": ["name", "type"],
24
+ "properties": {
25
+ "name": {
26
+ "type": "string",
27
+ "pattern": "^[a-z][a-z0-9_]*$",
28
+ "description": "Parameter name. Lowercase snake_case, so it is safe both as a wire key and as an in-body ${name} reference."
29
+ },
30
+ "type": {
31
+ "enum": ["string", "number", "boolean", "enum", "path"],
32
+ "description": "Closed set on purpose: an unknown type must FAIL validation rather than reach a host that cannot interpret it."
33
+ },
34
+ "required": {
35
+ "type": "boolean",
36
+ "description": "Absent means optional."
37
+ },
38
+ "default": {
39
+ "description": "Value used when the caller supplies none. Any JSON type — it is checked against `type` by the consuming generator, not here."
40
+ },
41
+ "enum": {
42
+ "type": "array",
43
+ "minItems": 1,
44
+ "items": {
45
+ "type": "string",
46
+ "minLength": 1,
47
+ "pattern": "^[^|\\]>]+$",
48
+ "description": "A value. `|`, `]` and `>` are excluded because they are the delimiters of the derived argument-hint — permitting them would make the hint ambiguous and break the round-trip the hint gate asserts."
49
+ },
50
+ "description": "Allowed values. Only meaningful when `type` is `enum`."
51
+ },
52
+ "description": {
53
+ "type": "string",
54
+ "minLength": 1,
55
+ "description": "One line, shown to a caller choosing a value."
56
+ }
57
+ }
58
+ }
59
+ },
16
60
  "name": {
17
61
  "type": "string",
18
62
  "pattern": "^[a-z][a-z0-9-]*$",
@@ -50,6 +50,15 @@ export interface Finding {
50
50
  title: string;
51
51
  detail: string;
52
52
  file?: string;
53
+ /**
54
+ * Set when the tree mechanically DISPROVES the finding's premise — today
55
+ * only for the deletion class: the finding says an artifact was removed,
56
+ * the range deletes nothing by that name, and the artifact is still in the
57
+ * tree. Carries the disproof, so the finding stays readable rather than
58
+ * disappearing. A finding carrying this is not merge-blocking; see
59
+ * `classifyBlocking`.
60
+ */
61
+ contradicted?: string;
53
62
  }
54
63
 
55
64
  /**
@@ -74,6 +83,13 @@ export function findingId(f: Pick<Finding, 'kind' | 'title' | 'file'>): string {
74
83
  * block ONLY on the narrow security/claim × {critical,high} intersection.
75
84
  */
76
85
  export function classifyBlocking(f: Finding): boolean {
86
+ // A premise the tree DISPROVES cannot block a merge. This is not leniency
87
+ // and not a heuristic: `contradicted` is set only where a deletion claim
88
+ // was checked against `git diff --name-status base...HEAD` AND against the
89
+ // working tree, and both said the named artifact is present and undeleted.
90
+ // See `contradictedByTree` for why this class needed a mechanical disproof
91
+ // rather than one more sentence in the prompt.
92
+ if ((f.contradicted ?? '').trim() !== '') return false;
77
93
  return (f.kind === 'security' || f.kind === 'claim') && (f.severity === 'critical' || f.severity === 'high');
78
94
  }
79
95
 
@@ -120,6 +136,271 @@ function changedFiles(baseRef: string, cwd: string = REPO_ROOT): string[] {
120
136
  .filter(isReviewablePath);
121
137
  }
122
138
 
139
+ /**
140
+ * The paths the range actually DELETES, from `git diff --name-status`.
141
+ *
142
+ * Deliberately NOT filtered by `isReviewablePath`: the question this answers is
143
+ * "did the range remove this artifact", and a removal under `dist/agent-src/`
144
+ * is a removal even though the projection is never sent to the reviewer. A
145
+ * filter here would turn a real deletion into a disproof of itself.
146
+ *
147
+ * Renames are reported by `--name-status` as `R<score>\told\tnew`, and the old
148
+ * path IS gone, so it is counted. A roadmap moved into `archive/` shows up as a
149
+ * genuine deletion of its old path, which is exactly the reading a finding
150
+ * about that path deserves.
151
+ */
152
+ export function rangeDeletions(baseRef: string, cwd: string = REPO_ROOT): Set<string> {
153
+ return parseDeletions(nameStatusText(baseRef, cwd));
154
+ }
155
+
156
+ /**
157
+ * The paths the range MODIFIES, adds, or renames TO — everything it touched
158
+ * that still has a path afterwards.
159
+ *
160
+ * The complement of `rangeDeletions` over the same `--name-status` output, and
161
+ * the input `contradictedByTree` needs to tell "the range did not touch this"
162
+ * from "the range changed the inside of this". Unfiltered for the same reason
163
+ * `rangeDeletions` is: the question is what the RANGE did, not what the
164
+ * reviewer was sent.
165
+ */
166
+ export function rangeModifications(baseRef: string, cwd: string = REPO_ROOT): Set<string> {
167
+ return parseModifications(nameStatusText(baseRef, cwd));
168
+ }
169
+
170
+ /** One `--name-status` read, shared by both parses so the range is read once. */
171
+ function nameStatusText(baseRef: string, cwd: string = REPO_ROOT): string {
172
+ const r = spawnSync('git', ['-c', 'core.quotePath=false', 'diff', '--name-status', `${baseRef}...HEAD`], {
173
+ cwd,
174
+ encoding: 'utf8',
175
+ });
176
+ if (r.status !== 0) {
177
+ throw new Error(`git diff --name-status failed: ${(r.stderr ?? '').toString().slice(0, 300)}`);
178
+ }
179
+ return r.stdout ?? '';
180
+ }
181
+
182
+ /** Pure core of `rangeDeletions`, so the parse is testable without a repo. */
183
+ export function parseDeletions(nameStatus: string): Set<string> {
184
+ const out = new Set<string>();
185
+ for (const line of nameStatus.split('\n')) {
186
+ const cols = line.split('\t');
187
+ const status = (cols[0] ?? '').trim();
188
+ if (status === '') continue;
189
+ if (status === 'D' && cols[1]) out.add(cols[1].trim());
190
+ // A rename removes its OLD path; `R100\told\tnew` puts it in column 1.
191
+ if (status.startsWith('R') && cols[1]) out.add(cols[1].trim());
192
+ }
193
+ return out;
194
+ }
195
+
196
+ /**
197
+ * Pure core of `rangeModifications` — every path the range left standing.
198
+ *
199
+ * A `D` row contributes nothing. A rename contributes its NEW path (column 2),
200
+ * never its old one: the old path is a deletion and is parsed as such. Every
201
+ * other status (`M`, `A`, `C`, `T`, and the `U` of an unmerged path) has its
202
+ * path in column 1 and that path exists after the range.
203
+ */
204
+ export function parseModifications(nameStatus: string): Set<string> {
205
+ const out = new Set<string>();
206
+ for (const line of nameStatus.split('\n')) {
207
+ const cols = line.split('\t');
208
+ const status = (cols[0] ?? '').trim();
209
+ if (status === '' || status === 'D') continue;
210
+ // `R<score>\told\tnew` / `C<score>\tsrc\tdest` — the surviving path is column 2.
211
+ const col = status.startsWith('R') || status.startsWith('C') ? cols[2] : cols[1];
212
+ if (col) out.add(col.trim());
213
+ }
214
+ return out;
215
+ }
216
+
217
+ /**
218
+ * Names a finding points at: its `file`, plus every backticked token in the
219
+ * title and detail that could be a path or an artifact directory name.
220
+ *
221
+ * Backticks are the convention the review prompt asks for and the one every
222
+ * measured finding used, so this reads what the model actually writes rather
223
+ * than guessing at prose.
224
+ */
225
+ export function namedArtifacts(f: Pick<Finding, 'title' | 'detail' | 'file'>): string[] {
226
+ const out = new Set<string>();
227
+ if (f.file) out.add(f.file);
228
+ for (const m of `${f.title}\n${f.detail}`.matchAll(/`([^`\n]{2,120})`/g)) {
229
+ const tok = (m[1] ?? '').trim();
230
+ // A token with whitespace is prose in backticks, not a name.
231
+ if (tok !== '' && !/\s/.test(tok)) out.add(tok);
232
+ }
233
+ return [...out];
234
+ }
235
+
236
+ /** Does the finding's text assert that something was removed from the tree? */
237
+ export function assertsDeletion(f: Pick<Finding, 'title' | 'detail'>): boolean {
238
+ return /\b(deleted|removed|deletion|removal)\b/i.test(`${f.title}\n${f.detail}`);
239
+ }
240
+
241
+ /**
242
+ * The disproof, or null when there is none.
243
+ *
244
+ * WHY THIS IS MECHANICAL AND NOT A PROMPT LINE. The review runs over a
245
+ * per-file PARTITION of the span and, on a large release, over only part of it
246
+ * — 202 of 401 files at the 16.1.0 cut. A reviewer holding half the files
247
+ * cannot distinguish "this artifact is absent from my chunk" from "this
248
+ * artifact was deleted", and on that cut it reported two `critical security`
249
+ * deletions of skills the range never touched. Both blocked the release and
250
+ * cost a full adjudication cycle each.
251
+ *
252
+ * The prompt already carries a sentence for the neighbouring class ("Do NOT
253
+ * report a feature as not in the diff when it is present in the release
254
+ * range") and the same run violated it, which is the evidence that a sentence
255
+ * is not the control here.
256
+ *
257
+ * The test is conservative in the direction that matters: a disproof needs a
258
+ * named artifact that is BOTH absent from the range's deletion set AND present
259
+ * in the tree AND untouched by the range.
260
+ *
261
+ * THE THIRD HALF, AND WHY IT WAS MISSING. The first two were written against
262
+ * WHOLE-FILE deletion, where "a genuine deletion satisfies neither half" is
263
+ * true. It is false for a removal asserted INSIDE a file: "digest verification
264
+ * was removed in `install.ts`" names a path the range MODIFIED, so the path is
265
+ * absent from the deletion set and present in the tree, and the finding was
266
+ * de-blocked as though it had been checked. A path the range modified is
267
+ * evidence the range touched that artifact — the opposite of this disproof's
268
+ * premise — so it can no longer carry one.
269
+ *
270
+ * WHAT THIS GATE STILL CANNOT DECIDE, and says nothing about. The disproof is
271
+ * built entirely out of PATHS: the deletion set, the modification set, and the
272
+ * working tree. A claim about something that has no path of its own — a
273
+ * symbol, an export, a config key, a behaviour, a sentence inside a surviving
274
+ * file — cannot be resolved to a candidate, so no half of the test applies and
275
+ * the finding is returned unannotated. That is the intended reading: the gate
276
+ * cannot decide such a claim, it is not asserting the claim is true, and an
277
+ * operator adjudicating the block is reading an UNCHECKED finding rather than
278
+ * an unrefuted one. Widening the test to cover that class would mean deciding
279
+ * a claim from the same partition the reviewer had, which is the fabrication
280
+ * this mechanism exists to absorb.
281
+ */
282
+ export function contradictedByTree(
283
+ f: Pick<Finding, 'title' | 'detail' | 'file'>,
284
+ deleted: ReadonlySet<string>,
285
+ exists: (p: string) => boolean,
286
+ modified: ReadonlySet<string> = new Set<string>(),
287
+ ): string | null {
288
+ if (!assertsDeletion(f)) return null;
289
+ for (const name of namedArtifacts(f)) {
290
+ // Any deletion whose path mentions the token leaves the claim standing.
291
+ if ([...deleted].some((d) => d === name || d.includes(name))) continue;
292
+ const candidates = [name, `src/skills/${name}/SKILL.md`, `src/rules/${name}.md`, `src/scripts/${name}`];
293
+ // The range touched something this token resolves to — a whole-artifact
294
+ // disproof cannot be read off a path the range changed. Checked over
295
+ // EVERY candidate, not only the one `exists` happens to hit first: the
296
+ // conservative direction is fewer disproofs, never more.
297
+ if (candidates.some((c) => modified.has(c))) continue;
298
+ const hit = candidates.find((c) => exists(c));
299
+ if (hit !== undefined) {
300
+ return (
301
+ `the range deletes nothing matching \`${name}\`, and \`${hit}\` is present in the tree — ` +
302
+ 'checked against `git diff --name-status base...HEAD` and the working tree, not against the review chunk.'
303
+ );
304
+ }
305
+ }
306
+ return null;
307
+ }
308
+
309
+ /**
310
+ * The two counts road-to-a-fact-plane Step 2.3 pre-registers as its falsifier,
311
+ * written into the findings artifact so the next cut can be read rather than
312
+ * recomputed.
313
+ *
314
+ * `asserting_removal` is the population — findings that make a checkable
315
+ * factual claim of the deletion class. `disproved_by_tree` is how many of them
316
+ * the range and the tree refuted. The premise of the fact block is that
317
+ * supplying the range's facts lowers the first and, with it, the second; a
318
+ * `disproved_by_tree` that does not fall after the block ships is the honest
319
+ * null, and the roadmap records that result rather than widening the mechanism
320
+ * to chase it.
321
+ *
322
+ * Every recorded cut so far reports `disproved_by_tree` as 0 for a reason that
323
+ * is not a measurement: the annotation pass landed after the last of them, so
324
+ * the number was never produced. The baseline is "never observed", not
325
+ * "observed as zero", and the first cut after this change is the first reading.
326
+ */
327
+ export function factClaimCounts(findings: readonly Finding[]): {
328
+ asserting_removal: number;
329
+ disproved_by_tree: number;
330
+ } {
331
+ return {
332
+ asserting_removal: findings.filter((f) => assertsDeletion(f)).length,
333
+ disproved_by_tree: findings.filter((f) => (f.contradicted ?? '').trim() !== '').length,
334
+ };
335
+ }
336
+
337
+ /** Render one list of the fact block, or an explicit `(none)`. */
338
+ function factList(label: string, paths: readonly string[]): string {
339
+ const head = `${label} — ${String(paths.length)}:`;
340
+ return paths.length === 0 ? `${head}\n (none)` : `${head}\n${paths.map((p) => ` ${p}`).join('\n')}`;
341
+ }
342
+
343
+ /**
344
+ * The range's facts, serialised for the reviewer.
345
+ *
346
+ * WHY THE REVIEWER IS HANDED THESE RATHER THAN LEFT TO INFER THEM. The review
347
+ * runs over a per-file PARTITION of the span and, on a large release, over only
348
+ * part of it — 202 of 401 files at the 16.1.0 cut. From inside one chunk, "this
349
+ * path is not in front of me" and "this path was deleted" are the same
350
+ * observation, and on that cut the model resolved the ambiguity the wrong way
351
+ * twice, in `critical security` findings that each cost an adjudication cycle.
352
+ * `contradictedByTree` catches that class AFTER the spend; this block removes
353
+ * the inference that produces it.
354
+ *
355
+ * Exactly three lists, all of them already computed by this gate: the paths the
356
+ * range deleted, the paths it changed and left standing, and the working tree's
357
+ * presence check over both — reported by its EXCEPTIONS, because the normal
358
+ * case is empty and a third full copy of the same paths would spend budget on
359
+ * a line the reviewer can derive. Nothing else is added: the block is paths, so
360
+ * it scales with file count and not with change size, and the six-request
361
+ * ceiling stays a budget question this does not reopen.
362
+ */
363
+ export function factBlock(
364
+ deleted: ReadonlySet<string>,
365
+ modified: ReadonlySet<string>,
366
+ exists: (p: string) => boolean,
367
+ ): string {
368
+ const del = [...deleted].sort();
369
+ const mod = [...modified].sort();
370
+ return [
371
+ '=== RANGE FACTS (AUTHORITATIVE) ===',
372
+ 'Read from `git diff --name-status <base>...HEAD` and from the working tree,',
373
+ 'NOT from the diff below. The diff below is ONE PARTITION of this range: a',
374
+ 'path missing from it is a path outside your chunk, never a deleted path.',
375
+ 'A claim that contradicts these lists is a defect in the finding, not a',
376
+ 'finding about the tree — do not report it.',
377
+ '',
378
+ factList('DELETED BY THIS RANGE', del),
379
+ '',
380
+ factList('CHANGED BY THIS RANGE, AND THE TREE HOLDS THEM', mod),
381
+ '',
382
+ 'PRESENCE CHECK (exceptions to the two lists above):',
383
+ factList(' deleted, yet still present in the working tree', del.filter((p) => exists(p))),
384
+ factList(' changed, yet absent from the working tree', mod.filter((p) => !exists(p))),
385
+ '=== END RANGE FACTS ===',
386
+ '',
387
+ '',
388
+ ].join('\n');
389
+ }
390
+
391
+ /** Annotate every finding the tree disproves. Pure given `deleted` + `exists`. */
392
+ export function annotateContradicted(
393
+ findings: readonly Finding[],
394
+ deleted: ReadonlySet<string>,
395
+ exists: (p: string) => boolean,
396
+ modified: ReadonlySet<string> = new Set<string>(),
397
+ ): Finding[] {
398
+ return findings.map((f) => {
399
+ const why = contradictedByTree(f, deleted, exists, modified);
400
+ return why === null ? f : { ...f, contradicted: why };
401
+ });
402
+ }
403
+
123
404
  function diffText(baseRef: string, files: string[], cwd: string = REPO_ROOT): string {
124
405
  if (files.length === 0) return '';
125
406
  const r = spawnSync('git', ['diff', `${baseRef}...HEAD`, '--', ...files], {
@@ -459,6 +740,14 @@ export interface ReviewPlan {
459
740
  * it thrown away.
460
741
  */
461
742
  partition: DiffPartition;
743
+ /**
744
+ * The range facts prepended to EVERY request — see `factBlock`.
745
+ *
746
+ * Computed in the plan rather than at send time so `--dry-run` can print
747
+ * the request the live run would send, and so the partition budget can
748
+ * reserve the block's length instead of discovering it after the split.
749
+ */
750
+ facts: string;
462
751
  /** Model calls the live run will make — one per budgeted chunk. */
463
752
  requests: number;
464
753
  /** Files no request will carry, with the reason. */
@@ -499,22 +788,33 @@ export function buildPlan(baseRef: string, cwd: string = REPO_ROOT): ReviewPlan
499
788
  // REQUEST COUNT rather than only a token estimate. This gate spends money
500
789
  // per request, and a plan that reports "~N tokens" without saying how many
501
790
  // calls that becomes hides the number the maintainer is deciding about.
502
- const partition = partitionDiff(perFileDiffs(analysisBase, files, cwd));
791
+ // The facts ride on every request, so the per-request diff budget has to
792
+ // pay for them. Reserving here rather than at send time is what keeps the
793
+ // block from silently pushing a chunk over the provider's cap — the exact
794
+ // failure four consecutive releases already spent a review on.
795
+ const facts = factBlock(rangeDeletions(analysisBase, cwd), rangeModifications(analysisBase, cwd), (p) =>
796
+ existsSync(path.join(cwd, p)),
797
+ );
798
+ const partition = partitionDiff(
799
+ perFileDiffs(analysisBase, files, cwd),
800
+ Math.max(1, PROMPT_BUDGET_CHARS - facts.length),
801
+ );
503
802
  return {
504
803
  partition,
804
+ facts,
505
805
  requests: partition.chunks.length,
506
806
  unreviewed: partition.unreviewed,
507
807
  skills: [...REVIEW_SKILLS],
508
808
  files,
509
809
  analysisBase,
510
810
  ...(release ? { release } : {}),
511
- promptChars: systemPrompt.length + diff.length,
811
+ promptChars: systemPrompt.length + facts.length + diff.length,
512
812
  escalation,
513
813
  note:
514
814
  files.length === 0
515
815
  ? 'No reviewable (non-generated) files changed — the live review would no-op.'
516
816
  : `${files.length} reviewable file(s); live review would send ~${Math.ceil(
517
- (systemPrompt.length * Math.max(1, partition.chunks.length) + diff.length) /
817
+ ((systemPrompt.length + facts.length) * Math.max(1, partition.chunks.length) + diff.length) /
518
818
  BUDGET_CHARS_PER_TOKEN,
519
819
  )} input tokens across ${partition.chunks.length} request(s)` +
520
820
  (partition.unreviewed.length > 0
@@ -639,7 +939,11 @@ export function renderReview(findings: Finding[], enforce: boolean, escalation:
639
939
  // read as inconsistent with the narrow `blocking.length` verdict count.
640
940
  const rows = findings
641
941
  .map((f) => {
642
- const marker = classifyBlocking(f) ? 'Blocking' : 'Advisory';
942
+ const marker = (f.contradicted ?? '') !== ''
943
+ ? 'Contradicted'
944
+ : classifyBlocking(f)
945
+ ? 'Blocking'
946
+ : 'Advisory';
643
947
  return `| ${findingId(f)} | ${f.severity} (${marker}) | ${f.kind} | ${f.file ?? '—'} | ${f.title} |`;
644
948
  })
645
949
  .join('\n');
@@ -653,6 +957,12 @@ export function renderReview(findings: Finding[], enforce: boolean, escalation:
653
957
  // 12-hex id looks exactly like a short SHA, so the rendered comment now says
654
958
  // what it is and where its disposition lives. This is the only place a reader
655
959
  // of the comment can learn it without opening the source.
960
+ const contradicted = findings.filter((f) => (f.contradicted ?? '') !== '');
961
+ const contradictedNote = contradicted.length === 0
962
+ ? ''
963
+ : '\n\n**Contradicted by the tree** — these assert a removal the range does not contain, '
964
+ + 'so they are reported and NOT counted as blocking:\n'
965
+ + contradicted.map((f) => `- \`${findingId(f)}\` ${f.title} — ${f.contradicted ?? ''}`).join('\n');
656
966
  const idNote =
657
967
  '\n\n`id` above is a FINDING id — the first 12 hex of sha256(kind|title|file), '
658
968
  + '**not a commit SHA**. Searching the git log for it finds nothing, by design. '
@@ -662,9 +972,20 @@ export function renderReview(findings: Finding[], enforce: boolean, escalation:
662
972
  // demands committed dispositions. The comment is transport, never the
663
973
  // record — the record is `agents/evidence/release-findings/<version>.json`.
664
974
  const machine = `\n\n<!-- release-findings-json: ${JSON.stringify(
665
- findings.map((f) => ({ finding_id: findingId(f), severity: f.severity, kind: f.kind, title: f.title, file: f.file ?? null })),
975
+ findings.map((f) => ({
976
+ finding_id: findingId(f),
977
+ severity: f.severity,
978
+ kind: f.kind,
979
+ title: f.title,
980
+ file: f.file ?? null,
981
+ // Carried into the trigger block, not only the rendered table: the
982
+ // PR-comment path of `check_finding_dispositions` reads THIS, and
983
+ // would otherwise demand a disposition for a claim the tree already
984
+ // disproved.
985
+ ...((f.contradicted ?? '') !== '' ? { contradicted: f.contradicted } : {}),
986
+ })),
666
987
  )} -->`;
667
- return `${banner}\n\n${verdictLine}\n\n| id | severity | kind | file | finding |\n|---|---|---|---|---|\n${rows}${idNote}${cov}${esc}${machine}`;
988
+ return `${banner}\n\n${verdictLine}\n\n| id | severity | kind | file | finding |\n|---|---|---|---|---|\n${rows}${contradictedNote}${idNote}${cov}${esc}${machine}`;
668
989
  }
669
990
 
670
991
  function postReview(body: string): void {
@@ -700,6 +1021,16 @@ export function main(argv: string[]): 0 | 2 {
700
1021
 
701
1022
  const plan = buildPlan(baseRef);
702
1023
 
1024
+ // The request the live run would send, verbatim and unspent. A summary
1025
+ // would be this script's description of the request rather than the
1026
+ // request, and the whole point of the fact block is that what the reviewer
1027
+ // is handed can be read rather than inferred — including by a maintainer.
1028
+ if (argv.includes('--print-request')) {
1029
+ const first = plan.partition.chunks[0];
1030
+ process.stdout.write(plan.facts + (first?.text ?? '(no chunk — nothing to review in this range)\n'));
1031
+ return 0;
1032
+ }
1033
+
703
1034
  if (dryRun) {
704
1035
  process.stdout.write(
705
1036
  `self-review-gate (dry-run — no spend, advisory):\n` +
@@ -812,7 +1143,10 @@ export function main(argv: string[]): 0 | 2 {
812
1143
  let reviewedChunks = 0;
813
1144
  let filesRead = 0;
814
1145
  for (const [i, chunk] of partition.chunks.entries()) {
815
- const resp = client.ask(systemPrompt, chunk.text, 4096);
1146
+ // Facts FIRST, then the partition. The reviewer reads what the
1147
+ // range did before it reads the fragment it was given, so "absent
1148
+ // from my chunk" can no longer be mistaken for "deleted".
1149
+ const resp = client.ask(systemPrompt, plan.facts + chunk.text, 4096);
816
1150
  if (resp.error) {
817
1151
  process.stdout.write(
818
1152
  `::warning::self-review-gate — chunk ${String(i + 1)}/` +
@@ -854,6 +1188,16 @@ export function main(argv: string[]): 0 | 2 {
854
1188
  filesTotal: plan.files.length,
855
1189
  unreviewed,
856
1190
  };
1191
+ // Ground-truth pass, ONCE, before anything reads the finding set. A
1192
+ // deletion claim the range and the tree both disprove is annotated
1193
+ // here and stops being merge-blocking; it is still written to the
1194
+ // artifact and still rendered, carrying its disproof.
1195
+ const reviewed = annotateContradicted(
1196
+ dedupeFindings(findings),
1197
+ rangeDeletions(baseRef),
1198
+ (p) => existsSync(path.join(REPO_ROOT, p)),
1199
+ rangeModifications(baseRef),
1200
+ );
857
1201
  const outIdx = argv.indexOf('--findings-out');
858
1202
  if (outIdx >= 0 && argv[outIdx + 1]) {
859
1203
  // Durable ingestion input for the disposition ledger (release-truth
@@ -869,7 +1213,12 @@ export function main(argv: string[]): 0 | 2 {
869
1213
  schema_version: 1,
870
1214
  ...independenceFields(['anthropic']),
871
1215
  coverage,
872
- findings: dedupeFindings(findings).map((f) => ({
1216
+ // Readable, not recomputable: the falsifier for the
1217
+ // supplied-facts change is a count over the NEXT cut,
1218
+ // and a number a reader has to re-derive from 50
1219
+ // findings is a number nobody checks.
1220
+ fact_claims: factClaimCounts(reviewed),
1221
+ findings: reviewed.map((f) => ({
873
1222
  finding_id: findingId(f),
874
1223
  ...f,
875
1224
  })),
@@ -879,10 +1228,10 @@ export function main(argv: string[]): 0 | 2 {
879
1228
  ) + '\n',
880
1229
  );
881
1230
  }
882
- const body = renderReview(dedupeFindings(findings), enforce, plan.escalation, coverage);
1231
+ const body = renderReview(reviewed, enforce, plan.escalation, coverage);
883
1232
  postReview(body);
884
1233
 
885
- const code = gateVerdict(dedupeFindings(findings), { enforce });
1234
+ const code = gateVerdict(reviewed, { enforce });
886
1235
  if (code === 2) {
887
1236
  process.stdout.write('::error::self-review-gate — merge-blocking findings under enforce mode.\n');
888
1237
  }