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.
- package/README.md +96 -210
- package/dist/{agents-BDFHF4T3.js → agents-E43Y3HNU.js} +10 -7
- package/dist/chunk-7SN4L4PH.js +150 -0
- package/dist/chunk-7SN4L4PH.js.map +1 -0
- package/dist/{chunk-2NIAOCIC.js → chunk-AAFQANKW.js} +358 -97
- package/dist/chunk-AAFQANKW.js.map +1 -0
- package/dist/chunk-AQXETQHW.js +113 -0
- package/dist/chunk-AQXETQHW.js.map +1 -0
- package/dist/chunk-B3PF6JPQ.js +212 -0
- package/dist/chunk-B3PF6JPQ.js.map +1 -0
- package/dist/chunk-CFCUOV3Q.js +669 -0
- package/dist/chunk-CFCUOV3Q.js.map +1 -0
- package/dist/chunk-CWELWPWQ.js +32 -0
- package/dist/chunk-CWELWPWQ.js.map +1 -0
- package/dist/chunk-DI7ABPNQ.js +352 -0
- package/dist/chunk-DI7ABPNQ.js.map +1 -0
- package/dist/{chunk-VU4FLXV5.js → chunk-FQ66DECN.js} +31 -4
- package/dist/chunk-FQ66DECN.js.map +1 -0
- package/dist/{chunk-VIWUCJ4V.js → chunk-FTCPTHIJ.js} +57 -432
- package/dist/chunk-FTCPTHIJ.js.map +1 -0
- package/dist/{review-status-GWQYY77L.js → chunk-GFP3PIPB.js} +14 -7
- package/dist/chunk-GFP3PIPB.js.map +1 -0
- package/dist/chunk-GR6ZZMCX.js +816 -0
- package/dist/chunk-GR6ZZMCX.js.map +1 -0
- package/dist/chunk-HJSM6E6U.js +1038 -0
- package/dist/chunk-HJSM6E6U.js.map +1 -0
- package/dist/{chunk-XP2DXWYP.js → chunk-HZT2AOPN.js} +164 -39
- package/dist/chunk-HZT2AOPN.js.map +1 -0
- package/dist/chunk-JQBV3Q2W.js +29 -0
- package/dist/chunk-JQBV3Q2W.js.map +1 -0
- package/dist/{chunk-BWGFN44T.js → chunk-JT4O4YVM.js} +28 -16
- package/dist/chunk-JT4O4YVM.js.map +1 -0
- package/dist/chunk-NTO3EDB3.js +600 -0
- package/dist/chunk-NTO3EDB3.js.map +1 -0
- package/dist/{chunk-JY7R7V4G.js → chunk-OMNXYPXC.js} +2 -2
- package/dist/chunk-OMNXYPXC.js.map +1 -0
- package/dist/chunk-PELXV435.js +215 -0
- package/dist/chunk-PELXV435.js.map +1 -0
- package/dist/chunk-PPRFKTVC.js +154 -0
- package/dist/chunk-PPRFKTVC.js.map +1 -0
- package/dist/chunk-WQG2TYCB.js +677 -0
- package/dist/chunk-WQG2TYCB.js.map +1 -0
- package/dist/{chunk-HCTJFIJJ.js → chunk-YLPSQAM2.js} +2 -2
- package/dist/{chunk-HCTJFIJJ.js.map → chunk-YLPSQAM2.js.map} +1 -1
- package/dist/{chunk-6HXKTOD7.js → chunk-ZTFNYOC7.js} +53 -38
- package/dist/chunk-ZTFNYOC7.js.map +1 -0
- package/dist/cli/index.js +5103 -3165
- package/dist/cli/index.js.map +1 -1
- package/dist/{config-BOAMSKTF.js → config-4CJNUE3O.js} +7 -3
- package/dist/dashboard/prompts/merge-agent.md +217 -0
- package/dist/dashboard/prompts/review-agent.md +409 -0
- package/dist/dashboard/prompts/sync-main.md +84 -0
- package/dist/dashboard/prompts/test-agent.md +283 -0
- package/dist/dashboard/prompts/work-agent.md +249 -0
- package/dist/dashboard/public/assets/index-BxpjweAL.css +32 -0
- package/dist/dashboard/public/assets/index-DQHkwvvJ.js +743 -0
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/server.js +17619 -4044
- package/dist/{dns-L3L2BB27.js → dns-7BDJSD3E.js} +4 -2
- package/dist/{feedback-writer-AAKF5BTK.js → feedback-writer-LVZ5TFYZ.js} +8 -4
- package/dist/feedback-writer-LVZ5TFYZ.js.map +1 -0
- package/dist/hume-WMAUBBV2.js +13 -0
- package/dist/index.d.ts +162 -40
- package/dist/index.js +67 -23
- package/dist/index.js.map +1 -1
- package/dist/{projects-VXRUCMLM.js → projects-JEIVIYC6.js} +3 -3
- package/dist/rally-RKFSWC7E.js +10 -0
- package/dist/{remote-agents-Z3R2A5BN.js → remote-agents-TFSMW7GN.js} +2 -2
- package/dist/{remote-workspace-2G6V2KNP.js → remote-workspace-AHVHQEES.js} +8 -8
- package/dist/review-status-EPFG4XM7.js +19 -0
- package/dist/shadow-state-5MDP6YXH.js +30 -0
- package/dist/shadow-state-5MDP6YXH.js.map +1 -0
- package/dist/{specialist-context-N32QBNNQ.js → specialist-context-ZC6A4M3I.js} +8 -7
- package/dist/{specialist-context-N32QBNNQ.js.map → specialist-context-ZC6A4M3I.js.map} +1 -1
- package/dist/{specialist-logs-GF3YV4KL.js → specialist-logs-KLGJCEUL.js} +7 -6
- package/dist/specialist-logs-KLGJCEUL.js.map +1 -0
- package/dist/{specialists-JBIW6MP4.js → specialists-O4HWDJL5.js} +7 -6
- package/dist/specialists-O4HWDJL5.js.map +1 -0
- package/dist/tldr-daemon-T3THOUGT.js +21 -0
- package/dist/tldr-daemon-T3THOUGT.js.map +1 -0
- package/dist/traefik-QN7R5I6V.js +19 -0
- package/dist/traefik-QN7R5I6V.js.map +1 -0
- package/dist/tunnel-W2GZBLEV.js +13 -0
- package/dist/tunnel-W2GZBLEV.js.map +1 -0
- package/dist/workspace-manager-IE4JL2JP.js +22 -0
- package/dist/workspace-manager-IE4JL2JP.js.map +1 -0
- package/package.json +2 -2
- package/scripts/heartbeat-hook +37 -10
- package/scripts/patches/llm-tldr-tsx-support.py +109 -0
- package/scripts/pre-tool-hook +26 -15
- package/scripts/record-cost-event.js +177 -43
- package/scripts/record-cost-event.ts +87 -3
- package/scripts/statusline.sh +169 -0
- package/scripts/stop-hook +21 -11
- package/scripts/tldr-post-edit +72 -0
- package/scripts/tldr-read-enforcer +275 -0
- package/scripts/work-agent-stop-hook +137 -0
- package/skills/check-merged/SKILL.md +143 -0
- package/skills/crash-investigation/SKILL.md +301 -0
- package/skills/github-cli/SKILL.md +185 -0
- package/skills/myn-standards/SKILL.md +351 -0
- package/skills/pan-reopen/SKILL.md +65 -0
- package/skills/pan-sync-main/SKILL.md +87 -0
- package/skills/pan-tldr/SKILL.md +149 -0
- package/skills/react-best-practices/SKILL.md +125 -0
- package/skills/spec-readiness/REPORT-TEMPLATE.md +158 -0
- package/skills/spec-readiness/SCORING-REFERENCE.md +369 -0
- package/skills/spec-readiness/SKILL.md +400 -0
- package/skills/spec-readiness-setup/SKILL.md +361 -0
- package/skills/workspace-status/SKILL.md +56 -0
- package/skills/write-spec/SKILL.md +138 -0
- package/templates/traefik/dynamic/panopticon.yml.template +0 -5
- package/templates/traefik/traefik.yml +0 -8
- package/dist/chunk-2NIAOCIC.js.map +0 -1
- package/dist/chunk-3XAB4IXF.js +0 -51
- package/dist/chunk-3XAB4IXF.js.map +0 -1
- package/dist/chunk-6HXKTOD7.js.map +0 -1
- package/dist/chunk-BBCUK6N2.js +0 -241
- package/dist/chunk-BBCUK6N2.js.map +0 -1
- package/dist/chunk-BWGFN44T.js.map +0 -1
- package/dist/chunk-ELK6Q7QI.js +0 -545
- package/dist/chunk-ELK6Q7QI.js.map +0 -1
- package/dist/chunk-JY7R7V4G.js.map +0 -1
- package/dist/chunk-LYSBSZYV.js +0 -1523
- package/dist/chunk-LYSBSZYV.js.map +0 -1
- package/dist/chunk-VIWUCJ4V.js.map +0 -1
- package/dist/chunk-VU4FLXV5.js.map +0 -1
- package/dist/chunk-XP2DXWYP.js.map +0 -1
- package/dist/dashboard/public/assets/index-C7X6LP5Z.css +0 -32
- package/dist/dashboard/public/assets/index-ClYqpcAJ.js +0 -645
- package/dist/feedback-writer-AAKF5BTK.js.map +0 -1
- package/dist/review-status-GWQYY77L.js.map +0 -1
- package/dist/traefik-CUJM6K5Z.js +0 -12
- /package/dist/{agents-BDFHF4T3.js.map → agents-E43Y3HNU.js.map} +0 -0
- /package/dist/{config-BOAMSKTF.js.map → config-4CJNUE3O.js.map} +0 -0
- /package/dist/{dns-L3L2BB27.js.map → dns-7BDJSD3E.js.map} +0 -0
- /package/dist/{projects-VXRUCMLM.js.map → hume-WMAUBBV2.js.map} +0 -0
- /package/dist/{remote-agents-Z3R2A5BN.js.map → projects-JEIVIYC6.js.map} +0 -0
- /package/dist/{specialist-logs-GF3YV4KL.js.map → rally-RKFSWC7E.js.map} +0 -0
- /package/dist/{specialists-JBIW6MP4.js.map → remote-agents-TFSMW7GN.js.map} +0 -0
- /package/dist/{remote-workspace-2G6V2KNP.js.map → remote-workspace-AHVHQEES.js.map} +0 -0
- /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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
625
|
+
const { loadContextDigest } = await import("./specialist-context-ZC6A4M3I.js");
|
|
557
626
|
const contextDigest = loadContextDigest(projectKey, specialistType);
|
|
558
|
-
const { createRunLog: createRunLog2 } = await import("./specialist-logs-
|
|
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-
|
|
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-
|
|
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-
|
|
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 ${
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
810
|
-
const { getSpecialistRetention } = await import("./projects-
|
|
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-
|
|
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-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1175
|
-
await sendKeysAsync(tmuxSession, shortMessage);
|
|
1298
|
+
messageToSend = `Read and execute the task in: ${taskFile}`;
|
|
1176
1299
|
} else {
|
|
1177
|
-
|
|
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-
|
|
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: ${
|
|
1212
|
-
Workspace: ${
|
|
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
|
-
|
|
1216
|
-
1.
|
|
1217
|
-
2.
|
|
1218
|
-
3.
|
|
1219
|
-
4.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
{
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
1354
|
-
2.
|
|
1355
|
-
3.
|
|
1356
|
-
4. Only fail
|
|
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
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
1621
|
+
## Step 3: Baseline Comparison (ONLY if failures found)
|
|
1374
1622
|
|
|
1375
|
-
|
|
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
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
2319
|
+
//# sourceMappingURL=chunk-AAFQANKW.js.map
|