jules-orchestrator-kit 0.72.3 → 0.73.1

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 (79) 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 +5 -37
  13. package/CHANGELOG.md +197 -23
  14. package/JULES_RULES_TEMPLATE.md +45 -123
  15. package/LICENSE +1 -1
  16. package/README.md +143 -471
  17. package/bin/agentctl.mjs +437 -143
  18. package/bin/init.js +6 -3
  19. package/index.mjs +9 -6
  20. package/package.json +3 -5
  21. package/scripts/asset-integrity-check.mjs +1 -1
  22. package/scripts/doc-sync-check.mjs +32 -0
  23. package/scripts/generate-command-reference.mjs +39 -0
  24. package/scripts/jules-dispatch.mjs +12 -113
  25. package/scripts/jules-merge-swarm.mjs +8 -196
  26. package/scripts/jules-patch.mjs +7 -8
  27. package/scripts/jules-queue-runner.mjs +6 -8
  28. package/scripts/jules-scan-todos.mjs +10 -38
  29. package/scripts/jules-self-audit.mjs +8 -139
  30. package/scripts/jules-status.mjs +32 -38
  31. package/scripts/jules-webhook-receiver.mjs +1 -1
  32. package/src/assertions.mjs +5 -50
  33. package/src/bidi-guard.mjs +36 -0
  34. package/src/budget.mjs +3 -14
  35. package/src/config.mjs +2 -6
  36. package/src/dashboard.mjs +7 -9
  37. package/src/dispatch.mjs +212 -0
  38. package/src/engine.mjs +41 -17
  39. package/src/evidence.mjs +10 -41
  40. package/src/execution-envelope.mjs +13 -1
  41. package/src/flaky-ledger.mjs +1 -1
  42. package/src/fs-atomic.mjs +72 -0
  43. package/src/git.mjs +298 -27
  44. package/src/mcp.mjs +290 -3
  45. package/src/memory.mjs +5 -1
  46. package/src/merge-swarm.mjs +202 -0
  47. package/src/ops/cli-intent.mjs +1 -0
  48. package/src/ops/command-registry.mjs +796 -70
  49. package/src/ops/doctor-registry.mjs +134 -47
  50. package/src/ops/handover.mjs +3 -27
  51. package/src/ops/pr-harvest.mjs +1 -1
  52. package/src/prompt-guard.mjs +27 -3
  53. package/src/provider.mjs +51 -7
  54. package/src/remediation.mjs +2 -2
  55. package/src/review-repair.mjs +1 -2
  56. package/src/role-resolver.mjs +113 -3
  57. package/src/router.mjs +19 -11
  58. package/src/runtime-env.mjs +67 -0
  59. package/src/scaffold.mjs +3 -1
  60. package/src/scope-guard.mjs +249 -0
  61. package/src/secret-scanner.mjs +530 -0
  62. package/src/security.mjs +79 -2971
  63. package/src/self-audit.mjs +140 -0
  64. package/src/session-ops.mjs +30 -1
  65. package/src/stability.mjs +8 -1
  66. package/src/state.mjs +45 -0
  67. package/src/swarm.mjs +76 -0
  68. package/src/task-optimizer.mjs +1 -1
  69. package/src/telemetry.mjs +23 -0
  70. package/src/test-tamper-guard.mjs +2173 -0
  71. package/src/todo-scanner.mjs +129 -0
  72. package/src/web-templates.mjs +3 -3
  73. package/src/webhook.mjs +10 -3
  74. package/src/wizard-init.mjs +12 -20
  75. package/src/wizard-task.mjs +38 -11
  76. package/.agent/prompts/Sentinel.md +0 -18
  77. package/AGENTS.md +0 -135
  78. package/ROADMAP_V1.md +0 -257
  79. 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,6 +313,8 @@ 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" },
@@ -301,12 +326,30 @@ async function main() {
301
326
  allowPositionals: true,
302
327
  });
303
328
 
304
- const promptContent = resolvePromptInput(values, positionals);
329
+ const { content: promptContent, sourceFile } = resolvePromptDetails(values, positionals, root);
305
330
  if (!promptContent) {
306
- 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/.");
307
332
  process.exit(1);
308
333
  }
309
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
+
310
353
  // `task create --role` has always rejected a role it cannot resolve;
311
354
  // `dispatch --role` used to drop it without a word and hand the work to a
312
355
  // generic agent, so the two commands disagreed about what the same flag
