@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/README.md +17 -25
  3. package/bin/specgate.ts +23 -5
  4. package/dist/bin/specgate.js +19 -4
  5. package/dist/bin/specgate.js.map +1 -1
  6. package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js +56 -0
  7. package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js.map +1 -1
  8. package/dist/packages/core/src/application/ValidateProjectUseCase.js +54 -0
  9. package/dist/packages/core/src/application/ValidateProjectUseCase.js.map +1 -1
  10. package/dist/packages/core/src/domain/AdversarialProbe.js +75 -0
  11. package/dist/packages/core/src/domain/AdversarialProbe.js.map +1 -0
  12. package/dist/packages/core/src/domain/AgentPrompt.js +52 -3
  13. package/dist/packages/core/src/domain/AgentPrompt.js.map +1 -1
  14. package/dist/packages/core/src/domain/DeclaredArtifacts.js +25 -0
  15. package/dist/packages/core/src/domain/DeclaredArtifacts.js.map +1 -1
  16. package/dist/packages/core/src/domain/DoneVerification.js +89 -0
  17. package/dist/packages/core/src/domain/DoneVerification.js.map +1 -0
  18. package/dist/packages/core/src/domain/GateCommandIntegrity.js +139 -0
  19. package/dist/packages/core/src/domain/GateCommandIntegrity.js.map +1 -0
  20. package/dist/packages/core/src/domain/GherkinQuality.js +19 -3
  21. package/dist/packages/core/src/domain/GherkinQuality.js.map +1 -1
  22. package/dist/packages/core/src/domain/HarnessConfig.js +6 -0
  23. package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -1
  24. package/dist/packages/core/src/domain/MultiStack.js +96 -0
  25. package/dist/packages/core/src/domain/MultiStack.js.map +1 -0
  26. package/dist/packages/core/src/domain/NewRequirement.js +46 -0
  27. package/dist/packages/core/src/domain/NewRequirement.js.map +1 -0
  28. package/dist/packages/core/src/domain/Precedents.js +96 -0
  29. package/dist/packages/core/src/domain/Precedents.js.map +1 -0
  30. package/dist/packages/core/src/domain/RequirementPlan.js +1 -0
  31. package/dist/packages/core/src/domain/RequirementPlan.js.map +1 -1
  32. package/dist/packages/core/src/domain/RequirementRemoval.js +161 -0
  33. package/dist/packages/core/src/domain/RequirementRemoval.js.map +1 -0
  34. package/dist/packages/core/src/domain/ScenarioCoverage.js +178 -0
  35. package/dist/packages/core/src/domain/ScenarioCoverage.js.map +1 -0
  36. package/dist/packages/core/src/domain/SpecSections.js +206 -0
  37. package/dist/packages/core/src/domain/SpecSections.js.map +1 -0
  38. package/dist/packages/core/src/domain/TestCommand.js +82 -0
  39. package/dist/packages/core/src/domain/TestCommand.js.map +1 -0
  40. package/dist/packages/core/src/domain/TraceabilityFormat.js +45 -2
  41. package/dist/packages/core/src/domain/TraceabilityFormat.js.map +1 -1
  42. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +31 -0
  43. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
  44. package/dist/packages/core/test/unit/adversarial-probe.test.js +81 -0
  45. package/dist/packages/core/test/unit/adversarial-probe.test.js.map +1 -0
  46. package/dist/packages/core/test/unit/done-verification.test.js +84 -0
  47. package/dist/packages/core/test/unit/done-verification.test.js.map +1 -0
  48. package/dist/packages/core/test/unit/gate-command-integrity.test.js +114 -0
  49. package/dist/packages/core/test/unit/gate-command-integrity.test.js.map +1 -0
  50. package/dist/packages/core/test/unit/gherkin-quality.test.js +28 -0
  51. package/dist/packages/core/test/unit/gherkin-quality.test.js.map +1 -1
  52. package/dist/packages/core/test/unit/multi-stack.test.js +86 -0
  53. package/dist/packages/core/test/unit/multi-stack.test.js.map +1 -0
  54. package/dist/packages/core/test/unit/new-requirement.test.js +67 -0
  55. package/dist/packages/core/test/unit/new-requirement.test.js.map +1 -0
  56. package/dist/packages/core/test/unit/precedents.test.js +99 -0
  57. package/dist/packages/core/test/unit/precedents.test.js.map +1 -0
  58. package/dist/packages/core/test/unit/requirement-removal.test.js +124 -0
  59. package/dist/packages/core/test/unit/requirement-removal.test.js.map +1 -0
  60. package/dist/packages/core/test/unit/scenario-coverage.test.js +146 -0
  61. package/dist/packages/core/test/unit/scenario-coverage.test.js.map +1 -0
  62. package/dist/packages/core/test/unit/spec-sections.test.js +146 -0
  63. package/dist/packages/core/test/unit/spec-sections.test.js.map +1 -0
  64. package/dist/packages/core/test/unit/test-command.test.js +81 -0
  65. package/dist/packages/core/test/unit/test-command.test.js.map +1 -0
  66. package/dist/packages/core/test/unit/validate-project.test.js +21 -0
  67. package/dist/packages/core/test/unit/validate-project.test.js.map +1 -1
  68. package/dist/scripts/agents/commands.js +1 -1
  69. package/dist/scripts/agents/commands.js.map +1 -1
  70. package/dist/scripts/agents/init.js +59 -15
  71. package/dist/scripts/agents/init.js.map +1 -1
  72. package/dist/scripts/change/instructions.js +2 -2
  73. package/dist/scripts/change/instructions.js.map +1 -1
  74. package/dist/scripts/check.js +10 -0
  75. package/dist/scripts/check.js.map +1 -0
  76. package/dist/scripts/cli/commands/change/ChangeCommand.js +2 -2
  77. package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
  78. package/dist/scripts/cli/commands/config/CiInitCommand.js +1 -1
  79. package/dist/scripts/cli/commands/config/CiInitCommand.js.map +1 -1
  80. package/dist/scripts/cli/commands/harness/RunCommand.js +214 -9
  81. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
  82. package/dist/scripts/cli/commands/pack/InferPackCommand.js +331 -31
  83. package/dist/scripts/cli/commands/pack/InferPackCommand.js.map +1 -1
  84. package/dist/scripts/cli/commands/project/AdoptCommand.js +86 -9
  85. package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
  86. package/dist/scripts/cli/commands/project/InitProjectCommand.js +286 -45
  87. package/dist/scripts/cli/commands/project/InitProjectCommand.js.map +1 -1
  88. package/dist/scripts/cli/commands/project/UpdateCommand.js +118 -8
  89. package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -1
  90. package/dist/scripts/cli/commands/quality/CheckCommand.js +184 -0
  91. package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -0
  92. package/dist/scripts/cli/commands/quality/DoctorCommand.js +20 -2
  93. package/dist/scripts/cli/commands/quality/DoctorCommand.js.map +1 -1
  94. package/dist/scripts/cli/commands/quality/FixCommand.js +1 -1
  95. package/dist/scripts/cli/commands/quality/FixCommand.js.map +1 -1
  96. package/dist/scripts/cli/commands/quality/ReportCommand.js +1 -1
  97. package/dist/scripts/cli/commands/quality/ReportCommand.js.map +1 -1
  98. package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +406 -24
  99. package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -1
  100. package/dist/scripts/cli/commands/spec/DoneCommand.js +108 -0
  101. package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
  102. package/dist/scripts/cli/commands/spec/NewCommand.js +178 -0
  103. package/dist/scripts/cli/commands/spec/NewCommand.js.map +1 -0
  104. package/dist/scripts/cli/commands/spec/PlanCommand.js +8 -2
  105. package/dist/scripts/cli/commands/spec/PlanCommand.js.map +1 -1
  106. package/dist/scripts/cli/commands/spec/ReqCommand.js +124 -4
  107. package/dist/scripts/cli/commands/spec/ReqCommand.js.map +1 -1
  108. package/dist/scripts/cli/commands/spec/StatusCommand.js +84 -32
  109. package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
  110. package/dist/scripts/docs/build-site.js +145 -35
  111. package/dist/scripts/docs/build-site.js.map +1 -1
  112. package/dist/scripts/docs/components.js +208 -0
  113. package/dist/scripts/docs/components.js.map +1 -0
  114. package/dist/scripts/docs/highlight.js +180 -0
  115. package/dist/scripts/docs/highlight.js.map +1 -0
  116. package/dist/scripts/docs/icons.js +63 -0
  117. package/dist/scripts/docs/icons.js.map +1 -0
  118. package/dist/scripts/docs/nav.js +9 -3
  119. package/dist/scripts/docs/nav.js.map +1 -1
  120. package/dist/scripts/gen_mcp_tools.js +22 -2
  121. package/dist/scripts/gen_mcp_tools.js.map +1 -1
  122. package/dist/scripts/harness/init.js +101 -40
  123. package/dist/scripts/harness/init.js.map +1 -1
  124. package/dist/scripts/harness/worktree-lock.js +161 -0
  125. package/dist/scripts/harness/worktree-lock.js.map +1 -0
  126. package/dist/scripts/infer_pack.js +4 -1
  127. package/dist/scripts/infer_pack.js.map +1 -1
  128. package/dist/scripts/lib/surface.js +88 -38
  129. package/dist/scripts/lib/surface.js.map +1 -1
  130. package/dist/scripts/new.js +10 -0
  131. package/dist/scripts/new.js.map +1 -0
  132. package/dist/scripts/plugin/gate-hook.js +2 -2
  133. package/dist/scripts/plugin/gate-hook.js.map +1 -1
  134. package/package.json +5 -2
  135. package/templates/adopt/AI_RULES.md.tpl +1 -1
  136. package/templates/adopt/spec.md.tpl +4 -4
  137. package/templates/base/spec.md.tpl +12 -0
  138. package/templates/ci/azure.yml.tpl +2 -2
  139. package/templates/ci/github.yml.tpl +2 -2
  140. package/templates/ci/gitlab.yml.tpl +1 -1
  141. package/templates/ci/jenkins.tpl +1 -1
  142. package/templates/contracts/test-strategy.md.tpl +2 -2
  143. package/templates/harness/harness.config.yaml.tpl +3 -3
  144. 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-tdd` + the project test command are the reward signal,
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-tdd` + the project test command.
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-tdd, then the optional test command). */
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
- const validate = (0, node_child_process_1.spawnSync)(process.execPath, [VALIDATE_SCRIPT, worktreeDir, "--strict-tdd"], {
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-tdd",
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: test.stdout + test.stderr,
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
- const gate = runGate(worktreeDir, settings.testCmd, timeoutMs, req, settings);
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 { result: "pass", attempts: attempt, attemptLog };
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
- const add = git(projectDir, args);
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, {