@juzser/blacksmith 0.4.0 → 0.5.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/agents/{auditor.md → bs-auditor.md} +5 -5
  2. package/.claude/agents/{coder.md → bs-coder.md} +8 -7
  3. package/.claude/agents/{grader.md → bs-grader.md} +4 -4
  4. package/.claude/agents/{merger.md → bs-merger.md} +4 -4
  5. package/.claude/agents/{planner.md → bs-planner.md} +20 -6
  6. package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
  7. package/.claude/agents/{reviewer.md → bs-reviewer.md} +13 -5
  8. package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
  9. package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +6 -6
  10. package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +15 -10
  11. package/.claude/agents/{tester.md → bs-tester.md} +40 -12
  12. package/.claude/agents/bs-uiux.md +257 -0
  13. package/.claude/agents/{verifier.md → bs-verifier.md} +4 -4
  14. package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
  15. package/.claude/skills/bs/SKILL.md +99 -21
  16. package/.claude/skills/bs/audit.md +34 -27
  17. package/.claude/skills/bs/dispatch.md +100 -50
  18. package/.claude/skills/bs/lessons.md +28 -14
  19. package/.claude/skills/bs/mcp.md +4 -4
  20. package/.claude/skills/bs/new.md +3 -3
  21. package/.claude/skills/bs/plan.md +27 -18
  22. package/.claude/skills/bs/report.md +11 -11
  23. package/.claude/skills/bs/run.md +181 -49
  24. package/.claude/skills/bs/status.md +14 -7
  25. package/.claude/skills/bs/ui.md +21 -5
  26. package/.claude/skills/bs/waivers.md +78 -2
  27. package/.claude/skills/bs/wave.md +109 -53
  28. package/README.md +53 -32
  29. package/factory/orchestrator/dist/agentNames.js +43 -0
  30. package/factory/orchestrator/dist/agents-registry.js +11 -4
  31. package/factory/orchestrator/dist/agentsSync.js +48 -14
  32. package/factory/orchestrator/dist/artifacts.js +31 -0
  33. package/factory/orchestrator/dist/attribution.js +4 -0
  34. package/factory/orchestrator/dist/audit.js +125 -17
  35. package/factory/orchestrator/dist/budgetAlarm.js +132 -37
  36. package/factory/orchestrator/dist/budgets.js +302 -84
  37. package/factory/orchestrator/dist/claims.js +9 -19
  38. package/factory/orchestrator/dist/cli.js +790 -100
  39. package/factory/orchestrator/dist/cliName.js +27 -0
  40. package/factory/orchestrator/dist/crosscheck.js +136 -8
  41. package/factory/orchestrator/dist/daemon.js +154 -36
  42. package/factory/orchestrator/dist/db/projector.js +278 -29
  43. package/factory/orchestrator/dist/db/queries.js +1831 -94
  44. package/factory/orchestrator/dist/db/schema.js +57 -4
  45. package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
  46. package/factory/orchestrator/dist/delegation.js +86 -19
  47. package/factory/orchestrator/dist/diffstat.js +15 -8
  48. package/factory/orchestrator/dist/dispatchLint.js +13 -37
  49. package/factory/orchestrator/dist/effort.js +14 -2
  50. package/factory/orchestrator/dist/effortTiers.js +13 -0
  51. package/factory/orchestrator/dist/env.js +26 -0
  52. package/factory/orchestrator/dist/epic.js +323 -38
  53. package/factory/orchestrator/dist/epicBudget.js +93 -0
  54. package/factory/orchestrator/dist/epicWidth.js +22 -5
  55. package/factory/orchestrator/dist/errorIssues.js +10 -4
  56. package/factory/orchestrator/dist/escalation.js +2 -2
  57. package/factory/orchestrator/dist/events.js +105 -3
  58. package/factory/orchestrator/dist/feedback.js +169 -0
  59. package/factory/orchestrator/dist/findingContext.js +26 -0
  60. package/factory/orchestrator/dist/findings.js +83 -12
  61. package/factory/orchestrator/dist/gate.js +120 -10
  62. package/factory/orchestrator/dist/goalCheck.js +2 -2
  63. package/factory/orchestrator/dist/harness.js +25 -17
  64. package/factory/orchestrator/dist/hookDecision.js +92 -3
  65. package/factory/orchestrator/dist/integration.js +9 -7
  66. package/factory/orchestrator/dist/judgePreflight.js +25 -4
  67. package/factory/orchestrator/dist/judgeRoles.js +47 -0
  68. package/factory/orchestrator/dist/judgeStopHook.js +90 -32
  69. package/factory/orchestrator/dist/judges.js +419 -21
  70. package/factory/orchestrator/dist/lessons.js +190 -11
  71. package/factory/orchestrator/dist/mcp.js +6 -6
  72. package/factory/orchestrator/dist/paths.js +10 -7
  73. package/factory/orchestrator/dist/plan.js +269 -33
  74. package/factory/orchestrator/dist/planQuorum.js +5 -2
  75. package/factory/orchestrator/dist/policy.js +1275 -30
  76. package/factory/orchestrator/dist/projectName.js +25 -0
  77. package/factory/orchestrator/dist/promptCapture.js +306 -0
  78. package/factory/orchestrator/dist/promptHook.js +33 -0
  79. package/factory/orchestrator/dist/promptImport.js +281 -0
  80. package/factory/orchestrator/dist/prompts.js +13 -0
  81. package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
  82. package/factory/orchestrator/dist/providers/types.js +1 -1
  83. package/factory/orchestrator/dist/queue.js +4 -4
  84. package/factory/orchestrator/dist/quorum.js +18 -5
  85. package/factory/orchestrator/dist/results.js +25 -7
  86. package/factory/orchestrator/dist/roadmap.js +10 -5
  87. package/factory/orchestrator/dist/run-cli.js +15 -10
  88. package/factory/orchestrator/dist/runner.js +2 -2
  89. package/factory/orchestrator/dist/schemas.js +56 -8
  90. package/factory/orchestrator/dist/severity.js +8 -4
  91. package/factory/orchestrator/dist/spec.js +177 -13
  92. package/factory/orchestrator/dist/specChange.js +13 -9
  93. package/factory/orchestrator/dist/stack.js +6 -6
  94. package/factory/orchestrator/dist/storeRoot.js +90 -0
  95. package/factory/orchestrator/dist/symbols.js +261 -6
  96. package/factory/orchestrator/dist/taskEvents.js +179 -27
  97. package/factory/orchestrator/dist/taskStatus.js +14 -0
  98. package/factory/orchestrator/dist/testerAudit.js +31 -12
  99. package/factory/orchestrator/dist/testgate.js +16 -13
  100. package/factory/orchestrator/dist/uiuxGate.js +83 -0
  101. package/factory/orchestrator/dist/usage.js +97 -40
  102. package/factory/orchestrator/dist/waivers.js +39 -5
  103. package/factory/orchestrator/dist/waveBudget.js +2 -0
  104. package/factory/orchestrator/dist/waveConcurrency.js +143 -7
  105. package/factory/orchestrator/dist/workroot.js +1 -1
  106. package/factory/orchestrator/dist/worktree.js +61 -19
  107. package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
  108. package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
  109. package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
  110. package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
  111. package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
  112. package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
  113. package/factory/policies/budgets.yml +123 -33
  114. package/factory/policies/crosscheck.yml +51 -13
  115. package/factory/policies/delegation.yml +3 -3
  116. package/factory/policies/effort.yml +18 -10
  117. package/factory/policies/guardrails.yml +8 -6
  118. package/factory/policies/harness.yml +5 -5
  119. package/factory/policies/lessons.md +53 -2
  120. package/factory/policies/scheduler.yml +4 -4
  121. package/factory/policies/sensitive-paths.yml +1 -1
  122. package/factory/policies/severity.yml +13 -1
  123. package/factory/policies/stack.yml +6 -6
  124. package/factory/policies/taxonomy.yml +9 -3
  125. package/factory/policies/worktree.yml +15 -9
  126. package/factory/specs/roadmap.md +7 -7
  127. package/factory/specs/schema/event.schema.json +8 -1
  128. package/factory/specs/schema/result.schema.json +21 -8
  129. package/factory/specs/schema/task-spec.schema.json +12 -0
  130. package/package.json +16 -12
  131. package/.claude/agents/uiux.md +0 -133
