jules-orchestrator-kit 0.38.0 ā 0.38.2
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/rules/jules-protocol.md +3 -0
- package/JULES_RULES_TEMPLATE.md +3 -1
- package/README.md +163 -307
- package/bin/agentctl.mjs +177 -7
- package/bin/init.js +8 -8
- package/index.mjs +14 -2
- package/package.json +1 -1
- package/scripts/command-resolver.mjs +1 -2
- package/scripts/doc-sync-check.mjs +36 -1
- package/scripts/jules-create.mjs +1 -1
- package/scripts/jules-dispatch.mjs +1 -2
- package/scripts/jules-nightly.mjs +2 -2
- package/scripts/jules-patch.mjs +2 -2
- package/scripts/jules-queue-runner.mjs +1 -1
- package/scripts/jules-scan-todos.mjs +1 -1
- package/scripts/jules-self-audit.mjs +1 -2
- package/scripts/jules-status.mjs +1 -1
- package/scripts/release.mjs +55 -0
- package/scripts/run-tests.mjs +15 -1
- package/scripts/utils.mjs +14 -9
- package/src/asset-integrity.mjs +76 -0
- package/src/asset_integrity.mjs +1 -76
- package/src/config.mjs +42 -23
- package/src/dag-engine.mjs +44 -6
- package/src/engine.mjs +18 -11
- package/src/execution-envelope.mjs +111 -0
- package/src/execution_envelope.mjs +1 -111
- package/src/git.mjs +6 -3
- package/src/merge-blocks.mjs +178 -1
- package/src/ops/command-registry.mjs +25 -0
- package/src/ops/handover.mjs +411 -0
- package/src/ops/ide-scaffold.mjs +3 -3
- package/src/ops/next-step.mjs +6 -2
- package/src/prompt-guard.mjs +34 -3
- package/src/provider.mjs +191 -86
- package/src/role-resolver.mjs +31 -0
- package/src/rules-budget.mjs +179 -0
- package/src/rules_budget.mjs +1 -179
- package/src/security.mjs +98 -56
- package/src/state.mjs +11 -6
- package/src/task-optimizer.mjs +10 -2
- package/src/tui.mjs +1 -1
- package/src/webhook.mjs +2 -2
- package/src/wizard-task.mjs +4 -30
package/bin/agentctl.mjs
CHANGED
|
@@ -46,7 +46,7 @@ Commands:
|
|
|
46
46
|
swarm Run parallel task swarm
|
|
47
47
|
mcp Start stdio Model Context Protocol (MCP) server
|
|
48
48
|
clean Clean stale branches, worktrees, locks, and ledgers
|
|
49
|
-
lock <action> Manage mutex locks (acquire | release | status
|
|
49
|
+
lock <action> Manage mutex locks (acquire | release | status)
|
|
50
50
|
doctor Run system diagnostics and stack resolution checks
|
|
51
51
|
bootstrap Bootstrap zero-test repository with verification oracle
|
|
52
52
|
review-repair Parse PR review comments and synthesize OODA repair tasks
|
|
@@ -58,6 +58,7 @@ Commands:
|
|
|
58
58
|
test-gen Scaffold & run automated TDD Red-to-Green test cycle (--run)
|
|
59
59
|
mcp init Scaffold IDE integration config (cursor | vscode | claude | all)
|
|
60
60
|
rollback Restore git state & working tree to atomic pre-flight checkpoint
|
|
61
|
+
handover Inspect or generate Baton Pass session handover envelopes (list | show | create | prune)
|
|
61
62
|
resume Resume warm session with human response (--response "<text>")
|
|
62
63
|
escalate Dispatch or manage webhook escalation incidents (--flush, --status, --clear)
|
|
63
64
|
flaky Manage Wilson-quarantined tests and dispatch healing swarm (status | heal | reset)
|
|
@@ -259,6 +260,25 @@ async function main() {
|
|
|
259
260
|
}
|
|
260
261
|
console.log(`-----------------------------------------------------`);
|
|
261
262
|
console.log(`Overall Result: ${res.ok ? "APPROVED (Exit 0)" : `REJECTED (Exit ${res.code})`}\n`);
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
if (res.code === 3) {
|
|
265
|
+
console.log(`š” Remediation Hint (Exit 3 Scope Violation):`);
|
|
266
|
+
console.log(` ⢠To allow protected files in this run, pass: agentctl gate --allow-protected`);
|
|
267
|
+
console.log(` ⢠Or remove protected/denied paths from the diff before dispatching.\n`);
|
|
268
|
+
} else if (res.code === 5) {
|
|
269
|
+
console.log(`š” Remediation Hint (Exit 5 Diff Payload Overflow):`);
|
|
270
|
+
console.log(` ⢠Total diff exceeds ${config.limits?.diffKb || 75} KB limit.`);
|
|
271
|
+
console.log(` ⢠Split the task into smaller atomic tasks using: agentctl task create\n`);
|
|
272
|
+
} else if (res.code === 6) {
|
|
273
|
+
console.log(`š” Remediation Hint (Exit 6 Secret Leak Prevented):`);
|
|
274
|
+
console.log(` ⢠High-entropy credential or secret detected in patch.`);
|
|
275
|
+
console.log(` ⢠Scrub credential from source and rotate any exposed keys immediately.\n`);
|
|
276
|
+
} else if (res.code === 4) {
|
|
277
|
+
console.log(`š” Remediation Hint (Exit 4 OODA Repair Exhausted):`);
|
|
278
|
+
console.log(` ⢠Automated self-repair could not pass tests cleanly.`);
|
|
279
|
+
console.log(` ⢠Review error fingerprints via: agentctl doctor\n`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
262
282
|
}
|
|
263
283
|
|
|
264
284
|
process.exit(typeof res.code === "number" ? res.code : 0);
|
|
@@ -279,7 +299,7 @@ async function main() {
|
|
|
279
299
|
|
|
280
300
|
const queueDir = getQueueDir(root);
|
|
281
301
|
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
282
|
-
console.log(`Found ${files.length} queued task(s) in .agent/queue/`);
|
|
302
|
+
console.log(`Found ${files.length} queued task(s) in .agent/jules-queue/`);
|
|
283
303
|
if (files.length > 0) {
|
|
284
304
|
const concurrency = values.concurrency ? Number(values.concurrency) : undefined;
|
|
285
305
|
const results = await run(null, {
|
|
@@ -750,13 +770,47 @@ async function main() {
|
|
|
750
770
|
|
|
751
771
|
case "rollback": {
|
|
752
772
|
const { restoreCheckpoint } = await import("../src/ops/checkpoint.mjs");
|
|
753
|
-
const
|
|
773
|
+
const { createHandover } = await import("../src/ops/handover.mjs");
|
|
774
|
+
const { values, positionals } = parseArgs({
|
|
775
|
+
args: args.slice(1),
|
|
776
|
+
options: {
|
|
777
|
+
reason: { type: "string", short: "r" },
|
|
778
|
+
intent: { type: "string", short: "i" },
|
|
779
|
+
handover: { type: "boolean", default: true },
|
|
780
|
+
json: { type: "boolean", short: "j" },
|
|
781
|
+
},
|
|
782
|
+
allowPositionals: true,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const targetId = positionals[0] || "--latest";
|
|
754
786
|
try {
|
|
755
787
|
const res = restoreCheckpoint(targetId, { root });
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
788
|
+
let hoResult = null;
|
|
789
|
+
if (values.handover !== false) {
|
|
790
|
+
try {
|
|
791
|
+
hoResult = createHandover(root, {
|
|
792
|
+
sessionId: res.id,
|
|
793
|
+
status: "rolled-back",
|
|
794
|
+
intent: values.intent || "Restored working tree to pre-flight checkpoint",
|
|
795
|
+
landmines: values.reason ? [values.reason] : ["Session cancelled or rolled back by operator"],
|
|
796
|
+
branch: res.branch,
|
|
797
|
+
headSha: res.headSha,
|
|
798
|
+
});
|
|
799
|
+
} catch (_) {}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (values.json) {
|
|
803
|
+
console.log(JSON.stringify({ ok: true, ...res, handover: hoResult?.filePath }, null, 2));
|
|
804
|
+
} else {
|
|
805
|
+
console.log(`\nā
Git Checkpoint Restored Successfully!`);
|
|
806
|
+
console.log(` Session ID : ${res.id}`);
|
|
807
|
+
console.log(` HEAD SHA : ${res.headSha || "N/A"}`);
|
|
808
|
+
console.log(` RestoredAt : ${res.restoredAt}`);
|
|
809
|
+
if (hoResult) {
|
|
810
|
+
console.log(` Handover : ${hoResult.filePath}`);
|
|
811
|
+
}
|
|
812
|
+
console.log("");
|
|
813
|
+
}
|
|
760
814
|
process.exit(0);
|
|
761
815
|
} catch (err) {
|
|
762
816
|
console.error(`ā Rollback Failed: ${err.message}`);
|
|
@@ -765,6 +819,122 @@ async function main() {
|
|
|
765
819
|
break;
|
|
766
820
|
}
|
|
767
821
|
|
|
822
|
+
case "handover": {
|
|
823
|
+
const {
|
|
824
|
+
createHandover,
|
|
825
|
+
loadHandover,
|
|
826
|
+
listHandovers,
|
|
827
|
+
pruneHandovers,
|
|
828
|
+
formatHandoverPromptContext,
|
|
829
|
+
} = await import("../src/ops/handover.mjs");
|
|
830
|
+
|
|
831
|
+
const subAction = args[1] || "list";
|
|
832
|
+
const { values, positionals } = parseArgs({
|
|
833
|
+
args: args.slice(2),
|
|
834
|
+
options: {
|
|
835
|
+
intent: { type: "string", short: "i" },
|
|
836
|
+
status: { type: "string", short: "s", default: "aborted" },
|
|
837
|
+
completed: { type: "string", short: "c" },
|
|
838
|
+
assumptions: { type: "string", short: "a" },
|
|
839
|
+
landmines: { type: "string", short: "l" },
|
|
840
|
+
"next-steps": { type: "string", short: "n" },
|
|
841
|
+
limit: { type: "string" },
|
|
842
|
+
json: { type: "boolean", short: "j" },
|
|
843
|
+
context: { type: "boolean" },
|
|
844
|
+
},
|
|
845
|
+
allowPositionals: true,
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
if (subAction === "list") {
|
|
849
|
+
let items = listHandovers(root);
|
|
850
|
+
const limitNum = parseInt(values.limit || "20", 10);
|
|
851
|
+
if (!isNaN(limitNum) && limitNum > 0) {
|
|
852
|
+
items = items.slice(0, limitNum);
|
|
853
|
+
}
|
|
854
|
+
if (values.json) {
|
|
855
|
+
console.log(JSON.stringify({ ok: true, count: items.length, handovers: items }, null, 2));
|
|
856
|
+
} else {
|
|
857
|
+
console.log(`\nš Baton Pass Handover Envelopes (${items.length})`);
|
|
858
|
+
console.log(`--------------------------------------------------`);
|
|
859
|
+
if (items.length === 0) {
|
|
860
|
+
console.log(` (No handovers found in .agent/handovers/)`);
|
|
861
|
+
} else {
|
|
862
|
+
items.forEach((h, idx) => {
|
|
863
|
+
console.log(` ${idx + 1}. [${h.status.toUpperCase()}] ${h.sessionId} (${h.createdAt.substring(0, 16)})`);
|
|
864
|
+
if (h.intent) console.log(` Intent: ${h.intent.substring(0, 80)}`);
|
|
865
|
+
console.log(` File : ${h.filePath}\n`);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
console.log(`--------------------------------------------------\n`);
|
|
869
|
+
}
|
|
870
|
+
process.exit(0);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (subAction === "show") {
|
|
874
|
+
const targetId = positionals[0];
|
|
875
|
+
if (!targetId) {
|
|
876
|
+
console.error("Error: Session ID or file path is required for agentctl handover show <sessionId>");
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
const ho = loadHandover(root, targetId);
|
|
881
|
+
if (values.context) {
|
|
882
|
+
console.log(formatHandoverPromptContext(ho));
|
|
883
|
+
} else if (values.json) {
|
|
884
|
+
console.log(JSON.stringify({ ok: true, handover: ho }, null, 2));
|
|
885
|
+
} else {
|
|
886
|
+
console.log(ho.rawMarkdown);
|
|
887
|
+
}
|
|
888
|
+
process.exit(0);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
console.error(`ā Error loading handover: ${err.message}`);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (subAction === "create") {
|
|
896
|
+
const targetId = positionals[0] || `session-${Date.now()}`;
|
|
897
|
+
try {
|
|
898
|
+
const res = createHandover(root, {
|
|
899
|
+
sessionId: targetId,
|
|
900
|
+
status: values.status,
|
|
901
|
+
intent: values.intent,
|
|
902
|
+
completed: values.completed,
|
|
903
|
+
assumptions: values.assumptions,
|
|
904
|
+
landmines: values.landmines,
|
|
905
|
+
nextSteps: values["next-steps"],
|
|
906
|
+
});
|
|
907
|
+
if (values.json) {
|
|
908
|
+
console.log(JSON.stringify({ ok: true, ...res }, null, 2));
|
|
909
|
+
} else {
|
|
910
|
+
console.log(`\nā
Handover Envelope Created Successfully!`);
|
|
911
|
+
console.log(` Session ID : ${res.sessionId}`);
|
|
912
|
+
console.log(` Status : ${res.status}`);
|
|
913
|
+
console.log(` File Path : ${res.filePath}\n`);
|
|
914
|
+
}
|
|
915
|
+
process.exit(0);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
console.error(`ā Error creating handover: ${err.message}`);
|
|
918
|
+
process.exit(1);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (subAction === "prune") {
|
|
923
|
+
const limitNum = parseInt(values.limit || "20", 10);
|
|
924
|
+
const count = pruneHandovers(root, limitNum);
|
|
925
|
+
if (values.json) {
|
|
926
|
+
console.log(JSON.stringify({ ok: true, pruned: count }, null, 2));
|
|
927
|
+
} else {
|
|
928
|
+
console.log(`ā
Pruned ${count} old handover manifest(s).`);
|
|
929
|
+
}
|
|
930
|
+
process.exit(0);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
console.error(`Error: Unknown handover subcommand '${subAction}'. Use list, show, create, or prune.`);
|
|
934
|
+
process.exit(1);
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
|
|
768
938
|
case "resume": {
|
|
769
939
|
const { createProvider } = await import("../src/provider.mjs");
|
|
770
940
|
const sessionId = args[1];
|
package/bin/init.js
CHANGED
|
@@ -249,14 +249,14 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
|
249
249
|
pkg.scripts = pkg.scripts || {};
|
|
250
250
|
let updated = false;
|
|
251
251
|
const julesScripts = {
|
|
252
|
-
"jules:dispatch": "
|
|
253
|
-
"jules:queue": "
|
|
254
|
-
"jules:create": "
|
|
255
|
-
"jules:status": "
|
|
256
|
-
"jules:audit": "
|
|
257
|
-
"jules:scan": "
|
|
258
|
-
"jules:swarm": "
|
|
259
|
-
"jules:nightly": "
|
|
252
|
+
"jules:dispatch": "agentctl dispatch",
|
|
253
|
+
"jules:queue": "agentctl queue",
|
|
254
|
+
"jules:create": "agentctl task create",
|
|
255
|
+
"jules:status": "agentctl status",
|
|
256
|
+
"jules:audit": "agentctl gate",
|
|
257
|
+
"jules:scan": "agentctl scan",
|
|
258
|
+
"jules:swarm": "agentctl swarm",
|
|
259
|
+
"jules:nightly": "agentctl clean"
|
|
260
260
|
};
|
|
261
261
|
|
|
262
262
|
for (const [key, val] of Object.entries(julesScripts)) {
|
package/index.mjs
CHANGED
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
ProviderSchemaError,
|
|
33
33
|
parseRetryAfter,
|
|
34
34
|
} from "./src/provider.mjs";
|
|
35
|
+
export { resolveRoutedProvider } from "./src/router.mjs";
|
|
35
36
|
export {
|
|
36
37
|
detectPolyglotStack,
|
|
37
38
|
resolveWorkspaceBoundary,
|
|
@@ -71,8 +72,8 @@ export {
|
|
|
71
72
|
verifyExecutionEnvelope,
|
|
72
73
|
freezeExecutionEnvelope,
|
|
73
74
|
hashExecutionEnvelope,
|
|
74
|
-
} from "./src/
|
|
75
|
-
export { checkAssetIntegrity } from "./src/
|
|
75
|
+
} from "./src/execution-envelope.mjs";
|
|
76
|
+
export { checkAssetIntegrity } from "./src/asset-integrity.mjs";
|
|
76
77
|
export { classifyRiskTier, RISK_TIERS } from "./src/risk.mjs";
|
|
77
78
|
export { recordRemediation, queryRemediations } from "./src/remediation.mjs";
|
|
78
79
|
export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
|
|
@@ -178,3 +179,14 @@ export {
|
|
|
178
179
|
synthesizeFlakyHealingTask,
|
|
179
180
|
runFlakyHealingSwarm,
|
|
180
181
|
} from "./src/flaky-ledger.mjs";
|
|
182
|
+
|
|
183
|
+
// Baton Pass Session Handover Engine
|
|
184
|
+
export {
|
|
185
|
+
createHandover,
|
|
186
|
+
loadHandover,
|
|
187
|
+
listHandovers,
|
|
188
|
+
pruneHandovers,
|
|
189
|
+
formatHandoverPromptContext,
|
|
190
|
+
getHandoverDir,
|
|
191
|
+
HandoverError,
|
|
192
|
+
} from "./src/ops/handover.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.2",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for Google Jules (jules) autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -53,7 +53,23 @@ export function parseTestCounts(out = "") {
|
|
|
53
53
|
const m = String(out).match(new RegExp(`^[^\\n]*?(?:ā¹|#)\\s*${key}\\s+(\\d+)\\s*$`, "m"));
|
|
54
54
|
return m ? Number(m[1]) : null;
|
|
55
55
|
};
|
|
56
|
-
|
|
56
|
+
// Names, not just a count. This gate runs where nobody is watching the
|
|
57
|
+
// output, and "2 failing" costs a round-trip through CI to turn into
|
|
58
|
+
// something actionable. The spec reporter prints each failure twice (inline
|
|
59
|
+
// and again in its summary), hence the dedupe.
|
|
60
|
+
const failures = [
|
|
61
|
+
...String(out).matchAll(/^not ok \d+ - (.+?)$/gm),
|
|
62
|
+
...String(out).matchAll(/^\s*ā\s+(.+?)\s+\(\d/gm),
|
|
63
|
+
].map((m) => m[1].trim());
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
tests: num("tests"),
|
|
67
|
+
pass: num("pass"),
|
|
68
|
+
suites: num("suites"),
|
|
69
|
+
todo: num("todo"),
|
|
70
|
+
fail: num("fail"),
|
|
71
|
+
failures: [...new Set(failures)],
|
|
72
|
+
};
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
function readIfExists(path) {
|
|
@@ -238,10 +254,29 @@ export function checkDocSync(root = process.cwd(), opts = {}) {
|
|
|
238
254
|
const claimedSuites = claim ? Number(claim[2]) : null;
|
|
239
255
|
|
|
240
256
|
let { tests, suites } = opts;
|
|
257
|
+
let measuredFail = null;
|
|
258
|
+
let measuredFailures = [];
|
|
241
259
|
if (tests === undefined || suites === undefined) {
|
|
242
260
|
const measured = measureTestCounts(root);
|
|
243
261
|
tests = tests ?? measured.pass;
|
|
244
262
|
suites = suites ?? measured.suites;
|
|
263
|
+
measuredFail = measured.fail;
|
|
264
|
+
measuredFailures = measured.failures || [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// A failing suite still prints a summary, so the pass count silently drops
|
|
268
|
+
// and the README check reports a number mismatch ā which reads as stale
|
|
269
|
+
// documentation and sends the reader looking for skipped tests. Say what
|
|
270
|
+
// actually happened instead. Only meaningful when this measured the suite
|
|
271
|
+
// itself; release.mjs passes counts in from a run it already verified.
|
|
272
|
+
if (measuredFail !== null) {
|
|
273
|
+
add(
|
|
274
|
+
"test suite passes",
|
|
275
|
+
measuredFail === 0,
|
|
276
|
+
measuredFail === 0
|
|
277
|
+
? "no failing tests"
|
|
278
|
+
: `${measuredFail} failing: ${measuredFailures.slice(0, 3).join("; ") || "(names unparsed)"}${measuredFailures.length > 3 ? ` ā¦+${measuredFailures.length - 3}` : ""}`
|
|
279
|
+
);
|
|
245
280
|
}
|
|
246
281
|
|
|
247
282
|
if (claimedTests === null) {
|
package/scripts/jules-create.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Delegates execution to bin/agentctl.mjs dispatch.
|
|
4
|
+
* Task dispatch entrypoint: delegates execution to engine dispatch pipeline.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { dispatch } from "../src/engine.mjs";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Nightly repository maintenance and worktree pruning.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { worktreePrune } from "../src/git.mjs";
|
|
8
8
|
|
|
9
9
|
if (process.argv[1] && process.argv[1].endsWith("jules-nightly.mjs")) {
|
|
10
10
|
worktreePrune();
|
|
11
|
-
console.log("
|
|
11
|
+
console.log("Nightly maintenance complete.");
|
|
12
12
|
process.exit(0);
|
|
13
13
|
}
|
package/scripts/jules-patch.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Session patch fetching utility.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export async function fetchSessionPatch(sessionId, _options = {}) {
|
|
@@ -18,6 +18,6 @@ export async function fetchSessionPatch(sessionId, _options = {}) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (process.argv[1] && process.argv[1].endsWith("jules-patch.mjs")) {
|
|
21
|
-
console.log("
|
|
21
|
+
console.log("Patch fetch completed.");
|
|
22
22
|
process.exit(0);
|
|
23
23
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Delegates execution to src/engine.mjs gate().
|
|
4
|
+
* Self-audit gatekeeper: runs 4-phase safety gate against working tree or base branch.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { existsSync } from "node:fs";
|
package/scripts/jules-status.mjs
CHANGED
package/scripts/release.mjs
CHANGED
|
@@ -57,6 +57,61 @@ console.log("1b. Verifying documentation is in sync with package.json & test sui
|
|
|
57
57
|
console.log(" ā
Documentation is in sync.\n");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// 1c. Continuous-integration gate (blocking). Step 1 ran the suite on this one
|
|
61
|
+
// machine. Every cross-platform break this repository has shipped was green
|
|
62
|
+
// locally and red on the matrix ā a forward-slash path assertion, a
|
|
63
|
+
// case-folded deny rule ā so a local pass says nothing about Windows or
|
|
64
|
+
// macOS. `gh` resolves the repository from the git remote, so this is not
|
|
65
|
+
// specific to any one fork.
|
|
66
|
+
console.log("1c. Verifying CI is green for HEAD...");
|
|
67
|
+
if (process.argv.includes("--skip-ci-check")) {
|
|
68
|
+
console.log(" ā ļø Skipped via --skip-ci-check.\n");
|
|
69
|
+
} else {
|
|
70
|
+
const headSha = execSync("git rev-parse HEAD", { cwd: root, encoding: "utf-8" }).trim();
|
|
71
|
+
let runs;
|
|
72
|
+
try {
|
|
73
|
+
const raw = execSync("gh run list --limit 40 --json headSha,status,conclusion,workflowName,url", {
|
|
74
|
+
cwd: root,
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
77
|
+
});
|
|
78
|
+
runs = JSON.parse(raw).filter((r) => r.headSha === headSha);
|
|
79
|
+
} catch {
|
|
80
|
+
console.error("ā Release Aborted: could not query CI status via `gh`.");
|
|
81
|
+
console.error(" Run `gh auth login`, or re-run with --skip-ci-check to release without it.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (runs.length === 0) {
|
|
86
|
+
console.error(`ā Release Aborted: no CI run found for HEAD (${headSha.slice(0, 7)}).`);
|
|
87
|
+
console.error(" Push the commit and let CI finish first, or re-run with --skip-ci-check.");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A run that is still going is not a pass. Releasing on a pending matrix is
|
|
92
|
+
// exactly how a red Windows job gets published.
|
|
93
|
+
const pending = runs.filter((r) => r.status !== "completed");
|
|
94
|
+
const failed = runs.filter((r) => !["success", "skipped", "neutral"].includes(r.conclusion));
|
|
95
|
+
|
|
96
|
+
for (const r of runs) {
|
|
97
|
+
const state = r.status !== "completed" ? r.status : r.conclusion;
|
|
98
|
+
const mark = pending.includes(r) ? "ā³" : failed.includes(r) ? "ā" : "ā
";
|
|
99
|
+
console.log(` ${mark} ${r.workflowName.padEnd(32)} ${state}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (pending.length > 0) {
|
|
103
|
+
console.error(`\nā Release Aborted: ${pending.length} CI run(s) still in progress for HEAD.`);
|
|
104
|
+
console.error(` Wait for them to finish: ${pending[0].url}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
if (failed.length > 0) {
|
|
108
|
+
console.error(`\nā Release Aborted: ${failed.length} CI run(s) not green for HEAD.`);
|
|
109
|
+
for (const r of failed) console.error(` ${r.workflowName}: ${r.conclusion} ā ${r.url}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log(` ā
All ${runs.length} CI run(s) green for ${headSha.slice(0, 7)}.\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
60
115
|
// 2. Extract release notes from CHANGELOG.md
|
|
61
116
|
console.log(`2. Extracting release notes for ${tagName} from CHANGELOG.md...`);
|
|
62
117
|
let notes = "";
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -10,7 +10,21 @@ const testFiles = readdirSync(testDir)
|
|
|
10
10
|
.sort()
|
|
11
11
|
.map((f) => join("test", f));
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// A hung test has to fail, not stall. The readKeypresses() stdin regression
|
|
14
|
+
// failed by hanging rather than throwing: without a per-test deadline that
|
|
15
|
+
// burns a CI job to GitHub's six-hour default instead of going red in
|
|
16
|
+
// seconds. The slowest genuine test on the Windows runner is ~2s, so this is
|
|
17
|
+
// two orders of magnitude of headroom and only ever fires on a real hang.
|
|
18
|
+
const timeoutMs = Number(process.env.JULES_TEST_TIMEOUT_MS) || 60_000;
|
|
19
|
+
|
|
20
|
+
// `--test-timeout` landed in Node 20.6. package.json engines allows >=20.0.0,
|
|
21
|
+
// and an unrecognised flag makes Node abort before running anything ā so on an
|
|
22
|
+
// older runtime the suite runs without the deadline rather than not at all.
|
|
23
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
24
|
+
const supportsTestTimeout = major > 20 || (major === 20 && minor >= 6);
|
|
25
|
+
const timeoutArgs = supportsTestTimeout ? [`--test-timeout=${timeoutMs}`] : [];
|
|
26
|
+
|
|
27
|
+
const res = spawnSync(process.execPath, ["--test", ...timeoutArgs, ...testFiles], {
|
|
14
28
|
cwd: root,
|
|
15
29
|
stdio: "inherit",
|
|
16
30
|
env: process.env,
|
package/scripts/utils.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Re-exports utilities from modular src/ domain modules.
|
|
2
|
+
* Common scripting utilities and helper functions.
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
7
6
|
import { join, resolve } from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import { resolveRoot } from "../src/config.mjs";
|
|
10
|
-
import { appendLedger, checkDailyBudget as baseCheckDailyBudget } from "../src/state.mjs";
|
|
10
|
+
import { appendLedger, checkDailyBudget as baseCheckDailyBudget, verifyLedgerIntegrity as baseVerifyLedgerIntegrity } from "../src/state.mjs";
|
|
11
11
|
|
|
12
12
|
export { loadConfig, resolveRoot, normalizePath } from "../src/config.mjs";
|
|
13
13
|
export {
|
|
@@ -45,7 +45,7 @@ export function loadEnv(targetDir = process.cwd()) {
|
|
|
45
45
|
if (!existsSync(envPath)) return;
|
|
46
46
|
try {
|
|
47
47
|
const raw = readFileSync(envPath, "utf-8");
|
|
48
|
-
for (const line of raw.split(
|
|
48
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
49
49
|
const trimmed = line.trim();
|
|
50
50
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
51
51
|
const clean = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
@@ -79,7 +79,7 @@ export function logToHistory(filename, content) {
|
|
|
79
79
|
const dateStr = new Date().toISOString().split("T")[0];
|
|
80
80
|
const historyDir = resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/history");
|
|
81
81
|
if (!existsSync(historyDir)) {
|
|
82
|
-
try {
|
|
82
|
+
try { mkdirSync(historyDir, { recursive: true }); } catch (_) {}
|
|
83
83
|
}
|
|
84
84
|
const filePath = join(historyDir, `${dateStr}-${filename}`);
|
|
85
85
|
try { writeFileSync(filePath, content, "utf-8"); } catch (_) {}
|
|
@@ -145,7 +145,7 @@ export function reserveDailyBudget(maxSessions = 300, taskKey = "", root = resol
|
|
|
145
145
|
// The id is what makes the reservation releasable. Written without one, a
|
|
146
146
|
// reservation counted against the day and no rollback, commit or reconcile
|
|
147
147
|
// could ever name it again ā it stayed charged until the ledger rotated.
|
|
148
|
-
const reservationId = `res-${Date.now()}-${
|
|
148
|
+
const reservationId = `res-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
149
149
|
appendLedger({ event: "budget_reserved", reservationId, key: taskKey }, root);
|
|
150
150
|
const check = checkDailyBudget(root, maxSessions);
|
|
151
151
|
return { ok: check.ok, used: check.used, budget: maxSessions, reservationId };
|
|
@@ -155,12 +155,17 @@ export function verifyLedgerIntegrity(filePath) {
|
|
|
155
155
|
if (!existsSync(filePath)) return { ok: false, count: 0 };
|
|
156
156
|
try {
|
|
157
157
|
const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
158
|
+
let hasHashes = true;
|
|
158
159
|
for (const line of lines) {
|
|
159
|
-
JSON.parse(line);
|
|
160
|
+
const obj = JSON.parse(line);
|
|
161
|
+
if (!obj.hash) hasHashes = false;
|
|
162
|
+
}
|
|
163
|
+
if (hasHashes && lines.length > 0) {
|
|
164
|
+
return baseVerifyLedgerIntegrity(filePath);
|
|
160
165
|
}
|
|
161
166
|
return { ok: true, count: lines.length, lastHash: "sha256-verified" };
|
|
162
|
-
} catch (
|
|
163
|
-
return { ok: false, count: 0, error: "Invalid JSON in ledger" };
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return { ok: false, count: 0, error: err.message || "Invalid JSON in ledger" };
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
|