jules-orchestrator-kit 0.52.8 → 0.54.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 +68 -8
- package/bin/agentctl.mjs +327 -24
- package/bin/init.js +87 -74
- package/index.mjs +10 -0
- package/package.json +5 -2
- package/scripts/jules-create.mjs +5 -16
- package/scripts/jules-patch.mjs +18 -10
- package/scripts/stale-base-check.mjs +1 -1
- package/src/ci-templates.mjs +199 -0
- package/src/config-edit.mjs +138 -0
- package/src/config.mjs +39 -11
- package/src/engine.mjs +2 -21
- package/src/env-aliases.mjs +80 -0
- package/src/git.mjs +79 -0
- package/src/mcp.mjs +19 -1
- package/src/ops/cli-intent.mjs +29 -0
- package/src/ops/command-registry.mjs +19 -10
- package/src/ops/doctor-registry.mjs +75 -32
- package/src/ops/ide-scaffold.mjs +22 -16
- package/src/ops/next-step.mjs +38 -7
- package/src/ops/verify-output.mjs +38 -0
- package/src/profiles.mjs +188 -0
- package/src/provider-readiness.mjs +284 -0
- package/src/security.mjs +42 -1
- package/src/tui.mjs +14 -3
- package/src/wizard-init.mjs +88 -12
- package/src/wizard-task.mjs +22 -5
- package/src/ops/doctor-planner.mjs +0 -220
- package/src/ops/receipts.mjs +0 -171
- package/src/ops/swarm-actions.mjs +0 -156
- package/src/ops/task-actions.mjs +0 -214
- package/src/ops/transaction.mjs +0 -278
- package/src/ux/capabilities.mjs +0 -180
- package/src/ux/diff-viewer.mjs +0 -289
- package/src/ux/layout.mjs +0 -195
- package/src/ux/log-viewer.mjs +0 -110
- package/src/ux/palette.mjs +0 -179
- package/src/ux/queue-model.mjs +0 -206
- package/src/ux/renderer.mjs +0 -188
- package/src/ux/swarm-model.mjs +0 -146
- package/src/ux/terminal-session.mjs +0 -215
- package/src/ux/widgets.mjs +0 -323
- /package/src/{ux/key-decoder.mjs → key-decoder.mjs} +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { PROFILE_NAMES } from "./profiles.mjs";
|
|
4
|
+
import { PROVIDER_DESCRIPTORS } from "./provider-readiness.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* In-place edits to the repository's agent manifest.
|
|
8
|
+
*
|
|
9
|
+
* Surgical text edits rather than parse-and-reserialise: the kit's YAML reader
|
|
10
|
+
* is a subset parser, so round-tripping the file through it would drop every
|
|
11
|
+
* comment the wizard wrote and any key the subset does not model. The manifest
|
|
12
|
+
* is a file a human maintains; a tool that rewrites it must leave the rest of
|
|
13
|
+
* it alone.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The manifest this repository uses, or null when it has none.
|
|
18
|
+
* @param {string} root
|
|
19
|
+
* @returns {string|null}
|
|
20
|
+
*/
|
|
21
|
+
function findManifest(root) {
|
|
22
|
+
return [join(root, ".agent", "config.yml"), join(root, ".agent", "jules.yml")].find((f) => existsSync(f)) || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rewrite the manifest from a line transform, atomically.
|
|
27
|
+
* @param {string} file
|
|
28
|
+
* @param {(lines: string[]) => string[]} transform
|
|
29
|
+
* @returns {{ ok: boolean, file?: string, error?: string }}
|
|
30
|
+
*/
|
|
31
|
+
function editManifest(file, transform) {
|
|
32
|
+
let text;
|
|
33
|
+
try {
|
|
34
|
+
text = readFileSync(file, "utf-8");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { ok: false, error: `Could not read ${file}: ${err.message}` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
40
|
+
const lines = transform(text.split(/\r?\n/));
|
|
41
|
+
|
|
42
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(tmp, lines.join(eol), "utf-8");
|
|
45
|
+
renameSync(tmp, file);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { ok: false, error: `Could not write ${file}: ${err.message}` };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, file };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Set the top-level `provider:` key.
|
|
54
|
+
*
|
|
55
|
+
* `agentctl providers` used to point at `agentctl init --provider <name>` to
|
|
56
|
+
* change providers, which restarts the whole onboarding wizard — plan question
|
|
57
|
+
* and all — to edit one line. This is that one line.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} root
|
|
60
|
+
* @param {string} provider
|
|
61
|
+
* @returns {{ ok: boolean, file?: string, error?: string }}
|
|
62
|
+
*/
|
|
63
|
+
export function setConfigProvider(root, provider) {
|
|
64
|
+
const name = String(provider || "").trim();
|
|
65
|
+
if (!name) {
|
|
66
|
+
return { ok: false, error: `Provider name required. Known presets: ${Object.keys(PROVIDER_DESCRIPTORS).join(", ")}` };
|
|
67
|
+
}
|
|
68
|
+
if (!/^[\w.-]+$/.test(name)) {
|
|
69
|
+
return { ok: false, error: `Invalid provider name '${provider}'.` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const file = findManifest(root);
|
|
73
|
+
if (!file) return { ok: false, error: "No .agent/config.yml found. Run `agentctl init` first." };
|
|
74
|
+
|
|
75
|
+
return editManifest(file, (lines) => {
|
|
76
|
+
const idx = lines.findIndex((l) => /^provider:\s*/.test(l));
|
|
77
|
+
if (idx >= 0) {
|
|
78
|
+
lines[idx] = `provider: ${name}`;
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
// Keep it near the top where the wizard writes it, after `version:` if
|
|
82
|
+
// present, so a hand-read manifest still leads with what it is.
|
|
83
|
+
const versionIdx = lines.findIndex((l) => /^version:\s*/.test(l));
|
|
84
|
+
lines.splice(versionIdx >= 0 ? versionIdx + 1 : 0, 0, `provider: ${name}`);
|
|
85
|
+
return lines;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set `verify.profile` in the repository's manifest, in place.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} root
|
|
93
|
+
* @param {string} profile - one of {@link PROFILE_NAMES}
|
|
94
|
+
* @returns {{ ok: boolean, file?: string, error?: string }}
|
|
95
|
+
*/
|
|
96
|
+
export function setVerificationProfile(root, profile) {
|
|
97
|
+
const name = String(profile || "").toLowerCase();
|
|
98
|
+
if (!PROFILE_NAMES.includes(name)) {
|
|
99
|
+
return { ok: false, error: `Unknown profile '${profile}'. Choose one of: ${PROFILE_NAMES.join(", ")}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const file = findManifest(root);
|
|
103
|
+
if (!file) {
|
|
104
|
+
return { ok: false, error: "No .agent/config.yml found. Run `agentctl init` first." };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return editManifest(file, (lines) => {
|
|
108
|
+
|
|
109
|
+
// Find the `verify:` mapping and the `profile:` key nested directly under it.
|
|
110
|
+
let verifyIdx = -1;
|
|
111
|
+
let profileIdx = -1;
|
|
112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
113
|
+
if (/^verify:\s*$/.test(lines[i])) {
|
|
114
|
+
verifyIdx = i;
|
|
115
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
116
|
+
// A non-indented, non-blank line ends the block.
|
|
117
|
+
if (lines[j].trim() !== "" && !/^\s/.test(lines[j])) break;
|
|
118
|
+
if (/^\s+profile:\s*/.test(lines[j])) {
|
|
119
|
+
profileIdx = j;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (profileIdx >= 0) {
|
|
128
|
+
const indent = lines[profileIdx].match(/^(\s*)/)[1];
|
|
129
|
+
lines[profileIdx] = `${indent}profile: ${name}`;
|
|
130
|
+
} else if (verifyIdx >= 0) {
|
|
131
|
+
lines.splice(verifyIdx + 1, 0, ` profile: ${name}`);
|
|
132
|
+
} else {
|
|
133
|
+
if (lines.length && lines[lines.length - 1] !== "") lines.push("");
|
|
134
|
+
lines.push("verify:", ` profile: ${name}`, "");
|
|
135
|
+
}
|
|
136
|
+
return lines;
|
|
137
|
+
});
|
|
138
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -293,6 +293,10 @@ export function detectPackageManager(root = process.cwd(), pkg = {}) {
|
|
|
293
293
|
return "npm";
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
import { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
|
|
297
|
+
|
|
298
|
+
export { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS };
|
|
299
|
+
|
|
296
300
|
import {
|
|
297
301
|
detectPolyglotStack,
|
|
298
302
|
resolveWorkspaceBoundary,
|
|
@@ -321,6 +325,10 @@ export function detectStack(projectRoot = process.cwd()) {
|
|
|
321
325
|
export function resolveVerify(root = process.cwd(), userVerify = {}) {
|
|
322
326
|
const s = detectStack(root);
|
|
323
327
|
return {
|
|
328
|
+
// Carried through so `verify.profile` can gate stack-specific gates (V8
|
|
329
|
+
// diff coverage only exists where the test command runs on Node) without
|
|
330
|
+
// every caller re-running stack detection.
|
|
331
|
+
stack: s.stack || "unknown",
|
|
324
332
|
setup: userVerify.setup ?? s.setupCmd ?? "",
|
|
325
333
|
lint: userVerify.lint ?? s.fmtCmd ?? "",
|
|
326
334
|
test: userVerify.test ?? s.testCmd ?? "",
|
|
@@ -501,22 +509,42 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
501
509
|
if (normalizedLimits[k] === undefined) delete normalizedLimits[k];
|
|
502
510
|
}
|
|
503
511
|
|
|
512
|
+
// The verification commands, resolved once, so a profile can be expanded
|
|
513
|
+
// against the same values the gate will actually run.
|
|
514
|
+
const mergedVerify = {
|
|
515
|
+
setup: rawSetup ?? autoVerify.setup ?? "",
|
|
516
|
+
lint: rawLint ?? autoVerify.lint ?? "",
|
|
517
|
+
test: rawTest ?? autoVerify.test,
|
|
518
|
+
unit: rawUnit ?? rawTest ?? autoVerify.unit ?? autoVerify.test,
|
|
519
|
+
fuzz: rawFuzz ?? autoVerify.fuzz ?? "",
|
|
520
|
+
invariant: rawInvariant ?? autoVerify.invariant ?? "",
|
|
521
|
+
e2e: rawE2e ?? autoVerify.e2e ?? "",
|
|
522
|
+
teardown: rawTeardown ?? autoVerify.teardown ?? "",
|
|
523
|
+
build: rawBuild ?? autoVerify.build,
|
|
524
|
+
policy: parsed.verify?.policy ?? autoVerify.policy,
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// A hand-written `verify.stages:` is the operator being explicit and always
|
|
528
|
+
// wins; `verify.profile:` is the shorthand, expanded here rather than frozen
|
|
529
|
+
// into the scaffolded YAML so it stays correct when the stack changes.
|
|
530
|
+
const rawProfile = parsed.verify?.profile ?? parsed.profile ?? null;
|
|
531
|
+
const explicitStages = parsed.verify?.stages ?? autoVerify.stages ?? null;
|
|
532
|
+
const profilePlan =
|
|
533
|
+
!explicitStages && rawProfile
|
|
534
|
+
? buildProfileStages(rawProfile, { stack: autoVerify.stack, verify: mergedVerify })
|
|
535
|
+
: null;
|
|
536
|
+
|
|
504
537
|
const config = {
|
|
505
538
|
version: parsed.version || DEFAULTS.version,
|
|
506
539
|
provider: parsed.provider || DEFAULTS.provider,
|
|
507
540
|
tier: activeTier,
|
|
508
541
|
verify: {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
e2e: rawE2e ?? autoVerify.e2e ?? "",
|
|
516
|
-
teardown: rawTeardown ?? autoVerify.teardown ?? "",
|
|
517
|
-
build: rawBuild ?? autoVerify.build,
|
|
518
|
-
stages: parsed.verify?.stages ?? autoVerify.stages ?? null,
|
|
519
|
-
policy: parsed.verify?.policy ?? autoVerify.policy,
|
|
542
|
+
...mergedVerify,
|
|
543
|
+
stages: explicitStages ?? profilePlan?.stages ?? null,
|
|
544
|
+
profile: profilePlan?.profile ?? (rawProfile ? String(rawProfile).toLowerCase() : null),
|
|
545
|
+
// Named so `doctor` can tell an operator that the gate they enabled is
|
|
546
|
+
// running one fewer check than they think, and why.
|
|
547
|
+
profileSkipped: profilePlan?.skipped ?? [],
|
|
520
548
|
timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 60000,
|
|
521
549
|
},
|
|
522
550
|
evidence: {
|
package/src/engine.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { hydratePrompt, harvestFailure } from "./memory.mjs";
|
|
|
19
19
|
import { resolveRolePrompt } from "./role-resolver.mjs";
|
|
20
20
|
|
|
21
21
|
import { runAssertion } from "./assertions.mjs";
|
|
22
|
+
import { buildDefaultStages } from "./profiles.mjs";
|
|
22
23
|
import {
|
|
23
24
|
computeDirectoryHash,
|
|
24
25
|
generateEvidenceManifest,
|
|
@@ -283,27 +284,7 @@ export async function gate(opts = {}) {
|
|
|
283
284
|
if (Array.isArray(trustedVerify.stages) && trustedVerify.stages.length > 0) {
|
|
284
285
|
stagesToRun.push(...trustedVerify.stages);
|
|
285
286
|
} else {
|
|
286
|
-
|
|
287
|
-
stagesToRun.push({ id: "setup", kind: "setup", cmd: trustedVerify.setup, required: true, networkAccess: "allow" });
|
|
288
|
-
}
|
|
289
|
-
if (trustedVerify.lint) {
|
|
290
|
-
stagesToRun.push({ id: "lint", kind: "lint", cmd: trustedVerify.lint, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
291
|
-
}
|
|
292
|
-
if (trustedVerify.test || trustedVerify.unit) {
|
|
293
|
-
stagesToRun.push({ id: "unit", kind: "test", cmd: trustedVerify.test || trustedVerify.unit, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
294
|
-
}
|
|
295
|
-
if (trustedVerify.fuzz) {
|
|
296
|
-
stagesToRun.push({ id: "fuzz", kind: "fuzz", cmd: trustedVerify.fuzz, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
297
|
-
}
|
|
298
|
-
if (trustedVerify.invariant) {
|
|
299
|
-
stagesToRun.push({ id: "invariant", kind: "invariant", cmd: trustedVerify.invariant, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
300
|
-
}
|
|
301
|
-
if (trustedVerify.e2e) {
|
|
302
|
-
stagesToRun.push({ id: "e2e", kind: "e2e", cmd: trustedVerify.e2e, required: true, networkAccess: "allow" });
|
|
303
|
-
}
|
|
304
|
-
if (trustedVerify.build) {
|
|
305
|
-
stagesToRun.push({ id: "build", kind: "build", cmd: trustedVerify.build, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
306
|
-
}
|
|
287
|
+
stagesToRun.push(...buildDefaultStages(trustedVerify));
|
|
307
288
|
}
|
|
308
289
|
|
|
309
290
|
try {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor-neutral spellings for every environment variable the kit reads.
|
|
3
|
+
*
|
|
4
|
+
* Every knob in this codebase is spelled `JULES_*`, which is accurate for the
|
|
5
|
+
* provider it was written against and wrong for every other one: a repository
|
|
6
|
+
* driving `claude-code` or `codex` still had to export `JULES_SWARM_CONCURRENCY`
|
|
7
|
+
* to set worker slots, and `JULES_API_KEY` to set a key that never goes to
|
|
8
|
+
* Jules. Renaming the variables outright would break every existing checkout
|
|
9
|
+
* and CI job, so both spellings are supported and this table is the only place
|
|
10
|
+
* that has to know they are the same knob.
|
|
11
|
+
*
|
|
12
|
+
* Direction is deliberate: `AGENT_*` is the name to document, `JULES_*` is the
|
|
13
|
+
* name the code reads, and the alias fills the second from the first. A
|
|
14
|
+
* `JULES_*` value already in the environment always wins, so adding an
|
|
15
|
+
* `AGENT_*` export can never change the behaviour of a working setup.
|
|
16
|
+
*
|
|
17
|
+
* @type {Record<string, string>}
|
|
18
|
+
*/
|
|
19
|
+
export const ENV_ALIASES = {
|
|
20
|
+
AGENT_API_KEY: "JULES_API_KEY",
|
|
21
|
+
AGENT_API_KEYS: "JULES_API_KEYS",
|
|
22
|
+
AGENT_API_KEY_SECONDARY: "JULES_API_KEY_SECONDARY",
|
|
23
|
+
AGENT_API_URL: "JULES_API_URL",
|
|
24
|
+
AGENT_REPO: "JULES_REPO",
|
|
25
|
+
AGENT_PROJECT_ID: "JULES_PROJECT_ID",
|
|
26
|
+
AGENT_PROJECT_ROOT: "JULES_PROJECT_ROOT",
|
|
27
|
+
AGENT_TIER: "JULES_TIER",
|
|
28
|
+
AGENT_DAILY_BUDGET: "JULES_DAILY_BUDGET",
|
|
29
|
+
AGENT_MAX_DIFF_KB: "JULES_MAX_DIFF_KB",
|
|
30
|
+
AGENT_DRY_RUN: "JULES_DRY_RUN",
|
|
31
|
+
AGENT_REPOLESS: "JULES_REPOLESS",
|
|
32
|
+
AGENT_SWARM_CONCURRENCY: "JULES_SWARM_CONCURRENCY",
|
|
33
|
+
AGENT_SWARM_STAGGER_MS: "JULES_SWARM_STAGGER_MS",
|
|
34
|
+
AGENT_PACE_MS: "JULES_PACE_MS",
|
|
35
|
+
AGENT_SLOT_INDEX: "JULES_SLOT_INDEX",
|
|
36
|
+
AGENT_SLOT_TOTAL: "JULES_SLOT_TOTAL",
|
|
37
|
+
AGENT_USE_WORKTREES: "JULES_USE_WORKTREES",
|
|
38
|
+
AGENT_ALLOW_COMMAND_FILE_CHANGES: "JULES_ALLOW_COMMAND_FILE_CHANGES",
|
|
39
|
+
AGENT_ALLOW_AGENT_RULE_CHANGES: "JULES_ALLOW_AGENT_RULE_CHANGES",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fill the legacy `JULES_*` variables from their `AGENT_*` equivalents.
|
|
44
|
+
*
|
|
45
|
+
* Mutates the passed environment object in place — that is the point: every
|
|
46
|
+
* downstream module reads `process.env` directly, and rewriting hundreds of
|
|
47
|
+
* call sites to consult an alias table would be a far larger change with far
|
|
48
|
+
* more places to get it wrong. Called once at process entry.
|
|
49
|
+
*
|
|
50
|
+
* An empty-string alias is treated as unset, matching how the rest of the kit
|
|
51
|
+
* reads keys (`(process.env.X || "").trim()`), so `AGENT_API_KEY=` in a .env
|
|
52
|
+
* file does not shadow a real `JULES_API_KEY`.
|
|
53
|
+
*
|
|
54
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
55
|
+
* @returns {string[]} canonical names that were populated from an alias
|
|
56
|
+
*/
|
|
57
|
+
export function applyEnvAliases(env = process.env) {
|
|
58
|
+
const applied = [];
|
|
59
|
+
for (const [alias, canonical] of Object.entries(ENV_ALIASES)) {
|
|
60
|
+
const aliasValue = env[alias];
|
|
61
|
+
if (aliasValue === undefined || String(aliasValue).trim() === "") continue;
|
|
62
|
+
const existing = env[canonical];
|
|
63
|
+
if (existing !== undefined && String(existing).trim() !== "") continue;
|
|
64
|
+
env[canonical] = aliasValue;
|
|
65
|
+
applied.push(canonical);
|
|
66
|
+
}
|
|
67
|
+
return applied;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Both spellings of one knob, for help text and diagnostics that should name
|
|
72
|
+
* the vendor-neutral variable first.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} canonical - a `JULES_*` name
|
|
75
|
+
* @returns {{ canonical: string, alias: string|null }}
|
|
76
|
+
*/
|
|
77
|
+
export function describeEnvVar(canonical) {
|
|
78
|
+
const alias = Object.entries(ENV_ALIASES).find(([, c]) => c === canonical)?.[0] || null;
|
|
79
|
+
return { canonical, alias };
|
|
80
|
+
}
|
package/src/git.mjs
CHANGED
|
@@ -454,6 +454,85 @@ export function parseGitHubRepo(remoteUrl) {
|
|
|
454
454
|
* @param {string} [root=process.cwd()]
|
|
455
455
|
* @returns {string}
|
|
456
456
|
*/
|
|
457
|
+
/**
|
|
458
|
+
* Work out which branch this repository actually treats as its base.
|
|
459
|
+
*
|
|
460
|
+
* `main` was hardcoded as the scaffolded default, which is wrong the moment
|
|
461
|
+
* `git init` picks `master` (still the default on many installed gits) or the
|
|
462
|
+
* project standardised on `develop`. The failure was not graceful: the first
|
|
463
|
+
* `agentctl check` could not resolve the base ref and rejected with exit 1.
|
|
464
|
+
*
|
|
465
|
+
* Resolution order, strongest evidence first:
|
|
466
|
+
* 1. `origin/HEAD` — the remote states its own default branch.
|
|
467
|
+
* 2. A local `main`, then `master` — the conventional names, preferred over
|
|
468
|
+
* the checked-out branch so running `init` from a feature branch does not
|
|
469
|
+
* record that feature branch as the base for everything after.
|
|
470
|
+
* 3. The current branch — covers a fresh `git init` with no commits, where
|
|
471
|
+
* HEAD points at an unborn branch that `--show-current` still names.
|
|
472
|
+
* 4. `main`, when there is no git information at all to go on.
|
|
473
|
+
*
|
|
474
|
+
* @param {string} [root=process.cwd()]
|
|
475
|
+
* @returns {string}
|
|
476
|
+
*/
|
|
477
|
+
/**
|
|
478
|
+
* Split a list of repo-relative paths into the ones git already tracks and the
|
|
479
|
+
* ones it does not.
|
|
480
|
+
*
|
|
481
|
+
* Used to tell an onboarding mistake apart from an agent overstepping. Both
|
|
482
|
+
* arrive as the same exit-3 scope violation on the same protected paths, but
|
|
483
|
+
* "the files `init` just wrote are not committed yet" and "something edited the
|
|
484
|
+
* rules it is governed by" need opposite advice, and the operator who has just
|
|
485
|
+
* met the tool gets the wrong half if the two are not distinguished.
|
|
486
|
+
*
|
|
487
|
+
* @param {string} root
|
|
488
|
+
* @param {string[]} files - repo-relative paths
|
|
489
|
+
* @returns {{ tracked: string[], untracked: string[] }}
|
|
490
|
+
*/
|
|
491
|
+
export function partitionTracked(root = process.cwd(), files = []) {
|
|
492
|
+
const list = files.filter((f) => typeof f === "string" && f && !f.startsWith("-"));
|
|
493
|
+
if (list.length === 0) return { tracked: [], untracked: [] };
|
|
494
|
+
let out = "";
|
|
495
|
+
try {
|
|
496
|
+
out = git(["ls-files", "--", ...list], { cwd: root, ignoreError: true }) || "";
|
|
497
|
+
} catch (_) {
|
|
498
|
+
// Without an answer, claim nothing is tracked-or-not: callers fall back to
|
|
499
|
+
// the generic advice rather than to a guess.
|
|
500
|
+
return { tracked: [], untracked: [] };
|
|
501
|
+
}
|
|
502
|
+
const tracked = new Set(out.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
503
|
+
return {
|
|
504
|
+
tracked: list.filter((f) => tracked.has(f)),
|
|
505
|
+
untracked: list.filter((f) => !tracked.has(f)),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function detectDefaultBranch(root = process.cwd()) {
|
|
510
|
+
// `git()` returns trimmed stdout, and "" for a non-zero exit under
|
|
511
|
+
// ignoreError — there is no status field to read.
|
|
512
|
+
const ask = (args) => {
|
|
513
|
+
try {
|
|
514
|
+
return git(args, { cwd: root, ignoreError: true }) || "";
|
|
515
|
+
} catch (_) {
|
|
516
|
+
return "";
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const remoteHead = ask(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
|
|
521
|
+
if (remoteHead.startsWith("origin/")) {
|
|
522
|
+
const name = remoteHead.slice("origin/".length).trim();
|
|
523
|
+
if (name) return name;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for (const candidate of ["main", "master"]) {
|
|
527
|
+
if (ask(["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`])) return candidate;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const current = ask(["branch", "--show-current"]);
|
|
531
|
+
if (current) return current;
|
|
532
|
+
|
|
533
|
+
return "main";
|
|
534
|
+
}
|
|
535
|
+
|
|
457
536
|
export function resolveGitRemoteOrigin(root = process.cwd()) {
|
|
458
537
|
try {
|
|
459
538
|
const raw = git(["config", "--get", "remote.origin.url"], { cwd: root, ignoreError: true });
|
package/src/mcp.mjs
CHANGED
|
@@ -143,7 +143,7 @@ export const MCP_TOOLS = [
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
name: "get_jules_status",
|
|
146
|
-
description: "Retrieve orchestrator status
|
|
146
|
+
description: "Retrieve orchestrator status: daily task budget, active locks, stack diagnostics, the configured provider and whether it is reachable, and the verification stages the gate will run.",
|
|
147
147
|
inputSchema: {
|
|
148
148
|
type: "object",
|
|
149
149
|
properties: {},
|
|
@@ -439,6 +439,10 @@ export async function handleMcpRequest(request, opts = {}) {
|
|
|
439
439
|
const stackInfo = detectStack(root);
|
|
440
440
|
const budget = budgetStatus(config, root);
|
|
441
441
|
const locks = lockStatus(root);
|
|
442
|
+
const { probeProvider } = await import("./provider-readiness.mjs");
|
|
443
|
+
const { buildDefaultStages } = await import("./profiles.mjs");
|
|
444
|
+
const providerProbe = probeProvider(config.provider || "jules");
|
|
445
|
+
const profileStages = config.verify.stages || buildDefaultStages(config.verify);
|
|
442
446
|
const status = {
|
|
443
447
|
version: MCP_SERVER_INFO.version,
|
|
444
448
|
root,
|
|
@@ -455,6 +459,20 @@ export async function handleMcpRequest(request, opts = {}) {
|
|
|
455
459
|
},
|
|
456
460
|
activeLocksCount: locks.length,
|
|
457
461
|
locks,
|
|
462
|
+
// An IDE agent orienting itself needs to know which agent this repo
|
|
463
|
+
// dispatches to and how hard its work will be verified before it
|
|
464
|
+
// starts, not after the gate rejects it.
|
|
465
|
+
provider: {
|
|
466
|
+
name: providerProbe.name,
|
|
467
|
+
kind: providerProbe.kind,
|
|
468
|
+
ready: providerProbe.ready,
|
|
469
|
+
reason: providerProbe.reason,
|
|
470
|
+
},
|
|
471
|
+
verification: {
|
|
472
|
+
profile: config.verify.profile,
|
|
473
|
+
stages: profileStages.map((st) => st.id),
|
|
474
|
+
skipped: (config.verify.profileSkipped || []).map((sk) => sk.id),
|
|
475
|
+
},
|
|
458
476
|
};
|
|
459
477
|
return {
|
|
460
478
|
jsonrpc: "2.0",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a command invocation is asking to be walked through a wizard, or has
|
|
3
|
+
* already said everything the wizard would ask.
|
|
4
|
+
*
|
|
5
|
+
* `agentctl task create --title X --prompt Y` states the whole task and then
|
|
6
|
+
* opened the interactive wizard anyway, which re-asked for the title and the
|
|
7
|
+
* prompt in a terminal and hung a CI job that had no terminal to answer with.
|
|
8
|
+
* Neither `--yes` nor `--non-interactive` was passed, so the CLI concluded
|
|
9
|
+
* "interactive" from the absence of a flag rather than from the presence of the
|
|
10
|
+
* answers.
|
|
11
|
+
*
|
|
12
|
+
* Kept as a pure function, out of the CLI script, because that script executes
|
|
13
|
+
* on import and so cannot be exercised from a test.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} input
|
|
16
|
+
* @param {boolean} [input.interactive] - `--interactive` was passed explicitly
|
|
17
|
+
* @param {boolean} [input.nonInteractive] - `--non-interactive` / `--no-interactive`
|
|
18
|
+
* @param {boolean} [input.yes] - `--yes`
|
|
19
|
+
* @param {boolean} [input.fullySpecified] - every field the wizard would ask for is present
|
|
20
|
+
* @returns {boolean|undefined} true/false to force a mode, undefined to let the
|
|
21
|
+
* callee decide from whether stdin is a TTY
|
|
22
|
+
*/
|
|
23
|
+
export function resolveWizardInteractivity(input = {}) {
|
|
24
|
+
// An explicit `--interactive` always wins: the flags may be intended as seeds
|
|
25
|
+
// to edit rather than as a complete invocation.
|
|
26
|
+
if (input.interactive === true) return true;
|
|
27
|
+
if (input.nonInteractive || input.yes || input.fullySpecified) return false;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
@@ -70,35 +70,44 @@ export const COMMAND_REGISTRY = [
|
|
|
70
70
|
shortcuts: ["d", "doc"],
|
|
71
71
|
examples: [
|
|
72
72
|
"agentctl doctor",
|
|
73
|
-
"agentctl doctor --
|
|
74
|
-
"agentctl doctor --fix safe --yes",
|
|
73
|
+
"agentctl doctor --probe",
|
|
75
74
|
"agentctl doctor --json",
|
|
76
75
|
],
|
|
76
|
+
// `--interactive`, `--fix` and `--yes` were listed here and implemented
|
|
77
|
+
// nowhere: the report carries remediation entries, but nothing applies
|
|
78
|
+
// them. Advertising a flag the command silently ignores is the same defect
|
|
79
|
+
// as documenting `queue` as a read-only browser. They come back to this
|
|
80
|
+
// list when an apply step exists.
|
|
77
81
|
flags: [
|
|
78
|
-
{ name: "
|
|
79
|
-
{ name: "fix", type: "string", description: "Apply fix classes (e.g. 'safe' or check IDs)" },
|
|
80
|
-
{ name: "probe", type: "boolean", description: "Enable active network/execution probes" },
|
|
82
|
+
{ name: "probe", type: "boolean", description: "Actively start the provider CLI to check it answers, rather than only finding it on PATH" },
|
|
81
83
|
{ name: "json", type: "boolean", description: "Output structured JSON doctor report" },
|
|
82
|
-
{ name: "yes", type: "boolean", description: "Bypass interactive confirmation for safe fixes" },
|
|
83
84
|
],
|
|
84
85
|
},
|
|
85
86
|
{
|
|
86
87
|
id: "queue",
|
|
88
|
+
// This described a passive viewer — "Browse and manage", `mutates: false`,
|
|
89
|
+
// `risk: low` — while the handler runs the queue: it dispatches every task
|
|
90
|
+
// to the provider and spends budget. Someone reading `--help` before their
|
|
91
|
+
// first run was told the opposite of what the command does.
|
|
87
92
|
path: ["queue"],
|
|
88
93
|
title: "queue",
|
|
89
|
-
description: "
|
|
94
|
+
description: "Execute pending task envelopes: dispatches each to the provider and moves it out of the queue",
|
|
90
95
|
category: "Operate",
|
|
91
|
-
mutates:
|
|
92
|
-
risk: "
|
|
96
|
+
mutates: true,
|
|
97
|
+
risk: "moderate",
|
|
93
98
|
interactive: "optional",
|
|
94
99
|
requiresRepository: true,
|
|
95
100
|
shortcuts: ["q"],
|
|
96
101
|
examples: [
|
|
102
|
+
"agentctl queue --dry-run",
|
|
97
103
|
"agentctl queue",
|
|
98
|
-
"agentctl queue --
|
|
104
|
+
"agentctl queue --dag --concurrency 3",
|
|
99
105
|
"agentctl queue --json",
|
|
100
106
|
],
|
|
101
107
|
flags: [
|
|
108
|
+
{ name: "dag", type: "boolean", description: "Resolve depends-on order via Kahn's algorithm before running" },
|
|
109
|
+
{ name: "concurrency", type: "string", description: "Parallel worker slots (defaults to limits.concurrency)" },
|
|
110
|
+
{ name: "dry-run", type: "boolean", description: "Report what would run without dispatching or moving anything" },
|
|
102
111
|
{ name: "interactive", type: "boolean", description: "Open full-screen queue dashboard" },
|
|
103
112
|
{ name: "json", type: "boolean", description: "Output structured JSON queue snapshot" },
|
|
104
113
|
{ name: "limit", type: "string", description: "Maximum tasks to include in snapshot" },
|