jules-orchestrator-kit 0.72.2 → 0.73.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 (80) hide show
  1. package/.agent/prompts/{Overseer.md → Auditor.md} +3 -3
  2. package/.agent/prompts/{Alchemist.md → Database.md} +1 -1
  3. package/.agent/prompts/Debugger.md +25 -0
  4. package/.agent/prompts/{Scribe.md → Docs.md} +8 -5
  5. package/.agent/prompts/{Spectator.md → E2E.md} +9 -6
  6. package/.agent/prompts/{Janitor.md → Hygiene.md} +2 -2
  7. package/.agent/prompts/{Bolt.md → Performance.md} +1 -1
  8. package/.agent/prompts/Resilience.md +20 -0
  9. package/.agent/prompts/Security.md +21 -0
  10. package/.agent/prompts/Testing.md +30 -0
  11. package/.agent/prompts/Types.md +19 -0
  12. package/.agent/rules/jules-protocol.md +4 -3
  13. package/AGENTS.md +78 -96
  14. package/CHANGELOG.md +193 -0
  15. package/JULES_RULES_TEMPLATE.md +83 -96
  16. package/LICENSE +1 -1
  17. package/README.md +77 -439
  18. package/ROADMAP_V1.md +22 -132
  19. package/bin/agentctl.mjs +443 -144
  20. package/bin/init.js +6 -3
  21. package/index.mjs +9 -6
  22. package/package.json +1 -1
  23. package/scripts/asset-integrity-check.mjs +1 -1
  24. package/scripts/doc-sync-check.mjs +47 -0
  25. package/scripts/generate-command-reference.mjs +39 -0
  26. package/scripts/jules-dispatch.mjs +12 -113
  27. package/scripts/jules-merge-swarm.mjs +8 -196
  28. package/scripts/jules-patch.mjs +7 -8
  29. package/scripts/jules-queue-runner.mjs +6 -8
  30. package/scripts/jules-scan-todos.mjs +10 -38
  31. package/scripts/jules-self-audit.mjs +8 -139
  32. package/scripts/jules-status.mjs +32 -38
  33. package/scripts/jules-webhook-receiver.mjs +1 -1
  34. package/src/assertions.mjs +5 -50
  35. package/src/bidi-guard.mjs +36 -0
  36. package/src/budget.mjs +3 -14
  37. package/src/config.mjs +2 -6
  38. package/src/dashboard.mjs +7 -9
  39. package/src/dispatch.mjs +212 -0
  40. package/src/engine.mjs +40 -16
  41. package/src/evidence.mjs +10 -41
  42. package/src/execution-envelope.mjs +13 -1
  43. package/src/flaky-ledger.mjs +1 -1
  44. package/src/fs-atomic.mjs +72 -0
  45. package/src/git.mjs +298 -27
  46. package/src/mcp.mjs +296 -7
  47. package/src/memory.mjs +0 -0
  48. package/src/merge-swarm.mjs +202 -0
  49. package/src/ops/cli-intent.mjs +1 -0
  50. package/src/ops/command-registry.mjs +796 -70
  51. package/src/ops/doctor-registry.mjs +134 -47
  52. package/src/ops/handover.mjs +3 -27
  53. package/src/ops/pr-harvest.mjs +1 -1
  54. package/src/prompt-guard.mjs +33 -3
  55. package/src/provider.mjs +51 -7
  56. package/src/remediation.mjs +2 -2
  57. package/src/role-resolver.mjs +113 -3
  58. package/src/router.mjs +19 -11
  59. package/src/runtime-env.mjs +67 -0
  60. package/src/scaffold.mjs +3 -1
  61. package/src/scope-guard.mjs +249 -0
  62. package/src/secret-scanner.mjs +530 -0
  63. package/src/security.mjs +81 -2972
  64. package/src/self-audit.mjs +140 -0
  65. package/src/session-ops.mjs +29 -0
  66. package/src/stability.mjs +8 -1
  67. package/src/stack-detector.mjs +5 -2
  68. package/src/state.mjs +45 -0
  69. package/src/swarm.mjs +76 -0
  70. package/src/task-optimizer.mjs +34 -7
  71. package/src/telemetry.mjs +23 -0
  72. package/src/test-tamper-guard.mjs +2173 -0
  73. package/src/todo-scanner.mjs +129 -0
  74. package/src/web-templates.mjs +3 -3
  75. package/src/webhook.mjs +10 -3
  76. package/src/wizard-init.mjs +12 -20
  77. package/src/wizard-oracle.mjs +4 -3
  78. package/src/wizard-task.mjs +37 -9
  79. package/.agent/prompts/Sentinel.md +0 -18
  80. package/scripts/utils.mjs +0 -241