@@ -316,13 +359,14 @@ async function main() {
316
359
  const { resolveRolePrompt } = await import("../src/role-resolver.mjs");
317
360
  if (!resolveRolePrompt(root, values.role, { config })) {
318
361
  console.error(
319
- `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).`
320
363
  );
321
364
  console.error(` Run 'agentctl init' to scaffold the shipped role prompts.`);
322
365
  process.exit(1);
323
366
  }
324
367
  }
325
368
 
369
+ const autoApprove = Boolean(values["auto-approve-plans"] || values["auto-approve"]);
326
370
  const task = {
327
371
  title: values.title || "CLI Dispatch Task",
328
372
  prompt: promptContent,
@@ -332,7 +376,7 @@ async function main() {
332
376
  branch: values.branch,
333
377
  repoless: values.repoless,
334
378
  autoPr: values["auto-pr"],
335
- requirePlanApproval: values["require-plan-approval"],
379
+ requirePlanApproval: autoApprove ? false : values["require-plan-approval"],
336
380
  checkPremise: values["check-premise"] || values.idempotent,
337
381
  author: values.author,
338
382
  verifyCmd: values["verify-cmd"] || values.verify,
@@ -377,6 +421,19 @@ async function main() {
377
421
  }
378
422
  }
379
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
+ }
380
437
  process.exit(0);
