codework 0.1.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/AGENTS.md +8 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/cli.js +1506 -0
- package/package.json +34 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command, CommanderError } from "commander";
|
|
5
|
+
import { realpathSync } from "fs";
|
|
6
|
+
import process5 from "process";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// src/args.ts
|
|
10
|
+
var KEY_VALUE_ARG = /^([a-z][a-z0-9-]*)=(.*)$/;
|
|
11
|
+
function preprocessArgv(argv) {
|
|
12
|
+
return argv.map((arg) => {
|
|
13
|
+
if (arg.startsWith("-")) {
|
|
14
|
+
return arg;
|
|
15
|
+
}
|
|
16
|
+
const match = KEY_VALUE_ARG.exec(arg);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return arg;
|
|
19
|
+
}
|
|
20
|
+
return `--${match[1]}=${match[2]}`;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/commands/doctor.ts
|
|
25
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
26
|
+
import path4 from "path";
|
|
27
|
+
import process4 from "process";
|
|
28
|
+
|
|
29
|
+
// src/core/events.ts
|
|
30
|
+
import { readFile as readFile2, appendFile } from "fs/promises";
|
|
31
|
+
|
|
32
|
+
// src/core/state.ts
|
|
33
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
34
|
+
import path from "path";
|
|
35
|
+
import process2 from "process";
|
|
36
|
+
|
|
37
|
+
// src/core/validate.ts
|
|
38
|
+
var CodeworkError = class extends Error {
|
|
39
|
+
exitCode;
|
|
40
|
+
constructor(exitCode, message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "CodeworkError";
|
|
43
|
+
this.exitCode = exitCode;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function required(value, optionName) {
|
|
47
|
+
if (value === void 0 || value.trim() === "") {
|
|
48
|
+
throw new CodeworkError(2, `Missing required option: ${optionName}`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function slugifyIdentifier(value, label) {
|
|
53
|
+
const raw = required(value, label).trim();
|
|
54
|
+
if (raw === "." || raw === ".." || raw.includes("..") || raw.includes("/") || raw.includes("\\")) {
|
|
55
|
+
throw new CodeworkError(
|
|
56
|
+
2,
|
|
57
|
+
`Invalid ${label}: value must not be empty, '.', '..', contain '..', '/', or '\\'.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const slug = raw.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
61
|
+
if (slug === "" || slug === "." || slug === ".." || slug.includes("..")) {
|
|
62
|
+
throw new CodeworkError(2, `Invalid ${label}: normalized slug is not safe.`);
|
|
63
|
+
}
|
|
64
|
+
return { value: raw, slug };
|
|
65
|
+
}
|
|
66
|
+
function normalizeFollow(value) {
|
|
67
|
+
const follow = required(value, "--follow").trim();
|
|
68
|
+
if (follow === "user" || follow === "self") {
|
|
69
|
+
return follow;
|
|
70
|
+
}
|
|
71
|
+
return slugifyIdentifier(follow, "--follow").value;
|
|
72
|
+
}
|
|
73
|
+
function normalizeTo(value) {
|
|
74
|
+
const to = value?.trim() || "all";
|
|
75
|
+
if (to === "all") {
|
|
76
|
+
return to;
|
|
77
|
+
}
|
|
78
|
+
return slugifyIdentifier(to, "--to").value;
|
|
79
|
+
}
|
|
80
|
+
function validateMessageKind(value) {
|
|
81
|
+
if (value === "note" || value === "directive" || value === "question" || value === "blocker") {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
throw new CodeworkError(2, `Invalid --kind: expected note, directive, question, or blocker.`);
|
|
85
|
+
}
|
|
86
|
+
function validateFormat(value) {
|
|
87
|
+
const format = value ?? "text";
|
|
88
|
+
if (format === "text" || format === "json") {
|
|
89
|
+
return format;
|
|
90
|
+
}
|
|
91
|
+
throw new CodeworkError(2, `Invalid --format: expected text or json.`);
|
|
92
|
+
}
|
|
93
|
+
function validateTextSize(value, optionName) {
|
|
94
|
+
const text = required(value, optionName);
|
|
95
|
+
const byteLength = new TextEncoder().encode(text).length;
|
|
96
|
+
if (byteLength > 64 * 1024) {
|
|
97
|
+
throw new CodeworkError(2, `${optionName} is too large: maximum is 64KB per event.`);
|
|
98
|
+
}
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
function parsePositiveInteger(value, optionName, fallback) {
|
|
102
|
+
if (value === void 0) {
|
|
103
|
+
return fallback;
|
|
104
|
+
}
|
|
105
|
+
const parsed = Number.parseInt(value, 10);
|
|
106
|
+
if (!Number.isFinite(parsed) || parsed < 0 || String(parsed) !== String(value)) {
|
|
107
|
+
throw new CodeworkError(2, `Invalid ${optionName}: expected a non-negative integer.`);
|
|
108
|
+
}
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/core/state.ts
|
|
113
|
+
function nowIso() {
|
|
114
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
115
|
+
}
|
|
116
|
+
function stateFilePath(workspaceDir) {
|
|
117
|
+
return path.join(workspaceDir, "state.json");
|
|
118
|
+
}
|
|
119
|
+
function eventsFilePath(workspaceDir) {
|
|
120
|
+
return path.join(workspaceDir, "events.ndjson");
|
|
121
|
+
}
|
|
122
|
+
function agentsDirPath(workspaceDir) {
|
|
123
|
+
return path.join(workspaceDir, "agents");
|
|
124
|
+
}
|
|
125
|
+
async function writeJsonAtomic(filePath, value) {
|
|
126
|
+
const tmpPath = `${filePath}.${process2.pid}.${Date.now()}.tmp`;
|
|
127
|
+
await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}
|
|
128
|
+
`, "utf8");
|
|
129
|
+
await rename(tmpPath, filePath);
|
|
130
|
+
}
|
|
131
|
+
async function writeTextAtomic(filePath, value) {
|
|
132
|
+
const tmpPath = `${filePath}.${process2.pid}.${Date.now()}.tmp`;
|
|
133
|
+
await writeFile(tmpPath, value, "utf8");
|
|
134
|
+
await rename(tmpPath, filePath);
|
|
135
|
+
}
|
|
136
|
+
async function loadState(workspaceDir) {
|
|
137
|
+
try {
|
|
138
|
+
const raw = await readFile(stateFilePath(workspaceDir), "utf8");
|
|
139
|
+
return JSON.parse(raw);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const code = error.code;
|
|
142
|
+
if (code === "ENOENT") {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
throw new CodeworkError(1, `Unable to read workspace state: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function requireState(paths) {
|
|
149
|
+
if (!paths.workspaceDir) {
|
|
150
|
+
throw new CodeworkError(2, "Missing required option: --workspace");
|
|
151
|
+
}
|
|
152
|
+
const state = await loadState(paths.workspaceDir);
|
|
153
|
+
if (!state) {
|
|
154
|
+
throw new CodeworkError(2, `Workspace does not exist: ${paths.workspace ?? "(unknown)"}`);
|
|
155
|
+
}
|
|
156
|
+
return state;
|
|
157
|
+
}
|
|
158
|
+
async function saveState(workspaceDir, state) {
|
|
159
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
160
|
+
await mkdir(agentsDirPath(workspaceDir), { recursive: true });
|
|
161
|
+
await writeJsonAtomic(stateFilePath(workspaceDir), state);
|
|
162
|
+
await Promise.all(
|
|
163
|
+
Object.values(state.agents).map(
|
|
164
|
+
(agent) => writeJsonAtomic(path.join(agentsDirPath(workspaceDir), `${agent.slug}.json`), agent)
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
async function resetWorkspaceFiles(workspaceDir) {
|
|
169
|
+
await rm(stateFilePath(workspaceDir), { force: true });
|
|
170
|
+
await rm(eventsFilePath(workspaceDir), { force: true });
|
|
171
|
+
await rm(agentsDirPath(workspaceDir), { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
function createWorkspaceState(paths, workspace) {
|
|
174
|
+
if (!paths.workspaceSlug) {
|
|
175
|
+
throw new CodeworkError(2, "Missing required option: --workspace");
|
|
176
|
+
}
|
|
177
|
+
const ts = nowIso();
|
|
178
|
+
return {
|
|
179
|
+
schemaVersion: 1,
|
|
180
|
+
workspace,
|
|
181
|
+
workspaceSlug: paths.workspaceSlug,
|
|
182
|
+
createdAt: ts,
|
|
183
|
+
updatedAt: ts,
|
|
184
|
+
root: paths.root,
|
|
185
|
+
nextEventId: 1,
|
|
186
|
+
agents: {},
|
|
187
|
+
warnings: []
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function addOrRejoinAgent(state, input) {
|
|
191
|
+
const slugged = slugifyIdentifier(input.name, "--name");
|
|
192
|
+
const existing = state.agents[slugged.slug];
|
|
193
|
+
const ts = nowIso();
|
|
194
|
+
if (existing) {
|
|
195
|
+
existing.follow = input.follow;
|
|
196
|
+
existing.role = input.role || existing.role;
|
|
197
|
+
existing.lastSeenAt = ts;
|
|
198
|
+
existing.active = true;
|
|
199
|
+
existing.sessionCount += 1;
|
|
200
|
+
if (input.cursorEventId !== void 0) {
|
|
201
|
+
existing.cursorEventId = input.cursorEventId;
|
|
202
|
+
}
|
|
203
|
+
state.updatedAt = ts;
|
|
204
|
+
return { agent: existing, rejoin: true };
|
|
205
|
+
}
|
|
206
|
+
const agent = {
|
|
207
|
+
name: slugged.value,
|
|
208
|
+
slug: slugged.slug,
|
|
209
|
+
role: input.role,
|
|
210
|
+
follow: input.follow,
|
|
211
|
+
joinedAt: ts,
|
|
212
|
+
lastSeenAt: ts,
|
|
213
|
+
active: true,
|
|
214
|
+
sessionCount: 1,
|
|
215
|
+
cursorEventId: input.cursorEventId ?? 0
|
|
216
|
+
};
|
|
217
|
+
state.agents[agent.slug] = agent;
|
|
218
|
+
state.updatedAt = ts;
|
|
219
|
+
return { agent, rejoin: false };
|
|
220
|
+
}
|
|
221
|
+
function findAgent(state, name) {
|
|
222
|
+
if (!name) {
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
const slug = slugifyIdentifier(name, "--name").slug;
|
|
226
|
+
return state.agents[slug];
|
|
227
|
+
}
|
|
228
|
+
function findAgentByFollowValue(state, follow) {
|
|
229
|
+
if (follow === "user" || follow === "self") {
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
const slug = slugifyIdentifier(follow, "--follow").slug;
|
|
233
|
+
return state.agents[slug];
|
|
234
|
+
}
|
|
235
|
+
function touchAgent(state, name, cursorEventId) {
|
|
236
|
+
const agent = findAgent(state, name);
|
|
237
|
+
if (!agent) {
|
|
238
|
+
throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
|
|
239
|
+
}
|
|
240
|
+
agent.lastSeenAt = nowIso();
|
|
241
|
+
if (cursorEventId !== void 0) {
|
|
242
|
+
agent.cursorEventId = cursorEventId;
|
|
243
|
+
}
|
|
244
|
+
state.updatedAt = agent.lastSeenAt;
|
|
245
|
+
return agent;
|
|
246
|
+
}
|
|
247
|
+
function createEvent(state, input) {
|
|
248
|
+
const event = {
|
|
249
|
+
id: state.nextEventId,
|
|
250
|
+
ts: nowIso(),
|
|
251
|
+
workspace: state.workspace,
|
|
252
|
+
actor: input.actor,
|
|
253
|
+
type: input.type,
|
|
254
|
+
payload: input.payload
|
|
255
|
+
};
|
|
256
|
+
if (input.to !== void 0) {
|
|
257
|
+
event.to = input.to;
|
|
258
|
+
}
|
|
259
|
+
if (input.kind !== void 0) {
|
|
260
|
+
event.kind = input.kind;
|
|
261
|
+
}
|
|
262
|
+
state.nextEventId += 1;
|
|
263
|
+
state.updatedAt = event.ts;
|
|
264
|
+
return event;
|
|
265
|
+
}
|
|
266
|
+
function latestEventId(state) {
|
|
267
|
+
return state.nextEventId - 1;
|
|
268
|
+
}
|
|
269
|
+
async function truncateEvents(workspaceDir) {
|
|
270
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
271
|
+
await writeTextAtomic(eventsFilePath(workspaceDir), "");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/core/events.ts
|
|
275
|
+
async function readEvents(workspaceDir) {
|
|
276
|
+
try {
|
|
277
|
+
const raw = await readFile2(eventsFilePath(workspaceDir), "utf8");
|
|
278
|
+
return raw.split(/\r?\n/).filter((line) => line.trim() !== "").map((line, index) => {
|
|
279
|
+
try {
|
|
280
|
+
return JSON.parse(line);
|
|
281
|
+
} catch {
|
|
282
|
+
throw new CodeworkError(1, `Invalid JSON in events.ndjson at line ${index + 1}.`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const code = error.code;
|
|
287
|
+
if (code === "ENOENT") {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function appendEvents(workspaceDir, events) {
|
|
294
|
+
if (events.length === 0) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const lines = events.map((event) => JSON.stringify(event)).join("\n");
|
|
298
|
+
await appendFile(eventsFilePath(workspaceDir), `${lines}
|
|
299
|
+
`, "utf8");
|
|
300
|
+
}
|
|
301
|
+
function targetMatchesAgent(to, agent) {
|
|
302
|
+
if (!to || to === "all") {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
return slugifyIdentifier(to, "--to").slug === agent.slug;
|
|
307
|
+
} catch {
|
|
308
|
+
return to === agent.name;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function isOwnOperationalEvent(event, agent) {
|
|
312
|
+
return event.actor === agent.name && event.type !== "warning.created";
|
|
313
|
+
}
|
|
314
|
+
function relevantUnreadEvents(state, events, agent, since) {
|
|
315
|
+
const cursor = since ?? agent.cursorEventId;
|
|
316
|
+
return events.filter((event) => event.id > cursor).filter((event) => targetMatchesAgent(event.to, agent)).filter((event) => !isOwnOperationalEvent(event, agent)).sort((a, b) => priorityForEvent(state, agent, b) - priorityForEvent(state, agent, a) || a.id - b.id);
|
|
317
|
+
}
|
|
318
|
+
function priorityForEvent(state, agent, event) {
|
|
319
|
+
const follow = agent.follow;
|
|
320
|
+
const fromFollow = follow !== "user" && follow !== "self" && event.actor === follow;
|
|
321
|
+
if (event.kind === "blocker") {
|
|
322
|
+
return 100;
|
|
323
|
+
}
|
|
324
|
+
if (fromFollow && event.kind === "directive") {
|
|
325
|
+
return 90;
|
|
326
|
+
}
|
|
327
|
+
if (fromFollow && event.kind === "question") {
|
|
328
|
+
return 80;
|
|
329
|
+
}
|
|
330
|
+
if (event.type === "warning.created") {
|
|
331
|
+
return 70;
|
|
332
|
+
}
|
|
333
|
+
if (state.warnings.some((warning) => event.payload.message === warning.message)) {
|
|
334
|
+
return 60;
|
|
335
|
+
}
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
function latestEvents(events, tail) {
|
|
339
|
+
if (tail <= 0) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
return events.slice(Math.max(0, events.length - tail));
|
|
343
|
+
}
|
|
344
|
+
function advanceCursorToLatest(state, agent) {
|
|
345
|
+
agent.cursorEventId = latestEventId(state);
|
|
346
|
+
agent.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
347
|
+
state.updatedAt = agent.lastSeenAt;
|
|
348
|
+
}
|
|
349
|
+
function formatEvent(event) {
|
|
350
|
+
const parts = [
|
|
351
|
+
`id=${event.id}`,
|
|
352
|
+
`ts=${event.ts}`,
|
|
353
|
+
`type=${event.type}`,
|
|
354
|
+
`actor=${event.actor}`,
|
|
355
|
+
`to=${event.to ?? "all"}`
|
|
356
|
+
];
|
|
357
|
+
if (event.kind) {
|
|
358
|
+
parts.push(`kind=${event.kind}`);
|
|
359
|
+
}
|
|
360
|
+
const payload = compactPayload(event.payload);
|
|
361
|
+
if (payload !== "") {
|
|
362
|
+
parts.push(`payload=${payload}`);
|
|
363
|
+
}
|
|
364
|
+
return `- ${parts.join(" ")}`;
|
|
365
|
+
}
|
|
366
|
+
function compactPayload(payload) {
|
|
367
|
+
const keys = ["message", "summary", "tests", "changed", "next", "blockers", "reason", "role", "follow", "goal"];
|
|
368
|
+
const rendered = keys.filter((key) => payload[key] !== void 0).map((key) => `${key}=${JSON.stringify(payload[key])}`);
|
|
369
|
+
const fallback = rendered.length > 0 ? rendered.join(" ") : JSON.stringify(payload);
|
|
370
|
+
if (fallback.length <= 1200) {
|
|
371
|
+
return fallback;
|
|
372
|
+
}
|
|
373
|
+
return `${fallback.slice(0, 1200)}...`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/core/lock.ts
|
|
377
|
+
import { mkdir as mkdir2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
378
|
+
import os from "os";
|
|
379
|
+
import path2 from "path";
|
|
380
|
+
import process3 from "process";
|
|
381
|
+
var LOCK_RETRY_MS = 50;
|
|
382
|
+
var LOCK_TIMEOUT_MS = 2e3;
|
|
383
|
+
var STALE_LOCK_MS = 3e4;
|
|
384
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
385
|
+
async function readLockOwner(lockDir) {
|
|
386
|
+
try {
|
|
387
|
+
const raw = await readFile3(path2.join(lockDir, "owner.json"), "utf8");
|
|
388
|
+
return JSON.parse(raw);
|
|
389
|
+
} catch {
|
|
390
|
+
return void 0;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function removeStaleLock(lockDir) {
|
|
394
|
+
const owner = await readLockOwner(lockDir);
|
|
395
|
+
if (!owner) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const startedAt = Date.parse(owner.startedAt);
|
|
399
|
+
if (!Number.isFinite(startedAt) || Date.now() - startedAt <= STALE_LOCK_MS) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
await rm2(lockDir, { recursive: true, force: true });
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
async function acquireLock(workspaceDir) {
|
|
406
|
+
await mkdir2(workspaceDir, { recursive: true });
|
|
407
|
+
const lockDir = path2.join(workspaceDir, "lock");
|
|
408
|
+
const started = Date.now();
|
|
409
|
+
while (Date.now() - started <= LOCK_TIMEOUT_MS) {
|
|
410
|
+
try {
|
|
411
|
+
await mkdir2(lockDir);
|
|
412
|
+
const owner = {
|
|
413
|
+
pid: process3.pid,
|
|
414
|
+
hostname: safeHostname(),
|
|
415
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
416
|
+
};
|
|
417
|
+
await writeFile2(path2.join(lockDir, "owner.json"), `${JSON.stringify(owner, null, 2)}
|
|
418
|
+
`, "utf8");
|
|
419
|
+
return lockDir;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
const code = error.code;
|
|
422
|
+
if (code !== "EEXIST") {
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
if (await removeStaleLock(lockDir)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
await sleep(LOCK_RETRY_MS);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
throw new CodeworkError(3, "Workspace state is locked by another Codework process.");
|
|
432
|
+
}
|
|
433
|
+
async function withWorkspaceLock(workspaceDir, fn) {
|
|
434
|
+
const lockDir = await acquireLock(workspaceDir);
|
|
435
|
+
try {
|
|
436
|
+
return await fn();
|
|
437
|
+
} finally {
|
|
438
|
+
await rm2(lockDir, { recursive: true, force: true });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function safeHostname() {
|
|
442
|
+
try {
|
|
443
|
+
return os.hostname();
|
|
444
|
+
} catch {
|
|
445
|
+
return process3.env.HOSTNAME || "unknown";
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/core/paths.ts
|
|
450
|
+
import { execFileSync } from "child_process";
|
|
451
|
+
import path3 from "path";
|
|
452
|
+
function findRepositoryRoot(cwd, debug = false) {
|
|
453
|
+
try {
|
|
454
|
+
const stdout = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
455
|
+
cwd,
|
|
456
|
+
encoding: "utf8",
|
|
457
|
+
stdio: ["ignore", "pipe", debug ? "inherit" : "ignore"]
|
|
458
|
+
});
|
|
459
|
+
const root = stdout.trim();
|
|
460
|
+
return root === "" ? cwd : root;
|
|
461
|
+
} catch {
|
|
462
|
+
return cwd;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function resolveCodeworkPaths(options) {
|
|
466
|
+
const cwd = path3.resolve(options.cwd || process.cwd());
|
|
467
|
+
const root = findRepositoryRoot(cwd, options.debug);
|
|
468
|
+
const home = options.home ? path3.resolve(cwd, options.home) : path3.join(root, ".codework");
|
|
469
|
+
if (!options.workspace) {
|
|
470
|
+
return { cwd, root, home };
|
|
471
|
+
}
|
|
472
|
+
const workspace = slugifyIdentifier(options.workspace, "--workspace");
|
|
473
|
+
const workspaceDir = path3.join(home, "workspaces", workspace.slug);
|
|
474
|
+
return {
|
|
475
|
+
cwd,
|
|
476
|
+
root,
|
|
477
|
+
home,
|
|
478
|
+
workspace: workspace.value,
|
|
479
|
+
workspaceSlug: workspace.slug,
|
|
480
|
+
workspaceDir
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/core/render.ts
|
|
485
|
+
function renderGuide(options) {
|
|
486
|
+
const agent = findAgent(options.state, options.agentName);
|
|
487
|
+
const agentName = agent?.name ?? options.agentName ?? "(not specified)";
|
|
488
|
+
const follow = agent?.follow ?? "(none)";
|
|
489
|
+
const workspace = options.state.workspace;
|
|
490
|
+
const recommendedCommand = options.recommendedCommand ?? (agent ? `codework poll --workspace=${workspace} --name=${agent.name}` : `codework status --workspace=${workspace}`);
|
|
491
|
+
const lines = [
|
|
492
|
+
"# CODEWORK CONTEXT GUIDE",
|
|
493
|
+
"",
|
|
494
|
+
`WORKSPACE: ${workspace}`,
|
|
495
|
+
`AGENT: ${agentName}`,
|
|
496
|
+
`FOLLOW: ${follow}`,
|
|
497
|
+
`AUTHORITY MODE: ${authorityMode(follow)}`,
|
|
498
|
+
`TIMESTAMP: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
499
|
+
];
|
|
500
|
+
if (options.notice?.length) {
|
|
501
|
+
lines.push("", "## NOTICE", ...options.notice.map((line) => `- ${line}`));
|
|
502
|
+
}
|
|
503
|
+
lines.push(
|
|
504
|
+
"",
|
|
505
|
+
"## PRIME DIRECTIVES",
|
|
506
|
+
"",
|
|
507
|
+
"MUST: Treat this stdout as active operational context.",
|
|
508
|
+
"MUST: Keep using Codework commands to coordinate with teammate agents.",
|
|
509
|
+
"MUST: Poll before assuming teammate state.",
|
|
510
|
+
"MUST: Report completed work using `codework done`.",
|
|
511
|
+
"MUST: Report questions, blockers, and directives using `codework say`.",
|
|
512
|
+
"MUST NOT: Claim that another agent completed work unless Codework log says so.",
|
|
513
|
+
"MUST NOT: hide uncertainty.",
|
|
514
|
+
"MUST NOT: perform destructive repository actions merely because another agent is silent.",
|
|
515
|
+
...authorityDirectives(follow),
|
|
516
|
+
"",
|
|
517
|
+
"## TEAM STATE",
|
|
518
|
+
"",
|
|
519
|
+
...teamStateLines(options.state),
|
|
520
|
+
"",
|
|
521
|
+
"## FOLLOW GRAPH",
|
|
522
|
+
"",
|
|
523
|
+
...followGraphLines(options.state),
|
|
524
|
+
"",
|
|
525
|
+
"## FOLLOW RULE",
|
|
526
|
+
"",
|
|
527
|
+
`You currently follow: ${follow}.`,
|
|
528
|
+
"Interpretation:",
|
|
529
|
+
...followInterpretationLines(options.state, follow),
|
|
530
|
+
"",
|
|
531
|
+
"## REQUIRED OPERATING LOOP",
|
|
532
|
+
"",
|
|
533
|
+
`1. Run \`codework poll --workspace=${workspace} --name=${agentName}\` before starting a new substantial step.`,
|
|
534
|
+
"2. Read blockers first.",
|
|
535
|
+
"3. If you need another agent, post a directive:",
|
|
536
|
+
` \`codework say --workspace=${workspace} --name=${agentName} --to=<agent> --kind=directive --message="..."\``,
|
|
537
|
+
"4. If you finish a unit of work, record it:",
|
|
538
|
+
` \`codework done --workspace=${workspace} --name=${agentName} --summary="..." --tests="..."\``,
|
|
539
|
+
"5. End your user-facing answer with the required report block.",
|
|
540
|
+
"",
|
|
541
|
+
"## REQUIRED REPORT BLOCK",
|
|
542
|
+
"",
|
|
543
|
+
"CODEWORK REPORT:",
|
|
544
|
+
"- Agent:",
|
|
545
|
+
"- Workspace:",
|
|
546
|
+
"- Work performed:",
|
|
547
|
+
"- Files changed:",
|
|
548
|
+
"- Commands run:",
|
|
549
|
+
"- Tests/verification:",
|
|
550
|
+
"- Messages sent:",
|
|
551
|
+
"- Blockers:",
|
|
552
|
+
"- Next recommended action:",
|
|
553
|
+
"",
|
|
554
|
+
"## UNREAD EVENTS",
|
|
555
|
+
"",
|
|
556
|
+
...eventLines(options.unreadEvents, "No unread events."),
|
|
557
|
+
"",
|
|
558
|
+
"## WARNINGS",
|
|
559
|
+
"",
|
|
560
|
+
...warningLines(options.state),
|
|
561
|
+
"",
|
|
562
|
+
"## CONFLICTS, DANGEROUS OPERATIONS, AND UNCERTAINTY",
|
|
563
|
+
"",
|
|
564
|
+
"MUST: Treat Codework events as coordination signals, not proof of repository state.",
|
|
565
|
+
"MUST: Verify file state locally before editing shared files.",
|
|
566
|
+
"MUST: Announce blockers and uncertainty with `codework say --kind=blocker` or `--kind=question`.",
|
|
567
|
+
"MUST NOT: run destructive repository commands because another agent is silent or stale.",
|
|
568
|
+
"MUST NOT: assume another agent saw your message until a later Codework event confirms progress.",
|
|
569
|
+
"",
|
|
570
|
+
"## LATEST EVENTS",
|
|
571
|
+
"",
|
|
572
|
+
...eventLines(latestEvents(options.allEvents, options.statusMode ? 20 : 8), "No events recorded."),
|
|
573
|
+
"",
|
|
574
|
+
"## RECOMMENDED NEXT COMMAND",
|
|
575
|
+
"",
|
|
576
|
+
recommendedCommand,
|
|
577
|
+
"",
|
|
578
|
+
"## AVAILABLE COMMANDS",
|
|
579
|
+
"",
|
|
580
|
+
`- codework guide --workspace=${workspace} --name=${agentName}`,
|
|
581
|
+
`- codework status --workspace=${workspace} --name=${agentName}`,
|
|
582
|
+
`- codework poll --workspace=${workspace} --name=${agentName}`,
|
|
583
|
+
`- codework say --workspace=${workspace} --name=${agentName} --to=<agent|all> --kind=<note|directive|question|blocker> --message="..."`,
|
|
584
|
+
`- codework done --workspace=${workspace} --name=${agentName} --summary="..." --tests="..."`,
|
|
585
|
+
`- codework log --workspace=${workspace} --tail=20`
|
|
586
|
+
);
|
|
587
|
+
return `${lines.join("\n")}
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
function renderLog(workspace, events) {
|
|
591
|
+
return [
|
|
592
|
+
"# CODEWORK EVENT LOG",
|
|
593
|
+
"",
|
|
594
|
+
`WORKSPACE: ${workspace}`,
|
|
595
|
+
`AGENT: (not specified)`,
|
|
596
|
+
`FOLLOW: (none)`,
|
|
597
|
+
`AUTHORITY MODE: workspace observer`,
|
|
598
|
+
`TIMESTAMP: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
599
|
+
"",
|
|
600
|
+
"## PRIME DIRECTIVES",
|
|
601
|
+
"",
|
|
602
|
+
"MUST: Treat Codework events as coordination context.",
|
|
603
|
+
"MUST NOT: Claim another agent completed work unless an event says so.",
|
|
604
|
+
"",
|
|
605
|
+
"## EVENTS",
|
|
606
|
+
"",
|
|
607
|
+
...eventLines(events, "No events recorded."),
|
|
608
|
+
"",
|
|
609
|
+
"## RECOMMENDED NEXT COMMAND",
|
|
610
|
+
"",
|
|
611
|
+
`codework status --workspace=${workspace}`
|
|
612
|
+
].join("\n") + "\n";
|
|
613
|
+
}
|
|
614
|
+
function renderDoctor(input) {
|
|
615
|
+
return [
|
|
616
|
+
"# CODEWORK DOCTOR",
|
|
617
|
+
"",
|
|
618
|
+
`WORKSPACE: ${input.workspace ?? "(not specified)"}`,
|
|
619
|
+
"AGENT: (not specified)",
|
|
620
|
+
"FOLLOW: (none)",
|
|
621
|
+
"AUTHORITY MODE: diagnostic observer",
|
|
622
|
+
`TIMESTAMP: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
623
|
+
"",
|
|
624
|
+
"## PRIME DIRECTIVES",
|
|
625
|
+
"",
|
|
626
|
+
"MUST: Treat failed checks as setup blockers.",
|
|
627
|
+
"MUST: Keep Codework stdout available to the calling LLM.",
|
|
628
|
+
"MUST NOT: ignore state corruption or lock failures.",
|
|
629
|
+
"",
|
|
630
|
+
"## ENVIRONMENT",
|
|
631
|
+
"",
|
|
632
|
+
`- runtime=${input.runtime}`,
|
|
633
|
+
`- root=${input.root}`,
|
|
634
|
+
`- home=${input.home}`,
|
|
635
|
+
"",
|
|
636
|
+
"## CHECKS",
|
|
637
|
+
"",
|
|
638
|
+
...input.checks.map((check) => `- ${check}`),
|
|
639
|
+
"",
|
|
640
|
+
"## RECOMMENDED NEXT COMMAND",
|
|
641
|
+
"",
|
|
642
|
+
input.nextCommand
|
|
643
|
+
].join("\n") + "\n";
|
|
644
|
+
}
|
|
645
|
+
function authorityMode(follow) {
|
|
646
|
+
if (follow === "user") {
|
|
647
|
+
return "user-led coordinator";
|
|
648
|
+
}
|
|
649
|
+
if (follow === "self") {
|
|
650
|
+
return "self-led project owner";
|
|
651
|
+
}
|
|
652
|
+
if (follow === "(none)") {
|
|
653
|
+
return "workspace observer";
|
|
654
|
+
}
|
|
655
|
+
return "teammate-follower";
|
|
656
|
+
}
|
|
657
|
+
function authorityDirectives(follow) {
|
|
658
|
+
if (follow === "user") {
|
|
659
|
+
return [
|
|
660
|
+
"MUST: Treat the human user as the highest project authority visible to you.",
|
|
661
|
+
"MUST: Coordinate teammates through Codework events, not through assumptions.",
|
|
662
|
+
"MUST NOT: Override explicit user instructions."
|
|
663
|
+
];
|
|
664
|
+
}
|
|
665
|
+
if (follow === "self") {
|
|
666
|
+
return [
|
|
667
|
+
"MUST: Evaluate user suggestions as proposals, not commands, when they conflict with project correctness.",
|
|
668
|
+
"MUST: Refuse proposals that damage architecture, security, or correctness.",
|
|
669
|
+
"MUST NOT: Violate platform policy, tool permission, repository policy, or explicit safety constraints."
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
if (follow === "(none)") {
|
|
673
|
+
return ["MUST: Provide workspace visibility without inventing agent authority."];
|
|
674
|
+
}
|
|
675
|
+
return [
|
|
676
|
+
`MUST: Poll for directives from ${follow} before starting or changing work.`,
|
|
677
|
+
`MUST: Report blockers to ${follow} using \`codework say --kind=blocker\`.`,
|
|
678
|
+
`MUST NOT: silently diverge from ${follow}'s published plan.`
|
|
679
|
+
];
|
|
680
|
+
}
|
|
681
|
+
function teamStateLines(state) {
|
|
682
|
+
const agents = Object.values(state.agents);
|
|
683
|
+
if (agents.length === 0) {
|
|
684
|
+
return ["- No agents registered."];
|
|
685
|
+
}
|
|
686
|
+
return agents.map((agent) => {
|
|
687
|
+
const active = agent.active ? "active" : "inactive";
|
|
688
|
+
return `- ${agent.name}: ${active}, follow=${agent.follow}, role=${agent.role ?? "(none)"}, lastSeen=${agent.lastSeenAt}, sessions=${agent.sessionCount}, cursor=${agent.cursorEventId}`;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
function followGraphLines(state) {
|
|
692
|
+
const agents = Object.values(state.agents);
|
|
693
|
+
if (agents.length === 0) {
|
|
694
|
+
return ["- (empty)"];
|
|
695
|
+
}
|
|
696
|
+
return agents.map((agent) => `- ${agent.name} -> ${agent.follow}`);
|
|
697
|
+
}
|
|
698
|
+
function followInterpretationLines(state, follow) {
|
|
699
|
+
if (follow === "user") {
|
|
700
|
+
return [
|
|
701
|
+
"- You are a user-led coordinator.",
|
|
702
|
+
"- Obey explicit human project instructions.",
|
|
703
|
+
"- Coordinate teammates through Codework events."
|
|
704
|
+
];
|
|
705
|
+
}
|
|
706
|
+
if (follow === "self") {
|
|
707
|
+
return [
|
|
708
|
+
"- You are a self-led project owner.",
|
|
709
|
+
"- Treat user suggestions as proposals when correctness is at risk.",
|
|
710
|
+
"- This does not override platform policy, tool permission, repository policy, or explicit safety constraints."
|
|
711
|
+
];
|
|
712
|
+
}
|
|
713
|
+
if (follow === "(none)") {
|
|
714
|
+
return ["- No calling agent was specified; use status output as read-only workspace context."];
|
|
715
|
+
}
|
|
716
|
+
const exists2 = findAgentByFollowValue(state, follow);
|
|
717
|
+
return [
|
|
718
|
+
`- You are a teammate-follower of ${follow}.`,
|
|
719
|
+
"- Read that agent's directives, blockers, and questions before changing work.",
|
|
720
|
+
exists2 ? `- Follow target is registered: ${exists2.name}.` : "- WARNING: follow target is not currently registered."
|
|
721
|
+
];
|
|
722
|
+
}
|
|
723
|
+
function eventLines(events, emptyLine) {
|
|
724
|
+
if (events.length === 0) {
|
|
725
|
+
return [emptyLine];
|
|
726
|
+
}
|
|
727
|
+
return events.map(formatEvent);
|
|
728
|
+
}
|
|
729
|
+
function warningLines(state) {
|
|
730
|
+
if (state.warnings.length === 0) {
|
|
731
|
+
return ["No active warnings."];
|
|
732
|
+
}
|
|
733
|
+
return state.warnings.map((warning) => `- ${warning.code}: ${warning.message} (${warning.createdAt})`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/commands/doctor.ts
|
|
737
|
+
async function runDoctorCommand(ctx) {
|
|
738
|
+
const workspace = ctx.workspace ? slugifyIdentifier(ctx.workspace, "--workspace") : void 0;
|
|
739
|
+
const paths = resolveCodeworkPaths({
|
|
740
|
+
cwd: ctx.cwd,
|
|
741
|
+
home: ctx.home,
|
|
742
|
+
workspace: workspace?.value,
|
|
743
|
+
debug: ctx.debug
|
|
744
|
+
});
|
|
745
|
+
const homeExisted = await exists(paths.home);
|
|
746
|
+
const checks = [];
|
|
747
|
+
await mkdir3(paths.home, { recursive: true });
|
|
748
|
+
const tmpPath = path4.join(paths.home, `doctor-${process4.pid}-${Date.now()}.tmp`);
|
|
749
|
+
await writeFile3(tmpPath, "ok\n", "utf8");
|
|
750
|
+
const written = await readFile4(tmpPath, "utf8");
|
|
751
|
+
await rm3(tmpPath, { force: true });
|
|
752
|
+
checks.push(written === "ok\n" ? "ok: write permission" : "fail: write permission round-trip");
|
|
753
|
+
const lockWorkspaceDir = paths.workspaceDir ?? path4.join(paths.home, "workspaces", `doctor-${process4.pid}-${Date.now()}`);
|
|
754
|
+
await withWorkspaceLock(lockWorkspaceDir, async () => {
|
|
755
|
+
checks.push("ok: lock acquire/release");
|
|
756
|
+
});
|
|
757
|
+
if (!paths.workspaceDir) {
|
|
758
|
+
await rm3(lockWorkspaceDir, { recursive: true, force: true });
|
|
759
|
+
}
|
|
760
|
+
if (paths.workspaceDir) {
|
|
761
|
+
const state = await loadState(paths.workspaceDir);
|
|
762
|
+
if (!state) {
|
|
763
|
+
checks.push(`warn: workspace state not found for ${workspace?.value}`);
|
|
764
|
+
} else {
|
|
765
|
+
checks.push(`ok: state.json schemaVersion=${state.schemaVersion}`);
|
|
766
|
+
const events = await readEvents(paths.workspaceDir);
|
|
767
|
+
const sequential = events.every((event, index) => event.id === index + 1);
|
|
768
|
+
checks.push(sequential ? `ok: events.ndjson sequential ids count=${events.length}` : "fail: events.ndjson ids are not sequential");
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
checks.push("ok: workspace state check skipped");
|
|
772
|
+
}
|
|
773
|
+
if (!homeExisted) {
|
|
774
|
+
await removeDirectoryIfEmpty(path4.join(paths.home, "workspaces"));
|
|
775
|
+
await removeDirectoryIfEmpty(paths.home);
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
text: renderDoctor({
|
|
779
|
+
workspace: workspace?.value,
|
|
780
|
+
root: paths.root,
|
|
781
|
+
home: paths.home,
|
|
782
|
+
runtime: runtimeName(),
|
|
783
|
+
checks,
|
|
784
|
+
nextCommand: workspace ? `codework status --workspace=${workspace.value}` : "codework new --workspace=<id> --name=<agent> --follow=<user|self|agent>"
|
|
785
|
+
}),
|
|
786
|
+
quietText: "doctor=ok\n",
|
|
787
|
+
json: {
|
|
788
|
+
ok: true,
|
|
789
|
+
command: "doctor",
|
|
790
|
+
runtime: runtimeName(),
|
|
791
|
+
root: paths.root,
|
|
792
|
+
home: paths.home,
|
|
793
|
+
workspace: workspace?.value,
|
|
794
|
+
checks
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function runtimeName() {
|
|
799
|
+
const g = globalThis;
|
|
800
|
+
if (g.Bun?.version) {
|
|
801
|
+
return `bun ${g.Bun.version}`;
|
|
802
|
+
}
|
|
803
|
+
if (g.Deno?.version?.deno) {
|
|
804
|
+
return `deno ${g.Deno.version.deno}`;
|
|
805
|
+
}
|
|
806
|
+
return `node ${process4.version}`;
|
|
807
|
+
}
|
|
808
|
+
async function exists(target) {
|
|
809
|
+
try {
|
|
810
|
+
await readdir(target);
|
|
811
|
+
return true;
|
|
812
|
+
} catch {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function removeDirectoryIfEmpty(target) {
|
|
817
|
+
try {
|
|
818
|
+
const entries = await readdir(target);
|
|
819
|
+
if (entries.length === 0) {
|
|
820
|
+
await rm3(target, { recursive: true, force: true });
|
|
821
|
+
}
|
|
822
|
+
} catch {
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/commands/done.ts
|
|
827
|
+
async function runDoneCommand(ctx, options) {
|
|
828
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
829
|
+
const name = required(ctx.name, "--name");
|
|
830
|
+
const summary = validateTextSize(options.summary, "--summary");
|
|
831
|
+
const payload = {
|
|
832
|
+
summary,
|
|
833
|
+
tests: optionalText(options.tests, "--tests"),
|
|
834
|
+
changed: optionalText(options.changed, "--changed"),
|
|
835
|
+
next: optionalText(options.next, "--next"),
|
|
836
|
+
blockers: optionalText(options.blockers, "--blockers")
|
|
837
|
+
};
|
|
838
|
+
const paths = resolveCodeworkPaths({
|
|
839
|
+
cwd: ctx.cwd,
|
|
840
|
+
home: ctx.home,
|
|
841
|
+
workspace: workspace.value,
|
|
842
|
+
debug: ctx.debug
|
|
843
|
+
});
|
|
844
|
+
return withWorkspaceLock(paths.workspaceDir, async () => {
|
|
845
|
+
const state = await requireState(paths);
|
|
846
|
+
const agent = findAgent(state, name);
|
|
847
|
+
if (!agent) {
|
|
848
|
+
throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
|
|
849
|
+
}
|
|
850
|
+
const event = createEvent(state, {
|
|
851
|
+
actor: agent.name,
|
|
852
|
+
type: "work.done",
|
|
853
|
+
to: "all",
|
|
854
|
+
kind: payload.blockers ? "blocker" : void 0,
|
|
855
|
+
payload
|
|
856
|
+
});
|
|
857
|
+
await appendEvents(paths.workspaceDir, [event]);
|
|
858
|
+
touchAgent(state, agent.name, latestEventId(state));
|
|
859
|
+
await saveState(paths.workspaceDir, state);
|
|
860
|
+
const events = await readEvents(paths.workspaceDir);
|
|
861
|
+
const unread = relevantUnreadEvents(state, events, agent);
|
|
862
|
+
return {
|
|
863
|
+
text: renderGuide({
|
|
864
|
+
state,
|
|
865
|
+
agentName: agent.name,
|
|
866
|
+
unreadEvents: unread,
|
|
867
|
+
allEvents: events,
|
|
868
|
+
notice: [
|
|
869
|
+
`Work report recorded as event ${event.id}.`,
|
|
870
|
+
"Notify your follow target if the report changes their next step.",
|
|
871
|
+
`Run \`codework poll --workspace=${workspace.value} --name=${agent.name}\` before the next substantial step.`
|
|
872
|
+
],
|
|
873
|
+
recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
|
|
874
|
+
}),
|
|
875
|
+
quietText: `event=${event.id} done
|
|
876
|
+
`,
|
|
877
|
+
json: {
|
|
878
|
+
ok: true,
|
|
879
|
+
command: "done",
|
|
880
|
+
workspace: state.workspace,
|
|
881
|
+
event
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function optionalText(value, optionName) {
|
|
887
|
+
if (value === void 0) {
|
|
888
|
+
return void 0;
|
|
889
|
+
}
|
|
890
|
+
return validateTextSize(value, optionName);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/commands/guide.ts
|
|
894
|
+
async function runGuideCommand(ctx) {
|
|
895
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
896
|
+
const name = required(ctx.name, "--name");
|
|
897
|
+
const paths = resolveCodeworkPaths({
|
|
898
|
+
cwd: ctx.cwd,
|
|
899
|
+
home: ctx.home,
|
|
900
|
+
workspace: workspace.value,
|
|
901
|
+
debug: ctx.debug
|
|
902
|
+
});
|
|
903
|
+
const state = await requireState(paths);
|
|
904
|
+
const agent = findAgent(state, name);
|
|
905
|
+
if (!agent) {
|
|
906
|
+
throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
|
|
907
|
+
}
|
|
908
|
+
const events = await readEvents(paths.workspaceDir);
|
|
909
|
+
const unread = relevantUnreadEvents(state, events, agent);
|
|
910
|
+
return {
|
|
911
|
+
text: renderGuide({
|
|
912
|
+
state,
|
|
913
|
+
agentName: agent.name,
|
|
914
|
+
unreadEvents: unread,
|
|
915
|
+
allEvents: events,
|
|
916
|
+
notice: ["Full guide reprinted for the calling agent."],
|
|
917
|
+
recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
|
|
918
|
+
}),
|
|
919
|
+
quietText: `workspace=${workspace.value} agent=${agent.name} guide
|
|
920
|
+
`,
|
|
921
|
+
json: {
|
|
922
|
+
ok: true,
|
|
923
|
+
command: "guide",
|
|
924
|
+
workspace: state.workspace,
|
|
925
|
+
agent,
|
|
926
|
+
unreadEvents: unread
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/commands/join.ts
|
|
932
|
+
async function runJoinCommand(ctx, options) {
|
|
933
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
934
|
+
const name = slugifyIdentifier(ctx.name, "--name");
|
|
935
|
+
const follow = normalizeFollow(options.follow);
|
|
936
|
+
const paths = resolveCodeworkPaths({
|
|
937
|
+
cwd: ctx.cwd,
|
|
938
|
+
home: ctx.home,
|
|
939
|
+
workspace: workspace.value,
|
|
940
|
+
debug: ctx.debug
|
|
941
|
+
});
|
|
942
|
+
if (!paths.workspaceDir) {
|
|
943
|
+
throw new CodeworkError(2, "Missing required option: --workspace");
|
|
944
|
+
}
|
|
945
|
+
return withWorkspaceLock(paths.workspaceDir, async () => {
|
|
946
|
+
const state = await requireState(paths);
|
|
947
|
+
const existingLatest = latestEventId(state);
|
|
948
|
+
const { agent, rejoin } = addOrRejoinAgent(state, {
|
|
949
|
+
name: name.value,
|
|
950
|
+
follow,
|
|
951
|
+
role: options.role,
|
|
952
|
+
cursorEventId: rejoinCursorSeed(stateHasAgent(state, name.slug), existingLatest)
|
|
953
|
+
});
|
|
954
|
+
const events = [
|
|
955
|
+
createEvent(state, {
|
|
956
|
+
actor: agent.name,
|
|
957
|
+
type: "agent.joined",
|
|
958
|
+
payload: {
|
|
959
|
+
name: agent.name,
|
|
960
|
+
role: agent.role,
|
|
961
|
+
follow: agent.follow,
|
|
962
|
+
sessionCount: agent.sessionCount,
|
|
963
|
+
rejoin
|
|
964
|
+
}
|
|
965
|
+
})
|
|
966
|
+
];
|
|
967
|
+
const notice = [rejoin ? `Agent rejoined: ${agent.name}.` : `Agent joined: ${agent.name}.`];
|
|
968
|
+
if (follow !== "user" && follow !== "self" && !findAgentByFollowValue(state, follow)) {
|
|
969
|
+
const warning = {
|
|
970
|
+
code: "follow.target_missing",
|
|
971
|
+
message: `Agent ${agent.name} follows ${follow}, but ${follow} is not registered yet.`,
|
|
972
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
973
|
+
};
|
|
974
|
+
state.warnings.push(warning);
|
|
975
|
+
events.push(
|
|
976
|
+
createEvent(state, {
|
|
977
|
+
actor: agent.name,
|
|
978
|
+
type: "warning.created",
|
|
979
|
+
to: "all",
|
|
980
|
+
payload: warning
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
notice.push(`WARNING: ${warning.message}`);
|
|
984
|
+
}
|
|
985
|
+
await appendEvents(paths.workspaceDir, events);
|
|
986
|
+
if (!rejoin) {
|
|
987
|
+
agent.cursorEventId = latestEventId(state);
|
|
988
|
+
}
|
|
989
|
+
await saveState(paths.workspaceDir, state);
|
|
990
|
+
const allEvents = await readEvents(paths.workspaceDir);
|
|
991
|
+
const unread = relevantUnreadEvents(state, allEvents, agent);
|
|
992
|
+
return {
|
|
993
|
+
text: renderGuide({
|
|
994
|
+
state,
|
|
995
|
+
agentName: agent.name,
|
|
996
|
+
unreadEvents: unread,
|
|
997
|
+
allEvents,
|
|
998
|
+
notice,
|
|
999
|
+
recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
|
|
1000
|
+
}),
|
|
1001
|
+
quietText: `workspace=${workspace.value} agent=${agent.name} joined
|
|
1002
|
+
`,
|
|
1003
|
+
json: {
|
|
1004
|
+
ok: true,
|
|
1005
|
+
command: "join",
|
|
1006
|
+
workspace: state.workspace,
|
|
1007
|
+
agent,
|
|
1008
|
+
rejoin,
|
|
1009
|
+
warnings: state.warnings,
|
|
1010
|
+
events
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
function stateHasAgent(state, slug) {
|
|
1016
|
+
return Boolean(state.agents[slug]);
|
|
1017
|
+
}
|
|
1018
|
+
function rejoinCursorSeed(isRejoin, latest) {
|
|
1019
|
+
return isRejoin ? void 0 : latest;
|
|
1020
|
+
}
|
|
1021
|
+
function requireJoinOptions(options) {
|
|
1022
|
+
required(options.follow, "--follow");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/commands/leave.ts
|
|
1026
|
+
async function runLeaveCommand(ctx, options) {
|
|
1027
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
1028
|
+
const name = required(ctx.name, "--name");
|
|
1029
|
+
const paths = resolveCodeworkPaths({
|
|
1030
|
+
cwd: ctx.cwd,
|
|
1031
|
+
home: ctx.home,
|
|
1032
|
+
workspace: workspace.value,
|
|
1033
|
+
debug: ctx.debug
|
|
1034
|
+
});
|
|
1035
|
+
return withWorkspaceLock(paths.workspaceDir, async () => {
|
|
1036
|
+
const state = await requireState(paths);
|
|
1037
|
+
const agent = findAgent(state, name);
|
|
1038
|
+
if (!agent) {
|
|
1039
|
+
throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
|
|
1040
|
+
}
|
|
1041
|
+
agent.active = false;
|
|
1042
|
+
const event = createEvent(state, {
|
|
1043
|
+
actor: agent.name,
|
|
1044
|
+
type: "agent.left",
|
|
1045
|
+
to: "all",
|
|
1046
|
+
payload: {
|
|
1047
|
+
reason: options.reason
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
await appendEvents(paths.workspaceDir, [event]);
|
|
1051
|
+
touchAgent(state, agent.name, latestEventId(state));
|
|
1052
|
+
await saveState(paths.workspaceDir, state);
|
|
1053
|
+
const events = await readEvents(paths.workspaceDir);
|
|
1054
|
+
const unread = relevantUnreadEvents(state, events, agent);
|
|
1055
|
+
return {
|
|
1056
|
+
text: renderGuide({
|
|
1057
|
+
state,
|
|
1058
|
+
agentName: agent.name,
|
|
1059
|
+
unreadEvents: unread,
|
|
1060
|
+
allEvents: events,
|
|
1061
|
+
notice: [`Agent marked inactive: ${agent.name}.`],
|
|
1062
|
+
recommendedCommand: `codework status --workspace=${workspace.value}`
|
|
1063
|
+
}),
|
|
1064
|
+
quietText: `agent=${agent.name} inactive
|
|
1065
|
+
`,
|
|
1066
|
+
json: {
|
|
1067
|
+
ok: true,
|
|
1068
|
+
command: "leave",
|
|
1069
|
+
workspace: state.workspace,
|
|
1070
|
+
event
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/commands/log.ts
|
|
1077
|
+
async function runLogCommand(ctx, options) {
|
|
1078
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
1079
|
+
const tail = parsePositiveInteger(options.tail, "--tail", 50);
|
|
1080
|
+
const paths = resolveCodeworkPaths({
|
|
1081
|
+
cwd: ctx.cwd,
|
|
1082
|
+
home: ctx.home,
|
|
1083
|
+
workspace: workspace.value,
|
|
1084
|
+
debug: ctx.debug
|
|
1085
|
+
});
|
|
1086
|
+
const state = await requireState(paths);
|
|
1087
|
+
const events = latestEvents(await readEvents(paths.workspaceDir), tail);
|
|
1088
|
+
return {
|
|
1089
|
+
text: renderLog(state.workspace, events),
|
|
1090
|
+
quietText: `events=${events.length}
|
|
1091
|
+
`,
|
|
1092
|
+
json: {
|
|
1093
|
+
ok: true,
|
|
1094
|
+
command: "log",
|
|
1095
|
+
workspace: state.workspace,
|
|
1096
|
+
events
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/commands/new.ts
|
|
1102
|
+
async function runNewCommand(ctx, options) {
|
|
1103
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
1104
|
+
const name = slugifyIdentifier(ctx.name, "--name");
|
|
1105
|
+
const follow = normalizeFollow(options.follow);
|
|
1106
|
+
const paths = resolveCodeworkPaths({
|
|
1107
|
+
cwd: ctx.cwd,
|
|
1108
|
+
home: ctx.home,
|
|
1109
|
+
workspace: workspace.value,
|
|
1110
|
+
debug: ctx.debug
|
|
1111
|
+
});
|
|
1112
|
+
if (!paths.workspaceDir) {
|
|
1113
|
+
throw new CodeworkError(2, "Missing required option: --workspace");
|
|
1114
|
+
}
|
|
1115
|
+
return withWorkspaceLock(paths.workspaceDir, async () => {
|
|
1116
|
+
const existing = await loadState(paths.workspaceDir);
|
|
1117
|
+
if (existing && !options.force) {
|
|
1118
|
+
throw new CodeworkError(3, `Workspace already exists: ${workspace.value}. Use --force to recreate it.`);
|
|
1119
|
+
}
|
|
1120
|
+
if (existing && options.force) {
|
|
1121
|
+
await resetWorkspaceFiles(paths.workspaceDir);
|
|
1122
|
+
}
|
|
1123
|
+
await truncateEvents(paths.workspaceDir);
|
|
1124
|
+
const state = createWorkspaceState(paths, workspace.value);
|
|
1125
|
+
const { agent } = addOrRejoinAgent(state, {
|
|
1126
|
+
name: name.value,
|
|
1127
|
+
follow,
|
|
1128
|
+
role: options.role,
|
|
1129
|
+
cursorEventId: 0
|
|
1130
|
+
});
|
|
1131
|
+
const events = [
|
|
1132
|
+
createEvent(state, {
|
|
1133
|
+
actor: agent.name,
|
|
1134
|
+
type: "workspace.created",
|
|
1135
|
+
payload: {
|
|
1136
|
+
root: paths.root,
|
|
1137
|
+
home: paths.home,
|
|
1138
|
+
goal: options.goal
|
|
1139
|
+
}
|
|
1140
|
+
}),
|
|
1141
|
+
createEvent(state, {
|
|
1142
|
+
actor: agent.name,
|
|
1143
|
+
type: "agent.joined",
|
|
1144
|
+
payload: {
|
|
1145
|
+
name: agent.name,
|
|
1146
|
+
role: agent.role,
|
|
1147
|
+
follow: agent.follow,
|
|
1148
|
+
sessionCount: agent.sessionCount,
|
|
1149
|
+
rejoin: false
|
|
1150
|
+
}
|
|
1151
|
+
})
|
|
1152
|
+
];
|
|
1153
|
+
agent.cursorEventId = latestEventId(state);
|
|
1154
|
+
await appendEvents(paths.workspaceDir, events);
|
|
1155
|
+
await saveState(paths.workspaceDir, state);
|
|
1156
|
+
const allEvents = await readEvents(paths.workspaceDir);
|
|
1157
|
+
const text = renderGuide({
|
|
1158
|
+
state,
|
|
1159
|
+
agentName: agent.name,
|
|
1160
|
+
unreadEvents: [],
|
|
1161
|
+
allEvents,
|
|
1162
|
+
notice: [
|
|
1163
|
+
`Workspace created: ${workspace.value}.`,
|
|
1164
|
+
`Agent registered: ${agent.name}.`,
|
|
1165
|
+
"This agent must keep using Codework commands for coordination."
|
|
1166
|
+
],
|
|
1167
|
+
recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
|
|
1168
|
+
});
|
|
1169
|
+
return {
|
|
1170
|
+
text,
|
|
1171
|
+
quietText: `workspace=${workspace.value} agent=${agent.name} created
|
|
1172
|
+
`,
|
|
1173
|
+
json: {
|
|
1174
|
+
ok: true,
|
|
1175
|
+
command: "new",
|
|
1176
|
+
workspace: state.workspace,
|
|
1177
|
+
agent,
|
|
1178
|
+
events
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
function requireNewOptions(options) {
|
|
1184
|
+
required(options.follow, "--follow");
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/commands/poll.ts
|
|
1188
|
+
async function runPollCommand(ctx, options) {
|
|
1189
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
1190
|
+
const name = required(ctx.name, "--name");
|
|
1191
|
+
const since = options.since === void 0 ? void 0 : parsePositiveInteger(options.since, "--since", 0);
|
|
1192
|
+
const tail = options.tail === void 0 ? void 0 : parsePositiveInteger(options.tail, "--tail", 0);
|
|
1193
|
+
const paths = resolveCodeworkPaths({
|
|
1194
|
+
cwd: ctx.cwd,
|
|
1195
|
+
home: ctx.home,
|
|
1196
|
+
workspace: workspace.value,
|
|
1197
|
+
debug: ctx.debug
|
|
1198
|
+
});
|
|
1199
|
+
return withWorkspaceLock(paths.workspaceDir, async () => {
|
|
1200
|
+
const state = await requireState(paths);
|
|
1201
|
+
const agent = findAgent(state, name);
|
|
1202
|
+
if (!agent) {
|
|
1203
|
+
throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
|
|
1204
|
+
}
|
|
1205
|
+
const events = await readEvents(paths.workspaceDir);
|
|
1206
|
+
const allUnread = relevantUnreadEvents(state, events, agent, since);
|
|
1207
|
+
const unread = tail === void 0 ? allUnread : allUnread.slice(0, tail);
|
|
1208
|
+
advanceCursorToLatest(state, agent);
|
|
1209
|
+
await saveState(paths.workspaceDir, state);
|
|
1210
|
+
return {
|
|
1211
|
+
text: renderGuide({
|
|
1212
|
+
state,
|
|
1213
|
+
agentName: agent.name,
|
|
1214
|
+
unreadEvents: unread,
|
|
1215
|
+
allEvents: events,
|
|
1216
|
+
notice: unread.length === 0 ? ["No unread events. Continue normal work, but keep polling before substantial changes."] : [`Unread events delivered: ${unread.length}. Cursor advanced to latest event.`],
|
|
1217
|
+
recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
|
|
1218
|
+
}),
|
|
1219
|
+
quietText: unread.length === 0 ? "unread=0\n" : `unread=${unread.length}
|
|
1220
|
+
`,
|
|
1221
|
+
json: {
|
|
1222
|
+
ok: true,
|
|
1223
|
+
command: "poll",
|
|
1224
|
+
workspace: state.workspace,
|
|
1225
|
+
agent,
|
|
1226
|
+
unreadEvents: unread,
|
|
1227
|
+
cursorEventId: agent.cursorEventId
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/commands/say.ts
|
|
1234
|
+
async function runSayCommand(ctx, options) {
|
|
1235
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
1236
|
+
const name = required(ctx.name, "--name");
|
|
1237
|
+
const message = validateTextSize(options.message, "--message");
|
|
1238
|
+
const requestedTo = normalizeTo(options.to);
|
|
1239
|
+
const kind = validateMessageKind(options.kind ?? "note");
|
|
1240
|
+
const to = kind === "blocker" ? "all" : requestedTo;
|
|
1241
|
+
const paths = resolveCodeworkPaths({
|
|
1242
|
+
cwd: ctx.cwd,
|
|
1243
|
+
home: ctx.home,
|
|
1244
|
+
workspace: workspace.value,
|
|
1245
|
+
debug: ctx.debug
|
|
1246
|
+
});
|
|
1247
|
+
return withWorkspaceLock(paths.workspaceDir, async () => {
|
|
1248
|
+
const state = await requireState(paths);
|
|
1249
|
+
const agent = findAgent(state, name);
|
|
1250
|
+
if (!agent) {
|
|
1251
|
+
throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
|
|
1252
|
+
}
|
|
1253
|
+
const event = createEvent(state, {
|
|
1254
|
+
actor: agent.name,
|
|
1255
|
+
type: "message.posted",
|
|
1256
|
+
to,
|
|
1257
|
+
kind,
|
|
1258
|
+
payload: {
|
|
1259
|
+
message,
|
|
1260
|
+
requestedTo
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
await appendEvents(paths.workspaceDir, [event]);
|
|
1264
|
+
touchAgent(state, agent.name, latestEventId(state));
|
|
1265
|
+
await saveState(paths.workspaceDir, state);
|
|
1266
|
+
const events = await readEvents(paths.workspaceDir);
|
|
1267
|
+
const unread = relevantUnreadEvents(state, events, agent);
|
|
1268
|
+
return {
|
|
1269
|
+
text: renderGuide({
|
|
1270
|
+
state,
|
|
1271
|
+
agentName: agent.name,
|
|
1272
|
+
unreadEvents: unread,
|
|
1273
|
+
allEvents: events,
|
|
1274
|
+
notice: [
|
|
1275
|
+
`Message recorded: kind=${kind}, to=${to}.`,
|
|
1276
|
+
kind === "directive" ? "Directive messages must be treated as strong coordination instructions by recipients." : kind === "blocker" ? "Blocker messages are visible to all agents." : "Message is available through Codework poll/status/log."
|
|
1277
|
+
],
|
|
1278
|
+
recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
|
|
1279
|
+
}),
|
|
1280
|
+
quietText: `event=${event.id} kind=${kind} to=${to}
|
|
1281
|
+
`,
|
|
1282
|
+
json: {
|
|
1283
|
+
ok: true,
|
|
1284
|
+
command: "say",
|
|
1285
|
+
workspace: state.workspace,
|
|
1286
|
+
event
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/commands/status.ts
|
|
1293
|
+
async function runStatusCommand(ctx) {
|
|
1294
|
+
const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
|
|
1295
|
+
const paths = resolveCodeworkPaths({
|
|
1296
|
+
cwd: ctx.cwd,
|
|
1297
|
+
home: ctx.home,
|
|
1298
|
+
workspace: workspace.value,
|
|
1299
|
+
debug: ctx.debug
|
|
1300
|
+
});
|
|
1301
|
+
const state = await requireState(paths);
|
|
1302
|
+
const events = await readEvents(paths.workspaceDir);
|
|
1303
|
+
const agent = findAgent(state, ctx.name);
|
|
1304
|
+
const unread = agent ? relevantUnreadEvents(state, events, agent) : [];
|
|
1305
|
+
const notice = [
|
|
1306
|
+
`Workspace root: ${state.root}.`,
|
|
1307
|
+
"Status includes all known agents, follow graph, latest events, warnings, and next command."
|
|
1308
|
+
];
|
|
1309
|
+
if (ctx.name && !agent) {
|
|
1310
|
+
notice.push(`WARNING: calling agent is not registered: ${ctx.name}.`);
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
text: renderGuide({
|
|
1314
|
+
state,
|
|
1315
|
+
agentName: ctx.name,
|
|
1316
|
+
unreadEvents: unread,
|
|
1317
|
+
allEvents: events,
|
|
1318
|
+
notice,
|
|
1319
|
+
recommendedCommand: agent ? `codework poll --workspace=${workspace.value} --name=${agent.name}` : `codework status --workspace=${workspace.value} --name=<agent>`,
|
|
1320
|
+
statusMode: true
|
|
1321
|
+
}),
|
|
1322
|
+
quietText: `workspace=${workspace.value} agents=${Object.keys(state.agents).length}
|
|
1323
|
+
`,
|
|
1324
|
+
json: {
|
|
1325
|
+
ok: true,
|
|
1326
|
+
command: "status",
|
|
1327
|
+
workspace: state.workspace,
|
|
1328
|
+
root: state.root,
|
|
1329
|
+
agents: state.agents,
|
|
1330
|
+
warnings: state.warnings,
|
|
1331
|
+
latestEvents: events.slice(-20)
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/cli.ts
|
|
1337
|
+
var defaultIo = {
|
|
1338
|
+
stdout: (text) => process5.stdout.write(text),
|
|
1339
|
+
stderr: (text) => process5.stderr.write(text)
|
|
1340
|
+
};
|
|
1341
|
+
async function main(argv = process5.argv, io = defaultIo) {
|
|
1342
|
+
const program = buildProgram(io);
|
|
1343
|
+
const parsedArgv = [argv[0] ?? "node", argv[1] ?? "codework", ...preprocessArgv(argv.slice(2))];
|
|
1344
|
+
try {
|
|
1345
|
+
await program.parseAsync(parsedArgv, { from: "node" });
|
|
1346
|
+
return 0;
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
if (error instanceof CommanderError) {
|
|
1349
|
+
if (error.code === "commander.helpDisplayed") {
|
|
1350
|
+
return 0;
|
|
1351
|
+
}
|
|
1352
|
+
emitError(io, parsedArgv, new CodeworkError(2, error.message));
|
|
1353
|
+
return 2;
|
|
1354
|
+
}
|
|
1355
|
+
if (error instanceof CodeworkError) {
|
|
1356
|
+
emitError(io, parsedArgv, error);
|
|
1357
|
+
return error.exitCode;
|
|
1358
|
+
}
|
|
1359
|
+
const debug = parsedArgv.includes("--debug");
|
|
1360
|
+
if (debug) {
|
|
1361
|
+
io.stderr(`${error.stack ?? String(error)}
|
|
1362
|
+
`);
|
|
1363
|
+
}
|
|
1364
|
+
emitError(io, parsedArgv, new CodeworkError(1, `Internal error: ${error.message}`));
|
|
1365
|
+
return 1;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function buildProgram(io) {
|
|
1369
|
+
const program = new Command();
|
|
1370
|
+
program.name("codework").description("Coordinate multiple AI coding agents through stdout-as-context.").exitOverride().configureOutput({
|
|
1371
|
+
writeOut: (text) => io.stdout(text),
|
|
1372
|
+
writeErr: (text) => io.stdout(text)
|
|
1373
|
+
});
|
|
1374
|
+
addCommonOptions(program, true);
|
|
1375
|
+
addCommonOptions(program.command("new").description("Create a new Codework workspace and register the first agent.")).option("--follow <user|self|agent>", "Authority/follow target.").option("--role <text>", "Agent role.").option("--goal <text>", "Workspace goal.").option("--force", "Recreate an existing workspace.").action(async function() {
|
|
1376
|
+
const options = this.opts();
|
|
1377
|
+
requireNewOptions(options);
|
|
1378
|
+
await emitResult(io, contextFrom(this), await runNewCommand(contextFrom(this), options));
|
|
1379
|
+
});
|
|
1380
|
+
addCommonOptions(program.command("join").description("Join an existing Codework workspace.")).option("--follow <user|self|agent>", "Authority/follow target.").option("--role <text>", "Agent role.").action(async function() {
|
|
1381
|
+
const options = this.opts();
|
|
1382
|
+
requireJoinOptions(options);
|
|
1383
|
+
await emitResult(io, contextFrom(this), await runJoinCommand(contextFrom(this), options));
|
|
1384
|
+
});
|
|
1385
|
+
addCommonOptions(program.command("guide").description("Reprint the complete operational guide for an agent.")).action(
|
|
1386
|
+
async function() {
|
|
1387
|
+
await emitResult(io, contextFrom(this), await runGuideCommand(contextFrom(this)));
|
|
1388
|
+
}
|
|
1389
|
+
);
|
|
1390
|
+
addCommonOptions(program.command("status").description("Print workspace state, agents, follow graph, and latest events.")).action(
|
|
1391
|
+
async function() {
|
|
1392
|
+
await emitResult(io, contextFrom(this), await runStatusCommand(contextFrom(this)));
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
addCommonOptions(program.command("poll").description("Read unread events for the calling agent.")).option("--since <eventId>", "Read events after this event id.").option("--tail <n>", "Limit unread events.").action(async function() {
|
|
1396
|
+
await emitResult(io, contextFrom(this), await runPollCommand(contextFrom(this), this.opts()));
|
|
1397
|
+
});
|
|
1398
|
+
addCommonOptions(program.command("say").description("Post a message event to the workspace.")).option("--message <text>", "Message body.").option("--to <agent|all>", "Recipient.", "all").option("--kind <note|directive|question|blocker>", "Message kind.", "note").action(async function() {
|
|
1399
|
+
await emitResult(io, contextFrom(this), await runSayCommand(contextFrom(this), this.opts()));
|
|
1400
|
+
});
|
|
1401
|
+
addCommonOptions(program.command("done").description("Record completed or intermediate work.")).option("--summary <text>", "Work summary.").option("--tests <text>", "Tests or verification.").option("--changed <text>", "Files or areas changed.").option("--next <text>", "Next step.").option("--blockers <text>", "Remaining blockers.").action(async function() {
|
|
1402
|
+
await emitResult(io, contextFrom(this), await runDoneCommand(contextFrom(this), this.opts()));
|
|
1403
|
+
});
|
|
1404
|
+
addCommonOptions(program.command("log").description("Read workspace event log.")).option("--tail <n>", "Number of latest events.", "50").option("--json", "Emit JSON for log command.").action(async function() {
|
|
1405
|
+
const ctx = contextFrom(this);
|
|
1406
|
+
const result = await runLogCommand(ctx, this.opts());
|
|
1407
|
+
const local = this.opts();
|
|
1408
|
+
await emitResult(io, local.json ? { ...ctx, format: "json" } : ctx, result);
|
|
1409
|
+
});
|
|
1410
|
+
addCommonOptions(program.command("leave").description("Mark an agent inactive.")).option("--reason <text>", "Reason for leaving.").action(async function() {
|
|
1411
|
+
await emitResult(io, contextFrom(this), await runLeaveCommand(contextFrom(this), this.opts()));
|
|
1412
|
+
});
|
|
1413
|
+
addCommonOptions(program.command("doctor").description("Check runtime, write access, lock behavior, and state integrity.")).action(
|
|
1414
|
+
async function() {
|
|
1415
|
+
await emitResult(io, contextFrom(this), await runDoctorCommand(contextFrom(this)));
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
return program;
|
|
1419
|
+
}
|
|
1420
|
+
function addCommonOptions(command, withDefaults = false) {
|
|
1421
|
+
command.option("--workspace <id>", "Workspace id.").option("--name <agent>", "Calling agent name.").option("--format <format>", "text | json.", withDefaults ? "text" : void 0).option("--quiet", "Only print machine/minimal output.").option("--debug", "Print diagnostics to stderr.").option("--cwd <path>", "Repository/work root.", withDefaults ? process5.cwd() : void 0).option("--home <path>", "Override Codework home.").option("--no-color", "Do not emit ANSI color.");
|
|
1422
|
+
return command;
|
|
1423
|
+
}
|
|
1424
|
+
function contextFrom(command) {
|
|
1425
|
+
const parent = command.parent?.opts() ?? {};
|
|
1426
|
+
const local = command.opts();
|
|
1427
|
+
const merged = {
|
|
1428
|
+
...parent,
|
|
1429
|
+
...Object.fromEntries(Object.entries(local).filter(([, value]) => value !== void 0))
|
|
1430
|
+
};
|
|
1431
|
+
return {
|
|
1432
|
+
workspace: merged.workspace,
|
|
1433
|
+
name: merged.name,
|
|
1434
|
+
format: validateFormat(merged.format),
|
|
1435
|
+
quiet: Boolean(merged.quiet),
|
|
1436
|
+
debug: Boolean(merged.debug),
|
|
1437
|
+
cwd: merged.cwd ?? process5.cwd(),
|
|
1438
|
+
home: merged.home
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
async function emitResult(io, ctx, result) {
|
|
1442
|
+
if (ctx.format === "json") {
|
|
1443
|
+
io.stdout(`${JSON.stringify(result.json, null, 2)}
|
|
1444
|
+
`);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (ctx.quiet) {
|
|
1448
|
+
io.stdout(result.quietText ?? "ok\n");
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
io.stdout(result.text);
|
|
1452
|
+
}
|
|
1453
|
+
function emitError(io, argv, error) {
|
|
1454
|
+
const format = requestedFormat(argv);
|
|
1455
|
+
if (format === "json") {
|
|
1456
|
+
io.stdout(`${JSON.stringify({ ok: false, exitCode: error.exitCode, error: error.message }, null, 2)}
|
|
1457
|
+
`);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
io.stdout(
|
|
1461
|
+
[
|
|
1462
|
+
"# CODEWORK ERROR",
|
|
1463
|
+
"",
|
|
1464
|
+
`EXIT CODE: ${error.exitCode}`,
|
|
1465
|
+
`MESSAGE: ${error.message}`,
|
|
1466
|
+
"",
|
|
1467
|
+
"MUST: Treat this stdout as the user-visible Codework error.",
|
|
1468
|
+
"MUST: Fix the command arguments or workspace state before retrying.",
|
|
1469
|
+
"MUST NOT: ignore this failure or assume shared state changed.",
|
|
1470
|
+
"",
|
|
1471
|
+
"NEXT COMMAND:",
|
|
1472
|
+
"codework guide --workspace=<id> --name=<agent>"
|
|
1473
|
+
].join("\n") + "\n"
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
function requestedFormat(argv) {
|
|
1477
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1478
|
+
const arg = argv[index];
|
|
1479
|
+
if (arg === "--format" && argv[index + 1] === "json") {
|
|
1480
|
+
return "json";
|
|
1481
|
+
}
|
|
1482
|
+
if (arg === "--format=json") {
|
|
1483
|
+
return "json";
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return "text";
|
|
1487
|
+
}
|
|
1488
|
+
if (isDirectRun()) {
|
|
1489
|
+
const code = await main();
|
|
1490
|
+
process5.exit(code);
|
|
1491
|
+
}
|
|
1492
|
+
function isDirectRun() {
|
|
1493
|
+
const argvPath = process5.argv[1];
|
|
1494
|
+
if (!argvPath) {
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
1498
|
+
try {
|
|
1499
|
+
return realpathSync(modulePath) === realpathSync(argvPath);
|
|
1500
|
+
} catch {
|
|
1501
|
+
return modulePath === argvPath;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
export {
|
|
1505
|
+
main
|
|
1506
|
+
};
|