panopticon-cli 0.4.33 → 0.5.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 (52) hide show
  1. package/README.md +96 -210
  2. package/dist/{agents-VLK4BMVA.js → agents-E43Y3HNU.js} +5 -5
  3. package/dist/{chunk-ASY7T35E.js → chunk-AAFQANKW.js} +231 -76
  4. package/dist/chunk-AAFQANKW.js.map +1 -0
  5. package/dist/{chunk-KJ2TRXNK.js → chunk-FTCPTHIJ.js} +47 -420
  6. package/dist/chunk-FTCPTHIJ.js.map +1 -0
  7. package/dist/{chunk-PI7Y3PSN.js → chunk-GR6ZZMCX.js} +25 -6
  8. package/dist/chunk-GR6ZZMCX.js.map +1 -0
  9. package/dist/chunk-HJSM6E6U.js +1038 -0
  10. package/dist/chunk-HJSM6E6U.js.map +1 -0
  11. package/dist/{chunk-BKCWRMUX.js → chunk-HZT2AOPN.js} +81 -9
  12. package/dist/chunk-HZT2AOPN.js.map +1 -0
  13. package/dist/{chunk-XFR2DLMR.js → chunk-NTO3EDB3.js} +3 -3
  14. package/dist/{chunk-XFR2DLMR.js.map → chunk-NTO3EDB3.js.map} +1 -1
  15. package/dist/{chunk-RBUO57TC.js → chunk-PPRFKTVC.js} +2 -2
  16. package/dist/chunk-PPRFKTVC.js.map +1 -0
  17. package/dist/{chunk-XKT5MHPT.js → chunk-WQG2TYCB.js} +2 -2
  18. package/dist/cli/index.js +1383 -880
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/dashboard/prompts/work-agent.md +2 -0
  21. package/dist/dashboard/public/assets/{index-UjZq6ykz.css → index-BxpjweAL.css} +1 -1
  22. package/dist/dashboard/public/assets/index-DQHkwvvJ.js +743 -0
  23. package/dist/dashboard/public/index.html +2 -2
  24. package/dist/dashboard/server.js +3593 -2052
  25. package/dist/index.d.ts +10 -1
  26. package/dist/index.js +5 -3
  27. package/dist/index.js.map +1 -1
  28. package/dist/{specialist-context-T3NBMCIE.js → specialist-context-ZC6A4M3I.js} +4 -4
  29. package/dist/{specialist-logs-CVKD3YJ3.js → specialist-logs-KLGJCEUL.js} +4 -4
  30. package/dist/{specialists-TKAP6T6Z.js → specialists-O4HWDJL5.js} +4 -4
  31. package/dist/{traefik-QX4ZV4YG.js → traefik-QN7R5I6V.js} +2 -2
  32. package/dist/{workspace-manager-KLHUCIZV.js → workspace-manager-IE4JL2JP.js} +2 -2
  33. package/package.json +1 -1
  34. package/scripts/stop-hook +7 -0
  35. package/scripts/work-agent-stop-hook +137 -0
  36. package/skills/myn-standards/SKILL.md +351 -0
  37. package/skills/write-spec/SKILL.md +138 -0
  38. package/dist/chunk-7XNJJBH6.js +0 -538
  39. package/dist/chunk-7XNJJBH6.js.map +0 -1
  40. package/dist/chunk-ASY7T35E.js.map +0 -1
  41. package/dist/chunk-BKCWRMUX.js.map +0 -1
  42. package/dist/chunk-KJ2TRXNK.js.map +0 -1
  43. package/dist/chunk-PI7Y3PSN.js.map +0 -1
  44. package/dist/chunk-RBUO57TC.js.map +0 -1
  45. package/dist/dashboard/public/assets/index-kAJqtLDO.js +0 -708
  46. /package/dist/{agents-VLK4BMVA.js.map → agents-E43Y3HNU.js.map} +0 -0
  47. /package/dist/{chunk-XKT5MHPT.js.map → chunk-WQG2TYCB.js.map} +0 -0
  48. /package/dist/{specialist-context-T3NBMCIE.js.map → specialist-context-ZC6A4M3I.js.map} +0 -0
  49. /package/dist/{specialist-logs-CVKD3YJ3.js.map → specialist-logs-KLGJCEUL.js.map} +0 -0
  50. /package/dist/{specialists-TKAP6T6Z.js.map → specialists-O4HWDJL5.js.map} +0 -0
  51. /package/dist/{traefik-QX4ZV4YG.js.map → traefik-QN7R5I6V.js.map} +0 -0
  52. /package/dist/{workspace-manager-KLHUCIZV.js.map → workspace-manager-IE4JL2JP.js.map} +0 -0
