jules-orchestrator-kit 0.73.1 → 0.75.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/bin/agentctl.mjs CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  import { parseArgs } from "node:util";
4
4
  import { readFileSync, existsSync, readdirSync, statSync, renameSync, mkdirSync } from "node:fs";
5
- import { join, resolve } from "node:path";
5
+ import { join, resolve, relative } from "node:path";
6
6
  import { applyEnvAliases } from "../src/env-aliases.mjs";
7
7
  import { selectFailureOutput } from "../src/ops/verify-output.mjs";
8
+ import { diagnoseVerifyFailure } from "../src/ops/toolchain-diagnostics.mjs";
8
9
  import { loadConfig, resolveRoot, detectStack, bootstrapZeroTestRepo } from "../src/config.mjs";
9
10
  import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
10
- import { acquireLock, releaseLock, lockStatus, getQueueDir } from "../src/state.mjs";
11
- import { parseEnvelopeHeader } from "../src/envelope.mjs";
11
+ import { acquireLock, releaseLock, reapStaleLocks, lockStatus, getQueueDir } from "../src/state.mjs";
12
+ import { parseEnvelopeHeader, validateEnvelope } from "../src/envelope.mjs";
12
13
  import { worktreePrune } from "../src/git.mjs";
13
14
  import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
14
15
  import { KIT_VERSION } from "../src/version.mjs";
@@ -22,7 +23,18 @@ import { COMMAND_REGISTRY } from "../src/ops/command-registry.mjs";
22
23
  applyEnvAliases(process.env);
23
24
 
24
25
  const args = process.argv.slice(2);
25
- const command = args[0];
26
+ const requestedCommand = args[0] || "";
27
+ const gateCommands = new Set(["gate", "check", "audit"]);
28
+
29
+ if (gateCommands.has(requestedCommand) && args.includes("--fix")) {
30
+ console.error("Error: `agentctl gate` is non-mutating; `--fix` no longer dispatches automated repairs.");
31
+ console.error("Run the gate normally, then use `agentctl repair` explicitly with the failure trace you want repaired.");
32
+ process.exit(2);
33
+ }
34
+
35
+ // `repair` is the canonical spelling; `fix` remains a 0.x compatibility
36
+ // alias. Both route through the same existing implementation below.
37
+ const command = requestedCommand;
26
38
 
27
39
  export const VERSION = KIT_VERSION;
28
40
 
