@revealui/harnesses 0.1.3 → 0.1.5
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/dist/chunk-JG6CAG4A.js +540 -0
- package/dist/chunk-JG6CAG4A.js.map +1 -0
- package/dist/{chunk-HH2PJYQN.js → chunk-WOOR63LH.js} +13 -14
- package/dist/chunk-WOOR63LH.js.map +1 -0
- package/dist/{chunk-JDI6B2IB.js → chunk-XXEKWC6F.js} +2 -2
- package/dist/{chunk-JDI6B2IB.js.map → chunk-XXEKWC6F.js.map} +1 -1
- package/dist/cli.js +9 -9
- package/dist/cli.js.map +1 -1
- package/dist/content/index.js +1 -1
- package/dist/index.js +3 -3
- package/dist/workboard/index.d.ts +115 -61
- package/dist/workboard/index.js +5 -1
- package/package.json +2 -2
- package/dist/chunk-FJGN6DTH.js +0 -387
- package/dist/chunk-FJGN6DTH.js.map +0 -1
- package/dist/chunk-HH2PJYQN.js.map +0 -1
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// src/workboard/file-lock.ts
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
openSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
writeSync
|
|
10
|
+
} from "fs";
|
|
11
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
12
|
+
var RETRY_MS = 50;
|
|
13
|
+
function isPidAlive(pid) {
|
|
14
|
+
try {
|
|
15
|
+
process.kill(pid, 0);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function acquireLock(lockPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
22
|
+
const deadline = Date.now() + timeoutMs;
|
|
23
|
+
while (Date.now() < deadline) {
|
|
24
|
+
try {
|
|
25
|
+
const fd = openSync(lockPath, "wx");
|
|
26
|
+
writeSync(fd, String(process.pid));
|
|
27
|
+
closeSync(fd);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err.code !== "EEXIST") return false;
|
|
31
|
+
try {
|
|
32
|
+
const holderPid = Number.parseInt(readFileSync(lockPath, "utf8").trim(), 10);
|
|
33
|
+
if (!(Number.isNaN(holderPid) || isPidAlive(holderPid))) {
|
|
34
|
+
unlinkSync(lockPath);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const spinUntil = Date.now() + RETRY_MS;
|
|
41
|
+
while (Date.now() < spinUntil) {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
function releaseLock(lockPath) {
|
|
48
|
+
try {
|
|
49
|
+
unlinkSync(lockPath);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function withLock(lockPath, fn, timeoutMs) {
|
|
54
|
+
const acquired = acquireLock(lockPath, timeoutMs);
|
|
55
|
+
if (!acquired) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Failed to acquire lock: ${lockPath} (timeout ${timeoutMs ?? DEFAULT_TIMEOUT_MS}ms)`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return fn();
|
|
62
|
+
} finally {
|
|
63
|
+
releaseLock(lockPath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function withLockAsync(lockPath, fn, timeoutMs) {
|
|
67
|
+
const acquired = acquireLock(lockPath, timeoutMs);
|
|
68
|
+
if (!acquired) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Failed to acquire lock: ${lockPath} (timeout ${timeoutMs ?? DEFAULT_TIMEOUT_MS}ms)`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return await fn();
|
|
75
|
+
} finally {
|
|
76
|
+
releaseLock(lockPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function atomicWriteSync(filePath, content) {
|
|
80
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
81
|
+
writeFileSync(tmpPath, content, "utf8");
|
|
82
|
+
renameSync(tmpPath, filePath);
|
|
83
|
+
}
|
|
84
|
+
function lockPathFor(workboardPath) {
|
|
85
|
+
return workboardPath.replace(/\.md$/, ".lock");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/workboard/session-identity.ts
|
|
89
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
90
|
+
function detectSessionType() {
|
|
91
|
+
try {
|
|
92
|
+
let pid = process.ppid;
|
|
93
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
94
|
+
if (!pid || pid <= 1) break;
|
|
95
|
+
const cmdline = readFileSync2(`/proc/${pid}/cmdline`, "utf8").replace(/\0/g, " ").toLowerCase();
|
|
96
|
+
if (cmdline.includes("zed")) return "zed";
|
|
97
|
+
if (cmdline.includes("cursor")) return "cursor";
|
|
98
|
+
const status = readFileSync2(`/proc/${pid}/status`, "utf8");
|
|
99
|
+
const m = status.match(/PPid:\s+(\d+)/);
|
|
100
|
+
if (!m) break;
|
|
101
|
+
pid = parseInt(m[1] ?? "0", 10);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
const termProgram = (process.env.TERM_PROGRAM ?? "").toLowerCase();
|
|
106
|
+
if (termProgram.includes("zed")) return "zed";
|
|
107
|
+
if (termProgram.includes("cursor")) return "cursor";
|
|
108
|
+
return "terminal";
|
|
109
|
+
}
|
|
110
|
+
function deriveSessionId(type, existingIds) {
|
|
111
|
+
const matching = existingIds.filter((id) => id.startsWith(`${type}-`)).map((id) => parseInt(id.split("-")[1] ?? "0", 10)).filter((n) => !Number.isNaN(n));
|
|
112
|
+
const maxN = matching.length > 0 ? Math.max(...matching) : 0;
|
|
113
|
+
return `${type}-${maxN + 1}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/workboard/workboard-manager.ts
|
|
117
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
118
|
+
import { readFile } from "fs/promises";
|
|
119
|
+
import { resolve } from "path";
|
|
120
|
+
var STALE_THRESHOLD_MS = 4 * 60 * 60 * 1e3;
|
|
121
|
+
var STARTING_STALE_MS = 60 * 60 * 1e3;
|
|
122
|
+
var SECTION_MAP = {
|
|
123
|
+
agents: "agents",
|
|
124
|
+
active_sessions: "agents",
|
|
125
|
+
sessions: "agents",
|
|
126
|
+
tasks: "tasks",
|
|
127
|
+
blocked: "blocked",
|
|
128
|
+
done: "done",
|
|
129
|
+
log: "log",
|
|
130
|
+
recent: "log"
|
|
131
|
+
};
|
|
132
|
+
function normalizeSectionName(title) {
|
|
133
|
+
const key = title.toLowerCase().replace(/\s+/g, "_");
|
|
134
|
+
return SECTION_MAP[key] || null;
|
|
135
|
+
}
|
|
136
|
+
function splitRow(line) {
|
|
137
|
+
if (!line.startsWith("|")) return null;
|
|
138
|
+
return line.split("|").slice(1, -1).map((c) => c.trim());
|
|
139
|
+
}
|
|
140
|
+
function isSeparatorRow(cells) {
|
|
141
|
+
return cells.length > 0 && cells.every((c) => /^[-:]+$/.test(c));
|
|
142
|
+
}
|
|
143
|
+
function parseTable(lines) {
|
|
144
|
+
if (lines.length < 2) return { columns: [], rows: [] };
|
|
145
|
+
const headerCells = splitRow(lines[0]);
|
|
146
|
+
if (!headerCells) return { columns: [], rows: [] };
|
|
147
|
+
const sepCells = splitRow(lines[1]);
|
|
148
|
+
if (!(sepCells && isSeparatorRow(sepCells))) return { columns: [], rows: [] };
|
|
149
|
+
const columns = headerCells.map(
|
|
150
|
+
(h) => h.toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "")
|
|
151
|
+
);
|
|
152
|
+
const rows = [];
|
|
153
|
+
for (let i = 2; i < lines.length; i++) {
|
|
154
|
+
const cells = splitRow(lines[i]);
|
|
155
|
+
if (!cells || isSeparatorRow(cells)) continue;
|
|
156
|
+
const row = {};
|
|
157
|
+
for (let j = 0; j < columns.length; j++) {
|
|
158
|
+
const col = columns[j];
|
|
159
|
+
const raw = (cells[j] || "").trim();
|
|
160
|
+
row[col] = raw === "\u2014" ? "" : raw;
|
|
161
|
+
}
|
|
162
|
+
rows.push(row);
|
|
163
|
+
}
|
|
164
|
+
return { columns, rows };
|
|
165
|
+
}
|
|
166
|
+
function parseWorkboard(content) {
|
|
167
|
+
const lines = content.split("\n");
|
|
168
|
+
const rawSections = [];
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const line = lines[i];
|
|
171
|
+
const m = line.match(/^##\s+(.+)/);
|
|
172
|
+
if (m?.[1]) rawSections.push({ title: m[1].trim(), line: i });
|
|
173
|
+
}
|
|
174
|
+
const state = {
|
|
175
|
+
preamble: [],
|
|
176
|
+
agents: [],
|
|
177
|
+
tasks: [],
|
|
178
|
+
blocked: [],
|
|
179
|
+
done: [],
|
|
180
|
+
log: [],
|
|
181
|
+
_extra: {}
|
|
182
|
+
};
|
|
183
|
+
const firstLine = rawSections.length > 0 ? rawSections[0]?.line : lines.length;
|
|
184
|
+
state.preamble = lines.slice(0, firstLine);
|
|
185
|
+
for (let i = 0; i < rawSections.length; i++) {
|
|
186
|
+
const sec = rawSections[i];
|
|
187
|
+
const nextLine = i + 1 < rawSections.length ? rawSections[i + 1]?.line : lines.length;
|
|
188
|
+
const body = lines.slice(sec.line + 1, nextLine);
|
|
189
|
+
const normalized = normalizeSectionName(sec.title);
|
|
190
|
+
if (normalized === "agents" || normalized === "tasks" || normalized === "blocked" || normalized === "done") {
|
|
191
|
+
const tableLines = body.filter((l) => l.startsWith("|"));
|
|
192
|
+
if (tableLines.length >= 2) {
|
|
193
|
+
const { rows } = parseTable(tableLines);
|
|
194
|
+
state[normalized].length = 0;
|
|
195
|
+
state[normalized].push(...rows);
|
|
196
|
+
}
|
|
197
|
+
} else if (normalized === "log") {
|
|
198
|
+
state.log = body.filter((l) => l.startsWith("- "));
|
|
199
|
+
} else {
|
|
200
|
+
state._extra[sec.title] = lines.slice(sec.line, nextLine).join("\n");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return state;
|
|
204
|
+
}
|
|
205
|
+
function padCell(value, width) {
|
|
206
|
+
const str = String(value || "\u2014");
|
|
207
|
+
return str.length >= width ? str : str + " ".repeat(width - str.length);
|
|
208
|
+
}
|
|
209
|
+
function serializeTable(headers, rows) {
|
|
210
|
+
if (headers.length === 0) return "(none)";
|
|
211
|
+
const widths = headers.map((h) => {
|
|
212
|
+
const dataMax = rows.reduce((max, row) => Math.max(max, String(row[h] || "\u2014").length), 0);
|
|
213
|
+
return Math.max(h.length, dataMax, 3);
|
|
214
|
+
});
|
|
215
|
+
const headerLine = `| ${headers.map((h, i) => padCell(h, widths[i])).join(" | ")} |`;
|
|
216
|
+
const sepLine = `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`;
|
|
217
|
+
const dataLines = rows.map(
|
|
218
|
+
(row) => `| ${headers.map((h, i) => padCell(row[h] || "", widths[i])).join(" | ")} |`
|
|
219
|
+
);
|
|
220
|
+
return [headerLine, sepLine, ...dataLines].join("\n");
|
|
221
|
+
}
|
|
222
|
+
function serializeWorkboard(state) {
|
|
223
|
+
const out = [];
|
|
224
|
+
if (state.preamble.length > 0) {
|
|
225
|
+
out.push(...state.preamble);
|
|
226
|
+
if (state.preamble[state.preamble.length - 1] !== "") out.push("");
|
|
227
|
+
}
|
|
228
|
+
out.push("## Agents", "");
|
|
229
|
+
out.push(
|
|
230
|
+
serializeTable(
|
|
231
|
+
["id", "env", "started", "task", "files", "updated"],
|
|
232
|
+
state.agents
|
|
233
|
+
),
|
|
234
|
+
""
|
|
235
|
+
);
|
|
236
|
+
out.push("## Tasks", "");
|
|
237
|
+
if (state.tasks.length > 0) {
|
|
238
|
+
out.push(
|
|
239
|
+
serializeTable(
|
|
240
|
+
["id", "task", "pri", "status", "owner", "gh", "updated", "notes"],
|
|
241
|
+
state.tasks
|
|
242
|
+
),
|
|
243
|
+
""
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
out.push("(none)", "");
|
|
247
|
+
}
|
|
248
|
+
out.push("## Blocked", "");
|
|
249
|
+
if (state.blocked.length > 0) {
|
|
250
|
+
out.push(
|
|
251
|
+
serializeTable(
|
|
252
|
+
["id", "task", "blocker", "gh", "notes"],
|
|
253
|
+
state.blocked
|
|
254
|
+
),
|
|
255
|
+
""
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
out.push("(none)", "");
|
|
259
|
+
}
|
|
260
|
+
out.push("## Done", "");
|
|
261
|
+
if (state.done.length > 0) {
|
|
262
|
+
out.push(
|
|
263
|
+
serializeTable(
|
|
264
|
+
["id", "task", "owner", "completed", "gh", "notes"],
|
|
265
|
+
state.done
|
|
266
|
+
),
|
|
267
|
+
""
|
|
268
|
+
);
|
|
269
|
+
} else {
|
|
270
|
+
out.push("(none)", "");
|
|
271
|
+
}
|
|
272
|
+
out.push("## Log", "");
|
|
273
|
+
if (state.log.length > 0) out.push(...state.log);
|
|
274
|
+
out.push("");
|
|
275
|
+
for (const content of Object.values(state._extra)) {
|
|
276
|
+
out.push(content, "");
|
|
277
|
+
}
|
|
278
|
+
return out.join("\n");
|
|
279
|
+
}
|
|
280
|
+
var WorkboardManager = class {
|
|
281
|
+
constructor(workboardPath) {
|
|
282
|
+
this.workboardPath = workboardPath;
|
|
283
|
+
const resolved = resolve(workboardPath);
|
|
284
|
+
if (!resolved.endsWith(".md")) {
|
|
285
|
+
throw new Error("Invalid workboard path: must be a .md file");
|
|
286
|
+
}
|
|
287
|
+
this.lockPath = lockPathFor(resolved);
|
|
288
|
+
}
|
|
289
|
+
lockPath;
|
|
290
|
+
// ---- Read/Write ----
|
|
291
|
+
read() {
|
|
292
|
+
try {
|
|
293
|
+
return parseWorkboard(readFileSync3(this.workboardPath, "utf8"));
|
|
294
|
+
} catch {
|
|
295
|
+
return emptyState();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
write(state) {
|
|
299
|
+
withLock(this.lockPath, () => {
|
|
300
|
+
atomicWriteSync(this.workboardPath, serializeWorkboard(state));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async readAsync() {
|
|
304
|
+
try {
|
|
305
|
+
return parseWorkboard(await readFile(this.workboardPath, "utf8"));
|
|
306
|
+
} catch {
|
|
307
|
+
return emptyState();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async writeAsync(state) {
|
|
311
|
+
await withLockAsync(this.lockPath, async () => {
|
|
312
|
+
atomicWriteSync(this.workboardPath, serializeWorkboard(state));
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// ---- Agent methods ----
|
|
316
|
+
registerAgent(agent) {
|
|
317
|
+
withLock(this.lockPath, () => {
|
|
318
|
+
const state = this.readUnlocked();
|
|
319
|
+
const idx = state.agents.findIndex((a) => a.id === agent.id);
|
|
320
|
+
if (idx >= 0) {
|
|
321
|
+
state.agents[idx] = agent;
|
|
322
|
+
} else {
|
|
323
|
+
state.agents.push(agent);
|
|
324
|
+
}
|
|
325
|
+
this.writeUnlocked(state);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
unregisterAgent(id) {
|
|
329
|
+
withLock(this.lockPath, () => {
|
|
330
|
+
const state = this.readUnlocked();
|
|
331
|
+
state.agents = state.agents.filter((a) => a.id !== id);
|
|
332
|
+
this.writeUnlocked(state);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
updateAgent(id, updates) {
|
|
336
|
+
withLock(this.lockPath, () => {
|
|
337
|
+
const state = this.readUnlocked();
|
|
338
|
+
const idx = state.agents.findIndex((a) => a.id === id);
|
|
339
|
+
if (idx < 0) return;
|
|
340
|
+
state.agents[idx] = { ...state.agents[idx], ...updates };
|
|
341
|
+
this.writeUnlocked(state);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// ---- Task claiming ----
|
|
345
|
+
/** Claim an available or partial task. Returns true on success. */
|
|
346
|
+
claimTask(taskId, agentId) {
|
|
347
|
+
let success = false;
|
|
348
|
+
withLock(this.lockPath, () => {
|
|
349
|
+
const state = this.readUnlocked();
|
|
350
|
+
const task = state.tasks.find((t) => t.id === taskId);
|
|
351
|
+
if (!task) return;
|
|
352
|
+
if (task.status !== "available" && task.status !== "partial") return;
|
|
353
|
+
task.status = "claimed";
|
|
354
|
+
task.owner = agentId;
|
|
355
|
+
task.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
356
|
+
this.writeUnlocked(state);
|
|
357
|
+
success = true;
|
|
358
|
+
});
|
|
359
|
+
return success;
|
|
360
|
+
}
|
|
361
|
+
/** Move a task from Tasks to Done. Returns true on success. */
|
|
362
|
+
completeTask(taskId, agentId) {
|
|
363
|
+
let success = false;
|
|
364
|
+
withLock(this.lockPath, () => {
|
|
365
|
+
const state = this.readUnlocked();
|
|
366
|
+
const idx = state.tasks.findIndex((t) => t.id === taskId);
|
|
367
|
+
if (idx === -1) return;
|
|
368
|
+
const task = state.tasks.splice(idx, 1)[0];
|
|
369
|
+
state.done.unshift({
|
|
370
|
+
id: task.id,
|
|
371
|
+
task: task.task,
|
|
372
|
+
owner: agentId || task.owner,
|
|
373
|
+
completed: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
374
|
+
gh: task.gh || "",
|
|
375
|
+
notes: task.notes || ""
|
|
376
|
+
});
|
|
377
|
+
this.writeUnlocked(state);
|
|
378
|
+
success = true;
|
|
379
|
+
});
|
|
380
|
+
return success;
|
|
381
|
+
}
|
|
382
|
+
/** Mark a claimed task as partial (agent stopped mid-work). */
|
|
383
|
+
markPartial(taskId, notes) {
|
|
384
|
+
let success = false;
|
|
385
|
+
withLock(this.lockPath, () => {
|
|
386
|
+
const state = this.readUnlocked();
|
|
387
|
+
const task = state.tasks.find((t) => t.id === taskId);
|
|
388
|
+
if (!task) return;
|
|
389
|
+
task.status = "partial";
|
|
390
|
+
task.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
391
|
+
if (notes) task.notes = notes;
|
|
392
|
+
this.writeUnlocked(state);
|
|
393
|
+
success = true;
|
|
394
|
+
});
|
|
395
|
+
return success;
|
|
396
|
+
}
|
|
397
|
+
/** Release a claimed/partial task back to available. */
|
|
398
|
+
releaseTask(taskId) {
|
|
399
|
+
let success = false;
|
|
400
|
+
withLock(this.lockPath, () => {
|
|
401
|
+
const state = this.readUnlocked();
|
|
402
|
+
const task = state.tasks.find((t) => t.id === taskId);
|
|
403
|
+
if (!task) return;
|
|
404
|
+
task.status = "available";
|
|
405
|
+
task.owner = "";
|
|
406
|
+
task.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
407
|
+
this.writeUnlocked(state);
|
|
408
|
+
success = true;
|
|
409
|
+
});
|
|
410
|
+
return success;
|
|
411
|
+
}
|
|
412
|
+
/** Move a blocked task to Tasks as available. */
|
|
413
|
+
unblockTask(taskId, pri = "P2") {
|
|
414
|
+
let success = false;
|
|
415
|
+
withLock(this.lockPath, () => {
|
|
416
|
+
const state = this.readUnlocked();
|
|
417
|
+
const idx = state.blocked.findIndex((t) => t.id === taskId);
|
|
418
|
+
if (idx === -1) return;
|
|
419
|
+
const blocked = state.blocked.splice(idx, 1)[0];
|
|
420
|
+
state.tasks.push({
|
|
421
|
+
id: blocked.id,
|
|
422
|
+
task: blocked.task,
|
|
423
|
+
pri,
|
|
424
|
+
status: "available",
|
|
425
|
+
owner: "",
|
|
426
|
+
gh: blocked.gh || "",
|
|
427
|
+
updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
428
|
+
notes: blocked.notes || ""
|
|
429
|
+
});
|
|
430
|
+
this.writeUnlocked(state);
|
|
431
|
+
success = true;
|
|
432
|
+
});
|
|
433
|
+
return success;
|
|
434
|
+
}
|
|
435
|
+
// ---- File claims ----
|
|
436
|
+
claimFiles(id, files) {
|
|
437
|
+
withLock(this.lockPath, () => {
|
|
438
|
+
const state = this.readUnlocked();
|
|
439
|
+
const agent = state.agents.find((a) => a.id === id);
|
|
440
|
+
if (!agent) return;
|
|
441
|
+
agent.files = files.join(", ");
|
|
442
|
+
agent.updated = `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`;
|
|
443
|
+
this.writeUnlocked(state);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
releaseFiles(id) {
|
|
447
|
+
withLock(this.lockPath, () => {
|
|
448
|
+
const state = this.readUnlocked();
|
|
449
|
+
const agent = state.agents.find((a) => a.id === id);
|
|
450
|
+
if (!agent) return;
|
|
451
|
+
agent.files = "";
|
|
452
|
+
agent.updated = `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`;
|
|
453
|
+
this.writeUnlocked(state);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// ---- Log ----
|
|
457
|
+
addLogEntry(agentId, description) {
|
|
458
|
+
withLock(this.lockPath, () => {
|
|
459
|
+
const state = this.readUnlocked();
|
|
460
|
+
const now = /* @__PURE__ */ new Date();
|
|
461
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
462
|
+
const timeStr = now.toISOString().slice(11, 16);
|
|
463
|
+
state.log.unshift(`- [${dateStr} ${timeStr}] ${agentId}: ${description}`);
|
|
464
|
+
if (state.log.length > 20) state.log.splice(20);
|
|
465
|
+
this.writeUnlocked(state);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// ---- Queries ----
|
|
469
|
+
detectStale() {
|
|
470
|
+
const state = this.read();
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
return state.agents.filter((a) => {
|
|
473
|
+
try {
|
|
474
|
+
const age = now - new Date(a.updated).getTime();
|
|
475
|
+
const threshold = a.task === "(starting)" ? STARTING_STALE_MS : STALE_THRESHOLD_MS;
|
|
476
|
+
return age > threshold;
|
|
477
|
+
} catch {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
checkConflicts(mySessionId, files) {
|
|
483
|
+
const state = this.read();
|
|
484
|
+
const conflicts = [];
|
|
485
|
+
for (const agent of state.agents) {
|
|
486
|
+
if (agent.id === mySessionId) continue;
|
|
487
|
+
const theirFiles = agent.files.split(",").map((f) => f.trim()).filter(Boolean);
|
|
488
|
+
const overlapping = files.filter(
|
|
489
|
+
(myFile) => theirFiles.some(
|
|
490
|
+
(theirFile) => myFile === theirFile || myFile.startsWith(theirFile.replace("**", "")) || theirFile.startsWith(myFile.replace("**", ""))
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
if (overlapping.length > 0) {
|
|
494
|
+
conflicts.push({
|
|
495
|
+
thisSession: mySessionId,
|
|
496
|
+
otherSession: agent.id,
|
|
497
|
+
overlappingFiles: overlapping
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { clean: conflicts.length === 0, conflicts };
|
|
502
|
+
}
|
|
503
|
+
getClaimedTasks(agentId) {
|
|
504
|
+
const state = this.read();
|
|
505
|
+
return state.tasks.filter(
|
|
506
|
+
(t) => t.status === "claimed" && t.owner === agentId
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
// ---- Internal ----
|
|
510
|
+
readUnlocked() {
|
|
511
|
+
try {
|
|
512
|
+
return parseWorkboard(readFileSync3(this.workboardPath, "utf8"));
|
|
513
|
+
} catch {
|
|
514
|
+
return emptyState();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
writeUnlocked(state) {
|
|
518
|
+
atomicWriteSync(this.workboardPath, serializeWorkboard(state));
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
function emptyState() {
|
|
522
|
+
return { preamble: [], agents: [], tasks: [], blocked: [], done: [], log: [], _extra: {} };
|
|
523
|
+
}
|
|
524
|
+
var registerSession = WorkboardManager.prototype.registerAgent;
|
|
525
|
+
var unregisterSession = WorkboardManager.prototype.unregisterAgent;
|
|
526
|
+
|
|
527
|
+
export {
|
|
528
|
+
acquireLock,
|
|
529
|
+
releaseLock,
|
|
530
|
+
withLock,
|
|
531
|
+
withLockAsync,
|
|
532
|
+
atomicWriteSync,
|
|
533
|
+
lockPathFor,
|
|
534
|
+
WorkboardManager,
|
|
535
|
+
registerSession,
|
|
536
|
+
unregisterSession,
|
|
537
|
+
detectSessionType,
|
|
538
|
+
deriveSessionId
|
|
539
|
+
};
|
|
540
|
+
//# sourceMappingURL=chunk-JG6CAG4A.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/workboard/file-lock.ts","../src/workboard/session-identity.ts","../src/workboard/workboard-manager.ts"],"sourcesContent":["import {\n closeSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n writeSync,\n} from 'node:fs';\n\nconst DEFAULT_TIMEOUT_MS = 2000;\nconst RETRY_MS = 50;\n\n/**\n * Check whether a process is still running.\n * Uses signal 0 which doesn't actually send a signal — just checks existence.\n */\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Acquire an exclusive file lock using O_EXCL (kernel-level atomic create).\n * Writes the current PID to the lock file for dead-holder detection.\n *\n * Spins with a 50ms interval until timeout. If the current lock holder\n * is dead (process no longer running), the lock is stolen.\n *\n * @returns true if the lock was acquired, false on timeout\n */\nexport function acquireLock(lockPath: string, timeoutMs = DEFAULT_TIMEOUT_MS): boolean {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n const fd = openSync(lockPath, 'wx');\n writeSync(fd, String(process.pid));\n closeSync(fd);\n return true;\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') return false;\n // Lock exists — check if holder is alive\n try {\n const holderPid = Number.parseInt(readFileSync(lockPath, 'utf8').trim(), 10);\n if (!(Number.isNaN(holderPid) || isPidAlive(holderPid))) {\n // Holder crashed — steal the lock\n unlinkSync(lockPath);\n continue;\n }\n } catch {\n // Lock file disappeared between checks — retry immediately\n continue;\n }\n // Busy-wait\n const spinUntil = Date.now() + RETRY_MS;\n while (Date.now() < spinUntil) {\n /* spin */\n }\n }\n }\n return false;\n}\n\n/**\n * Release a file lock. Swallows ENOENT (already released).\n */\nexport function releaseLock(lockPath: string): void {\n try {\n unlinkSync(lockPath);\n } catch {\n // Already released or never acquired — non-fatal\n }\n}\n\n/**\n * Execute a synchronous function while holding the file lock.\n * The lock is always released, even if fn throws.\n */\nexport function withLock<T>(lockPath: string, fn: () => T, timeoutMs?: number): T {\n const acquired = acquireLock(lockPath, timeoutMs);\n if (!acquired) {\n throw new Error(\n `Failed to acquire lock: ${lockPath} (timeout ${timeoutMs ?? DEFAULT_TIMEOUT_MS}ms)`,\n );\n }\n try {\n return fn();\n } finally {\n releaseLock(lockPath);\n }\n}\n\n/**\n * Execute an async function while holding the file lock.\n * The lock is always released, even if fn throws.\n */\nexport async function withLockAsync<T>(\n lockPath: string,\n fn: () => Promise<T>,\n timeoutMs?: number,\n): Promise<T> {\n const acquired = acquireLock(lockPath, timeoutMs);\n if (!acquired) {\n throw new Error(\n `Failed to acquire lock: ${lockPath} (timeout ${timeoutMs ?? DEFAULT_TIMEOUT_MS}ms)`,\n );\n }\n try {\n return await fn();\n } finally {\n releaseLock(lockPath);\n }\n}\n\n/**\n * Write a file atomically: write to a temporary file, then rename.\n * rename() on the same filesystem is atomic at the kernel level.\n */\nexport function atomicWriteSync(filePath: string, content: string): void {\n const tmpPath = `${filePath}.tmp.${process.pid}`;\n writeFileSync(tmpPath, content, 'utf8');\n renameSync(tmpPath, filePath);\n}\n\n/**\n * Derive a lock path from a workboard path (.md → .lock).\n */\nexport function lockPathFor(workboardPath: string): string {\n return workboardPath.replace(/\\.md$/, '.lock');\n}\n","import { readFileSync } from 'node:fs';\n\n/** Type of session detected from the runtime environment. */\nexport type SessionType = 'zed' | 'cursor' | 'terminal';\n\n/**\n * Detects whether the current process is running inside an AI tool (Zed, Cursor)\n * or a plain terminal session by walking the parent process chain.\n *\n * Uses /proc/<pid>/cmdline on Linux/WSL. Falls back to TERM_PROGRAM env var.\n */\nexport function detectSessionType(): SessionType {\n // Walk parent process chain looking for known AI tool process names.\n try {\n let pid = process.ppid;\n for (let depth = 0; depth < 8; depth++) {\n if (!pid || pid <= 1) break;\n const cmdline = readFileSync(`/proc/${pid}/cmdline`, 'utf8')\n .replace(/\\0/g, ' ')\n .toLowerCase();\n if (cmdline.includes('zed')) return 'zed';\n if (cmdline.includes('cursor')) return 'cursor';\n const status = readFileSync(`/proc/${pid}/status`, 'utf8');\n const m = status.match(/PPid:\\s+(\\d+)/);\n if (!m) break;\n pid = parseInt(m[1] ?? '0', 10);\n }\n } catch {\n // /proc not available (macOS, Windows non-WSL).\n }\n\n // Fallback: TERM_PROGRAM env var (set by some terminal emulators and IDEs).\n const termProgram = (process.env.TERM_PROGRAM ?? '').toLowerCase();\n if (termProgram.includes('zed')) return 'zed';\n if (termProgram.includes('cursor')) return 'cursor';\n\n return 'terminal';\n}\n\n/**\n * Derives a session ID (e.g. \"zed-1\", \"terminal-2\") given a type and a list\n * of existing session IDs already in the workboard.\n *\n * Picks the next available numeric suffix to avoid collisions.\n */\nexport function deriveSessionId(type: SessionType, existingIds: string[]): string {\n const matching = existingIds\n .filter((id) => id.startsWith(`${type}-`))\n .map((id) => parseInt(id.split('-')[1] ?? '0', 10))\n .filter((n) => !Number.isNaN(n));\n\n const maxN = matching.length > 0 ? Math.max(...matching) : 0;\n return `${type}-${maxN + 1}`;\n}\n","import { readFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { atomicWriteSync, lockPathFor, withLock, withLockAsync } from './file-lock.js';\nimport type {\n ConflictResult,\n WorkboardAgent,\n WorkboardState,\n WorkboardTask,\n} from './workboard-protocol.js';\n\nconst STALE_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours\nconst STARTING_STALE_MS = 60 * 60 * 1000; // 1 hour\n\n// ---------------------------------------------------------------------------\n// Section name normalization (backward compat with v1 workboard)\n// ---------------------------------------------------------------------------\n\nconst SECTION_MAP: Record<\n string,\n keyof Pick<WorkboardState, 'agents' | 'tasks' | 'blocked' | 'done' | 'log'> | null\n> = {\n agents: 'agents',\n active_sessions: 'agents',\n sessions: 'agents',\n tasks: 'tasks',\n blocked: 'blocked',\n done: 'done',\n log: 'log',\n recent: 'log',\n};\n\nfunction normalizeSectionName(title: string): string | null {\n const key = title.toLowerCase().replace(/\\s+/g, '_');\n return SECTION_MAP[key] || null;\n}\n\n// ---------------------------------------------------------------------------\n// Table parsing\n// ---------------------------------------------------------------------------\n\nfunction splitRow(line: string): string[] | null {\n if (!line.startsWith('|')) return null;\n return line\n .split('|')\n .slice(1, -1)\n .map((c) => c.trim());\n}\n\nfunction isSeparatorRow(cells: string[]): boolean {\n return cells.length > 0 && cells.every((c) => /^[-:]+$/.test(c));\n}\n\nfunction parseTable(lines: string[]): { columns: string[]; rows: Record<string, string>[] } {\n if (lines.length < 2) return { columns: [], rows: [] };\n\n const headerCells = splitRow(lines[0]!);\n if (!headerCells) return { columns: [], rows: [] };\n\n const sepCells = splitRow(lines[1]!);\n if (!(sepCells && isSeparatorRow(sepCells))) return { columns: [], rows: [] };\n\n const columns = headerCells.map((h) =>\n h\n .toLowerCase()\n .replace(/[^a-z0-9_]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, ''),\n );\n\n const rows: Record<string, string>[] = [];\n for (let i = 2; i < lines.length; i++) {\n const cells = splitRow(lines[i]!);\n if (!cells || isSeparatorRow(cells)) continue;\n const row: Record<string, string> = {};\n for (let j = 0; j < columns.length; j++) {\n const col = columns[j]!;\n const raw = (cells[j] || '').trim();\n row[col] = raw === '—' ? '' : raw;\n }\n rows.push(row);\n }\n\n return { columns, rows };\n}\n\n// ---------------------------------------------------------------------------\n// Parse\n// ---------------------------------------------------------------------------\n\nfunction parseWorkboard(content: string): WorkboardState {\n const lines = content.split('\\n');\n const rawSections: Array<{ title: string; line: number }> = [];\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n const m = line.match(/^##\\s+(.+)/);\n if (m?.[1]) rawSections.push({ title: m[1].trim(), line: i });\n }\n\n const state: WorkboardState = {\n preamble: [],\n agents: [],\n tasks: [],\n blocked: [],\n done: [],\n log: [],\n _extra: {},\n };\n\n const firstLine = rawSections.length > 0 ? rawSections[0]?.line : lines.length;\n state.preamble = lines.slice(0, firstLine);\n\n for (let i = 0; i < rawSections.length; i++) {\n const sec = rawSections[i]!;\n const nextLine = i + 1 < rawSections.length ? rawSections[i + 1]?.line : lines.length;\n const body = lines.slice(sec.line + 1, nextLine);\n const normalized = normalizeSectionName(sec.title);\n\n if (\n normalized === 'agents' ||\n normalized === 'tasks' ||\n normalized === 'blocked' ||\n normalized === 'done'\n ) {\n const tableLines = body.filter((l) => l.startsWith('|'));\n if (tableLines.length >= 2) {\n const { rows } = parseTable(tableLines);\n (state[normalized] as unknown as Record<string, string>[]).length = 0;\n (state[normalized] as unknown as Record<string, string>[]).push(...rows);\n }\n } else if (normalized === 'log') {\n state.log = body.filter((l) => l.startsWith('- '));\n } else {\n state._extra[sec.title] = lines.slice(sec.line, nextLine).join('\\n');\n }\n }\n\n return state;\n}\n\n// ---------------------------------------------------------------------------\n// Serialize\n// ---------------------------------------------------------------------------\n\nfunction padCell(value: string | undefined, width: number): string {\n const str = String(value || '—');\n return str.length >= width ? str : str + ' '.repeat(width - str.length);\n}\n\nfunction serializeTable(headers: string[], rows: Record<string, string>[]): string {\n if (headers.length === 0) return '(none)';\n\n const widths = headers.map((h) => {\n const dataMax = rows.reduce((max, row) => Math.max(max, String(row[h] || '—').length), 0);\n return Math.max(h.length, dataMax, 3);\n });\n\n const headerLine = `| ${headers.map((h, i) => padCell(h, widths[i]!)).join(' | ')} |`;\n const sepLine = `| ${widths.map((w) => '-'.repeat(w)).join(' | ')} |`;\n const dataLines = rows.map(\n (row) => `| ${headers.map((h, i) => padCell(row[h] || '', widths[i]!)).join(' | ')} |`,\n );\n\n return [headerLine, sepLine, ...dataLines].join('\\n');\n}\n\nfunction serializeWorkboard(state: WorkboardState): string {\n const out: string[] = [];\n\n if (state.preamble.length > 0) {\n out.push(...state.preamble);\n if (state.preamble[state.preamble.length - 1] !== '') out.push('');\n }\n\n out.push('## Agents', '');\n out.push(\n serializeTable(\n ['id', 'env', 'started', 'task', 'files', 'updated'],\n state.agents as unknown as Record<string, string>[],\n ),\n '',\n );\n\n out.push('## Tasks', '');\n if (state.tasks.length > 0) {\n out.push(\n serializeTable(\n ['id', 'task', 'pri', 'status', 'owner', 'gh', 'updated', 'notes'],\n state.tasks as unknown as Record<string, string>[],\n ),\n '',\n );\n } else {\n out.push('(none)', '');\n }\n\n out.push('## Blocked', '');\n if (state.blocked.length > 0) {\n out.push(\n serializeTable(\n ['id', 'task', 'blocker', 'gh', 'notes'],\n state.blocked as unknown as Record<string, string>[],\n ),\n '',\n );\n } else {\n out.push('(none)', '');\n }\n\n out.push('## Done', '');\n if (state.done.length > 0) {\n out.push(\n serializeTable(\n ['id', 'task', 'owner', 'completed', 'gh', 'notes'],\n state.done as unknown as Record<string, string>[],\n ),\n '',\n );\n } else {\n out.push('(none)', '');\n }\n\n out.push('## Log', '');\n if (state.log.length > 0) out.push(...state.log);\n out.push('');\n\n for (const content of Object.values(state._extra)) {\n out.push(content, '');\n }\n\n return out.join('\\n');\n}\n\n// ---------------------------------------------------------------------------\n// WorkboardManager\n// ---------------------------------------------------------------------------\n\n/**\n * WorkboardManager — reads, parses, and writes .claude/workboard.md (v2).\n *\n * Supports both v1 (Sessions/Recent) and v2 (Agents/Tasks/Blocked/Done/Log)\n * formats for backward compatibility. Always serializes to v2.\n *\n * All mutating methods use file locking (O_EXCL) to prevent race conditions.\n * Writes are atomic (tmp file + rename).\n */\nexport class WorkboardManager {\n private readonly lockPath: string;\n\n constructor(private readonly workboardPath: string) {\n const resolved = resolve(workboardPath);\n if (!resolved.endsWith('.md')) {\n throw new Error('Invalid workboard path: must be a .md file');\n }\n this.lockPath = lockPathFor(resolved);\n }\n\n // ---- Read/Write ----\n\n read(): WorkboardState {\n try {\n return parseWorkboard(readFileSync(this.workboardPath, 'utf8'));\n } catch {\n return emptyState();\n }\n }\n\n write(state: WorkboardState): void {\n withLock(this.lockPath, () => {\n atomicWriteSync(this.workboardPath, serializeWorkboard(state));\n });\n }\n\n async readAsync(): Promise<WorkboardState> {\n try {\n return parseWorkboard(await readFile(this.workboardPath, 'utf8'));\n } catch {\n return emptyState();\n }\n }\n\n async writeAsync(state: WorkboardState): Promise<void> {\n await withLockAsync(this.lockPath, async () => {\n atomicWriteSync(this.workboardPath, serializeWorkboard(state));\n });\n }\n\n // ---- Agent methods ----\n\n registerAgent(agent: WorkboardAgent): void {\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const idx = state.agents.findIndex((a) => a.id === agent.id);\n if (idx >= 0) {\n state.agents[idx] = agent;\n } else {\n state.agents.push(agent);\n }\n this.writeUnlocked(state);\n });\n }\n\n unregisterAgent(id: string): void {\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n state.agents = state.agents.filter((a) => a.id !== id);\n this.writeUnlocked(state);\n });\n }\n\n updateAgent(id: string, updates: Partial<WorkboardAgent>): void {\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const idx = state.agents.findIndex((a) => a.id === id);\n if (idx < 0) return;\n state.agents[idx] = { ...state.agents[idx]!, ...updates };\n this.writeUnlocked(state);\n });\n }\n\n // ---- Task claiming ----\n\n /** Claim an available or partial task. Returns true on success. */\n claimTask(taskId: string, agentId: string): boolean {\n let success = false;\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const task = state.tasks.find((t) => t.id === taskId);\n if (!task) return;\n if (task.status !== 'available' && task.status !== 'partial') return;\n task.status = 'claimed';\n task.owner = agentId;\n task.updated = new Date().toISOString().slice(0, 10);\n this.writeUnlocked(state);\n success = true;\n });\n return success;\n }\n\n /** Move a task from Tasks to Done. Returns true on success. */\n completeTask(taskId: string, agentId: string): boolean {\n let success = false;\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const idx = state.tasks.findIndex((t) => t.id === taskId);\n if (idx === -1) return;\n const task = state.tasks.splice(idx, 1)[0]!;\n state.done.unshift({\n id: task.id,\n task: task.task,\n owner: agentId || task.owner,\n completed: new Date().toISOString().slice(0, 10),\n gh: task.gh || '',\n notes: task.notes || '',\n });\n this.writeUnlocked(state);\n success = true;\n });\n return success;\n }\n\n /** Mark a claimed task as partial (agent stopped mid-work). */\n markPartial(taskId: string, notes: string): boolean {\n let success = false;\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const task = state.tasks.find((t) => t.id === taskId);\n if (!task) return;\n task.status = 'partial';\n task.updated = new Date().toISOString().slice(0, 10);\n if (notes) task.notes = notes;\n this.writeUnlocked(state);\n success = true;\n });\n return success;\n }\n\n /** Release a claimed/partial task back to available. */\n releaseTask(taskId: string): boolean {\n let success = false;\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const task = state.tasks.find((t) => t.id === taskId);\n if (!task) return;\n task.status = 'available';\n task.owner = '';\n task.updated = new Date().toISOString().slice(0, 10);\n this.writeUnlocked(state);\n success = true;\n });\n return success;\n }\n\n /** Move a blocked task to Tasks as available. */\n unblockTask(taskId: string, pri: string = 'P2'): boolean {\n let success = false;\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const idx = state.blocked.findIndex((t) => t.id === taskId);\n if (idx === -1) return;\n const blocked = state.blocked.splice(idx, 1)[0]!;\n state.tasks.push({\n id: blocked.id,\n task: blocked.task,\n pri: pri as WorkboardTask['pri'],\n status: 'available',\n owner: '',\n gh: blocked.gh || '',\n updated: new Date().toISOString().slice(0, 10),\n notes: blocked.notes || '',\n });\n this.writeUnlocked(state);\n success = true;\n });\n return success;\n }\n\n // ---- File claims ----\n\n claimFiles(id: string, files: string[]): void {\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const agent = state.agents.find((a) => a.id === id);\n if (!agent) return;\n agent.files = files.join(', ');\n agent.updated = `${new Date().toISOString().slice(0, 16)}Z`;\n this.writeUnlocked(state);\n });\n }\n\n releaseFiles(id: string): void {\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const agent = state.agents.find((a) => a.id === id);\n if (!agent) return;\n agent.files = '';\n agent.updated = `${new Date().toISOString().slice(0, 16)}Z`;\n this.writeUnlocked(state);\n });\n }\n\n // ---- Log ----\n\n addLogEntry(agentId: string, description: string): void {\n withLock(this.lockPath, () => {\n const state = this.readUnlocked();\n const now = new Date();\n const dateStr = now.toISOString().slice(0, 10);\n const timeStr = now.toISOString().slice(11, 16);\n state.log.unshift(`- [${dateStr} ${timeStr}] ${agentId}: ${description}`);\n if (state.log.length > 20) state.log.splice(20);\n this.writeUnlocked(state);\n });\n }\n\n // ---- Queries ----\n\n detectStale(): WorkboardAgent[] {\n const state = this.read();\n const now = Date.now();\n return state.agents.filter((a) => {\n try {\n const age = now - new Date(a.updated).getTime();\n const threshold = a.task === '(starting)' ? STARTING_STALE_MS : STALE_THRESHOLD_MS;\n return age > threshold;\n } catch {\n return false;\n }\n });\n }\n\n checkConflicts(mySessionId: string, files: string[]): ConflictResult {\n const state = this.read();\n const conflicts: ConflictResult['conflicts'] = [];\n\n for (const agent of state.agents) {\n if (agent.id === mySessionId) continue;\n const theirFiles = agent.files\n .split(',')\n .map((f) => f.trim())\n .filter(Boolean);\n const overlapping = files.filter((myFile) =>\n theirFiles.some(\n (theirFile) =>\n myFile === theirFile ||\n myFile.startsWith(theirFile.replace('**', '')) ||\n theirFile.startsWith(myFile.replace('**', '')),\n ),\n );\n if (overlapping.length > 0) {\n conflicts.push({\n thisSession: mySessionId,\n otherSession: agent.id,\n overlappingFiles: overlapping,\n });\n }\n }\n\n return { clean: conflicts.length === 0, conflicts };\n }\n\n getClaimedTasks(agentId: string): WorkboardTask[] {\n const state = this.read();\n return state.tasks.filter(\n (t) => t.status === 'claimed' && t.owner === agentId,\n ) as WorkboardTask[];\n }\n\n // ---- Internal ----\n\n private readUnlocked(): WorkboardState {\n try {\n return parseWorkboard(readFileSync(this.workboardPath, 'utf8'));\n } catch {\n return emptyState();\n }\n }\n\n private writeUnlocked(state: WorkboardState): void {\n atomicWriteSync(this.workboardPath, serializeWorkboard(state));\n }\n}\n\nfunction emptyState(): WorkboardState {\n return { preamble: [], agents: [], tasks: [], blocked: [], done: [], log: [], _extra: {} };\n}\n\n// Re-export for backward compat\n/** @deprecated Use registerAgent instead */\nexport const registerSession = WorkboardManager.prototype.registerAgent;\n/** @deprecated Use unregisterAgent instead */\nexport const unregisterSession = WorkboardManager.prototype.unregisterAgent;\n"],"mappings":"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","names":["readFileSync","readFileSync","readFileSync"]}
|