jules-orchestrator-kit 0.32.3 → 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 +124 -13
- package/index.mjs +28 -4
- package/package.json +3 -3
- package/scripts/command-resolver.mjs +15 -1
- package/scripts/jules-merge-swarm.mjs +1 -1
- package/src/config.mjs +48 -3
- package/src/dag-engine.mjs +118 -0
- package/src/engine.mjs +239 -73
- package/src/evidence.mjs +435 -0
- package/src/mcp.mjs +39 -1
- package/src/memory.mjs +2 -2
- package/src/ops/evidence-actions.mjs +74 -0
- package/src/provider.mjs +37 -7
- package/src/router.mjs +200 -0
- package/src/security.mjs +55 -1
- package/src/stack-detector.mjs +372 -21
- 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/security.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { openSync, writeSync, fsyncSync, closeSync, renameSync, realpathSync, ex
|
|
|
2
2
|
import { dirname, join, basename } from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { normalizePath } from "./config.mjs";
|
|
5
|
+
import { detectCrossPackageBoundaryViolations } from "./stack-detector.mjs";
|
|
5
6
|
|
|
6
7
|
export const HIGH_CONFIDENCE_PATTERNS = [
|
|
7
8
|
/\bghp_[A-Za-z0-9_]{36,255}\b/g,
|
|
@@ -287,6 +288,50 @@ export function checkEdgeRuntimeImports(diffOrText = "", options = {}) {
|
|
|
287
288
|
};
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
export function checkCrossPackageImports(diffOrText = "", root = process.cwd(), options = {}) {
|
|
292
|
+
if (!diffOrText || typeof diffOrText !== "string") return { ok: true, violations: [] };
|
|
293
|
+
|
|
294
|
+
const violations = [];
|
|
295
|
+
|
|
296
|
+
if (diffOrText.includes("+++ b/")) {
|
|
297
|
+
const lines = diffOrText.split("\n");
|
|
298
|
+
let currentFile = null;
|
|
299
|
+
let currentAdded = [];
|
|
300
|
+
|
|
301
|
+
const flushFile = () => {
|
|
302
|
+
if (currentFile && currentAdded.length > 0) {
|
|
303
|
+
const fileViolations = detectCrossPackageBoundaryViolations([currentFile], root, {
|
|
304
|
+
fileContents: { [currentFile]: currentAdded.join("\n") },
|
|
305
|
+
...options,
|
|
306
|
+
});
|
|
307
|
+
violations.push(...fileViolations);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
if (line.startsWith("+++ b/")) {
|
|
313
|
+
flushFile();
|
|
314
|
+
currentFile = line.slice(6).trim();
|
|
315
|
+
currentAdded = [];
|
|
316
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
317
|
+
currentAdded.push(line.slice(1));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
flushFile();
|
|
321
|
+
} else if (options.file) {
|
|
322
|
+
const fileViolations = detectCrossPackageBoundaryViolations([options.file], root, {
|
|
323
|
+
fileContents: { [options.file]: diffOrText },
|
|
324
|
+
...options,
|
|
325
|
+
});
|
|
326
|
+
violations.push(...fileViolations);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
ok: violations.length === 0,
|
|
331
|
+
violations,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
290
335
|
export function scanDiff(diffTextStr = "", options = {}) {
|
|
291
336
|
if (!diffTextStr) return { ok: true, findings: [] };
|
|
292
337
|
const addedLines = diffTextStr
|
|
@@ -312,8 +357,17 @@ export function scanDiff(diffTextStr = "", options = {}) {
|
|
|
312
357
|
}
|
|
313
358
|
}
|
|
314
359
|
|
|
360
|
+
const root = options.root || process.cwd();
|
|
361
|
+
const crossPkgRes = checkCrossPackageImports(diffTextStr, root, options);
|
|
362
|
+
if (!crossPkgRes.ok) {
|
|
363
|
+
for (const v of crossPkgRes.violations) {
|
|
364
|
+
findings.push({ severity: "HIGH", type: "CROSS_PACKAGE_BOUNDARY_VIOLATION", description: v.reason });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
315
368
|
return {
|
|
316
|
-
ok: !hasHigh && !hasLow && edgeRes.ok,
|
|
369
|
+
ok: !hasHigh && !hasLow && edgeRes.ok && crossPkgRes.ok,
|
|
317
370
|
findings,
|
|
318
371
|
};
|
|
319
372
|
}
|
|
373
|
+
|