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.
Files changed (43) hide show
  1. package/README.md +68 -8
  2. package/bin/agentctl.mjs +327 -24
  3. package/bin/init.js +87 -74
  4. package/index.mjs +10 -0
  5. package/package.json +5 -2
  6. package/scripts/jules-create.mjs +5 -16
  7. package/scripts/jules-patch.mjs +18 -10
  8. package/scripts/stale-base-check.mjs +1 -1
  9. package/src/ci-templates.mjs +199 -0
  10. package/src/config-edit.mjs +138 -0
  11. package/src/config.mjs +39 -11
  12. package/src/engine.mjs +2 -21
  13. package/src/env-aliases.mjs +80 -0
  14. package/src/git.mjs +79 -0
  15. package/src/mcp.mjs +19 -1
  16. package/src/ops/cli-intent.mjs +29 -0
  17. package/src/ops/command-registry.mjs +19 -10
  18. package/src/ops/doctor-registry.mjs +75 -32
  19. package/src/ops/ide-scaffold.mjs +22 -16
  20. package/src/ops/next-step.mjs +38 -7
  21. package/src/ops/verify-output.mjs +38 -0
  22. package/src/profiles.mjs +188 -0
  23. package/src/provider-readiness.mjs +284 -0
  24. package/src/security.mjs +42 -1
  25. package/src/tui.mjs +14 -3
  26. package/src/wizard-init.mjs +88 -12
  27. package/src/wizard-task.mjs +22 -5
  28. package/src/ops/doctor-planner.mjs +0 -220
  29. package/src/ops/receipts.mjs +0 -171
  30. package/src/ops/swarm-actions.mjs +0 -156
  31. package/src/ops/task-actions.mjs +0 -214
  32. package/src/ops/transaction.mjs +0 -278
  33. package/src/ux/capabilities.mjs +0 -180
  34. package/src/ux/diff-viewer.mjs +0 -289
  35. package/src/ux/layout.mjs +0 -195
  36. package/src/ux/log-viewer.mjs +0 -110
  37. package/src/ux/palette.mjs +0 -179
  38. package/src/ux/queue-model.mjs +0 -206
  39. package/src/ux/renderer.mjs +0 -188
  40. package/src/ux/swarm-model.mjs +0 -146
  41. package/src/ux/terminal-session.mjs +0 -215
  42. package/src/ux/widgets.mjs +0 -323
  43. /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
- setup: rawSetup ?? autoVerify.setup ?? "",
510
- lint: rawLint ?? autoVerify.lint ?? "",
511
- test: rawTest ?? autoVerify.test,
512
- unit: rawUnit ?? rawTest ?? autoVerify.unit ?? autoVerify.test,
513
- fuzz: rawFuzz ?? autoVerify.fuzz ?? "",
514
- invariant: rawInvariant ?? autoVerify.invariant ?? "",
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
- if (trustedVerify.setup) {
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 including daily task budget, active locks, and stack diagnostics.",
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 --interactive",
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: "interactive", type: "boolean", description: "Open full-screen diagnostic matrix" },
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: "Browse and manage canonical task queue",
94
+ description: "Execute pending task envelopes: dispatches each to the provider and moves it out of the queue",
90
95
  category: "Operate",
91
- mutates: false,
92
- risk: "low",
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 --interactive",
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" },