package/bin/agentctl.mjs CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { parseArgs } from "node:util";
4
- import { readFileSync, existsSync, readdirSync } from "node:fs";
4
+ import { readFileSync, existsSync, readdirSync, statSync, renameSync, mkdirSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  import { applyEnvAliases } from "../src/env-aliases.mjs";
7
7
  import { selectFailureOutput } from "../src/ops/verify-output.mjs";
8
8
  import { loadConfig, resolveRoot, detectStack, bootstrapZeroTestRepo } from "../src/config.mjs";
9
9
  import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
10
10
  import { acquireLock, releaseLock, lockStatus, getQueueDir } from "../src/state.mjs";
11
+ import { parseEnvelopeHeader } from "../src/envelope.mjs";
11
12
  import { worktreePrune } from "../src/git.mjs";
12
13
  import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
13
14
  import { KIT_VERSION } from "../src/version.mjs";
14
15
  import { budgetStatus, listOpenReservations, releaseOpenReservations, resolveConcurrency } from "../src/budget.mjs";
16
+ import { COMMAND_REGISTRY } from "../src/ops/command-registry.mjs";
15
17
 
16
18
  // Vendor-neutral env spellings are filled in before any module reads
17
19
  // process.env, so `AGENT_API_KEY` works everywhere `JULES_API_KEY` does. A
@@ -42,78 +44,52 @@ export function formatBudgetLine(b) {
42
44
  }
43
45
 
44
46
  export function printHelp() {
45
- console.log(`
46
- agentctl v${VERSION} — Universal Agent Orchestrator & Safety Gatekeeper
47
-
48
- Usage: agentctl <command> [options]
49
-
50
- Commands:
51
- dispatch | create Dispatch a single task to an AI agent (--role <name>, --tier fast|complex, --check-premise)
52
- check Run all-in-one CI security, rules, and stack verification gate
53
- gate | audit Run CI security and verification gate against current branch
54
- mutate | mutation Run zero-dependency diff mutation testing harness (--min-score, --max-mutants)
55
- coverage Run native zero-dependency V8 diff coverage check (--min, --cmd)
56
- probe | stability Run test flakiness stability probe across N repetitions (--repeat, --cmd)
57
- perf | event-loop Monitor Node.js event loop delay and Big-O lag (--max-ms, --cmd)
58
- fix Auto-repair from piped terminal logs or error trace (npm test 2>&1 | agentctl fix)
59
- rules <action> Audit rule token budgets or compile rule sentinels (check | compile)
60
- queue Run pending task queue (--dag, --concurrency <n>)
61
- swarm Run parallel task swarm
62
- mcp Start stdio Model Context Protocol (MCP) server
63
- clean Clean stale branches, worktrees, locks, and ledgers
64
- lock <action> Manage mutex locks (acquire [--ttl <min>] [--pid <n>] | release | status)
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)
70
- bootstrap Bootstrap zero-test repository with verification oracle
71
- review-repair Parse PR review comments and synthesize OODA repair tasks
72
- dashboard Start local HTTP telemetry and audit dashboard
73
- init Scaffold .agent/ config and run onboarding wizard
74
- task create Interactively author and scope a Jules task envelope (--template <name>, --role <name>, --tier fast|complex)
75
- task optimize Linter & optimizer for Jules task prompts (--fix, --json, --web)
76
- task template List and generate task templates (--list, --json)
77
- test-gen Scaffold & run automated TDD Red-to-Green test cycle (--run)
78
- mcp init Scaffold IDE integration config (cursor | vscode | claude | all)
79
- rollback Restore git state & working tree to atomic pre-flight checkpoint
80
- handover Inspect or generate Baton Pass session handover envelopes (list | show | create | prune)
81
- resume Resume warm session with human response (--response "<text>")
82
- plan approve <id> Approve pending plan for a Jules session (:approvePlan)
83
- session get <id> Retrieve remote session status from provider API
84
- patch <id> Extract and test/apply git patch from a Jules session (--apply, --save)
85
- retry <id> Retry failed session with automated failure-trace injection (--role)
86
- prune Batch-archive or delete stale sessions via Jules API (--age 7d, --yes)
87
- pr harvest Triage, verify CI, and auto-merge low-risk agent PRs (--auto, --tier r0,r1)
88
- escalate Dispatch or manage webhook escalation incidents (--flush, --status, --clear)
89
- flaky Manage Wilson-quarantined tests and dispatch healing swarm (status | heal | reset)
90
- status Display queue and system status summary
91
- budget Show the 24h task budget, worker slots and their provenance (reset --yes)
92
- scan Scan codebase for TODO/FIXME task candidates
93
- hydrate [prompt] Prepend active system learnings and baton-pass state to a prompt
94
- harvest Harvest failure traces and record/quarantine resolution rules
95
- learning add Record a system learning rule into .agent/knowledge/
96
- evidence <action> Manage cryptographic audit evidence (generate | verify | show)
97
- assert Run declarative zero-dependency verification assertion primitives
98
- version Output agentctl version
99
-
100
- Options:
101
- --prompt, -p Task prompt text — dispatch, task create and task optimize
102
- also accept it as a positional argument
103
- --prompt-file, -f Read the prompt from a file (-f is --fix on task optimize)
104
- --role, -r Specify specialist agent role (overseer | bolt | sentinel | janitor)
105
- --tier Force routing tier when router.enabled (fast | complex) — see .agent/config.yml router:
106
- --check-premise Verify task goal/oracle passes locally before burning API budget
107
- --dag Execute queue tasks via DAG dependency resolution
108
- --dry-run, -d Simulate action without making API calls or modifying git
109
- --mode, -m Gate evaluation mode (working-tree | committed | staged)
110
- --repoless Dispatch task in repoless execution mode
111
- --source, -s Specify Jules repository source name
112
- --branch, -b Specify target starting branch
113
- --json, -j Emit machine-readable JSON output
114
- --json-report <path> Write structured machine-readable JSON diagnostics report
115
- --help, -h Show command help
116
- `);
47
+ // Single source of truth: the command list is generated from
48
+ // COMMAND_REGISTRY, so adding a descriptor automatically updates --help,
49
+ // `help <command>` (via formatCommandHelp) and docs/COMMAND_REFERENCE.md.
50
+ const CATEGORY_ORDER = ["Create", "Inspect", "Operate", "Repair", "Configure"];
51
+ const lines = [];
52
+ lines.push("");
53
+ lines.push(`agentctl v${VERSION} — Universal Agent Orchestrator & Safety Gatekeeper`);
54
+ lines.push("");
55
+ lines.push("Usage: agentctl <command> [options]");
56
+ lines.push("");
57
+ lines.push("Commands:");
58
+ for (const category of CATEGORY_ORDER) {
59
+ const cmds = COMMAND_REGISTRY.filter((c) => c.category === category);
60
+ if (cmds.length === 0) continue;
61
+ lines.push(` --- ${category} ---`);
62
+ for (const cmd of cmds) {
63
+ const name = cmd.path.join(" ");
64
+ const flagHint =
65
+ cmd.flags.length > 0
66
+ ? ` (${cmd.flags.slice(0, 3).map((f) => `--${f.name}`).join(", ")}${cmd.flags.length > 3 ? ", ..." : ""})`
67
+ : "";
68
+ lines.push(` ${name.padEnd(20)} ${cmd.description}${flagHint}`);
69
+ }
70
+ }
71
+ lines.push(" version Output agentctl version");
72
+ lines.push("");
73
+ lines.push("Run `agentctl help <command>` or `agentctl <command> --help` for full flag documentation.");
74
+ lines.push("");
75
+ lines.push("Options:");
76
+ lines.push(" --prompt, -p Task prompt text — dispatch, task create and task optimize");
77
+ lines.push(" also accept it as a positional argument");
78
+ lines.push(" --prompt-file, -f Read the prompt from a file (-f is --fix on task optimize)");
79
+ lines.push(" --role, -r Specify specialist agent role (auditor | performance | security | hygiene | resilience | types | debugger | testing | e2e | database | docs | a11y)");
80
+ 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");
82
+ lines.push(" --dag Execute queue tasks via DAG dependency resolution");
83
+ lines.push(" --dry-run, -d Simulate action without making API calls or modifying git");
84
+ lines.push(" --mode, -m Gate evaluation mode (working-tree | committed | staged)");
85
+ lines.push(" --repoless Dispatch task in repoless execution mode");
86
+ lines.push(" --source, -s Specify Jules repository source name");
87
+ lines.push(" --branch, -b Specify target starting branch");
88
+ lines.push(" --json, -j Emit machine-readable JSON output");
89
+ lines.push(" --json-report <path> Write structured machine-readable JSON diagnostics report");
90
+ lines.push(" --help, -h Show command help");
91
+ lines.push("");
92
+ console.log(lines.join("\n"));
117
93
  }
118
94
 
119
95
  /**
@@ -130,17 +106,65 @@ Options:
130
106
  * @param {string[]} [positionals] Remaining free arguments.
131
107
  * @returns {string} The prompt, or "" when none was supplied.
132
108
  */
133
- function resolvePromptInput(values, positionals = []) {
109
+ /**
110
+ * Resolve the prompt text and source path a command was given.
111
+ *
112
+ * @param {Record<string, unknown>} values Parsed flags.
113
+ * @param {string[]} [positionals] Remaining free arguments.
114
+ * @param {string} [root] Repository root.
115
+ * @returns {{ content: string, sourceFile: string | null }}
116
+ */
117
+ function resolvePromptDetails(values, positionals = [], root = process.cwd()) {
134
118
  const file = values["prompt-file"] || values.file;
135
119
  if (file) {
136
120
  if (!existsSync(file)) {
137
121
  console.error(`Error: prompt file not found: ${file}`);
138
122
  process.exit(1);
139
123
  }
140
- return readFileSync(file, "utf-8");
124
+ return { content: readFileSync(file, "utf-8"), sourceFile: file };
141
125
  }
142
- if (values.prompt) return String(values.prompt);
143
- return positionals.join(" ").trim();
126
+ if (values.prompt) return { content: String(values.prompt), sourceFile: null };
127
+
128
+ if (positionals.length === 1) {
129
+ const raw = String(positionals[0]).trim();
130
+ const candidateFiles = [
131
+ raw,
132
+ resolve(root, raw),
133
+ join(root, ".agent", "jules-queue", raw),
134
+ join(root, ".agent", "jules-queue", `${raw}.md`),
135
+ ];
136
+ for (const cand of candidateFiles) {
137
+ if (cand && existsSync(cand)) {
138
+ try {
139
+ if (statSync(cand).isFile()) {
140
+ return { content: readFileSync(cand, "utf-8"), sourceFile: cand };
141
+ }
142
+ } catch (_) {}
143
+ }
144
+ }
145
+ }
146
+
147
+ if (positionals.length === 0) {
148
+ const queueDir = getQueueDir(root);
149
+ if (existsSync(queueDir)) {
150
+ const candidates = readdirSync(queueDir)
151
+ .filter((f) => f.endsWith(".md") && !f.startsWith(".") && f !== "README.md" && isTaskFile(f, queueDir))
152
+ .sort();
153
+ if (candidates.length > 0) {
154
+ const selected = join(queueDir, candidates[0]);
155
+ if (!values?.json) {
156
+ console.log(`Auto-selected pending queue task: .agent/jules-queue/${candidates[0]}`);
157
+ }
158
+ return { content: readFileSync(selected, "utf-8"), sourceFile: selected };
159
+ }
160
+ }
161
+ }
162
+
163
+ return { content: positionals.join(" ").trim(), sourceFile: null };
164
+ }
165
+
166
+ function resolvePromptInput(values, positionals = [], root = process.cwd()) {
167
+ return resolvePromptDetails(values, positionals, root).content;
144
168
  }
145
169
 
146
170
  /**
@@ -275,8 +299,7 @@ async function main() {
275
299
  const config = loadConfig(root);
276
300
 
277
301
  switch (command) {
278
- case "dispatch":
279
- case "create": {
302
+ case "dispatch": {
280
303
  const { values, positionals } = parseArgs({
281
304
  args: args.slice(1),
282
305
  options: {
@@ -290,21 +313,43 @@ async function main() {
290
313
  repoless: { type: "boolean" },
291
314
  "auto-pr": { type: "boolean" },
292
315
  "require-plan-approval": { type: "boolean" },
316
+ "auto-approve-plans": { type: "boolean" },
317
+ "auto-approve": { type: "boolean" },
293
318
  "check-premise": { type: "boolean" },
294
319
  idempotent: { type: "boolean" },
295
320
  author: { type: "string" },
321
+ "verify-cmd": { type: "string", short: "v" },
322
+ verify: { type: "string" },
296
323
  "dry-run": { type: "boolean", short: "d" },
297
324
  json: { type: "boolean", short: "j" },
298
325
  },
299
326
  allowPositionals: true,
300
327
  });
301
328
 
302
- const promptContent = resolvePromptInput(values, positionals);
329
+ const { content: promptContent, sourceFile } = resolvePromptDetails(values, positionals, root);
303
330
  if (!promptContent) {
304
- console.error("Error: a prompt is required — pass it as --prompt, --prompt-file, or a positional argument.");
331
+ console.error("Error: a prompt is required — pass it as --prompt, --prompt-file, a task file path, or create one in .agent/jules-queue/.");
305
332
  process.exit(1);
306
333
  }
307
334
 
335
+ // If promptContent has an envelope header, inherit defaults
336
+ const envelopeMeta = parseEnvelopeHeader(promptContent);
337
+ if (envelopeMeta) {
338
+ values.title = values.title || envelopeMeta.title;
339
+ values.role = values.role || envelopeMeta.role;
340
+ values.tier = values.tier || envelopeMeta.tier;
341
+ values["verify-cmd"] = values["verify-cmd"] || values.verify || envelopeMeta.verifyCmd;
342
+ if (values["auto-pr"] === undefined && envelopeMeta.flags?.autoPr !== undefined) {
343
+ values["auto-pr"] = envelopeMeta.flags.autoPr;
344
+ }
345
+ if (values["require-plan-approval"] === undefined && envelopeMeta.flags?.requirePlanApproval !== undefined) {
346
+ values["require-plan-approval"] = envelopeMeta.flags.requirePlanApproval;
347
+ }
348
+ if (values.repoless === undefined && envelopeMeta.flags?.repoless !== undefined) {
349
+ values.repoless = envelopeMeta.flags.repoless;
350
+ }
351
+ }
352
+
308
353
  // `task create --role` has always rejected a role it cannot resolve;
309
354
  // `dispatch --role` used to drop it without a word and hand the work to a
310
355
  // generic agent, so the two commands disagreed about what the same flag
@@ -314,13 +359,14 @@ async function main() {
314
359
  const { resolveRolePrompt } = await import("../src/role-resolver.mjs");
315
360
  if (!resolveRolePrompt(root, values.role, { config })) {
316
361
  console.error(
317
- `Error: Unknown agent role '${values.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Overseer, Bolt, Sentinel, Janitor).`
362
+ `Error: Unknown agent role '${values.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Auditor, Performance, Security, Hygiene, Testing).`
318
363
  );
319
364
  console.error(` Run 'agentctl init' to scaffold the shipped role prompts.`);
320
365
  process.exit(1);
321
366
  }
322
367
  }
323
368
 
369
+ const autoApprove = Boolean(values["auto-approve-plans"] || values["auto-approve"]);
324
370
  const task = {
325
371
  title: values.title || "CLI Dispatch Task",
326
372
  prompt: promptContent,
@@ -330,9 +376,10 @@ async function main() {
330
376
  branch: values.branch,
331
377
  repoless: values.repoless,
332
378
  autoPr: values["auto-pr"],
333
- requirePlanApproval: values["require-plan-approval"],
379
+ requirePlanApproval: autoApprove ? false : values["require-plan-approval"],
334
380
  checkPremise: values["check-premise"] || values.idempotent,
335
381
  author: values.author,
382
+ verifyCmd: values["verify-cmd"] || values.verify,
336
383
  };
337
384
 
338
385
  try {
@@ -374,6 +421,19 @@ async function main() {
374
421
  }
375
422
  }
376
423
  }
424
+ if (session && session.ok !== false && !values["dry-run"] && sourceFile) {
425
+ const queueDir = getQueueDir(root);
426
+ const resolvedSrc = resolve(root, sourceFile);
427
+ if (resolvedSrc.startsWith(queueDir) && existsSync(resolvedSrc)) {
428
+ const completedDir = join(queueDir, "completed");
429
+ if (!existsSync(completedDir)) mkdirSync(completedDir, { recursive: true });
430
+ const baseName = resolvedSrc.slice(queueDir.length).replace(/^[/\\]+/, "");
431
+ const dstPath = join(completedDir, baseName);
432
+ if (!existsSync(dstPath)) {
433
+ renameSync(resolvedSrc, dstPath);
434
+ }
435
+ }
436
+ }
377
437
  process.exit(0);
378
438
  } catch (err) {
379
439
  const exitCode = typeof err.code === "number" ? err.code : 1;
@@ -818,6 +878,10 @@ async function main() {
818
878
  min: { type: "string", short: "m" },
819
879
  "min-pass-rate": { type: "string" },
820
880
  cmd: { type: "string", short: "c" },
881
+ "test-cmd": { type: "string", short: "t" },
882
+ "verify-cmd": { type: "string" },
883
+ record: { type: "boolean" },
884
+ "no-record": { type: "boolean" },
821
885
  json: { type: "boolean", short: "j" },
822
886
  },
823
887
  allowPositionals: true,
@@ -826,8 +890,10 @@ async function main() {
826
890
  const root = resolveRoot();
827
891
  const repeat = values.repeat ? parseInt(values.repeat, 10) : (values.iterations ? parseInt(values.iterations, 10) : 5);
828
892
  const minPassRate = values.min ? parseFloat(values.min) : (values["min-pass-rate"] ? parseFloat(values["min-pass-rate"]) : 1.0);
893
+ const targetCmd = values["test-cmd"] || values["verify-cmd"] || values.cmd;
894
+ const record = values["no-record"] ? false : values.record !== false;
829
895
 
830
- const report = runStabilityProbe(values.cmd, { root, repeat, minPassRate });
896
+ const report = runStabilityProbe(targetCmd, { root, repeat, minPassRate, record });
831
897
 
832
898
  if (values.json) {
833
899
  console.log(JSON.stringify(report, null, 2));
@@ -846,6 +912,11 @@ async function main() {
846
912
  }
847
913
  }
848
914
 
915
+ if (record && !report.ok) {
916
+ console.log(`\n Recorded probe results to .agent/state/flaky.jsonl.`);
917
+ console.log(` Check status or trigger auto-healing: agentctl flaky status | agentctl flaky heal`);
918
+ }
919
+
849
920
  console.log(`------------------------------------------------------------------`);
850
921
  console.log(`Overall Result: ${report.ok ? "APPROVED (Deterministic 100% Pass — Exit 0)" : "REJECTED (Flaky / Intermittent Failures — Exit 1)"}\n`);
851
922
  }
@@ -917,10 +988,11 @@ async function main() {
917
988
  errorInput = readFileSync(values.file, "utf-8");
918
989
  } else if (values.input) {
919
990
  errorInput = values.input;
920
- } else if (positionals.slice(1).length > 0) {
921
- errorInput = positionals.slice(1).join(" ");
991
+ } else if (positionals.length > 0) {
992
+ errorInput = positionals.join(" ");
922
993
  } else if (!process.stdin.isTTY) {
923
- errorInput = readFileSync(0, "utf-8");
994
+ const { text } = await import("node:stream/consumers");
995
+ errorInput = await text(process.stdin);
924
996
  }
925
997
 
926
998
  if (!errorInput.trim()) {
@@ -936,6 +1008,7 @@ async function main() {
936
1008
  prompt: `Fix the following test/build failure:\n\n${cleanTrace.slice(0, 4000)}`,
937
1009
  title: "Automated Failure Repair",
938
1010
  verifyCmd: values.cmd || "npm test",
1011
+ allowHighEntropy: true,
939
1012
  });
940
1013
 
941
1014
  if (values.json) {
@@ -979,7 +1052,7 @@ async function main() {
979
1052
  allowPositionals: true,
980
1053
  });
981
1054
 
982
- const sessionId = values.session || positionals[0] || positionals[1];
1055
+ const sessionId = values.session || positionals[0];
983
1056
  if (!sessionId) {
984
1057
  console.error("❌ Error: Missing required session ID. Usage: agentctl patch <session_id> [--apply] [--save <path>]");
985
1058
  process.exit(1);
@@ -1030,7 +1103,7 @@ async function main() {
1030
1103
  allowPositionals: true,
1031
1104
  });
1032
1105
 
1033
- const sessionId = values.session || positionals[0] || positionals[1];
1106
+ const sessionId = values.session || positionals[0];
1034
1107
  if (!sessionId) {
1035
1108
  console.error("❌ Error: Missing required session ID. Usage: agentctl retry <session_id> [--role <role>]");
1036
1109
  process.exit(1);
@@ -1312,7 +1385,21 @@ async function main() {
1312
1385
  }
1313
1386
 
1314
1387
  case "budget": {
1315
- const action = args[1];
1388
+ const { values: budgetValues, positionals: budgetPositionals } = parseArgs({
1389
+ args: args.slice(1),
1390
+ options: {
1391
+ "dry-run": { type: "boolean" },
1392
+ yes: { type: "boolean", short: "y" },
1393
+ all: { type: "boolean" },
1394
+ "by-user": { type: "boolean", short: "u" },
1395
+ json: { type: "boolean" },
1396
+ },
1397
+ allowPositionals: true,
1398
+ // strict:false so `reset` can refuse unknown flags with its own
1399
+ // ledger-aware message instead of a generic parseArgs throw.
1400
+ strict: false,
1401
+ });
1402
+ const action = budgetPositionals[0];
1316
1403
  const b = budgetStatus(config, root);
1317
1404
 
1318
1405
  if (action === "reset") {
@@ -1330,11 +1417,11 @@ async function main() {
1330
1417
  console.error("Accepted: --dry-run, --yes/-y, --all. Nothing was released.");
1331
1418
  process.exit(2);
1332
1419
  }
1333
- const dryRun = args.includes("--dry-run");
1334
- const confirmed = args.includes("--yes") || args.includes("-y");
1420
+ const dryRun = Boolean(budgetValues["dry-run"]);
1421
+ const confirmed = Boolean(budgetValues.yes);
1335
1422
  // Committed reservations reached the provider, so releasing them makes
1336
1423
  // the local count understate real usage. That has to be deliberate.
1337
- const includeCommitted = args.includes("--all");
1424
+ const includeCommitted = Boolean(budgetValues.all);
1338
1425
  if (!dryRun && !confirmed) {
1339
1426
  const open = listOpenReservations(root);
1340
1427
  const committed = open.filter((r) => r.committed).length;
@@ -1361,7 +1448,7 @@ async function main() {
1361
1448
  process.exit(0);
1362
1449
  }
1363
1450
 
1364
- if (args.includes("--by-user") || args.includes("-u")) {
1451
+ if (budgetValues["by-user"]) {
1365
1452
  console.log("📊 Task Budget Attribution (Rolling 24h Window)");
1366
1453
  console.log(`Daily Limit : ${b.limit} Tasks | Used: ${b.used} | Remaining: ${b.remaining}\n`);
1367
1454
  const users = Object.entries(b.byUser || {});
@@ -1382,7 +1469,7 @@ async function main() {
1382
1469
  process.exit(0);
1383
1470
  }
1384
1471
 
1385
- if (args.includes("--json")) {
1472
+ if (budgetValues.json) {
1386
1473
  console.log(JSON.stringify({ ok: true, budget: { ...b, scope: "this-repository" } }, null, 2));
1387
1474
  process.exit(0);
1388
1475
  }
@@ -1408,7 +1495,16 @@ async function main() {
1408
1495
  }
1409
1496
 
1410
1497
  case "lock": {
1411
- const action = args[1];
1498
+ const { values: lockValues, positionals: lockPositionals } = parseArgs({
1499
+ args: args.slice(1),
1500
+ options: {
1501
+ ttl: { type: "string" },
1502
+ pid: { type: "string" },
1503
+ json: { type: "boolean", short: "j" },
1504
+ },
1505
+ allowPositionals: true,
1506
+ });
1507
+ const action = lockPositionals[0];
1412
1508
  if (action === "acquire") {
1413
1509
  // A lock taken from the command line outlives the command. The process
1414
1510
  // that runs `lock acquire` writes the record and exits; the agent it
@@ -1419,27 +1515,26 @@ async function main() {
1419
1515
  // `--pid` is the escape hatch for a caller that *does* have a durable
1420
1516
  // process to point at: the lock then releases itself when that process
1421
1517
  // dies, exactly as an in-process acquire does.
1422
- const flagIdx = args.findIndex((a, i) => i >= 2 && a.startsWith("--"));
1423
- const positional = flagIdx === -1 ? args.slice(2) : args.slice(2, flagIdx);
1424
- const { values } = parseArgs({
1425
- args: flagIdx === -1 ? [] : args.slice(flagIdx),
1426
- options: {
1427
- ttl: { type: "string" },
1428
- pid: { type: "string" },
1429
- },
1430
- allowPositionals: false,
1431
- });
1432
- const agent = positional[0] || "agent";
1433
- const taskId = positional[1] || "task-1";
1518
+ const positional = lockPositionals.slice(1);
1519
+ if (positional.length < 3) {
1520
+ console.error("Usage: agentctl lock acquire <agent> <task_id> <file_path...> [--ttl <minutes>] [--pid <pid>]");
1521
+ process.exit(1);
1522
+ }
1523
+ const agent = positional[0];
1524
+ const taskId = positional[1];
1434
1525
  const filePaths = positional.slice(2);
1435
- const ttlMinutes = Number(values.ttl);
1436
- const ownerPid = Number(values.pid);
1526
+ const ttlMinutes = Number(lockValues.ttl);
1527
+ const ownerPid = Number(lockValues.pid);
1437
1528
  const bindsToProcess = Number.isInteger(ownerPid) && ownerPid > 0;
1438
1529
  const res = acquireLock(agent, taskId, filePaths, root, {
1439
1530
  lease: !bindsToProcess,
1440
1531
  ownerPid: bindsToProcess ? ownerPid : undefined,
1441
1532
  ttlMs: Number.isFinite(ttlMinutes) && ttlMinutes > 0 ? ttlMinutes * 60_000 : undefined,
1442
1533
  });
1534
+ if (lockValues.json) {
1535
+ console.log(JSON.stringify(res, null, 2));
1536
+ process.exit(res.ok ? 0 : 1);
1537
+ }
1443
1538
  if (res.ok) {
1444
1539
  const held = filePaths.length ? ` (${filePaths.length} path${filePaths.length === 1 ? "" : "s"})` : "";
1445
1540
  const until = bindsToProcess ? `bound to pid ${ownerPid}` : `expires ${new Date(Date.now() + (Number.isFinite(ttlMinutes) && ttlMinutes > 0 ? ttlMinutes : 120) * 60_000).toISOString()}`;
@@ -1453,8 +1548,16 @@ async function main() {
1453
1548
  process.exit(1);
1454
1549
  }
1455
1550
  } else if (action === "release") {
1456
- const taskId = args[2] || "task-1";
1551
+ const taskId = lockPositionals[1];
1552
+ if (!taskId) {
1553
+ console.error("Usage: agentctl lock release <task_id>");
1554
+ process.exit(1);
1555
+ }
1457
1556
  const ok = releaseLock(taskId, root);
1557
+ if (lockValues.json) {
1558
+ console.log(JSON.stringify({ ok, taskId }, null, 2));
1559
+ process.exit(ok ? 0 : 1);
1560
+ }
1458
1561
  if (ok) {
1459
1562
  console.log(`✅ Released lock for ${taskId}`);
1460
1563
  } else {
@@ -1463,7 +1566,21 @@ async function main() {
1463
1566
  }
1464
1567
  } else {
1465
1568
  const locks = lockStatus(root);
1466
- console.log(`Active Locks (${locks.length}):`, locks);
1569
+ if (lockValues.json) {
1570
+ console.log(JSON.stringify({ ok: true, locks }, null, 2));
1571
+ process.exit(0);
1572
+ }
1573
+ if (locks.length === 0) {
1574
+ console.log("Active Locks: 0 (no held locks)");
1575
+ } else {
1576
+ console.log(`Active Locks (${locks.length}):`);
1577
+ for (const l of locks) {
1578
+ const files = Array.isArray(l.files) ? l.files.join(", ") : (l.files || "");
1579
+ const expires = l.expiresAt ? `expires ${l.expiresAt}` : (l.ownerPid ? `pid ${l.ownerPid}` : "active");
1580
+ console.log(` • taskId: ${l.taskId || l.id} [${l.agent || "unknown"}] (${expires})`);
1581
+ if (files) console.log(` Paths: ${files}`);
1582
+ }
1583
+ }
1467
1584
  }
1468
1585
  process.exit(0);
1469
1586
  break;
@@ -1498,7 +1615,9 @@ async function main() {
1498
1615
  console.log(`--------------------------------------------------`);
1499
1616
  console.log(` Project Root : ${root}`);
1500
1617
  console.log(` Config File : ${config._file || "None (Using defaults)"}`);
1501
- console.log(` Detected Stack : ${detectStack(root).stack}`);
1618
+ const stackInfo = detectStack(root);
1619
+ const edgeSuffix = stackInfo.isEdgeRuntime ? ` (Edge: ${stackInfo.edgePlatform || "generic"})` : "";
1620
+ console.log(` Detected Stack : ${stackInfo.stack}${edgeSuffix}`);
1502
1621
  console.log(` Test Command : ${config.verify.test || "(None)"}`);
1503
1622
  console.log(` Build Command : ${config.verify.build || "(None)"}`);
1504
1623
  console.log(` Daily Budget : ${formatBudgetLine(budgetStatus(config, root))}`);
@@ -1526,11 +1645,16 @@ async function main() {
1526
1645
 
1527
1646
  case "provider":
1528
1647
  case "providers": {
1648
+ const { values, positionals } = parseArgs({
1649
+ args: args.slice(1),
1650
+ options: { json: { type: "boolean", short: "j" } },
1651
+ allowPositionals: true,
1652
+ });
1529
1653
  // `agentctl providers` used to tell the operator to run
1530
1654
  // `agentctl init --provider <name>` to switch — which restarts the whole
1531
1655
  // onboarding wizard, plan question included, to change one line.
1532
- if (args[1] === "set") {
1533
- const target = args[2];
1656
+ if (positionals[0] === "set") {
1657
+ const target = positionals[1];
1534
1658
  if (!target) {
1535
1659
  console.error(`❌ Usage: agentctl provider set <name> (see: agentctl providers)`);
1536
1660
  process.exit(2);
@@ -1550,11 +1674,6 @@ async function main() {
1550
1674
  process.exit(0);
1551
1675
  }
1552
1676
 
1553
- const { values } = parseArgs({
1554
- args: args.slice(1),
1555
- options: { json: { type: "boolean", short: "j" } },
1556
- allowPositionals: true,
1557
- });
1558
1677
  const { detectAvailableProviders, probeProvider } = await import("../src/provider-readiness.mjs");
1559
1678
  const probes = detectAvailableProviders();
1560
1679
  const active = config.provider || "jules";
@@ -1702,11 +1821,16 @@ async function main() {
1702
1821
  options: {
1703
1822
  force: { type: "boolean", short: "f" },
1704
1823
  "dry-run": { type: "boolean", short: "d" },
1824
+ json: { type: "boolean", short: "j" },
1705
1825
  },
1706
1826
  allowPositionals: true,
1707
1827
  });
1708
1828
 
1709
1829
  const res = bootstrapZeroTestRepo(root, { force: values.force });
1830
+ if (values.json) {
1831
+ console.log(JSON.stringify({ ok: res.bootstrapped, ...res }, null, 2));
1832
+ process.exit(res.bootstrapped ? 0 : 1);
1833
+ }
1710
1834
  if (res.bootstrapped) {
1711
1835
  console.log(`✅ Zero-test repo bootstrapped successfully!`);
1712
1836
  console.log(` Stack detected : ${res.stack}`);
@@ -1727,7 +1851,12 @@ async function main() {
1727
1851
  }
1728
1852
 
1729
1853
  case "review-repair": {
1730
- const fileArg = args[1];
1854
+ const { positionals: reviewPositionals } = parseArgs({
1855
+ args: args.slice(1),
1856
+ options: {},
1857
+ allowPositionals: true,
1858
+ });
1859
+ const fileArg = reviewPositionals[0];
1731
1860
  if (!fileArg || !existsSync(fileArg)) {
1732
1861
  console.error("Error: Please provide a valid JSON file containing PR review comments.");
1733
1862
  process.exit(1);
@@ -1745,9 +1874,33 @@ async function main() {
1745
1874
  }
1746
1875
 
1747
1876
  case "dashboard": {
1748
- const port = Number(args[1] || 4100);
1877
+ // Accepts both `agentctl dashboard 3000` and `agentctl dashboard --port
1878
+ // 3000`. The old code read args[1] raw, so the documented flag form fed
1879
+ // the literal string "--port" into Number() and died with an uncaught
1880
+ // ERR_SOCKET_BAD_PORT instead of a usable error.
1881
+ let dashboardOpts;
1882
+ try {
1883
+ const parsed = parseArgs({
1884
+ args: args.slice(1),
1885
+ options: {
1886
+ port: { type: "string" },
1887
+ host: { type: "string" },
1888
+ },
1889
+ allowPositionals: true,
1890
+ });
1891
+ dashboardOpts = { port: parsed.values.port, host: parsed.values.host, positional: parsed.positionals[0] };
1892
+ } catch (err) {
1893
+ console.error(`Error: Invalid port argument: ${err.message}`);
1894
+ process.exit(1);
1895
+ }
1896
+ const rawPort = dashboardOpts.port ?? dashboardOpts.positional ?? "4100";
1897
+ const port = Number(rawPort);
1898
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
1899
+ console.error(`Error: Invalid port "${rawPort}". Expected an integer between 1024 and 65535.`);
1900
+ process.exit(1);
1901
+ }
1749
1902
  const { startDashboardServer } = await import("../src/dashboard.mjs");
1750
- startDashboardServer(port, root);
1903
+ startDashboardServer(port, root, dashboardOpts.host || "127.0.0.1");
1751
1904
  break;
1752
1905
  }
1753
1906
 
@@ -1769,7 +1922,27 @@ async function main() {
1769
1922
  allowPositionals: true,
1770
1923
  });
1771
1924
 
1772
- const isInteractive = values.interactive !== false && !values["non-interactive"] && !values["no-interactive"] && !values.yes;
1925
+ if (values.tier) {
1926
+ const validTiers = ["free", "pro", "ultra", "enterprise"];
1927
+ const lowerTier = values.tier.toLowerCase();
1928
+ if (!validTiers.includes(lowerTier)) {
1929
+ if (["minimal", "standard", "max"].includes(lowerTier)) {
1930
+ console.error(`❌ Invalid tier "${values.tier}". Valid tiers: ${validTiers.join(", ")}. Did you mean '--profile ${values.tier}'?`);
1931
+ } else {
1932
+ console.error(`❌ Invalid tier "${values.tier}". Valid tiers: ${validTiers.join(", ")}.`);
1933
+ }
1934
+ process.exit(1);
1935
+ }
1936
+ }
1937
+
1938
+ const isInteractive =
1939
+ values.interactive === true ||
1940
+ (values.interactive !== false &&
1941
+ Boolean(process.stdin.isTTY) &&
1942
+ !process.env.CI &&
1943
+ !values["non-interactive"] &&
1944
+ !values["no-interactive"] &&
1945
+ !values.yes);
1773
1946
 
1774
1947
  const { runInitWizard } = await import("../src/wizard-init.mjs");
1775
1948
  const res = await runInitWizard(root, {
@@ -1828,7 +2001,15 @@ async function main() {
1828
2001
  // tree for files init had just written, which reads as the tool
1829
2002
  // catching the user cheating on step three.
1830
2003
  const rootContracts = ["SPEC.md", "CONSTRAINTS.md", "DESIGN.md"].filter((f) => existsSync(join(root, f)));
1831
- const filesToAdd = [".agent", "AGENTS.md", ...rootContracts, ".gitignore"].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)));
1832
2013
  console.log(`\n Commit the manifest and contracts so the gate does not read them as agent edits:`);
1833
2014
  console.log(` git add ${filesToAdd.join(" ")} && git commit -m "chore: add agent config"`);
1834
2015
 
@@ -1874,6 +2055,8 @@ async function main() {
1874
2055
  interactive: values.interactive,
1875
2056
  nonInteractive: values["non-interactive"] || values["no-interactive"],
1876
2057
  yes: values.yes,
2058
+ ci: Boolean(process.env.CI),
2059
+ isTTY: Boolean(process.stdin.isTTY),
1877
2060
  // A title and a prompt together state the whole task; there is
1878
2061
  // nothing left for the wizard to ask.
1879
2062
  fullySpecified: Boolean(values.title && resolvePromptInput(values, positionals)),
@@ -2027,14 +2210,32 @@ async function main() {
2027
2210
  }
2028
2211
 
2029
2212
  case "status": {
2213
+ const { values: statusValues } = parseArgs({
2214
+ args: args.slice(1),
2215
+ options: {
2216
+ json: { type: "boolean" },
2217
+ },
2218
+ allowPositionals: true,
2219
+ });
2030
2220
  const queueDir = getQueueDir(root);
2031
2221
  const files = existsSync(queueDir) ? readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir)) : [];
2222
+ const budget = budgetStatus(config, root);
2223
+ if (statusValues.json) {
2224
+ console.log(JSON.stringify({
2225
+ ok: true,
2226
+ version: VERSION,
2227
+ root,
2228
+ pendingTasks: files.length,
2229
+ activeLocks: lockStatus(root).length,
2230
+ budget,
2231
+ }, null, 2));
2232
+ process.exit(0);
2233
+ }
2032
2234
  console.log(`\n📊 agentctl Status Summary (v${VERSION})`);
2033
2235
  console.log(`--------------------------------------------------`);
2034
2236
  console.log(` Project Root : ${root}`);
2035
2237
  console.log(` Pending Tasks : ${files.length}`);
2036
2238
  console.log(` Active VFS Locks : ${lockStatus(root).length}`);
2037
- const budget = budgetStatus(config, root);
2038
2239
  console.log(` Daily Budget : ${formatBudgetLine(budget)}`);
2039
2240
  console.log(`--------------------------------------------------\n`);
2040
2241
  process.exit(0);
@@ -2042,7 +2243,7 @@ async function main() {
2042
2243
  }
2043
2244
 
2044
2245
  case "scan": {
2045
- const { scanCodebaseForTodos } = await import("../scripts/jules-scan-todos.mjs");
2246
+ const { scanCodebaseForTodos } = await import("../src/todo-scanner.mjs");
2046
2247
  const todos = scanCodebaseForTodos(root);
2047
2248
  console.log(`\n🔍 Scanned ${todos.length} TODO/FIXME annotation(s).`);
2048
2249
  for (const t of todos.slice(0, 10)) {
@@ -2230,8 +2431,7 @@ async function main() {
2230
2431
 
2231
2432
  case "resume": {
2232
2433
  const { createProvider } = await import("../src/provider.mjs");
2233
- const sessionId = args[1];
2234
- const { values } = parseArgs({
2434
+ const { values, positionals } = parseArgs({
2235
2435
  args: args.slice(1),
2236
2436
  options: {
2237
2437
  response: { type: "string", short: "r" },
@@ -2240,13 +2440,14 @@ async function main() {
2240
2440
  },
2241
2441
  allowPositionals: true,
2242
2442
  });
2443
+ const sessionId = positionals[0];
2243
2444
 
2244
2445
  if (!sessionId || sessionId.startsWith("-")) {
2245
2446
  console.error("Error: Session ID is required for agentctl resume <sessionId>.");
2246
2447
  process.exit(1);
2247
2448
  }
2248
2449
 
2249
- const responseText = values.response || args.slice(2).join(" ");
2450
+ const responseText = values.response || positionals.slice(1).join(" ");
2250
2451
  if (!responseText) {
2251
2452
  console.error("Error: --response text is required to resume warm session.");
2252
2453
  process.exit(1);
@@ -2273,7 +2474,6 @@ async function main() {
2273
2474
  case "plan": {
2274
2475
  const subAction = args[1];
2275
2476
  if (subAction === "approve") {
2276
- const sessionId = args[2];
2277
2477
  const { values, positionals } = parseArgs({
2278
2478
  args: args.slice(2),
2279
2479
  options: {
@@ -2282,7 +2482,7 @@ async function main() {
2282
2482
  },
2283
2483
  allowPositionals: true,
2284
2484
  });
2285
- const targetSessionId = sessionId && !sessionId.startsWith("-") ? sessionId : positionals?.[0];
2485
+ const targetSessionId = positionals?.[0];
2286
2486
  if (!targetSessionId) {
2287
2487
  console.error("Error: Session ID is required for agentctl plan approve <sessionId>.");
2288
2488
  process.exit(1);
@@ -2310,7 +2510,6 @@ async function main() {
2310
2510
  }
2311
2511
 
2312
2512
  case "approve": {
2313
- const sessionId = args[1];
2314
2513
  const { values, positionals } = parseArgs({
2315
2514
  args: args.slice(1),
2316
2515
  options: {
@@ -2319,7 +2518,7 @@ async function main() {
2319
2518
  },
2320
2519
  allowPositionals: true,
2321
2520
  });
2322
- const targetSessionId = sessionId && !sessionId.startsWith("-") ? sessionId : positionals?.[0];
2521
+ const targetSessionId = positionals?.[0];
2323
2522
  if (!targetSessionId) {
2324
2523
  console.error("Error: Session ID is required for agentctl approve <sessionId>.");
2325
2524
  process.exit(1);
@@ -2346,7 +2545,6 @@ async function main() {
2346
2545
  case "session": {
2347
2546
  const subAction = args[1];
2348
2547
  if (subAction === "get" || subAction === "status") {
2349
- const sessionId = args[2];
2350
2548
  const { values, positionals } = parseArgs({
2351
2549
  args: args.slice(2),
2352
2550
  options: {
@@ -2355,7 +2553,7 @@ async function main() {
2355
2553
  },
2356
2554
  allowPositionals: true,
2357
2555
  });
2358
- const targetSessionId = sessionId && !sessionId.startsWith("-") ? sessionId : positionals?.[0];
2556
+ const targetSessionId = positionals?.[0];
2359
2557
  if (!targetSessionId) {
2360
2558
  console.error("Error: Session ID is required for agentctl session get <sessionId>.");
2361
2559
  process.exit(1);
@@ -2377,7 +2575,103 @@ async function main() {
2377
2575
  process.exit(1);
2378
2576
  }
2379
2577
  }
2380
- console.error(`Error: Unknown session subcommand '${subAction}'. Use get.`);
2578
+
2579
+ if (subAction === "list" || subAction === "ls") {
2580
+ const { values } = parseArgs({
2581
+ args: args.slice(2),
2582
+ options: {
2583
+ limit: { type: "string", short: "l" },
2584
+ "page-size": { type: "string" },
2585
+ remote: { type: "boolean" },
2586
+ "dry-run": { type: "boolean", short: "d" },
2587
+ json: { type: "boolean", short: "j" },
2588
+ },
2589
+ allowPositionals: true,
2590
+ });
2591
+
2592
+ const limit = Number(values.limit || values["page-size"]) || 20;
2593
+ const { createProvider } = await import("../src/provider.mjs");
2594
+ const provider = createProvider(config.provider || "jules", config);
2595
+
2596
+ try {
2597
+ const res = await provider.listSessions({
2598
+ root,
2599
+ pageSize: limit,
2600
+ dryRun: values["dry-run"],
2601
+ });
2602
+ const remoteSessions = Array.isArray(res?.sessions) ? res.sessions : [];
2603
+
2604
+ const localSessions = [];
2605
+ try {
2606
+ const { getStateDir } = await import("../src/state.mjs");
2607
+ const { readdirSync, readFileSync, existsSync } = await import("node:fs");
2608
+ const stateDir = getStateDir(root);
2609
+ if (existsSync(stateDir)) {
2610
+ const files = readdirSync(stateDir).filter((f) => f.startsWith("ledger-") && f.endsWith(".jsonl"));
2611
+ for (const file of files) {
2612
+ const lines = readFileSync(join(stateDir, file), "utf-8").split("\n").filter(Boolean);
2613
+ for (const line of lines) {
2614
+ try {
2615
+ const parsed = JSON.parse(line);
2616
+ if (parsed.sessionId) {
2617
+ localSessions.push({
2618
+ id: parsed.sessionId,
2619
+ status: parsed.status || "DISPATCHED",
2620
+ timestamp: parsed.timestamp,
2621
+ title: parsed.task || parsed.title || "",
2622
+ source: "local-ledger",
2623
+ });
2624
+ }
2625
+ } catch (_) {}
2626
+ }
2627
+ }
2628
+ }
2629
+ } catch (_) {}
2630
+
2631
+ const sessionMap = new Map();
2632
+ for (const s of localSessions) {
2633
+ sessionMap.set(s.id, s);
2634
+ }
2635
+ for (const s of remoteSessions) {
2636
+ const sid = s.id || s.name;
2637
+ if (sid) {
2638
+ sessionMap.set(sid, {
2639
+ id: sid,
2640
+ status: s.state || s.status || "UNKNOWN",
2641
+ createTime: s.createTime || s.created_at,
2642
+ title: s.title || s.prompt?.slice(0, 60) || "",
2643
+ source: "remote",
2644
+ });
2645
+ }
2646
+ }
2647
+
2648
+ const combined = Array.from(sessionMap.values()).slice(0, limit);
2649
+
2650
+ if (values.json) {
2651
+ console.log(JSON.stringify({ ok: true, sessions: combined, count: combined.length, nextPageToken: res?.nextPageToken }, null, 2));
2652
+ } else {
2653
+ console.log(`\n📋 Jules Sessions List (${combined.length} found):`);
2654
+ console.log("--------------------------------------------------");
2655
+ if (combined.length === 0) {
2656
+ console.log(" No active or recent sessions found.");
2657
+ } else {
2658
+ for (const s of combined) {
2659
+ const sid = (s.id || "unknown").slice(0, 24).padEnd(26);
2660
+ const st = (s.status || "UNKNOWN").padEnd(14);
2661
+ const desc = s.title || s.source || "";
2662
+ console.log(` • ${sid} [${st}] ${desc}`);
2663
+ }
2664
+ }
2665
+ console.log("--------------------------------------------------\n");
2666
+ }
2667
+ process.exit(0);
2668
+ } catch (err) {
2669
+ console.error(`❌ Session List Failed: ${err.message}`);
2670
+ process.exit(1);
2671
+ }
2672
+ }
2673
+
2674
+ console.error(`Error: Unknown session subcommand '${subAction}'. Use get or list.`);
2381
2675
  process.exit(1);
2382
2676
  break;
2383
2677
  }
@@ -2567,7 +2861,7 @@ async function main() {
2567
2861
  args: args.slice(2),
2568
2862
  options: {
2569
2863
  dispatch: { type: "boolean" },
2570
- role: { type: "string", short: "r", default: "janitor" },
2864
+ role: { type: "string", short: "r", default: "hygiene" },
2571
2865
  "test-cmd": { type: "string", short: "t" },
2572
2866
  "dry-run": { type: "boolean", short: "d" },
2573
2867
  json: { type: "boolean", short: "j" },
@@ -2672,7 +2966,7 @@ async function main() {
2672
2966
 
2673
2967
  case "test-gen": {
2674
2968
  const { scaffoldTddTest, runTddCycle } = await import("../src/ops/tdd-generator.mjs");
2675
- const { values } = parseArgs({
2969
+ const { values, positionals } = parseArgs({
2676
2970
  args: args.slice(1),
2677
2971
  options: {
2678
2972
  title: { type: "string", short: "t" },
@@ -2684,8 +2978,8 @@ async function main() {
2684
2978
  allowPositionals: true,
2685
2979
  });
2686
2980
 
2687
- const title = values.title || args[1] || "feature-spec";
2688
- const specText = values.spec || args.slice(2).join(" ") || "TDD requirement specification.";
2981
+ const title = values.title || positionals[0] || "feature-spec";
2982
+ const specText = values.spec || positionals.slice(1).join(" ") || "TDD requirement specification.";
2689
2983
 
2690
2984
  try {
2691
2985
  if (values.run) {
@@ -2814,11 +3108,16 @@ async function main() {
2814
3108
  }
2815
3109
 
2816
3110
  case "learning": {
2817
- const subcmd = args[1];
3111
+ const { positionals: learningPositionals } = parseArgs({
3112
+ args: args.slice(1),
3113
+ options: {},
3114
+ allowPositionals: true,
3115
+ });
3116
+ const subcmd = learningPositionals[0];
2818
3117
  if (subcmd === "add") {
2819
3118
  const { recordLearning } = await import("../src/memory.mjs");
2820
- const trigger = args[2];
2821
- const solution = args[3];
3119
+ const trigger = learningPositionals[1];
3120
+ const solution = learningPositionals[2];
2822
3121
  if (!trigger || !solution) {
2823
3122
  console.error('Usage: agentctl learning add "<trigger/symptom>" "<solution>"');
2824
3123
  process.exit(1);