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/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.38.2",
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
- const tier = classifyRiskTier(files);
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
  }