tmux-ide 1.3.1 → 2.0.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/bin/cli.js +4 -216
- package/bin/cli.ts +536 -0
- package/package.json +27 -14
- package/src/__tests__/support.ts +186 -0
- package/src/attach.ts +20 -0
- package/src/cli.test.ts +314 -0
- package/src/command-center/discovery.test.ts +265 -0
- package/src/command-center/discovery.ts +296 -0
- package/src/command-center/index.ts +31 -0
- package/src/command-center/schemas.ts +57 -0
- package/src/command-center/server.test.ts +818 -0
- package/src/command-center/server.ts +1426 -0
- package/src/config-cli.test.ts +86 -0
- package/src/config.test.ts +205 -0
- package/src/config.ts +313 -0
- package/src/detect.test.ts +142 -0
- package/src/detect.ts +248 -0
- package/src/dispatch.ts +102 -0
- package/src/doctor.test.ts +119 -0
- package/src/doctor.ts +167 -0
- package/src/init.test.ts +101 -0
- package/src/init.ts +144 -0
- package/src/inspect.test.ts +67 -0
- package/src/inspect.ts +212 -0
- package/src/integration.test.ts.skip +276 -0
- package/src/js-yaml.d.ts +10 -0
- package/src/launch.test.ts +222 -0
- package/src/launch.ts +494 -0
- package/src/lib/auth/auth-service.test.ts +178 -0
- package/src/lib/auth/auth-service.ts +227 -0
- package/src/lib/auth/middleware.test.ts +119 -0
- package/src/lib/auth/middleware.ts +56 -0
- package/src/lib/auth/types.ts +13 -0
- package/src/lib/authorship.test.ts +242 -0
- package/src/lib/authorship.ts +280 -0
- package/src/lib/cast/recorder.test.ts +127 -0
- package/src/lib/cast/recorder.ts +148 -0
- package/src/lib/daemon-watchdog.ts +102 -0
- package/src/lib/daemon.ts +417 -0
- package/src/lib/dot-path.test.ts +66 -0
- package/src/lib/dot-path.ts +17 -0
- package/src/lib/errors.ts +44 -0
- package/src/lib/event-log.test.ts +189 -0
- package/src/lib/event-log.ts +237 -0
- package/src/lib/github-pr.ts +182 -0
- package/src/lib/hq/client.test.ts +154 -0
- package/src/lib/hq/client.ts +142 -0
- package/src/lib/hq/mdns.test.ts +54 -0
- package/src/lib/hq/mdns.ts +88 -0
- package/src/lib/hq/registry.test.ts +237 -0
- package/src/lib/hq/registry.ts +145 -0
- package/src/lib/hq/types.ts +44 -0
- package/src/lib/ipc/socket-protocol.test.ts +126 -0
- package/src/lib/ipc/socket-protocol.ts +324 -0
- package/src/lib/launch-plan.test.ts +119 -0
- package/src/lib/launch-plan.ts +90 -0
- package/src/lib/log.test.ts +39 -0
- package/src/lib/log.ts +71 -0
- package/src/lib/metrics.test.ts +213 -0
- package/src/lib/metrics.ts +345 -0
- package/src/lib/orchestrator.test.ts +2358 -0
- package/src/lib/orchestrator.ts +1792 -0
- package/src/lib/output.ts +76 -0
- package/src/lib/plan-store.test.ts +148 -0
- package/src/lib/plan-store.ts +131 -0
- package/src/lib/research.test.ts +208 -0
- package/src/lib/research.ts +475 -0
- package/src/lib/round-trip.test.ts +341 -0
- package/src/lib/session-monitor.test.ts +152 -0
- package/src/lib/session-monitor.ts +282 -0
- package/src/lib/session-options.test.ts +146 -0
- package/src/lib/session-options.ts +100 -0
- package/src/lib/shell.test.ts +20 -0
- package/src/lib/shell.ts +8 -0
- package/src/lib/sizes.test.ts +73 -0
- package/src/lib/sizes.ts +36 -0
- package/src/lib/skill-registry.test.ts +134 -0
- package/src/lib/skill-registry.ts +86 -0
- package/src/lib/slugify.test.ts +35 -0
- package/src/lib/slugify.ts +10 -0
- package/src/lib/task-store.ts +352 -0
- package/src/lib/tmux.test.ts +500 -0
- package/src/lib/tmux.ts +372 -0
- package/src/lib/token-tracker.test.ts +102 -0
- package/src/lib/token-tracker.ts +70 -0
- package/src/lib/tunnels/cloudflare.test.ts +73 -0
- package/src/lib/tunnels/cloudflare.ts +254 -0
- package/src/lib/tunnels/manager.test.ts +63 -0
- package/src/lib/tunnels/manager.ts +132 -0
- package/src/lib/tunnels/ngrok.test.ts +94 -0
- package/src/lib/tunnels/ngrok.ts +261 -0
- package/src/lib/tunnels/tailscale.test.ts +103 -0
- package/src/lib/tunnels/tailscale.ts +1048 -0
- package/src/lib/tunnels/types.ts +44 -0
- package/src/lib/validation.test.ts +289 -0
- package/src/lib/validation.ts +123 -0
- package/src/lib/webhook.ts +51 -0
- package/src/lib/workflow-store.ts +209 -0
- package/src/lib/ws-v3/protocol.test.ts +127 -0
- package/src/lib/ws-v3/protocol.ts +160 -0
- package/src/lib/yaml-io.test.ts +125 -0
- package/src/lib/yaml-io.ts +27 -0
- package/src/ls.test.ts +76 -0
- package/src/ls.ts +40 -0
- package/src/metrics-cli.ts +177 -0
- package/src/notify.ts +85 -0
- package/src/orchestrator-status.ts +228 -0
- package/src/plan.ts +117 -0
- package/src/postinstall.test.ts +79 -0
- package/src/remote.test.ts +115 -0
- package/src/remote.ts +148 -0
- package/src/restart.ts +24 -0
- package/src/schemas/domain.ts +422 -0
- package/src/schemas/ide-config.ts +148 -0
- package/src/schemas/index.ts +53 -0
- package/src/send.test.ts +70 -0
- package/src/send.ts +181 -0
- package/src/skill.test.ts +114 -0
- package/src/skill.ts +138 -0
- package/src/status.ts +66 -0
- package/src/stop.test.ts +176 -0
- package/src/stop.ts +47 -0
- package/src/task.test.ts +1466 -0
- package/src/task.ts +1143 -0
- package/src/tunnel.test.ts +80 -0
- package/src/tunnel.ts +133 -0
- package/src/types.ts +15 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/terminal/index.ts +9 -0
- package/src/ui/types.ts +91 -0
- package/src/ui/web/base.css +80 -0
- package/src/ui/web/components/Box.tsx +59 -0
- package/src/ui/web/components/Input.tsx +32 -0
- package/src/ui/web/components/ScrollBox.tsx +60 -0
- package/src/ui/web/components/Text.tsx +28 -0
- package/src/ui/web/hooks.ts +106 -0
- package/src/ui/web/index.ts +27 -0
- package/src/ui/web/render.ts +77 -0
- package/src/ui/web/utils/color.test.ts +93 -0
- package/src/ui/web/utils/color.ts +27 -0
- package/src/validate.test.ts +385 -0
- package/src/validate.ts +217 -0
- package/src/widgets/changes/index.tsx +576 -0
- package/src/widgets/config/index.tsx +440 -0
- package/src/widgets/costs/index.tsx +216 -0
- package/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/src/widgets/explorer/footer.tsx +20 -0
- package/src/widgets/explorer/header.tsx +23 -0
- package/src/widgets/explorer/index.tsx +408 -0
- package/src/widgets/explorer/tree-model.test.ts +214 -0
- package/src/widgets/explorer/tree-model.ts +103 -0
- package/src/widgets/explorer/tree.tsx +156 -0
- package/src/widgets/lib/config-model.ts +116 -0
- package/src/widgets/lib/files.test.ts +103 -0
- package/src/widgets/lib/files.ts +88 -0
- package/src/widgets/lib/git.test.ts +151 -0
- package/src/widgets/lib/git.ts +88 -0
- package/src/widgets/lib/pane-comms.test.ts +178 -0
- package/src/widgets/lib/pane-comms.ts +192 -0
- package/src/widgets/lib/theme.ts +151 -0
- package/src/widgets/lib/watcher.ts +50 -0
- package/src/widgets/mission-control/activity-feed.tsx +59 -0
- package/src/widgets/mission-control/agent-panel.tsx +132 -0
- package/src/widgets/mission-control/command-bar.tsx +63 -0
- package/src/widgets/mission-control/index.tsx +751 -0
- package/src/widgets/mission-control/task-panel.tsx +238 -0
- package/src/widgets/preview/index.tsx +415 -0
- package/src/widgets/resolve.ts +40 -0
- package/src/widgets/setup/agent-naming.tsx +112 -0
- package/src/widgets/setup/config-tree.tsx +238 -0
- package/src/widgets/setup/detect-panel.tsx +72 -0
- package/src/widgets/setup/field-editor.tsx +265 -0
- package/src/widgets/setup/footer.tsx +107 -0
- package/src/widgets/setup/index.tsx +340 -0
- package/src/widgets/setup/layout-picker.tsx +96 -0
- package/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/src/widgets/setup/review-panel.tsx +140 -0
- package/src/widgets/setup/setup-model.test.ts +303 -0
- package/src/widgets/setup/setup-model.ts +188 -0
- package/src/widgets/tasks/index.tsx +129 -0
- package/src/widgets/tasks/task-detail.tsx +309 -0
- package/src/widgets/tasks/task-form.tsx +193 -0
- package/src/widgets/tasks/task-list.tsx +205 -0
- package/src/widgets/tasks/task-model.test.ts +473 -0
- package/src/widgets/tasks/task-model.ts +157 -0
- package/templates/AGENTS.md +20 -0
- package/templates/missions.yml +41 -0
- package/templates/skills/backend.md +30 -0
- package/templates/skills/frontend.md +30 -0
- package/templates/skills/general-worker.md +30 -0
- package/templates/skills/researcher.md +69 -0
- package/templates/skills/reviewer.md +30 -0
- package/dist/attach.d.ts +0 -3
- package/dist/attach.js +0 -14
- package/dist/config.d.ts +0 -5
- package/dist/config.js +0 -269
- package/dist/detect.d.ts +0 -15
- package/dist/detect.js +0 -228
- package/dist/doctor.d.ts +0 -3
- package/dist/doctor.js +0 -66
- package/dist/init.d.ts +0 -4
- package/dist/init.js +0 -67
- package/dist/inspect.d.ts +0 -54
- package/dist/inspect.js +0 -118
- package/dist/launch.d.ts +0 -22
- package/dist/launch.js +0 -174
- package/dist/lib/dot-path.d.ts +0 -2
- package/dist/lib/dot-path.js +0 -17
- package/dist/lib/errors.d.ts +0 -29
- package/dist/lib/errors.js +0 -37
- package/dist/lib/launch-plan.d.ts +0 -6
- package/dist/lib/launch-plan.js +0 -40
- package/dist/lib/output.d.ts +0 -9
- package/dist/lib/output.js +0 -65
- package/dist/lib/session-monitor.d.ts +0 -12
- package/dist/lib/session-monitor.js +0 -160
- package/dist/lib/session-options.d.ts +0 -13
- package/dist/lib/session-options.js +0 -85
- package/dist/lib/sizes.d.ts +0 -16
- package/dist/lib/sizes.js +0 -36
- package/dist/lib/tmux.d.ts +0 -42
- package/dist/lib/tmux.js +0 -225
- package/dist/lib/yaml-io.d.ts +0 -10
- package/dist/lib/yaml-io.js +0 -24
- package/dist/ls.d.ts +0 -3
- package/dist/ls.js +0 -35
- package/dist/restart.d.ts +0 -4
- package/dist/restart.js +0 -13
- package/dist/status.d.ts +0 -3
- package/dist/status.js +0 -29
- package/dist/stop.d.ts +0 -3
- package/dist/stop.js +0 -21
- package/dist/types.d.ts +0 -41
- package/dist/types.js +0 -1
- package/dist/validate.d.ts +0 -4
- package/dist/validate.js +0 -172
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { loadMission, nextTaskId, saveTask, type Task } from "./task-store.ts";
|
|
11
|
+
import { appendEvent, readEvents, type OrchestratorEvent } from "./event-log.ts";
|
|
12
|
+
import { sendCommand, type PaneInfo } from "../widgets/lib/pane-comms.ts";
|
|
13
|
+
import type { OrchestratorConfig, OrchestratorState } from "./orchestrator.ts";
|
|
14
|
+
|
|
15
|
+
export interface ResearchState {
|
|
16
|
+
lastResearchAt: Record<string, string>;
|
|
17
|
+
missionStartAnalyzed: boolean;
|
|
18
|
+
milestoneTaskCounts: Record<string, number>;
|
|
19
|
+
activeResearchTaskId: string | null;
|
|
20
|
+
retryWindow: { timestamp: string; taskId: string }[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ResearchTrigger {
|
|
24
|
+
type: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
context?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ResearchConfigShape {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
triggers?: {
|
|
32
|
+
mission_start?: boolean;
|
|
33
|
+
milestone_progress?: number;
|
|
34
|
+
milestone_complete?: boolean;
|
|
35
|
+
periodic_minutes?: number;
|
|
36
|
+
retry_cluster?: boolean;
|
|
37
|
+
stall_detected?: boolean;
|
|
38
|
+
discovered_issue?: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RESEARCH_STATE_FILE = ".tasks/research-state.json";
|
|
43
|
+
const DEFAULT_COOLDOWN_MINUTES = 30;
|
|
44
|
+
const SPINNERS = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⠂⠒⠢⠆⠐⠠⠄◐◓◑◒✳|/\\-] /;
|
|
45
|
+
const SHELL_COMMANDS = new Set(["zsh", "bash", "sh", "fish"]);
|
|
46
|
+
const AGENT_PREFIXES = ["claude", "codex"];
|
|
47
|
+
const VERSION_PATTERN = /^\d+\.\d+/;
|
|
48
|
+
|
|
49
|
+
function getResearchStatePath(dir: string): string {
|
|
50
|
+
return join(dir, RESEARCH_STATE_FILE);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function defaultResearchState(): ResearchState {
|
|
54
|
+
return {
|
|
55
|
+
lastResearchAt: {},
|
|
56
|
+
missionStartAnalyzed: false,
|
|
57
|
+
milestoneTaskCounts: {},
|
|
58
|
+
activeResearchTaskId: null,
|
|
59
|
+
retryWindow: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function loadResearchState(dir: string): ResearchState {
|
|
64
|
+
const path = getResearchStatePath(dir);
|
|
65
|
+
if (!existsSync(path)) return defaultResearchState();
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as Partial<ResearchState>;
|
|
68
|
+
return {
|
|
69
|
+
lastResearchAt: parsed.lastResearchAt ?? {},
|
|
70
|
+
missionStartAnalyzed: parsed.missionStartAnalyzed ?? false,
|
|
71
|
+
milestoneTaskCounts: parsed.milestoneTaskCounts ?? {},
|
|
72
|
+
activeResearchTaskId: parsed.activeResearchTaskId ?? null,
|
|
73
|
+
retryWindow: Array.isArray(parsed.retryWindow) ? parsed.retryWindow : [],
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return defaultResearchState();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function saveResearchState(dir: string, state: ResearchState): void {
|
|
81
|
+
const tasksDir = join(dir, ".tasks");
|
|
82
|
+
if (!existsSync(tasksDir)) mkdirSync(tasksDir, { recursive: true });
|
|
83
|
+
const path = getResearchStatePath(dir);
|
|
84
|
+
const tmpPath = path + ".tmp";
|
|
85
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2) + "\n");
|
|
86
|
+
renameSync(tmpPath, path);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getResearchConfig(
|
|
90
|
+
config: Partial<OrchestratorConfig> & { research?: ResearchConfigShape },
|
|
91
|
+
) {
|
|
92
|
+
return config.research ?? {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasCooldown(
|
|
96
|
+
researchState: ResearchState,
|
|
97
|
+
type: string,
|
|
98
|
+
minutes = DEFAULT_COOLDOWN_MINUTES,
|
|
99
|
+
): boolean {
|
|
100
|
+
if (minutes <= 0) return false;
|
|
101
|
+
const last = researchState.lastResearchAt[type];
|
|
102
|
+
if (!last) return false;
|
|
103
|
+
return Date.now() - Date.parse(last) < minutes * 60_000;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function latestEventAfter(
|
|
107
|
+
events: OrchestratorEvent[],
|
|
108
|
+
type: string,
|
|
109
|
+
after: string | undefined,
|
|
110
|
+
): OrchestratorEvent | null {
|
|
111
|
+
const afterTs = after ? Date.parse(after) : 0;
|
|
112
|
+
const filtered = events
|
|
113
|
+
.filter((event) => event.type === type && Date.parse(event.timestamp) > afterTs)
|
|
114
|
+
.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
115
|
+
return filtered[0] ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function evaluateTriggers(
|
|
119
|
+
config: Partial<OrchestratorConfig> & { dir: string; research?: ResearchConfigShape },
|
|
120
|
+
_state: OrchestratorState,
|
|
121
|
+
researchState: ResearchState,
|
|
122
|
+
tasks: Task[],
|
|
123
|
+
events: OrchestratorEvent[],
|
|
124
|
+
): ResearchTrigger[] {
|
|
125
|
+
const research = getResearchConfig(config);
|
|
126
|
+
if (!research.enabled) return [];
|
|
127
|
+
if (researchState.activeResearchTaskId) return [];
|
|
128
|
+
|
|
129
|
+
const mission = loadMission(config.dir);
|
|
130
|
+
const triggers = research.triggers ?? {};
|
|
131
|
+
const results: ResearchTrigger[] = [];
|
|
132
|
+
|
|
133
|
+
if (triggers.mission_start && mission && !researchState.missionStartAnalyzed) {
|
|
134
|
+
if (!hasCooldown(researchState, "mission_start")) {
|
|
135
|
+
researchState.missionStartAnalyzed = true;
|
|
136
|
+
results.push({
|
|
137
|
+
type: "mission_start",
|
|
138
|
+
reason: `Mission "${mission.title}" started and needs an initial audit`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const milestoneStep = triggers.milestone_progress ?? 0;
|
|
144
|
+
if (mission && milestoneStep > 0) {
|
|
145
|
+
for (const milestone of mission.milestones) {
|
|
146
|
+
const doneCount = tasks.filter(
|
|
147
|
+
(task) =>
|
|
148
|
+
task.milestone === milestone.id &&
|
|
149
|
+
task.status === "done" &&
|
|
150
|
+
!task.tags.includes("research"),
|
|
151
|
+
).length;
|
|
152
|
+
const previousCount = researchState.milestoneTaskCounts[milestone.id] ?? 0;
|
|
153
|
+
const previousBucket = Math.floor(previousCount / milestoneStep);
|
|
154
|
+
const currentBucket = Math.floor(doneCount / milestoneStep);
|
|
155
|
+
if (doneCount > 0 && currentBucket > previousBucket) {
|
|
156
|
+
results.push({
|
|
157
|
+
type: "milestone_progress",
|
|
158
|
+
reason: `Milestone ${milestone.id} reached ${doneCount} completed task(s)`,
|
|
159
|
+
context: {
|
|
160
|
+
milestoneId: milestone.id,
|
|
161
|
+
completedCount: doneCount,
|
|
162
|
+
milestoneTitle: milestone.title,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
researchState.milestoneTaskCounts[milestone.id] = doneCount;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (triggers.milestone_complete) {
|
|
171
|
+
const event = latestEventAfter(
|
|
172
|
+
events,
|
|
173
|
+
"milestone_complete",
|
|
174
|
+
researchState.lastResearchAt.milestone_complete,
|
|
175
|
+
);
|
|
176
|
+
if (event && !hasCooldown(researchState, "milestone_complete")) {
|
|
177
|
+
results.push({
|
|
178
|
+
type: "milestone_complete",
|
|
179
|
+
reason: event.message,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
(triggers.periodic_minutes ?? 0) > 0 &&
|
|
186
|
+
!hasCooldown(researchState, "periodic", triggers.periodic_minutes)
|
|
187
|
+
) {
|
|
188
|
+
results.push({
|
|
189
|
+
type: "periodic",
|
|
190
|
+
reason: `Periodic research interval elapsed (${triggers.periodic_minutes} minute window)`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (triggers.retry_cluster) {
|
|
195
|
+
const windowStart = Date.now() - 15 * 60_000;
|
|
196
|
+
researchState.retryWindow = events
|
|
197
|
+
.filter((event) => event.type === "retry" && Date.parse(event.timestamp) >= windowStart)
|
|
198
|
+
.map((event) => ({ timestamp: event.timestamp, taskId: event.taskId ?? "unknown" }));
|
|
199
|
+
if (researchState.retryWindow.length >= 3 && !hasCooldown(researchState, "retry_cluster")) {
|
|
200
|
+
results.push({
|
|
201
|
+
type: "retry_cluster",
|
|
202
|
+
reason: `${researchState.retryWindow.length} retries clustered in the last 15 minutes`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (triggers.stall_detected) {
|
|
208
|
+
const event = latestEventAfter(events, "stall", researchState.lastResearchAt.stall_detected);
|
|
209
|
+
if (event && !hasCooldown(researchState, "stall_detected")) {
|
|
210
|
+
results.push({
|
|
211
|
+
type: "stall_detected",
|
|
212
|
+
reason: event.message,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (triggers.discovered_issue) {
|
|
218
|
+
const event = latestEventAfter(
|
|
219
|
+
events,
|
|
220
|
+
"discovered_issue",
|
|
221
|
+
researchState.lastResearchAt.discovered_issue,
|
|
222
|
+
);
|
|
223
|
+
if (event && !hasCooldown(researchState, "discovered_issue")) {
|
|
224
|
+
results.push({
|
|
225
|
+
type: "discovered_issue",
|
|
226
|
+
reason: event.message,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function promptInstructions(type: string): string {
|
|
235
|
+
switch (type) {
|
|
236
|
+
case "mission_start":
|
|
237
|
+
return `Perform an initial codebase-analysis and architecture-audit.\nIdentify major risks, missing contracts, and likely execution bottlenecks.`;
|
|
238
|
+
case "milestone_progress":
|
|
239
|
+
return `Perform a progress audit.\nCheck whether recent completed work is coherent, whether validation coverage still matches implementation, and whether follow-up tasks are needed.`;
|
|
240
|
+
case "milestone_complete":
|
|
241
|
+
return `Perform a completion review.\nLook for hidden regressions, weak evidence, and any missing remediation before the next milestone advances.`;
|
|
242
|
+
case "retry_cluster":
|
|
243
|
+
return `Perform an incident-analysis.\nFind why multiple retries are clustering and propose concrete corrective actions or task reshaping.`;
|
|
244
|
+
case "stall_detected":
|
|
245
|
+
return `Perform issue-triage on the stalled work.\nIdentify likely blockers, missing context, or decomposition problems.`;
|
|
246
|
+
case "discovered_issue":
|
|
247
|
+
return `Perform issue-triage on the newly discovered problem.\nClarify severity, scope, and the best next task to create.`;
|
|
248
|
+
case "periodic":
|
|
249
|
+
default:
|
|
250
|
+
return `Perform a continuous internal audit.\nReview architecture, tests, contracts, and task quality. Surface actionable findings only.`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function buildResearchPrompt(
|
|
255
|
+
dir: string,
|
|
256
|
+
type: string,
|
|
257
|
+
context: Record<string, unknown> = {},
|
|
258
|
+
): string {
|
|
259
|
+
const mission = loadMission(dir);
|
|
260
|
+
const taskId = typeof context.taskId === "string" ? context.taskId : "<TASK_ID>";
|
|
261
|
+
let prompt = `# Research Task: ${type}\n\n`;
|
|
262
|
+
|
|
263
|
+
if (mission) {
|
|
264
|
+
prompt += `## Mission: ${mission.title}\n`;
|
|
265
|
+
if (mission.description) prompt += `${mission.description}\n`;
|
|
266
|
+
prompt += "\n";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (context.reason) {
|
|
270
|
+
prompt += `## Trigger Context\n${String(context.reason)}\n\n`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (context.milestoneId || context.milestoneTitle || context.completedCount != null) {
|
|
274
|
+
prompt += `## Milestone Context\n`;
|
|
275
|
+
if (context.milestoneId) prompt += `Milestone: ${String(context.milestoneId)}\n`;
|
|
276
|
+
if (context.milestoneTitle) prompt += `Title: ${String(context.milestoneTitle)}\n`;
|
|
277
|
+
if (context.completedCount != null)
|
|
278
|
+
prompt += `Completed tasks: ${String(context.completedCount)}\n`;
|
|
279
|
+
prompt += "\n";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
prompt += `## Instructions\n${promptInstructions(type)}\n\n`;
|
|
283
|
+
prompt += `## Reporting Protocol\n`;
|
|
284
|
+
prompt += `1. Investigate only what is necessary to produce actionable findings.\n`;
|
|
285
|
+
prompt += `2. If you find concrete issues, update or create follow-up tasks as needed.\n`;
|
|
286
|
+
prompt += `3. Finish by recording your result with:\n`;
|
|
287
|
+
prompt += ` tmux-ide task done ${taskId} --proof "key findings and evidence" --summary "short research takeaway"\n`;
|
|
288
|
+
prompt += `4. Include severity, impact, and the recommended next action in your proof.\n`;
|
|
289
|
+
|
|
290
|
+
return prompt.trim();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isIdlePane(pane: PaneInfo): boolean {
|
|
294
|
+
const cmd = pane.currentCommand.toLowerCase();
|
|
295
|
+
if (SHELL_COMMANDS.has(cmd)) return true;
|
|
296
|
+
if (AGENT_PREFIXES.some((prefix) => cmd.startsWith(prefix))) return !SPINNERS.test(pane.title);
|
|
297
|
+
if (VERSION_PATTERN.test(cmd)) return !SPINNERS.test(pane.title);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function findResearchPane(panes: PaneInfo[], masterPane: string | null): PaneInfo | null {
|
|
302
|
+
const explicit = panes.find((pane) => pane.role === "researcher" && isIdlePane(pane));
|
|
303
|
+
if (explicit) return explicit;
|
|
304
|
+
|
|
305
|
+
const idleFallback = panes.filter((pane) => {
|
|
306
|
+
if (pane.role === "lead") return false;
|
|
307
|
+
if (masterPane && pane.title === masterPane) return false;
|
|
308
|
+
return isIdlePane(pane);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (idleFallback.length < 2) return null;
|
|
312
|
+
return idleFallback[0] ?? null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function activeMilestoneId(dir: string): string | null {
|
|
316
|
+
const mission = loadMission(dir);
|
|
317
|
+
return mission?.milestones.find((milestone) => milestone.status === "active")?.id ?? null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function dispatchResearch(
|
|
321
|
+
config: Pick<OrchestratorConfig, "session" | "dir" | "masterPane" | "maxConcurrentAgents"> & {
|
|
322
|
+
research?: ResearchConfigShape;
|
|
323
|
+
},
|
|
324
|
+
state: OrchestratorState,
|
|
325
|
+
researchState: ResearchState,
|
|
326
|
+
tasks: Task[],
|
|
327
|
+
panes: PaneInfo[],
|
|
328
|
+
triggerOverride?: ResearchTrigger,
|
|
329
|
+
): Task | null {
|
|
330
|
+
if (researchState.activeResearchTaskId) return null;
|
|
331
|
+
|
|
332
|
+
const inProgressCount = tasks.filter((task) => task.status === "in-progress").length;
|
|
333
|
+
if (inProgressCount >= config.maxConcurrentAgents - 1) return null;
|
|
334
|
+
|
|
335
|
+
const triggers = triggerOverride
|
|
336
|
+
? [triggerOverride]
|
|
337
|
+
: evaluateTriggers(config, state, researchState, tasks, readEvents(config.dir));
|
|
338
|
+
const trigger = triggers[0];
|
|
339
|
+
if (!trigger) return null;
|
|
340
|
+
|
|
341
|
+
const pane = findResearchPane(panes, config.masterPane);
|
|
342
|
+
if (!pane) return null;
|
|
343
|
+
|
|
344
|
+
const id = nextTaskId(config.dir);
|
|
345
|
+
const now = new Date().toISOString();
|
|
346
|
+
const task: Task = {
|
|
347
|
+
id,
|
|
348
|
+
title: `Research: ${trigger.type.replace(/_/g, " ")}`,
|
|
349
|
+
description: trigger.reason,
|
|
350
|
+
goal: null,
|
|
351
|
+
status: "in-progress",
|
|
352
|
+
assignee: pane.name ?? pane.title,
|
|
353
|
+
priority: 3,
|
|
354
|
+
created: now,
|
|
355
|
+
updated: now,
|
|
356
|
+
tags: ["research", trigger.type],
|
|
357
|
+
proof: null,
|
|
358
|
+
retryCount: 0,
|
|
359
|
+
maxRetries: 5,
|
|
360
|
+
lastError: null,
|
|
361
|
+
nextRetryAt: null,
|
|
362
|
+
depends_on: [],
|
|
363
|
+
milestone:
|
|
364
|
+
typeof trigger.context?.milestoneId === "string"
|
|
365
|
+
? trigger.context.milestoneId
|
|
366
|
+
: activeMilestoneId(config.dir),
|
|
367
|
+
specialty: "researcher",
|
|
368
|
+
fulfills: [],
|
|
369
|
+
discoveredIssues: [],
|
|
370
|
+
salientSummary: null,
|
|
371
|
+
};
|
|
372
|
+
saveTask(config.dir, task);
|
|
373
|
+
|
|
374
|
+
const dispatchDir = join(config.dir, ".tasks", "dispatch");
|
|
375
|
+
if (!existsSync(dispatchDir)) mkdirSync(dispatchDir, { recursive: true });
|
|
376
|
+
const prompt = buildResearchPrompt(config.dir, trigger.type, {
|
|
377
|
+
...trigger.context,
|
|
378
|
+
reason: trigger.reason,
|
|
379
|
+
taskId: id,
|
|
380
|
+
});
|
|
381
|
+
const filename = `research-${id}.md`;
|
|
382
|
+
writeFileSync(join(dispatchDir, filename), prompt);
|
|
383
|
+
|
|
384
|
+
const sent = sendCommand(
|
|
385
|
+
config.session,
|
|
386
|
+
pane.id,
|
|
387
|
+
`Read and execute: .tasks/dispatch/${filename}`,
|
|
388
|
+
);
|
|
389
|
+
if (!sent) {
|
|
390
|
+
task.status = "todo";
|
|
391
|
+
task.assignee = null;
|
|
392
|
+
task.updated = new Date().toISOString();
|
|
393
|
+
saveTask(config.dir, task);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
researchState.activeResearchTaskId = id;
|
|
398
|
+
researchState.lastResearchAt[trigger.type] = now;
|
|
399
|
+
saveResearchState(config.dir, researchState);
|
|
400
|
+
state.lastActivity.set(pane.id, Date.now());
|
|
401
|
+
|
|
402
|
+
appendEvent(config.dir, {
|
|
403
|
+
timestamp: now,
|
|
404
|
+
type: "research_dispatch",
|
|
405
|
+
taskId: id,
|
|
406
|
+
target: pane.title,
|
|
407
|
+
researchType: trigger.type,
|
|
408
|
+
message: `Dispatched research task ${id} (${trigger.type}) to ${pane.title}`,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return task;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function researchLibraryFile(type: string): string {
|
|
415
|
+
switch (type) {
|
|
416
|
+
case "mission_start":
|
|
417
|
+
case "milestone_progress":
|
|
418
|
+
case "milestone_complete":
|
|
419
|
+
case "periodic":
|
|
420
|
+
return "research-findings.md";
|
|
421
|
+
case "retry_cluster":
|
|
422
|
+
case "stall_detected":
|
|
423
|
+
case "discovered_issue":
|
|
424
|
+
case "issue-triage":
|
|
425
|
+
case "incident-analysis":
|
|
426
|
+
return "learnings.md";
|
|
427
|
+
case "architecture-audit":
|
|
428
|
+
case "codebase-analysis":
|
|
429
|
+
return "architecture.md";
|
|
430
|
+
case "test-coverage":
|
|
431
|
+
return "testing.md";
|
|
432
|
+
case "contract-audit":
|
|
433
|
+
return "validation.md";
|
|
434
|
+
case "code-review":
|
|
435
|
+
return "reviews.md";
|
|
436
|
+
default:
|
|
437
|
+
return "research-findings.md";
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function processResearchCompletion(
|
|
442
|
+
config: Pick<OrchestratorConfig, "dir">,
|
|
443
|
+
researchState: ResearchState,
|
|
444
|
+
task: Task,
|
|
445
|
+
): void {
|
|
446
|
+
if (!task.tags.includes("research")) return;
|
|
447
|
+
|
|
448
|
+
const researchType = task.tags.find((tag) => tag !== "research") ?? "research";
|
|
449
|
+
const summary = task.salientSummary ?? task.proof?.notes ?? task.description ?? task.title;
|
|
450
|
+
const libraryDir = join(config.dir, ".tmux-ide", "library");
|
|
451
|
+
if (!existsSync(libraryDir)) mkdirSync(libraryDir, { recursive: true });
|
|
452
|
+
|
|
453
|
+
const filename = researchLibraryFile(researchType);
|
|
454
|
+
const body = [`## ${task.id}: ${task.title}`, `Type: ${researchType}`, summary, "---", ""].join(
|
|
455
|
+
"\n",
|
|
456
|
+
);
|
|
457
|
+
appendFileSync(join(libraryDir, filename), body + "\n");
|
|
458
|
+
|
|
459
|
+
researchState.activeResearchTaskId =
|
|
460
|
+
researchState.activeResearchTaskId === task.id ? null : researchState.activeResearchTaskId;
|
|
461
|
+
researchState.lastResearchAt[researchType] = task.updated;
|
|
462
|
+
if (researchType === "mission_start") {
|
|
463
|
+
researchState.missionStartAnalyzed = true;
|
|
464
|
+
}
|
|
465
|
+
saveResearchState(config.dir, researchState);
|
|
466
|
+
|
|
467
|
+
appendEvent(config.dir, {
|
|
468
|
+
timestamp: task.updated,
|
|
469
|
+
type: "research_finding",
|
|
470
|
+
taskId: task.id,
|
|
471
|
+
researchType,
|
|
472
|
+
summary,
|
|
473
|
+
message: `Recorded research findings for task ${task.id}`,
|
|
474
|
+
});
|
|
475
|
+
}
|