jules-orchestrator-kit 0.29.1 → 0.32.0

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.
Files changed (46) hide show
  1. package/README.md +169 -86
  2. package/bin/agentctl.mjs +267 -4
  3. package/bin/mcp-server.mjs +0 -0
  4. package/index.mjs +21 -1
  5. package/package.json +1 -1
  6. package/scripts/stale-base-check.mjs +2 -2
  7. package/src/config.mjs +18 -2
  8. package/src/dag-engine.mjs +76 -0
  9. package/src/engine.mjs +331 -32
  10. package/src/envelope.mjs +32 -2
  11. package/src/execution_envelope.mjs +9 -1
  12. package/src/git.mjs +41 -0
  13. package/src/mcp.mjs +34 -0
  14. package/src/memory.mjs +148 -0
  15. package/src/ops/checkpoint.mjs +167 -0
  16. package/src/ops/command-registry.mjs +274 -0
  17. package/src/ops/doctor-planner.mjs +214 -0
  18. package/src/ops/doctor-registry.mjs +442 -0
  19. package/src/ops/ide-scaffold.mjs +139 -0
  20. package/src/ops/receipts.mjs +171 -0
  21. package/src/ops/swarm-actions.mjs +156 -0
  22. package/src/ops/task-actions.mjs +214 -0
  23. package/src/ops/tdd-generator.mjs +115 -0
  24. package/src/ops/transaction.mjs +278 -0
  25. package/src/prompt-guard.mjs +8 -1
  26. package/src/provider.mjs +164 -8
  27. package/src/remediation.mjs +164 -0
  28. package/src/security.mjs +14 -6
  29. package/src/stack-detector.mjs +15 -5
  30. package/src/state.mjs +71 -7
  31. package/src/task-optimizer.mjs +306 -0
  32. package/src/telemetry.mjs +35 -2
  33. package/src/tui.mjs +2 -6
  34. package/src/ux/capabilities.mjs +180 -0
  35. package/src/ux/diff-viewer.mjs +289 -0
  36. package/src/ux/key-decoder.mjs +281 -0
  37. package/src/ux/layout.mjs +195 -0
  38. package/src/ux/log-viewer.mjs +110 -0
  39. package/src/ux/palette.mjs +179 -0
  40. package/src/ux/queue-model.mjs +206 -0
  41. package/src/ux/renderer.mjs +188 -0
  42. package/src/ux/swarm-model.mjs +146 -0
  43. package/src/ux/terminal-session.mjs +215 -0
  44. package/src/ux/widgets.mjs +323 -0
  45. package/src/webhook.mjs +120 -0
  46. package/src/wizard-task.mjs +6 -0
package/bin/agentctl.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { parseArgs } from "node:util";
4
4
  import { readFileSync, existsSync, readdirSync } from "node:fs";
5
- import { join } from "node:path";
5
+ import { join, resolve } from "node:path";
6
6
  import { loadConfig, resolveRoot, detectStack, bootstrapZeroTestRepo } from "../src/config.mjs";
7
7
  import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
8
8
  import { acquireLock, releaseLock, lockStatus, checkDailyBudget, getQueueDir } from "../src/state.mjs";
@@ -14,7 +14,7 @@ const command = args[0];
14
14
 
