jules-orchestrator-kit 0.55.0 → 0.57.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/README.md +2 -1
- package/bin/agentctl.mjs +69 -7
- package/package.json +1 -1
- package/scripts/run-tests.mjs +78 -3
- package/src/assertions.mjs +5 -0
- package/src/config.mjs +7 -0
- package/src/dag-engine.mjs +1 -1
- package/src/engine.mjs +72 -0
- package/src/git.mjs +10 -1
- package/src/mutation.mjs +11 -1
- package/src/ops/command-registry.mjs +12 -8
- package/src/ops/tdd-generator.mjs +93 -24
- package/src/provider.mjs +9 -2
- package/src/session-ops.mjs +12 -0
- package/src/state.mjs +34 -0
- package/src/wizard-init.mjs +20 -0
package/README.md
CHANGED
|
@@ -110,6 +110,7 @@ repository rather than from a template.
|
|
|
110
110
|
|
|
111
111
|
| What differs | How it is resolved | Inspect / override |
|
|
112
112
|
| :--- | :--- | :--- |
|
|
113
|
+
| **Which suites to run** | A monorepo change resolves to the sub-projects it touches (`verify.scope: affected`), widening back to the root command as soon as it reaches a shared file. Off by default, on for repositories `init` detects as monorepos. | `agentctl check --json` · `verify.scope` in `.agent/config.yml` |
|
|
113
114
|
| **The stack** | `detectStack()` recognises 24+ ecosystems (Cargo, Go, Python/Django, Maven/Gradle, .NET, PHP/Laravel, Ruby, Elixir, Swift, Flutter/Dart, CMake, Bun, Deno, Node + Turbo/pnpm/Nx workspaces) and derives the setup, lint, test and build commands from the manifest it finds. | `agentctl doctor` · `verify:` in `.agent/config.yml` |
|
|
114
115
|
| **The agent** | `provider:` selects Google Jules (hosted REST), the Claude Code CLI, the Codex CLI or the Gemini CLI. Readiness means a credential for the hosted one and a binary on `PATH` for the local ones — never both. | `agentctl providers` · `agentctl init --provider <name>` |
|
|
115
116
|
| **How hard to verify** | `verify.profile` expands at load time into a stage pipeline that skips gates the runtime cannot support, and says which and why. | `agentctl profile` · `agentctl profile --set max` |
|
|
@@ -203,7 +204,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
203
204
|
* **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
|
|
204
205
|
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
|
|
205
206
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
206
|
-
* **Verified Test Suite:** Tested with **
|
|
207
|
+
* **Verified Test Suite:** Tested with **864 unit tests across 121 suites passing in < 15.0s**.
|
|
207
208
|
|
|
208
209
|
<br/>
|
|
209
210
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -570,8 +570,16 @@ async function main() {
|
|
|
570
570
|
if (values.committed) selectedMode = "committed";
|
|
571
571
|
if (values["working-tree"]) selectedMode = "working-tree";
|
|
572
572
|
|
|
573
|
-
|
|
574
|
-
|
|
573
|
+
// `Number(x) || default` swallows a legitimate zero: `--min-score 0`,
|
|
574
|
+
// the way to run the harness for its report without a threshold, silently
|
|
575
|
+
// enforced 80. Both flags already carry a parseArgs default, so the only
|
|
576
|
+
// thing left to guard against is a value that is not a number at all.
|
|
577
|
+
const parseNumericFlag = (raw, fallback) => {
|
|
578
|
+
const n = Number(raw);
|
|
579
|
+
return Number.isFinite(n) ? n : fallback;
|
|
580
|
+
};
|
|
581
|
+
const minScore = parseNumericFlag(values["min-score"], 80);
|
|
582
|
+
const maxMutants = parseNumericFlag(values["max-mutants"], 20);
|
|
575
583
|
const testCmd = values.cmd || config.verify?.test || "npm test";
|
|
576
584
|
|
|
577
585
|
if (!values.json) {
|
|
@@ -597,7 +605,11 @@ async function main() {
|
|
|
597
605
|
console.log(` • Mutants Killed (Fail) : ${report.killedMutants} ✅`);
|
|
598
606
|
console.log(` • Mutants Survived (Pass) : ${report.survivedMutants} ${report.survivedMutants > 0 ? "⚠️" : ""}`);
|
|
599
607
|
console.log(` • Errors / Timeouts : ${report.errorMutants}`);
|
|
600
|
-
console.log(
|
|
608
|
+
console.log(
|
|
609
|
+
report.scored === false
|
|
610
|
+
? ` • Mutation Score : n/a — ${report.reason}`
|
|
611
|
+
: ` • Mutation Score : ${report.mutationScore}% (Required: ${report.minScore}%)`
|
|
612
|
+
);
|
|
601
613
|
console.log(` • Duration : ${report.durationMs}ms\n`);
|
|
602
614
|
|
|
603
615
|
if (report.survivors.length > 0) {
|
|
@@ -1069,7 +1081,29 @@ async function main() {
|
|
|
1069
1081
|
});
|
|
1070
1082
|
|
|
1071
1083
|
const queueDir = getQueueDir(root);
|
|
1072
|
-
|
|
1084
|
+
// The DAG runner accepts `.json` and `.task` envelopes as well as
|
|
1085
|
+
// Markdown, and does its own discovery — but this count gated whether it
|
|
1086
|
+
// was ever called, using the Markdown-only filter. A queue holding only
|
|
1087
|
+
// JSON envelopes reported "0 queued task(s)" and did nothing, with no
|
|
1088
|
+
// indication that the files were there and understood.
|
|
1089
|
+
const queueEntries = readdirSync(queueDir);
|
|
1090
|
+
let files;
|
|
1091
|
+
if (values.dag) {
|
|
1092
|
+
const { isDagTaskFile } = await import("../src/dag-engine.mjs");
|
|
1093
|
+
files = queueEntries.filter((f) => {
|
|
1094
|
+
if (f === "completed" || f.startsWith(".")) return false;
|
|
1095
|
+
if (!/\.(md|json|task)$/.test(f)) return false;
|
|
1096
|
+
let content = "";
|
|
1097
|
+
try {
|
|
1098
|
+
content = readFileSync(join(queueDir, f), "utf-8");
|
|
1099
|
+
} catch (_) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
return isDagTaskFile(f, content, isTaskFile);
|
|
1103
|
+
});
|
|
1104
|
+
} else {
|
|
1105
|
+
files = queueEntries.filter((f) => isTaskFile(f, queueDir));
|
|
1106
|
+
}
|
|
1073
1107
|
console.log(`Found ${files.length} queued task(s) in .agent/jules-queue/`);
|
|
1074
1108
|
if (files.length > 0) {
|
|
1075
1109
|
const concurrency = values.concurrency ? Number(values.concurrency) : undefined;
|
|
@@ -1092,11 +1126,30 @@ async function main() {
|
|
|
1092
1126
|
}
|
|
1093
1127
|
|
|
1094
1128
|
case "swarm": {
|
|
1095
|
-
|
|
1129
|
+
// This case had no parseArgs at all: `--json` and `--dry-run` were
|
|
1130
|
+
// accepted by the shell, documented in the registry, and silently
|
|
1131
|
+
// discarded — so a rehearsal dispatched for real and a script asking for
|
|
1132
|
+
// JSON got decorated prose.
|
|
1133
|
+
const { values } = parseArgs({
|
|
1134
|
+
args: args.slice(1),
|
|
1135
|
+
options: {
|
|
1136
|
+
concurrency: { type: "string", short: "c" },
|
|
1137
|
+
"dry-run": { type: "boolean", short: "d" },
|
|
1138
|
+
json: { type: "boolean", short: "j" },
|
|
1139
|
+
},
|
|
1140
|
+
allowPositionals: true,
|
|
1141
|
+
strict: false,
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1096
1144
|
const queueDir = getQueueDir(root);
|
|
1097
1145
|
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
1146
|
+
if (!values.json) console.log("🚀 Running Swarm Orchestrator...");
|
|
1098
1147
|
if (files.length === 0) {
|
|
1099
|
-
|
|
1148
|
+
if (values.json) {
|
|
1149
|
+
console.log(JSON.stringify({ ok: true, processed: 0, results: [], dryRun: Boolean(values["dry-run"]) }, null, 2));
|
|
1150
|
+
} else {
|
|
1151
|
+
console.log("No pending tasks found for swarm.");
|
|
1152
|
+
}
|
|
1100
1153
|
process.exit(0);
|
|
1101
1154
|
}
|
|
1102
1155
|
const tasks = files.map((f) => ({
|
|
@@ -1104,7 +1157,16 @@ async function main() {
|
|
|
1104
1157
|
title: f.replace(/\.md$/, ""),
|
|
1105
1158
|
prompt: readFileSync(join(queueDir, f), "utf-8"),
|
|
1106
1159
|
}));
|
|
1107
|
-
const results = await run(tasks, {
|
|
1160
|
+
const results = await run(tasks, {
|
|
1161
|
+
root,
|
|
1162
|
+
config,
|
|
1163
|
+
concurrency: values.concurrency ? Number(values.concurrency) : config.limits.concurrency || 3,
|
|
1164
|
+
dryRun: values["dry-run"],
|
|
1165
|
+
});
|
|
1166
|
+
if (values.json) {
|
|
1167
|
+
console.log(JSON.stringify({ ok: true, dryRun: Boolean(values["dry-run"]), ...results }, null, 2));
|
|
1168
|
+
process.exit((results.results || []).some((r) => r && r.ok === false) ? 1 : 0);
|
|
1169
|
+
}
|
|
1108
1170
|
process.exit(reportRunOutcome(results));
|
|
1109
1171
|
break;
|
|
1110
1172
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for autonomous coding agents — Google Jules, Claude Code, Codex and Gemini CLI.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { cpus } from "node:os";
|
|
5
6
|
|
|
6
7
|
const root = process.cwd();
|
|
7
8
|
const testDir = join(root, "test");
|
|
@@ -24,10 +25,84 @@ const [major, minor] = process.versions.node.split(".").map(Number);
|
|
|
24
25
|
const supportsTestTimeout = major > 20 || (major === 20 && minor >= 6);
|
|
25
26
|
const timeoutArgs = supportsTestTimeout ? [`--test-timeout=${timeoutMs}`] : [];
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
// Node runs one test file per core by default. Several suites here spawn a
|
|
29
|
+
// verification command of their own — `npm test`, and in the monorepo fixtures
|
|
30
|
+
// an `npm test --workspaces` that fans out to one node per package — so the
|
|
31
|
+
// real peak is a multiple of the file count, not the file count. On a
|
|
32
|
+
// twelve-core laptop that saturates every core for the length of the run,
|
|
33
|
+
// which cooks the machine and makes the throughput assertions (1000 telemetry
|
|
34
|
+
// appends under 6s) fail for reasons that have nothing to do with the code.
|
|
35
|
+
//
|
|
36
|
+
// Half the cores keeps the suite comfortably parallel while leaving room for
|
|
37
|
+
// the children it spawns. JULES_TEST_CONCURRENCY overrides it — CI runners
|
|
38
|
+
// with two cores are already below this and are unaffected.
|
|
39
|
+
const cpuCount = Math.max(1, cpus().length);
|
|
40
|
+
const envConcurrency = Number(process.env.JULES_TEST_CONCURRENCY);
|
|
41
|
+
const concurrency = Number.isFinite(envConcurrency) && envConcurrency > 0
|
|
42
|
+
? Math.floor(envConcurrency)
|
|
43
|
+
: Math.max(2, Math.floor(cpuCount / 2));
|
|
44
|
+
const concurrencyArgs = supportsTestTimeout ? [`--test-concurrency=${concurrency}`] : [];
|
|
45
|
+
|
|
46
|
+
// The runner leads its own process group, and nothing outlives it.
|
|
47
|
+
//
|
|
48
|
+
// `spawnSync` kills only the direct child when a run is interrupted, and this
|
|
49
|
+
// suite's children spawn children of their own — a verification command, an
|
|
50
|
+
// `npm` that fans out to a node per workspace, a git subprocess per fixture.
|
|
51
|
+
// Interrupting a run therefore orphaned everything below the first level, and
|
|
52
|
+
// those orphans kept running and kept spawning: a laptop accumulated enough of
|
|
53
|
+
// them across several interrupted runs to exhaust 32 GB of RAM and 24 GB of
|
|
54
|
+
// swap, and two CI runners logged pages of "Terminate orphan process" at the
|
|
55
|
+
// end of a job that had already failed.
|
|
56
|
+
//
|
|
57
|
+
// Detaching makes the child a process-group leader, so one signal to the
|
|
58
|
+
// negated pid reaches the whole tree.
|
|
59
|
+
const child = spawn(process.execPath, ["--test", ...timeoutArgs, ...concurrencyArgs, ...testFiles], {
|
|
28
60
|
cwd: root,
|
|
29
61
|
stdio: "inherit",
|
|
30
62
|
env: process.env,
|
|
63
|
+
detached: process.platform !== "win32",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Kill the runner and everything it spawned.
|
|
68
|
+
*
|
|
69
|
+
* POSIX takes a signal to `-pid`, which addresses the whole process group.
|
|
70
|
+
* Windows has no such concept, so `taskkill /T` walks the tree instead.
|
|
71
|
+
*/
|
|
72
|
+
function reap(signal = "SIGKILL") {
|
|
73
|
+
if (!child.pid) return;
|
|
74
|
+
try {
|
|
75
|
+
if (process.platform === "win32") {
|
|
76
|
+
spawnSync("taskkill", ["/T", "/F", "/PID", String(child.pid)], { stdio: "ignore" });
|
|
77
|
+
} else {
|
|
78
|
+
process.kill(-child.pid, signal);
|
|
79
|
+
}
|
|
80
|
+
} catch (_) {
|
|
81
|
+
// Already gone, or never had a group. Nothing left to reap either way.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ctrl-C, a CI cancellation and a harness teardown all arrive as signals; each
|
|
86
|
+
// must take the tree with it rather than detaching it from its parent.
|
|
87
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
88
|
+
process.on(sig, () => {
|
|
89
|
+
reap("SIGTERM");
|
|
90
|
+
// Give the group a moment to unwind, then make sure.
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
reap("SIGKILL");
|
|
93
|
+
process.exit(130);
|
|
94
|
+
}, 2000).unref();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
child.on("exit", (code, signal) => {
|
|
99
|
+
// Stragglers outlive a clean exit too: a test that spawned a server and
|
|
100
|
+
// failed before its teardown leaves it running.
|
|
101
|
+
reap("SIGKILL");
|
|
102
|
+
process.exit(signal ? 1 : code ?? 1);
|
|
31
103
|
});
|
|
32
104
|
|
|
33
|
-
|
|
105
|
+
child.on("error", (err) => {
|
|
106
|
+
console.error(`Test runner failed to start: ${err.message}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
package/src/assertions.mjs
CHANGED
|
@@ -513,6 +513,11 @@ export function assertMutation(config = {}, root = process.cwd()) {
|
|
|
513
513
|
});
|
|
514
514
|
|
|
515
515
|
const diagnostics = [];
|
|
516
|
+
if (report.scored === false) {
|
|
517
|
+
// Not a failure, but not a pass worth trusting either: say so, so a green
|
|
518
|
+
// stage cannot be read as "the diff survived mutation testing".
|
|
519
|
+
diagnostics.push(report.reason || "No mutants could be generated from the added lines.");
|
|
520
|
+
}
|
|
516
521
|
if (!report.ok) {
|
|
517
522
|
diagnostics.push(
|
|
518
523
|
`Mutation score ${report.mutationScore}% below required threshold of ${minScore}% (${report.killedMutants}/${report.totalMutants} killed, ${report.survivedMutants} survived).`
|
package/src/config.mjs
CHANGED
|
@@ -522,6 +522,13 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
522
522
|
teardown: rawTeardown ?? autoVerify.teardown ?? "",
|
|
523
523
|
build: rawBuild ?? autoVerify.build,
|
|
524
524
|
policy: parsed.verify?.policy ?? autoVerify.policy,
|
|
525
|
+
// "global" runs the repository's own verify commands; "affected" resolves
|
|
526
|
+
// the changed files to their sub-projects and runs only those suites. Opt-in
|
|
527
|
+
// on purpose: silently narrowing which tests run is the same class of defect
|
|
528
|
+
// as approving a change that ran none, and an existing repository's gate
|
|
529
|
+
// must not change meaning on an upgrade. `agentctl init` writes "affected"
|
|
530
|
+
// for a repository it detects as a monorepo.
|
|
531
|
+
scope: parsed.verify?.scope === "affected" ? "affected" : "global",
|
|
525
532
|
// Whether the gate may approve a change that ran no verification at all.
|
|
526
533
|
// True by default: "nothing to run" is not a pass, and a security tool that
|
|
527
534
|
// says APPROVED after checking nothing is worse than no tool. A repository
|
package/src/dag-engine.mjs
CHANGED
|
@@ -431,7 +431,7 @@ export function resolveAffectedTests(modifiedFiles = [], options = {}) {
|
|
|
431
431
|
* @param {Function} isTaskFile - `isTaskFile` from engine.mjs, passed in to avoid a circular import.
|
|
432
432
|
* @returns {boolean}
|
|
433
433
|
*/
|
|
434
|
-
function isDagTaskFile(fileName, content, isTaskFile) {
|
|
434
|
+
export function isDagTaskFile(fileName, content, isTaskFile) {
|
|
435
435
|
if (fileName.endsWith(".task")) return true;
|
|
436
436
|
if (fileName.endsWith(".md")) return isTaskFile(fileName, content);
|
|
437
437
|
if (fileName.endsWith(".json")) {
|
package/src/engine.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { resolveRolePrompt } from "./role-resolver.mjs";
|
|
|
20
20
|
|
|
21
21
|
import { runAssertion } from "./assertions.mjs";
|
|
22
22
|
import { buildDefaultStages } from "./profiles.mjs";
|
|
23
|
+
import { resolveWorkspaceBoundary } from "./stack-detector.mjs";
|
|
23
24
|
import {
|
|
24
25
|
computeDirectoryHash,
|
|
25
26
|
generateEvidenceManifest,
|
|
@@ -201,6 +202,7 @@ export async function gate(opts = {}) {
|
|
|
201
202
|
// Read from the base commit like every other trusted field: an
|
|
202
203
|
// uncommitted `required: false` must not be able to switch the gate off.
|
|
203
204
|
required: parsed.verify?.required !== undefined ? parsed.verify.required !== false : config.verify.required !== false,
|
|
205
|
+
scope: parsed.verify?.scope || config.verify.scope || "global",
|
|
204
206
|
timeoutMs: parsed.verify?.timeoutMs || parsed.verify?.timeout_ms || config.verify.timeoutMs,
|
|
205
207
|
};
|
|
206
208
|
}
|
|
@@ -319,9 +321,52 @@ export async function gate(opts = {}) {
|
|
|
319
321
|
NODE_OPTIONS: guardNodeOptions,
|
|
320
322
|
};
|
|
321
323
|
|
|
324
|
+
// Node's test runner talks to its children through NODE_TEST_CONTEXT and
|
|
325
|
+
// NODE_CHANNEL_FD. Inherited by a verification command that is itself a
|
|
326
|
+
// `node --test` run, the child switches into child-reporter mode and its
|
|
327
|
+
// failures stop reaching the exit code — the gate then sees exit 0 and
|
|
328
|
+
// approves a change whose tests failed. src/perf.mjs already strips these for
|
|
329
|
+
// the same reason; the gate, which is the one that decides, did not.
|
|
330
|
+
for (const key of Object.keys(testEnv)) {
|
|
331
|
+
if (key.startsWith("NODE_TEST_") || key.startsWith("NODE_CHANNEL_")) delete testEnv[key];
|
|
332
|
+
}
|
|
333
|
+
|
|
322
334
|
let flakyVerdictResult = null;
|
|
323
335
|
const verifyTimeout = trustedVerify.timeoutMs || 60000;
|
|
324
336
|
|
|
337
|
+
// Monorepo scoping: run the suites the change can actually break.
|
|
338
|
+
//
|
|
339
|
+
// `resolveWorkspaceBoundary()` shipped, was drawn in the architecture
|
|
340
|
+
// diagrams and documented as a headline feature — and was called by nothing.
|
|
341
|
+
// Every change in a monorepo ran the root suite, so a one-package edit was
|
|
342
|
+
// gated on every other package's tests. It resolves the changed files to
|
|
343
|
+
// their sub-projects and composes the per-project commands.
|
|
344
|
+
//
|
|
345
|
+
// It stays opt-in (`verify.scope: affected`) because narrowing what runs is
|
|
346
|
+
// only safe when someone asked for it, and it yields to the global command
|
|
347
|
+
// whenever a shared file is touched or no sub-project command is found — a
|
|
348
|
+
// narrower run that misses the breakage is worse than a slow one.
|
|
349
|
+
let boundary = null;
|
|
350
|
+
if (trustedVerify.scope === "affected" && !(Array.isArray(trustedVerify.stages) && trustedVerify.stages.length > 0)) {
|
|
351
|
+
try {
|
|
352
|
+
boundary = resolveWorkspaceBoundary(files, root);
|
|
353
|
+
} catch (_) {
|
|
354
|
+
boundary = null;
|
|
355
|
+
}
|
|
356
|
+
if (boundary && boundary.isMonorepo && !boundary.globalFallback && boundary.testCmd) {
|
|
357
|
+
trustedVerify = {
|
|
358
|
+
...trustedVerify,
|
|
359
|
+
test: boundary.testCmd,
|
|
360
|
+
unit: boundary.testCmd,
|
|
361
|
+
build: boundary.buildCmd || trustedVerify.build,
|
|
362
|
+
};
|
|
363
|
+
appendTelemetry(root, "verify_scope_narrowed", {
|
|
364
|
+
projects: boundary.projects.map((p) => p.path),
|
|
365
|
+
testCmd: boundary.testCmd,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
325
370
|
// Build sequential execution pipeline (Setup -> Lint -> Test/Unit -> Fuzz -> Invariant -> E2E -> Build -> Server -> Teardown)
|
|
326
371
|
const stagesToRun = [];
|
|
327
372
|
if (Array.isArray(trustedVerify.stages) && trustedVerify.stages.length > 0) {
|
|
@@ -994,6 +1039,33 @@ export async function dispatch(task = {}, opts = {}) {
|
|
|
994
1039
|
|
|
995
1040
|
const cleanTask = { ...task, prompt: envelopedPrompt };
|
|
996
1041
|
|
|
1042
|
+
// Snapshot the tree before anything can change it.
|
|
1043
|
+
//
|
|
1044
|
+
// `agentctl rollback` shipped, was documented, and could never work:
|
|
1045
|
+
// createCheckpoint() was defined and called from nowhere, so the checkpoint
|
|
1046
|
+
// directory was always empty and the command answered "No checkpoints found"
|
|
1047
|
+
// to everyone who reached for it — at exactly the moment they needed it. An
|
|
1048
|
+
// exec provider edits this working tree directly, and `patch --apply` writes
|
|
1049
|
+
// into it later, so this is the last moment the pre-agent state exists.
|
|
1050
|
+
//
|
|
1051
|
+
// Never fatal: a repository that cannot be snapshotted (no git, no disk) must
|
|
1052
|
+
// still be able to dispatch. A missing checkpoint costs a rollback; a throw
|
|
1053
|
+
// here costs the task.
|
|
1054
|
+
if (!opts.dryRun && opts.checkpoint !== false) {
|
|
1055
|
+
try {
|
|
1056
|
+
const { createCheckpoint } = await import("./ops/checkpoint.mjs");
|
|
1057
|
+
const checkpointId = String(task.id || task.taskId || `dispatch-${Date.now()}`).replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
1058
|
+
const snapshot = createCheckpoint(checkpointId, { root });
|
|
1059
|
+
appendTelemetry(root, "checkpoint_created", {
|
|
1060
|
+
id: snapshot.id,
|
|
1061
|
+
headSha: snapshot.headSha,
|
|
1062
|
+
uncommittedFiles: snapshot.uncommittedFiles?.length || 0,
|
|
1063
|
+
});
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
console.warn(`⚠️ Could not create a pre-flight checkpoint (${err.message}). \`agentctl rollback\` will not be able to restore this dispatch.`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
997
1069
|
const runDispatch = () => provider.dispatch(cleanTask, { root, dryRun: opts.dryRun });
|
|
998
1070
|
|
|
999
1071
|
// An estimated ceiling may warn but must not block: refusing a dispatch the
|
package/src/git.mjs
CHANGED
|
@@ -384,8 +384,17 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
|
|
|
384
384
|
const fullPath = join(root, file);
|
|
385
385
|
if (existsSync(fullPath)) {
|
|
386
386
|
const content = readFileSync(fullPath, "utf-8");
|
|
387
|
+
const addedLines = content.split(/\r?\n/);
|
|
388
|
+
// The `@@` header is not decoration. Every consumer that needs to know
|
|
389
|
+
// *which* line an addition is on parses it — the mutation harness and
|
|
390
|
+
// the V8 diff-coverage mapper both walk hunks — so a synthetic diff
|
|
391
|
+
// without one made untracked files invisible to them. Both silently
|
|
392
|
+
// reported nothing to do for a brand-new file, which is precisely
|
|
393
|
+
// where untested code arrives. The secret scanner never noticed
|
|
394
|
+
// because it only reads `+` lines.
|
|
387
395
|
untrackedDiff += `\ndiff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}\n`;
|
|
388
|
-
untrackedDiff +=
|
|
396
|
+
untrackedDiff += `@@ -0,0 +1,${addedLines.length} @@\n`;
|
|
397
|
+
untrackedDiff += addedLines.map((line) => `+${line}`).join("\n") + "\n";
|
|
389
398
|
}
|
|
390
399
|
} catch (_) {}
|
|
391
400
|
}
|
package/src/mutation.mjs
CHANGED
|
@@ -676,13 +676,23 @@ export function runMutationTest(options = {}) {
|
|
|
676
676
|
const selectedMutants = candidates.slice(0, maxMutants);
|
|
677
677
|
|
|
678
678
|
if (selectedMutants.length === 0) {
|
|
679
|
+
// A diff with no mutable operators yields no mutants, and reporting that as
|
|
680
|
+
// "100%" told operators their untested code had a perfect score — the exact
|
|
681
|
+
// false confidence the harness exists to remove. There is no score to
|
|
682
|
+
// report, so there is none: `mutationScore` is null and `reason` says why.
|
|
683
|
+
//
|
|
684
|
+
// `ok` stays true. Nothing was falsifiable, so nothing failed to be
|
|
685
|
+
// falsified; failing here would block every diff that only adds imports,
|
|
686
|
+
// constants or markdown, and a gate that cries wolf gets switched off.
|
|
679
687
|
return {
|
|
680
688
|
ok: true,
|
|
681
689
|
totalMutants: 0,
|
|
682
690
|
killedMutants: 0,
|
|
683
691
|
survivedMutants: 0,
|
|
684
692
|
errorMutants: 0,
|
|
685
|
-
mutationScore:
|
|
693
|
+
mutationScore: null,
|
|
694
|
+
scored: false,
|
|
695
|
+
reason: "No mutable operators in the added lines — nothing to falsify, so no score was computed.",
|
|
686
696
|
minScore,
|
|
687
697
|
results: [],
|
|
688
698
|
survivors: [],
|
|
@@ -115,23 +115,27 @@ export const COMMAND_REGISTRY = [
|
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
id: "swarm",
|
|
118
|
+
// Described as an inspector — `mutates: false`, `risk: low` — while the
|
|
119
|
+
// handler dispatches every queued task in parallel and spends budget.
|
|
120
|
+
// `--interactive` was advertised and implemented nowhere.
|
|
118
121
|
path: ["swarm"],
|
|
119
122
|
title: "swarm",
|
|
120
|
-
description: "
|
|
123
|
+
description: "Dispatch every queued task in parallel across worker slots",
|
|
121
124
|
category: "Operate",
|
|
122
|
-
mutates:
|
|
123
|
-
risk: "
|
|
124
|
-
interactive: "
|
|
125
|
+
mutates: true,
|
|
126
|
+
risk: "moderate",
|
|
127
|
+
interactive: "never",
|
|
125
128
|
requiresRepository: true,
|
|
126
129
|
shortcuts: ["s"],
|
|
127
130
|
examples: [
|
|
131
|
+
"agentctl swarm --dry-run",
|
|
128
132
|
"agentctl swarm",
|
|
129
|
-
"agentctl swarm --
|
|
130
|
-
"agentctl swarm --json",
|
|
133
|
+
"agentctl swarm --concurrency 3 --json",
|
|
131
134
|
],
|
|
132
135
|
flags: [
|
|
133
|
-
{ name: "
|
|
134
|
-
{ name: "
|
|
136
|
+
{ name: "concurrency", type: "string", description: "Parallel worker slots (defaults to limits.concurrency)" },
|
|
137
|
+
{ name: "dry-run", type: "boolean", description: "Report what would run without dispatching" },
|
|
138
|
+
{ name: "json", type: "boolean", description: "Output structured JSON swarm result" },
|
|
135
139
|
],
|
|
136
140
|
},
|
|
137
141
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { detectPolyglotStack } from "../stack-detector.mjs";
|
|
4
4
|
import { resolveRoot } from "../config.mjs";
|
|
5
5
|
import { runCmd } from "../git.mjs";
|
|
@@ -26,45 +26,100 @@ export function scaffoldTddTest(spec = {}, options = {}) {
|
|
|
26
26
|
const details = spec.spec || "Feature requirement specification assertion.";
|
|
27
27
|
|
|
28
28
|
const stack = detectPolyglotStack(root);
|
|
29
|
-
const testDir = join(root, "test");
|
|
30
|
-
try {
|
|
31
|
-
mkdirSync(testDir, { recursive: true });
|
|
32
|
-
} catch (_) {}
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
// The generated oracle has to be written in the language its runner speaks.
|
|
31
|
+
//
|
|
32
|
+
// This used to emit a Node test file for every stack and then, in a Python
|
|
33
|
+
// project, run `pytest generated-x.test.mjs`. pytest exits 4 on a file it
|
|
34
|
+
// cannot collect, and the cycle read any non-zero exit as RED — so it
|
|
35
|
+
// reported a verified failing test, and locked an uncollectable file into
|
|
36
|
+
// scope.deny, having proven nothing at all.
|
|
37
|
+
const identifier = title.replace(/-/g, "_");
|
|
38
|
+
|
|
39
|
+
// The one string that must appear in the runner's output for the failure to
|
|
40
|
+
// be the *assertion* failing rather than the file never being collected.
|
|
41
|
+
const redMarker = `TDD Assertion Failed: Requirement '${title}' is not yet implemented.`;
|
|
42
|
+
const comment = (prefix) => details.split("\n").map((line) => `${prefix} ${line}`).join("\n");
|
|
43
|
+
|
|
44
|
+
let relDir = "test";
|
|
45
|
+
let fileName = `generated-${title}.test.mjs`;
|
|
46
|
+
let codeContent;
|
|
47
|
+
let testCmdParts;
|
|
48
|
+
let testCmdStr;
|
|
49
|
+
|
|
50
|
+
if (stack.stack === "python" || stack.stack === "django") {
|
|
51
|
+
fileName = `test_generated_${identifier}.py`;
|
|
52
|
+
codeContent = `def test_tdd_oracle_${identifier}():
|
|
53
|
+
# REQUIREMENT SPECIFICATION:
|
|
54
|
+
${comment(" #")}
|
|
55
|
+
is_implemented = False
|
|
56
|
+
assert is_implemented, "${redMarker}"
|
|
57
|
+
`;
|
|
58
|
+
testCmdParts = ["pytest", `${relDir}/${fileName}`];
|
|
59
|
+
testCmdStr = `pytest ${relDir}/${fileName}`;
|
|
60
|
+
} else if (stack.stack === "cargo") {
|
|
61
|
+
// Cargo discovers integration tests in tests/, not test/.
|
|
62
|
+
relDir = "tests";
|
|
63
|
+
fileName = `generated_${identifier}.rs`;
|
|
64
|
+
codeContent = `#[test]
|
|
65
|
+
fn tdd_oracle_${identifier}() {
|
|
66
|
+
// REQUIREMENT SPECIFICATION:
|
|
67
|
+
${comment(" //")}
|
|
68
|
+
let is_implemented = false;
|
|
69
|
+
assert!(is_implemented, "${redMarker}");
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
testCmdParts = ["cargo", "test", "--test", `generated_${identifier}`];
|
|
73
|
+
testCmdStr = `cargo test --test generated_${identifier}`;
|
|
74
|
+
} else if (stack.stack === "go") {
|
|
75
|
+
fileName = `generated_${identifier}_test.go`;
|
|
76
|
+
codeContent = `package ${relDir}
|
|
36
77
|
|
|
37
|
-
|
|
78
|
+
import "testing"
|
|
79
|
+
|
|
80
|
+
func TestTddOracle${identifier.replace(/_/g, "")}(t *testing.T) {
|
|
81
|
+
// REQUIREMENT SPECIFICATION:
|
|
82
|
+
${comment("\t//")}
|
|
83
|
+
isImplemented := false
|
|
84
|
+
if !isImplemented {
|
|
85
|
+
t.Fatalf("${redMarker}")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
testCmdParts = ["go", "test", `./${relDir}/`];
|
|
90
|
+
testCmdStr = `go test ./${relDir}/`;
|
|
91
|
+
} else {
|
|
92
|
+
codeContent = `import test from "node:test";
|
|
38
93
|
import assert from "node:assert/strict";
|
|
39
94
|
|
|
40
95
|
test("TDD Oracle: ${title}", () => {
|
|
41
96
|
// REQUIREMENT SPECIFICATION:
|
|
42
|
-
|
|
97
|
+
${comment(" //")}
|
|
43
98
|
|
|
44
99
|
const isImplemented = false;
|
|
45
|
-
assert.equal(isImplemented, true, "
|
|
100
|
+
assert.equal(isImplemented, true, "${redMarker}");
|
|
46
101
|
});
|
|
47
102
|
`;
|
|
103
|
+
testCmdParts = ["node", "--test", join(root, relDir, fileName)];
|
|
104
|
+
testCmdStr = `node --test ${relDir}/${fileName}`;
|
|
105
|
+
}
|
|
48
106
|
|
|
49
|
-
|
|
107
|
+
const testDir = join(root, relDir);
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(testDir, { recursive: true });
|
|
110
|
+
} catch (_) {}
|
|
50
111
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
let testCmdStr = `node --test ${relativePath}`;
|
|
54
|
-
if (stack.stack === "python") {
|
|
55
|
-
testCmd = ["pytest", filePath];
|
|
56
|
-
testCmdStr = `pytest ${relativePath}`;
|
|
57
|
-
} else if (stack.stack === "cargo") {
|
|
58
|
-
testCmd = ["cargo", "test", "--test", title];
|
|
59
|
-
testCmdStr = `cargo test --test ${title}`;
|
|
60
|
-
}
|
|
112
|
+
const filePath = join(testDir, fileName);
|
|
113
|
+
writeFileSync(filePath, codeContent, "utf-8");
|
|
61
114
|
|
|
62
115
|
return {
|
|
63
116
|
filePath,
|
|
64
|
-
relativePath
|
|
117
|
+
relativePath: `${relDir}/${fileName}`,
|
|
65
118
|
codeContent,
|
|
66
|
-
testCmd,
|
|
119
|
+
testCmd: testCmdParts,
|
|
67
120
|
testCmdStr,
|
|
121
|
+
stack: stack.stack,
|
|
122
|
+
redMarker,
|
|
68
123
|
};
|
|
69
124
|
}
|
|
70
125
|
|
|
@@ -98,7 +153,21 @@ export async function runTddCycle(spec = {}, options = {}) {
|
|
|
98
153
|
|
|
99
154
|
if (redOutput.status === 0) {
|
|
100
155
|
throw new TddError(
|
|
101
|
-
`TDD RED check failed: Test '
|
|
156
|
+
`TDD RED check failed: Test '${scaffolded.relativePath}' passed initially! TDD tests must be falsifiable and fail before implementation.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// A non-zero exit is not proof the assertion ran. `pytest` exits 4 on a file
|
|
161
|
+
// it cannot collect and 5 when it collects nothing; a runner pointed at a
|
|
162
|
+
// file it does not understand exits non-zero for that reason alone. Reading
|
|
163
|
+
// either as RED is how a Node test file in a Python project came to be
|
|
164
|
+
// reported as a verified failing oracle. The generated assertion carries a
|
|
165
|
+
// marker; if the runner never reached it, the marker is not in the output.
|
|
166
|
+
const redText = `${redOutput.stdout || ""}\n${redOutput.stderr || ""}`;
|
|
167
|
+
if (scaffolded.redMarker && !redText.includes(scaffolded.redMarker)) {
|
|
168
|
+
throw new TddError(
|
|
169
|
+
`TDD RED check inconclusive: '${scaffolded.testCmdStr}' exited ${redOutput.status} without reaching the generated assertion, so nothing was proven falsifiable. ` +
|
|
170
|
+
`The runner most likely could not collect '${scaffolded.relativePath}' (detected stack: ${scaffolded.stack}). Output:\n${redText.trim().slice(0, 500)}`
|
|
102
171
|
);
|
|
103
172
|
}
|
|
104
173
|
|
package/src/provider.mjs
CHANGED
|
@@ -673,10 +673,17 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
673
673
|
},
|
|
674
674
|
|
|
675
675
|
async getSession(sessionId, ctx = {}) {
|
|
676
|
-
if (!
|
|
676
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
677
|
+
// `listSources()` reuses this method purely as an authenticated GET, and
|
|
678
|
+
// passes `customUrl` with no session to fetch. The guard rejected the
|
|
679
|
+
// empty id before the url was even looked at, so listing a repository's
|
|
680
|
+
// connected sources threw a TypeError against the live API every time —
|
|
681
|
+
// a failure no dry-run or unit test could reach, because both stop
|
|
682
|
+
// before the request. A session id is required only when the url is
|
|
683
|
+
// going to be built out of one.
|
|
684
|
+
if (!ctx.customUrl && (!sessionId || typeof sessionId !== "string")) {
|
|
677
685
|
throw new TypeError("getSession() requires a valid sessionId string");
|
|
678
686
|
}
|
|
679
|
-
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
680
687
|
|
|
681
688
|
const pool = ctx.tokenPool || config.tokenPool || TokenPool.fromEnv(config);
|
|
682
689
|
const rawToken = pool.getNextToken() || process.env.JULES_API_KEY || (ctx.allowLegacyKey ? process.env.GEMINI_API_KEY : "") || "";
|
package/src/session-ops.mjs
CHANGED
|
@@ -160,6 +160,18 @@ export async function applySessionPatch(sessionId, opts = {}) {
|
|
|
160
160
|
|
|
161
161
|
// 2. If apply requested, execute git apply
|
|
162
162
|
if (opts.apply) {
|
|
163
|
+
// Writing an agent's patch into the working tree is the other moment a
|
|
164
|
+
// rollback target has to exist. The hosted provider works server-side, so
|
|
165
|
+
// dispatch never touched this tree — this is the first time it changes.
|
|
166
|
+
if (opts.checkpoint !== false) {
|
|
167
|
+
try {
|
|
168
|
+
const { createCheckpoint } = await import("./ops/checkpoint.mjs");
|
|
169
|
+
createCheckpoint(`patch-${String(sessionId).replace(/[^A-Za-z0-9_.-]/g, "-")}`, { root });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.warn(`⚠️ Could not snapshot before applying the patch (${err.message}); \`agentctl rollback\` will not cover it.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
163
175
|
const applyRes = spawnSync("git", ["apply", "-"], {
|
|
164
176
|
cwd: root,
|
|
165
177
|
input: res.patch,
|
package/src/state.mjs
CHANGED
|
@@ -560,6 +560,40 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
|
|
|
560
560
|
}
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
// The lock file is named after the task, so `existsSync(lockFile)` above only
|
|
564
|
+
// ever asked "is this same task already running?". The `files` argument — the
|
|
565
|
+
// whole point of the call — was stored as metadata and never compared against
|
|
566
|
+
// anything, so two agents could hold locks on the same file at the same time
|
|
567
|
+
// and each be told it had exclusive access. Check the paths, not just the id.
|
|
568
|
+
const requested = new Set(
|
|
569
|
+
(Array.isArray(files) ? files : [])
|
|
570
|
+
.filter((f) => typeof f === "string" && f)
|
|
571
|
+
.map((f) => normalizePath(f))
|
|
572
|
+
);
|
|
573
|
+
if (requested.size > 0) {
|
|
574
|
+
for (const held of lockStatus(root)) {
|
|
575
|
+
if (!held || held.taskId === taskId) continue;
|
|
576
|
+
const recordedStart = held.processStartTime ?? held.starttime ?? null;
|
|
577
|
+
const stillHeld =
|
|
578
|
+
isPidAlive(held.pid, recordedStart) &&
|
|
579
|
+
!(held.acquiredAt && Date.now() - new Date(held.acquiredAt).getTime() > 7200000);
|
|
580
|
+
if (!stillHeld) continue;
|
|
581
|
+
|
|
582
|
+
const overlap = (Array.isArray(held.files) ? held.files : [])
|
|
583
|
+
.map((f) => normalizePath(f))
|
|
584
|
+
.filter((f) => requested.has(f));
|
|
585
|
+
if (overlap.length > 0) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
holder: held.agent,
|
|
589
|
+
taskId: held.taskId,
|
|
590
|
+
pid: held.pid,
|
|
591
|
+
conflictingFiles: overlap,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
563
597
|
const startTime = getProcessStartTime(process.pid);
|
|
564
598
|
const concurrencyGroup = String(opts?.concurrencyGroup || opts?.concurrency_group || "").trim();
|
|
565
599
|
const payload = {
|
package/src/wizard-init.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { parseYaml, TIER_PRESETS, VENDOR_TIERS, FALLBACK_TIER } from "./config.mjs";
|
|
4
4
|
import { suggestProvider, detectAvailableProviders } from "./provider-readiness.mjs";
|
|
5
5
|
import { detectDefaultBranch } from "./git.mjs";
|
|
6
|
+
import { resolveWorkspaceBoundary } from "./stack-detector.mjs";
|
|
6
7
|
import { PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
|
|
7
8
|
import { detectStackOracles, runVerificationProbe } from "./wizard-oracle.mjs";
|
|
8
9
|
import { select, multiSelect, input, confirm, spinner, isTTY } from "./tui.mjs";
|
|
@@ -180,6 +181,21 @@ export function planInit(root = process.cwd(), options = {}) {
|
|
|
180
181
|
// whose git chose `master`, or whose team standardised on `develop`.
|
|
181
182
|
const baseBranch = options.baseBranch || existingConfig.base_branch || detectDefaultBranch(root);
|
|
182
183
|
|
|
184
|
+
// A monorepo that runs every package's suite for a one-package change is the
|
|
185
|
+
// complaint the boundary resolver was written to answer, so a repository
|
|
186
|
+
// detected as one starts with it on. Existing repositories keep whatever they
|
|
187
|
+
// already stated; nobody's gate changes meaning because they upgraded.
|
|
188
|
+
const detectedMonorepo = (() => {
|
|
189
|
+
if (options.verifyScope) return options.verifyScope === "affected";
|
|
190
|
+
if (existingConfig.verify?.scope) return existingConfig.verify.scope === "affected";
|
|
191
|
+
try {
|
|
192
|
+
return Boolean(resolveWorkspaceBoundary([], root).isMonorepo);
|
|
193
|
+
} catch (_) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
})();
|
|
197
|
+
const verifyScope = detectedMonorepo ? "affected" : "global";
|
|
198
|
+
|
|
183
199
|
const limitsBlock = isCustomLimits
|
|
184
200
|
? `\nlimits:\n concurrency: ${limits.concurrency}\n daily_tasks: ${limits.daily_tasks}\n stagger_ms: ${limits.stagger_ms}\n diff_kb: ${limits.diff_kb}\n`
|
|
185
201
|
: "";
|
|
@@ -195,6 +211,9 @@ ${limitsBlock}
|
|
|
195
211
|
verify:
|
|
196
212
|
# minimal | standard | max — see: agentctl profile --list
|
|
197
213
|
profile: ${profile}
|
|
214
|
+
# global runs the repository's own commands; affected resolves changed files
|
|
215
|
+
# to their sub-projects and runs only those suites (monorepos)
|
|
216
|
+
scope: ${verifyScope}
|
|
198
217
|
test: "${verify.test}"
|
|
199
218
|
build: "${verify.build}"
|
|
200
219
|
lint: "${verify.lint}"
|
|
@@ -236,6 +255,7 @@ allow_paths: ${allowPaths.length > 0 ? "\n" + allowPaths.map((p) => ` - "${p}"`
|
|
|
236
255
|
provider,
|
|
237
256
|
profile,
|
|
238
257
|
baseBranch,
|
|
258
|
+
verifyScope,
|
|
239
259
|
verify,
|
|
240
260
|
limits,
|
|
241
261
|
presets: selectedPresets,
|