@@ -1,25 +1,29 @@
1
1
  import {
2
+ capturePaneAsync,
2
3
  checkHook,
4
+ confirmDelivery,
3
5
  getModelId,
4
6
  init_hooks,
5
7
  init_tmux,
6
8
  init_work_type_router,
7
9
  popFromHook,
8
10
  pushToHook,
9
- sendKeysAsync
10
- } from "./chunk-KJ2TRXNK.js";
11
+ sendKeysAsync,
12
+ waitForClaudePrompt
13
+ } from "./chunk-FTCPTHIJ.js";
11
14
  import {
12
15
  init_pipeline_notifier,
13
16
  notifyPipeline
14
17
  } from "./chunk-JQBV3Q2W.js";
15
18
  import {
19
+ clearCredentialFileAuth,
16
20
  getProviderEnv,
17
21
  getProviderForModel,
18
22
  init_providers,
19
23
  init_settings,
20
24
  loadSettings,
21
25
  setupCredentialFileAuth
22
- } from "./chunk-7XNJJBH6.js";
26
+ } from "./chunk-HJSM6E6U.js";
23
27
  import {
24
28
  init_projects,
25
29
  projects_exports
@@ -393,6 +397,40 @@ import { homedir as homedir2 } from "os";
393
397
  import { exec } from "child_process";
394
398
  import { promisify } from "util";
395
399
  import { randomUUID } from "crypto";
400
+ async function resolveWorkspaceGitInfo(workspace, taskBranch) {
401
+ const gitDirs = [];
402
+ let branch = taskBranch || "unknown";
403
+ if (!workspace || workspace === "unknown") {
404
+ return { gitDirs, branch, isPolyrepo: false };
405
+ }
406
+ if (existsSync3(join4(workspace, ".git"))) {
407
+ gitDirs.push(workspace);
408
+ } else {
409
+ try {
410
+ const entries = readdirSync2(workspace, { withFileTypes: true });
411
+ for (const entry of entries) {
412
+ if (entry.isDirectory() && existsSync3(join4(workspace, entry.name, ".git"))) {
413
+ gitDirs.push(join4(workspace, entry.name));
414
+ }
415
+ }
416
+ } catch {
417
+ }
418
+ }
419
+ if (branch === "unknown" && gitDirs.length > 0) {
420
+ try {
421
+ const { stdout } = await execAsync(
422
+ `cd "${gitDirs[0]}" && git branch --show-current`,
423
+ { encoding: "utf-8", timeout: 5e3 }
424
+ );
425
+ const detected = stdout.trim();
426
+ if (detected) {
427
+ branch = detected;
428
+ }
429
+ } catch {
430
+ }
431
+ }
432
+ return { gitDirs, branch, isPolyrepo: gitDirs.length > 1 };
433
+ }
396
434
  function getProviderEnvForModel(model) {
397
435
  const provider = getProviderForModel(model);
398
436
  if (provider.name === "anthropic") return {};
@@ -584,9 +622,9 @@ function recordWake(name, sessionId) {
584
622
  }
585
623
  async function spawnEphemeralSpecialist(projectKey, specialistType, task) {
586
624
  ensureProjectSpecialistDir(projectKey, specialistType);
587
- const { loadContextDigest } = await import("./specialist-context-T3NBMCIE.js");
625
+ const { loadContextDigest } = await import("./specialist-context-ZC6A4M3I.js");
588
626
  const contextDigest = loadContextDigest(projectKey, specialistType);
589
- const { createRunLog: createRunLog2 } = await import("./specialist-logs-CVKD3YJ3.js");
627
+ const { createRunLog: createRunLog2 } = await import("./specialist-logs-KLGJCEUL.js");
590
628
  const { runId, filePath: logFilePath } = createRunLog2(
591
629
  projectKey,
592
630
  specialistType,
@@ -611,6 +649,8 @@ async function spawnEphemeralSpecialist(projectKey, specialistType, task) {
611
649
  const providerConfig = getProviderForModel(model);
612
650
  if (providerConfig.authType === "credential-file") {
613
651
  setupCredentialFileAuth(providerConfig, cwd);
652
+ } else {
653
+ clearCredentialFileAuth(cwd);
614
654
  }
615
655
  const permissionFlags = specialistType === "merge-agent" ? "--dangerously-skip-permissions --permission-mode bypassPermissions" : "--dangerously-skip-permissions";
616
656
  const agentDir = join4(homedir2(), ".panopticon", "agents", tmuxSession);
@@ -633,7 +673,7 @@ echo "## Specialist completed task"
633
673
  `tmux new-session -d -s "${tmuxSession}"${envFlags} "bash '${launcherScript}'"`,
634
674
  { encoding: "utf-8" }
635
675
  );
636
- const { saveAgentRuntimeState } = await import("./agents-VLK4BMVA.js");
676
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
637
677
  saveAgentRuntimeState(tmuxSession, {
638
678
  state: "active",
639
679
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -718,14 +758,23 @@ Update status via API:
718
758
  - If tests pass: POST to /api/workspaces/${task.issueId}/review-status with {"testStatus":"passed"}
719
759
  - If tests fail: POST with {"testStatus":"failed","testNotes":"..."}`;
720
760
  break;
721
- case "merge-agent":
761
+ case "merge-agent": {
762
+ const bInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);
763
+ if (bInfo.isPolyrepo) {
764
+ prompt += `This is a POLYREPO project with ${bInfo.gitDirs.length} repos: ${bInfo.gitDirs.map((d) => basename2(d)).join(", ")}.
765
+ You must merge each repo separately.
766
+
767
+ `;
768
+ }
722
769
  prompt += `Your task:
723
770
  1. Fetch the latest main branch
724
- 2. Attempt to merge ${task.branch} into main
771
+ 2. Attempt to merge ${bInfo.branch} into main
725
772
  3. Resolve conflicts intelligently if needed
726
773
  4. Run tests to verify merge is clean
727
- 5. Complete merge if tests pass`;
774
+ 5. Complete merge if tests pass
775
+ 6. NEVER use git push --force`;
728
776
  break;
777
+ }
729
778
  }
730
779
  prompt += `
731
780
 
@@ -818,7 +867,7 @@ async function terminateSpecialist(projectKey, specialistType) {
818
867
  console.error(`[specialist] Failed to kill tmux session ${tmuxSession}:`, error);
819
868
  }
820
869
  if (metadata.currentRun) {
821
- const { finalizeRunLog: finalizeRunLog2 } = await import("./specialist-logs-CVKD3YJ3.js");
870
+ const { finalizeRunLog: finalizeRunLog2 } = await import("./specialist-logs-KLGJCEUL.js");
822
871
  try {
823
872
  finalizeRunLog2(projectKey, specialistType, metadata.currentRun, {
824
873
  status: metadata.lastRunStatus || "incomplete",
@@ -831,19 +880,19 @@ async function terminateSpecialist(projectKey, specialistType) {
831
880
  }
832
881
  const key = `${projectKey}-${specialistType}`;
833
882
  gracePeriodStates.delete(key);
834
- const { saveAgentRuntimeState } = await import("./agents-VLK4BMVA.js");
883
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
835
884
  saveAgentRuntimeState(tmuxSession, {
836
885
  state: "suspended",
837
886
  lastActivity: (/* @__PURE__ */ new Date()).toISOString()
838
887
  });
839
- const { scheduleDigestGeneration } = await import("./specialist-context-T3NBMCIE.js");
888
+ const { scheduleDigestGeneration } = await import("./specialist-context-ZC6A4M3I.js");
840
889
  scheduleDigestGeneration(projectKey, specialistType);
841
890
  scheduleLogCleanup(projectKey, specialistType);
842
891
  }
843
892
  function scheduleLogCleanup(projectKey, specialistType) {
844
893
  Promise.resolve().then(async () => {
845
894
  try {
846
- const { cleanupOldLogs: cleanupOldLogs2 } = await import("./specialist-logs-CVKD3YJ3.js");
895
+ const { cleanupOldLogs: cleanupOldLogs2 } = await import("./specialist-logs-KLGJCEUL.js");
847
896
  const { getSpecialistRetention } = await import("./projects-JEIVIYC6.js");
848
897
  const retention = getSpecialistRetention(projectKey);
849
898
  const deleted = cleanupOldLogs2(projectKey, specialistType, { maxDays: retention.max_days, maxRuns: retention.max_runs });
@@ -1022,7 +1071,7 @@ async function getSpecialistStatus(name, projectKey) {
1022
1071
  const sessionId = getSessionId(name);
1023
1072
  const running = await isRunning(name, projectKey);
1024
1073
  const contextTokens = countContextTokens(name);
1025
- const { getAgentRuntimeState } = await import("./agents-VLK4BMVA.js");
1074
+ const { getAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1026
1075
  const tmuxSession = getTmuxSessionName(name, projectKey);
1027
1076
  const runtimeState = getAgentRuntimeState(tmuxSession);
1028
1077
  let state;
@@ -1100,6 +1149,8 @@ Say: "I am the ${name} specialist, ready and waiting for tasks."`;
1100
1149
  const providerCfg = getProviderForModel(model);
1101
1150
  if (providerCfg.authType === "credential-file") {
1102
1151
  setupCredentialFileAuth(providerCfg, cwd);
1152
+ } else {
1153
+ clearCredentialFileAuth(cwd);
1103
1154
  }
1104
1155
  const agentDir = join4(homedir2(), ".panopticon", "agents", tmuxSession);
1105
1156
  await execAsync(`mkdir -p "${agentDir}"`, { encoding: "utf-8" });
@@ -1162,9 +1213,7 @@ async function resetSpecialist(name) {
1162
1213
  const tmuxSession = getTmuxSessionName(name);
1163
1214
  try {
1164
1215
  await execAsync(`tmux send-keys -t "${tmuxSession}" C-c`, { encoding: "utf-8" });
1165
- await new Promise((resolve) => setTimeout(resolve, 200));
1166
- await sendKeysAsync(tmuxSession, "cd ~");
1167
- await new Promise((resolve) => setTimeout(resolve, 200));
1216
+ await new Promise((resolve) => setTimeout(resolve, 500));
1168
1217
  await execAsync(`tmux send-keys -t "${tmuxSession}" C-u`, { encoding: "utf-8" });
1169
1218
  await new Promise((resolve) => setTimeout(resolve, 100));
1170
1219
  } catch (error) {
@@ -1200,6 +1249,8 @@ async function wakeSpecialist(name, taskPrompt, options = {}) {
1200
1249
  const provCfg = getProviderForModel(model);
1201
1250
  if (provCfg.authType === "credential-file") {
1202
1251
  setupCredentialFileAuth(provCfg, cwd);
1252
+ } else {
1253
+ clearCredentialFileAuth(cwd);
1203
1254
  }
1204
1255
  const permissionFlags = name === "merge-agent" ? "--dangerously-skip-permissions --permission-mode bypassPermissions" : "--dangerously-skip-permissions";
1205
1256
  let claudeCmd;
@@ -1215,7 +1266,10 @@ async function wakeSpecialist(name, taskPrompt, options = {}) {
1215
1266
  { encoding: "utf-8" }
1216
1267
  );
1217
1268
  if (waitForReady) {
1218
- await new Promise((resolve) => setTimeout(resolve, 3e3));
1269
+ const ready = await waitForClaudePrompt(tmuxSession, 15e3);
1270
+ if (!ready) {
1271
+ console.warn(`[specialist] ${name}: prompt not detected within 15s, proceeding anyway`);
1272
+ }
1219
1273
  }
1220
1274
  } catch (error) {
1221
1275
  const msg = error instanceof Error ? error.message : String(error);
@@ -1228,21 +1282,43 @@ async function wakeSpecialist(name, taskPrompt, options = {}) {
1228
1282
  }
1229
1283
  }
1230
1284
  await resetSpecialist(name);
1285
+ const promptReady = await waitForClaudePrompt(tmuxSession, wasAlreadyRunning ? 5e3 : 15e3);
1286
+ if (!promptReady) {
1287
+ console.warn(`[specialist] ${name}: prompt not detected after reset, proceeding anyway`);
1288
+ }
1231
1289
  try {
1232
1290
  const isLargePrompt = taskPrompt.length > 500 || taskPrompt.includes("\n");
1291
+ let messageToSend;
1233
1292
  if (isLargePrompt) {
1234
1293
  if (!existsSync3(TASKS_DIR)) {
1235
1294
  mkdirSync2(TASKS_DIR, { recursive: true });
1236
1295
  }
1237
1296
  const taskFile = join4(TASKS_DIR, `${name}-${Date.now()}.md`);
1238
1297
  writeFileSync(taskFile, taskPrompt, "utf-8");
1239
- const shortMessage = `Read and execute the task in: ${taskFile}`;
1240
- await sendKeysAsync(tmuxSession, shortMessage);
1298
+ messageToSend = `Read and execute the task in: ${taskFile}`;
1241
1299
  } else {
1242
- await sendKeysAsync(tmuxSession, taskPrompt);
1300
+ messageToSend = taskPrompt;
1301
+ }
1302
+ const outputBefore = await capturePaneAsync(tmuxSession, 50);
1303
+ await sendKeysAsync(tmuxSession, messageToSend);
1304
+ const delivered = await confirmDelivery(tmuxSession, outputBefore, 1e4);
1305
+ if (!delivered) {
1306
+ console.warn(`[specialist] ${name}: no activity detected after task send, retrying...`);
1307
+ const retryBefore = await capturePaneAsync(tmuxSession, 50);
1308
+ await sendKeysAsync(tmuxSession, messageToSend);
1309
+ const retryDelivered = await confirmDelivery(tmuxSession, retryBefore, 1e4);
1310
+ if (!retryDelivered) {
1311
+ return {
1312
+ success: false,
1313
+ message: `Task message not received by specialist ${name} after retry`,
1314
+ tmuxSession,
1315
+ wasAlreadyRunning,
1316
+ error: "delivery_failed"
1317
+ };
1318
+ }
1243
1319
  }
1244
1320
  recordWake(name, sessionId || void 0);
1245
- const { saveAgentRuntimeState } = await import("./agents-VLK4BMVA.js");
1321
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1246
1322
  saveAgentRuntimeState(tmuxSession, {
1247
1323
  state: "active",
1248
1324
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1270,46 +1346,65 @@ async function wakeSpecialistWithTask(name, task) {
1270
1346
  const apiUrl = process.env.DASHBOARD_URL || `http://localhost:${apiPort}`;
1271
1347
  let prompt;
1272
1348
  switch (name) {
1273
- case "merge-agent":
1349
+ case "merge-agent": {
1350
+ const mergeWorkspace = task.workspace || "unknown";
1351
+ const mergeInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);
1352
+ const mergeBranch = mergeInfo.branch;
1353
+ const mergeRepoInstructions = mergeInfo.isPolyrepo ? `
1354
+ IMPORTANT: This is a POLYREPO project. There are ${mergeInfo.gitDirs.length} separate git repositories to merge:
1355
+ ${mergeInfo.gitDirs.map((d, i) => `${i + 1}. ${basename2(d)}: ${d}`).join("\n")}
1356
+
1357
+ The workspace root is NOT a git repo. You must cd into each subdirectory to run git commands.
1358
+ You MUST complete the merge for ALL repos.
1359
+ ` : "";
1274
1360
  prompt = `New merge task for ${task.issueId}:
1275
1361
 
1276
- Branch: ${task.branch || "unknown"}
1277
- Workspace: ${task.workspace || "unknown"}
1362
+ Branch: ${mergeBranch}
1363
+ Workspace: ${mergeWorkspace}
1364
+ ${mergeInfo.isPolyrepo ? `Polyrepo: git repos in subdirectories: ${mergeInfo.gitDirs.map((d) => basename2(d)).join(", ")}` : ""}
1278
1365
  ${task.prUrl ? `PR URL: ${task.prUrl}` : ""}
1279
-
1280
- Your task:
1281
- 1. Fetch the latest main branch
1282
- 2. Attempt to merge ${task.branch} into main
1283
- 3. If conflicts arise, resolve them intelligently based on context
1284
- 4. Run the test suite to verify the merge is clean
1285
- 5. If tests pass, complete the merge and push
1286
- 6. If tests fail, analyze the failures and either fix them or report back
1287
-
1288
- When done, provide feedback on:
1289
- - Any conflicts encountered and how you resolved them
1290
- - Test results
1291
- - Any patterns you notice that future agents should be aware of
1292
-
1293
- Use the send-feedback-to-agent skill to report findings back to the issue agent.`;
1366
+ ${mergeRepoInstructions}
1367
+ For ${mergeInfo.isPolyrepo ? "EACH repo" : "the repo"}, perform these steps:
1368
+
1369
+ PHASE 1 \u2014 SYNC & BASELINE (before merge):
1370
+ 1. ${mergeInfo.isPolyrepo ? "cd into the repo directory" : `cd ${mergeWorkspace}`}
1371
+ 2. git checkout main
1372
+ 3. git fetch origin main
1373
+ 4. Sync local main with origin/main:
1374
+ Run: git rev-list --left-right --count main...origin/main
1375
+ If REMOTE_AHEAD > 0: git rebase origin/main
1376
+ If rebase conflicts: abort and report failure.
1377
+ 5. Run tests on main to establish a baseline. Record BASELINE_PASS and BASELINE_FAIL.
1378
+
1379
+ PHASE 2 \u2014 MERGE:
1380
+ 6. git merge ${mergeBranch} --no-edit
1381
+ 7. If conflicts: resolve them intelligently, then git add and git commit
1382
+ 8. If clean merge: the merge commit is auto-created (or fast-forward)
1383
+
1384
+ PHASE 3 \u2014 VERIFY:
1385
+ 9. Run tests again. Record MERGE_PASS and MERGE_FAIL.
1386
+
1387
+ PHASE 4 \u2014 DECIDE:
1388
+ 10. Compare results:
1389
+ - If MERGE_FAIL > BASELINE_FAIL (NEW test failures): ROLLBACK with git reset --hard ORIG_HEAD
1390
+ - If MERGE_FAIL <= BASELINE_FAIL (no new failures): PUSH with git push origin main
1391
+ - Pre-existing failures on main are NOT a reason to rollback
1392
+
1393
+ PHASE 5 \u2014 REPORT:
1394
+ 11. Call the Panopticon API to report results:
1395
+ curl -s -X POST ${apiUrl}/api/specialists/done \\
1396
+ -H "Content-Type: application/json" \\
1397
+ -d '{"specialist":"merge","issueId":"${task.issueId}","status":"passed|failed","notes":"<summary>"}'
1398
+
1399
+ CRITICAL: You MUST call the /api/specialists/done endpoint whether you succeed or fail.
1400
+ CRITICAL: NEVER use git push --force.
1401
+ CRITICAL: Do NOT delete the feature branch.`;
1294
1402
  break;
1403
+ }
1295
1404
  case "review-agent": {
1296
1405
  const workspace = task.workspace || "unknown";
1297
- let gitDirs = [];
1298
- if (workspace !== "unknown") {
1299
- if (existsSync3(join4(workspace, ".git"))) {
1300
- gitDirs = [workspace];
1301
- } else {
1302
- try {
1303
- const entries = readdirSync2(workspace, { withFileTypes: true });
1304
- for (const entry of entries) {
1305
- if (entry.isDirectory() && existsSync3(join4(workspace, entry.name, ".git"))) {
1306
- gitDirs.push(join4(workspace, entry.name));
1307
- }
1308
- }
1309
- } catch {
1310
- }
1311
- }
1312
- }
1406
+ const reviewGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);
1407
+ const gitDirs = reviewGitInfo.gitDirs;
1313
1408
  const gitDir = gitDirs[0] || workspace;
1314
1409
  let staleBranch = false;
1315
1410
  if (workspace !== "unknown" && gitDirs.length > 0) {
@@ -1332,7 +1427,7 @@ Use the send-feedback-to-agent skill to report findings back to the issue agent.
1332
1427
  });
1333
1428
  console.log(`[specialist] review-agent: auto-passed ${task.issueId} (stale branch)`);
1334
1429
  const tmuxSession = getTmuxSessionName("review-agent");
1335
- const { saveAgentRuntimeState } = await import("./agents-VLK4BMVA.js");
1430
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1336
1431
  saveAgentRuntimeState(tmuxSession, {
1337
1432
  state: "idle",
1338
1433
  lastActivity: (/* @__PURE__ */ new Date()).toISOString()
@@ -1433,14 +1528,62 @@ curl -s -X POST ${apiUrl}/api/specialists/test-agent/queue -H "Content-Type: app
1433
1528
  \u26A0\uFE0F VERIFICATION: After running each curl, confirm you see valid JSON output. If you get an error, report it.`;
1434
1529
  break;
1435
1530
  }
1436
- case "test-agent":
1531
+ case "test-agent": {
1532
+ const testWorkspace = task.workspace || "unknown";
1533
+ const testGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);
1534
+ const testIsPolyrepo = testGitInfo.isPolyrepo;
1535
+ const { extractTeamPrefix, findProjectByTeam } = await import("./projects-JEIVIYC6.js");
1536
+ const testTeamPrefix = extractTeamPrefix(task.issueId);
1537
+ const testProjectConfig = testTeamPrefix ? findProjectByTeam(testTeamPrefix) : null;
1538
+ const testConfigs = testProjectConfig?.tests;
1539
+ let testCommands = "";
1540
+ let baselineCommands = "";
1541
+ const featureName = task.issueId.toLowerCase();
1542
+ const mainWorkspacePath = testWorkspace.replace(/workspaces\/feature-[^/]+/, "workspaces/main");
1543
+ const projectRootPath = testProjectConfig?.path || testWorkspace.replace(/\/workspaces\/.*/, "");
1544
+ if (testConfigs && Object.keys(testConfigs).length > 0) {
1545
+ const testEntries = Object.entries(testConfigs);
1546
+ const testSuites = [];
1547
+ const baselineSuites = [];
1548
+ for (const [name2, cfg] of testEntries) {
1549
+ const testDir = testIsPolyrepo ? `${testWorkspace}/${cfg.path}` : cfg.path === "." ? testWorkspace : `${testWorkspace}/${cfg.path}`;
1550
+ const baseDir = testIsPolyrepo ? `${mainWorkspacePath}/${cfg.path}` : cfg.path === "." ? mainWorkspacePath : `${mainWorkspacePath}/${cfg.path}`;
1551
+ const fallbackDir = cfg.path === "." ? projectRootPath : `${projectRootPath}/${cfg.path}`;
1552
+ testSuites.push(`echo "\\n=== Test suite: ${name2} (${cfg.type}) ===" && cd "${testDir}" && ${cfg.command} 2>&1; echo "EXIT_CODE_${name2}: $?"`);
1553
+ baselineSuites.push(`echo "\\n=== Baseline: ${name2} (${cfg.type}) ===" && cd "${baseDir}" 2>/dev/null && ${cfg.command} 2>&1 || (cd "${fallbackDir}" 2>/dev/null && ${cfg.command} 2>&1) || echo "BASELINE_SKIP_${name2}: could not run baseline"; echo "EXIT_CODE_${name2}: $?"`);
1554
+ }
1555
+ testCommands = testSuites.map((cmd, i) => `# Suite ${i + 1}
1556
+ ${cmd}`).join("\n");
1557
+ baselineCommands = baselineSuites.map((cmd, i) => `# Suite ${i + 1}
1558
+ ${cmd}`).join("\n");
1559
+ } else if (testIsPolyrepo) {
1560
+ const testSuites = [];
1561
+ const baselineSuites = [];
1562
+ for (const gitDir of testGitInfo.gitDirs) {
1563
+ const repoName = basename2(gitDir);
1564
+ testSuites.push(`echo "\\n=== ${repoName} ===" && cd "${gitDir}" && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo "No test runner found"; fi; echo "EXIT_CODE_${repoName}: $?"`);
1565
+ const baseDir = `${mainWorkspacePath}/${repoName}`;
1566
+ baselineSuites.push(`echo "\\n=== Baseline: ${repoName} ===" && cd "${baseDir}" 2>/dev/null && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo "No test runner found"; fi; echo "EXIT_CODE_${repoName}: $?"`);
1567
+ }
1568
+ testCommands = testSuites.join("\n");
1569
+ baselineCommands = baselineSuites.join("\n");
1570
+ } else {
1571
+ testCommands = `cd "${testWorkspace}" && npm test 2>&1; echo "EXIT_CODE: $?"`;
1572
+ baselineCommands = `cd "${mainWorkspacePath}" 2>/dev/null && npm test 2>&1 || (cd "${projectRootPath}" && npm test 2>&1); echo "EXIT_CODE: $?"`;
1573
+ }
1574
+ const testConfigSummary = testConfigs ? Object.entries(testConfigs).map(([name2, cfg]) => `- **${name2}** (${cfg.type}): \`${cfg.command}\` in \`${cfg.path}/\``).join("\n") : testIsPolyrepo ? testGitInfo.gitDirs.map((d) => `- **${basename2(d)}**: auto-detected`).join("\n") : "- Single test suite at workspace root";
1437
1575
  prompt = `New test task for ${task.issueId}:
1438
1576
 
1439
1577
  Branch: ${task.branch || "unknown"}
1440
- Workspace: ${task.workspace || "unknown"}
1578
+ Workspace: ${testWorkspace}
1579
+ ${testIsPolyrepo ? `Polyrepo: git repos in subdirectories: ${testGitInfo.gitDirs.map((d) => basename2(d)).join(", ")}` : ""}
1580
+
1581
+ ## Test Suites
1582
+
1583
+ ${testConfigSummary}
1441
1584
 
1442
1585
  Your task:
1443
- 1. Run the full test suite \u2014 redirect output to file, read only summaries
1586
+ 1. Run ALL test suites \u2014 redirect output to file, read only summaries
1444
1587
  2. If ALL pass, skip baseline and report PASS
1445
1588
  3. If failures, run baseline on main and compare
1446
1589
  4. Only fail for NEW regressions (not pre-existing)
@@ -1454,32 +1597,43 @@ Raw test output from large suites (1000+ tests) WILL fill your context and cause
1454
1597
  ## CRITICAL: Bash Timeout for Test Commands
1455
1598
 
1456
1599
  **ALWAYS use timeout: 300000 (5 minutes) when running test commands.**
1600
+ For Maven/Spring Boot tests, use timeout: 600000 (10 minutes) \u2014 they take longer.
1457
1601
 
1458
1602
  ## Step 1: Run Feature Branch Tests
1459
1603
 
1604
+ ${testIsPolyrepo || testConfigs && Object.keys(testConfigs).length > 1 ? `**Run ALL test suites** \u2014 each suite is a separate repo/runner. Redirect ALL output to one file.` : ""}
1605
+
1460
1606
  \`\`\`bash
1461
- cd ${task.workspace || "unknown"} && npm test 2>&1 > /tmp/test-feature.txt; echo "EXIT_CODE: $?"
1462
- # Use timeout: 300000 for this command
1463
- tail -20 /tmp/test-feature.txt
1607
+ (
1608
+ ${testCommands}
1609
+ ) > /tmp/test-feature.txt 2>&1
1610
+ # Use timeout: ${testConfigs && Object.values(testConfigs).some((c) => c.type === "maven") ? "600000" : "300000"} for this command
1611
+ echo "--- Feature test output tail ---"
1612
+ tail -40 /tmp/test-feature.txt
1613
+ grep "EXIT_CODE" /tmp/test-feature.txt
1464
1614
  \`\`\`
1465
1615
 
1466
1616
  ## Step 2: Check Results
1467
1617
 
1468
- - If ALL tests pass (exit code 0) \u2192 skip baseline, go to "Update Status"
1469
- - If failures exist \u2192 continue to Step 3
1618
+ - If ALL exit codes are 0 \u2192 skip baseline, go to "Update Status"
1619
+ - If any failures \u2192 continue to Step 3
1470
1620
 
1471
1621
  ## Step 3: Baseline Comparison (ONLY if failures found)
1472
1622
 
1473
1623
  \`\`\`bash
1474
- cd ${task.context?.workspace ? task.context.workspace.replace(/workspaces\/feature-[^/]+/, "") : "unknown"} && npm test 2>&1 > /tmp/test-main.txt; echo "EXIT_CODE: $?"
1475
- # Use timeout: 300000 for this command
1476
- tail -20 /tmp/test-main.txt
1624
+ (
1625
+ ${baselineCommands}
1626
+ ) > /tmp/test-main.txt 2>&1
1627
+ # Use timeout: ${testConfigs && Object.values(testConfigs).some((c) => c.type === "maven") ? "600000" : "300000"} for this command
1628
+ echo "--- Baseline test output tail ---"
1629
+ tail -40 /tmp/test-main.txt
1630
+ grep "EXIT_CODE" /tmp/test-main.txt
1477
1631
  \`\`\`
1478
1632
 
1479
1633
  Then compare failures (targeted, NOT full output):
1480
1634
  \`\`\`bash
1481
- grep -E "FAIL|\u2717|Error|failed" /tmp/test-feature.txt | head -30
1482
- grep -E "FAIL|\u2717|Error|failed" /tmp/test-main.txt | head -30
1635
+ grep -E "FAIL|\u2717|Error|failed|BUILD FAILURE" /tmp/test-feature.txt | head -30
1636
+ grep -E "FAIL|\u2717|Error|failed|BUILD FAILURE" /tmp/test-main.txt | head -30
1483
1637
  \`\`\`
1484
1638
 
1485
1639
  Tests that fail on BOTH = pre-existing (don't block). Tests that fail ONLY on feature = NEW regression (block).
@@ -1493,12 +1647,12 @@ You MUST execute the appropriate curl command and verify it succeeds. Do NOT jus
1493
1647
 
1494
1648
  If NO new regressions (tests PASS):
1495
1649
  \`\`\`bash
1496
- curl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H "Content-Type: application/json" -d '{"testStatus":"passed","testNotes":"[summary including pre-existing failures if any]"}' | jq .
1650
+ curl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H "Content-Type: application/json" -d '{"testStatus":"passed","testNotes":"[summary including pre-existing failures if any, and which suites were tested]"}' | jq .
1497
1651
  \`\`\`
1498
1652
 
1499
1653
  If NEW regressions found (tests FAIL):
1500
1654
  \`\`\`bash
1501
- curl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H "Content-Type: application/json" -d '{"testStatus":"failed","testNotes":"[describe NEW failures only]"}' | jq .
1655
+ curl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H "Content-Type: application/json" -d '{"testStatus":"failed","testNotes":"[describe NEW failures only \u2014 specify which suite/repo]"}' | jq .
1502
1656
  \`\`\`
1503
1657
  Then use send-feedback-to-agent skill to notify issue agent of NEW failures only.
1504
1658
 
@@ -1508,6 +1662,7 @@ Then use send-feedback-to-agent skill to notify issue agent of NEW failures only
1508
1662
 
1509
1663
  IMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.`;
1510
1664
  break;
1665
+ }
1511
1666
  default:
1512
1667
  prompt = `Task for ${task.issueId}: Please process this task and report findings.`;
1513
1668
  }
@@ -1516,7 +1671,7 @@ IMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.
1516
1671
  async function wakeSpecialistOrQueue(name, task, options = {}) {
1517
1672
  const { priority = "normal", source = "handoff" } = options;
1518
1673
  const running = await isRunning(name);
1519
- const { getAgentRuntimeState } = await import("./agents-VLK4BMVA.js");
1674
+ const { getAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1520
1675
  const tmuxSession = getTmuxSessionName(name);
1521
1676
  const runtimeState = getAgentRuntimeState(tmuxSession);
1522
1677
  const idle = runtimeState?.state === "idle" || runtimeState?.state === "suspended";
@@ -1547,7 +1702,7 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
1547
1702
  };
1548
1703
  }
1549
1704
  }
1550
- const { saveAgentRuntimeState } = await import("./agents-VLK4BMVA.js");
1705
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1551
1706
  saveAgentRuntimeState(tmuxSession, {
1552
1707
  state: "active",
1553
1708
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1667,7 +1822,7 @@ async function sendFeedbackToAgent(feedback) {
1667
1822
  return false;
1668
1823
  }
1669
1824
  try {
1670
- const { messageAgent } = await import("./agents-VLK4BMVA.js");
1825
+ const { messageAgent } = await import("./agents-E43Y3HNU.js");
1671
1826
  const msg = `SPECIALIST FEEDBACK: ${fromSpecialist} reported ${feedback.feedbackType.toUpperCase()} for ${toIssueId}.
1672
1827
  Read and address: ${fileResult.relativePath}`;
1673
1828
  await messageAgent(agentSession, msg);
@@ -2161,4 +2316,4 @@ export {
2161
2316
  getFeedbackStats,
2162
2317
  init_specialists
2163
2318
  };
2164
- //# sourceMappingURL=chunk-ASY7T35E.js.map
2319
+ //# sourceMappingURL=chunk-AAFQANKW.js.map