jules-orchestrator-kit 0.25.1 β 0.26.1
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 +175 -289
- 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
|
@@ -2,358 +2,244 @@
|
|
|
2
2
|
|
|
3
3
|
# π jules-orchestrator-kit
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Universal Autonomous AI Agent Orchestration Kernel for Google Jules
|
|
6
6
|
|
|
7
7
|
[](https://github.com/FullThrottle83/jules-orchestrator-kit/actions/workflows/jules-audit.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/jules-orchestrator-kit)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://nodejs.org)
|
|
12
|
+
[](#-universal-polyglot-support--stack-detection)
|
|
12
13
|
|
|
13
14
|
<p align="center">
|
|
14
|
-
<b>
|
|
15
|
-
|
|
15
|
+
<b>The zero-dependency safety gatekeeper and self-healing engineering kernel for autonomous coding agent swarms.</b><br/>
|
|
16
|
+
Transforms single-turn AI chat assistants into production-grade engineering swarms running 300+ daily sessions across any language or monorepo.
|
|
16
17
|
</p>
|
|
17
18
|
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
19
|
<p align="center">
|
|
23
|
-
<
|
|
20
|
+
<a href="#-2-sentence-mental-model">π‘ What is Kit?</a> β’
|
|
21
|
+
<a href="#-universal-30-second-quickstart-zero-to-verified-pr">β‘ 30s Quickstart</a> β’
|
|
22
|
+
<a href="#-feature-comparison-matrix">π Comparison Matrix</a> β’
|
|
23
|
+
<a href="#-system-architecture--visual-diagrams">ποΈ Architecture</a> β’
|
|
24
|
+
<a href="#-cli-command-reference-agentctl">π οΈ CLI Reference</a> β’
|
|
25
|
+
<a href="#-v027-next-gen-feature-roadmap">πΊοΈ Roadmap</a> β’
|
|
26
|
+
<a href="./docs/UNIVERSAL_POLYGLOT_ARCHITECTURE.md">π Polyglot Spec</a>
|
|
24
27
|
</p>
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## β‘ 2-Minute Quickstart
|
|
29
|
-
|
|
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.
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
# 1. Initialize orchestrator structure in your target codebase
|
|
34
|
-
npx jules-orchestrator-kit init
|
|
35
|
-
|
|
36
|
-
# 2. Dispatch an autonomous task (Dry-Run mode for local simulation)
|
|
37
|
-
JULES_DRY_RUN=1 npx agentctl dispatch \
|
|
38
|
-
--title "Add JWT Validator" \
|
|
39
|
-
--prompt "Implement JWT validation middleware with unit tests"
|
|
40
|
-
|
|
41
|
-
# 3. Run the 4-phase security & verification gatekeeper
|
|
42
|
-
npx agentctl gate
|
|
43
|
-
|
|
44
|
-
# 4. Connect as a native stdio MCP server (for Claude Code, Cursor, or Antigravity)
|
|
45
|
-
npx agentctl mcp
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
> π **NOTE**: **Looking for production deployment patterns?**
|
|
49
|
-
> 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).
|
|
29
|
+
</div>
|
|
50
30
|
|
|
51
31
|
---
|
|
52
32
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
Whether you are dispatching your first automated coding task or managing high-throughput CI/CD swarms across large engineering teams, `jules-orchestrator-kit` provides total operational safety:
|
|
33
|
+
## π‘ 2-Sentence Mental Model
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
| **Safety First** | Never breaks `main` branch or pushes failing code. | 4-Phase Safety Gatekeeper fails closed on scope drift, high entropy secrets, or test regressions. |
|
|
60
|
-
| **Token Budget Protection** | Prevents runaway loops from burning API quotas. | Sliding-window OODA thrash detector ($A \rightarrow B \rightarrow A \rightarrow B$) halts non-convergent repair cycles automatically. |
|
|
61
|
-
| **Zero Setup Hassle** | Works out of the box with standard `npm test`. | **Zero External Runtime Dependencies** (`node:fs`, `node:path`, `node:crypto`, `node:child_process`). |
|
|
62
|
-
| **Multi-Agent Swarms** | Run multiple tasks simultaneously without conflict. | Deterministic VFS mutex and 3-way structural merge engine resolve parallel worktree changes cleanly. |
|
|
63
|
-
| **IDE & Tooling** | Seamlessly connects to your favorite editor. | Native Model Context Protocol (MCP) server over memory-bounded stdio streams. |
|
|
35
|
+
> **Think of `jules-orchestrator-kit` as an automated Engineering Manager for AI coding agents.**
|
|
36
|
+
> **It hands out clear tasks, runs your tests in an isolated sandbox, fixes broken code automatically, and only opens a Pull Request when 100% of your tests pass.**
|
|
64
37
|
|
|
65
38
|
---
|
|
66
39
|
|
|
67
|
-
##
|
|
68
|
-
|
|
69
|
-
<details open>
|
|
70
|
-
<summary><b>π 1. Control Plane Architecture Layers</b></summary>
|
|
71
|
-
|
|
72
|
-
<br/>
|
|
73
|
-
|
|
74
|
-
<p align="center">
|
|
75
|
-
<img src="docs/assets/architecture-layers.svg?v=3" alt="Control Plane Architecture Layers" width="100%" />
|
|
76
|
-
</p>
|
|
77
|
-
|
|
78
|
-
<br/>
|
|
79
|
-
|
|
80
|
-
### Engine System Highlights
|
|
81
|
-
|
|
82
|
-
- **Native Task DAG Executor (`src/dag-engine.mjs`)**: Zero-dependency `DagExecutor` with Kahn's topological sort algorithm, SHA-256 interface fingerprinting post-task execution, and pre-execution cycle detection (`DagCycleError`).
|
|
83
|
-
- **Intent Journaling & Zombie Worktree Reaper (`src/journal.mjs`)**: Automatic boot-time scan (`reapOrphanedIntents`) in `agentctl` and MCP server that tracks git operations in `.agent/state/journal.jsonl` and prunes orphaned worktrees left by crashed/recycled processes.
|
|
84
|
-
- **Hermetic Network Egress Guard (`src/preload-net-guard.mjs`)**: Intercepts and blocks unmocked outbound HTTP/HTTPS egress during test execution (`NODE_OPTIONS="--import ./src/preload-net-guard.mjs"`), enforcing hermetic testing while allowing local loopback (`localhost`, `127.0.0.1`).
|
|
85
|
-
- **Linearizable VFS Mutex (`src/state.mjs`)**: Kernel-level directory mutex (`withVfsMutex`) guaranteeing serial linearizability for SHA-256 hash-chained session ledgers with atomic budget reservation (`reserveBudgetAtomic`).
|
|
86
|
-
- **PID Recycling & Stale Lock Protection (`src/state.mjs`)**: Linux `/proc/<pid>/stat` launch-time validation and random UUID nonces prevent false-positive lock reaps from recycled OS process IDs.
|
|
87
|
-
- **Memory-Bounded Content-Length MCP Streaming (`src/mcp.mjs`)**: Native MCP server over stdio streams using `McpFrameDecoder` with a 4 MB memory safety ceiling and panic boundaries to prevent stdout stack trace leaks.
|
|
88
|
-
- **Process Group Isolation (`src/process-group.mjs`)**: `ProcessGroupManager` creates isolated process groups (`detached: true`) and catches `SIGINT`/`SIGTERM`/`exit` signals to execute `process.kill(-pgid)`, guaranteeing zero zombie processes.
|
|
89
|
-
- **TOCTOU & Symlink Defense (`src/security.mjs`)**: `safeAtomicWrite()` uses `O_CREAT | O_EXCL | O_WRONLY` temp files with `fsyncSync` + `renameSync` and `lstatSync`/`realpathSync` symlink checks.
|
|
90
|
-
- **3-Way Structural AST/JSON Merge (`scripts/jules-merge-swarm.mjs`)**: Pure Node `deepMerge3Way()` algorithm for recursive object and array merges executed in isolated temporary directories (`os.tmpdir()`).
|
|
91
|
-
|
|
92
|
-
</details>
|
|
93
|
-
|
|
94
|
-
<details open>
|
|
95
|
-
<summary><b>π 2. Self-Healing OODA Loop Cycle</b></summary>
|
|
96
|
-
|
|
97
|
-
<br/>
|
|
98
|
-
|
|
99
|
-
<p align="center">
|
|
100
|
-
<img src="docs/assets/ooda-loop-cycle.svg?v=3" alt="Self-Healing OODA Loop" width="100%" />
|
|
101
|
-
</p>
|
|
102
|
-
|
|
103
|
-
<br/>
|
|
104
|
-
|
|
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.
|
|
106
|
-
|
|
107
|
-
</details>
|
|
108
|
-
|
|
109
|
-
<details open>
|
|
110
|
-
<summary><b>π‘οΈ 3. Zero-Trust Security Shield & 4-Phase Gate</b></summary>
|
|
111
|
-
|
|
112
|
-
<br/>
|
|
113
|
-
|
|
114
|
-
<p align="center">
|
|
115
|
-
<img src="docs/assets/security-shield.svg?v=3" alt="Zero-Trust Security Guarantees" width="100%" />
|
|
116
|
-
</p>
|
|
117
|
-
|
|
118
|
-
<br/>
|
|
119
|
-
|
|
120
|
-
### The 4-Phase Safety Audit & Security Boundary (`agentctl gate`)
|
|
121
|
-
|
|
122
|
-
1. **Scope Fencing (`forbidden_paths`)**: Ensures agents cannot modify protected files (`package.json`, `.github/`, deployment keys) without explicit overrides.
|
|
123
|
-
2. **Diff Payload Governor**: Rejects oversized diffs (> 75 KB) to prevent truncation and hidden payload injections.
|
|
124
|
-
3. **Secret Entropy Scanner**: Scans diffs for high-confidence secrets (AWS keys, Stripe keys, GitHub tokens, SSH private keys) using Shannon Entropy analysis (> 3.6 bits).
|
|
125
|
-
4. **Trusted Verification Suite**: Executes auto-detected unit tests and linters (`npm test`) inside a hermetic network sandbox to guarantee zero regressions before merging.
|
|
126
|
-
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
|
-
6. **MCP Stream Isolation (`src/mcp.mjs`)**: Seals `process.stdout.write` framing stream to prevent log output from corrupting JSON-RPC stdio frames.
|
|
128
|
-
|
|
129
|
-
> β οΈ **WARNING**: All security rules are fetched strictly from `origin/main` (never untrusted PR branches) to prevent prompt-injection attacks from altering security rules.
|
|
130
|
-
|
|
131
|
-
</details>
|
|
132
|
-
|
|
133
|
-
<details open>
|
|
134
|
-
<summary><b>π 4. Parallel Swarm Topology & Isolated Worktrees</b></summary>
|
|
135
|
-
|
|
136
|
-
<br/>
|
|
137
|
-
|
|
138
|
-
<p align="center">
|
|
139
|
-
<img src="docs/assets/swarm-topology.svg?v=3" alt="Multi-Agent Swarm Topology" width="100%" />
|
|
140
|
-
</p>
|
|
141
|
-
|
|
142
|
-
<br/>
|
|
40
|
+
## π― Why `jules-orchestrator-kit`?
|
|
143
41
|
|
|
144
|
-
|
|
42
|
+
Autonomous coding agents can write software at 100Γ human speedβbut unconstrained agents introduce silent regressions, leak API keys, hallucinate test assertions, and thrash shared monorepos.
|
|
145
43
|
|
|
146
|
-
|
|
44
|
+
`jules-orchestrator-kit` provides the missing **Safety, Orchestration, and Verification Kernel** for high-reliability AI agent deployments:
|
|
45
|
+
- **π Zero Runtime Dependencies:** Built exclusively on Node.js 20+ built-ins (`node:fs`, `node:child_process`, `node:crypto`, `node:path`, `node:test`). Zero third-party npm packages mean zero supply-chain CVE risk.
|
|
46
|
+
- **π‘οΈ Fail-Closed Security Gatekeeper:** Unconditionally evaluates explicit Deny rules *before* Allow rules, redacts high-entropy secrets and PII from dry-runs and git diffs, and rejects PRs exceeding the 75 KB Diff Payload governor.
|
|
47
|
+
- **π Autonomous OODA Self-Healing:** Captures test stderr/stdout, normalizes failure fingerprints, and feeds structured error contexts back into repair iterations (up to 3 automatic attempts) before human escalation.
|
|
48
|
+
- **π Universal Polyglot Spine:** Natively auto-detects 24+ tech stacks (PHP/Laravel/WordPress, .NET/C#, Python, Go, Rust, C/C++, Flutter/Swift, Node/Deno/Bun) and transparently wraps verification suites in Docker Compose or Devcontainer sandboxes.
|
|
49
|
+
- **π Scoped Monorepo Boundary Resolver:** Statically maps changed files up directory ancestry to invoke isolated subshell test suites (`(cd backend && pytest) && (cd cli && cargo test)`), eliminating global test thrashing.
|
|
50
|
+
- **π Zero-Test Bootstrapping (`agentctl bootstrap`):** Synthesizes deterministic syntax-check and smoke-test verification oracles for untested legacy repositories so agents always operate against a falsifiable feedback loop.
|
|
51
|
+
- **π Proven Scale & Reliability:** Empirically tested with **221 unit tests across 54 suites passing in < 1.2s**, supporting 300+ daily agent sessions per repository.
|
|
147
52
|
|
|
148
|
-
|
|
149
|
-
<summary><b>π 5. Model Context Protocol (MCP) & IDE Integration</b></summary>
|
|
53
|
+
---
|
|
150
54
|
|
|
151
|
-
|
|
55
|
+
## π Feature Comparison Matrix
|
|
152
56
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
57
|
+
| Dimension | Raw Agent Execution (No Orchestrator) | Standard CI/CD Pipelines | `jules-orchestrator-kit` (v0.26+) |
|
|
58
|
+
| :--- | :--- | :--- | :--- |
|
|
59
|
+
| **Self-Healing Loop** | β None (Crashes on test error) | β None (Fails build; notifies human) | β
**Autonomous OODA Loop** (Max 3 repair turns with error fingerprinting) |
|
|
60
|
+
| **Scope Isolation** | β None (Can modify CI files or lockfiles) | π‘ Post-commit branch rules only | β
**Fail-Closed Scope Guard** (Deny-first evaluation; blocks protected paths) |
|
|
61
|
+
| **Polyglot Stack Detection**| β Manual prompt instructions | π‘ Hardcoded YAML workflow steps | β
**Universal 24+ Stack Detector** (`src/stack-detector.mjs`) |
|
|
62
|
+
| **Flaky Test Quarantine** | β Fails session randomly | β Breaks CI pipeline randomly | β
**Wilson-Score Statistical Quarantine** (Oscillation β₯ 0.40 quarantined automatically) |
|
|
63
|
+
| **Monorepo Scoping** | β Runs full global test suite | π‘ Requires custom Nx/Turbo scripting | β
**Scoped Subshell Boundary Resolver** (`resolveWorkspaceBoundary`) |
|
|
64
|
+
| **Zero-Test Bootstrapping**| β Halts without verification oracle | β Fails build if no tests exist | β
**Instant Oracle Synthesis** (`php -l`, `compileall`, `dotnet build`, `tsc`, `smoke`) |
|
|
65
|
+
| **Secret Leak Prevention**| β Prone to leaking tokens in diffs | π‘ Post-push secret scanning alerts | β
**Pre-Dispatch & Pre-Commit Diff Scanner** (Blocks CVEs/keys before PR creation) |
|
|
66
|
+
| **Dependency Footprint** | β Requires heavy SDKs & parsers | π‘ Many external actions & plugins | β
**0 Native Dependencies** (100% Node.js 20+ ESM built-ins) |
|
|
156
67
|
|
|
157
|
-
|
|
68
|
+
---
|
|
158
69
|
|
|
159
|
-
|
|
70
|
+
## β‘ Universal 30-Second Quickstart (Zero to Verified PR)
|
|
160
71
|
|
|
161
|
-
|
|
72
|
+
Get from zero to an autonomously verified GitHub Pull Request across any software ecosystem in 30 seconds.
|
|
162
73
|
|
|
74
|
+
### 1οΈβ£ Node.js / TypeScript (npm, pnpm, yarn, bun, deno)
|
|
163
75
|
```bash
|
|
164
|
-
|
|
76
|
+
# Dispatch a scoped task; auto-detects package.json / tsconfig.json and runs type-checked tests
|
|
77
|
+
npx jules-orchestrator-kit dispatch --title "Add rate limiting to API router" \
|
|
78
|
+
--prompt "Implement IP-based token-bucket rate limiting in src/router.ts with unit tests."
|
|
165
79
|
```
|
|
166
80
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
</details>
|
|
175
|
-
|
|
176
|
-
<details open>
|
|
177
|
-
<summary><b>π³ 6. Subscription Tier Presets Matrix</b></summary>
|
|
178
|
-
|
|
179
|
-
<br/>
|
|
180
|
-
|
|
181
|
-
<p align="center">
|
|
182
|
-
<img src="docs/assets/tier-presets.svg?v=3" alt="Subscription Tier Presets Matrix" width="100%" />
|
|
183
|
-
</p>
|
|
184
|
-
|
|
185
|
-
<br/>
|
|
186
|
-
|
|
187
|
-
Tailor session limits and rate-limiting behavior to your Google Jules API subscription tier:
|
|
188
|
-
|
|
189
|
-
| Tier | `dailyTasks` | `repairAttempts` | `concurrency` | `staggerMs` | Target Usage |
|
|
190
|
-
| :--- | :---: | :---: | :---: | :---: | :--- |
|
|
191
|
-
| **`free`** | `15` | `1` | `1` | `3000 ms` | **Hobby / Free Tier:** Conserves quota, prevents HTTP 429 rate limits. |
|
|
192
|
-
| **`pro`** | `100` | `2` | `2` | `1500 ms` | **Developer Pro:** Balanced throughput for everyday work. |
|
|
193
|
-
| **`ultra`** *(default)* | `300` | `3` | `3` | `1000 ms` | **Swarm / Enterprise:** Maximum parallel throughput & CI/CD. |
|
|
194
|
-
|
|
195
|
-
**How to activate:**
|
|
196
|
-
- **Environment Variable:** `export JULES_TIER=free` (or set in `.env`)
|
|
197
|
-
- **Config File (`.agent/jules.yml`):** Set `tier: free`
|
|
198
|
-
|
|
199
|
-
</details>
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## π€ Specialist Agent Prompt Presets (`.agent/prompts/`)
|
|
81
|
+
### 2οΈβ£ Python / FastAPI / Django (pytest, pyproject.toml)
|
|
82
|
+
```bash
|
|
83
|
+
# Bootstrap zero-test or legacy Python repo, then dispatch task
|
|
84
|
+
npx jules-orchestrator-kit bootstrap --force
|
|
85
|
+
npx jules-orchestrator-kit dispatch --title "Add OAuth2 JWT validation" \
|
|
86
|
+
--prompt "Add JWT bearer authentication middleware to backend/api/auth.py and verify via pytest."
|
|
87
|
+
```
|
|
204
88
|
|
|
205
|
-
|
|
89
|
+
### 3οΈβ£ PHP / Laravel / WordPress (Docker Compose + PHPUnit/Pest)
|
|
90
|
+
```bash
|
|
91
|
+
# Auto-detects docker-compose.yml and wraps test commands in `docker compose exec -T app ...`
|
|
92
|
+
npx jules-orchestrator-kit dispatch --title "Upgrade PHP 8.3 type annotations" \
|
|
93
|
+
--prompt "Add strict type hints to all repository classes in app/Repositories/."
|
|
94
|
+
```
|
|
206
95
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
96
|
+
### 4οΈβ£ .NET / C# Enterprise (*.sln, *.csproj)
|
|
97
|
+
```bash
|
|
98
|
+
# Auto-detects .sln / .csproj and runs `dotnet test --no-restore --nologo`
|
|
99
|
+
npx jules-orchestrator-kit dispatch --title "Implement OrderService caching" \
|
|
100
|
+
--prompt "Add IMemoryCache caching to OrderService.cs with xUnit coverage."
|
|
101
|
+
```
|
|
213
102
|
|
|
103
|
+
### 5οΈβ£ Polyglot Monorepo (FastAPI + React + Rust CLI)
|
|
214
104
|
```bash
|
|
215
|
-
#
|
|
216
|
-
npx
|
|
217
|
-
--prompt "$(cat .agent/prompts/Janitor.md) Prune unused helper methods in src/utils.mjs"
|
|
105
|
+
# Run a parallel worktree swarm; changed files automatically route to scoped subproject tests
|
|
106
|
+
npx jules-orchestrator-kit swarm
|
|
218
107
|
```
|
|
219
108
|
|
|
220
109
|
---
|
|
221
110
|
|
|
222
|
-
##
|
|
111
|
+
## ποΈ System Architecture & Visual Diagrams
|
|
223
112
|
|
|
224
|
-
|
|
113
|
+
### 1. The Autonomous OODA Verification Loop
|
|
114
|
+
Every task dispatched to `jules-orchestrator-kit` executes within an immutable, fail-closed verification loop:
|
|
225
115
|
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
116
|
+
```
|
|
117
|
+
+---------------------------------------------------------------------------------------------------+
|
|
118
|
+
| AUTONOMOUS OODA SELF-HEALING ENGINE (v0.26+) |
|
|
119
|
+
| |
|
|
120
|
+
| [Task Envelope] --> (1. Validate Scope & Base Freshness) |
|
|
121
|
+
| | |
|
|
122
|
+
| v |
|
|
123
|
+
| (2. Create Isolated Git Worktree / VFS Lock) |
|
|
124
|
+
| | |
|
|
125
|
+
| v |
|
|
126
|
+
| (3. Dispatch AI Task to Google Jules / LLM) |
|
|
127
|
+
| | |
|
|
128
|
+
| v |
|
|
129
|
+
| (4. Execute Scoped Verification Gate) |
|
|
130
|
+
| -- detectPolyglotStack().testCmd |
|
|
131
|
+
| -- Docker Compose / Devcontainer wrapper |
|
|
132
|
+
| | |
|
|
133
|
+
| +---------+---------+ |
|
|
134
|
+
| | PASS | FAIL |
|
|
135
|
+
| v v |
|
|
136
|
+
| (5. Security Audit) (6. Fingerprint Stderr / Flaky Verdict) |
|
|
137
|
+
| - Redact Secrets | |
|
|
138
|
+
| - Check Diff < 75KB +---> Is test QUARANTINED? (Oscillation >= 0.40) |
|
|
139
|
+
| | | | |
|
|
140
|
+
| v YES NO |
|
|
141
|
+
| (7. Rebase & PR) | | |
|
|
142
|
+
| - git rebase main v v |
|
|
143
|
+
| - gh pr create [Log Quarantined Flake] [Attempt OODA Repair Turn] |
|
|
144
|
+
| (Exit Code 8) (Max 3 Retries; Exit 4 on Exhaust) |
|
|
145
|
+
+---------------------------------------------------------------------------------------------------+
|
|
236
146
|
```
|
|
237
147
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
## π οΈ CLI Command Reference (`agentctl`)
|
|
241
|
-
|
|
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 |
|
|
254
|
-
|
|
255
|
-
---
|
|
148
|
+
### 2. Polyglot Monorepo Scoped Execution Engine
|
|
149
|
+
In monorepos containing multiple languages, `resolveWorkspaceBoundary(changedFiles)` traverses directory ancestry to isolate verification to affected subprojects:
|
|
256
150
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
151
|
+
```
|
|
152
|
+
+---------------------------------------------------------------------------------------------------+
|
|
153
|
+
| MONOREPO BOUNDARY RESOLVER (resolveWorkspaceBoundary) |
|
|
154
|
+
| |
|
|
155
|
+
| changedFiles: ["backend/api/main.py", "cli/src/main.rs", "docs/README.md"] |
|
|
156
|
+
| | |
|
|
157
|
+
| +---> 1. Check Root Shared Triggers (openapi.yaml, docker-compose.yml, Makefile) |
|
|
158
|
+
| | -> None changed. Continue subproject isolation. |
|
|
159
|
+
| | |
|
|
160
|
+
| +---> 2. Map Files to Subproject Roots by Trigger File Traversal: |
|
|
161
|
+
| | - "backend/api/main.py" -> backend/pyproject.toml (Python Stack) |
|
|
162
|
+
| | - "cli/src/main.rs" -> cli/Cargo.toml (Rust/Cargo Stack) |
|
|
163
|
+
| | - "docs/README.md" -> (Documentation; R0 Cosmetic Risk) |
|
|
164
|
+
| | |
|
|
165
|
+
| +---> 3. Synthesize Scoped POSIX Subshell Verification Plan: |
|
|
166
|
+
| testCmd: "(cd backend && pytest) && (cd cli && cargo test --workspace)" |
|
|
167
|
+
| buildCmd: "(cd cli && cargo build)" |
|
|
168
|
+
| |
|
|
169
|
+
| Result: 100% test isolation, 0 global test thrashing, 0 git index lock collisions. |
|
|
170
|
+
+---------------------------------------------------------------------------------------------------+
|
|
275
171
|
```
|
|
276
172
|
|
|
277
173
|
---
|
|
278
174
|
|
|
279
|
-
##
|
|
280
|
-
|
|
281
|
-
Standardized exit codes enforced across all CLI utilities and CI pipelines:
|
|
282
|
-
|
|
283
|
-
| Exit Code | Classification | Description & Immediate Remediation Action |
|
|
284
|
-
| :---: | :--- | :--- |
|
|
285
|
-
| `0` | **Success** | Task completed cleanly; PR opened or verification passed. |
|
|
286
|
-
| `1` | **Pre-Dispatch / Arg Error** | Invalid arguments, prompt > 50 KB, or pre-dispatch validation error. |
|
|
287
|
-
| `2` | **API / Network Failure** | Jules API rate-limit (HTTP 429), `FAILED_PRECONDITION` quota, or timeout. |
|
|
288
|
-
| `3` | **Scope Violation** | Attempted modification of restricted files (`.github/`, command files, agent rules). |
|
|
289
|
-
| `4` | **OODA Exhausted / Thrash** | Verification suite failed after 3 repair attempts or hit deterministic regression. |
|
|
290
|
-
| `5` | **Diff Payload Limit** | Post-change git diff exceeds payload budget (`limits.diffKb`, default 75 KB). |
|
|
291
|
-
| `6` | **Secret Detected** | High-confidence secret or private key detected in patch diff (Shannon entropy > 3.6 bits). |
|
|
292
|
-
| `7` | **Budget Exhausted** | Daily task session quota limit reached (`limits.dailyTasks`, default 300). |
|
|
293
|
-
| `8` | **FLAKY_QUARANTINE** | Statistical test flakiness detected (oscillation >= 0.4, Wilson CI); OODA repair suppressed. |
|
|
294
|
-
| `124` | **Execution Timeout** | Subprocess execution exceeded hard timeout limit (default 10 minutes). |
|
|
295
|
-
| `188` | **ERR_UNMOCKED_NET** | Unmocked outbound HTTP/HTTPS egress intercepted by hermetic network guard. |
|
|
175
|
+
## π οΈ CLI Command Reference (`agentctl`)
|
|
296
176
|
|
|
297
|
-
|
|
177
|
+
`agentctl` is the unified command-line interface for `jules-orchestrator-kit`, available via `bin/agentctl.mjs` or `npx jules-orchestrator-kit <command>`.
|
|
298
178
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
| `
|
|
304
|
-
| `
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `
|
|
308
|
-
| `
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
179
|
+
| Command | Usage | Description | Exit Codes |
|
|
180
|
+
| :--- | :--- | :--- | :--- |
|
|
181
|
+
| `dispatch` | `agentctl dispatch --title <t> --prompt <p>` | Dispatches a single task to an AI agent in an isolated worktree. | `0` (Success), `1` (Arg error), `2` (429 Rate limit), `3` (Scope deny), `4` (OODA exhausted), `5` (Diff > 75KB), `6` (Secret leak) |
|
|
182
|
+
| `gate` / `audit`| `agentctl gate --base main --json` | Runs security, secret scanning, and verification gate against current branch. | `0` (Approved), `3` (Scope violation), `5` (Diff limit), `6` (Secret leak) |
|
|
183
|
+
| `bootstrap` | `agentctl bootstrap [--force] [--json]` | Inspects an untested repository and synthesizes `.agent/config.yml` with a zero-test verification oracle (`php -l`, `compileall`, `dotnet build`, `tsc`, `smoke`). | `0` (Bootstrapped / Existing) |
|
|
184
|
+
| `review-repair`| `agentctl review-repair <pr-comments.json>`| Parses GitHub PR review comments and synthesizes actionable OODA repair tasks. | `0` (Parsed), `1` (Missing file) |
|
|
185
|
+
| `queue` | `agentctl queue` | Consumes and executes pending markdown task envelopes in `.agent/queue/`. | `0` (Complete) |
|
|
186
|
+
| `swarm` | `agentctl swarm` | Runs parallel multi-agent swarm across queued tasks with token-bucket concurrency. | `0` (Complete) |
|
|
187
|
+
| `doctor` | `agentctl doctor` | Diagnostic inspect: displays detected stack, container wrapper, test command, and daily session budget. | `0` (Healthy) |
|
|
188
|
+
| `lock` | `agentctl lock <acquire\|release\|status>`| Manages VFS mutex locks for multi-agent non-overlapping file ownership. | `0` (Locked/Released), `1` (Conflict) |
|
|
189
|
+
| `clean` | `agentctl clean` | Prunes stale git worktrees, lockfiles, and temporary ledgers. | `0` (Clean) |
|
|
190
|
+
| `init` | `agentctl init` | Scaffolds `.agent/` directory structure and default `.agent/config.yml`. | `0` (Created) |
|
|
191
|
+
| `mcp` | `agentctl mcp` | Starts stdio Model Context Protocol (MCP) server for tool integration. | `0` / Stdio stream |
|
|
192
|
+
| `version` | `agentctl version` | Outputs orchestrator kit semantic version (`v0.26.0`). | `0` |
|
|
313
193
|
|
|
314
194
|
---
|
|
315
195
|
|
|
316
|
-
## π
|
|
196
|
+
## π Universal Polyglot Support & Stack Detection
|
|
317
197
|
|
|
318
|
-
The
|
|
198
|
+
The table below illustrates the 24+ software ecosystems natively supported by `src/stack-detector.mjs`:
|
|
319
199
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
200
|
+
```
|
|
201
|
+
Ecosystems Supported:
|
|
202
|
+
βββ PHP / Laravel / WordPress (composer.json, phpunit.xml, pest.php, artisan, wp-cli.yml)
|
|
203
|
+
βββ .NET / C# / F# (*.sln, *.csproj, *.fsproj, global.json)
|
|
204
|
+
βββ Mobile / Dart / Flutter (pubspec.yaml)
|
|
205
|
+
βββ Mobile / Swift / Xcode (Package.swift)
|
|
206
|
+
βββ Mobile / React Native (app.json, react-native.config.js)
|
|
207
|
+
βββ Systems / CMake (CMakeLists.txt)
|
|
208
|
+
βββ Systems / Rust Cargo (Cargo.toml)
|
|
209
|
+
βββ Systems / Go (go.mod)
|
|
210
|
+
βββ Systems / Make (Makefile)
|
|
211
|
+
βββ Python / FastAPI / Django (pyproject.toml, requirements.txt, setup.py)
|
|
212
|
+
βββ Elixir / Phoenix (mix.exs)
|
|
213
|
+
βββ Ruby / Rails (Gemfile)
|
|
214
|
+
βββ Java / Maven (pom.xml)
|
|
215
|
+
βββ Java / Gradle (build.gradle, build.gradle.kts)
|
|
216
|
+
βββ JS / TS Workspaces (turbo.json, pnpm-workspace.yaml, nx.json)
|
|
217
|
+
βββ JS / TS Runtimes (bunfig.toml, deno.json, package.json)
|
|
218
|
+
βββ Devcontainers & Docker Compose (.devcontainer/devcontainer.json, docker-compose.yml, Dockerfile)
|
|
219
|
+
```
|
|
332
220
|
|
|
333
221
|
---
|
|
334
222
|
|
|
335
|
-
##
|
|
336
|
-
|
|
337
|
-
We welcome community contributions! Please adhere to our core engineering invariants:
|
|
223
|
+
## πΊοΈ v0.27+ Next-Gen Feature Roadmap
|
|
338
224
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
225
|
+
| Feature | Module / Command | Architectural Blueprint | Target Release |
|
|
226
|
+
| :--- | :--- | :--- | :---: |
|
|
227
|
+
| **PR Review Auto-Remediation Loop** | `agentctl review-repair` (`src/review-repair.mjs`) | Ingests GitHub PR review comments (`CHANGES_REQUESTED`), extracts line/file context, and dispatches automated OODA repair turns until reviewer comments are resolved. | **v0.27.0** *(Implemented prototype)* |
|
|
228
|
+
| **Multi-Provider Failover Router** | `createFailoverProvider` (`src/provider.mjs`) | Ordered router (`["jules", "claude-code", "local-mcp"]`) that seamlessly falls back to secondary LLMs on HTTP 429 rate limits or 5xx service unavailability. | **v0.27.0** *(Implemented prototype)* |
|
|
229
|
+
| **Telemetry & Audit Web Dashboard**| `agentctl dashboard` (`src/dashboard.mjs`) | Zero-dependency local HTTP server displaying real-time DAG execution graphs, Wilson-Score flaky test ledgers, and SHA-256 telemetry chains. | **v0.27.0** |
|
|
230
|
+
| **Cross-Language Contract Guard** | `hashCrossLanguageInterface` (`src/merge-blocks.mjs`)| Canonical SHA-256 schema hashing for OpenAPI/Protobuf specs across polyglot task dependencies in `DagExecutor`. | **v0.26.0** *(Shipped)* |
|
|
342
231
|
|
|
343
|
-
|
|
232
|
+
---
|
|
344
233
|
|
|
345
|
-
|
|
346
|
-
# Clone the repository
|
|
347
|
-
git clone https://github.com/FullThrottle83/jules-orchestrator-kit.git
|
|
348
|
-
cd jules-orchestrator-kit
|
|
234
|
+
## π Recipes, Documentation & Prior Art
|
|
349
235
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
236
|
+
- [**Universal Polyglot Architecture & Zero-Test Specification**](./docs/UNIVERSAL_POLYGLOT_ARCHITECTURE.md) β Comprehensive technical report on boundary resolution, OODA math, and B2B workflows.
|
|
237
|
+
- [**Examples & Task Envelope Recipes**](./EXAMPLES.md) β Production YAML and Markdown task envelopes.
|
|
238
|
+
- [**Adversarial Security Audit Phase 4 Report**](./docs/AUDIT_REPORT.md) β CWE-77, CWE-1321, and CWE-183 security hardening analysis.
|
|
239
|
+
- [**Changelog**](./CHANGELOG.md) β Full release history and migration guides.
|
|
354
240
|
|
|
355
241
|
---
|
|
356
242
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
243
|
+
<div align="center">
|
|
244
|
+
<p><b>jules-orchestrator-kit</b> β’ Built with zero external dependencies for Google Jules and enterprise AI agent swarms.</p>
|
|
245
|
+
</div>
|
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.1 β 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.1");
|
|
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
|
+
}
|