@juzser/blacksmith 0.2.0 → 0.4.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 +3 -0
- package/.claude/agents/grader.md +12 -5
- package/.claude/agents/reviewer.md +3 -0
- package/.claude/agents/security-reviewer.md +3 -0
- package/.claude/agents/spec-reviewer.md +3 -0
- package/.claude/agents/verifier.md +3 -0
- package/.claude/skills/bs/audit.md +25 -9
- package/.claude/skills/bs/dispatch.md +60 -7
- package/.claude/skills/bs/plan.md +33 -1
- package/.claude/skills/bs/run.md +18 -13
- package/.claude/skills/bs/wave.md +68 -10
- package/README.md +4 -4
- package/factory/orchestrator/dist/agentsSync.js +178 -0
- package/factory/orchestrator/dist/audit.js +134 -17
- package/factory/orchestrator/dist/autonomy.js +25 -0
- package/factory/orchestrator/dist/budgets.js +91 -2
- package/factory/orchestrator/dist/claims.js +8 -4
- package/factory/orchestrator/dist/cli.js +394 -26
- package/factory/orchestrator/dist/crossFinding.js +20 -0
- package/factory/orchestrator/dist/daemon.js +230 -41
- package/factory/orchestrator/dist/db/projector.js +93 -36
- package/factory/orchestrator/dist/db/queries.js +58 -35
- package/factory/orchestrator/dist/db/schema.js +24 -0
- package/factory/orchestrator/dist/dispatchAudit.js +10 -2
- package/factory/orchestrator/dist/dispatchLint.js +132 -0
- package/factory/orchestrator/dist/epic.js +192 -39
- package/factory/orchestrator/dist/epicWidth.js +25 -20
- package/factory/orchestrator/dist/errorIssues.js +209 -24
- package/factory/orchestrator/dist/eventOrder.js +102 -0
- package/factory/orchestrator/dist/events.js +73 -91
- package/factory/orchestrator/dist/findingContext.js +1 -13
- package/factory/orchestrator/dist/findings.js +132 -11
- package/factory/orchestrator/dist/gh.js +29 -6
- package/factory/orchestrator/dist/git.js +20 -2
- package/factory/orchestrator/dist/harness.js +92 -10
- package/factory/orchestrator/dist/hookDecision.js +320 -37
- package/factory/orchestrator/dist/impact.js +24 -2
- package/factory/orchestrator/dist/integration.js +11 -1
- package/factory/orchestrator/dist/issueReporter.js +415 -0
- package/factory/orchestrator/dist/judgeStopHook.js +181 -0
- package/factory/orchestrator/dist/judges.js +1 -1
- package/factory/orchestrator/dist/lessons.js +46 -18
- package/factory/orchestrator/dist/logCache.js +291 -0
- package/factory/orchestrator/dist/paths.js +10 -0
- package/factory/orchestrator/dist/plan.js +138 -4
- package/factory/orchestrator/dist/planQuorum.js +1 -1
- package/factory/orchestrator/dist/policy.js +57 -11
- package/factory/orchestrator/dist/projects.js +12 -1
- package/factory/orchestrator/dist/queue.js +477 -29
- package/factory/orchestrator/dist/scheduler.js +51 -0
- package/factory/orchestrator/dist/severity.js +40 -8
- package/factory/orchestrator/dist/spec.js +70 -0
- package/factory/orchestrator/dist/taskEvents.js +13 -4
- package/factory/orchestrator/dist/taskStatus.js +74 -0
- package/factory/orchestrator/dist/taxonomy.js +11 -0
- package/factory/orchestrator/dist/testgate.js +32 -3
- package/factory/orchestrator/dist/usage.js +42 -9
- package/factory/orchestrator/dist/waivers.js +60 -9
- package/factory/orchestrator/dist/waveConcurrency.js +29 -0
- package/factory/orchestrator/dist/waveNext.js +12 -10
- package/factory/orchestrator/dist/waveSchedule.js +84 -0
- package/factory/orchestrator/dist/worktree.js +19 -6
- package/factory/orchestrator/drizzle/0012_issue_reports_error_issues.sql +20 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +7 -0
- package/factory/policies/budgets.yml +9 -2
- package/factory/policies/crosscheck.yml +2 -2
- package/factory/policies/lessons.md +298 -0
- package/factory/policies/scheduler.yml +27 -2
- package/factory/policies/taxonomy.yml +10 -3
- package/factory/specs/roadmap.md +70 -23
- package/package.json +8 -8
|
@@ -14,22 +14,25 @@
|
|
|
14
14
|
// the same modules are fine and stay up here: tsc erases them, so they cost
|
|
15
15
|
// nothing at runtime. `test/cliBoot.test.ts` reads the built graph and fails if
|
|
16
16
|
// the database layer creeps back into it.
|
|
17
|
-
import { spawn } from 'node:child_process';
|
|
17
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
18
18
|
import { mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { pathToFileURL } from 'node:url';
|
|
21
|
+
import { resetAgentMaxTurns, syncAgentMaxTurns } from './agentsSync.js';
|
|
21
22
|
import { parseArgs } from './args.js';
|
|
22
23
|
import { closeAudit, consolidateAudit, cutAudit, decideAudit, openAudit, recordAudit, resolveAudit, } from './audit.js';
|
|
23
24
|
import { checkBudgetAlarm } from './budgetAlarm.js';
|
|
24
|
-
import { loadBudgetPolicy } from './budgets.js';
|
|
25
|
+
import { budgetEnvOverrides, loadBudgetPolicy } from './budgets.js';
|
|
25
26
|
import { collectCommittedChanges, loadWorktreePolicy, postRunCheck, readClaimList, validateWave, writeRootCheck, } from './claims.js';
|
|
26
27
|
import { collectCoverageEvidence } from './coverage.js';
|
|
27
28
|
import { loadCrosscheckPolicy } from './crosscheck.js';
|
|
28
29
|
import { independentFinderRequest, reconcile, runIndependentFinder, } from './crossFinding.js';
|
|
29
30
|
import { checkDelegationGrants, checkDelegationLog, loadDelegationPolicy } from './delegation.js';
|
|
30
31
|
import { checkDispatchAsymmetry } from './dispatchAudit.js';
|
|
32
|
+
import { lintDispatchPrompt } from './dispatchLint.js';
|
|
31
33
|
import { loadDotEnv } from './dotenv.js';
|
|
32
34
|
import { loadEffortPolicy, resolveEffort } from './effort.js';
|
|
35
|
+
import { ISSUE_CANDIDATE_EVENT_TYPES } from './errorIssues.js';
|
|
33
36
|
import { SmithError } from './errors.js';
|
|
34
37
|
import { checkEscalationLadder } from './escalation.js';
|
|
35
38
|
import { appendEvent, filterEvents, followEvents, listSessionIds, mergeSessionLogs, readEvents, readLineageEvents, requireSession, sessionTree, startSession, tailEvents, } from './events.js';
|
|
@@ -41,25 +44,28 @@ import { decideHookPayload } from './hookDecision.js';
|
|
|
41
44
|
import { checkWorktreeImmutable, fingerprintWorktree, } from './immutability.js';
|
|
42
45
|
import { collectExportDiffs, exportImpact, waveImpact } from './impact.js';
|
|
43
46
|
import { integrationHeadSha, runIntegrationCheck } from './integration.js';
|
|
47
|
+
import { previewOutcomes, reportErrors } from './issueReporter.js';
|
|
44
48
|
import { judgePreflight } from './judgePreflight.js';
|
|
45
49
|
import { outstandingJudges, readJudgeTurns, recordJudgeDispatch, recordJudgeReport, } from './judges.js';
|
|
46
50
|
import { addMcpSurface, resolveMcpSurface, runMcpCheck } from './mcp.js';
|
|
47
51
|
import { DOTENV_PATH, LESSONS_MD_PATH, lessonsReadPath, REPO_ROOT, SANDBOX_LEASE_DIR, STATE_DB_PATH, } from './paths.js';
|
|
48
|
-
import { diffPlans, livePlanTasks, resolveTaskId, validatePlan, } from './plan.js';
|
|
52
|
+
import { bareTaskId, diffPlans, livePlanTasks, planProjectResolverForTaskRef, planSelfFallbackForTaskRef, resolveTaskId, validatePlan, } from './plan.js';
|
|
49
53
|
import { runPlanQuorum } from './planQuorum.js';
|
|
50
54
|
import { detectCurrentBranch, detectRepoRoot, evaluateCommand, loadGuardrailPolicy, } from './policy.js';
|
|
55
|
+
import { factoryProjects } from './projects.js';
|
|
51
56
|
import { recordUserPrompt } from './prompts.js';
|
|
52
57
|
import { checkBrief, wrapIngested } from './provenance.js';
|
|
53
58
|
import { runJudge } from './providers/index.js';
|
|
54
|
-
import { admit, adopt, step } from './queue.js';
|
|
59
|
+
import { admit, adopt, batchStep, groupForBatch, step } from './queue.js';
|
|
55
60
|
import { stampResultEnvelope } from './results.js';
|
|
61
|
+
import { FACTORY_PROJECT, isErrorTrackerWritable, loadRoadmap } from './roadmap.js';
|
|
56
62
|
import { checkRuntime } from './runtime.js';
|
|
57
63
|
import { checkSameMistakeKpi } from './sameMistakeKpi.js';
|
|
58
64
|
import { activeSandboxFor, closeSandbox, listSandboxes, openSandbox, } from './sandbox.js';
|
|
59
65
|
import { registerProjectInRoadmap, scaffoldProject } from './scaffold.js';
|
|
60
66
|
import { loadSensitivePathsPolicy, securityTriggers, } from './security.js';
|
|
61
67
|
import { parseLessons } from './severity.js';
|
|
62
|
-
import { amendPlan, recordSpecReview } from './spec.js';
|
|
68
|
+
import { amendPlan, recordSpecReview, taskSuccessors } from './spec.js';
|
|
63
69
|
import { approveSpecChange, listSpecChanges, proposeSpecChange, rejectSpecChange, } from './specChange.js';
|
|
64
70
|
import { checkStack, loadStackAnswers } from './stack.js';
|
|
65
71
|
import { buildSymbolGraph, collectSources } from './symbols.js';
|
|
@@ -79,6 +85,29 @@ function printJson(value) {
|
|
|
79
85
|
function readJsonFile(filePath) {
|
|
80
86
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
81
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* `--checks <file>`'s intake, both places it's read (`gate run`,
|
|
90
|
+
* `integration check`). Same fail-closed shape as `boundedIntFlag`, moved
|
|
91
|
+
* from a flag to a JSON field: a check's own `timeout_ms` goes straight into
|
|
92
|
+
* a `setTimeout` call in testgate.ts, and a non-positive or non-integer value
|
|
93
|
+
* there does not fail loudly — it fails as either an instant, unexplained
|
|
94
|
+
* timeout or one that silently never fires. Above 2^31-1 Node clamps the
|
|
95
|
+
* delay to 1ms, so an oversized value is the instant kind too. Refused here,
|
|
96
|
+
* at the point the file is read, rather than let a gate run act on either.
|
|
97
|
+
*/
|
|
98
|
+
const MAX_CHECK_TIMEOUT_MS = 2_147_483_647;
|
|
99
|
+
function readChecksFile(filePath) {
|
|
100
|
+
const checks = readJsonFile(filePath);
|
|
101
|
+
for (const check of checks) {
|
|
102
|
+
const timeoutMs = check.timeout_ms;
|
|
103
|
+
if (timeoutMs === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_CHECK_TIMEOUT_MS) {
|
|
106
|
+
throw new SmithError('gate.invalid-check-timeout', `"${check.name}" in ${filePath} has timeout_ms ${JSON.stringify(timeoutMs)}; it must be a positive whole number of milliseconds, at most ${MAX_CHECK_TIMEOUT_MS}.`, { name: check.name, timeout_ms: timeoutMs, file: filePath });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return checks;
|
|
110
|
+
}
|
|
82
111
|
/** A task spec's `budget.tokens` when it is a usable positive number, else null. */
|
|
83
112
|
function declaredTokens(budget) {
|
|
84
113
|
if (typeof budget !== 'object' || budget === null)
|
|
@@ -298,7 +327,111 @@ function eventContextFromFlags(flags) {
|
|
|
298
327
|
function eventOptsFromFlags(flags) {
|
|
299
328
|
return flags['state-dir'] ? { stateDir: flags['state-dir'] } : {};
|
|
300
329
|
}
|
|
301
|
-
/**
|
|
330
|
+
/** The runner `issues report` hands to `reportErrors`: git and `gh` through one call, never a throw. */
|
|
331
|
+
function issueCommandRunner(cmd, args) {
|
|
332
|
+
try {
|
|
333
|
+
const stdout = execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
334
|
+
return { status: 0, stdout, stderr: '' };
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
const e = err;
|
|
338
|
+
return {
|
|
339
|
+
status: typeof e.status === 'number' ? e.status : null,
|
|
340
|
+
stdout: e.stdout ?? '',
|
|
341
|
+
stderr: e.stderr ?? '',
|
|
342
|
+
...(typeof e.code === 'string' ? { spawnError: e.code } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** `--epic`/`--since` narrow the CANDIDATES, never the `issue-reported` history dedup reads. */
|
|
347
|
+
function scopeIssueCandidates(events, flags) {
|
|
348
|
+
const epic = flags.epic;
|
|
349
|
+
const since = flags.since;
|
|
350
|
+
if (epic === undefined && since === undefined)
|
|
351
|
+
return events;
|
|
352
|
+
return events.filter(({ record }) => {
|
|
353
|
+
if (!ISSUE_CANDIDATE_EVENT_TYPES.has(record.event_type))
|
|
354
|
+
return true;
|
|
355
|
+
if (epic !== undefined && !(record.task_id ?? '').startsWith(`${epic}/`))
|
|
356
|
+
return false;
|
|
357
|
+
return since === undefined || record.ts >= since;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Both `issues` actions read the same inputs; only the entry point differs.
|
|
362
|
+
*
|
|
363
|
+
* `resolveProject` and `selfFallback` together close the privacy-leak gap an
|
|
364
|
+
* unstamped row otherwise falls into: a session or plan driving a project
|
|
365
|
+
* other than this factory writes error/gate/task-added rows with no
|
|
366
|
+
* top-level `project` stamp. Both answer from the row's own task ref's
|
|
367
|
+
* epic's plan file (D-246's `db/projector.ts` precedent, restated here
|
|
368
|
+
* without a DB dependency `cli.ts`'s boot graph cannot carry) -- a plan on
|
|
369
|
+
* disk naming a project wins outright via `resolveProject`; a plan on disk
|
|
370
|
+
* naming none is this checkout's own epic, so `selfFallback` resolves it to
|
|
371
|
+
* `FACTORY_PROJECT`; no plan found at all (a foreign epic, or a bare ref)
|
|
372
|
+
* stays `null` from both, and `reportErrors`/`previewOutcomes` skip that row
|
|
373
|
+
* with `skipped-unresolved-project` rather than ever defaulting it into this
|
|
374
|
+
* factory's own repository.
|
|
375
|
+
*
|
|
376
|
+
* The two are kept as SEPARATE resolvers, not one combined
|
|
377
|
+
* `planProjectResolverForTaskRefOrSelf`, so `withSessionFallback`
|
|
378
|
+
* (`errorIssues.ts`) can try them as distinct tiers with a session's own
|
|
379
|
+
* stamp riding between them: `resolveProject` first, then any `project`
|
|
380
|
+
* stamp elsewhere in the same session (or a bare ref's `task-added`
|
|
381
|
+
* `epic_id` re-asked of `resolveProject`), and only then `selfFallback`
|
|
382
|
+
* (S2-c) -- a session-mate's real stamp naming a different project must
|
|
383
|
+
* outrank this weakest, locally-guessed signal, which a single combined
|
|
384
|
+
* resolver tried as one first tier could not guarantee.
|
|
385
|
+
*
|
|
386
|
+
* `--specs-dir` (via `planOptsFromFlags`) and `--state-dir` (via
|
|
387
|
+
* `eventOptsFromFlags`) are two independent flags on purpose: `--state-dir`
|
|
388
|
+
* says where THIS session's own events live, `--specs-dir` says where the
|
|
389
|
+
* epics IT references keep their plans, and a session's events and the
|
|
390
|
+
* plans its tasks belong to are not always colocated (the daemon's own
|
|
391
|
+
* event log and a target checkout's specs tree, for one). Requiring them to
|
|
392
|
+
* travel together would make `issues report` unable to point one flag at a
|
|
393
|
+
* remote-events state dir while resolving projects from this checkout's own
|
|
394
|
+
* specs/active, which is `smith`'s ordinary daemon posture. Callers who do
|
|
395
|
+
* mean the same tree for both pass the same path to both flags.
|
|
396
|
+
*
|
|
397
|
+
* `lineageEvents` is the session's UNSCOPED lineage, read before
|
|
398
|
+
* `scopeIssueCandidates` narrows `events` to `--epic`/`--since` (S3):
|
|
399
|
+
* `ORIGIN_STAMP_EVENT_TYPES` overlaps the scoped `ISSUE_CANDIDATE_EVENT_TYPES`
|
|
400
|
+
* set, so `sessionProjectStamps` reading straight off the narrowed `events`
|
|
401
|
+
* could lose a session-mate's stamp purely because `--epic`/`--since`
|
|
402
|
+
* excluded that row, changing an unrelated in-scope row's resolved project
|
|
403
|
+
* -- and therefore its fingerprint -- based only on which scoping flags were
|
|
404
|
+
* passed. `reportErrors`/`previewOutcomes` fold `events` for candidates but
|
|
405
|
+
* consult `lineageEvents` for the session-stamp tier, so `--epic`/`--since`
|
|
406
|
+
* narrow what gets reported without perturbing how a row's project is
|
|
407
|
+
* resolved.
|
|
408
|
+
*/
|
|
409
|
+
async function issueInputs(flags) {
|
|
410
|
+
const sessionId = requireFlag(flags, 'session');
|
|
411
|
+
const eventOpts = eventOptsFromFlags(flags);
|
|
412
|
+
requireSession(sessionId, eventOpts);
|
|
413
|
+
const lineageEvents = await readLineageEvents(sessionId, eventOpts);
|
|
414
|
+
const events = scopeIssueCandidates(lineageEvents, flags);
|
|
415
|
+
const milestones = loadRoadmap(flags['roadmap-path']);
|
|
416
|
+
const isEnabled = (project) => isErrorTrackerWritable(milestones, project);
|
|
417
|
+
const planOpts = planOptsFromFlags(flags);
|
|
418
|
+
const resolveProject = planProjectResolverForTaskRef(planOpts);
|
|
419
|
+
const selfFallback = planSelfFallbackForTaskRef(FACTORY_PROJECT, planOpts);
|
|
420
|
+
return {
|
|
421
|
+
events,
|
|
422
|
+
isEnabled,
|
|
423
|
+
register: factoryProjects(),
|
|
424
|
+
eventOpts,
|
|
425
|
+
resolveProject,
|
|
426
|
+
selfFallback,
|
|
427
|
+
lineageEvents,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Where plan version files are read from and written to; defaults to
|
|
432
|
+
* factory/specs/active. See `issueInputs`'s doc comment for why this
|
|
433
|
+
* deliberately does not travel with `--state-dir`.
|
|
434
|
+
*/
|
|
302
435
|
function planOptsFromFlags(flags) {
|
|
303
436
|
return flags['specs-dir'] ? { specsDir: flags['specs-dir'] } : {};
|
|
304
437
|
}
|
|
@@ -687,8 +820,19 @@ async function nextWaveInputFrom(planFile, flags) {
|
|
|
687
820
|
? await readAddedTasks({ sessionId: flags.session }, eventOptsFromFlags(flags))
|
|
688
821
|
: [];
|
|
689
822
|
const planIds = new Set(planOnDisk.tasks.map((t) => t.task_id));
|
|
823
|
+
// A lineage is not one epic. `readAddedTasks` reads the WHOLE lineage
|
|
824
|
+
// (D-119), so a resumed session that ever ran a different, earlier epic
|
|
825
|
+
// carries that epic's `task-added` rows too — and a row an operator
|
|
826
|
+
// abandoned non-terminal (never merged, waived, or superseded) reads back
|
|
827
|
+
// here exactly like a genuine follow-up of THIS plan. Unfiltered, it was
|
|
828
|
+
// spliced into `plan.tasks` below and reached `computeNextWave` as a
|
|
829
|
+
// candidate or an occupied claim-holder: proposed as this epic's own work,
|
|
830
|
+
// and able to defer this epic's tasks on a claim overlap that has nothing
|
|
831
|
+
// to do with them. `wave check` never has this problem because it only
|
|
832
|
+
// ever looks at the ids an operator names — matching that, a follow-up
|
|
833
|
+
// only counts here when its own `task-added` named this plan's epic.
|
|
690
834
|
const followUps = logged
|
|
691
|
-
.filter((t) => !planIds.has(t.taskId))
|
|
835
|
+
.filter((t) => !planIds.has(t.taskId) && t.epicId === planOnDisk.epic_id)
|
|
692
836
|
.map((t) => ({
|
|
693
837
|
task_id: t.taskId,
|
|
694
838
|
// The log is the only register that holds this task's status, so a
|
|
@@ -853,7 +997,20 @@ async function main() {
|
|
|
853
997
|
}
|
|
854
998
|
if (namespace === 'audit' && action === 'resolve') {
|
|
855
999
|
const [projectDir] = requirePositionals(positional, usageFor('audit resolve'));
|
|
856
|
-
|
|
1000
|
+
// Comma-split, not repeated: a fingerprint has no commas, and deferring
|
|
1001
|
+
// more than one at a time is the common case (D-33's precedent, `plan
|
|
1002
|
+
// amend --findings`).
|
|
1003
|
+
const except = flags.except
|
|
1004
|
+
? flags.except
|
|
1005
|
+
.split(',')
|
|
1006
|
+
.map((fp) => fp.trim())
|
|
1007
|
+
.filter((fp) => fp !== '')
|
|
1008
|
+
: undefined;
|
|
1009
|
+
printJson(await resolveAudit(projectDir, requireFlag(flags, 'epic'), eventContextFromFlags(flags), eventOptsFromFlags(flags), {
|
|
1010
|
+
...(except ? { except } : {}),
|
|
1011
|
+
...(flags.plan ? { plan: readJsonFile(flags.plan) } : {}),
|
|
1012
|
+
planOpts: planOptsFromFlags(flags),
|
|
1013
|
+
}));
|
|
857
1014
|
return 0;
|
|
858
1015
|
}
|
|
859
1016
|
if (namespace === 'audit' && action === 'close') {
|
|
@@ -959,6 +1116,14 @@ async function main() {
|
|
|
959
1116
|
// what went unnoticed.
|
|
960
1117
|
const edges = await emitEdgesRecorded(plan, ctx, opts);
|
|
961
1118
|
const added = written.filter((e) => e.record.event_type === 'task-added').length;
|
|
1119
|
+
// Criterion 7: a plan that serializes by claim geometry alone used to be
|
|
1120
|
+
// invisible until `/bs run` actually hit the ceiling. Dynamic for the
|
|
1121
|
+
// same reason `wave schedule` imports it dynamically -- nothing an
|
|
1122
|
+
// ingest that never schedules needs should be on the boot path every
|
|
1123
|
+
// `smith --help` walks. No repo scan here: `parallel_with` is
|
|
1124
|
+
// claims-and-edges only, so the input carries no crossings.
|
|
1125
|
+
const { computePlanParallelism } = await import('./waveSchedule.js');
|
|
1126
|
+
const parallelism = computePlanParallelism(plan, loadWorktreePolicy());
|
|
962
1127
|
printJson({
|
|
963
1128
|
epic: plan.epic_id,
|
|
964
1129
|
version: plan.version,
|
|
@@ -966,6 +1131,7 @@ async function main() {
|
|
|
966
1131
|
superseded: written.length - added,
|
|
967
1132
|
skipped: plan.tasks.length - added,
|
|
968
1133
|
edges: edges.length,
|
|
1134
|
+
parallelism,
|
|
969
1135
|
});
|
|
970
1136
|
return 0;
|
|
971
1137
|
}
|
|
@@ -1197,7 +1363,15 @@ async function main() {
|
|
|
1197
1363
|
if (result.valid && !coupled && !blocked && flags.dry !== 'true') {
|
|
1198
1364
|
await emitWaveAdmitted(plan, taskIds, eventContextFromFlags(flags), eventOptsFromFlags(flags), admissionBudget(budget, overridden ? rationale : undefined));
|
|
1199
1365
|
}
|
|
1200
|
-
|
|
1366
|
+
// Names only, and only when some differ from budgets.yml: which caps came
|
|
1367
|
+
// from the box's env rather than the file.
|
|
1368
|
+
const envOverrides = budgetEnvOverrides(loadBudgetPolicy(flags['budget-policy'], {}));
|
|
1369
|
+
printJson({
|
|
1370
|
+
...result,
|
|
1371
|
+
symbolImpact,
|
|
1372
|
+
budget,
|
|
1373
|
+
...(envOverrides.length > 0 ? { budgetEnvOverrides: envOverrides } : {}),
|
|
1374
|
+
});
|
|
1201
1375
|
return result.valid && !coupled && !blocked ? 0 : 1;
|
|
1202
1376
|
}
|
|
1203
1377
|
if (namespace === 'wave' && action === 'next') {
|
|
@@ -1227,9 +1401,13 @@ async function main() {
|
|
|
1227
1401
|
// Static would be fine — waveSchedule.js reaches nothing waveNext.js has
|
|
1228
1402
|
// not already put on the boot path — but the input assembly it is handed
|
|
1229
1403
|
// may dynamically import `db/projector.js`, so the await is here anyway.
|
|
1230
|
-
const { scheduleWaves } = await import('./waveSchedule.js');
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1404
|
+
const { computePlanParallelism, scheduleWaves } = await import('./waveSchedule.js');
|
|
1405
|
+
const input = await nextWaveInputFrom(planFile, flags);
|
|
1406
|
+
const schedule = scheduleWaves(input);
|
|
1407
|
+
// Same `size` block `plan ingest` prints under `parallelism`, so the
|
|
1408
|
+
// operator sees the small-epic bound from either command.
|
|
1409
|
+
const { size } = computePlanParallelism(input.plan, input.policy);
|
|
1410
|
+
printJson({ ...schedule, size });
|
|
1233
1411
|
// Writes nothing, for the reason `wave next` writes nothing and one more:
|
|
1234
1412
|
// every round after the first is a simulation. The tasks it marks complete
|
|
1235
1413
|
// were completed by nobody, and a log that recorded them would be claiming
|
|
@@ -1348,7 +1526,21 @@ async function main() {
|
|
|
1348
1526
|
const projectDirs = resolveProjectDirs(repeated.project, {
|
|
1349
1527
|
self: flags['no-self'] !== 'true',
|
|
1350
1528
|
});
|
|
1351
|
-
|
|
1529
|
+
// Same resolver `issues report`/`preview` and the daemon use (D-246): an
|
|
1530
|
+
// error-report proposal's fingerprint is hashed over its `project`
|
|
1531
|
+
// (computeFingerprint), so this verb has to resolve a row's project the
|
|
1532
|
+
// same way the reporter that later clears the proposal does. A divergent
|
|
1533
|
+
// resolver here -- or none, which folds every row to `project: null` --
|
|
1534
|
+
// hashes a different fingerprint than the reporter's `issue-reported`
|
|
1535
|
+
// event names, so the proposal `scheduler run` keeps proposing never
|
|
1536
|
+
// matches the one already answered and is re-appended forever (S1-b).
|
|
1537
|
+
const input = {
|
|
1538
|
+
events,
|
|
1539
|
+
now,
|
|
1540
|
+
projectDirs,
|
|
1541
|
+
resolveProjectForTaskRef: planProjectResolverForTaskRef(planOptsFromFlags(flags)),
|
|
1542
|
+
selfFallbackForTaskRef: planSelfFallbackForTaskRef(FACTORY_PROJECT, planOptsFromFlags(flags)),
|
|
1543
|
+
};
|
|
1352
1544
|
if (dry) {
|
|
1353
1545
|
printJson({ proposals: computeProposals(input) });
|
|
1354
1546
|
return 0;
|
|
@@ -1395,11 +1587,17 @@ async function main() {
|
|
|
1395
1587
|
// This clone joins the pass by default (contract clause 1); `--no-self`
|
|
1396
1588
|
// is the refusal path.
|
|
1397
1589
|
const { resolveProjectDirs } = await import('./projects.js');
|
|
1590
|
+
// Same resolver as `scheduler run` above, for the same reason (S1-b): a
|
|
1591
|
+
// divergent or absent resolver hashes a different fingerprint than the
|
|
1592
|
+
// reporter's `issue-reported` event names, so an already-answered
|
|
1593
|
+
// error-report proposal never clears.
|
|
1398
1594
|
const proposals = computeProposals({
|
|
1399
1595
|
events,
|
|
1400
1596
|
now,
|
|
1401
1597
|
policy,
|
|
1402
1598
|
projectDirs: resolveProjectDirs(repeated.project, { self: flags['no-self'] !== 'true' }),
|
|
1599
|
+
resolveProjectForTaskRef: planProjectResolverForTaskRef(planOptsFromFlags(flags)),
|
|
1600
|
+
selfFallbackForTaskRef: planSelfFallbackForTaskRef(FACTORY_PROJECT, planOptsFromFlags(flags)),
|
|
1403
1601
|
});
|
|
1404
1602
|
// A RecheckProposal names a task and no paths, so without this the
|
|
1405
1603
|
// security match would see an opaque id and clear a recheck of
|
|
@@ -1448,6 +1646,15 @@ async function main() {
|
|
|
1448
1646
|
...eventOptsFromFlags(flags),
|
|
1449
1647
|
projectDirs: resolveProjectDirs(repeated.project, { self }),
|
|
1450
1648
|
readProjects: () => factoryProjects().filter((ref) => self || !ref.self),
|
|
1649
|
+
// Same D-246 self-fallback as issueInputs (see its doc comment): a
|
|
1650
|
+
// plan on disk naming no project is this checkout's own epic, so it
|
|
1651
|
+
// reads as FACTORY_PROJECT rather than staying an unresolved null the
|
|
1652
|
+
// daemon would report as an unlabeled finding and the on/off switch
|
|
1653
|
+
// for 'black-smith' would never get asked about. Split into strict
|
|
1654
|
+
// (resolveProjectForTaskRef) and self-only (selfFallbackForTaskRef)
|
|
1655
|
+
// tiers so a session's own stamp can ride between them (S2-c).
|
|
1656
|
+
resolveProjectForTaskRef: planProjectResolverForTaskRef(planOptsFromFlags(flags)),
|
|
1657
|
+
selfFallbackForTaskRef: planSelfFallbackForTaskRef(FACTORY_PROJECT, planOptsFromFlags(flags)),
|
|
1451
1658
|
...(flags.db ? { dbPath: flags.db } : {}),
|
|
1452
1659
|
...(flags['no-db'] === 'true' ? { projectDb: false } : {}),
|
|
1453
1660
|
};
|
|
@@ -1583,6 +1790,25 @@ async function main() {
|
|
|
1583
1790
|
}
|
|
1584
1791
|
if (namespace === 'worktree' && action === 'create') {
|
|
1585
1792
|
const [projectDir, epic, taskId] = requirePositionals(positional, usageFor('worktree create'));
|
|
1793
|
+
if (flags.from !== undefined) {
|
|
1794
|
+
// `--from` reuses a predecessor's commits, so it may only be spent on a
|
|
1795
|
+
// task the log actually names as that predecessor's successor — never
|
|
1796
|
+
// on a bare "I want the same branch history" request. `--session` is
|
|
1797
|
+
// what supplies the log to check against; without it there is nothing
|
|
1798
|
+
// to refuse against, so demand it explicitly rather than silently
|
|
1799
|
+
// trusting an unauthenticated `--from`.
|
|
1800
|
+
const sessionId = requireFlag(flags, 'session');
|
|
1801
|
+
const events = await readLineageEvents(sessionId, eventOptsFromFlags(flags));
|
|
1802
|
+
const successors = taskSuccessors(events, epic);
|
|
1803
|
+
const from = flags.from;
|
|
1804
|
+
const expected = bareTaskId(epic, taskId);
|
|
1805
|
+
const actual = successors.get(from) ?? successors.get(`${epic}/${from}`);
|
|
1806
|
+
if (actual === undefined || bareTaskId(epic, actual) !== expected) {
|
|
1807
|
+
throw new SmithError('worktree.not-a-successor', `${epic}/${taskId} is not logged as the successor of ${epic}/${from}; run \`plan propose\`/\`plan approve\` with a supersede pairing them first.`, { epic, taskId, from });
|
|
1808
|
+
}
|
|
1809
|
+
printJson(createTaskWorktree(projectDir, epic, taskId, { from }));
|
|
1810
|
+
return 0;
|
|
1811
|
+
}
|
|
1586
1812
|
printJson(createTaskWorktree(projectDir, epic, taskId));
|
|
1587
1813
|
return 0;
|
|
1588
1814
|
}
|
|
@@ -1699,6 +1925,28 @@ async function main() {
|
|
|
1699
1925
|
if (events && !flags.plan) {
|
|
1700
1926
|
throw new SmithError('cli.missing-flag', 'queue run --session also needs --plan <plan.json>: a merge may only be logged under the task id the plan declares.', { epic });
|
|
1701
1927
|
}
|
|
1928
|
+
// --batch (roadmap merge-lanes) groups tasks with groupForBatch before
|
|
1929
|
+
// any of them run, and that grouping needs two things `--tasks` alone
|
|
1930
|
+
// does not carry: the plan's `edges`, so a task never stacks with
|
|
1931
|
+
// something it depends_on, and — for a tasks-file entry that omits its
|
|
1932
|
+
// own `claims` — the plan's claim list, so a hand-typed batch run is not
|
|
1933
|
+
// blind to a disjointness `wave admit` already knows. Same shape as the
|
|
1934
|
+
// `--session` guard just above, and for the same reason: refuse the run
|
|
1935
|
+
// whole rather than group some tasks correctly and others by guesswork.
|
|
1936
|
+
if (flags.batch === 'true' && !flags.plan) {
|
|
1937
|
+
throw new SmithError('cli.missing-flag', 'queue run --batch also needs --plan <plan.json>: grouping reads its dependency edges and claim lists from there.', { epic });
|
|
1938
|
+
}
|
|
1939
|
+
// `attemptCandidate` runs the epic's test command directly against the
|
|
1940
|
+
// whole batch candidate — there is no per-task file set to narrow it to,
|
|
1941
|
+
// so a `--select-test-cmd` template would either render nonsensically or
|
|
1942
|
+
// silently run the full suite while the outcome claimed it was selected
|
|
1943
|
+
// (the same lie D-260 refuses above). Refuse the combination instead of
|
|
1944
|
+
// ignoring the flag.
|
|
1945
|
+
if (flags.batch === 'true' && selectTestCmd !== undefined) {
|
|
1946
|
+
throw new SmithError('cli.incompatible-flags', 'queue run --batch does not support --select-test-cmd: a batch candidate has no single task to narrow the test command to.', { epic });
|
|
1947
|
+
}
|
|
1948
|
+
let edges = [];
|
|
1949
|
+
let claimsById = new Map();
|
|
1702
1950
|
if (flags.plan) {
|
|
1703
1951
|
const plan = readJsonFile(flags.plan);
|
|
1704
1952
|
// …and the plan is not the only register: a follow-up minted by
|
|
@@ -1710,6 +1958,11 @@ async function main() {
|
|
|
1710
1958
|
const loggedIds = logged.map((t) => t.taskId);
|
|
1711
1959
|
for (const task of tasks)
|
|
1712
1960
|
task.taskId = resolveTaskId(plan, task.taskId, loggedIds);
|
|
1961
|
+
edges = plan.edges.map((e) => ({ task: e.task, dependsOn: e.dependsOn }));
|
|
1962
|
+
claimsById = new Map(plan.tasks.map((t) => [
|
|
1963
|
+
t.task_id,
|
|
1964
|
+
Array.isArray(t.claims) ? t.claims.filter((c) => typeof c === 'string') : [],
|
|
1965
|
+
]));
|
|
1713
1966
|
// D-186: the ids are the plan's now, so the order can be too. `--tasks`
|
|
1714
1967
|
// is hand-written, and merging in the order someone typed lets a task
|
|
1715
1968
|
// land before the task it declares `depends_on` — the epic's cumulative
|
|
@@ -1717,21 +1970,81 @@ async function main() {
|
|
|
1717
1970
|
// and blames the wrong branch when it goes red. `admit()` exists for
|
|
1718
1971
|
// exactly this and had no caller until here; a follow-up the plan never
|
|
1719
1972
|
// declared carries no edges, so it just sorts by id among its peers.
|
|
1720
|
-
const order = admit(tasks.map((t) => ({ task_id: t.taskId })),
|
|
1973
|
+
const order = admit(tasks.map((t) => ({ task_id: t.taskId })), edges);
|
|
1721
1974
|
// Stable: two tasks the plan does not order keep the order admit gave
|
|
1722
1975
|
// them, and a duplicated id is still run twice rather than dropped.
|
|
1723
1976
|
tasks.sort((a, b) => order.indexOf(a.taskId) - order.indexOf(b.taskId));
|
|
1724
1977
|
}
|
|
1978
|
+
const stepOpts = {
|
|
1979
|
+
projectDir,
|
|
1980
|
+
epic,
|
|
1981
|
+
testCmd,
|
|
1982
|
+
...(selectTestCmd !== undefined ? { selectTestCmd } : {}),
|
|
1983
|
+
...(events ? { events } : {}),
|
|
1984
|
+
};
|
|
1725
1985
|
const outcomes = [];
|
|
1726
1986
|
let allMerged = true;
|
|
1987
|
+
if (flags.batch === 'true') {
|
|
1988
|
+
const byId = new Map(tasks.map((t) => [t.taskId, t]));
|
|
1989
|
+
// A tasks-file entry's own `claims` wins over the plan's — the plan
|
|
1990
|
+
// describes what the task was authored to touch, the tasks file is
|
|
1991
|
+
// what actually ran, and the two can disagree the same way a diff can
|
|
1992
|
+
// outgrow the claim it was cut from.
|
|
1993
|
+
const groupable = tasks.map((t) => ({
|
|
1994
|
+
taskId: t.taskId,
|
|
1995
|
+
claims: t.claims ?? claimsById.get(t.taskId),
|
|
1996
|
+
}));
|
|
1997
|
+
const groups = groupForBatch(groupable, edges, loadWorktreePolicy().serializeAlwaysGlobs);
|
|
1998
|
+
const batches = [];
|
|
1999
|
+
for (const group of groups) {
|
|
2000
|
+
const groupTasks = group.map((id) => {
|
|
2001
|
+
const t = byId.get(id);
|
|
2002
|
+
if (!t) {
|
|
2003
|
+
// groupForBatch only ever returns ids it was handed, so this
|
|
2004
|
+
// would mean the group and --tasks have drifted apart — a wrong
|
|
2005
|
+
// candidate is worse than a refusal.
|
|
2006
|
+
throw new SmithError('queue.batch-unknown-task', `--batch grouped task "${id}" but no task by that id was in --tasks.`, { epic, taskId: id });
|
|
2007
|
+
}
|
|
2008
|
+
return t;
|
|
2009
|
+
});
|
|
2010
|
+
// groupForBatch's own contract (see its doc comment): a group of one
|
|
2011
|
+
// is not a batch, it's a task that closed the open group before it —
|
|
2012
|
+
// no candidate to stack, so it runs through the plain step it would
|
|
2013
|
+
// have taken without --batch at all, rather than through batchStep's
|
|
2014
|
+
// fold/candidate-worktree machinery for a "stack" of one.
|
|
2015
|
+
if (groupTasks.length === 1) {
|
|
2016
|
+
const [only] = groupTasks;
|
|
2017
|
+
const outcome = await step(only, stepOpts);
|
|
2018
|
+
outcomes.push(outcome);
|
|
2019
|
+
const landed = outcome.outcome === 'merged';
|
|
2020
|
+
// `step` refuses before any test runs on `nothing-to-merge` (D-30's
|
|
2021
|
+
// uncommitted-work guard) and `rebase-conflict` — 0 suites ran, not
|
|
2022
|
+
// 1, for either. Every other outcome it can return (`merged`,
|
|
2023
|
+
// `tests-failed`, `integration-dirty`) only happens after
|
|
2024
|
+
// `runTestCmd`.
|
|
2025
|
+
const suiteRuns = outcome.outcome === 'nothing-to-merge' || outcome.outcome === 'rebase-conflict' ? 0 : 1;
|
|
2026
|
+
batches.push({ task_ids: group, suite_runs: suiteRuns, landed });
|
|
2027
|
+
if (!landed) {
|
|
2028
|
+
allMerged = false;
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
else {
|
|
2033
|
+
const result = await batchStep(groupTasks, stepOpts);
|
|
2034
|
+
outcomes.push(...result.outcomes);
|
|
2035
|
+
const landed = result.outcomes.every((o) => o.outcome === 'merged');
|
|
2036
|
+
batches.push({ task_ids: group, suite_runs: result.suiteRuns, landed });
|
|
2037
|
+
if (!landed) {
|
|
2038
|
+
allMerged = false;
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
printJson({ outcomes, batches });
|
|
2044
|
+
return allMerged ? 0 : 1;
|
|
2045
|
+
}
|
|
1727
2046
|
for (const task of tasks) {
|
|
1728
|
-
const outcome = await step(task,
|
|
1729
|
-
projectDir,
|
|
1730
|
-
epic,
|
|
1731
|
-
testCmd,
|
|
1732
|
-
...(selectTestCmd !== undefined ? { selectTestCmd } : {}),
|
|
1733
|
-
...(events ? { events } : {}),
|
|
1734
|
-
});
|
|
2047
|
+
const outcome = await step(task, stepOpts);
|
|
1735
2048
|
outcomes.push(outcome);
|
|
1736
2049
|
if (outcome.outcome !== 'merged') {
|
|
1737
2050
|
allMerged = false;
|
|
@@ -1960,6 +2273,27 @@ async function main() {
|
|
|
1960
2273
|
printJson(report);
|
|
1961
2274
|
return report.ok ? 0 : 1;
|
|
1962
2275
|
}
|
|
2276
|
+
if (namespace === 'dispatch' && action === 'lint') {
|
|
2277
|
+
// dispatch.md "Carry into the prompt" / "Declare each judge's artifact":
|
|
2278
|
+
// catches a stated turn budget that disagrees with the template's
|
|
2279
|
+
// `maxTurns` in either direction, a template it cannot read at all, and a
|
|
2280
|
+
// judge prompt missing (or mismatching) its declared-artifact line,
|
|
2281
|
+
// before the agent ever runs. Fails closed: any applicable status but
|
|
2282
|
+
// `ok` exits 1. See dispatchLint.ts for why each status means what it
|
|
2283
|
+
// means. `-` reads stdin, same convention as `prompt wrap`/`prompt record`.
|
|
2284
|
+
const [file] = requirePositionals(positional, usageFor('dispatch lint'), 1);
|
|
2285
|
+
const prompt = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8');
|
|
2286
|
+
const report = await lintDispatchPrompt({
|
|
2287
|
+
prompt,
|
|
2288
|
+
role: requireFlag(flags, 'role'),
|
|
2289
|
+
taskId: requireFlag(flags, 'task'),
|
|
2290
|
+
sessionId: requireFlag(flags, 'session'),
|
|
2291
|
+
...(flags['agents-dir'] ? { agentsDir: flags['agents-dir'] } : {}),
|
|
2292
|
+
eventOpts: eventOptsFromFlags(flags),
|
|
2293
|
+
});
|
|
2294
|
+
printJson(report);
|
|
2295
|
+
return report.exitCode;
|
|
2296
|
+
}
|
|
1963
2297
|
if (namespace === 'tester' && action === 'check') {
|
|
1964
2298
|
// The other half of `dispatch check`: that one asks whether the critic ran
|
|
1965
2299
|
// on the finder's model, this one asks whether a tester ran at all, in a
|
|
@@ -2087,7 +2421,10 @@ async function main() {
|
|
|
2087
2421
|
const spec = readJsonFile(specFile);
|
|
2088
2422
|
// The worktree is a full checkout, so it is both halves of the question:
|
|
2089
2423
|
// the diff this task committed, and everyone in the repo who imports it.
|
|
2090
|
-
|
|
2424
|
+
// The promises go in too: a promised file the collector cannot read is
|
|
2425
|
+
// reported unverified rather than dropped, which is the only way absent
|
|
2426
|
+
// from the diff keeps meaning untouched.
|
|
2427
|
+
const diffs = collectExportDiffs(worktreeDir, collectCommittedChanges(worktreeDir), spec.keeps_exports ?? []);
|
|
2091
2428
|
const graph = buildSymbolGraph(collectSources(worktreeDir));
|
|
2092
2429
|
const report = exportImpact(graph, diffs, spec.claims, spec.keeps_exports ?? []);
|
|
2093
2430
|
printJson(report);
|
|
@@ -2121,6 +2458,24 @@ async function main() {
|
|
|
2121
2458
|
printJson(result);
|
|
2122
2459
|
return 0;
|
|
2123
2460
|
}
|
|
2461
|
+
if (namespace === 'issues' && action === 'report') {
|
|
2462
|
+
// The one path in this epic that runs `gh` for real.
|
|
2463
|
+
const { events, isEnabled, register, eventOpts, resolveProject, selfFallback, lineageEvents } = await issueInputs(flags);
|
|
2464
|
+
const clock = () => new Date().toISOString();
|
|
2465
|
+
printJson(await reportErrors(events, isEnabled, register, issueCommandRunner, clock, eventOpts, resolveProject, selfFallback, lineageEvents));
|
|
2466
|
+
return 0;
|
|
2467
|
+
}
|
|
2468
|
+
if (namespace === 'issues' && action === 'preview') {
|
|
2469
|
+
// Same inputs, no `gh` and no event: the runner throws if anything
|
|
2470
|
+
// reaches it, so a preview that spawned would fail loudly, not quietly.
|
|
2471
|
+
const { events, isEnabled, register, resolveProject, selfFallback, lineageEvents } = await issueInputs(flags);
|
|
2472
|
+
const neverRun = (cmd) => {
|
|
2473
|
+
throw new Error(`issues preview must never run a command, asked for ${cmd}`);
|
|
2474
|
+
};
|
|
2475
|
+
const clock = () => new Date().toISOString();
|
|
2476
|
+
printJson(await previewOutcomes(events, isEnabled, register, neverRun, clock, resolveProject, selfFallback, lineageEvents));
|
|
2477
|
+
return 0;
|
|
2478
|
+
}
|
|
2124
2479
|
// The judge sandbox (A3). `worktree fingerprint`/`verify` above detect a
|
|
2125
2480
|
// judge that wrote; these three refuse it up front. The orchestrator opens a
|
|
2126
2481
|
// lease before handing a worktree to a judge and closes it when the verdict
|
|
@@ -2185,6 +2540,18 @@ async function main() {
|
|
|
2185
2540
|
printJson(summarizeHarnesses({ policy: loadHarnessPolicy(flags.policy) }));
|
|
2186
2541
|
return 0;
|
|
2187
2542
|
}
|
|
2543
|
+
// A turn cap is per role (the template's frontmatter), never per task, and
|
|
2544
|
+
// Claude Code reads it at spawn — so the box-level override has to land in
|
|
2545
|
+
// the file. The edit is local and left uncommitted on purpose; `--reset`
|
|
2546
|
+
// puts back what git HEAD says.
|
|
2547
|
+
if (namespace === 'agents' && action === 'sync') {
|
|
2548
|
+
const agentsDir = flags['agents-dir'];
|
|
2549
|
+
const dryRun = flags['dry-run'] === 'true';
|
|
2550
|
+
printJson(flags.reset === 'true'
|
|
2551
|
+
? resetAgentMaxTurns({ ...(agentsDir ? { agentsDir } : {}), dryRun })
|
|
2552
|
+
: syncAgentMaxTurns({ ...(agentsDir ? { agentsDir } : {}), dryRun }));
|
|
2553
|
+
return 0;
|
|
2554
|
+
}
|
|
2188
2555
|
if (namespace === 'harness' && action === 'plan') {
|
|
2189
2556
|
const tier = flags.tier;
|
|
2190
2557
|
const invocation = planWorkerTurn({
|
|
@@ -2281,7 +2648,7 @@ async function main() {
|
|
|
2281
2648
|
const { runGate } = await import('./gate.js');
|
|
2282
2649
|
const [taskId] = requirePositionals(positional, usageFor('gate run'));
|
|
2283
2650
|
const worktreeDir = requireFlag(flags, 'worktree');
|
|
2284
|
-
const checks =
|
|
2651
|
+
const checks = readChecksFile(requireFlag(flags, 'checks'));
|
|
2285
2652
|
// The result file has the same two intake shapes as findings below, and for
|
|
2286
2653
|
// the same reason. With `--agent`, `--result` is the worker's half —
|
|
2287
2654
|
// run_status/structured_output/artifacts — and the dispatcher stamps the
|
|
@@ -2337,7 +2704,7 @@ async function main() {
|
|
|
2337
2704
|
// close per role: a judge that splits its findings across two files still
|
|
2338
2705
|
// owes one turn, and a second report against it would be a duplicate.
|
|
2339
2706
|
//
|
|
2340
|
-
// FD-1
|
|
2707
|
+
// FD-1: `--grader` is the same hand-over for the grader. Its
|
|
2341
2708
|
// verdict document is not a findings list, so until `judge report` learned
|
|
2342
2709
|
// the shape the grader's turn stayed open with its verdict on the command
|
|
2343
2710
|
// line, and the gate blocked on the judge it was about to read.
|
|
@@ -2449,7 +2816,8 @@ async function main() {
|
|
|
2449
2816
|
sessionId,
|
|
2450
2817
|
...(flags.epic ? { epicId: flags.epic } : {}),
|
|
2451
2818
|
});
|
|
2452
|
-
|
|
2819
|
+
const envOverrides = budgetEnvOverrides(loadBudgetPolicy(flags.policy, {}));
|
|
2820
|
+
printJson(envOverrides.length > 0 ? { ...report, budgetEnvOverrides: envOverrides } : report);
|
|
2453
2821
|
return report.ok ? 0 : 1;
|
|
2454
2822
|
}
|
|
2455
2823
|
// The consumer architecture §9.7's "monotonically decreasing same-mistake
|
|
@@ -2477,7 +2845,7 @@ async function main() {
|
|
|
2477
2845
|
if (namespace === 'integration' && action === 'check') {
|
|
2478
2846
|
const epicId = requireFlag(flags, 'epic');
|
|
2479
2847
|
const projectDir = requireFlag(flags, 'project');
|
|
2480
|
-
const checks =
|
|
2848
|
+
const checks = readChecksFile(requireFlag(flags, 'checks'));
|
|
2481
2849
|
const ctx = eventContextFromFlags(flags);
|
|
2482
2850
|
const record = await runIntegrationCheck({
|
|
2483
2851
|
epicId,
|
|
@@ -123,6 +123,26 @@ export function reconcile(input) {
|
|
|
123
123
|
if (!nativeByFingerprint.has(finding.fingerprint)) {
|
|
124
124
|
nativeByFingerprint.set(finding.fingerprint, finding);
|
|
125
125
|
}
|
|
126
|
+
// Also key by the fingerprint computeFingerprint() would produce for this
|
|
127
|
+
// record TODAY, not just the one stored at raise time. `input.native`
|
|
128
|
+
// comes straight out of listFindings() (a lineage-wide, possibly
|
|
129
|
+
// long-lived read) while `group.fingerprint` below is always computed
|
|
130
|
+
// fresh from the independent provider's evidence -- so a native finding
|
|
131
|
+
// raised before issue #178 widened the normalizer (its summary containing
|
|
132
|
+
// a line span the old algorithm did not strip) would otherwise never
|
|
133
|
+
// match an independent finder's evidence for the identical bug, and get
|
|
134
|
+
// reported as `co-located`/`independent-only` plus a duplicate
|
|
135
|
+
// `native-only` instead of `corroborated`.
|
|
136
|
+
if (finding.file_path !== undefined) {
|
|
137
|
+
const recomputed = computeFingerprint({
|
|
138
|
+
filePath: finding.file_path,
|
|
139
|
+
category: finding.finding_category,
|
|
140
|
+
summary: finding.summary,
|
|
141
|
+
});
|
|
142
|
+
if (!nativeByFingerprint.has(recomputed)) {
|
|
143
|
+
nativeByFingerprint.set(recomputed, finding);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
126
146
|
if (finding.file_path === undefined)
|
|
127
147
|
continue;
|
|
128
148
|
const key = coLocationKey(finding.file_path, finding.finding_category);
|