@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,140 @@
1
+ /**
2
+ * Orchestration telemetry recorder — CLI.
3
+ *
4
+ * Appends ONE validated audit-log-v1 line (with an `orchestration` object) to
5
+ * the monthly audit log, from the counts the orchestrating agent has after an
6
+ * auto-dispatch. One command instead of hand-authored JSON — the reliable,
7
+ * lock-respecting (2026-06-30 no-hook decision) capture path.
8
+ *
9
+ * Usage:
10
+ * ./scripts-run src/scripts/orchestration_record \
11
+ * --spawn-count 1 --token-delta -72000 --provenance measured \
12
+ * --tier-chosen lite --tier-source inferred --task-class read-only-fanout \
13
+ * [--tiers sonnet,opus] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
14
+ * [--dir <audit-dir>] [--dry-run]
15
+ *
16
+ * Read by `src/scripts/orchestration_savings_report.ts`.
17
+ */
18
+
19
+ import * as crypto from 'node:crypto';
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { pathToFileURL } from 'node:url';
23
+
24
+ import {
25
+ buildOrchestrationLine,
26
+ type Provenance,
27
+ type TierChosen,
28
+ type TierSource,
29
+ type DispatchOutcome,
30
+ type VerifyMode,
31
+ type Band,
32
+ type LinePhase,
33
+ type LineOutcome,
34
+ type RecordInput,
35
+ } from './_lib/orchestration_record.js';
36
+
37
+ const DEFAULT_DIR = 'agents/runtime/state/audit';
38
+
39
+ type Flags = Record<string, string | boolean>;
40
+
41
+ function parseFlags(argv: string[]): Flags {
42
+ const flags: Flags = {};
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const a = argv[i];
45
+ if (a === undefined || !a.startsWith('--')) continue;
46
+ const eq = a.indexOf('=');
47
+ if (eq !== -1) {
48
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
49
+ continue;
50
+ }
51
+ const key = a.slice(2);
52
+ const next = argv[i + 1];
53
+ if (next !== undefined && !next.startsWith('--')) {
54
+ flags[key] = next;
55
+ i++;
56
+ } else {
57
+ flags[key] = true; // boolean flag (e.g. --dry-run)
58
+ }
59
+ }
60
+ return flags;
61
+ }
62
+
63
+ function str(flags: Flags, key: string): string | undefined {
64
+ const v = flags[key];
65
+ return typeof v === 'string' ? v : undefined;
66
+ }
67
+
68
+ function int(flags: Flags, key: string): number | undefined {
69
+ const v = str(flags, key);
70
+ if (v === undefined) return undefined;
71
+ const n = Number(v);
72
+ return Number.isFinite(n) ? n : NaN; // NaN → caught by the lib's isInt validation
73
+ }
74
+
75
+ /** Deterministic content-hash id (contract allows "ULID or content hash"). */
76
+ function contentHashId(input: RecordInput): string {
77
+ const canonical = JSON.stringify({
78
+ ts: input.ts,
79
+ spawn_count: input.spawn_count,
80
+ token_delta: input.token_delta,
81
+ tier_chosen: input.tier_chosen ?? null,
82
+ task_class: input.task_class ?? null,
83
+ });
84
+ return crypto.createHash('sha256').update(canonical).digest('hex').slice(0, 26).toUpperCase();
85
+ }
86
+
87
+ export function main(argv: string[] = process.argv.slice(2)): number {
88
+ const flags = parseFlags(argv);
89
+ const dir = str(flags, 'dir') ?? DEFAULT_DIR;
90
+ const dryRun = flags['dry-run'] === true;
91
+ const ts = new Date().toISOString();
92
+
93
+ const partial: Omit<RecordInput, 'id'> = {
94
+ spawn_count: int(flags, 'spawn-count') ?? NaN,
95
+ token_delta: int(flags, 'token-delta') ?? NaN,
96
+ token_delta_provenance: str(flags, 'provenance') as Provenance | undefined,
97
+ tiers: str(flags, 'tiers')?.split(',').map((s) => s.trim()).filter(Boolean),
98
+ tier_chosen: str(flags, 'tier-chosen') as TierChosen | undefined,
99
+ tier_source: str(flags, 'tier-source') as TierSource | undefined,
100
+ task_class: str(flags, 'task-class'),
101
+ task_size_estimate: int(flags, 'task-size-estimate'),
102
+ wall_clock_ms: int(flags, 'wall-clock-ms'),
103
+ dispatch_tokens: int(flags, 'dispatch-tokens'),
104
+ session_tier: str(flags, 'session-tier'),
105
+ dispatch_outcome: str(flags, 'dispatch-outcome') as DispatchOutcome | undefined,
106
+ verify_mode: str(flags, 'verify-mode') as VerifyMode | undefined,
107
+ phase: str(flags, 'phase') as LinePhase | undefined,
108
+ outcome: str(flags, 'outcome') as LineOutcome | undefined,
109
+ confidence_band: str(flags, 'confidence-band') as Band | undefined,
110
+ risk_class: str(flags, 'risk-class') as Band | undefined,
111
+ persona: str(flags, 'persona'),
112
+ work_id: str(flags, 'work-id'),
113
+ ts,
114
+ };
115
+ const input: RecordInput = { ...partial, id: contentHashId({ ...partial, id: '' }) };
116
+
117
+ const { line, errors } = buildOrchestrationLine(input);
118
+ if (errors.length || line === null) {
119
+ process.stderr.write('orchestration_record: refused — invalid input:\n');
120
+ for (const e of errors) process.stderr.write(` - ${e}\n`);
121
+ return 1;
122
+ }
123
+
124
+ const serialized = JSON.stringify(line);
125
+ if (dryRun) {
126
+ process.stdout.write(serialized + '\n');
127
+ return 0;
128
+ }
129
+
130
+ fs.mkdirSync(dir, { recursive: true });
131
+ const file = path.join(dir, `${ts.slice(0, 7)}.jsonl`);
132
+ fs.appendFileSync(file, serialized + '\n', 'utf8');
133
+ process.stdout.write(`orchestration telemetry recorded → ${file} (id ${line.id})\n`);
134
+ return 0;
135
+ }
136
+
137
+ if (process.argv[1] !== undefined) {
138
+ const invokedUrl = pathToFileURL(path.resolve(process.argv[1])).href;
139
+ if (import.meta.url === invokedUrl) process.exit(main());
140
+ }
@@ -6,10 +6,12 @@
6
6
  * mutation. Handles a missing/empty audit dir gracefully.