15
15
  function printHelp() {
16
16
  console.log(`
17
- 🚀 agentctl v0.29.1 — Universal Agent Orchestrator & Safety Gatekeeper
17
+ 🚀 agentctl v0.32.0 — Universal Agent Orchestrator & Safety Gatekeeper
18
18
 
19
19
  Usage: agentctl <command> [options]
20
20
 
@@ -32,8 +32,16 @@ Commands:
32
32
  dashboard Start local HTTP telemetry and audit dashboard
33
33
  init Scaffold .agent/ config and run onboarding wizard
34
34
  task create Interactively author and scope a Jules task envelope
35
+ task optimize Linter & optimizer for Jules task prompts (--fix, --json)
36
+ test-gen Scaffold & run automated TDD Red-to-Green test cycle (--run)
37
+ mcp init Scaffold IDE integration config (cursor | vscode | claude | all)
38
+ rollback Restore git state & working tree to atomic pre-flight checkpoint
39
+ resume Resume warm session with human response (--response "<text>")
35
40
  status Display queue and system status summary
36
41
  scan Scan codebase for TODO/FIXME task candidates
42
+ hydrate [prompt] Prepend active system learnings and baton-pass state to a prompt
43
+ harvest Harvest failure traces and record/quarantine resolution rules
44
+ learning add Record a system learning rule into .agent/knowledge/
37
45
  version Output agentctl version
38
46
 
39
47
  Options:
@@ -54,7 +62,7 @@ async function main() {
54
62
  }
55
63
 
56
64
  if (command === "version" || command === "--version" || command === "-v") {
57
- console.log("agentctl v0.29.1");
65
+ console.log("agentctl v0.32.0");
58
66
  process.exit(0);
59
67
  }
60
68
 
@@ -391,8 +399,66 @@ async function main() {
391
399
  console.log(` Auto-PR : ${res.plan.flags.autoPr}`);
392
400
  }
393
401
  process.exit(0);
402
+ } else if (subCommand === "optimize") {
403
+ const { values, positionals } = parseArgs({
404
+ args: args.slice(2),
405
+ options: {
406
+ fix: { type: "boolean", short: "f" },
407
+ file: { type: "string" },
408
+ dir: { type: "string", short: "d" },
409
+ json: { type: "boolean", short: "j" },
410
+ "verify-cmd": { type: "string", short: "v" },
411
+ },
412
+ allowPositionals: true,
413
+ });
414
+
415
+ const { scorePromptFalsifiability, optimizeTaskPrompt } = await import("../src/task-optimizer.mjs");
416
+ const targetDir = values.dir ? resolve(values.dir) : root;
417
+ let promptText = positionals.join(" ");
418
+
419
+ if (values.file) {
420
+ if (existsSync(values.file)) {
421
+ promptText = readFileSync(values.file, "utf-8");
422
+ } else {
423
+ console.error(`Error: File '${values.file}' does not exist.`);
424
+ process.exit(1);
425
+ }
426
+ }
427
+
428
+ if (values.fix) {
429
+ const opt = optimizeTaskPrompt(promptText, { rootDir: targetDir, verifyCmd: values["verify-cmd"] });
430
+ if (values.json) {
431
+ console.log(JSON.stringify(opt, null, 2));
432
+ } else {
433
+ console.log(opt.optimizedPrompt);
434
+ }
435
+ process.exit(0);
436
+ }
437
+
438
+ const analysis = scorePromptFalsifiability(promptText, { rootDir: targetDir, verifyCmd: values["verify-cmd"] });
439
+ if (values.json) {
440
+ console.log(JSON.stringify(analysis, null, 2));
441
+ } else {
442
+ console.log(`\n🎯 Task Prompt Falsifiability Analysis`);
443
+ console.log(`--------------------------------------------------`);
444
+ console.log(` Score / Grade : ${analysis.score} / 100 (${analysis.grade})`);
445
+ console.log(` Falsifiable : ${analysis.isFalsifiable ? "✅ YES" : "❌ NO"}`);
446
+ if (analysis.oracle.command) {
447
+ console.log(` Oracle Command : "${analysis.oracle.command}" (${analysis.oracle.autoDetected ? "Auto-detected" : "User-supplied"})`);
448
+ }
449
+ if (analysis.issues.length > 0) {
450
+ console.log(`\n Issues Identified (${analysis.issues.length}):`);
451
+ analysis.issues.forEach((i) => console.log(` - [${i.type}] ${i.message} (-${i.penalty} pts)`));
452
+ }
453
+ if (analysis.suggestions.length > 0) {
454
+ console.log(`\n Suggestions for Improvement:`);
455
+ analysis.suggestions.forEach((s) => console.log(` - ${s}`));
456
+ }
457
+ console.log(`--------------------------------------------------\n`);
458
+ }
459
+ process.exit(analysis.isFalsifiable ? 0 : 1);
394
460
  } else {
395
- console.error(`Unknown task subcommand '${subCommand}'. Supported: agentctl task create`);
461
+ console.error(`Unknown task subcommand '${subCommand}'. Supported: agentctl task create, agentctl task optimize`);
396
462
  process.exit(1);
397
463
  }
398
464
  break;
@@ -427,12 +493,209 @@ async function main() {
427
493
  break;
428
494
  }
429
495
 
496
+ case "rollback": {
497
+ const { restoreCheckpoint } = await import("../src/ops/checkpoint.mjs");
498
+ const targetId = args[1] || "--latest";
499
+ try {
500
+ const res = restoreCheckpoint(targetId, { root });
501
+ console.log(`\n✅ Git Checkpoint Restored Successfully!`);
502
+ console.log(` Session ID : ${res.id}`);
503
+ console.log(` HEAD SHA : ${res.headSha || "N/A"}`);
504
+ console.log(` RestoredAt : ${res.restoredAt}\n`);
505
+ process.exit(0);
506
+ } catch (err) {
507
+ console.error(`❌ Rollback Failed: ${err.message}`);
508
+ process.exit(1);
509
+ }
510
+ break;
511
+ }
512
+
513
+ case "resume": {
514
+ const { createProvider } = await import("../src/provider.mjs");
515
+ const sessionId = args[1];
516
+ const { values } = parseArgs({
517
+ args: args.slice(1),
518
+ options: {
519
+ response: { type: "string", short: "r" },
520
+ "dry-run": { type: "boolean", short: "d" },
521
+ json: { type: "boolean", short: "j" },
522
+ },
523
+ allowPositionals: true,
524
+ });
525
+
526
+ if (!sessionId || sessionId.startsWith("-")) {
527
+ console.error("Error: Session ID is required for agentctl resume <sessionId>.");
528
+ process.exit(1);
529
+ }
530
+
531
+ const responseText = values.response || args.slice(2).join(" ");
532
+ if (!responseText) {
533
+ console.error("Error: --response text is required to resume warm session.");
534
+ process.exit(1);
535
+ }
536
+
537
+ const provider = createProvider(config.provider || "jules", config);
538
+ try {
539
+ const res = await provider.resume(sessionId, responseText, { root, dryRun: values["dry-run"] });
540
+ if (values.json) {
541
+ console.log(JSON.stringify(res, null, 2));
542
+ } else {
543
+ console.log(`\n✅ Warm Session Resumed Successfully!`);
544
+ console.log(` Session ID : ${res.id}`);
545
+ console.log(` Status : ${res.status}\n`);
546
+ }
547
+ process.exit(0);
548
+ } catch (err) {
549
+ console.error(`❌ Resume Failed: ${err.message}`);
550
+ process.exit(1);
551
+ }
552
+ break;
553
+ }
554
+
555
+ case "test-gen": {
556
+ const { scaffoldTddTest, runTddCycle } = await import("../src/ops/tdd-generator.mjs");
557
+ const { values } = parseArgs({
558
+ args: args.slice(1),
559
+ options: {
560
+ title: { type: "string", short: "t" },
561
+ spec: { type: "string", short: "s" },
562
+ run: { type: "boolean", short: "r" },
563
+ json: { type: "boolean", short: "j" },
564
+ },
565
+ allowPositionals: true,
566
+ });
567
+
568
+ const title = values.title || args[1] || "feature-spec";
569
+ const specText = values.spec || args.slice(2).join(" ") || "TDD requirement specification.";
570
+
571
+ try {
572
+ if (values.run) {
573
+ const res = await runTddCycle({ title, spec: specText }, { root });
574
+ if (values.json) {
575
+ console.log(JSON.stringify(res, null, 2));
576
+ } else {
577
+ console.log(`\n🔴 TDD RED Check Verified!`);
578
+ console.log(` Test File : ${res.testFile}`);
579
+ console.log(` Scope Lock : Locked into scope.deny`);
580
+ console.log(` Status : Ready for green implementation dispatch\n`);
581
+ }
582
+ } else {
583
+ const scaffold = scaffoldTddTest({ title, spec: specText }, { root });
584
+ if (values.json) {
585
+ console.log(JSON.stringify(scaffold, null, 2));
586
+ } else {
587
+ console.log(`\n🧪 TDD Test Scaffolded Successfully!`);
588
+ console.log(` File Path : ${scaffold.relativePath}`);
589
+ console.log(` Command : ${scaffold.testCmd}\n`);
590
+ }
591
+ }
592
+ process.exit(0);
593
+ } catch (err) {
594
+ console.error(`❌ TDD Generation Failed: ${err.message}`);
595
+ process.exit(1);
596
+ }
597
+ break;
598
+ }
599
+
430
600
  case "mcp": {
601
+ const subAction = args[1];
602
+ if (subAction === "init") {
603
+ const { scaffoldIdeConfig } = await import("../src/ops/ide-scaffold.mjs");
604
+ const { values } = parseArgs({
605
+ args: args.slice(2),
606
+ options: {
607
+ target: { type: "string", short: "t", default: "all" },
608
+ json: { type: "boolean", short: "j" },
609
+ },
610
+ allowPositionals: true,
611
+ });
612
+
613
+ const target = values.target || args[2] || "all";
614
+ try {
615
+ const res = scaffoldIdeConfig(target, { root });
616
+ if (values.json) {
617
+ console.log(JSON.stringify(res, null, 2));
618
+ } else {
619
+ console.log(`\n🔌 IDE MCP Config Scaffolded Successfully!`);
620
+ console.log(` Target : ${res.target}`);
621
+ for (const item of res.results) {
622
+ console.log(` - ${item.target.toUpperCase()} : ${item.file}`);
623
+ }
624
+ console.log("");
625
+ }
626
+ process.exit(0);
627
+ } catch (err) {
628
+ console.error(`❌ IDE MCP Scaffold Failed: ${err.message}`);
629
+ process.exit(1);
630
+ }
631
+ }
632
+
431
633
  const { startMcpServer } = await import("../src/mcp.mjs");
432
634
  startMcpServer();
433
635
  break;
434
636
  }
435
637
 
638
+ case "hydrate": {
639
+ const { hydratePrompt } = await import("../src/memory.mjs");
640
+ const promptInput = args.slice(1).join(" ") || "";
641
+ const result = hydratePrompt(root, promptInput);
642
+ console.log(result);
643
+ process.exit(0);
644
+ }
645
+
646
+ case "harvest": {
647
+ const { harvestFailure } = await import("../src/memory.mjs");
648
+ const { values } = parseArgs({
649
+ args: args.slice(1),
650
+ options: {
651
+ "exit-code": { type: "string" },
652
+ log: { type: "string" },
653
+ diff: { type: "string" },
654
+ task: { type: "string" },
655
+ agent: { type: "string" },
656
+ json: { type: "boolean", short: "j" },
657
+ },
658
+ allowPositionals: true,
659
+ });
660
+
661
+ const exitCode = parseInt(values["exit-code"] || "4", 10);
662
+ const logPath = values.log || "";
663
+ const diffText = values.diff || "";
664
+ const taskId = values.task || "unknown";
665
+ const agent = values.agent || "jules";
666
+
667
+ const res = harvestFailure(root, { exitCode, logPath, diffText, taskId, agent });
668
+ if (values.json) {
669
+ console.log(JSON.stringify(res, null, 2));
670
+ } else {
671
+ if (res.status === "HARVESTED") {
672
+ console.log(`🌾 Harvested failure candidate: ${res.candidate.trigger}`);
673
+ console.log(` Solution: ${res.candidate.solution}`);
674
+ } else {
675
+ console.log(`⚠️ Harvest rejected: ${res.reason}`);
676
+ }
677
+ }
678
+ process.exit(0);
679
+ }
680
+
681
+ case "learning": {
682
+ const subcmd = args[1];
683
+ if (subcmd === "add") {
684
+ const { recordLearning } = await import("../src/memory.mjs");
685
+ const trigger = args[2];
686
+ const solution = args[3];
687
+ if (!trigger || !solution) {
688
+ console.error('Usage: agentctl learning add "<trigger/symptom>" "<solution>"');
689
+ process.exit(1);
690
+ }
691
+ const res = recordLearning(root, { trigger, solution });
692
+ console.log(`✅ System learning recorded. Total learnings: ${res.count}`);
693
+ process.exit(0);
694
+ }
695
+ console.error('Usage: agentctl learning add "<trigger/symptom>" "<solution>"');
696
+ process.exit(1);
697
+ }
698
+
436
699
  default:
437
700
  console.error(`Unknown command: ${command}`);
438
701
  printHelp();
File without changes
package/index.mjs CHANGED
@@ -61,7 +61,7 @@ export {
61
61
  } from "./src/execution_envelope.mjs";
62
62
  export { checkAssetIntegrity } from "./src/asset_integrity.mjs";
63
63
  export { classifyRiskTier, RISK_TIERS } from "./src/risk.mjs";
64
- export { checkRulesBudget } from "./src/rules_budget.mjs";
64
+ export { recordRemediation, queryRemediations } from "./src/remediation.mjs";
65
65
  export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
66
66
  export { journalIntent, journalDone, reapOrphanedIntents, reapStaleMutexDirs } from "./src/journal.mjs";
67
67
 
@@ -85,4 +85,24 @@ export { planInit, loadPresets, runInitWizard, TIER_PROFILES, BUILTIN_PRESETS }
85
85
  // Guided Task Authoring Subsystem
86
86
  export { planTaskCreate, runTaskCreateWizard, GUARDRAIL_FOOTER } from "./src/wizard-task.mjs";
87
87
 
88
+ // Prompt Falsifiability & Task Optimizer Engine
89
+ export { scorePromptFalsifiability, optimizeTaskPrompt, levenshteinDistance, extractPathTokens } from "./src/task-optimizer.mjs";
90
+
91
+ // Atomic Git Checkpoints & Rollback
92
+ export { createCheckpoint, restoreCheckpoint, listCheckpoints, pruneCheckpoints, CheckpointError } from "./src/ops/checkpoint.mjs";
93
+
94
+ // Webhook & HITL Escalation Bridge
95
+ export { dispatchEscalation, verifySignature, parseWebhookPayload, routeWebhookEvent, createWebhookServer } from "./src/webhook.mjs";
96
+
97
+ // PR Review Evidence Bundler & Dev Server Probe
98
+ export { synthesizePrDescription, probeDevServer } from "./src/engine.mjs";
99
+
100
+ // Automated TDD Red-to-Green Harness
101
+ export { scaffoldTddTest, runTddCycle, TddError } from "./src/ops/tdd-generator.mjs";
102
+
103
+ // IDE Native MCP Scaffolder
104
+ export { scaffoldIdeConfig, IdeScaffoldError } from "./src/ops/ide-scaffold.mjs";
105
+
106
+
107
+
88
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.29.1",
3
+ "version": "0.32.0",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,6 @@ try {
21
21
  console.log("✅ Stale-base predicate check passed.");
22
22
  process.exit(0);
23
23
  } catch (err) {
24
- console.warn(`⚠️ Stale-base check skipped (git base ref not available or shallow clone): ${err.message}`);
25
- process.exit(0);
24
+ console.error(`❌ STALE BASE GATE FAIL: Could not resolve base branch "${baseBranch}": ${err.message}`);
25
+ process.exit(1);
26
26
  }
package/src/config.mjs CHANGED
@@ -196,9 +196,19 @@ export function detectStack(projectRoot = process.cwd()) {
196
196
  return detectPolyglotStack(projectRoot);
197
197
  }
198
198
 
199
- export function resolveVerify(root = process.cwd()) {
199
+ export function resolveVerify(root = process.cwd(), userVerify = {}) {
200
200
  const s = detectStack(root);
201
- return { test: s.testCmd || "", build: s.buildCmd || "" };
201
+ return {
202
+ setup: userVerify.setup ?? s.setupCmd ?? "",
203
+ test: userVerify.test ?? s.testCmd ?? "",
204
+ teardown: userVerify.teardown ?? s.teardownCmd ?? "",
205
+ build: userVerify.build ?? s.buildCmd ?? "",
206
+ server: userVerify.server ? {
207
+ command: userVerify.server.command || "",
208
+ url: userVerify.server.url || "http://localhost:3000",
209
+ timeoutMs: Number(userVerify.server.timeoutMs || 15000),
210
+ } : null,
211
+ };
202
212
  }
203
213
 
204
214
  export const TIER_PRESETS = {
@@ -247,8 +257,11 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
247
257
  }
248
258
  }
249
259
 
260
+ const setupCmd = parsed.setup_cmd || parsed.verify?.setup || "";
250
261
  const testCmd = parsed.test_cmd || parsed.verify?.test || "";
262
+ const teardownCmd = parsed.teardown_cmd || parsed.verify?.teardown || "";
251
263
  const buildCmd = parsed.build_cmd || parsed.verify?.build || "";
264
+ const verifyTimeoutMs = parsed.verify?.timeoutMs ?? parsed.verify?.timeout_ms ?? 60000;
252
265
  const autoVerify = resolveVerify(root);
253
266
 
254
267
  const activeTier = String(process.env.JULES_TIER || parsed.tier || "ultra").toLowerCase();
@@ -275,8 +288,11 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
275
288
  provider: parsed.provider || DEFAULTS.provider,
276
289
  tier: activeTier,
277
290
  verify: {
291
+ setup: setupCmd || autoVerify.setup || "",
278
292
  test: testCmd || autoVerify.test,
293
+ teardown: teardownCmd || autoVerify.teardown || "",
279
294
  build: buildCmd || autoVerify.build,
295
+ timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 60000,
280
296
  },
281
297
  scope: normalizeScope(parsed),
282
298
  limits: {
@@ -330,3 +330,79 @@ export class DagExecutor {
330
330
  });
331
331
  }
332
332
  }
333
+
334
+ /**
335
+ * Global Contract Trigger List:
336
+ * Files matching these patterns are considered global structural contracts.
337
+ * If ANY modified file matches these patterns, selective test execution MUST yield to full test suite execution (returns null).
338
+ */
339
+ export const GLOBAL_CONTRACT_PATTERNS = [
340
+ /^package(-lock)?\.json$/,
341
+ /^pnpm-lock\.yaml$/,
342
+ /^yarn\.lock$/,
343
+ /^tsconfig.*\.json$/,
344
+ /^schema\./,
345
+ /\.d\.ts$/,
346
+ /^\.env/,
347
+ /^\.agent\//,
348
+ /^Cargo\.(toml|lock)$/,
349
+ /^go\.(mod|sum)$/,
350
+ /^composer\.(json|lock)$/,
351
+ ];
352
+
353
+ /**
354
+ * Checks if a relative file path matches global contract triggers.
355
+ */
356
+ export function isGlobalContractFile(filePath = "") {
357
+ if (typeof filePath !== "string") return false;
358
+ const normalized = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
359
+ const baseName = normalized.split("/").pop();
360
+ return GLOBAL_CONTRACT_PATTERNS.some((pattern) => pattern.test(normalized) || pattern.test(baseName));
361
+ }
362
+
363
+ /**
364
+ * Resolves affected test files based on modified files and static import analysis.
365
+ * Returns null if full test suite should be executed (due to global contract change or unknown dependency),
366
+ * or an array of affected test file paths.
367
+ */
368
+ export function resolveAffectedTests(modifiedFiles = [], options = {}) {
369
+ if (!Array.isArray(modifiedFiles) || modifiedFiles.length === 0) {
370
+ return [];
371
+ }
372
+
373
+ // 1. Check Global Contract Trigger List
374
+ for (const file of modifiedFiles) {
375
+ if (isGlobalContractFile(file)) {
376
+ return null; // Force full test suite execution
377
+ }
378
+ }
379
+
380
+ const knownTestFiles = Array.isArray(options.knownTestFiles) ? options.knownTestFiles : [];
381
+ const testSuffixRegex = options.testSuffixRegex || /\.(test|spec)\.(m?[jt]sx?|py|rs|go)$/i;
382
+
383
+ const affected = new Set();
384
+
385
+ for (const file of modifiedFiles) {
386
+ if (typeof file !== "string") continue;
387
+ const normalized = file.replace(/\\/g, "/").replace(/^\.\//, "");
388
+
389
+ // If the modified file itself is a test file, add it directly
390
+ if (testSuffixRegex.test(normalized)) {
391
+ affected.add(normalized);
392
+ continue;
393
+ }
394
+
395
+ // Match known test files that import or reference this file/module
396
+ const baseNameNoExt = normalized.split("/").pop().replace(/\.[^/.]+$/, "");
397
+ for (const testFile of knownTestFiles) {
398
+ if (typeof testFile !== "string") continue;
399
+ const normTest = testFile.replace(/\\/g, "/").replace(/^\.\//, "");
400
+ if (normTest.includes(baseNameNoExt)) {
401
+ affected.add(normTest);
402
+ }
403
+ }
404
+ }
405
+
406
+ return affected.size > 0 ? Array.from(affected) : null;
407
+ }
408
+