jules-orchestrator-kit 0.32.8 → 0.34.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
@@ -120,7 +120,7 @@ Autonomous coding agents can write software at 100× human speed—but unconstra
120
120
 
121
121
  * **🚀 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.
122
122
 
123
- * **📈 Proven Scale & Reliability:** Empirically tested with **461 unit tests across 66 suites passing in < 10.0s**, supporting 300+ daily agent sessions per repository. An adversarial red-team suite (`test/adversarial-claims.test.mjs`) continuously attempts to falsify the safety guarantees documented above — 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.
123
+ * **📈 Proven Scale & Reliability:** Empirically tested with **510 unit tests across 77 suites passing in < 10.0s**. An adversarial red-team suite (`test/adversarial-claims.test.mjs`) continuously attempts to falsify the safety guarantees documented above — 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.
124
124
 
125
125
  <br/>
126
126
 
@@ -297,9 +297,9 @@ scope:
297
297
  limits:
298
298
  diffKb: 75 # 75 KB Diff Payload Governor limit
299
299
  promptKb: 50 # Maximum prompt payload size
300
- dailyTasks: 300 # Daily task session quota limit
300
+ dailyTasks: 300 # Task quota per rolling 24h window (not per calendar day)
301
301
  repairAttempts: 3 # Maximum OODA repair iterations
302
- concurrency: 1 # Worker slot concurrency limit
302
+ concurrency: 15 # Worker slots; the ultra plan allows up to 60
303
303
 
304
304
  # Dynamic Complexity & Cost Router — opt-in, disabled by default.
305
305
  # Provider-agnostic: "fast"/"complex" accept any provider key ("jules" |
