@event4u/agent-config 16.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 (246) 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 +297 -137
  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 +74 -27
  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 +47 -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/skills/accessibility-auditor/data/manifest.json +3 -3
  40. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  41. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  42. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  43. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  44. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  45. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  46. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  47. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  48. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  49. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  50. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  52. package/dist/agent-src/templates/agent-settings.md +1 -1
  53. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  54. package/dist/agent-src/templates/roadmaps.md +188 -0
  55. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  56. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  58. package/dist/cli-delegate/check_update_banner.js +40 -2
  59. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  60. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  61. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  62. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  63. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  64. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  65. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  66. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  67. package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
  68. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  69. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  70. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  71. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  72. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  73. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  74. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  75. package/dist/cli-delegate/cmd_conformance.js +90 -56
  76. package/dist/cli-delegate/cmd_doctor.js +7 -5
  77. package/dist/cli-delegate/cmd_explain.js +2 -2
  78. package/dist/cli-delegate/cmd_export.js +9 -7
  79. package/dist/cli-delegate/cmd_fleet.js +4 -2
  80. package/dist/cli-delegate/cmd_handoff.js +4 -4
  81. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  82. package/dist/cli-delegate/cmd_migrate.js +2 -2
  83. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  84. package/dist/cli-delegate/cmd_preflight.js +4 -2
  85. package/dist/cli-delegate/cmd_prune.js +10 -8
  86. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  87. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  88. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  89. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  90. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  91. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  92. package/dist/cli-delegate/cmd_sync.js +10 -8
  93. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  94. package/dist/cli-delegate/cmd_update.js +1 -1
  95. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  96. package/dist/cli-delegate/cmd_validate.js +12 -10
  97. package/dist/cli-delegate/cmd_versions.js +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +85 -38
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +4 -4
  102. package/dist/discovery/dormancy-report.md +1 -1
  103. package/dist/discovery/orphan-report.md +1 -1
  104. package/dist/discovery/packs.json +10 -8
  105. package/dist/discovery/trust-report.md +3 -3
  106. package/dist/discovery/workspaces.json +8 -6
  107. package/dist/hooks/dispatch.js +1832 -1007
  108. package/dist/install/claudeRuleRewrite.js +14 -3
  109. package/dist/install/conflict.js +60 -10
  110. package/dist/install/conflictTracker.js +218 -0
  111. package/dist/install/fsPrimitives.js +54 -0
  112. package/dist/install/install.mjs +2007 -1481
  113. package/dist/install/manifestFiles.js +259 -0
  114. package/dist/install/preflight.js +14 -2
  115. package/dist/install/preserve.js +157 -0
  116. package/dist/install/recordedOwnership.js +132 -0
  117. package/dist/install/txlog.js +11 -2
  118. package/dist/mcp/registry-manifest.json +3 -3
  119. package/dist/mcp/server.json +2 -2
  120. package/dist/mcp/server.mjs +398 -39
  121. package/dist/router.json +1 -1
  122. package/dist/scripts/_lib/agent_settings.js +3 -2
  123. package/dist/scripts/_lib/fs_atomic.js +167 -0
  124. package/dist/scripts/_lib/installed_tools.js +460 -0
  125. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  126. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  127. package/dist/server/routes/install.js +8 -0
  128. package/dist/server/routes/wizard.js +9 -3
  129. package/dist/server/schemas/settings.js +17 -5
  130. package/dist/shared/missionExecution.js +97 -0
  131. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  132. package/dist/ui/index.html +1 -1
  133. package/docs/MIGRATION.md +103 -0
  134. package/docs/contracts/settings-classes.md +26 -7
  135. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  136. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  137. package/docs/guidelines/design-fidelity-routing.md +29 -0
  138. package/package.json +1 -1
  139. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  140. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  141. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  142. package/src/config/agent-settings.template.yml +139 -27
  143. package/src/config/auto-rule-budget.json +16 -0
  144. package/src/config/ci-local-parity.yml +18 -5
  145. package/src/config/continuity-surface.json +21 -0
  146. package/src/config/gate-coverage.yml +146 -13
  147. package/src/config/gate-violation-baselines.json +6 -6
  148. package/src/config/hook-token-budget.json +2 -0
  149. package/src/config/loop-surfaces.yaml +215 -0
  150. package/src/config/platform-anchor.json +3 -2
  151. package/src/config/release-gate-locality.yml +11 -0
  152. package/src/config/routing-coverage-seed.json +18 -4
  153. package/src/config/rule-enforcement-baseline.json +0 -1
  154. package/src/install/claudeRuleRewrite.ts +16 -2
  155. package/src/install/conflict.ts +65 -9
  156. package/src/install/conflictTracker.ts +299 -0
  157. package/src/install/fsPrimitives.ts +54 -0
  158. package/src/install/manifestFiles.ts +312 -0
  159. package/src/install/preflight.ts +16 -2
  160. package/src/install/preserve.ts +205 -0
  161. package/src/install/recordedOwnership.ts +149 -0
  162. package/src/install/txlog.ts +11 -2
  163. package/src/install/types.ts +19 -4
  164. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  165. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  166. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  167. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  168. package/src/scripts/_cli/doctor_execution.ts +90 -0
  169. package/src/scripts/_lib/activation_payload.ts +247 -0
  170. package/src/scripts/_lib/agent_settings.ts +4 -2
  171. package/src/scripts/_lib/authority_path.ts +102 -0
  172. package/src/scripts/_lib/cascade_base.ts +94 -0
  173. package/src/scripts/_lib/changelog_eras.ts +7 -2
  174. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  175. package/src/scripts/_lib/council_transport.ts +129 -0
  176. package/src/scripts/_lib/delivery_ready.ts +112 -0
  177. package/src/scripts/_lib/forge_protection.ts +267 -0
  178. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  179. package/src/scripts/_lib/json_python_parity.ts +127 -0
  180. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  181. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  182. package/src/scripts/_lib/mission_record.ts +190 -0
  183. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  184. package/src/scripts/_lib/obligations.ts +402 -0
  185. package/src/scripts/_lib/orchestration_record.ts +11 -20
  186. package/src/scripts/_lib/release_highlights.ts +199 -10
  187. package/src/scripts/_lib/release_holds.ts +493 -0
  188. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  189. package/src/scripts/_lib/rides_along.ts +102 -0
  190. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  191. package/src/scripts/_lib/test_delta.ts +166 -0
  192. package/src/scripts/_lib/test_provenance.ts +171 -0
  193. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  194. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  195. package/src/scripts/ai_council/config.ts +8 -11
  196. package/src/scripts/ai_council/fallback_config.ts +5 -3
  197. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  198. package/src/scripts/ask_block_census.ts +285 -15
  199. package/src/scripts/check_enforcement_matrix.ts +624 -0
  200. package/src/scripts/check_gate_reachability.ts +173 -10
  201. package/src/scripts/check_generator_sync.ts +954 -0
  202. package/src/scripts/check_preamble_payload_budget.ts +75 -2
  203. package/src/scripts/check_release_highlights.ts +33 -1
  204. package/src/scripts/check_release_holds.ts +567 -0
  205. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  206. package/src/scripts/check_test_delta.ts +294 -0
  207. package/src/scripts/check_test_weakening.ts +299 -0
  208. package/src/scripts/closure_scan.ts +333 -0
  209. package/src/scripts/compile_hook_manifest.ts +43 -3
  210. package/src/scripts/council_record_shape.ts +209 -0
  211. package/src/scripts/hook_manifest.json +1 -1
  212. package/src/scripts/hook_manifest.yaml +88 -9
  213. package/src/scripts/hooks/concern_registry.ts +4 -0
  214. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  215. package/src/scripts/hooks/evidence_independence.ts +96 -0
  216. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  217. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  218. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  219. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  220. package/src/scripts/hooks_doctor.ts +85 -0
  221. package/src/scripts/hooks_status.ts +19 -0
  222. package/src/scripts/install.ts +106 -206
  223. package/src/scripts/janitor.ts +7 -0
  224. package/src/scripts/lint_decision_classes.ts +464 -0
  225. package/src/scripts/lint_hook_manifest.ts +11 -0
  226. package/src/scripts/lint_major_migration_sections.ts +450 -0
  227. package/src/scripts/lint_plan_risk_register.ts +47 -3
  228. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  229. package/src/scripts/lint_roadmap_producers.ts +202 -0
  230. package/src/scripts/measure_rule_budget.ts +101 -5
  231. package/src/scripts/new_roadmap.ts +37 -0
  232. package/src/scripts/orchestration_record.ts +0 -5
  233. package/src/scripts/release.ts +14 -98
  234. package/src/scripts/release_drill.ts +267 -1
  235. package/src/scripts/release_publication.ts +51 -0
  236. package/src/scripts/report_enforcement_drift.ts +566 -0
  237. package/src/scripts/report_host_injection_effect.ts +3 -11
  238. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  239. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  240. package/src/scripts/schemas/command.schema.json +4 -0
  241. package/src/scripts/scope_growth.ts +219 -0
  242. package/src/scripts/trigger_eval_grandfather.json +0 -1
  243. package/src/scripts/ui_conformance_probe.ts +622 -0
  244. package/src/server/schemas/settings.ts +31 -7
  245. package/src/shared/missionExecution.ts +126 -0
  246. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -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 */
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * check_test_weakening — a test that asserts less than it did needs a verdict.
4
+ *
5
+ * `road-to-adversarial-verification-and-long-runs` Phase 2.4, second of two.
6
+ * `evaluator-independence` § Tests are evaluators forbids an implementer from
7
+ * silently weakening an assertion, deleting a failing test, skipping it, or
8
+ * loosening a threshold. This gate sees the shape of that in a diff.
9
+ *
10
+ * **NET counts, never raw removals — the whole gate turns on this.** Editing an
11
+ * assertion removes one line and adds another, so a gate counting removals reds
12
+ * every legitimate test edit. A gate that reds every PR gets its exemption
13
+ * widened until it finds nothing, which is Risk 4 of the roadmap that
14
+ * commissioned it and the allowlist-growth antipattern `autonomous-execution`
15
+ * already names. A NET loss is a different, much narrower claim: after this
16
+ * diff, the file asserts less than before.
17
+ *
18
+ * **What makes a weakening legitimate is an independent verdict**, and this gate
19
+ * checks only that one was COMMITTED in the same diff. Whether it actually
20
+ * contains an independent verdict is a judgement no gate can make, so it is not
21
+ * claimed here.
22
+ *
23
+ * Usage:
24
+ * check_test_weakening [--diff-file F] [--files F …] [--base-ref REF]
25
+ * [--root DIR] [--quiet] [--self-test]
26
+ *
27
+ * Exit codes: 0 = pass (or discharged) · 1 = fail · 2 = usage error.
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 {
40
+ isTestPath,
41
+ isVerdictArtefact,
42
+ isWeakening,
43
+ parseUnifiedDiff,
44
+ weakeningSignal,
45
+ type WeakeningSignal,
46
+ } from './_lib/test_delta.js';
47
+
48
+ const _HERE = fileURLToPath(import.meta.url);
49
+ const REPO = path.resolve(path.dirname(_HERE), '..', '..');
50
+
51
+ export interface WeakeningReport {
52
+ readonly signals: readonly WeakeningSignal[];
53
+ readonly discharged: boolean;
54
+ readonly scanned: number;
55
+ }
56
+
57
+ /** The decision, pure: which test files weakened, and was a verdict committed. */
58
+ export function analyse(diff: string, changedFiles: readonly string[]): WeakeningReport {
59
+ const files = parseUnifiedDiff(diff).filter((f) => isTestPath(f.path));
60
+ const signals = files.map(weakeningSignal).filter(isWeakening);
61
+ return {
62
+ signals,
63
+ discharged: changedFiles.some(isVerdictArtefact),
64
+ scanned: files.length,
65
+ };
66
+ }
67
+
68
+ /** One human-readable line per weakened file. */
69
+ export function renderSignal(s: WeakeningSignal): string {
70
+ const parts: string[] = [];
71
+ if (s.assertionsLost > 0) parts.push(`${String(s.assertionsLost)} assertion(s) net-removed`);
72
+ if (s.suppressionsGained > 0) {
73
+ parts.push(`${String(s.suppressionsGained)} suppression(s) net-added`);
74
+ }
75
+ return `${s.path}: ${parts.join(' · ')}`;
76
+ }
77
+
78
+ function git(args: readonly string[], root: string): string | null {
79
+ const r = spawnSync('git', [...args], { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
80
+ return r.status === 0 ? r.stdout : null;
81
+ }
82
+
83
+ function resolveBaseRef(explicit: string | null, root: string): string {
84
+ if (explicit !== null) return explicit;
85
+ for (const ref of ['origin/main', 'origin/master', 'main', 'master']) {
86
+ if (git(['rev-parse', '--verify', '--quiet', ref], root) !== null) return ref;
87
+ }
88
+ return 'HEAD~1';
89
+ }
90
+
91
+ function usage(): string {
92
+ return (
93
+ 'usage: check_test_weakening [--diff-file F] [--files F …] [--base-ref REF]\n' +
94
+ ' [--root DIR] [--quiet] [--self-test]\n'
95
+ );
96
+ }
97
+
98
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
99
+ if (argv.includes('--help') || argv.includes('-h')) {
100
+ process.stdout.write(usage());
101
+ return 0;
102
+ }
103
+ if (argv.includes('--self-test')) return selfTest();
104
+
105
+ const quiet = argv.includes('--quiet');
106
+ const at = (flag: string): string | null => {
107
+ const i = argv.indexOf(flag);
108
+ return i >= 0 ? (argv[i + 1] ?? null) : null;
109
+ };
110
+ const root = at('--root') ?? REPO;
111
+
112
+ let diff: string;
113
+ let changed: string[];
114
+ const diffFile = at('--diff-file');
115
+ const fi = argv.indexOf('--files');
116
+ if (diffFile !== null) {
117
+ try {
118
+ diff = fs.readFileSync(diffFile, 'utf8');
119
+ } catch {
120
+ process.stderr.write(`check_test_weakening: cannot read ${diffFile}\n`);
121
+ return 2;
122
+ }
123
+ changed = fi >= 0 ? argv.slice(fi + 1).filter((a) => !a.startsWith('--')) : [];
124
+ } else {
125
+ const base = resolveBaseRef(at('--base-ref'), root);
126
+ const d = git(['diff', '--unified=0', `${base}...HEAD`], root);
127
+ const names = git(['diff', '--name-only', `${base}...HEAD`], root);
128
+ if (d === null || names === null) {
129
+ process.stderr.write('check_test_weakening: git diff failed\n');
130
+ return 2;
131
+ }
132
+ diff = d;
133
+ changed = names.split('\n').map((s) => s.trim()).filter((s) => s !== '');
134
+ }
135
+
136
+ // Per-file accounting: a changed test file either measured clean, measured
137
+ // weakened, or was discharged by a committed verdict. Without it, "no test
138
+ // file changed" and "every test file was skipped" print the same green line.
139
+ const parsed = parseUnifiedDiff(diff);
140
+ const ledger = new GateLedger('check_test_weakening');
141
+ ledger.plan(parsed.map((f) => `file:${f.path}`));
142
+ const report = analyse(diff, changed);
143
+ const weakened = new Set(report.signals.map((s) => s.path));
144
+ for (const f of parsed) {
145
+ const target = `file:${f.path}`;
146
+ if (!isTestPath(f.path)) {
147
+ ledger.outOfScope(target, 'not_applicable_kind');
148
+ continue;
149
+ }
150
+ if (!weakened.has(f.path)) ledger.complete(target);
151
+ else if (report.discharged) ledger.outOfScope(target, 'declared_exemption');
152
+ else ledger.fail(target, renderSignal(weakeningSignal(f)));
153
+ }
154
+ ledger.report();
155
+
156
+ reportScanned({
157
+ gate: 'check_test_weakening',
158
+ scanned: report.scanned,
159
+ units: 'changed test file(s)',
160
+ roots: ['<the diff against the base ref>'],
161
+ // A diff touching no test file is the normal case for a docs or config
162
+ // change, and reporting it as a dead scope would make the gate cry wolf
163
+ // on most of the PRs it runs against.
164
+ allowEmpty: 'EMPTY_VALID: a diff that changes no test file has nothing to weaken',
165
+ });
166
+
167
+ if (report.signals.length === 0) {
168
+ if (!quiet) {
169
+ process.stdout.write(
170
+ `✅ check_test_weakening: ${String(report.scanned)} test file(s), no net weakening\n`,
171
+ );
172
+ }
173
+ return 0;
174
+ }
175
+
176
+ if (report.discharged) {
177
+ process.stdout.write(
178
+ `✅ check_test_weakening: ${String(report.signals.length)} weakened file(s), ` +
179
+ 'discharged by an independent-verdict artefact under agents/evidence/reviews/\n',
180
+ );
181
+ return 0;
182
+ }
183
+
184
+ process.stderr.write(
185
+ `❌ check_test_weakening: ${String(report.signals.length)} test file(s) assert less ` +
186
+ 'than before, with no independent-verdict artefact in this diff:\n' +
187
+ report.signals.map((s) => ` - ${renderSignal(s)}\n`).join('') +
188
+ ' An implementer may not weaken a test silently (src/rules/evaluator-independence.md\n' +
189
+ ' § Tests are evaluators). Where the test is genuinely wrong: evidence → independent\n' +
190
+ ' test review → council or team, then commit the verdict under agents/evidence/reviews/.\n',
191
+ );
192
+ return 1;
193
+ }
194
+
195
+ function selfTest(): number {
196
+ const withDiff = (diff: string, files: readonly string[] = []): number => {
197
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ctw-'));
198
+ const f = path.join(dir, 'd.diff');
199
+ fs.writeFileSync(f, diff);
200
+ try {
201
+ const args = ['--diff-file', f, '--quiet'];
202
+ if (files.length > 0) args.push('--files', ...files);
203
+ return runGateCli(REPO, 'src/scripts/check_test_weakening.ts', args, dir);
204
+ } finally {
205
+ fs.rmSync(dir, { recursive: true, force: true });
206
+ }
207
+ };
208
+ const header = (p: string): string => `diff --git a/${p} b/${p}\n--- a/${p}\n+++ b/${p}\n@@\n`;
209
+
210
+ return runSelfTest({
211
+ gate: 'check_test_weakening',
212
+ minCases: 6,
213
+ minRejectCases: 3,
214
+ cases: [
215
+ {
216
+ name: 'a net-removed assertion is REJECTED',
217
+ expect: 'reject',
218
+ run: () =>
219
+ withDiff(header('tests/a.test.ts') + "- expect(x).toBe(1);\n"),
220
+ },
221
+ {
222
+ name: 'an added it.skip is REJECTED',
223
+ expect: 'reject',
224
+ run: () => withDiff(header('tests/a.test.ts') + "+ it.skip('x', () => {});\n"),
225
+ },
226
+ {
227
+ name: 'an added @pytest.mark.skip is REJECTED — not a one-framework gate',
228
+ expect: 'reject',
229
+ run: () => withDiff(header('tests/a_test.py') + '+@pytest.mark.skip\n'),
230
+ },
231
+ {
232
+ name: 'EDITING an assertion is not weakening — the net is zero',
233
+ expect: 'accept',
234
+ run: () =>
235
+ withDiff(
236
+ header('tests/a.test.ts') +
237
+ "- expect(x).toBe(1);\n+ expect(x).toBe(2);\n",
238
+ ),
239
+ },
240
+ {
241
+ name: 'ADDING assertions is never weakening',
242
+ expect: 'accept',
243
+ run: () =>
244
+ withDiff(
245
+ header('tests/a.test.ts') +
246
+ "+ expect(x).toBe(1);\n+ expect(y).toBe(2);\n",
247
+ ),
248
+ },
249
+ {
250
+ name: 'DELETING a whole test file is the maximal weakening, and REDS',
251
+ expect: 'reject',
252
+ run: () =>
253
+ withDiff(
254
+ 'diff --git a/tests/a.test.ts b/tests/a.test.ts\n' +
255
+ '--- a/tests/a.test.ts\n+++ /dev/null\n@@\n' +
256
+ '-expect(x).toBe(1);\n',
257
+ ),
258
+ },
259
+ {
260
+ name: 'a removed assertion in PRODUCTION code is out of scope',
261
+ expect: 'accept',
262
+ run: () => withDiff(header('src/a.ts') + '- expect(x).toBe(1);\n'),
263
+ },
264
+ {
265
+ name: 'REMOVING a skip is the opposite of weakening',
266
+ expect: 'accept',
267
+ run: () => withDiff(header('tests/a.test.ts') + "- it.skip('x', () => {});\n"),
268
+ },
269
+ {
270
+ name: 'a DIFF FIXTURE held as string data is not a suppression',
271
+ expect: 'accept',
272
+ run: () =>
273
+ withDiff(
274
+ header('tests/gate.test.ts') +
275
+ '+ \'+@pytest.mark.skip\\n\',\n',
276
+ ),
277
+ },
278
+ {
279
+ name: '…and the same construct written as CODE still reds',
280
+ expect: 'reject',
281
+ run: () => withDiff(header('tests/gate.test.ts') + '+@pytest.mark.skip\n'),
282
+ },
283
+ {
284
+ name: 'a committed verdict artefact discharges a real weakening',
285
+ expect: 'accept',
286
+ run: () =>
287
+ withDiff(header('tests/a.test.ts') + '- expect(x).toBe(1);\n', [
288
+ 'agents/evidence/reviews/some-review.md',
289
+ ]),
290
+ },
291
+ ],
292
+ });
293
+ }
294
+
295
+ /* c8 ignore start — CLI entry, exercised by the self-test rather than in-process. */
296
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1] ?? '')).href) {
297
+ process.exit(main());
298
+ }
299
+ /* c8 ignore stop */