tmux-ide 1.2.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 +33 -14
- package/src/__tests__/support.ts +186 -0
- package/src/{attach.js → attach.ts} +7 -4
- 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.js → detect.ts} +31 -15
- 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.js → inspect.ts} +95 -10
- 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.js → output.ts} +18 -10
- 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.js → session-options.ts} +11 -6
- 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.js → sizes.ts} +4 -4
- 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.js → yaml-io.ts} +5 -4
- package/src/ls.test.ts +76 -0
- package/src/{ls.js → ls.ts} +4 -4
- 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/src/config.js +0 -341
- package/src/doctor.js +0 -91
- package/src/init.js +0 -73
- package/src/launch.js +0 -245
- package/src/lib/dot-path.js +0 -16
- package/src/lib/errors.js +0 -35
- package/src/lib/launch-plan.js +0 -49
- package/src/lib/session-monitor.js +0 -179
- package/src/lib/tmux.js +0 -237
- package/src/restart.js +0 -16
- package/src/status.js +0 -35
- package/src/stop.js +0 -25
- package/src/validate.js +0 -154
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
ensureTasksDir,
|
|
7
|
+
saveMission,
|
|
8
|
+
saveGoal,
|
|
9
|
+
saveTask,
|
|
10
|
+
loadTask,
|
|
11
|
+
loadGoal,
|
|
12
|
+
loadMission,
|
|
13
|
+
loadTasksForGoal,
|
|
14
|
+
type Mission,
|
|
15
|
+
type Goal,
|
|
16
|
+
type Task,
|
|
17
|
+
} from "./task-store.ts";
|
|
18
|
+
import { appendEvent, readEvents } from "./event-log.ts";
|
|
19
|
+
import {
|
|
20
|
+
buildProjectDetail,
|
|
21
|
+
computeStats,
|
|
22
|
+
computeGoalProgress,
|
|
23
|
+
type SessionInfo,
|
|
24
|
+
} from "../command-center/discovery.ts";
|
|
25
|
+
import type { PaneInfo } from "../widgets/lib/pane-comms.ts";
|
|
26
|
+
import { tagContent, extractMarks, calculateStats } from "./authorship.ts";
|
|
27
|
+
|
|
28
|
+
let tmpDir: string;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-roundtrip-"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("task lifecycle round-trip", () => {
|
|
39
|
+
it("creates, dispatches, completes a task with events", () => {
|
|
40
|
+
ensureTasksDir(tmpDir);
|
|
41
|
+
|
|
42
|
+
const mission: Mission = {
|
|
43
|
+
title: "Build feature X",
|
|
44
|
+
description: "End-to-end feature delivery",
|
|
45
|
+
status: "active",
|
|
46
|
+
branch: null,
|
|
47
|
+
milestones: [],
|
|
48
|
+
created: "2026-03-21T10:00:00Z",
|
|
49
|
+
updated: "2026-03-21T10:00:00Z",
|
|
50
|
+
};
|
|
51
|
+
saveMission(tmpDir, mission);
|
|
52
|
+
|
|
53
|
+
const goal: Goal = {
|
|
54
|
+
id: "01",
|
|
55
|
+
title: "Backend API",
|
|
56
|
+
description: "REST endpoints",
|
|
57
|
+
status: "in-progress",
|
|
58
|
+
acceptance: "All endpoints return 200",
|
|
59
|
+
priority: 1,
|
|
60
|
+
created: "2026-03-21T10:00:00Z",
|
|
61
|
+
updated: "2026-03-21T10:00:00Z",
|
|
62
|
+
assignee: null,
|
|
63
|
+
specialty: null,
|
|
64
|
+
milestone: null,
|
|
65
|
+
};
|
|
66
|
+
saveGoal(tmpDir, goal);
|
|
67
|
+
|
|
68
|
+
const task: Task = {
|
|
69
|
+
id: "001",
|
|
70
|
+
title: "Implement GET /users",
|
|
71
|
+
description: "List users endpoint",
|
|
72
|
+
goal: "01",
|
|
73
|
+
status: "todo",
|
|
74
|
+
assignee: null,
|
|
75
|
+
priority: 1,
|
|
76
|
+
created: "2026-03-21T10:00:00Z",
|
|
77
|
+
updated: "2026-03-21T10:00:00Z",
|
|
78
|
+
|
|
79
|
+
tags: ["backend"],
|
|
80
|
+
proof: null,
|
|
81
|
+
retryCount: 0,
|
|
82
|
+
maxRetries: 5,
|
|
83
|
+
lastError: null,
|
|
84
|
+
nextRetryAt: null,
|
|
85
|
+
depends_on: [],
|
|
86
|
+
milestone: null,
|
|
87
|
+
specialty: null,
|
|
88
|
+
fulfills: [],
|
|
89
|
+
discoveredIssues: [],
|
|
90
|
+
salientSummary: null,
|
|
91
|
+
};
|
|
92
|
+
saveTask(tmpDir, task);
|
|
93
|
+
|
|
94
|
+
// Simulate dispatch
|
|
95
|
+
const loaded = loadTask(tmpDir, "001")!;
|
|
96
|
+
expect(loaded).toBeTruthy();
|
|
97
|
+
loaded.assignee = "François";
|
|
98
|
+
loaded.status = "in-progress";
|
|
99
|
+
loaded.updated = "2026-03-21T10:05:00Z";
|
|
100
|
+
saveTask(tmpDir, loaded);
|
|
101
|
+
|
|
102
|
+
appendEvent(tmpDir, {
|
|
103
|
+
timestamp: "2026-03-21T10:05:00Z",
|
|
104
|
+
type: "dispatch",
|
|
105
|
+
taskId: "001",
|
|
106
|
+
agent: "François",
|
|
107
|
+
message: 'Dispatched "Implement GET /users" to François',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Simulate completion
|
|
111
|
+
const inProgress = loadTask(tmpDir, "001")!;
|
|
112
|
+
expect(inProgress.status).toBe("in-progress");
|
|
113
|
+
expect(inProgress.assignee).toBe("François");
|
|
114
|
+
inProgress.status = "done";
|
|
115
|
+
inProgress.proof = { tests: { passed: 1, total: 1 } };
|
|
116
|
+
inProgress.updated = "2026-03-21T10:30:00Z";
|
|
117
|
+
saveTask(tmpDir, inProgress);
|
|
118
|
+
|
|
119
|
+
appendEvent(tmpDir, {
|
|
120
|
+
timestamp: "2026-03-21T10:30:00Z",
|
|
121
|
+
type: "completion",
|
|
122
|
+
taskId: "001",
|
|
123
|
+
agent: "François",
|
|
124
|
+
message: 'Task "Implement GET /users" completed',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Verify final state
|
|
128
|
+
const done = loadTask(tmpDir, "001")!;
|
|
129
|
+
expect(done.status).toBe("done");
|
|
130
|
+
expect(done.proof).toEqual({ tests: { passed: 1, total: 1 } });
|
|
131
|
+
expect(done.assignee).toBe("François");
|
|
132
|
+
|
|
133
|
+
const events = readEvents(tmpDir);
|
|
134
|
+
expect(events.length).toBe(2);
|
|
135
|
+
expect(events[0]!.type).toBe("dispatch");
|
|
136
|
+
expect(events[1]!.type).toBe("completion");
|
|
137
|
+
expect(events[0]!.taskId).toBe("001");
|
|
138
|
+
expect(events[1]!.taskId).toBe("001");
|
|
139
|
+
|
|
140
|
+
const goalTasks = loadTasksForGoal(tmpDir, "01");
|
|
141
|
+
expect(goalTasks.length).toBe(1);
|
|
142
|
+
expect(goalTasks[0]!.status).toBe("done");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("discovery round-trip", () => {
|
|
147
|
+
it("builds project detail from on-disk data with mock panes", () => {
|
|
148
|
+
ensureTasksDir(tmpDir);
|
|
149
|
+
|
|
150
|
+
const mission: Mission = {
|
|
151
|
+
title: "Test Project",
|
|
152
|
+
description: "Integration test",
|
|
153
|
+
status: "active",
|
|
154
|
+
branch: null,
|
|
155
|
+
milestones: [],
|
|
156
|
+
created: "2026-03-21T10:00:00Z",
|
|
157
|
+
updated: "2026-03-21T10:00:00Z",
|
|
158
|
+
};
|
|
159
|
+
saveMission(tmpDir, mission);
|
|
160
|
+
|
|
161
|
+
const goal: Goal = {
|
|
162
|
+
id: "01",
|
|
163
|
+
title: "Frontend",
|
|
164
|
+
description: "Build UI",
|
|
165
|
+
status: "in-progress",
|
|
166
|
+
acceptance: "UI renders",
|
|
167
|
+
priority: 1,
|
|
168
|
+
created: "2026-03-21T10:00:00Z",
|
|
169
|
+
updated: "2026-03-21T10:00:00Z",
|
|
170
|
+
assignee: "François",
|
|
171
|
+
specialty: "frontend",
|
|
172
|
+
};
|
|
173
|
+
saveGoal(tmpDir, goal);
|
|
174
|
+
|
|
175
|
+
const task: Task = {
|
|
176
|
+
id: "001",
|
|
177
|
+
title: "Build header",
|
|
178
|
+
description: "",
|
|
179
|
+
goal: "01",
|
|
180
|
+
status: "in-progress",
|
|
181
|
+
assignee: "François",
|
|
182
|
+
priority: 1,
|
|
183
|
+
created: "2026-03-21T10:00:00Z",
|
|
184
|
+
updated: "2026-03-21T10:05:00Z",
|
|
185
|
+
|
|
186
|
+
tags: [],
|
|
187
|
+
proof: null,
|
|
188
|
+
retryCount: 0,
|
|
189
|
+
maxRetries: 5,
|
|
190
|
+
lastError: null,
|
|
191
|
+
nextRetryAt: null,
|
|
192
|
+
depends_on: [],
|
|
193
|
+
milestone: null,
|
|
194
|
+
specialty: null,
|
|
195
|
+
fulfills: [],
|
|
196
|
+
discoveredIssues: [],
|
|
197
|
+
salientSummary: null,
|
|
198
|
+
};
|
|
199
|
+
saveTask(tmpDir, task);
|
|
200
|
+
|
|
201
|
+
const mockPane: PaneInfo = {
|
|
202
|
+
id: "%1",
|
|
203
|
+
index: 0,
|
|
204
|
+
title: "Agent 1",
|
|
205
|
+
currentCommand: "claude",
|
|
206
|
+
width: 80,
|
|
207
|
+
height: 24,
|
|
208
|
+
active: false,
|
|
209
|
+
role: null,
|
|
210
|
+
name: null,
|
|
211
|
+
type: null,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const info: SessionInfo = {
|
|
215
|
+
name: "test-session",
|
|
216
|
+
dir: tmpDir,
|
|
217
|
+
mission,
|
|
218
|
+
goals: [goal],
|
|
219
|
+
tasks: [task],
|
|
220
|
+
panes: [mockPane],
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const detail = buildProjectDetail(info);
|
|
224
|
+
expect(detail.session).toBe("test-session");
|
|
225
|
+
expect(detail.dir).toBe(tmpDir);
|
|
226
|
+
expect(detail.mission).toBeTruthy();
|
|
227
|
+
expect(detail.mission!.created).toBe("2026-03-21T10:00:00Z");
|
|
228
|
+
expect(detail.mission!.updated).toBe("2026-03-21T10:00:00Z");
|
|
229
|
+
expect(detail.tasks.length).toBe(1);
|
|
230
|
+
expect(detail.tasks[0]!.created).toBe("2026-03-21T10:00:00Z");
|
|
231
|
+
expect(detail.tasks[0]!.updated).toBe("2026-03-21T10:05:00Z");
|
|
232
|
+
expect(detail.goals.length).toBe(1);
|
|
233
|
+
expect(detail.goals[0]!.assignee).toBe("François");
|
|
234
|
+
expect(detail.goals[0]!.specialty).toBe("frontend");
|
|
235
|
+
expect(detail.agents.length).toBe(1);
|
|
236
|
+
expect(detail.agents[0]!.isBusy).toBe(false);
|
|
237
|
+
|
|
238
|
+
const stats = computeStats(info);
|
|
239
|
+
expect(stats.totalTasks).toBe(1);
|
|
240
|
+
expect(stats.doneTasks).toBe(0);
|
|
241
|
+
expect(stats.agents).toBe(1);
|
|
242
|
+
|
|
243
|
+
const progress = computeGoalProgress([goal], [task]);
|
|
244
|
+
expect(progress.length).toBe(1);
|
|
245
|
+
expect(progress[0]!.progress).toBe(0);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("normalizer backward compatibility", () => {
|
|
250
|
+
it("fills defaults for legacy task missing retry and depends_on fields", () => {
|
|
251
|
+
ensureTasksDir(tmpDir);
|
|
252
|
+
writeFileSync(
|
|
253
|
+
join(tmpDir, ".tasks", "tasks", "001-legacy.json"),
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
id: "001",
|
|
256
|
+
title: "Legacy task",
|
|
257
|
+
description: "",
|
|
258
|
+
goal: null,
|
|
259
|
+
status: "todo",
|
|
260
|
+
assignee: null,
|
|
261
|
+
priority: 1,
|
|
262
|
+
created: "2026-01-01T00:00:00Z",
|
|
263
|
+
updated: "2026-01-01T00:00:00Z",
|
|
264
|
+
|
|
265
|
+
tags: [],
|
|
266
|
+
proof: { note: "old note format" },
|
|
267
|
+
}) + "\n",
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const task = loadTask(tmpDir, "001")!;
|
|
271
|
+
expect(task).toBeTruthy();
|
|
272
|
+
expect(task.retryCount).toBe(0);
|
|
273
|
+
expect(task.maxRetries).toBe(5);
|
|
274
|
+
expect(task.lastError).toBe(null);
|
|
275
|
+
expect(task.nextRetryAt).toBe(null);
|
|
276
|
+
expect(task.depends_on).toEqual([]);
|
|
277
|
+
expect(task.proof!.notes).toBe("old note format");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("fills defaults for legacy goal missing assignee, specialty, timestamps", () => {
|
|
281
|
+
ensureTasksDir(tmpDir);
|
|
282
|
+
writeFileSync(
|
|
283
|
+
join(tmpDir, ".tasks", "goals", "01-legacy.json"),
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
id: "01",
|
|
286
|
+
title: "Legacy goal",
|
|
287
|
+
description: "",
|
|
288
|
+
status: "todo",
|
|
289
|
+
acceptance: "",
|
|
290
|
+
priority: 1,
|
|
291
|
+
}) + "\n",
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const goal = loadGoal(tmpDir, "01")!;
|
|
295
|
+
expect(goal).toBeTruthy();
|
|
296
|
+
expect(goal.assignee).toBe(null);
|
|
297
|
+
expect(goal.specialty).toBe(null);
|
|
298
|
+
expect(goal.created).toBe("1970-01-01T00:00:00.000Z");
|
|
299
|
+
expect(goal.updated).toBe("1970-01-01T00:00:00.000Z");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("fills defaults for legacy mission missing timestamps", () => {
|
|
303
|
+
ensureTasksDir(tmpDir);
|
|
304
|
+
writeFileSync(
|
|
305
|
+
join(tmpDir, ".tasks", "mission.json"),
|
|
306
|
+
JSON.stringify({
|
|
307
|
+
title: "Legacy mission",
|
|
308
|
+
description: "Old format",
|
|
309
|
+
}) + "\n",
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const mission = loadMission(tmpDir)!;
|
|
313
|
+
expect(mission).toBeTruthy();
|
|
314
|
+
expect(mission.title).toBe("Legacy mission");
|
|
315
|
+
expect(mission.created).toBe("1970-01-01T00:00:00.000Z");
|
|
316
|
+
expect(mission.updated).toBe("1970-01-01T00:00:00.000Z");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("authorship round-trip", () => {
|
|
321
|
+
it("tags content, extracts marks, and calculates stats", () => {
|
|
322
|
+
const original = "# Plan\nSome content here\n\n## Details\nMore text";
|
|
323
|
+
|
|
324
|
+
const tagged = tagContent(original, "ai:Claude");
|
|
325
|
+
|
|
326
|
+
const { content, marks } = extractMarks(tagged);
|
|
327
|
+
expect(content).toBe(original);
|
|
328
|
+
expect(marks).toBeTruthy();
|
|
329
|
+
expect(marks.marks).toBeTruthy();
|
|
330
|
+
|
|
331
|
+
const markList = Object.values(marks.marks);
|
|
332
|
+
expect(markList.length > 0).toBeTruthy();
|
|
333
|
+
const aiMark = markList.find((m) => m.by === "ai:Claude");
|
|
334
|
+
expect(aiMark).toBeTruthy();
|
|
335
|
+
|
|
336
|
+
const stats = calculateStats(marks.marks);
|
|
337
|
+
expect(stats.aiPercent > 0).toBeTruthy();
|
|
338
|
+
expect(stats.totalChars > 0).toBeTruthy();
|
|
339
|
+
expect(stats.humanPercent).toBe(0);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { computeAgentStates, computePortPanes } from "./session-monitor.ts";
|
|
3
|
+
|
|
4
|
+
describe("computeAgentStates", () => {
|
|
5
|
+
it("returns null for non-agent panes", () => {
|
|
6
|
+
const panes = [
|
|
7
|
+
{ id: "%0", pid: "100", cmd: "zsh", title: "Shell" },
|
|
8
|
+
{ id: "%1", pid: "101", cmd: "node", title: "Dev Server" },
|
|
9
|
+
];
|
|
10
|
+
const states = computeAgentStates(panes);
|
|
11
|
+
expect(states.get("%0")).toBe(null);
|
|
12
|
+
expect(states.get("%1")).toBe(null);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("detects idle claude pane", () => {
|
|
16
|
+
const panes = [{ id: "%0", pid: "100", cmd: "claude", title: "Claude Code" }];
|
|
17
|
+
const states = computeAgentStates(panes);
|
|
18
|
+
expect(states.get("%0")).toBe("idle");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("detects busy claude pane with Braille spinner", () => {
|
|
22
|
+
const panes = [{ id: "%0", pid: "100", cmd: "claude", title: "\u280B Working on something" }];
|
|
23
|
+
const states = computeAgentStates(panes);
|
|
24
|
+
expect(states.get("%0")).toBe("busy");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("detects busy codex pane with spinner", () => {
|
|
28
|
+
const panes = [{ id: "%0", pid: "100", cmd: "codex", title: "\u2839 Thinking" }];
|
|
29
|
+
const states = computeAgentStates(panes);
|
|
30
|
+
expect(states.get("%0")).toBe("busy");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("handles case-insensitive command matching", () => {
|
|
34
|
+
const panes = [{ id: "%0", pid: "100", cmd: "Claude", title: "idle title" }];
|
|
35
|
+
const states = computeAgentStates(panes);
|
|
36
|
+
expect(states.get("%0")).toBe("idle");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles missing cmd and title gracefully", () => {
|
|
40
|
+
const panes = [{ id: "%0", pid: "100", cmd: undefined, title: undefined }];
|
|
41
|
+
const states = computeAgentStates(panes);
|
|
42
|
+
expect(states.get("%0")).toBe(null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("handles mixed panes correctly", () => {
|
|
46
|
+
const panes = [
|
|
47
|
+
{ id: "%0", pid: "100", cmd: "claude", title: "\u280B Busy" },
|
|
48
|
+
{ id: "%1", pid: "101", cmd: "zsh", title: "Shell" },
|
|
49
|
+
{ id: "%2", pid: "102", cmd: "claude", title: "Idle Claude" },
|
|
50
|
+
];
|
|
51
|
+
const states = computeAgentStates(panes);
|
|
52
|
+
expect(states.get("%0")).toBe("busy");
|
|
53
|
+
expect(states.get("%1")).toBe(null);
|
|
54
|
+
expect(states.get("%2")).toBe("idle");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("detects agent via @ide_role lead regardless of command", () => {
|
|
58
|
+
const panes = [{ id: "%0", pid: "100", cmd: "2.1.80", title: "Claude Code", role: "lead" }];
|
|
59
|
+
const states = computeAgentStates(panes);
|
|
60
|
+
expect(states.get("%0")).toBe("idle");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("detects busy teammate via @ide_role with spinner", () => {
|
|
64
|
+
const panes = [
|
|
65
|
+
{ id: "%0", pid: "100", cmd: "node", title: "\u2839 Working", role: "teammate" },
|
|
66
|
+
];
|
|
67
|
+
const states = computeAgentStates(panes);
|
|
68
|
+
expect(states.get("%0")).toBe("busy");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("ignores widget panes even with @ide_role", () => {
|
|
72
|
+
const panes = [{ id: "%0", pid: "100", cmd: "bun", title: "Explorer", role: "widget" }];
|
|
73
|
+
const states = computeAgentStates(panes);
|
|
74
|
+
expect(states.get("%0")).toBe(null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("falls back to command detection when no @ide_role is set", () => {
|
|
78
|
+
const panes = [{ id: "%0", pid: "100", cmd: "claude", title: "Agent 1" }];
|
|
79
|
+
const states = computeAgentStates(panes);
|
|
80
|
+
expect(states.get("%0")).toBe("idle");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("computePortPanes", () => {
|
|
85
|
+
it("returns empty set when no listeners", () => {
|
|
86
|
+
const panes = [{ id: "%0", pid: "100", cmd: "zsh", title: "Shell" }];
|
|
87
|
+
const result = computePortPanes(panes, {
|
|
88
|
+
listeners: new Set(),
|
|
89
|
+
tree: new Map(),
|
|
90
|
+
});
|
|
91
|
+
expect(result.size).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("maps a listener PID directly to its pane", () => {
|
|
95
|
+
const panes = [{ id: "%0", pid: "100", cmd: "node", title: "Dev" }];
|
|
96
|
+
const result = computePortPanes(panes, {
|
|
97
|
+
listeners: new Set(["100"]),
|
|
98
|
+
tree: new Map([["100", "1"]]),
|
|
99
|
+
});
|
|
100
|
+
expect(result.has("%0")).toBeTruthy();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("walks up the process tree to find pane owner", () => {
|
|
104
|
+
const panes = [{ id: "%0", pid: "100", cmd: "zsh", title: "Shell" }];
|
|
105
|
+
// Listener PID 300 -> parent 200 -> parent 100 (pane pid)
|
|
106
|
+
const tree = new Map([
|
|
107
|
+
["300", "200"],
|
|
108
|
+
["200", "100"],
|
|
109
|
+
["100", "1"],
|
|
110
|
+
]);
|
|
111
|
+
const result = computePortPanes(panes, {
|
|
112
|
+
listeners: new Set(["300"]),
|
|
113
|
+
tree,
|
|
114
|
+
});
|
|
115
|
+
expect(result.has("%0")).toBeTruthy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not match listener to unrelated pane", () => {
|
|
119
|
+
const panes = [{ id: "%0", pid: "100", cmd: "zsh", title: "Shell" }];
|
|
120
|
+
// Listener PID 300 walks up to 200 -> 1, never reaching 100
|
|
121
|
+
const tree = new Map([
|
|
122
|
+
["300", "200"],
|
|
123
|
+
["200", "1"],
|
|
124
|
+
["100", "1"],
|
|
125
|
+
]);
|
|
126
|
+
const result = computePortPanes(panes, {
|
|
127
|
+
listeners: new Set(["300"]),
|
|
128
|
+
tree,
|
|
129
|
+
});
|
|
130
|
+
expect(result.size).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("handles multiple panes with different listeners", () => {
|
|
134
|
+
const panes = [
|
|
135
|
+
{ id: "%0", pid: "100", cmd: "node", title: "Web" },
|
|
136
|
+
{ id: "%1", pid: "200", cmd: "node", title: "API" },
|
|
137
|
+
];
|
|
138
|
+
const tree = new Map([
|
|
139
|
+
["150", "100"],
|
|
140
|
+
["250", "200"],
|
|
141
|
+
["100", "1"],
|
|
142
|
+
["200", "1"],
|
|
143
|
+
]);
|
|
144
|
+
const result = computePortPanes(panes, {
|
|
145
|
+
listeners: new Set(["150", "250"]),
|
|
146
|
+
tree,
|
|
147
|
+
});
|
|
148
|
+
expect(result.has("%0")).toBeTruthy();
|
|
149
|
+
expect(result.has("%1")).toBeTruthy();
|
|
150
|
+
expect(result.size).toBe(2);
|
|
151
|
+
});
|
|
152
|
+
});
|