@rsaglobaltech/specgate 0.8.1 → 0.10.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/CHANGELOG.md +196 -0
- package/README.md +17 -25
- package/bin/specgate.ts +23 -5
- package/dist/bin/specgate.js +19 -4
- package/dist/bin/specgate.js.map +1 -1
- package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js +56 -0
- package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js.map +1 -1
- package/dist/packages/core/src/application/ValidateProjectUseCase.js +54 -0
- package/dist/packages/core/src/application/ValidateProjectUseCase.js.map +1 -1
- package/dist/packages/core/src/domain/AdversarialProbe.js +75 -0
- package/dist/packages/core/src/domain/AdversarialProbe.js.map +1 -0
- package/dist/packages/core/src/domain/AgentPrompt.js +52 -3
- package/dist/packages/core/src/domain/AgentPrompt.js.map +1 -1
- package/dist/packages/core/src/domain/DeclaredArtifacts.js +25 -0
- package/dist/packages/core/src/domain/DeclaredArtifacts.js.map +1 -1
- package/dist/packages/core/src/domain/DoneVerification.js +89 -0
- package/dist/packages/core/src/domain/DoneVerification.js.map +1 -0
- package/dist/packages/core/src/domain/GateCommandIntegrity.js +139 -0
- package/dist/packages/core/src/domain/GateCommandIntegrity.js.map +1 -0
- package/dist/packages/core/src/domain/GherkinQuality.js +19 -3
- package/dist/packages/core/src/domain/GherkinQuality.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessConfig.js +6 -0
- package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -1
- package/dist/packages/core/src/domain/MultiStack.js +96 -0
- package/dist/packages/core/src/domain/MultiStack.js.map +1 -0
- package/dist/packages/core/src/domain/NewRequirement.js +46 -0
- package/dist/packages/core/src/domain/NewRequirement.js.map +1 -0
- package/dist/packages/core/src/domain/Precedents.js +96 -0
- package/dist/packages/core/src/domain/Precedents.js.map +1 -0
- package/dist/packages/core/src/domain/RequirementPlan.js +1 -0
- package/dist/packages/core/src/domain/RequirementPlan.js.map +1 -1
- package/dist/packages/core/src/domain/RequirementRemoval.js +161 -0
- package/dist/packages/core/src/domain/RequirementRemoval.js.map +1 -0
- package/dist/packages/core/src/domain/ScenarioCoverage.js +178 -0
- package/dist/packages/core/src/domain/ScenarioCoverage.js.map +1 -0
- package/dist/packages/core/src/domain/SpecSections.js +206 -0
- package/dist/packages/core/src/domain/SpecSections.js.map +1 -0
- package/dist/packages/core/src/domain/TestCommand.js +82 -0
- package/dist/packages/core/src/domain/TestCommand.js.map +1 -0
- package/dist/packages/core/src/domain/TraceabilityFormat.js +45 -2
- package/dist/packages/core/src/domain/TraceabilityFormat.js.map +1 -1
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +31 -0
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
- package/dist/packages/core/test/unit/adversarial-probe.test.js +81 -0
- package/dist/packages/core/test/unit/adversarial-probe.test.js.map +1 -0
- package/dist/packages/core/test/unit/done-verification.test.js +84 -0
- package/dist/packages/core/test/unit/done-verification.test.js.map +1 -0
- package/dist/packages/core/test/unit/gate-command-integrity.test.js +114 -0
- package/dist/packages/core/test/unit/gate-command-integrity.test.js.map +1 -0
- package/dist/packages/core/test/unit/gherkin-quality.test.js +28 -0
- package/dist/packages/core/test/unit/gherkin-quality.test.js.map +1 -1
- package/dist/packages/core/test/unit/multi-stack.test.js +86 -0
- package/dist/packages/core/test/unit/multi-stack.test.js.map +1 -0
- package/dist/packages/core/test/unit/new-requirement.test.js +67 -0
- package/dist/packages/core/test/unit/new-requirement.test.js.map +1 -0
- package/dist/packages/core/test/unit/precedents.test.js +99 -0
- package/dist/packages/core/test/unit/precedents.test.js.map +1 -0
- package/dist/packages/core/test/unit/requirement-removal.test.js +124 -0
- package/dist/packages/core/test/unit/requirement-removal.test.js.map +1 -0
- package/dist/packages/core/test/unit/scenario-coverage.test.js +146 -0
- package/dist/packages/core/test/unit/scenario-coverage.test.js.map +1 -0
- package/dist/packages/core/test/unit/spec-sections.test.js +146 -0
- package/dist/packages/core/test/unit/spec-sections.test.js.map +1 -0
- package/dist/packages/core/test/unit/test-command.test.js +81 -0
- package/dist/packages/core/test/unit/test-command.test.js.map +1 -0
- package/dist/packages/core/test/unit/validate-project.test.js +21 -0
- package/dist/packages/core/test/unit/validate-project.test.js.map +1 -1
- package/dist/scripts/agents/commands.js +1 -1
- package/dist/scripts/agents/commands.js.map +1 -1
- package/dist/scripts/agents/init.js +59 -15
- package/dist/scripts/agents/init.js.map +1 -1
- package/dist/scripts/change/instructions.js +2 -2
- package/dist/scripts/change/instructions.js.map +1 -1
- package/dist/scripts/check.js +10 -0
- package/dist/scripts/check.js.map +1 -0
- package/dist/scripts/cli/commands/change/ChangeCommand.js +2 -2
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/config/CiInitCommand.js +1 -1
- package/dist/scripts/cli/commands/config/CiInitCommand.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +214 -9
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/InferPackCommand.js +331 -31
- package/dist/scripts/cli/commands/pack/InferPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js +86 -9
- package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/InitProjectCommand.js +286 -45
- package/dist/scripts/cli/commands/project/InitProjectCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/UpdateCommand.js +118 -8
- package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +184 -0
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -0
- package/dist/scripts/cli/commands/quality/DoctorCommand.js +20 -2
- package/dist/scripts/cli/commands/quality/DoctorCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/FixCommand.js +1 -1
- package/dist/scripts/cli/commands/quality/FixCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ReportCommand.js +1 -1
- package/dist/scripts/cli/commands/quality/ReportCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +406 -24
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js +108 -0
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/NewCommand.js +178 -0
- package/dist/scripts/cli/commands/spec/NewCommand.js.map +1 -0
- package/dist/scripts/cli/commands/spec/PlanCommand.js +8 -2
- package/dist/scripts/cli/commands/spec/PlanCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ReqCommand.js +124 -4
- package/dist/scripts/cli/commands/spec/ReqCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StatusCommand.js +84 -32
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/docs/build-site.js +145 -35
- package/dist/scripts/docs/build-site.js.map +1 -1
- package/dist/scripts/docs/components.js +208 -0
- package/dist/scripts/docs/components.js.map +1 -0
- package/dist/scripts/docs/highlight.js +180 -0
- package/dist/scripts/docs/highlight.js.map +1 -0
- package/dist/scripts/docs/icons.js +63 -0
- package/dist/scripts/docs/icons.js.map +1 -0
- package/dist/scripts/docs/nav.js +9 -3
- package/dist/scripts/docs/nav.js.map +1 -1
- package/dist/scripts/gen_mcp_tools.js +22 -2
- package/dist/scripts/gen_mcp_tools.js.map +1 -1
- package/dist/scripts/harness/init.js +101 -40
- package/dist/scripts/harness/init.js.map +1 -1
- package/dist/scripts/harness/worktree-lock.js +161 -0
- package/dist/scripts/harness/worktree-lock.js.map +1 -0
- package/dist/scripts/infer_pack.js +4 -1
- package/dist/scripts/infer_pack.js.map +1 -1
- package/dist/scripts/lib/surface.js +88 -38
- package/dist/scripts/lib/surface.js.map +1 -1
- package/dist/scripts/new.js +10 -0
- package/dist/scripts/new.js.map +1 -0
- package/dist/scripts/plugin/gate-hook.js +2 -2
- package/dist/scripts/plugin/gate-hook.js.map +1 -1
- package/package.json +5 -2
- package/templates/adopt/AI_RULES.md.tpl +1 -1
- package/templates/adopt/spec.md.tpl +4 -4
- package/templates/base/spec.md.tpl +12 -0
- package/templates/ci/azure.yml.tpl +2 -2
- package/templates/ci/github.yml.tpl +2 -2
- package/templates/ci/gitlab.yml.tpl +1 -1
- package/templates/ci/jenkins.tpl +1 -1
- package/templates/contracts/test-strategy.md.tpl +2 -2
- package/templates/harness/harness.config.yaml.tpl +3 -3
- package/templates/harness/prompt-prefix.md.tpl +2 -5
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A spec-driven repo is already an environment for an agent: `plan` is the
|
|
7
7
|
* task queue, the feature file + AI_RULES.md are the per-task context,
|
|
8
|
-
* `validate --strict
|
|
8
|
+
* `validate --strict` + the project test command are the reward signal,
|
|
9
9
|
* and `done` is the state transition. This command is the missing
|
|
10
10
|
* orchestration layer — it runs plan → context → agent → verify → done
|
|
11
11
|
* without a human copy-pasting prompts.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `harness/REQ-NNN` branch:
|
|
15
15
|
* 1. Build a self-contained prompt (Gherkin + AI_RULES + paths + retry feedback).
|
|
16
16
|
* 2. Shell out to the user-configured agent ({prompt_file} placeholder).
|
|
17
|
-
* 3. Gate: `validate --strict
|
|
17
|
+
* 3. Gate: `validate --strict` + the project test command.
|
|
18
18
|
* 4. Green → `done REQ-NNN` + commit. Red → retry N times feeding the failure.
|
|
19
19
|
* 5. Emit a pass/fail/attempts report.
|
|
20
20
|
*
|
|
@@ -73,6 +73,7 @@ const GherkinQuality_1 = require("../../../../packages/core/src/domain/GherkinQu
|
|
|
73
73
|
const HarnessConfigFile_1 = require("../../../../packages/core/src/infrastructure/HarnessConfigFile");
|
|
74
74
|
const agent_1 = require("../../../lib/agent");
|
|
75
75
|
const WriteScope_1 = require("../../../../packages/core/src/domain/WriteScope");
|
|
76
|
+
const GateCommandIntegrity_1 = require("../../../../packages/core/src/domain/GateCommandIntegrity");
|
|
76
77
|
const EmptyAttempt_1 = require("../../../../packages/core/src/domain/EmptyAttempt");
|
|
77
78
|
const DeclaredArtifacts_1 = require("../../../../packages/core/src/domain/DeclaredArtifacts");
|
|
78
79
|
const ResumeState_1 = require("../../../../packages/core/src/domain/ResumeState");
|
|
@@ -81,6 +82,8 @@ const ProfileMatch_1 = require("../../../../packages/core/src/domain/ProfileMatc
|
|
|
81
82
|
const TraceabilityFormat_1 = require("../../../../packages/core/src/domain/TraceabilityFormat");
|
|
82
83
|
const RequirementPlan_1 = require("../../../../packages/core/src/domain/RequirementPlan");
|
|
83
84
|
const RunBudget_1 = require("../../../../packages/core/src/domain/RunBudget");
|
|
85
|
+
const worktree_lock_1 = require("../../../harness/worktree-lock");
|
|
86
|
+
const AdversarialProbe_1 = require("../../../../packages/core/src/domain/AdversarialProbe");
|
|
84
87
|
const CucumberMessages_1 = require("../../../../packages/core/src/domain/CucumberMessages");
|
|
85
88
|
const HarnessRun_1 = require("../../../../packages/core/src/domain/HarnessRun");
|
|
86
89
|
// Three levels up from dist/scripts/cli/commands/harness is dist/scripts,
|
|
@@ -145,6 +148,10 @@ function usage() {
|
|
|
145
148
|
" --budget-seconds <n> Wall-clock ceiling for the whole run. On reaching it\n" +
|
|
146
149
|
" the run stops cleanly and still reports what it did.\n" +
|
|
147
150
|
" --max-requirements <n> Attempt at most n requirements in this run.\n" +
|
|
151
|
+
" --verbose Stream the gate command's output as it runs, on\n" +
|
|
152
|
+
" stderr. Without it the output only appears when the\n" +
|
|
153
|
+
" gate fails, so a run that takes minutes shows\n" +
|
|
154
|
+
" nothing and a hung test looks like a working one.\n" +
|
|
148
155
|
" --skip-not-ready Skip requirements an agent could not succeed at — no\n" +
|
|
149
156
|
" feature, an unrunnable scenario, unmet dependencies,\n" +
|
|
150
157
|
" Deprecated or Needs Clarification. Default: warn and\n" +
|
|
@@ -177,6 +184,7 @@ function parseArgs(argv) {
|
|
|
177
184
|
resume: false,
|
|
178
185
|
format: "text",
|
|
179
186
|
dryRun: false,
|
|
187
|
+
verbose: false,
|
|
180
188
|
strictArtifacts: false,
|
|
181
189
|
skipNotReady: false,
|
|
182
190
|
budgetSeconds: 0,
|
|
@@ -246,6 +254,9 @@ function parseArgs(argv) {
|
|
|
246
254
|
else if (token === "--format") {
|
|
247
255
|
args.format = argv[++i] || "";
|
|
248
256
|
}
|
|
257
|
+
else if (token === "--verbose") {
|
|
258
|
+
args.verbose = true;
|
|
259
|
+
}
|
|
249
260
|
else if (token === "--dry-run") {
|
|
250
261
|
args.dryRun = true;
|
|
251
262
|
}
|
|
@@ -323,7 +334,7 @@ function runPlan(projectDir) {
|
|
|
323
334
|
// can easily produce >1 MB of dependency-download log, and `spawnSync`'s
|
|
324
335
|
// default 1 MB ceiling otherwise kills the gate with ENOBUFS.
|
|
325
336
|
const SUBPROCESS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
326
|
-
/** Run the gate (validate --strict
|
|
337
|
+
/** Run the gate (validate --strict, then the optional test command). */
|
|
327
338
|
/**
|
|
328
339
|
* What Cucumber says it did, when it can be asked (F5).
|
|
329
340
|
*
|
|
@@ -358,7 +369,13 @@ function readGateMessages(worktreeDir, reportPath) {
|
|
|
358
369
|
return run.parsed ? run : null;
|
|
359
370
|
}
|
|
360
371
|
function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
|
|
361
|
-
|
|
372
|
+
// `--delivering`: the requirement is still Draft while its gate runs, and a
|
|
373
|
+
// Draft row owes no files. Without it the gate would skip the one
|
|
374
|
+
// requirement this attempt exists to deliver.
|
|
375
|
+
const validateArgs = [VALIDATE_SCRIPT, worktreeDir, "--strict"];
|
|
376
|
+
if (req.requirement)
|
|
377
|
+
validateArgs.push("--delivering", req.requirement);
|
|
378
|
+
const validate = (0, node_child_process_1.spawnSync)(process.execPath, validateArgs, {
|
|
362
379
|
encoding: "utf8",
|
|
363
380
|
timeout: timeoutMs,
|
|
364
381
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
@@ -366,7 +383,7 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
|
|
|
366
383
|
if (validate.status !== 0) {
|
|
367
384
|
return {
|
|
368
385
|
ok: false,
|
|
369
|
-
stage: "validate --strict
|
|
386
|
+
stage: "validate --strict",
|
|
370
387
|
output: validate.stdout + validate.stderr,
|
|
371
388
|
hint: "",
|
|
372
389
|
};
|
|
@@ -384,12 +401,23 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
|
|
|
384
401
|
// dependencies has no node_modules here. Verified the hard way: an agent
|
|
385
402
|
// spent its first attempt installing them and timed out. The gate command
|
|
386
403
|
// is the right place to say so, since only the project knows how.
|
|
404
|
+
// `--verbose` streams instead of capturing, and streams to **fd 2**.
|
|
405
|
+
//
|
|
406
|
+
// `spawnSync` cannot do both: the buffers only exist once the child has
|
|
407
|
+
// exited, which is the moment streaming stops being useful. Streaming is
|
|
408
|
+
// the point of the flag — during a run that takes minutes there is nothing
|
|
409
|
+
// to watch, and a gate that hangs looks exactly like a gate that is working.
|
|
410
|
+
//
|
|
411
|
+
// Stderr rather than inherited stdout, because `--format json` promises one
|
|
412
|
+
// parseable document on stdout. Letting the test command write there is
|
|
413
|
+
// precisely H18: prose and JSON sharing a stream.
|
|
387
414
|
const test = (0, node_child_process_1.spawnSync)(resolved, {
|
|
388
415
|
shell: true,
|
|
389
416
|
cwd: worktreeDir,
|
|
390
417
|
encoding: "utf8",
|
|
391
418
|
timeout: timeoutMs,
|
|
392
419
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
420
|
+
stdio: settings.verbose ? ["ignore", 2, 2] : undefined,
|
|
393
421
|
});
|
|
394
422
|
if (test.status !== 0) {
|
|
395
423
|
// Name the command. A gate that silently does the wrong thing — running
|
|
@@ -399,7 +427,11 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
|
|
|
399
427
|
return {
|
|
400
428
|
ok: false,
|
|
401
429
|
stage: `test command: ${resolved}`,
|
|
402
|
-
output
|
|
430
|
+
// Streamed output is not captured, so say where it went rather than
|
|
431
|
+
// recording an empty string a reader would take for "printed nothing".
|
|
432
|
+
output: settings.verbose
|
|
433
|
+
? "(streamed above — run without --verbose to capture it in the report)"
|
|
434
|
+
: test.stdout + test.stderr,
|
|
403
435
|
};
|
|
404
436
|
}
|
|
405
437
|
// Exit zero is where the old gate stopped. This is the part that catches a
|
|
@@ -470,6 +502,75 @@ function discardWorktreeChanges(worktreeDir) {
|
|
|
470
502
|
// reviewer's edits must not discard the evidence.
|
|
471
503
|
git(worktreeDir, ["clean", "-fd", "-e", EmptyAttempt_1.PROMPT_ARCHIVE_DIR]);
|
|
472
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Let an adversary try to break a green implementation (D3).
|
|
507
|
+
*
|
|
508
|
+
* Sequence, and the order is the whole design: the gate has already passed, so
|
|
509
|
+
* the work is committable; the adversary writes a test in the worktree; the
|
|
510
|
+
* project's own test command runs again; whatever the adversary wrote is
|
|
511
|
+
* discarded so the branch carries the implementer's work alone.
|
|
512
|
+
*
|
|
513
|
+
* Never throws and never changes the verdict. The gate stays the only judge —
|
|
514
|
+
* an adversary can always assert a behaviour the requirement never promised,
|
|
515
|
+
* and a failing probe that blocked the run would stop correct work.
|
|
516
|
+
*/
|
|
517
|
+
function runAdversarialProbe(worktreeDir, req, settings, timeoutMs, promptFor) {
|
|
518
|
+
const profile = settings.adversaryProfile;
|
|
519
|
+
const command = settings.profileAgents[profile] || settings.agent;
|
|
520
|
+
if (!command)
|
|
521
|
+
return AdversarialProbe_1.NO_PROBE;
|
|
522
|
+
const instruction = `The implementation below already passes its gate. Your job is to try to ` +
|
|
523
|
+
`**break** it: write one additional test, in the project's existing test ` +
|
|
524
|
+
`style, for a case ${req.requirement} should handle and might not — an ` +
|
|
525
|
+
`empty input, a boundary, a duplicate, a failure path. Write the test and ` +
|
|
526
|
+
`nothing else. Do not modify the implementation, the specification or any ` +
|
|
527
|
+
`existing test.`;
|
|
528
|
+
const promptFile = path.join(os.tmpdir(), `csda-probe-${req.requirement}-${crypto.randomBytes(4).toString("hex")}.md`);
|
|
529
|
+
try {
|
|
530
|
+
fs.writeFileSync(promptFile, promptFor(instruction), "utf8");
|
|
531
|
+
const agent = (0, node_child_process_1.spawnSync)((0, HarnessRun_1.substituteAgentCommand)(command, promptFile), {
|
|
532
|
+
shell: true,
|
|
533
|
+
cwd: worktreeDir,
|
|
534
|
+
encoding: "utf8",
|
|
535
|
+
timeout: timeoutMs,
|
|
536
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
537
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
538
|
+
});
|
|
539
|
+
const wrote = git(worktreeDir, ["status", "--porcelain"])
|
|
540
|
+
.stdout.split("\n")
|
|
541
|
+
.map((line) => line.slice(3).trim())
|
|
542
|
+
.filter((line) => line && !line.startsWith(EmptyAttempt_1.PROMPT_ARCHIVE_DIR));
|
|
543
|
+
if (wrote.length === 0) {
|
|
544
|
+
// Nothing written is not a failure: an adversary that cannot think of a
|
|
545
|
+
// case is telling you something mildly reassuring.
|
|
546
|
+
return { ran: true, broke: false, detail: "The adversary wrote no test.", wrote: [] };
|
|
547
|
+
}
|
|
548
|
+
const after = runGate(worktreeDir, settings.testCmd, timeoutMs, req, settings);
|
|
549
|
+
return {
|
|
550
|
+
ran: true,
|
|
551
|
+
broke: !after.ok,
|
|
552
|
+
detail: [`${agent.stdout || ""}${agent.stderr || ""}`.trim(), after.output || ""]
|
|
553
|
+
.filter(Boolean)
|
|
554
|
+
.join("\n\n")
|
|
555
|
+
.slice(0, 4000),
|
|
556
|
+
wrote,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// Insurance that fails is still only insurance; it must not take the run.
|
|
561
|
+
return AdversarialProbe_1.NO_PROBE;
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
try {
|
|
565
|
+
fs.rmSync(promptFile, { force: true });
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
/* a temp file nobody will read again */
|
|
569
|
+
}
|
|
570
|
+
// The branch carries the implementer's work and nothing else.
|
|
571
|
+
discardWorktreeChanges(worktreeDir);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
473
574
|
/**
|
|
474
575
|
* Did the agent edit the contract it was being judged against? (A1, closes H16)
|
|
475
576
|
*
|
|
@@ -560,6 +661,52 @@ function checkWriteScopeInWorktree(worktreeDir, settings) {
|
|
|
560
661
|
`(\`specgate change new\`), reviewed by a person — not in this attempt.`,
|
|
561
662
|
};
|
|
562
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Did the agent rewrite the definition the gate command depends on? (#167)
|
|
666
|
+
*
|
|
667
|
+
* The write-scope guard covers the contract — spec.md, AI_RULES.md, features/**
|
|
668
|
+
* — and not the file that defines the command the gate runs. Measured with a
|
|
669
|
+
* stub agent that wrote no implementation at all and only replaced
|
|
670
|
+
* `"test": "node --test tests/*.test.js"` with `"echo 'all good'"` over a red
|
|
671
|
+
* suite: `✅ REQ-001 pass (1 attempt)`.
|
|
672
|
+
*
|
|
673
|
+
* This does not protect the manifest — adding a dependency mid-implementation
|
|
674
|
+
* is legitimate, and a guard that blocks it gets switched off. It compares one
|
|
675
|
+
* value: the script the gate actually invokes, at the base commit and now.
|
|
676
|
+
*
|
|
677
|
+
* Returns null when there is nothing to report, including when the gate command
|
|
678
|
+
* is one whose meaning does not live in a file we can read.
|
|
679
|
+
*/
|
|
680
|
+
function checkGateCommandInWorktree(worktreeDir, baseRef, testCmd) {
|
|
681
|
+
if (!testCmd)
|
|
682
|
+
return null;
|
|
683
|
+
const probe = (0, GateCommandIntegrity_1.gateCommandIntegrity)(testCmd, "{}", "{}");
|
|
684
|
+
if (!probe.script)
|
|
685
|
+
return null;
|
|
686
|
+
const manifest = probe.script.manifest;
|
|
687
|
+
const show = git(worktreeDir, ["show", `${baseRef}:${manifest}`]);
|
|
688
|
+
const before = show.status === 0 ? show.stdout : null;
|
|
689
|
+
const currentPath = path.join(worktreeDir, manifest);
|
|
690
|
+
const after = fs.existsSync(currentPath) ? fs.readFileSync(currentPath, "utf8") : null;
|
|
691
|
+
const result = (0, GateCommandIntegrity_1.gateCommandIntegrity)(testCmd, before, after);
|
|
692
|
+
if (!result.checked || !result.changed)
|
|
693
|
+
return null;
|
|
694
|
+
return {
|
|
695
|
+
manifest,
|
|
696
|
+
script: result.script.script,
|
|
697
|
+
message: `agent_rewrote_gate_command — the gate runs \`${testCmd}\`, and this attempt ` +
|
|
698
|
+
`changed what that means.\n\n` +
|
|
699
|
+
` ${manifest} → scripts.${result.script.script}\n` +
|
|
700
|
+
` before: ${result.before === null ? "(absent)" : result.before}\n` +
|
|
701
|
+
` after: ${result.after === null ? "(absent)" : result.after}\n\n` +
|
|
702
|
+
`The command the gate runs is part of the contract this requirement is ` +
|
|
703
|
+
`judged against, exactly like the scenario is. Weakening it makes a green ` +
|
|
704
|
+
`gate meaningless.\n\n` +
|
|
705
|
+
`Revert that script and make the suite pass as it stands. If the command is ` +
|
|
706
|
+
`genuinely wrong, that is a change for a person to make — not something to ` +
|
|
707
|
+
`fix inside the attempt being measured by it.`,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
563
710
|
/**
|
|
564
711
|
* Every path the agent wrote in this worktree — added and modified alike.
|
|
565
712
|
*
|
|
@@ -614,6 +761,10 @@ function attemptRequirement(req, ctx) {
|
|
|
614
761
|
const attemptLog = [];
|
|
615
762
|
/** The reviewer's findings from the previous attempt, fed into this prompt. */
|
|
616
763
|
let reviewFindings = "";
|
|
764
|
+
// Once per requirement, not once per attempt: probing an implementation that
|
|
765
|
+
// already failed tells you nothing, and probing every retry multiplies the
|
|
766
|
+
// cost of the one step in the loop that is pure insurance.
|
|
767
|
+
let probe = AdversarialProbe_1.NO_PROBE;
|
|
617
768
|
const firstAttempt = Math.max(1, (ctx.resumeAt && ctx.resumeAt.attempt) || 1);
|
|
618
769
|
for (let attempt = firstAttempt; attempt <= settings.maxAttempts; attempt += 1) {
|
|
619
770
|
info(`${req.requirement}: attempt ${attempt}/${settings.maxAttempts}`);
|
|
@@ -651,6 +802,10 @@ function attemptRequirement(req, ctx) {
|
|
|
651
802
|
reviewFindings: step.advisory ? undefined : reviewFindings || undefined,
|
|
652
803
|
attempt,
|
|
653
804
|
maxAttempts: settings.maxAttempts,
|
|
805
|
+
// Read from the worktree, which carries the matrix at the base
|
|
806
|
+
// commit — the precedent must be work already accepted, not a
|
|
807
|
+
// sibling requirement this same run happens to be writing.
|
|
808
|
+
withPrecedents: settings.promptPrecedents === true,
|
|
654
809
|
});
|
|
655
810
|
const promptFile = path.join(os.tmpdir(), `csda-harness-prompt-${req.requirement}-${crypto.randomBytes(4).toString("hex")}.md`);
|
|
656
811
|
fs.writeFileSync(promptFile, prompt, "utf8");
|
|
@@ -744,7 +899,22 @@ function attemptRequirement(req, ctx) {
|
|
|
744
899
|
record("write-scope");
|
|
745
900
|
continue;
|
|
746
901
|
}
|
|
747
|
-
|
|
902
|
+
// Same reason as the write-scope guard, one level up: a gate command the
|
|
903
|
+
// agent has just loosened cannot fail either.
|
|
904
|
+
const gateCmd = checkGateCommandInWorktree(worktreeDir, "HEAD", settings.testCmd);
|
|
905
|
+
if (gateCmd) {
|
|
906
|
+
previousFailure = gateCmd.message;
|
|
907
|
+
warn(`${req.requirement}: the agent rewrote the gate command`);
|
|
908
|
+
warn(` ${gateCmd.manifest} → scripts.${gateCmd.script}`);
|
|
909
|
+
record("write-scope");
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
// `verbose` is a run flag, not a project setting, so it travels on the
|
|
913
|
+
// context rather than in `harness.config.yaml`.
|
|
914
|
+
const gate = runGate(worktreeDir, settings.testCmd, timeoutMs, req, {
|
|
915
|
+
...settings,
|
|
916
|
+
verbose: ctx.verbose,
|
|
917
|
+
});
|
|
748
918
|
if (!gate.ok) {
|
|
749
919
|
previousFailure =
|
|
750
920
|
`Gate failed at: ${gate.stage}\n\n` + (gate.hint ? `⚠ ${gate.hint}\n\n` : "") + gate.output;
|
|
@@ -793,8 +963,37 @@ function attemptRequirement(req, ctx) {
|
|
|
793
963
|
record("commit");
|
|
794
964
|
continue;
|
|
795
965
|
}
|
|
966
|
+
// The adversary runs **after** the commit, and the order is not cosmetic:
|
|
967
|
+
// before it, `git status` shows the implementer's uncommitted work, so
|
|
968
|
+
// discarding "what the probe wrote" would throw the implementation away and
|
|
969
|
+
// report a pass over an empty branch. That is the H19 family — a green
|
|
970
|
+
// verdict over work that is not there — and it was reintroduced here once.
|
|
971
|
+
if ((0, AdversarialProbe_1.shouldProbe)({
|
|
972
|
+
profile: settings.adversaryProfile,
|
|
973
|
+
gatePassed: true,
|
|
974
|
+
alreadyProbed: probe.ran,
|
|
975
|
+
})) {
|
|
976
|
+
info(`${req.requirement}: probing with '${settings.adversaryProfile}'`);
|
|
977
|
+
probe = runAdversarialProbe(worktreeDir, req, settings, timeoutMs, (instruction) => (0, prompt_1.buildPrompt)(req, worktreeDir, {
|
|
978
|
+
promptPrefix: settings.promptPrefix,
|
|
979
|
+
hint: instruction,
|
|
980
|
+
withPrecedents: settings.promptPrecedents === true,
|
|
981
|
+
}));
|
|
982
|
+
if (probe.broke) {
|
|
983
|
+
const finding = (0, AdversarialProbe_1.probeFinding)(req.requirement, probe);
|
|
984
|
+
warn(`${req.requirement}: ${finding.message} [${finding.code}]`);
|
|
985
|
+
warn(` fix: ${finding.fix}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
796
988
|
record("pass");
|
|
797
|
-
return {
|
|
989
|
+
return {
|
|
990
|
+
result: "pass",
|
|
991
|
+
attempts: attempt,
|
|
992
|
+
attemptLog,
|
|
993
|
+
// The probe never changes the verdict; it travels beside it so
|
|
994
|
+
// `harness report` and the run record can show what it found.
|
|
995
|
+
...(probe.ran ? { adversarial: probe } : {}),
|
|
996
|
+
};
|
|
798
997
|
}
|
|
799
998
|
// Every attempt is spent. Commit what the agent produced anyway, on the
|
|
800
999
|
// branch, before the worktree is removed.
|
|
@@ -1290,7 +1489,11 @@ function processRequirement(req, ctx) {
|
|
|
1290
1489
|
const args = resuming
|
|
1291
1490
|
? ["worktree", "add", dir, branch]
|
|
1292
1491
|
: ["worktree", "add", "-b", branch, dir, baseRef];
|
|
1293
|
-
|
|
1492
|
+
// Serialised across processes: `--concurrency` gives each requirement its
|
|
1493
|
+
// own process, and two `git worktree add` calls in one repository race on
|
|
1494
|
+
// the half-written `.git/worktrees/<name>/` entry. Only the creation is
|
|
1495
|
+
// inside the lock — the agent and the gate stay parallel.
|
|
1496
|
+
const add = (0, worktree_lock_1.withWorktreeLock)(projectDir, () => git(projectDir, args));
|
|
1294
1497
|
if (add.status !== 0) {
|
|
1295
1498
|
return {
|
|
1296
1499
|
requirement: req.requirement,
|
|
@@ -1824,6 +2027,7 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
1824
2027
|
const prompt = (0, prompt_1.buildPrompt)(req, projectDir, {
|
|
1825
2028
|
promptPrefix: settings.promptPrefix,
|
|
1826
2029
|
hint: hintByReq.get(req.requirement),
|
|
2030
|
+
withPrecedents: settings.promptPrecedents === true,
|
|
1827
2031
|
});
|
|
1828
2032
|
process.stdout.write(`\n${"═".repeat(72)}\n${req.requirement} (${req.category}) → branch harness/${req.requirement}\n${"═".repeat(72)}\n`);
|
|
1829
2033
|
process.stdout.write(prompt + "\n");
|
|
@@ -1865,6 +2069,7 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
1865
2069
|
budgetSeconds: args.budgetSeconds,
|
|
1866
2070
|
maxRequirements: args.maxRequirements,
|
|
1867
2071
|
strictArtifacts: args.strictArtifacts,
|
|
2072
|
+
verbose: args.verbose,
|
|
1868
2073
|
};
|
|
1869
2074
|
const startedAt = new Date().toISOString();
|
|
1870
2075
|
const results = await runLevels(pending, ctx, {
|