jules-orchestrator-kit 0.38.2 → 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/.agent/rules/jules-protocol.md +1 -1
- package/JULES_RULES_TEMPLATE.md +12 -8
- package/README.md +45 -8
- package/bin/agentctl.mjs +297 -9
- package/index.mjs +15 -1
- package/package.json +1 -1
- package/scripts/ci-scope-guard.mjs +186 -0
- package/scripts/jules-merge-swarm.mjs +13 -3
- package/src/assertions.mjs +579 -0
- package/src/budget.mjs +48 -6
- package/src/config.mjs +69 -6
- package/src/dag-engine.mjs +22 -1
- package/src/engine.mjs +133 -23
- package/src/evidence.mjs +111 -2
- package/src/execution-envelope.mjs +23 -7
- package/src/flaky-ledger.mjs +4 -1
- package/src/ops/command-registry.mjs +32 -0
- package/src/ops/doctor-planner.mjs +9 -3
- package/src/ops/pr-harvest.mjs +349 -0
- package/src/provider.mjs +491 -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/task-optimizer.mjs +9 -1
- package/src/telemetry.mjs +65 -0
- package/src/web-templates.mjs +372 -7
- package/src/wizard-init.mjs +13 -3
- package/src/wizard-task.mjs +76 -6
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";
|
|
@@ -149,8 +150,20 @@ export {
|
|
|
149
150
|
verifyEvidenceManifest,
|
|
150
151
|
generateEvidenceMarkdown,
|
|
151
152
|
computeEvidenceHash,
|
|
153
|
+
exportJsonReport,
|
|
152
154
|
} from "./src/evidence.mjs";
|
|
153
155
|
|
|
156
|
+
// Declarative Assertion Primitives
|
|
157
|
+
export {
|
|
158
|
+
assertDirSize,
|
|
159
|
+
assertFileSize,
|
|
160
|
+
assertFilePatterns,
|
|
161
|
+
assertFileExists,
|
|
162
|
+
runAssertion,
|
|
163
|
+
formatBytes,
|
|
164
|
+
resolveBytesLimit,
|
|
165
|
+
} from "./src/assertions.mjs";
|
|
166
|
+
|
|
154
167
|
export {
|
|
155
168
|
resolveDailyLimit,
|
|
156
169
|
budgetStatus,
|
|
@@ -161,6 +174,7 @@ export {
|
|
|
161
174
|
listOpenReservations,
|
|
162
175
|
releaseOpenReservations,
|
|
163
176
|
resolveConcurrency,
|
|
177
|
+
resolveAmbientIdentity,
|
|
164
178
|
CEILING_FILE,
|
|
165
179
|
} from "./src/budget.mjs";
|
|
166
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
|
}
|