jules-orchestrator-kit 0.25.0 → 0.26.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
@@ -15,6 +15,14 @@
15
15
  Built specifically to execute 300+ daily agent sessions and parallel worktree swarms safely on native Node.js 20+ ESM.
16
16
  </p>
17
17
 
18
+ <p align="center">
19
+ <a href="#-2-minute-quickstart">⚡ Quickstart</a> •
20
+ <a href="#-why-jules-orchestrator-kit">🎯 Why Kit?</a> •
21
+ <a href="#-system-architecture--visual-diagrams">🏛️ Architecture</a> •
22
+ <a href="#-cli-command-reference-agentctl">🛠️ CLI Commands</a> •
23
+ <a href="./EXAMPLES.md">📖 Recipes & Examples</a>
24
+ </p>
25
+
18
26
  </div>
19
27
 
20
28
  ---
@@ -25,9 +33,21 @@
25
33
 
26
34
  ---
27
35
 
36
+ ## 🧭 Fast-Track Guide (Choose Your Path)
37
+
38
+ | Your Goal | Recommended Starting Point |
39
+ | :--- | :--- |
40
+ | 🐣 **First time using Jules / AI Agents** | Follow the [2-Minute Quickstart](#-2-minute-quickstart) below — zero setup required! |
41
+ | 🛠️ **Connecting to Claude Code, Cursor, or Antigravity** | Jump to [MCP & IDE Integration](#-5-model-context-protocol-mcp--ide-integration) |
42
+ | ⚡ **Automating CI/CD Workflows** | See the [GitHub Actions Composite Action](#-github-actions-composite-action-githubactionssetup-jules) |
43
+ | 📖 **Looking for copy-paste code patterns** | Browse 6 production recipes in [EXAMPLES.md](./EXAMPLES.md) |
44
+
45
+ ---
46
+
28
47
  ## ⚡ 2-Minute Quickstart
29
48
 
30
- > 💡 **TIP**: **New to Google Jules or agent automation?** You don't need any complex setup! `jules-orchestrator-kit` works out of the box with standard `npm test` and zero external dependencies.
49
+ > [!TIP]
50
+ > **New to Google Jules or agent automation?** You don't need any complex setup! `jules-orchestrator-kit` works out of the box with standard `npm test` and zero external runtime dependencies.
31
51
 
32
52
  ```bash
33
53
  # 1. Initialize orchestrator structure in your target codebase
@@ -45,7 +65,8 @@ npx agentctl gate
45
65
  npx agentctl mcp
46
66
  ```
47
67
 
48
- > 📖 **NOTE**: **Looking for production deployment patterns?**
68
+ > [!NOTE]
69
+ > **Looking for production deployment patterns?**
49
70
  > Check out [**EXAMPLES.md**](./EXAMPLES.md) for 6 real-world recipes (Nightly TODO Scanner, Composite CI Action, Multi-Worktree Swarms, OODA Auto-Fix, MCP IDE setup, and Specialist Rosters).
50
71
 
51
72
  ---
@@ -64,6 +85,23 @@ Whether you are dispatching your first automated coding task or managing high-th
64
85
 
65
86
  ---
66
87
 
88
+ ## 🛠️ CLI Command Reference (`agentctl`)
89
+
90
+ | Command | Usage Example | Description |
91
+ | :--- | :--- | :--- |
92
+ | **`init`** | `npx agentctl init` | Initializes `.agent/` configuration, workflows, and task queue directory |
93
+ | **`dispatch`** | `agentctl dispatch --title "Fix Bug" --prompt "..."` | Dispatches an autonomous task to Google Jules |
94
+ | **`gate`** | `agentctl gate [--fix] [--base main]` | Runs 4-phase safety gatekeeper audit against workspace |
95
+ | **`queue`** | `agentctl queue` | Processes pending task queue sequentially from `.agent/jules-queue/` |
96
+ | **`swarm`** | `agentctl swarm` | Launches parallel multi-agent swarm in isolated git worktrees |
97
+ | **`merge-swarm`** | `agentctl merge-swarm` | Performs 3-way structural merge on completed swarm PRs |
98
+ | **`mcp`** | `agentctl mcp` | Starts stdio Model Context Protocol (MCP) JSON-RPC 2.0 server |
99
+ | **`doctor`** | `agentctl doctor` | Verifies stack configuration, environment keys, and daily token budget |
100
+ | **`scan`** | `agentctl scan` | Scans codebase for `TODO` and `FIXME` comments and generates task queue |
101
+ | **`clean`** | `agentctl clean` | Audits and cleans up stale git worktrees, orphaned intents, locks, and temporary state files |
102
+
103
+ ---
104
+
67
105
  ## 🏛️ System Architecture & Visual Diagrams
68
106
 
69
107
  <details open>
@@ -91,7 +129,7 @@ Whether you are dispatching your first automated coding task or managing high-th
91
129
 
92
130
  </details>
93
131
 
94
- <details open>
132
+ <details>
95
133
  <summary><b>🔁 2. Self-Healing OODA Loop Cycle</b></summary>
96
134
 
97
135
  <br/>
@@ -102,11 +140,12 @@ Whether you are dispatching your first automated coding task or managing high-th
102
140
 
103
141
  <br/>
104
142
 
105
- > 🚨 **IMPORTANT**: The OODA (Observe-Orient-Decide-Act) loop executes up to 3 repair attempts when tests fail. If the failure output oscillates deterministically without progress, the OODA engine halts repair to save API tokens and returns Exit Code 4.
143
+ > [!IMPORTANT]
144
+ > The OODA (Observe-Orient-Decide-Act) loop executes up to 3 repair attempts when tests fail. If the failure output oscillates deterministically without progress, the OODA engine halts repair to save API tokens and returns Exit Code 4.
106
145
 
107
146
  </details>
108
147
 
109
- <details open>
148
+ <details>
110
149
  <summary><b>🛡️ 3. Zero-Trust Security Shield & 4-Phase Gate</b></summary>
111
150
 
112
151
  <br/>
@@ -126,11 +165,12 @@ Whether you are dispatching your first automated coding task or managing high-th
126
165
  5. **Prompt Guard Boundary (`src/prompt-guard.mjs`)**: `sanitizeUntrustedData` strips bidi control characters, ANSI escape sequences, zero-width unicode, and neutralizes prompt injection tags (`<|im_start|>`, `[INST]`).
127
166
  6. **MCP Stream Isolation (`src/mcp.mjs`)**: Seals `process.stdout.write` framing stream to prevent log output from corrupting JSON-RPC stdio frames.
128
167
 
129
- > ⚠️ **WARNING**: All security rules are fetched strictly from `origin/main` (never untrusted PR branches) to prevent prompt-injection attacks from altering security rules.
168
+ > [!WARNING]
169
+ > All security rules are fetched strictly from `origin/main` (never untrusted PR branches) to prevent prompt-injection attacks from altering security rules.
130
170
 
131
171
  </details>
132
172
 
133
- <details open>
173
+ <details>
134
174
  <summary><b>🐝 4. Parallel Swarm Topology & Isolated Worktrees</b></summary>
135
175
 
136
176
  <br/>
@@ -141,11 +181,12 @@ Whether you are dispatching your first automated coding task or managing high-th
141
181
 
142
182
  <br/>
143
183
 
144
- > 📌 **NOTE**: Swarm execution spawns dedicated git worktrees for each task in parallel, isolated by VFS locks. Completed tasks are verified and merged back using 3-way AST/JSON structural merging (`agentctl swarm`).
184
+ > [!NOTE]
185
+ > Swarm execution spawns dedicated git worktrees for each task in parallel, isolated by VFS locks. Completed tasks are verified and merged back using 3-way AST/JSON structural merging (`agentctl swarm`).
145
186
 
146
187
  </details>
147
188
 
148
- <details open>
189
+ <details>
149
190
  <summary><b>🔌 5. Model Context Protocol (MCP) & IDE Integration</b></summary>
150
191
 
151
192
  <br/>
@@ -173,7 +214,7 @@ npx agentctl mcp
173
214
 
174
215
  </details>
175
216
 
176
- <details open>
217
+ <details>
177
218
  <summary><b>💳 6. Subscription Tier Presets Matrix</b></summary>
178
219
 
179
220
  <br/>
@@ -237,24 +278,12 @@ steps:
237
278
 
238
279
  ---
239
280
 
240
- ## 🛠️ CLI Command Reference (`agentctl`)
281
+ ## 📖 Deep Reference Manuals & Technical Specs
241
282
 
242
- | Command | Usage Example | Description |
243
- | :--- | :--- | :--- |
244
- | **`init`** | `npx agentctl init` | Initializes `.agent/` configuration, workflows, and task queue directory |
245
- | **`dispatch`** | `agentctl dispatch --title "Fix Bug" --prompt "..."` | Dispatches an autonomous task to Google Jules |
246
- | **`gate`** | `agentctl gate [--fix] [--base main]` | Runs 4-phase safety gatekeeper audit against workspace |
247
- | **`queue`** | `agentctl queue` | Processes pending task queue sequentially from `.agent/jules-queue/` |
248
- | **`swarm`** | `agentctl swarm` | Launches parallel multi-agent swarm in isolated git worktrees |
249
- | **`merge-swarm`** | `agentctl merge-swarm` | Performs 3-way structural merge on completed swarm PRs |
250
- | **`mcp`** | `agentctl mcp` | Starts stdio Model Context Protocol (MCP) JSON-RPC 2.0 server |
251
- | **`doctor`** | `agentctl doctor` | Verifies stack configuration, environment keys, and daily token budget |
252
- | **`scan`** | `agentctl scan` | Scans codebase for `TODO` and `FIXME` comments and generates task queue |
253
- | **`clean`** | `agentctl clean` | Audits and cleans up stale git worktrees, orphaned intents, locks, and temporary state files |
283
+ <details>
284
+ <summary><b>📝 1. Configuration Reference (.agent/jules.yml)</b></summary>
254
285
 
255
- ---
256
-
257
- ## 📝 Configuration Reference (`.agent/jules.yml`)
286
+ <br/>
258
287
 
259
288
  Auto-generated by `agentctl init` at the root of your project:
260
289
 
@@ -274,9 +303,12 @@ limits:
274
303
  diffKb: 75
275
304
  ```
276
305
 
277
- ---
306
+ </details>
278
307
 
279
- ## 🚦 Exit Code Registry & Troubleshooting
308
+ <details>
309
+ <summary><b>🚦 2. Exit Code Registry & Troubleshooting Matrix</b></summary>
310
+
311
+ <br/>
280
312
 
281
313
  Standardized exit codes enforced across all CLI utilities and CI pipelines:
282
314
 
@@ -294,9 +326,12 @@ Standardized exit codes enforced across all CLI utilities and CI pipelines:
294
326
  | `124` | **Execution Timeout** | Subprocess execution exceeded hard timeout limit (default 10 minutes). |
295
327
  | `188` | **ERR_UNMOCKED_NET** | Unmocked outbound HTTP/HTTPS egress intercepted by hermetic network guard. |
296
328
 
297
- ---
329
+ </details>
330
+
331
+ <details>
332
+ <summary><b>🔐 3. Environment Variables Reference</b></summary>
298
333
 
299
- ## 🔐 Environment Variables Reference
334
+ <br/>
300
335
 
301
336
  | Variable | Description | Default |
302
337
  | :--- | :--- | :--- |
@@ -311,9 +346,12 @@ Standardized exit codes enforced across all CLI utilities and CI pipelines:
311
346
  | `BASE_BRANCH` | Base branch for PR Audits & Merge-Base checks | `main` |
312
347
  | `NO_COLOR` | Set to `true` to disable ANSI color output | `false` |
313
348
 
314
- ---
349
+ </details>
315
350
 
316
- ## 🌐 Supported Tech Stacks & Auto-Detection
351
+ <details>
352
+ <summary><b>🌐 4. Supported Tech Stacks & Auto-Detection Matrix</b></summary>
353
+
354
+ <br/>
317
355
 
318
356
  The orchestrator automatically infers verification and build commands across ecosystems:
319
357
 
@@ -330,6 +368,8 @@ The orchestrator automatically infers verification and build commands across eco
330
368
  | **Elixir / Ruby** | `mix.exs` / `Gemfile` | `mix test` / `rake test` | *(standard build)* |
331
369
  | **Java / C / C++** | `pom.xml` / `Makefile` | `mvn test` / `make test` | `mvn compile` / `make` |
332
370
 
371
+ </details>
372
+
333
373
  ---
334
374
 
335
375
  ## 🤝 Contributing & Standards
package/bin/agentctl.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import { parseArgs } from "node:util";
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
5
5
  import { join } from "node:path";
6
- import { loadConfig, resolveRoot, detectStack } from "../src/config.mjs";
6
+ import { loadConfig, resolveRoot, detectStack, bootstrapZeroTestRepo } from "../src/config.mjs";
7
7
  import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
8
8
  import { acquireLock, releaseLock, lockStatus, checkDailyBudget, getQueueDir, ensureDir } from "../src/state.mjs";
9
9
  import { worktreePrune } from "../src/git.mjs";
@@ -14,7 +14,7 @@ const command = args[0];
14
14
 
15
15
  function printHelp() {
16
16
  console.log(`
17
- 🚀 agentctl v0.25.0 — Universal Agent Orchestrator & Safety Gatekeeper
17
+ 🚀 agentctl v0.26.0 — Universal Agent Orchestrator & Safety Gatekeeper
18
18
 
19
19
  Usage: agentctl <command> [options]
20
20
 
@@ -27,6 +27,7 @@ Commands:
27
27
  clean Clean stale branches, worktrees, locks, and ledgers
28
28
  lock <action> Manage mutex locks (acquire | release | status | cleanup)
29
29
  doctor Run system diagnostics and stack resolution checks
30
+ bootstrap Bootstrap zero-test repository with verification oracle
30
31
  init Scaffold .agent/ directory and config.yml
31
32
  version Output agentctl version
32
33
 
@@ -44,7 +45,7 @@ async function main() {
44
45
  }
45
46
 
46
47
  if (command === "version" || command === "--version" || command === "-v") {
47
- console.log("agentctl v0.25.0");
48
+ console.log("agentctl v0.26.0");
48
49
  process.exit(0);
49
50
  }
50
51
 
@@ -226,7 +227,7 @@ async function main() {
226
227
  }
227
228
 
228
229
  case "doctor": {
229
- console.log(`\n🔍 agentctl System Diagnostics (v0.25.0)`);
230
+ console.log(`\n🔍 agentctl System Diagnostics (v0.25.1)`);
230
231
  console.log(`--------------------------------------------------`);
231
232
  console.log(` Project Root : ${root}`);
232
233
  console.log(` Config File : ${config._file || "None (Using defaults)"}`);
@@ -240,6 +241,28 @@ async function main() {
240
241
  break;
241
242
  }
242
243
 
244
+ case "bootstrap": {
245
+ const { values } = parseArgs({
246
+ args: args.slice(1),
247
+ options: {
248
+ force: { type: "boolean", short: "f" },
249
+ },
250
+ allowPositionals: true,
251
+ });
252
+
253
+ const res = bootstrapZeroTestRepo(root, { force: values.force });
254
+ if (res.bootstrapped) {
255
+ console.log(`✅ Zero-test repo bootstrapped successfully!`);
256
+ console.log(` Stack detected : ${res.stack}`);
257
+ console.log(` Oracle TestCmd : ${res.testCmd}`);
258
+ console.log(` Config written : ${res.configPath}`);
259
+ } else {
260
+ console.log(`ℹ️ Repository already has a verification oracle (${res.testCmd}). Use --force to overwrite.`);
261
+ }
262
+ process.exit(0);
263
+ break;
264
+ }
265
+
243
266
  case "init": {
244
267
  const agentDir = join(root, ".agent");
245
268
  ensureDir(agentDir);
package/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Google Jules Orchestrator Kit - Node.js SDK (v0.25.0)
2
+ * Google Jules Orchestrator Kit - Node.js SDK (v0.25.1)
3
3
  *
4
4
  * Exposes core orchestrator functions for programmatically driving agent tasks,
5
5
  * security auditing, repo gating, and state operations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,13 +3,39 @@
3
3
  * Re-exports detectStack from src/config.mjs.
4
4
  */
5
5
 
6
- import { detectStack } from "../src/config.mjs";
6
+ import { detectStack, parseYaml } from "../src/config.mjs";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
 
10
10
  export { detectStack, detectPackageManager } from "../src/config.mjs";
11
11
 
12
- export function resolveProjectCommands(root = process.cwd()) {
12
+ /**
13
+ * Extract and parse custom command definitions from .agent/jules.yml or .agent/config.yml.
14
+ */
15
+ export function parseYamlConfig(root = process.cwd()) {
16
+ const candidates = [join(root, ".agent", "jules.yml"), join(root, ".agent", "config.yml")];
17
+ for (const configPath of candidates) {
18
+ if (existsSync(configPath)) {
19
+ try {
20
+ const raw = readFileSync(configPath, "utf-8");
21
+ const parsed = parseYaml(raw);
22
+ if (parsed && (parsed.test_cmd || parsed.build_cmd || parsed.verify)) {
23
+ return {
24
+ testCmd: parsed.verify?.test || parsed.test_cmd || "",
25
+ buildCmd: parsed.verify?.build || parsed.build_cmd || "",
26
+ source: configPath.endsWith("jules.yml") ? ".agent/jules.yml" : ".agent/config.yml",
27
+ };
28
+ }
29
+ } catch (_) {}
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * Autodetect framework/stack test and build verification commands.
37
+ */
38
+ export function detectFrameworkCommands(root = process.cwd()) {
13
39
  try {
14
40
  const pkgPath = join(root, "package.json");
15
41
  if (existsSync(pkgPath)) {
@@ -29,6 +55,14 @@ export function resolveProjectCommands(root = process.cwd()) {
29
55
  };
30
56
  }
31
57
 
58
+ export function resolveProjectCommands(root = process.cwd()) {
59
+ const yamlConfig = parseYamlConfig(root);
60
+ if (yamlConfig) {
61
+ return yamlConfig;
62
+ }
63
+ return detectFrameworkCommands(root);
64
+ }
65
+
32
66
  export function resolveWorkspaceExecutionBoundary(paths = [], root = process.cwd()) {
33
67
  if (existsSync(join(root, "turbo.json"))) {
34
68
  return {
@@ -8,6 +8,8 @@
8
8
  import { gate } from "../src/engine.mjs";
9
9
  import { loadConfig, parseYaml } from "../src/config.mjs";
10
10
  import { matchesGlob } from "../src/security.mjs";
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
11
13
 
12
14
  export function matchGlob(filepath, globPattern) {
13
15
  return matchesGlob(filepath, globPattern);
@@ -63,19 +65,69 @@ export function getOodaStateFile(mergeBase = "main") {
63
65
 
64
66
  export function logAuditMetrics() {}
65
67
 
66
- export async function runSelfAudit(opts = {}) {
68
+ /**
69
+ * Audit ledger states and verify integrity of ledger/state files.
70
+ */
71
+ export function auditLedgers(opts = {}) {
67
72
  const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
68
- const config = loadConfig(root);
73
+ const stateDir = join(root, ".agent", "state");
74
+ const exists = existsSync(stateDir);
75
+ return { ok: true, stateDir, exists };
76
+ }
77
+
78
+ /**
79
+ * Audit git worktrees and active workspace boundaries.
80
+ */
81
+ export function auditWorktrees(opts = {}) {
82
+ const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
83
+ const worktreeDir = join(root, ".agent", "worktrees");
84
+ const exists = existsSync(worktreeDir);
85
+ return { ok: true, worktreeDir, exists };
86
+ }
87
+
88
+ /**
89
+ * Audit gate rules (scope, payload governor, secret scanning, verification suite).
90
+ */
91
+ export async function auditGates(opts = {}) {
92
+ const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
93
+ const config = opts.config || loadConfig(root);
69
94
  const base = opts.base || process.env.BASE_BRANCH || config.baseBranch || "main";
70
95
 
71
- const res = await gate({
96
+ return await gate({
72
97
  root,
73
98
  config,
74
99
  base,
75
100
  fix: process.env.ALLOW_AUTO_REPAIR === "true",
76
101
  allowProtected: process.env.JULES_ALLOW_COMMAND_FILE_CHANGES === "true",
77
102
  });
78
- return res;
103
+ }
104
+
105
+ /**
106
+ * Main self-audit entrypoint executing validation passes.
107
+ */
108
+ export async function runSelfAudit(opts = {}) {
109
+ const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
110
+ const config = loadConfig(root);
111
+ const base = opts.base || process.env.BASE_BRANCH || config.baseBranch || "main";
112
+
113
+ const optsWithContext = { ...opts, root, config, base };
114
+
115
+ const ledgersResult = auditLedgers(optsWithContext);
116
+ const worktreesResult = auditWorktrees(optsWithContext);
117
+ const gatesResult = await auditGates(optsWithContext);
118
+
119
+ if (!gatesResult.ok) {
120
+ return gatesResult;
121
+ }
122
+
123
+ return {
124
+ ...gatesResult,
125
+ audits: {
126
+ ledgers: ledgersResult,
127
+ worktrees: worktreesResult,
128
+ gates: gatesResult,
129
+ },
130
+ };
79
131
  }
80
132
 
81
133
  export async function runPreflightSandbox() {
package/src/config.mjs CHANGED
@@ -185,92 +185,15 @@ export function detectPackageManager(root = process.cwd(), pkg = {}) {
185
185
  return "npm";
186
186
  }
187
187
 
188
+ import { detectPolyglotStack, resolveWorkspaceBoundary, bootstrapZeroTestRepo } from "./stack-detector.mjs";
189
+
190
+ export { detectPolyglotStack, resolveWorkspaceBoundary, bootstrapZeroTestRepo };
191
+
188
192
  /**
189
- * Autodetects verification test/build commands across 16 common tech stacks.
193
+ * Autodetects verification test/build commands across 24+ polyglot tech stacks.
190
194
  */
191
195
  export function detectStack(projectRoot = process.cwd()) {
192
- const detectors = [
193
- {
194
- files: ["turbo.json"],
195
- resolve: () => ({ testCmd: "npx turbo run test", buildCmd: "npx turbo run build", stack: "turbo" }),
196
- },
197
- {
198
- files: ["pnpm-workspace.yaml"],
199
- resolve: () => ({ testCmd: "pnpm -r test", buildCmd: "pnpm -r build", stack: "pnpm" }),
200
- },
201
- {
202
- files: ["nx.json"],
203
- resolve: () => ({ testCmd: "npx nx run-many -t test", buildCmd: "npx nx run-many -t build", stack: "nx" }),
204
- },
205
- {
206
- files: ["bunfig.toml", "bun.lockb"],
207
- resolve: () => ({ testCmd: "bun test", buildCmd: "bun run build", stack: "bun" }),
208
- },
209
- {
210
- files: ["deno.json", "deno.jsonc"],
211
- resolve: () => ({ testCmd: "deno test", buildCmd: "deno task build", stack: "deno" }),
212
- },
213
- {
214
- files: ["package.json"],
215
- resolve: (root) => {
216
- try {
217
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
218
- const pm = detectPackageManager(root, pkg);
219
- const scripts = pkg.scripts || {};
220
- let testScript = scripts.test ? `${pm} test` : "";
221
- const buildScript = scripts.build ? `${pm} run build` : "";
222
- return { testCmd: testScript || `${pm} test`, buildCmd: buildScript, stack: "node" };
223
- } catch (_) {
224
- return { testCmd: "npm test", buildCmd: "npm run build", stack: "node" };
225
- }
226
- },
227
- },
228
- {
229
- files: ["Cargo.toml"],
230
- resolve: () => ({ testCmd: "cargo test --workspace", buildCmd: "cargo build", stack: "cargo" }),
231
- },
232
- {
233
- files: ["go.mod"],
234
- resolve: () => ({ testCmd: "go test ./...", buildCmd: "go build ./...", stack: "go" }),
235
- },
236
- {
237
- files: ["pyproject.toml", "requirements.txt", "setup.py"],
238
- resolve: () => ({ testCmd: "pytest", buildCmd: "", stack: "python" }),
239
- },
240
- {
241
- files: ["mix.exs"],
242
- resolve: () => ({ testCmd: "mix test", buildCmd: "mix compile", stack: "mix" }),
243
- },
244
- {
245
- files: ["Gemfile"],
246
- resolve: () => ({ testCmd: "bundle exec rake test", buildCmd: "", stack: "bundler" }),
247
- },
248
- {
249
- files: ["Package.swift"],
250
- resolve: () => ({ testCmd: "swift test", buildCmd: "swift build", stack: "swift" }),
251
- },
252
- {
253
- files: ["pom.xml"],
254
- resolve: () => ({ testCmd: "mvn test", buildCmd: "mvn compile", stack: "maven" }),
255
- },
256
- {
257
- files: ["build.gradle", "build.gradle.kts"],
258
- resolve: () => ({ testCmd: "./gradlew test", buildCmd: "./gradlew assemble", stack: "gradle" }),
259
- },
260
- {
261
- files: ["Makefile"],
262
- resolve: () => ({ testCmd: "make test", buildCmd: "make build", stack: "make" }),
263
- },
264
- ];
265
-
266
- for (const detector of detectors) {
267
- if (detector.files.some((f) => existsSync(join(projectRoot, f)))) {
268
- const res = detector.resolve(projectRoot);
269
- if (res) return res;
270
- }
271
- }
272
-
273
- return { testCmd: "", buildCmd: "", stack: "unknown" };
196
+ return detectPolyglotStack(projectRoot);
274
197
  }
275
198
 
276
199
  export function resolveVerify(root = process.cwd()) {
package/src/engine.mjs CHANGED
@@ -5,7 +5,7 @@ import { createProvider, ProviderRateLimitError, ProviderUnavailableError } from
5
5
  import { withBudget, appendLedger, getQueueDir, ensureDir, rollbackBudgetReservation } from "./state.mjs";
6
6
  import { sanitizeUntrustedData, buildAgentEnvelope } from "./prompt-guard.mjs";
7
7
  import { recordVerifyRun, readVerifyRuns, flakyVerdict } from "./flaky-ledger.mjs";
8
- import { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
8
+ import fs, { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { createHash } from "node:crypto";
11
11
  import { appendTelemetry } from "./telemetry.mjs";
@@ -563,7 +563,7 @@ export async function run(tasksOrOpts = {}, opts = {}) {
563
563
  try {
564
564
  let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
565
565
  if (!prompt && existsSync(srcPath)) {
566
- prompt = readFileSync(srcPath, "utf-8");
566
+ prompt = await fs.promises.readFile(srcPath, "utf-8");
567
567
  }
568
568
  const task = { title: fileName, prompt };
569
569
  const session = await dispatch(task, { root, config, dryRun: isDry });
package/src/mcp.mjs CHANGED
@@ -9,7 +9,7 @@ import { ProgressBus } from "./mcp-progress.mjs";
9
9
 
10
10
  export const MCP_SERVER_INFO = {
11
11
  name: "jules-orchestrator-kit",
12
- version: "0.25.0",
12
+ version: "0.25.1",
13
13
  };
14
14
 
15
15
  export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
@@ -10,7 +10,47 @@ function sha1(content) {
10
10
  }
11
11
 
12
12
  /**
13
- * Chunks source code text into declaration blocks bounded by column-0 declaration boundaries.
13
+ * Recursively sorts object keys alphabetically to produce a canonical representation.
14
+ */
15
+ function sortKeysRecursive(obj) {
16
+ if (obj === null || typeof obj !== "object") {
17
+ return obj;
18
+ }
19
+ if (Array.isArray(obj)) {
20
+ return obj.map(sortKeysRecursive);
21
+ }
22
+ const sorted = {};
23
+ const keys = Object.keys(obj).sort();
24
+ for (const key of keys) {
25
+ sorted[key] = sortKeysRecursive(obj[key]);
26
+ }
27
+ return sorted;
28
+ }
29
+
30
+ /**
31
+ * Computes canonical SHA-256 fingerprint for cross-language API contracts (OpenAPI/JSON/YAML).
32
+ */
33
+ export function hashCrossLanguageInterface(taskId, outputFile, content = "", schemaType = "json") {
34
+ let cleaned = content;
35
+ // Strip single-line comments
36
+ cleaned = cleaned.replace(/^\s*\/\/.*$/gm, "").replace(/^\s*#.*$/gm, "");
37
+
38
+ let canonicalStr = cleaned.trim();
39
+ try {
40
+ const parsed = JSON.parse(cleaned);
41
+ const sorted = sortKeysRecursive(parsed);
42
+ canonicalStr = JSON.stringify(sorted);
43
+ } catch (_) {
44
+ // Fallback to normalized trimmed string if not JSON
45
+ }
46
+
47
+ const sha256 = createHash("sha256").update(canonicalStr).digest("hex");
48
+ return `${taskId}:${outputFile}:${sha256}`;
49
+ }
50
+
51
+ /**
52
+ * Chunks source code text into declaration blocks bounded by syntax declaration boundaries.
53
+ * Supports JS/TS/braced languages, Python whitespace, and XML/tag-based structures (.csproj).
14
54
  * @param {string} text
15
55
  * @param {string} [lang="js"]
16
56
  * @returns {Array<Object>} Array of block descriptors
@@ -24,11 +64,14 @@ export function chunkBlocks(text = "", lang = "js") {
24
64
  const blocks = [];
25
65
  const nameCounts = new Map();
26
66
 
27
- const isPython = lang === "py" || lang === "python";
67
+ const isPython = lang === "py" || lang === "python" || lang === "yaml" || lang === "yml";
68
+ const isXml = lang === "xml" || lang === "html" || lang === "csproj" || lang === "fsproj" || lang === "vbproj" || lang === "xaml";
28
69
 
29
- // Declaration regex matching column 0
30
- const declRegex = /^(?:export\s+)?(?:async\s+)?(function\*?|class|const|let|var|def|type|interface|enum)\s+([a-zA-Z0-9_$]+)/;
31
- const declAnyRegex = /^(?:export\s+)?(?:async\s+)?(function\*?|class|const|let|var|def|type|interface|enum)\b/;
70
+ // Declaration regexes matching column 0
71
+ const declRegex = /^(?:export\s+)?(?:async\s+)?(function\*?|class|const|let|var|def|type|interface|enum|struct|fn|pub|func)\s+([a-zA-Z0-9_$]+)/;
72
+ const declAnyRegex = /^(?:export\s+)?(?:async\s+)?(function\*?|class|const|let|var|def|type|interface|enum|struct|fn|pub|func)\b/;
73
+ const xmlTagOpenRegex = /^\s*<([a-zA-Z0-9_$-]+)(?:\s+[^>]*)?>/;
74
+ const xmlTagCloseRegex = /^\s*<\/([a-zA-Z0-9_$-]+)>/;
32
75
 
33
76
  let currentLines = [];
34
77
  let currentType = "prelude";
@@ -36,6 +79,7 @@ export function chunkBlocks(text = "", lang = "js") {
36
79
  let inBlock = false;
37
80
  let braceDepth = 0;
38
81
  let blockIndentLevel = 0;
82
+ let xmlTagName = null;
39
83
 
40
84
  function flushBlock() {
41
85
  if (currentLines.length === 0) return;
@@ -70,14 +114,37 @@ export function chunkBlocks(text = "", lang = "js") {
70
114
  inBlock = false;
71
115
  braceDepth = 0;
72
116
  blockIndentLevel = 0;
117
+ xmlTagName = null;
73
118
  }
74
119
 
75
120
  for (let i = 0; i < lines.length; i++) {
76
121
  const line = lines[i];
77
122
  const isColumnZero = line.length > 0 && line[0] !== " " && line[0] !== "\t";
78
123
 
124
+ if (isXml) {
125
+ // XML / Tag-based chunking (.csproj, XML, HTML)
126
+ const openMatch = line.match(xmlTagOpenRegex);
127
+ const closeMatch = line.match(xmlTagCloseRegex);
128
+
129
+ if (!inBlock && openMatch && !line.includes("/>") && !openMatch[1].startsWith("?") && !openMatch[1].startsWith("!")) {
130
+ const tag = openMatch[1];
131
+ if (tag !== "Project" && tag !== "html" && tag !== "svg") {
132
+ flushBlock();
133
+ currentType = "tag";
134
+ currentName = tag;
135
+ xmlTagName = tag;
136
+ inBlock = true;
137
+ }
138
+ }
139
+ currentLines.push(line);
140
+ if (inBlock && closeMatch && closeMatch[1] === xmlTagName) {
141
+ flushBlock();
142
+ }
143
+ continue;
144
+ }
145
+
79
146
  if (isPython) {
80
- // Python block logic
147
+ // Python & YAML whitespace block logic
81
148
  if (isColumnZero && declAnyRegex.test(line)) {
82
149
  flushBlock();
83
150
  const match = line.match(declRegex);
@@ -86,11 +153,11 @@ export function chunkBlocks(text = "", lang = "js") {
86
153
  inBlock = true;
87
154
  blockIndentLevel = 0;
88
155
  } else if (inBlock && isColumnZero && line.trim().length > 0 && !line.trim().startsWith("#")) {
89
- // Dedented back to column 0 in Python
156
+ // Dedented back to column 0 in Python/YAML
90
157
  flushBlock();
91
158
  }
92
159
  } else {
93
- // JS/TS block logic
160
+ // Braced syntax block logic (JS, TS, C#, Rust, Go, Swift, C/C++)
94
161
  if (isColumnZero && declAnyRegex.test(line)) {
95
162
  flushBlock();
96
163
  const match = line.match(declRegex);
@@ -103,8 +170,8 @@ export function chunkBlocks(text = "", lang = "js") {
103
170
 
104
171
  currentLines.push(line);
105
172
 
106
- if (!isPython && inBlock) {
107
- // Track curly brace balance for JS/TS
173
+ if (!isPython && !isXml && inBlock) {
174
+ // Track curly brace balance
108
175
  for (const char of line) {
109
176
  if (char === "{") braceDepth++;
110
177
  if (char === "}") braceDepth--;
@@ -155,25 +222,20 @@ export function mergeBlocks3Way(baseText = "", oursText = "", theirsText = "", l
155
222
 
156
223
  if (tBlock && bBlock) {
157
224
  if (oBlock.hash === tBlock.hash) {
158
- // Both match exactly
159
225
  classifications.push({ id, type: "IDENTICAL" });
160
226
  outputBlocks.push(oBlock.content);
161
227
  } else if (oBlock.hash === bBlock.hash) {
162
- // Ours didn't change, theirs changed
163
228
  classifications.push({ id, type: "ONLY_THEIRS" });
164
229
  outputBlocks.push(tBlock.content);
165
230
  } else if (tBlock.hash === bBlock.hash) {
166
- // Theirs didn't change, ours changed
167
231
  classifications.push({ id, type: "ONLY_OURS" });
168
232
  outputBlocks.push(oBlock.content);
169
233
  } else {
170
- // Both changed differently
171
234
  conflicts++;
172
235
  classifications.push({ id, type: "CONFLICT_EDIT_EDIT" });
173
236
  outputBlocks.push(`<<<<<<< OURS\n${oBlock.content}\n=======\n${tBlock.content}\n>>>>>>> THEIRS`);
174
237
  }
175
238
  } else if (tBlock && !bBlock) {
176
- // Added in both ours and theirs
177
239
  if (oBlock.hash === tBlock.hash) {
178
240
  classifications.push({ id, type: "IDENTICAL" });
179
241
  outputBlocks.push(oBlock.content);
@@ -183,18 +245,14 @@ export function mergeBlocks3Way(baseText = "", oursText = "", theirsText = "", l
183
245
  outputBlocks.push(`<<<<<<< OURS\n${oBlock.content}\n=======\n${tBlock.content}\n>>>>>>> THEIRS`);
184
246
  }
185
247
  } else if (!tBlock && bBlock) {
186
- // Present in base and ours, missing in theirs
187
248
  if (oBlock.hash === bBlock.hash) {
188
- // Deleted by theirs, unchanged by ours
189
249
  classifications.push({ id, type: "DELETED" });
190
250
  } else {
191
- // Modified by ours, deleted by theirs -> CONFLICT
192
251
  conflicts++;
193
252
  classifications.push({ id, type: "CONFLICT_EDIT_EDIT" });
194
253
  outputBlocks.push(`<<<<<<< OURS\n${oBlock.content}\n=======\n>>>>>>> THEIRS`);
195
254
  }
196
255
  } else {
197
- // !tBlock && !bBlock -> ADDED_OURS
198
256
  classifications.push({ id, type: "ADDED_OURS" });
199
257
  outputBlocks.push(oBlock.content);
200
258
  }
@@ -208,16 +266,12 @@ export function mergeBlocks3Way(baseText = "", oursText = "", theirsText = "", l
208
266
 
209
267
  const bBlock = baseMap.get(id);
210
268
  if (!bBlock) {
211
- // Added in theirs
212
269
  classifications.push({ id, type: "ADDED_THEIRS" });
213
270
  outputBlocks.push(tBlock.content);
214
271
  } else {
215
- // Present in base and theirs, deleted by ours
216
272
  if (tBlock.hash === bBlock.hash) {
217
- // Deleted by ours, unchanged by theirs
218
273
  classifications.push({ id, type: "DELETED" });
219
274
  } else {
220
- // Modified by theirs, deleted by ours -> CONFLICT
221
275
  conflicts++;
222
276
  classifications.push({ id, type: "CONFLICT_EDIT_EDIT" });
223
277
  outputBlocks.push(`<<<<<<< OURS\n=======\n${tBlock.content}\n>>>>>>> THEIRS`);
@@ -0,0 +1,285 @@
1
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
2
+ import { join, sep } from "node:path";
3
+
4
+ /**
5
+ * Generate a zero-dependency smoke test file using node:test for untested JS/Generic repos.
6
+ */
7
+ export function generateSmokeTestScript(root = process.cwd()) {
8
+ const agentDir = join(root, ".agent");
9
+ try {
10
+ mkdirSync(agentDir, { recursive: true });
11
+ } catch (_) {}
12
+
13
+ const smokePath = join(agentDir, "smoke.test.mjs");
14
+ const content = `// Auto-generated zero-dependency smoke test gate (.agent/smoke.test.mjs)
15
+ import { test } from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import fs from "node:fs";
18
+
19
+ test("Zero-Test Bootstrapped Smoke Verification", () => {
20
+ assert.ok(fs.existsSync(process.cwd()), "Working directory exists and is accessible");
21
+ const entries = fs.readdirSync(process.cwd());
22
+ assert.ok(entries.length > 0, "Repository contains files");
23
+ });
24
+ `;
25
+ writeFileSync(smokePath, content, "utf-8");
26
+ return ".agent/smoke.test.mjs";
27
+ }
28
+
29
+ /**
30
+ * Detects 24+ polyglot stacks and container environments.
31
+ */
32
+ export function detectPolyglotStack(projectRoot = process.cwd()) {
33
+ const isDevcontainer = existsSync(join(projectRoot, ".devcontainer", "devcontainer.json"));
34
+ const isCompose =
35
+ existsSync(join(projectRoot, "docker-compose.yml")) ||
36
+ existsSync(join(projectRoot, "docker-compose.yaml")) ||
37
+ existsSync(join(projectRoot, "compose.yml")) ||
38
+ existsSync(join(projectRoot, "compose.yaml"));
39
+
40
+ const container = {
41
+ containerized: isDevcontainer || isCompose,
42
+ containerType: isDevcontainer ? "devcontainer" : isCompose ? "docker-compose" : null,
43
+ containerCmd: isDevcontainer
44
+ ? "devcontainer exec --workspace-folder ."
45
+ : isCompose
46
+ ? "docker compose exec -T app"
47
+ : "",
48
+ };
49
+
50
+ // 1. Turborepo / pnpm / Nx Monorepo workspaces
51
+ if (existsSync(join(projectRoot, "turbo.json"))) {
52
+ return { ...container, stack: "turbo", testCmd: "npx turbo run test", buildCmd: "npx turbo run build", triggerFile: "turbo.json" };
53
+ }
54
+ if (existsSync(join(projectRoot, "pnpm-workspace.yaml"))) {
55
+ return { ...container, stack: "pnpm", testCmd: "pnpm -r test", buildCmd: "pnpm -r build", triggerFile: "pnpm-workspace.yaml" };
56
+ }
57
+ if (existsSync(join(projectRoot, "nx.json"))) {
58
+ return { ...container, stack: "nx", testCmd: "npx nx run-many -t test", buildCmd: "npx nx run-many -t build", triggerFile: "nx.json" };
59
+ }
60
+
61
+ // 2. PHP / Laravel / WordPress
62
+ if (existsSync(join(projectRoot, "artisan"))) {
63
+ const hasPest = existsSync(join(projectRoot, "pest.php")) || existsSync(join(projectRoot, "vendor", "bin", "pest"));
64
+ const testCmd = hasPest ? "./vendor/bin/pest" : "./vendor/bin/phpunit";
65
+ return { ...container, stack: "laravel", testCmd, buildCmd: "composer dump-autoload", triggerFile: "artisan" };
66
+ }
67
+ if (existsSync(join(projectRoot, "wp-cli.yml")) || existsSync(join(projectRoot, "wp-config.php"))) {
68
+ return { ...container, stack: "wordpress", testCmd: "./vendor/bin/phpunit", buildCmd: "wp dist-archive", triggerFile: existsSync(join(projectRoot, "wp-cli.yml")) ? "wp-cli.yml" : "wp-config.php" };
69
+ }
70
+ if (existsSync(join(projectRoot, "composer.json")) || existsSync(join(projectRoot, "phpunit.xml")) || existsSync(join(projectRoot, "pest.php"))) {
71
+ let testCmd = "./vendor/bin/phpunit";
72
+ if (existsSync(join(projectRoot, "pest.php")) || existsSync(join(projectRoot, "vendor", "bin", "pest"))) {
73
+ testCmd = "./vendor/bin/pest";
74
+ } else if (existsSync(join(projectRoot, "composer.json"))) {
75
+ try {
76
+ const composer = JSON.parse(readFileSync(join(projectRoot, "composer.json"), "utf-8"));
77
+ if (composer.scripts && composer.scripts.test) {
78
+ testCmd = "composer test";
79
+ }
80
+ } catch (_) {}
81
+ }
82
+ const triggerFile = existsSync(join(projectRoot, "composer.json")) ? "composer.json" : existsSync(join(projectRoot, "pest.php")) ? "pest.php" : "phpunit.xml";
83
+ return { ...container, stack: "php", testCmd, buildCmd: "composer dump-autoload", triggerFile };
84
+ }
85
+
86
+ // 3. Mobile & Cross-Platform (pubspec.yaml, Package.swift, app.json)
87
+ if (existsSync(join(projectRoot, "pubspec.yaml"))) {
88
+ let isFlutter = false;
89
+ try {
90
+ const content = readFileSync(join(projectRoot, "pubspec.yaml"), "utf-8");
91
+ if (content.includes("flutter:") || content.includes("sdk: flutter")) {
92
+ isFlutter = true;
93
+ }
94
+ } catch (_) {}
95
+ if (isFlutter) {
96
+ return { ...container, stack: "flutter", testCmd: "flutter test", buildCmd: "flutter build apk --debug", triggerFile: "pubspec.yaml" };
97
+ }
98
+ return { ...container, stack: "dart", testCmd: "dart test", buildCmd: "dart compile exe bin/main.dart", triggerFile: "pubspec.yaml" };
99
+ }
100
+ if (existsSync(join(projectRoot, "Package.swift"))) {
101
+ return { ...container, stack: "swift", testCmd: "swift test", buildCmd: "swift build", triggerFile: "Package.swift" };
102
+ }
103
+ if (existsSync(join(projectRoot, "app.json")) || existsSync(join(projectRoot, "react-native.config.js"))) {
104
+ const triggerFile = existsSync(join(projectRoot, "app.json")) ? "app.json" : "react-native.config.js";
105
+ return { ...container, stack: "react-native", testCmd: "npm test", buildCmd: "npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/main.jsbundle", triggerFile };
106
+ }
107
+
108
+ // 4. Systems & C/C++ / Rust / Go / Make
109
+ if (existsSync(join(projectRoot, "CMakeLists.txt"))) {
110
+ return { ...container, stack: "cmake", testCmd: "ctest --test-dir build --output-on-failure", buildCmd: "cmake --build build", triggerFile: "CMakeLists.txt" };
111
+ }
112
+ if (existsSync(join(projectRoot, "Cargo.toml"))) {
113
+ return { ...container, stack: "cargo", testCmd: "cargo test --workspace", buildCmd: "cargo build", triggerFile: "Cargo.toml" };
114
+ }
115
+ if (existsSync(join(projectRoot, "go.mod"))) {
116
+ return { ...container, stack: "go", testCmd: "go test ./...", buildCmd: "go build ./...", triggerFile: "go.mod" };
117
+ }
118
+ if (existsSync(join(projectRoot, "Makefile"))) {
119
+ return { ...container, stack: "make", testCmd: "make test", buildCmd: "make build", triggerFile: "Makefile" };
120
+ }
121
+
122
+ // 5. Python / Elixir / Ruby / Java
123
+ if (existsSync(join(projectRoot, "pyproject.toml")) || existsSync(join(projectRoot, "requirements.txt")) || existsSync(join(projectRoot, "setup.py"))) {
124
+ const triggerFile = existsSync(join(projectRoot, "pyproject.toml")) ? "pyproject.toml" : existsSync(join(projectRoot, "requirements.txt")) ? "requirements.txt" : "setup.py";
125
+ return { ...container, stack: "python", testCmd: "pytest", buildCmd: "python3 -m compileall -q .", triggerFile };
126
+ }
127
+ if (existsSync(join(projectRoot, "mix.exs"))) {
128
+ return { ...container, stack: "mix", testCmd: "mix test", buildCmd: "mix compile", triggerFile: "mix.exs" };
129
+ }
130
+ if (existsSync(join(projectRoot, "Gemfile"))) {
131
+ return { ...container, stack: "bundler", testCmd: "bundle exec rake test", buildCmd: "bundle exec rake build", triggerFile: "Gemfile" };
132
+ }
133
+ if (existsSync(join(projectRoot, "pom.xml"))) {
134
+ return { ...container, stack: "maven", testCmd: "mvn test", buildCmd: "mvn compile", triggerFile: "pom.xml" };
135
+ }
136
+ if (existsSync(join(projectRoot, "build.gradle")) || existsSync(join(projectRoot, "build.gradle.kts"))) {
137
+ const triggerFile = existsSync(join(projectRoot, "build.gradle")) ? "build.gradle" : "build.gradle.kts";
138
+ return { ...container, stack: "gradle", testCmd: "./gradlew test", buildCmd: "./gradlew assemble", triggerFile };
139
+ }
140
+
141
+ // 6. JS / TS Runtimes
142
+ if (existsSync(join(projectRoot, "bunfig.toml")) || existsSync(join(projectRoot, "bun.lockb"))) {
143
+ const triggerFile = existsSync(join(projectRoot, "bunfig.toml")) ? "bunfig.toml" : "bun.lockb";
144
+ return { ...container, stack: "bun", testCmd: "bun test", buildCmd: "bun run build", triggerFile };
145
+ }
146
+ if (existsSync(join(projectRoot, "deno.json")) || existsSync(join(projectRoot, "deno.jsonc"))) {
147
+ const triggerFile = existsSync(join(projectRoot, "deno.json")) ? "deno.json" : "deno.jsonc";
148
+ return { ...container, stack: "deno", testCmd: "deno test", buildCmd: "deno task build", triggerFile };
149
+ }
150
+ if (existsSync(join(projectRoot, "package.json"))) {
151
+ let testScript = "npm test";
152
+ let buildScript = "npm run build";
153
+ try {
154
+ const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
155
+ if (pkg && typeof pkg === "object") {
156
+ const scripts = pkg.scripts || {};
157
+ testScript = scripts.test ? "npm test" : "npm test";
158
+ buildScript = scripts.build ? "npm run build" : "";
159
+ }
160
+ } catch (_) {
161
+ testScript = "npm test";
162
+ buildScript = "npm run build";
163
+ }
164
+ return { ...container, stack: "node", testCmd: testScript, buildCmd: buildScript, triggerFile: "package.json" };
165
+ }
166
+
167
+ // 7. .NET / C# / F# / PHP / Python root file extension fallback
168
+ try {
169
+ const rootFiles = readdirSync(projectRoot);
170
+ const slnFile = rootFiles.find((f) => f.endsWith(".sln") || f.endsWith(".csproj") || f.endsWith(".fsproj"));
171
+ if (slnFile) {
172
+ return { ...container, stack: "dotnet", testCmd: "dotnet test --no-restore --nologo", buildCmd: "dotnet build --no-incremental --nologo", triggerFile: slnFile };
173
+ }
174
+ const phpFile = rootFiles.find((f) => f.endsWith(".php"));
175
+ if (phpFile) {
176
+ return { ...container, stack: "php", testCmd: 'php -l $(find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*")', buildCmd: "composer dump-autoload", triggerFile: phpFile };
177
+ }
178
+ const pyFile = rootFiles.find((f) => f.endsWith(".py"));
179
+ if (pyFile) {
180
+ return { ...container, stack: "python", testCmd: "pytest", buildCmd: "python3 -m compileall -q .", triggerFile: pyFile };
181
+ }
182
+ } catch (_) {}
183
+
184
+ return { ...container, stack: "unknown", testCmd: "", buildCmd: "", triggerFile: null };
185
+ }
186
+
187
+ /**
188
+ * Resolves workspace boundaries in polyglot monorepos by mapping changed files to subprojects.
189
+ */
190
+ export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd()) {
191
+ const rootStack = detectPolyglotStack(root);
192
+ if (!changedFiles || changedFiles.length === 0) {
193
+ return { isMonorepo: false, globalFallback: false, projects: [{ path: ".", ...rootStack }], testCmd: rootStack.testCmd, buildCmd: rootStack.buildCmd };
194
+ }
195
+
196
+ const sharedTriggers = new Set(["openapi.yaml", "openapi.json", "schema.graphql", "docker-compose.yml", "Makefile", "turbo.json", "pnpm-workspace.yaml", "nx.json"]);
197
+ for (const file of changedFiles) {
198
+ const baseName = file.split(sep).pop();
199
+ if (sharedTriggers.has(baseName) || file.startsWith(".agent/") || file.startsWith(".github/")) {
200
+ return { isMonorepo: true, globalFallback: true, projects: [{ path: ".", ...rootStack }], testCmd: rootStack.testCmd, buildCmd: rootStack.buildCmd };
201
+ }
202
+ }
203
+
204
+ const projectMap = new Map();
205
+ for (const file of changedFiles) {
206
+ const parts = file.split(sep);
207
+ let resolved = false;
208
+
209
+ for (let i = parts.length - 1; i >= 1; i--) {
210
+ const subDir = parts.slice(0, i).join("/");
211
+ const subStack = detectPolyglotStack(join(root, subDir));
212
+ if (subStack.stack !== "unknown" && subStack.triggerFile) {
213
+ if (!projectMap.has(subDir)) {
214
+ projectMap.set(subDir, { path: subDir, ...subStack });
215
+ }
216
+ resolved = true;
217
+ break;
218
+ }
219
+ }
220
+
221
+ if (!resolved && !projectMap.has(".")) {
222
+ projectMap.set(".", { path: ".", ...rootStack });
223
+ }
224
+ }
225
+
226
+ const projects = Array.from(projectMap.values());
227
+ const testCmds = projects
228
+ .filter((p) => p.testCmd)
229
+ .map((p) => (p.path === "." ? p.testCmd : `(cd ${p.path} && ${p.testCmd})`));
230
+ const buildCmds = projects
231
+ .filter((p) => p.buildCmd)
232
+ .map((p) => (p.path === "." ? p.buildCmd : `(cd ${p.path} && ${p.buildCmd})`));
233
+
234
+ return {
235
+ isMonorepo: projects.length > 1,
236
+ globalFallback: false,
237
+ projects,
238
+ testCmd: testCmds.join(" && ") || rootStack.testCmd,
239
+ buildCmd: buildCmds.join(" && ") || rootStack.buildCmd,
240
+ };
241
+ }
242
+
243
+ export function bootstrapZeroTestRepo(root = process.cwd(), options = {}) {
244
+ const detected = detectPolyglotStack(root);
245
+ const configPath = join(root, ".agent", "config.yml");
246
+ const hasConfig = existsSync(configPath);
247
+ const hasRealTestSuite =
248
+ hasConfig ||
249
+ existsSync(join(root, "phpunit.xml")) ||
250
+ existsSync(join(root, "pest.php")) ||
251
+ existsSync(join(root, "turbo.json")) ||
252
+ existsSync(join(root, "pnpm-workspace.yaml")) ||
253
+ existsSync(join(root, "nx.json"));
254
+
255
+ if (!options.force && hasRealTestSuite) {
256
+ return { bootstrapped: false, reason: "EXISTING_VERIFICATION_ORACLE", testCmd: detected.testCmd };
257
+ }
258
+
259
+ let testCmd = "";
260
+ if (detected.stack === "php" || detected.stack === "laravel" || detected.stack === "wordpress") {
261
+ testCmd = 'php -l $(find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*")';
262
+ } else if (detected.stack === "python") {
263
+ testCmd = "python3 -m compileall -q -x '^\\./(\\.\\venv|venv|node_modules|\\.git)/' .";
264
+ } else if (detected.stack === "dotnet") {
265
+ testCmd = "dotnet build --no-incremental --nologo";
266
+ } else if (detected.stack === "cargo") {
267
+ testCmd = "cargo check --workspace --all-targets";
268
+ } else if (detected.stack === "go") {
269
+ testCmd = "go vet ./...";
270
+ } else if (existsSync(join(root, "tsconfig.json"))) {
271
+ testCmd = "npx tsc --noEmit";
272
+ } else {
273
+ const smokePath = generateSmokeTestScript(root);
274
+ testCmd = `node --test ${smokePath}`;
275
+ }
276
+
277
+ try {
278
+ mkdirSync(join(root, ".agent"), { recursive: true });
279
+ } catch (_) {}
280
+
281
+ const cfg = `version: 1\nprovider: jules\ntier: ultra\nverify:\n test: "${testCmd}"\n build: "${detected.buildCmd || ""}"\nlimits:\n diff_kb: 75\n daily_tasks: 300\n repair_attempts: 3\nbranch_prefix: agent/\nbase_branch: main\n`;
282
+ writeFileSync(configPath, cfg, "utf-8");
283
+
284
+ return { bootstrapped: true, stack: detected.stack, testCmd, configPath };
285
+ }