package/bin/agentctl.mjs CHANGED
@@ -5,18 +5,37 @@ import { readFileSync, existsSync, readdirSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  import { loadConfig, resolveRoot, detectStack, bootstrapZeroTestRepo } from "../src/config.mjs";
7
7
  import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
8
- import { acquireLock, releaseLock, lockStatus, checkDailyBudget, getQueueDir } from "../src/state.mjs";
8
+ import { acquireLock, releaseLock, lockStatus, getQueueDir } from "../src/state.mjs";
9
9
  import { worktreePrune } from "../src/git.mjs";
10
10
  import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
11
+ import { KIT_VERSION } from "../src/version.mjs";
12
+ import { budgetStatus, listOpenReservations, releaseOpenReservations, resolveConcurrency } from "../src/budget.mjs";
11
13
 
12
14
  const args = process.argv.slice(2);
13
15
  const command = args[0];
14
16
 
15
- export const VERSION = "0.32.8";
17
+ export const VERSION = KIT_VERSION;
18
+
19
+ /**
20
+ * Render the budget line so the number is never mistaken for the vendor's own
21
+ * counter. The ledger counts what *this checkout* dispatched; sessions started
22
+ * from the web UI or another machine spend the same quota unseen, so a bare
23
+ * "N / M used" invites the reader to trust a figure that cannot be complete.
24
+ * "last 24h", not "today": the provider's allowance resets on a rolling
25
+ * window, so a figure labelled by the calendar day would be a different number
26
+ * from the one being enforced.
27
+ * @param {{ used: number, limit: number, source: string, certain: boolean }} b
28
+ */
29
+ export function formatBudgetLine(b) {
30
+ const scope = `${b.used} / ${b.limit} used in the last 24h (this repo)`;
31
+ if (b.source === "learned") return `${scope} — provider refused further work`;
32
+ if (b.certain) return `${scope} — limit from ${b.source === "env" ? "JULES_DAILY_BUDGET" : "config"}`;
33
+ return `${scope} — limit estimated from tier "${b.tier || "?"}", not enforced`;
34
+ }
16
35
 
17
36
  export function printHelp() {
18
37
  console.log(`
19
- 🚀 agentctl v0.32.8 — Universal Agent Orchestrator & Safety Gatekeeper
38
+ 🚀 agentctl v${VERSION} — Universal Agent Orchestrator & Safety Gatekeeper
20
39
 
21
40
  Usage: agentctl <command> [options]
22
41
 
@@ -41,6 +60,7 @@ Commands:
41
60
  rollback Restore git state & working tree to atomic pre-flight checkpoint
42
61
  resume Resume warm session with human response (--response "<text>")
43
62
  status Display queue and system status summary
63
+ budget Show the 24h task budget, worker slots and their provenance (reset --yes)
44
64
  scan Scan codebase for TODO/FIXME task candidates
45
65
  hydrate [prompt] Prepend active system learnings and baton-pass state to a prompt
46
66
  harvest Harvest failure traces and record/quarantine resolution rules
@@ -64,11 +84,33 @@ Options:
64
84
 
65
85
 
66
86
  async function main() {
67
- if (!command || command === "--help" || command === "-h") {
87
+ if (command === "--help" || command === "-h") {
68
88
  printHelp();
69
89
  process.exit(0);
70
90
  }
71
91
 
92
+ // Bare `agentctl` answers "what do I do next" rather than dumping thirty
93
+ // commands. The help text is a reference for people who already know the
94
+ // tool; a newcomer cannot tell which entry is step one, and guessing wrong
95
+ // costs them a confusing failure instead of a hint.
96
+ if (!command) {
97
+ const { resolveNextStep, renderNextStep } = await import("../src/ops/next-step.mjs");
98
+ const cwd = process.cwd();
99
+ let budgetLine = "";
100
+ let where = cwd;
101
+ try {
102
+ const nextRoot = resolveRoot();
103
+ where = nextRoot;
104
+ budgetLine = formatBudgetLine(budgetStatus(loadConfig(nextRoot), nextRoot));
105
+ } catch (_) {
106
+ // Outside a repository there is no root to load a config from; the
107
+ // next step below is `git init`, which does not need one.
108
+ }
109
+ const next = resolveNextStep(where);
110
+ console.log(renderNextStep({ version: VERSION, root: where, next, budgetLine: next.blocking ? "" : budgetLine }));
111
+ process.exit(0);
112
+ }
113
+
72
114
  if (command === "version" || command === "--version" || command === "-v") {
73
115
  console.log(`agentctl v${VERSION}`);
74
116
  process.exit(0);
@@ -282,6 +324,55 @@ async function main() {
282
324
  break;
283
325
  }
284
326
 
327
+ case "budget": {
328
+ const action = args[1];
329
+ const b = budgetStatus(config, root);
330
+
331
+ if (action === "reset") {
332
+ // The count is local-only, so an operator who knows their real usage
333
+ // must be able to correct it. Appending `budget_released` keeps the
334
+ // hash chain intact — the ledger is corrected forwards, never edited.
335
+ const dryRun = args.includes("--dry-run");
336
+ const confirmed = args.includes("--yes") || args.includes("-y");
337
+ if (!dryRun && !confirmed) {
338
+ const open = listOpenReservations(root);
339
+ console.log(`Would release ${open.length} open reservation(s) from the last 24 hours.`);
340
+ console.log("This rewrites nothing — it appends `budget_released` entries.");
341
+ console.log("Re-run with --yes to confirm, or --dry-run for detail.");
342
+ process.exit(0);
343
+ }
344
+ const res = releaseOpenReservations({ root, dryRun, reason: "operator-reconcile" });
345
+ const verb = dryRun ? "Would release" : "Released";
346
+ console.log(`${verb} ${res.released} reservation(s) (${res.committed} committed, ${res.uncommitted} never closed).`);
347
+ if (!dryRun) {
348
+ const after = budgetStatus(loadConfig(root), root);
349
+ console.log(`Daily Budget : ${formatBudgetLine(after)}`);
350
+ }
351
+ process.exit(0);
352
+ }
353
+
354
+ if (args.includes("--json")) {
355
+ console.log(JSON.stringify({ ok: true, budget: { ...b, scope: "this-repository" } }, null, 2));
356
+ process.exit(0);
357
+ }
358
+
359
+ const slots = resolveConcurrency(config);
360
+ console.log(`Task Budget : ${formatBudgetLine(b)}`);
361
+ console.log(` ${b.note}`);
362
+ console.log(` Window opened at ${b.windowStart} — the quota resets ${b.windowHours}h after each task,`);
363
+ console.log(" not at midnight, so this count spans yesterday's ledger too.");
364
+ console.log(` Open reservations in the window: ${listOpenReservations(root).length}`);
365
+ console.log("");
366
+ console.log(`Worker Slots : ${slots.concurrency} concurrent`);
367
+ console.log(` ${slots.note}`);
368
+ console.log("");
369
+ console.log("The ledger counts this checkout only — sessions started from the Jules");
370
+ console.log("web UI or another machine spend the same quota without appearing here.");
371
+ console.log("Use `agentctl budget reset --yes` to reconcile a count you know is wrong.");
372
+ process.exit(0);
373
+ break;
374
+ }
375
+
285
376
  case "lock": {
286
377
  const action = args[1];
287
378
  if (action === "acquire") {
@@ -313,6 +404,24 @@ async function main() {
313
404
  }
314
405
 
315
406
  case "doctor": {
407
+ const { values } = parseArgs({
408
+ args: args.slice(1),
409
+ options: { json: { type: "boolean", short: "j" } },
410
+ allowPositionals: true,
411
+ strict: false,
412
+ });
413
+
414
+ // runDoctorChecks() and its seven checks existed but nothing rendered
415
+ // them: this command printed a hand-written summary, so findings like a
416
+ // git-tracked .env were computed, tested, and never shown to anyone.
417
+ const { runDoctorChecks } = await import("../src/ops/doctor-registry.mjs");
418
+ const report = await runDoctorChecks({ root });
419
+
420
+ if (values.json) {
421
+ console.log(JSON.stringify(report, null, 2));
422
+ process.exit(report.summary.fail > 0 ? 1 : 0);
423
+ }
424
+
316
425
  console.log(`\n🔍 agentctl System Diagnostics (v${VERSION})`);
317
426
  console.log(`--------------------------------------------------`);
318
427
  console.log(` Project Root : ${root}`);
@@ -320,10 +429,22 @@ async function main() {
320
429
  console.log(` Detected Stack : ${detectStack(root).stack}`);
321
430
  console.log(` Test Command : ${config.verify.test || "(None)"}`);
322
431
  console.log(` Build Command : ${config.verify.build || "(None)"}`);
323
- const budget = checkDailyBudget(root, config.limits.dailyTasks);
324
- console.log(` Daily Budget : ${budget.used} / ${budget.budget} sessions used`);
325
- console.log(`--------------------------------------------------\n`);
326
- process.exit(0);
432
+ console.log(` Daily Budget : ${formatBudgetLine(budgetStatus(config, root))}`);
433
+ console.log(`--------------------------------------------------`);
434
+
435
+ const icon = { pass: "✅", warn: "⚠️ ", fail: "❌", skip: "⏭️ ", unknown: "❔" };
436
+ for (const r of report.results) {
437
+ console.log(` ${icon[r.status] || "•"} ${r.title}`);
438
+ if (r.status !== "pass") {
439
+ console.log(` ${r.summary}`);
440
+ for (const fix of r.remediation || []) console.log(` → ${fix.summary}`);
441
+ }
442
+ }
443
+
444
+ const { pass, warn, fail } = report.summary;
445
+ console.log(`--------------------------------------------------`);
446
+ console.log(` ${pass} passed, ${warn} warning(s), ${fail} failure(s)\n`);
447
+ process.exit(fail > 0 ? 1 : 0);
327
448
  break;
328
449
  }
329
450
 
@@ -578,8 +699,8 @@ async function main() {
578
699
  console.log(` Project Root : ${root}`);
579
700
  console.log(` Pending Tasks : ${files.length}`);
580
701
  console.log(` Active VFS Locks : ${lockStatus(root).length}`);
581
- const budget = checkDailyBudget(root, config.limits.dailyTasks);
582
- console.log(` Daily Budget : ${budget.used} / ${budget.budget} used`);
702
+ const budget = budgetStatus(config, root);
703
+ console.log(` Daily Budget : ${formatBudgetLine(budget)}`);
583
704
  console.log(`--------------------------------------------------\n`);
584
705
  process.exit(0);
585
706
  break;
package/index.mjs CHANGED
@@ -49,6 +49,9 @@ export {
49
49
  rollbackBudgetReservation,
50
50
  withBudget,
51
51
  checkDailyBudget,
52
+ scanBudgetWindow,
53
+ getLedgerPathsInWindow,
54
+ ROLLING_WINDOW_MS,
52
55
  acquireLock,
53
56
  releaseLock,
54
57
  lockStatus,
@@ -130,3 +133,19 @@ export {
130
133
  generateEvidenceMarkdown,
131
134
  computeEvidenceHash,
132
135
  } from "./src/evidence.mjs";
136
+
137
+ export {
138
+ resolveDailyLimit,
139
+ budgetStatus,
140
+ readObservedCeiling,
141
+ readActiveCeiling,
142
+ recordObservedCeiling,
143
+ isDailyQuotaRejection,
144
+ listOpenReservations,
145
+ releaseOpenReservations,
146
+ resolveConcurrency,
147
+ CEILING_FILE,
148
+ } from "./src/budget.mjs";
149
+ export { KIT_VERSION } from "./src/version.mjs";
150
+ export { VENDOR_TIERS, FALLBACK_TIER } from "./src/config.mjs";
151
+ export { tierOptions } from "./src/wizard-init.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.32.8",
3
+ "version": "0.34.0",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,7 @@
16
16
  * Exit codes: 0 = in sync, 1 = drift detected.
17
17
  */
18
18
 
19
- import { readFileSync, existsSync } from "node:fs";
19
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import { execSync } from "node:child_process";
22
22
  import { resolveRoot } from "../src/config.mjs";
@@ -60,6 +60,33 @@ function readIfExists(path) {
60
60
  return existsSync(path) ? readFileSync(path, "utf-8") : null;
61
61
  }
62
62
 
63
+ /**
64
+ * Version numbers that appear in shipped sources but do not describe this kit.
65
+ * Kept as an explicit, justified list rather than a looser pattern, so a real
66
+ * drift can never slip through by resembling one of these.
67
+ */
68
+ const FOREIGN_VERSIONS = [
69
+ { file: "src/ops/doctor-registry.mjs", value: "20.0.0", why: "minimum supported Node.js runtime" },
70
+ { file: "src/ops/ide-scaffold.mjs", value: "2.0.0", why: "VS Code tasks.json schema version" },
71
+ { file: "src/version.mjs", value: "0.0.0", why: "documented fallback when package.json is absent" },
72
+ ];
73
+
74
+ function isForeignVersion(relPath, value) {
75
+ return FOREIGN_VERSIONS.some((f) => f.file === relPath && f.value === value);
76
+ }
77
+
78
+ /** Every .mjs under `dir`, recursively. */
79
+ function listSourceFiles(dir, acc = []) {
80
+ if (!existsSync(dir)) return acc;
81
+ for (const entry of readdirSync(dir)) {
82
+ if (entry.startsWith(".") || entry === "node_modules") continue;
83
+ const full = join(dir, entry);
84
+ if (statSync(full).isDirectory()) listSourceFiles(full, acc);
85
+ else if (entry.endsWith(".mjs")) acc.push(full);
86
+ }
87
+ return acc;
88
+ }
89
+
63
90
  /**
64
91
  * Verifies documentation is in sync with package.json and the real test counts.
65
92
  * @param {string} [root]
@@ -80,18 +107,52 @@ export function checkDocSync(root = process.cwd(), opts = {}) {
80
107
  if (cli === null) {
81
108
  add("agentctl present", false, "bin/agentctl.mjs not found");
82
109
  } else {
110
+ // Either the version is derived from the manifest — which cannot drift —
111
+ // or it is a literal, which must match. The derived form is preferred;
112
+ // this check exists to stop a literal creeping back in unnoticed.
113
+ const derived = /export const VERSION\s*=\s*KIT_VERSION/.test(cli);
83
114
  const constMatch = cli.match(/export const VERSION\s*=\s*"([^"]+)"/);
84
- add(
85
- "agentctl VERSION const",
86
- constMatch?.[1] === version,
87
- constMatch ? `found "${constMatch[1]}", expected "${version}"` : "no `export const VERSION` found"
88
- );
115
+ if (derived) {
116
+ const versionModule = readIfExists(join(root, "src", "version.mjs"));
117
+ const readsManifest = Boolean(versionModule && /package\.json/.test(versionModule));
118
+ add(
119
+ "agentctl VERSION const",
120
+ readsManifest,
121
+ readsManifest ? "derived from package.json via src/version.mjs" : "VERSION derives from KIT_VERSION but src/version.mjs does not read package.json"
122
+ );
123
+ } else {
124
+ add(
125
+ "agentctl VERSION const",
126
+ constMatch?.[1] === version,
127
+ constMatch ? `found "${constMatch[1]}", expected "${version}"` : "no `export const VERSION` found"
128
+ );
129
+ }
89
130
 
90
- const stale = [...cli.matchAll(/agentctl v(\d+\.\d+\.\d+)/g)].map((m) => m[1]).filter((v) => v !== version);
131
+ // Scan everything shipped, not just the CLI: the MCP server, dashboard and
132
+ // init wizard each hardcoded a version and sat three minor releases behind
133
+ // while this gate only ever looked at bin/agentctl.mjs and stayed green.
134
+ const shipped = [
135
+ ...listSourceFiles(join(root, "src")),
136
+ join(root, "bin", "agentctl.mjs"),
137
+ ];
138
+ const stale = [];
139
+ for (const file of shipped) {
140
+ const text = readIfExists(file);
141
+ if (text === null) continue;
142
+ const rel = file.replace(root + "/", "");
143
+ for (const m of text.matchAll(/\bv?(\d+\.\d+\.\d+)\b/g)) {
144
+ if (m[1] === version) continue;
145
+ // Only flag figures presented as *this kit's* version.
146
+ const context = text.slice(Math.max(0, m.index - 60), m.index).toLowerCase();
147
+ if (!/version|agentctl|orchestrator kit|kit config/.test(context)) continue;
148
+ if (isForeignVersion(rel, m[1], context)) continue;
149
+ stale.push(`${rel}: ${m[1]}`);
150
+ }
151
+ }
91
152
  add(
92
153
  "agentctl banner/version strings",
93
154
  stale.length === 0,
94
- stale.length ? `stale version string(s): ${[...new Set(stale)].join(", ")}` : `all reference v${version}`
155
+ stale.length ? `stale version string(s): ${[...new Set(stale)].join(", ")}` : `all shipped sources reference v${version}`
95
156
  );
96
157
  }
97
158
 
package/scripts/utils.mjs CHANGED
@@ -142,9 +142,13 @@ export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300) {
142
142
  * explicit root to keep callers (notably tests) off the operator's real ledger.
143
143
  */
144
144
  export function reserveDailyBudget(maxSessions = 300, taskKey = "", root = resolveRoot()) {
145
- appendLedger({ event: "budget_reserved", key: taskKey }, root);
145
+ // The id is what makes the reservation releasable. Written without one, a
146
+ // reservation counted against the day and no rollback, commit or reconcile
147
+ // could ever name it again — it stayed charged until the ledger rotated.
148
+ const reservationId = `res-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
149
+ appendLedger({ event: "budget_reserved", reservationId, key: taskKey }, root);
146
150
  const check = checkDailyBudget(root, maxSessions);
147
- return { ok: check.ok, used: check.used, budget: maxSessions };
151
+ return { ok: check.ok, used: check.used, budget: maxSessions, reservationId };
148
152
  }
149
153
 
150
154
  export function verifyLedgerIntegrity(filePath) {