jules-orchestrator-kit 0.74.0 → 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 +13 -0
- package/README.md +2 -2
- package/bin/agentctl.mjs +372 -20
- package/package.json +1 -1
- package/scripts/ci-scope-guard.mjs +30 -7
- package/src/bidi-guard.mjs +190 -15
- package/src/config.mjs +40 -1
- package/src/engine.mjs +184 -47
- package/src/git.mjs +15 -0
- package/src/migrate.mjs +493 -0
- package/src/ops/command-registry.mjs +91 -4
- 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/scaffold.mjs +49 -1
- 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 +11 -0
- package/src/wizard-task.mjs +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.75.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Interactive Terminal Navigation Hub (`src/ops/tui-menu.mjs`)**: Added zero-external-dependency terminal UI hub accessible via `agentctl menu`, `tui`, `ui`, or `-i`, using Node.js ESM built-ins (`readline`, ANSI cursor escape sequences). Supports arrow-key navigation, hotkey selection, command preview, contextual execution, and direct handoff to interactive wizards.
|
|
14
|
+
- **Fuzzy Typo Correction & Suggestions (`bin/agentctl.mjs`)**: Implemented Levenshtein-distance suggestion engine for unrecognized CLI commands, surfacing closest matches (e.g. `agentctl dsipatch` suggests `dispatch`).
|
|
15
|
+
- **Actionable Scope Diagnostics & Protected Scope Flag (`src/wizard-task.mjs`)**: Added `--allow-protected` flag to `agentctl task create` alongside detailed diagnostic breakdowns of which paths are protected (e.g. `package.json`, `.agent/protected-paths.json`) when task objectives touch sensitive repository infrastructure.
|
|
16
|
+
- **Task Payload Preview on Dry-Run (`bin/agentctl.mjs`, `src/engine.mjs`)**: `agentctl dispatch --dry-run` now prints formatted task payloads, verification commands, and risk tier assignments before execution.
|
|
17
|
+
- **Maintainer Approval Path for Test Changes (`.github/workflows/agent-scope-guard.yml`, `scripts/ci-scope-guard.mjs`)**: Established narrow maintainer approval protocol (`allow-p:<HEAD_SHA>`, `allow-e:<HEAD_SHA>`) allowing verified modifications to test expectations or protected paths under explicit commit-bound review.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Goal Evidence Enforcement (`src/engine.mjs`)**: Disallowed silent early termination on goal tasks lacking explicit verification evidence.
|
|
21
|
+
- **Test Environment Variable Bleed (`src/engine.mjs`)**: Sanitized test execution environment to prevent PR label bypass tokens from leaking into child test runs during gate evaluation.
|
|
22
|
+
|
|
10
23
|
## [0.74.0] - 2026-09-19
|
|
11
24
|
|
|
12
25
|
### Added
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ You can also run local checks without connecting an agent provider.
|
|
|
17
17
|
`agentctl repair` explicitly for repair workflows. Provider output and passing
|
|
18
18
|
checks still need review before merging.
|
|
19
19
|
|
|
20
|
-
The current release is **v0.
|
|
20
|
+
The current release is **v0.75.0**, a pre-1.0 release. A long-term stability policy is a
|
|
21
21
|
[v1.0 goal](https://github.com/FullThrottle83/jules-orchestrator-kit/blob/main/ROADMAP_V1.md).
|
|
22
22
|
|
|
23
23
|
## Quickstart
|
|
@@ -174,7 +174,7 @@ npm run package-integrity
|
|
|
174
174
|
npm run guard-reach
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
The recorded baseline is **
|
|
177
|
+
The recorded baseline is **1650 unit tests across 210 suites**. Doc-sync compares
|
|
178
178
|
that count with an actual run. Counts do not establish correctness for every
|
|
179
179
|
provider or project. See
|
|
180
180
|
[contributing](https://github.com/FullThrottle83/jules-orchestrator-kit/blob/main/CONTRIBUTING.md)
|
package/bin/agentctl.mjs
CHANGED
|
@@ -5,10 +5,11 @@ import { readFileSync, existsSync, readdirSync, statSync, renameSync, mkdirSync
|
|
|
5
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";
|
|
@@ -89,7 +90,8 @@ export function printHelp() {
|
|
|
89
90
|
lines.push(" --prompt-file, -f Read the prompt from a file (-f is --fix on task optimize)");
|
|
90
91
|
lines.push(" --role, -r Specify specialist agent role (auditor | performance | security | hygiene | resilience | types | debugger | testing | e2e | database | docs | a11y)");
|
|
91
92
|
lines.push(" --tier Force routing tier when router.enabled (fast | complex) — see .agent/config.yml router:");
|
|
92
|
-
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");
|
|
93
95
|
lines.push(" --dag Execute queue tasks via DAG dependency resolution");
|
|
94
96
|
lines.push(" --dry-run, -d Simulate action without making API calls or modifying git");
|
|
95
97
|
lines.push(" --mode, -m Gate evaluation mode (working-tree | committed | staged)");
|
|
@@ -268,6 +270,11 @@ async function main() {
|
|
|
268
270
|
// tool; a newcomer cannot tell which entry is step one, and guessing wrong
|
|
269
271
|
// costs them a confusing failure instead of a hint.
|
|
270
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
|
+
}
|
|
271
278
|
const { resolveNextStep, renderNextStep } = await import("../src/ops/next-step.mjs");
|
|
272
279
|
const cwd = process.cwd();
|
|
273
280
|
let budgetLine = "";
|
|
@@ -308,9 +315,14 @@ async function main() {
|
|
|
308
315
|
// Init is a configuration boundary, not an operational maintenance command.
|
|
309
316
|
// In particular, --dry-run must not create .agent/ indirectly by reaping
|
|
310
317
|
// journal intents or mutex directories before init itself gets control.
|
|
311
|
-
|
|
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") {
|
|
312
321
|
reapOrphanedIntents(root);
|
|
313
322
|
reapStaleMutexDirs(root);
|
|
323
|
+
if (command !== "lock") {
|
|
324
|
+
reapStaleLocks(root);
|
|
325
|
+
}
|
|
314
326
|
}
|
|
315
327
|
const config = loadConfig(root);
|
|
316
328
|
|
|
@@ -332,6 +344,7 @@ async function main() {
|
|
|
332
344
|
"auto-approve-plans": { type: "boolean" },
|
|
333
345
|
"auto-approve": { type: "boolean" },
|
|
334
346
|
"check-premise": { type: "boolean" },
|
|
347
|
+
"goal-check": { type: "string" },
|
|
335
348
|
idempotent: { type: "boolean" },
|
|
336
349
|
author: { type: "string" },
|
|
337
350
|
"verify-cmd": { type: "string", short: "v" },
|
|
@@ -394,6 +407,7 @@ async function main() {
|
|
|
394
407
|
autoPr: values["auto-pr"],
|
|
395
408
|
requirePlanApproval: autoApprove ? false : values["require-plan-approval"],
|
|
396
409
|
checkPremise: values["check-premise"] || values.idempotent,
|
|
410
|
+
goalCheck: values["goal-check"],
|
|
397
411
|
author: values.author,
|
|
398
412
|
verifyCmd: values["verify-cmd"] || values.verify,
|
|
399
413
|
};
|
|
@@ -413,7 +427,7 @@ async function main() {
|
|
|
413
427
|
} else {
|
|
414
428
|
if (session.status === "ALREADY_SATISFIED" || session.skipped) {
|
|
415
429
|
console.log(`\n⚡ Task Already Satisfied (skipped dispatch):`);
|
|
416
|
-
console.log(` Reason: ${session.reason || "
|
|
430
|
+
console.log(` Reason: ${session.reason || "Explicit goal check passed on a clean revision."}`);
|
|
417
431
|
} else if (values["dry-run"]) {
|
|
418
432
|
// The dry run reached the provider adapter and stopped short of the
|
|
419
433
|
// call. Printing the same "Dispatched Successfully!" banner as a
|
|
@@ -424,9 +438,16 @@ async function main() {
|
|
|
424
438
|
console.log(` Title : ${task.title}`);
|
|
425
439
|
console.log(` Provider : ${session.provider || config.provider || "jules"}`);
|
|
426
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}`);
|
|
427
443
|
if (session._routeTier) {
|
|
428
444
|
console.log(` Router Tier : ${session._routeTier} (${session._routeReason || "n/a"})`);
|
|
429
445
|
}
|
|
446
|
+
if (task.prompt) {
|
|
447
|
+
console.log(`\n--- Task Payload Preview ---`);
|
|
448
|
+
console.log(String(task.prompt).trim());
|
|
449
|
+
console.log(`----------------------------`);
|
|
450
|
+
}
|
|
430
451
|
console.log(`\n Re-run without --dry-run to dispatch for real.`);
|
|
431
452
|
} else {
|
|
432
453
|
console.log(`\n✅ Task Dispatched Successfully!`);
|
|
@@ -690,9 +711,14 @@ async function main() {
|
|
|
690
711
|
console.log(` • Or set it by hand: verify.test in ${config._file || ".agent/config.yml"}`);
|
|
691
712
|
console.log(` • Scope- and secret-scanning only, on purpose? Set verify.required: false there.\n`);
|
|
692
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);
|
|
693
717
|
console.log(`💡 Remediation Hint (Exit ${res.code} Verification Failed):`);
|
|
694
|
-
|
|
695
|
-
|
|
718
|
+
for (const line of diagnosis.lines) {
|
|
719
|
+
console.log(` • ${line}`);
|
|
720
|
+
}
|
|
721
|
+
console.log("");
|
|
696
722
|
} else if (res.code === 3) {
|
|
697
723
|
// The same violation has two very different causes. Right after
|
|
698
724
|
// `init`, every offending path is a file the tool itself just wrote
|
|
@@ -1038,17 +1064,53 @@ async function main() {
|
|
|
1038
1064
|
}
|
|
1039
1065
|
process.exit(0);
|
|
1040
1066
|
} else {
|
|
1041
|
-
|
|
1067
|
+
if (!values.json) {
|
|
1068
|
+
console.log(`\n🔧 Dispatching OODA Repair Loop for captured failure trace...`);
|
|
1069
|
+
}
|
|
1042
1070
|
const repairRes = await repair(
|
|
1043
1071
|
{ stderr: cleanTrace, command: values.cmd || "verify" },
|
|
1044
1072
|
{ root, dryRun: values["dry-run"], author: values.author }
|
|
1045
1073
|
);
|
|
1046
1074
|
|
|
1047
1075
|
if (values.json) {
|
|
1048
|
-
|
|
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;
|
|
1049
1081
|
} else {
|
|
1050
1082
|
console.log(`------------------------------------------------------------------`);
|
|
1051
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
|
+
}
|
|
1052
1114
|
}
|
|
1053
1115
|
|
|
1054
1116
|
process.exit(repairRes.ok ? 0 : 1);
|
|
@@ -1517,6 +1579,7 @@ async function main() {
|
|
|
1517
1579
|
options: {
|
|
1518
1580
|
ttl: { type: "string" },
|
|
1519
1581
|
pid: { type: "string" },
|
|
1582
|
+
"dry-run": { type: "boolean", short: "d" },
|
|
1520
1583
|
json: { type: "boolean", short: "j" },
|
|
1521
1584
|
},
|
|
1522
1585
|
allowPositionals: true,
|
|
@@ -1581,22 +1644,49 @@ async function main() {
|
|
|
1581
1644
|
console.log(`❌ Lock for ${taskId} not found or release failed`);
|
|
1582
1645
|
process.exit(1);
|
|
1583
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);
|
|
1584
1664
|
} else {
|
|
1585
|
-
const
|
|
1665
|
+
const allLocks = lockStatus(root);
|
|
1666
|
+
const activeLocks = allLocks.filter((l) => l.live);
|
|
1667
|
+
const staleLocks = allLocks.filter((l) => !l.live);
|
|
1586
1668
|
if (lockValues.json) {
|
|
1587
|
-
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));
|
|
1588
1670
|
process.exit(0);
|
|
1589
1671
|
}
|
|
1590
|
-
if (
|
|
1672
|
+
if (allLocks.length === 0) {
|
|
1591
1673
|
console.log("Active Locks: 0 (no held locks)");
|
|
1592
1674
|
} else {
|
|
1593
|
-
console.log(`Active Locks (${
|
|
1594
|
-
for (const l of
|
|
1675
|
+
console.log(`Active Locks (${activeLocks.length}):`);
|
|
1676
|
+
for (const l of activeLocks) {
|
|
1595
1677
|
const files = Array.isArray(l.files) ? l.files.join(", ") : (l.files || "");
|
|
1596
1678
|
const expires = l.expiresAt ? `expires ${l.expiresAt}` : (l.ownerPid ? `pid ${l.ownerPid}` : "active");
|
|
1597
1679
|
console.log(` • taskId: ${l.taskId || l.id} [${l.agent || "unknown"}] (${expires})`);
|
|
1598
1680
|
if (files) console.log(` Paths: ${files}`);
|
|
1599
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
|
+
}
|
|
1600
1690
|
}
|
|
1601
1691
|
}
|
|
1602
1692
|
process.exit(0);
|
|
@@ -1921,6 +2011,15 @@ async function main() {
|
|
|
1921
2011
|
break;
|
|
1922
2012
|
}
|
|
1923
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
|
+
|
|
1924
2023
|
case "init": {
|
|
1925
2024
|
const { values } = parseArgs({
|
|
1926
2025
|
args: args.slice(1),
|
|
@@ -2099,6 +2198,7 @@ async function main() {
|
|
|
2099
2198
|
yes: { type: "boolean", short: "y" },
|
|
2100
2199
|
json: { type: "boolean", short: "j" },
|
|
2101
2200
|
"dry-run": { type: "boolean", short: "d" },
|
|
2201
|
+
"allow-protected": { type: "boolean" },
|
|
2102
2202
|
},
|
|
2103
2203
|
allowPositionals: true,
|
|
2104
2204
|
});
|
|
@@ -2129,6 +2229,7 @@ async function main() {
|
|
|
2129
2229
|
repoless: values.repoless,
|
|
2130
2230
|
interactive: isInteractive,
|
|
2131
2231
|
dryRun: values["dry-run"],
|
|
2232
|
+
allowProtected: Boolean(values["allow-protected"]),
|
|
2132
2233
|
});
|
|
2133
2234
|
|
|
2134
2235
|
if (values.json) {
|
|
@@ -2255,8 +2356,87 @@ async function main() {
|
|
|
2255
2356
|
console.log(`--------------------------------------------------\n`);
|
|
2256
2357
|
}
|
|
2257
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);
|
|
2258
2438
|
} else {
|
|
2259
|
-
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`);
|
|
2260
2440
|
process.exit(1);
|
|
2261
2441
|
}
|
|
2262
2442
|
break;
|
|
@@ -2279,7 +2459,7 @@ async function main() {
|
|
|
2279
2459
|
version: VERSION,
|
|
2280
2460
|
root,
|
|
2281
2461
|
pendingTasks: files.length,
|
|
2282
|
-
activeLocks: lockStatus(root).length,
|
|
2462
|
+
activeLocks: lockStatus(root, { activeOnly: true }).length,
|
|
2283
2463
|
budget,
|
|
2284
2464
|
}, null, 2));
|
|
2285
2465
|
process.exit(0);
|
|
@@ -2288,7 +2468,7 @@ async function main() {
|
|
|
2288
2468
|
console.log(`--------------------------------------------------`);
|
|
2289
2469
|
console.log(` Project Root : ${root}`);
|
|
2290
2470
|
console.log(` Pending Tasks : ${files.length}`);
|
|
2291
|
-
console.log(` Active VFS Locks : ${lockStatus(root).length}`);
|
|
2471
|
+
console.log(` Active VFS Locks : ${lockStatus(root, { activeOnly: true }).length}`);
|
|
2292
2472
|
console.log(` Daily Budget : ${formatBudgetLine(budget)}`);
|
|
2293
2473
|
console.log(`--------------------------------------------------\n`);
|
|
2294
2474
|
process.exit(0);
|
|
@@ -3332,10 +3512,182 @@ async function main() {
|
|
|
3332
3512
|
break;
|
|
3333
3513
|
}
|
|
3334
3514
|
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
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.`);
|
|
3338
3689
|
process.exit(1);
|
|
3690
|
+
}
|
|
3339
3691
|
}
|
|
3340
3692
|
}
|
|
3341
3693
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for autonomous coding agents \u2014 Google Jules, Claude Code, Codex and Gemini CLI.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,8 +22,12 @@ import { normalizePath } from "../src/config.mjs";
|
|
|
22
22
|
const EXIT_SCOPE_VIOLATION = 3;
|
|
23
23
|
const EXIT_ERROR = 1;
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Primary label prefix that lets a human consciously land a protected-path change bound to commit SHA.
|
|
27
|
+
* Conforms to GitHub's 50-character maximum label limit: "allow-p:<40-hex-SHA>" is 48 characters.
|
|
28
|
+
*/
|
|
29
|
+
export const BYPASS_LABEL = "allow-p";
|
|
30
|
+
export const LEGACY_BYPASS_LABEL = "allow-protected-paths";
|
|
27
31
|
|
|
28
32
|
function gitShow(ref, path, cwd) {
|
|
29
33
|
return execFileSync("git", ["show", `${ref}:${path}`], {
|
|
@@ -101,7 +105,18 @@ export function listChangedFiles(opts = {}) {
|
|
|
101
105
|
*/
|
|
102
106
|
export function evaluateScopeGuard(files = [], patterns = [], opts = {}) {
|
|
103
107
|
const labels = (opts.labels || []).map((l) => String(l).toLowerCase().trim());
|
|
104
|
-
const
|
|
108
|
+
const headSha = String(opts.headSha || "").trim().toLowerCase();
|
|
109
|
+
|
|
110
|
+
let bypassed = false;
|
|
111
|
+
if (headSha) {
|
|
112
|
+
if (/^[0-9a-f]{40}$/i.test(headSha)) {
|
|
113
|
+
const boundLabel = `${BYPASS_LABEL}:${headSha}`;
|
|
114
|
+
const legacyBoundLabel = `${LEGACY_BYPASS_LABEL}:${headSha}`;
|
|
115
|
+
bypassed = labels.includes(boundLabel) || labels.includes(legacyBoundLabel);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
bypassed = labels.includes(BYPASS_LABEL) || labels.includes(LEGACY_BYPASS_LABEL);
|
|
119
|
+
}
|
|
105
120
|
|
|
106
121
|
// Matching always runs at full strength; the label only decides whether a
|
|
107
122
|
// match blocks. Passing `allowProtected` into checkScope instead would make
|
|
@@ -152,17 +167,25 @@ function main() {
|
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
const labels = parseLabels(process.env.PR_LABELS);
|
|
155
|
-
const result = evaluateScopeGuard(files, patterns, { labels });
|
|
170
|
+
const result = evaluateScopeGuard(files, patterns, { labels, headSha });
|
|
156
171
|
|
|
157
172
|
console.log(`Protected patterns (${patterns.length}): ${patterns.join(", ")}`);
|
|
158
173
|
console.log(`Changed files (${files.length}):`);
|
|
159
174
|
for (const f of files) console.log(` ${f}`);
|
|
160
175
|
|
|
176
|
+
const labelName = headSha ? `${BYPASS_LABEL}:${headSha}` : BYPASS_LABEL;
|
|
177
|
+
const legacyLabelName = headSha ? `${LEGACY_BYPASS_LABEL}:${headSha}` : LEGACY_BYPASS_LABEL;
|
|
178
|
+
const activeLabel = labels.includes(labelName)
|
|
179
|
+
? labelName
|
|
180
|
+
: labels.includes(legacyLabelName)
|
|
181
|
+
? legacyLabelName
|
|
182
|
+
: labelName;
|
|
183
|
+
|
|
161
184
|
if (result.bypassed && result.violations.length > 0) {
|
|
162
185
|
for (const v of result.violations) {
|
|
163
|
-
console.log(`::warning file=${v.file}::Protected path modified under "${
|
|
186
|
+
console.log(`::warning file=${v.file}::Protected path modified under "${activeLabel}": ${v.reason}`);
|
|
164
187
|
}
|
|
165
|
-
console.log(`\nLabel "${
|
|
188
|
+
console.log(`\nLabel "${activeLabel}" is present — ${result.violations.length} protected-path match(es) allowed by human review.`);
|
|
166
189
|
process.exit(0);
|
|
167
190
|
}
|
|
168
191
|
|
|
@@ -176,7 +199,7 @@ function main() {
|
|
|
176
199
|
}
|
|
177
200
|
console.error(
|
|
178
201
|
`::error::PR modifies ${result.violations.length} protected file(s). ` +
|
|
179
|
-
`Apply the "${
|
|
202
|
+
`Apply the "${labelName}" label after human review to land this intentionally.`
|
|
180
203
|
);
|
|
181
204
|
process.exit(EXIT_SCOPE_VIOLATION);
|
|
182
205
|
}
|