@coopcli/specsketch 5.5.4 → 5.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +863 -252
- package/package.json +77 -74
package/dist/cli/index.js
CHANGED
|
@@ -1,18 +1,343 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// server/cli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { homedir as
|
|
6
|
-
import { dirname as
|
|
4
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "node:fs";
|
|
5
|
+
import { homedir as homedir4 } from "node:os";
|
|
6
|
+
import { dirname as dirname4, extname, join as join7, normalize as normalize2, resolve as resolve3 } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
9
9
|
import { serve } from "@hono/node-server";
|
|
10
10
|
import Anthropic from "@anthropic-ai/sdk";
|
|
11
|
+
|
|
12
|
+
// ../agent-harness/src/model-auth.ts
|
|
13
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
var ModelAuthError = class extends Error {
|
|
17
|
+
};
|
|
18
|
+
var API_ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
19
|
+
function hasAntAuthProfile(home) {
|
|
20
|
+
try {
|
|
21
|
+
return readdirSync(join(home, ".config", "anthropic", "credentials")).some(
|
|
22
|
+
(f) => f.endsWith(".json")
|
|
23
|
+
);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveModelCredential(opts = {}) {
|
|
29
|
+
const env = opts.env ?? process.env;
|
|
30
|
+
const home = opts.home ?? homedir();
|
|
31
|
+
const useApi = Boolean(opts.useApi) || Boolean(env.COOP_USE_API);
|
|
32
|
+
const hasApiKey = Boolean(env.ANTHROPIC_API_KEY);
|
|
33
|
+
const hasAntAuth = Boolean(env.ANTHROPIC_AUTH_TOKEN) || hasAntAuthProfile(home);
|
|
34
|
+
const hasSubscription = existsSync(join(home, ".claude", ".credentials.json"));
|
|
35
|
+
if (useApi) {
|
|
36
|
+
if (hasApiKey || hasAntAuth) return apiResult(env, hasApiKey ? "api-key" : "ant-auth");
|
|
37
|
+
throw new ModelAuthError(
|
|
38
|
+
"--api / COOP_USE_API is set but no API credential was found. Set ANTHROPIC_API_KEY=sk-ant-\u2026 or run `ant auth login`."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (hasSubscription) {
|
|
42
|
+
const subprocessEnv = { ...env };
|
|
43
|
+
for (const k of API_ENV_KEYS) delete subprocessEnv[k];
|
|
44
|
+
return {
|
|
45
|
+
path: "subscription",
|
|
46
|
+
billsApiCredits: false,
|
|
47
|
+
notice: "Model auth: Max/Pro subscription login \u2014 billing your subscription.",
|
|
48
|
+
subprocessEnv
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (hasApiKey) return apiResult(env, "api-key");
|
|
52
|
+
if (hasAntAuth) return apiResult(env, "ant-auth");
|
|
53
|
+
throw new ModelAuthError(
|
|
54
|
+
"No Anthropic model credential found. Either:\n \u2022 log in your own Claude Code for subscription billing: claude login\n \u2022 or use API credits: export ANTHROPIC_API_KEY=sk-ant-\u2026 (or: ant auth login)"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
function apiResult(env, path) {
|
|
58
|
+
return {
|
|
59
|
+
path,
|
|
60
|
+
billsApiCredits: true,
|
|
61
|
+
notice: `Model auth: ${path === "api-key" ? "ANTHROPIC_API_KEY" : "ant auth OAuth token"} \u2014 billing API credits, NOT your Max subscription.`,
|
|
62
|
+
subprocessEnv: { ...env }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ../agent-harness/src/claude.ts
|
|
67
|
+
function makeGate(profile) {
|
|
68
|
+
return async (toolName, input) => {
|
|
69
|
+
if (!profile.allowedTools.includes(toolName)) {
|
|
70
|
+
return {
|
|
71
|
+
behavior: "deny",
|
|
72
|
+
message: `Tool "${toolName}" is not in the headless allowlist for agent "${profile.identity}".`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (toolName === "Bash") {
|
|
76
|
+
const command = typeof input["command"] === "string" ? input["command"] : "";
|
|
77
|
+
for (const pattern of profile.blockedBashPatterns) {
|
|
78
|
+
if (pattern.test(command)) {
|
|
79
|
+
return {
|
|
80
|
+
behavior: "deny",
|
|
81
|
+
message: `Bash command blocked by headless profile pattern ${String(pattern)} for agent "${profile.identity}".`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (profile.gatedTools.includes(toolName) && !profile.allowGated) {
|
|
87
|
+
return {
|
|
88
|
+
behavior: "deny",
|
|
89
|
+
message: `Tool "${toolName}" is a gated side-effect tool and this profile does not allow gated tools (allowGated: false).`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { behavior: "allow", updatedInput: input };
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function destructiveBashHook(profile) {
|
|
96
|
+
return {
|
|
97
|
+
matcher: "Bash",
|
|
98
|
+
hooks: [
|
|
99
|
+
async (input) => {
|
|
100
|
+
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
101
|
+
const ti = input.tool_input;
|
|
102
|
+
const command = typeof ti === "object" && ti !== null ? String(ti.command ?? "") : "";
|
|
103
|
+
for (const pattern of profile.blockedBashPatterns) {
|
|
104
|
+
if (pattern.test(command)) {
|
|
105
|
+
return {
|
|
106
|
+
continue: true,
|
|
107
|
+
hookSpecificOutput: {
|
|
108
|
+
hookEventName: "PreToolUse",
|
|
109
|
+
permissionDecision: "deny",
|
|
110
|
+
permissionDecisionReason: `Destructive command blocked by headless profile (${String(pattern)}) for agent "${profile.identity}".`
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { continue: true };
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function* mapMessage(message, expectStructuredOutput) {
|
|
121
|
+
switch (message.type) {
|
|
122
|
+
case "system": {
|
|
123
|
+
if (message.subtype === "init") {
|
|
124
|
+
yield { type: "start", sessionId: message.session_id };
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
case "assistant": {
|
|
129
|
+
const assistant = message;
|
|
130
|
+
for (const block of assistant.message.content) {
|
|
131
|
+
if (block.type === "text") {
|
|
132
|
+
yield { type: "text", text: block.text };
|
|
133
|
+
} else if (block.type === "thinking") {
|
|
134
|
+
yield { type: "thinking", text: block.thinking };
|
|
135
|
+
} else if (block.type === "tool_use") {
|
|
136
|
+
yield { type: "tool_use", id: block.id, name: block.name, input: block.input };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
case "user": {
|
|
142
|
+
const user = message;
|
|
143
|
+
const content = user.message.content;
|
|
144
|
+
if (typeof content === "string") return;
|
|
145
|
+
for (const block of content) {
|
|
146
|
+
if (block.type === "tool_result") {
|
|
147
|
+
yield {
|
|
148
|
+
type: "tool_result",
|
|
149
|
+
id: block.tool_use_id,
|
|
150
|
+
isError: block.is_error ?? false,
|
|
151
|
+
content: block.content
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case "result": {
|
|
158
|
+
if (message.subtype === "success") {
|
|
159
|
+
const missingStructured = expectStructuredOutput && message.structured_output === void 0;
|
|
160
|
+
yield {
|
|
161
|
+
type: "result",
|
|
162
|
+
text: message.result,
|
|
163
|
+
costUsd: message.total_cost_usd,
|
|
164
|
+
isError: message.is_error || missingStructured,
|
|
165
|
+
...message.structured_output !== void 0 ? { structuredOutput: message.structured_output } : {}
|
|
166
|
+
};
|
|
167
|
+
} else {
|
|
168
|
+
yield {
|
|
169
|
+
type: "result",
|
|
170
|
+
text: message.errors.join("\n"),
|
|
171
|
+
costUsd: message.total_cost_usd,
|
|
172
|
+
isError: true
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function buildOptions(input, resolved) {
|
|
182
|
+
const profile = input.profile;
|
|
183
|
+
const options = {
|
|
184
|
+
cwd: input.cwd,
|
|
185
|
+
// CRITICAL: an EXPLICIT settingSources: [] loads NOTHING. (The SDK's own
|
|
186
|
+
// default — when the option is omitted — loads ALL sources; we always
|
|
187
|
+
// pass the profile's list so inheritance is an explicit choice, and the
|
|
188
|
+
// profile's sources are what yield LifeOS inheritance.)
|
|
189
|
+
settingSources: profile.settingSources,
|
|
190
|
+
// Never 'bypassPermissions' / 'acceptEdits' — PermissionProfile's type
|
|
191
|
+
// only admits 'default' | 'plan' | 'dontAsk'.
|
|
192
|
+
permissionMode: profile.permissionMode,
|
|
193
|
+
allowedTools: profile.allowedTools,
|
|
194
|
+
canUseTool: makeGate(profile),
|
|
195
|
+
// Containment: destructive-Bash denial must survive a bare allowlisted
|
|
196
|
+
// `Bash` (allow rules short-circuit canUseTool), so it is enforced by a
|
|
197
|
+
// PreToolUse hook, which the SDK evaluates before allow rules.
|
|
198
|
+
hooks: { PreToolUse: [destructiveBashHook(profile)] },
|
|
199
|
+
// Options.env REPLACES the subprocess environment (no merge). The
|
|
200
|
+
// resolver hands back the FULL inherited env (HOME/PATH preserved) with
|
|
201
|
+
// ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN stripped on the subscription
|
|
202
|
+
// path, so the subprocess cannot prefer an API key over the login.
|
|
203
|
+
env: resolved.subprocessEnv
|
|
204
|
+
};
|
|
205
|
+
if (profile.disallowedTools) {
|
|
206
|
+
options.disallowedTools = profile.disallowedTools;
|
|
207
|
+
}
|
|
208
|
+
if (profile.mcpServers) {
|
|
209
|
+
options.mcpServers = profile.mcpServers;
|
|
210
|
+
}
|
|
211
|
+
if (profile.strictMcp !== void 0) {
|
|
212
|
+
options.strictMcpConfig = profile.strictMcp;
|
|
213
|
+
}
|
|
214
|
+
if (input.outputSchema) {
|
|
215
|
+
options.outputFormat = { type: "json_schema", schema: input.outputSchema };
|
|
216
|
+
}
|
|
217
|
+
return options;
|
|
218
|
+
}
|
|
219
|
+
function verifyBilling(resolved, apiKeySource, notify) {
|
|
220
|
+
if (apiKeySource === void 0) return;
|
|
221
|
+
const billedViaApiKey = apiKeySource !== "none";
|
|
222
|
+
if (resolved.path === "subscription" && billedViaApiKey) {
|
|
223
|
+
notify(
|
|
224
|
+
`Warning: the subscription path was selected but the run authenticated from an API-key source ('${apiKeySource}') \u2014 this run bills API credits, not your subscription.`
|
|
225
|
+
);
|
|
226
|
+
} else if (resolved.path !== "subscription" && !billedViaApiKey) {
|
|
227
|
+
notify(
|
|
228
|
+
`Warning: the ${resolved.path} (API credits) path was selected but the run authenticated as first-party (no API key) \u2014 this run bills the subscription.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var SDK_UNAVAILABLE_MESSAGE = "Claude Agent SDK not available \u2014 reinstall the CLI (do not use --omit=optional). Global install is required for the agent path; pnpm dlx/npx re-download the ~268 MB native binary.";
|
|
233
|
+
function isSdkAbsence(error) {
|
|
234
|
+
const code = error?.code;
|
|
235
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
return /Native CLI binary.*not found/i.test(message);
|
|
238
|
+
}
|
|
239
|
+
async function loadSdkQuery(loader) {
|
|
240
|
+
try {
|
|
241
|
+
const mod = await (loader ?? (() => import("@anthropic-ai/claude-agent-sdk")))();
|
|
242
|
+
return mod.query;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (isSdkAbsence(error)) throw new Error(SDK_UNAVAILABLE_MESSAGE, { cause: error });
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function createClaudeHarness(deps) {
|
|
249
|
+
return {
|
|
250
|
+
id: "claude",
|
|
251
|
+
async *run(input) {
|
|
252
|
+
const resolved = resolveModelCredential({
|
|
253
|
+
env: deps?.env,
|
|
254
|
+
home: deps?.home,
|
|
255
|
+
useApi: input.useApi
|
|
256
|
+
});
|
|
257
|
+
const notify = input.onNotice ?? ((message) => console.error(message));
|
|
258
|
+
notify(resolved.notice);
|
|
259
|
+
const queryFn = deps?.queryFn ?? await loadSdkQuery(deps?.sdkLoader);
|
|
260
|
+
const expectStructuredOutput = input.outputSchema !== void 0;
|
|
261
|
+
try {
|
|
262
|
+
const stream = queryFn({ prompt: input.prompt, options: buildOptions(input, resolved) });
|
|
263
|
+
for await (const message of stream) {
|
|
264
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
265
|
+
verifyBilling(resolved, message.apiKeySource, notify);
|
|
266
|
+
}
|
|
267
|
+
yield* mapMessage(message, expectStructuredOutput);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (isSdkAbsence(error)) throw new Error(SDK_UNAVAILABLE_MESSAGE, { cause: error });
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ../agent-harness/src/profiles.ts
|
|
278
|
+
var SIDE_EFFECT_GATED_TOOLS = ["SendMessage", "Send", "Deploy", "Publish"];
|
|
279
|
+
var BLOCKED_BASH_PATTERNS = [
|
|
280
|
+
/\bgit\s+push\b/,
|
|
281
|
+
/\bwrangler\s+deploy\b/,
|
|
282
|
+
/\brm\s+-rf\b/,
|
|
283
|
+
/\bnpm\s+publish\b/
|
|
284
|
+
];
|
|
285
|
+
function blockedBashPatterns() {
|
|
286
|
+
return [...BLOCKED_BASH_PATTERNS];
|
|
287
|
+
}
|
|
288
|
+
function defaultHeadlessProfile(identity) {
|
|
289
|
+
return {
|
|
290
|
+
allowedTools: ["Read", "Grep", "Glob", "WebFetch"],
|
|
291
|
+
gatedTools: [...SIDE_EFFECT_GATED_TOOLS],
|
|
292
|
+
allowGated: false,
|
|
293
|
+
blockedBashPatterns: blockedBashPatterns(),
|
|
294
|
+
permissionMode: "default",
|
|
295
|
+
settingSources: [],
|
|
296
|
+
identity
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function lifeOSProfile(identity, mcpServers) {
|
|
300
|
+
const profile = {
|
|
301
|
+
allowedTools: [
|
|
302
|
+
"Read",
|
|
303
|
+
"Grep",
|
|
304
|
+
"Glob",
|
|
305
|
+
"WebFetch",
|
|
306
|
+
"WebSearch",
|
|
307
|
+
"Write",
|
|
308
|
+
"Edit",
|
|
309
|
+
"NotebookEdit",
|
|
310
|
+
// NOTE: a bare 'Bash' allow rule auto-approves the tool and
|
|
311
|
+
// short-circuits canUseTool — blockedBashPatterns stay enforced only
|
|
312
|
+
// because the harness installs them as a PreToolUse hook (claude.ts
|
|
313
|
+
// destructiveBashHook), which the SDK evaluates before allow rules.
|
|
314
|
+
"Bash",
|
|
315
|
+
"Agent",
|
|
316
|
+
"Task",
|
|
317
|
+
"Skill",
|
|
318
|
+
"TodoWrite"
|
|
319
|
+
],
|
|
320
|
+
gatedTools: [...SIDE_EFFECT_GATED_TOOLS],
|
|
321
|
+
allowGated: false,
|
|
322
|
+
blockedBashPatterns: blockedBashPatterns(),
|
|
323
|
+
permissionMode: "default",
|
|
324
|
+
settingSources: ["user", "project", "local"],
|
|
325
|
+
identity
|
|
326
|
+
};
|
|
327
|
+
if (mcpServers) {
|
|
328
|
+
profile.mcpServers = mcpServers;
|
|
329
|
+
profile.strictMcp = true;
|
|
330
|
+
}
|
|
331
|
+
return profile;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// server/cli.ts
|
|
11
335
|
import { WebSocketServer } from "ws";
|
|
12
336
|
|
|
13
337
|
// server/app.ts
|
|
14
338
|
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
|
|
15
339
|
import { Hono } from "hono";
|
|
340
|
+
import { z as z2 } from "zod";
|
|
16
341
|
|
|
17
342
|
// src/lib/schema.ts
|
|
18
343
|
import { z } from "zod";
|
|
@@ -137,8 +462,8 @@ var SessionSnapshot = z.object({
|
|
|
137
462
|
});
|
|
138
463
|
|
|
139
464
|
// server/references.ts
|
|
140
|
-
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
141
|
-
import { basename, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
465
|
+
import { readFileSync, readdirSync as readdirSync2, realpathSync, statSync } from "node:fs";
|
|
466
|
+
import { basename, isAbsolute, join as join2, normalize, relative, resolve, sep } from "node:path";
|
|
142
467
|
var REF_FILE_BYTE_LIMIT = 64 * 1024;
|
|
143
468
|
var REF_TOTAL_BYTE_LIMIT = 256 * 1024;
|
|
144
469
|
var REF_DIR_FILE_LIMIT = 50;
|
|
@@ -164,14 +489,14 @@ function collectEntries(root) {
|
|
|
164
489
|
if (files.length + dirs.length >= WALK_ENTRY_LIMIT) return;
|
|
165
490
|
let entries;
|
|
166
491
|
try {
|
|
167
|
-
entries =
|
|
492
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
168
493
|
} catch {
|
|
169
494
|
return;
|
|
170
495
|
}
|
|
171
496
|
for (const entry of entries) {
|
|
172
497
|
if (files.length + dirs.length >= WALK_ENTRY_LIMIT) return;
|
|
173
498
|
if (SEARCH_IGNORE.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
174
|
-
const full =
|
|
499
|
+
const full = join2(dir, entry.name);
|
|
175
500
|
if (entry.isDirectory()) {
|
|
176
501
|
dirs.push(relative(root, full));
|
|
177
502
|
walk(full);
|
|
@@ -258,7 +583,7 @@ function expandReferences(text, root) {
|
|
|
258
583
|
if (included >= REF_DIR_FILE_LIMIT || totalBytes >= REF_TOTAL_BYTE_LIMIT) break;
|
|
259
584
|
let body;
|
|
260
585
|
try {
|
|
261
|
-
body = readClamped(
|
|
586
|
+
body = readClamped(join2(rootReal, relPath));
|
|
262
587
|
} catch {
|
|
263
588
|
continue;
|
|
264
589
|
}
|
|
@@ -305,7 +630,7 @@ function resolveOne(rootReal, ref) {
|
|
|
305
630
|
if (real !== rootReal && !insideRoot(rootReal, real)) {
|
|
306
631
|
return { reason: "resolves outside the working directory" };
|
|
307
632
|
}
|
|
308
|
-
const dirFiles = collectFiles(direct).map((p) => relative(rootReal,
|
|
633
|
+
const dirFiles = collectFiles(direct).map((p) => relative(rootReal, join2(direct, p))).sort((a, b) => {
|
|
309
634
|
const depth = a.split(sep).length - b.split(sep).length;
|
|
310
635
|
return depth !== 0 ? depth : a.localeCompare(b);
|
|
311
636
|
});
|
|
@@ -331,7 +656,7 @@ function resolveOne(rootReal, ref) {
|
|
|
331
656
|
if (matches.length === 0) return { reason: "not found" };
|
|
332
657
|
const best = matches[0];
|
|
333
658
|
try {
|
|
334
|
-
const body = readClamped(
|
|
659
|
+
const body = readClamped(join2(rootReal, best));
|
|
335
660
|
return {
|
|
336
661
|
path: best,
|
|
337
662
|
...body,
|
|
@@ -513,6 +838,7 @@ function bundleFilePaths(b) {
|
|
|
513
838
|
...b.specs.map((s) => `specs/${s.capability}/spec.md`)
|
|
514
839
|
];
|
|
515
840
|
}
|
|
841
|
+
var SPEC_BUNDLE_JSON_SCHEMA = z2.toJSONSchema(SpecBundle, { target: "draft-7" });
|
|
516
842
|
var FABLE_BETAS = ["server-side-fallback-2026-06-01"];
|
|
517
843
|
var FABLE_FALLBACKS = [{ model: "claude-opus-4-8" }];
|
|
518
844
|
function parseWithModelGate(anthropic, request) {
|
|
@@ -546,6 +872,9 @@ function anthropicErrorResponse(c, err, kind) {
|
|
|
546
872
|
}
|
|
547
873
|
function createApp(anthropic, workspace, getShare, options) {
|
|
548
874
|
const refRoot = options === void 0 ? process.cwd() : options.refRoot;
|
|
875
|
+
const harness = options?.harness;
|
|
876
|
+
const reviewProfile = lifeOSProfile("specsketch-review");
|
|
877
|
+
const generateProfile = defaultHeadlessProfile("specsketch-generate");
|
|
549
878
|
const app = new Hono();
|
|
550
879
|
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
551
880
|
app.get(
|
|
@@ -624,25 +953,65 @@ Produce the OpenSpec change proposal for this architecture as structured output.
|
|
|
624
953
|
Referenced files (resolved from @-references in the input):
|
|
625
954
|
|
|
626
955
|
${refContext}` : "");
|
|
627
|
-
let
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
956
|
+
let output;
|
|
957
|
+
if (harness) {
|
|
958
|
+
let structured;
|
|
959
|
+
let resultText = "";
|
|
960
|
+
let resultError = false;
|
|
961
|
+
try {
|
|
962
|
+
for await (const ev of harness.run({
|
|
963
|
+
prompt: `${system}
|
|
964
|
+
|
|
965
|
+
${prompt}`,
|
|
966
|
+
cwd: refRoot ?? process.cwd(),
|
|
967
|
+
session: workspace?.changeName ?? "specsketch-generate",
|
|
968
|
+
profile: generateProfile,
|
|
969
|
+
outputSchema: SPEC_BUNDLE_JSON_SCHEMA
|
|
970
|
+
})) {
|
|
971
|
+
if (ev.type === "result") {
|
|
972
|
+
structured = ev.structuredOutput;
|
|
973
|
+
resultText = ev.text;
|
|
974
|
+
resultError = ev.isError;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
} catch (err) {
|
|
978
|
+
return anthropicErrorResponse(c, err, "generation-failed");
|
|
979
|
+
}
|
|
980
|
+
if (structured === void 0) {
|
|
981
|
+
return resultError ? c.json(
|
|
982
|
+
{ error: "generation-failed", detail: resultText || "the agent run failed" },
|
|
983
|
+
502
|
|
984
|
+
) : c.json({ error: "model returned no structured output" }, 502);
|
|
985
|
+
}
|
|
986
|
+
const checked = SpecBundle.safeParse(structured);
|
|
987
|
+
if (!checked.success) {
|
|
988
|
+
return c.json(
|
|
989
|
+
{ error: "model returned invalid structured output", detail: checked.error.issues },
|
|
990
|
+
502
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
output = checked.data;
|
|
994
|
+
} else {
|
|
995
|
+
let msg;
|
|
996
|
+
try {
|
|
997
|
+
msg = await parseWithModelGate(anthropic, {
|
|
998
|
+
model,
|
|
999
|
+
max_tokens: 16e3,
|
|
1000
|
+
// non-streaming stays under the SDK HTTP timeout
|
|
1001
|
+
system,
|
|
1002
|
+
messages: [{ role: "user", content: prompt }],
|
|
1003
|
+
output_config: { format: zodOutputFormat(SpecBundle) }
|
|
1004
|
+
// No temperature/top_p or thinking config — both 400 on modern models.
|
|
1005
|
+
});
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
return anthropicErrorResponse(c, err, "generation-failed");
|
|
1008
|
+
}
|
|
1009
|
+
if (msg.stop_reason === "refusal") {
|
|
1010
|
+
return c.json({ error: "refusal", detail: msg.stop_details }, 422);
|
|
1011
|
+
}
|
|
1012
|
+
if (!msg.parsed_output) return c.json({ error: "model returned no structured output" }, 502);
|
|
1013
|
+
output = SpecBundle.parse(msg.parsed_output);
|
|
643
1014
|
}
|
|
644
|
-
if (!msg.parsed_output) return c.json({ error: "model returned no structured output" }, 502);
|
|
645
|
-
const output = SpecBundle.parse(msg.parsed_output);
|
|
646
1015
|
const bundle = workspace ? { ...output, changeName: workspace.changeName } : output;
|
|
647
1016
|
const changedFiles = workspace ? workspace.writeBundle(bundle) : bundleFilePaths(bundle);
|
|
648
1017
|
const seed = await makeSeed(anthropic, model, conversation, changedFiles, bundle);
|
|
@@ -654,43 +1023,56 @@ ${refContext}` : "");
|
|
|
654
1023
|
if (!parsed.success) {
|
|
655
1024
|
return c.json({ error: "invalid request", detail: parsed.error.issues }, 400);
|
|
656
1025
|
}
|
|
657
|
-
const { message, conversation
|
|
1026
|
+
const { message, conversation } = parsed.data;
|
|
658
1027
|
const bundle = parsed.data.bundle ?? workspace?.toBundle() ?? null;
|
|
659
1028
|
if (!bundle) return c.json({ error: "no spec to chat about \u2014 generate one first" }, 400);
|
|
1029
|
+
if (!harness) {
|
|
1030
|
+
return c.json(
|
|
1031
|
+
{ error: "chat-unavailable", detail: "no agent harness configured for this server" },
|
|
1032
|
+
500
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
660
1035
|
const refs = expandReferences(message, refRoot);
|
|
661
1036
|
const refContext = renderReferenceContext(refs);
|
|
662
|
-
const history = conversation.map((m) =>
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
${
|
|
674
|
-
messages: [
|
|
675
|
-
...history,
|
|
676
|
-
{
|
|
677
|
-
role: "user",
|
|
678
|
-
content: message + (refContext ? `
|
|
1037
|
+
const history = conversation.map((m) => `[${m.role === "user" ? "user" : "assistant"}]
|
|
1038
|
+
${m.text}`).join("\n\n");
|
|
1039
|
+
const prompt = `${CHAT_SYSTEM}
|
|
1040
|
+
|
|
1041
|
+
${renderBundleContext(bundle)}` + (history ? `
|
|
1042
|
+
|
|
1043
|
+
Conversation so far:
|
|
1044
|
+
|
|
1045
|
+
${history}` : "") + `
|
|
1046
|
+
|
|
1047
|
+
[user]
|
|
1048
|
+
${message}` + (refContext ? `
|
|
679
1049
|
|
|
680
1050
|
Referenced files (resolved from @-references in the message):
|
|
681
1051
|
|
|
682
|
-
${refContext}` : "")
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1052
|
+
${refContext}` : "");
|
|
1053
|
+
let answer = "";
|
|
1054
|
+
let resultText = "";
|
|
1055
|
+
let resultError = false;
|
|
1056
|
+
try {
|
|
1057
|
+
for await (const ev of harness.run({
|
|
1058
|
+
prompt,
|
|
1059
|
+
cwd: refRoot ?? process.cwd(),
|
|
1060
|
+
session: workspace?.changeName ?? bundle.changeName ?? "specsketch-review",
|
|
1061
|
+
profile: reviewProfile
|
|
1062
|
+
})) {
|
|
1063
|
+
if (ev.type === "text") answer += ev.text;
|
|
1064
|
+
else if (ev.type === "result") {
|
|
1065
|
+
resultText = ev.text;
|
|
1066
|
+
resultError = ev.isError;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
686
1069
|
} catch (err) {
|
|
687
1070
|
return anthropicErrorResponse(c, err, "chat-failed");
|
|
688
1071
|
}
|
|
689
|
-
|
|
690
|
-
|
|
1072
|
+
const text = answer.trim() || resultText.trim();
|
|
1073
|
+
if (!text) {
|
|
1074
|
+
return resultError ? c.json({ error: "chat-failed", detail: resultText || "the agent run failed" }, 502) : c.json({ error: "model returned no text" }, 502);
|
|
691
1075
|
}
|
|
692
|
-
const text = textOf(msg);
|
|
693
|
-
if (!text) return c.json({ error: "model returned no text" }, 502);
|
|
694
1076
|
const reply = { role: "assistant", text, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
695
1077
|
workspace?.writeChat([
|
|
696
1078
|
...conversation,
|
|
@@ -711,27 +1093,27 @@ ${refContext}` : "")
|
|
|
711
1093
|
}
|
|
712
1094
|
|
|
713
1095
|
// server/coop-auth.ts
|
|
714
|
-
import { homedir as
|
|
715
|
-
import { join as
|
|
1096
|
+
import { homedir as homedir3 } from "node:os";
|
|
1097
|
+
import { join as join4 } from "node:path";
|
|
716
1098
|
|
|
717
1099
|
// ../shared/src/cli-auth.ts
|
|
718
1100
|
import { exec } from "node:child_process";
|
|
719
1101
|
import { randomBytes } from "node:crypto";
|
|
720
|
-
import { existsSync } from "node:fs";
|
|
1102
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
721
1103
|
import { createServer } from "node:http";
|
|
722
1104
|
import { hostname as hostname2 } from "node:os";
|
|
723
1105
|
import { URL as URL2 } from "node:url";
|
|
724
1106
|
|
|
725
1107
|
// ../shared/src/cli-config.ts
|
|
726
1108
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
727
|
-
import { homedir, hostname } from "node:os";
|
|
728
|
-
import { join as
|
|
1109
|
+
import { homedir as homedir2, hostname } from "node:os";
|
|
1110
|
+
import { join as join3, dirname } from "node:path";
|
|
729
1111
|
|
|
730
1112
|
// ../shared/src/schemas.ts
|
|
731
|
-
import { z as
|
|
732
|
-
var planTierSchema =
|
|
733
|
-
var jurisdictionSchema =
|
|
734
|
-
var locationHintSchema =
|
|
1113
|
+
import { z as z3 } from "zod";
|
|
1114
|
+
var planTierSchema = z3.enum(["hobby", "squad", "org"]);
|
|
1115
|
+
var jurisdictionSchema = z3.enum(["eu", "fedramp"]);
|
|
1116
|
+
var locationHintSchema = z3.enum([
|
|
735
1117
|
"wnam",
|
|
736
1118
|
"enam",
|
|
737
1119
|
"weur",
|
|
@@ -742,135 +1124,135 @@ var locationHintSchema = z2.enum([
|
|
|
742
1124
|
"me",
|
|
743
1125
|
"sam"
|
|
744
1126
|
]);
|
|
745
|
-
var sessionNameSchema =
|
|
1127
|
+
var sessionNameSchema = z3.string().min(1).max(63).regex(
|
|
746
1128
|
/^[a-z0-9][a-z0-9-]*$/,
|
|
747
1129
|
"Session name must be lowercase alphanumeric with hyphens, starting with a letter or digit"
|
|
748
1130
|
);
|
|
749
|
-
var teamNameSchema =
|
|
1131
|
+
var teamNameSchema = z3.string().min(1).max(63).regex(
|
|
750
1132
|
/^[a-z0-9][a-z0-9-]*$/,
|
|
751
1133
|
"Team name must be lowercase alphanumeric with hyphens"
|
|
752
1134
|
);
|
|
753
|
-
var messageKindSchema =
|
|
754
|
-
var messageContentSchema =
|
|
1135
|
+
var messageKindSchema = z3.enum(["text", "context", "request", "system"]);
|
|
1136
|
+
var messageContentSchema = z3.object({
|
|
755
1137
|
kind: messageKindSchema,
|
|
756
1138
|
// No .max() here: the 10 000-char body limit is business logic owned by the
|
|
757
1139
|
// SessionActor (MAX_BODY_BYTES), which rejects with 413 MESSAGE_TOO_LARGE on
|
|
758
1140
|
// both REST and WS. A schema cap would pre-empt it as a generic 400.
|
|
759
|
-
body:
|
|
1141
|
+
body: z3.string().min(1),
|
|
760
1142
|
// Two-arg z.record: identical semantics in zod 3, required form in zod 4
|
|
761
1143
|
// (specsketch bundles this file against zod 4 — keep every construct dual-safe).
|
|
762
|
-
metadata:
|
|
1144
|
+
metadata: z3.record(z3.string(), z3.unknown()).optional()
|
|
763
1145
|
});
|
|
764
|
-
var createTeamInputSchema =
|
|
1146
|
+
var createTeamInputSchema = z3.object({
|
|
765
1147
|
name: teamNameSchema
|
|
766
1148
|
});
|
|
767
|
-
var createSessionInputSchema =
|
|
1149
|
+
var createSessionInputSchema = z3.object({
|
|
768
1150
|
name: sessionNameSchema,
|
|
769
|
-
teamId:
|
|
1151
|
+
teamId: z3.string().optional(),
|
|
770
1152
|
jurisdiction: jurisdictionSchema.optional(),
|
|
771
1153
|
locationHint: locationHintSchema.optional()
|
|
772
1154
|
});
|
|
773
|
-
var sendMessageInputSchema =
|
|
1155
|
+
var sendMessageInputSchema = z3.object({
|
|
774
1156
|
content: messageContentSchema
|
|
775
1157
|
});
|
|
776
|
-
var inviteInputSchema =
|
|
777
|
-
email:
|
|
778
|
-
role:
|
|
1158
|
+
var inviteInputSchema = z3.object({
|
|
1159
|
+
email: z3.string().email(),
|
|
1160
|
+
role: z3.enum(["admin", "member"]).default("member")
|
|
779
1161
|
});
|
|
780
|
-
var joinTeamInputSchema =
|
|
781
|
-
inviteCode:
|
|
1162
|
+
var joinTeamInputSchema = z3.object({
|
|
1163
|
+
inviteCode: z3.string().min(1)
|
|
782
1164
|
});
|
|
783
|
-
var markReadInputSchema =
|
|
784
|
-
notificationIds:
|
|
1165
|
+
var markReadInputSchema = z3.object({
|
|
1166
|
+
notificationIds: z3.array(z3.string().min(1))
|
|
785
1167
|
});
|
|
786
|
-
var pollQuerySchema =
|
|
787
|
-
since:
|
|
788
|
-
limit:
|
|
789
|
-
session:
|
|
1168
|
+
var pollQuerySchema = z3.object({
|
|
1169
|
+
since: z3.string().optional(),
|
|
1170
|
+
limit: z3.coerce.number().int().min(1).max(100).default(50),
|
|
1171
|
+
session: z3.string().optional()
|
|
790
1172
|
});
|
|
791
|
-
var messagesQuerySchema =
|
|
792
|
-
cursor:
|
|
793
|
-
limit:
|
|
1173
|
+
var messagesQuerySchema = z3.object({
|
|
1174
|
+
cursor: z3.string().optional(),
|
|
1175
|
+
limit: z3.coerce.number().int().min(1).max(100).default(50)
|
|
794
1176
|
});
|
|
795
|
-
var teamTypeSchema =
|
|
796
|
-
var teamRoleSchema =
|
|
797
|
-
var accessPermissionSchema =
|
|
798
|
-
var accessGrantInputSchema =
|
|
799
|
-
userId:
|
|
1177
|
+
var teamTypeSchema = z3.enum(["personal", "shared"]);
|
|
1178
|
+
var teamRoleSchema = z3.enum(["owner", "admin", "member"]);
|
|
1179
|
+
var accessPermissionSchema = z3.enum(["read", "write", "admin"]);
|
|
1180
|
+
var accessGrantInputSchema = z3.object({
|
|
1181
|
+
userId: z3.string().min(1),
|
|
800
1182
|
sessionName: sessionNameSchema.nullable().default(null),
|
|
801
1183
|
// null = general
|
|
802
1184
|
permission: accessPermissionSchema
|
|
803
1185
|
});
|
|
804
|
-
var machineIdSchema =
|
|
1186
|
+
var machineIdSchema = z3.string().min(1).max(63).regex(
|
|
805
1187
|
/^[a-z0-9][a-z0-9-]*$/,
|
|
806
1188
|
"Machine ID must be lowercase alphanumeric with hyphens, starting with a letter or digit"
|
|
807
1189
|
);
|
|
808
|
-
var clerkProfileInputSchema =
|
|
809
|
-
email:
|
|
810
|
-
displayName:
|
|
811
|
-
avatarUrl:
|
|
812
|
-
provider:
|
|
813
|
-
providerUserId:
|
|
1190
|
+
var clerkProfileInputSchema = z3.object({
|
|
1191
|
+
email: z3.string().email().nullable(),
|
|
1192
|
+
displayName: z3.string().min(1).max(100),
|
|
1193
|
+
avatarUrl: z3.string().url().nullable(),
|
|
1194
|
+
provider: z3.string().min(1).max(50),
|
|
1195
|
+
providerUserId: z3.string().min(1).max(100)
|
|
814
1196
|
});
|
|
815
|
-
var sessionEnrollmentSchema =
|
|
816
|
-
sessionId:
|
|
817
|
-
team:
|
|
818
|
-
teamName:
|
|
819
|
-
key:
|
|
820
|
-
enrolledAt:
|
|
821
|
-
apiUrl:
|
|
822
|
-
machineId:
|
|
1197
|
+
var sessionEnrollmentSchema = z3.object({
|
|
1198
|
+
sessionId: z3.string(),
|
|
1199
|
+
team: z3.string(),
|
|
1200
|
+
teamName: z3.string().optional(),
|
|
1201
|
+
key: z3.string().startsWith("api_"),
|
|
1202
|
+
enrolledAt: z3.string().datetime(),
|
|
1203
|
+
apiUrl: z3.string().url().optional(),
|
|
1204
|
+
machineId: z3.string().optional()
|
|
823
1205
|
});
|
|
824
|
-
var authConfigSchema =
|
|
825
|
-
key:
|
|
826
|
-
userId:
|
|
827
|
-
displayName:
|
|
828
|
-
email:
|
|
829
|
-
machineId:
|
|
830
|
-
authenticatedAt:
|
|
1206
|
+
var authConfigSchema = z3.object({
|
|
1207
|
+
key: z3.string().startsWith("api_"),
|
|
1208
|
+
userId: z3.string().min(1),
|
|
1209
|
+
displayName: z3.string().nullable().optional().default(null),
|
|
1210
|
+
email: z3.string().email().nullable(),
|
|
1211
|
+
machineId: z3.string().nullable(),
|
|
1212
|
+
authenticatedAt: z3.string().datetime()
|
|
831
1213
|
});
|
|
832
1214
|
var configBaseFields = {
|
|
833
|
-
apiUrl:
|
|
834
|
-
machineId:
|
|
835
|
-
sessions:
|
|
836
|
-
defaults:
|
|
837
|
-
team:
|
|
838
|
-
session:
|
|
1215
|
+
apiUrl: z3.string().url(),
|
|
1216
|
+
machineId: z3.string().nullable(),
|
|
1217
|
+
sessions: z3.record(z3.string(), sessionEnrollmentSchema),
|
|
1218
|
+
defaults: z3.object({
|
|
1219
|
+
team: z3.string().nullable(),
|
|
1220
|
+
session: z3.string().nullable()
|
|
839
1221
|
}),
|
|
840
|
-
poll:
|
|
841
|
-
lastPollAt:
|
|
1222
|
+
poll: z3.object({
|
|
1223
|
+
lastPollAt: z3.string().datetime().nullable()
|
|
842
1224
|
})
|
|
843
1225
|
};
|
|
844
|
-
var coopConfigV1Schema =
|
|
845
|
-
version:
|
|
1226
|
+
var coopConfigV1Schema = z3.object({
|
|
1227
|
+
version: z3.literal(1),
|
|
846
1228
|
...configBaseFields
|
|
847
1229
|
});
|
|
848
|
-
var coopConfigV2Schema =
|
|
849
|
-
version:
|
|
1230
|
+
var coopConfigV2Schema = z3.object({
|
|
1231
|
+
version: z3.literal(2),
|
|
850
1232
|
...configBaseFields,
|
|
851
1233
|
auth: authConfigSchema.optional()
|
|
852
1234
|
});
|
|
853
|
-
var profileNameSchema =
|
|
1235
|
+
var profileNameSchema = z3.string().regex(
|
|
854
1236
|
/^[a-z0-9][a-z0-9_-]{0,63}$/,
|
|
855
1237
|
"Profile name must be lowercase alphanumeric with dashes/underscores, max 64 chars"
|
|
856
1238
|
);
|
|
857
|
-
var coopProfileSchema =
|
|
858
|
-
apiUrl:
|
|
1239
|
+
var coopProfileSchema = z3.object({
|
|
1240
|
+
apiUrl: z3.string().url(),
|
|
859
1241
|
auth: authConfigSchema.optional(),
|
|
860
|
-
sessions:
|
|
861
|
-
defaults:
|
|
862
|
-
team:
|
|
863
|
-
session:
|
|
1242
|
+
sessions: z3.record(z3.string(), sessionEnrollmentSchema),
|
|
1243
|
+
defaults: z3.object({
|
|
1244
|
+
team: z3.string().nullable(),
|
|
1245
|
+
session: z3.string().nullable()
|
|
864
1246
|
}),
|
|
865
|
-
poll:
|
|
866
|
-
lastPollAt:
|
|
1247
|
+
poll: z3.object({
|
|
1248
|
+
lastPollAt: z3.string().datetime().nullable()
|
|
867
1249
|
})
|
|
868
1250
|
});
|
|
869
|
-
var coopConfigV3ObjectSchema =
|
|
870
|
-
version:
|
|
871
|
-
machineId:
|
|
872
|
-
defaultProfile:
|
|
873
|
-
profiles:
|
|
1251
|
+
var coopConfigV3ObjectSchema = z3.object({
|
|
1252
|
+
version: z3.literal(3),
|
|
1253
|
+
machineId: z3.string().nullable(),
|
|
1254
|
+
defaultProfile: z3.string(),
|
|
1255
|
+
profiles: z3.record(profileNameSchema, coopProfileSchema)
|
|
874
1256
|
});
|
|
875
1257
|
function requireDefaultProfilePointer(config, ctx) {
|
|
876
1258
|
if (!(config.defaultProfile in config.profiles)) {
|
|
@@ -884,7 +1266,7 @@ function requireDefaultProfilePointer(config, ctx) {
|
|
|
884
1266
|
var coopConfigV3Schema = coopConfigV3ObjectSchema.superRefine(
|
|
885
1267
|
requireDefaultProfilePointer
|
|
886
1268
|
);
|
|
887
|
-
var coopConfigSchema =
|
|
1269
|
+
var coopConfigSchema = z3.discriminatedUnion("version", [
|
|
888
1270
|
coopConfigV1Schema,
|
|
889
1271
|
coopConfigV2Schema,
|
|
890
1272
|
coopConfigV3ObjectSchema
|
|
@@ -893,20 +1275,39 @@ var coopConfigSchema = z2.discriminatedUnion("version", [
|
|
|
893
1275
|
requireDefaultProfilePointer(config, ctx);
|
|
894
1276
|
}
|
|
895
1277
|
});
|
|
896
|
-
var hookStdinSchema =
|
|
897
|
-
session_id:
|
|
898
|
-
cwd:
|
|
899
|
-
hook_event_name:
|
|
1278
|
+
var hookStdinSchema = z3.object({
|
|
1279
|
+
session_id: z3.string().optional(),
|
|
1280
|
+
cwd: z3.string().optional(),
|
|
1281
|
+
hook_event_name: z3.string().optional()
|
|
900
1282
|
}).passthrough();
|
|
901
|
-
var wsClientMessageSchema =
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1283
|
+
var wsClientMessageSchema = z3.discriminatedUnion("type", [
|
|
1284
|
+
z3.object({ type: z3.literal("message"), content: messageContentSchema }),
|
|
1285
|
+
z3.object({ type: z3.literal("ack"), data: z3.object({ messageId: z3.string() }) }),
|
|
1286
|
+
z3.object({ type: z3.literal("presence_request") }),
|
|
1287
|
+
z3.object({ type: z3.literal("ping") })
|
|
905
1288
|
]);
|
|
1289
|
+
var workKindSchema = z3.enum(["implement", "generate"]);
|
|
1290
|
+
var workCompletionStatusSchema = z3.enum(["succeeded", "failed"]);
|
|
1291
|
+
var poolJoinInputSchema = z3.object({}).passthrough();
|
|
1292
|
+
var poolCheckInInputSchema = z3.object({}).passthrough();
|
|
1293
|
+
var poolCompleteInputSchema = z3.object({
|
|
1294
|
+
workId: z3.string().min(1),
|
|
1295
|
+
status: workCompletionStatusSchema,
|
|
1296
|
+
resultRef: z3.string().max(512),
|
|
1297
|
+
assignmentSeq: z3.number().int().nonnegative()
|
|
1298
|
+
});
|
|
1299
|
+
var poolEnqueueInputSchema = z3.object({
|
|
1300
|
+
kind: workKindSchema,
|
|
1301
|
+
source: z3.string().min(1),
|
|
1302
|
+
dedupKey: z3.string().min(1).max(512).optional()
|
|
1303
|
+
});
|
|
1304
|
+
var poolCancelInputSchema = z3.object({
|
|
1305
|
+
workId: z3.string().min(1)
|
|
1306
|
+
});
|
|
906
1307
|
|
|
907
1308
|
// ../shared/src/cli-config.ts
|
|
908
|
-
var DEFAULT_CONFIG_DIR =
|
|
909
|
-
var DEFAULT_CONFIG_PATH =
|
|
1309
|
+
var DEFAULT_CONFIG_DIR = join3(homedir2(), ".coopcli");
|
|
1310
|
+
var DEFAULT_CONFIG_PATH = join3(DEFAULT_CONFIG_DIR, "config.json");
|
|
910
1311
|
var PROD_API_URL = "https://api.coopcli.com";
|
|
911
1312
|
function emptyProfile(apiUrl) {
|
|
912
1313
|
return {
|
|
@@ -1383,7 +1784,7 @@ var CliAuthError = class extends Error {
|
|
|
1383
1784
|
async function resolveCoopCredentials(opts) {
|
|
1384
1785
|
const env = opts.env ?? process.env;
|
|
1385
1786
|
const { configPath, loginHint } = opts;
|
|
1386
|
-
if (!
|
|
1787
|
+
if (!existsSync2(configPath)) {
|
|
1387
1788
|
throw new CliAuthError(`No CoopCLI config found at ${configPath}.
|
|
1388
1789
|
${loginHint}`, "missing-config");
|
|
1389
1790
|
}
|
|
@@ -1425,7 +1826,7 @@ ${loginHint}`, "no-login");
|
|
|
1425
1826
|
var CoopAuthError = class extends Error {
|
|
1426
1827
|
};
|
|
1427
1828
|
var LOGIN_HINT = "--share needs a CoopCLI account. Run `specsketch login` (it opens your browser and saves credentials to ~/.coopcli/config.json), then rerun with --share.";
|
|
1428
|
-
async function readCoopAuth(configPath =
|
|
1829
|
+
async function readCoopAuth(configPath = join4(homedir3(), ".coopcli", "config.json"), env = process.env) {
|
|
1429
1830
|
try {
|
|
1430
1831
|
return await resolveCoopCredentials({
|
|
1431
1832
|
configPath,
|
|
@@ -1688,14 +2089,6 @@ var ApiClient = class _ApiClient {
|
|
|
1688
2089
|
latest: count
|
|
1689
2090
|
});
|
|
1690
2091
|
}
|
|
1691
|
-
async queryAIContext(teamId, sessionName, input) {
|
|
1692
|
-
return this.request(
|
|
1693
|
-
"POST",
|
|
1694
|
-
this.userPath(`/sessions/${encodeURIComponent(sessionName)}/ai-context`),
|
|
1695
|
-
input,
|
|
1696
|
-
{ team: teamId }
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
2092
|
async listSessionFiles(teamId, sessionName) {
|
|
1700
2093
|
const result = await this.request(
|
|
1701
2094
|
"GET",
|
|
@@ -1783,6 +2176,72 @@ var ApiClient = class _ApiClient {
|
|
|
1783
2176
|
const size = parseInt(res.headers.get("Content-Length") ?? "0", 10);
|
|
1784
2177
|
return { filename, body: res.body, size };
|
|
1785
2178
|
}
|
|
2179
|
+
// ── Agent pool (specsesh-agent-pool) — user-scoped routes ──────────────
|
|
2180
|
+
/** Enroll this machine in its org's execute pool. Identity comes from the key. */
|
|
2181
|
+
async poolJoin() {
|
|
2182
|
+
return this.request("POST", this.userPath("/pool/join"), {});
|
|
2183
|
+
}
|
|
2184
|
+
/** Check in; may return one assigned work item in the same round-trip (FIFO). */
|
|
2185
|
+
async poolCheckIn() {
|
|
2186
|
+
return this.request("POST", this.userPath("/pool/check-in"), {});
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* The org pool's roster. `specsesh dispatch` reads it to surface a live-agent
|
|
2190
|
+
* count so a dispatch into a pool with zero live agents is a visible no-signal
|
|
2191
|
+
* enqueue, not a silent success (specsesh-work-dispatch §4.4).
|
|
2192
|
+
*/
|
|
2193
|
+
async poolRoster() {
|
|
2194
|
+
const body = await this.request("GET", this.userPath("/pool/roster"));
|
|
2195
|
+
return body.agents;
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Report completion of an assigned work item. `assignmentSeq` is REQUIRED
|
|
2199
|
+
* (specsesh-task-lifecycle §5) — echo back the epoch from the `WorkItem`
|
|
2200
|
+
* this caller was handed at check-in, so the DO's ABA fence can reject a
|
|
2201
|
+
* stale completion from a since-superseded assignment.
|
|
2202
|
+
*/
|
|
2203
|
+
async poolComplete(workId, status, resultRef, assignmentSeq) {
|
|
2204
|
+
await this.request("POST", this.userPath("/pool/complete"), { workId, status, resultRef, assignmentSeq });
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Dispatch a task record into the org pool as one work item (idempotent on
|
|
2208
|
+
* the qualified `dedupKey`). CLI producer of specsesh-work-dispatch; returns
|
|
2209
|
+
* the (possibly already-existing) work id.
|
|
2210
|
+
*/
|
|
2211
|
+
async poolEnqueue(kind, source, dedupKey) {
|
|
2212
|
+
return this.request("POST", this.userPath("/pool/enqueue"), { kind, source, dedupKey });
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Org-scoped cancel (specsesh-task-lifecycle §2) — any authenticated member
|
|
2216
|
+
* of the org may cancel any work item in its pool, regardless of which
|
|
2217
|
+
* agent (if any) holds it. `queued` cancels directly; `assigned`/`running`
|
|
2218
|
+
* cancels and frees the holding agent (no redelivery); an already-terminal
|
|
2219
|
+
* item rejects.
|
|
2220
|
+
*/
|
|
2221
|
+
async poolCancel(workId) {
|
|
2222
|
+
await this.request("POST", this.userPath("/pool/cancel"), { workId });
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Query a single work item's current status (specsesh-task-lifecycle §4) —
|
|
2226
|
+
* tenant-scoped, independent of whether the caller is the agent (if any)
|
|
2227
|
+
* holding it. Throws `CoopApiError` (`POOL_WORK_NOT_FOUND`, 404) if the
|
|
2228
|
+
* item does not exist in this organization's pool.
|
|
2229
|
+
*/
|
|
2230
|
+
async poolWork(workId) {
|
|
2231
|
+
const body = await this.request(
|
|
2232
|
+
"GET",
|
|
2233
|
+
this.userPath(`/pool/work/${encodeURIComponent(workId)}`)
|
|
2234
|
+
);
|
|
2235
|
+
return body.work;
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* List every work item currently in the organization's pool, regardless of
|
|
2239
|
+
* status (specsesh-task-lifecycle §4).
|
|
2240
|
+
*/
|
|
2241
|
+
async poolWorkList() {
|
|
2242
|
+
const body = await this.request("GET", this.userPath("/pool/work"));
|
|
2243
|
+
return body.items;
|
|
2244
|
+
}
|
|
1786
2245
|
getWebSocketUrl(session) {
|
|
1787
2246
|
const wsBase = this.apiUrl.replace(
|
|
1788
2247
|
/^https?:\/\//,
|
|
@@ -1796,98 +2255,98 @@ var ApiClient = class _ApiClient {
|
|
|
1796
2255
|
};
|
|
1797
2256
|
|
|
1798
2257
|
// ../shared/src/canvas-protocol.ts
|
|
1799
|
-
import { z as
|
|
2258
|
+
import { z as z4 } from "zod";
|
|
1800
2259
|
var CANVAS_PROTOCOL_VERSION = 1;
|
|
1801
|
-
var canvasElementSchema =
|
|
1802
|
-
id:
|
|
1803
|
-
version:
|
|
1804
|
-
versionNonce:
|
|
1805
|
-
isDeleted:
|
|
2260
|
+
var canvasElementSchema = z4.object({
|
|
2261
|
+
id: z4.string().min(1),
|
|
2262
|
+
version: z4.number().int().nonnegative(),
|
|
2263
|
+
versionNonce: z4.number().int(),
|
|
2264
|
+
isDeleted: z4.boolean().optional()
|
|
1806
2265
|
}).passthrough();
|
|
1807
|
-
var canvasFilesSchema =
|
|
1808
|
-
var canvasSceneSchema =
|
|
1809
|
-
elements:
|
|
2266
|
+
var canvasFilesSchema = z4.record(z4.string(), z4.unknown());
|
|
2267
|
+
var canvasSceneSchema = z4.object({
|
|
2268
|
+
elements: z4.array(canvasElementSchema),
|
|
1810
2269
|
files: canvasFilesSchema.optional()
|
|
1811
2270
|
});
|
|
1812
|
-
var canvasParticipantSchema =
|
|
1813
|
-
id:
|
|
1814
|
-
role:
|
|
1815
|
-
name:
|
|
1816
|
-
color:
|
|
2271
|
+
var canvasParticipantSchema = z4.object({
|
|
2272
|
+
id: z4.string().min(1),
|
|
2273
|
+
role: z4.enum(["host", "guest"]),
|
|
2274
|
+
name: z4.string().optional(),
|
|
2275
|
+
color: z4.string().optional()
|
|
1817
2276
|
});
|
|
1818
|
-
var canvasHelloSchema =
|
|
1819
|
-
type:
|
|
1820
|
-
protocolVersion:
|
|
1821
|
-
role:
|
|
2277
|
+
var canvasHelloSchema = z4.object({
|
|
2278
|
+
type: z4.literal("hello"),
|
|
2279
|
+
protocolVersion: z4.number().int().positive(),
|
|
2280
|
+
role: z4.enum(["host", "guest"]),
|
|
1822
2281
|
/** Stable id across reconnects, if the client has one. */
|
|
1823
|
-
participantId:
|
|
1824
|
-
name:
|
|
2282
|
+
participantId: z4.string().min(1).max(64).optional(),
|
|
2283
|
+
name: z4.string().max(120).optional()
|
|
1825
2284
|
});
|
|
1826
|
-
var canvasClientOpSchema =
|
|
1827
|
-
type:
|
|
2285
|
+
var canvasClientOpSchema = z4.object({
|
|
2286
|
+
type: z4.literal("op"),
|
|
1828
2287
|
/** Changed or soft-deleted elements only — never the full scene. */
|
|
1829
|
-
elements:
|
|
2288
|
+
elements: z4.array(canvasElementSchema).min(1),
|
|
1830
2289
|
/** New binary files referenced by the changed elements, if any. */
|
|
1831
2290
|
files: canvasFilesSchema.optional(),
|
|
1832
2291
|
/** Client-local monotonic sequence, used by the sender for echo dedupe. */
|
|
1833
|
-
clientSeq:
|
|
2292
|
+
clientSeq: z4.number().int().nonnegative()
|
|
1834
2293
|
});
|
|
1835
|
-
var canvasClientMessageSchema =
|
|
2294
|
+
var canvasClientMessageSchema = z4.discriminatedUnion("type", [
|
|
1836
2295
|
canvasHelloSchema,
|
|
1837
2296
|
canvasClientOpSchema
|
|
1838
2297
|
]);
|
|
1839
|
-
var canvasSnapshotSchema =
|
|
1840
|
-
type:
|
|
2298
|
+
var canvasSnapshotSchema = z4.object({
|
|
2299
|
+
type: z4.literal("snapshot"),
|
|
1841
2300
|
scene: canvasSceneSchema,
|
|
1842
|
-
revision:
|
|
1843
|
-
participants:
|
|
2301
|
+
revision: z4.number().int().nonnegative(),
|
|
2302
|
+
participants: z4.array(canvasParticipantSchema),
|
|
1844
2303
|
/** Echoes the server's protocol version so clients can detect skew. */
|
|
1845
|
-
protocolVersion:
|
|
2304
|
+
protocolVersion: z4.number().int().positive(),
|
|
1846
2305
|
/** The participant id the server assigned to (or kept for) this client. */
|
|
1847
|
-
participantId:
|
|
2306
|
+
participantId: z4.string()
|
|
1848
2307
|
});
|
|
1849
|
-
var canvasServerOpSchema =
|
|
1850
|
-
type:
|
|
2308
|
+
var canvasServerOpSchema = z4.object({
|
|
2309
|
+
type: z4.literal("op"),
|
|
1851
2310
|
/** The elements that won reconciliation (never echoed to the sender). */
|
|
1852
|
-
elements:
|
|
2311
|
+
elements: z4.array(canvasElementSchema),
|
|
1853
2312
|
files: canvasFilesSchema.optional(),
|
|
1854
|
-
revision:
|
|
2313
|
+
revision: z4.number().int().positive(),
|
|
1855
2314
|
/** Participant id of the sender. */
|
|
1856
|
-
from:
|
|
2315
|
+
from: z4.string()
|
|
1857
2316
|
});
|
|
1858
|
-
var canvasPresenceSchema =
|
|
1859
|
-
type:
|
|
1860
|
-
participants:
|
|
2317
|
+
var canvasPresenceSchema = z4.object({
|
|
2318
|
+
type: z4.literal("presence"),
|
|
2319
|
+
participants: z4.array(canvasParticipantSchema)
|
|
1861
2320
|
});
|
|
1862
|
-
var canvasEndSchema =
|
|
1863
|
-
type:
|
|
1864
|
-
reason:
|
|
2321
|
+
var canvasEndSchema = z4.object({
|
|
2322
|
+
type: z4.literal("end"),
|
|
2323
|
+
reason: z4.string()
|
|
1865
2324
|
});
|
|
1866
|
-
var canvasErrorSchema =
|
|
1867
|
-
type:
|
|
1868
|
-
code:
|
|
1869
|
-
message:
|
|
2325
|
+
var canvasErrorSchema = z4.object({
|
|
2326
|
+
type: z4.literal("error"),
|
|
2327
|
+
code: z4.string(),
|
|
2328
|
+
message: z4.string()
|
|
1870
2329
|
});
|
|
1871
|
-
var canvasServerMessageSchema =
|
|
2330
|
+
var canvasServerMessageSchema = z4.discriminatedUnion("type", [
|
|
1872
2331
|
canvasSnapshotSchema,
|
|
1873
2332
|
canvasServerOpSchema,
|
|
1874
2333
|
canvasPresenceSchema,
|
|
1875
2334
|
canvasEndSchema,
|
|
1876
2335
|
canvasErrorSchema
|
|
1877
2336
|
]);
|
|
1878
|
-
var canvasSessionCreatedSchema =
|
|
1879
|
-
sessionId:
|
|
2337
|
+
var canvasSessionCreatedSchema = z4.object({
|
|
2338
|
+
sessionId: z4.string().min(1),
|
|
1880
2339
|
/** Capability token — shown once; the server stores only its hash. */
|
|
1881
|
-
token:
|
|
1882
|
-
shareUrl:
|
|
1883
|
-
wsUrl:
|
|
2340
|
+
token: z4.string().min(1),
|
|
2341
|
+
shareUrl: z4.string().min(1),
|
|
2342
|
+
wsUrl: z4.string().min(1)
|
|
1884
2343
|
});
|
|
1885
|
-
var canvasSessionSummarySchema =
|
|
1886
|
-
sessionId:
|
|
1887
|
-
title:
|
|
1888
|
-
status:
|
|
1889
|
-
createdAt:
|
|
1890
|
-
endedAt:
|
|
2344
|
+
var canvasSessionSummarySchema = z4.object({
|
|
2345
|
+
sessionId: z4.string(),
|
|
2346
|
+
title: z4.string().nullable(),
|
|
2347
|
+
status: z4.enum(["active", "ended"]),
|
|
2348
|
+
createdAt: z4.number(),
|
|
2349
|
+
endedAt: z4.number().nullable()
|
|
1891
2350
|
});
|
|
1892
2351
|
function elementWins(local, incoming) {
|
|
1893
2352
|
if (incoming.version !== local.version) return incoming.version > local.version;
|
|
@@ -2171,37 +2630,178 @@ var RemoteSession = class _RemoteSession {
|
|
|
2171
2630
|
};
|
|
2172
2631
|
|
|
2173
2632
|
// server/workspace.ts
|
|
2174
|
-
import { createHash } from "node:crypto";
|
|
2633
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2175
2634
|
import {
|
|
2176
|
-
existsSync as
|
|
2177
|
-
mkdirSync,
|
|
2178
|
-
readFileSync as
|
|
2179
|
-
readdirSync as
|
|
2180
|
-
renameSync,
|
|
2181
|
-
writeFileSync
|
|
2635
|
+
existsSync as existsSync5,
|
|
2636
|
+
mkdirSync as mkdirSync2,
|
|
2637
|
+
readFileSync as readFileSync3,
|
|
2638
|
+
readdirSync as readdirSync4,
|
|
2639
|
+
renameSync as renameSync2,
|
|
2640
|
+
writeFileSync as writeFileSync2
|
|
2182
2641
|
} from "node:fs";
|
|
2183
|
-
import { basename as basename2, dirname as
|
|
2642
|
+
import { basename as basename2, dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
2643
|
+
|
|
2644
|
+
// ../shared/src/openspec-change-meta.ts
|
|
2645
|
+
import { createHash } from "node:crypto";
|
|
2646
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
2647
|
+
import { join as join5 } from "node:path";
|
|
2648
|
+
import { Document, parseDocument } from "yaml";
|
|
2649
|
+
import { z as z5 } from "zod";
|
|
2650
|
+
|
|
2651
|
+
// ../shared/src/fs-atomic.ts
|
|
2652
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
2653
|
+
import { dirname as dirname2 } from "node:path";
|
|
2654
|
+
function writeAtomicFile(target, content) {
|
|
2655
|
+
mkdirSync(dirname2(target), { recursive: true });
|
|
2656
|
+
const tmp = `${target}.tmp`;
|
|
2657
|
+
writeFileSync(tmp, content, "utf8");
|
|
2658
|
+
renameSync(tmp, target);
|
|
2659
|
+
}
|
|
2660
|
+
function readIfExists(target) {
|
|
2661
|
+
return existsSync3(target) ? readFileSync2(target, "utf8") : void 0;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// ../shared/src/openspec-change-meta.ts
|
|
2665
|
+
var CHANGE_META_FILE = "specplan.yaml";
|
|
2666
|
+
var nodeChangeMetaFs = {
|
|
2667
|
+
readIfExists(path) {
|
|
2668
|
+
return readIfExists(path);
|
|
2669
|
+
},
|
|
2670
|
+
writeAtomicFile(path, content) {
|
|
2671
|
+
writeAtomicFile(path, content);
|
|
2672
|
+
},
|
|
2673
|
+
exists(path) {
|
|
2674
|
+
return existsSync4(path);
|
|
2675
|
+
},
|
|
2676
|
+
readdir(path) {
|
|
2677
|
+
if (!existsSync4(path)) return [];
|
|
2678
|
+
return readdirSync3(path, { withFileTypes: true }).map((entry) => ({
|
|
2679
|
+
name: entry.name,
|
|
2680
|
+
directory: entry.isDirectory()
|
|
2681
|
+
}));
|
|
2682
|
+
},
|
|
2683
|
+
realpath(path) {
|
|
2684
|
+
return realpathSync2(path);
|
|
2685
|
+
}
|
|
2686
|
+
};
|
|
2687
|
+
var sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
2688
|
+
function listBodyFiles(changeDir, fsx = nodeChangeMetaFs) {
|
|
2689
|
+
const files = [];
|
|
2690
|
+
for (const name of ["proposal.md", "design.md", "tasks.md"]) {
|
|
2691
|
+
if (fsx.exists(join5(changeDir, name))) files.push(name);
|
|
2692
|
+
}
|
|
2693
|
+
const specsDir = join5(changeDir, "specs");
|
|
2694
|
+
for (const entry of fsx.readdir(specsDir).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2695
|
+
const rel = join5("specs", entry.name, "spec.md");
|
|
2696
|
+
if (entry.directory && fsx.exists(join5(changeDir, rel))) files.push(rel);
|
|
2697
|
+
}
|
|
2698
|
+
return files;
|
|
2699
|
+
}
|
|
2700
|
+
function bodyHash(changeDir, fsx = nodeChangeMetaFs) {
|
|
2701
|
+
const files = listBodyFiles(changeDir, fsx);
|
|
2702
|
+
if (files.length === 0) return null;
|
|
2703
|
+
const parts = files.map(
|
|
2704
|
+
(rel) => `${rel}
|
|
2705
|
+
${sha256(fsx.readIfExists(join5(changeDir, rel)) ?? "")}`
|
|
2706
|
+
);
|
|
2707
|
+
return `sha256:${sha256(parts.join("\n"))}`;
|
|
2708
|
+
}
|
|
2709
|
+
var SPEC_STATUSES = ["draft", "in-progress"];
|
|
2710
|
+
var SpecStatus = z5.enum(SPEC_STATUSES);
|
|
2711
|
+
var LEGACY_STATUS_COERCION = {
|
|
2712
|
+
active: "in-progress",
|
|
2713
|
+
done: "in-progress",
|
|
2714
|
+
deferred: "draft"
|
|
2715
|
+
};
|
|
2716
|
+
var StoredSpecStatus = z5.enum([...SPEC_STATUSES, "active", "done", "deferred"]).transform(
|
|
2717
|
+
(s) => s in LEGACY_STATUS_COERCION ? LEGACY_STATUS_COERCION[s] : s
|
|
2718
|
+
);
|
|
2719
|
+
var SpecMetaSchema = z5.object({
|
|
2720
|
+
specId: z5.string(),
|
|
2721
|
+
userStories: z5.array(z5.string()),
|
|
2722
|
+
dependencies: z5.array(z5.string()),
|
|
2723
|
+
/** Working state (retired values coerce on read); absent = draft. */
|
|
2724
|
+
status: StoredSpecStatus.optional(),
|
|
2725
|
+
/**
|
|
2726
|
+
* Approved axis (formerly `applied`) — fold-driven, SEPARATE from the
|
|
2727
|
+
* working status above. True when the proposal's delta has been folded
|
|
2728
|
+
* into specs/ and archived (apply-first: the fold happens at approval).
|
|
2729
|
+
* Absent = not-approved.
|
|
2730
|
+
*/
|
|
2731
|
+
approved: z5.boolean().optional(),
|
|
2732
|
+
/**
|
|
2733
|
+
* Legacy spelling of the fold axis (pre-rename metadata files). Read-only
|
|
2734
|
+
* compatibility: readers treat `applied: true` as `approved: true`; no
|
|
2735
|
+
* writer ever sets this field again.
|
|
2736
|
+
*/
|
|
2737
|
+
applied: z5.boolean().optional(),
|
|
2738
|
+
/**
|
|
2739
|
+
* Build flag — user-set, independent of the fold: true once the code has
|
|
2740
|
+
* been built to match the approved diff. Absent = not-implemented.
|
|
2741
|
+
*/
|
|
2742
|
+
implemented: z5.boolean().optional(),
|
|
2743
|
+
/**
|
|
2744
|
+
* Minimal implementation-task record written on fold (specplan-timeline-apply
|
|
2745
|
+
* "Implementation task record on fold"): the creation time and a reference to
|
|
2746
|
+
* the archived change's own id/path (from which the delta artifact is
|
|
2747
|
+
* materialized on demand). Deliberately carries NO status and NO dispatch
|
|
2748
|
+
* state — task execution/dispatch is a separate, not-yet-defined concern. A
|
|
2749
|
+
* field addition to this sidecar, not a redefinition of its format.
|
|
2750
|
+
*/
|
|
2751
|
+
task: z5.object({
|
|
2752
|
+
createdAt: z5.string(),
|
|
2753
|
+
source: z5.string()
|
|
2754
|
+
}).optional(),
|
|
2755
|
+
generation: z5.object({
|
|
2756
|
+
/** sha256 of the last generated body files — detects hand edits. */
|
|
2757
|
+
lastGeneratedBodyHash: z5.string(),
|
|
2758
|
+
lastGeneratedAt: z5.string()
|
|
2759
|
+
}).optional()
|
|
2760
|
+
});
|
|
2761
|
+
function writeChangeMeta(fsx, changeDir, meta) {
|
|
2762
|
+
const path = join5(changeDir, CHANGE_META_FILE);
|
|
2763
|
+
const raw = fsx.readIfExists(path);
|
|
2764
|
+
const doc = raw === void 0 ? new Document({
|
|
2765
|
+
specId: meta.specId,
|
|
2766
|
+
userStories: meta.userStories ?? [],
|
|
2767
|
+
dependencies: meta.dependencies ?? [],
|
|
2768
|
+
status: meta.status ?? "draft"
|
|
2769
|
+
}) : parseDocument(raw);
|
|
2770
|
+
if (meta.approved !== void 0) doc.setIn(["approved"], meta.approved);
|
|
2771
|
+
if (meta.implemented !== void 0) doc.setIn(["implemented"], meta.implemented);
|
|
2772
|
+
if (meta.task !== void 0) {
|
|
2773
|
+
doc.setIn(["task", "createdAt"], meta.task.createdAt);
|
|
2774
|
+
doc.setIn(["task", "source"], meta.task.source);
|
|
2775
|
+
}
|
|
2776
|
+
if (meta.generation !== void 0) {
|
|
2777
|
+
doc.setIn(["generation", "lastGeneratedBodyHash"], meta.generation.lastGeneratedBodyHash);
|
|
2778
|
+
doc.setIn(["generation", "lastGeneratedAt"], meta.generation.lastGeneratedAt);
|
|
2779
|
+
}
|
|
2780
|
+
fsx.writeAtomicFile(path, doc.toString());
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
// server/workspace.ts
|
|
2184
2784
|
var DIAGRAM_FILE = "diagram.excalidraw";
|
|
2185
2785
|
var SIDECAR_FILE = "specsketch.json";
|
|
2186
|
-
var
|
|
2786
|
+
var sha2562 = (s) => createHash2("sha256").update(s).digest("hex");
|
|
2187
2787
|
var ChangeWorkspace = class {
|
|
2188
2788
|
dir;
|
|
2189
2789
|
changeName;
|
|
2190
2790
|
constructor(dir) {
|
|
2191
2791
|
this.dir = resolve2(dir);
|
|
2192
2792
|
this.changeName = basename2(this.dir);
|
|
2193
|
-
|
|
2793
|
+
mkdirSync2(this.dir, { recursive: true });
|
|
2194
2794
|
}
|
|
2195
2795
|
writeAtomic(relPath, content) {
|
|
2196
|
-
const target =
|
|
2197
|
-
|
|
2796
|
+
const target = join6(this.dir, relPath);
|
|
2797
|
+
mkdirSync2(dirname3(target), { recursive: true });
|
|
2198
2798
|
const tmp = `${target}.tmp`;
|
|
2199
|
-
|
|
2200
|
-
|
|
2799
|
+
writeFileSync2(tmp, content, "utf8");
|
|
2800
|
+
renameSync2(tmp, target);
|
|
2201
2801
|
}
|
|
2202
2802
|
readIfExists(relPath) {
|
|
2203
|
-
const target =
|
|
2204
|
-
return
|
|
2803
|
+
const target = join6(this.dir, relPath);
|
|
2804
|
+
return existsSync5(target) ? readFileSync3(target, "utf8") : void 0;
|
|
2205
2805
|
}
|
|
2206
2806
|
// ── Scene ────────────────────────────────────────────────────────────────
|
|
2207
2807
|
loadScene() {
|
|
@@ -2262,11 +2862,11 @@ var ChangeWorkspace = class {
|
|
|
2262
2862
|
/** Current artifact files, however they were authored. */
|
|
2263
2863
|
readArtifacts() {
|
|
2264
2864
|
const specs = [];
|
|
2265
|
-
const specsDir =
|
|
2266
|
-
if (
|
|
2267
|
-
for (const entry of
|
|
2865
|
+
const specsDir = join6(this.dir, "specs");
|
|
2866
|
+
if (existsSync5(specsDir)) {
|
|
2867
|
+
for (const entry of readdirSync4(specsDir, { withFileTypes: true })) {
|
|
2268
2868
|
if (!entry.isDirectory()) continue;
|
|
2269
|
-
const spec = this.readIfExists(
|
|
2869
|
+
const spec = this.readIfExists(join6("specs", entry.name, "spec.md"));
|
|
2270
2870
|
if (spec !== void 0) specs.push({ capability: entry.name, spec });
|
|
2271
2871
|
}
|
|
2272
2872
|
}
|
|
@@ -2300,11 +2900,11 @@ var ChangeWorkspace = class {
|
|
|
2300
2900
|
"design.md": bundle.design,
|
|
2301
2901
|
"tasks.md": bundle.tasks
|
|
2302
2902
|
};
|
|
2303
|
-
for (const s of bundle.specs) files[
|
|
2903
|
+
for (const s of bundle.specs) files[join6("specs", s.capability, "spec.md")] = s.spec;
|
|
2304
2904
|
const written = [];
|
|
2305
2905
|
const manifest = {};
|
|
2306
2906
|
for (const [relPath, content] of Object.entries(files)) {
|
|
2307
|
-
manifest[relPath] =
|
|
2907
|
+
manifest[relPath] = sha2562(content);
|
|
2308
2908
|
if (this.readIfExists(relPath) === content) continue;
|
|
2309
2909
|
this.writeAtomic(relPath, content);
|
|
2310
2910
|
written.push(relPath);
|
|
@@ -2317,6 +2917,16 @@ var ChangeWorkspace = class {
|
|
|
2317
2917
|
// post-generation chat seed (generation-chat-reset-summary).
|
|
2318
2918
|
lastGeneration: { at: (/* @__PURE__ */ new Date()).toISOString(), changedFiles: written }
|
|
2319
2919
|
}));
|
|
2920
|
+
const lastGeneratedBodyHash = bodyHash(this.dir, nodeChangeMetaFs);
|
|
2921
|
+
if (lastGeneratedBodyHash) {
|
|
2922
|
+
writeChangeMeta(nodeChangeMetaFs, this.dir, {
|
|
2923
|
+
specId: this.changeName,
|
|
2924
|
+
userStories: [],
|
|
2925
|
+
dependencies: [],
|
|
2926
|
+
status: "draft",
|
|
2927
|
+
generation: { lastGeneratedBodyHash, lastGeneratedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2320
2930
|
return written;
|
|
2321
2931
|
}
|
|
2322
2932
|
// ── Spec review chat ─────────────────────────────────────────────────────
|
|
@@ -2405,15 +3015,15 @@ var MIME = {
|
|
|
2405
3015
|
".woff": "font/woff"
|
|
2406
3016
|
};
|
|
2407
3017
|
function resolveClientDir(moduleUrl) {
|
|
2408
|
-
const here =
|
|
3018
|
+
const here = dirname4(fileURLToPath(moduleUrl));
|
|
2409
3019
|
const candidates = [
|
|
2410
|
-
|
|
3020
|
+
join7(here, "..", "client"),
|
|
2411
3021
|
// packed: dist/cli -> dist/client
|
|
2412
|
-
|
|
3022
|
+
join7(here, "..", "dist", "client")
|
|
2413
3023
|
// repo: server/ -> dist/client
|
|
2414
3024
|
];
|
|
2415
3025
|
for (const dir of candidates) {
|
|
2416
|
-
if (
|
|
3026
|
+
if (existsSync6(join7(dir, "index.html"))) return dir;
|
|
2417
3027
|
}
|
|
2418
3028
|
return null;
|
|
2419
3029
|
}
|
|
@@ -2421,9 +3031,9 @@ function staticResponse(clientDir, pathname) {
|
|
|
2421
3031
|
const rel = normalize2(decodeURIComponent(pathname)).replace(/^\/+/, "");
|
|
2422
3032
|
const target = resolve3(clientDir, rel === "" ? "index.html" : rel);
|
|
2423
3033
|
if (!target.startsWith(resolve3(clientDir))) return null;
|
|
2424
|
-
const file =
|
|
2425
|
-
if (!
|
|
2426
|
-
return new Response(
|
|
3034
|
+
const file = existsSync6(target) && extname(target) ? target : join7(clientDir, "index.html");
|
|
3035
|
+
if (!existsSync6(file)) return null;
|
|
3036
|
+
return new Response(readFileSync4(file), {
|
|
2427
3037
|
headers: { "Content-Type": MIME[extname(file)] ?? "application/octet-stream" }
|
|
2428
3038
|
});
|
|
2429
3039
|
}
|
|
@@ -2435,12 +3045,12 @@ but "Generate Spec" needs one of:
|
|
|
2435
3045
|
\u2022 or the Anthropic CLI's login profile (no key handling needed):
|
|
2436
3046
|
brew install anthropics/tap/ant # macOS
|
|
2437
3047
|
ant auth login`;
|
|
2438
|
-
function detectCredentialSource(env = process.env, home =
|
|
3048
|
+
function detectCredentialSource(env = process.env, home = homedir4()) {
|
|
2439
3049
|
if (env.ANTHROPIC_API_KEY) return "ANTHROPIC_API_KEY";
|
|
2440
3050
|
if (env.ANTHROPIC_AUTH_TOKEN) return "ANTHROPIC_AUTH_TOKEN";
|
|
2441
3051
|
try {
|
|
2442
|
-
const dir =
|
|
2443
|
-
if (
|
|
3052
|
+
const dir = join7(home, ".config", "anthropic", "credentials");
|
|
3053
|
+
if (readdirSync5(dir).some((f) => f.endsWith(".json"))) return "anthropic profile";
|
|
2444
3054
|
} catch {
|
|
2445
3055
|
}
|
|
2446
3056
|
return null;
|
|
@@ -2507,7 +3117,8 @@ ${USAGE}` : err);
|
|
|
2507
3117
|
shareUrl: remote.shareUrl,
|
|
2508
3118
|
status: remote.status,
|
|
2509
3119
|
participants: remote.participants.length
|
|
2510
|
-
} : { active: false }
|
|
3120
|
+
} : { active: false },
|
|
3121
|
+
{ refRoot: process.cwd(), harness: createClaudeHarness() }
|
|
2511
3122
|
);
|
|
2512
3123
|
app.get("*", (c) => {
|
|
2513
3124
|
const res = staticResponse(clientDir, new URL(c.req.url).pathname);
|