jules-orchestrator-kit 0.52.8 → 0.53.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/README.md +66 -7
- package/bin/agentctl.mjs +172 -1
- package/bin/init.js +87 -74
- package/index.mjs +10 -0
- package/package.json +5 -2
- package/scripts/jules-create.mjs +5 -16
- package/scripts/jules-patch.mjs +18 -10
- package/scripts/stale-base-check.mjs +1 -1
- package/src/ci-templates.mjs +199 -0
- package/src/config.mjs +39 -11
- package/src/engine.mjs +2 -21
- package/src/env-aliases.mjs +80 -0
- package/src/mcp.mjs +19 -1
- package/src/ops/doctor-registry.mjs +53 -32
- package/src/ops/next-step.mjs +38 -7
- package/src/profiles-io.mjs +77 -0
- package/src/profiles.mjs +188 -0
- package/src/provider-readiness.mjs +215 -0
- package/src/security.mjs +42 -1
- package/src/tui.mjs +14 -3
- package/src/wizard-init.mjs +32 -4
- package/src/ops/doctor-planner.mjs +0 -220
- package/src/ops/receipts.mjs +0 -171
- package/src/ops/swarm-actions.mjs +0 -156
- package/src/ops/task-actions.mjs +0 -214
- package/src/ops/transaction.mjs +0 -278
- package/src/ux/capabilities.mjs +0 -180
- package/src/ux/diff-viewer.mjs +0 -289
- package/src/ux/layout.mjs +0 -195
- package/src/ux/log-viewer.mjs +0 -110
- package/src/ux/palette.mjs +0 -179
- package/src/ux/queue-model.mjs +0 -206
- package/src/ux/renderer.mjs +0 -188
- package/src/ux/swarm-model.mjs +0 -146
- package/src/ux/terminal-session.mjs +0 -215
- package/src/ux/widgets.mjs +0 -323
- /package/src/{ux/key-decoder.mjs → key-decoder.mjs} +0 -0
package/scripts/jules-create.mjs
CHANGED
|
@@ -4,25 +4,14 @@
|
|
|
4
4
|
* Quick task file creator for .agent/jules-queue/.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { getQueueDir } from "../src/state.mjs";
|
|
7
|
+
import { runTaskCreateWizard } from "../src/wizard-task.mjs";
|
|
10
8
|
|
|
11
|
-
const
|
|
12
|
-
const queueDir = getQueueDir();
|
|
13
|
-
const safeTitle = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
14
|
-
const filepath = join(queueDir, `TASK-${Date.now()}-${safeTitle}.md`);
|
|
15
|
-
|
|
16
|
-
const boilerplate = `# ${taskTitle}
|
|
17
|
-
|
|
18
|
-
## Objective
|
|
19
|
-
[Describe task goal]
|
|
20
|
-
`;
|
|
9
|
+
const prompt = process.argv.slice(2).join(" ").trim();
|
|
21
10
|
|
|
22
11
|
try {
|
|
23
|
-
|
|
24
|
-
console.log(
|
|
12
|
+
const result = await runTaskCreateWizard(process.cwd(), prompt ? { prompt } : {});
|
|
13
|
+
console.log(`✅ Scaffolded task envelope: ${result.taskFile}`);
|
|
25
14
|
} catch (err) {
|
|
26
|
-
console.error(
|
|
15
|
+
console.error(`❌ Failed to create task: ${err.message}`);
|
|
27
16
|
process.exit(1);
|
|
28
17
|
}
|
package/scripts/jules-patch.mjs
CHANGED
|
@@ -4,20 +4,28 @@
|
|
|
4
4
|
* Session patch fetching utility.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { extractSessionPatch } from "../src/session-ops.mjs";
|
|
8
|
+
|
|
9
|
+
export async function fetchSessionPatch(sessionId, options = {}) {
|
|
8
10
|
if (!sessionId || typeof sessionId !== "string") {
|
|
9
11
|
throw new Error("Session ID is required.");
|
|
10
12
|
}
|
|
11
|
-
return
|
|
12
|
-
sessionId,
|
|
13
|
-
state: "COMPLETED",
|
|
14
|
-
title: "Patch Session",
|
|
15
|
-
hasPatch: false,
|
|
16
|
-
diff: "",
|
|
17
|
-
};
|
|
13
|
+
return await extractSessionPatch(sessionId, options);
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
if (process.argv[1] && process.argv[1].endsWith("jules-patch.mjs")) {
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
const sessionId = process.argv[2];
|
|
18
|
+
if (!sessionId) {
|
|
19
|
+
console.error("Usage: node scripts/jules-patch.mjs <sessionId>");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
fetchSessionPatch(sessionId)
|
|
23
|
+
.then((res) => {
|
|
24
|
+
console.log(`Patch fetch completed for ${sessionId}: ${res.patch ? "Patch found" : "No patch"}`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
})
|
|
27
|
+
.catch((err) => {
|
|
28
|
+
console.error(`Patch fetch failed: ${err.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
23
31
|
}
|
|
@@ -8,7 +8,7 @@ const baseBranch = process.env.BASE_BRANCH || "main";
|
|
|
8
8
|
|
|
9
9
|
try {
|
|
10
10
|
const resolvedBase = resolveBase(root, baseBranch);
|
|
11
|
-
const behindStr = git(["rev-list", "--count",
|
|
11
|
+
const behindStr = git(["rev-list", "--count", `HEAD..${resolvedBase}`], { cwd: root, ignoreError: true });
|
|
12
12
|
const behindCount = parseInt(behindStr || "0", 10);
|
|
13
13
|
|
|
14
14
|
console.log(`[stale-base-check] Branch HEAD is ${behindCount} commits behind ${resolvedBase} (max allowed: ${maxBehind}).`);
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a CI job that runs the kit's gate, with the toolchain the detected
|
|
6
|
+
* stack actually needs.
|
|
7
|
+
*
|
|
8
|
+
* `init` used to copy this repository's *own* audit workflow into whatever
|
|
9
|
+
* repository it was scaffolding: a nine-way Node matrix that ran `npm install`,
|
|
10
|
+
* `npm test` and a script path that only exists here. Dropped into a Rust or
|
|
11
|
+
* Python repository it was red on the first push, for reasons that had nothing
|
|
12
|
+
* to do with that repository's code. A generated workflow is the only kind that
|
|
13
|
+
* can be correct in a repository the kit has never seen.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const CI_TARGETS = ["github", "gitlab"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Toolchain setup per stack, expressed once and rendered per CI target.
|
|
20
|
+
*
|
|
21
|
+
* Node is installed unconditionally and is not in this table: `agentctl` is a
|
|
22
|
+
* Node CLI, so the runner needs it regardless of what the project is written
|
|
23
|
+
* in. These are the *additional* runtimes.
|
|
24
|
+
*
|
|
25
|
+
* Actions are referenced by major version tag rather than by commit SHA. A
|
|
26
|
+
* generated file is a starting point the repository owns from the moment it is
|
|
27
|
+
* written, and pinning a SHA the owner did not choose ages into a silent
|
|
28
|
+
* security debt they never agreed to — the header tells them to pin.
|
|
29
|
+
*/
|
|
30
|
+
const STACK_TOOLCHAINS = {
|
|
31
|
+
python: { label: "Python 3.x", gh: [{ name: "Set up Python", uses: "actions/setup-python@v5", with: { "python-version": "3.x" } }], gl: "python:3" },
|
|
32
|
+
django: { label: "Python 3.x", gh: [{ name: "Set up Python", uses: "actions/setup-python@v5", with: { "python-version": "3.x" } }], gl: "python:3" },
|
|
33
|
+
go: { label: "Go (stable)", gh: [{ name: "Set up Go", uses: "actions/setup-go@v5", with: { "go-version": "stable" } }], gl: "golang:latest" },
|
|
34
|
+
cargo: { label: "Rust (preinstalled on hosted runners)", gh: [], gl: "rust:latest" },
|
|
35
|
+
maven: { label: "Temurin JDK 21", gh: [{ name: "Set up Java", uses: "actions/setup-java@v4", with: { distribution: "temurin", "java-version": "21" } }], gl: "maven:3-eclipse-temurin-21" },
|
|
36
|
+
gradle: { label: "Temurin JDK 21", gh: [{ name: "Set up Java", uses: "actions/setup-java@v4", with: { distribution: "temurin", "java-version": "21" } }], gl: "gradle:jdk21" },
|
|
37
|
+
dotnet: { label: ".NET 8", gh: [{ name: "Set up .NET", uses: "actions/setup-dotnet@v4", with: { "dotnet-version": "8.0.x" } }], gl: "mcr.microsoft.com/dotnet/sdk:8.0" },
|
|
38
|
+
php: { label: "PHP (preinstalled on hosted runners)", gh: [], gl: "php:8" },
|
|
39
|
+
laravel: { label: "PHP (preinstalled on hosted runners)", gh: [], gl: "php:8" },
|
|
40
|
+
wordpress: { label: "PHP (preinstalled on hosted runners)", gh: [], gl: "php:8" },
|
|
41
|
+
bundler: { label: "Ruby (preinstalled on hosted runners)", gh: [], gl: "ruby:3" },
|
|
42
|
+
mix: { label: "Elixir — add your own setup step", gh: [], gl: "elixir:latest" },
|
|
43
|
+
bun: { label: "Bun", gh: [{ name: "Set up Bun", uses: "oven-sh/setup-bun@v2" }], gl: "oven/bun:latest" },
|
|
44
|
+
deno: { label: "Deno", gh: [{ name: "Set up Deno", uses: "denoland/setup-deno@v2", with: { "deno-version": "v2.x" } }], gl: "denoland/deno:latest" },
|
|
45
|
+
flutter: { label: "Flutter — add your own setup step", gh: [], gl: "ghcr.io/cirruslabs/flutter:stable" },
|
|
46
|
+
dart: { label: "Dart — add your own setup step", gh: [], gl: "dart:stable" },
|
|
47
|
+
swift: { label: "Swift — add your own setup step", gh: [], gl: "swift:latest" },
|
|
48
|
+
cmake: { label: "CMake/CTest (preinstalled on hosted runners)", gh: [], gl: "gcc:latest" },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Renders one GitHub Actions step block at four-space indentation. */
|
|
52
|
+
function renderGhStep(step) {
|
|
53
|
+
const lines = [` - name: ${step.name}`, ` uses: ${step.uses}`];
|
|
54
|
+
if (step.with) {
|
|
55
|
+
lines.push(` with:`);
|
|
56
|
+
for (const [k, v] of Object.entries(step.with)) lines.push(` ${k}: "${v}"`);
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const HEADER = (stack, version) =>
|
|
62
|
+
`# Generated by jules-orchestrator-kit v${version} for a '${stack}' project.
|
|
63
|
+
#
|
|
64
|
+
# This file is yours now — edit it freely. Two things worth doing before you
|
|
65
|
+
# rely on it:
|
|
66
|
+
# 1. Pin each third-party action to a commit SHA (a tag can be moved).
|
|
67
|
+
# 2. Check that the runtime versions below match what the project targets.
|
|
68
|
+
#
|
|
69
|
+
# Regenerate with: agentctl ci init --force
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {object} opts
|
|
74
|
+
* @param {string} opts.target - "github" | "gitlab"
|
|
75
|
+
* @param {{stack: string}} opts.stack - result of detectStack()
|
|
76
|
+
* @param {object} [opts.config] - loaded config, for base_branch
|
|
77
|
+
* @param {string} [opts.version]
|
|
78
|
+
* @returns {{ file: string, content: string, setupSummary: string }}
|
|
79
|
+
*/
|
|
80
|
+
export function buildCiWorkflow(opts = {}) {
|
|
81
|
+
const target = CI_TARGETS.includes(opts.target) ? opts.target : "github";
|
|
82
|
+
const stackId = opts.stack?.stack || "unknown";
|
|
83
|
+
const base = opts.config?.baseBranch || "main";
|
|
84
|
+
const version = opts.version || "0";
|
|
85
|
+
const toolchain = STACK_TOOLCHAINS[stackId] || null;
|
|
86
|
+
const setupSummary = toolchain ? toolchain.label : "Node only";
|
|
87
|
+
|
|
88
|
+
if (target === "gitlab") {
|
|
89
|
+
const image = toolchain?.gl || "node:20";
|
|
90
|
+
const needsNode = image !== "node:20" && !image.startsWith("node");
|
|
91
|
+
const content = `${HEADER(stackId, version)}
|
|
92
|
+
agent-gate:
|
|
93
|
+
image: ${image}
|
|
94
|
+
stage: test
|
|
95
|
+
variables:
|
|
96
|
+
GIT_DEPTH: "0"
|
|
97
|
+
rules:
|
|
98
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
99
|
+
- if: $CI_COMMIT_BRANCH == "${base}"
|
|
100
|
+
script:${
|
|
101
|
+
needsNode
|
|
102
|
+
? `
|
|
103
|
+
# agentctl is a Node CLI, so the image needs Node even when the project is not.
|
|
104
|
+
- command -v node >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq nodejs npm)`
|
|
105
|
+
: ""
|
|
106
|
+
}
|
|
107
|
+
- npx --yes jules-orchestrator-kit check --mode committed --base "origin/${base}"
|
|
108
|
+
`;
|
|
109
|
+
return { file: ".gitlab-ci.agent-gate.yml", content, setupSummary };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const extraSteps = (toolchain?.gh || []).map(renderGhStep).join("\n");
|
|
113
|
+
const content = `${HEADER(stackId, version)}
|
|
114
|
+
name: Agent Safety Gate
|
|
115
|
+
|
|
116
|
+
on:
|
|
117
|
+
pull_request:
|
|
118
|
+
branches: [ ${base} ]
|
|
119
|
+
push:
|
|
120
|
+
branches: [ ${base} ]
|
|
121
|
+
|
|
122
|
+
permissions:
|
|
123
|
+
contents: read
|
|
124
|
+
|
|
125
|
+
jobs:
|
|
126
|
+
gate:
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
timeout-minutes: 20
|
|
129
|
+
steps:
|
|
130
|
+
- name: Checkout repository
|
|
131
|
+
uses: actions/checkout@v4
|
|
132
|
+
with:
|
|
133
|
+
# The gate compares the branch against its merge base, which needs
|
|
134
|
+
# full history rather than the default single-commit fetch.
|
|
135
|
+
fetch-depth: 0
|
|
136
|
+
persist-credentials: false
|
|
137
|
+
|
|
138
|
+
# agentctl is a Node CLI: the runner needs Node whatever the project is
|
|
139
|
+
# written in.
|
|
140
|
+
- name: Set up Node.js
|
|
141
|
+
uses: actions/setup-node@v4
|
|
142
|
+
with:
|
|
143
|
+
node-version: "20"
|
|
144
|
+
${extraSteps ? extraSteps + "\n" : ""}
|
|
145
|
+
- name: Run agent safety gate
|
|
146
|
+
run: npx --yes jules-orchestrator-kit check --mode committed --base "origin/${base}"
|
|
147
|
+
`;
|
|
148
|
+
return { file: ".github/workflows/agent-gate.yml", content, setupSummary };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Write the generated workflow, refusing to clobber an existing file unless
|
|
153
|
+
* asked.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} root
|
|
156
|
+
* @param {object} [opts] - see {@link buildCiWorkflow}, plus `force` and `dryRun`
|
|
157
|
+
* @returns {{ ok: boolean, file?: string, written?: boolean, stack?: string, setupSummary?: string, content?: string, error?: string }}
|
|
158
|
+
*/
|
|
159
|
+
export function writeCiWorkflow(root, opts = {}) {
|
|
160
|
+
if (opts.target && !CI_TARGETS.includes(opts.target)) {
|
|
161
|
+
return { ok: false, error: `Unknown CI target '${opts.target}'. Choose one of: ${CI_TARGETS.join(", ")}` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const built = buildCiWorkflow(opts);
|
|
165
|
+
const stackId = opts.stack?.stack || "unknown";
|
|
166
|
+
const abs = join(root, built.file);
|
|
167
|
+
|
|
168
|
+
if (opts.dryRun) {
|
|
169
|
+
return { ok: true, file: built.file, written: false, stack: stackId, setupSummary: built.setupSummary, content: built.content };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (existsSync(abs) && !opts.force) {
|
|
173
|
+
let identical = false;
|
|
174
|
+
try {
|
|
175
|
+
identical = readFileSync(abs, "utf-8") === built.content;
|
|
176
|
+
} catch (_) {
|
|
177
|
+
// Unreadable target: treat as different so the caller is told to --force
|
|
178
|
+
// rather than being told nothing changed.
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
file: built.file,
|
|
183
|
+
written: false,
|
|
184
|
+
identical,
|
|
185
|
+
stack: stackId,
|
|
186
|
+
setupSummary: built.setupSummary,
|
|
187
|
+
content: built.content,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
193
|
+
writeFileSync(abs, built.content, "utf-8");
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return { ok: false, error: `Could not write ${built.file}: ${err.message}` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { ok: true, file: built.file, written: true, stack: stackId, setupSummary: built.setupSummary, content: built.content };
|
|
199
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -293,6 +293,10 @@ export function detectPackageManager(root = process.cwd(), pkg = {}) {
|
|
|
293
293
|
return "npm";
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
import { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
|
|
297
|
+
|
|
298
|
+
export { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS };
|
|
299
|
+
|
|
296
300
|
import {
|
|
297
301
|
detectPolyglotStack,
|
|
298
302
|
resolveWorkspaceBoundary,
|
|
@@ -321,6 +325,10 @@ export function detectStack(projectRoot = process.cwd()) {
|
|
|
321
325
|
export function resolveVerify(root = process.cwd(), userVerify = {}) {
|
|
322
326
|
const s = detectStack(root);
|
|
323
327
|
return {
|
|
328
|
+
// Carried through so `verify.profile` can gate stack-specific gates (V8
|
|
329
|
+
// diff coverage only exists where the test command runs on Node) without
|
|
330
|
+
// every caller re-running stack detection.
|
|
331
|
+
stack: s.stack || "unknown",
|
|
324
332
|
setup: userVerify.setup ?? s.setupCmd ?? "",
|
|
325
333
|
lint: userVerify.lint ?? s.fmtCmd ?? "",
|
|
326
334
|
test: userVerify.test ?? s.testCmd ?? "",
|
|
@@ -501,22 +509,42 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
501
509
|
if (normalizedLimits[k] === undefined) delete normalizedLimits[k];
|
|
502
510
|
}
|
|
503
511
|
|
|
512
|
+
// The verification commands, resolved once, so a profile can be expanded
|
|
513
|
+
// against the same values the gate will actually run.
|
|
514
|
+
const mergedVerify = {
|
|
515
|
+
setup: rawSetup ?? autoVerify.setup ?? "",
|
|
516
|
+
lint: rawLint ?? autoVerify.lint ?? "",
|
|
517
|
+
test: rawTest ?? autoVerify.test,
|
|
518
|
+
unit: rawUnit ?? rawTest ?? autoVerify.unit ?? autoVerify.test,
|
|
519
|
+
fuzz: rawFuzz ?? autoVerify.fuzz ?? "",
|
|
520
|
+
invariant: rawInvariant ?? autoVerify.invariant ?? "",
|
|
521
|
+
e2e: rawE2e ?? autoVerify.e2e ?? "",
|
|
522
|
+
teardown: rawTeardown ?? autoVerify.teardown ?? "",
|
|
523
|
+
build: rawBuild ?? autoVerify.build,
|
|
524
|
+
policy: parsed.verify?.policy ?? autoVerify.policy,
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// A hand-written `verify.stages:` is the operator being explicit and always
|
|
528
|
+
// wins; `verify.profile:` is the shorthand, expanded here rather than frozen
|
|
529
|
+
// into the scaffolded YAML so it stays correct when the stack changes.
|
|
530
|
+
const rawProfile = parsed.verify?.profile ?? parsed.profile ?? null;
|
|
531
|
+
const explicitStages = parsed.verify?.stages ?? autoVerify.stages ?? null;
|
|
532
|
+
const profilePlan =
|
|
533
|
+
!explicitStages && rawProfile
|
|
534
|
+
? buildProfileStages(rawProfile, { stack: autoVerify.stack, verify: mergedVerify })
|
|
535
|
+
: null;
|
|
536
|
+
|
|
504
537
|
const config = {
|
|
505
538
|
version: parsed.version || DEFAULTS.version,
|
|
506
539
|
provider: parsed.provider || DEFAULTS.provider,
|
|
507
540
|
tier: activeTier,
|
|
508
541
|
verify: {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
e2e: rawE2e ?? autoVerify.e2e ?? "",
|
|
516
|
-
teardown: rawTeardown ?? autoVerify.teardown ?? "",
|
|
517
|
-
build: rawBuild ?? autoVerify.build,
|
|
518
|
-
stages: parsed.verify?.stages ?? autoVerify.stages ?? null,
|
|
519
|
-
policy: parsed.verify?.policy ?? autoVerify.policy,
|
|
542
|
+
...mergedVerify,
|
|
543
|
+
stages: explicitStages ?? profilePlan?.stages ?? null,
|
|
544
|
+
profile: profilePlan?.profile ?? (rawProfile ? String(rawProfile).toLowerCase() : null),
|
|
545
|
+
// Named so `doctor` can tell an operator that the gate they enabled is
|
|
546
|
+
// running one fewer check than they think, and why.
|
|
547
|
+
profileSkipped: profilePlan?.skipped ?? [],
|
|
520
548
|
timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 60000,
|
|
521
549
|
},
|
|
522
550
|
evidence: {
|
package/src/engine.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { hydratePrompt, harvestFailure } from "./memory.mjs";
|
|
|
19
19
|
import { resolveRolePrompt } from "./role-resolver.mjs";
|
|
20
20
|
|
|
21
21
|
import { runAssertion } from "./assertions.mjs";
|
|
22
|
+
import { buildDefaultStages } from "./profiles.mjs";
|
|
22
23
|
import {
|
|
23
24
|
computeDirectoryHash,
|
|
24
25
|
generateEvidenceManifest,
|
|
@@ -283,27 +284,7 @@ export async function gate(opts = {}) {
|
|
|
283
284
|
if (Array.isArray(trustedVerify.stages) && trustedVerify.stages.length > 0) {
|
|
284
285
|
stagesToRun.push(...trustedVerify.stages);
|
|
285
286
|
} else {
|
|
286
|
-
|
|
287
|
-
stagesToRun.push({ id: "setup", kind: "setup", cmd: trustedVerify.setup, required: true, networkAccess: "allow" });
|
|
288
|
-
}
|
|
289
|
-
if (trustedVerify.lint) {
|
|
290
|
-
stagesToRun.push({ id: "lint", kind: "lint", cmd: trustedVerify.lint, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
291
|
-
}
|
|
292
|
-
if (trustedVerify.test || trustedVerify.unit) {
|
|
293
|
-
stagesToRun.push({ id: "unit", kind: "test", cmd: trustedVerify.test || trustedVerify.unit, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
294
|
-
}
|
|
295
|
-
if (trustedVerify.fuzz) {
|
|
296
|
-
stagesToRun.push({ id: "fuzz", kind: "fuzz", cmd: trustedVerify.fuzz, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
297
|
-
}
|
|
298
|
-
if (trustedVerify.invariant) {
|
|
299
|
-
stagesToRun.push({ id: "invariant", kind: "invariant", cmd: trustedVerify.invariant, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
300
|
-
}
|
|
301
|
-
if (trustedVerify.e2e) {
|
|
302
|
-
stagesToRun.push({ id: "e2e", kind: "e2e", cmd: trustedVerify.e2e, required: true, networkAccess: "allow" });
|
|
303
|
-
}
|
|
304
|
-
if (trustedVerify.build) {
|
|
305
|
-
stagesToRun.push({ id: "build", kind: "build", cmd: trustedVerify.build, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
|
|
306
|
-
}
|
|
287
|
+
stagesToRun.push(...buildDefaultStages(trustedVerify));
|
|
307
288
|
}
|
|
308
289
|
|
|
309
290
|
try {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor-neutral spellings for every environment variable the kit reads.
|
|
3
|
+
*
|
|
4
|
+
* Every knob in this codebase is spelled `JULES_*`, which is accurate for the
|
|
5
|
+
* provider it was written against and wrong for every other one: a repository
|
|
6
|
+
* driving `claude-code` or `codex` still had to export `JULES_SWARM_CONCURRENCY`
|
|
7
|
+
* to set worker slots, and `JULES_API_KEY` to set a key that never goes to
|
|
8
|
+
* Jules. Renaming the variables outright would break every existing checkout
|
|
9
|
+
* and CI job, so both spellings are supported and this table is the only place
|
|
10
|
+
* that has to know they are the same knob.
|
|
11
|
+
*
|
|
12
|
+
* Direction is deliberate: `AGENT_*` is the name to document, `JULES_*` is the
|
|
13
|
+
* name the code reads, and the alias fills the second from the first. A
|
|
14
|
+
* `JULES_*` value already in the environment always wins, so adding an
|
|
15
|
+
* `AGENT_*` export can never change the behaviour of a working setup.
|
|
16
|
+
*
|
|
17
|
+
* @type {Record<string, string>}
|
|
18
|
+
*/
|
|
19
|
+
export const ENV_ALIASES = {
|
|
20
|
+
AGENT_API_KEY: "JULES_API_KEY",
|
|
21
|
+
AGENT_API_KEYS: "JULES_API_KEYS",
|
|
22
|
+
AGENT_API_KEY_SECONDARY: "JULES_API_KEY_SECONDARY",
|
|
23
|
+
AGENT_API_URL: "JULES_API_URL",
|
|
24
|
+
AGENT_REPO: "JULES_REPO",
|
|
25
|
+
AGENT_PROJECT_ID: "JULES_PROJECT_ID",
|
|
26
|
+
AGENT_PROJECT_ROOT: "JULES_PROJECT_ROOT",
|
|
27
|
+
AGENT_TIER: "JULES_TIER",
|
|
28
|
+
AGENT_DAILY_BUDGET: "JULES_DAILY_BUDGET",
|
|
29
|
+
AGENT_MAX_DIFF_KB: "JULES_MAX_DIFF_KB",
|
|
30
|
+
AGENT_DRY_RUN: "JULES_DRY_RUN",
|
|
31
|
+
AGENT_REPOLESS: "JULES_REPOLESS",
|
|
32
|
+
AGENT_SWARM_CONCURRENCY: "JULES_SWARM_CONCURRENCY",
|
|
33
|
+
AGENT_SWARM_STAGGER_MS: "JULES_SWARM_STAGGER_MS",
|
|
34
|
+
AGENT_PACE_MS: "JULES_PACE_MS",
|
|
35
|
+
AGENT_SLOT_INDEX: "JULES_SLOT_INDEX",
|
|
36
|
+
AGENT_SLOT_TOTAL: "JULES_SLOT_TOTAL",
|
|
37
|
+
AGENT_USE_WORKTREES: "JULES_USE_WORKTREES",
|
|
38
|
+
AGENT_ALLOW_COMMAND_FILE_CHANGES: "JULES_ALLOW_COMMAND_FILE_CHANGES",
|
|
39
|
+
AGENT_ALLOW_AGENT_RULE_CHANGES: "JULES_ALLOW_AGENT_RULE_CHANGES",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fill the legacy `JULES_*` variables from their `AGENT_*` equivalents.
|
|
44
|
+
*
|
|
45
|
+
* Mutates the passed environment object in place — that is the point: every
|
|
46
|
+
* downstream module reads `process.env` directly, and rewriting hundreds of
|
|
47
|
+
* call sites to consult an alias table would be a far larger change with far
|
|
48
|
+
* more places to get it wrong. Called once at process entry.
|
|
49
|
+
*
|
|
50
|
+
* An empty-string alias is treated as unset, matching how the rest of the kit
|
|
51
|
+
* reads keys (`(process.env.X || "").trim()`), so `AGENT_API_KEY=` in a .env
|
|
52
|
+
* file does not shadow a real `JULES_API_KEY`.
|
|
53
|
+
*
|
|
54
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
55
|
+
* @returns {string[]} canonical names that were populated from an alias
|
|
56
|
+
*/
|
|
57
|
+
export function applyEnvAliases(env = process.env) {
|
|
58
|
+
const applied = [];
|
|
59
|
+
for (const [alias, canonical] of Object.entries(ENV_ALIASES)) {
|
|
60
|
+
const aliasValue = env[alias];
|
|
61
|
+
if (aliasValue === undefined || String(aliasValue).trim() === "") continue;
|
|
62
|
+
const existing = env[canonical];
|
|
63
|
+
if (existing !== undefined && String(existing).trim() !== "") continue;
|
|
64
|
+
env[canonical] = aliasValue;
|
|
65
|
+
applied.push(canonical);
|
|
66
|
+
}
|
|
67
|
+
return applied;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Both spellings of one knob, for help text and diagnostics that should name
|
|
72
|
+
* the vendor-neutral variable first.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} canonical - a `JULES_*` name
|
|
75
|
+
* @returns {{ canonical: string, alias: string|null }}
|
|
76
|
+
*/
|
|
77
|
+
export function describeEnvVar(canonical) {
|
|
78
|
+
const alias = Object.entries(ENV_ALIASES).find(([, c]) => c === canonical)?.[0] || null;
|
|
79
|
+
return { canonical, alias };
|
|
80
|
+
}
|
package/src/mcp.mjs
CHANGED
|
@@ -143,7 +143,7 @@ export const MCP_TOOLS = [
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
name: "get_jules_status",
|
|
146
|
-
description: "Retrieve orchestrator status
|
|
146
|
+
description: "Retrieve orchestrator status: daily task budget, active locks, stack diagnostics, the configured provider and whether it is reachable, and the verification stages the gate will run.",
|
|
147
147
|
inputSchema: {
|
|
148
148
|
type: "object",
|
|
149
149
|
properties: {},
|
|
@@ -439,6 +439,10 @@ export async function handleMcpRequest(request, opts = {}) {
|
|
|
439
439
|
const stackInfo = detectStack(root);
|
|
440
440
|
const budget = budgetStatus(config, root);
|
|
441
441
|
const locks = lockStatus(root);
|
|
442
|
+
const { probeProvider } = await import("./provider-readiness.mjs");
|
|
443
|
+
const { buildDefaultStages } = await import("./profiles.mjs");
|
|
444
|
+
const providerProbe = probeProvider(config.provider || "jules");
|
|
445
|
+
const profileStages = config.verify.stages || buildDefaultStages(config.verify);
|
|
442
446
|
const status = {
|
|
443
447
|
version: MCP_SERVER_INFO.version,
|
|
444
448
|
root,
|
|
@@ -455,6 +459,20 @@ export async function handleMcpRequest(request, opts = {}) {
|
|
|
455
459
|
},
|
|
456
460
|
activeLocksCount: locks.length,
|
|
457
461
|
locks,
|
|
462
|
+
// An IDE agent orienting itself needs to know which agent this repo
|
|
463
|
+
// dispatches to and how hard its work will be verified before it
|
|
464
|
+
// starts, not after the gate rejects it.
|
|
465
|
+
provider: {
|
|
466
|
+
name: providerProbe.name,
|
|
467
|
+
kind: providerProbe.kind,
|
|
468
|
+
ready: providerProbe.ready,
|
|
469
|
+
reason: providerProbe.reason,
|
|
470
|
+
},
|
|
471
|
+
verification: {
|
|
472
|
+
profile: config.verify.profile,
|
|
473
|
+
stages: profileStages.map((st) => st.id),
|
|
474
|
+
skipped: (config.verify.profileSkipped || []).map((sk) => sk.id),
|
|
475
|
+
},
|
|
458
476
|
};
|
|
459
477
|
return {
|
|
460
478
|
jsonrpc: "2.0",
|
|
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
5
|
import { loadConfig } from "../config.mjs";
|
|
6
|
+
import { probeProvider, detectAvailableProviders } from "../provider-readiness.mjs";
|
|
6
7
|
import { resolveConcurrency } from "../budget.mjs";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -429,40 +430,60 @@ export async function runDoctorChecks(options = {}) {
|
|
|
429
430
|
});
|
|
430
431
|
}
|
|
431
432
|
|
|
432
|
-
// 7.
|
|
433
|
-
|
|
434
|
-
|
|
433
|
+
// 7. Provider readiness — for the provider this repository actually selected.
|
|
434
|
+
//
|
|
435
|
+
// This used to ask one question ("is JULES_API_KEY set?") and report it as a
|
|
436
|
+
// high-severity warning regardless of the configured provider, so a
|
|
437
|
+
// repository driving the Claude Code or Codex CLI was permanently told it was
|
|
438
|
+
// misconfigured over a key it neither needs nor should have.
|
|
439
|
+
let configuredProvider = "jules";
|
|
440
|
+
try {
|
|
441
|
+
configuredProvider = loadConfig(root).provider || "jules";
|
|
442
|
+
} catch (_) {
|
|
443
|
+
// Fall back to the default; config.present already reports a broken config.
|
|
444
|
+
}
|
|
445
|
+
const providerProbe = probeProvider(configuredProvider);
|
|
446
|
+
addResult({
|
|
447
|
+
id: "provider.key",
|
|
448
|
+
category: "Provider",
|
|
449
|
+
title: `Provider Readiness (${providerProbe.name})`,
|
|
450
|
+
status: providerProbe.ready ? "pass" : "warn",
|
|
451
|
+
severity: providerProbe.ready ? "info" : "high",
|
|
452
|
+
// Naming the variable, not the value: an operator who wonders which key a
|
|
453
|
+
// dispatch will use should not have to echo a secret to find out.
|
|
454
|
+
summary: `${providerProbe.label} — ${providerProbe.reason}`,
|
|
455
|
+
remediation: providerProbe.ready
|
|
456
|
+
? []
|
|
457
|
+
: [
|
|
458
|
+
{
|
|
459
|
+
summary: providerProbe.remedy,
|
|
460
|
+
risk: "low",
|
|
461
|
+
automatic: false,
|
|
462
|
+
requiresProbe: false,
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
evidence: [
|
|
466
|
+
{ label: "provider", value: providerProbe.name, sensitive: false },
|
|
467
|
+
{ label: "providerKind", value: providerProbe.kind, sensitive: false },
|
|
468
|
+
{ label: "ready", value: providerProbe.ready, sensitive: false },
|
|
469
|
+
{ label: "keySource", value: providerProbe.keySource || "", sensitive: false },
|
|
470
|
+
{ label: "binaryFound", value: Boolean(providerProbe.binPath), sensitive: false },
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// 7a. What else this machine could dispatch to. Purely informational: an
|
|
475
|
+
// operator blocked on one provider should not have to discover by reading
|
|
476
|
+
// source that three others are installed and ready.
|
|
477
|
+
const alternatives = detectAvailableProviders().filter((p) => p.ready && p.name !== providerProbe.name);
|
|
478
|
+
if (alternatives.length > 0) {
|
|
435
479
|
addResult({
|
|
436
|
-
id: "provider.
|
|
437
|
-
category: "
|
|
438
|
-
title: "
|
|
480
|
+
id: "provider.alternatives",
|
|
481
|
+
category: "Provider",
|
|
482
|
+
title: "Other Providers Available",
|
|
439
483
|
status: "pass",
|
|
440
484
|
severity: "info",
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
summary: `API key supplied via ${keyVar} (environment only — never written to config or sent anywhere but the provider)`,
|
|
444
|
-
evidence: [
|
|
445
|
-
{ label: "keyConfigured", value: true, sensitive: false },
|
|
446
|
-
{ label: "keySource", value: keyVar, sensitive: false },
|
|
447
|
-
],
|
|
448
|
-
});
|
|
449
|
-
} else {
|
|
450
|
-
addResult({
|
|
451
|
-
id: "provider.key",
|
|
452
|
-
category: "Jules",
|
|
453
|
-
title: "Jules Provider API Key",
|
|
454
|
-
status: "warn",
|
|
455
|
-
severity: "high",
|
|
456
|
-
summary: "Neither JULES_API_KEY nor GEMINI_API_KEY environment variable is set",
|
|
457
|
-
remediation: [
|
|
458
|
-
{
|
|
459
|
-
summary: "Export JULES_API_KEY in your shell profile, or place it in a git-ignored .env",
|
|
460
|
-
risk: "low",
|
|
461
|
-
automatic: false,
|
|
462
|
-
requiresProbe: false,
|
|
463
|
-
},
|
|
464
|
-
],
|
|
465
|
-
evidence: [{ label: "keyConfigured", value: false, sensitive: false }],
|
|
485
|
+
summary: `Also ready on this machine: ${alternatives.map((p) => p.name).join(", ")} — switch with provider: in .agent/config.yml`,
|
|
486
|
+
evidence: alternatives.map((p) => ({ label: p.name, value: p.reason, sensitive: false })),
|
|
466
487
|
});
|
|
467
488
|
}
|
|
468
489
|
|
|
@@ -477,7 +498,7 @@ export async function runDoctorChecks(options = {}) {
|
|
|
477
498
|
|
|
478
499
|
addResult({
|
|
479
500
|
id: "provider.key.dotenv",
|
|
480
|
-
category: "
|
|
501
|
+
category: "Provider",
|
|
481
502
|
title: "Local .env secrecy",
|
|
482
503
|
status: tracked ? "fail" : "pass",
|
|
483
504
|
severity: tracked ? "critical" : "info",
|