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
package/bin/agentctl.mjs CHANGED
@@ -3,6 +3,8 @@
3
3
  import { parseArgs } from "node:util";
4
4
  import { readFileSync, existsSync, readdirSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
+ import { applyEnvAliases } from "../src/env-aliases.mjs";
7
+ import { selectFailureOutput } from "../src/ops/verify-output.mjs";
6
8
  import { loadConfig, resolveRoot, detectStack, bootstrapZeroTestRepo } from "../src/config.mjs";
7
9
  import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
8
10
  import { acquireLock, releaseLock, lockStatus, getQueueDir } from "../src/state.mjs";
@@ -11,6 +13,12 @@ import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
11
13
  import { KIT_VERSION } from "../src/version.mjs";
12
14
  import { budgetStatus, listOpenReservations, releaseOpenReservations, resolveConcurrency } from "../src/budget.mjs";
13
15
 
16
+ // Vendor-neutral env spellings are filled in before any module reads
17
+ // process.env, so `AGENT_API_KEY` works everywhere `JULES_API_KEY` does. A
18
+ // value already present under the legacy name always wins — see
19
+ // src/env-aliases.mjs.
20
+ applyEnvAliases(process.env);
21
+
14
22
  const args = process.argv.slice(2);
15
23
  const command = args[0];
16
24
 
@@ -35,7 +43,7 @@ export function formatBudgetLine(b) {
35
43
 
36
44
  export function printHelp() {
37
45
  console.log(`
38
- 🚀 agentctl v${VERSION} — Universal Agent Orchestrator & Safety Gatekeeper
46
+ agentctl v${VERSION} — Universal Agent Orchestrator & Safety Gatekeeper
39
47
 
40
48
  Usage: agentctl <command> [options]
41
49
 
@@ -55,6 +63,10 @@ Commands:
55
63
  clean Clean stale branches, worktrees, locks, and ledgers
56
64
  lock <action> Manage mutex locks (acquire | release | status)
57
65
  doctor Run system diagnostics and stack resolution checks
66
+ providers List agent providers and whether this machine can reach them (--json)
67
+ provider set <name> Switch the active provider in .agent/config.yml
68
+ profile Show or set the verification profile (--list, --set <name>, --json)
69
+ ci init Generate a stack-aware CI gate workflow (--target github|gitlab, --force)
58
70
  bootstrap Bootstrap zero-test repository with verification oracle
59
71
  review-repair Parse PR review comments and synthesize OODA repair tasks
60
72
  dashboard Start local HTTP telemetry and audit dashboard
@@ -185,9 +197,7 @@ function printVerifyFailure(failure) {
185
197
  if (failure.command) console.log(` - Command: ${failure.command}`);
186
198
  for (const d of failure.diagnostics || []) console.log(` - ${d}`);
187
199
 
188
- // stderr is where a failing suite says what it expected; stdout is the
189
- // fallback for the runners that report everything there.
190
- const output = (failure.stderr || "").trim() || (failure.stdout || "").trim();
200
+ const output = selectFailureOutput(failure);
191
201
  if (!output) return;
192
202
  const lines = output.split("\n");
193
203
  const tail = lines.slice(-VERIFY_OUTPUT_TAIL_LINES);
@@ -409,6 +419,13 @@ async function main() {
409
419
  if (p.findings) {
410
420
  p.findings.forEach((f) => console.log(` - [${f.severity}] ${f.type}: ${f.description}`));
411
421
  }
422
+ // `git_resolution` reports its cause in `error` and nothing else.
423
+ // Never rendering it meant an unresolvable base branch printed a bare
424
+ // "Phase [GIT_RESOLUTION] : ❌ FAIL" and the operator had to re-run
425
+ // with --json to find out what the tool had objected to.
426
+ if (!p.ok && p.error) {
427
+ console.log(` - Error: ${p.error}`);
428
+ }
412
429
  if (!p.ok && p.failure) {
413
430
  printVerifyFailure(p.failure);
414
431
  }
@@ -417,13 +434,49 @@ async function main() {
417
434
  console.log(`Overall Result: ${res.ok ? "APPROVED (Exit 0)" : `REJECTED (Exit ${res.code})`}\n`);
418
435
  if (!res.ok) {
419
436
  const failedPhase = res.phases.find((p) => !p.ok)?.phase;
437
+ // The secret scanner also carries the integrity findings, so exit 6
438
+ // alone cannot pick the hint: a weakened assertion and a leaked key
439
+ // arrive under the same phase and the same code.
440
+ const secretsPhase = res.phases.find((p) => p.phase === "secrets" && !p.ok);
441
+ const findingTypes = new Set((secretsPhase?.findings || []).map((f) => f.type));
442
+ const INTEGRITY_TYPES = new Set([
443
+ "TEST_TAMPERING_DETECTED",
444
+ "EDGE_RUNTIME_VIOLATION",
445
+ "CROSS_PACKAGE_BOUNDARY_VIOLATION",
446
+ ]);
447
+ const onlyIntegrityFindings =
448
+ findingTypes.size > 0 && [...findingTypes].every((t) => INTEGRITY_TYPES.has(t));
449
+
420
450
  // Exit 3 is also what a strictTestLock tamper verdict returns, so the
421
451
  // code alone cannot pick the hint — a scope remediation for a rewritten
422
452
  // test file sends the operator to the wrong flag entirely.
423
453
  if (res.repairs) {
424
- console.log(`💡 Remediation Hint (Exit 4 OODA Repair Exhausted):`);
425
- console.log(` • Automated self-repair could not pass tests cleanly.`);
426
- console.log(` • Review error fingerprints via: agentctl doctor\n`);
454
+ // A repair loop that never reached the agent is not an exhausted
455
+ // repair loop. Telling someone their tests could not be fixed when
456
+ // the provider rejected the credential sends them to read test
457
+ // output that was never produced.
458
+ const providerFailure =
459
+ res.repairs.finalStatus === "PROVIDER_INFRASTRUCTURE_FAILURE" ||
460
+ (res.repairs.attempts || []).every((a) => a && a.ok === false && a.error);
461
+ const firstError = (res.repairs.attempts || []).find((a) => a && a.error)?.error || res.repairs.error;
462
+ if (providerFailure && firstError) {
463
+ console.log(`💡 Remediation Hint (Exit 4 — the repair agent never ran):`);
464
+ console.log(` • The provider rejected the dispatch, so no repair was attempted.`);
465
+ console.log(` • Provider error: ${firstError}`);
466
+ console.log(` • Check the provider is usable: agentctl providers`);
467
+ console.log(` • The verification failure above is unchanged — fix it locally, or retry once the provider works.\n`);
468
+ } else {
469
+ console.log(`💡 Remediation Hint (Exit 4 OODA Repair Exhausted):`);
470
+ console.log(` • Automated self-repair could not pass tests cleanly.`);
471
+ if (firstError) console.log(` • Last attempt error: ${firstError}`);
472
+ console.log(` • Review error fingerprints via: agentctl doctor\n`);
473
+ }
474
+ } else if (onlyIntegrityFindings) {
475
+ console.log(`💡 Remediation Hint (Exit ${res.code} Test Integrity Violation — no secret was found):`);
476
+ console.log(` • The diff weakens or removes verification rather than leaking a credential.`);
477
+ console.log(` • Restore the assertion the finding names. A requirement that is not met belongs`);
478
+ console.log(` RED with a stated reason, not silenced.`);
479
+ console.log(` • Nothing needs rotating: this exit code is shared with the secret scanner.\n`);
427
480
  } else if (res.flakyVerdict?.verdict === "QUARANTINED") {
428
481
  console.log(`💡 Remediation Hint (Exit 8 Flaky Test Quarantined):`);
429
482
  console.log(` • This command has alternated between pass and fail across recent runs.`);
@@ -436,9 +489,38 @@ async function main() {
436
489
  console.log(` • The stage above exited non-zero. Reproduce it locally, then re-run the gate.`);
437
490
  console.log(` • To let agentctl attempt the repair loop itself, pass: agentctl gate --fix\n`);
438
491
  } else if (res.code === 3) {
439
- console.log(`💡 Remediation Hint (Exit 3 Scope Violation):`);
440
- console.log(` • To allow protected files in this run, pass: agentctl gate --allow-protected`);
441
- console.log(` • Or remove protected/denied paths from the diff before dispatching.\n`);
492
+ // The same violation has two very different causes. Right after
493
+ // `init`, every offending path is a file the tool itself just wrote
494
+ // and has not committed — advising --allow-protected there teaches
495
+ // the newcomer to bypass the gate on their first run, when what
496
+ // they need is `git commit`.
497
+ const scopeFiles = (res.phases.find((p) => p.phase === "scope")?.violations || [])
498
+ .map((v) => v.file)
499
+ .filter(Boolean);
500
+ const { partitionTracked } = await import("../src/git.mjs");
501
+ const { tracked, untracked } = partitionTracked(root, scopeFiles);
502
+ const allNewScaffolding =
503
+ untracked.length > 0 &&
504
+ tracked.length === 0 &&
505
+ untracked.every((f) => /^(\.agent\/|AGENTS\.md$|SPEC\.md$|CONSTRAINTS\.md$|DESIGN\.md$)/.test(f));
506
+
507
+ if (allNewScaffolding) {
508
+ console.log(`💡 Remediation Hint (Exit ${res.code} — these are the files init just wrote):`);
509
+ console.log(` • The agent manifest and guardrails are gate-protected on purpose: an agent must`);
510
+ console.log(` not edit the rules it is governed by. Uncommitted, they read as exactly that.`);
511
+ console.log(` • Commit them once and the gate goes green:`);
512
+ console.log(` git add ${untracked.join(" ")} && git commit -m "chore: add agent config"\n`);
513
+ } else {
514
+ console.log(`💡 Remediation Hint (Exit ${res.code} Scope Violation):`);
515
+ console.log(` • To allow protected files in this run, pass: agentctl gate --allow-protected`);
516
+ console.log(` • Or remove protected/denied paths from the diff before dispatching.\n`);
517
+ }
518
+ } else if (failedPhase === "git_resolution") {
519
+ console.log(`💡 Remediation Hint (Exit ${res.code} Base Branch Unresolvable):`);
520
+ console.log(` • The gate compares your work against a base branch, and this one is not reachable.`);
521
+ console.log(` • This repository's branches: git branch -a`);
522
+ console.log(` • Point the gate at the right one: agentctl check --base <branch>`);
523
+ console.log(` • Or record it once in .agent/config.yml as: base_branch: <branch>\n`);
442
524
  } else if (res.code === 5) {
443
525
  console.log(`💡 Remediation Hint (Exit 5 Diff Payload Overflow):`);
444
526
  console.log(` • Total diff exceeds ${config.limits?.diffKb || 75} KB limit.`);
@@ -544,7 +626,11 @@ async function main() {
544
626
 
545
627
  const root = resolveRoot();
546
628
  const minCoverage = values.min ? parseFloat(values.min) : (values["min-coverage"] ? parseFloat(values["min-coverage"]) : 100);
547
- const diffStr = diffText(root, values.base || "main", values.mode || "working-tree");
629
+ // `config.baseBranch` was skipped here while every other gate honoured it,
630
+ // so `agentctl coverage` alone died on "Cannot resolve base reference
631
+ // main" in any repository not on `main`.
632
+ const coverageBase = values.base || config.baseBranch || "main";
633
+ const diffStr = diffText(root, coverageBase, values.mode || "working-tree");
548
634
 
549
635
  const covRes = runV8Coverage(values.cmd, { root });
550
636
  const report = calculateDiffCoverage(covRes.coverageByFile, diffStr, { root, minCoverage });
@@ -552,7 +638,7 @@ async function main() {
552
638
  if (values.json) {
553
639
  console.log(JSON.stringify({ ...report, testPass: covRes.ok }, null, 2));
554
640
  } else {
555
- console.log(`\n📊 V8 Native Diff Coverage Report (Base: ${values.base || "main"})`);
641
+ console.log(`\n📊 V8 Native Diff Coverage Report (Base: ${coverageBase})`);
556
642
  console.log(`------------------------------------------------------------------`);
557
643
  console.log(` Target Min Coverage : ${report.minCoverage}%`);
558
644
  console.log(` Achieved Coverage : ${report.score}%`);
@@ -1151,7 +1237,13 @@ async function main() {
1151
1237
  case "doctor": {
1152
1238
  const { values } = parseArgs({
1153
1239
  args: args.slice(1),
1154
- options: { json: { type: "boolean", short: "j" } },
1240
+ options: {
1241
+ json: { type: "boolean", short: "j" },
1242
+ // `--probe` was advertised in the command registry and declared
1243
+ // nowhere, so `runDoctorChecks` never saw `activeProbe` and the flag
1244
+ // was silently inert.
1245
+ probe: { type: "boolean" },
1246
+ },
1155
1247
  allowPositionals: true,
1156
1248
  strict: false,
1157
1249
  });
@@ -1160,7 +1252,7 @@ async function main() {
1160
1252
  // them: this command printed a hand-written summary, so findings like a
1161
1253
  // git-tracked .env were computed, tested, and never shown to anyone.
1162
1254
  const { runDoctorChecks } = await import("../src/ops/doctor-registry.mjs");
1163
- const report = await runDoctorChecks({ root });
1255
+ const report = await runDoctorChecks({ root, activeProbe: Boolean(values.probe) });
1164
1256
 
1165
1257
  if (values.json) {
1166
1258
  console.log(JSON.stringify(report, null, 2));
@@ -1180,7 +1272,11 @@ async function main() {
1180
1272
  const icon = { pass: "✅", warn: "⚠️ ", fail: "❌", skip: "⏭️ ", unknown: "❔" };
1181
1273
  for (const r of report.results) {
1182
1274
  console.log(` ${icon[r.status] || "•"} ${r.title}`);
1183
- if (r.status !== "pass") {
1275
+ // A passing row normally speaks for itself. Provider readiness does
1276
+ // not: green there means "a binary was found", and read as "the
1277
+ // provider works" it sends someone into a dispatch that cannot succeed.
1278
+ // Such a row asks to be spelled out even when it passes.
1279
+ if (r.status !== "pass" || r.alwaysShowSummary) {
1184
1280
  console.log(` ${r.summary}`);
1185
1281
  for (const fix of r.remediation || []) console.log(` → ${fix.summary}`);
1186
1282
  }
@@ -1193,6 +1289,178 @@ async function main() {
1193
1289
  break;
1194
1290
  }
1195
1291
 
1292
+ case "provider":
1293
+ case "providers": {
1294
+ // `agentctl providers` used to tell the operator to run
1295
+ // `agentctl init --provider <name>` to switch — which restarts the whole
1296
+ // onboarding wizard, plan question included, to change one line.
1297
+ if (args[1] === "set") {
1298
+ const target = args[2];
1299
+ if (!target) {
1300
+ console.error(`❌ Usage: agentctl provider set <name> (see: agentctl providers)`);
1301
+ process.exit(2);
1302
+ }
1303
+ const { setConfigProvider } = await import("../src/config-edit.mjs");
1304
+ const res = setConfigProvider(root, target);
1305
+ if (!res.ok) {
1306
+ console.error(`❌ ${res.error}`);
1307
+ process.exit(1);
1308
+ }
1309
+ const { probeProvider } = await import("../src/provider-readiness.mjs");
1310
+ const probe = probeProvider(target);
1311
+ console.log(`✅ Provider set to '${target}' in ${res.file}`);
1312
+ console.log(` ${probe.ready ? "Ready" : `Not ready — ${probe.remedy}`}`);
1313
+ if (!probe.known) console.log(` Note: '${target}' is not a built-in preset, so its readiness cannot be checked.`);
1314
+ console.log(` The manifest is gate-protected — commit it so the next check does not read it as an agent edit.`);
1315
+ process.exit(0);
1316
+ }
1317
+
1318
+ const { values } = parseArgs({
1319
+ args: args.slice(1),
1320
+ options: { json: { type: "boolean", short: "j" } },
1321
+ allowPositionals: true,
1322
+ });
1323
+ const { detectAvailableProviders, probeProvider } = await import("../src/provider-readiness.mjs");
1324
+ const probes = detectAvailableProviders();
1325
+ const active = config.provider || "jules";
1326
+ const activeProbe = probeProvider(active);
1327
+
1328
+ if (values.json) {
1329
+ console.log(JSON.stringify({ active, activeReady: activeProbe.ready, providers: probes }, null, 2));
1330
+ process.exit(0);
1331
+ }
1332
+
1333
+ console.log(`\n🔌 Agent providers\n`);
1334
+ for (const p of probes) {
1335
+ const marker = p.name === active ? "▸" : " ";
1336
+ console.log(` ${marker} ${p.ready ? "✅" : "⬜"} ${p.name.padEnd(14)} ${p.label}`);
1337
+ console.log(` ${p.reason}`);
1338
+ if (!p.ready) console.log(` Fix: ${p.remedy}`);
1339
+ }
1340
+ if (!probes.some((p) => p.name === active)) {
1341
+ console.log(` ▸ ⬜ ${active.padEnd(14)} ${activeProbe.label}`);
1342
+ console.log(` ${activeProbe.reason}`);
1343
+ }
1344
+ console.log(`\n Active: ${active} (provider: in ${config._file || ".agent/config.yml"})`);
1345
+ console.log(` Switch: agentctl provider set <name> · every gate below works with no provider at all`);
1346
+ console.log(` Note: for the CLI providers, "ready" means the binary is on PATH — it does not prove`);
1347
+ console.log(` the CLI is signed in. A dispatch is the first thing that can tell you that.\n`);
1348
+ process.exit(activeProbe.ready ? 0 : 1);
1349
+ break;
1350
+ }
1351
+
1352
+ case "profile": {
1353
+ const { values } = parseArgs({
1354
+ args: args.slice(1),
1355
+ options: {
1356
+ list: { type: "boolean", short: "l" },
1357
+ set: { type: "string" },
1358
+ json: { type: "boolean", short: "j" },
1359
+ },
1360
+ allowPositionals: true,
1361
+ });
1362
+ const { PROFILE_NAMES, PROFILE_DESCRIPTIONS, buildProfileStages, buildDefaultStages } = await import("../src/profiles.mjs");
1363
+
1364
+ if (values.set) {
1365
+ const name = String(values.set).toLowerCase();
1366
+ if (!PROFILE_NAMES.includes(name)) {
1367
+ console.error(`❌ Unknown profile '${values.set}'. Choose one of: ${PROFILE_NAMES.join(", ")}`);
1368
+ process.exit(2);
1369
+ }
1370
+ const { setVerificationProfile } = await import("../src/config-edit.mjs");
1371
+ const res = setVerificationProfile(root, name);
1372
+ if (!res.ok) {
1373
+ console.error(`❌ ${res.error}`);
1374
+ process.exit(1);
1375
+ }
1376
+ console.log(`✅ Verification profile set to '${name}' in ${res.file}`);
1377
+ console.log(` The manifest is gate-protected — commit it so the next check does not read it as an agent edit.`);
1378
+ process.exit(0);
1379
+ }
1380
+
1381
+ if (values.list) {
1382
+ if (values.json) {
1383
+ console.log(JSON.stringify(PROFILE_NAMES.map((n) => ({ name: n, description: PROFILE_DESCRIPTIONS[n] })), null, 2));
1384
+ } else {
1385
+ console.log(`\n🎚️ Verification profiles\n`);
1386
+ for (const n of PROFILE_NAMES) console.log(` ${n.padEnd(10)} ${PROFILE_DESCRIPTIONS[n]}`);
1387
+ console.log(`\n Set with: agentctl profile --set max\n`);
1388
+ }
1389
+ process.exit(0);
1390
+ }
1391
+
1392
+ const stack = detectStack(root).stack;
1393
+ const activeProfile = config.verify.profile || "(none — running the built-in pipeline)";
1394
+ const plan = config.verify.profile
1395
+ ? buildProfileStages(config.verify.profile, { stack, verify: config.verify })
1396
+ : { profile: null, stages: config.verify.stages || buildDefaultStages(config.verify), skipped: [] };
1397
+
1398
+ if (values.json) {
1399
+ console.log(JSON.stringify({ profile: config.verify.profile, stack, stages: plan.stages, skipped: plan.skipped }, null, 2));
1400
+ process.exit(0);
1401
+ }
1402
+
1403
+ console.log(`\n🎚️ Verification profile: ${activeProfile} (stack: ${stack})\n`);
1404
+ if (plan.stages.length === 0) {
1405
+ console.log(` No stages resolved — no verification command is configured or detectable.`);
1406
+ console.log(` Set verify.test in .agent/config.yml, or run: agentctl bootstrap`);
1407
+ }
1408
+ for (const st of plan.stages) {
1409
+ const what = st.assert ? `assert:${st.assert}` : st.cmd;
1410
+ console.log(` ${String(st.id).padEnd(14)} ${what}`);
1411
+ }
1412
+ for (const sk of plan.skipped) {
1413
+ console.log(` ${String(sk.id).padEnd(14)} — skipped: ${sk.reason}`);
1414
+ }
1415
+ console.log(`\n Change with: agentctl profile --set ${config.verify.profile === "max" ? "standard" : "max"}\n`);
1416
+ process.exit(0);
1417
+ break;
1418
+ }
1419
+
1420
+ case "ci": {
1421
+ const sub = args[1];
1422
+ if (sub !== "init") {
1423
+ console.error(`❌ Unknown 'ci' action '${sub || ""}'. Usage: agentctl ci init [--target github|gitlab] [--force]`);
1424
+ process.exit(2);
1425
+ }
1426
+ const { values } = parseArgs({
1427
+ args: args.slice(2),
1428
+ options: {
1429
+ target: { type: "string", default: "github" },
1430
+ force: { type: "boolean", short: "f" },
1431
+ "dry-run": { type: "boolean", short: "d" },
1432
+ json: { type: "boolean", short: "j" },
1433
+ },
1434
+ allowPositionals: true,
1435
+ });
1436
+ const { writeCiWorkflow } = await import("../src/ci-templates.mjs");
1437
+ const res = writeCiWorkflow(root, {
1438
+ target: values.target,
1439
+ force: values.force,
1440
+ dryRun: values["dry-run"],
1441
+ config,
1442
+ version: VERSION,
1443
+ stack: detectStack(root),
1444
+ });
1445
+
1446
+ if (values.json) {
1447
+ console.log(JSON.stringify(res, null, 2));
1448
+ process.exit(res.ok ? 0 : 1);
1449
+ }
1450
+ if (!res.ok) {
1451
+ console.error(`❌ ${res.error}`);
1452
+ process.exit(1);
1453
+ }
1454
+ console.log(`✅ ${res.written ? "Wrote" : "Would write"} ${res.file}`);
1455
+ console.log(` Stack: ${res.stack} · runtime setup: ${res.setupSummary}`);
1456
+ console.log(` The workflow runs: agentctl check --mode committed`);
1457
+ if (!res.written && !values["dry-run"]) {
1458
+ console.log(` (unchanged — pass --force to overwrite)`);
1459
+ }
1460
+ process.exit(0);
1461
+ break;
1462
+ }
1463
+
1196
1464
  case "bootstrap": {
1197
1465
  const { values } = parseArgs({
1198
1466
  args: args.slice(1),
@@ -1250,6 +1518,8 @@ async function main() {
1250
1518
  "no-interactive": { type: "boolean" },
1251
1519
  yes: { type: "boolean", short: "y" },
1252
1520
  tier: { type: "string", short: "t" },
1521
+ provider: { type: "string" },
1522
+ profile: { type: "string" },
1253
1523
  json: { type: "boolean", short: "j" },
1254
1524
  "dry-run": { type: "boolean", short: "d" },
1255
1525
  force: { type: "boolean", short: "f" },
@@ -1267,6 +1537,10 @@ async function main() {
1267
1537
  // re-run. Undefined lets the wizard seed the menu from the existing
1268
1538
  // config and fall back to FALLBACK_TIER when there is nothing to seed.
1269
1539
  tier: values.tier,
1540
+ // Both undefined by default so the wizard can detect a provider the
1541
+ // machine can actually reach and keep the profile already in config.
1542
+ provider: values.provider,
1543
+ profile: values.profile,
1270
1544
  allowDefaults: true,
1271
1545
  });
1272
1546
 
@@ -1282,6 +1556,17 @@ async function main() {
1282
1556
  } else {
1283
1557
  console.log(`✅ Onboarding complete! Manifest generated at ${res.configPath}`);
1284
1558
  console.log(` Tier: ${res.plan.tier.toUpperCase()} (${res.plan.limits.concurrency} worker(s), ${res.plan.limits.daily_tasks} daily tasks)`);
1559
+ {
1560
+ const { probeProvider } = await import("../src/provider-readiness.mjs");
1561
+ const probe = probeProvider(res.plan.provider);
1562
+ console.log(` Provider : ${probe.name} ${probe.ready ? "(ready)" : `(not ready — ${probe.remedy})`}`);
1563
+ const { buildProfileStages, describeProfilePlan } = await import("../src/profiles.mjs");
1564
+ const planned = buildProfileStages(res.plan.profile, {
1565
+ stack: res.plan.stack,
1566
+ verify: { ...res.plan.verify, unit: res.plan.verify.test },
1567
+ });
1568
+ console.log(` Verification Profile : ${res.plan.profile} → ${describeProfilePlan(planned)}`);
1569
+ }
1285
1570
  if (res.plan.verify.test) {
1286
1571
  console.log(` Verification Test Command : "${res.plan.verify.test}"`);
1287
1572
  } else {
@@ -1341,8 +1626,15 @@ async function main() {
1341
1626
  allowPositionals: true,
1342
1627
  });
1343
1628
 
1344
- const isNonInteractive = Boolean(values["non-interactive"] || values["no-interactive"] || values.yes);
1345
- const isInteractive = values.interactive === true ? true : (isNonInteractive ? false : undefined);
1629
+ const { resolveWizardInteractivity } = await import("../src/ops/cli-intent.mjs");
1630
+ const isInteractive = resolveWizardInteractivity({
1631
+ interactive: values.interactive,
1632
+ nonInteractive: values["non-interactive"] || values["no-interactive"],
1633
+ yes: values.yes,
1634
+ // A title and a prompt together state the whole task; there is
1635
+ // nothing left for the wizard to ask.
1636
+ fullySpecified: Boolean(values.title && resolvePromptInput(values, positionals)),
1637
+ });
1346
1638
 
1347
1639
  const { runTaskCreateWizard } = await import("../src/wizard-task.mjs");
1348
1640
  const res = await runTaskCreateWizard(root, {
@@ -1357,12 +1649,17 @@ async function main() {
1357
1649
  requirePlanApproval: values["require-plan-approval"],
1358
1650
  repoless: values.repoless,
1359
1651
  interactive: isInteractive,
1652
+ dryRun: values["dry-run"],
1360
1653
  });
1361
1654
 
1362
1655
  if (values.json) {
1363
1656
  console.log(JSON.stringify(res, null, 2));
1364
1657
  } else {
1365
- console.log(`✅ Task synthesized & queued at ${res.taskFile}`);
1658
+ console.log(
1659
+ res.dryRun
1660
+ ? `🧪 Dry run — envelope synthesized and validated, nothing written (would be ${res.taskFile})`
1661
+ : `✅ Task synthesized & queued at ${res.taskFile}`
1662
+ );
1366
1663
  console.log(` Task ID : ${res.plan.taskId}`);
1367
1664
  console.log(` Title : ${res.plan.title}`);
1368
1665
  if (res.plan.role) console.log(` Role : ${res.plan.role}`);
@@ -2178,26 +2475,32 @@ async function main() {
2178
2475
  const subAction = args[1];
2179
2476
  if (subAction === "init") {
2180
2477
  const { scaffoldIdeConfig } = await import("../src/ops/ide-scaffold.mjs");
2181
- const { values } = parseArgs({
2478
+ const { values, positionals } = parseArgs({
2182
2479
  args: args.slice(2),
2183
2480
  options: {
2184
- target: { type: "string", short: "t", default: "all" },
2481
+ // No `default: "all"` here. parseArgs sets a default eagerly, so
2482
+ // `values.target` was always truthy and the `|| args[2]` fallback
2483
+ // below could never be reached — `agentctl mcp init cursor`, the
2484
+ // spelling --help advertises, silently scaffolded Cursor, VS Code
2485
+ // and Claude Desktop alike. The default belongs at the end of the
2486
+ // resolution chain, not at the start of it.
2487
+ target: { type: "string", short: "t" },
2185
2488
  json: { type: "boolean", short: "j" },
2186
2489
  "dry-run": { type: "boolean", short: "d" },
2187
2490
  },
2188
2491
  allowPositionals: true,
2189
2492
  });
2190
2493
 
2191
- const target = values.target || args[2] || "all";
2494
+ const target = values.target || positionals[0] || "all";
2192
2495
  try {
2193
- const res = scaffoldIdeConfig(target, { root });
2496
+ const res = scaffoldIdeConfig(target, { root, dryRun: values["dry-run"] });
2194
2497
  if (values.json) {
2195
2498
  console.log(JSON.stringify(res, null, 2));
2196
2499
  } else {
2197
- console.log(`\n🔌 IDE MCP Config Scaffolded Successfully!`);
2500
+ console.log(res.dryRun ? `\n🧪 IDE MCP Config — dry run, nothing written` : `\n🔌 IDE MCP Config Scaffolded Successfully!`);
2198
2501
  console.log(` Target : ${res.target}`);
2199
2502
  for (const item of res.results) {
2200
- console.log(` - ${item.target.toUpperCase()} : ${item.file}`);
2503
+ console.log(` - ${item.target.toUpperCase()} : ${item.file}${res.dryRun ? " (would write)" : ""}`);
2201
2504
  }
2202
2505
  console.log("");
2203
2506
  }