panopticon-cli 0.4.32 → 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 (142) hide show
  1. package/README.md +96 -210
  2. package/dist/{agents-BDFHF4T3.js → agents-E43Y3HNU.js} +10 -7
  3. package/dist/chunk-7SN4L4PH.js +150 -0
  4. package/dist/chunk-7SN4L4PH.js.map +1 -0
  5. package/dist/{chunk-2NIAOCIC.js → chunk-AAFQANKW.js} +358 -97
  6. package/dist/chunk-AAFQANKW.js.map +1 -0
  7. package/dist/chunk-AQXETQHW.js +113 -0
  8. package/dist/chunk-AQXETQHW.js.map +1 -0
  9. package/dist/chunk-B3PF6JPQ.js +212 -0
  10. package/dist/chunk-B3PF6JPQ.js.map +1 -0
  11. package/dist/chunk-CFCUOV3Q.js +669 -0
  12. package/dist/chunk-CFCUOV3Q.js.map +1 -0
  13. package/dist/chunk-CWELWPWQ.js +32 -0
  14. package/dist/chunk-CWELWPWQ.js.map +1 -0
  15. package/dist/chunk-DI7ABPNQ.js +352 -0
  16. package/dist/chunk-DI7ABPNQ.js.map +1 -0
  17. package/dist/{chunk-VU4FLXV5.js → chunk-FQ66DECN.js} +31 -4
  18. package/dist/chunk-FQ66DECN.js.map +1 -0
  19. package/dist/{chunk-VIWUCJ4V.js → chunk-FTCPTHIJ.js} +57 -432
  20. package/dist/chunk-FTCPTHIJ.js.map +1 -0
  21. package/dist/{review-status-GWQYY77L.js → chunk-GFP3PIPB.js} +14 -7
  22. package/dist/chunk-GFP3PIPB.js.map +1 -0
  23. package/dist/chunk-GR6ZZMCX.js +816 -0
  24. package/dist/chunk-GR6ZZMCX.js.map +1 -0
  25. package/dist/chunk-HJSM6E6U.js +1038 -0
  26. package/dist/chunk-HJSM6E6U.js.map +1 -0
  27. package/dist/{chunk-XP2DXWYP.js → chunk-HZT2AOPN.js} +164 -39
  28. package/dist/chunk-HZT2AOPN.js.map +1 -0
  29. package/dist/chunk-JQBV3Q2W.js +29 -0
  30. package/dist/chunk-JQBV3Q2W.js.map +1 -0
  31. package/dist/{chunk-BWGFN44T.js → chunk-JT4O4YVM.js} +28 -16
  32. package/dist/chunk-JT4O4YVM.js.map +1 -0
  33. package/dist/chunk-NTO3EDB3.js +600 -0
  34. package/dist/chunk-NTO3EDB3.js.map +1 -0
  35. package/dist/{chunk-JY7R7V4G.js → chunk-OMNXYPXC.js} +2 -2
  36. package/dist/chunk-OMNXYPXC.js.map +1 -0
  37. package/dist/chunk-PELXV435.js +215 -0
  38. package/dist/chunk-PELXV435.js.map +1 -0
  39. package/dist/chunk-PPRFKTVC.js +154 -0
  40. package/dist/chunk-PPRFKTVC.js.map +1 -0
  41. package/dist/chunk-WQG2TYCB.js +677 -0
  42. package/dist/chunk-WQG2TYCB.js.map +1 -0
  43. package/dist/{chunk-HCTJFIJJ.js → chunk-YLPSQAM2.js} +2 -2
  44. package/dist/{chunk-HCTJFIJJ.js.map → chunk-YLPSQAM2.js.map} +1 -1
  45. package/dist/{chunk-6HXKTOD7.js → chunk-ZTFNYOC7.js} +53 -38
  46. package/dist/chunk-ZTFNYOC7.js.map +1 -0
  47. package/dist/cli/index.js +5103 -3165
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/{config-BOAMSKTF.js → config-4CJNUE3O.js} +7 -3
  50. package/dist/dashboard/prompts/merge-agent.md +217 -0
  51. package/dist/dashboard/prompts/review-agent.md +409 -0
  52. package/dist/dashboard/prompts/sync-main.md +84 -0
  53. package/dist/dashboard/prompts/test-agent.md +283 -0
  54. package/dist/dashboard/prompts/work-agent.md +249 -0
  55. package/dist/dashboard/public/assets/index-BxpjweAL.css +32 -0
  56. package/dist/dashboard/public/assets/index-DQHkwvvJ.js +743 -0
  57. package/dist/dashboard/public/index.html +2 -2
  58. package/dist/dashboard/server.js +17619 -4044
  59. package/dist/{dns-L3L2BB27.js → dns-7BDJSD3E.js} +4 -2
  60. package/dist/{feedback-writer-AAKF5BTK.js → feedback-writer-LVZ5TFYZ.js} +8 -4
  61. package/dist/feedback-writer-LVZ5TFYZ.js.map +1 -0
  62. package/dist/hume-WMAUBBV2.js +13 -0
  63. package/dist/index.d.ts +162 -40
  64. package/dist/index.js +67 -23
  65. package/dist/index.js.map +1 -1
  66. package/dist/{projects-VXRUCMLM.js → projects-JEIVIYC6.js} +3 -3
  67. package/dist/rally-RKFSWC7E.js +10 -0
  68. package/dist/{remote-agents-Z3R2A5BN.js → remote-agents-TFSMW7GN.js} +2 -2
  69. package/dist/{remote-workspace-2G6V2KNP.js → remote-workspace-AHVHQEES.js} +8 -8
  70. package/dist/review-status-EPFG4XM7.js +19 -0
  71. package/dist/shadow-state-5MDP6YXH.js +30 -0
  72. package/dist/shadow-state-5MDP6YXH.js.map +1 -0
  73. package/dist/{specialist-context-N32QBNNQ.js → specialist-context-ZC6A4M3I.js} +8 -7
  74. package/dist/{specialist-context-N32QBNNQ.js.map → specialist-context-ZC6A4M3I.js.map} +1 -1
  75. package/dist/{specialist-logs-GF3YV4KL.js → specialist-logs-KLGJCEUL.js} +7 -6
  76. package/dist/specialist-logs-KLGJCEUL.js.map +1 -0
  77. package/dist/{specialists-JBIW6MP4.js → specialists-O4HWDJL5.js} +7 -6
  78. package/dist/specialists-O4HWDJL5.js.map +1 -0
  79. package/dist/tldr-daemon-T3THOUGT.js +21 -0
  80. package/dist/tldr-daemon-T3THOUGT.js.map +1 -0
  81. package/dist/traefik-QN7R5I6V.js +19 -0
  82. package/dist/traefik-QN7R5I6V.js.map +1 -0
  83. package/dist/tunnel-W2GZBLEV.js +13 -0
  84. package/dist/tunnel-W2GZBLEV.js.map +1 -0
  85. package/dist/workspace-manager-IE4JL2JP.js +22 -0
  86. package/dist/workspace-manager-IE4JL2JP.js.map +1 -0
  87. package/package.json +2 -2
  88. package/scripts/heartbeat-hook +37 -10
  89. package/scripts/patches/llm-tldr-tsx-support.py +109 -0
  90. package/scripts/pre-tool-hook +26 -15
  91. package/scripts/record-cost-event.js +177 -43
  92. package/scripts/record-cost-event.ts +87 -3
  93. package/scripts/statusline.sh +169 -0
  94. package/scripts/stop-hook +21 -11
  95. package/scripts/tldr-post-edit +72 -0
  96. package/scripts/tldr-read-enforcer +275 -0
  97. package/scripts/work-agent-stop-hook +137 -0
  98. package/skills/check-merged/SKILL.md +143 -0
  99. package/skills/crash-investigation/SKILL.md +301 -0
  100. package/skills/github-cli/SKILL.md +185 -0
  101. package/skills/myn-standards/SKILL.md +351 -0
  102. package/skills/pan-reopen/SKILL.md +65 -0
  103. package/skills/pan-sync-main/SKILL.md +87 -0
  104. package/skills/pan-tldr/SKILL.md +149 -0
  105. package/skills/react-best-practices/SKILL.md +125 -0
  106. package/skills/spec-readiness/REPORT-TEMPLATE.md +158 -0
  107. package/skills/spec-readiness/SCORING-REFERENCE.md +369 -0
  108. package/skills/spec-readiness/SKILL.md +400 -0
  109. package/skills/spec-readiness-setup/SKILL.md +361 -0
  110. package/skills/workspace-status/SKILL.md +56 -0
  111. package/skills/write-spec/SKILL.md +138 -0
  112. package/templates/traefik/dynamic/panopticon.yml.template +0 -5
  113. package/templates/traefik/traefik.yml +0 -8
  114. package/dist/chunk-2NIAOCIC.js.map +0 -1
  115. package/dist/chunk-3XAB4IXF.js +0 -51
  116. package/dist/chunk-3XAB4IXF.js.map +0 -1
  117. package/dist/chunk-6HXKTOD7.js.map +0 -1
  118. package/dist/chunk-BBCUK6N2.js +0 -241
  119. package/dist/chunk-BBCUK6N2.js.map +0 -1
  120. package/dist/chunk-BWGFN44T.js.map +0 -1
  121. package/dist/chunk-ELK6Q7QI.js +0 -545
  122. package/dist/chunk-ELK6Q7QI.js.map +0 -1
  123. package/dist/chunk-JY7R7V4G.js.map +0 -1
  124. package/dist/chunk-LYSBSZYV.js +0 -1523
  125. package/dist/chunk-LYSBSZYV.js.map +0 -1
  126. package/dist/chunk-VIWUCJ4V.js.map +0 -1
  127. package/dist/chunk-VU4FLXV5.js.map +0 -1
  128. package/dist/chunk-XP2DXWYP.js.map +0 -1
  129. package/dist/dashboard/public/assets/index-C7X6LP5Z.css +0 -32
  130. package/dist/dashboard/public/assets/index-ClYqpcAJ.js +0 -645
  131. package/dist/feedback-writer-AAKF5BTK.js.map +0 -1
  132. package/dist/review-status-GWQYY77L.js.map +0 -1
  133. package/dist/traefik-CUJM6K5Z.js +0 -12
  134. /package/dist/{agents-BDFHF4T3.js.map → agents-E43Y3HNU.js.map} +0 -0
  135. /package/dist/{config-BOAMSKTF.js.map → config-4CJNUE3O.js.map} +0 -0
  136. /package/dist/{dns-L3L2BB27.js.map → dns-7BDJSD3E.js.map} +0 -0
  137. /package/dist/{projects-VXRUCMLM.js.map → hume-WMAUBBV2.js.map} +0 -0
  138. /package/dist/{remote-agents-Z3R2A5BN.js.map → projects-JEIVIYC6.js.map} +0 -0
  139. /package/dist/{specialist-logs-GF3YV4KL.js.map → rally-RKFSWC7E.js.map} +0 -0
  140. /package/dist/{specialists-JBIW6MP4.js.map → remote-agents-TFSMW7GN.js.map} +0 -0
  141. /package/dist/{remote-workspace-2G6V2KNP.js.map → remote-workspace-AHVHQEES.js.map} +0 -0
  142. /package/dist/{traefik-CUJM6K5Z.js.map → review-status-EPFG4XM7.js.map} +0 -0
