@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,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { constants as osConstants } from 'node:os';
|
|
3
|
+
import { BS_PREFIX, LEGACY_PREFIX } from './env.js';
|
|
3
4
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
4
5
|
const TAIL_LINES = 50;
|
|
5
6
|
/**
|
|
@@ -63,27 +64,29 @@ function signalNote(code, signal) {
|
|
|
63
64
|
* factory's own namespace.
|
|
64
65
|
*
|
|
65
66
|
* A command the factory launches on a project's behalf belongs to the
|
|
66
|
-
* project, not to the factory that launched it. Every `
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* factory
|
|
67
|
+
* project, not to the factory that launched it. Every `BS_*` variable — and,
|
|
68
|
+
* for the transition period, every legacy `SMITH_*` one — in this process
|
|
69
|
+
* was set for *this* process, and letting one through silently makes the
|
|
70
|
+
* factory's own invocation part of the project's verdict. `BS_HOME` (nee
|
|
71
|
+
* `SMITH_HOME`) is the one that bites: it moves `WORK_ROOT` off the
|
|
72
|
+
* worktree, so a suite that asserts its own layout fails on the launcher's
|
|
73
|
+
* environment alone, and anything the suite writes to state lands in the
|
|
74
|
+
* factory's live `state/` instead of the worktree's. The verdict then
|
|
75
|
+
* depends on how the factory happened to be invoked, which is not a verdict
|
|
76
|
+
* about the branch.
|
|
74
77
|
*
|
|
75
78
|
* The strip is a rule rather than a roster, deliberately: a roster is a list
|
|
76
|
-
* that goes stale the next time a `SMITH_*` variable is added, and it
|
|
77
|
-
* stale silently. Not every one of them is the factory configuring
|
|
78
|
-
* `
|
|
79
|
-
* says of that one "pass it per command"
|
|
79
|
+
* that goes stale the next time a `BS_*`/`SMITH_*` variable is added, and it
|
|
80
|
+
* goes stale silently. Not every one of them is the factory configuring
|
|
81
|
+
* itself — `BS_CROSSCHECK_OFFLINE` is an operator switch — but the runbook
|
|
82
|
+
* already says of that one "pass it per command"
|
|
80
83
|
* (`docs/runbooks/providers.md`, and it names an ambient copy of that switch
|
|
81
84
|
* as its own hazard). The command the operator means is the one running
|
|
82
85
|
* here, not the factory that spawned it, and stating it inside the command
|
|
83
86
|
* string works: the shell applies it after this strip.
|
|
84
87
|
*/
|
|
85
88
|
export function projectCommandEnv(env) {
|
|
86
|
-
return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith(
|
|
89
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith(BS_PREFIX) && !name.startsWith(LEGACY_PREFIX)));
|
|
87
90
|
}
|
|
88
91
|
/**
|
|
89
92
|
* Run one check command in its own process group (POSIX `setsid`, via
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolveArtifactPath } from './artifacts.js';
|
|
2
|
+
import { eventTaskId, readLineageEvents } from './events.js';
|
|
3
|
+
import { foldJudgeTurns, JUDGE_DISPATCH_EVENT_TYPE } from './judges.js';
|
|
4
|
+
import { taskIdsMatch } from './taskId.js';
|
|
5
|
+
/** `judges.ts`'s uiux role, spelled out here rather than imported: the string is the taxonomy value, not an implementation detail of judges.ts. */
|
|
6
|
+
const UIUX_ROLE = 'uiux';
|
|
7
|
+
/** desktop × mobile, light × dark — the four shots a visual pass has to cover. */
|
|
8
|
+
const REQUIRED_COMBOS = [
|
|
9
|
+
['desktop', 'light'],
|
|
10
|
+
['desktop', 'dark'],
|
|
11
|
+
['mobile', 'light'],
|
|
12
|
+
['mobile', 'dark'],
|
|
13
|
+
];
|
|
14
|
+
function latestTesterRecord(events, taskId) {
|
|
15
|
+
let latest = null;
|
|
16
|
+
events.forEach(({ record }, index) => {
|
|
17
|
+
if (record.event_type !== 'artifact-check-result')
|
|
18
|
+
return;
|
|
19
|
+
if (!taskIdsMatch(eventTaskId(record) ?? undefined, taskId))
|
|
20
|
+
return;
|
|
21
|
+
const head = record.payload.head;
|
|
22
|
+
if (typeof head !== 'string')
|
|
23
|
+
return; // an artifact-check-result with no --worktree behind it
|
|
24
|
+
const screenshots = Array.isArray(record.payload.screenshots)
|
|
25
|
+
? record.payload.screenshots
|
|
26
|
+
: [];
|
|
27
|
+
latest = { index, head, screenshots };
|
|
28
|
+
});
|
|
29
|
+
return latest;
|
|
30
|
+
}
|
|
31
|
+
/** The lineage position of the dispatch that opened a judge turn — `JudgeTurn.dispatchedAt` is wall-clock only. */
|
|
32
|
+
function dispatchIndex(events, taskId, role, kind, round) {
|
|
33
|
+
let found = -1;
|
|
34
|
+
events.forEach(({ record }, index) => {
|
|
35
|
+
if (record.event_type !== JUDGE_DISPATCH_EVENT_TYPE)
|
|
36
|
+
return;
|
|
37
|
+
if (!taskIdsMatch(eventTaskId(record) ?? undefined, taskId))
|
|
38
|
+
return;
|
|
39
|
+
if (record.payload.agent_role !== role)
|
|
40
|
+
return;
|
|
41
|
+
if (record.payload.judge_kind !== kind)
|
|
42
|
+
return;
|
|
43
|
+
if (record.payload.round !== round)
|
|
44
|
+
return;
|
|
45
|
+
found = index;
|
|
46
|
+
});
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `checkUiux`: the one function both the gate stage and `bs uiux check`
|
|
51
|
+
* (the preflight verb, D4) call, so the two can never disagree about the same
|
|
52
|
+
* fixture. Reads the lineage once and folds both judge turns and the tester's
|
|
53
|
+
* freshness record out of it — no separate dispatch, no separate store.
|
|
54
|
+
*/
|
|
55
|
+
export async function checkUiux(input, ctx, opts = {}) {
|
|
56
|
+
const events = await readLineageEvents(ctx.sessionId, opts);
|
|
57
|
+
const turns = foldJudgeTurns(events, input.taskId);
|
|
58
|
+
const specTurn = turns.find((t) => t.role === UIUX_ROLE && t.kind === 'spec');
|
|
59
|
+
if (!specTurn?.reported) {
|
|
60
|
+
return { outcome: 'blocked', reason: 'uiux-spec-missing' };
|
|
61
|
+
}
|
|
62
|
+
const tester = latestTesterRecord(events, input.taskId);
|
|
63
|
+
if (tester === null) {
|
|
64
|
+
return { outcome: 'blocked', reason: 'screenshots-missing' };
|
|
65
|
+
}
|
|
66
|
+
const haveCombos = new Set(tester.screenshots.map((s) => `${s.viewport}:${s.theme}`));
|
|
67
|
+
const allCombosPresent = REQUIRED_COMBOS.every(([viewport, theme]) => haveCombos.has(`${viewport}:${theme}`));
|
|
68
|
+
const allOnDisk = tester.screenshots.every((s) => resolveArtifactPath(input.taskId, s.path, input.artifactsDir) !== null);
|
|
69
|
+
if (!allCombosPresent || !allOnDisk) {
|
|
70
|
+
return { outcome: 'blocked', reason: 'screenshots-missing' };
|
|
71
|
+
}
|
|
72
|
+
if (input.head !== undefined && tester.head !== input.head) {
|
|
73
|
+
return { outcome: 'blocked', reason: 'screenshots-stale' };
|
|
74
|
+
}
|
|
75
|
+
const visualTurn = turns.find((t) => t.role === UIUX_ROLE && t.kind === 'visual');
|
|
76
|
+
const visualDispatchIndex = visualTurn === undefined
|
|
77
|
+
? -1
|
|
78
|
+
: dispatchIndex(events, input.taskId, UIUX_ROLE, 'visual', visualTurn.round);
|
|
79
|
+
if (!visualTurn?.reported || !(visualDispatchIndex > tester.index)) {
|
|
80
|
+
return { outcome: 'blocked', reason: 'uiux-visual-missing' };
|
|
81
|
+
}
|
|
82
|
+
return { outcome: 'pass' };
|
|
83
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What `
|
|
2
|
+
* What `bs` can be asked to do, written down once (P9-21).
|
|
3
3
|
*
|
|
4
4
|
* Before this, `smith` and `smith --help` both answered
|
|
5
5
|
* `{"error":{"message":"Unknown command: "}}`, so the only way to learn a
|
|
@@ -67,7 +67,7 @@ export const COMMANDS = [
|
|
|
67
67
|
command: 'audit resolve',
|
|
68
68
|
positionals: '<project-dir>',
|
|
69
69
|
flags: `--epic <epic-id> [--except <fp[,fp...]>] [--plan <plan.json>] [--specs-dir <dir>] ${EVENTS_DIR}`,
|
|
70
|
-
summary: "Append a fixed line for every carried finding the epic's newest plan still claims; list the rest as deferred. Run from the epic-close step.",
|
|
70
|
+
summary: "Append a fixed line for every carried finding a task in the epic's newest plan still claims, or a superseded task's change already merged; list the rest as deferred. Run from the epic-close step.",
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
command: 'audit close',
|
|
@@ -90,14 +90,14 @@ export const COMMANDS = [
|
|
|
90
90
|
{
|
|
91
91
|
command: 'plan quorum',
|
|
92
92
|
positionals: '',
|
|
93
|
-
flags: '--plan-version <n> --session <id> --causal-parent <event-id> [--epic <id>] [--plan <draft.json>] [--confidence <0-1>] [--out <file>] [--actor <name>] [--specs-dir <dir>] [--state-dir <dir>]',
|
|
93
|
+
flags: '--plan-version <n> --session <id> --causal-parent <event-id> [--epic <id>] [--plan <draft.json>] [--confidence <0-1>] [--project <dir>] [--out <file>] [--actor <name>] [--specs-dir <dir>] [--state-dir <dir>]',
|
|
94
94
|
summary: 'Critique-only review of a drafted plan: --plan <draft.json> before it is filed, or --epic for the filed version. Exit 1 means the operator must look first.',
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
command: 'plan amend',
|
|
98
98
|
positionals: '',
|
|
99
99
|
flags: `--plan <plan.json> --findings <id[,id...]> --rationale <text> --sites <path[,path...]> [--changes <changes.json>] [--specs-dir <dir>] ${EVENTS_DIR}`,
|
|
100
|
-
summary: 'Cut a new plan version against the spec findings that forced it. No finding, no amendment. --sites is every place the shape occurs, not only where it was reported.',
|
|
100
|
+
summary: 'Cut a new plan version against the spec findings that forced it. No finding, no amendment. --sites is every place the shape occurs, not only where it was reported. --specs-dir defaults to the directory --plan already lives in (#219), not factory/specs/active; an explicit --specs-dir that disagrees with it is refused rather than silently preferred.',
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
command: 'plan propose',
|
|
@@ -126,8 +126,11 @@ export const COMMANDS = [
|
|
|
126
126
|
{
|
|
127
127
|
command: 'plan ingest',
|
|
128
128
|
positionals: '<plan.json>',
|
|
129
|
-
flags: EVENTS_DIR
|
|
130
|
-
summary: 'Record every task and dependency the plan declares, before any wave names them.'
|
|
129
|
+
flags: `[--project <dir>] ${EVENTS_DIR}`,
|
|
130
|
+
summary: 'Record every task and dependency the plan declares, before any wave names them. ' +
|
|
131
|
+
'--project resolves the branch prefix a legacy `smith/<epic>/integration` epic keeps ' +
|
|
132
|
+
'using, rather than declaring `bs/...` for a branch `worktree create` will cut as ' +
|
|
133
|
+
'`smith/...`.',
|
|
131
134
|
},
|
|
132
135
|
{
|
|
133
136
|
command: 'wave check',
|
|
@@ -275,8 +278,10 @@ export const COMMANDS = [
|
|
|
275
278
|
{
|
|
276
279
|
command: 'queue run',
|
|
277
280
|
positionals: '<epic>',
|
|
278
|
-
flags: '--project <dir> --test-cmd <cmd> --tasks <tasks.json>
|
|
281
|
+
flags: '--project <dir> --test-cmd <cmd> --tasks <tasks.json> --session <id> --causal-parent <event-id> --plan <plan.json> [--plan-version <n>] [--actor <name>] [--select-test-cmd <cmd>] [--batch] [--state-dir <dir>]',
|
|
279
282
|
summary: 'Serially rebase, test, and merge task branches into the integration branch. ' +
|
|
283
|
+
'A merge is a fact the log must carry, so --session/--causal-parent/--plan are required — ' +
|
|
284
|
+
'refuses before any git write without them. ' +
|
|
280
285
|
'--batch (roadmap merge-lanes) groups claim-disjoint tasks with groupForBatch and ' +
|
|
281
286
|
'tests each group once instead of once per task — see queue.ts and the operator guide.',
|
|
282
287
|
},
|
|
@@ -293,8 +298,8 @@ export const COMMANDS = [
|
|
|
293
298
|
{
|
|
294
299
|
command: 'session start',
|
|
295
300
|
positionals: '<session-id>',
|
|
296
|
-
flags: '[--continues <event-id>] [--actor <name>] [--state-dir <dir>]',
|
|
297
|
-
summary: 'Open a session log with the one root event it is allowed. Exit 1 if the session is already open.',
|
|
301
|
+
flags: '[--continues <event-id>] [--prompt <event-id>] [--actor <name>] [--state-dir <dir>]',
|
|
302
|
+
summary: 'Open a session log with the one root event it is allowed; --prompt links the user_prompt that asked for it. Exit 1 if the session is already open.',
|
|
298
303
|
},
|
|
299
304
|
{
|
|
300
305
|
command: 'event append',
|
|
@@ -327,6 +332,36 @@ export const COMMANDS = [
|
|
|
327
332
|
flags: EVENTS_DIR,
|
|
328
333
|
summary: "Put the operator's own words on the timeline verbatim, and print the event id a dispatch hangs off.",
|
|
329
334
|
},
|
|
335
|
+
{
|
|
336
|
+
command: 'prompt capture',
|
|
337
|
+
positionals: '',
|
|
338
|
+
flags: '',
|
|
339
|
+
summary: 'Read one UserPromptSubmit hook payload on stdin and record it where the cwd is managed. Print one line, or nothing; always exit 0.',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
command: 'prompt import',
|
|
343
|
+
positionals: '',
|
|
344
|
+
flags: '--transcript <file> [--dry-run]',
|
|
345
|
+
summary: "Replay a Claude Code transcript's prompts and option answers into prompts-<cli session>, keeping their timestamps. Re-running imports nothing; --dry-run prints each event and writes none. Print a summary line.",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
command: 'feedback record',
|
|
349
|
+
positionals: '',
|
|
350
|
+
flags: `--task <task-id> [--body <text>|--body-file <file>] [--kind <must-fix|nice-to-have>] [--source <dashboard|github|cli>] [--external-id <id>] [--author <name>] ${EVENTS_DIR}`,
|
|
351
|
+
summary: "Record the operator's mid-run comment on a task. Print { event_id, feedback_id, deduped }; a repeated --external-id dedupes rather than appending.",
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
command: 'feedback pending',
|
|
355
|
+
positionals: '',
|
|
356
|
+
flags: '--session <id> [--task <task-id>] [--epic <id>] [--state-dir <dir>]',
|
|
357
|
+
summary: "List this session lineage's unresolved feedback, each with a suggestedAction: follow-up once its task is closed to further work, bounce otherwise.",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
command: 'feedback resolve',
|
|
361
|
+
positionals: '',
|
|
362
|
+
flags: `--feedback <id> --resolution <bounced|follow-up|dismissed> [--note <text>] [--follow-up-task <task-id>] ${EVENTS_DIR}`,
|
|
363
|
+
summary: 'Resolve one recorded feedback item. Rejects an unknown or already-resolved feedback id.',
|
|
364
|
+
},
|
|
330
365
|
{
|
|
331
366
|
command: 'claims check',
|
|
332
367
|
form: '--roots',
|
|
@@ -423,7 +458,7 @@ export const COMMANDS = [
|
|
|
423
458
|
{
|
|
424
459
|
command: 'harness plan',
|
|
425
460
|
positionals: '',
|
|
426
|
-
flags: '--role <role> --task <id> --prompt-file <path> [--harness <name>] [--worktree <dir>] [--policy <file>] [--tier <frontier|mid|small>] [--schema <name>]',
|
|
461
|
+
flags: '--role <role> --task <id> --prompt-file <path> [--harness <name>] [--worktree <dir>] [--policy <file>] [--tier <frontier|mid|small>] [--schema <name>] [--specs-dir <dir>]',
|
|
427
462
|
summary: 'Render the invocation that would run one worker turn, as JSON. Prints it; never starts it.',
|
|
428
463
|
},
|
|
429
464
|
{
|
|
@@ -460,7 +495,7 @@ export const COMMANDS = [
|
|
|
460
495
|
command: 'stack check',
|
|
461
496
|
positionals: '',
|
|
462
497
|
flags: '[--policy <file>]',
|
|
463
|
-
summary: 'Which stack answers the shipped templates honour, which they only record, and which make `
|
|
498
|
+
summary: 'Which stack answers the shipped templates honour, which they only record, and which make `bs new` refuse. Exit 1 on a refusal.',
|
|
464
499
|
},
|
|
465
500
|
{
|
|
466
501
|
command: 'policy check',
|
|
@@ -477,9 +512,21 @@ export const COMMANDS = [
|
|
|
477
512
|
{
|
|
478
513
|
command: 'gate run',
|
|
479
514
|
positionals: '<task-id>',
|
|
480
|
-
flags: `--worktree <dir> --checks <checks.json> --result <result.json> [--base <ref>] [--grader <file>] [--agent <role> --provider <name> --model-tier <tier> --input-tokens <n> --output-tokens <n>] [--evidence <file> --found-by <role> [--found-by-provider <name>]] [--findings <file>] [--no-findings <role>] [--lessons <lessons.md>] [--plan <file>] [--run-all] [--artifacts-dir <dir>] ${EVENTS_DIR}`,
|
|
515
|
+
flags: `--worktree <dir> --checks <checks.json> --result <result.json> [--project <dir>] [--base <ref>] [--grader <file>] [--agent <role> --provider <name> --model-tier <tier> --input-tokens <n> --output-tokens <n>] [--evidence <file> --found-by <role> [--found-by-provider <name>]] [--findings <file>] [--no-findings <role>] [--uiux-visual <file>] [--lessons <lessons.md>] [--plan <file>] [--run-all] [--artifacts-dir <dir>] ${EVENTS_DIR}`,
|
|
481
516
|
summary: 'The per-task gate: schema, tests, coverage, findings. Exit 1 when blocked.',
|
|
482
517
|
},
|
|
518
|
+
{
|
|
519
|
+
command: 'results record',
|
|
520
|
+
positionals: '',
|
|
521
|
+
flags: `--task <task-id> --result <result.json> [--agent <role> --provider <name> --model-tier <tier> [--input-tokens <n> --output-tokens <n>]] [--artifacts-dir <dir>] [--worktree <dir>] ${EVENTS_DIR}`,
|
|
522
|
+
summary: "Project a worker's Result on its own, with no worktree to certify and no tests to run: schema-check, task-result-recorded, artifact-check. Wave.md's tester has no gate to run one through; this is how its screenshots reach the dashboard the moment it finishes. --worktree records that worktree's HEAD and the Result's screenshot artifacts, the freshness evidence the uiux gate stage reads back. Exit 1 when blocked.",
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
command: 'uiux check',
|
|
526
|
+
positionals: '',
|
|
527
|
+
flags: `--task <task-id> --plan <plan.json> [--worktree <dir>] [--artifacts-dir <dir>] ${EVENTS_DIR}`,
|
|
528
|
+
summary: 'Preflight for the gate uiux stage: the same checkUiux a flagged task is gated on, so a dispatcher can ask before running the whole gate. Exit 1 when blocked.',
|
|
529
|
+
},
|
|
483
530
|
{
|
|
484
531
|
command: 'coverage check',
|
|
485
532
|
positionals: '<worktree-dir>',
|
|
@@ -489,7 +536,7 @@ export const COMMANDS = [
|
|
|
489
536
|
{
|
|
490
537
|
command: 'budget alarm',
|
|
491
538
|
positionals: '<session-id>',
|
|
492
|
-
flags: '[--epic <id>] [--policy <path>] [--state-dir <dir>]',
|
|
539
|
+
flags: '[--epic <id>] [--policy <path>] [--specs-dir <dir>] [--state-dir <dir>]',
|
|
493
540
|
summary: 'Epic spend against budgets.yml’s epic.alarm_ratio. Exit 1 on a crossing OR on a record too holey to rule one out.',
|
|
494
541
|
},
|
|
495
542
|
{
|
|
@@ -513,8 +560,8 @@ export const COMMANDS = [
|
|
|
513
560
|
{
|
|
514
561
|
command: 'epic spec-review',
|
|
515
562
|
positionals: '',
|
|
516
|
-
flags: `--epic <id> --project <dir> --plan <plan.json> --reviewed-by <role> [--reviewed-by-provider <name>] [--evidence <file>] ${EVENTS_DIR}`,
|
|
517
|
-
summary: 'Record the closing spec review, pinned to the integration head it was read at.',
|
|
563
|
+
flags: `--epic <id> --project <dir> --plan <plan.json> --reviewed-by <role> [--reviewed-by-provider <name>] [--evidence <file>] [--no-findings] ${EVENTS_DIR}`,
|
|
564
|
+
summary: 'Record the closing spec review, pinned to the integration head it was read at. Exactly one of --evidence or --no-findings; --no-findings attests the clean case.',
|
|
518
565
|
},
|
|
519
566
|
{
|
|
520
567
|
command: 'epic goal',
|
|
@@ -553,8 +600,8 @@ export const COMMANDS = [
|
|
|
553
600
|
{
|
|
554
601
|
command: 'findings for-dispatch',
|
|
555
602
|
positionals: '',
|
|
556
|
-
flags: '--session <id> --plan <plan.json> --task <task-id> [--state-dir <dir>]',
|
|
557
|
-
summary: "Print the open findings a dispatching task's own claims cover, as a prompt block.",
|
|
603
|
+
flags: '--session <id> [--plan <plan.json> --task <task-id>] [--state-dir <dir>]',
|
|
604
|
+
summary: "Print the open findings a dispatching task's own claims cover, as a prompt block; with no --plan/--task, every open finding in the epic.",
|
|
558
605
|
},
|
|
559
606
|
{
|
|
560
607
|
command: 'findings list',
|
|
@@ -595,10 +642,20 @@ export const COMMANDS = [
|
|
|
595
642
|
flags: EVENTS_DIR,
|
|
596
643
|
summary: 'Apply a batch of operator waiver decisions.',
|
|
597
644
|
},
|
|
645
|
+
{
|
|
646
|
+
command: 'waivers approve-task',
|
|
647
|
+
positionals: '<task-id>',
|
|
648
|
+
flags: `--note <why> ${EVENTS_DIR}`,
|
|
649
|
+
summary: 'Record the operator approval of one waived task. Without it a waived task blocks the epic close; an approval counts only for the plan version it was given at or later. Requires --actor operator.',
|
|
650
|
+
},
|
|
598
651
|
{
|
|
599
652
|
command: 'lessons candidates',
|
|
600
653
|
positionals: '',
|
|
601
|
-
|
|
654
|
+
// Only `candidates` takes `--state-dir` alongside STATS: it validates
|
|
655
|
+
// `--session` against the event log the same way `raise`/`approve`/
|
|
656
|
+
// `reject`/`db rebuild` do (P9-28), which is a per-session log, not a
|
|
657
|
+
// per-DB-query concern the other STATS-flagged commands share.
|
|
658
|
+
flags: `${STATS} [--state-dir <dir>]`,
|
|
602
659
|
summary: 'List the lesson candidates awaiting an approve or a reject.',
|
|
603
660
|
},
|
|
604
661
|
{
|
|
@@ -655,20 +712,20 @@ export const COMMANDS = [
|
|
|
655
712
|
{
|
|
656
713
|
command: 'judge dispatch',
|
|
657
714
|
positionals: '',
|
|
658
|
-
flags: `--task <task-id> --role <role> --artifact <
|
|
659
|
-
summary: 'Declare the file a judge owes, so a turn that never came back is visible.',
|
|
715
|
+
flags: `--task <task-id> --role <role> --artifact <absolute-path> --model <id> [--round <n>] [--provider <name>] [--model-tier <tier>] [--kind <spec|visual>] ${EVENTS_DIR}`,
|
|
716
|
+
summary: 'Declare the file a judge owes, so a turn that never came back is visible. --kind only applies to uiux, which opens a spec turn and a visual turn per task.',
|
|
660
717
|
},
|
|
661
718
|
{
|
|
662
719
|
command: 'judge report',
|
|
663
720
|
positionals: '',
|
|
664
|
-
flags: `--task <task-id> --role <role> [--round <n>] [--artifact <file>] [--no-findings] ${EVENTS_DIR}`,
|
|
665
|
-
summary:
|
|
721
|
+
flags: `--task <task-id> --role <role> [--round <n>] [--artifact <file>] [--no-findings] [--kind <spec|visual>] ${EVENTS_DIR}`,
|
|
722
|
+
summary: "Close a judge turn against the artifact it declared, written during this turn. --no-findings attests the clean case. --kind picks which of uiux's two open turns to close when both are open.",
|
|
666
723
|
},
|
|
667
724
|
{
|
|
668
725
|
command: 'judge outstanding',
|
|
669
726
|
positionals: '',
|
|
670
|
-
flags: '--session <id> --task <task-id> [--state-dir <dir>]',
|
|
671
|
-
summary: '
|
|
727
|
+
flags: '--session <id> --task <task-id> [--kind <spec|visual>] [--state-dir <dir>]',
|
|
728
|
+
summary: 'Print { taskId, sessionId, outstanding, count } for the judge turns still owed on a task. Exit 1 while count is non-zero.',
|
|
672
729
|
},
|
|
673
730
|
{
|
|
674
731
|
command: 'judge escalations',
|
|
@@ -679,8 +736,8 @@ export const COMMANDS = [
|
|
|
679
736
|
{
|
|
680
737
|
command: 'judge preflight',
|
|
681
738
|
positionals: '',
|
|
682
|
-
flags: '[--policy <file>]',
|
|
683
|
-
summary: 'Can the enabled providers be called at all, and would a promotion decide anything? Exit 1 on a provider that costs a call it cannot make.',
|
|
739
|
+
flags: '[--policy <file>] [--project <dir>]',
|
|
740
|
+
summary: 'Can the enabled providers be called at all, and would a promotion decide anything? Exit 1 on a provider that costs a call it cannot make. --project merges <dir>/.blacksmith/crosscheck.yml over the policy (OpenRouter substitution/extra judges).',
|
|
684
741
|
},
|
|
685
742
|
{
|
|
686
743
|
command: 'judge run',
|
|
@@ -691,25 +748,25 @@ export const COMMANDS = [
|
|
|
691
748
|
{
|
|
692
749
|
command: 'crossfind request',
|
|
693
750
|
positionals: '',
|
|
694
|
-
flags: '--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>]',
|
|
751
|
+
flags: '--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>] [--project <dir>] [--worktree <dir>]',
|
|
695
752
|
summary: 'Print the finder request without sending it — exactly what would leave the machine. Refuses when send_diff is false.',
|
|
696
753
|
},
|
|
697
754
|
{
|
|
698
755
|
command: 'crossfind reconcile',
|
|
699
756
|
positionals: '',
|
|
700
|
-
flags: '--task <id> --native <findings.json> --independent <runs.json> [--policy <file>]',
|
|
757
|
+
flags: '--task <id> --native <findings.json> --independent <runs.json> [--policy <file>] [--project <dir>] [--worktree <dir>]',
|
|
701
758
|
summary: 'Reconcile two saved finding lists offline. No provider, no log. Exit 1 when the result would change a gate.',
|
|
702
759
|
},
|
|
703
760
|
{
|
|
704
761
|
command: 'crossfind run',
|
|
705
762
|
positionals: '',
|
|
706
|
-
flags: `--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--status <status>] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>] ${EVENTS_DIR}`,
|
|
763
|
+
flags: `--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--status <status>] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>] [--project <dir>] [--worktree <dir>] ${EVENTS_DIR}`,
|
|
707
764
|
summary: 'Run the independent finder over a diff and reconcile it against the native findings. Exit 1 when the result would change a gate.',
|
|
708
765
|
},
|
|
709
766
|
{
|
|
710
767
|
command: 'ui serve',
|
|
711
768
|
positionals: '',
|
|
712
|
-
flags: '[--port <n>] [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>] [--now-iso <iso>]',
|
|
769
|
+
flags: '[--port <n>] [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>] [--now-iso <iso>] [--claude-config-dir <dir>] [--store <dir>...]',
|
|
713
770
|
summary: 'Serve the dashboard from the built ui/server.',
|
|
714
771
|
},
|
|
715
772
|
{
|
|
@@ -791,7 +848,7 @@ const COMMAND_NAMES = new Set(COMMANDS.map((doc) => doc.command));
|
|
|
791
848
|
const NAMESPACES = new Set(COMMANDS.map(namespaceOf));
|
|
792
849
|
/** The line that would have worked. Empty parts are dropped, never printed as a double space. */
|
|
793
850
|
export function usageLine(doc) {
|
|
794
|
-
return ['
|
|
851
|
+
return ['bs', doc.command, doc.positionals, doc.flags].filter((part) => part !== '').join(' ');
|
|
795
852
|
}
|
|
796
853
|
/** The positional argument names, in order — what a missing-argument error names. */
|
|
797
854
|
export function positionalNames(doc) {
|
|
@@ -833,7 +890,7 @@ export function flagSpecFor(namespace, action) {
|
|
|
833
890
|
if (namespace === undefined)
|
|
834
891
|
return undefined;
|
|
835
892
|
const key = action === undefined ? namespace : `${namespace} ${action}`;
|
|
836
|
-
// The second arm is what keeps the action-less verbs working: `
|
|
893
|
+
// The second arm is what keeps the action-less verbs working: `bs new
|
|
837
894
|
// my-project` spends its action slot on a positional. Same fallback as
|
|
838
895
|
// isDocumented, deliberately — a command that dispatches must also parse.
|
|
839
896
|
const docs = COMMANDS.filter((doc) => doc.command === key || doc.command === namespace).filter((doc) => doc.command === key || !COMMAND_NAMES.has(key));
|
|
@@ -861,23 +918,23 @@ function renderCommands(docs) {
|
|
|
861
918
|
}
|
|
862
919
|
/**
|
|
863
920
|
* The whole command list, or one namespace's slice of it. An unknown namespace
|
|
864
|
-
* throws rather than printing an empty list: "
|
|
921
|
+
* throws rather than printing an empty list: "bs has no such namespace" and
|
|
865
922
|
* "that namespace has no commands" must not be the same observable answer.
|
|
866
923
|
*/
|
|
867
924
|
export function usageText(namespace) {
|
|
868
925
|
if (namespace !== undefined && !NAMESPACES.has(namespace)) {
|
|
869
|
-
throw new SmithError('usage.unknown-namespace', `No such namespace: ${namespace}. Run "
|
|
926
|
+
throw new SmithError('usage.unknown-namespace', `No such namespace: ${namespace}. Run "bs --help" for the list.`, { namespace });
|
|
870
927
|
}
|
|
871
928
|
const docs = namespace === undefined ? COMMANDS : COMMANDS.filter((doc) => namespaceOf(doc) === namespace);
|
|
872
929
|
const header = namespace === undefined
|
|
873
930
|
? [
|
|
874
|
-
'Usage:
|
|
931
|
+
'Usage: bs <namespace> <action> [args] [--flags]',
|
|
875
932
|
'',
|
|
876
|
-
'
|
|
877
|
-
'
|
|
933
|
+
' bs <namespace> --help list one namespace',
|
|
934
|
+
' bs <command> --help show one command',
|
|
878
935
|
'',
|
|
879
936
|
].join('\n')
|
|
880
|
-
: `Usage:
|
|
937
|
+
: `Usage: bs ${namespace} <action> [args] [--flags]\n`;
|
|
881
938
|
let out = `${header}\n`;
|
|
882
939
|
let current = '';
|
|
883
940
|
for (const doc of docs) {
|
|
@@ -903,7 +960,7 @@ export function helpText(namespace, action) {
|
|
|
903
960
|
const exact = COMMANDS.filter((doc) => doc.command === key);
|
|
904
961
|
if (exact.length > 0)
|
|
905
962
|
return renderCommands(exact);
|
|
906
|
-
// `
|
|
963
|
+
// `bs new my-project --help`: the action slot holds a positional, so the
|
|
907
964
|
// namespace alone is the command.
|
|
908
965
|
const bare = COMMANDS.filter((doc) => doc.command === namespace);
|
|
909
966
|
if (bare.length > 0)
|
|
@@ -916,7 +973,7 @@ export function helpText(namespace, action) {
|
|
|
916
973
|
* Whether cli.ts's dispatcher has a branch for this pair. Checked before
|
|
917
974
|
* dispatch, so an undocumented command cannot run — which is the half of
|
|
918
975
|
* "the table and the dispatcher agree" that a test cannot enforce at runtime.
|
|
919
|
-
* The second arm is what keeps the action-less verbs working: `
|
|
976
|
+
* The second arm is what keeps the action-less verbs working: `bs new
|
|
920
977
|
* my-project` spends its action slot on a positional.
|
|
921
978
|
*/
|
|
922
979
|
export function isDocumented(namespace, action) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { isOperatorActor } from './actors.js';
|
|
1
2
|
import { SmithError } from './errors.js';
|
|
2
3
|
import { appendEvent, readLineageEvents } from './events.js';
|
|
3
4
|
import { computeFingerprint, foldFindings, listFindings, preWaiverStatus, staleFindings, transition, WAIVABLE_STATUSES, } from './findings.js';
|
|
4
5
|
export class WaiverError extends SmithError {
|
|
5
6
|
}
|
|
7
|
+
/** The event `approveTaskWaiver` writes and epic.ts's withGateEvidence reads. */
|
|
8
|
+
export const TASK_WAIVER_APPROVED_EVENT = 'task-waiver-approved';
|
|
6
9
|
/**
|
|
7
10
|
* Only S3/S4 findings are ever waived (severity.yml waiver_semantics) —
|
|
8
11
|
* single source of truth, reused by findings.ts's transition() so `→ waived`
|
|
@@ -129,6 +132,33 @@ export async function grantWaiver(fingerprint, operatorNote, ctx, opts = {}) {
|
|
|
129
132
|
await reconcileFindingsToWaived(fingerprint, waiverEvent.event_id, ctx, opts);
|
|
130
133
|
return waiverEvent;
|
|
131
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Record the operator's approval of one task's waiver. A task reaches
|
|
137
|
+
* `waived` through its plan row, which carries no gate run; this event is the
|
|
138
|
+
* evidence the epic gate (epic.ts's withGateEvidence) looks for instead.
|
|
139
|
+
*
|
|
140
|
+
* Refused unless the actor is the operator (actors.ts) and a rationale is
|
|
141
|
+
* given: the approval is only worth anything as a person's recorded decision.
|
|
142
|
+
* The default actor is the one every other operator decision defaults to.
|
|
143
|
+
*/
|
|
144
|
+
export async function approveTaskWaiver(taskId, operatorNote, ctx, opts = {}) {
|
|
145
|
+
const actor = ctx.actor ?? 'user';
|
|
146
|
+
if (!isOperatorActor(actor)) {
|
|
147
|
+
throw new WaiverError('waivers.not-operator', `Actor "${actor}" cannot approve a task waiver — only the operator can. Pass --actor operator.`, { actor, taskId });
|
|
148
|
+
}
|
|
149
|
+
if (operatorNote.trim() === '') {
|
|
150
|
+
throw new WaiverError('waivers.note-required', `Approving the waiver of task "${taskId}" needs a short rationale (--note).`, { taskId });
|
|
151
|
+
}
|
|
152
|
+
return appendEvent({
|
|
153
|
+
session_id: ctx.sessionId,
|
|
154
|
+
actor,
|
|
155
|
+
event_type: TASK_WAIVER_APPROVED_EVENT,
|
|
156
|
+
task_id: taskId,
|
|
157
|
+
plan_version: ctx.planVersion,
|
|
158
|
+
causal_parent: ctx.causalParent,
|
|
159
|
+
payload: { operator_note: operatorNote },
|
|
160
|
+
}, opts);
|
|
161
|
+
}
|
|
132
162
|
/**
|
|
133
163
|
* The mirror of reconcileFindingsToWaived, and it exists for the same reason
|
|
134
164
|
* (D-180). A denial that follows a grant revokes it — isWaived() folds
|
|
@@ -256,7 +286,7 @@ export async function applyBatch(decisions, ctx, opts = {}) {
|
|
|
256
286
|
`"${evidence.mergedTaskId}" has merged over since the finding was raised (matched by ` +
|
|
257
287
|
`${evidence.basis}: ${evidence.matched}). Its evidence describes code that no longer ` +
|
|
258
288
|
'exists, so this waiver would answer a question about a deleted file. Re-read it first: ' +
|
|
259
|
-
`
|
|
289
|
+
`bs findings reverify --finding ${finding.finding_id}`, {
|
|
260
290
|
fingerprint: decision.fingerprint,
|
|
261
291
|
findingId: finding.finding_id,
|
|
262
292
|
filePath: evidence.filePath,
|
|
@@ -266,14 +296,18 @@ export async function applyBatch(decisions, ctx, opts = {}) {
|
|
|
266
296
|
}
|
|
267
297
|
}
|
|
268
298
|
}
|
|
269
|
-
const
|
|
299
|
+
const events = [];
|
|
300
|
+
const findingIdsToCarry = new Set();
|
|
270
301
|
for (const decision of decisions) {
|
|
271
302
|
if (decision.decision === 'granted') {
|
|
272
|
-
|
|
303
|
+
events.push(await grantWaiver(decision.fingerprint, decision.operatorNote, ctx, opts));
|
|
273
304
|
}
|
|
274
305
|
else {
|
|
275
|
-
|
|
306
|
+
events.push(await denyWaiver(decision.fingerprint, decision.operatorNote, ctx, opts));
|
|
307
|
+
for (const finding of findingsByFingerprint.get(decision.fingerprint) ?? []) {
|
|
308
|
+
findingIdsToCarry.add(finding.finding_id);
|
|
309
|
+
}
|
|
276
310
|
}
|
|
277
311
|
}
|
|
278
|
-
return
|
|
312
|
+
return { events, findingIdsToCarry: [...findingIdsToCarry] };
|
|
279
313
|
}
|
|
@@ -153,6 +153,7 @@ export function checkWaveBudget(events, policy, proposed, options) {
|
|
|
153
153
|
if (options.epicId.trim().length === 0) {
|
|
154
154
|
return {
|
|
155
155
|
epicId: null,
|
|
156
|
+
tier: policy.tier,
|
|
156
157
|
capTokens,
|
|
157
158
|
measuredTokens: 0,
|
|
158
159
|
projectedTokens: 0,
|
|
@@ -189,6 +190,7 @@ export function checkWaveBudget(events, policy, proposed, options) {
|
|
|
189
190
|
const inFlight = inFlightTasks(events, epicId).length;
|
|
190
191
|
const base = {
|
|
191
192
|
epicId,
|
|
193
|
+
tier: policy.tier,
|
|
192
194
|
capTokens,
|
|
193
195
|
measuredTokens,
|
|
194
196
|
projectedTokens,
|