@event4u/agent-config 8.8.0 → 8.9.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 (131) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +109 -274
  3. package/README.md +2 -2
  4. package/dist/agent-src/commands/agent-handoff.md +39 -2
  5. package/dist/agent-src/commands/research/deep.md +8 -0
  6. package/dist/agent-src/commands/research/report.md +7 -0
  7. package/dist/agent-src/commands/roadmap/create.md +12 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +31 -5
  9. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
  10. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +21 -5
  11. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
  12. package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
  13. package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
  14. package/dist/agent-src/rules/agent-authority.md +1 -1
  15. package/dist/agent-src/rules/communication-through-line.md +1 -1
  16. package/dist/agent-src/rules/delegation-policy.md +5 -0
  17. package/dist/agent-src/rules/direct-answers.md +1 -1
  18. package/dist/agent-src/rules/downstream-changes.md +1 -0
  19. package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
  20. package/dist/agent-src/rules/git-history-discipline.md +18 -0
  21. package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
  22. package/dist/agent-src/rules/minimal-safe-diff.md +58 -10
  23. package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
  24. package/dist/agent-src/rules/question-not-instruction.md +76 -0
  25. package/dist/agent-src/rules/senior-engineering-discipline.md +1 -1
  26. package/dist/agent-src/rules/think-before-action.md +1 -0
  27. package/dist/agent-src/rules/tool-safety.md +8 -0
  28. package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
  29. package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
  30. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
  31. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +10 -3
  32. package/dist/agent-src/skills/code-review/SKILL.md +2 -0
  33. package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
  34. package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
  35. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
  36. package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
  37. package/dist/agent-src/skills/design-review/SKILL.md +29 -15
  38. package/dist/agent-src/skills/design-system-capture/SKILL.md +63 -24
  39. package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
  40. package/dist/agent-src/skills/frontend-render-security/SKILL.md +2 -2
  41. package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
  42. package/dist/agent-src/skills/mcp/SKILL.md +24 -0
  43. package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
  44. package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
  45. package/dist/agent-src/skills/roadmap-writing/SKILL.md +39 -5
  46. package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
  47. package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
  48. package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
  49. package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
  50. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
  51. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +12 -0
  52. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +29 -0
  53. package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
  54. package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +36 -0
  57. package/dist/cli/registry.js +3 -0
  58. package/dist/cli/registry.js.map +1 -1
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +90 -48
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +4 -4
  63. package/dist/discovery/orphan-report.md +1 -1
  64. package/dist/discovery/packs.json +11 -9
  65. package/dist/discovery/trust-report.md +2 -2
  66. package/dist/discovery/workspaces.json +9 -7
  67. package/dist/install/install.mjs +136 -1
  68. package/dist/mcp/registry-manifest.json +3 -3
  69. package/dist/router.json +1 -1
  70. package/dist/scripts/_lib/claude_builtin_names.js +145 -0
  71. package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
  72. package/dist/scripts/condense.js +14 -0
  73. package/dist/scripts/condense.js.map +1 -1
  74. package/docs/CLAIMS.md +34 -6
  75. package/docs/architecture.md +2 -2
  76. package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
  77. package/docs/benchmark.md +113 -0
  78. package/docs/catalog.md +20 -7
  79. package/docs/contracts/command-clusters.md +18 -0
  80. package/docs/contracts/design-artifact-verification.md +124 -0
  81. package/docs/contracts/injected-block-authenticity-model.md +66 -0
  82. package/docs/contracts/mcp-tool-inventory.md +28 -28
  83. package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
  84. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +9 -0
  85. package/docs/getting-started-by-role.md +1 -1
  86. package/docs/getting-started.md +1 -1
  87. package/docs/governance-advantage.md +1 -1
  88. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
  89. package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
  90. package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
  91. package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
  92. package/docs/guidelines/design-canon.md +1 -0
  93. package/docs/proof.md +36 -21
  94. package/docs/second-brain-scope.md +69 -26
  95. package/package.json +1 -1
  96. package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
  97. package/src/scripts/_dispatch.bash +27 -0
  98. package/src/scripts/_lib/claude_builtin_names.ts +148 -0
  99. package/src/scripts/_lib/file_slicer.ts +117 -0
  100. package/src/scripts/_lib/lexical_index.ts +155 -0
  101. package/src/scripts/_lib/orchestration_record.ts +170 -0
  102. package/src/scripts/_lib/orchestration_savings.ts +62 -9
  103. package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
  104. package/src/scripts/_lib/security_lint.ts +1 -0
  105. package/src/scripts/_lib/stat_index.ts +98 -0
  106. package/src/scripts/benchmark.ts +122 -0
  107. package/src/scripts/build_proof.ts +30 -17
  108. package/src/scripts/check_memory.ts +2 -0
  109. package/src/scripts/check_quality_regression.ts +51 -0
  110. package/src/scripts/check_roadmap_trackable.ts +3 -2
  111. package/src/scripts/condense.ts +14 -0
  112. package/src/scripts/discovery_graph.ts +0 -0
  113. package/src/scripts/external_sources_denylist.json +4 -1
  114. package/src/scripts/generate_index.ts +5 -2
  115. package/src/scripts/install-hooks.sh +25 -0
  116. package/src/scripts/install.ts +35 -2
  117. package/src/scripts/learning_sidecar.ts +0 -0
  118. package/src/scripts/lint_agent_skill_names.ts +29 -0
  119. package/src/scripts/lint_knowledge_scale.ts +4 -3
  120. package/src/scripts/lint_roadmap_complexity.ts +124 -0
  121. package/src/scripts/lint_versioned_cache.ts +195 -0
  122. package/src/scripts/mcp_server/consumer_tool_catalog.json +2 -1
  123. package/src/scripts/mcp_server/tools.ts +24 -1
  124. package/src/scripts/measure_lexical_ranking.ts +173 -0
  125. package/src/scripts/memory_lookup.ts +0 -0
  126. package/src/scripts/orchestration_record.ts +140 -0
  127. package/src/scripts/orchestration_savings_report.ts +29 -5
  128. package/src/scripts/schemas/skill.schema.json +4 -0
  129. package/src/scripts/score_domain_truth.ts +192 -0
  130. package/src/scripts/second_brain_retrieval.ts +278 -0
  131. package/src/scripts/second_brain_run.ts +341 -0
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Second-brain RETRIEVAL-PRECISION follow-up
4
+ * (road-to-second-brain-retrieval-precision).
5
+ *
6
+ * The Phase-2 delta run (road-to-second-brain-delta-proof) injected the exact
7
+ * prior fact — a context-value UPPER BOUND assuming perfect retrieval. This
8
+ * follow-up removes that assumption: it runs the REAL `memory_lookup` retrieval
9
+ * against a populated decision store whose distractors deliberately share query
10
+ * keywords with the needed entry, and measures:
11
+ *
12
+ * - precision@k — did retrieval surface the NEEDED decision in the top-k?
13
+ * DETERMINISTIC + free (no model), so reported even in
14
+ * --dry-run. This is the substrate's real retrieval quality.
15
+ * - end-to-end — three arms, scored like Phase 2:
16
+ * retrieval-on : inject the top-k RETRIEVED entries (confusers included)
17
+ * retrieval-off : no memory
18
+ * placebo : inject k FIXED unrelated entries (equal count, no query)
19
+ * retrieval-on vs off/placebo tests whether keyword-retrieved context — with
20
+ * whatever confuser noise it carries — actually helps the model answer.
21
+ *
22
+ * A low precision@k is a legitimate, honest finding: it is evidence for the
23
+ * lint_knowledge_scale / SQLite-FTS5 activation path (ADR-116), not a failure.
24
+ *
25
+ * Modes: --dry-run (real precision@k + stubbed arms, no spend) · --run (live
26
+ * arms, SPEND) · --host <id> · --seeds <n>. Exit 0 ok / 1 error / 2 usage.
27
+ */
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+ import { fileURLToPath, pathToFileURL } from 'node:url';
31
+
32
+ import * as YAML from 'yaml';
33
+
34
+ import { load_anthropic_key } from './ai_council/clients.js';
35
+ import {
36
+ _setIntakeRoot,
37
+ _setKnowledgeRoot,
38
+ _setMemoryRoot,
39
+ retrieve,
40
+ } from './memory_lookup.js';
41
+ import { scoreTask } from './second_brain_score.js';
42
+
43
+ const _HERE = fileURLToPath(import.meta.url);
44
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
45
+ export const STORE_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'second-brain', 'retrieval-store');
46
+ export const REPORT_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'reports');
47
+
48
+ const DEFAULT_HOST = 'claude-haiku-4-5-20251001';
49
+ const DEFAULT_SEEDS = 3;
50
+ /** Fixed unrelated entries for the placebo arm (equal-count inert context). */
51
+ const PLACEBO_IDS = ['dist-ci-runner', 'dist-lint-strict', 'dist-docs-site', 'dist-monorepo'];
52
+
53
+ export type Arm = 'retrieval-off' | 'retrieval-on' | 'placebo';
54
+ export const ARMS: readonly Arm[] = ['retrieval-off', 'retrieval-on', 'placebo'];
55
+
56
+ export interface RetrievalTask {
57
+ id: string;
58
+ query: string[];
59
+ needed: string;
60
+ prompt: string;
61
+ answer_key: { must_contain: string[]; must_not_contain: string[] };
62
+ }
63
+
64
+ export function bindStore(): void {
65
+ _setMemoryRoot(STORE_DIR);
66
+ _setKnowledgeRoot(path.join(STORE_DIR, 'knowledge-none'));
67
+ _setIntakeRoot(path.join(STORE_DIR, 'intake-none'));
68
+ }
69
+
70
+ export function loadCorpus(): { type: string; k: number; tasks: RetrievalTask[] } {
71
+ const doc = YAML.parse(fs.readFileSync(path.join(STORE_DIR, 'retrieval-corpus.yml'), 'utf-8')) as {
72
+ type: string;
73
+ k: number;
74
+ tasks: RetrievalTask[];
75
+ };
76
+ return doc;
77
+ }
78
+
79
+ /**
80
+ * Retrieve for a task's query. Returns the top-k bodies to inject PLUS the
81
+ * discrimination diagnostics the keyword scorer's tie behaviour demands:
82
+ * - rank of the needed entry in the full sorted list (-1 if absent)
83
+ * - tie_set_size: how many entries share the TOP score. The scorer gives 0.8
84
+ * to any keyword match and breaks ties by store order (not relevance), so a
85
+ * tie_set_size > 1 means retrieval did NOT rank the needed entry by
86
+ * relevance — it recalled it into a bucket. This is the honest
87
+ * discrimination signal (ADR-116 evidence), separate from precision@k.
88
+ */
89
+ export function retrieveFor(
90
+ task: RetrievalTask,
91
+ type: string,
92
+ k: number,
93
+ ): { topk: Array<{ id: string; body: string }>; rank: number; tieSetSize: number; topScore: number } {
94
+ const all = retrieve([type], task.query, 100);
95
+ const topScore = all.length ? all[0]!.score : 0;
96
+ const tieSetSize = all.filter((h) => h.score === topScore).length;
97
+ const rank = all.findIndex((h) => h.id === task.needed);
98
+ const topk = all.slice(0, k).map((h) => ({ id: h.id, body: String((h.entry as { body?: unknown }).body ?? '') }));
99
+ return { topk, rank, tieSetSize, topScore };
100
+ }
101
+
102
+ /** Read the fixed placebo entries' bodies straight from the store file. */
103
+ function _placeboBodies(type: string): string[] {
104
+ const doc = YAML.parse(fs.readFileSync(path.join(STORE_DIR, `${type}.yml`), 'utf-8')) as {
105
+ entries: Array<{ id: string; body?: string }>;
106
+ };
107
+ const byId = new Map(doc.entries.map((e) => [e.id, e.body ?? '']));
108
+ return PLACEBO_IDS.map((id) => byId.get(id) ?? '').filter(Boolean);
109
+ }
110
+
111
+ function _prompt(task: RetrievalTask, arm: Arm, retrieved: string[], placebo: string[]): string {
112
+ const preamble =
113
+ 'You are continuing a multi-session engineering project. Answer using the ' +
114
+ 'project decisions on record. Be concise and concrete.';
115
+ if (arm === 'retrieval-off') return `${preamble}\n\n${task.prompt}`;
116
+ const bodies = arm === 'retrieval-on' ? retrieved : placebo;
117
+ const block =
118
+ 'Retrieved project memory:\n' + bodies.map((b) => `- ${b.trim()}`).join('\n');
119
+ return `${preamble}\n\n${block}\n\n${task.prompt}`;
120
+ }
121
+
122
+ interface CallResult { text: string; tokens_in: number; tokens_out: number }
123
+ async function callAnthropic(model: string, prompt: string, apiKey: string): Promise<CallResult> {
124
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
125
+ method: 'POST',
126
+ headers: { 'content-type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
127
+ body: JSON.stringify({ model, max_tokens: 256, temperature: 0.7, messages: [{ role: 'user', content: prompt }] }),
128
+ });
129
+ if (!res.ok) throw new Error(`anthropic ${res.status}: ${(await res.text()).slice(0, 300)}`);
130
+ const data = (await res.json()) as {
131
+ content?: Array<{ type: string; text?: string }>;
132
+ usage?: { input_tokens?: number; output_tokens?: number };
133
+ };
134
+ const text = (data.content ?? []).filter((b) => b.type === 'text').map((b) => b.text ?? '').join('');
135
+ return { text, tokens_in: data.usage?.input_tokens ?? 0, tokens_out: data.usage?.output_tokens ?? 0 };
136
+ }
137
+
138
+ function _scoreObj(task: RetrievalTask) {
139
+ return { id: task.id, metric: 'retrieval-accuracy' as const, session_k: '', session_k1_prompt: task.prompt, answer_key: task.answer_key };
140
+ }
141
+ function stub(task: RetrievalTask, arm: Arm, neededRetrieved: boolean): string {
142
+ // retrieval-on passes ONLY if the needed entry was actually retrieved.
143
+ if (arm === 'retrieval-on' && neededRetrieved) {
144
+ return `${task.answer_key.must_contain.join(' ')} — per the retrieved decision.`;
145
+ }
146
+ return "I don't have that decision on record here.";
147
+ }
148
+
149
+ /** Two-sided sign-test p-value via exact binomial (ties excluded). */
150
+ export function signTestP(wins: number, losses: number): number {
151
+ const n = wins + losses;
152
+ if (n === 0) return 1;
153
+ const k = Math.min(wins, losses);
154
+ let cum = 0;
155
+ for (let i = 0; i <= k; i++) {
156
+ let c = 1;
157
+ for (let j = 0; j < i; j++) c = (c * (n - j)) / (j + 1);
158
+ cum += c;
159
+ }
160
+ return Math.min(1, (cum / 2 ** n) * 2);
161
+ }
162
+
163
+ export async function run(opts: { mode: 'dry-run' | 'live'; host: string; seeds: number }): Promise<Record<string, unknown>> {
164
+ bindStore();
165
+ const { type, k, tasks } = loadCorpus();
166
+ const apiKey = opts.mode === 'live' ? load_anthropic_key() : '';
167
+ const placebo = _placeboBodies(type);
168
+
169
+ let precisionHits = 0;
170
+ let tieSetSum = 0;
171
+ const perTask: Array<Record<string, unknown>> = [];
172
+ const armPass: Record<Arm, number> = { 'retrieval-off': 0, 'retrieval-on': 0, placebo: 0 };
173
+ const armTotal: Record<Arm, number> = { 'retrieval-off': 0, 'retrieval-on': 0, placebo: 0 };
174
+ const taskArmPass: Record<string, Record<Arm, number>> = {};
175
+ let calls = 0, tIn = 0, tOut = 0;
176
+
177
+ for (const task of tasks) {
178
+ const { topk, rank, tieSetSize } = retrieveFor(task, type, k);
179
+ const ids = topk.map((r) => r.id);
180
+ const neededInTopK = rank >= 0 && rank < k;
181
+ if (neededInTopK) precisionHits += 1;
182
+ tieSetSum += tieSetSize;
183
+ const tap: Record<Arm, number> = { 'retrieval-off': 0, 'retrieval-on': 0, placebo: 0 };
184
+
185
+ for (const arm of ARMS) {
186
+ for (let s = 0; s < opts.seeds; s++) {
187
+ let transcript: string;
188
+ if (opts.mode === 'dry-run') {
189
+ transcript = stub(task, arm, neededInTopK);
190
+ } else {
191
+ const r = await callAnthropic(opts.host, _prompt(task, arm, topk.map((x) => x.body), placebo), apiKey);
192
+ transcript = r.text; calls += 1; tIn += r.tokens_in; tOut += r.tokens_out;
193
+ }
194
+ armTotal[arm] += 1;
195
+ if (scoreTask(transcript, _scoreObj(task)).pass) { armPass[arm] += 1; tap[arm] += 1; }
196
+ }
197
+ }
198
+ taskArmPass[task.id] = tap;
199
+ perTask.push({ id: task.id, needed: task.needed, needed_rank: rank, needed_in_top_k: neededInTopK, tie_set_size: tieSetSize, top_k_ids: ids });
200
+ }
201
+
202
+ const precisionAtK = tasks.length ? precisionHits / tasks.length : 0;
203
+ const meanTieSet = tasks.length ? tieSetSum / tasks.length : 0;
204
+ const pair = (a: Arm, b: Arm) => {
205
+ let aw = 0, bw = 0, ties = 0;
206
+ for (const t of tasks) {
207
+ const tap = taskArmPass[t.id]!;
208
+ const pa = tap[a], pb = tap[b];
209
+ if (pa > pb) aw += 1; else if (pb > pa) bw += 1; else ties += 1;
210
+ }
211
+ return { a_wins: aw, b_wins: bw, ties, sign_p: signTestP(aw, bw) };
212
+ };
213
+ const onOff = pair('retrieval-on', 'retrieval-off');
214
+ const onPlacebo = pair('retrieval-on', 'placebo');
215
+ const verdict =
216
+ onOff.a_wins > onOff.b_wins && onOff.sign_p < 0.05 &&
217
+ onPlacebo.a_wins > onPlacebo.b_wins && onPlacebo.sign_p < 0.05 ? 'PASS' : 'NULL';
218
+
219
+ return {
220
+ schema_version: 1,
221
+ kind: `second-brain-retrieval-${opts.mode}`,
222
+ roadmap: 'road-to-second-brain-retrieval-precision',
223
+ host: opts.host,
224
+ seeds: opts.seeds,
225
+ mode: opts.mode,
226
+ k,
227
+ store: 'internal/bench/second-brain/retrieval-store/ (5 needed + 19 distractors, keyword-overlapping confusers)',
228
+ precision_at_k: precisionAtK,
229
+ precision_hits: precisionHits,
230
+ mean_tie_set_size: meanTieSet,
231
+ discrimination_note:
232
+ 'mean_tie_set_size > 1 means the keyword scorer gave the same top score to multiple ' +
233
+ 'entries and broke the tie by STORE ORDER, not relevance — precision@k reflects recall ' +
234
+ 'into the top-k, not relevance ranking. This is the ADR-116 (FTS5) signal.',
235
+ tasks: tasks.length,
236
+ per_task: perTask,
237
+ aggregate: Object.fromEntries(ARMS.map((a) => [a, { pass: armPass[a], total: armTotal[a] }])),
238
+ paired: { 'on-vs-off': onOff, 'on-vs-placebo': onPlacebo },
239
+ verdict,
240
+ note:
241
+ 'precision@k is deterministic (retrieval only, no spend); the arms are model-scored. ' +
242
+ 'A retrieval-on PASS means keyword retrieval both FOUND the right decision under confuser ' +
243
+ 'load AND the model used it; a NULL with high precision isolates a model-use gap, a NULL ' +
244
+ 'with low precision is evidence for the FTS5 activation path (ADR-116).',
245
+ cost: { calls, tokens_in: tIn, tokens_out: tOut },
246
+ };
247
+ }
248
+
249
+ export async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
250
+ const mode = argv.includes('--run') ? 'live' : argv.includes('--dry-run') ? 'dry-run' : null;
251
+ if (mode === null) { process.stderr.write('usage: second_brain_retrieval --dry-run | --run [--host <id>] [--seeds <n>]\n'); return 2; }
252
+ const hi = argv.indexOf('--host'); const host = hi >= 0 ? String(argv[hi + 1]) : DEFAULT_HOST;
253
+ const si = argv.indexOf('--seeds'); const seeds = si >= 0 ? Number(argv[si + 1]) : DEFAULT_SEEDS;
254
+ let report: Record<string, unknown>;
255
+ try { report = await run({ mode, host, seeds }); } catch (e) { process.stderr.write(`second_brain_retrieval: ${String(e)}\n`); return 1; }
256
+ if (mode === 'live') {
257
+ fs.mkdirSync(REPORT_DIR, { recursive: true });
258
+ const out = path.join(REPORT_DIR, 'second-brain-retrieval.json');
259
+ fs.writeFileSync(out, JSON.stringify(report, null, 2) + '\n');
260
+ process.stdout.write(`wrote ${path.relative(REPO_ROOT, out)}\n`);
261
+ }
262
+ const a = report.aggregate as Record<Arm, { pass: number; total: number }>;
263
+ process.stdout.write(
264
+ `${mode}: precision@${report.k}=${((report.precision_at_k as number) * 100).toFixed(0)}% (${report.precision_hits}/${report.tasks}) · ` +
265
+ `mean-tie-set=${(report.mean_tie_set_size as number).toFixed(1)} · ` +
266
+ `on=${a['retrieval-on'].pass}/${a['retrieval-on'].total} off=${a['retrieval-off'].pass}/${a['retrieval-off'].total} placebo=${a.placebo.pass}/${a.placebo.total} · ` +
267
+ `${report.verdict} · calls=${(report.cost as { calls: number }).calls}\n`,
268
+ );
269
+ return 0;
270
+ }
271
+
272
+ function _isCliEntry(): boolean {
273
+ if (process.argv[1] === undefined) return false;
274
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
275
+ if (import.meta.url === argvUrl) return true;
276
+ try { return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1])); } catch { return false; }
277
+ }
278
+ if (_isCliEntry()) { main().then((code) => process.exit(code)); }
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Second-brain paired measurement — substrate on vs off vs placebo
4
+ * (road-to-second-brain-delta-proof Phase 2).
5
+ *
6
+ * Runs the deterministic recall corpus (Phase 1) through a FIXED host under
7
+ * three arms and scores each transcript with `second_brain_score` (no
8
+ * model-in-the-loop grading):
9
+ *
10
+ * - memory-off — the k+1 question alone (no prior-session context): the
11
+ * no-memory baseline; the agent must re-derive from scratch.
12
+ * - memory-on — the question + the prior-session fact the substrate would
13
+ * surface (perfect retrieval): the substrate's ideal output.
14
+ * - placebo — the question + an EQUAL-BYTE inert block (no relevant
15
+ * content): isolates "relevant retrieved content" from "mere extra
16
+ * context", exactly as the discipline benchmark isolates content from
17
+ * length.
18
+ *
19
+ * A PASS requires memory-on to beat BOTH memory-off AND placebo (a sign test
20
+ * over the paired per-task scores). Otherwise an honest NULL is recorded.
21
+ *
22
+ * HONEST SCOPING (stated, not buried): the Phase-1 corpus is one-fact-per-task
23
+ * by construction, so memory-on injects the exact fact — this measures the
24
+ * CONTEXT-VALUE UPPER BOUND (does the right prior fact, perfectly retrieved,
25
+ * let the model answer) NOT the substrate's retrieval PRECISION under a large
26
+ * store. A large-store retrieval-precision corpus is the follow-up; this run
27
+ * bounds the ceiling and isolates it from mere-extra-context via the placebo.
28
+ *
29
+ * Modes:
30
+ * --dry-run build every prompt + score a STUBBED "ideal" transcript per
31
+ * arm (memory-on stub contains the fact, off/placebo do not) — no
32
+ * API calls, no spend; proves the harness + arms wire correctly.
33
+ * --run live calls to the fixed host (SPEND). Writes the pinned report.
34
+ * --host <id> fixed host model id (default claude-haiku-4-5-20251001).
35
+ * --seeds <n> repetitions per arm×task (default 3).
36
+ *
37
+ * Exit codes: 0 ok / 1 run error / 2 usage.
38
+ */
39
+ import * as fs from 'node:fs';
40
+ import * as path from 'node:path';
41
+ import { fileURLToPath, pathToFileURL } from 'node:url';
42
+
43
+ import { load_anthropic_key } from './ai_council/clients.js';
44
+ import { loadCorpus, scoreTask, type RecallTask } from './second_brain_score.js';
45
+
46
+ const _HERE = fileURLToPath(import.meta.url);
47
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
48
+ export const REPORT_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'reports');
49
+
50
+ const DEFAULT_HOST = 'claude-haiku-4-5-20251001';
51
+ const DEFAULT_SEEDS = 3;
52
+
53
+ export type Arm = 'memory-off' | 'memory-on' | 'placebo';
54
+ export const ARMS: readonly Arm[] = ['memory-off', 'memory-on', 'placebo'];
55
+
56
+ /** The prior-session memory block the substrate would surface for a task. */
57
+ function memoryBlock(task: RecallTask): string {
58
+ return `Prior-session memory (recalled by the second-brain substrate):\n- ${task.session_k}`;
59
+ }
60
+
61
+ /** Equal-byte inert filler, sized to the memory block, with no relevant content. */
62
+ function placeboBlock(task: RecallTask): string {
63
+ const target = memoryBlock(task).length;
64
+ const filler =
65
+ 'Prior-session memory (recalled by the second-brain substrate):\n- ' +
66
+ 'Note: routine housekeeping; no decisions or constraints were recorded in this window. ';
67
+ let out = filler;
68
+ while (out.length < target) out += 'Nothing further of record. ';
69
+ return out.slice(0, target);
70
+ }
71
+
72
+ /** Build the arm-specific prompt for a task. */
73
+ export function buildPrompt(task: RecallTask, arm: Arm): string {
74
+ const q = task.session_k1_prompt;
75
+ const preamble =
76
+ 'You are continuing a multi-session engineering project. Answer the ' +
77
+ 'question using what the project has decided so far. Be concise and concrete.';
78
+ if (arm === 'memory-off') {
79
+ return `${preamble}\n\n${q}`;
80
+ }
81
+ const block = arm === 'memory-on' ? memoryBlock(task) : placeboBlock(task);
82
+ return `${preamble}\n\n${block}\n\n${q}`;
83
+ }
84
+
85
+ // --- minimal Anthropic Messages call (reuses the repo key resolution) -------
86
+
87
+ interface CallResult {
88
+ text: string;
89
+ tokens_in: number;
90
+ tokens_out: number;
91
+ }
92
+
93
+ async function callAnthropic(
94
+ model: string,
95
+ prompt: string,
96
+ apiKey: string,
97
+ ): Promise<CallResult> {
98
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
99
+ method: 'POST',
100
+ headers: {
101
+ 'content-type': 'application/json',
102
+ 'x-api-key': apiKey,
103
+ 'anthropic-version': '2023-06-01',
104
+ },
105
+ body: JSON.stringify({
106
+ model,
107
+ max_tokens: 256,
108
+ temperature: 0.7,
109
+ messages: [{ role: 'user', content: prompt }],
110
+ }),
111
+ });
112
+ if (!res.ok) {
113
+ throw new Error(`anthropic ${res.status}: ${(await res.text()).slice(0, 300)}`);
114
+ }
115
+ const data = (await res.json()) as {
116
+ content?: Array<{ type: string; text?: string }>;
117
+ usage?: { input_tokens?: number; output_tokens?: number };
118
+ };
119
+ const text = (data.content ?? [])
120
+ .filter((b) => b.type === 'text')
121
+ .map((b) => b.text ?? '')
122
+ .join('');
123
+ return {
124
+ text,
125
+ tokens_in: data.usage?.input_tokens ?? 0,
126
+ tokens_out: data.usage?.output_tokens ?? 0,
127
+ };
128
+ }
129
+
130
+ /** Stub transcript for --dry-run: memory-on "recalls" the fact, others don't. */
131
+ function stubTranscript(task: RecallTask, arm: Arm): string {
132
+ if (arm === 'memory-on') {
133
+ return `${task.session_k} — carrying that forward: ${task.answer_key.must_contain.join(' ')}.`;
134
+ }
135
+ return "I don't have that from a prior session; I'd need it re-stated.";
136
+ }
137
+
138
+ interface ArmTaskResult {
139
+ task: string;
140
+ metric: string;
141
+ arm: Arm;
142
+ passes: number; // over seeds
143
+ seeds: number;
144
+ tokens_in: number;
145
+ tokens_out: number;
146
+ }
147
+
148
+ export interface RunReport {
149
+ schema_version: number;
150
+ kind: string;
151
+ roadmap: string;
152
+ host: string;
153
+ seeds: number;
154
+ mode: 'dry-run' | 'live';
155
+ scoping: string;
156
+ per_arm_task: ArmTaskResult[];
157
+ aggregate: Record<Arm, { pass: number; total: number; pass_rate: number }>;
158
+ paired: {
159
+ 'on-vs-off': { on_wins: number; off_wins: number; ties: number; sign_p: number };
160
+ 'on-vs-placebo': { on_wins: number; placebo_wins: number; ties: number; sign_p: number };
161
+ };
162
+ verdict: 'PASS' | 'NULL';
163
+ cost: { calls: number; tokens_in: number; tokens_out: number };
164
+ }
165
+
166
+ /** Two-sided sign-test p-value via exact binomial (ties excluded). */
167
+ export function signTestP(wins: number, losses: number): number {
168
+ const n = wins + losses;
169
+ if (n === 0) return 1;
170
+ const k = Math.min(wins, losses);
171
+ // sum of binomial tail P(X<=k) under p=0.5, times 2, capped at 1
172
+ let cum = 0;
173
+ for (let i = 0; i <= k; i++) cum += _choose(n, i);
174
+ const p = (cum / 2 ** n) * 2;
175
+ return Math.min(1, p);
176
+ }
177
+ function _choose(n: number, k: number): number {
178
+ let r = 1;
179
+ for (let i = 0; i < k; i++) r = (r * (n - i)) / (i + 1);
180
+ return r;
181
+ }
182
+
183
+ export async function run(opts: {
184
+ mode: 'dry-run' | 'live';
185
+ host: string;
186
+ seeds: number;
187
+ }): Promise<RunReport> {
188
+ const tasks = loadCorpus();
189
+ const apiKey = opts.mode === 'live' ? load_anthropic_key() : '';
190
+ const rows: ArmTaskResult[] = [];
191
+ let calls = 0;
192
+ let tIn = 0;
193
+ let tOut = 0;
194
+
195
+ for (const task of tasks) {
196
+ for (const arm of ARMS) {
197
+ let passes = 0;
198
+ let ai = 0;
199
+ let ao = 0;
200
+ for (let s = 0; s < opts.seeds; s++) {
201
+ let transcript: string;
202
+ if (opts.mode === 'dry-run') {
203
+ transcript = stubTranscript(task, arm);
204
+ } else {
205
+ const r = await callAnthropic(opts.host, buildPrompt(task, arm), apiKey);
206
+ transcript = r.text;
207
+ ai += r.tokens_in;
208
+ ao += r.tokens_out;
209
+ calls += 1;
210
+ tIn += r.tokens_in;
211
+ tOut += r.tokens_out;
212
+ }
213
+ if (scoreTask(transcript, task).pass) passes += 1;
214
+ }
215
+ rows.push({
216
+ task: task.id,
217
+ metric: task.metric,
218
+ arm,
219
+ passes,
220
+ seeds: opts.seeds,
221
+ tokens_in: ai,
222
+ tokens_out: ao,
223
+ });
224
+ }
225
+ }
226
+
227
+ const agg = {} as RunReport['aggregate'];
228
+ for (const arm of ARMS) {
229
+ const armRows = rows.filter((r) => r.arm === arm);
230
+ const pass = armRows.reduce((s, r) => s + r.passes, 0);
231
+ const total = armRows.reduce((s, r) => s + r.seeds, 0);
232
+ agg[arm] = { pass, total, pass_rate: total ? pass / total : 0 };
233
+ }
234
+
235
+ const pairCompare = (a: Arm, b: Arm) => {
236
+ let aw = 0;
237
+ let bw = 0;
238
+ let ties = 0;
239
+ for (const task of tasks) {
240
+ const ra = rows.find((r) => r.task === task.id && r.arm === a)!;
241
+ const rb = rows.find((r) => r.task === task.id && r.arm === b)!;
242
+ if (ra.passes > rb.passes) aw += 1;
243
+ else if (rb.passes > ra.passes) bw += 1;
244
+ else ties += 1;
245
+ }
246
+ return { aw, bw, ties, p: signTestP(aw, bw) };
247
+ };
248
+ const onOff = pairCompare('memory-on', 'memory-off');
249
+ const onPlacebo = pairCompare('memory-on', 'placebo');
250
+
251
+ // PASS: memory-on beats BOTH off and placebo, each significant at p<0.05.
252
+ const verdict: 'PASS' | 'NULL' =
253
+ onOff.aw > onOff.bw &&
254
+ onOff.p < 0.05 &&
255
+ onPlacebo.aw > onPlacebo.bw &&
256
+ onPlacebo.p < 0.05
257
+ ? 'PASS'
258
+ : 'NULL';
259
+
260
+ return {
261
+ schema_version: 1,
262
+ kind: `second-brain-delta-${opts.mode}`,
263
+ roadmap: 'road-to-second-brain-delta-proof',
264
+ host: opts.host,
265
+ seeds: opts.seeds,
266
+ mode: opts.mode,
267
+ scoping:
268
+ 'CONTEXT-VALUE UPPER BOUND on a one-fact-per-task corpus: memory-on injects the ' +
269
+ 'exact prior fact (perfect retrieval). Measures whether the right prior fact + the ' +
270
+ 'placebo-isolated retrieval mechanism let the model answer — NOT retrieval precision ' +
271
+ 'under a large store (that is the follow-up corpus).',
272
+ per_arm_task: rows,
273
+ aggregate: agg,
274
+ paired: {
275
+ 'on-vs-off': { on_wins: onOff.aw, off_wins: onOff.bw, ties: onOff.ties, sign_p: onOff.p },
276
+ 'on-vs-placebo': {
277
+ on_wins: onPlacebo.aw,
278
+ placebo_wins: onPlacebo.bw,
279
+ ties: onPlacebo.ties,
280
+ sign_p: onPlacebo.p,
281
+ },
282
+ },
283
+ verdict,
284
+ cost: { calls, tokens_in: tIn, tokens_out: tOut },
285
+ };
286
+ }
287
+
288
+ export async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
289
+ const mode = argv.includes('--run') ? 'live' : argv.includes('--dry-run') ? 'dry-run' : null;
290
+ if (mode === null) {
291
+ process.stderr.write('usage: second_brain_run --dry-run | --run [--host <id>] [--seeds <n>]\n');
292
+ return 2;
293
+ }
294
+ const hostIdx = argv.indexOf('--host');
295
+ const host = hostIdx >= 0 ? String(argv[hostIdx + 1]) : DEFAULT_HOST;
296
+ const seedIdx = argv.indexOf('--seeds');
297
+ const seeds = seedIdx >= 0 ? Number(argv[seedIdx + 1]) : DEFAULT_SEEDS;
298
+
299
+ let report: RunReport;
300
+ try {
301
+ report = await run({ mode, host, seeds });
302
+ } catch (e) {
303
+ process.stderr.write(`second_brain_run: ${String(e)}\n`);
304
+ return 1;
305
+ }
306
+
307
+ if (mode === 'live') {
308
+ fs.mkdirSync(REPORT_DIR, { recursive: true });
309
+ const out = path.join(REPORT_DIR, 'second-brain-delta.json');
310
+ fs.writeFileSync(out, JSON.stringify(report, null, 2) + '\n');
311
+ process.stdout.write(`wrote ${path.relative(REPO_ROOT, out)}\n`);
312
+ }
313
+ const a = report.aggregate;
314
+ process.stdout.write(
315
+ `${mode}: on=${a['memory-on'].pass}/${a['memory-on'].total} ` +
316
+ `off=${a['memory-off'].pass}/${a['memory-off'].total} ` +
317
+ `placebo=${a.placebo.pass}/${a.placebo.total} · ` +
318
+ `on-vs-off p=${report.paired['on-vs-off'].sign_p.toFixed(4)} ` +
319
+ `on-vs-placebo p=${report.paired['on-vs-placebo'].sign_p.toFixed(4)} · ` +
320
+ `${report.verdict} · calls=${report.cost.calls}\n`,
321
+ );
322
+ return 0;
323
+ }
324
+
325
+ function _isCliEntry(): boolean {
326
+ if (process.argv[1] === undefined) return false;
327
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
328
+ if (import.meta.url === argvUrl) return true;
329
+ try {
330
+ return (
331
+ fs.realpathSync(fileURLToPath(import.meta.url)) ===
332
+ fs.realpathSync(path.resolve(process.argv[1]))
333
+ );
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
339
+ if (_isCliEntry()) {
340
+ main().then((code) => process.exit(code));
341
+ }