@@ -78,7 +90,8 @@ export function printHelp() {
78
90
  lines.push(" --prompt-file, -f Read the prompt from a file (-f is --fix on task optimize)");
79
91
  lines.push(" --role, -r Specify specialist agent role (auditor | performance | security | hygiene | resilience | types | debugger | testing | e2e | database | docs | a11y)");
80
92
  lines.push(" --tier Force routing tier when router.enabled (fast | complex) — see .agent/config.yml router:");
81
- lines.push(" --check-premise Verify task goal/oracle passes locally before burning API budget");
93
+ lines.push(" --check-premise Skip only when an explicit --goal-check proves the task complete");
94
+ lines.push(" --goal-check <cmd> Objective-specific command; distinct from --verify-cmd");
82
95
  lines.push(" --dag Execute queue tasks via DAG dependency resolution");
83
96
  lines.push(" --dry-run, -d Simulate action without making API calls or modifying git");
84
97
  lines.push(" --mode, -m Gate evaluation mode (working-tree | committed | staged)");
@@ -257,6 +270,11 @@ async function main() {
257
270
  // tool; a newcomer cannot tell which entry is step one, and guessing wrong
258
271
  // costs them a confusing failure instead of a hint.
259
272
  if (!command) {
273
+ if (args.includes("-i") || args.includes("--interactive")) {
274
+ const { runTuiMenu } = await import("../src/ops/tui-menu.mjs");
275
+ await runTuiMenu(resolveRoot());
276
+ process.exit(0);
277
+ }
260
278
  const { resolveNextStep, renderNextStep } = await import("../src/ops/next-step.mjs");
261
279
  const cwd = process.cwd();
262
280
  let budgetLine = "";
@@ -294,8 +312,18 @@ async function main() {
294
312
  }
295
313
 
296
314
  const root = resolveRoot();
297
- reapOrphanedIntents(root);
298
- reapStaleMutexDirs(root);
315
+ // Init is a configuration boundary, not an operational maintenance command.
316
+ // In particular, --dry-run must not create .agent/ indirectly by reaping
317
+ // journal intents or mutex directories before init itself gets control.
318
+ // init, uninstall and migrate must not create kit state as a side-effect of
319
+ // startup maintenance — dry-run and "nothing installed" paths need a clean tree.
320
+ if (command !== "init" && command !== "uninstall" && command !== "migrate") {
321
+ reapOrphanedIntents(root);
322
+ reapStaleMutexDirs(root);
323
+ if (command !== "lock") {
324
+ reapStaleLocks(root);
325
+ }
326
+ }
299
327
  const config = loadConfig(root);
300
328
 
301
329
  switch (command) {
@@ -316,6 +344,7 @@ async function main() {
316
344
  "auto-approve-plans": { type: "boolean" },
317
345
  "auto-approve": { type: "boolean" },
318
346
  "check-premise": { type: "boolean" },
347
+ "goal-check": { type: "string" },
319
348
  idempotent: { type: "boolean" },
320
349
  author: { type: "string" },
321
350
  "verify-cmd": { type: "string", short: "v" },
@@ -361,7 +390,7 @@ async function main() {
361
390
  console.error(
362
391
  `Error: Unknown agent role '${values.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Auditor, Performance, Security, Hygiene, Testing).`
363
392
  );
364
- console.error(` Run 'agentctl init' to scaffold the shipped role prompts.`);
393
+ console.error(` Use a canonical shipped role name or add a repository override under .agent/prompts/.`);
365
394
  process.exit(1);
366
395
  }
367
396
  }
@@ -378,6 +407,7 @@ async function main() {
378
407
  autoPr: values["auto-pr"],
379
408
  requirePlanApproval: autoApprove ? false : values["require-plan-approval"],
380
409
  checkPremise: values["check-premise"] || values.idempotent,
410
+ goalCheck: values["goal-check"],
381
411
  author: values.author,
382
412
  verifyCmd: values["verify-cmd"] || values.verify,
383
413
  };
@@ -397,7 +427,7 @@ async function main() {
397
427
  } else {
398
428
  if (session.status === "ALREADY_SATISFIED" || session.skipped) {
399
429
  console.log(`\n⚡ Task Already Satisfied (skipped dispatch):`);
400
- console.log(` Reason: ${session.reason || "Verification oracle already passing on base branch."}`);
430
+ console.log(` Reason: ${session.reason || "Explicit goal check passed on a clean revision."}`);
401
431
  } else if (values["dry-run"]) {
402
432
  // The dry run reached the provider adapter and stopped short of the
403
433
  // call. Printing the same "Dispatched Successfully!" banner as a
@@ -408,9 +438,16 @@ async function main() {
408
438
  console.log(` Title : ${task.title}`);
409
439
  console.log(` Provider : ${session.provider || config.provider || "jules"}`);
410
440
  if (task.role) console.log(` Role : ${task.role}`);
441
+ if (task.branch) console.log(` Branch : ${task.branch}`);
442
+ if (task.verifyCmd) console.log(` Verify Cmd : ${task.verifyCmd}`);
411
443
  if (session._routeTier) {
412
444
  console.log(` Router Tier : ${session._routeTier} (${session._routeReason || "n/a"})`);
413
445
  }
446
+ if (task.prompt) {
447
+ console.log(`\n--- Task Payload Preview ---`);
448
+ console.log(String(task.prompt).trim());
449
+ console.log(`----------------------------`);
450
+ }
414
451
  console.log(`\n Re-run without --dry-run to dispatch for real.`);
415
452
  } else {
416
453
  console.log(`\n✅ Task Dispatched Successfully!`);
@@ -674,9 +711,14 @@ async function main() {
674
711
  console.log(` • Or set it by hand: verify.test in ${config._file || ".agent/config.yml"}`);
675
712
  console.log(` • Scope- and secret-scanning only, on purpose? Set verify.required: false there.\n`);
676
713
  } else if (failedPhase === "verify" || failedPhase === "evidence") {
714
+ const verifyFailure =
715
+ res.phases.find((p) => (p.phase === "verify" || p.phase === "evidence") && !p.ok)?.failure || {};
716
+ const diagnosis = diagnoseVerifyFailure(verifyFailure);
677
717
  console.log(`💡 Remediation Hint (Exit ${res.code} Verification Failed):`);
678
- console.log(` • The stage above exited non-zero. Reproduce it locally, then re-run the gate.`);
679
- console.log(` • To let agentctl attempt the repair loop itself, pass: agentctl gate --fix\n`);
718
+ for (const line of diagnosis.lines) {
719
+ console.log(` • ${line}`);
720
+ }
721
+ console.log("");
680
722
  } else if (res.code === 3) {
681
723
  // The same violation has two very different causes. Right after
682
724
  // `init`, every offending path is a file the tool itself just wrote
@@ -965,6 +1007,7 @@ async function main() {
965
1007
  break;
966
1008
  }
967
1009
 
1010
+ case "repair":
968
1011
  case "fix": {
969
1012
  const { repair, planTaskCreate, resolveRoot, redactSecrets } = await import("../index.mjs");
970
1013
  const { values, positionals } = parseArgs({
@@ -996,7 +1039,7 @@ async function main() {
996
1039
  }
997
1040
 
998
1041
  if (!errorInput.trim()) {
999
- console.error("❌ Error: No error log or failure input provided. Pipe stdout/stderr via `npm test 2>&1 | agentctl fix` or provide --file/--input.");
1042
+ console.error("❌ Error: No error log or failure input provided. Pipe stdout/stderr via `npm test 2>&1 | agentctl repair` or provide --file/--input.");
1000
1043
  process.exit(1);
1001
1044
  }
1002
1045
 
@@ -1021,17 +1064,53 @@ async function main() {
1021
1064
  }
1022
1065
  process.exit(0);
1023
1066
  } else {
1024
- console.log(`\n🔧 Dispatching OODA Repair Loop for captured failure trace...`);
1067
+ if (!values.json) {
1068
+ console.log(`\n🔧 Dispatching OODA Repair Loop for captured failure trace...`);
1069
+ }
1025
1070
  const repairRes = await repair(
1026
1071
  { stderr: cleanTrace, command: values.cmd || "verify" },
1027
1072
  { root, dryRun: values["dry-run"], author: values.author }
1028
1073
  );
1029
1074
 
1030
1075
  if (values.json) {
1031
- console.log(JSON.stringify(repairRes, null, 2));
1076
+ process.stdout.write(JSON.stringify(repairRes, null, 2) + "\n", () => {
1077
+ process.exit(repairRes.ok ? 0 : 1);
1078
+ });
1079
+ setTimeout(() => process.exit(repairRes.ok ? 0 : 1), 1000).unref();
1080
+ break;
1032
1081
  } else {
1033
1082
  console.log(`------------------------------------------------------------------`);
1034
1083
  console.log(`Repair Status: ${repairRes.ok ? "RESOLVED (Exit 0)" : `FAILED (${repairRes.finalStatus} — Exit 1)`}`);
1084
+ if (!repairRes.ok && Array.isArray(repairRes.attempts) && repairRes.attempts.length > 0) {
1085
+ console.log(`\nRepair attempt review:`);
1086
+ if (repairRes.repairDir) {
1087
+ console.log(` Retained under: ${repairRes.repairDir}`);
1088
+ }
1089
+ for (const attempt of repairRes.attempts) {
1090
+ const phase =
1091
+ attempt.phase ||
1092
+ attempt.diagnostics?.phase ||
1093
+ (attempt.verified === true ? "verified" : attempt.verified === false ? "verify" : "unknown");
1094
+ const errRaw = attempt.error || attempt.diagnostics?.error || "";
1095
+ const concise = String(errRaw).split("\n")[0].slice(0, 200);
1096
+ const diff = typeof attempt.diff === "string" ? attempt.diff : "";
1097
+ const fileCount = [...diff.matchAll(/^diff --git /gm)].length;
1098
+ const lineCount = diff
1099
+ .split("\n")
1100
+ .filter((line) => (line.startsWith("+") && !line.startsWith("+++")) || (line.startsWith("-") && !line.startsWith("---")))
1101
+ .length;
1102
+ console.log(` #${attempt.n}: phase=${phase} ok=${attempt.ok}${concise ? ` error=${JSON.stringify(concise)}` : ""}`);
1103
+ if (attempt.diff !== undefined) {
1104
+ console.log(` diff: ${fileCount} file(s), ${lineCount} changed line(s)`);
1105
+ }
1106
+ if (attempt.diagnostics?.patchPath) {
1107
+ console.log(` patch: ${attempt.diagnostics.patchPath}`);
1108
+ }
1109
+ if (attempt.diagnostics?.diagnosticsPath) {
1110
+ console.log(` diagnostics: ${attempt.diagnostics.diagnosticsPath}`);
1111
+ }
1112
+ }
1113
+ }
1035
1114
  }
1036
1115
 
1037
1116
  process.exit(repairRes.ok ? 0 : 1);
@@ -1500,6 +1579,7 @@ async function main() {
1500
1579
  options: {
1501
1580
  ttl: { type: "string" },
1502
1581
  pid: { type: "string" },
1582
+ "dry-run": { type: "boolean", short: "d" },
1503
1583
  json: { type: "boolean", short: "j" },
1504
1584
  },
1505
1585
  allowPositionals: true,
@@ -1564,22 +1644,49 @@ async function main() {
1564
1644
  console.log(`❌ Lock for ${taskId} not found or release failed`);
1565
1645
  process.exit(1);
1566
1646
  }
1647
+ } else if (action === "reap" || action === "prune") {
1648
+ const dryRun = Boolean(lockValues["dry-run"]);
1649
+ const res = reapStaleLocks(root, { dryRun });
1650
+ if (lockValues.json) {
1651
+ console.log(JSON.stringify(res, null, 2));
1652
+ process.exit(0);
1653
+ }
1654
+ if (res.reapedCount === 0) {
1655
+ console.log("✅ No stale locks found.");
1656
+ } else {
1657
+ const verb = dryRun ? "Would reap" : "Reaped";
1658
+ console.log(`✅ ${verb} ${res.reapedCount} stale lock(s):`);
1659
+ for (const item of res.reaped) {
1660
+ console.log(` • ${item.taskId || item.file} (${item.reason})`);
1661
+ }
1662
+ }
1663
+ process.exit(0);
1567
1664
  } else {
1568
- const locks = lockStatus(root);
1665
+ const allLocks = lockStatus(root);
1666
+ const activeLocks = allLocks.filter((l) => l.live);
1667
+ const staleLocks = allLocks.filter((l) => !l.live);
1569
1668
  if (lockValues.json) {
1570
- console.log(JSON.stringify({ ok: true, locks }, null, 2));
1669
+ console.log(JSON.stringify({ ok: true, locks: allLocks, activeCount: activeLocks.length, staleCount: staleLocks.length }, null, 2));
1571
1670
  process.exit(0);
1572
1671
  }
1573
- if (locks.length === 0) {
1672
+ if (allLocks.length === 0) {
1574
1673
  console.log("Active Locks: 0 (no held locks)");
1575
1674
  } else {
1576
- console.log(`Active Locks (${locks.length}):`);
1577
- for (const l of locks) {
1675
+ console.log(`Active Locks (${activeLocks.length}):`);
1676
+ for (const l of activeLocks) {
1578
1677
  const files = Array.isArray(l.files) ? l.files.join(", ") : (l.files || "");
1579
1678
  const expires = l.expiresAt ? `expires ${l.expiresAt}` : (l.ownerPid ? `pid ${l.ownerPid}` : "active");
1580
1679
  console.log(` • taskId: ${l.taskId || l.id} [${l.agent || "unknown"}] (${expires})`);
1581
1680
  if (files) console.log(` Paths: ${files}`);
1582
1681
  }
1682
+ if (staleLocks.length > 0) {
1683
+ console.log(`\nStale Locks (${staleLocks.length}):`);
1684
+ for (const l of staleLocks) {
1685
+ const reason = l.leased ? "expired lease" : `dead pid ${l.pid || "unknown"}`;
1686
+ console.log(` • taskId: ${l.taskId || l.id} [${l.agent || "unknown"}] (${reason})`);
1687
+ }
1688
+ console.log(` 💡 Run \`agentctl lock reap\` to clean up stale locks.`);
1689
+ }
1583
1690
  }
1584
1691
  }
1585
1692
  process.exit(0);
@@ -1904,6 +2011,15 @@ async function main() {
1904
2011
  break;
1905
2012
  }
1906
2013
 
2014
+ case "menu":
2015
+ case "tui":
2016
+ case "ui": {
2017
+ const { runTuiMenu } = await import("../src/ops/tui-menu.mjs");
2018
+ await runTuiMenu(root);
2019
+ process.exit(0);
2020
+ break;
2021
+ }
2022
+
1907
2023
  case "init": {
1908
2024
  const { values } = parseArgs({
1909
2025
  args: args.slice(1),
@@ -1957,19 +2073,54 @@ async function main() {
1957
2073
  provider: values.provider,
1958
2074
  profile: values.profile,
1959
2075
  allowDefaults: true,
2076
+ dryRun: values["dry-run"],
2077
+ // --force is retained as the explicit 0.x compatibility path: it
2078
+ // restores the legacy Jules manifest and full repository scaffold.
2079
+ legacyManifest: values.force,
2080
+ // JSON mode is a protocol surface: progress spinners must not prefix
2081
+ // the payload with human-readable status lines.
2082
+ stdout: values.json ? { write() {} } : process.stdout,
1960
2083
  });
1961
2084
 
1962
- // The wizard writes the manifest; the assets the CLI's documented
1963
- // features actually need — AGENTS.md, the role prompts, the guardrails,
1964
- // the gitignore entries — used to be scaffolded only by the separate
1965
- // `jules-init` binary that the README's quickstart never mentions.
1966
- const { scaffoldRepoAssets } = await import("../src/scaffold.mjs");
1967
- const scaffold = scaffoldRepoAssets(root, { force: values.force });
2085
+ // Default init deliberately owns only the canonical config plus the
2086
+ // runtime-state ignore block. The old full scaffold is opt-in via
2087
+ // --force (and remains available through the shipped jules-init binary).
2088
+ const {
2089
+ ensureGitignore,
2090
+ planGitignoreEntries,
2091
+ planScaffoldRepoAssets,
2092
+ scaffoldRepoAssets,
2093
+ } = await import("../src/scaffold.mjs");
2094
+ const plannedGitignore = planGitignoreEntries(root);
2095
+ const legacyPlan = values.force ? planScaffoldRepoAssets(root, { force: true }) : { created: [], gitignore: plannedGitignore };
2096
+ const scaffold =
2097
+ values.force && !values["dry-run"]
2098
+ ? scaffoldRepoAssets(root, { force: true })
2099
+ : { created: legacyPlan.created, gitignore: plannedGitignore };
2100
+ const gitignore =
2101
+ values["dry-run"]
2102
+ ? plannedGitignore
2103
+ : values.force
2104
+ ? scaffold.gitignore
2105
+ : ensureGitignore(root);
2106
+ const writes = [
2107
+ ...res.writes.map((p) => (relative(root, p) || p).replaceAll("\\", "/")),
2108
+ ...legacyPlan.created,
2109
+ ...(plannedGitignore.length > 0 ? [".gitignore"] : []),
2110
+ ].filter((p, index, all) => all.indexOf(p) === index);
1968
2111
 
1969
2112
  if (values.json) {
1970
- console.log(JSON.stringify({ ...res, scaffold }, null, 2));
2113
+ console.log(JSON.stringify({ ...res, writes, gitignore }, null, 2));
1971
2114
  } else {
1972
- console.log(`✅ Onboarding complete! Manifest generated at ${res.configPath}`);
2115
+ console.log(
2116
+ values["dry-run"]
2117
+ ? "🧪 Dry run — no files written."
2118
+ : `✅ Onboarding complete! Manifest generated at ${res.configPath}`
2119
+ );
2120
+ if (values["dry-run"]) {
2121
+ console.log(" Would write:");
2122
+ for (const path of writes) console.log(` - ${path}`);
2123
+ }
1973
2124
  console.log(` Tier: ${res.plan.tier.toUpperCase()} (${res.plan.limits.concurrency} worker(s), ${res.plan.limits.daily_tasks} daily tasks)`);
1974
2125
  {
1975
2126
  const { probeProvider } = await import("../src/provider-readiness.mjs");
@@ -1988,34 +2139,35 @@ async function main() {
1988
2139
  console.log(` Verification Test Command : None detected (run "agentctl bootstrap" to create a test oracle)`);
1989
2140
  }
1990
2141
  console.log(` Active Presets : ${res.plan.presets.join(", ")}`);
1991
- for (const item of scaffold.created) {
1992
- console.log(` Scaffolded : ${item}`);
1993
- }
1994
- if (scaffold.gitignore.length > 0) {
1995
- console.log(` Ignored runtime state : ${scaffold.gitignore.length} entries added to .gitignore`);
1996
- }
1997
-
1998
- // `.agent/config.yml` and `.agent/jules.yml` are both on the gate's deny
1999
- // list, by design — the agent must not edit its own rules. Leaving them
2000
- // uncommitted meant the very first `agentctl gate` rejected the working
2001
- // tree for files init had just written, which reads as the tool
2002
- // catching the user cheating on step three.
2003
- const rootContracts = ["SPEC.md", "CONSTRAINTS.md", "DESIGN.md"].filter((f) => existsSync(join(root, f)));
2004
- // A fresh `npm install`/`npm init -y` just created package manifests the
2005
- // scope rules `protect`. Leaving them out of the hint meant the very
2006
- // first `agentctl gate`/`agentctl task create` still rejected the tree
2007
- // even after the user committed exactly what this message listed. Fold
2008
- // the install-produced, currently-addable artifacts in so the suggested
2009
- // commit actually leaves the tree clean enough for step three.
2010
- const { addableOnboardingArtifacts } = await import("../src/git.mjs");
2011
- const filesToAdd = [".agent", "AGENTS.md", ...rootContracts, ...addableOnboardingArtifacts(root), ".gitignore"]
2012
- .filter((f) => existsSync(join(root, f)));
2013
- console.log(`\n Commit the manifest and contracts so the gate does not read them as agent edits:`);
2014
- console.log(` git add ${filesToAdd.join(" ")} && git commit -m "chore: add agent config"`);
2015
-
2016
- const { resolveNextStep, renderNextStep } = await import("../src/ops/next-step.mjs");
2017
- const next = resolveNextStep(root);
2018
- console.log(renderNextStep({ version: VERSION, root, next, budgetLine: "" }));
2142
+ if (values.force) {
2143
+ const verb = values["dry-run"] ? "Would scaffold (legacy)" : "Scaffolded (legacy)";
2144
+ for (const item of legacyPlan.created) {
2145
+ console.log(` ${verb.padEnd(26)}: ${item}`);
2146
+ }
2147
+ }
2148
+ if (!values["dry-run"] && gitignore.length > 0) {
2149
+ console.log(` Ignored runtime state : ${gitignore.length} entries added to .gitignore`);
2150
+ }
2151
+
2152
+ if (!values["dry-run"]) {
2153
+ // A fresh `npm install`/`npm init -y` may have created package
2154
+ // manifests the scope rules protect. Include only files that really
2155
+ // exist; minimal init itself owns .agent/config.yml and, when needed,
2156
+ // the .gitignore runtime-state block.
2157
+ const { addableOnboardingArtifacts } = await import("../src/git.mjs");
2158
+ const filesToAdd = [
2159
+ ".agent/config.yml",
2160
+ ...(values.force ? [".agent/jules.yml", ...legacyPlan.created] : []),
2161
+ ...addableOnboardingArtifacts(root),
2162
+ ".gitignore",
2163
+ ].filter((p, index, all) => all.indexOf(p) === index && existsSync(join(root, p)));
2164
+ console.log(`\n Commit the config so the gate does not read it as an agent edit:`);
2165
+ console.log(` git add ${filesToAdd.join(" ")} && git commit -m "chore: add agent config"`);
2166
+
2167
+ const { resolveNextStep, renderNextStep } = await import("../src/ops/next-step.mjs");
2168
+ const next = resolveNextStep(root);
2169
+ console.log(renderNextStep({ version: VERSION, root, next, budgetLine: "" }));
2170
+ }
2019
2171
  }
2020
2172
  process.exit(0);
2021
2173
  break;
@@ -2046,6 +2198,7 @@ async function main() {
2046
2198
  yes: { type: "boolean", short: "y" },
2047
2199
  json: { type: "boolean", short: "j" },
2048
2200
  "dry-run": { type: "boolean", short: "d" },
2201
+ "allow-protected": { type: "boolean" },
2049
2202
  },
2050
2203
  allowPositionals: true,
2051
2204
  });
@@ -2076,6 +2229,7 @@ async function main() {
2076
2229
  repoless: values.repoless,
2077
2230
  interactive: isInteractive,
2078
2231
  dryRun: values["dry-run"],
2232
+ allowProtected: Boolean(values["allow-protected"]),
2079
2233
  });
2080
2234
 
2081
2235
  if (values.json) {
@@ -2202,8 +2356,87 @@ async function main() {
2202
2356
  console.log(`--------------------------------------------------\n`);
2203
2357
  }
2204
2358
  process.exit(analysis.isFalsifiable ? 0 : 1);
2359
+ } else if (subCommand === "validate") {
2360
+ const { values, positionals } = parseArgs({
2361
+ args: args.slice(2),
2362
+ options: {
2363
+ json: { type: "boolean", short: "j" },
2364
+ },
2365
+ allowPositionals: true,
2366
+ });
2367
+
2368
+ const target = positionals[0];
2369
+ if (!target) {
2370
+ console.error("Usage: agentctl task validate <path-to-envelope.md|json> [--json]");
2371
+ process.exit(1);
2372
+ }
2373
+
2374
+ const targetPath = resolve(root, target);
2375
+ if (!existsSync(targetPath)) {
2376
+ console.error(`Error: Envelope file not found: ${target}`);
2377
+ process.exit(1);
2378
+ }
2379
+
2380
+ let payload = null;
2381
+ try {
2382
+ const content = readFileSync(targetPath, "utf-8");
2383
+ if (targetPath.endsWith(".json") || target.toLowerCase().endsWith(".json")) {
2384
+ payload = JSON.parse(content);
2385
+ } else {
2386
+ payload = parseEnvelopeHeader(content);
2387
+ if (!payload || typeof payload !== "object") {
2388
+ try {
2389
+ payload = JSON.parse(content);
2390
+ } catch {
2391
+ payload = null;
2392
+ }
2393
+ }
2394
+ }
2395
+ } catch (err) {
2396
+ console.error(`Error: Failed to read or parse envelope: ${err.message}`);
2397
+ process.exit(1);
2398
+ }
2399
+
2400
+ if (!payload || typeof payload !== "object") {
2401
+ console.error("❌ TASK ENVELOPE VALIDATION FAILED:");
2402
+ console.error(" - File does not contain a valid JSON payload or task envelope frontmatter.");
2403
+ process.exit(1);
2404
+ }
2405
+
2406
+ const res = validateEnvelope(payload, { root });
2407
+ const result = {
2408
+ ok: res.ok === true,
2409
+ file: target,
2410
+ errors: Array.isArray(res.errors) ? res.errors : [],
2411
+ warnings: Array.isArray(res.warnings) ? res.warnings : [],
2412
+ };
2413
+
2414
+ if (values.json) {
2415
+ console.log(JSON.stringify(result, null, 2));
2416
+ } else if (!result.ok) {
2417
+ console.error("❌ TASK ENVELOPE VALIDATION FAILED:");
2418
+ for (const err of result.errors) {
2419
+ console.error(` - ${err}`);
2420
+ }
2421
+ if (result.warnings.length > 0) {
2422
+ console.warn("⚠️ TASK ENVELOPE WARNINGS:");
2423
+ for (const w of result.warnings) {
2424
+ console.warn(` - ${w}`);
2425
+ }
2426
+ }
2427
+ } else {
2428
+ if (result.warnings.length > 0) {
2429
+ console.warn("⚠️ TASK ENVELOPE WARNINGS:");
2430
+ for (const w of result.warnings) {
2431
+ console.warn(` - ${w}`);
2432
+ }
2433
+ }
2434
+ console.log(`✅ Task envelope validated successfully: ${target}`);
2435
+ }
2436
+
2437
+ process.exit(result.ok ? 0 : 1);
2205
2438
  } else {
2206
- console.error(`Unknown task subcommand '${subCommand}'. Supported: agentctl task create, agentctl task optimize, agentctl task template`);
2439
+ console.error(`Unknown task subcommand '${subCommand}'. Supported: agentctl task create, agentctl task optimize, agentctl task template, agentctl task validate`);
2207
2440
  process.exit(1);
2208
2441
  }
2209
2442
  break;
@@ -2226,7 +2459,7 @@ async function main() {
2226
2459
  version: VERSION,
2227
2460
  root,
2228
2461
  pendingTasks: files.length,
2229
- activeLocks: lockStatus(root).length,
2462
+ activeLocks: lockStatus(root, { activeOnly: true }).length,
2230
2463
  budget,
2231
2464
  }, null, 2));
2232
2465
  process.exit(0);
@@ -2235,7 +2468,7 @@ async function main() {
2235
2468
  console.log(`--------------------------------------------------`);
2236
2469
  console.log(` Project Root : ${root}`);
2237
2470
  console.log(` Pending Tasks : ${files.length}`);
2238
- console.log(` Active VFS Locks : ${lockStatus(root).length}`);
2471
+ console.log(` Active VFS Locks : ${lockStatus(root, { activeOnly: true }).length}`);
2239
2472
  console.log(` Daily Budget : ${formatBudgetLine(budget)}`);
2240
2473
  console.log(`--------------------------------------------------\n`);
2241
2474
  process.exit(0);
@@ -3279,10 +3512,182 @@ async function main() {
3279
3512
  break;
3280
3513
  }
3281
3514
 
3282
- default:
3283
- console.error(`Unknown command: ${command}`);
3284
- printHelp();
3515
+ case "migrate": {
3516
+ const { values } = parseArgs({
3517
+ args: args.slice(1),
3518
+ options: {
3519
+ "dry-run": { type: "boolean", short: "d" },
3520
+ yes: { type: "boolean", short: "y" },
3521
+ json: { type: "boolean", short: "j" },
3522
+ },
3523
+ allowPositionals: true,
3524
+ });
3525
+
3526
+ const dryRun = Boolean(values["dry-run"]);
3527
+ const confirmed = Boolean(values.yes);
3528
+ const asJson = Boolean(values.json);
3529
+
3530
+ if (!dryRun && !confirmed) {
3531
+ if (asJson) {
3532
+ console.error(JSON.stringify({
3533
+ ok: false,
3534
+ error: "Refusing to migrate without --yes. Pass --dry-run to preview, or --yes to confirm.",
3535
+ }));
3536
+ } else {
3537
+ console.error("❌ Refusing to migrate without --yes. Pass --dry-run to preview, or --yes to confirm.");
3538
+ }
3539
+ process.exit(1);
3540
+ }
3541
+
3542
+ const { executeMigration } = await import("../src/migrate.mjs");
3543
+ const res = executeMigration(root, { dryRun });
3544
+
3545
+ if (!res.ok) {
3546
+ if (asJson) {
3547
+ console.log(JSON.stringify(res, null, 2));
3548
+ } else {
3549
+ console.error(`❌ ${res.error || "Migration failed."}`);
3550
+ }
3551
+ process.exit(1);
3552
+ }
3553
+
3554
+ if (asJson) {
3555
+ const payload = {
3556
+ ok: true,
3557
+ dryRun: Boolean(res.dryRun),
3558
+ migrated: res.migrated,
3559
+ warnings: res.warnings || [],
3560
+ };
3561
+ if (res.message) payload.message = res.message;
3562
+ console.log(JSON.stringify(payload, null, 2));
3563
+ process.exit(0);
3564
+ }
3565
+
3566
+ if (res.migrated.length === 0 && (!res.warnings || res.warnings.length === 0)) {
3567
+ console.log(`✅ ${res.message || "Repository is already in canonical v1 format."}`);
3568
+ process.exit(0);
3569
+ }
3570
+
3571
+ if (dryRun) {
3572
+ console.log("🧪 Dry run — no files modified.");
3573
+ if (res.migrated.length === 0) {
3574
+ console.log(" Nothing to migrate.");
3575
+ } else {
3576
+ console.log(" Would migrate:");
3577
+ for (const item of res.migrated) {
3578
+ console.log(` - [${item.type}] ${item.path}`);
3579
+ for (const change of item.changes || []) console.log(` • ${change}`);
3580
+ }
3581
+ }
3582
+ } else {
3583
+ console.log("✅ Migration applied.");
3584
+ for (const item of res.migrated) {
3585
+ console.log(` - [${item.type}] ${item.path}`);
3586
+ for (const change of item.changes || []) console.log(` • ${change}`);
3587
+ }
3588
+ }
3589
+
3590
+ if (res.warnings?.length) {
3591
+ console.log("⚠️ Warnings:");
3592
+ for (const w of res.warnings) console.log(` - ${w}`);
3593
+ }
3594
+
3595
+ process.exit(0);
3596
+ break;
3597
+ }
3598
+
3599
+ case "uninstall": {
3600
+ const { values } = parseArgs({
3601
+ args: args.slice(1),
3602
+ options: {
3603
+ "dry-run": { type: "boolean", short: "d" },
3604
+ yes: { type: "boolean", short: "y" },
3605
+ force: { type: "boolean", short: "f" },
3606
+ json: { type: "boolean", short: "j" },
3607
+ },
3608
+ allowPositionals: true,
3609
+ });
3610
+
3611
+ const dryRun = Boolean(values["dry-run"]);
3612
+ const confirmed = Boolean(values.yes);
3613
+ const force = Boolean(values.force);
3614
+ const asJson = Boolean(values.json);
3615
+
3616
+ if (!dryRun && !confirmed) {
3617
+ if (asJson) {
3618
+ console.error(JSON.stringify({
3619
+ ok: false,
3620
+ error: "Refusing to uninstall without --yes. Pass --dry-run to preview, or --yes to confirm.",
3621
+ }));
3622
+ } else {
3623
+ console.error("❌ Refusing to uninstall without --yes. Pass --dry-run to preview, or --yes to confirm.");
3624
+ }
3625
+ process.exit(1);
3626
+ }
3627
+
3628
+ const { uninstallKit } = await import("../src/uninstall.mjs");
3629
+ const res = uninstallKit(root, { dryRun, force });
3630
+
3631
+ if (!res.ok) {
3632
+ if (asJson) {
3633
+ console.log(JSON.stringify(res, null, 2));
3634
+ } else {
3635
+ console.error(`❌ ${res.error || "Uninstall failed."}`);
3636
+ }
3637
+ process.exit(1);
3638
+ }
3639
+
3640
+ if (asJson) {
3641
+ if (dryRun) {
3642
+ console.log(JSON.stringify({ ok: true, dryRun: true, willRemove: res.willRemove }, null, 2));
3643
+ } else {
3644
+ console.log(JSON.stringify({ ok: true, removed: res.removed }, null, 2));
3645
+ }
3646
+ process.exit(0);
3647
+ }
3648
+
3649
+ if (dryRun) {
3650
+ console.log("🧪 Dry run — no files modified.");
3651
+ console.log(" Would remove / clean:");
3652
+ for (const item of res.willRemove) console.log(` - ${item}`);
3653
+ if (force && (!res.willRemove.some((p) => p.startsWith(".agent/rules") || p.startsWith(".agent/prompts") || p.startsWith(".agent/workflows") || p.startsWith(".agent/jules-queue")))) {
3654
+ console.log(" (no legacy scaffold directories present)");
3655
+ }
3656
+ } else {
3657
+ console.log("✅ Kit configuration uninstalled.");
3658
+ console.log(" Removed / cleaned:");
3659
+ for (const item of res.removed) console.log(` - ${item}`);
3660
+ }
3661
+ process.exit(0);
3662
+ break;
3663
+ }
3664
+
3665
+ default: {
3666
+ const knownTopCommands = [
3667
+ ...new Set(
3668
+ COMMAND_REGISTRY.flatMap((c) => [c.path[0], ...(c.shortcuts || [])]).filter(Boolean)
3669
+ ),
3670
+ "help",
3671
+ "version",
3672
+ ];
3673
+ const { levenshteinDistance } = await import("../src/task-optimizer.mjs");
3674
+ let closest = null;
3675
+ let minDistance = Infinity;
3676
+ for (const known of knownTopCommands) {
3677
+ const dist = levenshteinDistance(command.toLowerCase(), known.toLowerCase());
3678
+ if (dist < minDistance) {
3679
+ minDistance = dist;
3680
+ closest = known;
3681
+ }
3682
+ }
3683
+ if (closest && minDistance <= 3) {
3684
+ console.error(`Unknown command: "${command}". Did you mean "${closest}"?`);
3685
+ } else {
3686
+ console.error(`Unknown command: "${command}".`);
3687
+ }
3688
+ console.error(`Run 'agentctl --help' to see all available commands.`);
3285
3689
  process.exit(1);
3690
+ }
3286
3691
  }
3287
3692
  }
3288
3693