@@ -1,7 +1,7 @@
1
1
  // Provider-agnostic judge contract (architecture §6): "any model that can
2
2
  // honor the I/O contract can serve" — a JudgeRequest is a plain diff+spec+
3
3
  // prior-findings prompt already assembled by the caller from
4
- // .claude/agents/{reviewer,verifier}.md's own contract, plus a schema
4
+ // .claude/agents/bs-{reviewer,verifier}.md's own contract, plus a schema
5
5
  // name the extracted response is validated against. Transports never see
6
6
  // more than `prompt` — the trust boundary (architecture §6 "External
7
7
  // providers judge; they never gain write access... their findings are
@@ -11,7 +11,7 @@ import { buildSymbolGraph, collectSources } from './symbols.js';
11
11
  import { emitTaskBlocked, emitWaveMerged } from './taskEvents.js';
12
12
  import { projectCommandEnv } from './testgate.js';
13
13
  import { renderSelectedTestCmd, selectTests } from './testSelect.js';
14
- import { integrationBranchName } from './worktree.js';
14
+ import { epicBranchPrefix, integrationBranchName } from './worktree.js';
15
15
  export class QueueError extends SmithError {
16
16
  }
17
17
  /**
@@ -141,7 +141,7 @@ async function certifyAndRebase(task, integrationBranch, logBlocked) {
141
141
  * non-merged outcome.
142
142
  */
