@event4u/agent-config 9.28.0 → 9.30.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +172 -5
  4. package/README.md +2 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/inbox.md +22 -1
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/explain-run.md +31 -1
  9. package/dist/agent-src/commands/team/adversarial.md +8 -7
  10. package/dist/agent-src/commands/team/delegate.md +14 -12
  11. package/dist/agent-src/commands/team/review.md +9 -8
  12. package/dist/agent-src/commands/team/status.md +9 -8
  13. package/dist/agent-src/commands/team.md +29 -19
  14. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  15. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  17. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +22 -1
  18. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
  19. package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
  20. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  21. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  22. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  23. package/dist/agent-src/rules/delegation-policy.md +11 -7
  24. package/dist/agent-src/rules/preservation-guard.md +9 -0
  25. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  26. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  27. package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
  28. package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
  29. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  30. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  31. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  32. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  33. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +17 -5
  34. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  35. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  36. package/dist/agent-src/templates/AGENTS.md +4 -4
  37. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  38. package/dist/agent-src/templates/roadmaps.md +23 -0
  39. package/dist/cli/registry.js +2 -0
  40. package/dist/cli/registry.js.map +1 -1
  41. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  42. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
  43. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
  44. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
  45. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
  46. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  47. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  48. package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
  49. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  50. package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
  51. package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
  52. package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
  53. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  54. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  55. package/dist/cli-delegate/cmd_conformance.js +2803 -137
  56. package/dist/cli-delegate/cmd_doctor.js +10 -5
  57. package/dist/cli-delegate/cmd_explain.js +2 -2
  58. package/dist/cli-delegate/cmd_export.js +4 -4
  59. package/dist/cli-delegate/cmd_handoff.js +88 -8
  60. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  61. package/dist/cli-delegate/cmd_migrate.js +2 -2
  62. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  63. package/dist/cli-delegate/cmd_prune.js +4 -4
  64. package/dist/cli-delegate/cmd_route_audit.js +3 -2
  65. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  66. package/dist/cli-delegate/cmd_session_recycle.js +177 -0
  67. package/dist/cli-delegate/cmd_settings_get.js +2 -8
  68. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  69. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +4 -4
  71. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  72. package/dist/cli-delegate/cmd_update.js +1 -1
  73. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  74. package/dist/cli-delegate/cmd_validate.js +4 -4
  75. package/dist/cli-delegate/cmd_versions.js +1 -1
  76. package/dist/discovery/deprecation-report.md +1 -1
  77. package/dist/discovery/discovery-manifest.json +72 -23
  78. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  79. package/dist/discovery/discovery-manifest.summary.md +3 -2
  80. package/dist/discovery/orphan-report.md +1 -1
  81. package/dist/discovery/packs.json +39 -2
  82. package/dist/discovery/trust-report.md +2 -2
  83. package/dist/discovery/workspaces.json +12 -4
  84. package/dist/hooks/dispatch.js +2278 -611
  85. package/dist/install/install.mjs +56 -17
  86. package/dist/mcp/registry-manifest.json +3 -3
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +4122 -699
  89. package/dist/router.json +1 -1
  90. package/dist/scripts/_lib/agent_settings.js +54 -0
  91. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  92. package/dist/scripts/condense.js +147 -2
  93. package/dist/scripts/condense.js.map +1 -1
  94. package/dist/server/schemas/settings.js +12 -7
  95. package/dist/server/schemas/settings.js.map +1 -1
  96. package/dist/shared/settingsCarveOut.js +0 -9
  97. package/dist/shared/settingsCarveOut.js.map +1 -1
  98. package/dist/shared/settingsClasses.js +11 -9
  99. package/dist/shared/settingsClasses.js.map +1 -1
  100. package/dist/ui/assets/index-B5qGYOJo.js +26 -0
  101. package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
  102. package/dist/ui/index.html +1 -1
  103. package/docs/contracts/settings-classes.md +58 -12
  104. package/package.json +1 -1
  105. package/src/agent-src/scripts/roadmap_gates.ts +312 -0
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
  107. package/src/agent-src/templates/AGENTS.md +4 -4
  108. package/src/config/agent-settings.template.yml +88 -66
  109. package/src/config/discovery/packs.yml +11 -0
  110. package/src/config/discovery/workspaces.yml +1 -1
  111. package/src/config/dispatch-economy-metrics.json +61 -0
  112. package/src/config/evaluator-budgets.json +4 -4
  113. package/src/config/gate-violation-baselines.json +3 -3
  114. package/src/config/hook-token-budget.json +91 -0
  115. package/src/config/recycle-threshold-budget.json +12 -0
  116. package/src/config/rule-enforcement-baseline.json +86 -0
  117. package/src/scripts/_cli/cmd_conformance.ts +31 -44
  118. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  119. package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
  120. package/src/scripts/_dispatch.bash +23 -0
  121. package/src/scripts/_lib/agent_settings.ts +58 -0
  122. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  123. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  124. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  125. package/src/scripts/_lib/host_capability.ts +63 -6
  126. package/src/scripts/_lib/judgment_ladder.ts +634 -0
  127. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  128. package/src/scripts/_lib/orchestration_record.ts +78 -10
  129. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  130. package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
  131. package/src/scripts/_lib/self_repair.ts +354 -13
  132. package/src/scripts/_lib/self_repair_store.ts +28 -0
  133. package/src/scripts/_lib/session_eol.ts +230 -0
  134. package/src/scripts/_lib/session_role.ts +42 -0
  135. package/src/scripts/_lib/subagent_capsule.ts +341 -0
  136. package/src/scripts/_lib/subagent_response.ts +94 -0
  137. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  138. package/src/scripts/_lib/subagent_steering.ts +9 -4
  139. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  140. package/src/scripts/_lib/worker_budget.ts +37 -2
  141. package/src/scripts/ai_council/clients.ts +126 -55
  142. package/src/scripts/ai_council/config.ts +94 -6
  143. package/src/scripts/ai_council/handoff.ts +113 -0
  144. package/src/scripts/ai_council/modes.ts +19 -4
  145. package/src/scripts/ai_council/orchestrator.ts +105 -0
  146. package/src/scripts/ai_council/quorum.ts +84 -0
  147. package/src/scripts/ai_council/session.ts +44 -0
  148. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  149. package/src/scripts/ai_team/availability.ts +110 -0
  150. package/src/scripts/ai_team/config.ts +34 -14
  151. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  152. package/src/scripts/ask_transport.ts +221 -0
  153. package/src/scripts/bench_critic_protocol.ts +396 -0
  154. package/src/scripts/bench_hook_injection.ts +234 -0
  155. package/src/scripts/check_adr_frontmatter.ts +84 -40
  156. package/src/scripts/check_branch_freshness.ts +1 -0
  157. package/src/scripts/check_corpus_staleness.ts +18 -4
  158. package/src/scripts/check_detector_corpus.ts +219 -0
  159. package/src/scripts/check_gitignore_freshness.ts +42 -8
  160. package/src/scripts/check_hook_bundle_freshness.ts +24 -1
  161. package/src/scripts/check_kernel_prefix_stability.ts +1 -0
  162. package/src/scripts/check_standing_rule_delivery.ts +73 -32
  163. package/src/scripts/check_static_layer_stability.ts +154 -0
  164. package/src/scripts/check_suppression_hygiene.ts +48 -0
  165. package/src/scripts/condense.ts +162 -2
  166. package/src/scripts/council_cli.ts +401 -25
  167. package/src/scripts/dispatch_economy_report.ts +448 -0
  168. package/src/scripts/dispatch_r2_reviewer.ts +15 -2
  169. package/src/scripts/explain_run.ts +206 -4
  170. package/src/scripts/forensics_report.ts +333 -0
  171. package/src/scripts/generate_settings_reference.ts +1 -0
  172. package/src/scripts/handoff_context_hook.ts +112 -4
  173. package/src/scripts/hook_manifest.yaml +117 -10
  174. package/src/scripts/hooks/concern_registry.ts +8 -0
  175. package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
  176. package/src/scripts/hooks/dispatch_hook.ts +39 -2
  177. package/src/scripts/hooks/edit_shape_hook.ts +303 -0
  178. package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
  179. package/src/scripts/hooks/reread_guard_hook.ts +314 -0
  180. package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
  181. package/src/scripts/hooks/session_eol_hook.ts +268 -0
  182. package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
  183. package/src/scripts/install-hooks.sh +1 -1
  184. package/src/scripts/language_mirror_hook.ts +89 -0
  185. package/src/scripts/lint_decision_impact.ts +14 -1
  186. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  187. package/src/scripts/lint_hook_manifest.ts +82 -0
  188. package/src/scripts/lint_never_silent.ts +276 -0
  189. package/src/scripts/lint_no_activation_gates.ts +271 -0
  190. package/src/scripts/lint_readme_serial_comma.ts +1 -0
  191. package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
  192. package/src/scripts/measure_turn_end_gate.ts +194 -0
  193. package/src/scripts/orchestration_record.ts +12 -1
  194. package/src/scripts/rebuild_hook_bundle.ts +178 -0
  195. package/src/scripts/release.ts +74 -2
  196. package/src/scripts/report_conformance_funnel.ts +284 -0
  197. package/src/scripts/report_imperative_density.ts +1 -0
  198. package/src/scripts/routing_doctor.ts +118 -36
  199. package/src/scripts/score_skill_selection.ts +19 -5
  200. package/src/scripts/self_repair_cli.ts +337 -59
  201. package/src/scripts/session_eol_report.ts +261 -0
  202. package/src/scripts/team_events_spike.ts +271 -0
  203. package/src/scripts/team_review_gate_hook.ts +31 -9
  204. package/src/server/schemas/settings.ts +24 -17
  205. package/src/shared/settingsCarveOut.ts +0 -10
  206. package/src/shared/settingsClasses.ts +11 -9
  207. package/src/templates/compact-instructions.md +40 -0
  208. package/dist/ui/assets/index-DmR0mbk4.js +0 -26
  209. package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rung-0.5 ask transport — one prompt, one answer, no harness, no session
