@rsaglobaltech/specgate 0.20.0 → 0.21.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 +115 -0
- package/README.md +31 -31
- package/bin/specgate.ts +25 -19
- package/dist/bin/specgate.js +15 -10
- package/dist/bin/specgate.js.map +1 -1
- package/dist/packages/core/src/application/AddRequirementUseCase.js +11 -1
- package/dist/packages/core/src/application/AddRequirementUseCase.js.map +1 -1
- package/dist/packages/core/src/application/ArchiveChangeUseCase.js +7 -2
- package/dist/packages/core/src/application/ArchiveChangeUseCase.js.map +1 -1
- package/dist/packages/core/src/application/ValidateProjectUseCase.js +9 -3
- package/dist/packages/core/src/application/ValidateProjectUseCase.js.map +1 -1
- package/dist/packages/core/src/domain/ArchivePlan.js.map +1 -1
- package/dist/packages/core/src/domain/ChangeTemplates.js +19 -0
- package/dist/packages/core/src/domain/ChangeTemplates.js.map +1 -1
- package/dist/packages/core/src/domain/DeltaSpec.js +14 -7
- package/dist/packages/core/src/domain/DeltaSpec.js.map +1 -1
- package/dist/packages/core/src/domain/DerivedMatrix.js +2 -2
- package/dist/packages/core/src/domain/DerivedMatrix.js.map +1 -1
- package/dist/packages/core/src/domain/Diagnostic.js.map +1 -1
- package/dist/packages/core/src/domain/DoneVerification.js +3 -3
- package/dist/packages/core/src/domain/DoneVerification.js.map +1 -1
- package/dist/packages/core/src/domain/DraftChecklist.js +1 -1
- package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
- package/dist/packages/core/src/domain/GherkinQuality.js +5 -2
- package/dist/packages/core/src/domain/GherkinQuality.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessEvents.js +129 -0
- package/dist/packages/core/src/domain/HarnessEvents.js.map +1 -0
- package/dist/packages/core/src/domain/HarnessRun.js.map +1 -1
- package/dist/packages/core/src/domain/Language.js +5 -2
- package/dist/packages/core/src/domain/Language.js.map +1 -1
- package/dist/packages/core/src/domain/PackContribution.js.map +1 -1
- package/dist/packages/core/src/domain/PackDelta.js +5 -2
- package/dist/packages/core/src/domain/PackDelta.js.map +1 -1
- package/dist/packages/core/src/domain/PackSpec.js +16 -2
- package/dist/packages/core/src/domain/PackSpec.js.map +1 -1
- package/dist/packages/core/src/domain/QuotedText.js +36 -0
- package/dist/packages/core/src/domain/QuotedText.js.map +1 -0
- package/dist/packages/core/src/domain/RequirementPlan.js.map +1 -1
- package/dist/packages/core/src/domain/SpecParser.js +2 -9
- package/dist/packages/core/src/domain/SpecParser.js.map +1 -1
- package/dist/packages/core/src/domain/TraceabilityFormat.js.map +1 -1
- package/dist/packages/core/src/domain/TraceabilityMatrix.js +3 -3
- package/dist/packages/core/src/domain/TraceabilityMatrix.js.map +1 -1
- package/dist/packages/core/src/domain/YamlLite.js +3 -0
- package/dist/packages/core/src/domain/YamlLite.js.map +1 -1
- package/dist/packages/core/src/infrastructure/ChangeWorkspace.js.map +1 -1
- package/dist/packages/core/src/infrastructure/ConsoleReporter.js.map +1 -1
- package/dist/packages/core/src/infrastructure/DiskLanguageRepository.js +1 -0
- package/dist/packages/core/src/infrastructure/DiskLanguageRepository.js.map +1 -1
- package/dist/packages/core/src/infrastructure/DiskPackDeltaRepository.js +3 -1
- package/dist/packages/core/src/infrastructure/DiskPackDeltaRepository.js.map +1 -1
- package/dist/packages/core/src/infrastructure/DiskPackRepository.js +0 -2
- package/dist/packages/core/src/infrastructure/DiskPackRepository.js.map +1 -1
- package/dist/packages/core/src/infrastructure/DiskRequirementGraphRepository.js.map +1 -1
- package/dist/packages/core/src/infrastructure/GitContributionStager.js +0 -4
- package/dist/packages/core/src/infrastructure/GitContributionStager.js.map +1 -1
- package/dist/packages/core/src/infrastructure/GitMergeDriver.js +0 -3
- package/dist/packages/core/src/infrastructure/GitMergeDriver.js.map +1 -1
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +7 -13
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
- package/dist/packages/core/src/infrastructure/PackChangeDeposit.js.map +1 -1
- package/dist/packages/core/src/infrastructure/RemotePackResolver.js +0 -7
- package/dist/packages/core/src/infrastructure/RemotePackResolver.js.map +1 -1
- package/dist/packages/core/src/infrastructure/SpecopsConfigFile.js.map +1 -1
- package/dist/packages/core/test/unit/declared-artifacts.test.js +6 -2
- package/dist/packages/core/test/unit/declared-artifacts.test.js.map +1 -1
- package/dist/packages/core/test/unit/derived-matrix.test.js +3 -0
- package/dist/packages/core/test/unit/derived-matrix.test.js.map +1 -1
- package/dist/packages/core/test/unit/done-verification.test.js +3 -0
- package/dist/packages/core/test/unit/done-verification.test.js.map +1 -1
- package/dist/packages/core/test/unit/matrix-dependencies.test.js.map +1 -1
- package/dist/packages/core/test/unit/reconciliation.test.js.map +1 -1
- package/dist/packages/core/test/unit/requirement-readiness.test.js +2 -2
- package/dist/packages/core/test/unit/requirement-readiness.test.js.map +1 -1
- package/dist/packages/core/test/unit/resume-state.test.js +4 -1
- package/dist/packages/core/test/unit/resume-state.test.js.map +1 -1
- package/dist/packages/core/test/unit/run-budget.test.js +3 -0
- package/dist/packages/core/test/unit/run-budget.test.js.map +1 -1
- package/dist/packages/core/test/unit/untouched-delta.test.js +52 -0
- package/dist/packages/core/test/unit/untouched-delta.test.js.map +1 -0
- package/dist/packages/core/test/unit/validate-project.test.js +6 -2
- package/dist/packages/core/test/unit/validate-project.test.js.map +1 -1
- package/dist/packages/mcp-spec-driven/src/server.js +4 -3
- package/dist/packages/mcp-spec-driven/src/server.js.map +1 -1
- package/dist/packages/mcp-spec-driven/src/tools.js.map +1 -1
- package/dist/scripts/agents/commands.js +49 -37
- package/dist/scripts/agents/commands.js.map +1 -1
- package/dist/scripts/agents/detect.js.map +1 -1
- package/dist/scripts/agents/init.js.map +1 -1
- package/dist/scripts/alm/cli.js +2 -2
- package/dist/scripts/alm/cli.js.map +1 -1
- package/dist/scripts/alm/core.js +1 -0
- package/dist/scripts/alm/core.js.map +1 -1
- package/dist/scripts/apply.js +9 -0
- package/dist/scripts/apply.js.map +1 -0
- package/dist/scripts/archive.js +9 -0
- package/dist/scripts/archive.js.map +1 -0
- package/dist/scripts/change/archive.js.map +1 -1
- package/dist/scripts/change/artifacts.js.map +1 -1
- package/dist/scripts/change/delta.js +0 -8
- package/dist/scripts/change/delta.js.map +1 -1
- package/dist/scripts/change/instructions.js +9 -11
- package/dist/scripts/change/instructions.js.map +1 -1
- package/dist/scripts/cli/commands/alm/PullCommand.js +4 -2
- package/dist/scripts/cli/commands/alm/PullCommand.js.map +1 -1
- package/dist/scripts/cli/commands/change/ChangeCommand.js +91 -17
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/config/ConfigSetCommand.js +1 -1
- package/dist/scripts/cli/commands/config/ConfigSetCommand.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +222 -34
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/mcp/McpInstallCommand.js +1 -0
- package/dist/scripts/cli/commands/mcp/McpInstallCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/BundlePackCommand.js +2 -1
- package/dist/scripts/cli/commands/pack/BundlePackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/ExpandDomainPackCommand.js +32 -17
- package/dist/scripts/cli/commands/pack/ExpandDomainPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/InferPackCommand.js +8 -5
- package/dist/scripts/cli/commands/pack/InferPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/LintPackCommand.js +36 -44
- package/dist/scripts/cli/commands/pack/LintPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/pack-args.js.map +1 -1
- package/dist/scripts/cli/commands/project/AddCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js +4 -2
- package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/InitFromPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/InitProjectCommand.js +2 -1
- package/dist/scripts/cli/commands/project/InitProjectCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/OnboardCommand.js +40 -4
- package/dist/scripts/cli/commands/project/OnboardCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/WizardCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +74 -10
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/DoctorCommand.js +8 -4
- package/dist/scripts/cli/commands/quality/DoctorCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/FixCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/LicenseCheckCommand.js +3 -1
- package/dist/scripts/cli/commands/quality/LicenseCheckCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ReportCommand.js +8 -3
- package/dist/scripts/cli/commands/quality/ReportCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +39 -30
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/VerifyCommand.js +3 -2
- package/dist/scripts/cli/commands/quality/VerifyCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js +182 -77
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DraftCommand.js +55 -32
- package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ExploreCommand.js +224 -0
- package/dist/scripts/cli/commands/spec/ExploreCommand.js.map +1 -0
- package/dist/scripts/cli/commands/spec/MatrixCommand.js +35 -5
- package/dist/scripts/cli/commands/spec/MatrixCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/NewCommand.js +4 -4
- package/dist/scripts/cli/commands/spec/NewCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/PetCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/PlanCommand.js +8 -3
- package/dist/scripts/cli/commands/spec/PlanCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ReqCommand.js +8 -9
- package/dist/scripts/cli/commands/spec/ReqCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StatusCommand.js +88 -21
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StudioCommand.js +76 -4
- package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
- package/dist/scripts/cli/commands/specops/ContributeCommand.js +6 -2
- package/dist/scripts/cli/commands/specops/ContributeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/specops/DiffCommand.js +19 -5
- package/dist/scripts/cli/commands/specops/DiffCommand.js.map +1 -1
- package/dist/scripts/cli/commands/specops/SyncCommand.js +4 -2
- package/dist/scripts/cli/commands/specops/SyncCommand.js.map +1 -1
- package/dist/scripts/docs/build-site.js.map +1 -1
- package/dist/scripts/docs/icons.js +0 -6
- package/dist/scripts/docs/icons.js.map +1 -1
- package/dist/scripts/docs/journey-storyboard.js +23 -20
- package/dist/scripts/docs/journey-storyboard.js.map +1 -1
- package/dist/scripts/docs/record-terminal.js +1 -1
- package/dist/scripts/docs/record-terminal.js.map +1 -1
- package/dist/scripts/docs/terminal-storyboard.js +24 -21
- package/dist/scripts/docs/terminal-storyboard.js.map +1 -1
- package/dist/scripts/draft/brief-setup.js +8 -7
- package/dist/scripts/draft/brief-setup.js.map +1 -1
- package/dist/scripts/draft/confined-agent.js +1 -0
- package/dist/scripts/draft/confined-agent.js.map +1 -1
- package/dist/scripts/draft/drafting-guide.js +6 -5
- package/dist/scripts/draft/drafting-guide.js.map +1 -1
- package/dist/scripts/explore.js +10 -0
- package/dist/scripts/explore.js.map +1 -0
- package/dist/scripts/gen_mcp_tools.js +15 -7
- package/dist/scripts/gen_mcp_tools.js.map +1 -1
- package/dist/scripts/harness/events.js +202 -0
- package/dist/scripts/harness/events.js.map +1 -0
- package/dist/scripts/harness/init.js +6 -2
- package/dist/scripts/harness/init.js.map +1 -1
- package/dist/scripts/harness/run.js +2 -1
- package/dist/scripts/harness/run.js.map +1 -1
- package/dist/scripts/lib/agent.js.map +1 -1
- package/dist/scripts/lib/base-ids.js +179 -0
- package/dist/scripts/lib/base-ids.js.map +1 -0
- package/dist/scripts/lib/capability-specs.js.map +1 -1
- package/dist/scripts/lib/diagnostics.js +0 -10
- package/dist/scripts/lib/diagnostics.js.map +1 -1
- package/dist/scripts/lib/harness-events.js +74 -0
- package/dist/scripts/lib/harness-events.js.map +1 -0
- package/dist/scripts/lib/in-flight.js +191 -0
- package/dist/scripts/lib/in-flight.js.map +1 -0
- package/dist/scripts/lib/live-model.js +145 -0
- package/dist/scripts/lib/live-model.js.map +1 -0
- package/dist/scripts/lib/live-page.js +89 -0
- package/dist/scripts/lib/live-page.js.map +1 -0
- package/dist/scripts/lib/monorepo-fanout.js.map +1 -1
- package/dist/scripts/lib/placeholders.js.map +1 -1
- package/dist/scripts/lib/project-root.js.map +1 -1
- package/dist/scripts/lib/run-with-deadline.js +6 -1
- package/dist/scripts/lib/run-with-deadline.js.map +1 -1
- package/dist/scripts/lib/surface.js +95 -30
- package/dist/scripts/lib/surface.js.map +1 -1
- package/dist/scripts/lib/tour.js +149 -0
- package/dist/scripts/lib/tour.js.map +1 -0
- package/dist/scripts/propose.js +21 -0
- package/dist/scripts/propose.js.map +1 -0
- package/dist/scripts/schema/cli.js +1 -1
- package/dist/scripts/schema/cli.js.map +1 -1
- package/dist/scripts/schema/registry.js +1 -0
- package/dist/scripts/schema/registry.js.map +1 -1
- package/dist/scripts/specops/add.js +2 -1
- package/dist/scripts/specops/add.js.map +1 -1
- package/dist/scripts/specops/against_lock.js +22 -25
- package/dist/scripts/specops/against_lock.js.map +1 -1
- package/dist/scripts/specops/as_change.js.map +1 -1
- package/dist/scripts/specops/lock.js +0 -5
- package/dist/scripts/specops/lock.js.map +1 -1
- package/dist/scripts/specops/manifest.js +0 -8
- package/dist/scripts/specops/manifest.js.map +1 -1
- package/dist/scripts/specops/remove.js +2 -1
- package/dist/scripts/specops/remove.js.map +1 -1
- package/dist/scripts/specops/verify.js.map +1 -1
- package/package.json +1 -1
- package/templates/adopt/spec.md.tpl +1 -1
- package/templates/adopt/traceability.md.tpl +1 -1
- package/templates/base/AGENTS.md.tpl +12 -6
- package/templates/harness/prompt-prefix.md.tpl +1 -1
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* 5. Emit a pass/fail/attempts report.
|
|
20
20
|
*
|
|
21
21
|
* Vendor-neutral by construction: the agent is any shell command. The
|
|
22
|
-
* harness never merges a branch — a human reviews and merges
|
|
22
|
+
* harness never merges a branch — a human reviews and merges — except with
|
|
23
|
+
* `--chain`, which a person asks for and which merges only what passed the
|
|
24
|
+
* gate twice: on its branch and merged (ADR-0036).
|
|
23
25
|
*/
|
|
24
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
27
|
if (k2 === undefined) k2 = k;
|
|
@@ -75,6 +77,25 @@ const node_child_process_1 = require("node:child_process");
|
|
|
75
77
|
const command_1 = require("../../../lib/command");
|
|
76
78
|
const project_root_1 = require("../../../lib/project-root");
|
|
77
79
|
const prompt_1 = require("../../../harness/prompt");
|
|
80
|
+
const init_1 = require("../../../harness/init");
|
|
81
|
+
const in_flight_1 = require("../../../lib/in-flight");
|
|
82
|
+
const harness_events_1 = require("../../../lib/harness-events");
|
|
83
|
+
const HarnessEvents_1 = require("../../../../packages/core/src/domain/HarnessEvents");
|
|
84
|
+
/**
|
|
85
|
+
* `read` against the project as it will be after its open changes are
|
|
86
|
+
* archived (ADR-0035), so the harness sees, prompts for and gates a change's
|
|
87
|
+
* requirements while they are in flight. The project itself when none is open.
|
|
88
|
+
*/
|
|
89
|
+
function judged(dir, read) {
|
|
90
|
+
const view = (0, in_flight_1.inFlightView)(dir);
|
|
91
|
+
try {
|
|
92
|
+
return read(view ? view.dir : dir, view ? view.toProject : (text) => text);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (view)
|
|
96
|
+
view.cleanup();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
78
99
|
const GherkinQuality_1 = require("../../../../packages/core/src/domain/GherkinQuality");
|
|
79
100
|
const HarnessConfigFile_1 = require("../../../../packages/core/src/infrastructure/HarnessConfigFile");
|
|
80
101
|
const agent_1 = require("../../../lib/agent");
|
|
@@ -156,6 +177,15 @@ function usage() {
|
|
|
156
177
|
" --req <REQ-NNN> Limit to specific requirement(s); repeatable.\n" +
|
|
157
178
|
" --project-dir <path> Project root (auto-detected from cwd if omitted).\n" +
|
|
158
179
|
" --base-branch <ref> Branch/ref each worktree is cut from (default: current HEAD).\n" +
|
|
180
|
+
" --setup-cmd <cmd> Run in each new worktree before the agent. Default: the\n" +
|
|
181
|
+
" install the lockfile implies (npm ci, pnpm, yarn);\n" +
|
|
182
|
+
" `none` runs nothing. The agent gets SPECGATE_REQ and\n" +
|
|
183
|
+
" SPECGATE_ATTEMPT in its environment.\n" +
|
|
184
|
+
" --chain One requirement at a time, in order, each cut from the\n" +
|
|
185
|
+
" previous merge: a green one is merged into the branch\n" +
|
|
186
|
+
" you are on (a merge commit, its prompts left on its\n" +
|
|
187
|
+
" branch), the gate runs again on the merge, and a red\n" +
|
|
188
|
+
" result is undone. Stops at the first failure.\n" +
|
|
159
189
|
" --timeout <seconds> Per-agent-invocation timeout (default 1200).\n" +
|
|
160
190
|
" --keep-worktrees Do not remove worktrees after each requirement.\n" +
|
|
161
191
|
" --force Recreate harness/REQ-NNN branches that already exist.\n" +
|
|
@@ -212,6 +242,8 @@ function parseArgs(argv) {
|
|
|
212
242
|
push: false,
|
|
213
243
|
remote: "",
|
|
214
244
|
prCmd: "",
|
|
245
|
+
setupCmd: "",
|
|
246
|
+
chain: false,
|
|
215
247
|
};
|
|
216
248
|
for (let i = 0; i < argv.length; i += 1) {
|
|
217
249
|
const token = argv[i];
|
|
@@ -247,6 +279,12 @@ function parseArgs(argv) {
|
|
|
247
279
|
else if (token === "--base-branch") {
|
|
248
280
|
args.baseBranch = argv[++i] || "";
|
|
249
281
|
}
|
|
282
|
+
else if (token === "--setup-cmd") {
|
|
283
|
+
args.setupCmd = argv[++i] || "";
|
|
284
|
+
}
|
|
285
|
+
else if (token === "--chain") {
|
|
286
|
+
args.chain = true;
|
|
287
|
+
}
|
|
250
288
|
else if (token === "--timeout") {
|
|
251
289
|
const n = Number(argv[++i]);
|
|
252
290
|
if (!Number.isInteger(n) || n < 1)
|
|
@@ -335,9 +373,9 @@ function branchExists(projectDir, branch) {
|
|
|
335
373
|
return r.status === 0 && r.stdout.trim() !== "";
|
|
336
374
|
}
|
|
337
375
|
function runPlan(projectDir) {
|
|
338
|
-
const r = (0, node_child_process_1.spawnSync)(process.execPath, [PLAN_SCRIPT, "--project-dir",
|
|
376
|
+
const r = judged(projectDir, (dir) => (0, node_child_process_1.spawnSync)(process.execPath, [PLAN_SCRIPT, "--project-dir", dir, "--format", "json"], {
|
|
339
377
|
encoding: "utf8",
|
|
340
|
-
});
|
|
378
|
+
}));
|
|
341
379
|
if (r.status !== 0) {
|
|
342
380
|
throw new Error(`plan failed:\n${r.stderr || r.stdout}`);
|
|
343
381
|
}
|
|
@@ -346,6 +384,7 @@ function runPlan(projectDir) {
|
|
|
346
384
|
parsed = JSON.parse(r.stdout);
|
|
347
385
|
}
|
|
348
386
|
catch (err) {
|
|
387
|
+
// `JSON.parse` only ever throws a SyntaxError.
|
|
349
388
|
throw new Error(`plan produced invalid JSON: ${err.message}`);
|
|
350
389
|
}
|
|
351
390
|
return parsed;
|
|
@@ -388,21 +427,26 @@ function readGateMessages(worktreeDir, reportPath) {
|
|
|
388
427
|
const run = (0, CucumberMessages_1.parseCucumberMessages)(raw);
|
|
389
428
|
return run.parsed ? run : null;
|
|
390
429
|
}
|
|
391
|
-
function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
|
|
430
|
+
function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, report = () => { }) {
|
|
392
431
|
// `--delivering`: the requirement is still Draft while its gate runs, and a
|
|
393
432
|
// Draft row owes no files. Without it the gate would skip the one
|
|
394
433
|
// requirement this attempt exists to deliver.
|
|
395
434
|
// The agent's tests are new evidence for a generated matrix: refresh it so
|
|
396
435
|
// the gate judges the attempt, not the matrix as it was before it.
|
|
397
436
|
(0, MatrixCommand_1.refreshDerivedMatrix)(worktreeDir);
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
437
|
+
const validate = judged(worktreeDir, (dir, toProject) => {
|
|
438
|
+
const validateArgs = [VALIDATE_SCRIPT, dir, "--strict"];
|
|
439
|
+
if (req.requirement)
|
|
440
|
+
validateArgs.push("--delivering", req.requirement);
|
|
441
|
+
const run = (0, node_child_process_1.spawnSync)(process.execPath, validateArgs, {
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
timeout: timeoutMs,
|
|
444
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
445
|
+
});
|
|
446
|
+
// The agent is told about its worktree, not about the copy it was judged in.
|
|
447
|
+
return { ...run, stdout: toProject(run.stdout || ""), stderr: toProject(run.stderr || "") };
|
|
405
448
|
});
|
|
449
|
+
report("validate --strict", validate.status === 0 ? "pass" : "fail");
|
|
406
450
|
if (validate.status !== 0) {
|
|
407
451
|
return {
|
|
408
452
|
ok: false,
|
|
@@ -422,6 +466,7 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
|
|
|
422
466
|
timeout: timeoutMs,
|
|
423
467
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
424
468
|
});
|
|
469
|
+
report(`architecture check: ${archCmd}`, arch.status === 0 ? "pass" : "fail");
|
|
425
470
|
if (arch.status !== 0) {
|
|
426
471
|
return {
|
|
427
472
|
ok: false,
|
|
@@ -463,6 +508,7 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
|
|
|
463
508
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
464
509
|
stdio: settings.verbose ? ["ignore", 2, 2] : undefined,
|
|
465
510
|
});
|
|
511
|
+
report(`test command: ${(0, HarnessEvents_1.maskCommand)(resolved)}`, test.status === 0 ? "pass" : "fail", (0, HarnessEvents_1.testCounts)(`${test.stdout || ""}${test.stderr || ""}`));
|
|
466
512
|
if (test.status !== 0) {
|
|
467
513
|
// Name the command. A gate that silently does the wrong thing — running
|
|
468
514
|
// the whole suite because a filter did not apply, say — produces a
|
|
@@ -732,7 +778,7 @@ function checkWriteScopeInWorktree(worktreeDir, settings) {
|
|
|
732
778
|
`makes a passing gate meaningless. Revert them and satisfy the scenario as written.\n` +
|
|
733
779
|
(evidence ? `\nWhat was changed:\n\n\`\`\`diff\n${evidence}\n\`\`\`\n` : "") +
|
|
734
780
|
`\nIf the change is genuinely required, it belongs in a spec change ` +
|
|
735
|
-
`(\`specgate
|
|
781
|
+
`(\`specgate propose\`), reviewed by a person — not in this attempt.`,
|
|
736
782
|
};
|
|
737
783
|
}
|
|
738
784
|
/**
|
|
@@ -765,12 +811,14 @@ function checkGateCommandInWorktree(worktreeDir, baseRef, testCmd) {
|
|
|
765
811
|
const result = (0, GateCommandIntegrity_1.gateCommandIntegrity)(testCmd, before, after);
|
|
766
812
|
if (!result.checked || !result.changed)
|
|
767
813
|
return null;
|
|
814
|
+
// A checked result always names its script: the probe above resolved it.
|
|
815
|
+
const changed = result.script;
|
|
768
816
|
return {
|
|
769
817
|
manifest,
|
|
770
|
-
script:
|
|
818
|
+
script: changed.script,
|
|
771
819
|
message: `agent_rewrote_gate_command — the gate runs \`${testCmd}\`, and this attempt ` +
|
|
772
820
|
`changed what that means.\n\n` +
|
|
773
|
-
` ${manifest} → scripts.${
|
|
821
|
+
` ${manifest} → scripts.${changed.script}\n` +
|
|
774
822
|
` before: ${result.before === null ? "(absent)" : result.before}\n` +
|
|
775
823
|
` after: ${result.after === null ? "(absent)" : result.after}\n\n` +
|
|
776
824
|
`The command the gate runs is part of the contract this requirement is ` +
|
|
@@ -842,13 +890,23 @@ function attemptRequirement(req, ctx) {
|
|
|
842
890
|
const firstAttempt = Math.max(1, (ctx.resumeAt && ctx.resumeAt.attempt) || 1);
|
|
843
891
|
/** Set when another attempt cannot change the outcome (#50). */
|
|
844
892
|
let stopRetrying = false;
|
|
893
|
+
// The live view's file (ADR-0037), in the project, not the worktree.
|
|
894
|
+
const eventsFile = (0, harness_events_1.eventsFileFor)(ctx.projectDir || worktreeDir, req.requirement);
|
|
845
895
|
for (let attempt = firstAttempt; attempt <= settings.maxAttempts; attempt += 1) {
|
|
846
896
|
info(`${req.requirement}: attempt ${attempt}/${settings.maxAttempts}`);
|
|
847
897
|
const attemptStart = Date.now();
|
|
898
|
+
const event = (e) => (0, harness_events_1.appendEvent)(eventsFile, e, req.requirement, attempt);
|
|
899
|
+
event({ type: "attempt", state: "start" });
|
|
848
900
|
let agentMs = 0;
|
|
849
901
|
/** Which roles ran this attempt, in order — the run record's answer to "who did this". */
|
|
850
902
|
const profilesUsed = [];
|
|
851
903
|
const record = (stage) => {
|
|
904
|
+
event({
|
|
905
|
+
type: "attempt",
|
|
906
|
+
state: "end",
|
|
907
|
+
result: stage === "pass" ? "pass" : "fail",
|
|
908
|
+
...(stage === "pass" ? {} : { stage }),
|
|
909
|
+
});
|
|
852
910
|
attemptLog.push({
|
|
853
911
|
attempt,
|
|
854
912
|
endedAt: stage,
|
|
@@ -871,7 +929,7 @@ function attemptRequirement(req, ctx) {
|
|
|
871
929
|
// implementing step that follows must see what it said. Building once
|
|
872
930
|
// up front meant findings only ever reached the *next* attempt, which
|
|
873
931
|
// is not what a reviewer is for.
|
|
874
|
-
const prompt = (0, prompt_1.buildPrompt)(req,
|
|
932
|
+
const prompt = judged(worktreeDir, (dir) => (0, prompt_1.buildPrompt)(req, dir, {
|
|
875
933
|
promptPrefix: settings.promptPrefix,
|
|
876
934
|
hint,
|
|
877
935
|
previousFailure: previousFailure || undefined,
|
|
@@ -882,7 +940,7 @@ function attemptRequirement(req, ctx) {
|
|
|
882
940
|
// commit — the precedent must be work already accepted, not a
|
|
883
941
|
// sibling requirement this same run happens to be writing.
|
|
884
942
|
withPrecedents: settings.promptPrecedents === true,
|
|
885
|
-
});
|
|
943
|
+
}));
|
|
886
944
|
const promptFile = path.join(os.tmpdir(), `specgate-harness-prompt-${req.requirement}-${crypto.randomBytes(4).toString("hex")}.md`);
|
|
887
945
|
fs.writeFileSync(promptFile, prompt, "utf8");
|
|
888
946
|
archivePrompt(worktreeDir, req.requirement, attempt, step, prompt);
|
|
@@ -903,6 +961,15 @@ function attemptRequirement(req, ctx) {
|
|
|
903
961
|
cwd: worktreeDir,
|
|
904
962
|
timeoutMs,
|
|
905
963
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
964
|
+
// Which requirement and attempt this is, for agents and the wrappers
|
|
965
|
+
// around them (ADR-0036).
|
|
966
|
+
env: {
|
|
967
|
+
SPECGATE_REQ: req.requirement,
|
|
968
|
+
SPECGATE_ATTEMPT: String(attempt),
|
|
969
|
+
[HarnessEvents_1.EVENTS_FILE_VAR]: eventsFile,
|
|
970
|
+
[HarnessEvents_1.EVENTS_REQ_VAR]: req.requirement,
|
|
971
|
+
[HarnessEvents_1.EVENTS_ATTEMPT_VAR]: String(attempt),
|
|
972
|
+
},
|
|
906
973
|
});
|
|
907
974
|
// Node reports a timeout as an errno-carrying Error; the base `Error`
|
|
908
975
|
// type the spawnSync signature declares does not have `code`.
|
|
@@ -997,10 +1064,7 @@ function attemptRequirement(req, ctx) {
|
|
|
997
1064
|
}
|
|
998
1065
|
// `verbose` is a run flag, not a project setting, so it travels on the
|
|
999
1066
|
// context rather than in `harness.config.yaml`.
|
|
1000
|
-
const gate = runGate(worktreeDir, settings.testCmd, timeoutMs, req, {
|
|
1001
|
-
...settings,
|
|
1002
|
-
verbose: ctx.verbose,
|
|
1003
|
-
});
|
|
1067
|
+
const gate = runGate(worktreeDir, settings.testCmd, timeoutMs, req, { ...settings, verbose: ctx.verbose }, (stage, result, extra) => event({ type: "gate", stage, result, ...(extra || {}) }));
|
|
1004
1068
|
if (!gate.ok) {
|
|
1005
1069
|
previousFailure =
|
|
1006
1070
|
`Gate failed at: ${gate.stage}\n\n` + (gate.hint ? `⚠ ${gate.hint}\n\n` : "") + gate.output;
|
|
@@ -1260,7 +1324,7 @@ function resolveGeneratedConflicts(worktree) {
|
|
|
1260
1324
|
* @param passed REQ → branch, for the dependencies that passed in this run
|
|
1261
1325
|
*/
|
|
1262
1326
|
function deriveBase(projectDir, reqId, deps, passed, ctx) {
|
|
1263
|
-
const branches = deps.map((d) => passed.get(d)).filter(Boolean);
|
|
1327
|
+
const branches = deps.map((d) => passed.get(d)).filter((b) => Boolean(b));
|
|
1264
1328
|
if (branches.length === 0)
|
|
1265
1329
|
return { base: ctx.baseRef };
|
|
1266
1330
|
if (branches.length === 1)
|
|
@@ -1382,7 +1446,7 @@ function preserveFailedAttempt(worktreeDir, req, failure) {
|
|
|
1382
1446
|
`wip(${req.requirement}): FAILED the gate — do not merge as is\n\n` +
|
|
1383
1447
|
`${firstLine}\n\n` +
|
|
1384
1448
|
"Committed by `specgate harness run` so the attempt is reviewable rather than\n" +
|
|
1385
|
-
"discarded. The requirement is still Draft: `specgate
|
|
1449
|
+
"discarded. The requirement is still Draft: `specgate apply` never ran.",
|
|
1386
1450
|
]);
|
|
1387
1451
|
return commit.status === 0;
|
|
1388
1452
|
}
|
|
@@ -1811,6 +1875,7 @@ function printReport(results, format, recorded) {
|
|
|
1811
1875
|
process.stdout.write(` ${ui.muted(`Run recorded in ${recorded} — specgate harness report sums them up`)}\n`);
|
|
1812
1876
|
}
|
|
1813
1877
|
if (pass > 0) {
|
|
1878
|
+
// `pass > 0`, so there is one.
|
|
1814
1879
|
const first = results.find((r) => r.result === "pass");
|
|
1815
1880
|
process.stdout.write(ui.next(`git diff HEAD...${first.branch}`, "review and merge the harness/* branches you trust"));
|
|
1816
1881
|
}
|
|
@@ -1832,7 +1897,7 @@ function printReport(results, format, recorded) {
|
|
|
1832
1897
|
* scoped to one requirement, so a parallel run and a serial run execute the
|
|
1833
1898
|
* same code.
|
|
1834
1899
|
*/
|
|
1835
|
-
async
|
|
1900
|
+
const dispatchLevel = async (runnable, byId, hintByReq, ctx, concurrency) => {
|
|
1836
1901
|
if (concurrency <= 1) {
|
|
1837
1902
|
const results = [];
|
|
1838
1903
|
for (const id of runnable) {
|
|
@@ -1844,6 +1909,7 @@ async function dispatchLevel(runnable, byId, hintByReq, ctx, concurrency) {
|
|
|
1844
1909
|
results.push(notAttempted(byId.get(id), id, ctx.stopReason()));
|
|
1845
1910
|
continue;
|
|
1846
1911
|
}
|
|
1912
|
+
// Every id in a level comes from the queue `byId` was built from.
|
|
1847
1913
|
results.push(processRequirement(byId.get(id), {
|
|
1848
1914
|
...ctx,
|
|
1849
1915
|
baseRef: ctx.baseFor ? ctx.baseFor(id) : ctx.baseRef,
|
|
@@ -1853,6 +1919,99 @@ async function dispatchLevel(runnable, byId, hintByReq, ctx, concurrency) {
|
|
|
1853
1919
|
return results;
|
|
1854
1920
|
}
|
|
1855
1921
|
return runWorkers(runnable, byId, ctx, concurrency);
|
|
1922
|
+
};
|
|
1923
|
+
/**
|
|
1924
|
+
* `--chain` (ADR-0036): the one place the harness merges, and only on request.
|
|
1925
|
+
*
|
|
1926
|
+
* Each requirement is cut from the head the previous merge produced, so it
|
|
1927
|
+
* builds on everything before it. A green one is merged into the branch the
|
|
1928
|
+
* run started on with a merge commit — one revertible commit per requirement,
|
|
1929
|
+
* its prompt archive left on its own branch — and the gate runs again on the
|
|
1930
|
+
* merged result: green alone is not green together. A red result is undone,
|
|
1931
|
+
* and the chain stops at the first requirement that does not pass.
|
|
1932
|
+
*/
|
|
1933
|
+
async function runChain(pending, ctx, hintByReq) {
|
|
1934
|
+
const projectDir = ctx.projectDir;
|
|
1935
|
+
const results = [];
|
|
1936
|
+
for (let i = 0; i < pending.length; i += 1) {
|
|
1937
|
+
const req = pending[i];
|
|
1938
|
+
const stop = (reason) => {
|
|
1939
|
+
for (const rest of pending.slice(i + 1)) {
|
|
1940
|
+
results.push(notAttempted(rest, rest.requirement, reason));
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
const [result] = await runLevels([req], ctx, {
|
|
1944
|
+
concurrency: 1,
|
|
1945
|
+
hintByReq,
|
|
1946
|
+
runOne: dispatchLevel,
|
|
1947
|
+
});
|
|
1948
|
+
results.push(result);
|
|
1949
|
+
if (!result || result.result !== "pass") {
|
|
1950
|
+
stop(`the chain stopped at ${req.requirement}`);
|
|
1951
|
+
break;
|
|
1952
|
+
}
|
|
1953
|
+
const branch = `harness/${req.requirement}`;
|
|
1954
|
+
const merge = git(projectDir, ["merge", "--no-ff", "--no-commit", branch]);
|
|
1955
|
+
if (merge.status !== 0) {
|
|
1956
|
+
const unresolved = resolveGeneratedConflicts(projectDir);
|
|
1957
|
+
if (unresolved.length > 0) {
|
|
1958
|
+
git(projectDir, ["merge", "--abort"]);
|
|
1959
|
+
Object.assign(result, {
|
|
1960
|
+
result: "fail",
|
|
1961
|
+
error: `merge into the chain's branch conflicted in: ${unresolved.join(", ")}`,
|
|
1962
|
+
});
|
|
1963
|
+
stop(`the chain stopped at ${req.requirement}`);
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
// The prompts stay on the requirement's branch, for review and --resume;
|
|
1968
|
+
// they never reach the branch the chain merges into.
|
|
1969
|
+
const prompts = git(projectDir, [
|
|
1970
|
+
"diff",
|
|
1971
|
+
"--cached",
|
|
1972
|
+
"--name-only",
|
|
1973
|
+
"--diff-filter=A",
|
|
1974
|
+
"HEAD",
|
|
1975
|
+
"--",
|
|
1976
|
+
EmptyAttempt_1.PROMPT_ARCHIVE_DIR,
|
|
1977
|
+
])
|
|
1978
|
+
.stdout.split("\n")
|
|
1979
|
+
.filter(Boolean);
|
|
1980
|
+
if (prompts.length > 0) {
|
|
1981
|
+
git(projectDir, ["rm", "-q", "--cached", "--", ...prompts]);
|
|
1982
|
+
for (const f of prompts)
|
|
1983
|
+
fs.rmSync(path.join(projectDir, f), { force: true });
|
|
1984
|
+
}
|
|
1985
|
+
const title = String(req.title || req.use_case || req.useCase || "").replace(/^UC-\d+\s*/, "");
|
|
1986
|
+
git(projectDir, [
|
|
1987
|
+
"commit",
|
|
1988
|
+
"-q",
|
|
1989
|
+
"-m",
|
|
1990
|
+
`Merge ${branch}${title ? ` — ${title}` : ""}\n\n` +
|
|
1991
|
+
`Merged by \`specgate harness run --chain\` after its gate passed on ${branch}.`,
|
|
1992
|
+
]);
|
|
1993
|
+
const eventsFile = (0, harness_events_1.eventsFileFor)(projectDir, req.requirement);
|
|
1994
|
+
const attempt = Number(result.attempts) || undefined;
|
|
1995
|
+
(0, harness_events_1.appendEvent)(eventsFile, { type: "merge", into: "HEAD", branch }, req.requirement, attempt);
|
|
1996
|
+
const gate = runGate(projectDir, ctx.settings.testCmd, ctx.timeoutMs, req, ctx.settings, (stage, res, extra) => (0, harness_events_1.appendEvent)(eventsFile, { type: "gate", stage: `post-merge ${stage}`, result: res, ...(extra || {}) }, req.requirement, attempt));
|
|
1997
|
+
if (!gate.ok) {
|
|
1998
|
+
git(projectDir, ["reset", "-q", "--hard", "HEAD~1"]);
|
|
1999
|
+
Object.assign(result, {
|
|
2000
|
+
result: "fail",
|
|
2001
|
+
error: `post-merge gate: ${gate.stage}\n${String(gate.output || "").slice(-4000)}`,
|
|
2002
|
+
});
|
|
2003
|
+
warn(`${req.requirement}: green on ${branch}, red once merged — the merge was undone`);
|
|
2004
|
+
stop(`the chain stopped at ${req.requirement}`);
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
// The gate may regenerate the matrix: it belongs to the merge commit.
|
|
2008
|
+
if (!isGitClean(projectDir)) {
|
|
2009
|
+
git(projectDir, ["add", "-A"]);
|
|
2010
|
+
git(projectDir, ["commit", "-q", "--amend", "--no-edit"]);
|
|
2011
|
+
}
|
|
2012
|
+
info(`${req.requirement}: merged into the chain's branch, gate green`);
|
|
2013
|
+
}
|
|
2014
|
+
return results;
|
|
1856
2015
|
}
|
|
1857
2016
|
/** A requirement the budget never let start. Not a failure — it was not tried. */
|
|
1858
2017
|
function notAttempted(req, id, reason) {
|
|
@@ -1942,6 +2101,8 @@ function workerArgs(id, ctx) {
|
|
|
1942
2101
|
String(ctx.timeoutSeconds),
|
|
1943
2102
|
"--max-attempts",
|
|
1944
2103
|
String(ctx.settings.maxAttempts),
|
|
2104
|
+
"--setup-cmd",
|
|
2105
|
+
ctx.settings.setupCmd || "none",
|
|
1945
2106
|
...(ctx.force ? ["--force"] : []),
|
|
1946
2107
|
...(ctx.keepWorktrees ? ["--keep-worktrees"] : []),
|
|
1947
2108
|
...(ctx.settings.agent ? ["--agent", ctx.settings.agent] : []),
|
|
@@ -2000,8 +2161,14 @@ async function runLevels(pending, ctx, opts) {
|
|
|
2000
2161
|
};
|
|
2001
2162
|
const budgetStartedAt = Date.now();
|
|
2002
2163
|
let started = 0;
|
|
2164
|
+
// Asserted rather than annotated: it is assigned inside `mayStart`, and an
|
|
2165
|
+
// annotated `null` would stay narrowed to `null` everywhere below.
|
|
2003
2166
|
let stoppedBy = null;
|
|
2004
|
-
const
|
|
2167
|
+
const schedule = (0, HarnessRun_1.scheduleLevels)(pending);
|
|
2168
|
+
const { levels, cycles, graph } = schedule;
|
|
2169
|
+
// REQ → the requirements in this run it waits for. Core builds it as a plain
|
|
2170
|
+
// object and does not declare its shape.
|
|
2171
|
+
const dependsOn = schedule.dependsOn;
|
|
2005
2172
|
const byId = new Map(pending.map((r) => [r.requirement, r]));
|
|
2006
2173
|
if (cycles.length > 0) {
|
|
2007
2174
|
// `validate` is the gate that reports the cycle properly. Here we only
|
|
@@ -2036,7 +2203,7 @@ async function runLevels(pending, ctx, opts) {
|
|
|
2036
2203
|
const derivationFailures = new Map();
|
|
2037
2204
|
const baseFor = (id) => {
|
|
2038
2205
|
const derived = deriveBase(ctx.projectDir, id, dependsOn[id] || [], passedBranches, ctx);
|
|
2039
|
-
if (derived
|
|
2206
|
+
if ("error" in derived) {
|
|
2040
2207
|
derivationFailures.set(id, derived.error);
|
|
2041
2208
|
return ctx.baseRef;
|
|
2042
2209
|
}
|
|
@@ -2068,6 +2235,7 @@ async function runLevels(pending, ctx, opts) {
|
|
|
2068
2235
|
// that, and again with `--budget-seconds`.
|
|
2069
2236
|
const levelResults = await runOne(attemptable, byId, hintByReq, {
|
|
2070
2237
|
...ctx,
|
|
2238
|
+
// Only `attemptable` ids reach the runner, and each has a base.
|
|
2071
2239
|
baseFor: (id) => bases.get(id),
|
|
2072
2240
|
// The level asks before starting each requirement. `started` counts
|
|
2073
2241
|
// what was actually begun, so the count ceiling and the clock agree.
|
|
@@ -2135,6 +2303,12 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2135
2303
|
const projectDir = (0, project_root_1.resolveProjectDir)(args.projectDir, { requireSentinel: true });
|
|
2136
2304
|
const fileConfig = (0, HarnessConfigFile_1.readHarnessConfig)(projectDir);
|
|
2137
2305
|
const settings = (0, HarnessConfigFile_1.resolveHarnessSettings)(fileConfig, args);
|
|
2306
|
+
// A fresh worktree has no dependencies. Unless told, install what the
|
|
2307
|
+
// lockfile pins (ADR-0036); `--setup-cmd none` runs nothing.
|
|
2308
|
+
if (settings.setupCmd === "none")
|
|
2309
|
+
settings.setupCmd = "";
|
|
2310
|
+
else if (!settings.setupCmd)
|
|
2311
|
+
settings.setupCmd = (0, init_1.detectSetupCommand)(projectDir);
|
|
2138
2312
|
// D1: profile match rules live in `.harness/profiles.yaml`, and a project
|
|
2139
2313
|
// may declare profiles without ever writing a `harness.config.yaml`.
|
|
2140
2314
|
// Reaching them through the config reader made them silently absent
|
|
@@ -2188,13 +2362,15 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2188
2362
|
}
|
|
2189
2363
|
const hintByReq = new Map((plan.next_steps || []).map((s) => [s.requirement, s.hint]));
|
|
2190
2364
|
if (args.dryRun) {
|
|
2191
|
-
process.stdout.write(ui.header("harness run", `dry run · ${pending.length} requirement(s) would be processed`)
|
|
2365
|
+
process.stdout.write(ui.header("harness run", `dry run · ${pending.length} requirement(s) would be processed`) +
|
|
2366
|
+
ui.kv("Setup", settings.setupCmd || "none", 14) +
|
|
2367
|
+
(args.chain ? ui.kv("Chain", "each green requirement merged into this branch", 14) : ""));
|
|
2192
2368
|
for (const req of pending) {
|
|
2193
|
-
const prompt = (0, prompt_1.buildPrompt)(req,
|
|
2369
|
+
const prompt = judged(projectDir, (dir) => (0, prompt_1.buildPrompt)(req, dir, {
|
|
2194
2370
|
promptPrefix: settings.promptPrefix,
|
|
2195
2371
|
hint: hintByReq.get(req.requirement),
|
|
2196
2372
|
withPrecedents: settings.promptPrecedents === true,
|
|
2197
|
-
});
|
|
2373
|
+
}));
|
|
2198
2374
|
process.stdout.write("\n" +
|
|
2199
2375
|
ui.section(`${req.requirement} · ${req.category} → branch harness/${req.requirement}`) +
|
|
2200
2376
|
"\n");
|
|
@@ -2214,6 +2390,15 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2214
2390
|
const identity = gitIdentityProblem(projectDir);
|
|
2215
2391
|
if (identity)
|
|
2216
2392
|
throw new Error(identity);
|
|
2393
|
+
if (args.chain) {
|
|
2394
|
+
if (args.baseBranch) {
|
|
2395
|
+
throw new Error("--chain merges into the branch you are on: check that branch out instead of --base-branch.");
|
|
2396
|
+
}
|
|
2397
|
+
const current = git(projectDir, ["rev-parse", "--abbrev-ref", "HEAD"]).stdout.trim();
|
|
2398
|
+
if (!current || current === "HEAD") {
|
|
2399
|
+
throw new Error("--chain needs a branch to merge into; HEAD is detached.");
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2217
2402
|
const baseRef = args.baseBranch || "HEAD";
|
|
2218
2403
|
const concurrencyNote = settings.concurrency > 1 ? `, up to ${settings.concurrency} at a time` : "";
|
|
2219
2404
|
if (jsonMode || process.env.SPECGATE_HARNESS_WORKER) {
|
|
@@ -2245,11 +2430,13 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2245
2430
|
verbose: args.verbose,
|
|
2246
2431
|
};
|
|
2247
2432
|
const startedAt = new Date().toISOString();
|
|
2248
|
-
const results =
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2433
|
+
const results = args.chain
|
|
2434
|
+
? await runChain(pending, ctx, hintByReq)
|
|
2435
|
+
: await runLevels(pending, ctx, {
|
|
2436
|
+
concurrency: settings.concurrency,
|
|
2437
|
+
hintByReq,
|
|
2438
|
+
runOne: dispatchLevel,
|
|
2439
|
+
});
|
|
2253
2440
|
// Only the run that a person started writes a record. A worker is one
|
|
2254
2441
|
// slice of that run, and its own record would be a second copy of the
|
|
2255
2442
|
// same requirement — `harness report` reads every file in the directory,
|
|
@@ -2279,6 +2466,7 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2279
2466
|
process.exit(failed > 0 ? 1 : 0);
|
|
2280
2467
|
}
|
|
2281
2468
|
catch (err) {
|
|
2469
|
+
const thrown = err;
|
|
2282
2470
|
let isJson = false;
|
|
2283
2471
|
try {
|
|
2284
2472
|
isJson = parseArgs(this.args).format === "json";
|
|
@@ -2288,7 +2476,7 @@ class RunCommand extends command_1.BaseCommand {
|
|
|
2288
2476
|
}
|
|
2289
2477
|
const io = (0, agent_1.agentIo)(isJson);
|
|
2290
2478
|
return io.fail({ results: null }, [
|
|
2291
|
-
{ severity: "error", code: "harness_run_error", message:
|
|
2479
|
+
{ severity: "error", code: "harness_run_error", message: thrown.message || String(err) },
|
|
2292
2480
|
]);
|
|
2293
2481
|
}
|
|
2294
2482
|
}
|