jules-orchestrator-kit 0.32.4 → 0.32.5
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 +46 -10
- package/bin/agentctl.mjs +122 -13
- package/index.mjs +18 -3
- package/package.json +1 -1
- package/scripts/command-resolver.mjs +15 -1
- package/src/config.mjs +31 -0
- package/src/dag-engine.mjs +118 -0
- package/src/engine.mjs +237 -71
- package/src/evidence.mjs +435 -0
- package/src/mcp.mjs +39 -1
- package/src/ops/evidence-actions.mjs +74 -0
- package/src/provider.mjs +37 -7
- package/src/router.mjs +200 -0
- package/src/stack-detector.mjs +28 -0
- package/src/wizard-task.mjs +66 -5
package/src/router.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { normalizePath } from "./config.mjs";
|
|
2
|
+
import { matchesGlob } from "./security.mjs";
|
|
3
|
+
import { extractPathTokens } from "./task-optimizer.mjs";
|
|
4
|
+
import { createProvider, createFailoverProvider } from "./provider.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dynamic Complexity & Cost Router (Roadmap v0.33.0).
|
|
8
|
+
*
|
|
9
|
+
* Zero-dependency, rule-based heuristic classifier — no ML classifier, no
|
|
10
|
+
* external routing service. Routes trivial tasks (typos, lockfile bumps,
|
|
11
|
+
* lint fixes) to a cheap/fast provider while reserving the primary provider
|
|
12
|
+
* for complex, multi-file, or safety-sensitive work. Opt-in via
|
|
13
|
+
* `config.router.enabled` — disabled by default, zero behavior change for
|
|
14
|
+
* existing users.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const ROUTE_TIERS = {
|
|
18
|
+
FAST: "fast",
|
|
19
|
+
COMPLEX: "complex",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const TRIVIAL_SIGNALS = [
|
|
23
|
+
/\btypo(s)?\b/i,
|
|
24
|
+
/\brenam(e|ing|ed)\b/i,
|
|
25
|
+
/\bbump(ed|ing)?\b/i,
|
|
26
|
+
/\bversion bump\b/i,
|
|
27
|
+
/\block ?file\b/i,
|
|
28
|
+
/\bformat(ting)?\b/i,
|
|
29
|
+
/\blint(ing)?\s*(fix(es)?|error)?\b/i,
|
|
30
|
+
/\bwhitespace\b/i,
|
|
31
|
+
/\b(update|fix)\s+comment(s)?\b/i,
|
|
32
|
+
/\bdead code\b/i,
|
|
33
|
+
/\bunused (import|variable)s?\b/i,
|
|
34
|
+
/\breadme\b/i,
|
|
35
|
+
/\btypo fix\b/i,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const COMPLEX_SIGNALS = [
|
|
39
|
+
/\brefactor(ing)?\b/i,
|
|
40
|
+
/\barchitect(ure)?\b/i,
|
|
41
|
+
/\bmigrat(e|ion|ing)\b/i,
|
|
42
|
+
/\bredesign\b/i,
|
|
43
|
+
/\bconcurrency\b/i,
|
|
44
|
+
/\brace condition\b/i,
|
|
45
|
+
/\bsecurity\b/i,
|
|
46
|
+
/\bauth(entication|orization)?\b/i,
|
|
47
|
+
/\bschema\b/i,
|
|
48
|
+
/\bbreaking change\b/i,
|
|
49
|
+
/\bmulti-file\b/i,
|
|
50
|
+
/\bcross-package\b/i,
|
|
51
|
+
/\bdatabase\b/i,
|
|
52
|
+
/\bpayment(s)?\b/i,
|
|
53
|
+
/\bencrypt(ion)?\b/i,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Sentinel handles security-sensitive work; the primary provider is always used.
|
|
57
|
+
const FORCE_COMPLEX_ROLES = new Set(["sentinel"]);
|
|
58
|
+
const FAST_LEANING_ROLES = new Set(["janitor", "bolt"]);
|
|
59
|
+
const COMPLEX_LEANING_ROLES = new Set(["overseer", "sentinel"]);
|
|
60
|
+
|
|
61
|
+
// Supplements config.scope.deny — these are never eligible for the fast tier
|
|
62
|
+
// regardless of user scope config, mirroring src/risk.mjs's RESTRICTED_PATH_PATTERNS.
|
|
63
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
64
|
+
"**/auth/**",
|
|
65
|
+
"**/migrations/**",
|
|
66
|
+
"**/pricing/**",
|
|
67
|
+
"**/secrets/**",
|
|
68
|
+
"**/*.pem",
|
|
69
|
+
"**/*.key",
|
|
70
|
+
".github/**",
|
|
71
|
+
".agent/config.yml",
|
|
72
|
+
".agent/jules.yml",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function collectReferencedPaths(task) {
|
|
76
|
+
const fromPrompt = extractPathTokens(task.prompt || "");
|
|
77
|
+
const explicit = Array.isArray(task.targetFiles)
|
|
78
|
+
? task.targetFiles
|
|
79
|
+
: Array.isArray(task.referenced_paths)
|
|
80
|
+
? task.referenced_paths
|
|
81
|
+
: [];
|
|
82
|
+
return [...new Set([...fromPrompt, ...explicit])];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function touchesSensitivePath(paths, config) {
|
|
86
|
+
const denyPatterns = [...SENSITIVE_PATH_PATTERNS, ...((config && config.scope && config.scope.deny) || [])];
|
|
87
|
+
for (const rawPath of paths) {
|
|
88
|
+
const p = normalizePath(rawPath);
|
|
89
|
+
for (const pattern of denyPatterns) {
|
|
90
|
+
if (matchesGlob(p, pattern)) return { path: p, pattern };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Heuristically classifies a task envelope into a FAST or COMPLEX routing tier.
|
|
98
|
+
* @param {object} task - Task envelope (title, prompt, role, targetFiles, tier).
|
|
99
|
+
* @param {object} config - Loaded orchestrator config (uses scope.deny, router.threshold).
|
|
100
|
+
* @returns {{tier: string, score: number|null, forced: boolean, reason: string, signals?: string[]}}
|
|
101
|
+
*/
|
|
102
|
+
export function classifyTaskComplexity(task = {}, config = {}) {
|
|
103
|
+
const explicitTier = task.tier || task.complexity;
|
|
104
|
+
if (explicitTier === ROUTE_TIERS.FAST || explicitTier === ROUTE_TIERS.COMPLEX) {
|
|
105
|
+
return { tier: explicitTier, score: null, forced: true, reason: `Explicit task tier override: '${explicitTier}'` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const paths = collectReferencedPaths(task);
|
|
109
|
+
const sensitiveHit = touchesSensitivePath(paths, config);
|
|
110
|
+
if (sensitiveHit) {
|
|
111
|
+
return {
|
|
112
|
+
tier: ROUTE_TIERS.COMPLEX,
|
|
113
|
+
score: null,
|
|
114
|
+
forced: true,
|
|
115
|
+
reason: `Touches sensitive path '${sensitiveHit.path}' (matches '${sensitiveHit.pattern}')`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const role = String(task.role || "").toLowerCase();
|
|
120
|
+
if (FORCE_COMPLEX_ROLES.has(role)) {
|
|
121
|
+
return { tier: ROUTE_TIERS.COMPLEX, score: null, forced: true, reason: `Role '${role}' always routes to the primary provider` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const text = `${task.title || ""} ${task.prompt || ""}`;
|
|
125
|
+
let score = 0;
|
|
126
|
+
const signals = [];
|
|
127
|
+
|
|
128
|
+
for (const pattern of COMPLEX_SIGNALS) {
|
|
129
|
+
if (pattern.test(text)) {
|
|
130
|
+
score += 2;
|
|
131
|
+
signals.push(`+2 complex keyword (${pattern.source})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const pattern of TRIVIAL_SIGNALS) {
|
|
135
|
+
if (pattern.test(text)) {
|
|
136
|
+
score -= 1;
|
|
137
|
+
signals.push(`-1 trivial keyword (${pattern.source})`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (paths.length >= 4) {
|
|
142
|
+
score += 3;
|
|
143
|
+
signals.push(`+3 touches ${paths.length} files`);
|
|
144
|
+
} else if (paths.length > 0 && paths.length <= 1) {
|
|
145
|
+
score -= 1;
|
|
146
|
+
signals.push(`-1 touches a single file`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const promptLen = (task.prompt || "").length;
|
|
150
|
+
if (promptLen > 1200) {
|
|
151
|
+
score += 1;
|
|
152
|
+
signals.push(`+1 long prompt (${promptLen} chars)`);
|
|
153
|
+
} else if (promptLen > 0 && promptLen < 200) {
|
|
154
|
+
score -= 1;
|
|
155
|
+
signals.push(`-1 short prompt (${promptLen} chars)`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (COMPLEX_LEANING_ROLES.has(role)) {
|
|
159
|
+
score += 1;
|
|
160
|
+
signals.push(`+1 role '${role}' leans complex`);
|
|
161
|
+
}
|
|
162
|
+
if (FAST_LEANING_ROLES.has(role)) {
|
|
163
|
+
score -= 2;
|
|
164
|
+
signals.push(`-2 role '${role}' leans fast`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const threshold = Number.isFinite(config?.router?.threshold) ? config.router.threshold : 0;
|
|
168
|
+
const tier = score > threshold ? ROUTE_TIERS.COMPLEX : ROUTE_TIERS.FAST;
|
|
169
|
+
|
|
170
|
+
return { tier, score, forced: false, threshold, signals, reason: `Heuristic score ${score} vs threshold ${threshold}` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolves the provider adapter to dispatch a task through, honoring
|
|
175
|
+
* `config.router` (opt-in Dynamic Complexity & Cost Router). Disabled by
|
|
176
|
+
* default — returns the primary provider unchanged. FAST-tier tasks are
|
|
177
|
+
* wrapped in a failover cascade so a rate-limited/unavailable fast provider
|
|
178
|
+
* falls through to the primary provider automatically.
|
|
179
|
+
* @param {object} task
|
|
180
|
+
* @param {object} config
|
|
181
|
+
* @returns {{provider: object, routed: boolean, classification: object|null}}
|
|
182
|
+
*/
|
|
183
|
+
export function resolveRoutedProvider(task = {}, config = {}) {
|
|
184
|
+
const routerCfg = config.router || {};
|
|
185
|
+
const complexSpec = routerCfg.complex || config.provider || "jules";
|
|
186
|
+
|
|
187
|
+
if (!routerCfg.enabled) {
|
|
188
|
+
return { provider: createProvider(config.provider, config), routed: false, classification: null };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const classification = classifyTaskComplexity(task, config);
|
|
192
|
+
|
|
193
|
+
if (classification.tier === ROUTE_TIERS.COMPLEX) {
|
|
194
|
+
return { provider: createProvider(complexSpec, config), routed: true, classification };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fastSpec = routerCfg.fast || "gemini-flash";
|
|
198
|
+
const provider = createFailoverProvider([fastSpec, complexSpec], config);
|
|
199
|
+
return { provider, routed: true, classification };
|
|
200
|
+
}
|
package/src/stack-detector.mjs
CHANGED
|
@@ -155,6 +155,31 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
|
|
|
155
155
|
return { ...container, stack: "make", testCmd: "make test", buildCmd: "make build", triggerFile: "Makefile" };
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// 4b. Web3 / Solidity / Foundry & Hardhat
|
|
159
|
+
if (existsSync(join(projectRoot, "foundry.toml")) || existsSync(join(projectRoot, "remappings.txt"))) {
|
|
160
|
+
const triggerFile = existsSync(join(projectRoot, "foundry.toml")) ? "foundry.toml" : "remappings.txt";
|
|
161
|
+
return {
|
|
162
|
+
...container,
|
|
163
|
+
stack: "foundry",
|
|
164
|
+
testCmd: "forge test --offline",
|
|
165
|
+
buildCmd: "forge build --offline",
|
|
166
|
+
fmtCmd: "forge fmt --check",
|
|
167
|
+
fuzzCmd: "forge test --offline --match-test testFuzz",
|
|
168
|
+
invariantCmd: "forge test --offline --match-test invariant",
|
|
169
|
+
triggerFile,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (existsSync(join(projectRoot, "hardhat.config.js")) || existsSync(join(projectRoot, "hardhat.config.ts"))) {
|
|
173
|
+
const triggerFile = existsSync(join(projectRoot, "hardhat.config.ts")) ? "hardhat.config.ts" : "hardhat.config.js";
|
|
174
|
+
return {
|
|
175
|
+
...container,
|
|
176
|
+
stack: "hardhat",
|
|
177
|
+
testCmd: "npx hardhat test",
|
|
178
|
+
buildCmd: "npx hardhat compile",
|
|
179
|
+
triggerFile,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
// 5. Python / Django / Elixir / Ruby / Java
|
|
159
184
|
if (existsSync(join(projectRoot, "manage.py"))) {
|
|
160
185
|
return { ...container, stack: "django", testCmd: "python manage.py test --keepdb", buildCmd: "python manage.py check", triggerFile: "manage.py" };
|
|
@@ -247,6 +272,9 @@ export function findSubprojectRoot(fileRelPath = "", root = process.cwd()) {
|
|
|
247
272
|
|
|
248
273
|
const manifestFiles = [
|
|
249
274
|
"package.json",
|
|
275
|
+
"foundry.toml",
|
|
276
|
+
"hardhat.config.js",
|
|
277
|
+
"hardhat.config.ts",
|
|
250
278
|
"Cargo.toml",
|
|
251
279
|
"go.mod",
|
|
252
280
|
"pyproject.toml",
|
package/src/wizard-task.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { writeFileSync, readFileSync, readdirSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
3
|
import { loadConfig } from "./config.mjs";
|
|
4
4
|
import { gate } from "./engine.mjs";
|
|
5
5
|
import { scanDiff, shannonEntropy } from "./security.mjs";
|
|
@@ -21,9 +21,38 @@ HARD CONSTRAINTS:
|
|
|
21
21
|
|
|
22
22
|
const TRIVIAL_ORACLES = new Set(["true", "echo", ":", "false", "exit 0", "exit 1", "echo ok"]);
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Resolves specialist agent role markdown prompt from .agent/prompts/
|
|
26
|
+
* @param {string} [root=process.cwd()]
|
|
27
|
+
* @param {string} [roleName=""]
|
|
28
|
+
* @returns {{ role: string, path: string, content: string } | null}
|
|
29
|
+
*/
|
|
30
|
+
export function resolveRolePrompt(root = process.cwd(), roleName = "") {
|
|
31
|
+
if (!roleName || typeof roleName !== "string") return null;
|
|
32
|
+
const cleanName = roleName.trim().toLowerCase();
|
|
33
|
+
const promptsDir = join(root, ".agent", "prompts");
|
|
34
|
+
if (!existsSync(promptsDir)) return null;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const files = readdirSync(promptsDir);
|
|
38
|
+
const matched = files.find(
|
|
39
|
+
(f) => f.toLowerCase() === `${cleanName}.md` || f.toLowerCase() === cleanName
|
|
40
|
+
);
|
|
41
|
+
if (matched) {
|
|
42
|
+
const fullPath = join(promptsDir, matched);
|
|
43
|
+
return {
|
|
44
|
+
role: matched.replace(/\.md$/i, ""),
|
|
45
|
+
path: fullPath,
|
|
46
|
+
content: readFileSync(fullPath, "utf-8").trim(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
/**
|
|
25
54
|
* Pure planning core for task creation & envelope synthesis.
|
|
26
|
-
* Supports web task templates, exploration budgets, and critic agent steering.
|
|
55
|
+
* Supports web task templates, specialist roles, exploration budgets, and critic agent steering.
|
|
27
56
|
* @param {string} root
|
|
28
57
|
* @param {object} inputObj
|
|
29
58
|
* @returns {{
|
|
@@ -33,6 +62,8 @@ const TRIVIAL_ORACLES = new Set(["true", "echo", ":", "false", "exit 0", "exit 1
|
|
|
33
62
|
* prompt: string,
|
|
34
63
|
* fullPrompt: string,
|
|
35
64
|
* verifyCmd: string,
|
|
65
|
+
* role?: string,
|
|
66
|
+
* dependsOn?: string[],
|
|
36
67
|
* flags: { autoPr: boolean, requirePlanApproval: boolean, repoless: boolean, startingBranch: string },
|
|
37
68
|
* secretFindings: Array<any>,
|
|
38
69
|
* taskFileContent: string
|
|
@@ -45,7 +76,22 @@ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
|
|
|
45
76
|
let rawPrompt = inputObj.prompt || "";
|
|
46
77
|
let verifyCmd = (inputObj.verifyCmd || config.verify.test || config.verify.build || "").trim();
|
|
47
78
|
|
|
48
|
-
//
|
|
79
|
+
// 0a. Process Specialist Role if specified
|
|
80
|
+
let resolvedRole = null;
|
|
81
|
+
if (inputObj.role) {
|
|
82
|
+
resolvedRole = resolveRolePrompt(root, inputObj.role);
|
|
83
|
+
if (!resolvedRole) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Unknown agent role '${inputObj.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Overseer, Bolt, Sentinel, Janitor).`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
rawPrompt = `${resolvedRole.content}\n\n${rawPrompt}`.trim();
|
|
89
|
+
if (!title || title === "Agent Task") {
|
|
90
|
+
title = `${resolvedRole.role}: ${inputObj.title || "Specialist Task"}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 0b. Process Template if specified
|
|
49
95
|
if (inputObj.template) {
|
|
50
96
|
const tpl = getWebTemplate(inputObj.template);
|
|
51
97
|
if (!tpl) {
|
|
@@ -57,7 +103,7 @@ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
|
|
|
57
103
|
criticGuidance: inputObj.criticGuidance
|
|
58
104
|
});
|
|
59
105
|
rawPrompt = rawPrompt ? `${rawPrompt}\n\n${synthesized.prompt}` : synthesized.fullEnvelope;
|
|
60
|
-
if (!title) title = synthesized.title;
|
|
106
|
+
if (!title || title === "Agent Task") title = synthesized.title;
|
|
61
107
|
if (!verifyCmd) verifyCmd = synthesized.verifyCmd;
|
|
62
108
|
}
|
|
63
109
|
|
|
@@ -123,12 +169,24 @@ ${GUARDRAIL_FOOTER}`;
|
|
|
123
169
|
|
|
124
170
|
const promptAnalysis = scorePromptFalsifiability(rawPrompt, { rootDir: root, verifyCmd });
|
|
125
171
|
|
|
172
|
+
const rawDepends = inputObj.dependsOn || inputObj.depends;
|
|
173
|
+
const dependsOn = Array.isArray(rawDepends)
|
|
174
|
+
? rawDepends
|
|
175
|
+
: typeof rawDepends === "string"
|
|
176
|
+
? rawDepends.split(",").map((s) => s.trim()).filter(Boolean)
|
|
177
|
+
: undefined;
|
|
178
|
+
|
|
179
|
+
const tier = inputObj.tier === "fast" || inputObj.tier === "complex" ? inputObj.tier : undefined;
|
|
180
|
+
|
|
126
181
|
const envelopeMetadata = {
|
|
127
182
|
version: 1,
|
|
128
183
|
id: taskId,
|
|
129
184
|
title,
|
|
130
185
|
flags,
|
|
131
186
|
verifyCmd,
|
|
187
|
+
role: resolvedRole ? resolvedRole.role : (inputObj.role || undefined),
|
|
188
|
+
dependsOn,
|
|
189
|
+
tier,
|
|
132
190
|
falsifiabilityScore: promptAnalysis.score,
|
|
133
191
|
grade: promptAnalysis.grade,
|
|
134
192
|
};
|
|
@@ -148,6 +206,9 @@ ${fullPrompt}
|
|
|
148
206
|
prompt: rawPrompt,
|
|
149
207
|
fullPrompt,
|
|
150
208
|
verifyCmd,
|
|
209
|
+
role: envelopeMetadata.role,
|
|
210
|
+
dependsOn,
|
|
211
|
+
tier,
|
|
151
212
|
flags,
|
|
152
213
|
secretFindings,
|
|
153
214
|
promptAnalysis,
|