381
438
  } catch (err) {
382
439
  const exitCode = typeof err.code === "number" ? err.code : 1;
@@ -821,6 +878,10 @@ async function main() {
821
878
  min: { type: "string", short: "m" },
822
879
  "min-pass-rate": { type: "string" },
823
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" },
824
885
  json: { type: "boolean", short: "j" },
825
886
  },
826
887
  allowPositionals: true,
@@ -829,8 +890,10 @@ async function main() {
829
890
  const root = resolveRoot();
830
891
  const repeat = values.repeat ? parseInt(values.repeat, 10) : (values.iterations ? parseInt(values.iterations, 10) : 5);
831
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;
832
895
 
833
- const report = runStabilityProbe(values.cmd, { root, repeat, minPassRate });
896
+ const report = runStabilityProbe(targetCmd, { root, repeat, minPassRate, record });
834
897
 
835
898
  if (values.json) {
836
899
  console.log(JSON.stringify(report, null, 2));
@@ -849,6 +912,11 @@ async function main() {
849
912
  }
850
913
  }
851
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
+
852
920
  console.log(`------------------------------------------------------------------`);
853
921
  console.log(`Overall Result: ${report.ok ? "APPROVED (Deterministic 100% Pass — Exit 0)" : "REJECTED (Flaky / Intermittent Failures — Exit 1)"}\n`);
854
922
  }
@@ -920,10 +988,11 @@ async function main() {
920
988
  errorInput = readFileSync(values.file, "utf-8");
921
989
  } else if (values.input) {
922
990
  errorInput = values.input;
923
- } else if (positionals.slice(1).length > 0) {
924
- errorInput = positionals.slice(1).join(" ");
991
+ } else if (positionals.length > 0) {
992
+ errorInput = positionals.join(" ");
925
993
  } else if (!process.stdin.isTTY) {
926
- errorInput = readFileSync(0, "utf-8");
994
+ const { text } = await import("node:stream/consumers");
995
+ errorInput = await text(process.stdin);
927
996
  }
928
997
 
929
998
  if (!errorInput.trim()) {
@@ -939,6 +1008,7 @@ async function main() {
939
1008
  prompt: `Fix the following test/build failure:\n\n${cleanTrace.slice(0, 4000)}`,
940
1009
  title: "Automated Failure Repair",
941
1010
  verifyCmd: values.cmd || "npm test",
1011
+ allowHighEntropy: true,
942
1012
  });
943
1013
 
944
1014
  if (values.json) {
@@ -982,7 +1052,7 @@ async function main() {
982
1052
  allowPositionals: true,
983
1053
  });
984
1054
 
985
- const sessionId = values.session || positionals[0] || positionals[1];
1055
+ const sessionId = values.session || positionals[0];
986
1056
  if (!sessionId) {
987
1057
  console.error("❌ Error: Missing required session ID. Usage: agentctl patch <session_id> [--apply] [--save <path>]");
988
1058
  process.exit(1);
@@ -1033,7 +1103,7 @@ async function main() {
1033
1103
  allowPositionals: true,
1034
1104
  });
1035
1105
 
1036
- const sessionId = values.session || positionals[0] || positionals[1];
1106
+ const sessionId = values.session || positionals[0];
1037
1107
  if (!sessionId) {
1038
1108
  console.error("❌ Error: Missing required session ID. Usage: agentctl retry <session_id> [--role <role>]");
1039
1109
  process.exit(1);
@@ -1315,7 +1385,21 @@ async function main() {
1315
1385
  }
1316
1386
 
1317
1387
  case "budget": {
1318
- 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];
1319
1403
  const b = budgetStatus(config, root);
1320
1404
 
1321
1405
  if (action === "reset") {
@@ -1333,11 +1417,11 @@ async function main() {
1333
1417
  console.error("Accepted: --dry-run, --yes/-y, --all. Nothing was released.");
1334
1418
  process.exit(2);
1335
1419
  }
1336
- const dryRun = args.includes("--dry-run");
1337
- const confirmed = args.includes("--yes") || args.includes("-y");
1420
+ const dryRun = Boolean(budgetValues["dry-run"]);
1421
+ const confirmed = Boolean(budgetValues.yes);
1338
1422
  // Committed reservations reached the provider, so releasing them makes
1339
1423
  // the local count understate real usage. That has to be deliberate.
1340
- const includeCommitted = args.includes("--all");
1424
+ const includeCommitted = Boolean(budgetValues.all);
1341
1425
  if (!dryRun && !confirmed) {
1342
1426
  const open = listOpenReservations(root);
1343
1427
  const committed = open.filter((r) => r.committed).length;
@@ -1364,7 +1448,7 @@ async function main() {
1364
1448
  process.exit(0);
1365
1449
  }
1366
1450
 
1367
- if (args.includes("--by-user") || args.includes("-u")) {
1451
+ if (budgetValues["by-user"]) {
1368
1452
  console.log("📊 Task Budget Attribution (Rolling 24h Window)");
1369
1453
  console.log(`Daily Limit : ${b.limit} Tasks | Used: ${b.used} | Remaining: ${b.remaining}\n`);
1370
1454
  const users = Object.entries(b.byUser || {});
@@ -1385,7 +1469,7 @@ async function main() {
1385
1469
  process.exit(0);
1386
1470
  }
1387
1471
 
1388
- if (args.includes("--json")) {
1472
+ if (budgetValues.json) {
1389
1473
  console.log(JSON.stringify({ ok: true, budget: { ...b, scope: "this-repository" } }, null, 2));
1390
1474
  process.exit(0);
1391
1475
  }
@@ -1411,7 +1495,16 @@ async function main() {
1411
1495
  }
1412
1496
 
1413
1497
  case "lock": {
1414
- 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];
1415
1508
  if (action === "acquire") {
1416
1509
  // A lock taken from the command line outlives the command. The process
1417
1510
  // that runs `lock acquire` writes the record and exits; the agent it
@@ -1422,27 +1515,26 @@ async function main() {
1422
1515
  // `--pid` is the escape hatch for a caller that *does* have a durable
1423
1516
  // process to point at: the lock then releases itself when that process
1424
1517
  // dies, exactly as an in-process acquire does.
1425
- const flagIdx = args.findIndex((a, i) => i >= 2 && a.startsWith("--"));
1426
- const positional = flagIdx === -1 ? args.slice(2) : args.slice(2, flagIdx);
1427
- const { values } = parseArgs({
1428
- args: flagIdx === -1 ? [] : args.slice(flagIdx),
1429
- options: {
1430
- ttl: { type: "string" },
1431
- pid: { type: "string" },
1432
- },
1433
- allowPositionals: false,
1434
- });
1435
- const agent = positional[0] || "agent";
1436
- 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];
1437
1525
  const filePaths = positional.slice(2);
1438
- const ttlMinutes = Number(values.ttl);
1439
- const ownerPid = Number(values.pid);
1526
+ const ttlMinutes = Number(lockValues.ttl);
1527
+ const ownerPid = Number(lockValues.pid);
1440
1528
  const bindsToProcess = Number.isInteger(ownerPid) && ownerPid > 0;
1441
1529
  const res = acquireLock(agent, taskId, filePaths, root, {
1442
1530
  lease: !bindsToProcess,
1443
1531
  ownerPid: bindsToProcess ? ownerPid : undefined,
1444
1532
  ttlMs: Number.isFinite(ttlMinutes) && ttlMinutes > 0 ? ttlMinutes * 60_000 : undefined,
1445
1533
  });
1534
+ if (lockValues.json) {
1535
+ console.log(JSON.stringify(res, null, 2));
1536
+ process.exit(res.ok ? 0 : 1);
1537
+ }
1446
1538
  if (res.ok) {
1447
1539
  const held = filePaths.length ? ` (${filePaths.length} path${filePaths.length === 1 ? "" : "s"})` : "";
1448
1540
  const until = bindsToProcess ? `bound to pid ${ownerPid}` : `expires ${new Date(Date.now() + (Number.isFinite(ttlMinutes) && ttlMinutes > 0 ? ttlMinutes : 120) * 60_000).toISOString()}`;
@@ -1456,8 +1548,16 @@ async function main() {
1456
1548
  process.exit(1);
1457
1549
  }
1458
1550
  } else if (action === "release") {
1459
- 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
+ }
1460
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
+ }
1461
1561
  if (ok) {
1462
1562
  console.log(`✅ Released lock for ${taskId}`);
1463
1563
  } else {
@@ -1466,7 +1566,21 @@ async function main() {
1466
1566
  }
1467
1567
  } else {
1468
1568
  const locks = lockStatus(root);
1469
- 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
+ }
1470
1584
  }
1471
1585
  process.exit(0);
1472
1586
  break;
@@ -1531,11 +1645,16 @@ async function main() {
1531
1645
 
1532
1646
  case "provider":
1533
1647
  case "providers": {
1648
+ const { values, positionals } = parseArgs({
1649
+ args: args.slice(1),
1650
+ options: { json: { type: "boolean", short: "j" } },
1651
+ allowPositionals: true,
1652
+ });
1534
1653
  // `agentctl providers` used to tell the operator to run
1535
1654
  // `agentctl init --provider <name>` to switch — which restarts the whole
1536
1655
  // onboarding wizard, plan question included, to change one line.
1537
- if (args[1] === "set") {
1538
- const target = args[2];
1656
+ if (positionals[0] === "set") {
1657
+ const target = positionals[1];
1539
1658
  if (!target) {
1540
1659
  console.error(`❌ Usage: agentctl provider set <name> (see: agentctl providers)`);
1541
1660
  process.exit(2);
@@ -1555,11 +1674,6 @@ async function main() {
1555
1674
  process.exit(0);
1556
1675
  }
1557
1676
 
1558
- const { values } = parseArgs({
1559
- args: args.slice(1),
1560
- options: { json: { type: "boolean", short: "j" } },
1561
- allowPositionals: true,
1562
- });
1563
1677
  const { detectAvailableProviders, probeProvider } = await import("../src/provider-readiness.mjs");
1564
1678
  const probes = detectAvailableProviders();
1565
1679
  const active = config.provider || "jules";
@@ -1707,11 +1821,16 @@ async function main() {
1707
1821
  options: {
1708
1822
  force: { type: "boolean", short: "f" },
1709
1823
  "dry-run": { type: "boolean", short: "d" },
1824
+ json: { type: "boolean", short: "j" },
1710
1825
  },
1711
1826
  allowPositionals: true,
1712
1827
  });
1713
1828
 
1714
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
+ }
1715
1834
  if (res.bootstrapped) {
1716
1835
  console.log(`✅ Zero-test repo bootstrapped successfully!`);
1717
1836
  console.log(` Stack detected : ${res.stack}`);
@@ -1732,7 +1851,12 @@ async function main() {
1732
1851
  }
1733
1852
 
1734
1853
  case "review-repair": {
1735
- 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];
1736
1860
  if (!fileArg || !existsSync(fileArg)) {
1737
1861
  console.error("Error: Please provide a valid JSON file containing PR review comments.");
1738
1862
  process.exit(1);
@@ -1750,9 +1874,33 @@ async function main() {
1750
1874
  }
1751
1875
 
1752
1876
  case "dashboard": {
1753
- 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
+ }
1754
1902
  const { startDashboardServer } = await import("../src/dashboard.mjs");
1755
- startDashboardServer(port, root);
1903
+ startDashboardServer(port, root, dashboardOpts.host || "127.0.0.1");
1756
1904
  break;
1757
1905
  }
1758
1906
 
@@ -1774,7 +1922,27 @@ async function main() {
1774
1922
  allowPositionals: true,
1775
1923
  });
1776
1924
 
1777
- 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);
1778
1946
 
1779
1947
  const { runInitWizard } = await import("../src/wizard-init.mjs");
1780
1948
  const res = await runInitWizard(root, {
@@ -1833,7 +2001,15 @@ async function main() {
1833
2001
  // tree for files init had just written, which reads as the tool
1834
2002
  // catching the user cheating on step three.
1835
2003
  const rootContracts = ["SPEC.md", "CONSTRAINTS.md", "DESIGN.md"].filter((f) => existsSync(join(root, f)));
1836
- 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)));
1837
2013
  console.log(`\n Commit the manifest and contracts so the gate does not read them as agent edits:`);
1838
2014
  console.log(` git add ${filesToAdd.join(" ")} && git commit -m "chore: add agent config"`);
1839
2015
 
@@ -1879,6 +2055,8 @@ async function main() {
1879
2055
  interactive: values.interactive,
1880
2056
  nonInteractive: values["non-interactive"] || values["no-interactive"],
1881
2057
  yes: values.yes,
2058
+ ci: Boolean(process.env.CI),
2059
+ isTTY: Boolean(process.stdin.isTTY),
1882
2060
  // A title and a prompt together state the whole task; there is
1883
2061
  // nothing left for the wizard to ask.
1884
2062
  fullySpecified: Boolean(values.title && resolvePromptInput(values, positionals)),
@@ -2032,14 +2210,32 @@ async function main() {
2032
2210
  }
2033
2211
 
2034
2212
  case "status": {
2213
+ const { values: statusValues } = parseArgs({
2214
+ args: args.slice(1),
2215
+ options: {
2216
+ json: { type: "boolean" },
2217
+ },
2218
+ allowPositionals: true,
2219
+ });
2035
2220
  const queueDir = getQueueDir(root);
2036
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
+ }
2037
2234
  console.log(`\n📊 agentctl Status Summary (v${VERSION})`);
2038
2235
  console.log(`--------------------------------------------------`);
2039
2236
  console.log(` Project Root : ${root}`);
2040
2237
  console.log(` Pending Tasks : ${files.length}`);
2041
2238
  console.log(` Active VFS Locks : ${lockStatus(root).length}`);
2042
- const budget = budgetStatus(config, root);
2043
2239
  console.log(` Daily Budget : ${formatBudgetLine(budget)}`);
2044
2240
  console.log(`--------------------------------------------------\n`);
2045
2241
  process.exit(0);
@@ -2047,7 +2243,7 @@ async function main() {
2047
2243
  }
2048
2244
 
2049
2245
  case "scan": {
2050
- const { scanCodebaseForTodos } = await import("../scripts/jules-scan-todos.mjs");
2246
+ const { scanCodebaseForTodos } = await import("../src/todo-scanner.mjs");
2051
2247
  const todos = scanCodebaseForTodos(root);
2052
2248
  console.log(`\n🔍 Scanned ${todos.length} TODO/FIXME annotation(s).`);
2053
2249
  for (const t of todos.slice(0, 10)) {
@@ -2235,8 +2431,7 @@ async function main() {
2235
2431
 
2236
2432
  case "resume": {
2237
2433
  const { createProvider } = await import("../src/provider.mjs");
2238
- const sessionId = args[1];
2239
- const { values } = parseArgs({
2434
+ const { values, positionals } = parseArgs({
2240
2435
  args: args.slice(1),
2241
2436
  options: {
2242
2437
  response: { type: "string", short: "r" },
@@ -2245,13 +2440,14 @@ async function main() {
2245
2440
  },
2246
2441
  allowPositionals: true,
2247
2442
  });
2443
+ const sessionId = positionals[0];
2248
2444
 
2249
2445
  if (!sessionId || sessionId.startsWith("-")) {
2250
2446
  console.error("Error: Session ID is required for agentctl resume <sessionId>.");
2251
2447
  process.exit(1);
2252
2448
  }
2253
2449
 
2254
- const responseText = values.response || args.slice(2).join(" ");
2450
+ const responseText = values.response || positionals.slice(1).join(" ");
2255
2451
  if (!responseText) {
2256
2452
  console.error("Error: --response text is required to resume warm session.");
2257
2453
  process.exit(1);
@@ -2278,7 +2474,6 @@ async function main() {
2278
2474
  case "plan": {
2279
2475
  const subAction = args[1];
2280
2476
  if (subAction === "approve") {
2281
- const sessionId = args[2];
2282
2477
  const { values, positionals } = parseArgs({
2283
2478
  args: args.slice(2),
2284
2479
  options: {
@@ -2287,7 +2482,7 @@ async function main() {
2287
2482
  },
2288
2483
  allowPositionals: true,
2289
2484
  });
2290
- const targetSessionId = sessionId && !sessionId.startsWith("-") ? sessionId : positionals?.[0];
2485
+ const targetSessionId = positionals?.[0];
2291
2486
  if (!targetSessionId) {
2292
2487
  console.error("Error: Session ID is required for agentctl plan approve <sessionId>.");
2293
2488
  process.exit(1);
@@ -2315,7 +2510,6 @@ async function main() {
2315
2510
  }
2316
2511
 
2317
2512
  case "approve": {
2318
- const sessionId = args[1];
2319
2513
  const { values, positionals } = parseArgs({
2320
2514
  args: args.slice(1),
2321
2515
  options: {
@@ -2324,7 +2518,7 @@ async function main() {
2324
2518
  },
2325
2519
  allowPositionals: true,
2326
2520
  });
2327
- const targetSessionId = sessionId && !sessionId.startsWith("-") ? sessionId : positionals?.[0];
2521
+ const targetSessionId = positionals?.[0];
2328
2522
  if (!targetSessionId) {
2329
2523
  console.error("Error: Session ID is required for agentctl approve <sessionId>.");
2330
2524
  process.exit(1);
@@ -2351,7 +2545,6 @@ async function main() {
2351
2545
  case "session": {
2352
2546
  const subAction = args[1];
2353
2547
  if (subAction === "get" || subAction === "status") {
2354
- const sessionId = args[2];
2355
2548
  const { values, positionals } = parseArgs({
2356
2549
  args: args.slice(2),
2357
2550
  options: {
@@ -2360,7 +2553,7 @@ async function main() {
2360
2553
  },
2361
2554
  allowPositionals: true,
2362
2555
  });
2363
- const targetSessionId = sessionId && !sessionId.startsWith("-") ? sessionId : positionals?.[0];
2556
+ const targetSessionId = positionals?.[0];
2364
2557
  if (!targetSessionId) {
2365
2558
  console.error("Error: Session ID is required for agentctl session get <sessionId>.");
2366
2559
  process.exit(1);
@@ -2382,7 +2575,103 @@ async function main() {
2382
2575
  process.exit(1);
2383
2576
  }
2384
2577
  }
2385
- 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.`);
2386
2675
  process.exit(1);
2387
2676
  break;
2388
2677
  }
@@ -2572,7 +2861,7 @@ async function main() {
2572
2861
  args: args.slice(2),
2573
2862
  options: {
2574
2863
  dispatch: { type: "boolean" },
2575
- role: { type: "string", short: "r", default: "janitor" },
2864
+ role: { type: "string", short: "r", default: "hygiene" },
2576
2865
  "test-cmd": { type: "string", short: "t" },
2577
2866
  "dry-run": { type: "boolean", short: "d" },
2578
2867
  json: { type: "boolean", short: "j" },
@@ -2677,7 +2966,7 @@ async function main() {
2677
2966
 
2678
2967
  case "test-gen": {
2679
2968
  const { scaffoldTddTest, runTddCycle } = await import("../src/ops/tdd-generator.mjs");
2680
- const { values } = parseArgs({
2969
+ const { values, positionals } = parseArgs({
2681
2970
  args: args.slice(1),
2682
2971
  options: {
2683
2972
  title: { type: "string", short: "t" },
@@ -2689,8 +2978,8 @@ async function main() {
2689
2978
  allowPositionals: true,
2690
2979
  });
2691
2980
 
2692
- const title = values.title || args[1] || "feature-spec";
2693
- 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.";
2694
2983
 
2695
2984
  try {
2696
2985
  if (values.run) {
@@ -2819,11 +3108,16 @@ async function main() {
2819
3108
  }
2820
3109
 
2821
3110
  case "learning": {
2822
- 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];
2823
3117
  if (subcmd === "add") {
2824
3118
  const { recordLearning } = await import("../src/memory.mjs");
2825
- const trigger = args[2];
2826
- const solution = args[3];
3119
+ const trigger = learningPositionals[1];
3120
+ const solution = learningPositionals[2];
2827
3121
  if (!trigger || !solution) {
2828
3122
  console.error('Usage: agentctl learning add "<trigger/symptom>" "<solution>"');
2829
3123
  process.exit(1);