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.
- package/.agent/prompts/{Overseer.md → Auditor.md} +3 -3
- package/.agent/prompts/{Alchemist.md → Database.md} +1 -1
- package/.agent/prompts/Debugger.md +25 -0
- package/.agent/prompts/{Scribe.md → Docs.md} +8 -5
- package/.agent/prompts/{Spectator.md → E2E.md} +9 -6
- package/.agent/prompts/{Janitor.md → Hygiene.md} +2 -2
- package/.agent/prompts/{Bolt.md → Performance.md} +1 -1
- package/.agent/prompts/Resilience.md +20 -0
- package/.agent/prompts/Security.md +21 -0
- package/.agent/prompts/Testing.md +30 -0
- package/.agent/prompts/Types.md +19 -0
- package/.agent/rules/jules-protocol.md +5 -37
- package/CHANGELOG.md +197 -23
- package/JULES_RULES_TEMPLATE.md +45 -123
- package/LICENSE +1 -1
- package/README.md +143 -471
- package/bin/agentctl.mjs +437 -143
- package/bin/init.js +6 -3
- package/index.mjs +9 -6
- package/package.json +3 -5
- package/scripts/asset-integrity-check.mjs +1 -1
- package/scripts/doc-sync-check.mjs +32 -0
- package/scripts/generate-command-reference.mjs +39 -0
- package/scripts/jules-dispatch.mjs +12 -113
- package/scripts/jules-merge-swarm.mjs +8 -196
- package/scripts/jules-patch.mjs +7 -8
- package/scripts/jules-queue-runner.mjs +6 -8
- package/scripts/jules-scan-todos.mjs +10 -38
- package/scripts/jules-self-audit.mjs +8 -139
- package/scripts/jules-status.mjs +32 -38
- package/scripts/jules-webhook-receiver.mjs +1 -1
- package/src/assertions.mjs +5 -50
- package/src/bidi-guard.mjs +36 -0
- package/src/budget.mjs +3 -14
- package/src/config.mjs +2 -6
- package/src/dashboard.mjs +7 -9
- package/src/dispatch.mjs +212 -0
- package/src/engine.mjs +41 -17
- package/src/evidence.mjs +10 -41
- package/src/execution-envelope.mjs +13 -1
- package/src/flaky-ledger.mjs +1 -1
- package/src/fs-atomic.mjs +72 -0
- package/src/git.mjs +298 -27
- package/src/mcp.mjs +290 -3
- package/src/memory.mjs +5 -1
- package/src/merge-swarm.mjs +202 -0
- package/src/ops/cli-intent.mjs +1 -0
- package/src/ops/command-registry.mjs +796 -70
- package/src/ops/doctor-registry.mjs +134 -47
- package/src/ops/handover.mjs +3 -27
- package/src/ops/pr-harvest.mjs +1 -1
- package/src/prompt-guard.mjs +27 -3
- package/src/provider.mjs +51 -7
- package/src/remediation.mjs +2 -2
- package/src/review-repair.mjs +1 -2
- package/src/role-resolver.mjs +113 -3
- package/src/router.mjs +19 -11
- package/src/runtime-env.mjs +67 -0
- package/src/scaffold.mjs +3 -1
- package/src/scope-guard.mjs +249 -0
- package/src/secret-scanner.mjs +530 -0
- package/src/security.mjs +79 -2971
- package/src/self-audit.mjs +140 -0
- package/src/session-ops.mjs +30 -1
- package/src/stability.mjs +8 -1
- package/src/state.mjs +45 -0
- package/src/swarm.mjs +76 -0
- package/src/task-optimizer.mjs +1 -1
- package/src/telemetry.mjs +23 -0
- package/src/test-tamper-guard.mjs +2173 -0
- package/src/todo-scanner.mjs +129 -0
- package/src/web-templates.mjs +3 -3
- package/src/webhook.mjs +10 -3
- package/src/wizard-init.mjs +12 -20
- package/src/wizard-task.mjs +38 -11
- package/.agent/prompts/Sentinel.md +0 -18
- package/AGENTS.md +0 -135
- package/ROADMAP_V1.md +0 -257
- 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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
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(
|
|
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.
|
|
924
|
-
errorInput = positionals.
|
|
991
|
+
} else if (positionals.length > 0) {
|
|
992
|
+
errorInput = positionals.join(" ");
|
|
925
993
|
} else if (!process.stdin.isTTY) {
|
|
926
|
-
|
|
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]
|
|
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]
|
|
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
|
|
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 =
|
|
1337
|
-
const confirmed =
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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(
|
|
1439
|
-
const ownerPid = Number(
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
1538
|
-
const target =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("../
|
|
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
|
|
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 ||
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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: "
|
|
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 ||
|
|
2693
|
-
const specText = values.spec ||
|
|
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
|
|
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 =
|
|
2826
|
-
const solution =
|
|
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);
|