@event4u/agent-config 8.7.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 (171) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +109 -222
  3. package/CONTRIBUTING.md +25 -0
  4. package/README.md +7 -4
  5. package/dist/agent-src/commands/agent-handoff.md +39 -2
  6. package/dist/agent-src/commands/research/deep.md +8 -0
  7. package/dist/agent-src/commands/research/report.md +7 -0
  8. package/dist/agent-src/commands/roadmap/create.md +12 -0
  9. package/dist/agent-src/commands/roadmap/process-full.md +31 -5
  10. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
  11. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
  12. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
  13. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +36 -5
  14. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
  15. package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
  16. package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
  17. package/dist/agent-src/rules/active-remediation.md +92 -0
  18. package/dist/agent-src/rules/agent-authority.md +1 -1
  19. package/dist/agent-src/rules/broken-access-control.md +106 -0
  20. package/dist/agent-src/rules/communication-through-line.md +42 -0
  21. package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
  22. package/dist/agent-src/rules/delegation-policy.md +5 -0
  23. package/dist/agent-src/rules/direct-answers.md +1 -1
  24. package/dist/agent-src/rules/downstream-changes.md +1 -0
  25. package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
  26. package/dist/agent-src/rules/git-history-discipline.md +18 -0
  27. package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
  28. package/dist/agent-src/rules/minimal-safe-diff.md +66 -1
  29. package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
  30. package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
  31. package/dist/agent-src/rules/question-not-instruction.md +76 -0
  32. package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
  33. package/dist/agent-src/rules/think-before-action.md +1 -0
  34. package/dist/agent-src/rules/tool-safety.md +8 -0
  35. package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
  36. package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
  37. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
  38. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +112 -0
  39. package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
  40. package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
  41. package/dist/agent-src/skills/code-review/SKILL.md +2 -0
  42. package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
  43. package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
  44. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
  45. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
  46. package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
  47. package/dist/agent-src/skills/design-review/SKILL.md +29 -15
  48. package/dist/agent-src/skills/design-system-capture/SKILL.md +63 -24
  49. package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
  50. package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
  51. package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
  52. package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
  53. package/dist/agent-src/skills/mcp/SKILL.md +24 -0
  54. package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
  55. package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
  56. package/dist/agent-src/skills/roadmap-writing/SKILL.md +46 -5
  57. package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
  58. package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
  59. package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
  60. package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
  61. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
  62. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +112 -0
  63. package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
  64. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +72 -2
  65. package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
  66. package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
  67. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  68. package/dist/agent-src/templates/roadmaps.md +60 -0
  69. package/dist/cli/registry.js +3 -0
  70. package/dist/cli/registry.js.map +1 -1
  71. package/dist/discovery/deprecation-report.md +1 -1
  72. package/dist/discovery/discovery-manifest.json +268 -46
  73. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  74. package/dist/discovery/discovery-manifest.summary.md +4 -4
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +18 -8
  77. package/dist/discovery/trust-report.md +11 -11
  78. package/dist/discovery/workspaces.json +16 -6
  79. package/dist/install/install.mjs +141 -3
  80. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  81. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  82. package/dist/mcp/registry-manifest.json +4 -4
  83. package/dist/router.json +1 -1
  84. package/dist/scripts/_lib/claude_builtin_names.js +145 -0
  85. package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
  86. package/dist/scripts/condense.js +14 -0
  87. package/dist/scripts/condense.js.map +1 -1
  88. package/dist/server/schemas/settings.js +2 -1
  89. package/dist/server/schemas/settings.js.map +1 -1
  90. package/docs/CLAIMS.md +58 -2
  91. package/docs/architecture.md +3 -3
  92. package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
  93. package/docs/benchmark.md +113 -0
  94. package/docs/catalog.md +32 -5
  95. package/docs/contracts/command-clusters.md +18 -0
  96. package/docs/contracts/design-artifact-verification.md +124 -0
  97. package/docs/contracts/injected-block-authenticity-model.md +66 -0
  98. package/docs/contracts/mcp-tool-inventory.md +31 -30
  99. package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
  100. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
  101. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -2
  102. package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
  103. package/docs/decisions/INDEX.md +1 -0
  104. package/docs/experiences/content_creator.md +2 -0
  105. package/docs/experiences/finance.md +2 -0
  106. package/docs/experiences/founder.md +2 -0
  107. package/docs/experiences/ops.md +2 -0
  108. package/docs/featured-skills.md +2 -2
  109. package/docs/getting-started-by-role.md +1 -1
  110. package/docs/getting-started.md +1 -1
  111. package/docs/governance-advantage.md +1 -1
  112. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
  113. package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
  114. package/docs/guidelines/agent-infra/memory-access.md +12 -0
  115. package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
  116. package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
  117. package/docs/guidelines/design-canon.md +1 -0
  118. package/docs/proof.md +60 -3
  119. package/docs/release-runbook.md +138 -0
  120. package/docs/second-brain-scope.md +116 -0
  121. package/docs/succession.md +80 -0
  122. package/package.json +2 -2
  123. package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
  124. package/src/config/agent-settings.template.yml +25 -6
  125. package/src/scripts/_dispatch.bash +27 -0
  126. package/src/scripts/_lib/claude_builtin_names.ts +148 -0
  127. package/src/scripts/_lib/file_slicer.ts +117 -0
  128. package/src/scripts/_lib/lexical_index.ts +155 -0
  129. package/src/scripts/_lib/orchestration_gate.ts +12 -8
  130. package/src/scripts/_lib/orchestration_record.ts +170 -0
  131. package/src/scripts/_lib/orchestration_savings.ts +180 -0
  132. package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
  133. package/src/scripts/_lib/security_lint.ts +1 -0
  134. package/src/scripts/_lib/stat_index.ts +98 -0
  135. package/src/scripts/benchmark.ts +122 -0
  136. package/src/scripts/build_proof.ts +51 -0
  137. package/src/scripts/chat_history.ts +79 -7
  138. package/src/scripts/check_memory.ts +2 -0
  139. package/src/scripts/check_quality_regression.ts +51 -0
  140. package/src/scripts/check_roadmap_trackable.ts +3 -2
  141. package/src/scripts/condense.ts +14 -0
  142. package/src/scripts/discovery_graph.ts +0 -0
  143. package/src/scripts/domain_soundness_status.ts +236 -0
  144. package/src/scripts/external_sources_denylist.json +4 -1
  145. package/src/scripts/generate_index.ts +17 -0
  146. package/src/scripts/hot_context_hook.ts +47 -2
  147. package/src/scripts/install-hooks.sh +25 -0
  148. package/src/scripts/install.ts +35 -2
  149. package/src/scripts/learning_sidecar.ts +0 -0
  150. package/src/scripts/lint_agent_skill_names.ts +29 -0
  151. package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
  152. package/src/scripts/lint_knowledge_scale.ts +4 -3
  153. package/src/scripts/lint_roadmap_complexity.ts +124 -0
  154. package/src/scripts/lint_versioned_cache.ts +195 -0
  155. package/src/scripts/mcp_server/consumer_tool_catalog.json +23 -3
  156. package/src/scripts/mcp_server/tools.ts +182 -1
  157. package/src/scripts/measure_lexical_ranking.ts +173 -0
  158. package/src/scripts/memory_lookup.ts +0 -0
  159. package/src/scripts/memory_replay.ts +285 -0
  160. package/src/scripts/orchestration_record.ts +140 -0
  161. package/src/scripts/orchestration_savings_report.ts +124 -0
  162. package/src/scripts/schemas/domain-truth.schema.json +94 -0
  163. package/src/scripts/schemas/evals.schema.json +113 -0
  164. package/src/scripts/schemas/skill.schema.json +4 -0
  165. package/src/scripts/score_domain_truth.ts +192 -0
  166. package/src/scripts/second_brain_retrieval.ts +278 -0
  167. package/src/scripts/second_brain_run.ts +341 -0
  168. package/src/scripts/second_brain_score.ts +186 -0
  169. package/src/scripts/session_memory_index.ts +94 -0
  170. package/src/scripts/skill_eval_coverage.ts +318 -0
  171. package/src/server/schemas/settings.ts +5 -2
