@lifeaitools/clauth 1.5.78 → 1.5.80
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/cli/assets/codevelop/launcher-active.cmd.template +20 -0
- package/cli/assets/codevelop/launcher-static.cmd.template +7 -0
- package/cli/assets/codevelop/windows-terminal.profiles.json +48 -0
- package/cli/commands/codevelop.js +907 -0
- package/cli/commands/serve.js +618 -23
- package/cli/index.js +40 -0
- package/cli/studio-debug.js +0 -37
- package/install.ps1 +21 -7
- package/package.json +1 -1
- package/scripts/postinstall.js +20 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
// cli/commands/codevelop.js
|
|
2
|
+
// Windows Terminal installer + launcher for Claude/Codex co-development.
|
|
3
|
+
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import http from "http";
|
|
7
|
+
import https from "https";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { spawn, spawnSync } from "child_process";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_REPO = "C:\\Dev\\regen-root";
|
|
14
|
+
const DEFAULT_PORT = 53137;
|
|
15
|
+
const RESERVED_PORTS = new Set([52437, 52438]);
|
|
16
|
+
const CLAUDE_PROFILE = "Co Develop Claude";
|
|
17
|
+
const CODEX_PROFILE = "Co Develop Codex";
|
|
18
|
+
const ASSET_DIR = path.resolve(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "codevelop"));
|
|
19
|
+
|
|
20
|
+
function asBool(value) {
|
|
21
|
+
return value === true || value === "true";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStateDir(repo) {
|
|
25
|
+
return path.join(repo, ".rdc", "co-develop");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getClauthAppDir() {
|
|
29
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getLauncherPath(peer) {
|
|
33
|
+
return path.join(getClauthAppDir(), `codevelop-${peer}.cmd`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getActiveLauncherPath(peer) {
|
|
37
|
+
return path.join(getClauthAppDir(), `codevelop-${peer}-active.cmd`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getContextPath(peer) {
|
|
41
|
+
return path.join(getClauthAppDir(), `codevelop-${peer}-context.txt`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getRequestPath() {
|
|
45
|
+
return path.join(getClauthAppDir(), "codevelop-request.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cmdValue(value) {
|
|
49
|
+
return String(value ?? "").replace(/%/g, "%%").replace(/"/g, "'");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderTemplate(template, values) {
|
|
53
|
+
return template.replace(/\$\{([A-Z_]+)\}/g, (_, key) => values[key] ?? "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderValue(value, values) {
|
|
57
|
+
if (typeof value === "string") return renderTemplate(value, values);
|
|
58
|
+
if (Array.isArray(value)) return value.map(item => renderValue(item, values));
|
|
59
|
+
if (value && typeof value === "object") {
|
|
60
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, renderValue(child, values)]));
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readCodevelopConfig() {
|
|
66
|
+
return readJsonFile(path.join(ASSET_DIR, "windows-terminal.profiles.json"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeLauncherScripts(repo) {
|
|
70
|
+
const appDir = getClauthAppDir();
|
|
71
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
const template = fs.readFileSync(path.join(ASSET_DIR, "launcher-static.cmd.template"), "utf8");
|
|
74
|
+
const launchers = {
|
|
75
|
+
claude: getLauncherPath("claude"),
|
|
76
|
+
codex: getLauncherPath("codex"),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
for (const [peer, launcherPath] of Object.entries(launchers)) {
|
|
80
|
+
fs.writeFileSync(launcherPath, renderTemplate(template, {
|
|
81
|
+
ACTIVE_LAUNCHER: getActiveLauncherPath(peer),
|
|
82
|
+
REPO: repo,
|
|
83
|
+
PORT: String(DEFAULT_PORT),
|
|
84
|
+
}).replace(/\n/g, "\r\n"), "utf8");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return launchers;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeActiveLauncherScripts(manifest) {
|
|
91
|
+
const appDir = getClauthAppDir();
|
|
92
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
const codexCmd = path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "npm", "codex.cmd");
|
|
95
|
+
const claudeCmd = "claude";
|
|
96
|
+
const cliPath = path.resolve(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "index.js"));
|
|
97
|
+
const template = fs.readFileSync(path.join(ASSET_DIR, "launcher-active.cmd.template"), "utf8");
|
|
98
|
+
|
|
99
|
+
for (const peer of ["claude", "codex"]) {
|
|
100
|
+
const peerConfig = manifest.peers[peer];
|
|
101
|
+
const prompt = [
|
|
102
|
+
`You are peer=${peer} in co-development session ${manifest.session_id}.`,
|
|
103
|
+
`Your default partner is ${peerConfig.target_peer}.`,
|
|
104
|
+
`Use clauth codevelop tools at ${manifest.transport.base_url} to send and receive partner messages.`,
|
|
105
|
+
`When you need partner help, run: clauth codevelop ask --to ${peerConfig.target_peer} --wait --task "<specific task>".`,
|
|
106
|
+
"When you complete a partner request, run: clauth codevelop reply --turn <turn_id> --summary \"...\" --evidence \"...\" --files-changed \"...\".",
|
|
107
|
+
"Do not hand-write curl JSON unless the CLI command fails.",
|
|
108
|
+
"On startup, acknowledge co-development readiness, poll your inbox, and report any queued partner messages.",
|
|
109
|
+
"Reply with evidence, files changed, commits, blockers, and next action.",
|
|
110
|
+
`Manifest: ${path.join(getStateDir(manifest.repo), `${manifest.session_id}.json`)}`,
|
|
111
|
+
].join(" ");
|
|
112
|
+
const contextPath = getContextPath(peer);
|
|
113
|
+
fs.writeFileSync(contextPath, `${prompt}\n`, "utf8");
|
|
114
|
+
|
|
115
|
+
const command = peer === "claude"
|
|
116
|
+
? `${claudeCmd} -n "Co Develop Claude" --append-system-prompt "${cmdValue(prompt)}" %*`
|
|
117
|
+
: `call "${codexCmd}" -C "${cmdValue(peerConfig.cwd)}" %*`;
|
|
118
|
+
|
|
119
|
+
fs.writeFileSync(getActiveLauncherPath(peer), renderTemplate(template, {
|
|
120
|
+
SESSION_ID: cmdValue(manifest.session_id),
|
|
121
|
+
PEER: peer,
|
|
122
|
+
TARGET_PEER: cmdValue(peerConfig.target_peer),
|
|
123
|
+
BASE_URL: cmdValue(manifest.transport.base_url),
|
|
124
|
+
MANIFEST: cmdValue(path.join(getStateDir(manifest.repo), `${manifest.session_id}.json`)),
|
|
125
|
+
CONTEXT_FILE: cmdValue(contextPath),
|
|
126
|
+
CLAUTH_CLI: cmdValue(cliPath),
|
|
127
|
+
REPO: cmdValue(manifest.repo),
|
|
128
|
+
CWD: cmdValue(peerConfig.cwd),
|
|
129
|
+
PRINT_COMMAND: peer === "claude" ? "claude -n \"Co Develop Claude\" --append-system-prompt [co-develop system prompt]" : `codex -C "${cmdValue(peerConfig.cwd)}"`,
|
|
130
|
+
COMMAND: command,
|
|
131
|
+
}).replace(/\n/g, "\r\n"), "utf8");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getWindowsTerminalSettingsPath() {
|
|
136
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
137
|
+
const candidates = [
|
|
138
|
+
path.join(localAppData, "Packages", "Microsoft.WindowsTerminal_8wekyb3d8bbwe", "LocalState", "settings.json"),
|
|
139
|
+
path.join(localAppData, "Packages", "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", "LocalState", "settings.json"),
|
|
140
|
+
path.join(localAppData, "Microsoft", "Windows Terminal", "settings.json"),
|
|
141
|
+
];
|
|
142
|
+
return candidates.find(p => fs.existsSync(p)) || candidates[0];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readJsonFile(filePath) {
|
|
146
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function upsertProfile(list, profile) {
|
|
150
|
+
const existing = list.find(p => p.name === profile.name || p.guid === profile.guid);
|
|
151
|
+
if (existing) {
|
|
152
|
+
Object.assign(existing, profile);
|
|
153
|
+
} else {
|
|
154
|
+
list.push(profile);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function installWindowsTerminalProfiles({ repo = DEFAULT_REPO, dryRun = false } = {}) {
|
|
159
|
+
if (process.platform !== "win32") {
|
|
160
|
+
throw new Error("Windows Terminal profile install is only supported on Windows.");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const settingsPath = getWindowsTerminalSettingsPath();
|
|
164
|
+
if (!fs.existsSync(settingsPath)) {
|
|
165
|
+
throw new Error(`Windows Terminal settings not found at ${settingsPath}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const settings = readJsonFile(settingsPath);
|
|
169
|
+
settings.profiles ||= {};
|
|
170
|
+
settings.profiles.list ||= [];
|
|
171
|
+
const launchers = dryRun
|
|
172
|
+
? { claude: getLauncherPath("claude"), codex: getLauncherPath("codex") }
|
|
173
|
+
: writeLauncherScripts(repo);
|
|
174
|
+
const config = readCodevelopConfig();
|
|
175
|
+
const profiles = config.profiles.map(profile => renderValue(profile, {
|
|
176
|
+
REPO: repo,
|
|
177
|
+
CLAUTH_APPDATA: getClauthAppDir(),
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
for (const profile of profiles) upsertProfile(settings.profiles.list, profile);
|
|
181
|
+
|
|
182
|
+
const backupPath = `${settingsPath}.bak-codevelop-${new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-")}`;
|
|
183
|
+
if (!dryRun) {
|
|
184
|
+
fs.copyFileSync(settingsPath, backupPath);
|
|
185
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { settingsPath, backupPath: dryRun ? null : backupPath, profiles, launchers };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function requestJson(method, url, body) {
|
|
192
|
+
const opts = { method, headers: {} };
|
|
193
|
+
if (body !== undefined) {
|
|
194
|
+
opts.headers["content-type"] = "application/json";
|
|
195
|
+
opts.body = JSON.stringify(body);
|
|
196
|
+
}
|
|
197
|
+
const response = await fetch(url, opts);
|
|
198
|
+
const text = await response.text();
|
|
199
|
+
let data = {};
|
|
200
|
+
try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
|
|
201
|
+
if (!response.ok) throw new Error(`${method} ${url} failed: HTTP ${response.status} ${text}`);
|
|
202
|
+
return data;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function testPing(baseUrl) {
|
|
206
|
+
try {
|
|
207
|
+
const result = await requestJson("GET", `${baseUrl}/ping`);
|
|
208
|
+
return result?.status === "ok";
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function startIsolatedClauth(port) {
|
|
215
|
+
const cliPath = path.resolve(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "index.js"));
|
|
216
|
+
const child = spawn(process.execPath, [cliPath, "serve", "foreground", "--port", String(port), "--isolated"], {
|
|
217
|
+
detached: true,
|
|
218
|
+
stdio: "ignore",
|
|
219
|
+
windowsHide: true,
|
|
220
|
+
});
|
|
221
|
+
child.unref();
|
|
222
|
+
return child.pid;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function waitForPing(baseUrl) {
|
|
226
|
+
for (let i = 0; i < 30; i += 1) {
|
|
227
|
+
if (await testPing(baseUrl)) return true;
|
|
228
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ensureWorktree(repo, sessionId, dryRun) {
|
|
234
|
+
const codexRepo = `C:\\Dev\\regen-root-codex-${sessionId}`;
|
|
235
|
+
const branch = `codex/${sessionId}`;
|
|
236
|
+
if (!dryRun && !fs.existsSync(codexRepo)) {
|
|
237
|
+
const result = spawnSync("git", ["-C", repo, "worktree", "add", "-b", branch, codexRepo, "develop"], {
|
|
238
|
+
stdio: "inherit",
|
|
239
|
+
shell: false,
|
|
240
|
+
});
|
|
241
|
+
if (result.status !== 0) throw new Error(`git worktree add failed with exit code ${result.status}`);
|
|
242
|
+
}
|
|
243
|
+
return { codexRepo, branch };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function writeManifest({ repo, sessionId, name, baseUrl, status, codexRepo, branch, startedClauthPid, dryRun }) {
|
|
247
|
+
const stateDir = getStateDir(repo);
|
|
248
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
249
|
+
const manifestPath = path.join(stateDir, `${sessionId}.json`);
|
|
250
|
+
const manifest = {
|
|
251
|
+
session_id: sessionId,
|
|
252
|
+
name,
|
|
253
|
+
repo,
|
|
254
|
+
mode: "start-pair",
|
|
255
|
+
transport: { type: "clauth-codevelop", base_url: baseUrl },
|
|
256
|
+
peers: {
|
|
257
|
+
claude: {
|
|
258
|
+
role: "supervisor",
|
|
259
|
+
cwd: repo,
|
|
260
|
+
branch: "develop",
|
|
261
|
+
target_peer: "codex",
|
|
262
|
+
stream_url: `${baseUrl}/codevelop/${sessionId}/claude/stream`,
|
|
263
|
+
},
|
|
264
|
+
codex: {
|
|
265
|
+
role: "implementation_partner",
|
|
266
|
+
cwd: codexRepo,
|
|
267
|
+
branch,
|
|
268
|
+
target_peer: "claude",
|
|
269
|
+
stream_url: `${baseUrl}/codevelop/${sessionId}/codex/stream`,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
clauth_status: status,
|
|
273
|
+
clauth_process: startedClauthPid
|
|
274
|
+
? { pid: startedClauthPid, started_by_launcher: true, isolated: true }
|
|
275
|
+
: { started_by_launcher: false, isolated: true },
|
|
276
|
+
commands: {
|
|
277
|
+
claude: `Windows Terminal profile: ${CLAUDE_PROFILE}`,
|
|
278
|
+
codex: `Windows Terminal profile: ${CODEX_PROFILE}`,
|
|
279
|
+
wt: buildWtArgs().display,
|
|
280
|
+
},
|
|
281
|
+
manual_prompt_required: false,
|
|
282
|
+
context_contract: {
|
|
283
|
+
context_file: null,
|
|
284
|
+
required_reads: [],
|
|
285
|
+
instruction: "The initiating peer should provide a context file or plan/architecture path with each substantive ask.",
|
|
286
|
+
},
|
|
287
|
+
created_at: new Date().toISOString(),
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
291
|
+
if (!dryRun) {
|
|
292
|
+
fs.writeFileSync(path.join(stateDir, "active.json"), `${JSON.stringify({
|
|
293
|
+
session_id: sessionId,
|
|
294
|
+
manifest: manifestPath,
|
|
295
|
+
updated_at: new Date().toISOString(),
|
|
296
|
+
}, null, 2)}\n`, "utf8");
|
|
297
|
+
writeActiveLauncherScripts(manifest);
|
|
298
|
+
}
|
|
299
|
+
return manifestPath;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildWtArgs() {
|
|
303
|
+
const args = readCodevelopConfig().layout.wtArgs;
|
|
304
|
+
return { args, display: `wt.exe ${args.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}` };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function startCodevelop(opts = {}) {
|
|
308
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
309
|
+
const port = Number(opts.port || DEFAULT_PORT);
|
|
310
|
+
if (RESERVED_PORTS.has(port)) throw new Error(`Refusing to use reserved clauth port ${port}`);
|
|
311
|
+
const baseUrl = opts.baseUrl || `http://127.0.0.1:${port}`;
|
|
312
|
+
const dryRun = asBool(opts.dryRun);
|
|
313
|
+
const noOpen = asBool(opts.noOpen);
|
|
314
|
+
const name = opts.name || `codev-${path.basename(repo)}-${new Date().toISOString().replace(/[-:]/g, "").slice(0, 15)}`;
|
|
315
|
+
const contextFile = opts.context || opts.contextFile || null;
|
|
316
|
+
|
|
317
|
+
let startedClauthPid = null;
|
|
318
|
+
if (!(await testPing(baseUrl))) {
|
|
319
|
+
if (!asBool(opts.startIsolatedClauth)) {
|
|
320
|
+
throw new Error(`clauth is not reachable at ${baseUrl}. Re-run with --start-isolated-clauth.`);
|
|
321
|
+
}
|
|
322
|
+
startedClauthPid = startIsolatedClauth(port);
|
|
323
|
+
if (!(await waitForPing(baseUrl))) throw new Error(`isolated clauth did not become ready at ${baseUrl}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const started = await requestJson("POST", `${baseUrl}/codevelop/start`, { name, repo });
|
|
327
|
+
const sessionId = started.session_id;
|
|
328
|
+
await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: "claude", role: "supervisor", target_peer: "codex" });
|
|
329
|
+
await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: "codex", role: "implementation_partner", target_peer: "claude" });
|
|
330
|
+
const status = await requestJson("GET", `${baseUrl}/codevelop/${sessionId}/status`);
|
|
331
|
+
await sendStartupHandshake(baseUrl, sessionId);
|
|
332
|
+
|
|
333
|
+
const { codexRepo, branch } = ensureWorktree(repo, sessionId, dryRun);
|
|
334
|
+
const manifestPath = writeManifest({ repo, sessionId, name, baseUrl, status, codexRepo, branch, startedClauthPid, dryRun });
|
|
335
|
+
if (contextFile) {
|
|
336
|
+
const manifest = readJsonFile(manifestPath);
|
|
337
|
+
manifest.context_contract.context_file = contextFile;
|
|
338
|
+
manifest.context_contract.required_reads = [contextFile];
|
|
339
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
340
|
+
if (!dryRun) writeActiveLauncherScripts(manifest);
|
|
341
|
+
}
|
|
342
|
+
const wt = buildWtArgs();
|
|
343
|
+
|
|
344
|
+
console.log(`CODEVELOP_SESSION=${sessionId}`);
|
|
345
|
+
console.log(`MANIFEST=${manifestPath}`);
|
|
346
|
+
if (!dryRun) console.log(`ACTIVE=${path.join(getStateDir(repo), "active.json")}`);
|
|
347
|
+
console.log(`CLAUDE_STREAM=${baseUrl}/codevelop/${sessionId}/claude/stream`);
|
|
348
|
+
console.log(`CODEX_STREAM=${baseUrl}/codevelop/${sessionId}/codex/stream`);
|
|
349
|
+
if (startedClauthPid) console.log(`CLAUTH_PID=${startedClauthPid}`);
|
|
350
|
+
console.log("");
|
|
351
|
+
console.log("Windows Terminal command:");
|
|
352
|
+
console.log(wt.display);
|
|
353
|
+
|
|
354
|
+
if (!dryRun && !noOpen) {
|
|
355
|
+
spawn("wt.exe", wt.args, { detached: true, stdio: "ignore", windowsHide: false }).unref();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function sendStartupHandshake(baseUrl, sessionId) {
|
|
360
|
+
const messages = [
|
|
361
|
+
{
|
|
362
|
+
session_id: sessionId,
|
|
363
|
+
from: "system",
|
|
364
|
+
to: "claude",
|
|
365
|
+
type: "ready",
|
|
366
|
+
task: "Co-develop transport is ready. Partner codex is registered. Standard operating procedure: check partner status before assigning work.",
|
|
367
|
+
context: { startup: true, partner: "codex" },
|
|
368
|
+
expect: { action: "acknowledge when you begin collaboration" },
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
session_id: sessionId,
|
|
372
|
+
from: "system",
|
|
373
|
+
to: "codex",
|
|
374
|
+
type: "ready",
|
|
375
|
+
task: "Co-develop transport is ready. Partner claude is registered. Standard operating procedure: check partner status before accepting work.",
|
|
376
|
+
context: { startup: true, partner: "claude" },
|
|
377
|
+
expect: { action: "remain idle until addressed" },
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
for (const message of messages) {
|
|
382
|
+
await requestJson("POST", `${baseUrl}/codevelop/send`, message);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function getActiveManifest(repo, session = null) {
|
|
387
|
+
if (session) {
|
|
388
|
+
const directPath = path.resolve(String(session));
|
|
389
|
+
const manifestPath = fs.existsSync(directPath)
|
|
390
|
+
? directPath
|
|
391
|
+
: path.join(getStateDir(repo), `${session}.json`);
|
|
392
|
+
if (!fs.existsSync(manifestPath)) throw new Error(`Co-develop manifest not found for --session ${session}: ${manifestPath}`);
|
|
393
|
+
return readJsonFile(manifestPath);
|
|
394
|
+
}
|
|
395
|
+
if (process.env.CODEVELOP_MANIFEST && fs.existsSync(process.env.CODEVELOP_MANIFEST)) {
|
|
396
|
+
return readJsonFile(process.env.CODEVELOP_MANIFEST);
|
|
397
|
+
}
|
|
398
|
+
const activePath = path.join(getStateDir(repo), "active.json");
|
|
399
|
+
if (!fs.existsSync(activePath)) throw new Error(`No active co-develop session found at ${activePath}. Run clauth codevelop start first.`);
|
|
400
|
+
const active = readJsonFile(activePath);
|
|
401
|
+
if (!active.manifest || !fs.existsSync(active.manifest)) throw new Error(`Active co-develop manifest is missing: ${active.manifest}`);
|
|
402
|
+
return readJsonFile(active.manifest);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function sleep(ms) {
|
|
406
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function parseList(value) {
|
|
410
|
+
if (value === undefined || value === null || value === "") return [];
|
|
411
|
+
if (Array.isArray(value)) return value.flatMap(item => parseList(item));
|
|
412
|
+
const text = String(value).trim();
|
|
413
|
+
if (!text) return [];
|
|
414
|
+
if (text.startsWith("[")) {
|
|
415
|
+
try {
|
|
416
|
+
const parsed = JSON.parse(text);
|
|
417
|
+
if (Array.isArray(parsed)) return parsed.map(item => String(item));
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
return text
|
|
421
|
+
.split(/;|\|\|/)
|
|
422
|
+
.map(item => item.trim())
|
|
423
|
+
.filter(Boolean);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeOptionalPeer(peer) {
|
|
427
|
+
const value = String(peer || "").toLowerCase().trim();
|
|
428
|
+
if (!value) return null;
|
|
429
|
+
if (!["claude", "codex"].includes(value)) throw new Error("peer must be claude or codex");
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function inferFromPeer({ from, to, peer } = {}) {
|
|
434
|
+
const explicit = normalizeOptionalPeer(from) || normalizeOptionalPeer(peer) || normalizeOptionalPeer(process.env.CODEVELOP_PEER);
|
|
435
|
+
if (explicit) return explicit;
|
|
436
|
+
const target = normalizeOptionalPeer(to);
|
|
437
|
+
if (target === "claude") return "codex";
|
|
438
|
+
if (target === "codex") return "claude";
|
|
439
|
+
throw new Error("Unable to infer sender peer. Pass --from claude|codex or set CODEVELOP_PEER.");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function inferTargetPeer({ to, from } = {}) {
|
|
443
|
+
const explicit = normalizeOptionalPeer(to);
|
|
444
|
+
if (explicit) return explicit;
|
|
445
|
+
const sender = normalizeOptionalPeer(from);
|
|
446
|
+
if (sender === "claude") return "codex";
|
|
447
|
+
if (sender === "codex") return "claude";
|
|
448
|
+
throw new Error("Unable to infer target peer. Pass --to claude|codex.");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function pollPeer(manifest, peer) {
|
|
452
|
+
return requestJson("POST", `${manifest.transport.base_url}/codevelop/poll`, {
|
|
453
|
+
session_id: manifest.session_id,
|
|
454
|
+
peer_id: peer,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function waitForReply(manifest, peer, turnId, { timeoutMs = 300_000, intervalMs = 2_000 } = {}) {
|
|
459
|
+
const started = Date.now();
|
|
460
|
+
const seen = [];
|
|
461
|
+
while (Date.now() - started < timeoutMs) {
|
|
462
|
+
const inbox = await pollPeer(manifest, peer);
|
|
463
|
+
for (const message of inbox.messages || []) {
|
|
464
|
+
if (message.turn_id === turnId && message.type === "reply") {
|
|
465
|
+
return { reply: message, inbox, seen };
|
|
466
|
+
}
|
|
467
|
+
seen.push(message);
|
|
468
|
+
}
|
|
469
|
+
await sleep(intervalMs);
|
|
470
|
+
}
|
|
471
|
+
throw new Error(`Timed out waiting ${timeoutMs}ms for reply turn ${turnId} addressed to ${peer}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function launchPeer(opts = {}) {
|
|
475
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
476
|
+
const peer = String(opts.peer || "").toLowerCase();
|
|
477
|
+
if (!["claude", "codex"].includes(peer)) throw new Error("--peer must be claude or codex");
|
|
478
|
+
|
|
479
|
+
const manifest = getActiveManifest(repo, opts.session);
|
|
480
|
+
const peerConfig = manifest.peers?.[peer];
|
|
481
|
+
if (!peerConfig) throw new Error(`Manifest does not define peer '${peer}'`);
|
|
482
|
+
|
|
483
|
+
const env = {
|
|
484
|
+
...process.env,
|
|
485
|
+
CODEVELOP_SESSION: manifest.session_id,
|
|
486
|
+
CODEVELOP_PEER: peer,
|
|
487
|
+
CODEVELOP_TARGET: peerConfig.target_peer,
|
|
488
|
+
CODEVELOP_BASE_URL: manifest.transport.base_url,
|
|
489
|
+
CODEVELOP_MANIFEST: path.join(getStateDir(repo), `${manifest.session_id}.json`),
|
|
490
|
+
CELL_ROLE: `codevelop-${peer}`,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const prompt = `You are peer=${peer} in co-development session ${manifest.session_id}.
|
|
494
|
+
Your default partner is ${peerConfig.target_peer}.
|
|
495
|
+
Use clauth codevelop tools at ${manifest.transport.base_url} to send and receive partner messages.
|
|
496
|
+
Honor per-turn role and skill requests when provided.
|
|
497
|
+
When you need partner help, run: clauth codevelop ask --to ${peerConfig.target_peer} --wait --task "<specific task>".
|
|
498
|
+
When you complete a partner request, run: clauth codevelop reply --turn <turn_id> --summary "..." --evidence "..." --files-changed "...".
|
|
499
|
+
Do not hand-write curl JSON unless the CLI command fails.
|
|
500
|
+
On startup, acknowledge co-development readiness, poll your inbox, and report any queued partner messages.
|
|
501
|
+
Reply with evidence, files changed, commits, blockers, and next action.
|
|
502
|
+
Manifest: ${env.CODEVELOP_MANIFEST}`;
|
|
503
|
+
const contextPath = getContextPath(peer);
|
|
504
|
+
fs.writeFileSync(contextPath, `${prompt}\n`, "utf8");
|
|
505
|
+
env.CODEVELOP_CONTEXT = contextPath;
|
|
506
|
+
|
|
507
|
+
console.log("");
|
|
508
|
+
console.log(`Co-develop ${peer}`);
|
|
509
|
+
console.log(` session: ${manifest.session_id}`);
|
|
510
|
+
console.log(` partner: ${peerConfig.target_peer}`);
|
|
511
|
+
console.log(` base_url: ${manifest.transport.base_url}`);
|
|
512
|
+
console.log(` cwd: ${peerConfig.cwd}`);
|
|
513
|
+
console.log(` manifest: ${env.CODEVELOP_MANIFEST}`);
|
|
514
|
+
console.log("");
|
|
515
|
+
|
|
516
|
+
if (asBool(opts.printOnly)) {
|
|
517
|
+
console.log(peer === "claude"
|
|
518
|
+
? 'command: claude -n "Co Develop Claude" --append-system-prompt [co-develop system prompt]'
|
|
519
|
+
: `command: codex -C "${peerConfig.cwd}"`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const cmd = peer === "claude" ? "claude" : "codex";
|
|
524
|
+
const args = peer === "claude"
|
|
525
|
+
? ["-n", CLAUDE_PROFILE, "--append-system-prompt", prompt]
|
|
526
|
+
: ["-C", peerConfig.cwd];
|
|
527
|
+
const child = spawn(cmd, args, { cwd: peerConfig.cwd, env, stdio: "inherit", shell: process.platform === "win32" && peer === "codex" });
|
|
528
|
+
child.on("exit", code => process.exit(code ?? 0));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function checkPartner(opts = {}) {
|
|
532
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
533
|
+
const peer = normalizePeerArg(opts.peer);
|
|
534
|
+
const manifest = getActiveManifest(repo, opts.session);
|
|
535
|
+
const peerConfig = manifest.peers?.[peer];
|
|
536
|
+
if (!peerConfig) throw new Error(`Manifest does not define peer '${peer}'`);
|
|
537
|
+
|
|
538
|
+
const partner = peerConfig.target_peer;
|
|
539
|
+
const status = await requestJson("GET", `${manifest.transport.base_url}/codevelop/${manifest.session_id}/status`);
|
|
540
|
+
const current = status.peers?.[peer];
|
|
541
|
+
const target = status.peers?.[partner];
|
|
542
|
+
const pending = await requestJson("POST", `${manifest.transport.base_url}/codevelop/poll`, {
|
|
543
|
+
session_id: manifest.session_id,
|
|
544
|
+
peer_id: peer,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
console.log(`[co-develop] peer=${peer} partner=${partner}`);
|
|
548
|
+
console.log(`[co-develop] partner_status=${target ? "registered" : "missing"} partner_queue=${target?.queued ?? "n/a"}`);
|
|
549
|
+
console.log(`[co-develop] my_queue=${current?.queued ?? 0} startup_messages=${pending.count || 0}`);
|
|
550
|
+
for (const message of pending.messages || []) {
|
|
551
|
+
console.log(`[co-develop] ${message.from}->${message.to} ${message.type}: ${message.task || message.message || ""}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function syncPartner(opts = {}) {
|
|
556
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
557
|
+
const peer = normalizePeerArg(opts.peer);
|
|
558
|
+
const manifest = getActiveManifest(repo, opts.session);
|
|
559
|
+
const peerConfig = manifest.peers?.[peer];
|
|
560
|
+
if (!peerConfig) throw new Error(`Manifest does not define peer '${peer}'`);
|
|
561
|
+
|
|
562
|
+
const result = await requestJson("POST", `${manifest.transport.base_url}/codevelop/send`, {
|
|
563
|
+
session_id: manifest.session_id,
|
|
564
|
+
from: peer,
|
|
565
|
+
to: peerConfig.target_peer,
|
|
566
|
+
type: "ready",
|
|
567
|
+
task: `${peer} is ready to collaborate and listening.`,
|
|
568
|
+
context: {
|
|
569
|
+
cwd: peerConfig.cwd,
|
|
570
|
+
manifest: path.join(getStateDir(repo), `${manifest.session_id}.json`),
|
|
571
|
+
},
|
|
572
|
+
expect: { action: "acknowledge readiness" },
|
|
573
|
+
});
|
|
574
|
+
console.log(JSON.stringify(result, null, 2));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function askPartner(opts = {}) {
|
|
578
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
579
|
+
const manifest = getActiveManifest(repo, opts.session);
|
|
580
|
+
const to = inferTargetPeer(opts);
|
|
581
|
+
const from = inferFromPeer({ ...opts, to });
|
|
582
|
+
const task = opts.task || opts.args?.join(" ") || "";
|
|
583
|
+
if (!task.trim()) throw new Error("--task is required for codevelop ask");
|
|
584
|
+
const senderConfig = manifest.peers?.[from];
|
|
585
|
+
const targetConfig = manifest.peers?.[to];
|
|
586
|
+
if (!senderConfig) throw new Error(`Manifest does not define sender peer '${from}'`);
|
|
587
|
+
if (!targetConfig) throw new Error(`Manifest does not define target peer '${to}'`);
|
|
588
|
+
|
|
589
|
+
const message = {
|
|
590
|
+
session_id: manifest.session_id,
|
|
591
|
+
from,
|
|
592
|
+
to,
|
|
593
|
+
type: "build_request",
|
|
594
|
+
role: opts.role || "partner",
|
|
595
|
+
skill: opts.skill || "rdc:co-develop",
|
|
596
|
+
task,
|
|
597
|
+
context: {
|
|
598
|
+
repo: targetConfig.cwd || manifest.repo,
|
|
599
|
+
manifest: path.join(getStateDir(repo), `${manifest.session_id}.json`),
|
|
600
|
+
context_file: opts.context || null,
|
|
601
|
+
},
|
|
602
|
+
expect: {
|
|
603
|
+
response_format: "CO_DEVELOP_REPLY",
|
|
604
|
+
evidence_required: true,
|
|
605
|
+
commit_allowed: false,
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const sent = await requestJson("POST", `${manifest.transport.base_url}/codevelop/send`, message);
|
|
610
|
+
const output = {
|
|
611
|
+
sent,
|
|
612
|
+
session_id: manifest.session_id,
|
|
613
|
+
from,
|
|
614
|
+
to,
|
|
615
|
+
turn_id: sent.turn_id,
|
|
616
|
+
waiting: !!opts.wait,
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
if (opts.wait) {
|
|
620
|
+
const timeoutMs = Number(opts.timeoutMs || 300_000);
|
|
621
|
+
const intervalMs = Number(opts.intervalMs || 2_000);
|
|
622
|
+
const waited = await waitForReply(manifest, from, sent.turn_id, { timeoutMs, intervalMs });
|
|
623
|
+
output.reply = waited.reply;
|
|
624
|
+
output.seen_while_waiting = waited.seen.length;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
console.log(JSON.stringify(output, null, 2));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function replyPartner(opts = {}) {
|
|
631
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
632
|
+
const manifest = getActiveManifest(repo, opts.session);
|
|
633
|
+
const from = inferFromPeer(opts);
|
|
634
|
+
const to = inferTargetPeer({ to: opts.to, from });
|
|
635
|
+
if (!opts.turn) throw new Error("--turn is required for codevelop reply");
|
|
636
|
+
const senderConfig = manifest.peers?.[from];
|
|
637
|
+
if (!senderConfig) throw new Error(`Manifest does not define sender peer '${from}'`);
|
|
638
|
+
|
|
639
|
+
const result = await requestJson("POST", `${manifest.transport.base_url}/codevelop/send`, {
|
|
640
|
+
session_id: manifest.session_id,
|
|
641
|
+
turn_id: opts.turn,
|
|
642
|
+
from,
|
|
643
|
+
to,
|
|
644
|
+
type: "reply",
|
|
645
|
+
verdict: opts.verdict || "pass",
|
|
646
|
+
summary: opts.summary || "Partner task completed.",
|
|
647
|
+
evidence: parseList(opts.evidence),
|
|
648
|
+
files_changed: parseList(opts.filesChanged),
|
|
649
|
+
commits: parseList(opts.commits),
|
|
650
|
+
blockers: parseList(opts.blockers),
|
|
651
|
+
next: parseList(opts.next),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
console.log(JSON.stringify({
|
|
655
|
+
queued: result.queued,
|
|
656
|
+
session_id: manifest.session_id,
|
|
657
|
+
turn_id: opts.turn,
|
|
658
|
+
from,
|
|
659
|
+
to,
|
|
660
|
+
verdict: opts.verdict || "pass",
|
|
661
|
+
}, null, 2));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function inboxPeer(opts = {}) {
|
|
665
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
666
|
+
const manifest = getActiveManifest(repo, opts.session);
|
|
667
|
+
const peer = normalizeOptionalPeer(opts.peer) || normalizeOptionalPeer(opts.from) || normalizeOptionalPeer(process.env.CODEVELOP_PEER);
|
|
668
|
+
if (!peer) throw new Error("--peer is required for codevelop inbox");
|
|
669
|
+
const inbox = await pollPeer(manifest, peer);
|
|
670
|
+
console.log(JSON.stringify(inbox, null, 2));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function emitMonitorLine(payload) {
|
|
674
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function connectSse(url, { onEvent, onError }) {
|
|
678
|
+
const parsed = new URL(url);
|
|
679
|
+
const client = parsed.protocol === "https:" ? https : http;
|
|
680
|
+
const req = client.get(parsed, {
|
|
681
|
+
headers: {
|
|
682
|
+
accept: "text/event-stream",
|
|
683
|
+
"cache-control": "no-cache",
|
|
684
|
+
},
|
|
685
|
+
}, res => {
|
|
686
|
+
if (res.statusCode !== 200) {
|
|
687
|
+
onError?.(new Error(`SSE ${url} failed: HTTP ${res.statusCode}`));
|
|
688
|
+
res.resume();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
res.setEncoding("utf8");
|
|
693
|
+
let buffer = "";
|
|
694
|
+
let current = { event: "message", data: [] };
|
|
695
|
+
|
|
696
|
+
function flushEvent() {
|
|
697
|
+
if (!current.data.length) {
|
|
698
|
+
current = { event: "message", data: [] };
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const text = current.data.join("\n");
|
|
702
|
+
try {
|
|
703
|
+
onEvent?.({ event: current.event || "message", data: JSON.parse(text) });
|
|
704
|
+
} catch {
|
|
705
|
+
onEvent?.({ event: current.event || "message", data: text });
|
|
706
|
+
}
|
|
707
|
+
current = { event: "message", data: [] };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
res.on("data", chunk => {
|
|
711
|
+
buffer += chunk;
|
|
712
|
+
const lines = buffer.split(/\r?\n/);
|
|
713
|
+
buffer = lines.pop() || "";
|
|
714
|
+
for (const line of lines) {
|
|
715
|
+
if (line === "") {
|
|
716
|
+
flushEvent();
|
|
717
|
+
} else if (line.startsWith("event:")) {
|
|
718
|
+
current.event = line.slice(6).trim();
|
|
719
|
+
} else if (line.startsWith("data:")) {
|
|
720
|
+
current.data.push(line.slice(5).trimStart());
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
res.on("end", () => onError?.(new Error("SSE stream ended")));
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
req.on("error", err => onError?.(err));
|
|
728
|
+
return req;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function listenPeer(opts = {}) {
|
|
732
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
733
|
+
const manifest = getActiveManifest(repo, opts.session);
|
|
734
|
+
const peer = normalizeOptionalPeer(opts.peer) || normalizeOptionalPeer(opts.from) || normalizeOptionalPeer(process.env.CODEVELOP_PEER);
|
|
735
|
+
if (!peer) throw new Error("--peer is required for codevelop listen");
|
|
736
|
+
|
|
737
|
+
const streamUrl = manifest.peers?.[peer]?.stream_url
|
|
738
|
+
|| `${manifest.transport.base_url}/codevelop/${encodeURIComponent(manifest.session_id)}/${encodeURIComponent(peer)}/stream`;
|
|
739
|
+
const once = asBool(opts.once);
|
|
740
|
+
const reconnectMs = Number(opts.intervalMs || 2_000);
|
|
741
|
+
let stopped = false;
|
|
742
|
+
|
|
743
|
+
emitMonitorLine({
|
|
744
|
+
event: "codevelop_listen_ready",
|
|
745
|
+
session_id: manifest.session_id,
|
|
746
|
+
peer,
|
|
747
|
+
partner: manifest.peers?.[peer]?.target_peer || null,
|
|
748
|
+
stream_url: streamUrl,
|
|
749
|
+
action: "Claude/Codex should run clauth codevelop inbox --peer " + peer + " when codevelop_message arrives, then complete and reply.",
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
function connect() {
|
|
753
|
+
if (stopped) return;
|
|
754
|
+
connectSse(streamUrl, {
|
|
755
|
+
onEvent: ({ event, data }) => {
|
|
756
|
+
if (event !== "message" && event !== "stop") return;
|
|
757
|
+
emitMonitorLine({
|
|
758
|
+
event: event === "stop" ? "codevelop_stop" : "codevelop_message",
|
|
759
|
+
session_id: manifest.session_id,
|
|
760
|
+
peer,
|
|
761
|
+
turn_id: data?.turn_id || null,
|
|
762
|
+
from: data?.from || null,
|
|
763
|
+
to: data?.to || peer,
|
|
764
|
+
type: data?.type || event,
|
|
765
|
+
task: data?.task || null,
|
|
766
|
+
message: data?.message || null,
|
|
767
|
+
payload: data,
|
|
768
|
+
next: data?.type === "reply"
|
|
769
|
+
? "Read the reply and continue the waiting task."
|
|
770
|
+
: `Run clauth codevelop inbox --peer ${peer}, do the requested work, then run clauth codevelop reply --turn ${data?.turn_id || "<turn_id>"} ...`,
|
|
771
|
+
});
|
|
772
|
+
if (once) {
|
|
773
|
+
stopped = true;
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
onError: err => {
|
|
778
|
+
if (stopped) return;
|
|
779
|
+
emitMonitorLine({
|
|
780
|
+
event: "codevelop_listen_reconnect",
|
|
781
|
+
session_id: manifest.session_id,
|
|
782
|
+
peer,
|
|
783
|
+
error: err.message,
|
|
784
|
+
retry_ms: reconnectMs,
|
|
785
|
+
});
|
|
786
|
+
setTimeout(connect, reconnectMs);
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
connect();
|
|
792
|
+
await new Promise(() => {});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function requestPartner(opts = {}) {
|
|
796
|
+
const repo = opts.repo || DEFAULT_REPO;
|
|
797
|
+
const port = Number(opts.port || DEFAULT_PORT);
|
|
798
|
+
if (RESERVED_PORTS.has(port)) throw new Error(`Refusing to use reserved clauth port ${port}`);
|
|
799
|
+
const baseUrl = opts.baseUrl || `http://127.0.0.1:${port}`;
|
|
800
|
+
const peer = normalizePeerArg(opts.peer);
|
|
801
|
+
const partner = peer === "codex" ? "claude" : "codex";
|
|
802
|
+
const task = opts.task || "I need a co-development partner. Confirm you are ready, read the context file if provided, and wait for the first concrete assignment.";
|
|
803
|
+
const contextFile = opts.context || opts.contextFile || null;
|
|
804
|
+
const name = opts.name || `assist-${peer}-${new Date().toISOString().replace(/[-:]/g, "").slice(0, 15)}`;
|
|
805
|
+
|
|
806
|
+
if (!(await testPing(baseUrl))) {
|
|
807
|
+
if (!asBool(opts.startIsolatedClauth)) throw new Error(`clauth is not reachable at ${baseUrl}. Re-run with --start-isolated-clauth.`);
|
|
808
|
+
startIsolatedClauth(port);
|
|
809
|
+
if (!(await waitForPing(baseUrl))) throw new Error(`isolated clauth did not become ready at ${baseUrl}`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const started = await requestJson("POST", `${baseUrl}/codevelop/start`, {
|
|
813
|
+
name,
|
|
814
|
+
repo,
|
|
815
|
+
metadata: {
|
|
816
|
+
mode: "request-partner",
|
|
817
|
+
initiator: peer,
|
|
818
|
+
partner,
|
|
819
|
+
context_file: contextFile,
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
const sessionId = started.session_id;
|
|
823
|
+
await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: peer, role: "initiator", target_peer: partner });
|
|
824
|
+
await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: partner, role: "co_developer", target_peer: peer });
|
|
825
|
+
const status = await requestJson("GET", `${baseUrl}/codevelop/${sessionId}/status`);
|
|
826
|
+
|
|
827
|
+
const { codexRepo, branch } = ensureWorktree(repo, sessionId, false);
|
|
828
|
+
const manifestPath = writeManifest({ repo, sessionId, name, baseUrl, status, codexRepo, branch, startedClauthPid: null, dryRun: false });
|
|
829
|
+
const manifest = readJsonFile(manifestPath);
|
|
830
|
+
manifest.mode = "request-partner";
|
|
831
|
+
manifest.initiator = peer;
|
|
832
|
+
manifest.context_contract.context_file = contextFile;
|
|
833
|
+
manifest.context_contract.required_reads = contextFile ? [contextFile] : [];
|
|
834
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
835
|
+
writeActiveLauncherScripts(manifest);
|
|
836
|
+
|
|
837
|
+
const ask = [
|
|
838
|
+
"Co-development partner requested.",
|
|
839
|
+
`Initiator: ${peer}.`,
|
|
840
|
+
contextFile ? `Context file to read first: ${contextFile}.` : "No context file supplied.",
|
|
841
|
+
`Task: ${task}`,
|
|
842
|
+
].join(" ");
|
|
843
|
+
await requestJson("POST", `${baseUrl}/codevelop/send`, {
|
|
844
|
+
session_id: sessionId,
|
|
845
|
+
from: peer,
|
|
846
|
+
to: partner,
|
|
847
|
+
type: "partner_request",
|
|
848
|
+
task: ask,
|
|
849
|
+
context: {
|
|
850
|
+
mode: "request-partner",
|
|
851
|
+
context_file: contextFile,
|
|
852
|
+
manifest: manifestPath,
|
|
853
|
+
repo,
|
|
854
|
+
},
|
|
855
|
+
expect: {
|
|
856
|
+
response: "ready_ack",
|
|
857
|
+
instruction: "Acknowledge readiness before doing implementation work.",
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const wt = buildWtArgs();
|
|
862
|
+
console.log(`CODEVELOP_SESSION=${sessionId}`);
|
|
863
|
+
console.log(`MANIFEST=${manifestPath}`);
|
|
864
|
+
console.log(`PARTNER=${partner}`);
|
|
865
|
+
console.log(`CONTEXT=${contextFile || ""}`);
|
|
866
|
+
console.log("Windows Terminal command:");
|
|
867
|
+
console.log(wt.display);
|
|
868
|
+
if (!asBool(opts.noOpen)) spawn("wt.exe", wt.args, { detached: true, stdio: "ignore", windowsHide: false }).unref();
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function normalizePeerArg(peer) {
|
|
872
|
+
const value = String(peer || process.env.CODEVELOP_PEER || "").toLowerCase();
|
|
873
|
+
if (!["claude", "codex"].includes(value)) throw new Error("--peer must be claude or codex");
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export async function runCodevelop(opts = {}) {
|
|
878
|
+
const action = opts.action || "help";
|
|
879
|
+
if (action === "install-terminal") {
|
|
880
|
+
const result = installWindowsTerminalProfiles(opts);
|
|
881
|
+
console.log(chalk.green(`Installed Windows Terminal profiles: ${CLAUDE_PROFILE}, ${CODEX_PROFILE}`));
|
|
882
|
+
console.log(chalk.gray(`Settings: ${result.settingsPath}`));
|
|
883
|
+
if (result.backupPath) console.log(chalk.gray(`Backup: ${result.backupPath}`));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (action === "start") return startCodevelop(opts);
|
|
887
|
+
if (action === "launch-peer") return launchPeer(opts);
|
|
888
|
+
if (action === "check-partner") return checkPartner(opts);
|
|
889
|
+
if (action === "sync") return syncPartner(opts);
|
|
890
|
+
if (action === "request-partner") return requestPartner(opts);
|
|
891
|
+
if (action === "ask") return askPartner(opts);
|
|
892
|
+
if (action === "reply") return replyPartner(opts);
|
|
893
|
+
if (action === "inbox") return inboxPeer(opts);
|
|
894
|
+
if (action === "listen") return listenPeer(opts);
|
|
895
|
+
|
|
896
|
+
console.log(chalk.cyan("\nclauth codevelop\n"));
|
|
897
|
+
console.log(" clauth codevelop install-terminal [--repo C:\\Dev\\regen-root]");
|
|
898
|
+
console.log(" clauth codevelop start --port 53137 --start-isolated-clauth [--repo C:\\Dev\\regen-root]");
|
|
899
|
+
console.log(" clauth codevelop ask --to claude|codex --task \"...\" --wait");
|
|
900
|
+
console.log(" clauth codevelop reply --from claude|codex --turn turn-0001 --summary \"...\" --evidence \"...\"");
|
|
901
|
+
console.log(" clauth codevelop inbox --peer claude|codex");
|
|
902
|
+
console.log(" clauth codevelop listen --peer claude|codex [--once]");
|
|
903
|
+
console.log(" clauth codevelop request-partner --peer codex|claude --task \"...\" [--context path] [--repo C:\\Dev\\regen-root]");
|
|
904
|
+
console.log(" clauth codevelop launch-peer --peer claude|codex [--repo C:\\Dev\\regen-root]\n");
|
|
905
|
+
console.log(" clauth codevelop check-partner --peer claude|codex [--repo C:\\Dev\\regen-root]");
|
|
906
|
+
console.log(" clauth codevelop sync --peer claude|codex [--repo C:\\Dev\\regen-root]\n");
|
|
907
|
+
}
|