@@ -1,23 +1,39 @@
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-VIWUCJ4V.js";
11
+ sendKeysAsync,
12
+ waitForClaudePrompt
13
+ } from "./chunk-FTCPTHIJ.js";
14
+ import {
15
+ init_pipeline_notifier,
16
+ notifyPipeline
17
+ } from "./chunk-JQBV3Q2W.js";
18
+ import {
19
+ clearCredentialFileAuth,
20
+ getProviderEnv,
21
+ getProviderForModel,
22
+ init_providers,
23
+ init_settings,
24
+ loadSettings,
25
+ setupCredentialFileAuth
26
+ } from "./chunk-HJSM6E6U.js";
11
27
  import {
12
28
  init_projects,
13
29
  projects_exports
14
- } from "./chunk-JY7R7V4G.js";
30
+ } from "./chunk-OMNXYPXC.js";
15
31
  import {
16
32
  COSTS_DIR,
17
33
  PANOPTICON_HOME,
18
34
  getPanopticonHome,
19
35
  init_paths
20
- } from "./chunk-6HXKTOD7.js";
36
+ } from "./chunk-ZTFNYOC7.js";
21
37
  import {
22
38
  __esm,
23
39
  __export,
@@ -380,6 +396,59 @@ import { join as join4, basename as basename2 } from "path";
380
396
  import { homedir as homedir2 } from "os";
381
397
  import { exec } from "child_process";
382
398
  import { promisify } from "util";
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
+ }
434
+ function getProviderEnvForModel(model) {
435
+ const provider = getProviderForModel(model);
436
+ if (provider.name === "anthropic") return {};
437
+ const settings = loadSettings();
438
+ const apiKey = settings.api_keys?.[provider.name];
439
+ if (apiKey) {
440
+ return getProviderEnv(provider, apiKey);
441
+ }
442
+ console.warn(`[specialist] No API key for ${provider.displayName}, falling back to Anthropic`);
443
+ return {};
444
+ }
445
+ function buildTmuxEnvFlags(env) {
446
+ let flags = "";
447
+ for (const [key, value] of Object.entries(env)) {
448
+ flags += ` -e ${key}="${value.replace(/"/g, '\\"')}"`;
449
+ }
450
+ return flags;
451
+ }
383
452
  function initSpecialistsDirectory() {
384
453
  if (!existsSync3(SPECIALISTS_DIR)) {
385
454
  mkdirSync2(SPECIALISTS_DIR, { recursive: true });
@@ -553,9 +622,9 @@ function recordWake(name, sessionId) {
553
622
  }
554
623
  async function spawnEphemeralSpecialist(projectKey, specialistType, task) {
555
624
  ensureProjectSpecialistDir(projectKey, specialistType);
556
- const { loadContextDigest } = await import("./specialist-context-N32QBNNQ.js");
625
+ const { loadContextDigest } = await import("./specialist-context-ZC6A4M3I.js");
557
626
  const contextDigest = loadContextDigest(projectKey, specialistType);
558
- const { createRunLog: createRunLog2 } = await import("./specialist-logs-GF3YV4KL.js");
627
+ const { createRunLog: createRunLog2 } = await import("./specialist-logs-KLGJCEUL.js");
559
628
  const { runId, filePath: logFilePath } = createRunLog2(
560
629
  projectKey,
561
630
  specialistType,
@@ -568,13 +637,21 @@ async function spawnEphemeralSpecialist(projectKey, specialistType, task) {
568
637
  const tmuxSession = getTmuxSessionName(specialistType, projectKey);
569
638
  const cwd = process.env.HOME || "/home/exedev";
570
639
  try {
571
- let model = "claude-sonnet-4-5";
640
+ let model = "claude-sonnet-4-6";
572
641
  try {
573
642
  const workTypeId = `specialist-${specialistType}`;
574
643
  model = getModelId(workTypeId);
575
644
  } catch (error) {
576
645
  console.warn(`Warning: Could not resolve model for ${specialistType}, using default`);
577
646
  }
647
+ const providerEnv = getProviderEnvForModel(model);
648
+ const envFlags = buildTmuxEnvFlags(providerEnv);
649
+ const providerConfig = getProviderForModel(model);
650
+ if (providerConfig.authType === "credential-file") {
651
+ setupCredentialFileAuth(providerConfig, cwd);
652
+ } else {
653
+ clearCredentialFileAuth(cwd);
654
+ }
578
655
  const permissionFlags = specialistType === "merge-agent" ? "--dangerously-skip-permissions --permission-mode bypassPermissions" : "--dangerously-skip-permissions";
579
656
  const agentDir = join4(homedir2(), ".panopticon", "agents", tmuxSession);
580
657
  await execAsync(`mkdir -p "${agentDir}"`, { encoding: "utf-8" });
@@ -593,10 +670,10 @@ echo ""
593
670
  echo "## Specialist completed task"
594
671
  `, { mode: 493 });
595
672
  await execAsync(
596
- `tmux new-session -d -s "${tmuxSession}" "bash '${launcherScript}'"`,
673
+ `tmux new-session -d -s "${tmuxSession}"${envFlags} "bash '${launcherScript}'"`,
597
674
  { encoding: "utf-8" }
598
675
  );
599
- const { saveAgentRuntimeState } = await import("./agents-BDFHF4T3.js");
676
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
600
677
  saveAgentRuntimeState(tmuxSession, {
601
678
  state: "active",
602
679
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -620,7 +697,7 @@ echo "## Specialist completed task"
620
697
  }
621
698
  }
622
699
  async function buildTaskPrompt(projectKey, specialistType, task, contextDigest) {
623
- const { getSpecialistPromptOverride } = await import("./projects-VXRUCMLM.js");
700
+ const { getSpecialistPromptOverride } = await import("./projects-JEIVIYC6.js");
624
701
  const customPrompt = getSpecialistPromptOverride(projectKey, specialistType);
625
702
  let prompt = `# ${specialistType} Task - ${task.issueId}
626
703
 
@@ -681,14 +758,23 @@ Update status via API:
681
758
  - If tests pass: POST to /api/workspaces/${task.issueId}/review-status with {"testStatus":"passed"}
682
759
  - If tests fail: POST with {"testStatus":"failed","testNotes":"..."}`;
683
760
  break;
684
- 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
+ }
685
769
  prompt += `Your task:
686
770
  1. Fetch the latest main branch
687
- 2. Attempt to merge ${task.branch} into main
771
+ 2. Attempt to merge ${bInfo.branch} into main
688
772
  3. Resolve conflicts intelligently if needed
689
773
  4. Run tests to verify merge is clean
690
- 5. Complete merge if tests pass`;
774
+ 5. Complete merge if tests pass
775
+ 6. NEVER use git push --force`;
691
776
  break;
777
+ }
692
778
  }
693
779
  prompt += `
694
780
 
@@ -781,7 +867,7 @@ async function terminateSpecialist(projectKey, specialistType) {
781
867
  console.error(`[specialist] Failed to kill tmux session ${tmuxSession}:`, error);
782
868
  }
783
869
  if (metadata.currentRun) {
784
- const { finalizeRunLog: finalizeRunLog2 } = await import("./specialist-logs-GF3YV4KL.js");
870
+ const { finalizeRunLog: finalizeRunLog2 } = await import("./specialist-logs-KLGJCEUL.js");
785
871
  try {
786
872
  finalizeRunLog2(projectKey, specialistType, metadata.currentRun, {
787
873
  status: metadata.lastRunStatus || "incomplete",
@@ -794,20 +880,20 @@ async function terminateSpecialist(projectKey, specialistType) {
794
880
  }
795
881
  const key = `${projectKey}-${specialistType}`;
796
882
  gracePeriodStates.delete(key);
797
- const { saveAgentRuntimeState } = await import("./agents-BDFHF4T3.js");
883
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
798
884
  saveAgentRuntimeState(tmuxSession, {
799
885
  state: "suspended",
800
886
  lastActivity: (/* @__PURE__ */ new Date()).toISOString()
801
887
  });
802
- const { scheduleDigestGeneration } = await import("./specialist-context-N32QBNNQ.js");
888
+ const { scheduleDigestGeneration } = await import("./specialist-context-ZC6A4M3I.js");
803
889
  scheduleDigestGeneration(projectKey, specialistType);
804
890
  scheduleLogCleanup(projectKey, specialistType);
805
891
  }
806
892
  function scheduleLogCleanup(projectKey, specialistType) {
807
893
  Promise.resolve().then(async () => {
808
894
  try {
809
- const { cleanupOldLogs: cleanupOldLogs2 } = await import("./specialist-logs-GF3YV4KL.js");
810
- const { getSpecialistRetention } = await import("./projects-VXRUCMLM.js");
895
+ const { cleanupOldLogs: cleanupOldLogs2 } = await import("./specialist-logs-KLGJCEUL.js");
896
+ const { getSpecialistRetention } = await import("./projects-JEIVIYC6.js");
811
897
  const retention = getSpecialistRetention(projectKey);
812
898
  const deleted = cleanupOldLogs2(projectKey, specialistType, { maxDays: retention.max_days, maxRuns: retention.max_runs });
813
899
  if (deleted > 0) {
@@ -985,7 +1071,7 @@ async function getSpecialistStatus(name, projectKey) {
985
1071
  const sessionId = getSessionId(name);
986
1072
  const running = await isRunning(name, projectKey);
987
1073
  const contextTokens = countContextTokens(name);
988
- const { getAgentRuntimeState } = await import("./agents-BDFHF4T3.js");
1074
+ const { getAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
989
1075
  const tmuxSession = getTmuxSessionName(name, projectKey);
990
1076
  const runtimeState = getAgentRuntimeState(tmuxSession);
991
1077
  let state;
@@ -1045,7 +1131,7 @@ async function initializeSpecialist(name) {
1045
1131
  }
1046
1132
  const tmuxSession = getTmuxSessionName(name);
1047
1133
  const cwd = process.env.HOME || "/home/eltmon";
1048
- let model = "claude-sonnet-4-5";
1134
+ let model = "claude-sonnet-4-6";
1049
1135
  try {
1050
1136
  const workTypeId = `specialist-${name}`;
1051
1137
  model = getModelId(workTypeId);
@@ -1058,18 +1144,28 @@ Your role: ${name === "merge-agent" ? "Resolve merge conflicts and ensure clean
1058
1144
  You will be woken up when your services are needed. For now, acknowledge your initialization and wait.
1059
1145
  Say: "I am the ${name} specialist, ready and waiting for tasks."`;
1060
1146
  try {
1147
+ const providerEnv = getProviderEnvForModel(model);
1148
+ const envFlags = buildTmuxEnvFlags(providerEnv);
1149
+ const providerCfg = getProviderForModel(model);
1150
+ if (providerCfg.authType === "credential-file") {
1151
+ setupCredentialFileAuth(providerCfg, cwd);
1152
+ } else {
1153
+ clearCredentialFileAuth(cwd);
1154
+ }
1061
1155
  const agentDir = join4(homedir2(), ".panopticon", "agents", tmuxSession);
1062
1156
  await execAsync(`mkdir -p "${agentDir}"`, { encoding: "utf-8" });
1063
1157
  const promptFile = join4(agentDir, "identity-prompt.md");
1064
1158
  const launcherScript = join4(agentDir, "launcher.sh");
1065
1159
  writeFileSync(promptFile, identityPrompt);
1160
+ const newSessionId = randomUUID();
1066
1161
  writeFileSync(launcherScript, `#!/bin/bash
1067
1162
  cd "${cwd}"
1068
1163
  prompt=$(cat "${promptFile}")
1069
- exec claude --dangerously-skip-permissions --model ${model} "$prompt"
1164
+ exec claude --dangerously-skip-permissions --session-id "${newSessionId}" --model ${model} "$prompt"
1070
1165
  `, { mode: 493 });
1166
+ setSessionId(name, newSessionId);
1071
1167
  await execAsync(
1072
- `tmux new-session -d -s "${tmuxSession}" "bash '${launcherScript}'"`,
1168
+ `tmux new-session -d -s "${tmuxSession}"${envFlags} "bash '${launcherScript}'"`,
1073
1169
  { encoding: "utf-8" }
1074
1170
  );
1075
1171
  recordWake(name);
@@ -1117,9 +1213,7 @@ async function resetSpecialist(name) {
1117
1213
  const tmuxSession = getTmuxSessionName(name);
1118
1214
  try {
1119
1215
  await execAsync(`tmux send-keys -t "${tmuxSession}" C-c`, { encoding: "utf-8" });
1120
- await new Promise((resolve) => setTimeout(resolve, 200));
1121
- await sendKeysAsync(tmuxSession, "cd ~");
1122
- await new Promise((resolve) => setTimeout(resolve, 200));
1216
+ await new Promise((resolve) => setTimeout(resolve, 500));
1123
1217
  await execAsync(`tmux send-keys -t "${tmuxSession}" C-u`, { encoding: "utf-8" });
1124
1218
  await new Promise((resolve) => setTimeout(resolve, 100));
1125
1219
  } catch (error) {
@@ -1142,15 +1236,40 @@ async function wakeSpecialist(name, taskPrompt, options = {}) {
1142
1236
  }
1143
1237
  const cwd = process.env.HOME || "/home/eltmon";
1144
1238
  try {
1145
- const modelFlag = name === "merge-agent" ? "--model opus" : "";
1239
+ let model = "claude-sonnet-4-6";
1240
+ try {
1241
+ const workTypeId = `specialist-${name}`;
1242
+ model = getModelId(workTypeId);
1243
+ } catch (error) {
1244
+ console.warn(`[specialist] Could not resolve model for ${name}, using default`);
1245
+ }
1246
+ const modelFlag = `--model ${model}`;
1247
+ const providerEnv = getProviderEnvForModel(model);
1248
+ const envFlags = buildTmuxEnvFlags(providerEnv);
1249
+ const provCfg = getProviderForModel(model);
1250
+ if (provCfg.authType === "credential-file") {
1251
+ setupCredentialFileAuth(provCfg, cwd);
1252
+ } else {
1253
+ clearCredentialFileAuth(cwd);
1254
+ }
1146
1255
  const permissionFlags = name === "merge-agent" ? "--dangerously-skip-permissions --permission-mode bypassPermissions" : "--dangerously-skip-permissions";
1147
- const claudeCmd = sessionId ? `claude --resume "${sessionId}" ${modelFlag} ${permissionFlags}` : `claude ${modelFlag} ${permissionFlags}`;
1256
+ let claudeCmd;
1257
+ if (sessionId) {
1258
+ claudeCmd = `claude --resume "${sessionId}" ${modelFlag} ${permissionFlags}`;
1259
+ } else {
1260
+ const newSessionId = randomUUID();
1261
+ claudeCmd = `claude --session-id "${newSessionId}" ${modelFlag} ${permissionFlags}`;
1262
+ setSessionId(name, newSessionId);
1263
+ }
1148
1264
  await execAsync(
1149
- `tmux new-session -d -s "${tmuxSession}" -c "${cwd}" "${claudeCmd}"`,
1265
+ `tmux new-session -d -s "${tmuxSession}" -c "${cwd}"${envFlags} "${claudeCmd}"`,
1150
1266
  { encoding: "utf-8" }
1151
1267
  );
1152
1268
  if (waitForReady) {
1153
- 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
+ }
1154
1273
  }
1155
1274
  } catch (error) {
1156
1275
  const msg = error instanceof Error ? error.message : String(error);
@@ -1163,21 +1282,43 @@ async function wakeSpecialist(name, taskPrompt, options = {}) {
1163
1282
  }
1164
1283
  }
1165
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
+ }
1166
1289
  try {
1167
1290
  const isLargePrompt = taskPrompt.length > 500 || taskPrompt.includes("\n");
1291
+ let messageToSend;
1168
1292
  if (isLargePrompt) {
1169
1293
  if (!existsSync3(TASKS_DIR)) {
1170
1294
  mkdirSync2(TASKS_DIR, { recursive: true });
1171
1295
  }
1172
1296
  const taskFile = join4(TASKS_DIR, `${name}-${Date.now()}.md`);
1173
1297
  writeFileSync(taskFile, taskPrompt, "utf-8");
1174
- const shortMessage = `Read and execute the task in: ${taskFile}`;
1175
- await sendKeysAsync(tmuxSession, shortMessage);
1298
+ messageToSend = `Read and execute the task in: ${taskFile}`;
1176
1299
  } else {
1177
- 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
+ }
1178
1319
  }
1179
1320
  recordWake(name, sessionId || void 0);
1180
- const { saveAgentRuntimeState } = await import("./agents-BDFHF4T3.js");
1321
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1181
1322
  saveAgentRuntimeState(tmuxSession, {
1182
1323
  state: "active",
1183
1324
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1205,49 +1346,88 @@ async function wakeSpecialistWithTask(name, task) {
1205
1346
  const apiUrl = process.env.DASHBOARD_URL || `http://localhost:${apiPort}`;
1206
1347
  let prompt;
1207
1348
  switch (name) {
1208
- 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
+ ` : "";
1209
1360
  prompt = `New merge task for ${task.issueId}:
1210
1361
 
1211
- Branch: ${task.branch || "unknown"}
1212
- 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(", ")}` : ""}
1213
1365
  ${task.prUrl ? `PR URL: ${task.prUrl}` : ""}
1366
+ ${mergeRepoInstructions}
1367
+ For ${mergeInfo.isPolyrepo ? "EACH repo" : "the repo"}, perform these steps:
1214
1368
 
1215
- Your task:
1216
- 1. Fetch the latest main branch
1217
- 2. Attempt to merge ${task.branch} into main
1218
- 3. If conflicts arise, resolve them intelligently based on context
1219
- 4. Run the test suite to verify the merge is clean
1220
- 5. If tests pass, complete the merge and push
1221
- 6. If tests fail, analyze the failures and either fix them or report back
1222
-
1223
- When done, provide feedback on:
1224
- - Any conflicts encountered and how you resolved them
1225
- - Test results
1226
- - Any patterns you notice that future agents should be aware of
1227
-
1228
- Use the send-feedback-to-agent skill to report findings back to the issue agent.`;
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.`;
1229
1402
  break;
1403
+ }
1230
1404
  case "review-agent": {
1231
1405
  const workspace = task.workspace || "unknown";
1406
+ const reviewGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);
1407
+ const gitDirs = reviewGitInfo.gitDirs;
1408
+ const gitDir = gitDirs[0] || workspace;
1232
1409
  let staleBranch = false;
1233
- if (workspace !== "unknown") {
1410
+ if (workspace !== "unknown" && gitDirs.length > 0) {
1234
1411
  try {
1235
- const { stdout: diffOutput } = await execAsync(
1236
- `cd "${workspace}" && git fetch origin main 2>/dev/null; git diff --name-only main...HEAD 2>/dev/null`,
1237
- { encoding: "utf-8", timeout: 15e3 }
1238
- );
1239
- const changedFiles = diffOutput.trim().split("\n").filter((f) => f.length > 0);
1240
- if (changedFiles.length === 0) {
1412
+ let totalChangedFiles = 0;
1413
+ for (const dir of gitDirs) {
1414
+ const { stdout: dirDiff } = await execAsync(
1415
+ `cd "${dir}" && git fetch origin main 2>/dev/null; git diff --name-only main...HEAD 2>/dev/null`,
1416
+ { encoding: "utf-8", timeout: 15e3 }
1417
+ );
1418
+ totalChangedFiles += dirDiff.trim().split("\n").filter((f) => f.length > 0).length;
1419
+ }
1420
+ if (totalChangedFiles === 0) {
1241
1421
  staleBranch = true;
1242
1422
  console.log(`[specialist] review-agent: stale branch detected for ${task.issueId} \u2014 0 files changed vs main`);
1243
- const { setReviewStatus } = await import("./review-status-GWQYY77L.js");
1423
+ const { setReviewStatus } = await import("./review-status-EPFG4XM7.js");
1244
1424
  setReviewStatus(task.issueId.toUpperCase(), {
1245
1425
  reviewStatus: "passed",
1246
1426
  reviewNotes: "No changes to review \u2014 branch identical to main (already merged or stale)"
1247
1427
  });
1248
1428
  console.log(`[specialist] review-agent: auto-passed ${task.issueId} (stale branch)`);
1249
1429
  const tmuxSession = getTmuxSessionName("review-agent");
1250
- const { saveAgentRuntimeState } = await import("./agents-BDFHF4T3.js");
1430
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1251
1431
  saveAgentRuntimeState(tmuxSession, {
1252
1432
  state: "idle",
1253
1433
  lastActivity: (/* @__PURE__ */ new Date()).toISOString()
@@ -1258,10 +1438,14 @@ Use the send-feedback-to-agent skill to report findings back to the issue agent.
1258
1438
  console.warn(`[specialist] review-agent: stale branch pre-check failed for ${task.issueId}:`, err);
1259
1439
  }
1260
1440
  }
1441
+ const isPolyrepo = gitDirs.length > 1;
1442
+ const gitDiffCommands = gitDirs.length > 0 ? gitDirs.map((d) => `cd "${d}" && git diff --name-only main...HEAD`).join("\n") : `cd "${workspace}" && git diff --name-only main...HEAD`;
1443
+ const gitDiffFileCmd = gitDirs.length > 0 ? `cd "${gitDir}" && git diff main...HEAD -- <file>` : `cd "${workspace}" && git diff main...HEAD -- <file>`;
1261
1444
  prompt = `New review task for ${task.issueId}:
1262
1445
 
1263
1446
  Branch: ${task.branch || "unknown"}
1264
1447
  Workspace: ${workspace}
1448
+ ${isPolyrepo ? `Polyrepo: git repos in subdirectories: ${gitDirs.map((d) => basename2(d)).join(", ")}` : ""}
1265
1449
  ${task.prUrl ? `PR URL: ${task.prUrl}` : ""}
1266
1450
 
1267
1451
  Your task:
@@ -1276,11 +1460,12 @@ The TEST agent will run tests in the next step.
1276
1460
  ## How to Review Changes
1277
1461
 
1278
1462
  **Step 0 (CRITICAL):** First check if there are ANY changes to review:
1463
+ ${isPolyrepo ? `This is a polyrepo \u2014 run git diff in each repo subdirectory:` : ""}
1279
1464
  \`\`\`bash
1280
- cd ${workspace} && git diff --name-only main...HEAD
1465
+ ${gitDiffCommands}
1281
1466
  \`\`\`
1282
1467
 
1283
- **If the diff is EMPTY (0 files changed):** The branch is stale or already merged into main. In this case:
1468
+ **If the diff is EMPTY (0 files changed across all repos):** The branch is stale or already merged into main. In this case:
1284
1469
  1. Do NOT attempt a full review
1285
1470
  2. Update status as passed immediately:
1286
1471
  \`\`\`bash
@@ -1294,7 +1479,7 @@ pan work tell ${task.issueId} "Review complete: branch has 0 diff from main \u20
1294
1479
 
1295
1480
  **Step 1:** Get the list of changed files:
1296
1481
  \`\`\`bash
1297
- cd ${workspace} && git diff --name-only main...HEAD
1482
+ ${gitDiffCommands}
1298
1483
  \`\`\`
1299
1484
 
1300
1485
  **Step 2:** Read the CURRENT version of each changed file using the Read tool.
@@ -1302,7 +1487,7 @@ Review the actual file contents \u2014 do NOT rely solely on diff output.
1302
1487
 
1303
1488
  **Step 3:** If you need to see what specifically changed, use:
1304
1489
  \`\`\`bash
1305
- cd ${workspace} && git diff main...HEAD -- <file>
1490
+ ${gitDiffFileCmd}
1306
1491
  \`\`\`
1307
1492
 
1308
1493
  ## Avoiding False Positives
@@ -1343,47 +1528,118 @@ curl -s -X POST ${apiUrl}/api/specialists/test-agent/queue -H "Content-Type: app
1343
1528
  \u26A0\uFE0F VERIFICATION: After running each curl, confirm you see valid JSON output. If you get an error, report it.`;
1344
1529
  break;
1345
1530
  }
1346
- 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";
1347
1575
  prompt = `New test task for ${task.issueId}:
1348
1576
 
1349
1577
  Branch: ${task.branch || "unknown"}
1350
- 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}
1351
1584
 
1352
1585
  Your task:
1353
- 1. Run the full test suite on the feature branch
1354
- 2. Run the same test suite on the main branch (baseline)
1355
- 3. Compare results: identify which failures are NEW vs pre-existing
1356
- 4. Only fail the feature branch for NEW regressions
1586
+ 1. Run ALL test suites \u2014 redirect output to file, read only summaries
1587
+ 2. If ALL pass, skip baseline and report PASS
1588
+ 3. If failures, run baseline on main and compare
1589
+ 4. Only fail for NEW regressions (not pre-existing)
1357
1590
  5. Update status via API when done
1358
1591
 
1592
+ ## CRITICAL: Context Management \u2014 Output Redirection
1593
+
1594
+ **NEVER let full test output flow into your context.** Always redirect to file and read only summaries.
1595
+ Raw test output from large suites (1000+ tests) WILL fill your context and cause compaction, losing your task.
1596
+
1359
1597
  ## CRITICAL: Bash Timeout for Test Commands
1360
1598
 
1361
1599
  **ALWAYS use timeout: 300000 (5 minutes) when running test commands.**
1362
- Test suites commonly take 2-5 minutes. The default bash timeout is only 2 minutes and WILL cause premature failures.
1363
- Do NOT run test commands in background mode \u2014 run them directly with a 5-minute timeout.
1600
+ For Maven/Spring Boot tests, use timeout: 600000 (10 minutes) \u2014 they take longer.
1601
+
1602
+ ## Step 1: Run Feature Branch Tests
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.` : ""}
1364
1605
 
1365
- Example:
1366
1606
  \`\`\`bash
1367
- cd ${task.workspace || "unknown"} && npm test 2>&1 | tail -30
1368
- # Use timeout: 300000 for this command
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
1369
1614
  \`\`\`
1370
1615
 
1371
- ## CRITICAL: Baseline Comparison
1616
+ ## Step 2: Check Results
1617
+
1618
+ - If ALL exit codes are 0 \u2192 skip baseline, go to "Update Status"
1619
+ - If any failures \u2192 continue to Step 3
1372
1620
 
1373
- **You MUST compare test results against the main branch baseline.**
1621
+ ## Step 3: Baseline Comparison (ONLY if failures found)
1374
1622
 
1375
- Pre-existing failures that also occur on main branch should NOT block the feature branch.
1623
+ \`\`\`bash
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
1631
+ \`\`\`
1376
1632
 
1377
- Steps:
1378
- 1. Run \`npm test\` (or detected command) on the feature branch - record results (timeout: 300000)
1379
- 2. Run tests on main branch baseline (timeout: 300000): \`cd ${task.context?.workspace ? task.context.workspace.replace(/workspaces\/feature-[^/]+/, "") : "unknown"} && npm test 2>&1 | tail -30\`
1380
- 3. Compare: any test that fails on BOTH branches is pre-existing
1381
- 4. Only NEW failures (pass on main, fail on feature) should block
1633
+ Then compare failures (targeted, NOT full output):
1634
+ \`\`\`bash
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
1637
+ \`\`\`
1382
1638
 
1383
- **Pass criteria:** The feature branch introduces ZERO new test failures compared to main.
1384
- **Fail criteria:** The feature branch introduces one or more NEW test failures not present on main.
1639
+ Tests that fail on BOTH = pre-existing (don't block). Tests that fail ONLY on feature = NEW regression (block).
1385
1640
 
1386
- Report pre-existing failures as informational notes, but do NOT block the feature for them.
1641
+ **Pass criteria:** Feature branch introduces ZERO new test failures vs main.
1642
+ **Fail criteria:** Feature branch introduces NEW failures not present on main.
1387
1643
 
1388
1644
  ## REQUIRED: Update Status via API
1389
1645
 
@@ -1391,21 +1647,22 @@ You MUST execute the appropriate curl command and verify it succeeds. Do NOT jus
1391
1647
 
1392
1648
  If NO new regressions (tests PASS):
1393
1649
  \`\`\`bash
1394
- # EXECUTE THIS - verify you see JSON response with testStatus:"passed"
1395
- 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 .
1396
1651
  \`\`\`
1397
1652
 
1398
1653
  If NEW regressions found (tests FAIL):
1399
1654
  \`\`\`bash
1400
- # EXECUTE THIS - verify you see JSON response with testStatus:"failed"
1401
- 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 .
1402
1656
  \`\`\`
1403
1657
  Then use send-feedback-to-agent skill to notify issue agent of NEW failures only.
1404
1658
 
1405
1659
  \u26A0\uFE0F VERIFICATION: After running curl, confirm you see valid JSON output with the updated status. If you get an error or empty response, the update FAILED - report this.
1406
1660
 
1661
+ **NEVER run test commands without redirecting to a file.** This is not optional.
1662
+
1407
1663
  IMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.`;
1408
1664
  break;
1665
+ }
1409
1666
  default:
1410
1667
  prompt = `Task for ${task.issueId}: Please process this task and report findings.`;
1411
1668
  }
@@ -1414,7 +1671,7 @@ IMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.
1414
1671
  async function wakeSpecialistOrQueue(name, task, options = {}) {
1415
1672
  const { priority = "normal", source = "handoff" } = options;
1416
1673
  const running = await isRunning(name);
1417
- const { getAgentRuntimeState } = await import("./agents-BDFHF4T3.js");
1674
+ const { getAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1418
1675
  const tmuxSession = getTmuxSessionName(name);
1419
1676
  const runtimeState = getAgentRuntimeState(tmuxSession);
1420
1677
  const idle = runtimeState?.state === "idle" || runtimeState?.state === "suspended";
@@ -1445,7 +1702,7 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
1445
1702
  };
1446
1703
  }
1447
1704
  }
1448
- const { saveAgentRuntimeState } = await import("./agents-BDFHF4T3.js");
1705
+ const { saveAgentRuntimeState } = await import("./agents-E43Y3HNU.js");
1449
1706
  saveAgentRuntimeState(tmuxSession, {
1450
1707
  state: "active",
1451
1708
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1499,6 +1756,7 @@ function submitToSpecialistQueue(specialistName, task) {
1499
1756
  }
1500
1757
  };
1501
1758
  const queueItem = pushToHook(specialistName, item);
1759
+ notifyPipeline({ type: "task_queued", specialist: specialistName, issueId: task.issueId });
1502
1760
  const handoffEvent = createSpecialistHandoff(
1503
1761
  task.source,
1504
1762
  // From (e.g., 'review-agent' or 'issue-agent')
@@ -1544,7 +1802,7 @@ async function sendFeedbackToAgent(feedback) {
1544
1802
  }
1545
1803
  const agentSession = `agent-${toIssueId.toLowerCase()}`;
1546
1804
  const feedbackMessage = formatFeedbackForAgent(fullFeedback);
1547
- const { writeFeedbackFile } = await import("./feedback-writer-AAKF5BTK.js");
1805
+ const { writeFeedbackFile } = await import("./feedback-writer-LVZ5TFYZ.js");
1548
1806
  const specialistMap = {
1549
1807
  "review-agent": "review-agent",
1550
1808
  "test-agent": "test-agent",
@@ -1564,7 +1822,7 @@ async function sendFeedbackToAgent(feedback) {
1564
1822
  return false;
1565
1823
  }
1566
1824
  try {
1567
- const { messageAgent } = await import("./agents-BDFHF4T3.js");
1825
+ const { messageAgent } = await import("./agents-E43Y3HNU.js");
1568
1826
  const msg = `SPECIALIST FEEDBACK: ${fromSpecialist} reported ${feedback.feedbackType.toUpperCase()} for ${toIssueId}.
1569
1827
  Read and address: ${fileResult.relativePath}`;
1570
1828
  await messageAgent(agentSession, msg);
@@ -1670,8 +1928,11 @@ var init_specialists = __esm({
1670
1928
  init_paths();
1671
1929
  init_jsonl_parser();
1672
1930
  init_specialist_handoff_logger();
1931
+ init_settings();
1673
1932
  init_work_type_router();
1933
+ init_providers();
1674
1934
  init_tmux();
1935
+ init_pipeline_notifier();
1675
1936
  init_hooks();
1676
1937
  execAsync = promisify(exec);
1677
1938
  SPECIALISTS_DIR = join4(PANOPTICON_HOME, "specialists");
@@ -1887,18 +2148,18 @@ function getRecentRunLogs(projectKey, specialistType, count) {
1887
2148
  }
1888
2149
  function cleanupOldLogs(projectKey, specialistType, retention) {
1889
2150
  const { maxDays, maxRuns } = retention;
2151
+ const now = /* @__PURE__ */ new Date();
2152
+ const cutoffDate = new Date(now.getTime() - maxDays * 24 * 60 * 60 * 1e3);
1890
2153
  const allLogs = listRunLogs(projectKey, specialistType);
1891
2154
  if (allLogs.length === 0) {
1892
2155
  return 0;
1893
2156
  }
1894
- const now = /* @__PURE__ */ new Date();
1895
- const cutoffDate = new Date(now.getTime() - maxDays * 24 * 60 * 60 * 1e3);
1896
2157
  let deletedCount = 0;
1897
2158
  allLogs.forEach((log, index) => {
1898
2159
  if (index < maxRuns) {
1899
2160
  return;
1900
2161
  }
1901
- if (log.createdAt >= cutoffDate) {
2162
+ if (maxDays > 0 && log.createdAt >= cutoffDate) {
1902
2163
  return;
1903
2164
  }
1904
2165
  try {
@@ -2055,4 +2316,4 @@ export {
2055
2316
  getFeedbackStats,
2056
2317
  init_specialists
2057
2318
  };
2058
- //# sourceMappingURL=chunk-2NIAOCIC.js.map
2319
+ //# sourceMappingURL=chunk-AAFQANKW.js.map