4
+ * (road-to-token-economy-dispatch Phase 4.1).
5
+ *
6
+ * The judgment ladder's gap this closes: rung 0 routes mechanical slices to
7
+ * deterministic scripts; rung 1 jumps straight to a full subagent spawn
8
+ * whose measured median cold start is ~251k tokens (dispatch_floor,
9
+ * 2026-08-10). A single bounded QUESTION — a classification, a small
10
+ * verification, a one-file semantic lookup — needs neither a session nor
11
+ * tools: one completion over the reconciled council transport chain
12
+ * (cli → api → ∅) answers it at roughly two orders of magnitude below the
13
+ * spawn floor.
14
+ *
15
+ * Hard caps, per the roadmap's anti-loop discipline (Risk 2 — "rung 0.5
16
+ * becomes a hidden agent loop"):
17
+ * - ONE completion. No retry beyond the transport chain's own cli→api
18
+ * fallback semantics inside the client.
19
+ * - NO tool use, NO session, NO follow-up turn.
20
+ * - Honest ∅: council disabled / no member / transport failure → null
21
+ * (CLI exit 3), never a fabricated answer and never an escalation this
22
+ * module performs itself. Escalating to a spawn is the CALLER's
23
+ * decision and is recorded as such (`escalated` on the caller's line).
24
+ *
25
+ * Telemetry: every completed ask appends ONE orchestration_record line
26
+ * (route_taken: "ask", spawn_count: 0, dispatch_tokens measured from the
27
+ * provider response) — the substitution/adoption metrics registered in
28
+ * `src/config/dispatch-economy-metrics.json` read these lines.
29
+ *
30
+ * Reuses, never re-implements: `council_cli.load_settings` +
31
+ * `build_members` (member construction, auth, transport resolution,
32
+ * cli-first billing classification — all unchanged), `CouncilResponse`
33
+ * accounting, `_lib/orchestration_record` validation.
34
+ *
35
+ * Usage:
36
+ * ./scripts-run src/scripts/ask_transport "is X true in file Y?" \
37
+ * [--system "<system prompt>"] [--member <name>] [--format text|json] \
38
+ * [--no-record] [--dir <audit-dir>]
39
+ *
40
+ * Exit codes: 0 answer produced · 2 usage error · 3 honest-∅ (no transport).
41
+ */
42
+ import * as crypto from 'node:crypto';
43
+ import * as fs from 'node:fs';
44
+ import * as path from 'node:path';
45
+ import { fileURLToPath, pathToFileURL } from 'node:url';
46
+
47
+ import type { ExternalAIClient } from './ai_council/clients.js';
48
+ import { buildOrchestrationLine } from './_lib/orchestration_record.js';
49
+ import { DEFAULT_DIR } from './orchestration_record.js';
50
+
51
+ export interface AskResult {
52
+ answer: string;
53
+ member: string;
54
+ model: string;
55
+ input_tokens: number;
56
+ output_tokens: number;
57
+ latency_ms: number;
58
+ }
59
+
60
+ export interface AskOptions {
61
+ /** Injected members (tests). Default: council_cli.load_settings + build_members. */
62
+ members?: ExternalAIClient[];
63
+ /** Pick a specific member by provider/name; default: first configured. */
64
+ member?: string;
65
+ systemPrompt?: string;
66
+ /** Telemetry append target; null disables recording (tests / --no-record). */
67
+ auditDir?: string | null;
68
+ /** Injected clock/id (tests). */
69
+ now?: () => Date;
70
+ id?: () => string;
71
+ }
72
+
73
+ const DEFAULT_SYSTEM =
74
+ 'You are a single-completion answer service. Answer the question directly, ' +
75
+ 'concisely, and honestly. If the question cannot be answered without tools, ' +
76
+ 'files, or more context, reply exactly: INSUFFICIENT_CONTEXT — one line on ' +
77
+ 'what is missing. Never invent facts.';
78
+
79
+ /** Lazily import the council CLI surface so tests with injected members never load it. */
80
+ async function defaultMembers(): Promise<ExternalAIClient[]> {
81
+ const mod = await import('./council_cli.js');
82
+ const settings = mod.load_settings();
83
+ return mod.build_members(settings);
84
+ }
85
+
86
+ function recordAskLine(auditDir: string, result: AskResult | null, member: string, ts: string, id: string): void {
87
+ const { line, errors } = buildOrchestrationLine({
88
+ spawn_count: 0,
89
+ token_delta: 0,
90
+ token_delta_provenance: 'estimated',
91
+ route_taken: 'ask',
92
+ verify_mode: 'none',
93
+ dispatch_outcome: result === null ? 'BLOCKED' : 'DONE',
94
+ dispatch_tokens: result === null ? null : result.input_tokens + result.output_tokens,
95
+ wall_clock_ms: result === null ? 0 : Math.max(0, Math.round(result.latency_ms)),
96
+ agent_combo: [member || 'none'],
97
+ origin: 'dispatch-economy-2026',
98
+ ts,
99
+ id,
100
+ });
101
+ if (errors.length || line === null) return; // telemetry must never fail the ask
102
+ try {
103
+ const file = path.join(auditDir, `${ts.slice(0, 7)}.jsonl`);
104
+ fs.mkdirSync(path.dirname(file), { recursive: true });
105
+ fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
106
+ } catch {
107
+ /* fail-open — accounting never blocks the answer */
108
+ }
109
+ }
110
+
111
+ /**
112
+ * One bounded ask. Returns the answer or `null` (honest ∅ — council
113
+ * disabled, no member resolves, transport failure, or provider error).
114
+ */
115
+ export async function askOnce(prompt: string, opts: AskOptions = {}): Promise<AskResult | null> {
116
+ const trimmed = prompt.trim();
117
+ if (!trimmed) return null;
118
+
119
+ let members: ExternalAIClient[];
120
+ try {
121
+ members = opts.members ?? (await defaultMembers());
122
+ } catch {
123
+ return _finish(null, 'none', opts); // disabled / unreadable config → ∅
124
+ }
125
+ const memberName = (m: ExternalAIClient): string => {
126
+ const c = m as { name?: string; provider?: string };
127
+ return c.name || c.provider || 'unknown';
128
+ };
129
+ const pick =
130
+ opts.member !== undefined ? members.find((m) => memberName(m) === opts.member) : members[0];
131
+ if (pick === undefined) {
132
+ return _finish(null, opts.member ?? 'none', opts);
133
+ }
134
+ const provider = memberName(pick);
135
+
136
+ try {
137
+ const response = pick.ask(opts.systemPrompt ?? DEFAULT_SYSTEM, trimmed);
138
+ const r = response instanceof Promise ? await response : response;
139
+ if (r === null || r === undefined || (r.error !== null && r.error !== undefined && r.error !== '')) {
140
+ return _finish(null, provider, opts);
141
+ }
142
+ const text = (r.text ?? '').trim();
143
+ if (!text) return _finish(null, provider, opts);
144
+ return _finish(
145
+ {
146
+ answer: text,
147
+ member: provider,
148
+ model: r.model ?? 'unknown',
149
+ input_tokens: r.input_tokens ?? 0,
150
+ output_tokens: r.output_tokens ?? 0,
151
+ latency_ms: r.latency_ms ?? 0,
152
+ },
153
+ provider,
154
+ opts,
155
+ );
156
+ } catch {
157
+ return _finish(null, provider, opts); // ONE completion — no retry here
158
+ }
159
+ }
160
+
161
+ function _finish(result: AskResult | null, member: string, opts: AskOptions): AskResult | null {
162
+ const auditDir = opts.auditDir === undefined ? path.join(process.cwd(), DEFAULT_DIR) : opts.auditDir;
163
+ if (auditDir !== null) {
164
+ const ts = (opts.now ? opts.now() : new Date()).toISOString();
165
+ const id = opts.id ? opts.id() : crypto.randomUUID();
166
+ recordAskLine(auditDir, result, member, ts, id);
167
+ }
168
+ return result;
169
+ }
170
+
171
+ export async function main(argv: readonly string[]): Promise<number> {
172
+ let prompt = '';
173
+ let system: string | undefined;
174
+ let member: string | undefined;
175
+ let format: 'text' | 'json' = 'text';
176
+ let record = true;
177
+ let dir: string | undefined;
178
+ for (let i = 0; i < argv.length; i++) {
179
+ const a = argv[i] ?? '';
180
+ if (a === '--system') system = argv[++i];
181
+ else if (a === '--member') member = argv[++i];
182
+ else if (a === '--format') format = argv[++i] === 'json' ? 'json' : 'text';
183
+ else if (a === '--no-record') record = false;
184
+ else if (a === '--dir') dir = argv[++i];
185
+ else if (!a.startsWith('--')) prompt = prompt ? `${prompt} ${a}` : a;
186
+ }
187
+ if (!prompt.trim()) {
188
+ process.stderr.write('ask_transport: a question argument is required\n');
189
+ return 2;
190
+ }
191
+ const opts: AskOptions = {
192
+ ...(system !== undefined ? { systemPrompt: system } : {}),
193
+ ...(member !== undefined ? { member } : {}),
194
+ auditDir: record ? (dir ?? path.join(process.cwd(), DEFAULT_DIR)) : null,
195
+ };
196
+ const result = await askOnce(prompt, opts);
197
+ if (result === null) {
198
+ process.stderr.write('ask_transport: ∅ — no transport resolved or the single completion failed (no retry by design)\n');
199
+ return 3;
200
+ }
201
+ process.stdout.write(
202
+ format === 'json' ? JSON.stringify(result, null, 2) + '\n' : result.answer + '\n',
203
+ );
204
+ return 0;
205
+ }
206
+
207
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
208
+ function _isCliEntry(): boolean {
209
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
210
+ if (process.argv[1] === undefined) return false;
211
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
212
+ if (import.meta.url === argvUrl) return true;
213
+ try {
214
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ if (_isCliEntry()) {
220
+ main(process.argv.slice(2)).then((rc) => process.exit(rc));
221
+ }
@@ -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 };