143
143
  export async function step(task, opts) {
144
- const integrationBranch = integrationBranchName(opts.epic);
144
+ const integrationBranch = integrationBranchName(opts.epic, epicBranchPrefix(opts.projectDir, opts.epic));
145
145
  const events = opts.events;
146
146
  // Refused before certifyAndRebase's real `git rebase` or the merge below
147
147
  // ever runs: both mutate git for real, and used to be discovered only by
@@ -523,7 +523,7 @@ async function runGroup(tasks, opts, integrationBranch, logBlocked, knownRed = f
523
523
  * failing every task in the group for one task's sake.
524
524
  */
525
525
  export async function batchStep(tasks, opts) {
526
- const integrationBranch = integrationBranchName(opts.epic);
526
+ const integrationBranch = integrationBranchName(opts.epic, epicBranchPrefix(opts.projectDir, opts.epic));
527
527
  const events = opts.events;
528
528
  // Same envelope check as step(), and for the same reason: runGroup below
529
529
  // rebases and merges real git state for every task in the group before its
@@ -582,7 +582,7 @@ function isAncestor(projectDir, ancestor, descendant) {
582
582
  */
583
583
  export async function adopt(task, opts) {
584
584
  const { projectDir } = opts;
585
- const integrationBranch = integrationBranchName(opts.epic);
585
+ const integrationBranch = integrationBranchName(opts.epic, epicBranchPrefix(projectDir, opts.epic));
586
586
  for (const ref of [integrationBranch, task.branch]) {
587
587
  if (!resolveCommit(projectDir, ref)) {
588
588
  throw new QueueError('queue.adopt-unknown-branch', `Branch "${ref}" does not exist in ${projectDir} — there is nothing to adopt against.`, { branch: ref, taskId: task.taskId });
@@ -15,14 +15,15 @@
15
15
  // behavior. This is the mechanism that makes "flip crosscheck.yml" the
16
16
  // entire promotion step (docs/runbooks/providers.md).
17
17
  import { loadCrosscheckPolicy, providerModel } from './crosscheck.js';
18
+ import { sessionOwnerRole } from './delegation.js';
18
19
  import { SmithError } from './errors.js';
19
- import { appendEvent } from './events.js';
20
+ import { appendEvent, readLineageEvents } from './events.js';
20
21
  import { runJudge } from './providers/index.js';
21
22
  const DEFAULT_QUORUM_POLICY = { minProviders: 2 };
22
23
  /**
23
24
  * Bridge a raw JudgeResult into the binary confirm/refute quorum needs.
24
25
  * `kind: review` has no explicit verdict field (its contract is a findings
25
- * array, .claude/agents/reviewer.md) — presence of any finding IS the
26
+ * array, .claude/agents/bs-reviewer.md) — presence of any finding IS the
26
27
  * claim being judged ("this task has a real blocking issue"), so a
27
28
  * non-empty array reads as `confirm`. `kind: verify`/`plan-critique` carry
28
29
  * an explicit judge-verdict.schema.json object instead.
@@ -172,9 +173,15 @@ const KIND_TO_AGENT = {
172
173
  */
173
174
  export async function recordJudgeRun(input, ctx, opts = {}) {
174
175
  const agentRole = KIND_TO_AGENT[input.kind];
176
+ // Explicit --actor is authoritative and skips this lookup entirely; only an
177
+ // absent actor reaches for the delegated session's owner (issue #218, same
178
+ // shape as judges.ts's recordJudgeDispatch -- see delegatedActor there).
179
+ const actor = ctx.actor ??
180
+ sessionOwnerRole(await readLineageEvents(ctx.sessionId, opts), ctx.sessionId) ??
181
+ undefined;
175
182
  const dispatch = await appendEvent({
176
183
  session_id: ctx.sessionId,
177
- actor: ctx.actor ?? 'system',
184
+ actor: actor ?? 'system',
178
185
  event_type: 'dispatch_decision',
179
186
  task_id: input.taskId,
180
187
  plan_version: ctx.planVersion,
@@ -184,7 +191,7 @@ export async function recordJudgeRun(input, ctx, opts = {}) {
184
191
  provider: input.run.provider,
185
192
  model_tier: input.modelTier,
186
193
  model: input.model,
187
- reason: `cross-provider judge (${input.run.mode})`,
194
+ reason: `cross-provider judge (${input.run.mode})${input.run.via === 'openrouter' ? ' via openrouter' : ''}`,
188
195
  },
189
196
  }, opts);
190
197
  const ok = input.run.outcome.ok;
@@ -213,6 +220,7 @@ export async function recordJudgeRun(input, ctx, opts = {}) {
213
220
  model: input.model,
214
221
  kind: input.kind,
215
222
  mode: input.run.mode,
223
+ via: input.run.via ?? null,
216
224
  ok,
217
225
  verdict: ok ? verdict?.verdict : null,
218
226
  rationale: ok
@@ -324,7 +332,12 @@ export async function runQuorumCase(input, ctx, opts = {}) {
324
332
  },
325
333
  };
326
334
  }
327
- const run = { provider: providerName, mode: config.mode, outcome };
335
+ const run = {
336
+ provider: providerName,
337
+ mode: config.mode,
338
+ outcome,
339
+ via: config.transport === 'api' ? config.via : undefined,
340
+ };
328
341
  external.push(run);
329
342
  await recordJudgeRun({
330
343
  taskId: input.taskId,
@@ -46,8 +46,7 @@ export function stampResultEnvelope(agentResult, envelope) {
46
46
  if (overstep.length > 0) {
47
47
  throw new ResultError('results.agent-wrote-owned-field', `Result set dispatcher-owned field(s) ${overstep.join(', ')}. Agents return run_status, structured_output and artifacts only.`, { fields: [...overstep] });
48
48
  }
49
- const inputTokens = requireTokenCount('input_tokens', envelope.inputTokens);
50
- const outputTokens = requireTokenCount('output_tokens', envelope.outputTokens);
49
+ const tokenUsage = deriveTokenUsage(envelope);
51
50
  return {
52
51
  ...half,
53
52
  task_id: envelope.taskId,
@@ -56,10 +55,29 @@ export function stampResultEnvelope(agentResult, envelope) {
56
55
  model_tier: envelope.modelTier,
57
56
  // Derived, not accepted: a third number that can contradict the other two
58
57
  // is a third thing to get wrong.
59
- token_usage: {
60
- input_tokens: inputTokens,
61
- output_tokens: outputTokens,
62
- total_tokens: inputTokens + outputTokens,
63
- },
58
+ token_usage: tokenUsage,
59
+ };
60
+ }
61
+ /**
62
+ * `{input_tokens, output_tokens, total_tokens}` when the dispatcher has real
63
+ * numbers, or `{measured: false}` when it has neither (#220) — never one
64
+ * without the other, since half a measurement is not an honest "not measured"
65
+ * and not a count either.
66
+ */
67
+ function deriveTokenUsage(envelope) {
68
+ const { inputTokens, outputTokens } = envelope;
69
+ if (inputTokens === undefined && outputTokens === undefined) {
70
+ return { measured: false };
71
+ }
72
+ if (inputTokens === undefined || outputTokens === undefined) {
73
+ const missing = inputTokens === undefined ? 'input_tokens' : 'output_tokens';
74
+ throw new ResultError('results.partial-token-count', `input_tokens and output_tokens must both be given or both omitted (an honest "not measured" is neither alone); ${missing} is missing.`, { field: missing });
75
+ }
76
+ const input = requireTokenCount('input_tokens', inputTokens);
77
+ const output = requireTokenCount('output_tokens', outputTokens);
78
+ return {
79
+ input_tokens: input,
80
+ output_tokens: output,
81
+ total_tokens: input + output,
64
82
  };
65
83
  }
@@ -12,6 +12,7 @@
12
12
  import { readFileSync } from 'node:fs';
13
13
  import { SmithError } from './errors.js';
14
14
  import { roadmapReadPath } from './paths.js';
15
+ import { FACTORY_PROJECT_NAME, normalizeProjectName } from './projectName.js';
15
16
  export class RoadmapError extends SmithError {
16
17
  }
17
18
  /**
@@ -25,11 +26,12 @@ export class RoadmapError extends SmithError {
25
26
  * clone's parent holds a `black-smith` sibling that is a DIFFERENT repository,
26
27
  * so the factory's own checkout is `REPO_ROOT` and never a name lookup.
27
28
  *
28
- * Deliberately not imported from `db/queries.ts`'s `DEFAULT_PROJECT`, which
29
- * says the same word: that module reaches drizzle, and `test/cliBoot.test.ts`
30
- * pins that the CLI's module graph never does.
29
+ * Sourced from `projectName.ts`, not `db/queries.ts`'s `DEFAULT_PROJECT`,
30
+ * which says the same word: that module reaches drizzle, and
31
+ * `test/cliBoot.test.ts` pins that the CLI's module graph never does.
32
+ * `projectName.ts` has no dependency of its own, so both may import it.
31
33
  */
32
- export const FACTORY_PROJECT = 'black-smith';
34
+ export const FACTORY_PROJECT = FACTORY_PROJECT_NAME;
33
35
  export const MILESTONE_STATUSES = [
34
36
  'planned',
35
37
  'in-progress',
@@ -249,7 +251,10 @@ export function parseRoadmap(markdown) {
249
251
  }
250
252
  const projectVal = bulletValue(line, 'project');
251
253
  if (projectVal !== null && projectVal !== '') {
252
- project = projectVal;
254
+ // Old roadmap.md bullets may still spell the factory's own project
255
+ // the old way (history is immutable); normalized here so the
256
+ // projection built from it never stores the legacy name.
257
+ project = normalizeProjectName(projectVal);
253
258
  continue;
254
259
  }
255
260
  // An empty `- kind:` is absent, not invalid -- the same reading
@@ -7,12 +7,13 @@
7
7
  // test in test/runner.test.ts).
8
8
  import { readFileSync, writeFileSync } from 'node:fs';
9
9
  import { parseArgs } from './args.js';
10
+ import { warnIfLegacyName } from './cliName.js';
10
11
  import { runInvocation } from './runner.js';
11
- const HELP = `Usage: smith-run <invocation.json | -> [--prompt-file <path>] [--timeout-ms <n>] [--out <file>]
12
+ const HELP = `Usage: bs-run <invocation.json | -> [--prompt-file <path>] [--timeout-ms <n>] [--out <file>]
12
13
 
13
- Spawns ONE already-rendered worker invocation (the JSON \`smith harness plan\`
14
- prints) and reports what happened as JSON. This is not \`smith harness run\`:
15
- \`smith\` only ever renders an invocation and never starts it (§18 rule 3,
14
+ Spawns ONE already-rendered worker invocation (the JSON \`bs harness plan\`
15
+ prints) and reports what happened as JSON. This is not \`bs harness run\`:
16
+ \`bs\` only ever renders an invocation and never starts it (§18 rule 3,
16
17
  "nothing that observes may dispatch"), so starting the process an invocation
17
18
  describes lives in its own binary instead.
18
19
 
@@ -60,6 +61,10 @@ function exitCodeFor(outcome) {
60
61
  return 0;
61
62
  }
62
63
  async function main() {
64
+ // bs-rename, operator decision 3: a no-op unless argv[1]'s basename is the
65
+ // deprecated `smith-run`, in which case it writes one line to stderr,
66
+ // never stdout -- stdout carries the JSON RunOutcome.
67
+ warnIfLegacyName(process.argv[1], 'smith-run', 'bs-run');
63
68
  const argv = process.argv.slice(2);
64
69
  const parsed = parseArgs(argv, FLAG_SPEC);
65
70
  if (parsed.flags.help === 'true') {
@@ -68,7 +73,7 @@ async function main() {
68
73
  }
69
74
  const source = parsed.positional[0];
70
75
  if (source === undefined) {
71
- process.stderr.write('smith-run: missing <invocation.json | -> argument.\n\n');
76
+ process.stderr.write('bs-run: missing <invocation.json | -> argument.\n\n');
72
77
  process.stderr.write(HELP);
73
78
  return 1;
74
79
  }
@@ -78,14 +83,14 @@ async function main() {
78
83
  }
79
84
  catch (err) {
80
85
  const message = err instanceof Error ? err.message : String(err);
81
- process.stderr.write(`smith-run: could not read/parse invocation: ${message}\n`);
86
+ process.stderr.write(`bs-run: could not read/parse invocation: ${message}\n`);
82
87
  return 1;
83
88
  }
84
89
  const timeoutOverride = parsed.flags['timeout-ms'];
85
90
  if (timeoutOverride !== undefined && invocation.kind === 'cli') {
86
91
  const timeout_ms = Number.parseInt(timeoutOverride, 10);
87
92
  if (Number.isNaN(timeout_ms)) {
88
- process.stderr.write(`smith-run: --timeout-ms must be a number, got "${timeoutOverride}".\n`);
93
+ process.stderr.write(`bs-run: --timeout-ms must be a number, got "${timeoutOverride}".\n`);
89
94
  return 1;
90
95
  }
91
96
  invocation = { ...invocation, budget: { ...invocation.budget, timeout_ms } };
@@ -98,7 +103,7 @@ async function main() {
98
103
  }
99
104
  catch (err) {
100
105
  const message = err instanceof Error ? err.message : String(err);
101
- process.stderr.write(`smith-run: could not read --prompt-file: ${message}\n`);
106
+ process.stderr.write(`bs-run: could not read --prompt-file: ${message}\n`);
102
107
  return 1;
103
108
  }
104
109
  }
@@ -108,7 +113,7 @@ async function main() {
108
113
  }
109
114
  catch (err) {
110
115
  const message = err instanceof Error ? err.message : String(err);
111
- process.stderr.write(`smith-run: ${message}\n`);
116
+ process.stderr.write(`bs-run: ${message}\n`);
112
117
  return 1;
113
118
  }
114
119
  const json = `${JSON.stringify(outcome, null, 2)}\n`;
@@ -125,6 +130,6 @@ main().then((code) => {
125
130
  process.exitCode = code;
126
131
  }, (err) => {
127
132
  const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
128
- process.stderr.write(`smith-run: unexpected error: ${message}\n`);
133
+ process.stderr.write(`bs-run: unexpected error: ${message}\n`);
129
134
  process.exitCode = 1;
130
135
  });
@@ -170,8 +170,8 @@ function validate(answer, schema) {
170
170
  export async function runInvocation(invocation, opts = {}) {
171
171
  if (invocation.kind === 'in-process') {
172
172
  throw new RunnerError('runner.in-process', `Invocation for role "${invocation.role}" is in-process (harness "${invocation.harness}") — ` +
173
- 'smith-run starts programs, not Agent-tool subagents. In-process harnesses run inside the ' +
174
- 'orchestrator session itself and have no separate binary for smith-run to spawn.', { harness: invocation.harness, role: invocation.role });
173
+ 'bs-run starts programs, not Agent-tool subagents. In-process harnesses run inside the ' +
174
+ 'orchestrator session itself and have no separate binary for bs-run to spawn.', { harness: invocation.harness, role: invocation.role });
175
175
  }
176
176
  const now = opts.now ?? Date.now;
177
177
  const start = now();
@@ -154,7 +154,40 @@ function assertEveryAnnotationReachable(name, schema, pointers) {
154
154
  'onto the property (or onto `items`) that carries the value.', { schema: name, paths: unreachable });
155
155
  }
156
156
  function ajvErrorToIssue(err) {
157
- return { path: err.instancePath || '/', message: `${err.instancePath} ${err.message}`.trim() };
157
+ // additionalProperties names the offending key only in `params`, never in
158
+ // `instancePath`/`message` — without it, "must NOT have additional
159
+ // properties" is true of every extra key on the record at once, and an
160
+ // evidence file with three wrong field names (issue #233) reads as one
161
+ // undifferentiated complaint instead of three actionable ones.
162
+ const extra = err.keyword === 'additionalProperties'
163
+ ? err.params.additionalProperty
164
+ : undefined;
165
+ const base = `${err.instancePath} ${err.message}`.trim();
166
+ return { path: err.instancePath || '/', message: extra ? `${base}: ${extra}` : base };
167
+ }
168
+ /**
169
+ * The ajv-structural half of `validateRecord`, on its own: does this record
170
+ * have the shape the schema declares, with no taxonomy lookup at all.
171
+ *
172
+ * Split out for `validateEachShape` below, which checks a whole array of
173
+ * records (a judge's evidence file) against one schema before anything reads
174
+ * a taxonomy value out of any of them — a record that got the shape wrong
175
+ * (issue #233's wrong field names, a `failure_scenario` given as a string)
176
+ * must never reach code that assumes the shape is already right.
177
+ */
178
+ export function validateSchemaShape(schemas, schemaName, record) {
179
+ const compiled = schemas.get(schemaName);
180
+ if (!compiled) {
181
+ throw new SchemaError('schema.unknown-name', `Unknown schema "${schemaName}".`, {
182
+ schemaName,
183
+ known: [...schemas.keys()],
184
+ });
185
+ }
186
+ const schemaValid = compiled.validate(record);
187
+ if (!schemaValid) {
188
+ return { valid: false, errors: (compiled.validate.errors ?? []).map(ajvErrorToIssue) };
189
+ }
190
+ return { valid: true };
158
191
  }
159
192
  /**
160
193
  * Validate a record against a compiled schema by name, then resolve every
@@ -169,13 +202,8 @@ export function validateRecord(schemas, taxonomy, schemaName, record) {
169
202
  known: [...schemas.keys()],
170
203
  });
171
204
  }
172
- const errors = [];
173
- const schemaValid = compiled.validate(record);
174
- if (!schemaValid) {
175
- for (const err of compiled.validate.errors ?? []) {
176
- errors.push(ajvErrorToIssue(err));
177
- }
178
- }
205
+ const shape = validateSchemaShape(schemas, schemaName, record);
206
+ const errors = shape.valid ? [] : [...shape.errors];
179
207
  for (const pointer of compiled.taxonomyPointers) {
180
208
  for (const hit of resolveAtPath(record, pointer.segments)) {
181
209
  if (hit.value === undefined)
@@ -193,3 +221,23 @@ export function validateRecord(schemas, taxonomy, schemaName, record) {
193
221
  return { valid: false, errors };
194
222
  return { valid: true };
195
223
  }
224
+ /**
225
+ * Validate every element of an array against one named schema, structurally
226
+ * (`validateSchemaShape`, no taxonomy lookup), collecting every index that
227
+ * fails rather than stopping at the first.
228
+ *
229
+ * Shared by `findings.ts` (evidence a judge hands `findings raise --evidence`
230
+ * or `gate run --evidence`) and `judges.ts` (the same evidence shape arriving
231
+ * as a judge's reported artifact) — one schema, one failure-collection rule,
232
+ * so the two intake paths cannot drift on what "malformed" means (issue
233
+ * #233).
234
+ */
235
+ export function validateEachShape(schemas, schemaName, records) {
236
+ const failures = [];
237
+ records.forEach((record, index) => {
238
+ const result = validateSchemaShape(schemas, schemaName, record);
239
+ if (!result.valid)
240
+ failures.push({ index, errors: result.errors });
241
+ });
242
+ return failures;
243
+ }
@@ -69,11 +69,15 @@ export function parseSeverityPolicy(yamlText) {
69
69
  export function loadSeverityPolicy(filePath = SEVERITY_POLICY_PATH) {
70
70
  return parseSeverityPolicy(readFileSync(filePath, 'utf8'));
71
71
  }
72
- const ENTRY_HEADING = /^### /;
73
- const SECTION_HEADING = /^## (.+)$/;
74
- const BULLET = /^- ([a-z_]+):\s*(.*)$/;
72
+ // Exported (not just used by parseLessons below): lessons.ts's compileLessons
73
+ // reuses these four to salvage a pre-existing entry compileLessons() itself
74
+ // can't regenerate (no store row) — one parser, shared by both directions of
75
+ // the round-trip, so they can't drift apart on what an "entry" is.
76
+ export const ENTRY_HEADING = /^### /;
77
+ export const SECTION_HEADING = /^## (.+)$/;
78
+ export const BULLET = /^- ([a-z_]+):\s*(.*)$/;
75
79
  /** An indented, non-bullet line — a continuation of the previous bullet's value. */
76
- const CONTINUATION = /^\s+(\S.*)$/;
80
+ export const CONTINUATION = /^\s+(\S.*)$/;
77
81
  /**
78
82
  * taxonomy.yml `lesson_scope`, in compiled-section order — only entries under
79
83
  * one of these `##` sections are real lessons, which is also what stops a `##`