jules-orchestrator-kit 0.39.0 → 0.41.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/.agent/prompts/Bolt.md +10 -7
- package/.agent/prompts/Janitor.md +6 -6
- package/.agent/prompts/Task_Template.md +6 -5
- package/JULES_RULES_TEMPLATE.md +11 -7
- package/README.md +41 -8
- package/bin/agentctl.mjs +31 -1
- package/index.mjs +3 -1
- package/package.json +1 -1
- package/scripts/ci-scope-guard.mjs +186 -0
- package/scripts/jules-merge-swarm.mjs +13 -3
- package/src/budget.mjs +48 -6
- package/src/config.mjs +30 -4
- package/src/engine.mjs +5 -3
- package/src/evidence.mjs +61 -0
- package/src/execution-envelope.mjs +23 -7
- package/src/flaky-ledger.mjs +4 -1
- package/src/ops/doctor-planner.mjs +9 -3
- package/src/ops/pr-harvest.mjs +103 -13
- package/src/provider.mjs +188 -25
- package/src/review-repair.mjs +85 -7
- package/src/risk.mjs +134 -26
- package/src/role-resolver.mjs +54 -2
- package/src/router.mjs +64 -6
- package/src/state.mjs +15 -3
- package/src/telemetry.mjs +65 -0
- package/src/web-templates.mjs +154 -0
- package/src/wizard-init.mjs +13 -3
- package/src/wizard-task.mjs +76 -6
package/.agent/prompts/Bolt.md
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# Bolt - Performance & Payload Optimization Specialist ⚡
|
|
2
2
|
|
|
3
3
|
> **Role:** Codebase Micro-Optimizer & Payload Governor.
|
|
4
|
-
> **Scope:** Performance tuning,
|
|
4
|
+
> **Scope:** Performance tuning, artifact size reduction, and asset optimization with zero structural side-effects.
|
|
5
5
|
|
|
6
6
|
## Core Directives
|
|
7
7
|
|
|
8
8
|
1. **Payload Budgeting:**
|
|
9
|
-
- Keep total diff payload strictly under
|
|
10
|
-
-
|
|
9
|
+
- Keep total diff payload strictly under {{DIFF_KB}} KB (`git diff | wc -c`).
|
|
10
|
+
- Prefer this project's existing dependencies and its language's standard library over adding another third-party module. Removing a dependency whose job the standard library already does is in scope; adding one is not.
|
|
11
11
|
|
|
12
12
|
2. **Asset & Memory Optimization:**
|
|
13
|
-
- Replace heavy raster assets with modern WebP/AVIF
|
|
14
|
-
- Optimize hot execution paths: remove redundant
|
|
13
|
+
- Replace heavy raster assets with modern equivalents (WebP/AVIF) or clean vector graphics, where the project already serves such formats.
|
|
14
|
+
- Optimize hot execution paths: remove redundant allocations inside tight loops, and hoist work out of repeated calls.
|
|
15
15
|
|
|
16
|
-
3. **
|
|
17
|
-
-
|
|
16
|
+
3. **Evidence Before Claims:**
|
|
17
|
+
- A performance change requires numbers. Run the benchmark or timing measurement multiple times, compare medians, and state the delta. "Feels faster" is not a result, and a change below the noise floor is not an improvement.
|
|
18
|
+
|
|
19
|
+
4. **Zero Regressions Invariant:**
|
|
20
|
+
- Execute `{{VERIFY_TEST}}` before and after every optimization pass, and record both results.
|
|
18
21
|
- Never disable type-checks, skip tests, or alter public API signatures.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Janitor Protocol: Technical Debt & Dead Code Elimination
|
|
2
2
|
|
|
3
|
-
You are **Janitor**, a specialist autonomous agent optimized for technical debt elimination, dead code pruning, and
|
|
3
|
+
You are **Janitor**, a specialist autonomous agent optimized for technical debt elimination, dead code pruning, and conservative refactoring.
|
|
4
4
|
|
|
5
5
|
## Strict Operational Invariants
|
|
6
6
|
|
|
7
|
-
1. **
|
|
8
|
-
2. **Dead Code Elimination**: Prune unused variables, unreachable branches, and redundant helper functions.
|
|
9
|
-
3. **Atomic Payload Limit**: Keep total patch payload under
|
|
10
|
-
4. **Verification Requirement**: Execute `
|
|
11
|
-
5. **No Assert Weakening**: Never
|
|
7
|
+
1. **No New Dependencies**: Do NOT add third-party packages, libraries, or modules of any kind. Solve the task with this project's existing dependencies and its language's standard library. If a task genuinely cannot be completed without a new dependency, stop and say so instead of adding one.
|
|
8
|
+
2. **Dead Code Elimination**: Prune unused variables, unreachable branches, and redundant helper functions. Confirm a symbol has no remaining references across the whole repository before removing it — including dynamic lookups, reflection, and string-keyed access, which a definition-search will not find.
|
|
9
|
+
3. **Atomic Payload Limit**: Keep total patch payload under {{DIFF_KB}} KB (`git diff | wc -c`).
|
|
10
|
+
4. **Verification Requirement**: Execute `{{VERIFY_TEST}}` and `{{VERIFY_LINT}}` and ensure 100% of tests pass with 0 lint errors before completing work.
|
|
11
|
+
5. **No Assert Weakening**: Never weaken or remove test assertions to make a test pass. Leave an unmet requirement RED with a written rationale.
|
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
## Context
|
|
9
9
|
- **Project Goals:** [Describe key architectural or business goals.]
|
|
10
10
|
- **Key Files & Folders:** [List critical files, directories, or schemas, e.g. `src/auth.ts`, `schema.sql`.]
|
|
11
|
-
- **Tech Stack:** [List
|
|
11
|
+
- **Tech Stack:** [List this project's languages, frameworks, and libraries.]
|
|
12
12
|
|
|
13
13
|
## Requirements & Hard Constraints
|
|
14
14
|
- **Functional Requirements:** [List specific, non-negotiable functional requirements.]
|
|
15
15
|
- **Hard Constraints:**
|
|
16
|
-
- Do NOT introduce third-party
|
|
17
|
-
- Do NOT modify
|
|
18
|
-
- Keep total diff payload strictly under
|
|
16
|
+
- Do NOT introduce new third-party dependencies without explicit authorization.
|
|
17
|
+
- Do NOT modify this project's build manifest, lockfile, CI configuration, or agent scope files. Run `agentctl gate` to see the enforced set.
|
|
18
|
+
- Keep total diff payload strictly under {{DIFF_KB}} KB (`git diff | wc -c`).
|
|
19
19
|
|
|
20
20
|
## Verification Loop
|
|
21
|
-
- **Verification Command:** Execute
|
|
21
|
+
- **Verification Command:** Execute `{{VERIFY_TEST}}`.
|
|
22
22
|
- **Zero Errors Invariant:** Ensure 100% of tests pass cleanly with 0 errors before submitting.
|
|
23
|
+
- **Carry the Evidence:** Paste the actual terminal output. Exit code 0 proves the process survived, not that the change works.
|
|
23
24
|
|
|
24
25
|
## Expected Artifacts
|
|
25
26
|
- **Code Changes:** Clean, production-grade implementation preserving existing symbol contracts.
|
package/JULES_RULES_TEMPLATE.md
CHANGED
|
@@ -80,8 +80,8 @@ Jules automatically infers test and build verification commands via `scripts/com
|
|
|
80
80
|
|
|
81
81
|
## 6. Local CI Verification with Nektos Act
|
|
82
82
|
|
|
83
|
-
- **Pre-Push CI Validation**: When `.github/workflows/` exists and Nektos `act` is installed, execute `act push`
|
|
84
|
-
- **Log Inspection**: If local `act` CI fails, inspect
|
|
83
|
+
- **Pre-Push CI Validation**: When `.github/workflows/` exists and Nektos `act` is installed, execute `act push` to verify changes pass CI locally inside the VM before opening a PR. Skip this step if `act` is not on `PATH` — do not install it and do not invent a wrapper script for it.
|
|
84
|
+
- **Log Inspection**: If local `act` CI fails, inspect its output, resolve errors in code, and re-run verification before pushing.
|
|
85
85
|
- **Diff Payload Governor**: API forcefully truncates diff payloads > 80 KB. Keep total diff payload under 75 KB (`git diff | wc -c`).
|
|
86
86
|
|
|
87
87
|
---
|
|
@@ -101,7 +101,11 @@ To maximize the ratio of mergeable PRs vs. failed or hallucinated sessions, adhe
|
|
|
101
101
|
|
|
102
102
|
### Standard Jules Guardrails Footer
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
`agentctl task create` appends this automatically, generated from your own
|
|
105
|
+
`.agent/config.yml` scope — so the protected-path line lists *your* build
|
|
106
|
+
manifests (`Cargo.toml`, `go.mod`, `pyproject.toml`, `composer.json`, …) and
|
|
107
|
+
rebases onto *your* base branch. Fill the placeholders only for hand-written
|
|
108
|
+
dispatches; run `agentctl gate` to see the full enforced set.
|
|
105
109
|
|
|
106
110
|
```text
|
|
107
111
|
Read AGENTS.md and .agent/rules/jules-protocol.md BEFORE starting.
|
|
@@ -110,12 +114,12 @@ Follow all rules strictly.
|
|
|
110
114
|
TASK: <description>
|
|
111
115
|
|
|
112
116
|
HARD CONSTRAINTS:
|
|
113
|
-
- Do NOT modify
|
|
117
|
+
- Do NOT modify these protected paths: <your build manifest, lockfile, CI directory, and agent rules>.
|
|
114
118
|
- Diff Payload Governor: Keep total diff payload under 75 KB (`git diff | wc -c`) to prevent API truncation (~80 KB limit).
|
|
115
119
|
- Falsifiable & Evidence-Based: Attach full terminal verification output to PR. Never weaken assertions or delete failing tests to force a pass.
|
|
116
120
|
- Declare Scope Deviations: If modifying files outside task bounds, explicitly state rationale in PR.
|
|
117
|
-
- Verify before finishing: Run full type-check, lint, and
|
|
118
|
-
- BEFORE opening the PR: Run `git fetch origin
|
|
119
|
-
-
|
|
121
|
+
- Verify before finishing: Run the project's full type-check, lint, and test commands.
|
|
122
|
+
- BEFORE opening the PR: Run `git fetch origin <base> && git rebase origin/<base>`, then re-verify. If the rebase leaves an empty diff, the work already landed — do NOT submit.
|
|
123
|
+
- Remove any scratch files you created for debugging before submitting. Do not delete files that are part of the project.
|
|
120
124
|
```
|
|
121
125
|
|
package/README.md
CHANGED
|
@@ -129,9 +129,9 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
129
129
|
* **Cross-Platform Parity:** Verified 100% green across Linux, macOS (Darwin), and Windows on Node 20, 22, and 24.
|
|
130
130
|
* **Autonomous Self-Healing Loop:** Captures test stderr/stdout, fingerprints error traces, and feeds structured context back into automated repair turns (up to 3 attempts) before human escalation.
|
|
131
131
|
* **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
|
|
132
|
-
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors.
|
|
132
|
+
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
|
|
133
133
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
134
|
-
* **Verified Test Suite:** Tested with **
|
|
134
|
+
* **Verified Test Suite:** Tested with **671 unit tests across 84 suites passing in < 10.0s**.
|
|
135
135
|
|
|
136
136
|
<br/>
|
|
137
137
|
|
|
@@ -147,12 +147,13 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
147
147
|
| Command | Usage | Description | Exit Codes |
|
|
148
148
|
| :--- | :--- | :--- | :--- |
|
|
149
149
|
| `init` | `agentctl init [--interactive] [--tier pro]` | Interactive onboarding wizard & stack detector generating `.agent/config.yml`. | `0` (Created) |
|
|
150
|
+
| `budget` | `agentctl budget [--by-user] [--json] [reset]` | Reports rolling 24h task budget, quota headroom, and per-developer task attribution without external auth servers. | `0` (Status), `2` (Arg Error) |
|
|
150
151
|
| `task create` | `agentctl task create [--title <t>] [--prompt <p>] [--template <id>] [--role <name>] [--tier fast\|complex]` | Interactively authors & scopes falsifiable task envelopes with secret scrubbing, preflight gate checks, and DAG dependency wiring. | `0` (Queued), `1` (Secret/Unfalsifiable) |
|
|
151
|
-
| `task template` | `agentctl task template [<id>] [--list] [--json]` | Lists and synthesizes pre-calibrated task envelopes (Web & Agent Hardening: `web-cwv`, `web-wcag`, `web-seo`, `web-playwright`, `agent-dead-code-audit`, `web-flaky-heal`, `web-i18n`, `web-ai-access`, `agent-qa-mutation`, `agent-ci-falsify`, `agent-service-isolate`, `agent-error-paths`, `agent-security-audit`). | `0` (Listed/Synthesized) |
|
|
152
|
-
| `dispatch` | `agentctl dispatch [-p <prompt>] [-f <file>] [-r <role>] [-t <tier>] [--check-premise] [--auto-pr] [--repoless] [--dry-run]` | Dispatches autonomous task to the active provider with pre-flight idempotency checks, payload limits, and role prompt resolution. | `0` (Dispatched), `1` (Error) |
|
|
152
|
+
| `task template` | `agentctl task template [<id>] [--list] [--json]` | Lists and synthesizes pre-calibrated task envelopes (Web, Deep Think & Agent Hardening: `web-cwv`, `web-wcag`, `web-seo`, `web-playwright`, `agent-dead-code-audit`, `web-flaky-heal`, `web-i18n`, `web-ai-access`, `agent-qa-mutation`, `agent-ci-falsify`, `agent-service-isolate`, `agent-error-paths`, `agent-security-audit`, `deep-debug`, `deep-feature`, `deep-optimize`, `deep-harden`). | `0` (Listed/Synthesized) |
|
|
153
|
+
| `dispatch` | `agentctl dispatch [-p <prompt>] [-f <file>] [-r <role>] [-t <tier>] [--author <name>] [--check-premise] [--auto-pr] [--repoless] [--dry-run]` | Dispatches autonomous task to the active provider with pre-flight idempotency checks, payload limits, and role prompt resolution. | `0` (Dispatched), `1` (Error) |
|
|
153
154
|
| `plan approve` | `agentctl plan approve <sessionId> [--dry-run] [--json]` | Approves pending execution plan for an active Jules session (`:approvePlan`) with automatic 404/503 retry backoff. | `0` (Approved), `1` (Error) |
|
|
154
155
|
| `session get` | `agentctl session get <sessionId> [--dry-run] [--json]` | Retrieves live session lifecycle state from provider REST API with token rotation. | `0` (Fetched), `1` (Error) |
|
|
155
|
-
| `pr harvest` | `agentctl pr harvest [--tier r0,r1] [--limit <n>] [--auto] [--dry-run]` | Discovers open agent PRs, evaluates CI checks & risk tiers, and auto-squashes green low-risk changes autonomously. | `0` (Triaged/Merged), `1` (Error) |
|
|
156
|
+
| `pr harvest` | `agentctl pr harvest [--tier r0,r1] [--limit <n>] [--auto] [--allow-no-checks] [--dry-run]` | Discovers open agent PRs, evaluates CI checks & risk tiers, and auto-squashes green low-risk changes autonomously. A PR reporting **no** CI checks is skipped unless `--allow-no-checks` is passed, and an unavailable changed-file list blocks rather than classifying as low risk. | `0` (Triaged/Merged), `1` (Error) |
|
|
156
157
|
| `doctor` | `agentctl doctor [--json]` | Diagnostic DAG check runner & automated transactional self-repair engine. | `0` (Healthy), `1` (Failures) |
|
|
157
158
|
| `queue` | `agentctl queue [--dag] [--concurrency <n>] [--dry-run] [--json]` | Consumes and executes task envelopes in `.agent/jules-queue/` with Kahn's DAG dependency resolution. Non-task files (manifests, `README.md`) are skipped, and `--dry-run` previews without moving anything. | `0` (Complete) |
|
|
158
159
|
| `swarm` | `agentctl swarm [--json]` | Runs parallel multi-agent swarm across worker slots with PID liveness detection. | `0` (Complete) |
|
|
@@ -203,13 +204,28 @@ scope:
|
|
|
203
204
|
- ".agent/config.yml"
|
|
204
205
|
- "keys/**"
|
|
205
206
|
|
|
206
|
-
#
|
|
207
|
+
# Plan tier. Defaults to `free` when unset — the kit will not assume you are
|
|
208
|
+
# paying for a larger plan than you are. Set this to unlock your real limits.
|
|
209
|
+
tier: "free" # free | pro | ultra
|
|
210
|
+
|
|
211
|
+
# Risk model for auto-merge triage. Builtin patterns cover what is dangerous in
|
|
212
|
+
# any repository (CI, lockfiles, migrations, key material, IaC, auth). Add the
|
|
213
|
+
# paths that are sensitive to YOUR domain — these EXTEND the builtins.
|
|
214
|
+
risk:
|
|
215
|
+
restricted: # R3 — never auto-merged
|
|
216
|
+
- "**/pricing/**"
|
|
217
|
+
- "**/billing/**"
|
|
218
|
+
consequential: # R2 — always requires a human read
|
|
219
|
+
- "packages/api/**"
|
|
220
|
+
max_routine_diff_lines: 400
|
|
221
|
+
|
|
222
|
+
# Operational limits & governors (tier defaults shown; any key here overrides)
|
|
207
223
|
limits:
|
|
208
|
-
diffKb: 75 #
|
|
224
|
+
diffKb: 75 # Diff Payload Governor limit
|
|
209
225
|
promptKb: 50 # Maximum prompt payload size
|
|
210
226
|
dailyTasks: 300 # Task quota per rolling 24h window (not per calendar day)
|
|
211
227
|
repairAttempts: 3 # Maximum repair iterations
|
|
212
|
-
concurrency: 15 # Worker slots (free: 3, pro: 8, ultra: 15)
|
|
228
|
+
concurrency: 15 # Worker slots (defaults free: 3, pro: 8, ultra: 15)
|
|
213
229
|
|
|
214
230
|
# Dynamic Complexity & Cost Router — opt-in, disabled by default.
|
|
215
231
|
router:
|
|
@@ -312,6 +328,23 @@ const { provider, classification } = resolveRoutedProvider(
|
|
|
312
328
|
console.log(classification.tier); // "fast" | "complex"
|
|
313
329
|
```
|
|
314
330
|
|
|
331
|
+
### Syntax-Verified FAST Tier (`createSyntaxVerifiedProvider`)
|
|
332
|
+
`resolveRoutedProvider()` already wraps the FAST tier with this; use it directly only when composing your own provider cascade.
|
|
333
|
+
```javascript
|
|
334
|
+
import { createProvider, createSyntaxVerifiedProvider, loadConfig } from "jules-orchestrator-kit";
|
|
335
|
+
|
|
336
|
+
const config = loadConfig(process.cwd());
|
|
337
|
+
const fast = createSyntaxVerifiedProvider(
|
|
338
|
+
createProvider("gemini-flash", config),
|
|
339
|
+
createProvider("jules", config),
|
|
340
|
+
config
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// If gemini-flash leaves broken .js/.mjs/.cjs on disk, this transparently
|
|
344
|
+
// re-dispatches through "jules" instead of returning the broken result.
|
|
345
|
+
const result = await fast.dispatch({ prompt: "Fix a typo." }, { root: process.cwd() });
|
|
346
|
+
```
|
|
347
|
+
|
|
315
348
|
</details>
|
|
316
349
|
|
|
317
350
|
<br/>
|
package/bin/agentctl.mjs
CHANGED
|
@@ -155,6 +155,7 @@ async function main() {
|
|
|
155
155
|
"require-plan-approval": { type: "boolean" },
|
|
156
156
|
"check-premise": { type: "boolean" },
|
|
157
157
|
idempotent: { type: "boolean" },
|
|
158
|
+
author: { type: "string" },
|
|
158
159
|
"dry-run": { type: "boolean", short: "d" },
|
|
159
160
|
json: { type: "boolean", short: "j" },
|
|
160
161
|
},
|
|
@@ -186,6 +187,7 @@ async function main() {
|
|
|
186
187
|
autoPr: values["auto-pr"],
|
|
187
188
|
requirePlanApproval: values["require-plan-approval"],
|
|
188
189
|
checkPremise: values["check-premise"] || values.idempotent,
|
|
190
|
+
author: values.author,
|
|
189
191
|
};
|
|
190
192
|
|
|
191
193
|
try {
|
|
@@ -500,6 +502,27 @@ async function main() {
|
|
|
500
502
|
process.exit(0);
|
|
501
503
|
}
|
|
502
504
|
|
|
505
|
+
if (args.includes("--by-user") || args.includes("-u")) {
|
|
506
|
+
console.log("📊 Task Budget Attribution (Rolling 24h Window)");
|
|
507
|
+
console.log(`Daily Limit : ${b.limit} Tasks | Used: ${b.used} | Remaining: ${b.remaining}\n`);
|
|
508
|
+
const users = Object.entries(b.byUser || {});
|
|
509
|
+
if (users.length === 0) {
|
|
510
|
+
console.log(" No user activity recorded in the active 24h window.\n");
|
|
511
|
+
} else {
|
|
512
|
+
console.log(" Author Tasks Committed Pending");
|
|
513
|
+
console.log(" ------------------- ----- --------- -------");
|
|
514
|
+
for (const [user, stats] of users.sort(([, a], [, b]) => b.tasks - a.tasks)) {
|
|
515
|
+
const padUser = user.padEnd(20);
|
|
516
|
+
const padTasks = String(stats.tasks).padStart(5);
|
|
517
|
+
const padCommitted = String(stats.committed).padStart(9);
|
|
518
|
+
const padPending = String(stats.uncommitted).padStart(7);
|
|
519
|
+
console.log(` ${padUser} ${padTasks} ${padCommitted} ${padPending}`);
|
|
520
|
+
}
|
|
521
|
+
console.log("");
|
|
522
|
+
}
|
|
523
|
+
process.exit(0);
|
|
524
|
+
}
|
|
525
|
+
|
|
503
526
|
if (args.includes("--json")) {
|
|
504
527
|
console.log(JSON.stringify({ ok: true, budget: { ...b, scope: "this-repository" } }, null, 2));
|
|
505
528
|
process.exit(0);
|
|
@@ -663,7 +686,12 @@ async function main() {
|
|
|
663
686
|
const { runInitWizard } = await import("../src/wizard-init.mjs");
|
|
664
687
|
const res = await runInitWizard(root, {
|
|
665
688
|
interactive: values.interactive !== false,
|
|
666
|
-
|
|
689
|
+
// No `|| "pro"`: a hardcoded default here overrode both the tier picked
|
|
690
|
+
// in the menu and the tier already recorded in .agent/config.yml on a
|
|
691
|
+
// re-run. Undefined lets the wizard seed the menu from the existing
|
|
692
|
+
// config and fall back to FALLBACK_TIER when there is nothing to seed.
|
|
693
|
+
tier: values.tier,
|
|
694
|
+
allowDefaults: true,
|
|
667
695
|
});
|
|
668
696
|
|
|
669
697
|
if (values.json) {
|
|
@@ -1204,6 +1232,7 @@ async function main() {
|
|
|
1204
1232
|
limit: { type: "string" },
|
|
1205
1233
|
auto: { type: "boolean" },
|
|
1206
1234
|
merge: { type: "boolean" },
|
|
1235
|
+
"allow-no-checks": { type: "boolean" },
|
|
1207
1236
|
"dry-run": { type: "boolean", short: "d" },
|
|
1208
1237
|
json: { type: "boolean", short: "j" },
|
|
1209
1238
|
},
|
|
@@ -1216,6 +1245,7 @@ async function main() {
|
|
|
1216
1245
|
tier: values.tier,
|
|
1217
1246
|
limit,
|
|
1218
1247
|
auto: values.auto || values.merge,
|
|
1248
|
+
allowNoChecks: values["allow-no-checks"],
|
|
1219
1249
|
dryRun: values["dry-run"],
|
|
1220
1250
|
});
|
|
1221
1251
|
|
package/index.mjs
CHANGED
|
@@ -23,6 +23,7 @@ export { git, runCmd, resolveBase, changedFiles, diffBytes, diffText } from "./s
|
|
|
23
23
|
export {
|
|
24
24
|
createProvider,
|
|
25
25
|
createFailoverProvider,
|
|
26
|
+
createSyntaxVerifiedProvider,
|
|
26
27
|
JULES_PRESET,
|
|
27
28
|
CLAUDE_PRESET,
|
|
28
29
|
CODEX_PRESET,
|
|
@@ -97,7 +98,7 @@ export { detectStackOracles, runVerificationProbe } from "./src/wizard-oracle.mj
|
|
|
97
98
|
export { planInit, loadPresets, runInitWizard, TIER_PROFILES, BUILTIN_PRESETS } from "./src/wizard-init.mjs";
|
|
98
99
|
|
|
99
100
|
// Guided Task Authoring Subsystem
|
|
100
|
-
export { planTaskCreate, runTaskCreateWizard, GUARDRAIL_FOOTER } from "./src/wizard-task.mjs";
|
|
101
|
+
export { planTaskCreate, runTaskCreateWizard, GUARDRAIL_FOOTER, buildGuardrailFooter } from "./src/wizard-task.mjs";
|
|
101
102
|
|
|
102
103
|
// Prompt Falsifiability & Task Optimizer Engine
|
|
103
104
|
export { scorePromptFalsifiability, optimizeTaskPrompt, levenshteinDistance, extractPathTokens } from "./src/task-optimizer.mjs";
|
|
@@ -173,6 +174,7 @@ export {
|
|
|
173
174
|
listOpenReservations,
|
|
174
175
|
releaseOpenReservations,
|
|
175
176
|
resolveConcurrency,
|
|
177
|
+
resolveAmbientIdentity,
|
|
176
178
|
CEILING_FILE,
|
|
177
179
|
} from "./src/budget.mjs";
|
|
178
180
|
export { KIT_VERSION } from "./src/version.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for Google Jules (jules) autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CI Agent Scope Guard.
|
|
4
|
+
*
|
|
5
|
+
* Evaluates the files a pull request touches against the protected-paths
|
|
6
|
+
* manifest and fails the job when an agent has edited one of them.
|
|
7
|
+
*
|
|
8
|
+
* This exists as a Node entry point rather than inline shell because the shell
|
|
9
|
+
* version had to reimplement glob matching, and its glob-to-regex `sed`
|
|
10
|
+
* expression was invalid (`s/\*/[^/]*/g` — the `/` inside the character class
|
|
11
|
+
* closes the substitution). Under `bash -e` that aborted the step on the first
|
|
12
|
+
* modified file, so the guard never actually evaluated anything. Reusing
|
|
13
|
+
* `checkScope` removes the second implementation entirely: deny/protect
|
|
14
|
+
* matching now behaves identically in CI and locally, including the deliberate
|
|
15
|
+
* case-folding that a hand-rolled bash regex did not have.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { checkScope } from "../src/security.mjs";
|
|
19
|
+
import { normalizePath } from "../src/config.mjs";
|
|
20
|
+
|
|
21
|
+
/** Exit code 3 in the kit's registry: scope violation. */
|
|
22
|
+
const EXIT_SCOPE_VIOLATION = 3;
|
|
23
|
+
const EXIT_ERROR = 1;
|
|
24
|
+
|
|
25
|
+
/** Label that lets a human consciously land a protected-path change. */
|
|
26
|
+
export const BYPASS_LABEL = "allow-protected-paths";
|
|
27
|
+
|
|
28
|
+
function gitShow(ref, path, cwd) {
|
|
29
|
+
return execFileSync("git", ["show", `${ref}:${path}`], {
|
|
30
|
+
cwd,
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads the protected-paths manifest from the PR's *base* commit.
|
|
38
|
+
*
|
|
39
|
+
* Reading it from the head would let a pull request delete its own guard in the
|
|
40
|
+
* same diff it uses to edit a protected file, so the base commit is the only
|
|
41
|
+
* safe source. `baseSha` is preferred over `origin/<branch>` because the branch
|
|
42
|
+
* ref can advance mid-run while the SHA is pinned to what this PR targets.
|
|
43
|
+
*
|
|
44
|
+
* @param {{ baseSha?: string, baseRef?: string, root?: string }} opts
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
export function loadProtectedPatterns(opts = {}) {
|
|
48
|
+
const root = opts.root || process.cwd();
|
|
49
|
+
const manifestPath = ".agent/protected-paths.json";
|
|
50
|
+
const refs = [opts.baseSha, opts.baseRef ? `origin/${opts.baseRef}` : "", opts.baseRef].filter(Boolean);
|
|
51
|
+
|
|
52
|
+
let lastErr = null;
|
|
53
|
+
for (const ref of refs) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(gitShow(ref, manifestPath, root));
|
|
56
|
+
const patterns = Array.isArray(parsed.protected) ? parsed.protected.filter((p) => typeof p === "string" && p) : [];
|
|
57
|
+
if (patterns.length === 0) {
|
|
58
|
+
throw new Error(`${manifestPath} at ${ref} lists no protected patterns`);
|
|
59
|
+
}
|
|
60
|
+
return patterns;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
lastErr = err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fail closed: an unreadable manifest means the guard cannot make a decision,
|
|
67
|
+
// and "cannot decide" must never render as "approved".
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Unable to read ${manifestPath} from any of [${refs.join(", ")}]: ${lastErr ? lastErr.message : "no refs supplied"}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Lists the paths a pull request changes.
|
|
75
|
+
*
|
|
76
|
+
* `-z` and `core.quotePath=false` matter here: without them git splits on
|
|
77
|
+
* whitespace and octal-escapes non-ASCII names, so `docs/min plan.md` and
|
|
78
|
+
* `säkerhet/nyckel.pem` arrive as tokens that match no pattern — a protected
|
|
79
|
+
* file walking past the guard because of how it is spelled.
|
|
80
|
+
*
|
|
81
|
+
* @param {{ baseSha: string, headSha: string, root?: string }} opts
|
|
82
|
+
* @returns {string[]}
|
|
83
|
+
*/
|
|
84
|
+
export function listChangedFiles(opts = {}) {
|
|
85
|
+
const root = opts.root || process.cwd();
|
|
86
|
+
const raw = execFileSync(
|
|
87
|
+
"git",
|
|
88
|
+
["-c", "core.quotePath=false", "diff", "-z", "--name-only", opts.baseSha, opts.headSha],
|
|
89
|
+
{ cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024 }
|
|
90
|
+
);
|
|
91
|
+
return raw.split("\0").map(normalizePath).filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pure evaluation core, so the decision is testable without a git repository.
|
|
96
|
+
*
|
|
97
|
+
* @param {string[]} files
|
|
98
|
+
* @param {string[]} patterns
|
|
99
|
+
* @param {{ labels?: string[] }} [opts]
|
|
100
|
+
* @returns {{ ok: boolean, bypassed: boolean, violations: Array<object> }}
|
|
101
|
+
*/
|
|
102
|
+
export function evaluateScopeGuard(files = [], patterns = [], opts = {}) {
|
|
103
|
+
const labels = (opts.labels || []).map((l) => String(l).toLowerCase().trim());
|
|
104
|
+
const bypassed = labels.includes(BYPASS_LABEL);
|
|
105
|
+
|
|
106
|
+
// Matching always runs at full strength; the label only decides whether a
|
|
107
|
+
// match blocks. Passing `allowProtected` into checkScope instead would make
|
|
108
|
+
// a bypassed run report zero violations, and the job log is the record of
|
|
109
|
+
// what a human waved through.
|
|
110
|
+
const res = checkScope(files, { protect: patterns }, { allowProtected: false });
|
|
111
|
+
return { ok: bypassed || res.ok, bypassed, violations: res.violations };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parses the labels payload GitHub Actions exposes for a pull request.
|
|
116
|
+
* Accepts the raw `toJSON(...labels)` array or a plain comma-separated string.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} raw
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
export function parseLabels(raw = "") {
|
|
122
|
+
const text = String(raw || "").trim();
|
|
123
|
+
if (!text) return [];
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(text);
|
|
126
|
+
if (Array.isArray(parsed)) {
|
|
127
|
+
return parsed.map((l) => (typeof l === "string" ? l : l && l.name) || "").filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
return text.split(",").map((s) => s.trim()).filter(Boolean);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function main() {
|
|
134
|
+
const root = process.cwd();
|
|
135
|
+
const baseSha = (process.env.BASE_SHA || "").trim();
|
|
136
|
+
const headSha = (process.env.HEAD_SHA || "").trim();
|
|
137
|
+
const baseRef = (process.env.BASE_REF || "").trim();
|
|
138
|
+
|
|
139
|
+
if (!baseSha || !headSha) {
|
|
140
|
+
console.error("::error::BASE_SHA and HEAD_SHA must be set. This guard only runs on pull_request events.");
|
|
141
|
+
process.exit(EXIT_ERROR);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let patterns;
|
|
145
|
+
let files;
|
|
146
|
+
try {
|
|
147
|
+
patterns = loadProtectedPatterns({ baseSha, baseRef, root });
|
|
148
|
+
files = listChangedFiles({ baseSha, headSha, root });
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error(`::error::Agent Scope Guard could not evaluate this pull request: ${err.message}`);
|
|
151
|
+
process.exit(EXIT_ERROR);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const labels = parseLabels(process.env.PR_LABELS);
|
|
155
|
+
const result = evaluateScopeGuard(files, patterns, { labels });
|
|
156
|
+
|
|
157
|
+
console.log(`Protected patterns (${patterns.length}): ${patterns.join(", ")}`);
|
|
158
|
+
console.log(`Changed files (${files.length}):`);
|
|
159
|
+
for (const f of files) console.log(` ${f}`);
|
|
160
|
+
|
|
161
|
+
if (result.bypassed && result.violations.length > 0) {
|
|
162
|
+
for (const v of result.violations) {
|
|
163
|
+
console.log(`::warning file=${v.file}::Protected path modified under "${BYPASS_LABEL}": ${v.reason}`);
|
|
164
|
+
}
|
|
165
|
+
console.log(`\nLabel "${BYPASS_LABEL}" is present — ${result.violations.length} protected-path match(es) allowed by human review.`);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (result.ok) {
|
|
170
|
+
console.log("\nScope check passed. No protected files were modified.");
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const v of result.violations) {
|
|
175
|
+
console.error(`::error file=${v.file}::Protected path violation: ${v.reason}`);
|
|
176
|
+
}
|
|
177
|
+
console.error(
|
|
178
|
+
`::error::PR modifies ${result.violations.length} protected file(s). ` +
|
|
179
|
+
`Apply the "${BYPASS_LABEL}" label after human review to land this intentionally.`
|
|
180
|
+
);
|
|
181
|
+
process.exit(EXIT_SCOPE_VIOLATION);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (process.argv[1] && process.argv[1].endsWith("ci-scope-guard.mjs")) {
|
|
185
|
+
main();
|
|
186
|
+
}
|
|
@@ -11,7 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { classifyRiskTier, RISK_TIERS } from "../src/risk.mjs";
|
|
13
13
|
import { git, resolveBase } from "../src/git.mjs";
|
|
14
|
-
import { normalizePath } from "../src/config.mjs";
|
|
14
|
+
import { normalizePath, loadConfig } from "../src/config.mjs";
|
|
15
15
|
|
|
16
16
|
export const EXIT = Object.freeze({
|
|
17
17
|
SUCCESS: 0,
|
|
@@ -155,7 +155,7 @@ export function attemptCodeMergeFile(repoRoot, relPath, oursContent, baseContent
|
|
|
155
155
|
/**
|
|
156
156
|
* Checks safety gate against active worker locks and risk tiers.
|
|
157
157
|
*/
|
|
158
|
-
export function checkSafetyGate(branchName = "", projectRoot = process.cwd()) {
|
|
158
|
+
export function checkSafetyGate(branchName = "", projectRoot = process.cwd(), opts = {}) {
|
|
159
159
|
const locksDir = join(projectRoot, ".agent/state/locks");
|
|
160
160
|
if (existsSync(locksDir)) {
|
|
161
161
|
try {
|
|
@@ -179,7 +179,17 @@ export function checkSafetyGate(branchName = "", projectRoot = process.cwd()) {
|
|
|
179
179
|
const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedBase}...${branchName}`], { cwd: projectRoot, raw: true, ignoreError: true }) || "";
|
|
180
180
|
const files = raw.split("\0").map(normalizePath).filter(Boolean);
|
|
181
181
|
if (files.length > 0) {
|
|
182
|
-
|
|
182
|
+
// The repository's own risk paths must apply here too, or the gate and
|
|
183
|
+
// the harvester classify the same branch differently.
|
|
184
|
+
let config = opts.config;
|
|
185
|
+
if (!config) {
|
|
186
|
+
try {
|
|
187
|
+
config = loadConfig(projectRoot);
|
|
188
|
+
} catch (_) {
|
|
189
|
+
config = {};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const tier = classifyRiskTier(files, { config });
|
|
183
193
|
if (tier.tier === RISK_TIERS.R3) {
|
|
184
194
|
return { safe: false, reason: `R3 Restricted Path violation: ${tier.reason}` };
|
|
185
195
|
}
|
package/src/budget.mjs
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, openSync, fsyncSync, closeSync, renameSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { userInfo } from "node:os";
|
|
3
5
|
import { resolveRoot } from "./config.mjs";
|
|
4
6
|
import {
|
|
5
7
|
getStateDir,
|
|
6
8
|
ensureDir,
|
|
7
9
|
appendLedger,
|
|
8
|
-
checkDailyBudget,
|
|
9
10
|
scanBudgetWindow,
|
|
10
11
|
ROLLING_WINDOW_MS,
|
|
11
12
|
} from "./state.mjs";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Resolves ambient developer identity with zero dependencies and strict PII protection.
|
|
16
|
+
* Hierarchy: CLI flag -> GITHUB_ACTOR -> Git config user.email (stripped of domain) -> OS username -> anonymous-local
|
|
17
|
+
* @param {string|null} [cliOverride]
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function resolveAmbientIdentity(cliOverride = null) {
|
|
21
|
+
const sanitize = (str) =>
|
|
22
|
+
(str || "")
|
|
23
|
+
.toString()
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.split("@")[0] // Strip email domain to prevent PII leakage
|
|
27
|
+
.replace(/[^a-z0-9_.-]/g, ""); // Remove unsafe injection/special chars
|
|
28
|
+
|
|
29
|
+
if (cliOverride) {
|
|
30
|
+
const cleaned = sanitize(cliOverride);
|
|
31
|
+
if (cleaned) return cleaned;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (process.env.GITHUB_ACTOR) {
|
|
35
|
+
const actor = sanitize(process.env.GITHUB_ACTOR);
|
|
36
|
+
if (actor) return `ci-${actor}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const gitEmail = execSync("git config user.email", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
41
|
+
const cleaned = sanitize(gitEmail);
|
|
42
|
+
if (cleaned) return cleaned;
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const osUser = process.env.USER || process.env.USERNAME || userInfo().username;
|
|
47
|
+
const cleaned = sanitize(osUser);
|
|
48
|
+
if (cleaned) return `${cleaned}-local`;
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
|
|
51
|
+
return "anonymous-local";
|
|
52
|
+
}
|
|
53
|
+
|
|
13
54
|
/**
|
|
14
55
|
* Where the observed quota ceiling lives, outside the ledger so it survives
|
|
15
56
|
* rotation.
|
|
@@ -347,21 +388,22 @@ export function releaseOpenReservations(opts = {}) {
|
|
|
347
388
|
*/
|
|
348
389
|
export function budgetStatus(config, root = resolveRoot()) {
|
|
349
390
|
const resolved = resolveDailyLimit(config, root);
|
|
350
|
-
const
|
|
391
|
+
const scan = scanBudgetWindow(root);
|
|
351
392
|
return {
|
|
352
|
-
used:
|
|
393
|
+
used: scan.used,
|
|
353
394
|
limit: resolved.limit,
|
|
354
|
-
remaining:
|
|
395
|
+
remaining: Math.max(0, resolved.limit - scan.used),
|
|
396
|
+
byUser: scan.byUser || {},
|
|
355
397
|
tier: config?.tier || "unknown",
|
|
356
398
|
source: resolved.source,
|
|
357
399
|
certain: resolved.certain,
|
|
358
400
|
note: resolved.note,
|
|
359
401
|
// The count is a rolling 24h window, not a calendar day — surfaced so a
|
|
360
402
|
// caller reporting "used today" cannot quietly mean something else.
|
|
361
|
-
windowStart:
|
|
403
|
+
windowStart: scan.windowStart || "",
|
|
362
404
|
windowHours: ROLLING_WINDOW_MS / 3600000,
|
|
363
405
|
// Only a limit we actually know may stop a dispatch.
|
|
364
406
|
enforced: resolved.certain,
|
|
365
|
-
exhausted:
|
|
407
|
+
exhausted: scan.used >= resolved.limit,
|
|
366
408
|
};
|
|
367
409
|
}
|