@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.
- package/.claude/agents/{auditor.md → bs-auditor.md} +5 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +4 -4
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +13 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +6 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +15 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +4 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +34 -27
- package/.claude/skills/bs/dispatch.md +100 -50
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +27 -18
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +181 -49
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +109 -53
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +48 -14
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +125 -17
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +790 -100
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +154 -36
- package/factory/orchestrator/dist/db/projector.js +278 -29
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +13 -37
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +323 -38
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +10 -4
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +105 -3
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +83 -12
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +92 -3
- package/factory/orchestrator/dist/integration.js +9 -7
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +90 -32
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +10 -7
- package/factory/orchestrator/dist/plan.js +269 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +4 -4
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +177 -13
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +179 -27
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +16 -13
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +97 -40
- package/factory/orchestrator/dist/waivers.js +39 -5
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +61 -19
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +7 -7
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- 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:
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
*
|
|
29
|
-
* says the same word: that module reaches drizzle, and
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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 \`
|
|
14
|
-
prints) and reports what happened as JSON. This is not \`
|
|
15
|
-
\`
|
|
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('
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
-
'
|
|
174
|
-
'orchestrator session itself and have no separate binary for
|
|
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
|
-
|
|
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
|
|
173
|
-
const
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 `##`
|