jules-orchestrator-kit 0.32.3 → 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 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.32.3`). | `0` |
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
- function printHelp() {
15
+ export const VERSION = "0.32.4";
16
+
17
+ export function printHelp() {
16
18
  console.log(`
17
- 🚀 agentctl v0.32.3 — Universal Agent Orchestrator & Safety Gatekeeper
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.3");
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 { detectPolyglotStack, resolveWorkspaceBoundary, bootstrapZeroTestRepo } from "./src/stack-detector.mjs";
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",
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": "^9.39.5",
74
- "globals": "^17.8.0"
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 { changedFiles, git, resolveBase } from "../src/git.mjs";
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 { detectPolyglotStack, resolveWorkspaceBoundary, bootstrapZeroTestRepo } from "./stack-detector.mjs";
189
-
190
- export { detectPolyglotStack, resolveWorkspaceBoundary, bootstrapZeroTestRepo };
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, "&#124;").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 += `> **⚠️ TILL AGENTER:** Redigera ALDRIG denna fil manuellt. Registrera lärdomar via:\n`;
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 += `| Datum | Agent | Trigger / Symptom | Mandatory Rule / Solution |\n`;
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
+
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
2
- import { join, sep } from "node:path";
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 { isMonorepo: false, globalFallback: false, projects: [{ path: ".", ...rootStack }], testCmd: rootStack.testCmd, buildCmd: rootStack.buildCmd };
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(sep).pop();
564
+ const baseName = toPosixPath(file).split("/").pop();
245
565
  if (sharedTriggers.has(baseName) || file.startsWith(".agent/") || file.startsWith(".github/")) {
246
- return { isMonorepo: true, globalFallback: true, projects: [{ path: ".", ...rootStack }], testCmd: rootStack.testCmd, buildCmd: rootStack.buildCmd };
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 parts = file.split(sep);
253
- let resolved = false;
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 (subStack.stack !== "unknown" && subStack.triggerFile) {
259
- if (!projectMap.has(subDir)) {
260
- projectMap.set(subDir, { path: subDir, ...subStack });
261
- }
262
- resolved = true;
263
- break;
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