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