@rsaglobaltech/specgate 0.27.5 → 0.27.8
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/CHANGELOG.md +39 -0
- package/dist/packages/core/src/domain/DraftChecklist.js +43 -12
- package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessReport.js +46 -10
- package/dist/packages/core/src/domain/HarnessReport.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessRun.js.map +1 -1
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
- package/dist/scripts/cli/commands/config/ConfigSetCommand.js +22 -4
- package/dist/scripts/cli/commands/config/ConfigSetCommand.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +200 -25
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/QualityCommand.js +3 -0
- package/dist/scripts/cli/commands/quality/QualityCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DraftCommand.js +21 -0
- package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StatusCommand.js +35 -6
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/draft/brief-setup.js +23 -0
- package/dist/scripts/draft/brief-setup.js.map +1 -1
- package/dist/scripts/harness/report.js +7 -1
- package/dist/scripts/harness/report.js.map +1 -1
- package/dist/scripts/lib/agent-permission.js +31 -0
- package/dist/scripts/lib/agent-permission.js.map +1 -0
- package/dist/scripts/lib/live-model.js +28 -2
- package/dist/scripts/lib/live-model.js.map +1 -1
- package/dist/scripts/lib/live-page.js +10 -4
- package/dist/scripts/lib/live-page.js.map +1 -1
- package/dist/scripts/lib/quality-presets.js +3 -1
- package/dist/scripts/lib/quality-presets.js.map +1 -1
- package/dist/scripts/lib/quality-run.js +64 -3
- package/dist/scripts/lib/quality-run.js.map +1 -1
- package/dist/scripts/lib/shell-command.js +38 -1
- package/dist/scripts/lib/shell-command.js.map +1 -1
- package/dist/scripts/lib/sonar-local.js +1 -0
- package/dist/scripts/lib/sonar-local.js.map +1 -1
- package/dist/scripts/lib/studio-app-page.js +5 -2
- package/dist/scripts/lib/studio-app-page.js.map +1 -1
- package/dist/scripts/lib/studio-app.js +31 -7
- package/dist/scripts/lib/studio-app.js.map +1 -1
- package/package.json +1 -1
|
@@ -58,6 +58,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
58
58
|
})();
|
|
59
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
60
|
exports.RunCommand = exports.RUNS_DIR = void 0;
|
|
61
|
+
exports.promptInFlight = promptInFlight;
|
|
61
62
|
exports.setJsonMode = setJsonMode;
|
|
62
63
|
exports.error = error;
|
|
63
64
|
exports.parseArgs = parseArgs;
|
|
@@ -78,6 +79,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
78
79
|
const command_1 = require("../../../lib/command");
|
|
79
80
|
const project_root_1 = require("../../../lib/project-root");
|
|
80
81
|
const prompt_1 = require("../../../harness/prompt");
|
|
82
|
+
const agent_permission_1 = require("../../../lib/agent-permission");
|
|
81
83
|
const init_1 = require("../../../harness/init");
|
|
82
84
|
const in_flight_1 = require("../../../lib/in-flight");
|
|
83
85
|
const harness_events_1 = require("../../../lib/harness-events");
|
|
@@ -101,6 +103,17 @@ function judged(dir, read) {
|
|
|
101
103
|
view.cleanup();
|
|
102
104
|
}
|
|
103
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* A prompt built from the project as the harness judges it (REQ-268).
|
|
108
|
+
*
|
|
109
|
+
* The implementer's prompt was; the reviewer's and the adversary's read the
|
|
110
|
+
* worktree as written, where a reviewed draft's requirement has no text, and
|
|
111
|
+
* the reviewer was told to stop and report that REQ-002 did not exist
|
|
112
|
+
* (truck-flote, S53).
|
|
113
|
+
*/
|
|
114
|
+
function promptInFlight(req, dir, opts = {}) {
|
|
115
|
+
return judged(dir, (judgedDir) => (0, prompt_1.buildPrompt)(req, judgedDir, opts));
|
|
116
|
+
}
|
|
104
117
|
const GherkinQuality_1 = require("../../../../packages/core/src/domain/GherkinQuality");
|
|
105
118
|
const HarnessConfigFile_1 = require("../../../../packages/core/src/infrastructure/HarnessConfigFile");
|
|
106
119
|
const agent_1 = require("../../../lib/agent");
|
|
@@ -492,6 +505,41 @@ function baselineProblem(projectDir, running) {
|
|
|
492
505
|
"test. Write those tests or move those requirements back to Draft (`specgate check` shows " +
|
|
493
506
|
"it whole), or pass --skip-baseline.");
|
|
494
507
|
}
|
|
508
|
+
/** "0.4 s", "12 s", "3m 05s": how long a gate step took. */
|
|
509
|
+
function stepTime(ms) {
|
|
510
|
+
if (ms < 10_000)
|
|
511
|
+
return `${(ms / 1000).toFixed(1)} s`;
|
|
512
|
+
const secs = Math.round(ms / 1000);
|
|
513
|
+
if (secs < 60)
|
|
514
|
+
return `${secs} s`;
|
|
515
|
+
return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* REQ-273: a gate step is an event when it starts and when it ends, and a line
|
|
519
|
+
* of the run's output each time. A check that hung for 15 minutes printed
|
|
520
|
+
* nothing and showed nothing in `/live` until it ended (truck-flote, S54).
|
|
521
|
+
*/
|
|
522
|
+
function gateReporter(eventsFile, requirement, attempt, prefix = "") {
|
|
523
|
+
return (stage, result, extra) => {
|
|
524
|
+
const named = `${prefix}${stage}`;
|
|
525
|
+
// The line names the test step, not its command: what the command prints
|
|
526
|
+
// stays off a green run's output unless `--verbose` asks for it.
|
|
527
|
+
const shown = named.replace(/^((?:post-merge )?test command): [\s\S]*$/, "$1");
|
|
528
|
+
if (result === "start") {
|
|
529
|
+
(0, harness_events_1.appendEvent)(eventsFile, { type: "gate-start", stage: named, ...(extra || {}) }, requirement, attempt);
|
|
530
|
+
const where = extra && extra.image
|
|
531
|
+
? ` (${extra.runner} ${extra.image})`
|
|
532
|
+
: extra && extra.runner
|
|
533
|
+
? ` (${extra.runner})`
|
|
534
|
+
: "";
|
|
535
|
+
info(`${requirement}: started ${shown}${where}`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
(0, harness_events_1.appendEvent)(eventsFile, { type: "gate", stage: named, result, ...(extra || {}) }, requirement, attempt);
|
|
539
|
+
const ms = extra && typeof extra.ms === "number" ? ` in ${stepTime(extra.ms)}` : "";
|
|
540
|
+
info(`${requirement}: ${result === "pass" ? "passed" : "failed"} ${shown}${ms}`);
|
|
541
|
+
};
|
|
542
|
+
}
|
|
495
543
|
function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, report = () => { }) {
|
|
496
544
|
// `--delivering`: the requirement is still Draft while its gate runs, and a
|
|
497
545
|
// Draft row owes no files. Without it the gate would skip the one
|
|
@@ -500,6 +548,8 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
500
548
|
// the gate judges the attempt, not the matrix as it was before it.
|
|
501
549
|
(0, MatrixCommand_1.refreshDerivedMatrix)(worktreeDir);
|
|
502
550
|
let passHint = "";
|
|
551
|
+
report("validate --strict", "start");
|
|
552
|
+
const validateStart = Date.now();
|
|
503
553
|
const validate = judged(worktreeDir, (dir, toProject) => {
|
|
504
554
|
const validateArgs = [VALIDATE_SCRIPT, dir, "--strict"];
|
|
505
555
|
if (req.requirement)
|
|
@@ -512,7 +562,9 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
512
562
|
// The agent is told about its worktree, not about the copy it was judged in.
|
|
513
563
|
return { ...run, stdout: toProject(run.stdout || ""), stderr: toProject(run.stderr || "") };
|
|
514
564
|
});
|
|
515
|
-
report("validate --strict", validate.status === 0 ? "pass" : "fail"
|
|
565
|
+
report("validate --strict", validate.status === 0 ? "pass" : "fail", {
|
|
566
|
+
ms: Date.now() - validateStart,
|
|
567
|
+
});
|
|
516
568
|
if (validate.status !== 0) {
|
|
517
569
|
return {
|
|
518
570
|
ok: false,
|
|
@@ -524,8 +576,10 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
524
576
|
// REQ-199: a requirement used on a screen needs an end-to-end test that
|
|
525
577
|
// names it, when the project says where those live.
|
|
526
578
|
if (settings.e2eDir && req.requirement && (0, screen_requirements_1.onScreen)(worktreeDir, req.requirement)) {
|
|
579
|
+
report("e2e test", "start");
|
|
580
|
+
const e2eStart = Date.now();
|
|
527
581
|
const named = (0, screen_requirements_1.e2eTestsNaming)(worktreeDir, settings.e2eDir, req.requirement);
|
|
528
|
-
report("e2e test", named.length ? "pass" : "fail");
|
|
582
|
+
report("e2e test", named.length ? "pass" : "fail", { ms: Date.now() - e2eStart });
|
|
529
583
|
if (named.length === 0) {
|
|
530
584
|
return {
|
|
531
585
|
ok: false,
|
|
@@ -540,6 +594,8 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
540
594
|
// and its failure goes back to the agent like any other gate failure.
|
|
541
595
|
const archCmd = String(settings.archCmd || "").trim();
|
|
542
596
|
if (archCmd) {
|
|
597
|
+
report(`architecture check: ${archCmd}`, "start");
|
|
598
|
+
const archStart = Date.now();
|
|
543
599
|
// REQ-231: with arch_sarif, only a violation this attempt adds fails it.
|
|
544
600
|
const arch = settings.archSarif
|
|
545
601
|
? (0, quality_run_1.archRun)(worktreeDir, archCmd, settings.archSarif, "", req.requirement || "")
|
|
@@ -550,7 +606,9 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
550
606
|
timeout: timeoutMs,
|
|
551
607
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
552
608
|
});
|
|
553
|
-
report(`architecture check: ${archCmd}`, arch.status === 0 ? "pass" : "fail"
|
|
609
|
+
report(`architecture check: ${archCmd}`, arch.status === 0 ? "pass" : "fail", {
|
|
610
|
+
ms: Date.now() - archStart,
|
|
611
|
+
});
|
|
554
612
|
if (arch.status !== 0) {
|
|
555
613
|
return {
|
|
556
614
|
ok: false,
|
|
@@ -587,6 +645,8 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
587
645
|
// Stderr rather than inherited stdout, because `--format json` promises one
|
|
588
646
|
// parseable document on stdout. Letting the test command write there is
|
|
589
647
|
// precisely H18: prose and JSON sharing a stream.
|
|
648
|
+
report(`test command: ${(0, HarnessEvents_1.maskCommand)(resolved)}`, "start");
|
|
649
|
+
const testStart = Date.now();
|
|
590
650
|
const test = (0, node_child_process_1.spawnSync)((0, shell_command_1.forShell)(resolved), {
|
|
591
651
|
shell: true,
|
|
592
652
|
cwd: worktreeDir,
|
|
@@ -595,7 +655,10 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
595
655
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
596
656
|
stdio: settings.verbose ? ["ignore", 2, 2] : undefined,
|
|
597
657
|
});
|
|
598
|
-
report(`test command: ${(0, HarnessEvents_1.maskCommand)(resolved)}`, test.status === 0 ? "pass" : "fail",
|
|
658
|
+
report(`test command: ${(0, HarnessEvents_1.maskCommand)(resolved)}`, test.status === 0 ? "pass" : "fail", {
|
|
659
|
+
...(0, HarnessEvents_1.testCounts)(`${test.stdout || ""}${test.stderr || ""}`),
|
|
660
|
+
ms: Date.now() - testStart,
|
|
661
|
+
});
|
|
599
662
|
if (test.status !== 0) {
|
|
600
663
|
// Name the command. A gate that silently does the wrong thing — running
|
|
601
664
|
// the whole suite because a filter did not apply, say — produces a
|
|
@@ -642,8 +705,14 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
642
705
|
const ctx = (0, quality_run_1.gateContext)(worktreeDir, req.requirement || "", "");
|
|
643
706
|
const outcomes = [];
|
|
644
707
|
for (const stage of ["attempt", "end"]) {
|
|
645
|
-
|
|
646
|
-
|
|
708
|
+
// REQ-273: each check is reported as it starts and as it ends — the
|
|
709
|
+
// stage used to run every check before the first was reported.
|
|
710
|
+
const asItRuns = (check, run) => {
|
|
711
|
+
report(`quality: ${check.name}`, "start", {
|
|
712
|
+
runner: check.runner,
|
|
713
|
+
...(check.image ? { image: check.image } : {}),
|
|
714
|
+
});
|
|
715
|
+
const o = run();
|
|
647
716
|
// REQ-229: the step says what was measured, so /live shows it as it happens.
|
|
648
717
|
const said = o.skipped
|
|
649
718
|
? " (not run)"
|
|
@@ -657,6 +726,20 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
|
|
|
657
726
|
...(o.skipped ? { skipped: o.skipped } : {}),
|
|
658
727
|
...(o.measure ? { value: o.measure.value } : {}),
|
|
659
728
|
});
|
|
729
|
+
return o;
|
|
730
|
+
};
|
|
731
|
+
for (const o of (0, quality_run_1.runStages)(worktreeDir, quality, [stage], ctx, asItRuns, agentChecks(worktreeDir, req, settings, timeoutMs, testCmd))) {
|
|
732
|
+
outcomes.push(o);
|
|
733
|
+
// REQ-267: a check that could not run is not the agent's to fix.
|
|
734
|
+
if (o.gate && !o.ok && !o.skipped && o.couldNotRun) {
|
|
735
|
+
return {
|
|
736
|
+
ok: false,
|
|
737
|
+
stage: `quality check: ${o.name}`,
|
|
738
|
+
output: o.output,
|
|
739
|
+
couldNotRun: `the quality check ${o.name} could not run: it ${o.couldNotRun}`,
|
|
740
|
+
checks: outcomes,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
660
743
|
if (o.gate && !o.ok && !o.skipped) {
|
|
661
744
|
return {
|
|
662
745
|
ok: false,
|
|
@@ -790,7 +873,7 @@ function agentChecks(worktreeDir, req, settings, timeoutMs, testCmd) {
|
|
|
790
873
|
return settings.agent || "";
|
|
791
874
|
}
|
|
792
875
|
};
|
|
793
|
-
const promptFor = (instruction) => (
|
|
876
|
+
const promptFor = (instruction) => promptInFlight(req, worktreeDir, {
|
|
794
877
|
promptPrefix: settings.promptPrefix,
|
|
795
878
|
hint: instruction,
|
|
796
879
|
withPrecedents: settings.promptPrecedents === true,
|
|
@@ -1189,6 +1272,7 @@ function attemptRequirement(req, ctx) {
|
|
|
1189
1272
|
totalMs: Date.now() - attemptStart,
|
|
1190
1273
|
profiles: [...profilesUsed],
|
|
1191
1274
|
});
|
|
1275
|
+
ledgerAttempts(attemptLog);
|
|
1192
1276
|
};
|
|
1193
1277
|
// An attempt is a sequence of agent invocations, all bound to this one
|
|
1194
1278
|
// requirement. Attempt 1 is a single implementing step; a retry may run an
|
|
@@ -1330,17 +1414,21 @@ function attemptRequirement(req, ctx) {
|
|
|
1330
1414
|
}
|
|
1331
1415
|
// Before the gate: an agent that wrote nothing cannot have implemented
|
|
1332
1416
|
// anything, and the gate would approve it. See `producedNothing` (H19).
|
|
1417
|
+
// REQ-264: the same command writes nothing the next time either — three
|
|
1418
|
+
// attempts and 7 min 40 s said so in truck-flote (S50) — so it ends here.
|
|
1333
1419
|
if (producedNothing(worktreeDir)) {
|
|
1334
1420
|
previousFailure =
|
|
1335
1421
|
`You produced no files. Nothing in the worktree changed, so there is ` +
|
|
1336
1422
|
`nothing implementing ${req.requirement}.\n\n` +
|
|
1337
1423
|
`If you could not write, that is the problem to report — a non-interactive ` +
|
|
1338
1424
|
`agent without tool permissions reads the prompt and cannot act on it. ` +
|
|
1339
|
-
`See docs/
|
|
1425
|
+
`See docs/agent-setup.md, § Tool allowances.`;
|
|
1340
1426
|
warn(`${req.requirement}: the agent produced no files — nothing to gate`);
|
|
1341
|
-
warn(` fix:
|
|
1427
|
+
warn(` fix: give the agent write permission and the tools it needs (docs/agent-setup.md, § Tool allowances)`);
|
|
1428
|
+
warn(` not retrying: the same command would write nothing again`);
|
|
1342
1429
|
record("no-op");
|
|
1343
|
-
|
|
1430
|
+
stopRetrying = true;
|
|
1431
|
+
break;
|
|
1344
1432
|
}
|
|
1345
1433
|
// Before the gate: a scenario the agent has just loosened cannot fail, so a
|
|
1346
1434
|
// green gate proves nothing. See `checkWriteScopeInWorktree`.
|
|
@@ -1365,7 +1453,19 @@ function attemptRequirement(req, ctx) {
|
|
|
1365
1453
|
}
|
|
1366
1454
|
// `verbose` is a run flag, not a project setting, so it travels on the
|
|
1367
1455
|
// context rather than in `harness.config.yaml`.
|
|
1368
|
-
const gate = runGate(worktreeDir, settings.testCmd, timeoutMs, req, { ...settings, verbose: ctx.verbose }, (
|
|
1456
|
+
const gate = runGate(worktreeDir, settings.testCmd, timeoutMs, req, { ...settings, verbose: ctx.verbose }, gateReporter(eventsFile, req.requirement, attempt));
|
|
1457
|
+
if (!gate.ok && gate.couldNotRun) {
|
|
1458
|
+
// REQ-267: nothing in the code can fix the tool's own setup, and another
|
|
1459
|
+
// attempt would meet it again (truck-flote S52: 15 minutes each).
|
|
1460
|
+
previousFailure = `Gate stopped at: ${gate.stage}\n\n${gate.couldNotRun}\n\n${gate.output}`;
|
|
1461
|
+
warn(`${req.requirement}: ${gate.couldNotRun}`);
|
|
1462
|
+
for (const line of gate.output.trim().split("\n").slice(-5))
|
|
1463
|
+
warn(` ${line}`);
|
|
1464
|
+
warn(` not retrying: fix the check (harness.config.yaml) and run it again`);
|
|
1465
|
+
record("check");
|
|
1466
|
+
stopRetrying = true;
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1369
1469
|
if (!gate.ok) {
|
|
1370
1470
|
previousFailure =
|
|
1371
1471
|
`Gate failed at: ${gate.stage}\n\n` + (gate.hint ? `⚠ ${gate.hint}\n\n` : "") + gate.output;
|
|
@@ -1444,7 +1544,7 @@ function attemptRequirement(req, ctx) {
|
|
|
1444
1544
|
alreadyProbed: probe.ran,
|
|
1445
1545
|
})) {
|
|
1446
1546
|
info(`${req.requirement}: probing with '${settings.adversaryProfile}'`);
|
|
1447
|
-
probe = runAdversarialProbe(worktreeDir, req, settings, timeoutMs, (instruction) => (
|
|
1547
|
+
probe = runAdversarialProbe(worktreeDir, req, settings, timeoutMs, (instruction) => promptInFlight(req, worktreeDir, {
|
|
1448
1548
|
promptPrefix: settings.promptPrefix,
|
|
1449
1549
|
hint: instruction,
|
|
1450
1550
|
withPrecedents: settings.promptPrecedents === true,
|
|
@@ -1540,6 +1640,28 @@ function recordQuestions(projectDir, results) {
|
|
|
1540
1640
|
}
|
|
1541
1641
|
/** Where a run's record lands. Local to the machine — see `writeRunRecord`. */
|
|
1542
1642
|
exports.RUNS_DIR = path.join(".harness", "runs");
|
|
1643
|
+
/**
|
|
1644
|
+
* The run's record, written as it goes (REQ-269).
|
|
1645
|
+
*
|
|
1646
|
+
* It used to be written once, at the end, so a run stopped by hand — the
|
|
1647
|
+
* 27 minutes truck-flote spent on semgrep hanging (S52) — left no trace, and
|
|
1648
|
+
* `harness report` priced the requirement without them. Now it is written when
|
|
1649
|
+
* each requirement starts and each attempt ends; a killed run leaves a record
|
|
1650
|
+
* whose `running` says where it stopped.
|
|
1651
|
+
*/
|
|
1652
|
+
let ledger = null;
|
|
1653
|
+
function ledgerWrite() {
|
|
1654
|
+
if (!ledger)
|
|
1655
|
+
return;
|
|
1656
|
+
ledger.record.updatedAt = new Date().toISOString();
|
|
1657
|
+
writeRunRecord(ledger.projectDir, ledger.record);
|
|
1658
|
+
}
|
|
1659
|
+
function ledgerAttempts(attemptLog) {
|
|
1660
|
+
if (!ledger || !ledger.record.running)
|
|
1661
|
+
return;
|
|
1662
|
+
ledger.record.running.attemptLog = [...attemptLog];
|
|
1663
|
+
ledgerWrite();
|
|
1664
|
+
}
|
|
1543
1665
|
/**
|
|
1544
1666
|
* Write what a run did, so the next question about it has an answer.
|
|
1545
1667
|
*
|
|
@@ -1922,6 +2044,23 @@ function gitIdentityProblem(projectDir) {
|
|
|
1922
2044
|
`Fix: git config user.name "Your Name" && git config user.email "you@example.com"`);
|
|
1923
2045
|
}
|
|
1924
2046
|
function processRequirement(req, ctx) {
|
|
2047
|
+
if (ledger) {
|
|
2048
|
+
ledger.record.running = {
|
|
2049
|
+
requirement: req.requirement,
|
|
2050
|
+
startedAt: new Date().toISOString(),
|
|
2051
|
+
attemptLog: [],
|
|
2052
|
+
};
|
|
2053
|
+
ledgerWrite();
|
|
2054
|
+
}
|
|
2055
|
+
const result = processOneRequirement(req, ctx);
|
|
2056
|
+
if (ledger) {
|
|
2057
|
+
ledger.record.results.push(result);
|
|
2058
|
+
delete ledger.record.running;
|
|
2059
|
+
ledgerWrite();
|
|
2060
|
+
}
|
|
2061
|
+
return result;
|
|
2062
|
+
}
|
|
2063
|
+
function processOneRequirement(req, ctx) {
|
|
1925
2064
|
const { projectDir, baseRef, keepWorktrees, force } = ctx;
|
|
1926
2065
|
const branch = `harness/${req.requirement}`;
|
|
1927
2066
|
// D1: which profile this requirement matches, resolved once. The bounded
|
|
@@ -2049,8 +2188,13 @@ function processRequirement(req, ctx) {
|
|
|
2049
2188
|
try {
|
|
2050
2189
|
const setupCmd = String((ctx.settings && ctx.settings.setupCmd) || "").trim();
|
|
2051
2190
|
if (setupCmd && !survivor) {
|
|
2052
|
-
|
|
2191
|
+
const report = gateReporter((0, harness_events_1.eventsFileFor)(ctx.projectDir || dir, req.requirement), req.requirement, undefined);
|
|
2192
|
+
report(`setup: ${setupCmd}`, "start");
|
|
2193
|
+
const setupStart = Date.now();
|
|
2053
2194
|
const setup = (0, node_child_process_1.spawnSync)((0, shell_command_1.forShell)(setupCmd), { cwd: dir, shell: true, encoding: "utf8" });
|
|
2195
|
+
report(`setup: ${setupCmd}`, setup.status === 0 ? "pass" : "fail", {
|
|
2196
|
+
ms: Date.now() - setupStart,
|
|
2197
|
+
});
|
|
2054
2198
|
if (setup.status !== 0) {
|
|
2055
2199
|
return {
|
|
2056
2200
|
requirement: req.requirement,
|
|
@@ -2382,7 +2526,7 @@ async function runChain(pending, ctx, hintByReq) {
|
|
|
2382
2526
|
(0, harness_events_1.appendEvent)(eventsFile, { type: "merge", into: "HEAD", branch }, req.requirement, attempt);
|
|
2383
2527
|
const gate = runGate(projectDir, ctx.settings.testCmd, ctx.timeoutMs, req,
|
|
2384
2528
|
// The checks judged the branch already; the post-merge gate is the suite's.
|
|
2385
|
-
{ ...ctx.settings, quality: undefined }, (
|
|
2529
|
+
{ ...ctx.settings, quality: undefined }, gateReporter(eventsFile, req.requirement, attempt, "post-merge "));
|
|
2386
2530
|
if (!gate.ok) {
|
|
2387
2531
|
git(projectDir, ["reset", "-q", "--hard", "HEAD~1"]);
|
|
2388
2532
|
Object.assign(result, {
|
|
@@ -2724,6 +2868,24 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2724
2868
|
}
|
|
2725
2869
|
}
|
|
2726
2870
|
}
|
|
2871
|
+
// REQ-264: Claude Code in print mode writes nothing without permission.
|
|
2872
|
+
let permitted = false;
|
|
2873
|
+
const permit = (command) => {
|
|
2874
|
+
const p = (0, agent_permission_1.withAgentPermission)(command);
|
|
2875
|
+
permitted = permitted || p.added;
|
|
2876
|
+
return p.command;
|
|
2877
|
+
};
|
|
2878
|
+
if (settings.agent)
|
|
2879
|
+
settings.agent = permit(settings.agent);
|
|
2880
|
+
for (const name of Object.keys(settings.profileAgents || {})) {
|
|
2881
|
+
settings.profileAgents[name] = permit(settings.profileAgents[name]);
|
|
2882
|
+
}
|
|
2883
|
+
if (permitted) {
|
|
2884
|
+
info("Claude Code runs with --permission-mode acceptEdits: in print mode it cannot write " +
|
|
2885
|
+
"without it, and the harness's worktree confines what it writes.");
|
|
2886
|
+
}
|
|
2887
|
+
// REQ-271: an agent given on the command line is forgotten after the run.
|
|
2888
|
+
const unkept = Boolean(args.agent) && !(fileConfig && fileConfig.agent);
|
|
2727
2889
|
// A ladder of per-attempt profiles configures agents just as much as a
|
|
2728
2890
|
// single `agent:` does, so a project that declares one is configured.
|
|
2729
2891
|
const hasAgent = Boolean(settings.agent) || settings.attemptProfiles.length > 0;
|
|
@@ -2868,6 +3030,23 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2868
3030
|
(0, harness_events_1.writeQueue)(projectDir, pending.map((r) => r.requirement));
|
|
2869
3031
|
}
|
|
2870
3032
|
const startedAt = new Date().toISOString();
|
|
3033
|
+
const runRecordBase = {
|
|
3034
|
+
schemaVersion: 1,
|
|
3035
|
+
startedAt,
|
|
3036
|
+
baseRef,
|
|
3037
|
+
concurrency: settings.concurrency,
|
|
3038
|
+
maxAttempts: settings.maxAttempts,
|
|
3039
|
+
// Whether this run had a ceiling at all. Reading the ledger later,
|
|
3040
|
+
// a short run is a different thing depending on the answer.
|
|
3041
|
+
bounded: (0, RunBudget_1.hasBudget)({
|
|
3042
|
+
budgetSeconds: args.budgetSeconds,
|
|
3043
|
+
maxRequirements: args.maxRequirements,
|
|
3044
|
+
}),
|
|
3045
|
+
};
|
|
3046
|
+
if (!process.env.SPECGATE_HARNESS_WORKER) {
|
|
3047
|
+
ledger = { projectDir, record: { ...runRecordBase, finishedAt: "", results: [] } };
|
|
3048
|
+
ledgerWrite();
|
|
3049
|
+
}
|
|
2871
3050
|
const results = args.chain
|
|
2872
3051
|
? await runChain(pending, ctx, hintByReq)
|
|
2873
3052
|
: await runLevels(pending, ctx, {
|
|
@@ -2885,21 +3064,17 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2885
3064
|
const recordPath = process.env.SPECGATE_HARNESS_WORKER
|
|
2886
3065
|
? null
|
|
2887
3066
|
: writeRunRecord(projectDir, {
|
|
2888
|
-
|
|
2889
|
-
startedAt,
|
|
3067
|
+
...runRecordBase,
|
|
2890
3068
|
finishedAt: new Date().toISOString(),
|
|
2891
|
-
baseRef,
|
|
2892
|
-
concurrency: settings.concurrency,
|
|
2893
|
-
maxAttempts: settings.maxAttempts,
|
|
2894
|
-
// Whether this run had a ceiling at all. Reading the ledger later,
|
|
2895
|
-
// a short run is a different thing depending on the answer.
|
|
2896
|
-
bounded: (0, RunBudget_1.hasBudget)({
|
|
2897
|
-
budgetSeconds: args.budgetSeconds,
|
|
2898
|
-
maxRequirements: args.maxRequirements,
|
|
2899
|
-
}),
|
|
2900
3069
|
results,
|
|
2901
3070
|
});
|
|
3071
|
+
ledger = null;
|
|
2902
3072
|
printReport(results, args.format, recordPath ? path.relative(projectDir, recordPath) : undefined);
|
|
3073
|
+
// REQ-271: the studio's Build reads the agent from harness.config.yaml.
|
|
3074
|
+
if (unkept && !jsonMode && !process.env.SPECGATE_HARNESS_WORKER) {
|
|
3075
|
+
process.stdout.write(` ${ui.muted("Keep this agent for the next run and the studio's Build:")}\n` +
|
|
3076
|
+
` specgate config set agent "${args.agent.replace(/"/g, '\\"')}"\n\n`);
|
|
3077
|
+
}
|
|
2903
3078
|
// A blocked requirement was never attempted, so it is not a pass — but it
|
|
2904
3079
|
// is also not evidence that anything is broken beyond the failure that
|
|
2905
3080
|
// caused it. It still fails the run, because work was left undone.
|