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 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 **547 unit tests across 80 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.
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.37.0",
4
- "description": "Orchestration kit for running Google Jules autonomous agents.",
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 --test test/*.test.mjs",
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 isWin = process.platform === "win32";
53
- const execBin = useShell ? (isWin ? (process.env.ComSpec || "cmd.exe") : "/bin/sh") : binary;
54
- const execArgs = useShell ? (isWin ? ["/d", "/s", "/c", shellCmd] : ["-c", shellCmd]) : args;
55
-
56
- const stdout = execFileSync(execBin, execArgs, {
57
- cwd,
58
- encoding: "utf-8",
59
- shell: false,
60
- stdio: ["ignore", "pipe", "pipe"],
61
- env: opts.env || process.env,
62
- timeout,
63
- maxBuffer,
64
- });
65
- return { status: 0, stdout: stdout.trim(), stderr: "" };
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 && process.platform === "linux") {
502
+ if (expectedStartTime !== null && expectedStartTime !== undefined) {
496
503
  try {
497
504
  const actualStartTime = getProcessStartTime(pid);
498
- if (!actualStartTime || String(actualStartTime) !== String(expectedStartTime)) {
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
- for await (const chunk of stream) {
62
- const str = String(chunk);
63
- if (str === "\u0003") {
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 str;
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
- const rawAns = await rl.question(query);
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);
@@ -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
- if (!interactive && !options.allowDefaults && !options.tier) {
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
- tierOptions(),
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: true,
275
+ checked: defaultPresetSet.has(p.id),
260
276
  description: p.description,
261
277
  }));
262
278