@event4u/agent-config 9.27.0 → 9.29.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 (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +13 -12
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/conformance.md +24 -0
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/team/adversarial.md +8 -7
  9. package/dist/agent-src/commands/team/delegate.md +13 -12
  10. package/dist/agent-src/commands/team/review.md +9 -8
  11. package/dist/agent-src/commands/team/status.md +9 -8
  12. package/dist/agent-src/commands/team.md +29 -19
  13. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  14. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  15. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  16. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  18. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  19. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  20. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  21. package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
  22. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  23. package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
  24. package/dist/agent-src/rules/context-hygiene.md +3 -1
  25. package/dist/agent-src/rules/council-availability.md +93 -0
  26. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  27. package/dist/agent-src/rules/delegation-policy.md +38 -10
  28. package/dist/agent-src/rules/design-fidelity.md +3 -0
  29. package/dist/agent-src/rules/evaluator-independence.md +2 -1
  30. package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
  31. package/dist/agent-src/rules/finance-safety-floor.md +1 -1
  32. package/dist/agent-src/rules/git-history-discipline.md +1 -1
  33. package/dist/agent-src/rules/history-discipline.md +3 -1
  34. package/dist/agent-src/rules/legal-safety-floor.md +1 -1
  35. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
  36. package/dist/agent-src/rules/onboarding-gate.md +3 -1
  37. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
  38. package/dist/agent-src/rules/role-mode-adherence.md +1 -1
  39. package/dist/agent-src/rules/scale-discipline.md +3 -1
  40. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  41. package/dist/agent-src/rules/session-canary.md +2 -1
  42. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  43. package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
  44. package/dist/agent-src/rules/token-budget-discipline.md +4 -2
  45. package/dist/agent-src/rules/tool-safety.md +5 -1
  46. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  47. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  48. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  49. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  50. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  51. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  52. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  53. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  54. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  55. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  56. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  57. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  58. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  59. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  60. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  61. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  62. package/dist/agent-src/templates/AGENTS.md +8 -12
  63. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  64. package/dist/agent-src/templates/roadmaps.md +23 -0
  65. package/dist/cli/registry.js +7 -0
  66. package/dist/cli/registry.js.map +1 -1
  67. package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
  68. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
  69. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  70. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  71. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
  72. package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
  73. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
  74. package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
  75. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  76. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  77. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  78. package/dist/cli-delegate/cmd_conformance.js +10 -47
  79. package/dist/cli-delegate/cmd_converge.js +3 -3
  80. package/dist/cli-delegate/cmd_doctor.js +10 -6
  81. package/dist/cli-delegate/cmd_explain.js +80 -330
  82. package/dist/cli-delegate/cmd_export.js +6 -6
  83. package/dist/cli-delegate/cmd_fleet.js +3 -3
  84. package/dist/cli-delegate/cmd_handoff.js +3 -3
  85. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  86. package/dist/cli-delegate/cmd_migrate.js +3 -3
  87. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  88. package/dist/cli-delegate/cmd_preflight.js +1 -1
  89. package/dist/cli-delegate/cmd_prune.js +6 -6
  90. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  91. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  92. package/dist/cli-delegate/cmd_settings_get.js +266 -0
  93. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  94. package/dist/cli-delegate/cmd_settings_set.js +8 -2968
  95. package/dist/cli-delegate/cmd_sync.js +6 -6
  96. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  97. package/dist/cli-delegate/cmd_update.js +4 -4
  98. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  99. package/dist/cli-delegate/cmd_validate.js +6 -6
  100. package/dist/cli-delegate/cmd_versions.js +4 -4
  101. package/dist/discovery/deprecation-report.md +1 -1
  102. package/dist/discovery/discovery-manifest.json +164 -55
  103. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  104. package/dist/discovery/discovery-manifest.summary.md +4 -3
  105. package/dist/discovery/orphan-report.md +1 -1
  106. package/dist/discovery/packs.json +44 -5
  107. package/dist/discovery/trust-report.md +11 -11
  108. package/dist/discovery/workspaces.json +16 -6
  109. package/dist/hooks/dispatch.js +9341 -6131
  110. package/dist/install/install.mjs +739 -524
  111. package/dist/mcp/registry-manifest.json +3 -3
  112. package/dist/mcp/server.json +2 -2
  113. package/dist/mcp/server.mjs +777 -281
  114. package/dist/router.json +1 -1
  115. package/dist/scripts/_lib/agent_settings.js +134 -15
  116. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  117. package/dist/scripts/condense.js +147 -2
  118. package/dist/scripts/condense.js.map +1 -1
  119. package/dist/server/schemas/settings.js +6 -7
  120. package/dist/server/schemas/settings.js.map +1 -1
  121. package/dist/shared/settingsCarveOut.js +0 -9
  122. package/dist/shared/settingsCarveOut.js.map +1 -1
  123. package/dist/shared/settingsClasses.js +11 -9
  124. package/dist/shared/settingsClasses.js.map +1 -1
  125. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  126. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  127. package/dist/ui/index.html +1 -1
  128. package/docs/contracts/settings-classes.md +68 -17
  129. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  130. package/package.json +1 -1
  131. package/src/agent-src/templates/AGENTS.md +8 -12
  132. package/src/config/agent-settings.template.yml +52 -66
  133. package/src/config/budgets.yml +29 -0
  134. package/src/config/discovery/packs.yml +11 -0
  135. package/src/config/discovery/workspaces.yml +1 -1
  136. package/src/config/evaluator-budgets.json +4 -4
  137. package/src/config/gate-coverage.yml +26 -0
  138. package/src/config/gate-violation-baselines.json +3 -3
  139. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  140. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  141. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  142. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  143. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  144. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  145. package/src/scripts/_dispatch.bash +57 -0
  146. package/src/scripts/_lib/agent_settings.ts +140 -16
  147. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  148. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  149. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  150. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  151. package/src/scripts/_lib/host_capability.ts +144 -4
  152. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  153. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  154. package/src/scripts/_lib/orchestration_record.ts +37 -0
  155. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  156. package/src/scripts/_lib/prompt_shape.ts +88 -0
  157. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  158. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  159. package/src/scripts/_lib/self_repair.ts +489 -0
  160. package/src/scripts/_lib/self_repair_store.ts +128 -0
  161. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  162. package/src/scripts/_lib/subagent_response.ts +19 -0
  163. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  164. package/src/scripts/_lib/subagent_steering.ts +9 -4
  165. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  166. package/src/scripts/_lib/transcript_entry.ts +80 -0
  167. package/src/scripts/_lib/worker_budget.ts +37 -2
  168. package/src/scripts/ai_council/clients.ts +76 -54
  169. package/src/scripts/ai_council/config.ts +94 -6
  170. package/src/scripts/ai_council/handoff.ts +113 -0
  171. package/src/scripts/ai_council/modes.ts +19 -4
  172. package/src/scripts/ai_council/orchestrator.ts +105 -0
  173. package/src/scripts/ai_council/quorum.ts +84 -0
  174. package/src/scripts/ai_council/session.ts +44 -0
  175. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  176. package/src/scripts/ai_team/availability.ts +110 -0
  177. package/src/scripts/ai_team/config.ts +34 -14
  178. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  179. package/src/scripts/bench_critic_protocol.ts +396 -0
  180. package/src/scripts/check_branch_freshness.ts +255 -0
  181. package/src/scripts/check_council_config_location.ts +125 -1
  182. package/src/scripts/check_detector_corpus.ts +202 -0
  183. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  184. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  185. package/src/scripts/condense.ts +162 -2
  186. package/src/scripts/config/profiles.ts +13 -3
  187. package/src/scripts/conformance_scan.ts +272 -24
  188. package/src/scripts/council_availability_hook.ts +188 -0
  189. package/src/scripts/council_cli.ts +527 -32
  190. package/src/scripts/forensics_report.ts +333 -0
  191. package/src/scripts/hook_manifest.yaml +107 -26
  192. package/src/scripts/hooks/block_no_verify.ts +39 -2
  193. package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
  194. package/src/scripts/hooks/concern_registry.ts +10 -0
  195. package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
  196. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  197. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  198. package/src/scripts/install.ts +151 -0
  199. package/src/scripts/language_mirror_hook.ts +30 -37
  200. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  201. package/src/scripts/lint_never_silent.ts +276 -0
  202. package/src/scripts/lint_no_activation_gates.ts +271 -0
  203. package/src/scripts/measure_scope_dedup.ts +19 -13
  204. package/src/scripts/onboarding_gate_hook.ts +40 -1
  205. package/src/scripts/orchestration_record.ts +12 -1
  206. package/src/scripts/preamble_byte_census.ts +9 -20
  207. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  208. package/src/scripts/release.ts +74 -2
  209. package/src/scripts/report_carrier_divergence.ts +364 -0
  210. package/src/scripts/report_skill_activation.ts +257 -0
  211. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  212. package/src/scripts/routing_doctor.ts +95 -39
  213. package/src/scripts/score_skill_selection.ts +19 -5
  214. package/src/scripts/self_repair_cli.ts +470 -0
  215. package/src/scripts/self_repair_hook.ts +241 -0
  216. package/src/scripts/team_events_spike.ts +271 -0
  217. package/src/scripts/team_review_gate_hook.ts +31 -9
  218. package/src/server/schemas/settings.ts +10 -17
  219. package/src/shared/settingsCarveOut.ts +0 -10
  220. package/src/shared/settingsClasses.ts +11 -9
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Critic-protocol A/B runner (road-to-judgment-and-forensic-evidence Phase 2).
3
+ *
4
+ * Measures the `load_bearing` critic protocol against the shipped `legacy`
5
+ * adversarial-skeptic posture on the frozen corpus
6
+ * (internal/bench/adversarial-council/corpus.json) — the corpus on which the
7
+ * legacy posture scored a measured 100% false-positive rate on the 3
8
+ * controversial-but-correct clean controls (run 2026-07-21).
9
+ *
10
+ * Protocol (pre-registered in docs/CLAIMS.md `critic-protocol-load-bearing-ab`
11
+ * BEFORE any paid run; thresholds, prompts, and scorer semantics frozen there):
12
+ *
13
+ * Both arms run as ONE independent single-shot review per vendor
14
+ * (anthropic + openai, direct client calls — NEVER council_cli transport)
15
+ * over all 15 corpus items (12 defect fixtures + 3 clean controls).
16
+ *
17
+ * PER VENDOR, the load_bearing arm promotes iff BOTH:
18
+ * (1) FP rate on the 3 clean controls < 50%, AND
19
+ * (2) TP count on the 12 defect fixtures >= 80% of the legacy arm's
20
+ * TP count in the same run.
21
+ *
22
+ * Scoring is the existing deterministic scorer applied IDENTICALLY to both
23
+ * arms on findings only (caughtDefect / isFalsePositive). The protocol's
24
+ * `verdict` field is a secondary published signal; "flawed" with empty
25
+ * findings counts as `incoherent` (2026-08-09 council design pass) — never
26
+ * as a catch, never as an FP.
27
+ *
28
+ * `--mock` validates the pipeline in BOTH directions with zero spend (a
29
+ * passing arm must read PASS, a failing arm must read FAIL) — the roadmap's
30
+ * pre-spend tripwire. `--run` fires the paid cross-vendor calls
31
+ * (maintainer spend-gated). `--out <file>` writes the report JSON.
32
+ *
33
+ * The `legacy` default and the `critic_protocol` config key live in
34
+ * scripts/ai_council/config.ts; this runner takes the arms from the corpus
35
+ * loop directly (an A/B needs both regardless of the configured value).
36
+ */
37
+ // ledger-exempt: bench runner, not a CI gate — spend-gated on-demand measurement
38
+ // with a fixed 15-item corpus; the report itself carries the full denominator.
39
+ import * as fs from 'node:fs';
40
+ import * as path from 'node:path';
41
+
42
+ import {
43
+ AnthropicClient,
44
+ OpenAIClient,
45
+ load_anthropic_key,
46
+ load_openai_key,
47
+ type ExternalAIClient,
48
+ } from './ai_council/clients.js';
49
+ import {
50
+ caughtDefect,
51
+ isFalsePositive,
52
+ categoryFamily,
53
+ type Finding,
54
+ type GroundTruth,
55
+ } from './_lib/adversarial_bench_score.js';
56
+
57
+ const REPO_ROOT = process.cwd();
58
+ const CORPUS_PATH = path.join(REPO_ROOT, 'internal/bench/adversarial-council/corpus.json');
59
+
60
+ /** Pre-registered thresholds — frozen with the CLAIMS entry, never adjusted. */
61
+ const FP_CEILING = 0.5; // strict: FP rate must be < 0.5 on the 3 controls
62
+ const TP_RETENTION_FLOOR = 0.8; // load_bearing TP >= 0.8 × legacy TP, per vendor
63
+
64
+ interface CorpusItem {
65
+ id: string;
66
+ class: string;
67
+ defect_category: string;
68
+ is_clean: boolean;
69
+ files: { path: string; content: string }[];
70
+ ground_truth: { defect_files: string[]; defect_summary: string };
71
+ }
72
+
73
+ function loadCorpus(): CorpusItem[] {
74
+ return JSON.parse(fs.readFileSync(CORPUS_PATH, 'utf8')).items as CorpusItem[];
75
+ }
76
+ function groundTruth(item: CorpusItem): GroundTruth {
77
+ return {
78
+ id: item.id,
79
+ is_clean: item.is_clean,
80
+ defect_files: item.ground_truth.defect_files,
81
+ defect_category: item.is_clean ? 'none' : categoryFamily(item.defect_category),
82
+ };
83
+ }
84
+ function codeBlock(item: CorpusItem): string {
85
+ return item.files.map((f) => `FILE: ${f.path}\n\`\`\`ts\n${f.content}\`\`\``).join('\n\n');
86
+ }
87
+
88
+ const FINDINGS_SHAPE =
89
+ '{"findings":[{"file":"<file path>","category":"<defect category>","confidence":"high|medium|low","summary":"<one line>"}]}';
90
+
91
+ /**
92
+ * LEGACY arm — byte-identical to the prompt that produced the measured 100%
93
+ * FP on the clean controls (bench_adversarial_council.ts skepticPrompt).
94
+ */
95
+ function legacyPrompt(item: CorpusItem): string {
96
+ return [
97
+ 'You are an adversarial security/correctness skeptic. Assume this code may hide a subtle, non-obvious defect that a normal review missed — a masked access-control/SSRF/traversal flaw, a boundary/logic inversion, a concurrency or resource-leak edge, a cross-file contract mismatch. Hunt for it. Report only defects you can concretely justify; if after hard scrutiny it is genuinely correct, report an empty array (do not fabricate).',
98
+ `Respond with STRICT JSON only, exactly: ${FINDINGS_SHAPE}`,
99
+ '',
100
+ codeBlock(item),
101
+ ].join('\n');
102
+ }
103
+
104
+ const PROTOCOL_SHAPE =
105
+ '{"invariant":"<one sentence>","load_bearing_assumption":"<one sentence, names file+function>","failure_scenario_if_broken":"<one sentence>","cost_of_avoidance":"<one sentence>","succeeded_would_do":"<one sentence or \'nothing\'>","verdict":"holds|flawed","findings":[{"file":"<file path>","category":"<defect category>","confidence":"high|medium|low","summary":"<one line>"}]}';
106
+
107
+ /**
108
+ * LOAD_BEARING arm — the fixed protocol. Design integrated the 2026-08-09
109
+ * council convergence: the assumption must be tied to a named file+function
110
+ * (no platitudes), a concrete failure scenario forces the assumption to be
111
+ * load-bearing, and "holds" is an explicitly permitted, completed review.
112
+ */
113
+ function loadBearingPrompt(item: CorpusItem): string {
114
+ return [
115
+ 'You are a rigorous correctness critic running a fixed protocol. Do not free-associate defects. Work strictly in this order:',
116
+ '1. State the invariant this code must maintain to be correct, then name the SINGLE load-bearing assumption it rests on — the one whose failure would break that invariant. Tie it to a named file and function; a platitude ("inputs are valid") is not an assumption.',
117
+ '2. State the concrete failure scenario if that assumption broke: which input or state leads to which wrong behaviour. If you cannot state one, the assumption is not load-bearing — pick again.',
118
+ '3. State the cost of what this code is avoiding: what failure mode or complexity does its current shape avert?',
119
+ "4. State what someone who has already built a comparable thing successfully would do differently here, if anything ('nothing' is acceptable).",
120
+ '5. Now inspect the assumption against the code. If it survives inspection and the failure scenario is unreachable, your verdict is "holds" and findings is []. A defensible "holds" with the assumption and scenario stated is a completed review, not a refusal. If the assumption breaks, or the failure scenario is reachable, or you find another concrete defect you can justify against specific lines, your verdict is "flawed" and findings lists each one.',
121
+ `Respond with STRICT JSON only, exactly: ${PROTOCOL_SHAPE}`,
122
+ '',
123
+ codeBlock(item),
124
+ ].join('\n');
125
+ }
126
+
127
+ const SYSTEM_JSON =
128
+ 'You are a code reviewer. Output ONLY one JSON object — no prose, no markdown, no code fences. ' +
129
+ 'If the code is correct, the "findings" array is empty. Do not include any text before or after the JSON object.';
130
+
131
+ interface ArmResponse {
132
+ findings: Finding[];
133
+ verdict: 'holds' | 'flawed' | null; // null for the legacy arm (no verdict field)
134
+ }
135
+
136
+ function extractJsonObject(text: string): Record<string, unknown> | null {
137
+ if (!text) return null;
138
+ const start = text.indexOf('{');
139
+ if (start < 0) return null;
140
+ let depth = 0;
141
+ let end = -1;
142
+ for (let i = start; i < text.length; i++) {
143
+ if (text[i] === '{') depth++;
144
+ else if (text[i] === '}' && --depth === 0) {
145
+ end = i;
146
+ break;
147
+ }
148
+ }
149
+ if (end < 0) return null;
150
+ try {
151
+ return JSON.parse(text.slice(start, end + 1)) as Record<string, unknown>;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function parseArmResponse(text: string): ArmResponse {
158
+ const obj = extractJsonObject(text);
159
+ if (!obj) return { findings: [], verdict: null };
160
+ const arr = Array.isArray(obj.findings) ? obj.findings : [];
161
+ const findings = arr
162
+ .filter((f): f is Record<string, unknown> => typeof f === 'object' && f !== null)
163
+ .map((f) => ({
164
+ file: String(f.file ?? ''),
165
+ category: String(f.category ?? ''),
166
+ confidence: (['high', 'medium', 'low'].includes(String(f.confidence))
167
+ ? String(f.confidence)
168
+ : 'high') as 'high' | 'medium' | 'low',
169
+ }))
170
+ .filter((f) => f.file.length > 0);
171
+ const verdict = obj.verdict === 'holds' || obj.verdict === 'flawed' ? obj.verdict : null;
172
+ return { findings, verdict };
173
+ }
174
+
175
+ type Arm = 'legacy' | 'load_bearing';
176
+ type Vendor = 'anthropic' | 'openai';
177
+ const ARMS: readonly Arm[] = ['legacy', 'load_bearing'];
178
+ const VENDORS: readonly Vendor[] = ['anthropic', 'openai'];
179
+
180
+ /** fixtureId → arm → vendor → response */
181
+ type Reviews = Record<string, Record<Arm, Record<Vendor, ArmResponse>>>;
182
+
183
+ let _clients: Record<Vendor, ExternalAIClient> | null = null;
184
+ function getClients(): Record<Vendor, ExternalAIClient> {
185
+ if (!_clients) {
186
+ _clients = {
187
+ anthropic: new AnthropicClient({ model: 'claude-sonnet-4-5', api_key: load_anthropic_key() }),
188
+ openai: new OpenAIClient({ model: 'gpt-4o', api_key: load_openai_key() }),
189
+ };
190
+ }
191
+ return _clients;
192
+ }
193
+
194
+ async function realReviews(items: CorpusItem[]): Promise<Reviews> {
195
+ const cs = getClients();
196
+ const out: Reviews = {};
197
+ for (const item of items) {
198
+ const perArm = { legacy: {}, load_bearing: {} } as Reviews[string];
199
+ out[item.id] = perArm;
200
+ for (const arm of ARMS) {
201
+ const prompt = arm === 'legacy' ? legacyPrompt(item) : loadBearingPrompt(item);
202
+ for (const vendor of VENDORS) {
203
+ let text = '';
204
+ try {
205
+ const resp = await cs[vendor].ask(SYSTEM_JSON, prompt, 1600);
206
+ text = resp.text ?? '';
207
+ } catch (e) {
208
+ process.stderr.write(` ${vendor} error on ${item.id}/${arm}: ${(e as Error).message}\n`);
209
+ }
210
+ const r = parseArmResponse(text);
211
+ perArm[arm][vendor] = r;
212
+ process.stderr.write(
213
+ ` ${arm} ${item.id} ${vendor}: findings=${String(r.findings.length)} verdict=${r.verdict ?? '-'}\n`,
214
+ );
215
+ }
216
+ }
217
+ }
218
+ return out;
219
+ }
220
+
221
+ /**
222
+ * Mock reviews for pipeline validation in a chosen direction.
223
+ * `direction: 'pass'` — load_bearing keeps TP parity with legacy and stays
224
+ * silent on the controls. `direction: 'fail'` — load_bearing rubber-stamps
225
+ * (misses defects below the retention floor) so the verdict must read FAIL.
226
+ */
227
+ function mockReviews(items: CorpusItem[], direction: 'pass' | 'fail'): Reviews {
228
+ const hit = (i: CorpusItem): Finding[] => [
229
+ {
230
+ file: i.ground_truth.defect_files[0] ?? i.files[0]?.path ?? '',
231
+ category: i.defect_category,
232
+ confidence: 'high' as const,
233
+ },
234
+ ];
235
+ const fpHit = (i: CorpusItem): Finding[] => [
236
+ { file: i.files[0]?.path ?? '', category: 'correctness', confidence: 'high' as const },
237
+ ];
238
+ const out: Reviews = {};
239
+ const defects = items.filter((i) => !i.is_clean);
240
+ const keepIds = new Set(
241
+ // In 'fail' direction the load_bearing arm catches only half the
242
+ // defects legacy catches — below the 0.8 retention floor.
243
+ defects.filter((_, idx) => idx % 2 === 0).map((d) => d.id),
244
+ );
245
+ for (const item of items) {
246
+ const legacyResp: ArmResponse = item.is_clean
247
+ ? { findings: fpHit(item), verdict: null } // legacy FPs every control (the measured defect)
248
+ : { findings: hit(item), verdict: null };
249
+ const lbCatches = item.is_clean ? false : direction === 'pass' || keepIds.has(item.id);
250
+ const lbResp: ArmResponse = item.is_clean
251
+ ? { findings: [], verdict: 'holds' }
252
+ : lbCatches
253
+ ? { findings: hit(item), verdict: 'flawed' }
254
+ : { findings: [], verdict: 'holds' };
255
+ out[item.id] = {
256
+ legacy: { anthropic: legacyResp, openai: legacyResp },
257
+ load_bearing: { anthropic: lbResp, openai: lbResp },
258
+ };
259
+ }
260
+ return out;
261
+ }
262
+
263
+ interface VendorArmScore {
264
+ tp: number;
265
+ fp: number;
266
+ fp_rate: number;
267
+ incoherent: number;
268
+ holds_on_clean: number;
269
+ caught_ids: string[];
270
+ fp_ids: string[];
271
+ }
272
+
273
+ interface VendorVerdict {
274
+ legacy: VendorArmScore;
275
+ load_bearing: VendorArmScore;
276
+ tp_retention: number | null;
277
+ fp_pass: boolean;
278
+ tp_pass: boolean;
279
+ promotes: boolean;
280
+ }
281
+
282
+ function scoreVendor(items: CorpusItem[], reviews: Reviews, vendor: Vendor): VendorVerdict {
283
+ const defects = items.filter((i) => !i.is_clean);
284
+ const controls = items.filter((i) => i.is_clean);
285
+ const armScore = (arm: Arm): VendorArmScore => {
286
+ const caught: string[] = [];
287
+ let incoherent = 0;
288
+ let holdsOnClean = 0;
289
+ for (const d of defects) {
290
+ const r = reviews[d.id]?.[arm]?.[vendor] ?? { findings: [], verdict: null };
291
+ if (caughtDefect(groundTruth(d), r.findings)) caught.push(d.id);
292
+ if (r.verdict === 'flawed' && r.findings.length === 0) incoherent++;
293
+ }
294
+ const fpIds: string[] = [];
295
+ for (const c of controls) {
296
+ const r = reviews[c.id]?.[arm]?.[vendor] ?? { findings: [], verdict: null };
297
+ if (isFalsePositive(groundTruth(c), r.findings)) fpIds.push(c.id);
298
+ if (r.verdict === 'flawed' && r.findings.length === 0) incoherent++;
299
+ if (r.verdict === 'holds') holdsOnClean++;
300
+ }
301
+ return {
302
+ tp: caught.length,
303
+ fp: fpIds.length,
304
+ fp_rate: controls.length ? fpIds.length / controls.length : 0,
305
+ incoherent,
306
+ holds_on_clean: holdsOnClean,
307
+ caught_ids: caught,
308
+ fp_ids: fpIds,
309
+ };
310
+ };
311
+ const legacy = armScore('legacy');
312
+ const load_bearing = armScore('load_bearing');
313
+ const tp_retention = legacy.tp > 0 ? load_bearing.tp / legacy.tp : null;
314
+ const fp_pass = load_bearing.fp_rate < FP_CEILING;
315
+ // A legacy TP of 0 leaves retention undefined. NOT part of the frozen
316
+ // registration (which is silent on a zero baseline) — a conservative
317
+ // in-code rule, unexercised in the recorded run (legacy TP was 10 and 5):
318
+ // retention unshowable against a zero baseline → tp_pass false.
319
+ const tp_pass = tp_retention !== null && tp_retention >= TP_RETENTION_FLOOR;
320
+ return { legacy, load_bearing, tp_retention, fp_pass, tp_pass, promotes: fp_pass && tp_pass };
321
+ }
322
+
323
+ function buildReport(items: CorpusItem[], reviews: Reviews, mode: string): Record<string, unknown> {
324
+ const perVendor: Record<string, VendorVerdict> = {};
325
+ for (const vendor of VENDORS) perVendor[vendor] = scoreVendor(items, reviews, vendor);
326
+ const promotes = VENDORS.every((v) => perVendor[v]?.promotes === true);
327
+ const detail: Record<string, unknown> = {};
328
+ for (const item of items) {
329
+ const perItem: Record<string, unknown> = { is_clean: item.is_clean };
330
+ for (const arm of ARMS) {
331
+ for (const vendor of VENDORS) {
332
+ const r = reviews[item.id]?.[arm]?.[vendor];
333
+ perItem[`${arm}_${vendor}`] = r
334
+ ? { findings: r.findings.length, verdict: r.verdict }
335
+ : null;
336
+ }
337
+ }
338
+ detail[item.id] = perItem;
339
+ }
340
+ return {
341
+ mode,
342
+ corpus: 'internal/bench/adversarial-council/corpus.json',
343
+ claim: 'critic-protocol-load-bearing-ab (docs/CLAIMS.md, pre-registered 2026-08-09)',
344
+ protocol:
345
+ 'both arms single-shot per vendor over all 15 items; FP on findings only, identical scorer both arms; verdict secondary; flawed+empty findings = incoherent',
346
+ thresholds: { fp_ceiling: FP_CEILING, tp_retention_floor: TP_RETENTION_FLOOR },
347
+ per_vendor: perVendor,
348
+ verdict: {
349
+ promotes,
350
+ direction: promotes ? 'both thresholds met per vendor' : 'at least one threshold missed',
351
+ default_unchanged: 'legacy stays the default regardless (config critic_protocol)',
352
+ },
353
+ detail,
354
+ };
355
+ }
356
+
357
+ async function main(argv: string[]): Promise<number> {
358
+ const mock = argv.includes('--mock');
359
+ const run = argv.includes('--run');
360
+ const outIdx = argv.indexOf('--out');
361
+ const outFile = outIdx >= 0 ? argv[outIdx + 1] : null;
362
+ if (!mock && !run) {
363
+ process.stderr.write('usage: bench_critic_protocol (--mock | --run) [--out <file>]\n');
364
+ return 2;
365
+ }
366
+ const items = loadCorpus();
367
+ if (mock) {
368
+ // Tripwire (roadmap pre-mortem): the pipeline must discriminate in BOTH
369
+ // directions before any paid call.
370
+ const passReport = buildReport(items, mockReviews(items, 'pass'), 'mock-pass');
371
+ const failReport = buildReport(items, mockReviews(items, 'fail'), 'mock-fail');
372
+ const passOk = (passReport.verdict as { promotes: boolean }).promotes === true;
373
+ const failOk = (failReport.verdict as { promotes: boolean }).promotes === false;
374
+ process.stdout.write(JSON.stringify({ mock_pass: passReport, mock_fail: failReport }, null, 2) + '\n');
375
+ if (!passOk || !failOk) {
376
+ process.stderr.write(
377
+ `mock discrimination FAILED: pass-direction promotes=${String(passOk)}, fail-direction rejects=${String(failOk)} — do NOT run paid calls.\n`,
378
+ );
379
+ return 1;
380
+ }
381
+ process.stderr.write('mock discrimination OK in both directions.\n');
382
+ return 0;
383
+ }
384
+ const reviews = await realReviews(items);
385
+ const report = buildReport(items, reviews, 'run');
386
+ const json = JSON.stringify(report, null, 2) + '\n';
387
+ if (outFile) fs.writeFileSync(outFile, json);
388
+ process.stdout.write(json);
389
+ return 0;
390
+ }
391
+
392
+ if (import.meta.url === `file://${process.argv[1]}`) {
393
+ void main(process.argv.slice(2)).then((code) => process.exit(code));
394
+ }
395
+
396
+ export { parseArmResponse, scoreVendor, buildReport, mockReviews, legacyPrompt, loadBearingPrompt };
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Fail when this branch is behind the base it will be merged into — asked of
4
+ * the REMOTE, not of the local tracking ref.
5
+ *
6
+ * WHY THIS EXISTS — measured, not hypothetical.
7
+ *
8
+ * `feedback-update-branch-before-push` says: behind target → merge the base in
9
+ * and re-run the gates, before the push. On 2026-08-08 that obligation was
10
+ * satisfied in form and missed in substance. The run opened with
11
+ * `git fetch origin --prune`, branched from `origin/main` at `c0a3630eb`,
12
+ * worked for roughly ninety minutes, and pushed without fetching again. Two PRs
13
+ * merged inside that window, one of them shipping the same roadmap phase the
14
+ * branch was implementing. The PR opened `CONFLICTING`, and a concurrent branch
15
+ * had already published a different number for the same census.
16
+ *
17
+ * The failure is not a forgotten rule. It is a live-state fact read once and
18
+ * then carried: `direct-answers` Iron Law 2 forbids exactly that for branch and
19
+ * sync state, and a ninety-minute-old reading is memory, not a check. Nothing
20
+ * caught it, because nothing looked — `/roadmap:next` screens the remote before
21
+ * SELECTION, `task preflight` reads the tree, and the session register is
22
+ * advisory. The obligation was model-carried at the one point where its input
23
+ * decays fastest.
24
+ *
25
+ * THIS GATE ASKS THE REMOTE. A version that compared HEAD against the local
26
+ * `origin/main` ref would reproduce the very defect it exists to catch: that
27
+ * ref is exactly the stale reading that caused the incident. So it runs
28
+ * `git ls-remote`, one network round trip, and compares against the SHA the
29
+ * server reports right now.
30
+ *
31
+ * It never fetches, merges, or rebases for you. Choosing how to catch up is a
32
+ * git-history decision (`git-history-discipline`), and a gate that rewrites
33
+ * history to make itself pass is a worse failure than the one it reports.
34
+ */
35
+ import { execFileSync } from "node:child_process";
36
+ import fs from "node:fs";
37
+ import os from "node:os";
38
+ import path from "node:path";
39
+ import { fileURLToPath } from "node:url";
40
+
41
+ import { runGateCli, runSelfTest, type SelfTestCase } from "./_lib/gate_self_test.js";
42
+ import { reportScanned } from "./_lib/scan_scope.js";
43
+
44
+ const DEFAULT_BASE = "main";
45
+ const SELF_TEST_MIN_CASES = 3;
46
+ const SELF_TEST_MIN_REJECT = 1;
47
+ const SCRIPT_REL = "src/scripts/check_branch_freshness.ts";
48
+
49
+ function git(args: string[]): string | null {
50
+ try {
51
+ return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /** The SHA the server reports for `<base>` right now — never a tracking ref. */
58
+ export function remoteHead(base: string, remote = "origin"): string | null {
59
+ const out = git(["ls-remote", "--heads", remote, base]);
60
+ if (out === null || out === "") {
61
+ return null;
62
+ }
63
+ const sha = out.split(/\s+/)[0];
64
+ return sha !== undefined && /^[0-9a-f]{40}$/.test(sha) ? sha : null;
65
+ }
66
+
67
+ /**
68
+ * Is `sha` already contained in HEAD?
69
+ *
70
+ * `merge-base --is-ancestor` needs the object locally. A SHA the server just
71
+ * reported may not be here yet, which is itself the answer — the branch cannot
72
+ * contain a commit it has never seen.
73
+ */
74
+ export function containsCommit(sha: string): boolean | null {
75
+ if (git(["cat-file", "-e", `${sha}^{commit}`]) === null) {
76
+ return false;
77
+ }
78
+ try {
79
+ execFileSync("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { stdio: "ignore" });
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Build a throwaway origin + working clone, and optionally let a SECOND clone
88
+ * push so the working clone's tracking ref goes stale without it noticing.
89
+ *
90
+ * The stale-tracking-ref case is the whole point. A fixture that only advances
91
+ * the remote via the same clone would leave `origin/main` correct locally, and a
92
+ * gate reading the tracking ref would pass it — which is exactly the false green
93
+ * this gate exists to remove.
94
+ */
95
+ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
96
+ const origin = path.join(root, "origin.git");
97
+ const work = path.join(root, "work");
98
+ fs.mkdirSync(origin, { recursive: true });
99
+ const g = (args: string[], at: string): void => {
100
+ execFileSync("git", args, { cwd: at, stdio: "ignore" });
101
+ };
102
+ const write = (at: string, name: string, body: string): void => {
103
+ fs.writeFileSync(path.join(at, name), body);
104
+ g(["add", "-A"], at);
105
+ g(["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", name], at);
106
+ };
107
+ g(["init", "--bare", "-b", DEFAULT_BASE], origin);
108
+ g(["init", "-b", DEFAULT_BASE, work], root);
109
+ g(["remote", "add", "origin", origin], work);
110
+ write(work, "a.txt", "one");
111
+ g(["push", "-u", "origin", DEFAULT_BASE], work);
112
+ g(["checkout", "-b", "feat/x"], work);
113
+ write(work, "b.txt", "two");
114
+ if (opts.advanceRemote) {
115
+ const other = path.join(root, "other");
116
+ g(["clone", origin, other], root);
117
+ write(other, "c.txt", "three");
118
+ g(["push"], other);
119
+ }
120
+ return work;
121
+ }
122
+
123
+ export function selfTest(repoRoot: string): number {
124
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "cbf-selftest-"));
125
+ const run = (advanceRemote: boolean, onBase: boolean): number => {
126
+ const dir = fs.mkdtempSync(path.join(root, "case-"));
127
+ const work = buildFixture(dir, { advanceRemote });
128
+ if (onBase) {
129
+ execFileSync("git", ["checkout", DEFAULT_BASE], { cwd: work, stdio: "ignore" });
130
+ }
131
+ // The child must not inherit a CI flag, or every case would no-op green.
132
+ delete process.env["CI"];
133
+ delete process.env["GITHUB_ACTIONS"];
134
+ return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
135
+ };
136
+ const cases: SelfTestCase[] = [
137
+ {
138
+ name: "a branch whose tracking ref is stale while the remote has moved is REFUSED",
139
+ expect: "reject",
140
+ run: () => run(true, false),
141
+ },
142
+ {
143
+ name: "a branch containing the remote base passes",
144
+ expect: "accept",
145
+ run: () => run(false, false),
146
+ },
147
+ {
148
+ name: "standing on the base branch itself is a no-op, not a refusal",
149
+ expect: "accept",
150
+ run: () => run(false, true),
151
+ },
152
+ ];
153
+ try {
154
+ return runSelfTest({
155
+ gate: "check_branch_freshness",
156
+ cases,
157
+ minCases: SELF_TEST_MIN_CASES,
158
+ minRejectCases: SELF_TEST_MIN_REJECT,
159
+ });
160
+ } finally {
161
+ fs.rmSync(root, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Publish the scan scope: how many remote base refs this run actually resolved.
167
+ *
168
+ * One when the server answered, zero on every path that legitimately has
169
+ * nothing to ask — each with its reason, because a gate reporting success over
170
+ * an empty scope is the false green this repo keeps finding, and "there was
171
+ * nothing to check" and "I checked and it was fine" must not print the same.
172
+ */
173
+ function scanReport(scanned: number, allowEmpty?: string): void {
174
+ reportScanned({
175
+ gate: "check_branch_freshness",
176
+ scanned,
177
+ units: "remote base ref(s)",
178
+ roots: ["origin"],
179
+ ...(allowEmpty === undefined ? {} : { allowEmpty }),
180
+ });
181
+ }
182
+
183
+ export function main(argv: string[] = process.argv.slice(2)): number {
184
+ if (argv.includes("--self-test")) {
185
+ return selfTest(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
186
+ }
187
+ const quiet = argv.includes("--quiet");
188
+ const baseArg = argv.indexOf("--base");
189
+ const base = baseArg >= 0 ? (argv[baseArg + 1] ?? DEFAULT_BASE) : DEFAULT_BASE;
190
+
191
+ // CI merges or refuses on the server; a second opinion here would be noise.
192
+ if (process.env["CI"] === "true" || process.env["GITHUB_ACTIONS"] === "true") {
193
+ scanReport(0, "no-op in CI — the forge owns mergeability, and a second opinion here is noise");
194
+ return 0;
195
+ }
196
+
197
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
198
+ if (branch === null || branch === "HEAD") {
199
+ scanReport(0, "detached HEAD — there is no branch whose freshness could be asked");
200
+ return 0;
201
+ }
202
+ if (branch === base) {
203
+ scanReport(0, `standing on ${base} itself — a branch cannot be behind itself`);
204
+ return 0;
205
+ }
206
+
207
+ const sha = remoteHead(base);
208
+ if (sha === null) {
209
+ // Loud, not silent. An offline run has not verified the invariant, and a
210
+ // gate that passes on an invariant it never evaluated is the false green
211
+ // this repo keeps finding.
212
+ scanReport(0, `origin/${base} unreachable — reported NOT VERIFIED rather than green`);
213
+ process.stdout.write(
214
+ `check_branch_freshness: could not reach origin/${base} — NOT VERIFIED (offline?). ` +
215
+ "Re-run before pushing.\n",
216
+ );
217
+ return 0;
218
+ }
219
+
220
+ scanReport(1);
221
+ if (containsCommit(sha) === true) {
222
+ if (!quiet) {
223
+ process.stdout.write(`✅ branch is current with origin/${base} (${sha.slice(0, 9)})` + "\n");
224
+ }
225
+ return 0;
226
+ }
227
+
228
+ console.error(`❌ branch is BEHIND origin/${base} — the remote is at ${sha.slice(0, 9)}.`);
229
+ console.error("");
230
+ console.error(" Pushing now opens a PR that may conflict, and worse: another branch");
231
+ console.error(" may already have shipped what this one is implementing.");
232
+ console.error("");
233
+ console.error(` git fetch origin && git merge origin/${base}`);
234
+ console.error(" then re-run the gates before pushing.");
235
+ console.error("");
236
+ console.error(
237
+ " This gate asks the REMOTE, not your tracking ref — a fetch from earlier in",
238
+ );
239
+ console.error(" the session is a memory, not a check (direct-answers Iron Law 2).");
240
+ return 1;
241
+ }
242
+
243
+ const invokedDirectly = ((): boolean => {
244
+ const entry = process.argv[1];
245
+ if (entry === undefined) return false;
246
+ try {
247
+ return fs.realpathSync(path.resolve(entry)) === fs.realpathSync(fileURLToPath(import.meta.url));
248
+ } catch {
249
+ return false;
250
+ }
251
+ })();
252
+
253
+ if (invokedDirectly) {
254
+ process.exit(main());
255
+ }