7
7
  *
8
8
  * Usage:
9
- * ./scripts-run src/scripts/orchestration_savings_report [--dir <path>] [--format text|json]
9
+ * ./scripts-run src/scripts/orchestration_savings_report \
10
+ * [--dir <path>] [--format text|json] [--weights lite=1,medium=5,high=15]
10
11
  *
11
12
  * Default dir: agents/runtime/state/audit (the audit-log-v1 path where the
12
- * orchestration layer writes telemetry lines). See
13
+ * orchestration layer writes telemetry lines). `--weights` tunes the
14
+ * provider-neutral tier weights behind the MODELED cost-%. See
13
15
  * `src/agent-src/contexts/execution/orchestration-telemetry.md`.
14
16
  */
15
17
 
@@ -17,17 +19,30 @@ import * as fs from 'node:fs';
17
19
  import * as path from 'node:path';
18
20
  import { pathToFileURL } from 'node:url';
19
21
 
20
- import { aggregateOrchestrationSavings, type AuditLine, type SavingsReport } from './_lib/orchestration_savings.js';
22
+ import { aggregateOrchestrationSavings, DEFAULT_TIER_WEIGHTS, type AuditLine, type SavingsReport, type TierWeights } from './_lib/orchestration_savings.js';
21
23
 
22
24
  const DEFAULT_DIR = 'agents/runtime/state/audit';
