jules-orchestrator-kit 0.70.0 → 0.72.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/.agent/rules/jules-protocol.md +0 -1
- package/CHANGELOG.md +66 -0
- package/JULES_RULES_TEMPLATE.md +0 -1
- package/README.md +56 -1
- package/ROADMAP_V1.md +31 -6
- package/bin/agentctl.mjs +101 -8
- package/package.json +1 -1
- package/scripts/guard-reach-check.mjs +74 -8
- package/src/config.mjs +434 -5
- package/src/coverage.mjs +85 -13
- package/src/engine.mjs +106 -130
- package/src/git.mjs +104 -1
- package/src/guard-policy.mjs +581 -0
- package/src/ops/command-registry.mjs +109 -0
- package/src/ops/test-collection.mjs +103 -3
- package/src/security.mjs +554 -4
- package/src/stack-detector.mjs +73 -14
- package/src/test-paths.mjs +18 -1
- package/src/wizard-init.mjs +54 -27
- package/src/wizard-oracle.mjs +1 -1
- package/src/wizard-task.mjs +15 -1
package/src/stack-detector.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { whichBinary } from "./provider-readiness.mjs";
|
|
4
|
+
import { yamlScalar } from "./config.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The Python interpreter to invoke, by whatever name this machine has it under.
|
|
@@ -31,11 +32,48 @@ function pythonBin(env = process.env) {
|
|
|
31
32
|
* Falls back to the bare console script only when no interpreter can be found
|
|
32
33
|
* to host the module.
|
|
33
34
|
*/
|
|
34
|
-
export function pytestCmd(env = process.env) {
|
|
35
|
+
export function pytestCmd(env = process.env, root = null) {
|
|
36
|
+
const prefix = root && isSrcLayout(root) ? "PYTHONPATH=src " : "";
|
|
35
37
|
for (const name of ["python3", "python", "py"]) {
|
|
36
|
-
if (whichBinary(name, env)) return `${name} -m pytest`;
|
|
38
|
+
if (whichBinary(name, env)) return `${prefix}${name} -m pytest`;
|
|
39
|
+
}
|
|
40
|
+
return `${prefix}pytest`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Does this repository keep its package under `src/` rather than at the root?
|
|
45
|
+
*
|
|
46
|
+
* The sibling of the bug above, and a worse one. `-m` puts the *working
|
|
47
|
+
* directory* on `sys.path` — which is the fix for a module at the root, and
|
|
48
|
+
* no help at all when the package lives in `src/`. There, `import iniconfig`
|
|
49
|
+
* finds nothing in the working directory and falls through to whatever is
|
|
50
|
+
* installed in site-packages. The suite then runs green against a *different
|
|
51
|
+
* copy of the library than the one in the diff*: measured on `pytest-dev/
|
|
52
|
+
* iniconfig`, `_parse.py` gutted to `return False`, 49 tests passed, and the
|
|
53
|
+
* gate returned APPROVED (Exit 0).
|
|
54
|
+
*
|
|
55
|
+
* That is this project's worst failure shape — a check that examined
|
|
56
|
+
* something other than the thing under review, reporting a pass — and no
|
|
57
|
+
* amount of counting collected tests can see it, because the tests really
|
|
58
|
+
* did run. Only the import path can.
|
|
59
|
+
*
|
|
60
|
+
* `PYTHONPATH=src` is the ordinary spelling and puts the working tree first
|
|
61
|
+
* whether or not the package is also installed. Leading assignments are
|
|
62
|
+
* peeled into the child's environment by `runCommand`, so this needs no shell.
|
|
63
|
+
*
|
|
64
|
+
* Keyed on `__init__.py` under `src/`, which is the marker of a Python
|
|
65
|
+
* package and not of a Rust, C or JavaScript `src/` directory.
|
|
66
|
+
*/
|
|
67
|
+
export function isSrcLayout(root) {
|
|
68
|
+
const src = join(root, "src");
|
|
69
|
+
if (!existsSync(src)) return false;
|
|
70
|
+
try {
|
|
71
|
+
return readdirSync(src, { withFileTypes: true }).some(
|
|
72
|
+
(e) => e.isDirectory() && existsSync(join(src, e.name, "__init__.py"))
|
|
73
|
+
);
|
|
74
|
+
} catch (_) {
|
|
75
|
+
return false;
|
|
37
76
|
}
|
|
38
|
-
return "pytest";
|
|
39
77
|
}
|
|
40
78
|
|
|
41
79
|
/**
|
|
@@ -80,6 +118,27 @@ export function isPlaceholderTestScript(cmd) {
|
|
|
80
118
|
const trimmed = cmd.trim();
|
|
81
119
|
if (!trimmed) return true;
|
|
82
120
|
if (EMPTY_PROGRAM.test(trimmed)) return true;
|
|
121
|
+
|
|
122
|
+
// Node/Deno/Bun process.exit(0)
|
|
123
|
+
if (/^(?:\S*[/\\])?(?:node|nodejs|deno|bun)\b.*?(?:-e|--eval)\s+["'`]\s*(?:(?:void\s+0|0)?\s*;?\s*process\.exit\s*\(\s*0?\s*\)\s*;?)*\s*["'`]\s*$/.test(trimmed)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Python exit(0) / pass
|
|
128
|
+
if (/^(?:\S*[/\\])?python[\d.]*\b.*?-c\s+["'`]\s*(?:(?:import\s+sys\s*;?\s*)?(?:sys\.)?exit\s*\(\s*0?\s*\)|pass)?\s*;?\s*["'`]\s*$/.test(trimmed)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Shell running no-op / exit 0
|
|
133
|
+
if (/^(?:\S*[/\\])?(?:sh|bash|zsh|dash)\b.*?-c\s+["'`]?\s*(?::|true|exit\s+0)?\s*;?\s*["'`]?\s*$/.test(trimmed)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Pytest collection only without test execution
|
|
138
|
+
if (/\bpytest\b.*?(?:--collect-only\b)/.test(trimmed)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
83
142
|
// Drop the announcements; what matters is what the shell is left doing.
|
|
84
143
|
const remainder = trimmed
|
|
85
144
|
.split(/&&|;/)
|
|
@@ -234,7 +293,7 @@ export function oracleCandidates(root = process.cwd(), detected = "") {
|
|
|
234
293
|
} catch (_) {}
|
|
235
294
|
}
|
|
236
295
|
if (has("pytest.ini") || has("pyproject.toml") || has("setup.py") || has("tox.ini") || has("setup.cfg")) {
|
|
237
|
-
push(pytestCmd());
|
|
296
|
+
push(pytestCmd(process.env, root));
|
|
238
297
|
}
|
|
239
298
|
if (has("Cargo.toml")) push("cargo test");
|
|
240
299
|
if (has("go.mod")) push("go test ./...");
|
|
@@ -386,7 +445,7 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
|
|
|
386
445
|
}
|
|
387
446
|
if (existsSync(join(projectRoot, "pyproject.toml")) || existsSync(join(projectRoot, "requirements.txt")) || existsSync(join(projectRoot, "setup.py"))) {
|
|
388
447
|
const triggerFile = existsSync(join(projectRoot, "pyproject.toml")) ? "pyproject.toml" : existsSync(join(projectRoot, "requirements.txt")) ? "requirements.txt" : "setup.py";
|
|
389
|
-
return { ...container, stack: "python", testCmd: pytestCmd(), buildCmd: `${pythonBin()} -m compileall -q .`, triggerFile };
|
|
448
|
+
return { ...container, stack: "python", testCmd: pytestCmd(process.env, projectRoot), buildCmd: `${pythonBin()} -m compileall -q .`, triggerFile };
|
|
390
449
|
}
|
|
391
450
|
if (existsSync(join(projectRoot, "mix.exs"))) {
|
|
392
451
|
return { ...container, stack: "mix", testCmd: "mix test", buildCmd: "mix compile", triggerFile: "mix.exs" };
|
|
@@ -470,7 +529,7 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
|
|
|
470
529
|
}
|
|
471
530
|
const pyFile = rootFiles.find((f) => f.endsWith(".py"));
|
|
472
531
|
if (pyFile) {
|
|
473
|
-
return { ...container, stack: "python", testCmd: pytestCmd(), buildCmd: `${pythonBin()} -m compileall -q .`, triggerFile: pyFile };
|
|
532
|
+
return { ...container, stack: "python", testCmd: pytestCmd(process.env, projectRoot), buildCmd: `${pythonBin()} -m compileall -q .`, triggerFile: pyFile };
|
|
474
533
|
}
|
|
475
534
|
} catch (_) {}
|
|
476
535
|
|
|
@@ -966,19 +1025,19 @@ export function bootstrapZeroTestRepo(root = process.cwd(), options = {}) {
|
|
|
966
1025
|
try {
|
|
967
1026
|
let rawConfig = readFileSync(configPath, "utf-8");
|
|
968
1027
|
if (/^\s*test:\s*.*$/m.test(rawConfig)) {
|
|
969
|
-
rawConfig = rawConfig.replace(/^\s*test:\s*.*$/m, ` test:
|
|
1028
|
+
rawConfig = rawConfig.replace(/^\s*test:\s*.*$/m, () => ` test: ${yamlScalar(testCmd)}`);
|
|
970
1029
|
} else if (/^\s*verify:\s*$/m.test(rawConfig)) {
|
|
971
|
-
rawConfig = rawConfig.replace(/^\s*verify:\s*$/m, `verify:\n test:
|
|
1030
|
+
rawConfig = rawConfig.replace(/^\s*verify:\s*$/m, () => `verify:\n test: ${yamlScalar(testCmd)}`);
|
|
972
1031
|
} else {
|
|
973
|
-
rawConfig += `\nverify:\n test:
|
|
1032
|
+
rawConfig += `\nverify:\n test: ${yamlScalar(testCmd)}\n`;
|
|
974
1033
|
}
|
|
975
1034
|
if (detected.buildCmd && /^\s*build:\s*["']?["']?\s*$/m.test(rawConfig)) {
|
|
976
|
-
rawConfig = rawConfig.replace(/^\s*build:\s*.*$/m, ` build:
|
|
1035
|
+
rawConfig = rawConfig.replace(/^\s*build:\s*.*$/m, () => ` build: ${yamlScalar(detected.buildCmd)}`);
|
|
977
1036
|
}
|
|
978
1037
|
writeFileSync(configPath, rawConfig, "utf-8");
|
|
979
1038
|
} catch (_) {}
|
|
980
1039
|
} else {
|
|
981
|
-
const cfg = `version: 1\nprovider: jules\ntier: free\nverify:\n test:
|
|
1040
|
+
const cfg = `version: 1\nprovider: jules\ntier: free\nverify:\n test: ${yamlScalar(testCmd)}\n build: ${yamlScalar(detected.buildCmd || "")}\nlimits:\n diff_kb: 75\n daily_tasks: 15\n repair_attempts: 3\nbranch_prefix: agent/\nbase_branch: main\n`;
|
|
982
1041
|
writeFileSync(configPath, cfg, "utf-8");
|
|
983
1042
|
}
|
|
984
1043
|
|
|
@@ -987,12 +1046,12 @@ export function bootstrapZeroTestRepo(root = process.cwd(), options = {}) {
|
|
|
987
1046
|
try {
|
|
988
1047
|
let rawJules = readFileSync(julesPath, "utf-8");
|
|
989
1048
|
if (/^\s*test_cmd:\s*.*$/m.test(rawJules)) {
|
|
990
|
-
rawJules = rawJules.replace(/^\s*test_cmd:\s*.*$/m, `test_cmd:
|
|
1049
|
+
rawJules = rawJules.replace(/^\s*test_cmd:\s*.*$/m, () => `test_cmd: ${yamlScalar(testCmd)}`);
|
|
991
1050
|
} else {
|
|
992
|
-
rawJules += `\ntest_cmd:
|
|
1051
|
+
rawJules += `\ntest_cmd: ${yamlScalar(testCmd)}\n`;
|
|
993
1052
|
}
|
|
994
1053
|
if (detected.buildCmd && /^\s*build_cmd:\s*["']?["']?\s*$/m.test(rawJules)) {
|
|
995
|
-
rawJules = rawJules.replace(/^\s*build_cmd:\s*.*$/m, `build_cmd:
|
|
1054
|
+
rawJules = rawJules.replace(/^\s*build_cmd:\s*.*$/m, () => `build_cmd: ${yamlScalar(detected.buildCmd)}`);
|
|
996
1055
|
}
|
|
997
1056
|
writeFileSync(julesPath, rawJules, "utf-8");
|
|
998
1057
|
} catch (_) {}
|
package/src/test-paths.mjs
CHANGED
|
@@ -40,7 +40,15 @@ const TEST_DIR_SEGMENTS = new Set(["test", "tests", "spec", "specs", "__tests__"
|
|
|
40
40
|
* Covers, by directory: `test/`, `tests/`, `spec/`, `specs/`, `__tests__/` at
|
|
41
41
|
* any depth including the repository root. By filename: `foo.test.js`,
|
|
42
42
|
* `foo.spec.ts`, `foo_test.go`, `foo_spec.rb`, `test_calc.py` (pytest),
|
|
43
|
-
* `spec_helper.rb`,
|
|
43
|
+
* `spec_helper.rb`, Foundry's `Foo.t.sol` / `FooTest.sol`, and the canonical
|
|
44
|
+
* Node root test file — `test.js`, `test.mjs`, `test.ts` and the rest of the
|
|
45
|
+
* `.cjs`/`.jsx`/`.tsx` family. AVA runs the root `test.js` by convention
|
|
46
|
+
* ("test files starting with `test` … at the repo root"), and so does
|
|
47
|
+
* `node --test test.js`; P-Limit's whole suite lived in one and the guard
|
|
48
|
+
* watched none of it. The bare-name rule is limited to the Node family on
|
|
49
|
+
* purpose: `test.py` is not a pytest file (pytest collects `test_*.py`),
|
|
50
|
+
* `test.go` is not a Go test (`*_test.go`), and `test.rb` is neither an RSpec
|
|
51
|
+
* (`*_spec.rb`) nor a Minitest (`test_*.rb`) file.
|
|
44
52
|
*
|
|
45
53
|
* @param {string} file - Repo-relative path, either separator.
|
|
46
54
|
* @returns {boolean}
|
|
@@ -55,6 +63,7 @@ export function isTestPath(file) {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
const base = segments[segments.length - 1];
|
|
66
|
+
if (ROOT_NODE_TEST_RE.test(base)) return true;
|
|
58
67
|
return (
|
|
59
68
|
base.includes(".test.") ||
|
|
60
69
|
base.includes(".spec.") ||
|
|
@@ -66,3 +75,11 @@ export function isTestPath(file) {
|
|
|
66
75
|
base.endsWith(".t.sol")
|
|
67
76
|
);
|
|
68
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A supported Node runner's canonical root test file: `test` exactly, with a
|
|
81
|
+
* JavaScript-family extension. `test.helpers.js` and `tests.js` must not
|
|
82
|
+
* match — a name that starts with the word and keeps going is a support
|
|
83
|
+
* module, and the plural is an ordinary source file.
|
|
84
|
+
*/
|
|
85
|
+
const ROOT_NODE_TEST_RE = /^test\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/;
|
package/src/wizard-init.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, openSync, fsyncSync, closeSync, renameSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { parseYaml, TIER_PRESETS, VENDOR_TIERS, FALLBACK_TIER } from "./config.mjs";
|
|
3
|
+
import { parseYaml, yamlScalar, 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
6
|
import { resolveWorkspaceBoundary, oracleCandidates } from "./stack-detector.mjs";
|
|
7
7
|
import { PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
|
|
8
8
|
import { detectStackOracles, runVerificationProbe } from "./wizard-oracle.mjs";
|
|
9
|
-
import { parseCollectedTests } from "./ops/test-collection.mjs";
|
|
9
|
+
import { parseCollectedTests, producedNoOutput, looksLikeTestSuiteCommand } from "./ops/test-collection.mjs";
|
|
10
10
|
import { select, multiSelect, input, confirm, spinner, isTTY } from "./tui.mjs";
|
|
11
11
|
import { KIT_VERSION } from "./version.mjs";
|
|
12
12
|
|
|
@@ -130,22 +130,26 @@ export function planInit(root = process.cwd(), options = {}) {
|
|
|
130
130
|
// scaffolded limits always match what the runtime will later enforce.
|
|
131
131
|
const tierPresetLimits = TIER_PROFILES[tierName] || TIER_PROFILES[FALLBACK_TIER];
|
|
132
132
|
|
|
133
|
-
// Preserve existing config if present
|
|
133
|
+
// Preserve existing config if present (ignored when pristine requested)
|
|
134
134
|
let existingConfig = {};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
if (!options.pristine) {
|
|
136
|
+
const existingConfigPath = join(root, ".agent", "config.yml");
|
|
137
|
+
if (existsSync(existingConfigPath)) {
|
|
138
|
+
try {
|
|
139
|
+
existingConfig = parseYaml(readFileSync(existingConfigPath, "utf-8")) || {};
|
|
140
|
+
} catch (_) {}
|
|
141
|
+
}
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
// Preserve existing jules.yml if present
|
|
144
|
+
// Preserve existing jules.yml if present (ignored when pristine requested)
|
|
143
145
|
let existingJules = {};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
if (!options.pristine) {
|
|
147
|
+
const existingJulesPath = join(root, ".agent", "jules.yml");
|
|
148
|
+
if (existsSync(existingJulesPath)) {
|
|
149
|
+
try {
|
|
150
|
+
existingJules = parseYaml(readFileSync(existingJulesPath, "utf-8")) || {};
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
}
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
const customLimits = options.limits || existingConfig.limits;
|
|
@@ -201,6 +205,19 @@ export function planInit(root = process.cwd(), options = {}) {
|
|
|
201
205
|
? `\nlimits:\n concurrency: ${limits.concurrency}\n daily_tasks: ${limits.daily_tasks}\n stagger_ms: ${limits.stagger_ms}\n diff_kb: ${limits.diff_kb}\n`
|
|
202
206
|
: "";
|
|
203
207
|
|
|
208
|
+
// A generated comment must not begin with an ESLint directive keyword.
|
|
209
|
+
//
|
|
210
|
+
// `global`, `globals`, `exported`, `eslint`, `eslint-disable` and friends are
|
|
211
|
+
// configuration when they open a comment — in any language ESLint has a
|
|
212
|
+
// parser for, YAML included. This template began a line with "global runs
|
|
213
|
+
// the ...", which ESLint read as `/* global runs, the, ... */`: a declaration
|
|
214
|
+
// of globals named after each word of the sentence. Measured on
|
|
215
|
+
// `unjs/unimport`, that produced 18 `no-unused-vars` errors quoting
|
|
216
|
+
// individual English words back at the user, on a file `init` had written
|
|
217
|
+
// thirty seconds earlier.
|
|
218
|
+
//
|
|
219
|
+
// The word is unavoidable — `global` is the name of the setting being
|
|
220
|
+
// explained — so the sentence leads with the key instead.
|
|
204
221
|
const configYaml = `# Agent Orchestrator Kit Config (v${KIT_VERSION})
|
|
205
222
|
# provider: jules | claude-code | codex | gemini-flash (agentctl providers)
|
|
206
223
|
version: 1
|
|
@@ -212,16 +229,16 @@ ${limitsBlock}
|
|
|
212
229
|
verify:
|
|
213
230
|
# minimal | standard | max — see: agentctl profile --list
|
|
214
231
|
profile: ${profile}
|
|
215
|
-
# global runs the repository
|
|
216
|
-
# to their sub-projects
|
|
232
|
+
# scope: global runs the commands this repository declares; affected
|
|
233
|
+
# resolves changed files to their sub-projects, running only those suites
|
|
217
234
|
scope: ${verifyScope}
|
|
218
|
-
test:
|
|
235
|
+
test: ${yamlScalar(verify.test)}
|
|
219
236
|
# How long a verification stage may run before the gate kills it (default
|
|
220
237
|
# 300000). Raise it for a suite that legitimately takes longer.
|
|
221
238
|
timeout_ms: 300000
|
|
222
|
-
build:
|
|
223
|
-
lint:
|
|
224
|
-
typecheck:
|
|
239
|
+
build: ${yamlScalar(verify.build)}
|
|
240
|
+
lint: ${yamlScalar(verify.lint)}
|
|
241
|
+
typecheck: ${yamlScalar(verify.typecheck)}
|
|
225
242
|
|
|
226
243
|
presets:
|
|
227
244
|
${selectedPresets.map((p) => ` - ${p}`).join("\n")}
|
|
@@ -246,11 +263,11 @@ ${selectedPresets.map((p) => ` - ${p}`).join("\n")}
|
|
|
246
263
|
|
|
247
264
|
const julesYaml = `# Google Jules Repository Configuration (Version 2)
|
|
248
265
|
version: 2
|
|
249
|
-
test_cmd:
|
|
250
|
-
build_cmd:
|
|
266
|
+
test_cmd: ${yamlScalar(verify.test)}
|
|
267
|
+
build_cmd: ${yamlScalar(verify.build)}
|
|
251
268
|
forbidden_paths:
|
|
252
|
-
${forbiddenPaths.map((p) => ` -
|
|
253
|
-
allow_paths: ${allowPaths.length > 0 ? "\n" + allowPaths.map((p) => ` -
|
|
269
|
+
${forbiddenPaths.map((p) => ` - ${yamlScalar(p)}`).join("\n")}
|
|
270
|
+
allow_paths: ${allowPaths.length > 0 ? "\n" + allowPaths.map((p) => ` - ${yamlScalar(p)}`).join("\n") : "[]"}
|
|
254
271
|
`;
|
|
255
272
|
|
|
256
273
|
return {
|
|
@@ -330,8 +347,18 @@ export function loadPresets(root = process.cwd()) {
|
|
|
330
347
|
* cost of rejecting a candidate is trying the next one, where at gate time it
|
|
331
348
|
* would be a hard red on a repository that is fine.
|
|
332
349
|
*/
|
|
333
|
-
function probeVerdict(probeRes) {
|
|
350
|
+
function probeVerdict(probeRes, cmd) {
|
|
334
351
|
if (!probeRes.ok) return "failed";
|
|
352
|
+
// Writing nothing at all is `empty`, not `silent`. The distinction is the
|
|
353
|
+
// whole point: `silent` is the forgiving bucket that keeps a command the
|
|
354
|
+
// guard could not read, and `pnpm -r test` landed in it because it prints
|
|
355
|
+
// no output to be unreadable. So the candidate this verdict was introduced
|
|
356
|
+
// to reject was the one case it waved through, and every repository
|
|
357
|
+
// scaffolded on such a workspace kept it.
|
|
358
|
+
// Same rule as the gate's floor, from the same predicate: a command that
|
|
359
|
+
// claims to run a suite and printed nothing ran none. A static gate that
|
|
360
|
+
// printed nothing did what it promised, so it keeps the forgiving verdict.
|
|
361
|
+
if (looksLikeTestSuiteCommand(cmd) && producedNoOutput(probeRes.stdout, probeRes.stderr)) return "empty";
|
|
335
362
|
const { count } = parseCollectedTests(probeRes.stdout, probeRes.stderr);
|
|
336
363
|
if (count === null) return "silent";
|
|
337
364
|
return count > 0 ? "ran" : "empty";
|
|
@@ -341,7 +368,7 @@ async function resolveRunnableOracle(root, testCmd, options = {}) {
|
|
|
341
368
|
if (!testCmd) return testCmd;
|
|
342
369
|
const probeSp = spinner(`Probing oracle: ${testCmd}`, options);
|
|
343
370
|
const probeRes = await runVerificationProbe(testCmd, root);
|
|
344
|
-
const verdict = probeVerdict(probeRes);
|
|
371
|
+
const verdict = probeVerdict(probeRes, testCmd);
|
|
345
372
|
if (verdict === "ran") {
|
|
346
373
|
probeSp.stop(`Oracle verified successfully (${probeRes.durationMs}ms)`);
|
|
347
374
|
return testCmd;
|
|
@@ -364,7 +391,7 @@ async function resolveRunnableOracle(root, testCmd, options = {}) {
|
|
|
364
391
|
for (const cand of alternates) {
|
|
365
392
|
const altSp = spinner(`Trying ${cand}`, options);
|
|
366
393
|
const altRes = await runVerificationProbe(cand, root);
|
|
367
|
-
const altVerdict = probeVerdict(altRes);
|
|
394
|
+
const altVerdict = probeVerdict(altRes, cand);
|
|
368
395
|
if (altVerdict === "ran") {
|
|
369
396
|
altSp.stop(`${cand} runs here (${altRes.durationMs}ms) — using it instead`);
|
|
370
397
|
return cand;
|
package/src/wizard-oracle.mjs
CHANGED
|
@@ -87,7 +87,7 @@ export function detectStackOracles(root = process.cwd()) {
|
|
|
87
87
|
const flag = isCargoWs ? " --workspace" : "";
|
|
88
88
|
candidates.testCmd = `cargo test${flag}`;
|
|
89
89
|
candidates.buildCmd = `cargo build${flag}`;
|
|
90
|
-
candidates.lintCmd = `cargo clippy${flag}
|
|
90
|
+
candidates.lintCmd = `cargo clippy${flag}`;
|
|
91
91
|
candidates.typecheckCmd = `cargo check${flag}`;
|
|
92
92
|
}
|
|
93
93
|
|
package/src/wizard-task.mjs
CHANGED
|
@@ -261,7 +261,21 @@ ${fullPrompt}
|
|
|
261
261
|
* @returns {Promise<{ ok: boolean, dryRun: boolean, taskFile: string, written: boolean, plan: object }>}
|
|
262
262
|
*/
|
|
263
263
|
export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
|
|
264
|
-
|
|
264
|
+
// `-p` is the documented way to skip the questions, so it has to skip them.
|
|
265
|
+
//
|
|
266
|
+
// README: "Pass the prompt to skip straight to review:
|
|
267
|
+
// npx jules-orchestrator-kit task create -p 'Refactor the invoice module'".
|
|
268
|
+
// Interactivity was decided by `isTTY` alone, and `-p` was consulted only by
|
|
269
|
+
// the TODO-import branch below — so in a real terminal the advertised
|
|
270
|
+
// quickstart stopped at "? Task Title" and waited for a keypress forever,
|
|
271
|
+
// then asked for the instructions it had already been handed.
|
|
272
|
+
//
|
|
273
|
+
// It looked fine under test because a non-TTY run takes the headless path and
|
|
274
|
+
// never asks. That is the same defect the flag itself has: one rule, two
|
|
275
|
+
// paths, and only the path nobody was watching kept the old answer. Making
|
|
276
|
+
// `-p` mean the headless path everywhere makes the two agree by construction.
|
|
277
|
+
const promptSupplied = typeof options.prompt === "string" && options.prompt.trim() !== "";
|
|
278
|
+
const interactive = options.interactive !== false && !promptSupplied && isTTY(options.stdin || process.stdin);
|
|
265
279
|
|
|
266
280
|
let title = options.title;
|
|
267
281
|
let promptText = options.prompt;
|