@event4u/agent-config 8.10.0 → 8.12.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 (226) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/CHANGELOG.md +67 -149
  4. package/README.md +6 -6
  5. package/dist/agent-src/commands/challenge-me/vision.md +121 -32
  6. package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
  7. package/dist/agent-src/commands/commit/in-chunks.md +1 -1
  8. package/dist/agent-src/commands/commit.md +2 -1
  9. package/dist/agent-src/commands/cost/report.md +2 -1
  10. package/dist/agent-src/commands/feature/dev.md +4 -4
  11. package/dist/agent-src/commands/humanize.md +101 -0
  12. package/dist/agent-src/commands/jira-ticket.md +2 -2
  13. package/dist/agent-src/commands/memory/load.md +15 -0
  14. package/dist/agent-src/commands/optimize/project.md +12 -0
  15. package/dist/agent-src/commands/roadmap/create.md +12 -11
  16. package/dist/agent-src/commands/tests/create.md +1 -1
  17. package/dist/agent-src/commands/worktree/cleanup.md +22 -17
  18. package/dist/agent-src/commands/worktree/create.md +3 -1
  19. package/dist/agent-src/commands/worktree/status.md +12 -0
  20. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
  21. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
  22. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
  23. package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
  24. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +111 -85
  25. package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
  26. package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
  27. package/dist/agent-src/rules/architecture.md +6 -51
  28. package/dist/agent-src/rules/autonomous-execution.md +7 -37
  29. package/dist/agent-src/rules/broken-access-control.md +7 -45
  30. package/dist/agent-src/rules/code-comment-discipline.md +64 -0
  31. package/dist/agent-src/rules/context-hygiene.md +14 -0
  32. package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
  33. package/dist/agent-src/rules/delegation-policy.md +10 -8
  34. package/dist/agent-src/rules/design-fidelity.md +4 -0
  35. package/dist/agent-src/rules/git-history-discipline.md +4 -79
  36. package/dist/agent-src/rules/improve-before-implement.md +7 -66
  37. package/dist/agent-src/rules/legal-safety-floor.md +11 -55
  38. package/dist/agent-src/rules/no-roadmap-references.md +13 -77
  39. package/dist/agent-src/rules/persona-governance.md +5 -47
  40. package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
  41. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
  42. package/dist/agent-src/rules/roadmap-progress-sync.md +5 -56
  43. package/dist/agent-src/rules/think-before-action.md +10 -0
  44. package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
  45. package/dist/agent-src/rules/verify-before-complete.md +12 -0
  46. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +96 -12
  47. package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
  48. package/dist/agent-src/skills/api-design/SKILL.md +7 -5
  49. package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
  51. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  52. package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
  53. package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
  54. package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
  55. package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
  56. package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
  57. package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
  58. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
  59. package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
  60. package/dist/agent-src/skills/design-review/SKILL.md +23 -2
  61. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
  62. package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
  63. package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
  64. package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
  65. package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
  66. package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
  67. package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
  68. package/dist/agent-src/skills/laravel/SKILL.md +14 -1
  69. package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
  70. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
  71. package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
  72. package/dist/agent-src/skills/mcp/SKILL.md +1 -1
  73. package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
  74. package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
  75. package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
  76. package/dist/agent-src/skills/performance/SKILL.md +1 -1
  77. package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
  78. package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
  79. package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
  80. package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
  81. package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
  82. package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
  83. package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
  84. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
  85. package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
  86. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
  87. package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
  88. package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
  89. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
  90. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
  91. package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
  92. package/dist/agent-src/templates/agent-settings.md +3 -2
  93. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
  94. package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
  95. package/dist/agent-src/templates/roadmaps.md +14 -0
  96. package/dist/cli/commands/recordTriggerEval.js +4 -14
  97. package/dist/cli/commands/recordTriggerEval.js.map +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +157 -88
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +6 -6
  102. package/dist/discovery/orphan-report.md +1 -1
  103. package/dist/discovery/packs.json +13 -10
  104. package/dist/discovery/trust-report.md +3 -3
  105. package/dist/discovery/workspaces.json +14 -9
  106. package/dist/install/install.mjs +12 -7
  107. package/dist/mcp/registry-manifest.json +4 -4
  108. package/dist/router.json +1 -1
  109. package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
  110. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  111. package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
  112. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
  113. package/dist/scripts/condense.js +12 -2
  114. package/dist/scripts/condense.js.map +1 -1
  115. package/dist/server/schemas/settings.js +5 -2
  116. package/dist/server/schemas/settings.js.map +1 -1
  117. package/docs/CLAIMS.md +54 -5
  118. package/docs/architecture.md +4 -4
  119. package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
  120. package/docs/benchmark.md +90 -0
  121. package/docs/catalog.md +8 -5
  122. package/docs/command-flows.md +2 -1
  123. package/docs/contracts/ai-council-config.md +97 -0
  124. package/docs/contracts/command-clusters.md +1 -0
  125. package/docs/contracts/evidence-based-pruning.md +1 -0
  126. package/docs/contracts/hook-architecture-v1.md +25 -0
  127. package/docs/contracts/kernel-membership.md +35 -0
  128. package/docs/contracts/measurement-baseline.md +1 -0
  129. package/docs/contracts/persona-schema.md +49 -0
  130. package/docs/contracts/provider-lifecycle.md +29 -4
  131. package/docs/contracts/release-sizing.md +22 -0
  132. package/docs/contracts/write-engine.md +38 -1
  133. package/docs/customization.md +12 -0
  134. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
  135. package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
  136. package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
  137. package/docs/decisions/ADR-028-root-layout.md +1 -1
  138. package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
  139. package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
  140. package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
  141. package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
  142. package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
  143. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
  144. package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
  145. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
  146. package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
  147. package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
  148. package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
  149. package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
  150. package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
  151. package/docs/decisions/ADR-200-python-to-typescript-migration.md +11 -1
  152. package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
  153. package/docs/decisions/INDEX.md +13 -9
  154. package/docs/design/council-vs-solo-baseline.md +92 -0
  155. package/docs/design/cross-model-parity-eval.md +82 -0
  156. package/docs/design/length-neutral-judge-rerun.md +92 -0
  157. package/docs/design/utilization-window-criteria.md +51 -0
  158. package/docs/featured-skills.md +2 -2
  159. package/docs/getting-started-by-role.md +1 -1
  160. package/docs/getting-started.md +2 -2
  161. package/docs/governance-advantage.md +1 -1
  162. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
  163. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
  164. package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  165. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
  166. package/docs/guidelines/code-clarity.md +191 -1
  167. package/docs/guidelines/design-antipatterns.md +20 -0
  168. package/docs/guidelines/php/php-coding-patterns.md +12 -2
  169. package/docs/maintainers/branch-protection.md +87 -0
  170. package/docs/maintainers/system-map.md +85 -0
  171. package/docs/proof.md +32 -15
  172. package/docs/wedge/production-validator/production-validator.md +27 -0
  173. package/package.json +1 -1
  174. package/src/config/agent-settings.template.yml +38 -9
  175. package/src/scripts/_cli/cmd_doctor.ts +129 -1
  176. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
  177. package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
  178. package/src/scripts/_lib/knowledge_global.ts +50 -2
  179. package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
  180. package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
  181. package/src/scripts/_lib/lexical_index.ts +15 -2
  182. package/src/scripts/_lib/orchestration_record.ts +14 -0
  183. package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
  184. package/src/scripts/ai_council/chairman.ts +90 -0
  185. package/src/scripts/ai_council/clients.ts +18 -3
  186. package/src/scripts/ai_council/config.ts +157 -0
  187. package/src/scripts/ai_council/debate_gates.ts +89 -0
  188. package/src/scripts/ai_council/orchestrator.ts +252 -2
  189. package/src/scripts/ai_council/prompts.ts +121 -11
  190. package/src/scripts/ai_council/replay.ts +17 -0
  191. package/src/scripts/ai_council/stance_tally.ts +184 -0
  192. package/src/scripts/ai_tells_rules.ts +433 -0
  193. package/src/scripts/bench_humanizer_eval.ts +240 -0
  194. package/src/scripts/bench_persona_placebo.ts +308 -0
  195. package/src/scripts/bench_quality_rerun.ts +357 -0
  196. package/src/scripts/build_discovery_manifest.ts +6 -0
  197. package/src/scripts/build_proof.ts +30 -9
  198. package/src/scripts/check_artefact_count_messaging.ts +56 -0
  199. package/src/scripts/check_claims.ts +40 -0
  200. package/src/scripts/check_knowledge_cards.ts +33 -4
  201. package/src/scripts/check_portability.ts +1 -0
  202. package/src/scripts/check_rule_invariants.ts +10 -3
  203. package/src/scripts/complexity_report.ts +776 -0
  204. package/src/scripts/condense.ts +16 -2
  205. package/src/scripts/consumer_matrix.ts +292 -14
  206. package/src/scripts/council_cli.ts +203 -1
  207. package/src/scripts/detect_ai_tells.ts +312 -0
  208. package/src/scripts/explain_run.ts +593 -0
  209. package/src/scripts/install.ts +12 -0
  210. package/src/scripts/knowledge_global_cli.ts +235 -46
  211. package/src/scripts/lint_framework_leakage.ts +88 -7
  212. package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
  213. package/src/scripts/lint_knowledge_scale.ts +30 -0
  214. package/src/scripts/lint_originality_shingles.ts +210 -0
  215. package/src/scripts/orchestration_record.ts +1 -0
  216. package/src/scripts/rule_backlinks.ts +0 -0
  217. package/src/scripts/schemas/agent-settings.schema.json +11 -0
  218. package/src/scripts/self_review_gate.ts +41 -15
  219. package/src/scripts/skill_eval_coverage.ts +71 -3
  220. package/src/scripts/skill_linter.ts +1 -0
  221. package/src/scripts/trigger_eval_rotation.ts +344 -0
  222. package/src/scripts/update_counts.ts +5 -17
  223. package/src/scripts/worktree_cleanup_check.ts +263 -0
  224. package/src/server/schemas/settings.ts +8 -3
  225. package/src/scripts/analysis_freshness.ts +0 -362
  226. package/src/scripts/export_replay_corpus.ts +0 -208
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Persona-placebo benchmark (road-to-opt-council-deliberation Phase 4).
4
+ *
5
+ * Settles Source G's centerpiece claim by MEASUREMENT: does a famous-figure
6
+ * identity framing add judged quality beyond the method text it wraps?
7
+ *
8
+ * Three arms per fixture × provider (pre-registered):
9
+ * method — one of the five shipped advisor personas as the system prompt;
10
+ * figure — the SAME persona text, only the identity framing swapped to a
11
+ * (deceased) famous figure — text constant, identity varies;
12
+ * bare — a neutral advisor system prompt, no persona.
13
+ * Providers: anthropic (sonnet) + openai (gpt-4o) — the cross-provider axis;
14
+ * the per-provider split of the same data is the single-provider replication.
15
+ *
16
+ * Blind judging: the judge (opus) scores each answer 1–10 against the
17
+ * fixture's pre-registered rubric and NEVER sees arm labels or persona names.
18
+ *
19
+ * Pre-registered hypotheses:
20
+ * H1: method ≈ figure on judged quality (identity adds nothing);
21
+ * H2: provider diversity moves scores more than persona identity.
22
+ *
23
+ * CLI: --dry-run | [--max-usd N] [--limit N] [--output PATH]
24
+ * Exit: 0 wrote report · 1 error · 2 budget abort.
25
+ */
26
+ import { createRequire } from 'node:module';
27
+ import * as fs from 'node:fs';
28
+ import * as path from 'node:path';
29
+ import process from 'node:process';
30
+ import { fileURLToPath, pathToFileURL } from 'node:url';
31
+
32
+ import yaml from 'js-yaml';
33
+
34
+ import { signTestP } from './second_brain_retrieval.js';
35
+
36
+ const _HERE = fileURLToPath(import.meta.url);
37
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
38
+ const CORPUS = path.join(REPO_ROOT, 'internal/bench/corpora/persona-placebo.yaml');
39
+ const PERSONA_DIR = path.join(REPO_ROOT, 'src/agent-src/personas/advisors');
40
+ const OUT = path.join(REPO_ROOT, 'internal/bench/reports/persona-placebo.json');
41
+
42
+ export const ARMS = ['method', 'figure', 'bare'] as const;
43
+ export type Arm = (typeof ARMS)[number];
44
+
45
+ /** Persona → deceased famous figure (identity swap for the `figure` arm). */
46
+ export const FIGURE_MAP: Record<string, string> = {
47
+ 'first-principles': 'Richard Feynman',
48
+ contrarian: 'Charlie Munger',
49
+ executor: 'Grace Hopper',
50
+ expansionist: 'Ada Lovelace',
51
+ outsider: 'Alan Turing',
52
+ };
53
+
54
+ const PRICES: Array<{ needle: string; in_usd: number; out_usd: number }> = [
55
+ { needle: 'opus', in_usd: 15.0, out_usd: 75.0 },
56
+ { needle: 'sonnet', in_usd: 3.0, out_usd: 15.0 },
57
+ { needle: 'gpt-4o', in_usd: 2.5, out_usd: 10.0 },
58
+ ];
59
+ export function price_usd(model: string, i: number, o: number): number {
60
+ const p = PRICES.find((x) => model.includes(x.needle));
61
+ return p === undefined ? 0 : (i / 1e6) * p.in_usd + (o / 1e6) * p.out_usd;
62
+ }
63
+
64
+ export interface Fixture {
65
+ id: string;
66
+ prompt: string;
67
+ rubric: string;
68
+ }
69
+
70
+ export function load_fixtures(file = CORPUS): Fixture[] {
71
+ const doc = yaml.load(fs.readFileSync(file, 'utf-8')) as { fixtures?: unknown[] };
72
+ return (Array.isArray(doc?.fixtures) ? doc.fixtures : []).map((f) => {
73
+ const r = f as Record<string, unknown>;
74
+ return { id: String(r.id), prompt: String(r.prompt), rubric: String(r.rubric) };
75
+ });
76
+ }
77
+
78
+ export function load_personas(dir = PERSONA_DIR): Array<{ name: string; text: string }> {
79
+ return Object.keys(FIGURE_MAP)
80
+ .sort()
81
+ .map((name) => ({ name, text: fs.readFileSync(path.join(dir, `${name}.md`), 'utf-8') }));
82
+ }
83
+
84
+ /** Arm system prompts — `figure` holds the method text CONSTANT, swapping only identity. */
85
+ export function system_for(arm: Arm, persona: { name: string; text: string }): string {
86
+ if (arm === 'bare') {
87
+ return 'You are a senior engineering advisor. Answer the decision question with a firm recommendation.';
88
+ }
89
+ if (arm === 'method') {
90
+ return `You are an advisor working strictly by the following method:\n\n${persona.text}\n\nAnswer the decision question with a firm recommendation.`;
91
+ }
92
+ const figure = FIGURE_MAP[persona.name] as string;
93
+ return `You are ${figure}. Answer as ${figure} would, working strictly by the following method:\n\n${persona.text}\n\nAnswer the decision question with a firm recommendation.`;
94
+ }
95
+
96
+ export function judge_prompt(rubric: string, answer: string): string {
97
+ return (
98
+ `Score the answer below against this rubric on a 1-10 scale (10 = fully satisfies).\n` +
99
+ `Judge ONLY rubric satisfaction — ignore style, length, and who might have written it.\n\n` +
100
+ `Rubric: ${rubric}\n\n--- ANSWER ---\n${answer}\n\n` +
101
+ `Reply with exactly one line: "SCORE: <1-10>".`
102
+ );
103
+ }
104
+
105
+ export function parse_score(reply: string): number | null {
106
+ const m = /SCORE:\s*(\d{1,2})/i.exec(reply);
107
+ if (!m) return null;
108
+ const v = Number(m[1]);
109
+ return v >= 1 && v <= 10 ? v : null;
110
+ }
111
+
112
+ export function mean(xs: readonly number[]): number {
113
+ return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
114
+ }
115
+
116
+ interface Args {
117
+ dryRun: boolean;
118
+ limit: number | null;
119
+ maxUsd: number;
120
+ output: string;
121
+ }
122
+
123
+ function parse_args(argv: string[]): Args | number {
124
+ const a: Args = { dryRun: false, limit: null, maxUsd: 50, output: OUT };
125
+ for (let i = 0; i < argv.length; i += 1) {
126
+ const arg = argv[i];
127
+ if (arg === '--dry-run') a.dryRun = true;
128
+ else if (arg === '--limit') a.limit = Number(argv[(i += 1)]);
129
+ else if (arg === '--max-usd') a.maxUsd = Number(argv[(i += 1)]);
130
+ else if (arg === '--output') a.output = String(argv[(i += 1)] ?? a.output);
131
+ else if (arg === '-h' || arg === '--help') {
132
+ process.stdout.write('usage: bench_persona_placebo [--dry-run] [--limit N] [--max-usd X] [--output PATH]\n');
133
+ return 0;
134
+ } else {
135
+ process.stderr.write(`error: unknown argument: ${arg}\n`);
136
+ return 1;
137
+ }
138
+ }
139
+ return a;
140
+ }
141
+
142
+ interface AskClient {
143
+ ask(system: string, user: string): { text: string; input_tokens: number; output_tokens: number };
144
+ }
145
+
146
+ function main(argv: string[]): number {
147
+ const parsed = parse_args(argv);
148
+ if (typeof parsed === 'number') return parsed;
149
+ const args = parsed;
150
+
151
+ let fixtures = load_fixtures();
152
+ if (args.limit !== null && Number.isInteger(args.limit) && args.limit > 0) fixtures = fixtures.slice(0, args.limit);
153
+ const personas = load_personas();
154
+
155
+ let spent = 0;
156
+ const guard = (model: string, i: number, o: number): void => {
157
+ spent += price_usd(model, i, o);
158
+ if (spent > args.maxUsd) {
159
+ process.stderr.write(`\n❌ budget abort: $${spent.toFixed(2)} > cap $${args.maxUsd}\n`);
160
+ process.exit(2);
161
+ }
162
+ };
163
+
164
+ const GEN_MODELS: Array<{ provider: string; model: string }> = [
165
+ { provider: 'anthropic', model: 'claude-sonnet-4-5' },
166
+ { provider: 'openai', model: 'gpt-4o' },
167
+ ];
168
+ const JUDGE_MODEL = 'claude-opus-4-8';
169
+
170
+ let clients: Map<string, AskClient>;
171
+ let judge: AskClient;
172
+ if (args.dryRun) {
173
+ let n = 0;
174
+ const mk = (): AskClient => ({
175
+ ask: (_s: string, u: string) => ({
176
+ text: /SCORE/.test(u) || /ANSWER/.test(u) ? `SCORE: ${5 + ((n += 1) % 3)}` : `mock answer ${(n += 1)}`,
177
+ input_tokens: 200,
178
+ output_tokens: 100,
179
+ }),
180
+ });
181
+ clients = new Map(GEN_MODELS.map((g) => [g.provider, mk()]));
182
+ judge = mk();
183
+ process.stdout.write('⚠️ --dry-run: deterministic mock, no API.\n');
184
+ } else {
185
+ const require = createRequire(import.meta.url);
186
+ const c = require('./ai_council/clients.js') as {
187
+ AnthropicClient: new (o: { model: string; api_key: string }) => AskClient;
188
+ OpenAIClient: new (o: { model: string; api_key: string }) => AskClient;
189
+ load_anthropic_key: () => string;
190
+ load_openai_key: () => string;
191
+ };
192
+ const akey = c.load_anthropic_key();
193
+ clients = new Map([
194
+ ['anthropic', new c.AnthropicClient({ model: 'claude-sonnet-4-5', api_key: akey }) as AskClient],
195
+ ['openai', new c.OpenAIClient({ model: 'gpt-4o', api_key: c.load_openai_key() }) as AskClient],
196
+ ]);
197
+ judge = new c.AnthropicClient({ model: JUDGE_MODEL, api_key: akey });
198
+ process.stdout.write(
199
+ `Live run: ${fixtures.length} fixtures × 3 arms × ${GEN_MODELS.length} providers (+ blind judge). Cap $${args.maxUsd}.\n`,
200
+ );
201
+ }
202
+
203
+ // ── Generate + blind-judge every (fixture × provider × arm) cell ──────────
204
+ interface Cell {
205
+ fixture: string;
206
+ provider: string;
207
+ arm: Arm;
208
+ persona: string;
209
+ score: number | null;
210
+ }
211
+ const cells: Cell[] = [];
212
+ fixtures.forEach((fx, i) => {
213
+ const persona = personas[i % personas.length] as { name: string; text: string };
214
+ for (const g of GEN_MODELS) {
215
+ const client = clients.get(g.provider) as AskClient;
216
+ for (const arm of ARMS) {
217
+ const r = client.ask(system_for(arm, persona), fx.prompt);
218
+ guard(g.model, r.input_tokens, r.output_tokens);
219
+ const j = judge.ask('You are a strict rubric judge.', judge_prompt(fx.rubric, r.text));
220
+ guard(JUDGE_MODEL, j.input_tokens, j.output_tokens);
221
+ cells.push({ fixture: fx.id, provider: g.provider, arm, persona: persona.name, score: parse_score(j.text) });
222
+ }
223
+ process.stdout.write(` ${fx.id} × ${g.provider} done ($${spent.toFixed(2)})\n`);
224
+ }
225
+ });
226
+
227
+ // ── Stats (pre-registered) ─────────────────────────────────────────────────
228
+ const scores = (arm: Arm, provider?: string): number[] =>
229
+ cells
230
+ .filter((c) => c.arm === arm && c.score !== null && (provider === undefined || c.provider === provider))
231
+ .map((c) => c.score as number);
232
+ const arm_means: Record<string, number> = {};
233
+ for (const arm of ARMS) {
234
+ arm_means[arm] = mean(scores(arm));
235
+ for (const g of GEN_MODELS) arm_means[`${arm}:${g.provider}`] = mean(scores(arm, g.provider));
236
+ }
237
+ // H1: paired method-vs-figure per (fixture × provider)
238
+ let mfWins = 0;
239
+ let mfLosses = 0;
240
+ for (const fx of fixtures) {
241
+ for (const g of GEN_MODELS) {
242
+ const m = cells.find((c) => c.fixture === fx.id && c.provider === g.provider && c.arm === 'method')?.score;
243
+ const f = cells.find((c) => c.fixture === fx.id && c.provider === g.provider && c.arm === 'figure')?.score;
244
+ if (typeof m === 'number' && typeof f === 'number' && m !== f) {
245
+ if (m > f) mfWins += 1;
246
+ else mfLosses += 1;
247
+ }
248
+ }
249
+ }
250
+ const h1_p = signTestP(mfWins, mfLosses);
251
+ const identity_delta = Math.abs((arm_means['method'] as number) - (arm_means['figure'] as number));
252
+ const provider_delta = mean(
253
+ ARMS.map((a) => Math.abs((arm_means[`${a}:anthropic`] as number) - (arm_means[`${a}:openai`] as number))),
254
+ );
255
+ const h2_holds = provider_delta > identity_delta;
256
+ const persona_lift = Math.max(
257
+ (arm_means['method'] as number) - (arm_means['bare'] as number),
258
+ (arm_means['figure'] as number) - (arm_means['bare'] as number),
259
+ );
260
+
261
+ const verdict =
262
+ h1_p >= 0.05 && identity_delta < 0.5
263
+ ? `honest-null: identity framing adds nothing beyond the method text (Δ=${identity_delta.toFixed(2)}, p=${h1_p.toFixed(3)}); provider diversity ${h2_holds ? 'dominates' : 'does NOT dominate'} persona identity (provider Δ=${provider_delta.toFixed(2)})`
264
+ : `signal: method-vs-figure differ (Δ=${identity_delta.toFixed(2)}, p=${h1_p.toFixed(3)}) — inspect per-arm means before any adoption`;
265
+
266
+ const payload = {
267
+ generated_by: 'bench_persona_placebo',
268
+ dry_run: args.dryRun,
269
+ preregistered: {
270
+ hypotheses: ['H1: method ≈ figure (identity adds nothing beyond method text)', 'H2: provider diversity > persona identity'],
271
+ fixtures: fixtures.length,
272
+ arms: ARMS,
273
+ providers: GEN_MODELS.map((g) => g.provider),
274
+ judge: JUDGE_MODEL,
275
+ blind: 'judge never sees arm labels or persona names',
276
+ },
277
+ arm_means,
278
+ h1: { method_wins: mfWins, figure_wins: mfLosses, sign_p: h1_p, identity_delta },
279
+ h2: { provider_delta, identity_delta, holds: h2_holds },
280
+ persona_lift_vs_bare: persona_lift,
281
+ cells,
282
+ cost_usd_actual: Number(spent.toFixed(4)),
283
+ verdict,
284
+ };
285
+ fs.mkdirSync(path.dirname(args.output), { recursive: true });
286
+ fs.writeFileSync(args.output, JSON.stringify(payload, null, 2) + '\n');
287
+ process.stdout.write(
288
+ `\n→ wrote ${path.relative(REPO_ROOT, args.output)}\n` +
289
+ ` means: method=${(arm_means['method'] as number).toFixed(2)} figure=${(arm_means['figure'] as number).toFixed(2)} bare=${(arm_means['bare'] as number).toFixed(2)} · H1 p=${h1_p.toFixed(3)} · $${spent.toFixed(2)}\n` +
290
+ ` VERDICT: ${verdict}\n`,
291
+ );
292
+ return 0;
293
+ }
294
+
295
+ function _isCliEntry(): boolean {
296
+ if (process.argv[1] === undefined) return false;
297
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
298
+ try {
299
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+ if (_isCliEntry()) {
305
+ process.exit(main(process.argv.slice(2)));
306
+ }
307
+
308
+ export { main };
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Length-neutral thin-vs-eager judge RERUN (road-to-opt-measurement-unblock
4
+ * Phase 1; design: docs/design/length-neutral-judge-rerun.md).
5
+ *
6
+ * The 2026-07-09/11 runs were untrustworthy on three recorded failure modes;
7
+ * this rerun fixes each BY CONSTRUCTION:
8
+ * (a) length confound — pairs outside a ±15% output-token band are DROPPED
9
+ * (reported, never silently discarded) AND a Spearman-ρ win-vs-length
10
+ * diagnostic flags residual leakage (|ρ| ≥ 0.3 → verdict withheld);
11
+ * (b) judge inconsistency — TWO arm-blind judges from different provider
12
+ * families (strongest Anthropic tier + OpenAI), each judging both orders
13
+ * (reject-on-flip), reported as Cohen's κ with a ≥ 0.60 admissibility
14
+ * floor (`judgeKappa` — its first production caller);
15
+ * (c) underpowered — n is PRE-REGISTERED as the full labelled corpus before
16
+ * the first billable call; the achievable power at the corpus size is
17
+ * stated honestly in the report (corpus-limited, not grown post-hoc).
18
+ *
19
+ * Budget: --max-usd guard — cumulative actual spend (usage × PRICES) aborts
20
+ * the run before the call that would breach the cap.
21
+ *
22
+ * CLI:
23
+ * ./scripts-run src/scripts/bench_quality_rerun --dry-run
24
+ * ./scripts-run src/scripts/bench_quality_rerun --max-usd 250 [--limit N]
25
+ * [--gen-model claude-sonnet-4-5] [--judge1 claude-opus-4-8] [--judge2 gpt-4o]
26
+ *
27
+ * Exit codes: 0 wrote report · 1 error · 2 budget abort.
28
+ */
29
+ import { createRequire } from 'node:module';
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import process from 'node:process';
33
+ import { fileURLToPath, pathToFileURL } from 'node:url';
34
+
35
+ import {
36
+ assemble_contexts,
37
+ judge_prompt,
38
+ load_golden,
39
+ parse_verdict,
40
+ type GoldenTask,
41
+ } from './bench_quality_run.js';
42
+ import {
43
+ evaluatePair,
44
+ judgeKappa,
45
+ type JudgeFn,
46
+ type PairResult,
47
+ } from './check_quality_regression.js';
48
+ import { signTestP } from './second_brain_retrieval.js';
49
+
50
+ const _HERE = fileURLToPath(import.meta.url);
51
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
52
+ const OUT = path.join(REPO_ROOT, 'internal/bench/reports/quality-rerun-length-neutral.json');
53
+
54
+ /** Pre-registered analysis constants — fixed BEFORE the first billable call. */
55
+ export const PAIR_BAND = 0.15; // ±15% output-token band
56
+ export const KAPPA_FLOOR = 0.6;
57
+ export const SPEARMAN_FLAG = 0.3; // |ρ| at or above → length-confounded
58
+ export const EFFECT_FLOOR_PP = 10; // smallest effect worth shipping (pp from 50%)
59
+
60
+ /** USD per 1M tokens (mirrors internal/bench/pricing.yaml + public OpenAI). */
61
+ const PRICES: Array<{ needle: string; in_usd: number; out_usd: number }> = [
62
+ { needle: 'opus', in_usd: 15.0, out_usd: 75.0 },
63
+ { needle: 'sonnet', in_usd: 3.0, out_usd: 15.0 },
64
+ { needle: 'haiku', in_usd: 0.25, out_usd: 1.25 },
65
+ { needle: 'gpt-4o', in_usd: 2.5, out_usd: 10.0 },
66
+ ];
67
+
68
+ export function price_usd(model: string, in_tok: number, out_tok: number): number {
69
+ const p = PRICES.find((x) => model.includes(x.needle));
70
+ if (p === undefined) return 0;
71
+ return (in_tok / 1e6) * p.in_usd + (out_tok / 1e6) * p.out_usd;
72
+ }
73
+
74
+ /** ±band pairing filter on output tokens. */
75
+ export function within_band(thinTok: number, eagerTok: number, band = PAIR_BAND): boolean {
76
+ const mx = Math.max(thinTok, eagerTok);
77
+ if (mx === 0) return true;
78
+ return Math.abs(thinTok - eagerTok) / mx <= band;
79
+ }
80
+
81
+ /** Spearman rank correlation (average-rank ties). */
82
+ export function spearman(xs: readonly number[], ys: readonly number[]): number {
83
+ const n = Math.min(xs.length, ys.length);
84
+ if (n < 3) return 0;
85
+ const rank = (v: readonly number[]): number[] => {
86
+ const idx = v.map((x, i) => [x, i] as const).sort((a, b) => a[0] - b[0]);
87
+ const r = new Array<number>(n);
88
+ let i = 0;
89
+ while (i < n) {
90
+ let j = i;
91
+ while (j + 1 < n && (idx[j + 1] as [number, number])[0] === (idx[i] as [number, number])[0]) j++;
92
+ const avg = (i + j) / 2 + 1;
93
+ for (let k = i; k <= j; k++) r[(idx[k] as [number, number])[1]] = avg;
94
+ i = j + 1;
95
+ }
96
+ return r;
97
+ };
98
+ const rx = rank(xs.slice(0, n));
99
+ const ry = rank(ys.slice(0, n));
100
+ const mean = (n + 1) / 2;
101
+ let num = 0;
102
+ let dx = 0;
103
+ let dy = 0;
104
+ for (let i = 0; i < n; i++) {
105
+ const a = (rx[i] as number) - mean;
106
+ const b = (ry[i] as number) - mean;
107
+ num += a * b;
108
+ dx += a * a;
109
+ dy += b * b;
110
+ }
111
+ const den = Math.sqrt(dx * dy);
112
+ return den === 0 ? 0 : num / den;
113
+ }
114
+
115
+ export interface RerunVerdictInput {
116
+ kappa: number;
117
+ rho: number;
118
+ agreed_thin: number;
119
+ agreed_eager: number;
120
+ }
121
+
122
+ /** Pre-registered verdict logic (pure). */
123
+ export function decide_verdict(v: RerunVerdictInput): string {
124
+ if (!Number.isFinite(v.kappa) || v.kappa < KAPPA_FLOOR) {
125
+ return `inconclusive-low-kappa (κ=${v.kappa.toFixed(2)} < ${KAPPA_FLOOR})`;
126
+ }
127
+ if (Math.abs(v.rho) >= SPEARMAN_FLAG) {
128
+ return `inconclusive-length-confounded (|ρ|=${Math.abs(v.rho).toFixed(2)} ≥ ${SPEARMAN_FLAG})`;
129
+ }
130
+ const n = v.agreed_thin + v.agreed_eager;
131
+ if (n === 0) return 'inconclusive-no-decisive-pairs';
132
+ const p = signTestP(v.agreed_thin, v.agreed_eager);
133
+ const rate = v.agreed_thin / n;
134
+ const pp = Math.abs(rate - 0.5) * 100;
135
+ if (p < 0.05 && pp >= EFFECT_FLOOR_PP) {
136
+ return rate > 0.5
137
+ ? `ships-lift (thin wins ${(rate * 100).toFixed(1)}%, p=${p.toFixed(4)})`
138
+ : `honest-null-thin-loses (thin ${(rate * 100).toFixed(1)}%, p=${p.toFixed(4)})`;
139
+ }
140
+ return `inconclusive-underpowered (p=${p.toFixed(4)}, effect ${pp.toFixed(1)}pp, n=${n})`;
141
+ }
142
+
143
+ interface Args {
144
+ dryRun: boolean;
145
+ genModel: string;
146
+ judge1: string;
147
+ judge2: string;
148
+ limit: number | null;
149
+ maxUsd: number;
150
+ output: string;
151
+ }
152
+
153
+ function parse_args(argv: string[]): Args | number {
154
+ const a: Args = {
155
+ dryRun: false,
156
+ genModel: 'claude-sonnet-4-5',
157
+ judge1: 'claude-opus-4-8',
158
+ judge2: 'gpt-4o',
159
+ limit: null,
160
+ maxUsd: 250,
161
+ output: OUT,
162
+ };
163
+ for (let i = 0; i < argv.length; i += 1) {
164
+ const arg = argv[i];
165
+ if (arg === '--dry-run') a.dryRun = true;
166
+ else if (arg === '--gen-model') a.genModel = String(argv[(i += 1)] ?? a.genModel);
167
+ else if (arg === '--judge1') a.judge1 = String(argv[(i += 1)] ?? a.judge1);
168
+ else if (arg === '--judge2') a.judge2 = String(argv[(i += 1)] ?? a.judge2);
169
+ else if (arg === '--output') a.output = String(argv[(i += 1)] ?? a.output);
170
+ else if (arg === '--limit') a.limit = Number(argv[(i += 1)]);
171
+ else if (arg === '--max-usd') a.maxUsd = Number(argv[(i += 1)]);
172
+ else if (arg === '-h' || arg === '--help') {
173
+ process.stdout.write(
174
+ 'usage: bench_quality_rerun [--dry-run] [--gen-model M] [--judge1 M] [--judge2 M] [--limit N] [--max-usd X] [--output PATH]\n',
175
+ );
176
+ return 0;
177
+ } else {
178
+ process.stderr.write(`error: unknown argument: ${arg}\n`);
179
+ return 1;
180
+ }
181
+ }
182
+ return a;
183
+ }
184
+
185
+ interface AskClient {
186
+ ask(system: string, user: string): { text: string; input_tokens: number; output_tokens: number };
187
+ }
188
+
189
+ function main(argv: string[]): number {
190
+ const parsed = parse_args(argv);
191
+ if (typeof parsed === 'number') return parsed;
192
+ const args = parsed;
193
+
194
+ let tasks = load_golden();
195
+ if (args.limit !== null && Number.isInteger(args.limit) && args.limit > 0) {
196
+ tasks = tasks.slice(0, args.limit);
197
+ }
198
+ const preregistered_n = tasks.length;
199
+ const contexts = assemble_contexts();
200
+
201
+ let spent = 0;
202
+ const guard = (model: string, in_tok: number, out_tok: number): void => {
203
+ spent += price_usd(model, in_tok, out_tok);
204
+ if (spent > args.maxUsd) {
205
+ process.stderr.write(`\n❌ budget abort: cumulative $${spent.toFixed(2)} > cap $${args.maxUsd}\n`);
206
+ process.exit(2);
207
+ }
208
+ };
209
+
210
+ let gen: AskClient;
211
+ let j1: AskClient;
212
+ let j2: AskClient;
213
+ if (args.dryRun) {
214
+ let flip = 0;
215
+ const mk = (): AskClient => ({
216
+ ask: (_s: string, u: string) => ({
217
+ text: /VERDICT/.test(u) || /ANSWER A/.test(u) ? 'VERDICT: TIE' : `mock answer ${(flip += 1)}`,
218
+ input_tokens: 100,
219
+ output_tokens: 50,
220
+ }),
221
+ });
222
+ gen = mk();
223
+ j1 = mk();
224
+ j2 = mk();
225
+ process.stdout.write('⚠️ --dry-run: deterministic mock, no API.\n');
226
+ } else {
227
+ const require = createRequire(import.meta.url);
228
+ const c = require('./ai_council/clients.js') as {
229
+ AnthropicClient: new (o: { model: string; api_key: string }) => AskClient;
230
+ OpenAIClient: new (o: { model: string; api_key: string }) => AskClient;
231
+ load_anthropic_key: () => string;
232
+ load_openai_key: () => string;
233
+ };
234
+ const akey = c.load_anthropic_key();
235
+ gen = new c.AnthropicClient({ model: args.genModel, api_key: akey });
236
+ j1 = new c.AnthropicClient({ model: args.judge1, api_key: akey });
237
+ j2 = new c.OpenAIClient({ model: args.judge2, api_key: c.load_openai_key() });
238
+ process.stdout.write(
239
+ `Live rerun: n=${preregistered_n} (pre-registered) × 2 arms; judges ${args.judge1} + ${args.judge2}; cap $${args.maxUsd}.\n`,
240
+ );
241
+ }
242
+
243
+ // ── Generation (thin + eager per task, output tokens recorded) ────────────
244
+ const SYS =
245
+ 'You are an AI coding agent governed by the following always-loaded package rules. ' +
246
+ 'Answer the user request as that agent would, honouring these rules:\n\n';
247
+ interface Gen {
248
+ task: GoldenTask;
249
+ thin: string;
250
+ eager: string;
251
+ thin_tok: number;
252
+ eager_tok: number;
253
+ }
254
+ const gens: Gen[] = [];
255
+ for (const task of tasks) {
256
+ const rt = gen.ask(SYS + contexts.thin, task.prompt);
257
+ guard(args.genModel, rt.input_tokens, rt.output_tokens);
258
+ const re = gen.ask(SYS + contexts.eager, task.prompt);
259
+ guard(args.genModel, re.input_tokens, re.output_tokens);
260
+ gens.push({ task, thin: rt.text, eager: re.text, thin_tok: rt.output_tokens, eager_tok: re.output_tokens });
261
+ process.stdout.write(` gen ${task.id} (thin ${rt.output_tokens}t / eager ${re.output_tokens}t) $${spent.toFixed(2)}\n`);
262
+ }
263
+
264
+ // ── Pairing filter (±band on output tokens) ───────────────────────────────
265
+ const surviving = gens.filter((g) => within_band(g.thin_tok, g.eager_tok));
266
+ const dropped = gens.filter((g) => !within_band(g.thin_tok, g.eager_tok));
267
+ process.stdout.write(`pairing: ${surviving.length} surviving / ${dropped.length} dropped (±${PAIR_BAND * 100}% band)\n`);
268
+
269
+ // ── Two blind judges, both orders each ────────────────────────────────────
270
+ const judgeWith = (client: AskClient, model: string): PairResult[] =>
271
+ surviving.map((g) => {
272
+ const fn: JudgeFn = (_ctx, first, second) => {
273
+ const r = client.ask('You are a strict, length-neutral answer judge.', judge_prompt(g.task, first, second));
274
+ guard(model, r.input_tokens, r.output_tokens);
275
+ return parse_verdict(r.text);
276
+ };
277
+ return evaluatePair(g.task, g.thin, g.eager, fn);
278
+ });
279
+ const r1 = judgeWith(j1, args.judge1);
280
+ const r2 = judgeWith(j2, args.judge2);
281
+
282
+ // ── Stats (pre-registered) ─────────────────────────────────────────────────
283
+ const kappa = judgeKappa(r1, r2);
284
+ const byId2 = new Map(r2.map((r) => [r.id, r.winner]));
285
+ const agreed = r1.filter(
286
+ (r) => (r.winner === 'thin' || r.winner === 'eager') && byId2.get(r.id) === r.winner,
287
+ );
288
+ const agreed_thin = agreed.filter((r) => r.winner === 'thin').length;
289
+ const agreed_eager = agreed.filter((r) => r.winner === 'eager').length;
290
+ const tokDelta = new Map(surviving.map((g) => [g.task.id, g.thin_tok - g.eager_tok]));
291
+ const rho = spearman(
292
+ agreed.map((r) => tokDelta.get(r.id) ?? 0),
293
+ agreed.map((r) => (r.winner === 'thin' ? 1 : -1)),
294
+ );
295
+ const p = signTestP(agreed_thin, agreed_eager);
296
+ const verdict = decide_verdict({ kappa, rho, agreed_thin, agreed_eager });
297
+
298
+ const payload = {
299
+ generated_by: 'bench_quality_rerun',
300
+ design: 'docs/design/length-neutral-judge-rerun.md',
301
+ dry_run: args.dryRun,
302
+ gen_model: args.genModel,
303
+ judges: [args.judge1, args.judge2],
304
+ preregistered: {
305
+ n: preregistered_n,
306
+ pair_band: PAIR_BAND,
307
+ kappa_floor: KAPPA_FLOOR,
308
+ spearman_flag: SPEARMAN_FLAG,
309
+ effect_floor_pp: EFFECT_FLOOR_PP,
310
+ power_note:
311
+ 'n is corpus-limited (all labelled golden tasks); 80% power at ≥10pp needs ~190 decisive pairs — achievable power at this n is lower and stated rather than grown post-hoc.',
312
+ },
313
+ pairing: {
314
+ surviving: surviving.length,
315
+ dropped: dropped.length,
316
+ dropped_ids: dropped.map((g) => g.task.id),
317
+ },
318
+ judge_agreement: { kappa, kappa_floor: KAPPA_FLOOR },
319
+ length_diagnostic: { spearman_rho: rho, flag_at: SPEARMAN_FLAG, flagged: Math.abs(rho) >= SPEARMAN_FLAG },
320
+ result: {
321
+ agreed_decisive: agreed.length,
322
+ agreed_thin,
323
+ agreed_eager,
324
+ sign_test_p: p,
325
+ thin_win_rate: agreed.length > 0 ? agreed_thin / agreed.length : null,
326
+ },
327
+ judge1_results: r1,
328
+ judge2_results: r2,
329
+ cost_usd_actual: Number(spent.toFixed(4)),
330
+ verdict,
331
+ };
332
+ fs.mkdirSync(path.dirname(args.output), { recursive: true });
333
+ fs.writeFileSync(args.output, JSON.stringify(payload, null, 2) + '\n');
334
+ process.stdout.write(
335
+ `\n→ wrote ${path.relative(REPO_ROOT, args.output)}\n` +
336
+ ` κ=${Number.isFinite(kappa) ? kappa.toFixed(3) : 'NaN'} · ρ=${rho.toFixed(3)} · agreed thin ${agreed_thin} / eager ${agreed_eager} · p=${p.toFixed(4)} · $${spent.toFixed(2)}\n` +
337
+ ` VERDICT: ${verdict}\n`,
338
+ );
339
+ return 0;
340
+ }
341
+
342
+ function _isCliEntry(): boolean {
343
+ if (process.argv[1] === undefined) return false;
344
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
345
+ try {
346
+ return (
347
+ fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]))
348
+ );
349
+ } catch {
350
+ return false;
351
+ }
352
+ }
353
+ if (_isCliEntry()) {
354
+ process.exit(main(process.argv.slice(2)));
355
+ }
356
+
357
+ export { main };
@@ -419,6 +419,12 @@ function* _iter_artefacts(): Generator<[string, string]> {
419
419
  // `_classify` subagent branch derives the manifest payload (default-off,
420
420
  // packs:[]) — static projection only, never runtime-dispatched.
421
421
  for (const p of _collect('subagents', '*.md')) {
422
+ // `_`-prefixed files are injected partials (e.g. _prompt-defense.md),
423
+ // not projectable agents — never discovery artefacts. Mirrors the
424
+ // same filter in condense.generate_claude_subagents.
425
+ if (path.basename(p).startsWith('_')) {
426
+ continue;
427
+ }
422
428
  yield [p, 'subagent'];
423
429
  }
424
430
  }