jules-orchestrator-kit 0.72.2 → 0.73.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/{Overseer.md → Auditor.md} +3 -3
- package/.agent/prompts/{Alchemist.md → Database.md} +1 -1
- package/.agent/prompts/Debugger.md +25 -0
- package/.agent/prompts/{Scribe.md → Docs.md} +8 -5
- package/.agent/prompts/{Spectator.md → E2E.md} +9 -6
- package/.agent/prompts/{Janitor.md → Hygiene.md} +2 -2
- package/.agent/prompts/{Bolt.md → Performance.md} +1 -1
- package/.agent/prompts/Resilience.md +20 -0
- package/.agent/prompts/Security.md +21 -0
- package/.agent/prompts/Testing.md +30 -0
- package/.agent/prompts/Types.md +19 -0
- package/.agent/rules/jules-protocol.md +4 -3
- package/AGENTS.md +78 -96
- package/CHANGELOG.md +193 -0
- package/JULES_RULES_TEMPLATE.md +83 -96
- package/LICENSE +1 -1
- package/README.md +77 -439
- package/ROADMAP_V1.md +22 -132
- package/bin/agentctl.mjs +443 -144
- package/bin/init.js +6 -3
- package/index.mjs +9 -6
- package/package.json +1 -1
- package/scripts/asset-integrity-check.mjs +1 -1
- package/scripts/doc-sync-check.mjs +47 -0
- package/scripts/generate-command-reference.mjs +39 -0
- package/scripts/jules-dispatch.mjs +12 -113
- package/scripts/jules-merge-swarm.mjs +8 -196
- package/scripts/jules-patch.mjs +7 -8
- package/scripts/jules-queue-runner.mjs +6 -8
- package/scripts/jules-scan-todos.mjs +10 -38
- package/scripts/jules-self-audit.mjs +8 -139
- package/scripts/jules-status.mjs +32 -38
- package/scripts/jules-webhook-receiver.mjs +1 -1
- package/src/assertions.mjs +5 -50
- package/src/bidi-guard.mjs +36 -0
- package/src/budget.mjs +3 -14
- package/src/config.mjs +2 -6
- package/src/dashboard.mjs +7 -9
- package/src/dispatch.mjs +212 -0
- package/src/engine.mjs +40 -16
- package/src/evidence.mjs +10 -41
- package/src/execution-envelope.mjs +13 -1
- package/src/flaky-ledger.mjs +1 -1
- package/src/fs-atomic.mjs +72 -0
- package/src/git.mjs +298 -27
- package/src/mcp.mjs +296 -7
- package/src/memory.mjs +0 -0
- package/src/merge-swarm.mjs +202 -0
- package/src/ops/cli-intent.mjs +1 -0
- package/src/ops/command-registry.mjs +796 -70
- package/src/ops/doctor-registry.mjs +134 -47
- package/src/ops/handover.mjs +3 -27
- package/src/ops/pr-harvest.mjs +1 -1
- package/src/prompt-guard.mjs +33 -3
- package/src/provider.mjs +51 -7
- package/src/remediation.mjs +2 -2
- package/src/role-resolver.mjs +113 -3
- package/src/router.mjs +19 -11
- package/src/runtime-env.mjs +67 -0
- package/src/scaffold.mjs +3 -1
- package/src/scope-guard.mjs +249 -0
- package/src/secret-scanner.mjs +530 -0
- package/src/security.mjs +81 -2972
- package/src/self-audit.mjs +140 -0
- package/src/session-ops.mjs +29 -0
- package/src/stability.mjs +8 -1
- package/src/stack-detector.mjs +5 -2
- package/src/state.mjs +45 -0
- package/src/swarm.mjs +76 -0
- package/src/task-optimizer.mjs +34 -7
- package/src/telemetry.mjs +23 -0
- package/src/test-tamper-guard.mjs +2173 -0
- package/src/todo-scanner.mjs +129 -0
- package/src/web-templates.mjs +3 -3
- package/src/webhook.mjs +10 -3
- package/src/wizard-init.mjs +12 -20
- package/src/wizard-oracle.mjs +4 -3
- package/src/wizard-task.mjs +37 -9
- package/.agent/prompts/Sentinel.md +0 -18
- package/scripts/utils.mjs +0 -241
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
-
import { loadConfig } from "../config.mjs";
|
|
5
|
+
import { loadConfig, detectStack } from "../config.mjs";
|
|
6
6
|
import { probeProvider, detectAvailableProviders, probeProviderLiveness } from "../provider-readiness.mjs";
|
|
7
7
|
import { resolveConcurrency } from "../budget.mjs";
|
|
8
8
|
|
|
@@ -121,7 +121,11 @@ export async function runDoctorChecks(options = {}) {
|
|
|
121
121
|
// runtime.git
|
|
122
122
|
let gitPassed = false;
|
|
123
123
|
try {
|
|
124
|
-
const gitOut = execFileSync("git", ["--version"], {
|
|
124
|
+
const gitOut = execFileSync("git", ["--version"], {
|
|
125
|
+
encoding: "utf-8",
|
|
126
|
+
timeout: 5000,
|
|
127
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
128
|
+
});
|
|
125
129
|
gitPassed = true;
|
|
126
130
|
addResult({
|
|
127
131
|
id: "runtime.git",
|
|
@@ -167,18 +171,46 @@ export async function runDoctorChecks(options = {}) {
|
|
|
167
171
|
let isRepo = false;
|
|
168
172
|
let headSha = "";
|
|
169
173
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
const insideWorkTree = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
175
|
+
cwd: root,
|
|
176
|
+
encoding: "utf-8",
|
|
177
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
178
|
+
}).trim();
|
|
179
|
+
if (insideWorkTree === "true") {
|
|
180
|
+
isRepo = true;
|
|
181
|
+
try {
|
|
182
|
+
headSha = execFileSync("git", ["rev-parse", "--verify", "--quiet", "HEAD"], {
|
|
183
|
+
cwd: root,
|
|
184
|
+
encoding: "utf-8",
|
|
185
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
186
|
+
}).trim();
|
|
187
|
+
} catch (_) {}
|
|
188
|
+
|
|
189
|
+
if (headSha) {
|
|
190
|
+
addResult({
|
|
191
|
+
id: "repo.root",
|
|
192
|
+
category: "Repository",
|
|
193
|
+
title: "Repository Root Verification",
|
|
194
|
+
status: "pass",
|
|
195
|
+
severity: "info",
|
|
196
|
+
summary: `Valid repository at ${root} (HEAD: ${headSha.slice(0, 8)})`,
|
|
197
|
+
evidence: [{ label: "headSha", value: headSha, sensitive: false }],
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
addResult({
|
|
201
|
+
id: "repo.root",
|
|
202
|
+
category: "Repository",
|
|
203
|
+
title: "Repository Root Verification",
|
|
204
|
+
status: "pass",
|
|
205
|
+
severity: "info",
|
|
206
|
+
summary: `Valid repository at ${root} (unborn HEAD, 0 commits)`,
|
|
207
|
+
evidence: [{ label: "headSha", value: "", sensitive: false }],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch (_) {}
|
|
212
|
+
|
|
213
|
+
if (!isRepo) {
|
|
182
214
|
addResult({
|
|
183
215
|
id: "repo.root",
|
|
184
216
|
category: "Repository",
|
|
@@ -191,7 +223,11 @@ export async function runDoctorChecks(options = {}) {
|
|
|
191
223
|
|
|
192
224
|
if (isRepo) {
|
|
193
225
|
try {
|
|
194
|
-
const statusOut = execFileSync("git", ["status", "--porcelain"], {
|
|
226
|
+
const statusOut = execFileSync("git", ["status", "--porcelain"], {
|
|
227
|
+
cwd: root,
|
|
228
|
+
encoding: "utf-8",
|
|
229
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
230
|
+
}).trim();
|
|
195
231
|
const modifiedCount = statusOut ? statusOut.split("\n").length : 0;
|
|
196
232
|
if (modifiedCount === 0) {
|
|
197
233
|
addResult({
|
|
@@ -300,49 +336,96 @@ export async function runDoctorChecks(options = {}) {
|
|
|
300
336
|
}
|
|
301
337
|
|
|
302
338
|
// 4. Verification Oracle Checks
|
|
339
|
+
// Judge the command the gate will actually run — `verify.test` from the
|
|
340
|
+
// config, falling back to the npm `test` script — rather than package.json
|
|
341
|
+
// alone. package.json-only logic told a fresh Python/Rust/Go or zero-test
|
|
342
|
+
// repo "missing test script" (a warning, exit 0) while config carried the
|
|
343
|
+
// real command, or nothing at all; and a green `doctor` right before an
|
|
344
|
+
// `agentctl gate`/`agentctl task create` that both hard-fail with "No
|
|
345
|
+
// Verification Oracle" sent newcomers into a contradiction. When verification
|
|
346
|
+
// is required and there is no oracle, that is a genuine health failure and is
|
|
347
|
+
// reported red so `doctor`'s exit code matches what the gate will do.
|
|
303
348
|
const pkgPath = join(root, "package.json");
|
|
349
|
+
let pkgTestScript = "";
|
|
350
|
+
let pkgValid = false;
|
|
304
351
|
if (existsSync(pkgPath)) {
|
|
305
352
|
try {
|
|
306
353
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
addResult({
|
|
310
|
-
id: "oracle.test",
|
|
311
|
-
category: "Verification",
|
|
312
|
-
title: "Test Oracle Configuration",
|
|
313
|
-
status: "pass",
|
|
314
|
-
severity: "info",
|
|
315
|
-
summary: `Test oracle script found: "npm test" -> "${testScript}"`,
|
|
316
|
-
evidence: [{ label: "testScript", value: testScript, sensitive: false }],
|
|
317
|
-
});
|
|
318
|
-
} else {
|
|
319
|
-
addResult({
|
|
320
|
-
id: "oracle.test",
|
|
321
|
-
category: "Verification",
|
|
322
|
-
title: "Test Oracle Configuration",
|
|
323
|
-
status: "warn",
|
|
324
|
-
severity: "medium",
|
|
325
|
-
summary: "package.json missing test script entry",
|
|
326
|
-
});
|
|
327
|
-
}
|
|
354
|
+
pkgValid = true;
|
|
355
|
+
pkgTestScript = (pkg.scripts && pkg.scripts.test) || "";
|
|
328
356
|
} catch {
|
|
329
|
-
|
|
330
|
-
id: "oracle.test",
|
|
331
|
-
category: "Verification",
|
|
332
|
-
title: "Test Oracle Configuration",
|
|
333
|
-
status: "warn",
|
|
334
|
-
severity: "medium",
|
|
335
|
-
summary: "Malformed package.json file",
|
|
336
|
-
});
|
|
357
|
+
pkgValid = false;
|
|
337
358
|
}
|
|
338
|
-
}
|
|
359
|
+
}
|
|
360
|
+
let cfg = null;
|
|
361
|
+
try {
|
|
362
|
+
cfg = loadConfig(root);
|
|
363
|
+
} catch (_) {
|
|
364
|
+
// A config the loader rejects is already reported by config.present; fall
|
|
365
|
+
// through with null and treat verification as required (the default).
|
|
366
|
+
}
|
|
367
|
+
const configTest = (cfg?.verify?.test || "").trim();
|
|
368
|
+
// verify.required defaults to true; an operator who sets it false is saying
|
|
369
|
+
// "scope/secret gating only, on purpose" — which is a warning, not a failure.
|
|
370
|
+
const verificationRequired = cfg?.verify?.required !== false;
|
|
371
|
+
// The gate auto-detects a per-stack command even before init writes config
|
|
372
|
+
// (e.g. `python3 -m pytest` for a .py repo), so detection counts as an oracle
|
|
373
|
+
// too — otherwise a detected but not-yet-onboarded stack would report red.
|
|
374
|
+
const detectedTest = detectStack(root)?.testCmd || "";
|
|
375
|
+
const effectiveTest = configTest || pkgTestScript || detectedTest;
|
|
376
|
+
|
|
377
|
+
if (effectiveTest) {
|
|
378
|
+
const source = configTest
|
|
379
|
+
? "verify.test in config"
|
|
380
|
+
: pkgTestScript
|
|
381
|
+
? "package.json test script"
|
|
382
|
+
: "auto-detected from repository stack";
|
|
339
383
|
addResult({
|
|
340
384
|
id: "oracle.test",
|
|
341
385
|
category: "Verification",
|
|
342
386
|
title: "Test Oracle Configuration",
|
|
343
387
|
status: "pass",
|
|
344
388
|
severity: "info",
|
|
345
|
-
summary:
|
|
389
|
+
summary: `Verification oracle: "${effectiveTest}" (${source})`,
|
|
390
|
+
evidence: [{ label: "testCommand", value: effectiveTest, sensitive: false }],
|
|
391
|
+
});
|
|
392
|
+
} else if (existsSync(pkgPath) && !pkgValid) {
|
|
393
|
+
addResult({
|
|
394
|
+
id: "oracle.test",
|
|
395
|
+
category: "Verification",
|
|
396
|
+
title: "Test Oracle Configuration",
|
|
397
|
+
status: "warn",
|
|
398
|
+
severity: "medium",
|
|
399
|
+
summary: "Malformed package.json file",
|
|
400
|
+
});
|
|
401
|
+
} else if (!verificationRequired) {
|
|
402
|
+
addResult({
|
|
403
|
+
id: "oracle.test",
|
|
404
|
+
category: "Verification",
|
|
405
|
+
title: "Test Oracle Configuration",
|
|
406
|
+
status: "warn",
|
|
407
|
+
severity: "medium",
|
|
408
|
+
summary: "No verification oracle — verify.required: false (scope/secret gating only)",
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
addResult({
|
|
412
|
+
id: "oracle.test",
|
|
413
|
+
category: "Verification",
|
|
414
|
+
title: "Test Oracle Configuration",
|
|
415
|
+
status: "fail",
|
|
416
|
+
severity: "high",
|
|
417
|
+
summary:
|
|
418
|
+
"No verification command is configured, so the gate rejects every change with a No Verification Oracle finding",
|
|
419
|
+
fixes: [
|
|
420
|
+
{
|
|
421
|
+
id: "oracle.bootstrap",
|
|
422
|
+
title: "Generate a verification oracle",
|
|
423
|
+
summary: "Run `agentctl bootstrap` to create one for this stack, or set verify.test in .agent/config.yml",
|
|
424
|
+
risk: "low",
|
|
425
|
+
automatic: true,
|
|
426
|
+
requiresProbe: false,
|
|
427
|
+
},
|
|
428
|
+
],
|
|
346
429
|
});
|
|
347
430
|
}
|
|
348
431
|
|
|
@@ -514,7 +597,11 @@ export async function runDoctorChecks(options = {}) {
|
|
|
514
597
|
if (existsSync(envFile)) {
|
|
515
598
|
let tracked = false;
|
|
516
599
|
try {
|
|
517
|
-
const res = spawnSync("git", ["ls-files", "--error-unmatch", ".env"], {
|
|
600
|
+
const res = spawnSync("git", ["ls-files", "--error-unmatch", ".env"], {
|
|
601
|
+
cwd: root,
|
|
602
|
+
encoding: "utf-8",
|
|
603
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
604
|
+
});
|
|
518
605
|
tracked = res.status === 0;
|
|
519
606
|
} catch (_) {}
|
|
520
607
|
|
package/src/ops/handover.mjs
CHANGED
|
@@ -5,15 +5,9 @@ import {
|
|
|
5
5
|
readdirSync,
|
|
6
6
|
statSync,
|
|
7
7
|
unlinkSync,
|
|
8
|
-
renameSync,
|
|
9
|
-
openSync,
|
|
10
|
-
writeSync,
|
|
11
|
-
fsyncSync,
|
|
12
|
-
closeSync,
|
|
13
8
|
} from "node:fs";
|
|
14
9
|
import { join, resolve, relative, isAbsolute, basename } from "node:path";
|
|
15
|
-
import {
|
|
16
|
-
import { redactSecrets } from "../security.mjs";
|
|
10
|
+
import { redactSecrets, safeAtomicWrite } from "../security.mjs";
|
|
17
11
|
import { resolveRoot } from "../config.mjs";
|
|
18
12
|
|
|
19
13
|
export class HandoverError extends Error {
|
|
@@ -77,25 +71,6 @@ function sanitizeText(value) {
|
|
|
77
71
|
return redactSecrets(String(value).trim());
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
function writeFileAtomically(filePath, content) {
|
|
81
|
-
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
82
|
-
let fd;
|
|
83
|
-
try {
|
|
84
|
-
fd = openSync(tmpPath, "wx", 0o600);
|
|
85
|
-
writeSync(fd, content, "utf-8");
|
|
86
|
-
fsyncSync(fd);
|
|
87
|
-
closeSync(fd);
|
|
88
|
-
fd = undefined;
|
|
89
|
-
renameSync(tmpPath, filePath);
|
|
90
|
-
} catch (err) {
|
|
91
|
-
if (fd !== undefined) {
|
|
92
|
-
try { closeSync(fd); } catch (_) {}
|
|
93
|
-
}
|
|
94
|
-
try { unlinkSync(tmpPath); } catch (_) {}
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
74
|
/**
|
|
100
75
|
* Creates and persists a Baton Pass handover manifest in .agent/handovers/YYYY-MM-DD-[sessionId].md.
|
|
101
76
|
*
|
|
@@ -192,7 +167,8 @@ export function createHandover(root = resolveRoot(), data = {}, options = {}) {
|
|
|
192
167
|
bodySections.push("");
|
|
193
168
|
const fullContent = frontmatterLines.join("\n") + bodySections.join("\n");
|
|
194
169
|
|
|
195
|
-
|
|
170
|
+
// Handover transcripts can quote session content; keep the private 0600 mode.
|
|
171
|
+
safeAtomicWrite(filePath, fullContent, { mode: 0o600 });
|
|
196
172
|
|
|
197
173
|
const retention = typeof options.maxRetention === "number" ? options.maxRetention : 20;
|
|
198
174
|
if (retention > 0) {
|
package/src/ops/pr-harvest.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { classifyRiskTier, RISK_TIERS } from "../risk.mjs";
|
|
3
|
-
import { checkSafetyGate } from "
|
|
3
|
+
import { checkSafetyGate } from "../merge-swarm.mjs";
|
|
4
4
|
import { normalizePath, loadConfig } from "../config.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
package/src/prompt-guard.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* neutralizes LLM control role markers and prompt injection patterns, and wraps inputs in strict tags.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const ZERO_WIDTH_AND_BIDI_REGEX = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/
|
|
9
|
+
const ZERO_WIDTH_AND_BIDI_REGEX = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]|[\u{E0000}-\u{E007F}]/gu;
|
|
10
10
|
const ANSI_ESCAPE_REGEX = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
11
11
|
|
|
12
12
|
const CONTROL_TAGS_REGEX = /<\|im_start\|>|<\|im_end\|>|<\|system\|>|<\|assistant\|>|<\|user\|>|<\|endoftext\|>|\[INST\]|\[\/INST\]|<<SYS>>|<\/SYS>>/gi;
|
|
@@ -27,29 +27,59 @@ const INJECTION_PATTERNS = [
|
|
|
27
27
|
* to prevent false-positive safety classifier tripwires in Google VM environments.
|
|
28
28
|
*/
|
|
29
29
|
export const STERILE_VOCABULARY_MAP = [
|
|
30
|
+
{ pattern: /\bkill\s+-9\b/gi, replacement: "terminate with SIGTERM" },
|
|
31
|
+
{ pattern: /\bSIGKILL\b/gi, replacement: "SIGTERM" },
|
|
30
32
|
{ pattern: /\bkill\s+(?:the\s+)?processes\b/gi, replacement: "terminate the processes" },
|
|
31
33
|
{ pattern: /\bkill\s+(?:the\s+)?process\b/gi, replacement: "terminate the process" },
|
|
32
34
|
{ pattern: /\bkill\s+(?:the\s+)?pid\b/gi, replacement: "terminate the PID" },
|
|
35
|
+
{ pattern: /\breap\s+(?:the\s+)?(?:zombie\s+)?processes\b/gi, replacement: "collect child processes" },
|
|
36
|
+
{ pattern: /\breap\s+(?:the\s+)?(?:zombie\s+)?process\b/gi, replacement: "collect child process" },
|
|
37
|
+
{ pattern: /\breap\s+(?:the\s+)?zombies\b/gi, replacement: "collect child processes" },
|
|
33
38
|
{ pattern: /\bamputate\s+(?:dead\s+|unused\s+)?code\b/gi, replacement: "prune unused code" },
|
|
34
39
|
{ pattern: /\bamputate\b/gi, replacement: "prune" },
|
|
35
40
|
{ pattern: /\bsabotage\s+(?:the\s+)?tests?\b/gi, replacement: "mutate test logic" },
|
|
36
41
|
{ pattern: /\bsabotage\b/gi, replacement: "mutate" },
|
|
37
42
|
{ pattern: /\bdestroy\s+(?:the\s+)?(state|cache|file|data|directory)\b/gi, replacement: "purge the $1" },
|
|
38
43
|
{ pattern: /\bwipe\s+(?:the\s+)?(state|cache|directory|disk|table)\b/gi, replacement: "clear the $1" },
|
|
44
|
+
{ pattern: /\bexploit_payload\b/gi, replacement: "test_payload" },
|
|
39
45
|
];
|
|
40
46
|
|
|
41
47
|
/**
|
|
42
|
-
* Sanitizes aggressive phrases into clinical equivalents
|
|
48
|
+
* Sanitizes aggressive phrases into clinical equivalents while preserving
|
|
49
|
+
* code blocks (```...```) and inline backticks (`...`) verbatim.
|
|
43
50
|
*
|
|
44
51
|
* @param {string} text - Prompt text
|
|
45
52
|
* @returns {string} Sanitized prompt with clinical vocabulary
|
|
46
53
|
*/
|
|
47
54
|
export function sanitizePromptVocabulary(text) {
|
|
48
55
|
if (!text || typeof text !== "string") return text || "";
|
|
49
|
-
|
|
56
|
+
|
|
57
|
+
const codeSpans = [];
|
|
58
|
+
const placeholderPrefix = "@@VERBATIM_CODE_SPAN_";
|
|
59
|
+
|
|
60
|
+
// Protect fenced code blocks
|
|
61
|
+
let protectedText = text.replace(/```[\s\S]*?```/g, (match) => {
|
|
62
|
+
const idx = codeSpans.length;
|
|
63
|
+
codeSpans.push(match);
|
|
64
|
+
return `${placeholderPrefix}${idx}@@`;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Protect inline code spans
|
|
68
|
+
protectedText = protectedText.replace(/`[^`\n]+`/g, (match) => {
|
|
69
|
+
const idx = codeSpans.length;
|
|
70
|
+
codeSpans.push(match);
|
|
71
|
+
return `${placeholderPrefix}${idx}@@`;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let sanitized = protectedText;
|
|
50
75
|
for (const { pattern, replacement } of STERILE_VOCABULARY_MAP) {
|
|
51
76
|
sanitized = sanitized.replace(pattern, replacement);
|
|
52
77
|
}
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < codeSpans.length; i++) {
|
|
80
|
+
sanitized = sanitized.replace(`${placeholderPrefix}${i}@@`, codeSpans[i]);
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
return sanitized;
|
|
54
84
|
}
|
|
55
85
|
|
package/src/provider.mjs
CHANGED
|
@@ -185,19 +185,36 @@ const NAMED_PRESETS = {
|
|
|
185
185
|
* Multi-token manager with round-robin rotation, 429 quarantine/cooldown, and failover.
|
|
186
186
|
*/
|
|
187
187
|
export class TokenPool {
|
|
188
|
-
constructor(tokens = []) {
|
|
188
|
+
constructor(tokens = [], options = {}) {
|
|
189
189
|
this.tokens = Array.from(new Set(tokens.filter(Boolean)));
|
|
190
190
|
this.currentIndex = 0;
|
|
191
191
|
this.cooldowns = new Map(); // token -> cooldownExpiryTimestamp
|
|
192
192
|
this.usage24h = new Map(); // token -> count
|
|
193
|
+
this.limits = new Map(Object.entries(options.limits || {}));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getLimit(token, index) {
|
|
197
|
+
if (this.limits.has(token)) return this.limits.get(token);
|
|
198
|
+
return index === 0 ? 100 : 15;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
static fromEnv(config = {}) {
|
|
196
|
-
const rawList = (
|
|
202
|
+
const rawList = (
|
|
203
|
+
process.env.JULES_API_KEYS ||
|
|
204
|
+
process.env.JULES_API_KEY_SECONDARY ||
|
|
205
|
+
process.env.JULES_SECONDARY_TOKENS ||
|
|
206
|
+
process.env.AGENT_SECONDARY_TOKENS ||
|
|
207
|
+
""
|
|
208
|
+
)
|
|
197
209
|
.split(",")
|
|
198
210
|
.map((t) => t.trim())
|
|
199
211
|
.filter(Boolean);
|
|
200
|
-
const primary = (
|
|
212
|
+
const primary = (
|
|
213
|
+
process.env.JULES_API_KEY ||
|
|
214
|
+
process.env.JULES_MAIN_TOKEN ||
|
|
215
|
+
process.env.AGENT_MAIN_TOKEN ||
|
|
216
|
+
""
|
|
217
|
+
).trim();
|
|
201
218
|
const configKeys = Array.isArray(config.julesApiKeys) ? config.julesApiKeys : [];
|
|
202
219
|
const combined = Array.from(new Set([primary, ...rawList, ...configKeys].filter(Boolean)));
|
|
203
220
|
return new TokenPool(combined);
|
|
@@ -211,15 +228,38 @@ export class TokenPool {
|
|
|
211
228
|
if (this.tokens.length === 0) return "";
|
|
212
229
|
const now = Date.now();
|
|
213
230
|
|
|
214
|
-
//
|
|
231
|
+
// Collect available (non-cooldown) candidates
|
|
232
|
+
const available = [];
|
|
215
233
|
for (let i = 0; i < this.tokens.length; i++) {
|
|
216
234
|
const idx = (this.currentIndex + i) % this.tokens.length;
|
|
217
235
|
const token = this.tokens[idx];
|
|
218
236
|
const cooldownUntil = this.cooldowns.get(token) || 0;
|
|
219
237
|
if (now >= cooldownUntil) {
|
|
220
|
-
|
|
221
|
-
|
|
238
|
+
const usage = this.usage24h.get(token) || 0;
|
|
239
|
+
const limit = this.getLimit(token, idx);
|
|
240
|
+
const utilization = limit > 0 ? usage / limit : 1;
|
|
241
|
+
available.push({ token, idx, usage, limit, utilization });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (available.length > 0) {
|
|
246
|
+
// If any usage has been recorded, pick the one with lowest utilization
|
|
247
|
+
const hasUsage = available.some((a) => a.usage > 0);
|
|
248
|
+
if (hasUsage) {
|
|
249
|
+
let best = available[0];
|
|
250
|
+
for (const candidate of available) {
|
|
251
|
+
if (candidate.utilization < best.utilization) {
|
|
252
|
+
best = candidate;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.currentIndex = (best.idx + 1) % this.tokens.length;
|
|
256
|
+
return best.token;
|
|
222
257
|
}
|
|
258
|
+
|
|
259
|
+
// If no usage recorded yet, preserve exact round-robin behavior
|
|
260
|
+
const chosen = available[0];
|
|
261
|
+
this.currentIndex = (chosen.idx + 1) % this.tokens.length;
|
|
262
|
+
return chosen.token;
|
|
223
263
|
}
|
|
224
264
|
|
|
225
265
|
// If all are in cooldown, pick the one that expires earliest
|
|
@@ -253,6 +293,8 @@ export class TokenPool {
|
|
|
253
293
|
const cooldownUntil = this.cooldowns.get(token) || 0;
|
|
254
294
|
const inCooldown = now < cooldownUntil;
|
|
255
295
|
const maskedToken = token.length <= 8 ? "****" : `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
296
|
+
const limit = this.getLimit(token, index);
|
|
297
|
+
const usage = this.usage24h.get(token) || 0;
|
|
256
298
|
return {
|
|
257
299
|
id: `key-${index + 1}`,
|
|
258
300
|
index,
|
|
@@ -260,7 +302,9 @@ export class TokenPool {
|
|
|
260
302
|
maskedToken,
|
|
261
303
|
inCooldown,
|
|
262
304
|
cooldownRemainingMs: inCooldown ? cooldownUntil - now : 0,
|
|
263
|
-
usage
|
|
305
|
+
usage,
|
|
306
|
+
limit,
|
|
307
|
+
utilization: limit > 0 ? usage / limit : 1,
|
|
264
308
|
};
|
|
265
309
|
});
|
|
266
310
|
}
|
package/src/remediation.mjs
CHANGED
|
@@ -285,7 +285,7 @@ export function createWhackAMoleDetector(opts = {}) {
|
|
|
285
285
|
const oscillatingTests = [...cycleTests];
|
|
286
286
|
|
|
287
287
|
const testSummary = oscillatingTests.length > 0 ? oscillatingTests.join(" <-> ") : "tests";
|
|
288
|
-
const promptDirective = `[
|
|
288
|
+
const promptDirective = `[Test Oscillation Circuit Breaker Activated] Switching verification strategy: fixing one test repeatedly breaks another (<UNTRUSTED>${testSummary}</UNTRUSTED>). Do not add more conditional edge-case patches. Revert recent patches and refactor the core logic cleanly.`;
|
|
289
289
|
|
|
290
290
|
return {
|
|
291
291
|
whackAMole: true,
|
|
@@ -293,7 +293,7 @@ export function createWhackAMoleDetector(opts = {}) {
|
|
|
293
293
|
oscillatingTests,
|
|
294
294
|
occurrences,
|
|
295
295
|
promptDirective,
|
|
296
|
-
reason: `
|
|
296
|
+
reason: `Test Oscillation Circuit Breaker Activated: Test failure signature repeated across repair turns (${testSummary}).`,
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
|
package/src/role-resolver.mjs
CHANGED
|
@@ -19,6 +19,89 @@ import { loadConfig } from "./config.mjs";
|
|
|
19
19
|
*/
|
|
20
20
|
export const ROLE_PROMPT_TOKENS = ["VERIFY_TEST", "VERIFY_LINT", "VERIFY_BUILD", "DIFF_KB", "BASE_BRANCH"];
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Legacy and convenience aliases for the standard engineering roles.
|
|
24
|
+
*
|
|
25
|
+
* Strictly one-directional: every key is a non-canonical name and every
|
|
26
|
+
* value is a canonical role. Provides full backward-compatibility for prior
|
|
27
|
+
* command flags (--role bolt, etc.) without letting a canonical role ever
|
|
28
|
+
* resolve away from its own prompt file.
|
|
29
|
+
*/
|
|
30
|
+
export const ROLE_ALIASES = Object.freeze({
|
|
31
|
+
// Legacy RPG/fantasy names mapped to canonical engineering roles
|
|
32
|
+
overseer: "auditor",
|
|
33
|
+
bolt: "performance",
|
|
34
|
+
sentinel: "security",
|
|
35
|
+
janitor: "hygiene",
|
|
36
|
+
spectator: "e2e",
|
|
37
|
+
scribe: "docs",
|
|
38
|
+
alchemist: "database",
|
|
39
|
+
bulwark: "resilience",
|
|
40
|
+
typist: "types",
|
|
41
|
+
hunter: "debugger",
|
|
42
|
+
|
|
43
|
+
// Common operator shortcuts
|
|
44
|
+
perf: "performance",
|
|
45
|
+
sec: "security",
|
|
46
|
+
db: "database",
|
|
47
|
+
cleanup: "hygiene",
|
|
48
|
+
accessibility: "a11y",
|
|
49
|
+
documentation: "docs",
|
|
50
|
+
visual: "e2e",
|
|
51
|
+
playwright: "e2e",
|
|
52
|
+
schema: "database",
|
|
53
|
+
migration: "database",
|
|
54
|
+
reliability: "resilience",
|
|
55
|
+
typecheck: "types",
|
|
56
|
+
"type-safety": "types",
|
|
57
|
+
debug: "debugger",
|
|
58
|
+
"defect-fix": "debugger",
|
|
59
|
+
lead: "auditor",
|
|
60
|
+
coordinator: "auditor",
|
|
61
|
+
test: "testing",
|
|
62
|
+
"unit-test": "testing",
|
|
63
|
+
"integration-test": "testing",
|
|
64
|
+
qa: "testing",
|
|
65
|
+
tester: "testing",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const CANONICAL_ROLES = Object.freeze([
|
|
69
|
+
"auditor",
|
|
70
|
+
"performance",
|
|
71
|
+
"security",
|
|
72
|
+
"hygiene",
|
|
73
|
+
"a11y",
|
|
74
|
+
"docs",
|
|
75
|
+
"e2e",
|
|
76
|
+
"database",
|
|
77
|
+
"resilience",
|
|
78
|
+
"types",
|
|
79
|
+
"debugger",
|
|
80
|
+
"testing",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pre-consolidation prompt filenames, canonical role -> legacy file stem.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately NOT part of ROLE_ALIASES: the alias table answers "what did
|
|
87
|
+
* the operator mean", while this map answers "what might an older checkout
|
|
88
|
+
* still have on disk". Keeping the two separate is what lets ROLE_ALIASES
|
|
89
|
+
* stay strictly one-directional without stranding repos scaffolded before
|
|
90
|
+
* the legacy duplicates were deleted.
|
|
91
|
+
*/
|
|
92
|
+
const LEGACY_PROMPT_FILENAMES = Object.freeze({
|
|
93
|
+
auditor: "overseer",
|
|
94
|
+
performance: "bolt",
|
|
95
|
+
security: "sentinel",
|
|
96
|
+
hygiene: "janitor",
|
|
97
|
+
e2e: "spectator",
|
|
98
|
+
docs: "scribe",
|
|
99
|
+
database: "alchemist",
|
|
100
|
+
resilience: "bulwark",
|
|
101
|
+
types: "typist",
|
|
102
|
+
debugger: "hunter",
|
|
103
|
+
});
|
|
104
|
+
|
|
22
105
|
/**
|
|
23
106
|
* Substitutes `{{TOKEN}}` placeholders in a role prompt from resolved config.
|
|
24
107
|
*
|
|
@@ -51,14 +134,41 @@ export function hydrateRolePrompt(content = "", config = {}) {
|
|
|
51
134
|
export function resolveRolePrompt(root = process.cwd(), roleName = "", opts = {}) {
|
|
52
135
|
if (!roleName || typeof roleName !== "string") return null;
|
|
53
136
|
const cleanName = roleName.trim().toLowerCase();
|
|
137
|
+
const directAlias = ROLE_ALIASES[cleanName];
|
|
138
|
+
const canonical = directAlias && CANONICAL_ROLES.includes(directAlias)
|
|
139
|
+
? directAlias
|
|
140
|
+
: (CANONICAL_ROLES.includes(cleanName) ? cleanName : null);
|
|
141
|
+
|
|
142
|
+
const candidates = [];
|
|
143
|
+
if (canonical) {
|
|
144
|
+
candidates.push(canonical);
|
|
145
|
+
}
|
|
146
|
+
if (!candidates.includes(cleanName)) {
|
|
147
|
+
candidates.push(cleanName);
|
|
148
|
+
}
|
|
149
|
+
// Secondary fallback: a checkout scaffolded before consolidation may still
|
|
150
|
+
// hold only the legacy filename on disk (e.g. Bolt.md). The canonical file
|
|
151
|
+
// always wins when both exist; this branch just keeps old trees working.
|
|
152
|
+
if (canonical && LEGACY_PROMPT_FILENAMES[canonical] && !candidates.includes(LEGACY_PROMPT_FILENAMES[canonical])) {
|
|
153
|
+
candidates.push(LEGACY_PROMPT_FILENAMES[canonical]);
|
|
154
|
+
}
|
|
155
|
+
// Include direct alias if not yet present
|
|
156
|
+
if (directAlias && !candidates.includes(directAlias)) {
|
|
157
|
+
candidates.push(directAlias);
|
|
158
|
+
}
|
|
159
|
+
|
|
54
160
|
const promptsDir = join(root, ".agent", "prompts");
|
|
55
161
|
if (!existsSync(promptsDir)) return null;
|
|
56
162
|
|
|
57
163
|
try {
|
|
58
164
|
const files = readdirSync(promptsDir);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
165
|
+
let matched = null;
|
|
166
|
+
for (const cand of candidates) {
|
|
167
|
+
matched = files.find(
|
|
168
|
+
(f) => f.toLowerCase() === `${cand}.md` || f.toLowerCase() === cand
|
|
169
|
+
);
|
|
170
|
+
if (matched) break;
|
|
171
|
+
}
|
|
62
172
|
if (matched) {
|
|
63
173
|
const fullPath = join(promptsDir, matched);
|
|
64
174
|
const raw = readFileSync(fullPath, "utf-8").trim();
|