@@ -0,0 +1,113 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/event4u-app/agent-config/scripts/schemas/evals.schema.json",
4
+ "x-schemaVersion": "1.0.0",
5
+ "title": "Skill behavioural evals (evals.json)",
6
+ "$comment": "Behavioural-eval spec consumed by run_skill_evals.ts (scaffold/aggregate/report) and counted by skill_eval_coverage.ts. Distinct from triggers.json (activation/routing axis). Roadmap: road-to-skill-eval-coverage Phase 1. Assertion kinds mirror _grade_assertions() in run_skill_evals.ts exactly — add a kind there before adding it here.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": ["skill", "scenarios"],
10
+ "properties": {
11
+ "skill": {
12
+ "type": "string",
13
+ "pattern": "^[a-z][a-z0-9-]*$",
14
+ "description": "Skill slug; must match the parent skill directory name (evals/ lives under src/skills/<skill>/)."
15
+ },
16
+ "_calibration": {
17
+ "type": "string",
18
+ "description": "Optional free-text note recording calibration status / provenance (e.g. uncalibrated finding_floor placeholders). Advisory; ignored by the grader."
19
+ },
20
+ "skill_body_sha": {
21
+ "type": "string",
22
+ "pattern": "^[0-9a-f]{64}$",
23
+ "description": "Optional SHA-256 of the skill's SKILL.md body at the time these evals were authored/last verified. When present, lint_behavioural_eval_freshness flags the eval stale if the current SKILL.md sha differs — the body changed, so the eval may no longer exercise it. Absent = unpinned (out of scope for the staleness gate), mirroring lint_eval_freshness's upstream:null handling."
24
+ },
25
+ "scenarios": {
26
+ "type": "array",
27
+ "minItems": 1,
28
+ "description": "One or more behavioural cases. Each pairs a prompt fixture with deterministic pass/fail assertions on the produced output.",
29
+ "items": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": ["id", "prompt", "assertions"],
33
+ "properties": {
34
+ "id": {
35
+ "type": "string",
36
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
37
+ "description": "Stable scenario slug, unique within this file (run directories are named by it)."
38
+ },
39
+ "prompt": {
40
+ "type": "string",
41
+ "minLength": 1,
42
+ "description": "The input fixture handed to the skill under test."
43
+ },
44
+ "assertions": {
45
+ "type": "array",
46
+ "minItems": 1,
47
+ "description": "Pass/fail checks on the output. Prefer deterministic kinds (contains/file_exists/finding_floor); use rubric only where a deterministic check is impossible, and pin the judge as a known limit.",
48
+ "items": {
49
+ "oneOf": [
50
+ {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["kind", "value"],
54
+ "properties": {
55
+ "kind": { "const": "contains" },
56
+ "value": {
57
+ "type": "string",
58
+ "minLength": 1,
59
+ "description": "Substring the output must contain (deterministic)."
60
+ }
61
+ }
62
+ },
63
+ {
64
+ "type": "object",
65
+ "additionalProperties": false,
66
+ "required": ["kind", "path"],
67
+ "properties": {
68
+ "kind": { "const": "file_exists" },
69
+ "path": {
70
+ "type": "string",
71
+ "minLength": 1,
72
+ "description": "Path (run-relative or absolute) the skill must have produced (deterministic)."
73
+ }
74
+ }
75
+ },
76
+ {
77
+ "type": "object",
78
+ "additionalProperties": false,
79
+ "required": ["kind", "n"],
80
+ "properties": {
81
+ "kind": { "const": "finding_floor" },
82
+ "n": {
83
+ "type": "integer",
84
+ "minimum": 0,
85
+ "description": "Minimum number of findings the output must carry (counted deterministically). 0 is a trivially-passing floor (the grader passes when count >= n) — used where the case only guards against over-reporting."
86
+ },
87
+ "pattern": {
88
+ "type": "string",
89
+ "description": "Optional regex counted globally; without it, markdown list-item lines are counted (see _count_findings)."
90
+ }
91
+ }
92
+ },
93
+ {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "required": ["kind", "criterion"],
97
+ "properties": {
98
+ "kind": { "const": "rubric" },
99
+ "criterion": {
100
+ "type": "string",
101
+ "minLength": 1,
102
+ "description": "Natural-language pass criterion graded by a sub-agent. NON-deterministic — the grader returns pass:null until a pinned judge fills it. Use only where no deterministic check exists; record the judge as a known limit."
103
+ }
104
+ }
105
+ }
106
+ ]
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
@@ -13,6 +13,10 @@
13
13
  "pattern": "^[a-z][a-z0-9-]*$",
14
14
  "description": "Lowercase, hyphenated slug; must match the parent directory name."
15
15
  },
16
+ "user-invocable": {
17
+ "type": "boolean",
18
+ "description": "Claude Code Agent-Skills extension: false = the skill claims no /name slash command (only the model can load it). REQUIRED (false) when name equals a Claude Code built-in command or bundled skill (see scripts/_lib/claude_builtin_names.ts) so the suite never shadows the host's own /name. Other tools ignore the key."
19
+ },
16
20
  "description": {
17
21
  "type": "string",
18
22
  "minLength": 1,
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deterministic domain-truth scorer (road-to-domain-soundness Phase 3).
4
+ *
5
+ * Pure, LLM-free scoring for the `deterministic` half of the domain-truth
6
+ * fixtures (`skills/<skill>/evals/domain-truth.json`). Given a skill's run
7
+ * output for a case and the case's `expected` + `tolerance`, it extracts the
8
+ * numeric answer and returns pass/fail. It does NOT invoke any model — the
9
+ * expensive part (running the skill against the scenario on a fixed host) is
10
+ * the maintainer's Phase-3 gate; this only scores a captured output.
11
+ *
12
+ * Scoring contract (kept simple + gameable-input-resistant):
13
+ * - Prefer an explicit `ANSWER: <n>` line (case-insensitive) — the run
14
+ * prompt instructs the skill to end with one, so the echoed scenario
15
+ * numbers cannot be mistaken for the answer.
16
+ * - Fallback: the last numeric token in the output.
17
+ * - Normalization: strip `$`, thousands separators, surrounding whitespace,
18
+ * and a trailing unit token (`%`, `months`, `mo`, `x`, or stray letters).
19
+ * Scenarios fix the unit (e.g. "state in $M"), so no magnitude conversion
20
+ * is applied — a unit-instruction miss is a real fail, not a scorer bug.
21
+ * - Pass iff |extracted - expected| <= tolerance.
22
+ *
23
+ * Rubric cases are NOT scored here (they need a pinned judge, recorded as a
24
+ * known-limit) — this scorer skips any case whose `check.kind !== "deterministic"`.
25
+ */
26
+ import * as fs from 'node:fs';
27
+ import * as path from 'node:path';
28
+ import { fileURLToPath, pathToFileURL } from 'node:url';
29
+
30
+ export interface DeterministicCheck {
31
+ kind: 'deterministic';
32
+ expected: number | string;
33
+ tolerance?: number;
34
+ rationale: string;
35
+ }
36
+
37
+ export interface CaseResult {
38
+ id: string;
39
+ pass: boolean;
40
+ expected: number;
41
+ tolerance: number;
42
+ extracted: number | null;
43
+ reason: string;
44
+ }
45
+
46
+ /** Pull a single numeric answer out of a skill's run output. */
47
+ export function extractAnswer(output: string): number | null {
48
+ if (typeof output !== 'string' || output.trim() === '') return null;
49
+ // Prefer an explicit ANSWER: line (last one wins if repeated).
50
+ const answerLines = output.match(/ANSWER:\s*([^\n\r]+)/gi);
51
+ if (answerLines && answerLines.length > 0) {
52
+ const last = answerLines[answerLines.length - 1] as string;
53
+ const n = _firstNumber(last.replace(/ANSWER:\s*/i, ''));
54
+ if (n !== null) return n;
55
+ }
56
+ // Fallback: the last numeric token anywhere in the output.
57
+ return _lastNumber(output);
58
+ }
59
+
60
+ /** Normalize a fragment ("$1,350M", "7.5 months", "32%") to a bare number. */
61
+ function _firstNumber(fragment: string): number | null {
62
+ const cleaned = fragment.replace(/[$,]/g, '');
63
+ const m = cleaned.match(/-?\d+(?:\.\d+)?/);
64
+ if (!m) return null;
65
+ const v = Number(m[0]);
66
+ return Number.isFinite(v) ? v : null;
67
+ }
68
+
69
+ function _lastNumber(text: string): number | null {
70
+ const cleaned = text.replace(/[$,]/g, '');
71
+ const all = cleaned.match(/-?\d+(?:\.\d+)?/g);
72
+ if (!all || all.length === 0) return null;
73
+ const v = Number(all[all.length - 1]);
74
+ return Number.isFinite(v) ? v : null;
75
+ }
76
+
77
+ /** Score one deterministic case against a captured output string. */
78
+ export function scoreDeterministic(output: string, check: DeterministicCheck): {
79
+ pass: boolean;
80
+ extracted: number | null;
81
+ expected: number;
82
+ tolerance: number;
83
+ reason: string;
84
+ } {
85
+ const expected = typeof check.expected === 'number' ? check.expected : Number(check.expected);
86
+ const tolerance = check.tolerance ?? 0;
87
+ const extracted = extractAnswer(output);
88
+ if (extracted === null || !Number.isFinite(expected)) {
89
+ return {
90
+ pass: false,
91
+ extracted,
92
+ expected,
93
+ tolerance,
94
+ reason: extracted === null ? 'no numeric answer found in output' : 'expected is not numeric',
95
+ };
96
+ }
97
+ const delta = Math.abs(extracted - expected);
98
+ const pass = delta <= tolerance;
99
+ return {
100
+ pass,
101
+ extracted,
102
+ expected,
103
+ tolerance,
104
+ reason: pass
105
+ ? `|${extracted} - ${expected}| = ${_round(delta)} <= ${tolerance}`
106
+ : `|${extracted} - ${expected}| = ${_round(delta)} > ${tolerance}`,
107
+ };
108
+ }
109
+
110
+ function _round(n: number): number {
111
+ return Math.round(n * 1e6) / 1e6;
112
+ }
113
+
114
+ /**
115
+ * Score a whole fixture against a map of {caseId -> captured output}. Rubric
116
+ * cases are reported as `skipped` (kind !== deterministic), never silently
117
+ * passed.
118
+ */
119
+ export function scoreFixture(
120
+ fixture: { skill: string; cases: Array<{ id: string; check: { kind: string; [k: string]: unknown } }> },
121
+ outputs: Record<string, string>,
122
+ ): { skill: string; results: CaseResult[]; skipped: string[] } {
123
+ const results: CaseResult[] = [];
124
+ const skipped: string[] = [];
125
+ for (const c of fixture.cases) {
126
+ if (c.check.kind !== 'deterministic') {
127
+ skipped.push(c.id);
128
+ continue;
129
+ }
130
+ const output = outputs[c.id] ?? '';
131
+ const r = scoreDeterministic(output, c.check as unknown as DeterministicCheck);
132
+ results.push({
133
+ id: c.id,
134
+ pass: r.pass,
135
+ expected: r.expected,
136
+ tolerance: r.tolerance,
137
+ extracted: r.extracted,
138
+ reason: output === '' ? 'no captured output for this case' : r.reason,
139
+ });
140
+ }
141
+ return { skill: fixture.skill, results, skipped };
142
+ }
143
+
144
+ const _HERE = fileURLToPath(import.meta.url);
145
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
146
+
147
+ /**
148
+ * CLI: `score_domain_truth <skill> <outputs.json>` where outputs.json is a
149
+ * `{ "<caseId>": "<captured skill output>" }` map. Prints a per-case table and
150
+ * exits 0 iff every deterministic case passes (1 otherwise).
151
+ */
152
+ function _runCli(argv: string[]): number {
153
+ const [skill, outputsPath] = argv;
154
+ if (!skill || !outputsPath) {
155
+ process.stderr.write('usage: score_domain_truth <skill> <outputs.json>\n');
156
+ return 2;
157
+ }
158
+ const fixturePath = path.join(REPO_ROOT, 'src', 'skills', skill, 'evals', 'domain-truth.json');
159
+ if (!fs.existsSync(fixturePath)) {
160
+ process.stderr.write(`error: no domain-truth.json for skill '${skill}' at ${fixturePath}\n`);
161
+ return 1;
162
+ }
163
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf-8'));
164
+ const outputs = JSON.parse(fs.readFileSync(path.resolve(outputsPath), 'utf-8')) as Record<string, string>;
165
+ const { results, skipped } = scoreFixture(fixture, outputs);
166
+ process.stdout.write(`domain-truth run — ${skill}\n`);
167
+ for (const r of results) {
168
+ const mark = r.pass ? '✅' : '❌';
169
+ process.stdout.write(` ${mark} ${r.id}: extracted=${r.extracted} expected=${r.expected} (±${r.tolerance}) — ${r.reason}\n`);
170
+ }
171
+ if (skipped.length > 0) {
172
+ process.stdout.write(` · skipped (rubric, needs pinned judge): ${skipped.join(', ')}\n`);
173
+ }
174
+ const passed = results.filter((r) => r.pass).length;
175
+ process.stdout.write(` ${passed}/${results.length} deterministic cases pass\n`);
176
+ return passed === results.length && results.length > 0 ? 0 : 1;
177
+ }
178
+
179
+ function _isCliEntry(): boolean {
180
+ if (process.argv[1] === undefined) return false;
181
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
182
+ if (import.meta.url === argvUrl) return true;
183
+ try {
184
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ if (_isCliEntry()) {
191
+ process.exit(_runCli(process.argv.slice(2)));
192
+ }
@@ -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)); }