jules-orchestrator-kit 0.32.4 → 0.32.5

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
@@ -108,13 +108,19 @@ Autonomous coding agents can write software at 100× human speed—but unconstra
108
108
 
109
109
  * **💻 Native Interactive UX & Command Palette (`v0.30.0`):** Features a zero-dependency full-screen Terminal Engine (`capabilities`, `key-decoder`, `renderer`, `layout`, `widgets`), interactive diagnostic matrix (`agentctl doctor`), task queue/swarm managers (`agentctl queue`, `agentctl swarm`), and a searchable Command Palette.
110
110
 
111
- * **🌐 Universal Polyglot Spine:** Natively auto-detects 24+ tech stacks (PHP/Laravel/WordPress, .NET/C#, Python, Go, Rust, C/C++, Flutter/Swift, Node/Deno/Bun) and transparently wraps verification suites in Docker Compose or Devcontainer sandboxes.
111
+ * **🌐 Universal Polyglot Spine:** Natively auto-detects 26+ tech stacks (PHP/Laravel/WordPress, .NET/C#, Python, Go, Rust, C/C++, Flutter/Swift, Node/Deno/Bun, Solidity/Foundry/Hardhat) and transparently wraps verification suites in Docker Compose or Devcontainer sandboxes.
112
+
113
+ * **🧩 DAG-Ordered Task Queue & Specialist Agent Roles:** `agentctl queue --dag` resolves inter-task dependencies via Kahn's algorithm with cycle detection instead of linear FIFO order, and `--role <overseer|bolt|sentinel|janitor>` binds a task to a pre-defined specialist prompt persona resolved from `.agent/prompts/`.
114
+
115
+ * **🔏 Cryptographic Evidence Ledger (`agentctl evidence`):** Generates a SHA-256 manifest of changed files, test-file hashes, and tamper-detection locks for every verified task — a portable, offline-verifiable audit trail toward the roadmap's SOC2 compliance exporter.
116
+
117
+ * **💸 Dynamic Complexity & Cost Router (`router:` in `.agent/config.yml`, opt-in):** A zero-dependency, rule-based heuristic classifier (`src/router.mjs`) routes trivial tasks (typos, lint fixes, single-file lockfile bumps) to a cheap/fast provider — e.g. the built-in `gemini-flash` (Gemini CLI, headless mode) preset — while reserving your primary provider for complex, multi-file, or safety-sensitive work. Tasks touching `scope.deny`, `auth/**`, `migrations/**`, secrets, or using the `sentinel` role always force the primary provider regardless of score. Fully provider-agnostic: swap in any exec/HTTP provider spec for `router.fast`/`router.complex`, and override per-task with `--tier fast|complex`.
112
118
 
113
119
  * **📂 Scoped Monorepo Boundary Resolver:** Statically maps changed files up directory ancestry to invoke isolated subshell test suites (`(cd backend && pytest) && (cd cli && cargo test)`), eliminating global test thrashing.
114
120
 
115
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.
116
122
 
117
- * **📈 Proven Scale & Reliability:** Empirically tested with **368 unit tests across 52 suites passing in < 3.0s**, supporting 300+ daily agent sessions per repository.
123
+ * **📈 Proven Scale & Reliability:** Empirically tested with **421 unit tests across 59 suites passing in < 8.0s**, supporting 300+ daily agent sessions per repository.
118
124
 
119
125
  <br/>
120
126
 
@@ -166,7 +172,7 @@ To ensure maximum merge success, dispatch tasks according to our deterministic t
166
172
  | **Self-Healing Loop** | ❌ None (Crashes on test error) | ❌ None (Fails build; notifies human) | ✅ **Autonomous OODA Loop** (Max 3 repair turns with error fingerprinting) |
167
173
  | **Interactive UX Engine**| ❌ Raw unformatted CLI dumps | ❌ Non-interactive log outputs | ✅ **Native TUI Engine & Command Palette** (Zero-dependency alternate-screen TUI) |
168
174
  | **Scope Isolation** | ❌ None (Can modify CI files or lockfiles) | 🟡 Post-commit branch rules only | ✅ **Fail-Closed Scope Guard** (Deny-first evaluation; blocks protected paths) |
169
- | **Polyglot Stack Detection**| ❌ Manual prompt instructions | 🟡 Hardcoded YAML workflow steps | ✅ **Universal 24+ Stack Detector** (`src/config.mjs`) |
175
+ | **Polyglot Stack Detection**| ❌ Manual prompt instructions | 🟡 Hardcoded YAML workflow steps | ✅ **Universal 26+ Stack Detector** (`src/config.mjs`) |
170
176
  | **Flaky Test Quarantine** | ❌ Fails session randomly | ❌ Breaks CI pipeline randomly | ✅ **Wilson-Score Statistical Quarantine** (Oscillation ≥ 0.40 quarantined automatically) |
171
177
  | **Monorepo Scoping** | ❌ Runs full global test suite | 🟡 Requires custom Nx/Turbo scripting | ✅ **Scoped Subshell Boundary Resolver** (`resolveWorkspaceBoundary`) |
172
178
  | **Zero-Test Bootstrapping**| ❌ Halts without verification oracle | ❌ Fails build if no tests exist | ✅ **Instant Oracle Synthesis** (`php -l`, `compileall`, `dotnet build`, `tsc`, `smoke`) |
@@ -227,7 +233,7 @@ npx jules-orchestrator-kit queue --interactive
227
233
  <br/>
228
234
 
229
235
  <details>
230
- <summary><b>🔍 View All 24+ Supported Ecosystems & Stack Triggers</b></summary>
236
+ <summary><b>🔍 View All 26+ Supported Ecosystems & Stack Triggers</b></summary>
231
237
 
232
238
  <br/>
233
239
 
@@ -242,6 +248,8 @@ Ecosystems Natively Supported by src/config.mjs:
242
248
  ├── Systems / Rust Cargo (Cargo.toml)
243
249
  ├── Systems / Go (go.mod)
244
250
  ├── Systems / Make (Makefile)
251
+ ├── Web3 / Solidity Foundry (foundry.toml, remappings.txt) — offline-enforced forge test/build/fmt
252
+ ├── Web3 / Solidity Hardhat (hardhat.config.js, hardhat.config.ts)
245
253
  ├── Python / FastAPI / Django (pyproject.toml, requirements.txt, setup.py)
246
254
  ├── Elixir / Phoenix (mix.exs)
247
255
  ├── Ruby / Rails (Gemfile)
@@ -269,7 +277,7 @@ Ecosystems Natively Supported by src/config.mjs:
269
277
  # .agent/config.yml — Universal Orchestrator Configuration
270
278
 
271
279
  version: 1
272
- provider: "jules" # Provider key ("jules" | "claude-code" | "local")
280
+ provider: "jules" # Provider key ("jules" | "claude-code" | "codex" | "gemini-flash")
273
281
  baseBranch: "main" # Default target base branch
274
282
  branchPrefix: "agent/" # Prefix for task branches
275
283
 
@@ -292,6 +300,15 @@ limits:
292
300
  dailyTasks: 300 # Daily task session quota limit
293
301
  repairAttempts: 3 # Maximum OODA repair iterations
294
302
  concurrency: 1 # Worker slot concurrency limit
303
+
304
+ # Dynamic Complexity & Cost Router — opt-in, disabled by default.
305
+ # Provider-agnostic: "fast"/"complex" accept any provider key ("jules" |
306
+ # "claude-code" | "codex" | "gemini-flash") or an inline custom provider spec.
307
+ router:
308
+ enabled: false
309
+ fast: "gemini-flash" # Trivial/mechanical tasks (score <= threshold)
310
+ complex: "jules" # Complex/multi-file/safety-sensitive tasks; defaults to `provider`
311
+ threshold: 0 # Heuristic score above which a task escalates to `complex`
295
312
  ```
296
313
 
297
314
  <br/>
@@ -369,15 +386,15 @@ Native stdio server exposing task dispatch, gate verification, and risk auditing
369
386
  | Command | Usage | Description | Exit Codes |
370
387
  | :--- | :--- | :--- | :--- |
371
388
  | `init` | `agentctl init [--interactive] [--tier pro]` | Interactive onboarding wizard & stack oracle inspector generating `.agent/config.yml`. | `0` (Created) |
372
- | `task create` | `agentctl task create [--title <t>] [--prompt <p>] [--template <id>]` | Interactively authors & scopes falsifiable task envelopes with secret scrubbing & preflight gate checks. | `0` (Queued), `1` (Unfalsifiable / Secret leak) |
389
+ | `task create` | `agentctl task create [--title <t>] [--prompt <p>] [--template <id>] [--role <name>] [--tier fast\|complex] [--depends-on <id,...>]` | Interactively authors & scopes falsifiable task envelopes with secret scrubbing, preflight gate checks, specialist role resolution, DAG dependency wiring, and an optional Cost Router tier override. | `0` (Queued), `1` (Unfalsifiable / Secret leak) |
373
390
  | `task template` | `agentctl task template [<id>] [--list] [--json]` | Lists and synthesizes specialized web task envelopes (`web-cwv`, `web-wcag`, `web-seo`, `web-playwright`, `web-flaky-heal`). | `0` (Synthesized/Listed) |
374
391
  | `task optimize` | `agentctl task optimize "<prompt>" [--fix] [--web] [--json]` | Linter & optimizer injecting Google Labs 3-phase exploration budgets, critic steering, and web oracles. | `0` (Scored/Fixed) |
375
392
  | `test-gen` | `agentctl test-gen --title <t> --spec <s> [--run]` | Scaffolds falsifiable unit tests, verifies **RED** failure state, and locks test in `scope.deny`. | `0` (Scaffolded/Red) |
376
393
  | `rollback` | `agentctl rollback [sessionId \| --latest]` | Restores exact commit, uncommitted files, and cleans orphan task worktrees from pre-flight checkpoints. | `0` (Restored), `1` (Error) |
377
394
  | `resume` | `agentctl resume <sessionId> --response "<reply>"` | Streams engineer response back into active Google Jules warm session context window. | `0` (Resumed), `1` (Error) |
378
- | `dispatch` | `agentctl dispatch --title <t> --prompt <p>` | Dispatches a single task to an AI agent in an isolated worktree. | `0` (Success), `1` (Arg error), `2` (429 Rate limit), `3` (Scope deny), `4` (OODA exhausted), `5` (Diff > 75KB), `6` (Secret leak) |
395
+ | `dispatch` | `agentctl dispatch --title <t> --prompt <p> [--role <name>] [--tier fast\|complex]` | Dispatches a single task to an AI agent in an isolated worktree, optionally binding a specialist role prompt (`overseer`\|`bolt`\|`sentinel`\|`janitor`) and/or overriding the Cost Router tier. | `0` (Success), `1` (Arg error), `2` (429 Rate limit), `3` (Scope deny), `4` (OODA exhausted), `5` (Diff > 75KB), `6` (Secret leak) |
379
396
  | `doctor` | `agentctl doctor [--interactive] [--fix safe]` | Diagnostic DAG check runner & automated transactional repair planner. | `0` (Healthy) |
380
- | `queue` | `agentctl queue [--interactive] [--json]` | Consumes, inspects, and executes task envelopes in `.agent/jules-queue/` (supports `--json`). | `0` (Complete) |
397
+ | `queue` | `agentctl queue [--interactive] [--dag] [--concurrency <n>] [--json]` | Consumes, inspects, and executes task envelopes in `.agent/jules-queue/`; `--dag` resolves inter-task dependencies via Kahn's algorithm with cycle detection instead of linear FIFO order (supports `--json`). | `0` (Complete) |
381
398
  | `swarm` | `agentctl swarm [--interactive] [--json]` | Runs parallel multi-agent swarm across worker slots with process PID liveness detection (supports `--json`). | `0` (Complete) |
382
399
  | `scan` | `agentctl scan` | Scans codebase for TODO/FIXME annotations to seed task authoring. | `0` (Scanned) |
383
400
  | `review-repair`| `agentctl review-repair <pr-comments.json>`| Parses GitHub PR review comments and synthesizes actionable OODA repair tasks. | `0` (Parsed), `1` (Missing file) |
@@ -386,9 +403,10 @@ Native stdio server exposing task dispatch, gate verification, and risk auditing
386
403
  | `bootstrap` | `agentctl bootstrap [--force] [--json]` | Inspects an untested repository and synthesizes `.agent/config.yml` with a zero-test verification oracle (`php -l`, `compileall`, `dotnet build`, `tsc`, `smoke`). | `0` (Bootstrapped / Existing) |
387
404
  | `lock` | `agentctl lock <acquire\|release\|status>`| Manages VFS mutex locks for multi-agent non-overlapping file ownership. | `0` (Locked/Released), `1` (Conflict) |
388
405
  | `clean` | `agentctl clean` | Prunes stale git worktrees, lockfiles, and temporary ledgers. | `0` (Clean) |
406
+ | `evidence` | `agentctl evidence <generate\|verify\|show> [--manifest <path>] [--json]` | Generates, verifies, or prints a SHA-256 cryptographic evidence manifest (changed-file hashes + test-file tamper lock) for audit trails. | `0` (Verified/Generated), `1` (Tamper detected / Verification failed) |
389
407
  | `mcp` | `agentctl mcp` | Starts stdio Model Context Protocol (MCP) server for tool integration. | `0` / Stdio stream |
390
408
  | `mcp init` | `agentctl mcp init [--target cursor\|vscode\|claude\|all]` | 1-click scaffolding for Cursor (`.cursor/mcp.json`), VS Code tasks (`tasks.json`), and Claude Desktop. | `0` (Scaffolded) |
391
- | `version` | `agentctl version` | Outputs orchestrator kit semantic version (`v0.32.4`). | `0` |
409
+ | `version` | `agentctl version` | Outputs orchestrator kit semantic version (`v0.32.5`). | `0` |
392
410
 
393
411
  <br/>
394
412
 
@@ -419,7 +437,23 @@ const result = await provider.dispatch(
419
437
  );
420
438
  ```
421
439
 
422
- ### 3. Model Context Protocol (MCP) Server
440
+ ### 3. Dynamic Complexity & Cost Router (`resolveRoutedProvider`)
441
+ Opt-in, config-driven routing between a cheap/fast provider and your primary provider — see [Configuration Reference](#configuration) for the `router:` block. Programmatic usage mirrors `createFailoverProvider`:
442
+
443
+ ```javascript
444
+ import { resolveRoutedProvider, loadConfig } from "jules-orchestrator-kit";
445
+
446
+ const config = loadConfig(process.cwd()); // router.enabled must be true in .agent/config.yml
447
+ const { provider, classification } = resolveRoutedProvider(
448
+ { title: "Fix typo", prompt: "Fix a typo in the README." },
449
+ config
450
+ );
451
+ console.log(classification.tier); // "fast" | "complex"
452
+ ```
453
+
454
+ Ships with a `gemini-flash` preset (Gemini CLI headless mode, `gemini-3.6-flash`) as a batteries-included fast tier, but any provider key or custom spec works for `router.fast`/`router.complex` — the router is provider-agnostic by design, not tied to any single vendor.
455
+
456
+ ### 4. Model Context Protocol (MCP) Server
423
457
  Expose orchestrator gates and queue controls over stdio to client tools (Antigravity, Claude, Cursor):
424
458
  ```bash
425
459
  npx jules-orchestrator-kit mcp
@@ -436,6 +470,8 @@ npx jules-orchestrator-kit mcp
436
470
 
437
471
  | Feature | Module / Command | Architectural Description | Target Release |
438
472
  | :--- | :--- | :--- | :---: |
473
+ | **Dynamic Complexity & Cost Router** | `src/router.mjs`, `router:` in `.agent/config.yml` | Provider-agnostic, zero-dependency heuristic classifier routing trivial tasks to a fast/cheap provider (`gemini-flash` preset included) and complex/safety-sensitive tasks to the primary provider; opt-in, `--tier` override. | **Unreleased** *(main)* |
474
+ | **DAG Task Queue, Specialist Roles & Evidence Ledger** | `src/dag-engine.mjs`, `src/evidence.mjs`, `agentctl evidence` | Kahn's-algorithm dependency-ordered queue execution (`queue --dag`), `--role` specialist prompt resolution, and SHA-256 cryptographic evidence manifests with test-tamper locking. | **Unreleased** *(main)* |
439
475
  | **Warm Session Resumption & PR Bundler** | `src/provider.mjs`, `src/engine.mjs` | Multi-turn warm session context streaming via `POST /v1alpha/sessions/{id}:sendMessage` & evidence PR descriptions. | **v0.31.0** *(Shipped)* |
440
476
  | **TDD Harness & Prompt Falsifiability Linter** | `agentctl test-gen`, `agentctl task optimize` | Automated RED-state test generator, `scope.deny` test locking, and prompt testability linter with fuzzy path resolution. | **v0.31.0** *(Shipped)* |
441
477
  | **Atomic Git Checkpoint & Rollback** | `agentctl rollback` (`src/ops/checkpoint.mjs`) | Pre-flight git HEAD/stash snapshotting, atomic rollback restoration, and 10-session pruning rotation. | **v0.31.0** *(Shipped)* |
package/bin/agentctl.mjs CHANGED
@@ -12,18 +12,18 @@ import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
12
12
  const args = process.argv.slice(2);
13
13
  const command = args[0];
14
14
 
15
- export const VERSION = "0.32.4";
15
+ export const VERSION = "0.32.5";
16
16
 
17
17
  export function printHelp() {
18
18
  console.log(`
19
- 🚀 agentctl v0.32.4 — Universal Agent Orchestrator & Safety Gatekeeper
19
+ 🚀 agentctl v0.32.5 — Universal Agent Orchestrator & Safety Gatekeeper
20
20
 
21
21
  Usage: agentctl <command> [options]
22
22
 
23
23
  Commands:
24
- dispatch | create Dispatch a single task to an AI agent
24
+ dispatch | create Dispatch a single task to an AI agent (--role <name>, --tier fast|complex)
25
25
  gate | audit Run CI security and verification gate against current branch
26
- queue Run pending task queue
26
+ queue Run pending task queue (--dag, --concurrency <n>)
27
27
  swarm Run parallel task swarm
28
28
  mcp Start stdio Model Context Protocol (MCP) server
29
29
  clean Clean stale branches, worktrees, locks, and ledgers
@@ -33,7 +33,7 @@ Commands:
33
33
  review-repair Parse PR review comments and synthesize OODA repair tasks
34
34
  dashboard Start local HTTP telemetry and audit dashboard
35
35
  init Scaffold .agent/ config and run onboarding wizard
36
- task create Interactively author and scope a Jules task envelope (--template <name>)
36
+ task create Interactively author and scope a Jules task envelope (--template <name>, --role <name>, --tier fast|complex)
37
37
  task optimize Linter & optimizer for Jules task prompts (--fix, --json, --web)
38
38
  task template List and generate web development task templates (--list, --json)
39
39
  test-gen Scaffold & run automated TDD Red-to-Green test cycle (--run)
@@ -45,9 +45,13 @@ Commands:
45
45
  hydrate [prompt] Prepend active system learnings and baton-pass state to a prompt
46
46
  harvest Harvest failure traces and record/quarantine resolution rules
47
47
  learning add Record a system learning rule into .agent/knowledge/
48
+ evidence <action> Manage cryptographic audit evidence (generate | verify | show)
48
49
  version Output agentctl version
49
50
 
50
51
  Options:
52
+ --role, -r Specify specialist agent role (overseer | bolt | sentinel | janitor)
53
+ --tier Force routing tier when router.enabled (fast | complex) — see .agent/config.yml router:
54
+ --dag Execute queue tasks via DAG dependency resolution
51
55
  --dry-run, -d Simulate action without making API calls or modifying git
52
56
  --mode, -m Gate evaluation mode (working-tree | committed | staged)
53
57
  --repoless Dispatch task in repoless execution mode
@@ -65,7 +69,7 @@ async function main() {
65
69
  }
66
70
 
67
71
  if (command === "version" || command === "--version" || command === "-v") {
68
- console.log("agentctl v0.32.4");
72
+ console.log("agentctl v0.32.5");
69
73
  process.exit(0);
70
74
  }
71
75
 
@@ -83,6 +87,8 @@ async function main() {
83
87
  title: { type: "string", short: "t" },
84
88
  prompt: { type: "string", short: "p" },
85
89
  "prompt-file": { type: "string", short: "f" },
90
+ role: { type: "string", short: "r" },
91
+ tier: { type: "string" },
86
92
  source: { type: "string", short: "s" },
87
93
  branch: { type: "string", short: "b" },
88
94
  repoless: { type: "boolean" },
@@ -111,6 +117,8 @@ async function main() {
111
117
  const task = {
112
118
  title: values.title || "CLI Dispatch Task",
113
119
  prompt: promptContent,
120
+ role: values.role,
121
+ tier: values.tier === "fast" || values.tier === "complex" ? values.tier : undefined,
114
122
  source: values.source,
115
123
  branch: values.branch,
116
124
  repoless: values.repoless,
@@ -133,6 +141,9 @@ async function main() {
133
141
  console.log(`\n✅ Task Dispatched Successfully!`);
134
142
  console.log(` Session ID : ${session.id}`);
135
143
  console.log(` Session URL : ${session.url || "N/A"}`);
144
+ if (session._routeTier) {
145
+ console.log(` Router Tier : ${session._routeTier} (${session._routeReason || "n/a"})`);
146
+ }
136
147
  }
137
148
  process.exit(0);
138
149
  } catch (err) {
@@ -202,17 +213,34 @@ async function main() {
202
213
  }
203
214
 
204
215
  case "queue": {
216
+ const { values } = parseArgs({
217
+ args: args.slice(1),
218
+ options: {
219
+ dag: { type: "boolean" },
220
+ concurrency: { type: "string", short: "c" },
221
+ "dry-run": { type: "boolean", short: "d" },
222
+ json: { type: "boolean", short: "j" },
223
+ },
224
+ allowPositionals: true,
225
+ });
226
+
205
227
  const queueDir = getQueueDir(root);
206
228
  const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
207
229
  console.log(`Found ${files.length} queued task(s) in .agent/queue/`);
208
230
  if (files.length > 0) {
209
- const tasks = files.map((f) => ({
210
- id: f,
211
- title: f.replace(/\.md$/, ""),
212
- prompt: readFileSync(join(queueDir, f), "utf-8"),
213
- }));
214
- const results = await run(tasks, { root, config });
215
- console.log(`\nProcessed ${results.length} tasks.`);
231
+ const concurrency = values.concurrency ? Number(values.concurrency) : undefined;
232
+ const results = await run(null, {
233
+ root,
234
+ config,
235
+ dag: values.dag,
236
+ concurrency,
237
+ dryRun: values["dry-run"],
238
+ });
239
+ if (values.json) {
240
+ console.log(JSON.stringify(results, null, 2));
241
+ } else {
242
+ console.log(`\nProcessed ${results.processed || results.results?.length || 0} task(s).`);
243
+ }
216
244
  }
217
245
  process.exit(0);
218
246
  break;
@@ -374,7 +402,11 @@ async function main() {
374
402
  options: {
375
403
  title: { type: "string", short: "t" },
376
404
  prompt: { type: "string", short: "p" },
405
+ role: { type: "string", short: "r" },
406
+ tier: { type: "string" },
377
407
  template: { type: "string" },
408
+ depends: { type: "string" },
409
+ "depends-on": { type: "string" },
378
410
  "verify-cmd": { type: "string", short: "v" },
379
411
  "auto-pr": { type: "boolean" },
380
412
  "require-plan-approval": { type: "boolean" },
@@ -388,7 +420,10 @@ async function main() {
388
420
  const res = await runTaskCreateWizard(root, {
389
421
  title: values.title,
390
422
  prompt: values.prompt,
423
+ role: values.role,
424
+ tier: values.tier,
391
425
  template: values.template,
426
+ dependsOn: values["depends-on"] || values.depends,
392
427
  verifyCmd: values["verify-cmd"],
393
428
  autoPr: values["auto-pr"],
394
429
  requirePlanApproval: values["require-plan-approval"],
@@ -401,6 +436,9 @@ async function main() {
401
436
  console.log(`✅ Task synthesized & queued at ${res.taskFile}`);
402
437
  console.log(` Task ID : ${res.plan.taskId}`);
403
438
  console.log(` Title : ${res.plan.title}`);
439
+ if (res.plan.role) console.log(` Role : ${res.plan.role}`);
440
+ if (res.plan.tier) console.log(` Tier : ${res.plan.tier} (routing override)`);
441
+ if (res.plan.dependsOn && res.plan.dependsOn.length > 0) console.log(` DependsOn: ${res.plan.dependsOn.join(", ")}`);
404
442
  console.log(` Auto-PR : ${res.plan.flags.autoPr}`);
405
443
  }
406
444
  process.exit(0);
@@ -750,6 +788,77 @@ async function main() {
750
788
  process.exit(1);
751
789
  }
752
790
 
791
+ case "evidence": {
792
+ const subAction = args[1] || "show";
793
+ const { planEvidenceGenerate, planEvidenceVerify, planEvidenceShow } = await import("../src/ops/evidence-actions.mjs");
794
+ const { values } = parseArgs({
795
+ args: args.slice(2),
796
+ options: {
797
+ output: { type: "string", short: "o" },
798
+ manifest: { type: "string", short: "m" },
799
+ markdown: { type: "string" },
800
+ json: { type: "boolean", short: "j" },
801
+ },
802
+ allowPositionals: true,
803
+ });
804
+
805
+ if (subAction === "generate" || subAction === "create") {
806
+ const res = planEvidenceGenerate(root, {
807
+ output: values.output,
808
+ markdownOutput: values.markdown,
809
+ });
810
+ if (values.json) {
811
+ console.log(JSON.stringify(res, null, 2));
812
+ } else {
813
+ console.log(`\n🛡️ Cryptographic Evidence Manifest Generated!`);
814
+ console.log(` Manifest ID : ${res.manifest.manifestId}`);
815
+ console.log(` Signature : ${res.manifest.evidenceHash}`);
816
+ console.log(` Location : ${res.manifestPath}`);
817
+ console.log(` Test Files : ${res.manifest.testIntegrity.testFileCount}`);
818
+ console.log(` Tampered : ${res.manifest.testIntegrity.tamperDetected ? "YES (FAILED)" : "NO (VERIFIED)"}\n`);
819
+ }
820
+ process.exit(res.manifest.testIntegrity.tamperDetected ? 1 : 0);
821
+ } else if (subAction === "verify" || subAction === "check") {
822
+ const res = planEvidenceVerify(root, {
823
+ manifest: values.manifest,
824
+ });
825
+ if (values.json) {
826
+ console.log(JSON.stringify(res, null, 2));
827
+ } else {
828
+ if (res.ok) {
829
+ console.log(`\n✅ Evidence Verification PASSED`);
830
+ console.log(` Manifest ID : ${res.manifestId}`);
831
+ console.log(` Signature : ${res.evidenceHash}\n`);
832
+ } else {
833
+ console.error(`\n❌ Evidence Verification FAILED`);
834
+ console.error(` Reason : ${res.reason}`);
835
+ if (res.details) {
836
+ console.error(` Details : ${JSON.stringify(res.details)}\n`);
837
+ }
838
+ }
839
+ }
840
+ process.exit(res.ok ? 0 : 1);
841
+ } else if (subAction === "show" || subAction === "print") {
842
+ const res = planEvidenceShow(root, {
843
+ manifest: values.manifest,
844
+ });
845
+ if (values.json) {
846
+ console.log(JSON.stringify(res, null, 2));
847
+ } else {
848
+ if (res.ok) {
849
+ console.log(`\n${res.markdown}\n`);
850
+ } else {
851
+ console.error(`\n❌ Failed to show evidence: ${res.reason}\n`);
852
+ }
853
+ }
854
+ process.exit(res.ok ? 0 : 1);
855
+ } else {
856
+ console.error(`Unknown evidence subaction: ${subAction}. Use generate | verify | show.`);
857
+ process.exit(1);
858
+ }
859
+ break;
860
+ }
861
+
753
862
  default:
754
863
  console.error(`Unknown command: ${command}`);
755
864
  printHelp();
package/index.mjs CHANGED
@@ -109,9 +109,24 @@ export { synthesizePrDescription, probeDevServer } from "./src/engine.mjs";
109
109
  // Automated TDD Red-to-Green Harness
110
110
  export { scaffoldTddTest, runTddCycle, TddError } from "./src/ops/tdd-generator.mjs";
111
111
 
112
- // IDE Native MCP Scaffolder
113
- export { scaffoldIdeConfig, IdeScaffoldError } from "./src/ops/ide-scaffold.mjs";
114
-
112
+ // SPORE Memory & System Learnings
113
+ export { recordLearning, loadLearnings, hydratePrompt, harvestFailure, getLearningsPath, getSystemLearningsMdPath } from "./src/memory.mjs";
115
114
 
115
+ // Specialist Role Resolution & DAG Queue Execution
116
+ export { resolveRolePrompt } from "./src/wizard-task.mjs";
117
+ export { executeQueueDag, resolveAffectedTests } from "./src/dag-engine.mjs";
116
118
 
119
+ // IDE Native MCP Scaffolder
120
+ export { scaffoldIdeConfig, IdeScaffoldError } from "./src/ops/ide-scaffold.mjs";
117
121
 
122
+ // Cryptographic Evidence & Test Integrity Manifests
123
+ export {
124
+ computeFileHash,
125
+ computeDirectoryHash,
126
+ generateEvidenceManifest,
127
+ writeEvidenceManifest,
128
+ loadEvidenceManifest,
129
+ verifyEvidenceManifest,
130
+ generateEvidenceMarkdown,
131
+ computeEvidenceHash,
132
+ } from "./src/evidence.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.32.4",
3
+ "version": "0.32.5",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,13 @@ export function parseYamlConfig(root = process.cwd()) {
22
22
  if (parsed && (parsed.test_cmd || parsed.build_cmd || parsed.verify)) {
23
23
  return {
24
24
  testCmd: parsed.verify?.test || parsed.test_cmd || "",
25
+ lintCmd: parsed.verify?.lint || parsed.lint_cmd || "",
26
+ fuzzCmd: parsed.verify?.fuzz || parsed.fuzz_cmd || "",
27
+ invariantCmd: parsed.verify?.invariant || parsed.invariant_cmd || "",
28
+ e2eCmd: parsed.verify?.e2e || parsed.e2e_cmd || "",
25
29
  buildCmd: parsed.verify?.build || parsed.build_cmd || "",
30
+ policy: parsed.verify?.policy || { networkAccess: "allow", offline: false },
31
+ stages: parsed.verify?.stages || null,
26
32
  source: configPath.endsWith("jules.yml") ? ".agent/jules.yml" : ".agent/config.yml",
27
33
  };
28
34
  }
@@ -50,8 +56,16 @@ export function detectFrameworkCommands(root = process.cwd()) {
50
56
  const res = detectStack(root);
51
57
  return {
52
58
  testCmd: res.testCmd,
59
+ lintCmd: res.fmtCmd || "",
60
+ fuzzCmd: res.fuzzCmd || "",
61
+ invariantCmd: res.invariantCmd || "",
62
+ e2eCmd: res.e2eCmd || "",
53
63
  buildCmd: res.buildCmd,
54
- source: `package.json (${res.stack})`,
64
+ policy: {
65
+ networkAccess: res.stack === "foundry" ? "forbidden" : "allow",
66
+ offline: res.stack === "foundry",
67
+ },
68
+ source: `${res.triggerFile || "stack"} (${res.stack})`,
55
69
  };
56
70
  }
57
71
 
package/src/config.mjs CHANGED
@@ -214,9 +214,19 @@ export function resolveVerify(root = process.cwd(), userVerify = {}) {
214
214
  const s = detectStack(root);
215
215
  return {
216
216
  setup: userVerify.setup ?? s.setupCmd ?? "",
217
+ lint: userVerify.lint ?? s.fmtCmd ?? "",
217
218
  test: userVerify.test ?? s.testCmd ?? "",
219
+ unit: userVerify.unit ?? userVerify.test ?? s.testCmd ?? "",
220
+ fuzz: userVerify.fuzz ?? s.fuzzCmd ?? "",
221
+ invariant: userVerify.invariant ?? s.invariantCmd ?? "",
222
+ e2e: userVerify.e2e ?? s.e2eCmd ?? "",
218
223
  teardown: userVerify.teardown ?? s.teardownCmd ?? "",
219
224
  build: userVerify.build ?? s.buildCmd ?? "",
225
+ policy: {
226
+ networkAccess: userVerify.policy?.networkAccess || (s.stack === "foundry" ? "forbidden" : "allow"),
227
+ offline: userVerify.policy?.offline ?? (s.stack === "foundry"),
228
+ },
229
+ stages: Array.isArray(userVerify.stages) ? userVerify.stages : null,
220
230
  server: userVerify.server ? {
221
231
  command: userVerify.server.command || "",
222
232
  url: userVerify.server.url || "http://localhost:3000",
@@ -273,6 +283,10 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
273
283
 
274
284
  const setupCmd = parsed.setup_cmd || parsed.verify?.setup || "";
275
285
  const testCmd = parsed.test_cmd || parsed.verify?.test || "";
286
+ const lintCmd = parsed.lint_cmd || parsed.verify?.lint || "";
287
+ const fuzzCmd = parsed.fuzz_cmd || parsed.verify?.fuzz || "";
288
+ const invariantCmd = parsed.invariant_cmd || parsed.verify?.invariant || "";
289
+ const e2eCmd = parsed.e2e_cmd || parsed.verify?.e2e || "";
276
290
  const teardownCmd = parsed.teardown_cmd || parsed.verify?.teardown || "";
277
291
  const buildCmd = parsed.build_cmd || parsed.verify?.build || "";
278
292
  const verifyTimeoutMs = parsed.verify?.timeoutMs ?? parsed.verify?.timeout_ms ?? 60000;
@@ -303,11 +317,28 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
303
317
  tier: activeTier,
304
318
  verify: {
305
319
  setup: setupCmd || autoVerify.setup || "",
320
+ lint: lintCmd || autoVerify.lint || "",
306
321
  test: testCmd || autoVerify.test,
322
+ unit: parsed.verify?.unit || testCmd || autoVerify.unit || autoVerify.test,
323
+ fuzz: fuzzCmd || autoVerify.fuzz || "",
324
+ invariant: invariantCmd || autoVerify.invariant || "",
325
+ e2e: e2eCmd || autoVerify.e2e || "",
307
326
  teardown: teardownCmd || autoVerify.teardown || "",
308
327
  build: buildCmd || autoVerify.build,
328
+ stages: parsed.verify?.stages || autoVerify.stages || null,
329
+ policy: parsed.verify?.policy || autoVerify.policy,
309
330
  timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 60000,
310
331
  },
332
+ evidence: {
333
+ enabled: parsed.evidence?.enabled ?? true,
334
+ strictTestLock: parsed.evidence?.strict_test_lock ?? parsed.evidence?.strictTestLock ?? true,
335
+ },
336
+ router: {
337
+ enabled: parsed.router?.enabled ?? false,
338
+ fast: parsed.router?.fast || "gemini-flash",
339
+ complex: parsed.router?.complex || "",
340
+ threshold: Number.isFinite(Number(parsed.router?.threshold)) ? Number(parsed.router.threshold) : 0,
341
+ },
311
342
  scope: normalizeScope(parsed),
312
343
  limits: {
313
344
  ...DEFAULTS.limits,
@@ -406,3 +406,121 @@ export function resolveAffectedTests(modifiedFiles = [], options = {}) {
406
406
  return affected.size > 0 ? Array.from(affected) : null;
407
407
  }
408
408
 
409
+ /**
410
+ * Loads tasks from the queue directory and executes them via DagExecutor.
411
+ * Supports task files with metadata (JSON or Markdown envelopes with dependsOn).
412
+ * @param {string} [root=process.cwd()]
413
+ * @param {Object} [options]
414
+ * @param {number} [options.concurrency=1]
415
+ * @param {boolean} [options.dryRun=false]
416
+ * @param {Function} [options.dispatchFn]
417
+ * @param {Object} [options.config]
418
+ * @returns {Promise<{ processed: number, results: Array<any> }>}
419
+ */
420
+ export async function executeQueueDag(root = process.cwd(), options = {}) {
421
+ const { readdirSync, renameSync, mkdirSync } = await import("node:fs");
422
+ const { join, basename } = await import("node:path");
423
+
424
+ const queueDir = join(root, ".agent", "jules-queue");
425
+ const fallbackQueueDir = join(root, ".agent", "queue");
426
+ const actualQueueDir = existsSync(queueDir) ? queueDir : (existsSync(fallbackQueueDir) ? fallbackQueueDir : queueDir);
427
+ const completedDir = join(actualQueueDir, "completed");
428
+
429
+ if (!existsSync(actualQueueDir)) {
430
+ return { processed: 0, results: [] };
431
+ }
432
+ if (!existsSync(completedDir)) {
433
+ try {
434
+ mkdirSync(completedDir, { recursive: true });
435
+ } catch (_) {}
436
+ }
437
+
438
+ const files = readdirSync(actualQueueDir).filter((f) => {
439
+ if (f === "completed" || f.startsWith(".")) return false;
440
+ return f.endsWith(".md") || f.endsWith(".json") || f.endsWith(".task");
441
+ });
442
+
443
+ if (files.length === 0) {
444
+ return { processed: 0, results: [] };
445
+ }
446
+
447
+ const executor = new DagExecutor({ concurrency: options.concurrency || 1 });
448
+ const taskMap = new Map();
449
+ const results = [];
450
+
451
+ for (const file of files) {
452
+ if (basename(file) !== file) continue;
453
+ const fullPath = join(actualQueueDir, file);
454
+ const content = readFileSync(fullPath, "utf-8");
455
+ let taskId = file.replace(/\.(md|json|task)$/, "");
456
+ let dependsOn = [];
457
+ let title = taskId;
458
+ let prompt = content;
459
+ let role = undefined;
460
+ let tier = undefined;
461
+
462
+ // Check envelope header
463
+ const match = content.match(/<!--\s*JULES_TASK_ENVELOPE:\s*({[\s\S]*?})\s*-->/);
464
+ if (match) {
465
+ try {
466
+ const meta = JSON.parse(match[1]);
467
+ if (meta.id) taskId = meta.id;
468
+ if (meta.title) title = meta.title;
469
+ if (meta.role) role = meta.role;
470
+ if (meta.tier) tier = meta.tier;
471
+ if (Array.isArray(meta.dependsOn)) dependsOn = meta.dependsOn;
472
+ else if (typeof meta.dependsOn === "string") dependsOn = meta.dependsOn.split(",").map((s) => s.trim()).filter(Boolean);
473
+ } catch (_) {}
474
+ } else if (file.endsWith(".json")) {
475
+ try {
476
+ const parsed = JSON.parse(content);
477
+ if (parsed.id) taskId = parsed.id;
478
+ if (parsed.title) title = parsed.title;
479
+ if (parsed.prompt) prompt = parsed.prompt;
480
+ if (parsed.role) role = parsed.role;
481
+ if (parsed.tier) tier = parsed.tier;
482
+ if (Array.isArray(parsed.dependsOn)) dependsOn = parsed.dependsOn;
483
+ else if (typeof parsed.dependsOn === "string") dependsOn = parsed.dependsOn.split(",").map((s) => s.trim()).filter(Boolean);
484
+ } catch (_) {}
485
+ }
486
+
487
+ taskMap.set(taskId, { file, fullPath, taskId, title, prompt, role, tier, dependsOn });
488
+ }
489
+
490
+ // Register each task with its dependencies that are part of this queue run
491
+ for (const [taskId, t] of taskMap.entries()) {
492
+ const validDeps = t.dependsOn.filter((dep) => taskMap.has(dep));
493
+ executor.addTask({
494
+ id: taskId,
495
+ dependsOn: validDeps,
496
+ runner: async () => {
497
+ const srcPath = t.fullPath;
498
+ const taskObj = { id: t.taskId, title: t.title, prompt: t.prompt, role: t.role, tier: t.tier };
499
+ let session;
500
+ if (typeof options.dispatchFn === "function") {
501
+ session = await options.dispatchFn(taskObj, { root, config: options.config, dryRun: options.dryRun });
502
+ } else {
503
+ const { dispatch } = await import("./engine.mjs");
504
+ session = await dispatch(taskObj, { root, config: options.config, dryRun: options.dryRun });
505
+ }
506
+
507
+ if (session && session.ok === false) {
508
+ results.push({ file: t.file, taskId: t.taskId, ok: false, status: session.status, error: session.error, session });
509
+ throw new Error(`DAG Task '${t.taskId}' failed: ${session.error || session.status || "Unknown error"}`);
510
+ } else {
511
+ const dstPath = join(completedDir, t.file);
512
+ if (existsSync(srcPath)) {
513
+ if (!existsSync(dstPath)) {
514
+ renameSync(srcPath, dstPath);
515
+ }
516
+ }
517
+ results.push({ file: t.file, taskId: t.taskId, ok: true, session });
518
+ }
519
+ },
520
+ });
521
+ }
522
+
523
+ await executor.execute({ concurrency: options.concurrency || 1, root });
524
+ return { processed: results.length, results };
525
+ }
526
+