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/stack-detector.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { join,
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Generate a zero-dependency smoke test file using node:test for untested JS/Generic repos.
|
|
@@ -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" };
|
|
@@ -230,42 +255,366 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
|
|
|
230
255
|
return { ...container, stack: "unknown", testCmd: "", buildCmd: "", triggerFile: null };
|
|
231
256
|
}
|
|
232
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Normalizes file paths to POSIX slashes.
|
|
260
|
+
*/
|
|
261
|
+
function toPosixPath(p = "") {
|
|
262
|
+
return p.replace(/\\/g, "/");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Finds the nearest subproject root directory for a given relative file path.
|
|
267
|
+
*/
|
|
268
|
+
export function findSubprojectRoot(fileRelPath = "", root = process.cwd()) {
|
|
269
|
+
const normalized = toPosixPath(fileRelPath);
|
|
270
|
+
const parts = normalized.split("/");
|
|
271
|
+
if (parts.length <= 1) return ".";
|
|
272
|
+
|
|
273
|
+
const manifestFiles = [
|
|
274
|
+
"package.json",
|
|
275
|
+
"foundry.toml",
|
|
276
|
+
"hardhat.config.js",
|
|
277
|
+
"hardhat.config.ts",
|
|
278
|
+
"Cargo.toml",
|
|
279
|
+
"go.mod",
|
|
280
|
+
"pyproject.toml",
|
|
281
|
+
"setup.py",
|
|
282
|
+
"requirements.txt",
|
|
283
|
+
"composer.json",
|
|
284
|
+
"pom.xml",
|
|
285
|
+
"build.gradle",
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
// Walk up from parent directory of file to top-level folder
|
|
289
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
290
|
+
const candidateDir = parts.slice(0, i).join("/");
|
|
291
|
+
const fullCandidate = join(root, candidateDir);
|
|
292
|
+
if (existsSync(fullCandidate)) {
|
|
293
|
+
try {
|
|
294
|
+
const entries = readdirSync(fullCandidate);
|
|
295
|
+
const hasManifest = entries.some(
|
|
296
|
+
(e) => manifestFiles.includes(e) || e.endsWith(".csproj") || e.endsWith(".fsproj")
|
|
297
|
+
);
|
|
298
|
+
if (hasManifest) {
|
|
299
|
+
return candidateDir;
|
|
300
|
+
}
|
|
301
|
+
} catch (_) {}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return ".";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Detects illegal cross-package imports escaping subproject boundaries in monorepos.
|
|
310
|
+
*/
|
|
311
|
+
export function detectCrossPackageBoundaryViolations(changedFiles = [], root = process.cwd(), options = {}) {
|
|
312
|
+
const violations = [];
|
|
313
|
+
if (!changedFiles || changedFiles.length === 0) return violations;
|
|
314
|
+
|
|
315
|
+
const fileContents = options.fileContents || {};
|
|
316
|
+
|
|
317
|
+
// Regex patterns for relative imports across languages
|
|
318
|
+
const jsImportRegex = /(?:import\s+(?:[\w*\s{},]*\s+from\s+)?|export\s+(?:[\w*\s{},]*\s+from\s+)?|require\s*\(\s*|import\s*\(\s*)['"](\.[^'"]+)['"]/g;
|
|
319
|
+
const goImportRegex = /(?:^|\n)\s*(?:import\s+['"](\.[^'"]+)['"]|import\s*\([\s\S]*?['"](\.[^'"]+)['"][\s\S]*?\))/g;
|
|
320
|
+
const rustPathRegex = /#\[path\s*=\s*['"](\.[^'"]+)['"]\]/g;
|
|
321
|
+
const pyRelImportRegex = /(?:^|\n)\s*from\s+(\.{2,}\w*)\s+import/g;
|
|
322
|
+
|
|
323
|
+
for (const file of changedFiles) {
|
|
324
|
+
const posixFile = toPosixPath(file);
|
|
325
|
+
let content = fileContents[file] || fileContents[posixFile];
|
|
326
|
+
|
|
327
|
+
if (!content) {
|
|
328
|
+
const fullPath = join(root, file);
|
|
329
|
+
if (existsSync(fullPath)) {
|
|
330
|
+
try {
|
|
331
|
+
content = readFileSync(fullPath, "utf-8");
|
|
332
|
+
} catch (_) {}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!content || typeof content !== "string") continue;
|
|
337
|
+
|
|
338
|
+
const subproject = findSubprojectRoot(posixFile, root);
|
|
339
|
+
if (subproject === "." && !options.enforceRootBoundary) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const fileDir = posixFile.includes("/") ? posixFile.substring(0, posixFile.lastIndexOf("/")) : ".";
|
|
344
|
+
const ext = posixFile.includes(".") ? posixFile.substring(posixFile.lastIndexOf(".")).toLowerCase() : "";
|
|
345
|
+
const isJsTs = [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".svelte", ".vue"].includes(ext) || !ext;
|
|
346
|
+
const isGo = ext === ".go";
|
|
347
|
+
const isRust = ext === ".rs";
|
|
348
|
+
const isPython = ext === ".py";
|
|
349
|
+
|
|
350
|
+
const lines = content.split("\n");
|
|
351
|
+
|
|
352
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
353
|
+
const line = lines[lineIdx];
|
|
354
|
+
|
|
355
|
+
// 1. JS / TS imports
|
|
356
|
+
if (isJsTs) {
|
|
357
|
+
let match;
|
|
358
|
+
jsImportRegex.lastIndex = 0;
|
|
359
|
+
while ((match = jsImportRegex.exec(line)) !== null) {
|
|
360
|
+
const relTarget = match[1];
|
|
361
|
+
if (relTarget.startsWith("..")) {
|
|
362
|
+
const resolved = toPosixPath(join(fileDir, relTarget));
|
|
363
|
+
const relToSubproject = toPosixPath(relative(subproject, resolved));
|
|
364
|
+
if (relToSubproject.startsWith("..") || resolved === "." || (subproject !== "." && !resolved.startsWith(subproject + "/"))) {
|
|
365
|
+
violations.push({
|
|
366
|
+
file: posixFile,
|
|
367
|
+
subproject,
|
|
368
|
+
importTarget: relTarget,
|
|
369
|
+
resolvedTarget: resolved,
|
|
370
|
+
line: lineIdx + 1,
|
|
371
|
+
language: "javascript/typescript",
|
|
372
|
+
reason: `Cross-Package Boundary Violation: File "${posixFile}" in package "${subproject}" illegally imports "${relTarget}" (resolves to "${resolved}"), escaping its package boundary. Declare a workspace dependency instead of a relative cross-package file import.`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 2. Go relative imports escaping module
|
|
380
|
+
if (isGo) {
|
|
381
|
+
let match;
|
|
382
|
+
goImportRegex.lastIndex = 0;
|
|
383
|
+
while ((match = goImportRegex.exec(line)) !== null) {
|
|
384
|
+
const relTarget = match[1] || match[2];
|
|
385
|
+
if (relTarget && relTarget.startsWith("..")) {
|
|
386
|
+
const resolved = toPosixPath(join(fileDir, relTarget));
|
|
387
|
+
const relToSubproject = toPosixPath(relative(subproject, resolved));
|
|
388
|
+
if (relToSubproject.startsWith("..")) {
|
|
389
|
+
violations.push({
|
|
390
|
+
file: posixFile,
|
|
391
|
+
subproject,
|
|
392
|
+
importTarget: relTarget,
|
|
393
|
+
resolvedTarget: resolved,
|
|
394
|
+
line: lineIdx + 1,
|
|
395
|
+
language: "go",
|
|
396
|
+
reason: `Cross-Module Boundary Violation: Go file "${posixFile}" in module "${subproject}" illegally imports relative path "${relTarget}" escaping module root.`,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 3. Rust path attributes escaping crate
|
|
404
|
+
if (isRust) {
|
|
405
|
+
let match;
|
|
406
|
+
rustPathRegex.lastIndex = 0;
|
|
407
|
+
while ((match = rustPathRegex.exec(line)) !== null) {
|
|
408
|
+
const relTarget = match[1];
|
|
409
|
+
if (relTarget && relTarget.startsWith("..")) {
|
|
410
|
+
const resolved = toPosixPath(join(fileDir, relTarget));
|
|
411
|
+
const relToSubproject = toPosixPath(relative(subproject, resolved));
|
|
412
|
+
if (relToSubproject.startsWith("..")) {
|
|
413
|
+
violations.push({
|
|
414
|
+
file: posixFile,
|
|
415
|
+
subproject,
|
|
416
|
+
importTarget: relTarget,
|
|
417
|
+
resolvedTarget: resolved,
|
|
418
|
+
line: lineIdx + 1,
|
|
419
|
+
language: "rust",
|
|
420
|
+
reason: `Cross-Crate Boundary Violation: Rust file "${posixFile}" in crate "${subproject}" uses path attribute "${relTarget}" escaping crate root.`,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 4. Python relative imports escaping package
|
|
428
|
+
if (isPython) {
|
|
429
|
+
let match;
|
|
430
|
+
pyRelImportRegex.lastIndex = 0;
|
|
431
|
+
while ((match = pyRelImportRegex.exec(line)) !== null) {
|
|
432
|
+
const dots = match[1];
|
|
433
|
+
const dotCount = (dots.match(/\./g) || []).length;
|
|
434
|
+
// Count depth from subproject to fileDir
|
|
435
|
+
const depth = fileDir === subproject ? 0 : fileDir.slice(subproject.length + 1).split("/").length;
|
|
436
|
+
if (dotCount > depth + 1) {
|
|
437
|
+
violations.push({
|
|
438
|
+
file: posixFile,
|
|
439
|
+
subproject,
|
|
440
|
+
importTarget: dots,
|
|
441
|
+
resolvedTarget: "<parent-package>",
|
|
442
|
+
line: lineIdx + 1,
|
|
443
|
+
language: "python",
|
|
444
|
+
reason: `Cross-Package Boundary Violation: Python file "${posixFile}" in package "${subproject}" uses relative import with ${dotCount} dots, escaping package root.`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return violations;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Discovers monorepo packages and detects circular dependencies.
|
|
457
|
+
*/
|
|
458
|
+
export function detectCircularDependencies(root = process.cwd()) {
|
|
459
|
+
const packageMap = new Map(); // name -> { path, deps: [] }
|
|
460
|
+
const searchDirs = ["packages", "apps", "crates", "services", "libs", "modules", "backend", "frontend", "cli", "."];
|
|
461
|
+
|
|
462
|
+
for (const dir of searchDirs) {
|
|
463
|
+
const fullDir = join(root, dir);
|
|
464
|
+
if (!existsSync(fullDir)) continue;
|
|
465
|
+
|
|
466
|
+
const subDirs = dir === "." ? ["."] : [];
|
|
467
|
+
try {
|
|
468
|
+
if (dir !== ".") {
|
|
469
|
+
const entries = readdirSync(fullDir, { withFileTypes: true });
|
|
470
|
+
for (const e of entries) {
|
|
471
|
+
if (e.isDirectory()) subDirs.push(`${dir}/${e.name}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch (_) {}
|
|
475
|
+
|
|
476
|
+
for (const subDir of subDirs) {
|
|
477
|
+
const fullSubDir = join(root, subDir);
|
|
478
|
+
// 1. JS/TS package.json
|
|
479
|
+
const pkgJsonPath = join(fullSubDir, "package.json");
|
|
480
|
+
if (existsSync(pkgJsonPath)) {
|
|
481
|
+
try {
|
|
482
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
483
|
+
if (pkg.name) {
|
|
484
|
+
const allDeps = {
|
|
485
|
+
...(pkg.dependencies || {}),
|
|
486
|
+
...(pkg.devDependencies || {}),
|
|
487
|
+
...(pkg.peerDependencies || {}),
|
|
488
|
+
};
|
|
489
|
+
packageMap.set(pkg.name, {
|
|
490
|
+
name: pkg.name,
|
|
491
|
+
path: subDir,
|
|
492
|
+
type: "npm",
|
|
493
|
+
deps: Object.keys(allDeps),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
} catch (_) {}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 2. Rust Cargo.toml
|
|
500
|
+
const cargoPath = join(fullSubDir, "Cargo.toml");
|
|
501
|
+
if (existsSync(cargoPath)) {
|
|
502
|
+
try {
|
|
503
|
+
const content = readFileSync(cargoPath, "utf-8");
|
|
504
|
+
const nameMatch = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
|
|
505
|
+
if (nameMatch) {
|
|
506
|
+
const crateName = nameMatch[1];
|
|
507
|
+
const deps = [];
|
|
508
|
+
const depMatches = content.matchAll(/([a-zA-Z0-9_-]+)\s*=\s*\{[^}]*?path\s*=\s*["']([^"']+)["']/g);
|
|
509
|
+
for (const dm of depMatches) {
|
|
510
|
+
deps.push(dm[1]);
|
|
511
|
+
}
|
|
512
|
+
packageMap.set(crateName, {
|
|
513
|
+
name: crateName,
|
|
514
|
+
path: subDir,
|
|
515
|
+
type: "cargo",
|
|
516
|
+
deps,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
} catch (_) {}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Filter dependencies to only workspace internal packages
|
|
525
|
+
const knownNames = new Set(packageMap.keys());
|
|
526
|
+
const graph = new Map();
|
|
527
|
+
for (const [name, info] of packageMap.entries()) {
|
|
528
|
+
graph.set(name, info.deps.filter((d) => knownNames.has(d)));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Cycle detection via DFS
|
|
532
|
+
const cycles = [];
|
|
533
|
+
const visited = new Set();
|
|
534
|
+
const recStack = new Set();
|
|
535
|
+
const path = [];
|
|
536
|
+
|
|
537
|
+
function dfs(node) {
|
|
538
|
+
visited.add(node);
|
|
539
|
+
recStack.add(node);
|
|
540
|
+
path.push(node);
|
|
541
|
+
|
|
542
|
+
const neighbors = graph.get(node) || [];
|
|
543
|
+
for (const neighbor of neighbors) {
|
|
544
|
+
if (!visited.has(neighbor)) {
|
|
545
|
+
dfs(neighbor);
|
|
546
|
+
} else if (recStack.has(neighbor)) {
|
|
547
|
+
const cycleStartIndex = path.indexOf(neighbor);
|
|
548
|
+
const cycle = path.slice(cycleStartIndex).concat(neighbor);
|
|
549
|
+
cycles.push(cycle.join(" -> "));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
path.pop();
|
|
554
|
+
recStack.delete(node);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
for (const node of graph.keys()) {
|
|
558
|
+
if (!visited.has(node)) {
|
|
559
|
+
dfs(node);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
hasCycles: cycles.length > 0,
|
|
565
|
+
cycles,
|
|
566
|
+
packages: Array.from(packageMap.values()),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
233
570
|
/**
|
|
234
571
|
* Resolves workspace boundaries in polyglot monorepos by mapping changed files to subprojects.
|
|
235
572
|
*/
|
|
236
|
-
export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd()) {
|
|
573
|
+
export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd(), options = {}) {
|
|
237
574
|
const rootStack = detectPolyglotStack(root);
|
|
575
|
+
const boundaryViolations = detectCrossPackageBoundaryViolations(changedFiles, root, options);
|
|
576
|
+
const circular = detectCircularDependencies(root);
|
|
577
|
+
|
|
238
578
|
if (!changedFiles || changedFiles.length === 0) {
|
|
239
|
-
return {
|
|
579
|
+
return {
|
|
580
|
+
isMonorepo: circular.packages.length > 1,
|
|
581
|
+
globalFallback: false,
|
|
582
|
+
projects: [{ path: ".", ...rootStack }],
|
|
583
|
+
testCmd: rootStack.testCmd,
|
|
584
|
+
buildCmd: rootStack.buildCmd,
|
|
585
|
+
boundaryViolations,
|
|
586
|
+
circularDependencies: circular.cycles,
|
|
587
|
+
};
|
|
240
588
|
}
|
|
241
589
|
|
|
242
590
|
const sharedTriggers = new Set(["openapi.yaml", "openapi.json", "schema.graphql", "docker-compose.yml", "Makefile", "turbo.json", "pnpm-workspace.yaml", "nx.json"]);
|
|
243
591
|
for (const file of changedFiles) {
|
|
244
|
-
const baseName = file.split(
|
|
592
|
+
const baseName = toPosixPath(file).split("/").pop();
|
|
245
593
|
if (sharedTriggers.has(baseName) || file.startsWith(".agent/") || file.startsWith(".github/")) {
|
|
246
|
-
return {
|
|
594
|
+
return {
|
|
595
|
+
isMonorepo: true,
|
|
596
|
+
globalFallback: true,
|
|
597
|
+
projects: [{ path: ".", ...rootStack }],
|
|
598
|
+
testCmd: rootStack.testCmd,
|
|
599
|
+
buildCmd: rootStack.buildCmd,
|
|
600
|
+
boundaryViolations,
|
|
601
|
+
circularDependencies: circular.cycles,
|
|
602
|
+
};
|
|
247
603
|
}
|
|
248
604
|
}
|
|
249
605
|
|
|
250
606
|
const projectMap = new Map();
|
|
251
607
|
for (const file of changedFiles) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (let i = parts.length - 1; i >= 1; i--) {
|
|
256
|
-
const subDir = parts.slice(0, i).join("/");
|
|
608
|
+
const subDir = findSubprojectRoot(file, root);
|
|
609
|
+
if (subDir !== ".") {
|
|
257
610
|
const subStack = detectPolyglotStack(join(root, subDir));
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
611
|
+
if (!projectMap.has(subDir)) {
|
|
612
|
+
projectMap.set(subDir, { path: subDir, ...subStack });
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
if (!projectMap.has(".")) {
|
|
616
|
+
projectMap.set(".", { path: ".", ...rootStack });
|
|
264
617
|
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (!resolved && !projectMap.has(".")) {
|
|
268
|
-
projectMap.set(".", { path: ".", ...rootStack });
|
|
269
618
|
}
|
|
270
619
|
}
|
|
271
620
|
|
|
@@ -278,11 +627,13 @@ export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd()
|
|
|
278
627
|
.map((p) => (p.path === "." ? p.buildCmd : `(cd ${p.path} && ${p.buildCmd})`));
|
|
279
628
|
|
|
280
629
|
return {
|
|
281
|
-
isMonorepo: projects.length > 1,
|
|
630
|
+
isMonorepo: projects.length > 1 || circular.packages.length > 1,
|
|
282
631
|
globalFallback: false,
|
|
283
632
|
projects,
|
|
284
633
|
testCmd: testCmds.join(" && ") || rootStack.testCmd,
|
|
285
634
|
buildCmd: buildCmds.join(" && ") || rootStack.buildCmd,
|
|
635
|
+
boundaryViolations,
|
|
636
|
+
circularDependencies: circular.cycles,
|
|
286
637
|
};
|
|
287
638
|
}
|
|
288
639
|
|
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,
|