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
package/src/ls.test.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from "bun:test";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const cli = join(__dirname, "..", "bin", "cli.js");
|
|
8
|
+
|
|
9
|
+
// ls.js uses execSync directly (not our tmux.js layer), so we test via CLI
|
|
10
|
+
// to avoid needing to mock execSync at the module level.
|
|
11
|
+
|
|
12
|
+
let origLog;
|
|
13
|
+
let logged;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
logged = [];
|
|
17
|
+
origLog = console.log;
|
|
18
|
+
console.log = (...a) => logged.push(a.join(" "));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
console.log = origLog;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("ls", () => {
|
|
26
|
+
it("returns sessions array via CLI --json", () => {
|
|
27
|
+
const result = spawnSync("node", [cli, "ls", "--json"], {
|
|
28
|
+
encoding: "utf-8",
|
|
29
|
+
timeout: 10000,
|
|
30
|
+
});
|
|
31
|
+
expect(result.status).toBe(0);
|
|
32
|
+
const output = JSON.parse(result.stdout);
|
|
33
|
+
expect(Array.isArray(output.sessions)).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("parses session list with expected fields", () => {
|
|
37
|
+
const result = spawnSync("node", [cli, "ls", "--json"], {
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
timeout: 10000,
|
|
40
|
+
});
|
|
41
|
+
const output = JSON.parse(result.stdout);
|
|
42
|
+
// If there are sessions, each should have name, created, attached fields
|
|
43
|
+
for (const session of output.sessions) {
|
|
44
|
+
expect(typeof session.name === "string").toBeTruthy();
|
|
45
|
+
expect(typeof session.created === "string").toBeTruthy();
|
|
46
|
+
expect(typeof session.attached === "boolean").toBeTruthy();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns valid JSON structure even if sessions exist", () => {
|
|
51
|
+
const result = spawnSync("node", [cli, "ls", "--json"], {
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
timeout: 10000,
|
|
54
|
+
});
|
|
55
|
+
expect(result.status).toBe(0);
|
|
56
|
+
const output = JSON.parse(result.stdout);
|
|
57
|
+
expect(Object.hasOwn(output, "sessions")).toBeTruthy();
|
|
58
|
+
expect(Array.isArray(output.sessions)).toBeTruthy();
|
|
59
|
+
// Each session must have the expected shape
|
|
60
|
+
for (const s of output.sessions) {
|
|
61
|
+
expect(typeof s.name === "string" && s.name.length > 0).toBeTruthy();
|
|
62
|
+
expect(typeof s.created === "string").toBeTruthy();
|
|
63
|
+
expect(typeof s.attached === "boolean").toBeTruthy();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("prints human-readable output without --json", () => {
|
|
68
|
+
// Just verify it doesn't crash and produces output
|
|
69
|
+
const result = spawnSync("node", [cli, "ls"], {
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
timeout: 10000,
|
|
72
|
+
});
|
|
73
|
+
expect(result.status).toBe(0);
|
|
74
|
+
expect(result.stdout.length > 0).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/ls.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export async function ls({ json }: { json?: boolean } = {}): Promise<void> {
|
|
4
|
+
let raw: string;
|
|
5
|
+
try {
|
|
6
|
+
raw = execSync(
|
|
7
|
+
'tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}"',
|
|
8
|
+
{ encoding: "utf-8" },
|
|
9
|
+
).trim();
|
|
10
|
+
} catch {
|
|
11
|
+
if (json) {
|
|
12
|
+
console.log(JSON.stringify({ sessions: [] }));
|
|
13
|
+
} else {
|
|
14
|
+
console.log("No tmux sessions running.");
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sessions = raw.split("\n").map((line) => {
|
|
20
|
+
const [name, created, attached] = line.split("|");
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
created: new Date(parseInt(created!) * 1000).toISOString(),
|
|
24
|
+
attached: attached !== "0",
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (json) {
|
|
29
|
+
console.log(JSON.stringify({ sessions }, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Table output
|
|
34
|
+
console.log("SESSION".padEnd(24) + "CREATED".padEnd(22) + "ATTACHED");
|
|
35
|
+
console.log("─".repeat(54));
|
|
36
|
+
for (const s of sessions) {
|
|
37
|
+
const date = new Date(s.created).toLocaleString();
|
|
38
|
+
console.log(s.name!.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no"));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { computeMetrics, loadMissionHistory } from "./lib/metrics.ts";
|
|
3
|
+
|
|
4
|
+
function fmtDuration(ms: number): string {
|
|
5
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
6
|
+
if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
|
|
7
|
+
const h = Math.floor(ms / 3600000);
|
|
8
|
+
const m = Math.round((ms % 3600000) / 60000);
|
|
9
|
+
return `${h}h ${m}m`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fmtPct(n: number): string {
|
|
13
|
+
return `${Math.round(n * 100)}%`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function metricsCommand(
|
|
17
|
+
targetDir: string | undefined,
|
|
18
|
+
{
|
|
19
|
+
json = false,
|
|
20
|
+
sub,
|
|
21
|
+
}: {
|
|
22
|
+
json?: boolean;
|
|
23
|
+
sub?: string;
|
|
24
|
+
},
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const dir = resolve(targetDir ?? ".");
|
|
27
|
+
|
|
28
|
+
switch (sub) {
|
|
29
|
+
case "agents": {
|
|
30
|
+
const metrics = computeMetrics(dir);
|
|
31
|
+
if (json) {
|
|
32
|
+
console.log(JSON.stringify(metrics.agents, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (metrics.agents.length === 0) {
|
|
36
|
+
console.log("No agent data yet.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log("Agent Performance:");
|
|
40
|
+
for (const a of metrics.agents) {
|
|
41
|
+
const specs = a.specialties.length > 0 ? ` [${a.specialties.join(", ")}]` : "";
|
|
42
|
+
console.log(` ${a.name}${specs}`);
|
|
43
|
+
console.log(
|
|
44
|
+
` Tasks: ${a.taskCount} Retries: ${a.retryCount} Utilization: ${fmtPct(a.utilization)}`,
|
|
45
|
+
);
|
|
46
|
+
console.log(
|
|
47
|
+
` Active: ${fmtDuration(a.activeTimeMs)} Idle: ${fmtDuration(a.idleTimeMs)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "timeline": {
|
|
53
|
+
const metrics = computeMetrics(dir);
|
|
54
|
+
if (json) {
|
|
55
|
+
console.log(JSON.stringify(metrics.timeline, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (metrics.timeline.length === 0) {
|
|
59
|
+
console.log("No timeline data yet.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log("Activity Timeline:");
|
|
63
|
+
for (const t of metrics.timeline.slice(-20)) {
|
|
64
|
+
const ts = new Date(t.timestamp).toLocaleTimeString();
|
|
65
|
+
console.log(
|
|
66
|
+
` ${ts} done:${t.completedTasks} active:${t.activeTasks} busy:${t.busyAgents} idle:${t.idleAgents}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "eval": {
|
|
72
|
+
const metrics = computeMetrics(dir);
|
|
73
|
+
if (json) {
|
|
74
|
+
const bottlenecks = [...metrics.tasks.byMilestone]
|
|
75
|
+
.sort((a, b) => b.durationMs - a.durationMs)
|
|
76
|
+
.slice(0, 3);
|
|
77
|
+
const highRetryAgents = [...metrics.agents]
|
|
78
|
+
.filter((a) => a.retryCount > 0)
|
|
79
|
+
.sort((a, b) => b.retryCount - a.retryCount);
|
|
80
|
+
const ranking = [...metrics.agents].sort((a, b) => b.utilization - a.utilization);
|
|
81
|
+
console.log(JSON.stringify({ bottlenecks, highRetryAgents, ranking }, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log("Evaluation:");
|
|
85
|
+
// Bottleneck milestones
|
|
86
|
+
const slowest = [...metrics.tasks.byMilestone]
|
|
87
|
+
.filter((m) => m.durationMs > 0)
|
|
88
|
+
.sort((a, b) => b.durationMs - a.durationMs);
|
|
89
|
+
if (slowest.length > 0) {
|
|
90
|
+
console.log(" Slowest milestones:");
|
|
91
|
+
for (const m of slowest.slice(0, 3)) {
|
|
92
|
+
console.log(
|
|
93
|
+
` ${m.id} ${m.title}: ${fmtDuration(m.durationMs)} (${m.completedCount}/${m.taskCount} tasks)`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// High-retry agents
|
|
98
|
+
const retryAgents = metrics.agents.filter((a) => a.retryCount > 0);
|
|
99
|
+
if (retryAgents.length > 0) {
|
|
100
|
+
console.log(" High-retry agents:");
|
|
101
|
+
for (const a of retryAgents.sort((x, y) => y.retryCount - x.retryCount)) {
|
|
102
|
+
console.log(` ${a.name}: ${a.retryCount} retries across ${a.taskCount} tasks`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Agent ranking by utilization
|
|
106
|
+
if (metrics.agents.length > 0) {
|
|
107
|
+
console.log(" Agent ranking (by utilization):");
|
|
108
|
+
for (const a of [...metrics.agents].sort((x, y) => y.utilization - x.utilization)) {
|
|
109
|
+
console.log(` ${a.name}: ${fmtPct(a.utilization)} util, ${a.taskCount} tasks`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "history": {
|
|
115
|
+
const history = loadMissionHistory(dir);
|
|
116
|
+
if (json) {
|
|
117
|
+
console.log(JSON.stringify(history, null, 2));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (history.length === 0) {
|
|
121
|
+
console.log("No mission history yet.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log("Mission History:");
|
|
125
|
+
for (const m of history) {
|
|
126
|
+
console.log(` ${m.title} [${m.status}]`);
|
|
127
|
+
console.log(` Completed: ${m.completedAt} Duration: ${fmtDuration(m.wallClockMs)}`);
|
|
128
|
+
console.log(
|
|
129
|
+
` Tasks: ${m.taskCount} (${fmtPct(m.completionRate)} done) Agents: ${m.agentCount} Milestones: ${m.milestonesCompleted}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "help":
|
|
135
|
+
console.log(`Usage: tmux-ide metrics [subcommand] [--json]
|
|
136
|
+
|
|
137
|
+
(default) Session summary — duration, task rates, agent utilization
|
|
138
|
+
agents Per-agent table — tasks, time, utilization, specialties
|
|
139
|
+
timeline Activity timeline — sampled every 5 minutes
|
|
140
|
+
eval Evaluation — bottleneck milestones, high-retry tasks, agent ranking
|
|
141
|
+
history Cross-mission comparison from history`);
|
|
142
|
+
break;
|
|
143
|
+
default: {
|
|
144
|
+
// Default: session summary
|
|
145
|
+
const metrics = computeMetrics(dir);
|
|
146
|
+
if (json) {
|
|
147
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
console.log("Session Metrics:");
|
|
151
|
+
console.log(` Status: ${metrics.session.status}`);
|
|
152
|
+
console.log(` Duration: ${fmtDuration(metrics.session.durationMs)}`);
|
|
153
|
+
console.log(` Agents: ${metrics.session.agentCount}`);
|
|
154
|
+
console.log();
|
|
155
|
+
console.log("Tasks:");
|
|
156
|
+
console.log(
|
|
157
|
+
` Total: ${metrics.tasks.total} Done: ${metrics.tasks.completed} Failed: ${metrics.tasks.failed}`,
|
|
158
|
+
);
|
|
159
|
+
console.log(
|
|
160
|
+
` Completion rate: ${fmtPct(metrics.tasks.completionRate)} Retry rate: ${fmtPct(metrics.tasks.retryRate)}`,
|
|
161
|
+
);
|
|
162
|
+
if (metrics.tasks.avgDurationMs > 0) {
|
|
163
|
+
console.log(
|
|
164
|
+
` Avg duration: ${fmtDuration(metrics.tasks.avgDurationMs)} Median: ${fmtDuration(metrics.tasks.medianDurationMs)} P90: ${fmtDuration(metrics.tasks.p90DurationMs)}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (metrics.mission.title) {
|
|
168
|
+
console.log();
|
|
169
|
+
console.log(`Mission: ${metrics.mission.title} [${metrics.mission.status}]`);
|
|
170
|
+
console.log(` Milestones: ${metrics.mission.milestonesCompleted} completed`);
|
|
171
|
+
console.log(` Validation: ${fmtPct(metrics.mission.validationPassRate)} pass rate`);
|
|
172
|
+
console.log(` Wall clock: ${fmtDuration(metrics.mission.wallClockMs)}`);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/notify.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { getSessionName } from "./lib/yaml-io.ts";
|
|
3
|
+
import { getSessionState } from "./lib/tmux.ts";
|
|
4
|
+
import { listSessionPanes, sendCommand } from "./widgets/lib/pane-comms.ts";
|
|
5
|
+
import { appendEvent } from "./lib/event-log.ts";
|
|
6
|
+
import { IdeError } from "./lib/errors.ts";
|
|
7
|
+
|
|
8
|
+
interface NotifyOptions {
|
|
9
|
+
message?: string;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Send a short notification message to the lead/master pane.
|
|
15
|
+
*
|
|
16
|
+
* This is the counterpart to `tmux-ide dispatch` — dispatch sends tasks
|
|
17
|
+
* TO agents, notify sends completion messages BACK to the master.
|
|
18
|
+
*
|
|
19
|
+
* The message is always kept under 200 chars to avoid triggering the
|
|
20
|
+
* paste preview in Claude Code's TUI.
|
|
21
|
+
*/
|
|
22
|
+
export async function notify(
|
|
23
|
+
targetDir: string | null | undefined,
|
|
24
|
+
opts: NotifyOptions,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const dir = resolve(targetDir ?? ".");
|
|
27
|
+
const { message, json } = opts;
|
|
28
|
+
const { name: session } = getSessionName(dir);
|
|
29
|
+
|
|
30
|
+
if (!message) {
|
|
31
|
+
throw new IdeError("Missing message. Usage: tmux-ide notify <message>", {
|
|
32
|
+
code: "USAGE",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const state = getSessionState(session);
|
|
37
|
+
if (!state.running) {
|
|
38
|
+
throw new IdeError(`Session "${session}" is not running`, {
|
|
39
|
+
code: "SESSION_NOT_FOUND",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const panes = listSessionPanes(session);
|
|
44
|
+
const leadPane = panes.find((p) => p.role === "lead") ?? panes[0];
|
|
45
|
+
|
|
46
|
+
if (!leadPane) {
|
|
47
|
+
throw new IdeError("No panes found in session", {
|
|
48
|
+
code: "PANE_NOT_FOUND",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Keep message under 200 chars to avoid paste preview
|
|
53
|
+
const short = message.length > 180 ? message.slice(0, 177) + "..." : message;
|
|
54
|
+
|
|
55
|
+
sendCommand(session, leadPane.id, short);
|
|
56
|
+
|
|
57
|
+
// Log notify event
|
|
58
|
+
appendEvent(dir, {
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
type: "notify",
|
|
61
|
+
target: leadPane.name ?? leadPane.title,
|
|
62
|
+
paneId: leadPane.id,
|
|
63
|
+
message: short,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (json) {
|
|
67
|
+
console.log(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
ok: true,
|
|
70
|
+
session,
|
|
71
|
+
target: {
|
|
72
|
+
paneId: leadPane.id,
|
|
73
|
+
name: leadPane.name,
|
|
74
|
+
title: leadPane.title,
|
|
75
|
+
role: leadPane.role,
|
|
76
|
+
},
|
|
77
|
+
message: short,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const label = leadPane.name ?? leadPane.title;
|
|
84
|
+
console.log(`Notified "${label}" (${leadPane.id}): ${short}`);
|
|
85
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getSessionName, readConfig } from "./lib/yaml-io.ts";
|
|
5
|
+
import { getSessionState } from "./lib/tmux.ts";
|
|
6
|
+
import { loadTasks, loadMission } from "./lib/task-store.ts";
|
|
7
|
+
import { readEvents } from "./lib/event-log.ts";
|
|
8
|
+
import { listSessionPanes } from "./widgets/lib/pane-comms.ts";
|
|
9
|
+
import { isAgentPane, isAgentBusy, agentIdentifier } from "./lib/orchestrator.ts";
|
|
10
|
+
|
|
11
|
+
function formatElapsed(ms: number): string {
|
|
12
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
13
|
+
if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
|
|
14
|
+
return `${(ms / 3600000).toFixed(1)}h`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatRelative(iso: string): string {
|
|
18
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
19
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s ago`;
|
|
20
|
+
if (ms < 3600000) return `${Math.round(ms / 60000)}m ago`;
|
|
21
|
+
return `${(ms / 3600000).toFixed(1)}h ago`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function orchestratorStatus(
|
|
25
|
+
targetDir: string | undefined,
|
|
26
|
+
{ json }: { json?: boolean } = {},
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const dir = resolve(targetDir ?? ".");
|
|
29
|
+
const { name: session } = getSessionName(dir);
|
|
30
|
+
|
|
31
|
+
// Read config
|
|
32
|
+
let orchConfig: {
|
|
33
|
+
autoDispatch: boolean;
|
|
34
|
+
dispatchMode: string;
|
|
35
|
+
pollInterval: number;
|
|
36
|
+
stallTimeout: number;
|
|
37
|
+
maxConcurrentAgents: number;
|
|
38
|
+
masterPane: string | null;
|
|
39
|
+
} | null = null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const { config } = readConfig(dir);
|
|
43
|
+
const orch = config.orchestrator;
|
|
44
|
+
if (orch) {
|
|
45
|
+
orchConfig = {
|
|
46
|
+
autoDispatch: orch.auto_dispatch ?? true,
|
|
47
|
+
dispatchMode: orch.dispatch_mode ?? "tasks",
|
|
48
|
+
pollInterval: orch.poll_interval ?? 5000,
|
|
49
|
+
stallTimeout: orch.stall_timeout ?? 300000,
|
|
50
|
+
maxConcurrentAgents: orch.max_concurrent_agents ?? 10,
|
|
51
|
+
masterPane: orch.master_pane ?? null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// No config or unreadable
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if session is running
|
|
59
|
+
const state = getSessionState(session);
|
|
60
|
+
const running = state.running && orchConfig?.autoDispatch !== undefined;
|
|
61
|
+
|
|
62
|
+
// Read persisted orchestrator state
|
|
63
|
+
const statePath = join(dir, ".tasks", "orchestrator-state.json");
|
|
64
|
+
let claimedTasks: string[] = [];
|
|
65
|
+
let taskClaimTimes: Record<string, number> = {};
|
|
66
|
+
|
|
67
|
+
if (existsSync(statePath)) {
|
|
68
|
+
try {
|
|
69
|
+
const data = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
70
|
+
claimedTasks = data.claimedTasks ?? [];
|
|
71
|
+
taskClaimTimes = data.taskClaimTimes ?? {};
|
|
72
|
+
} catch {
|
|
73
|
+
// Corrupted state
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Load tasks, panes
|
|
78
|
+
const tasks = loadTasks(dir);
|
|
79
|
+
const mission = loadMission(dir);
|
|
80
|
+
|
|
81
|
+
// Agent info from tmux panes
|
|
82
|
+
const agents: Array<{
|
|
83
|
+
name: string;
|
|
84
|
+
paneId: string;
|
|
85
|
+
state: "busy" | "idle";
|
|
86
|
+
currentTask: { id: string; title: string; elapsed: string } | null;
|
|
87
|
+
}> = [];
|
|
88
|
+
|
|
89
|
+
if (state.running) {
|
|
90
|
+
const panes = listSessionPanes(session);
|
|
91
|
+
const agentPanes = panes.filter((p) => isAgentPane(p));
|
|
92
|
+
|
|
93
|
+
for (const pane of agentPanes) {
|
|
94
|
+
const name = agentIdentifier(pane);
|
|
95
|
+
const busy = isAgentBusy(pane);
|
|
96
|
+
const assignedTask = tasks.find((t) => t.status === "in-progress" && t.assignee === name);
|
|
97
|
+
|
|
98
|
+
let currentTask: { id: string; title: string; elapsed: string } | null = null;
|
|
99
|
+
if (assignedTask) {
|
|
100
|
+
const claimTime = taskClaimTimes[assignedTask.id];
|
|
101
|
+
const elapsed = claimTime
|
|
102
|
+
? formatElapsed(Date.now() - claimTime)
|
|
103
|
+
: formatElapsed(Date.now() - new Date(assignedTask.updated).getTime());
|
|
104
|
+
currentTask = { id: assignedTask.id, title: assignedTask.title, elapsed };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
agents.push({
|
|
108
|
+
name,
|
|
109
|
+
paneId: pane.id,
|
|
110
|
+
state: busy ? "busy" : "idle",
|
|
111
|
+
currentTask,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Categorize tasks
|
|
117
|
+
const inProgressTasks = tasks
|
|
118
|
+
.filter((t) => t.status === "in-progress")
|
|
119
|
+
.map((t) => {
|
|
120
|
+
const claimTime = taskClaimTimes[t.id];
|
|
121
|
+
const elapsed = claimTime
|
|
122
|
+
? formatElapsed(Date.now() - claimTime)
|
|
123
|
+
: formatElapsed(Date.now() - new Date(t.updated).getTime());
|
|
124
|
+
return { id: t.id, title: t.title, assignee: t.assignee ?? "unassigned", elapsed };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const doneTaskIds = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
|
|
128
|
+
const pendingTasks = tasks
|
|
129
|
+
.filter((t) => t.status === "todo")
|
|
130
|
+
.sort((a, b) => a.priority - b.priority)
|
|
131
|
+
.map((t) => {
|
|
132
|
+
const blockedBy = t.depends_on.filter((dep) => !doneTaskIds.has(dep));
|
|
133
|
+
return {
|
|
134
|
+
id: t.id,
|
|
135
|
+
title: t.title,
|
|
136
|
+
priority: t.priority,
|
|
137
|
+
blocked: blockedBy.length > 0,
|
|
138
|
+
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const recentlyDone = tasks
|
|
143
|
+
.filter((t) => t.status === "done")
|
|
144
|
+
.sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime())
|
|
145
|
+
.slice(0, 5)
|
|
146
|
+
.map((t) => ({ id: t.id, title: t.title, completedAt: t.updated }));
|
|
147
|
+
|
|
148
|
+
// Recent events
|
|
149
|
+
const allEvents = readEvents(dir);
|
|
150
|
+
const recentEvents = allEvents.slice(-10).reverse();
|
|
151
|
+
|
|
152
|
+
const data = {
|
|
153
|
+
running,
|
|
154
|
+
config: orchConfig,
|
|
155
|
+
agents,
|
|
156
|
+
tasks: { inProgress: inProgressTasks, pending: pendingTasks, recentlyDone },
|
|
157
|
+
claims: { count: claimedTasks.length, taskIds: claimedTasks },
|
|
158
|
+
recentEvents,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (json) {
|
|
162
|
+
console.log(JSON.stringify(data, null, 2));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Human-readable output
|
|
167
|
+
const noColor = "NO_COLOR" in process.env;
|
|
168
|
+
const bold = (s: string) => (noColor ? s : `\x1b[1m${s}\x1b[22m`);
|
|
169
|
+
const dim = (s: string) => (noColor ? s : `\x1b[2m${s}\x1b[22m`);
|
|
170
|
+
const green = (s: string) => (noColor ? s : `\x1b[32m${s}\x1b[39m`);
|
|
171
|
+
const yellow = (s: string) => (noColor ? s : `\x1b[33m${s}\x1b[39m`);
|
|
172
|
+
|
|
173
|
+
console.log(`${bold("Orchestrator:")} ${running ? green("running") : dim("stopped")}`);
|
|
174
|
+
|
|
175
|
+
if (orchConfig) {
|
|
176
|
+
const stallMin = Math.round(orchConfig.stallTimeout / 60000);
|
|
177
|
+
console.log(
|
|
178
|
+
` Poll: ${orchConfig.pollInterval}ms | Stall: ${stallMin}m | Mode: ${orchConfig.dispatchMode} | Auto-dispatch: ${orchConfig.autoDispatch ? "on" : "off"}`,
|
|
179
|
+
);
|
|
180
|
+
console.log(` Max concurrent: ${orchConfig.maxConcurrentAgents}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (mission) {
|
|
184
|
+
console.log(`\n${bold("Mission:")} ${mission.title}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Agents
|
|
188
|
+
const busyCount = agents.filter((a) => a.state === "busy").length;
|
|
189
|
+
console.log(`\n${bold("Agents")} (${busyCount} busy / ${agents.length} total):`);
|
|
190
|
+
if (agents.length === 0) {
|
|
191
|
+
console.log(` ${dim("No agents detected")}`);
|
|
192
|
+
}
|
|
193
|
+
for (const agent of agents) {
|
|
194
|
+
const stateLabel = agent.state === "busy" ? yellow("busy") : green("idle");
|
|
195
|
+
const taskLabel = agent.currentTask
|
|
196
|
+
? ` -> ${agent.currentTask.id} "${agent.currentTask.title}" (${agent.currentTask.elapsed})`
|
|
197
|
+
: "";
|
|
198
|
+
console.log(` ${agent.name.padEnd(12)} ${agent.paneId.padEnd(5)} ${stateLabel}${taskLabel}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// In-progress
|
|
202
|
+
if (inProgressTasks.length > 0) {
|
|
203
|
+
console.log(`\n${bold("In-progress")} (${inProgressTasks.length}):`);
|
|
204
|
+
for (const t of inProgressTasks) {
|
|
205
|
+
console.log(` ${t.id} "${t.title}" @${t.assignee} ${dim(t.elapsed)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Pending
|
|
210
|
+
if (pendingTasks.length > 0) {
|
|
211
|
+
console.log(`\n${bold("Pending")} (${pendingTasks.length}):`);
|
|
212
|
+
for (const t of pendingTasks.slice(0, 10)) {
|
|
213
|
+
const blocked = t.blocked ? dim(` (blocked by ${t.blockedBy!.join(", ")})`) : "";
|
|
214
|
+
console.log(` ${t.id} "${t.title}" P${t.priority}${blocked}`);
|
|
215
|
+
}
|
|
216
|
+
if (pendingTasks.length > 10) {
|
|
217
|
+
console.log(` ${dim(`... and ${pendingTasks.length - 10} more`)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Recent events
|
|
222
|
+
if (recentEvents.length > 0) {
|
|
223
|
+
console.log(`\n${bold("Recent Events")}:`);
|
|
224
|
+
for (const evt of recentEvents) {
|
|
225
|
+
console.log(` ${dim(formatRelative(evt.timestamp))} ${evt.type.padEnd(15)} ${evt.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|