jules-orchestrator-kit 0.25.1 → 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 +72 -32
- package/bin/agentctl.mjs +26 -3
- package/package.json +1 -1
- package/src/config.mjs +6 -83
- package/src/merge-blocks.mjs +77 -23
- package/src/stack-detector.mjs +285 -0
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
|
-
>
|
|
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
|
-
>
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
|
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
|
-
##
|
|
281
|
+
## 📖 Deep Reference Manuals & Technical Specs
|
|
241
282
|
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
48
|
+
console.log("agentctl v0.26.0");
|
|
48
49
|
process.exit(0);
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -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/package.json
CHANGED
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
|
|
193
|
+
* Autodetects verification test/build commands across 24+ polyglot tech stacks.
|
|
190
194
|
*/
|
|
191
195
|
export function detectStack(projectRoot = process.cwd()) {
|
|
192
|
-
|
|
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/merge-blocks.mjs
CHANGED
|
@@ -10,7 +10,47 @@ function sha1(content) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|