jules-orchestrator-kit 0.32.2 → 0.32.4
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 +1 -1
- package/bin/agentctl.mjs +5 -3
- package/index.mjs +10 -1
- package/package.json +3 -3
- package/scripts/jules-merge-swarm.mjs +1 -1
- package/src/config.mjs +17 -3
- package/src/engine.mjs +2 -2
- package/src/memory.mjs +2 -2
- package/src/security.mjs +55 -1
- package/src/stack-detector.mjs +344 -21
package/README.md
CHANGED
|
@@ -388,7 +388,7 @@ Native stdio server exposing task dispatch, gate verification, and risk auditing
|
|
|
388
388
|
| `clean` | `agentctl clean` | Prunes stale git worktrees, lockfiles, and temporary ledgers. | `0` (Clean) |
|
|
389
389
|
| `mcp` | `agentctl mcp` | Starts stdio Model Context Protocol (MCP) server for tool integration. | `0` / Stdio stream |
|
|
390
390
|
| `mcp init` | `agentctl mcp init [--target cursor\|vscode\|claude\|all]` | 1-click scaffolding for Cursor (`.cursor/mcp.json`), VS Code tasks (`tasks.json`), and Claude Desktop. | `0` (Scaffolded) |
|
|
391
|
-
| `version` | `agentctl version` | Outputs orchestrator kit semantic version (`v0.
|
|
391
|
+
| `version` | `agentctl version` | Outputs orchestrator kit semantic version (`v0.32.4`). | `0` |
|
|
392
392
|
|
|
393
393
|
<br/>
|
|
394
394
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -12,9 +12,11 @@ import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
|
|
|
12
12
|
const args = process.argv.slice(2);
|
|
13
13
|
const command = args[0];
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
export const VERSION = "0.32.4";
|
|
16
|
+
|
|
17
|
+
export function printHelp() {
|
|
16
18
|
console.log(`
|
|
17
|
-
🚀 agentctl v0.32.
|
|
19
|
+
🚀 agentctl v0.32.4 — Universal Agent Orchestrator & Safety Gatekeeper
|
|
18
20
|
|
|
19
21
|
Usage: agentctl <command> [options]
|
|
20
22
|
|
|
@@ -63,7 +65,7 @@ async function main() {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
66
|
-
console.log("agentctl v0.32.
|
|
68
|
+
console.log("agentctl v0.32.4");
|
|
67
69
|
process.exit(0);
|
|
68
70
|
}
|
|
69
71
|
|
package/index.mjs
CHANGED
|
@@ -13,6 +13,8 @@ export {
|
|
|
13
13
|
matchesGlob,
|
|
14
14
|
checkScope,
|
|
15
15
|
scanDiff,
|
|
16
|
+
checkEdgeRuntimeImports,
|
|
17
|
+
checkCrossPackageImports,
|
|
16
18
|
} from "./src/security.mjs";
|
|
17
19
|
export { sanitizeUntrustedData, buildAgentEnvelope } from "./src/prompt-guard.mjs";
|
|
18
20
|
export { isolateMcpStdout, writeMcpFrame } from "./src/mcp.mjs";
|
|
@@ -29,7 +31,14 @@ export {
|
|
|
29
31
|
ProviderSchemaError,
|
|
30
32
|
parseRetryAfter,
|
|
31
33
|
} from "./src/provider.mjs";
|
|
32
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
detectPolyglotStack,
|
|
36
|
+
resolveWorkspaceBoundary,
|
|
37
|
+
bootstrapZeroTestRepo,
|
|
38
|
+
findSubprojectRoot,
|
|
39
|
+
detectCrossPackageBoundaryViolations,
|
|
40
|
+
detectCircularDependencies,
|
|
41
|
+
} from "./src/stack-detector.mjs";
|
|
33
42
|
export {
|
|
34
43
|
appendLedger,
|
|
35
44
|
readLedger,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.4",
|
|
4
4
|
"description": "Orchestration kit for running Google Jules autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"author": "FullThrottle83",
|
|
71
71
|
"license": "MIT",
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"eslint": "^
|
|
74
|
-
"globals": "^17.
|
|
73
|
+
"eslint": "^10.8.1",
|
|
74
|
+
"globals": "^17.11.0"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -10,7 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { classifyRiskTier, RISK_TIERS } from "../src/risk.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { git, resolveBase } from "../src/git.mjs";
|
|
14
14
|
import { normalizePath } from "../src/config.mjs";
|
|
15
15
|
|
|
16
16
|
export const EXIT = Object.freeze({
|
package/src/config.mjs
CHANGED
|
@@ -185,9 +185,23 @@ export function detectPackageManager(root = process.cwd(), pkg = {}) {
|
|
|
185
185
|
return "npm";
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
import {
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
import {
|
|
189
|
+
detectPolyglotStack,
|
|
190
|
+
resolveWorkspaceBoundary,
|
|
191
|
+
bootstrapZeroTestRepo,
|
|
192
|
+
findSubprojectRoot,
|
|
193
|
+
detectCrossPackageBoundaryViolations,
|
|
194
|
+
detectCircularDependencies,
|
|
195
|
+
} from "./stack-detector.mjs";
|
|
196
|
+
|
|
197
|
+
export {
|
|
198
|
+
detectPolyglotStack,
|
|
199
|
+
resolveWorkspaceBoundary,
|
|
200
|
+
bootstrapZeroTestRepo,
|
|
201
|
+
findSubprojectRoot,
|
|
202
|
+
detectCrossPackageBoundaryViolations,
|
|
203
|
+
detectCircularDependencies,
|
|
204
|
+
};
|
|
191
205
|
|
|
192
206
|
/**
|
|
193
207
|
* Autodetects verification test/build commands across 24+ polyglot tech stacks.
|
package/src/engine.mjs
CHANGED
|
@@ -187,8 +187,8 @@ export async function gate(opts = {}) {
|
|
|
187
187
|
return { ok: false, code: 5, phases };
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
// Phase 3: Diff Secret Scanner
|
|
191
|
-
const secretResult = scanDiff(diffStr);
|
|
190
|
+
// Phase 3: Diff Secret Scanner & Security Checks
|
|
191
|
+
const secretResult = scanDiff(diffStr, { root });
|
|
192
192
|
phases.push({ phase: "secrets", ok: secretResult.ok, findings: secretResult.findings });
|
|
193
193
|
appendTelemetry(root, "gate_phase", { phase: "secrets", ok: secretResult.ok });
|
|
194
194
|
if (progressBus && progressToken) {
|
package/src/memory.mjs
CHANGED
|
@@ -61,9 +61,9 @@ export function recordLearning(root = process.cwd(), learning = {}) {
|
|
|
61
61
|
const sanitize = (str) => str.replace(/\|/g, "|").replace(/\r?\n/g, "<br>");
|
|
62
62
|
let md = `<!-- 🚫 AUTO-GENERATED. AGENTS: DO NOT EDIT THIS FILE DIRECTLY. -->\n`;
|
|
63
63
|
md += `# 🧠 System Learnings & Platform Quirks\n\n`;
|
|
64
|
-
md += `> **⚠️
|
|
64
|
+
md += `> **⚠️ TO AGENTS:** NEVER edit this file manually. Record learnings via:\n`;
|
|
65
65
|
md += `> \`agentctl learning add "<trigger>" "<solution>"\`\n\n`;
|
|
66
|
-
md += `|
|
|
66
|
+
md += `| Date | Agent | Trigger / Symptom | Mandatory Rule / Solution |\n`;
|
|
67
67
|
md += `| :--- | :--- | :--- | :--- |\n`;
|
|
68
68
|
|
|
69
69
|
db.forEach((l) => {
|
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
|
+
|
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.
|
|
@@ -230,42 +230,363 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
|
|
|
230
230
|
return { ...container, stack: "unknown", testCmd: "", buildCmd: "", triggerFile: null };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Normalizes file paths to POSIX slashes.
|
|
235
|
+
*/
|
|
236
|
+
function toPosixPath(p = "") {
|
|
237
|
+
return p.replace(/\\/g, "/");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Finds the nearest subproject root directory for a given relative file path.
|
|
242
|
+
*/
|
|
243
|
+
export function findSubprojectRoot(fileRelPath = "", root = process.cwd()) {
|
|
244
|
+
const normalized = toPosixPath(fileRelPath);
|
|
245
|
+
const parts = normalized.split("/");
|
|
246
|
+
if (parts.length <= 1) return ".";
|
|
247
|
+
|
|
248
|
+
const manifestFiles = [
|
|
249
|
+
"package.json",
|
|
250
|
+
"Cargo.toml",
|
|
251
|
+
"go.mod",
|
|
252
|
+
"pyproject.toml",
|
|
253
|
+
"setup.py",
|
|
254
|
+
"requirements.txt",
|
|
255
|
+
"composer.json",
|
|
256
|
+
"pom.xml",
|
|
257
|
+
"build.gradle",
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
// Walk up from parent directory of file to top-level folder
|
|
261
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
262
|
+
const candidateDir = parts.slice(0, i).join("/");
|
|
263
|
+
const fullCandidate = join(root, candidateDir);
|
|
264
|
+
if (existsSync(fullCandidate)) {
|
|
265
|
+
try {
|
|
266
|
+
const entries = readdirSync(fullCandidate);
|
|
267
|
+
const hasManifest = entries.some(
|
|
268
|
+
(e) => manifestFiles.includes(e) || e.endsWith(".csproj") || e.endsWith(".fsproj")
|
|
269
|
+
);
|
|
270
|
+
if (hasManifest) {
|
|
271
|
+
return candidateDir;
|
|
272
|
+
}
|
|
273
|
+
} catch (_) {}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return ".";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Detects illegal cross-package imports escaping subproject boundaries in monorepos.
|
|
282
|
+
*/
|
|
283
|
+
export function detectCrossPackageBoundaryViolations(changedFiles = [], root = process.cwd(), options = {}) {
|
|
284
|
+
const violations = [];
|
|
285
|
+
if (!changedFiles || changedFiles.length === 0) return violations;
|
|
286
|
+
|
|
287
|
+
const fileContents = options.fileContents || {};
|
|
288
|
+
|
|
289
|
+
// Regex patterns for relative imports across languages
|
|
290
|
+
const jsImportRegex = /(?:import\s+(?:[\w*\s{},]*\s+from\s+)?|export\s+(?:[\w*\s{},]*\s+from\s+)?|require\s*\(\s*|import\s*\(\s*)['"](\.[^'"]+)['"]/g;
|
|
291
|
+
const goImportRegex = /(?:^|\n)\s*(?:import\s+['"](\.[^'"]+)['"]|import\s*\([\s\S]*?['"](\.[^'"]+)['"][\s\S]*?\))/g;
|
|
292
|
+
const rustPathRegex = /#\[path\s*=\s*['"](\.[^'"]+)['"]\]/g;
|
|
293
|
+
const pyRelImportRegex = /(?:^|\n)\s*from\s+(\.{2,}\w*)\s+import/g;
|
|
294
|
+
|
|
295
|
+
for (const file of changedFiles) {
|
|
296
|
+
const posixFile = toPosixPath(file);
|
|
297
|
+
let content = fileContents[file] || fileContents[posixFile];
|
|
298
|
+
|
|
299
|
+
if (!content) {
|
|
300
|
+
const fullPath = join(root, file);
|
|
301
|
+
if (existsSync(fullPath)) {
|
|
302
|
+
try {
|
|
303
|
+
content = readFileSync(fullPath, "utf-8");
|
|
304
|
+
} catch (_) {}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!content || typeof content !== "string") continue;
|
|
309
|
+
|
|
310
|
+
const subproject = findSubprojectRoot(posixFile, root);
|
|
311
|
+
if (subproject === "." && !options.enforceRootBoundary) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const fileDir = posixFile.includes("/") ? posixFile.substring(0, posixFile.lastIndexOf("/")) : ".";
|
|
316
|
+
const ext = posixFile.includes(".") ? posixFile.substring(posixFile.lastIndexOf(".")).toLowerCase() : "";
|
|
317
|
+
const isJsTs = [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".svelte", ".vue"].includes(ext) || !ext;
|
|
318
|
+
const isGo = ext === ".go";
|
|
319
|
+
const isRust = ext === ".rs";
|
|
320
|
+
const isPython = ext === ".py";
|
|
321
|
+
|
|
322
|
+
const lines = content.split("\n");
|
|
323
|
+
|
|
324
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
325
|
+
const line = lines[lineIdx];
|
|
326
|
+
|
|
327
|
+
// 1. JS / TS imports
|
|
328
|
+
if (isJsTs) {
|
|
329
|
+
let match;
|
|
330
|
+
jsImportRegex.lastIndex = 0;
|
|
331
|
+
while ((match = jsImportRegex.exec(line)) !== null) {
|
|
332
|
+
const relTarget = match[1];
|
|
333
|
+
if (relTarget.startsWith("..")) {
|
|
334
|
+
const resolved = toPosixPath(join(fileDir, relTarget));
|
|
335
|
+
const relToSubproject = toPosixPath(relative(subproject, resolved));
|
|
336
|
+
if (relToSubproject.startsWith("..") || resolved === "." || (subproject !== "." && !resolved.startsWith(subproject + "/"))) {
|
|
337
|
+
violations.push({
|
|
338
|
+
file: posixFile,
|
|
339
|
+
subproject,
|
|
340
|
+
importTarget: relTarget,
|
|
341
|
+
resolvedTarget: resolved,
|
|
342
|
+
line: lineIdx + 1,
|
|
343
|
+
language: "javascript/typescript",
|
|
344
|
+
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.`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. Go relative imports escaping module
|
|
352
|
+
if (isGo) {
|
|
353
|
+
let match;
|
|
354
|
+
goImportRegex.lastIndex = 0;
|
|
355
|
+
while ((match = goImportRegex.exec(line)) !== null) {
|
|
356
|
+
const relTarget = match[1] || match[2];
|
|
357
|
+
if (relTarget && relTarget.startsWith("..")) {
|
|
358
|
+
const resolved = toPosixPath(join(fileDir, relTarget));
|
|
359
|
+
const relToSubproject = toPosixPath(relative(subproject, resolved));
|
|
360
|
+
if (relToSubproject.startsWith("..")) {
|
|
361
|
+
violations.push({
|
|
362
|
+
file: posixFile,
|
|
363
|
+
subproject,
|
|
364
|
+
importTarget: relTarget,
|
|
365
|
+
resolvedTarget: resolved,
|
|
366
|
+
line: lineIdx + 1,
|
|
367
|
+
language: "go",
|
|
368
|
+
reason: `Cross-Module Boundary Violation: Go file "${posixFile}" in module "${subproject}" illegally imports relative path "${relTarget}" escaping module root.`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 3. Rust path attributes escaping crate
|
|
376
|
+
if (isRust) {
|
|
377
|
+
let match;
|
|
378
|
+
rustPathRegex.lastIndex = 0;
|
|
379
|
+
while ((match = rustPathRegex.exec(line)) !== null) {
|
|
380
|
+
const relTarget = match[1];
|
|
381
|
+
if (relTarget && relTarget.startsWith("..")) {
|
|
382
|
+
const resolved = toPosixPath(join(fileDir, relTarget));
|
|
383
|
+
const relToSubproject = toPosixPath(relative(subproject, resolved));
|
|
384
|
+
if (relToSubproject.startsWith("..")) {
|
|
385
|
+
violations.push({
|
|
386
|
+
file: posixFile,
|
|
387
|
+
subproject,
|
|
388
|
+
importTarget: relTarget,
|
|
389
|
+
resolvedTarget: resolved,
|
|
390
|
+
line: lineIdx + 1,
|
|
391
|
+
language: "rust",
|
|
392
|
+
reason: `Cross-Crate Boundary Violation: Rust file "${posixFile}" in crate "${subproject}" uses path attribute "${relTarget}" escaping crate root.`,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 4. Python relative imports escaping package
|
|
400
|
+
if (isPython) {
|
|
401
|
+
let match;
|
|
402
|
+
pyRelImportRegex.lastIndex = 0;
|
|
403
|
+
while ((match = pyRelImportRegex.exec(line)) !== null) {
|
|
404
|
+
const dots = match[1];
|
|
405
|
+
const dotCount = (dots.match(/\./g) || []).length;
|
|
406
|
+
// Count depth from subproject to fileDir
|
|
407
|
+
const depth = fileDir === subproject ? 0 : fileDir.slice(subproject.length + 1).split("/").length;
|
|
408
|
+
if (dotCount > depth + 1) {
|
|
409
|
+
violations.push({
|
|
410
|
+
file: posixFile,
|
|
411
|
+
subproject,
|
|
412
|
+
importTarget: dots,
|
|
413
|
+
resolvedTarget: "<parent-package>",
|
|
414
|
+
line: lineIdx + 1,
|
|
415
|
+
language: "python",
|
|
416
|
+
reason: `Cross-Package Boundary Violation: Python file "${posixFile}" in package "${subproject}" uses relative import with ${dotCount} dots, escaping package root.`,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return violations;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Discovers monorepo packages and detects circular dependencies.
|
|
429
|
+
*/
|
|
430
|
+
export function detectCircularDependencies(root = process.cwd()) {
|
|
431
|
+
const packageMap = new Map(); // name -> { path, deps: [] }
|
|
432
|
+
const searchDirs = ["packages", "apps", "crates", "services", "libs", "modules", "backend", "frontend", "cli", "."];
|
|
433
|
+
|
|
434
|
+
for (const dir of searchDirs) {
|
|
435
|
+
const fullDir = join(root, dir);
|
|
436
|
+
if (!existsSync(fullDir)) continue;
|
|
437
|
+
|
|
438
|
+
const subDirs = dir === "." ? ["."] : [];
|
|
439
|
+
try {
|
|
440
|
+
if (dir !== ".") {
|
|
441
|
+
const entries = readdirSync(fullDir, { withFileTypes: true });
|
|
442
|
+
for (const e of entries) {
|
|
443
|
+
if (e.isDirectory()) subDirs.push(`${dir}/${e.name}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} catch (_) {}
|
|
447
|
+
|
|
448
|
+
for (const subDir of subDirs) {
|
|
449
|
+
const fullSubDir = join(root, subDir);
|
|
450
|
+
// 1. JS/TS package.json
|
|
451
|
+
const pkgJsonPath = join(fullSubDir, "package.json");
|
|
452
|
+
if (existsSync(pkgJsonPath)) {
|
|
453
|
+
try {
|
|
454
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
455
|
+
if (pkg.name) {
|
|
456
|
+
const allDeps = {
|
|
457
|
+
...(pkg.dependencies || {}),
|
|
458
|
+
...(pkg.devDependencies || {}),
|
|
459
|
+
...(pkg.peerDependencies || {}),
|
|
460
|
+
};
|
|
461
|
+
packageMap.set(pkg.name, {
|
|
462
|
+
name: pkg.name,
|
|
463
|
+
path: subDir,
|
|
464
|
+
type: "npm",
|
|
465
|
+
deps: Object.keys(allDeps),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
} catch (_) {}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 2. Rust Cargo.toml
|
|
472
|
+
const cargoPath = join(fullSubDir, "Cargo.toml");
|
|
473
|
+
if (existsSync(cargoPath)) {
|
|
474
|
+
try {
|
|
475
|
+
const content = readFileSync(cargoPath, "utf-8");
|
|
476
|
+
const nameMatch = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
|
|
477
|
+
if (nameMatch) {
|
|
478
|
+
const crateName = nameMatch[1];
|
|
479
|
+
const deps = [];
|
|
480
|
+
const depMatches = content.matchAll(/([a-zA-Z0-9_-]+)\s*=\s*\{[^}]*?path\s*=\s*["']([^"']+)["']/g);
|
|
481
|
+
for (const dm of depMatches) {
|
|
482
|
+
deps.push(dm[1]);
|
|
483
|
+
}
|
|
484
|
+
packageMap.set(crateName, {
|
|
485
|
+
name: crateName,
|
|
486
|
+
path: subDir,
|
|
487
|
+
type: "cargo",
|
|
488
|
+
deps,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
} catch (_) {}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Filter dependencies to only workspace internal packages
|
|
497
|
+
const knownNames = new Set(packageMap.keys());
|
|
498
|
+
const graph = new Map();
|
|
499
|
+
for (const [name, info] of packageMap.entries()) {
|
|
500
|
+
graph.set(name, info.deps.filter((d) => knownNames.has(d)));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Cycle detection via DFS
|
|
504
|
+
const cycles = [];
|
|
505
|
+
const visited = new Set();
|
|
506
|
+
const recStack = new Set();
|
|
507
|
+
const path = [];
|
|
508
|
+
|
|
509
|
+
function dfs(node) {
|
|
510
|
+
visited.add(node);
|
|
511
|
+
recStack.add(node);
|
|
512
|
+
path.push(node);
|
|
513
|
+
|
|
514
|
+
const neighbors = graph.get(node) || [];
|
|
515
|
+
for (const neighbor of neighbors) {
|
|
516
|
+
if (!visited.has(neighbor)) {
|
|
517
|
+
dfs(neighbor);
|
|
518
|
+
} else if (recStack.has(neighbor)) {
|
|
519
|
+
const cycleStartIndex = path.indexOf(neighbor);
|
|
520
|
+
const cycle = path.slice(cycleStartIndex).concat(neighbor);
|
|
521
|
+
cycles.push(cycle.join(" -> "));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
path.pop();
|
|
526
|
+
recStack.delete(node);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
for (const node of graph.keys()) {
|
|
530
|
+
if (!visited.has(node)) {
|
|
531
|
+
dfs(node);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
hasCycles: cycles.length > 0,
|
|
537
|
+
cycles,
|
|
538
|
+
packages: Array.from(packageMap.values()),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
233
542
|
/**
|
|
234
543
|
* Resolves workspace boundaries in polyglot monorepos by mapping changed files to subprojects.
|
|
235
544
|
*/
|
|
236
|
-
export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd()) {
|
|
545
|
+
export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd(), options = {}) {
|
|
237
546
|
const rootStack = detectPolyglotStack(root);
|
|
547
|
+
const boundaryViolations = detectCrossPackageBoundaryViolations(changedFiles, root, options);
|
|
548
|
+
const circular = detectCircularDependencies(root);
|
|
549
|
+
|
|
238
550
|
if (!changedFiles || changedFiles.length === 0) {
|
|
239
|
-
return {
|
|
551
|
+
return {
|
|
552
|
+
isMonorepo: circular.packages.length > 1,
|
|
553
|
+
globalFallback: false,
|
|
554
|
+
projects: [{ path: ".", ...rootStack }],
|
|
555
|
+
testCmd: rootStack.testCmd,
|
|
556
|
+
buildCmd: rootStack.buildCmd,
|
|
557
|
+
boundaryViolations,
|
|
558
|
+
circularDependencies: circular.cycles,
|
|
559
|
+
};
|
|
240
560
|
}
|
|
241
561
|
|
|
242
562
|
const sharedTriggers = new Set(["openapi.yaml", "openapi.json", "schema.graphql", "docker-compose.yml", "Makefile", "turbo.json", "pnpm-workspace.yaml", "nx.json"]);
|
|
243
563
|
for (const file of changedFiles) {
|
|
244
|
-
const baseName = file.split(
|
|
564
|
+
const baseName = toPosixPath(file).split("/").pop();
|
|
245
565
|
if (sharedTriggers.has(baseName) || file.startsWith(".agent/") || file.startsWith(".github/")) {
|
|
246
|
-
return {
|
|
566
|
+
return {
|
|
567
|
+
isMonorepo: true,
|
|
568
|
+
globalFallback: true,
|
|
569
|
+
projects: [{ path: ".", ...rootStack }],
|
|
570
|
+
testCmd: rootStack.testCmd,
|
|
571
|
+
buildCmd: rootStack.buildCmd,
|
|
572
|
+
boundaryViolations,
|
|
573
|
+
circularDependencies: circular.cycles,
|
|
574
|
+
};
|
|
247
575
|
}
|
|
248
576
|
}
|
|
249
577
|
|
|
250
578
|
const projectMap = new Map();
|
|
251
579
|
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("/");
|
|
580
|
+
const subDir = findSubprojectRoot(file, root);
|
|
581
|
+
if (subDir !== ".") {
|
|
257
582
|
const subStack = detectPolyglotStack(join(root, subDir));
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
583
|
+
if (!projectMap.has(subDir)) {
|
|
584
|
+
projectMap.set(subDir, { path: subDir, ...subStack });
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
if (!projectMap.has(".")) {
|
|
588
|
+
projectMap.set(".", { path: ".", ...rootStack });
|
|
264
589
|
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (!resolved && !projectMap.has(".")) {
|
|
268
|
-
projectMap.set(".", { path: ".", ...rootStack });
|
|
269
590
|
}
|
|
270
591
|
}
|
|
271
592
|
|
|
@@ -278,11 +599,13 @@ export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd()
|
|
|
278
599
|
.map((p) => (p.path === "." ? p.buildCmd : `(cd ${p.path} && ${p.buildCmd})`));
|
|
279
600
|
|
|
280
601
|
return {
|
|
281
|
-
isMonorepo: projects.length > 1,
|
|
602
|
+
isMonorepo: projects.length > 1 || circular.packages.length > 1,
|
|
282
603
|
globalFallback: false,
|
|
283
604
|
projects,
|
|
284
605
|
testCmd: testCmds.join(" && ") || rootStack.testCmd,
|
|
285
606
|
buildCmd: buildCmds.join(" && ") || rootStack.buildCmd,
|
|
607
|
+
boundaryViolations,
|
|
608
|
+
circularDependencies: circular.cycles,
|
|
286
609
|
};
|
|
287
610
|
}
|
|
288
611
|
|