jules-orchestrator-kit 0.37.0 → 0.38.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/README.md +1 -1
- package/bin/agentctl.mjs +4 -0
- package/package.json +3 -3
- package/scripts/doc-sync-check.mjs +27 -0
- package/scripts/run-tests.mjs +19 -0
- package/src/git.mjs +21 -15
- package/src/state.mjs +10 -3
- package/src/tui.mjs +88 -10
- package/src/wizard-init.mjs +23 -7
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ Autonomous coding agents can write software at 100× human speed—but unconstra
|
|
|
122
122
|
|
|
123
123
|
* **🚀 Zero-Test Bootstrapping (`agentctl bootstrap`):** Synthesizes deterministic syntax-check and smoke-test verification oracles for untested legacy repositories so agents always operate against a falsifiable feedback loop.
|
|
124
124
|
|
|
125
|
-
* **📈 Proven Scale & Reliability:** Empirically tested with **
|
|
125
|
+
* **📈 Proven Scale & Reliability:** Empirically tested with **555 unit tests across 81 suites passing in < 10.0s**. An adversarial red-team suite (`test/adversarial-claims.test.mjs`) continuously attempts to falsify the safety guarantees documented above — every probe in it currently holds, with no open gaps — including cross-platform probes for the case-insensitive filesystems on macOS and Windows — and a documentation-sync gate (`scripts/doc-sync-check.mjs`) blocks any release whose docs have drifted from the code.
|
|
126
126
|
|
|
127
127
|
<br/>
|
|
128
128
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -1278,6 +1278,10 @@ async function main() {
|
|
|
1278
1278
|
}
|
|
1279
1279
|
|
|
1280
1280
|
main().catch((err) => {
|
|
1281
|
+
if (err.name === "WizardCancelledError" || err.code === 130 || (err instanceof Error && err.message?.includes("cancelled by user"))) {
|
|
1282
|
+
console.log(`\n🛑 ${err.message || "Operation cancelled by user."}`);
|
|
1283
|
+
process.exit(130);
|
|
1284
|
+
}
|
|
1281
1285
|
console.error(`[FATAL ERROR] ${err.message}`);
|
|
1282
1286
|
const exitCode = typeof err.code === "number" ? err.code : 1;
|
|
1283
1287
|
process.exit(exitCode);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.38.0",
|
|
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",
|
|
7
7
|
"url": "git+https://github.com/FullThrottle83/jules-orchestrator-kit.git"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
38
|
"init": "node bin/init.js",
|
|
39
|
-
"test": "node
|
|
39
|
+
"test": "node scripts/run-tests.mjs",
|
|
40
40
|
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
41
41
|
"jules:queue": "node bin/agentctl.mjs queue",
|
|
42
42
|
"jules:create": "node scripts/jules-create.mjs",
|
|
@@ -186,6 +186,33 @@ export function checkDocSync(root = process.cwd(), opts = {}) {
|
|
|
186
186
|
? `v${unreleased.join(", v")} marked (Unreleased) but <= shipped v${version}`
|
|
187
187
|
: "no released version left marked (Unreleased)"
|
|
188
188
|
);
|
|
189
|
+
|
|
190
|
+
// An unchecked item under "Shipped Milestones" is a claim the release
|
|
191
|
+
// contradicts. v0.37.0 shipped base64 decoding while the deferral note
|
|
192
|
+
// under v0.32.6 still read "still does not decode" — two statements about
|
|
193
|
+
// the same feature, thirty lines apart, disagreeing. Both survived every
|
|
194
|
+
// other check here, because nothing compared a milestone to its own
|
|
195
|
+
// section heading. Deferrals belong under a target milestone, not a
|
|
196
|
+
// shipped one; moving the item is the fix, ticking it off is the other.
|
|
197
|
+
const shippedStart = roadmap.search(/^##\s.*Shipped Milestones/m);
|
|
198
|
+
if (shippedStart === -1) {
|
|
199
|
+
add("ROADMAP shipped items resolved", false, "no `## … Shipped Milestones` heading found");
|
|
200
|
+
} else {
|
|
201
|
+
const rest = roadmap.slice(shippedStart);
|
|
202
|
+
// Search from index 1 so the section's own heading cannot terminate it.
|
|
203
|
+
const nextHeading = rest.slice(1).search(/^##\s/m);
|
|
204
|
+
const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading + 1);
|
|
205
|
+
const openItems = [...section.matchAll(/^-\s\[ \]\s+(.+)$/gm)].map((m) =>
|
|
206
|
+
m[1].replace(/\*\*/g, "").split("—")[0].trim().slice(0, 60)
|
|
207
|
+
);
|
|
208
|
+
add(
|
|
209
|
+
"ROADMAP shipped items resolved",
|
|
210
|
+
openItems.length === 0,
|
|
211
|
+
openItems.length
|
|
212
|
+
? `unchecked under Shipped Milestones: ${openItems.join("; ")}`
|
|
213
|
+
: "every item under Shipped Milestones is resolved"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
189
216
|
}
|
|
190
217
|
|
|
191
218
|
// 3. CHANGELOG.md — a released version needs a matching entry (release.mjs
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const testDir = join(root, "test");
|
|
8
|
+
const testFiles = readdirSync(testDir)
|
|
9
|
+
.filter((f) => f.endsWith(".test.mjs"))
|
|
10
|
+
.sort()
|
|
11
|
+
.map((f) => join("test", f));
|
|
12
|
+
|
|
13
|
+
const res = spawnSync(process.execPath, ["--test", ...testFiles], {
|
|
14
|
+
cwd: root,
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
process.exit(res.status ?? 1);
|
package/src/git.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { normalizePath } from "./config.mjs";
|
|
@@ -49,20 +49,26 @@ export function runCmd(command, opts = {}) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
const stdout = useShell
|
|
53
|
+
? execSync(shellCmd, {
|
|
54
|
+
cwd,
|
|
55
|
+
encoding: "utf-8",
|
|
56
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
env: opts.env || process.env,
|
|
58
|
+
timeout,
|
|
59
|
+
maxBuffer,
|
|
60
|
+
})
|
|
61
|
+
: execFileSync(binary, args, {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
shell: false,
|
|
65
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
66
|
+
env: opts.env || process.env,
|
|
67
|
+
timeout,
|
|
68
|
+
maxBuffer,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return { status: 0, stdout: String(stdout || "").trim(), stderr: "" };
|
|
66
72
|
} catch (err) {
|
|
67
73
|
const isTimeout = err.code === "ETIMEDOUT" || (err.signal === "SIGTERM" && err.killed);
|
|
68
74
|
const isNobufs = err.code === "ENOBUFS" || (err.message && err.message.includes("maxBuffer"));
|
package/src/state.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { join, basename } from "node:path";
|
|
14
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
15
15
|
import { hostname } from "node:os";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
16
17
|
import { resolveRoot } from "./config.mjs";
|
|
17
18
|
import { redactSecrets } from "./security.mjs";
|
|
18
19
|
|
|
@@ -478,11 +479,17 @@ export function getProcessStartTime(pid) {
|
|
|
478
479
|
return parseProcStat(stat);
|
|
479
480
|
} catch (_) {}
|
|
480
481
|
}
|
|
482
|
+
if (process.platform === "darwin") {
|
|
483
|
+
try {
|
|
484
|
+
const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
485
|
+
return out.trim() || null;
|
|
486
|
+
} catch (_) {}
|
|
487
|
+
}
|
|
481
488
|
return null;
|
|
482
489
|
}
|
|
483
490
|
|
|
484
491
|
/**
|
|
485
|
-
* Checks if a PID is alive with PID-recycling start time validation on Linux.
|
|
492
|
+
* Checks if a PID is alive with PID-recycling start time validation on Linux and macOS.
|
|
486
493
|
*/
|
|
487
494
|
export function isPidAlive(pid, expectedStartTime = null) {
|
|
488
495
|
if (!pid || typeof pid !== "number") return false;
|
|
@@ -492,10 +499,10 @@ export function isPidAlive(pid, expectedStartTime = null) {
|
|
|
492
499
|
if (err?.code !== "EPERM") return false;
|
|
493
500
|
}
|
|
494
501
|
|
|
495
|
-
if (expectedStartTime !== null && expectedStartTime !== undefined
|
|
502
|
+
if (expectedStartTime !== null && expectedStartTime !== undefined) {
|
|
496
503
|
try {
|
|
497
504
|
const actualStartTime = getProcessStartTime(pid);
|
|
498
|
-
if (
|
|
505
|
+
if (actualStartTime && String(actualStartTime) !== String(expectedStartTime)) {
|
|
499
506
|
return false;
|
|
500
507
|
}
|
|
501
508
|
} catch (_) {
|
package/src/tui.mjs
CHANGED
|
@@ -42,10 +42,11 @@ export function styleText(text, style) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
import { WizardCancelledError } from "./ux/terminal-session.mjs";
|
|
45
|
+
import { createKeyDecoder } from "./ux/key-decoder.mjs";
|
|
45
46
|
export { WizardCancelledError };
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* Read keypress in raw mode from TTY input stream.
|
|
49
|
+
* Read keypress in raw mode from TTY input stream without destroying stream on early return.
|
|
49
50
|
* @param {import("node:stream").Readable} stream
|
|
50
51
|
* @returns {AsyncGenerator<string, void, unknown>}
|
|
51
52
|
*/
|
|
@@ -54,20 +55,84 @@ async function* readKeypresses(stream = process.stdin) {
|
|
|
54
55
|
if (stream.setRawMode) {
|
|
55
56
|
stream.setRawMode(true);
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
const decoder = createKeyDecoder();
|
|
60
|
+
const queue = [];
|
|
61
|
+
let waiting = null;
|
|
62
|
+
let error = null;
|
|
63
|
+
|
|
64
|
+
const onData = (chunk) => {
|
|
65
|
+
const events = decoder.push(chunk);
|
|
66
|
+
for (const ev of events) {
|
|
67
|
+
const str = ev.sequence;
|
|
68
|
+
if (waiting) {
|
|
69
|
+
const resolve = waiting;
|
|
70
|
+
waiting = null;
|
|
71
|
+
resolve({ value: str, done: false });
|
|
72
|
+
} else {
|
|
73
|
+
queue.push(str);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const onEnd = () => {
|
|
79
|
+
const flushed = decoder.flush();
|
|
80
|
+
for (const ev of flushed) {
|
|
81
|
+
queue.push(ev.sequence);
|
|
82
|
+
}
|
|
83
|
+
if (waiting) {
|
|
84
|
+
const resolve = waiting;
|
|
85
|
+
waiting = null;
|
|
86
|
+
if (queue.length > 0) {
|
|
87
|
+
resolve({ value: queue.shift(), done: false });
|
|
88
|
+
} else {
|
|
89
|
+
resolve({ done: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onError = (err) => {
|
|
95
|
+
error = err;
|
|
96
|
+
if (waiting) {
|
|
97
|
+
const reject = waiting;
|
|
98
|
+
waiting = null;
|
|
99
|
+
reject(err);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
stream.on("data", onData);
|
|
104
|
+
stream.on("end", onEnd);
|
|
105
|
+
stream.on("error", onError);
|
|
106
|
+
|
|
57
107
|
stream.resume();
|
|
58
108
|
stream.setEncoding("utf-8");
|
|
59
109
|
|
|
60
110
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
111
|
+
while (true) {
|
|
112
|
+
if (error) throw error;
|
|
113
|
+
let nextVal;
|
|
114
|
+
if (queue.length > 0) {
|
|
115
|
+
nextVal = queue.shift();
|
|
116
|
+
} else {
|
|
117
|
+
const res = await new Promise((resolve) => {
|
|
118
|
+
waiting = resolve;
|
|
119
|
+
});
|
|
120
|
+
if (res.done) break;
|
|
121
|
+
nextVal = res.value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (nextVal === "\u0003") {
|
|
64
125
|
// Ctrl+C SIGINT
|
|
65
126
|
process.stdout.write(ANSI.showCursor + "\n");
|
|
66
127
|
throw new WizardCancelledError("Wizard operation cancelled by user (SIGINT).");
|
|
67
128
|
}
|
|
68
|
-
yield
|
|
129
|
+
yield nextVal;
|
|
69
130
|
}
|
|
70
131
|
} finally {
|
|
132
|
+
stream.off("data", onData);
|
|
133
|
+
stream.off("end", onEnd);
|
|
134
|
+
stream.off("error", onError);
|
|
135
|
+
stream.pause();
|
|
71
136
|
if (stream.setRawMode) {
|
|
72
137
|
stream.setRawMode(Boolean(wasRaw));
|
|
73
138
|
}
|
|
@@ -135,11 +200,11 @@ export async function select(options, prompt, opts = {}) {
|
|
|
135
200
|
}
|
|
136
201
|
|
|
137
202
|
let moved = false;
|
|
138
|
-
if (key === "\u001b[A" || key === "k") {
|
|
203
|
+
if (key === "\u001b[A" || key === "\u001bOA" || key === "k") {
|
|
139
204
|
// Up
|
|
140
205
|
currentIdx = (currentIdx - 1 + options.length) % options.length;
|
|
141
206
|
moved = true;
|
|
142
|
-
} else if (key === "\u001b[B" || key === "j") {
|
|
207
|
+
} else if (key === "\u001b[B" || key === "\u001bOB" || key === "j") {
|
|
143
208
|
// Down
|
|
144
209
|
currentIdx = (currentIdx + 1) % options.length;
|
|
145
210
|
moved = true;
|
|
@@ -219,10 +284,10 @@ export async function multiSelect(options, prompt, opts = {}) {
|
|
|
219
284
|
}
|
|
220
285
|
|
|
221
286
|
let redraw = false;
|
|
222
|
-
if (key === "\u001b[A" || key === "k") {
|
|
287
|
+
if (key === "\u001b[A" || key === "\u001bOA" || key === "k") {
|
|
223
288
|
currentIdx = (currentIdx - 1 + options.length) % options.length;
|
|
224
289
|
redraw = true;
|
|
225
|
-
} else if (key === "\u001b[B" || key === "j") {
|
|
290
|
+
} else if (key === "\u001b[B" || key === "\u001bOB" || key === "j") {
|
|
226
291
|
currentIdx = (currentIdx + 1) % options.length;
|
|
227
292
|
redraw = true;
|
|
228
293
|
} else if (key === " ") {
|
|
@@ -276,7 +341,20 @@ export async function input(prompt, opts = {}) {
|
|
|
276
341
|
while (true) {
|
|
277
342
|
const defaultHint = defaultValue ? ` ${ANSI.dim}(${defaultValue})${ANSI.reset}` : "";
|
|
278
343
|
const query = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}${defaultHint}: `;
|
|
279
|
-
|
|
344
|
+
let rawAns;
|
|
345
|
+
try {
|
|
346
|
+
rawAns = await rl.question(query);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err.name === "AbortError" || err.code === "ABORT_ERR" || err.message?.includes("Ctrl+C")) {
|
|
349
|
+
stdout.write("\n");
|
|
350
|
+
throw new WizardCancelledError("Wizard operation cancelled by user (SIGINT).");
|
|
351
|
+
}
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
if (rawAns.includes("\u0003") || rawAns.includes("\x03")) {
|
|
355
|
+
stdout.write("\n");
|
|
356
|
+
throw new WizardCancelledError("Wizard operation cancelled by user (SIGINT).");
|
|
357
|
+
}
|
|
280
358
|
const ans = rawAns.trim() || defaultValue;
|
|
281
359
|
|
|
282
360
|
const validRes = await validate(ans);
|
package/src/wizard-init.mjs
CHANGED
|
@@ -220,11 +220,20 @@ export function loadPresets(root = process.cwd()) {
|
|
|
220
220
|
export async function runInitWizard(root = process.cwd(), options = {}) {
|
|
221
221
|
const interactive = options.interactive !== false && isTTY(options.stdin || process.stdin);
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
// Preserve existing config if present
|
|
224
|
+
let existingConfig = {};
|
|
225
|
+
const existingConfigPath = join(root, ".agent", "config.yml");
|
|
226
|
+
if (existsSync(existingConfigPath)) {
|
|
227
|
+
try {
|
|
228
|
+
existingConfig = parseYaml(readFileSync(existingConfigPath, "utf-8")) || {};
|
|
229
|
+
} catch (_) {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!interactive && !options.allowDefaults && !options.tier && !existingConfig.tier) {
|
|
224
233
|
throw new Error("Non-interactive init requires explicit options or allowDefaults: true");
|
|
225
234
|
}
|
|
226
235
|
|
|
227
|
-
let selectedTier = options.tier || "pro";
|
|
236
|
+
let selectedTier = options.tier || existingConfig.tier || "pro";
|
|
228
237
|
let testCmd = options.testCmd;
|
|
229
238
|
let buildCmd = options.buildCmd;
|
|
230
239
|
let selectedPresets = options.presets;
|
|
@@ -235,28 +244,35 @@ export async function runInitWizard(root = process.cwd(), options = {}) {
|
|
|
235
244
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
236
245
|
sp.stop(`Detected Stack: ${oracle.stack}`);
|
|
237
246
|
|
|
247
|
+
const optionsList = tierOptions();
|
|
248
|
+
const defaultTierIdx = Math.max(0, optionsList.findIndex((t) => t.value === selectedTier));
|
|
249
|
+
|
|
238
250
|
selectedTier = await select(
|
|
239
|
-
|
|
251
|
+
optionsList,
|
|
240
252
|
"Which plan does your Jules account use? (limits are adjustable later)",
|
|
241
|
-
options
|
|
253
|
+
{ ...options, defaultIdx: defaultTierIdx }
|
|
242
254
|
);
|
|
243
255
|
|
|
244
256
|
testCmd = await input("Verification Test Command", {
|
|
245
|
-
defaultValue: oracle.candidates.testCmd || "npm test",
|
|
257
|
+
defaultValue: testCmd || existingConfig.verify?.test || oracle.candidates.testCmd || "npm test",
|
|
246
258
|
stdin: options.stdin,
|
|
247
259
|
stdout: options.stdout,
|
|
248
260
|
});
|
|
249
261
|
|
|
250
262
|
buildCmd = await input("Verification Build Command", {
|
|
251
|
-
defaultValue: oracle.candidates.buildCmd || "",
|
|
263
|
+
defaultValue: buildCmd || existingConfig.verify?.build || oracle.candidates.buildCmd || "",
|
|
252
264
|
stdin: options.stdin,
|
|
253
265
|
stdout: options.stdout,
|
|
254
266
|
});
|
|
255
267
|
|
|
268
|
+
const defaultPresetSet = new Set(
|
|
269
|
+
selectedPresets || existingConfig.presets || BUILTIN_PRESETS.map((p) => p.id)
|
|
270
|
+
);
|
|
271
|
+
|
|
256
272
|
const presetOptions = BUILTIN_PRESETS.map((p) => ({
|
|
257
273
|
label: p.title,
|
|
258
274
|
value: p.id,
|
|
259
|
-
checked:
|
|
275
|
+
checked: defaultPresetSet.has(p.id),
|
|
260
276
|
description: p.description,
|
|
261
277
|
}));
|
|
262
278
|
|