jules-orchestrator-kit 0.1.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/JULES_RULES_TEMPLATE.md +52 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/bin/init.js +142 -0
- package/package.json +41 -0
- package/scripts/command-resolver.mjs +260 -0
- package/scripts/jules-dispatch.mjs +248 -0
- package/scripts/jules-nightly.mjs +116 -0
- package/scripts/jules-queue-runner.mjs +51 -0
- package/scripts/jules-self-audit.mjs +196 -0
- package/scripts/jules-swarm.mjs +87 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Google Jules Autonomous Worker Directives
|
|
2
|
+
|
|
3
|
+
These guidelines govern all automated coding tasks executed by Google Jules (`jules`).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Triage Directive (When to use Jules)
|
|
8
|
+
|
|
9
|
+
Dispatch tasks to Jules when ALL of the following apply:
|
|
10
|
+
1. Scoped code change with a clear objective.
|
|
11
|
+
2. Mechanically verifiable via automated test/build commands (`npm test`, `cargo test`, `pytest`, etc.).
|
|
12
|
+
3. Requires no interactive local debugging or visual UI tweaking.
|
|
13
|
+
4. Does NOT modify restricted files (`.github/`, deployment keys, or unreviewed database migrations).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. MCP Machine Directive & Read-Before-Write Invariants
|
|
18
|
+
|
|
19
|
+
```xml
|
|
20
|
+
<MCP_DIRECTIVE>
|
|
21
|
+
<system_state>HEADLESS_CI_MODE</system_state>
|
|
22
|
+
<strict_invariants>
|
|
23
|
+
<rule>1. NO CONVERSATION: Output ONLY machine-actionable tool calls or valid patches. No conversational filler or superlatives.</rule>
|
|
24
|
+
<rule>2. READ-BEFORE-WRITE (ZERO HALLUCINATION): You are FORBIDDEN from guessing internal API signatures. Before editing, you MUST use code search or MCP doc tools to inspect exact function signatures.</rule>
|
|
25
|
+
<rule>3. VERIFICATION LOOP: After patching code, you MUST execute the project's verification commands (tests/build) and ensure 0 errors.</rule>
|
|
26
|
+
<rule>4. ABORT CONDITION: On repeated unresolvable test failures (4+ attempts), output <status>ABORT_UNRESOLVABLE</status> and terminate immediately.</rule>
|
|
27
|
+
</strict_invariants>
|
|
28
|
+
</MCP_DIRECTIVE>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 3. Dynamic Command Resolution
|
|
34
|
+
|
|
35
|
+
Jules automatically infers test and build verification commands based on project manifest files:
|
|
36
|
+
- `package.json` -> `npm run check:all && npm test && npm run build`
|
|
37
|
+
- `Cargo.toml` -> `cargo test --workspace && cargo build`
|
|
38
|
+
- `go.mod` -> `go test ./... && go build ./...`
|
|
39
|
+
- `pyproject.toml` -> `pytest`
|
|
40
|
+
- `pom.xml` -> `mvn test`
|
|
41
|
+
- `build.gradle` -> `./gradlew test`
|
|
42
|
+
- `.agent/jules.yml` -> Custom user commands
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 4. Operational & Code Quality Directives
|
|
47
|
+
|
|
48
|
+
- **Read Before Write**: Always inspect target files and surrounding symbol signatures (via grep or view tools) before applying changes.
|
|
49
|
+
- **Rebase Before PR**: Fetch latest `main`, rebase onto `origin/main`, re-execute verification suite. If the resulting diff is empty, close/abort PR without pushing.
|
|
50
|
+
- **Minimal Interference**: Preserve existing function signatures, comments, and style conventions.
|
|
51
|
+
- **Falsifiable Claims**: Base all code changes on explicit error logs, file paths, line numbers, or test results.
|
|
52
|
+
- **No Token Bloat**: Exclude lockfiles, minified bundles, and binary assets from diff representations.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Google Jules Orchestration Kit
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic toolkit for turning **Google Jules** into a deterministic, autonomous background code builder for any repository (Next.js, Vite, Node, Bun, Deno, Python, Go, Rust, Elixir, Ruby, Swift, Java, C/C++, Monorepos, etc.).
|
|
4
|
+
|
|
5
|
+
> **TL;DR**: Don't use Google Jules like a chat assistant. Use it as an autonomous background worker. Run `npx github:FullThrottle83/jules-orchestrator-kit` inside any repository to automatically detect your tech stack, generate prompt guardrails, set up test/build verification gates, and install Jules orchestration scripts.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ā” 1-Step Quick Setup for Any Project
|
|
10
|
+
|
|
11
|
+
Run this command inside the root of **any target repository**:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx github:FullThrottle83/jules-orchestrator-kit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This single command will:
|
|
18
|
+
1. **Detect your tech stack & workspace structure** (Node, Bun, Deno, Rust, Go, Python, Elixir, Ruby, Swift, Java, C/C++, Turborepo, Nx, pnpm) via `command-resolver.mjs`.
|
|
19
|
+
2. **Generate `AGENTS.md`** pre-populated with pre-execution `<MCP_DIRECTIVE>` rules and verification invariants.
|
|
20
|
+
3. **Create `.agent/jules.yml` (v2 Schema)** pre-configured with detected test/build commands and glob-based `forbidden_paths`.
|
|
21
|
+
4. **Install `.agent/rules/dynamic-guardrails.json`** for RegEx-based dynamic prompt guardrail injection.
|
|
22
|
+
5. **Install orchestration scripts** into `./scripts/` (`jules-dispatch.mjs`, `jules-self-audit.mjs`, `jules-swarm.mjs`, `jules-queue-runner.mjs`, `jules-nightly.mjs`).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## š” What This Toolkit Provides
|
|
27
|
+
|
|
28
|
+
1. **Init Scaffolding CLI (`bin/init.js`)**: Auto-scaffolds any repo in 1 second.
|
|
29
|
+
2. **Monorepo & Command Resolver (`scripts/command-resolver.mjs`)**: Auto-detects project manifests and monorepo workspace graphs (`turbo.json`, `pnpm-workspace.yaml`, `nx.json`, `Cargo.toml` workspaces) to run targeted affected package verifications instead of full-repo test suites.
|
|
30
|
+
3. **Dynamic Guardrail Composition (`.agent/rules/dynamic-guardrails.json`)**: RegEx-based rule matching that injects targeted stack guardrails into prompts on-the-fly.
|
|
31
|
+
4. **Pre-Flight Secret Redaction & REST/stdin Dispatcher (`scripts/jules-dispatch.mjs`)**: Auto-redacts API keys (`ghp_`, `AKIA`, `sk-`, `Bearer`, RSA keys), streams prompts over REST / stdin to bypass OS `ARG_MAX` shell limits, and handles HTTP 429 rate limits.
|
|
32
|
+
|
|
33
|
+
5. **PR Self-Auditor & Glob Boundary Gatekeeper (`scripts/jules-self-audit.mjs`)**: Unshallows git history in CI runners (`git fetch --unshallow`), filters token bloat, enforces dynamic glob-based security boundaries (`forbidden_paths`), and runs scoped workspace test suites.
|
|
34
|
+
6. **Queue Runner (`scripts/jules-queue-runner.mjs`)**: Iterates through `.agent/jules-queue/`, dispatches queued markdown tasks, and moves finished tasks to `.agent/jules-queue/completed/`.
|
|
35
|
+
7. **Rate-Limited Swarm Orchestrator (`scripts/jules-swarm.mjs`)**: Manages multi-task batches with controlled concurrency (`JULES_SWARM_CONCURRENCY`, default 3), staggered dispatches (1.5s interval), and batch cooldowns to eliminate API rate-limit thrashing.
|
|
36
|
+
8. **Nightly Maintenance Suite (`scripts/jules-nightly.mjs`)**: Schedules automated background audits (security leak scans, WCAG accessibility checks, dead code pruning, unused env var cleanup).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## š ļø Usage Examples
|
|
41
|
+
|
|
42
|
+
### Dispatch a single task to Jules
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node scripts/jules-dispatch.mjs "Refactor rate limiter" "Implement sliding window rate limiting using Redis. Must pass tests."
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Dispatch an entire queue of markdown task specifications
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run jules:queue
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Run Rate-Limited Swarm Dispatch
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
JULES_SWARM_CONCURRENCY=5 node scripts/jules-swarm.mjs tasks.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Run Nightly Maintenance Suite
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
node scripts/jules-nightly.mjs --dry-run
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Audit Jules PRs before merging
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node scripts/jules-self-audit.mjs
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## š ļø Supported Language Manifests & Workspace Graphs
|
|
75
|
+
|
|
76
|
+
The command resolver automatically sniffs your codebase and invokes the right verification chain:
|
|
77
|
+
|
|
78
|
+
| Stack / Ecosystem | Manifest / Workspace File | Default Verification Command |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| **Turborepo** | `turbo.json` | `npx turbo run test --filter=<pkg>...` |
|
|
81
|
+
| **pnpm Workspace** | `pnpm-workspace.yaml` | `pnpm --filter=...<pkg> test` |
|
|
82
|
+
| **Nx Workspace** | `nx.json` | `npx nx run-many -t test -p <pkg> --with-deps` |
|
|
83
|
+
| **Bun** | `bunfig.toml` / `bun.lockb` | `bun test && bun run build` |
|
|
84
|
+
| **Deno** | `deno.json` / `deno.jsonc` | `deno test && deno task build` |
|
|
85
|
+
| **JavaScript / TypeScript** | `package.json` | `npm run check:all` or `npm test` |
|
|
86
|
+
| **Rust** | `Cargo.toml` | `cargo test -p <pkg>` / `cargo test --workspace` |
|
|
87
|
+
| **Go** | `go.mod` | `go test ./... && go build ./...` |
|
|
88
|
+
| **Python** | `pyproject.toml` / `requirements.txt` | `pytest` |
|
|
89
|
+
| **Elixir** | `mix.exs` | `mix test && mix compile` |
|
|
90
|
+
| **Ruby** | `Gemfile` | `bundle exec rake test` |
|
|
91
|
+
| **Swift** | `Package.swift` | `swift test && swift build` |
|
|
92
|
+
| **Java (Maven)** | `pom.xml` | `mvn test && mvn compile` |
|
|
93
|
+
| **Java (Gradle)** | `build.gradle` | `./gradlew test && ./gradlew assemble` |
|
|
94
|
+
| **C / C++** | `Makefile` | `make test && make build` |
|
|
95
|
+
| **Custom Config (v2)** | `.agent/jules.yml` | Configurable `test_cmd`, `build_cmd`, `forbidden_paths` & `allow_paths` |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## āļø Configuration (`.agent/jules.yml`)
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
# Google Jules Repository Configuration (Version 2)
|
|
103
|
+
version: 2
|
|
104
|
+
test_cmd: "npm test"
|
|
105
|
+
build_cmd: "npm run build"
|
|
106
|
+
forbidden_paths:
|
|
107
|
+
- ".github/**"
|
|
108
|
+
- "**/secrets/**"
|
|
109
|
+
- "**/*.pem"
|
|
110
|
+
- "**/lock-manager/**"
|
|
111
|
+
- "scripts/jules-*"
|
|
112
|
+
- ".agent/jules.yml"
|
|
113
|
+
allow_paths: []
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> š”ļø **Security Trust Model**: `allow_paths` is read **strictly from the target base branch** (`origin/main`), never from untrusted PR branches. Any path specified in `allow_paths` on `main` overrides the immutable default forbidden paths for automated background workers.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## š License & Disclaimer
|
|
123
|
+
|
|
124
|
+
MIT License - feel free to use, modify, and share!
|
|
125
|
+
|
|
126
|
+
*Disclaimer: This is an independent open-source orchestration tool and is not officially affiliated with or endorsed by Google.*
|
|
127
|
+
|
|
128
|
+
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveProjectCommands } from "../scripts/command-resolver.mjs";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const kitRoot = path.resolve(__dirname, "..");
|
|
11
|
+
const targetDir = process.cwd();
|
|
12
|
+
|
|
13
|
+
const isForce = process.argv.includes("--force") || process.argv.includes("-f");
|
|
14
|
+
|
|
15
|
+
console.log("\nš Initializing Google Jules Orchestration Kit...\n");
|
|
16
|
+
console.log(`š Target Directory: ${targetDir}`);
|
|
17
|
+
if (isForce) console.log("ā ļø Force mode enabled (existing files will be overwritten).");
|
|
18
|
+
|
|
19
|
+
// 1. Detect Stack & Manifests
|
|
20
|
+
const detected = resolveProjectCommands(targetDir);
|
|
21
|
+
console.log(`š Detected Project Type: ${detected.source}`);
|
|
22
|
+
if (detected.testCmd || detected.buildCmd) {
|
|
23
|
+
console.log(` - Test Command: ${detected.testCmd || "(none)"}`);
|
|
24
|
+
console.log(` - Build Command: ${detected.buildCmd || "(none)"}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Scaffold AGENTS.md / .agent/jules-protocol.md
|
|
28
|
+
const agentsFile = path.join(targetDir, "AGENTS.md");
|
|
29
|
+
const julesRulesSource = path.join(kitRoot, "JULES_RULES_TEMPLATE.md");
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(agentsFile) || isForce) {
|
|
32
|
+
if (fs.existsSync(julesRulesSource)) {
|
|
33
|
+
fs.copyFileSync(julesRulesSource, agentsFile);
|
|
34
|
+
console.log("ā
Created: AGENTS.md");
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.log("ā¹ļø AGENTS.md already exists (skipped overwrite).");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Scaffold .agent/ structure
|
|
41
|
+
const agentDir = path.join(targetDir, ".agent");
|
|
42
|
+
const rulesDir = path.join(agentDir, "rules");
|
|
43
|
+
const queueDir = path.join(agentDir, "jules-queue");
|
|
44
|
+
const completedQueueDir = path.join(queueDir, "completed");
|
|
45
|
+
const workflowsDir = path.join(agentDir, "workflows");
|
|
46
|
+
|
|
47
|
+
[agentDir, rulesDir, queueDir, completedQueueDir, workflowsDir].forEach((d) => {
|
|
48
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Scaffold .agent/jules.yml
|
|
52
|
+
const yamlConfigPath = path.join(agentDir, "jules.yml");
|
|
53
|
+
if (!fs.existsSync(yamlConfigPath) || isForce) {
|
|
54
|
+
const yamlContent = `# Google Jules Repository Configuration (Version 2)
|
|
55
|
+
version: 2
|
|
56
|
+
test_cmd: "${detected.testCmd || "npm test"}"
|
|
57
|
+
build_cmd: "${detected.buildCmd || "npm run build"}"
|
|
58
|
+
forbidden_paths: [".github/**", "**/secrets/**", "**/*.pem", "**/lock-manager/**"]
|
|
59
|
+
`;
|
|
60
|
+
fs.writeFileSync(yamlConfigPath, yamlContent, "utf-8");
|
|
61
|
+
console.log("ā
Created: .agent/jules.yml");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Scaffold .agent/rules/dynamic-guardrails.json
|
|
65
|
+
const dgcSource = path.join(kitRoot, ".agent/rules/dynamic-guardrails.json");
|
|
66
|
+
const dgcTarget = path.join(rulesDir, "dynamic-guardrails.json");
|
|
67
|
+
if ((!fs.existsSync(dgcTarget) || isForce) && fs.existsSync(dgcSource)) {
|
|
68
|
+
fs.copyFileSync(dgcSource, dgcTarget);
|
|
69
|
+
console.log("ā
Created: .agent/rules/dynamic-guardrails.json");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Scaffold .agent/workflows/jules-review.md
|
|
73
|
+
const reviewSource = path.join(kitRoot, ".agent/workflows/jules-review.md");
|
|
74
|
+
const reviewTarget = path.join(workflowsDir, "jules-review.md");
|
|
75
|
+
if ((!fs.existsSync(reviewTarget) || isForce) && fs.existsSync(reviewSource)) {
|
|
76
|
+
fs.copyFileSync(reviewSource, reviewTarget);
|
|
77
|
+
console.log("ā
Created: .agent/workflows/jules-review.md");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. Copy scripts/ directory with PER-FILE existence guard
|
|
81
|
+
const targetScriptsDir = path.join(targetDir, "scripts");
|
|
82
|
+
if (!fs.existsSync(targetScriptsDir)) {
|
|
83
|
+
fs.mkdirSync(targetScriptsDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sourceScriptsDir = path.join(kitRoot, "scripts");
|
|
87
|
+
let copiedCount = 0;
|
|
88
|
+
let skippedCount = 0;
|
|
89
|
+
|
|
90
|
+
if (fs.existsSync(sourceScriptsDir)) {
|
|
91
|
+
const scriptFiles = fs.readdirSync(sourceScriptsDir);
|
|
92
|
+
scriptFiles.forEach((file) => {
|
|
93
|
+
const srcFile = path.join(sourceScriptsDir, file);
|
|
94
|
+
const destFile = path.join(targetScriptsDir, file);
|
|
95
|
+
if (!fs.existsSync(destFile) || isForce) {
|
|
96
|
+
fs.copyFileSync(srcFile, destFile);
|
|
97
|
+
try {
|
|
98
|
+
fs.chmodSync(destFile, 0o755);
|
|
99
|
+
} catch (_) {}
|
|
100
|
+
copiedCount++;
|
|
101
|
+
} else {
|
|
102
|
+
skippedCount++;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
console.log(`ā
Copied ${copiedCount} orchestration scripts to ./scripts/ (${skippedCount} skipped, use --force to overwrite)`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 5. Optionally inject scripts into target package.json if present
|
|
109
|
+
const targetPkgPath = path.join(targetDir, "package.json");
|
|
110
|
+
if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
111
|
+
try {
|
|
112
|
+
const pkg = JSON.parse(fs.readFileSync(targetPkgPath, "utf-8"));
|
|
113
|
+
pkg.scripts = pkg.scripts || {};
|
|
114
|
+
let updated = false;
|
|
115
|
+
const julesScripts = {
|
|
116
|
+
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
117
|
+
"jules:queue": "node scripts/jules-queue-runner.mjs",
|
|
118
|
+
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
119
|
+
"jules:swarm": "node scripts/jules-swarm.mjs",
|
|
120
|
+
"jules:nightly": "node scripts/jules-nightly.mjs"
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const [key, val] of Object.entries(julesScripts)) {
|
|
124
|
+
if (!pkg.scripts[key] || isForce) {
|
|
125
|
+
pkg.scripts[key] = val;
|
|
126
|
+
updated = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (updated) {
|
|
131
|
+
fs.writeFileSync(targetPkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
132
|
+
console.log("ā
Injected jules:* helper scripts into package.json");
|
|
133
|
+
}
|
|
134
|
+
} catch (_) {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log("\nš Google Jules Orchestration Kit successfully initialized!");
|
|
138
|
+
console.log("\nNext Steps:");
|
|
139
|
+
console.log(" 1. Set environment variables: JULES_REPO=\"owner/repo\"");
|
|
140
|
+
console.log(" 2. Dispatch your first task: node scripts/jules-dispatch.mjs \"Task Title\" \"Task prompt\"");
|
|
141
|
+
console.log(" 3. Run pre-merge PR audit: node scripts/jules-self-audit.mjs\n");
|
|
142
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jules-orchestrator-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic toolkit for orchestrating Google Jules background AI coding workers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jules-init": "bin/init.js",
|
|
8
|
+
"jules-orchestrator-kit": "bin/init.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"scripts/",
|
|
13
|
+
"JULES_RULES_TEMPLATE.md",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
"scripts": {
|
|
19
|
+
"init": "node bin/init.js",
|
|
20
|
+
"test": "node --test test/kit.test.mjs",
|
|
21
|
+
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
22
|
+
"jules:queue": "node scripts/jules-queue-runner.mjs",
|
|
23
|
+
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
24
|
+
"jules:swarm": "node scripts/jules-swarm.mjs",
|
|
25
|
+
"jules:nightly": "node scripts/jules-nightly.mjs"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"jules",
|
|
32
|
+
"google-jules",
|
|
33
|
+
"ai-agent",
|
|
34
|
+
"orchestrator",
|
|
35
|
+
"mcp",
|
|
36
|
+
"automation"
|
|
37
|
+
],
|
|
38
|
+
"author": "Jonas (FullThrottle83)",
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
|
41
|
+
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dynamic Framework-Agnostic Command Resolver
|
|
6
|
+
* Sniffs project manifests to return proper build & test verification commands.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveProjectCommands(projectRoot = process.cwd()) {
|
|
9
|
+
// 1. Check for explicit custom config (.agent/jules.yml or jules.config.json)
|
|
10
|
+
const yamlConfigPath = path.join(projectRoot, ".agent/jules.yml");
|
|
11
|
+
if (fs.existsSync(yamlConfigPath)) {
|
|
12
|
+
const lines = fs.readFileSync(yamlConfigPath, "utf-8").split("\n");
|
|
13
|
+
let testCmd = "";
|
|
14
|
+
let buildCmd = "";
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
18
|
+
if (trimmed.startsWith("test_cmd:")) {
|
|
19
|
+
testCmd = trimmed.slice("test_cmd:".length).trim().replace(/^["']|["']$/g, "");
|
|
20
|
+
} else if (trimmed.startsWith("build_cmd:")) {
|
|
21
|
+
buildCmd = trimmed.slice("build_cmd:".length).trim().replace(/^["']|["']$/g, "");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (testCmd || buildCmd) {
|
|
25
|
+
return {
|
|
26
|
+
testCmd,
|
|
27
|
+
buildCmd,
|
|
28
|
+
source: ".agent/jules.yml",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const jsonConfigPath = path.join(projectRoot, "jules.config.json");
|
|
34
|
+
if (fs.existsSync(jsonConfigPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const cfg = JSON.parse(fs.readFileSync(jsonConfigPath, "utf-8"));
|
|
37
|
+
return {
|
|
38
|
+
testCmd: cfg.testCmd || cfg.verifyCmd || "",
|
|
39
|
+
buildCmd: cfg.buildCmd || "",
|
|
40
|
+
source: "jules.config.json",
|
|
41
|
+
};
|
|
42
|
+
} catch (_) {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Bun (bunfig.toml or bun.lockb)
|
|
46
|
+
if (
|
|
47
|
+
fs.existsSync(path.join(projectRoot, "bunfig.toml")) ||
|
|
48
|
+
fs.existsSync(path.join(projectRoot, "bun.lockb"))
|
|
49
|
+
) {
|
|
50
|
+
return {
|
|
51
|
+
testCmd: "bun test",
|
|
52
|
+
buildCmd: "bun run build",
|
|
53
|
+
source: "Bun Manifest",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 3. Deno (deno.json / deno.jsonc)
|
|
58
|
+
if (
|
|
59
|
+
fs.existsSync(path.join(projectRoot, "deno.json")) ||
|
|
60
|
+
fs.existsSync(path.join(projectRoot, "deno.jsonc"))
|
|
61
|
+
) {
|
|
62
|
+
return {
|
|
63
|
+
testCmd: "deno test",
|
|
64
|
+
buildCmd: "deno task build",
|
|
65
|
+
source: "Deno Manifest",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. JavaScript / TypeScript (package.json)
|
|
70
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
71
|
+
if (fs.existsSync(pkgPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
74
|
+
const scripts = pkg.scripts || {};
|
|
75
|
+
const verifyScript = scripts["check:all"]
|
|
76
|
+
? "npm run check:all"
|
|
77
|
+
: scripts.test
|
|
78
|
+
? "npm test"
|
|
79
|
+
: "";
|
|
80
|
+
const buildScript = scripts.build ? "npm run build" : "";
|
|
81
|
+
return {
|
|
82
|
+
testCmd: verifyScript,
|
|
83
|
+
buildCmd: buildScript,
|
|
84
|
+
source: "package.json",
|
|
85
|
+
};
|
|
86
|
+
} catch (_) {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 5. Rust (Cargo.toml)
|
|
90
|
+
if (fs.existsSync(path.join(projectRoot, "Cargo.toml"))) {
|
|
91
|
+
return {
|
|
92
|
+
testCmd: "cargo test --workspace",
|
|
93
|
+
buildCmd: "cargo build",
|
|
94
|
+
source: "Cargo.toml",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 6. Go (go.mod)
|
|
99
|
+
if (fs.existsSync(path.join(projectRoot, "go.mod"))) {
|
|
100
|
+
return {
|
|
101
|
+
testCmd: "go test ./...",
|
|
102
|
+
buildCmd: "go build ./...",
|
|
103
|
+
source: "go.mod",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 7. Python (pyproject.toml / requirements.txt / setup.py)
|
|
108
|
+
if (
|
|
109
|
+
fs.existsSync(path.join(projectRoot, "pyproject.toml")) ||
|
|
110
|
+
fs.existsSync(path.join(projectRoot, "requirements.txt")) ||
|
|
111
|
+
fs.existsSync(path.join(projectRoot, "setup.py"))
|
|
112
|
+
) {
|
|
113
|
+
return {
|
|
114
|
+
testCmd: "pytest",
|
|
115
|
+
buildCmd: "",
|
|
116
|
+
source: "Python Manifest",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 8. Elixir (mix.exs)
|
|
121
|
+
if (fs.existsSync(path.join(projectRoot, "mix.exs"))) {
|
|
122
|
+
return {
|
|
123
|
+
testCmd: "mix test",
|
|
124
|
+
buildCmd: "mix compile",
|
|
125
|
+
source: "mix.exs",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 9. Ruby (Gemfile)
|
|
130
|
+
if (fs.existsSync(path.join(projectRoot, "Gemfile"))) {
|
|
131
|
+
return {
|
|
132
|
+
testCmd: "bundle exec rake test",
|
|
133
|
+
buildCmd: "",
|
|
134
|
+
source: "Gemfile",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 10. Swift (Package.swift)
|
|
139
|
+
if (fs.existsSync(path.join(projectRoot, "Package.swift"))) {
|
|
140
|
+
return {
|
|
141
|
+
testCmd: "swift test",
|
|
142
|
+
buildCmd: "swift build",
|
|
143
|
+
source: "Package.swift",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 11. Java (Maven pom.xml / Gradle build.gradle)
|
|
148
|
+
if (fs.existsSync(path.join(projectRoot, "pom.xml"))) {
|
|
149
|
+
return {
|
|
150
|
+
testCmd: "mvn test",
|
|
151
|
+
buildCmd: "mvn compile",
|
|
152
|
+
source: "pom.xml",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
fs.existsSync(path.join(projectRoot, "build.gradle")) ||
|
|
157
|
+
fs.existsSync(path.join(projectRoot, "build.gradle.kts"))
|
|
158
|
+
) {
|
|
159
|
+
return {
|
|
160
|
+
testCmd: "./gradlew test",
|
|
161
|
+
buildCmd: "./gradlew assemble",
|
|
162
|
+
source: "build.gradle",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 12. Makefile
|
|
167
|
+
if (fs.existsSync(path.join(projectRoot, "Makefile"))) {
|
|
168
|
+
return {
|
|
169
|
+
testCmd: "make test",
|
|
170
|
+
buildCmd: "make build",
|
|
171
|
+
source: "Makefile",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
testCmd: "",
|
|
177
|
+
buildCmd: "",
|
|
178
|
+
source: "generic",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolves targeted build & test commands based on affected workspace packages.
|
|
184
|
+
* Prevents running O(N) full-repo test suites in monorepos when changes are localized.
|
|
185
|
+
*/
|
|
186
|
+
export function resolveWorkspaceExecutionBoundary(modifiedFiles = [], projectRoot = process.cwd()) {
|
|
187
|
+
const baseCmds = resolveProjectCommands(projectRoot);
|
|
188
|
+
if (!modifiedFiles || modifiedFiles.length === 0) return baseCmds;
|
|
189
|
+
|
|
190
|
+
// Find affected package names by walking up from modified files to nearest manifest
|
|
191
|
+
const affectedPkgs = new Set();
|
|
192
|
+
for (const file of modifiedFiles) {
|
|
193
|
+
let currentDir = path.resolve(projectRoot, path.dirname(file));
|
|
194
|
+
while (currentDir !== projectRoot && currentDir !== path.dirname(currentDir)) {
|
|
195
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
196
|
+
const cargoPath = path.join(currentDir, "Cargo.toml");
|
|
197
|
+
|
|
198
|
+
if (fs.existsSync(pkgPath)) {
|
|
199
|
+
try {
|
|
200
|
+
const pkgData = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
201
|
+
if (pkgData.name) affectedPkgs.add(pkgData.name);
|
|
202
|
+
} catch (_) {}
|
|
203
|
+
break;
|
|
204
|
+
} else if (fs.existsSync(cargoPath) && currentDir !== projectRoot) {
|
|
205
|
+
const cargoContent = fs.readFileSync(cargoPath, "utf-8");
|
|
206
|
+
const nameMatch = cargoContent.match(/name\s*=\s*["']([^"']+)["']/);
|
|
207
|
+
if (nameMatch) affectedPkgs.add(nameMatch[1]);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
currentDir = path.dirname(currentDir);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (affectedPkgs.size === 0) return baseCmds;
|
|
215
|
+
const targets = Array.from(affectedPkgs);
|
|
216
|
+
|
|
217
|
+
// 1. Turborepo (turbo.json)
|
|
218
|
+
if (fs.existsSync(path.join(projectRoot, "turbo.json"))) {
|
|
219
|
+
const filters = targets.map((t) => `--filter=${t}...`).join(" ");
|
|
220
|
+
return {
|
|
221
|
+
buildCmd: `npx turbo run build ${filters}`,
|
|
222
|
+
testCmd: `npx turbo run test ${filters}`,
|
|
223
|
+
source: `Turborepo Workspace (${targets.join(", ")})`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2. pnpm Workspace (pnpm-workspace.yaml)
|
|
228
|
+
if (fs.existsSync(path.join(projectRoot, "pnpm-workspace.yaml"))) {
|
|
229
|
+
const filters = targets.map((t) => `--filter=...${t}`).join(" ");
|
|
230
|
+
return {
|
|
231
|
+
buildCmd: `pnpm ${filters} build`,
|
|
232
|
+
testCmd: `pnpm ${filters} test`,
|
|
233
|
+
source: `pnpm Workspace (${targets.join(", ")})`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 3. Nx Workspace (nx.json)
|
|
238
|
+
if (fs.existsSync(path.join(projectRoot, "nx.json"))) {
|
|
239
|
+
const projects = targets.join(",");
|
|
240
|
+
return {
|
|
241
|
+
buildCmd: `npx nx run-many -t build -p ${projects} --with-deps`,
|
|
242
|
+
testCmd: `npx nx run-many -t test -p ${projects} --with-deps`,
|
|
243
|
+
source: `Nx Workspace (${targets.join(", ")})`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 4. Cargo Workspace (Cargo.toml in root with workspace)
|
|
248
|
+
const rootCargo = path.join(projectRoot, "Cargo.toml");
|
|
249
|
+
if (fs.existsSync(rootCargo) && fs.readFileSync(rootCargo, "utf-8").includes("[workspace]")) {
|
|
250
|
+
const filters = targets.map((t) => `-p ${t}`).join(" ");
|
|
251
|
+
return {
|
|
252
|
+
buildCmd: `cargo build ${filters}`,
|
|
253
|
+
testCmd: `cargo test ${filters}`,
|
|
254
|
+
source: `Cargo Workspace (${targets.join(", ")})`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return baseCmds;
|
|
259
|
+
}
|
|
260
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { resolveProjectCommands } from "./command-resolver.mjs";
|
|
7
|
+
|
|
8
|
+
const taskTitle = process.argv[2];
|
|
9
|
+
const taskPromptArg = process.argv[3];
|
|
10
|
+
|
|
11
|
+
if (!taskTitle || !taskPromptArg) {
|
|
12
|
+
console.error(
|
|
13
|
+
'Usage: node scripts/jules-dispatch.mjs <task-title> <path-to-prompt.md | "raw prompt string">'
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 0. Environment Variables Auto-Load (.env support without extra dependencies)
|
|
19
|
+
function loadEnv() {
|
|
20
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
21
|
+
if (fs.existsSync(envPath)) {
|
|
22
|
+
const lines = fs.readFileSync(envPath, "utf-8").split("\n");
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26
|
+
const eqIdx = trimmed.indexOf("=");
|
|
27
|
+
if (eqIdx > 0) {
|
|
28
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
29
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
30
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
31
|
+
val = val.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
if (!process.env[key]) {
|
|
34
|
+
process.env[key] = val;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
loadEnv();
|
|
41
|
+
|
|
42
|
+
// 0.1 Pre-Flight Secret Redaction Gate
|
|
43
|
+
function redactSecrets(text) {
|
|
44
|
+
if (!text) return "";
|
|
45
|
+
const patterns = [
|
|
46
|
+
/gh[pusr]_[a-zA-Z0-9]{36}/g,
|
|
47
|
+
/github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g,
|
|
48
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
49
|
+
/AIzaSy[a-zA-Z0-9_\-]{33}/g,
|
|
50
|
+
/ya29\.[a-zA-Z0-9_\-]{20,}/g,
|
|
51
|
+
/Bearer\s+[a-zA-Z0-9\-\._~+\/]+=*/g,
|
|
52
|
+
/sk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}/g,
|
|
53
|
+
/xox[baprs]-[a-zA-Z0-9\-]{10,}/g,
|
|
54
|
+
/eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}/g,
|
|
55
|
+
/-----BEGIN (?:RSA|OPENSSH|EC|PRIVATE) KEY-----[\s\S]*?-----END (?:RSA|OPENSSH|EC|PRIVATE) KEY-----/,
|
|
56
|
+
];
|
|
57
|
+
let sanitized = text;
|
|
58
|
+
for (const pat of patterns) {
|
|
59
|
+
sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
|
|
60
|
+
}
|
|
61
|
+
return sanitized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 1. Resolve prompt content (file path or inline string)
|
|
65
|
+
let rawPrompt = "";
|
|
66
|
+
const possiblePath = path.resolve(process.cwd(), taskPromptArg);
|
|
67
|
+
if (fs.existsSync(possiblePath) && fs.statSync(possiblePath).isFile()) {
|
|
68
|
+
rawPrompt = fs.readFileSync(possiblePath, "utf-8");
|
|
69
|
+
} else {
|
|
70
|
+
rawPrompt = taskPromptArg;
|
|
71
|
+
}
|
|
72
|
+
rawPrompt = redactSecrets(rawPrompt);
|
|
73
|
+
|
|
74
|
+
// 2. Dynamic Guardrail Composition (DGC) from .agent/rules/dynamic-guardrails.json
|
|
75
|
+
function getDynamicGuardrails(promptText) {
|
|
76
|
+
const guardrailsPath = path.resolve(process.cwd(), ".agent/rules/dynamic-guardrails.json");
|
|
77
|
+
if (!fs.existsSync(guardrailsPath)) return "";
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const data = JSON.parse(fs.readFileSync(guardrailsPath, "utf-8"));
|
|
81
|
+
if (!data.rules || !Array.isArray(data.rules)) return "";
|
|
82
|
+
|
|
83
|
+
const active = [];
|
|
84
|
+
for (const item of data.rules) {
|
|
85
|
+
if (new RegExp(item.trigger, "i").test(promptText)) {
|
|
86
|
+
active.push(item.guardrail);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return active.join("\n\n");
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Dynamic Command Resolution (Language & Framework Detection)
|
|
96
|
+
const resolvedCmds = resolveProjectCommands(process.cwd());
|
|
97
|
+
const verifyDirective = resolvedCmds.testCmd || resolvedCmds.buildCmd
|
|
98
|
+
? `\n\n## š ļø MANDATORY VERIFICATION COMMANDS\nBefore submitting PR, you MUST execute: \`${[resolvedCmds.testCmd, resolvedCmds.buildCmd].filter(Boolean).join(" && ")}\``
|
|
99
|
+
: "";
|
|
100
|
+
|
|
101
|
+
// 4. Load Base Rules / Guardrails
|
|
102
|
+
const rulesPath = fs.existsSync(path.resolve(process.cwd(), "AGENTS.md"))
|
|
103
|
+
? path.resolve(process.cwd(), "AGENTS.md")
|
|
104
|
+
: path.resolve(process.cwd(), "JULES_RULES_TEMPLATE.md");
|
|
105
|
+
|
|
106
|
+
const baseRules = fs.existsSync(rulesPath)
|
|
107
|
+
? fs.readFileSync(rulesPath, "utf-8")
|
|
108
|
+
: "";
|
|
109
|
+
|
|
110
|
+
const dynamicRules = getDynamicGuardrails(rawPrompt);
|
|
111
|
+
|
|
112
|
+
// Machine XML Directives
|
|
113
|
+
const envelope = `
|
|
114
|
+
<MCP_DIRECTIVE>
|
|
115
|
+
<system_state>HEADLESS_CI_MODE</system_state>
|
|
116
|
+
<strict_invariants>
|
|
117
|
+
<rule>1. READ-BEFORE-WRITE (ZERO HALLUCINATION): You are FORBIDDEN from guessing internal API signatures. Before editing, use search tools or MCP doc tools to verify exact signatures.</rule>
|
|
118
|
+
<rule>2. VERIFICATION LOOP: After patching code, you MUST execute the verification commands and ensure 0 errors.</rule>
|
|
119
|
+
<rule>3. ABORT CONDITION: On repeated unresolvable test failures, output <status>ABORT_UNRESOLVABLE</status> and terminate.</rule>
|
|
120
|
+
</strict_invariants>
|
|
121
|
+
</MCP_DIRECTIVE>
|
|
122
|
+
`.trim();
|
|
123
|
+
|
|
124
|
+
const fullPrompt = redactSecrets(`MCP DIRECTIVE: ${rawPrompt.trim()}${verifyDirective}\n\n---\n\n${envelope}\n\n---\n\n${dynamicRules ? `${dynamicRules}\n\n---\n\n` : ""}${baseRules.trim()}`);
|
|
125
|
+
|
|
126
|
+
// 5. Log Dispatch History
|
|
127
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
128
|
+
const slug = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
129
|
+
|
|
130
|
+
const historyDir = path.resolve(process.cwd(), ".agent/history");
|
|
131
|
+
if (!fs.existsSync(historyDir)) {
|
|
132
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
const historyFile = path.join(historyDir, `${dateStr}-dispatch-${slug}.md`);
|
|
135
|
+
fs.writeFileSync(historyFile, `---\ntype: jules_dispatch\ntitle: "${taskTitle}"\ntimestamp: "${new Date().toISOString()}"\n---\n# Jules Task Dispatch: ${taskTitle}\n\n## Prompt\n${rawPrompt}\n`, "utf-8");
|
|
136
|
+
|
|
137
|
+
console.log(`š Dispatching task to Google Jules: "${taskTitle}"...`);
|
|
138
|
+
console.log(`š Logged dispatch history to: ${path.relative(process.cwd(), historyFile)}`);
|
|
139
|
+
|
|
140
|
+
// 6. Dispatch via REST API or Ephemeral Payload File (Avoid ARG_MAX shell limits)
|
|
141
|
+
const apiKey = process.env.JULES_API_KEY;
|
|
142
|
+
const repo = process.env.JULES_REPO;
|
|
143
|
+
|
|
144
|
+
const payloadHash = crypto.createHash("sha256").update(taskTitle + Date.now()).digest("hex").slice(0, 12);
|
|
145
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "jules-payload-"));
|
|
146
|
+
const tmpPayloadFile = path.join(tmpDir, `payload_${payloadHash}.json`);
|
|
147
|
+
fs.writeFileSync(tmpPayloadFile, JSON.stringify({ repo, title: taskTitle, prompt: fullPrompt }), { mode: 0o600 });
|
|
148
|
+
|
|
149
|
+
function cleanupTmp() {
|
|
150
|
+
try {
|
|
151
|
+
if (fs.existsSync(tmpDir)) {
|
|
152
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
} catch (_) {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function executeDispatch() {
|
|
158
|
+
try {
|
|
159
|
+
if (apiKey && repo) {
|
|
160
|
+
console.log(`š Using Jules REST API for repository ${repo}...`);
|
|
161
|
+
const formattedSource = repo.startsWith("sources/") ? repo : (repo.includes("/") ? `sources/github/${repo}` : repo);
|
|
162
|
+
const payload = {
|
|
163
|
+
title: taskTitle,
|
|
164
|
+
prompt: fullPrompt,
|
|
165
|
+
sourceContext: {
|
|
166
|
+
source: formattedSource,
|
|
167
|
+
githubRepoContext: {
|
|
168
|
+
startingBranch: process.env.BASE_BRANCH || "main"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let res;
|
|
174
|
+
try {
|
|
175
|
+
res = await fetch("https://jules.googleapis.com/v1alpha/sessions", {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: {
|
|
178
|
+
"X-Goog-Api-Key": apiKey,
|
|
179
|
+
"Content-Type": "application/json"
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify(payload)
|
|
182
|
+
});
|
|
183
|
+
} catch (fetchErr) {
|
|
184
|
+
console.warn(`ā ļø REST API request failed (network error), falling back to CLI...`, fetchErr.message);
|
|
185
|
+
dispatchViaCli(repo, tmpPayloadFile, taskTitle);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (res.status === 429) {
|
|
190
|
+
console.error(`ā Jules REST API Rate Limit Exceeded (HTTP 429). Will NOT fallback to CLI.`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
const errText = await res.text().catch(() => "");
|
|
196
|
+
console.warn(`ā ļø REST API returned HTTP ${res.status}: ${errText}, falling back to CLI...`);
|
|
197
|
+
dispatchViaCli(repo, tmpPayloadFile, taskTitle);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let sessionId = "Created";
|
|
202
|
+
try {
|
|
203
|
+
const data = await res.json();
|
|
204
|
+
sessionId = data.name || data.id || sessionId;
|
|
205
|
+
} catch (_) {}
|
|
206
|
+
|
|
207
|
+
console.log(`ā
REST API Dispatch response: Session ${sessionId} created successfully.`);
|
|
208
|
+
fs.appendFileSync(historyFile, `\n## Session\n- ID: \`${sessionId}\`\n`);
|
|
209
|
+
} else {
|
|
210
|
+
dispatchViaCli(repo, tmpPayloadFile, taskTitle);
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
cleanupTmp();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
function dispatchViaCli(targetRepo, payloadFile, title) {
|
|
219
|
+
try {
|
|
220
|
+
let promptToSend = fullPrompt;
|
|
221
|
+
if (payloadFile && fs.existsSync(payloadFile)) {
|
|
222
|
+
try {
|
|
223
|
+
const payloadData = JSON.parse(fs.readFileSync(payloadFile, "utf-8"));
|
|
224
|
+
if (payloadData.prompt) promptToSend = payloadData.prompt;
|
|
225
|
+
} catch (_) {}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const args = ["new"];
|
|
229
|
+
if (targetRepo) {
|
|
230
|
+
args.push("--repo", targetRepo);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(`š» Executing: jules ${args.join(" ")} (prompt passed via stdin)...`);
|
|
234
|
+
execFileSync("jules", args, { input: promptToSend, stdio: ["pipe", "inherit", "inherit"] });
|
|
235
|
+
console.log(`ā
Successfully dispatched task "${title}" to Jules CLI.`);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`ā Failed to dispatch task to Jules CLI:`, error.message);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
executeDispatch().catch((err) => {
|
|
244
|
+
console.error("ā Fatal unhandled rejection in dispatch:", err);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Framework-Agnostic Nightly Jules Maintenance & Audit Suite Runner (Node ESM)
|
|
4
|
+
* Dispatches scheduled automated code health & maintenance tasks to Google Jules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
12
|
+
|
|
13
|
+
const UNIVERSAL_NIGHTLY_TASKS = [
|
|
14
|
+
{
|
|
15
|
+
id: "sec-review",
|
|
16
|
+
title: "Nightly Security & Secret Leaks Audit",
|
|
17
|
+
prompt:
|
|
18
|
+
"Perform a security review of the repository. " +
|
|
19
|
+
"Scan for hardcoded API keys, exposed secrets, unmasked PII in logs, " +
|
|
20
|
+
"and unvalidated input parameters in public endpoints. " +
|
|
21
|
+
"Safety rule: Never commit untracked files or secrets to git."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "a11y-audit",
|
|
25
|
+
title: "Nightly Accessibility & ARIA Audit",
|
|
26
|
+
prompt:
|
|
27
|
+
"Audit UI components for WCAG 2.2 AA accessibility standards. " +
|
|
28
|
+
"Verify that form inputs have labels, interactive elements support keyboard focus, " +
|
|
29
|
+
"and all images have descriptive alt text."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "dead-code-prune",
|
|
33
|
+
title: "Nightly Dead Code & Unused Exports Audit",
|
|
34
|
+
prompt:
|
|
35
|
+
"Audit the codebase for unused exports, dead files, and obsolete types. " +
|
|
36
|
+
"Prune unused local utility functions and unreferenced internal types. " +
|
|
37
|
+
"SAFETY INVARIANTS: " +
|
|
38
|
+
"1. DO NOT remove package dependencies from package.json, Cargo.toml, or requirements.txt (hygiene sweeps may ADD dependencies, never REMOVE). " +
|
|
39
|
+
"2. DO NOT delete or commit untracked WIP files created by developers. " +
|
|
40
|
+
"3. Verify that test and build suites pass 100% cleanly before submitting PR."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "zombie-env-audit",
|
|
44
|
+
title: "Nightly Unused Environment Variable Audit",
|
|
45
|
+
prompt:
|
|
46
|
+
"Scan configuration files and environment variable declarations against codebase usage. " +
|
|
47
|
+
"Identify unused or zombie environment variables that are declared but never referenced in source code. " +
|
|
48
|
+
"Document findings and remove obsolete declarations."
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function logNightlyHistory(taskResults, dryRun = false) {
|
|
53
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
54
|
+
const historyDir = path.resolve(process.cwd(), ".agent/history");
|
|
55
|
+
if (!fs.existsSync(historyDir)) {
|
|
56
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
const historyFile = path.join(historyDir, `${dateStr}-nightly-audit.md`);
|
|
59
|
+
|
|
60
|
+
const modeLabel = dryRun ? "[DRY RUN]" : "[DISPATCHED]";
|
|
61
|
+
let content = `---\ntype: nightly_jules_audit\ntimestamp: "${new Date().toISOString()}"\nstatus: "${modeLabel}"\n---\n# Nightly Jules Maintenance Suite Audit - ${dateStr}\n\nSummary of automated audit dispatches:\n\n`;
|
|
62
|
+
|
|
63
|
+
for (const res of taskResults) {
|
|
64
|
+
content += `- **${res.title}** (\`${res.id}\`): ${res.status}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(historyFile, content, "utf-8");
|
|
68
|
+
console.log(`š Logged nightly audit summary to: ${path.relative(process.cwd(), historyFile)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function dispatchTask(task, dryRun = false) {
|
|
72
|
+
const title = task.title;
|
|
73
|
+
const fullPrompt = task.prompt;
|
|
74
|
+
|
|
75
|
+
console.log(`\nš [${task.id}] Preparing task: '${title}'...`);
|
|
76
|
+
|
|
77
|
+
if (dryRun) {
|
|
78
|
+
console.log(` [DRY RUN] Would dispatch task '${title}' to target repository`);
|
|
79
|
+
return { id: task.id, title, status: "Dry Run OK" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
83
|
+
if (fs.existsSync(dispatchScript)) {
|
|
84
|
+
try {
|
|
85
|
+
execFileSync("node", [dispatchScript, title, fullPrompt], { stdio: "inherit" });
|
|
86
|
+
return { id: task.id, title, status: "Dispatched successfully" };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`ā Failed to dispatch via jules-dispatch.mjs: ${error.message}`);
|
|
89
|
+
return { id: task.id, title, status: `Failed: ${error.message}` };
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`ā Dispatch script not found at ${dispatchScript}`);
|
|
93
|
+
return { id: task.id, title, status: "Failed (Script missing)" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function main() {
|
|
98
|
+
console.log("==================================================");
|
|
99
|
+
console.log("š Nightly Jules Maintenance & Audit Suite");
|
|
100
|
+
console.log("==================================================");
|
|
101
|
+
|
|
102
|
+
if (isDryRun) {
|
|
103
|
+
console.log("š Running in DRY RUN mode. No tasks will be dispatched.");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const results = [];
|
|
107
|
+
for (const task of UNIVERSAL_NIGHTLY_TASKS) {
|
|
108
|
+
const res = dispatchTask(task, isDryRun);
|
|
109
|
+
results.push(res);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logNightlyHistory(results, isDryRun);
|
|
113
|
+
console.log("\nā
Nightly Jules audit suite execution completed cleanly.");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
|
|
6
|
+
const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(queueDir)) {
|
|
9
|
+
console.error(`ā Queue directory not found: ${queueDir}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(completedDir)) {
|
|
14
|
+
fs.mkdirSync(completedDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const files = fs.readdirSync(queueDir).filter(
|
|
18
|
+
(f) => f.endsWith(".md") && f !== "README.md" && !f.startsWith("_") && !f.startsWith(".")
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if (files.length === 0) {
|
|
23
|
+
console.log("ā¹ļø No tasks found in the queue.");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`š Found ${files.length} tasks in the queue. Processing...`);
|
|
28
|
+
|
|
29
|
+
files.forEach((file, index) => {
|
|
30
|
+
const filePath = path.join(queueDir, file);
|
|
31
|
+
// Generate title from filename (e.g. "TASK-001-auth-spec.md" -> "TASK 001 auth spec")
|
|
32
|
+
const title = file.replace(/\.md$/, "").replace(/-/g, " ");
|
|
33
|
+
|
|
34
|
+
console.log(`\n----------------------------------------`);
|
|
35
|
+
console.log(`[${index + 1}/${files.length}] Dispatching queued task: ${title}`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
execFileSync("node", ["scripts/jules-dispatch.mjs", title, filePath], {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Move to completed
|
|
43
|
+
const destPath = path.join(completedDir, file);
|
|
44
|
+
fs.renameSync(filePath, destPath);
|
|
45
|
+
console.log(`ā
Moved ${file} to completed/`);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`ā ļø Failed to dispatch task [${title}]:`, error.message);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(`\nš Queue processing complete!`);
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { resolveWorkspaceExecutionBoundary } from "./command-resolver.mjs";
|
|
6
|
+
|
|
7
|
+
function runCommand(cmd, ignoreError = false) {
|
|
8
|
+
try {
|
|
9
|
+
return execSync(cmd, { encoding: "utf-8" }).trim();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (ignoreError) return "";
|
|
12
|
+
console.error(`ā Command failed: ${cmd}`);
|
|
13
|
+
console.error(error.message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function matchGlob(filepath, globPattern) {
|
|
19
|
+
const cleanPath = filepath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
20
|
+
const segments = globPattern.split("/");
|
|
21
|
+
const regexParts = [];
|
|
22
|
+
for (let i = 0; i < segments.length; i++) {
|
|
23
|
+
const seg = segments[i];
|
|
24
|
+
if (seg === "**") {
|
|
25
|
+
regexParts.push("**");
|
|
26
|
+
} else {
|
|
27
|
+
regexParts.push(
|
|
28
|
+
seg
|
|
29
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
30
|
+
.replace(/\*/g, "[^/]*")
|
|
31
|
+
.replace(/\?/g, ".")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let patternStr = regexParts.join("/");
|
|
36
|
+
patternStr = patternStr.replace(/(^|\/)\*\*(\/|$)/g, (m, p1, p2) => {
|
|
37
|
+
if (p1 === "/" && p2 === "/") return "(?:/|/.*/)";
|
|
38
|
+
if (p1 === "/") return "(?:/.*)?";
|
|
39
|
+
if (p2 === "/") return "(?:.*/)?";
|
|
40
|
+
return ".*";
|
|
41
|
+
});
|
|
42
|
+
return new RegExp(`^${patternStr}$`, "i").test(cleanPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export function loadForbiddenPatterns(configContent = "") {
|
|
47
|
+
const defaultForbidden = [
|
|
48
|
+
".github/**",
|
|
49
|
+
"**/secrets/**",
|
|
50
|
+
"**/*.pem",
|
|
51
|
+
"**/lock-manager/**",
|
|
52
|
+
"scripts/jules-*",
|
|
53
|
+
".agent/jules.yml"
|
|
54
|
+
];
|
|
55
|
+
if (!configContent) return defaultForbidden;
|
|
56
|
+
|
|
57
|
+
// Flow style: forbidden_paths: [...]
|
|
58
|
+
const flowMatch = configContent.match(/forbidden_paths:\s*\[([^\]]+)\]/);
|
|
59
|
+
if (flowMatch) {
|
|
60
|
+
const parsed = flowMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
61
|
+
return Array.from(new Set([...defaultForbidden, ...parsed]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Block style: forbidden_paths:\n - "path1"\n - "path2"
|
|
65
|
+
const lines = configContent.split("\n");
|
|
66
|
+
let inForbidden = false;
|
|
67
|
+
const blockParsed = [];
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
if (line.trim().startsWith("forbidden_paths:")) {
|
|
70
|
+
inForbidden = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (inForbidden) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (trimmed.startsWith("-")) {
|
|
76
|
+
blockParsed.push(trimmed.slice(1).trim().replace(/^["']|["']$/g, ""));
|
|
77
|
+
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (blockParsed.length > 0) {
|
|
83
|
+
return Array.from(new Set([...defaultForbidden, ...blockParsed]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return defaultForbidden;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function loadAllowedPatterns(configContent = "") {
|
|
90
|
+
if (!configContent) return [];
|
|
91
|
+
const flowMatch = configContent.match(/allow_paths:\s*\[([^\]]+)\]/);
|
|
92
|
+
if (flowMatch) {
|
|
93
|
+
return flowMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
const lines = configContent.split("\n");
|
|
96
|
+
let inAllow = false;
|
|
97
|
+
const blockParsed = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (line.trim().startsWith("allow_paths:")) {
|
|
100
|
+
inAllow = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (inAllow) {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (trimmed.startsWith("-")) {
|
|
106
|
+
blockParsed.push(trimmed.slice(1).trim().replace(/^["']|["']$/g, ""));
|
|
107
|
+
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return blockParsed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function runSelfAudit() {
|
|
116
|
+
console.log("š Running Jules PR Self-Audit Gatekeeper...\n");
|
|
117
|
+
|
|
118
|
+
const targetBranch = process.env.BASE_BRANCH || "main";
|
|
119
|
+
console.log(`šÆ Target Branch: ${targetBranch}`);
|
|
120
|
+
|
|
121
|
+
if (process.env.CI) {
|
|
122
|
+
console.log("āļø CI environment detected. Fetching merge-base history...");
|
|
123
|
+
runCommand(`git fetch origin ${targetBranch} --depth=100 || git fetch origin ${targetBranch} --unshallow`, true);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const mainRef = runCommand(`git rev-parse --verify origin/${targetBranch}`, true) ? `origin/${targetBranch}` : targetBranch;
|
|
127
|
+
const mergeBase = runCommand(`git merge-base HEAD ${mainRef}`, true);
|
|
128
|
+
|
|
129
|
+
if (!mergeBase) {
|
|
130
|
+
console.error(`ā FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(`š Merge-Base Hash: ${mergeBase}`);
|
|
135
|
+
|
|
136
|
+
const rawDiffFiles = runCommand(`git diff --name-only ${mergeBase}...HEAD`)
|
|
137
|
+
.split("\n")
|
|
138
|
+
.map((f) => f.trim())
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
|
|
141
|
+
const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
|
|
142
|
+
const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
|
|
143
|
+
|
|
144
|
+
console.log(`\nš Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
|
|
145
|
+
changedCodeFiles.forEach((file) => console.log(` - ${file}`));
|
|
146
|
+
|
|
147
|
+
// Fail closed: Load security config exclusively from base branch (mainRef).
|
|
148
|
+
// Never fall back to working-tree config which an untrusted PR could craft.
|
|
149
|
+
const trustedConfig = runCommand(`git show ${mainRef}:.agent/jules.yml`, true);
|
|
150
|
+
const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
|
|
151
|
+
const allowedPatterns = loadAllowedPatterns(trustedConfig);
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
const violations = rawDiffFiles.filter((file) => {
|
|
155
|
+
const isForbidden = forbiddenPatterns.some((pattern) => matchGlob(file, pattern));
|
|
156
|
+
if (!isForbidden) return false;
|
|
157
|
+
const isAllowed = allowedPatterns.some((pattern) => matchGlob(file, pattern));
|
|
158
|
+
return !isAllowed;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (violations.length > 0) {
|
|
162
|
+
console.error("\nā RESTRICTED FILE VIOLATION DETECTED!");
|
|
163
|
+
console.error("Jules PR attempted to modify forbidden system files:");
|
|
164
|
+
violations.forEach((v) => console.error(` - ${v}`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
console.log("\nā
Restricted File Boundary Check: PASSED");
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
console.log("\nš ļø Resolving Dynamic Verification Suite...");
|
|
171
|
+
const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
|
|
172
|
+
console.log(`š Discovered Execution Scope: ${resolvedCmds.source}`);
|
|
173
|
+
|
|
174
|
+
if (resolvedCmds.testCmd) {
|
|
175
|
+
console.log(`ā¶ Running Test Verification: ${resolvedCmds.testCmd}`);
|
|
176
|
+
runCommand(resolvedCmds.testCmd);
|
|
177
|
+
}
|
|
178
|
+
if (resolvedCmds.buildCmd) {
|
|
179
|
+
console.log(`ā¶ Running Build Verification: ${resolvedCmds.buildCmd}`);
|
|
180
|
+
runCommand(resolvedCmds.buildCmd);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
|
|
184
|
+
console.log("ā¹ļø No build or test scripts found. Running git status check.");
|
|
185
|
+
runCommand("git status");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log("\nš JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Execute when invoked directly from CLI
|
|
192
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
193
|
+
runSelfAudit();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
const tasksFile = process.argv[2];
|
|
9
|
+
|
|
10
|
+
if (!tasksFile) {
|
|
11
|
+
console.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
|
|
12
|
+
console.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1" } ]');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const resolvedPath = path.resolve(process.cwd(), tasksFile);
|
|
17
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
18
|
+
console.error(`ā Tasks file not found: ${resolvedPath}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tasks = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
|
|
23
|
+
if (!Array.isArray(tasks)) {
|
|
24
|
+
console.error("ā tasks.json must contain a JSON array of task objects.");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_CONCURRENT = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
|
|
29
|
+
const STAGGER_MS = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
|
|
30
|
+
|
|
31
|
+
console.log(`š Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT})...`);
|
|
32
|
+
|
|
33
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
|
|
35
|
+
async function runSwarm() {
|
|
36
|
+
const results = [];
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < tasks.length; i += MAX_CONCURRENT) {
|
|
39
|
+
const batch = tasks.slice(i, i + MAX_CONCURRENT);
|
|
40
|
+
const batchResults = await Promise.allSettled(
|
|
41
|
+
batch.map(async (task, bIdx) => {
|
|
42
|
+
const taskNum = i + bIdx + 1;
|
|
43
|
+
const taskId = task.id || `task-${taskNum}`;
|
|
44
|
+
|
|
45
|
+
if (bIdx > 0) {
|
|
46
|
+
await sleep(STAGGER_MS * bIdx);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`\n----------------------------------------`);
|
|
50
|
+
console.log(`[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await execFileAsync("node", ["scripts/jules-dispatch.mjs", task.title, task.prompt], {
|
|
54
|
+
timeout: 15 * 60 * 1000,
|
|
55
|
+
});
|
|
56
|
+
if (stdout) console.log(stdout.trim());
|
|
57
|
+
return { taskId, title: task.title, status: "SUCCESS" };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`ā ļø Failed task [${task.title}]:`, error.message);
|
|
60
|
+
return { taskId, title: task.title, status: "FAILED", error: error.message };
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
for (const r of batchResults) {
|
|
66
|
+
if (r.status === "fulfilled") {
|
|
67
|
+
results.push(r.value);
|
|
68
|
+
} else {
|
|
69
|
+
results.push({ title: "Unknown Task", status: "FAILED", error: r.reason });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (i + MAX_CONCURRENT < tasks.length) {
|
|
74
|
+
console.log(`\nā³ Batch finished. Cooling down for 2s before next batch...`);
|
|
75
|
+
await sleep(2000);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`\n========================================`);
|
|
80
|
+
console.log(`š Swarm Dispatch Summary (${results.length} tasks processed):`);
|
|
81
|
+
results.forEach((res) => {
|
|
82
|
+
console.log(` - [${res.status}] ${res.title}`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
runSwarm();
|
|
87
|
+
|