23
25
 
24
26
  interface Options {
25
27
  dir: string;
26
28
  format: 'text' | 'json';
29
+ weights: TierWeights;
30
+ }
31
+
32
+ /** Parse `lite=1,medium=5,high=15` → weight map. Returns undefined if nothing valid parsed. */
33
+ function parseWeights(spec: string | undefined): TierWeights | undefined {
34
+ if (!spec) return undefined;
35
+ const w: TierWeights = {};
36
+ for (const pair of spec.split(',')) {
37
+ const [k, v] = pair.split('=');
38
+ const n = Number(v);
39
+ if (k && Number.isFinite(n)) w[k.trim()] = n;
40
+ }
41
+ return Object.keys(w).length ? w : undefined;
27
42
  }
28
43
 
29
44
  function parseArgs(argv: string[]): Options {
30
- const opts: Options = { dir: DEFAULT_DIR, format: 'text' };
45
+ const opts: Options = { dir: DEFAULT_DIR, format: 'text', weights: DEFAULT_TIER_WEIGHTS };
31
46
  for (let i = 0; i < argv.length; i++) {
32
47
  const a = argv[i];
33
48
  if (a === undefined) continue;
@@ -35,6 +50,8 @@ function parseArgs(argv: string[]): Options {
35
50
  else if (a.startsWith('--dir=')) opts.dir = a.slice('--dir='.length);
36
51
  else if (a === '--format') opts.format = (argv[++i] as Options['format']) ?? opts.format;
37
52
  else if (a.startsWith('--format=')) opts.format = a.slice('--format='.length) as Options['format'];
53
+ else if (a === '--weights') opts.weights = parseWeights(argv[++i]) ?? opts.weights;
54
+ else if (a.startsWith('--weights=')) opts.weights = parseWeights(a.slice('--weights='.length)) ?? opts.weights;
38
55
  }
39
56
  if (opts.format !== 'text' && opts.format !== 'json') opts.format = 'text';
40
57
  return opts;
@@ -77,6 +94,13 @@ function renderText(r: SavingsReport, dir: string): string {
77
94
  out.push(' by task_class (Δ tokens):');
78
95
  for (const [cls, d] of Object.entries(r.by_task_class).sort()) out.push(` ${cls}: ${d}`);
79
96
  }
97
+ const mc = r.modeled_cost;
98
+ if (mc.cost_reduction_pct !== null) {
99
+ out.push(` MODELED cost reduction: ${(mc.cost_reduction_pct * 100).toFixed(0)}% (over ${mc.covered_dispatches}/${r.dispatches} dispatch(es) with tier data)`);
100
+ out.push(` weights ${JSON.stringify(mc.weights)} · baseline ${mc.baseline_cost_units} → delegated ${mc.delegated_cost_units} cost-units · MODEL, not measured $`);
101
+ } else {
102
+ out.push(' MODELED cost reduction: n/a (needs dispatch_tokens + session_tier + tier_chosen on a dispatch)');
103
+ }
80
104
  out.push(' notes:');
81
105
  for (const n of r.notes) out.push(` - ${n}`);
82
106
  return out.join('\n');
@@ -85,7 +109,7 @@ function renderText(r: SavingsReport, dir: string): string {
85
109
  export function main(argv: string[] = process.argv.slice(2)): number {
86
110
  const opts = parseArgs(argv);
87
111
  const lines = readAuditLines(opts.dir);
88
- const report = aggregateOrchestrationSavings(lines);
112
+ const report = aggregateOrchestrationSavings(lines, opts.weights);
89
113
  if (opts.format === 'json') {
90
114
  process.stdout.write(JSON.stringify({ ...report, source: opts.dir }, null, 2) + '\n');
91
115
  } else {
@@ -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
+ }