@coopcli/specsketch 2026.826.3 → 2026.831.1
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 +967 -1560
- package/dist/client/assets/abnfDiagram-VCTEODGH-BRSPbDe2.js +1 -0
- package/dist/client/assets/arc-CydpUA4x.js +1 -0
- package/dist/client/assets/architecture-7GRP2DOG-DN-gDuKL.js +1 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-BnAWQ9BF.js +36 -0
- package/dist/client/assets/array-BifhSqXX.js +1 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-BLfEm7nQ.js +129 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-BiGdyDb3.js +38 -0
- package/dist/client/assets/channel-CViH2Cbt.js +1 -0
- package/dist/client/assets/chunk-2E4U76K2-B7jDhluq.js +2 -0
- package/dist/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/client/assets/chunk-4HAMMTFA-CeU3f92U.js +62 -0
- package/dist/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/client/assets/chunk-75Z2AOVW-4JCVdQWY.js +2 -0
- package/dist/client/assets/chunk-CLGD4ZFX-BOVbR6kW.js +1 -0
- package/dist/client/assets/chunk-DU6HZSFF-CIXLDUI_.js +127 -0
- package/dist/client/assets/chunk-F27PBJKO-B5GmXBLb.js +1 -0
- package/dist/client/assets/chunk-FOHPRMQF-CyTuVbu3.js +161 -0
- package/dist/client/assets/chunk-GMAD6QVW-D6jp_rLd.js +72 -0
- package/dist/client/assets/chunk-GVQU2GXP-BSS01C8s.js +1 -0
- package/dist/client/assets/chunk-IMKFNOWR-RYwZN0Xs.js +231 -0
- package/dist/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/client/assets/chunk-L3NEJ4N5-D0rNlikd.js +1 -0
- package/dist/client/assets/chunk-LNGE3PJU-DNNNwF8Y.js +34 -0
- package/dist/client/assets/chunk-OSK3NFVY-CxtLTZDw.js +10 -0
- package/dist/client/assets/chunk-P2QGCYS3-CYiUdWzq.js +1 -0
- package/dist/client/assets/chunk-POPQ4Y6H-DgkPLssL.js +1 -0
- package/dist/client/assets/chunk-PWAF6VOD-Db4-4OsA.js +1 -0
- package/dist/client/assets/chunk-SHT3W25Y-Ji-xYakZ.js +168 -0
- package/dist/client/assets/chunk-SVP7TREG-cmFzvno3.js +88 -0
- package/dist/client/assets/chunk-TICWLB2K-Cx1Qs9wQ.js +206 -0
- package/dist/client/assets/chunk-TLUHSLCS-BLXY086M.js +2 -0
- package/dist/client/assets/chunk-XXDRQBXY--jdiETSF.js +1 -0
- package/dist/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-BFPBK_Z1.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BFPBK_Z1.js +1 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-Ba6yLkVt.js +1 -0
- package/dist/client/assets/cynefin-OW5HDTMX-ra_KnFP6.js +1 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-Ct-LNR_1.js +62 -0
- package/dist/client/assets/cytoscape.esm-B3I8pqwA.js +321 -0
- package/dist/client/assets/dagre-BJJUWK_n.js +1 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DJi473sw.js +4 -0
- package/dist/client/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/dist/client/assets/diagram-S7CK7UJ4-kMRGU2Fv.js +30 -0
- package/dist/client/assets/diagram-UQ7AKVKN-YvBZDAJN.js +41 -0
- package/dist/client/assets/diagram-VSXAHHWV-DDK4yWa2.js +3 -0
- package/dist/client/assets/diagram-VX7I27RA-DGg18jEl.js +24 -0
- package/dist/client/assets/diagram-Z3DM3KII-i5D_IxwV.js +24 -0
- package/dist/client/assets/dist-fh2zfMMY.js +1 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-lD2h9EBl.js +1 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-xW2abP7i.js +99 -0
- package/dist/client/assets/eventmodeling-NTZA5JFV-DNmj4472.js +1 -0
- package/dist/client/assets/flowDiagram-HODETNUW-Df4CErRm.js +1 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BeymYKLD.js +292 -0
- package/dist/client/assets/gitGraph-4MIJSDKK-BEWLgvm2.js +1 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BUDPc-8k.js +106 -0
- package/dist/client/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/client/assets/index-5LnLmZOs.css +1 -0
- package/dist/client/assets/index-BWHy8TXM.js +1549 -0
- package/dist/client/assets/info-A6RAGUB7-CKfM1Jkg.js +1 -0
- package/dist/client/assets/infoDiagram-27XIBGKW-Ad-Gyaye.js +2 -0
- package/dist/client/assets/init-D6jRqBbL.js +1 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-KbTiiaHC.js +70 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CN2MkzPK.js +139 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-DhEzOLN4.js +89 -0
- package/dist/client/assets/katex-CddkPoXu.js +257 -0
- package/dist/client/assets/line-B90f4k6X.js +1 -0
- package/dist/client/assets/linear-CW7Oi12O.js +1 -0
- package/dist/client/assets/mermaid-parser.core-C9nLZzv4.js +7 -0
- package/dist/client/assets/mermaid.core-DQoThCaK.js +11 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-J048QoEO.js +96 -0
- package/dist/client/assets/ordinal-hYBb2elL.js +1 -0
- package/dist/client/assets/packet-AYTQ26CC-BKj_BZVA.js +1 -0
- package/dist/client/assets/path-BWPyau1x.js +1 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-DG0kAfWm.js +1 -0
- package/dist/client/assets/pie-WAS4IAKB-DE_LVWU7.js +1 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-Z3FhwRXc.js +39 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-Dw3TFUlr.js +7 -0
- package/dist/client/assets/radar-RG4KPBEZ-BtwXXEfu.js +1 -0
- package/dist/client/assets/railroad-74A4TZTK-CBKff9R-.js +1 -0
- package/dist/client/assets/railroad-abnf-HS5TGJTU-Bv7gdzGQ.js +1 -0
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-CCPDJ5pu.js +1 -0
- package/dist/client/assets/railroad-peg-WCYAUIDC-PBEP962D.js +1 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Cf_GxnJ6.js +1 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DeSfKgO1.js +84 -0
- package/dist/client/assets/rough.esm-CSKSodPl.js +1 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-ZJbsUmhM.js +40 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CZV-uNwY.js +162 -0
- package/dist/client/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dist/client/assets/src-CKIr8zwz.js +1 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-S2gUXjCW.js +1 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-BEJXWTYJ.js +1 -0
- package/dist/client/assets/swimlanes-42K2YHIH-D9K5izOo.js +1 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-ze5UMZmc.js +8 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-o1QNGpc1.js +120 -0
- package/dist/client/assets/treeView-Q6P3EWNA-shNxWNdU.js +1 -0
- package/dist/client/assets/treemap-WGGIJYW6-CTnOzBS1.js +1 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-C74kYmI6.js +34 -0
- package/dist/client/assets/wardley-WFR3VGLG-wa25fBU8.js +1 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DMCFZZJI.js +78 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-CZzaBxU9.js +7 -0
- package/dist/client/index.html +2 -2
- package/package.json +79 -87
- package/dist/client/assets/MarkdownEditorImpl-B7fANsTl.js +0 -16
- package/dist/client/assets/index-CLLS_dcT.css +0 -1
- package/dist/client/assets/index-DzRQXC_d.js +0 -197
package/dist/cli/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// server/cli.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync8 } from "node:fs";
|
|
5
5
|
import { homedir as homedir4 } from "node:os";
|
|
6
|
-
import { dirname as dirname6, extname, join as
|
|
6
|
+
import { dirname as dirname6, extname, join as join12, normalize as normalize2, resolve as resolve4 } 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";
|
|
@@ -16,6 +16,20 @@ import { join } from "node:path";
|
|
|
16
16
|
var ModelAuthError = class extends Error {
|
|
17
17
|
};
|
|
18
18
|
var API_ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
19
|
+
var THIRD_PARTY_ROUTING_KEYS = [
|
|
20
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
21
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
22
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
23
|
+
"CLAUDE_CODE_USE_MANTLE",
|
|
24
|
+
"CLAUDE_CODE_USE_GATEWAY",
|
|
25
|
+
"CLAUDE_CODE_USE_ANTHROPIC_AWS",
|
|
26
|
+
"CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD",
|
|
27
|
+
"ANTHROPIC_BASE_URL"
|
|
28
|
+
];
|
|
29
|
+
var SUBSCRIPTION_STRIPPED_ENV_KEYS = [
|
|
30
|
+
...API_ENV_KEYS,
|
|
31
|
+
...THIRD_PARTY_ROUTING_KEYS
|
|
32
|
+
];
|
|
19
33
|
function hasAntAuthProfile(home) {
|
|
20
34
|
try {
|
|
21
35
|
return readdirSync(join(home, ".config", "anthropic", "credentials")).some(
|
|
@@ -40,7 +54,7 @@ function resolveModelCredential(opts = {}) {
|
|
|
40
54
|
}
|
|
41
55
|
if (hasSubscription) {
|
|
42
56
|
const subprocessEnv = { ...env };
|
|
43
|
-
for (const k of
|
|
57
|
+
for (const k of SUBSCRIPTION_STRIPPED_ENV_KEYS) delete subprocessEnv[k];
|
|
44
58
|
return {
|
|
45
59
|
path: "subscription",
|
|
46
60
|
billsApiCredits: false,
|
|
@@ -117,9 +131,38 @@ function destructiveBashHook(profile) {
|
|
|
117
131
|
]
|
|
118
132
|
};
|
|
119
133
|
}
|
|
120
|
-
|
|
134
|
+
var KNOWN_CATEGORIES = {
|
|
135
|
+
authentication_failed: true,
|
|
136
|
+
oauth_org_not_allowed: true,
|
|
137
|
+
billing_error: true,
|
|
138
|
+
rate_limit: true,
|
|
139
|
+
overloaded: true,
|
|
140
|
+
invalid_request: true,
|
|
141
|
+
model_not_found: true,
|
|
142
|
+
server_error: true,
|
|
143
|
+
unknown: true,
|
|
144
|
+
max_output_tokens: true
|
|
145
|
+
};
|
|
146
|
+
var ABSENT_CATEGORY = "(none reported)";
|
|
147
|
+
function classifyFailure(category) {
|
|
148
|
+
if (typeof category !== "string" || category === "") {
|
|
149
|
+
return { category: ABSENT_CATEGORY, recognised: false };
|
|
150
|
+
}
|
|
151
|
+
return { category, recognised: Object.hasOwn(KNOWN_CATEGORIES, category) };
|
|
152
|
+
}
|
|
153
|
+
function* mapMessage(message, ctx) {
|
|
121
154
|
switch (message.type) {
|
|
122
155
|
case "system": {
|
|
156
|
+
if (message.subtype === "api_retry") {
|
|
157
|
+
yield {
|
|
158
|
+
type: "retry",
|
|
159
|
+
failure: classifyFailure(message.error),
|
|
160
|
+
attempt: message.attempt,
|
|
161
|
+
maxRetries: message.max_retries,
|
|
162
|
+
retryDelayMs: message.retry_delay_ms
|
|
163
|
+
};
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
123
166
|
if (message.subtype === "init") {
|
|
124
167
|
yield {
|
|
125
168
|
type: "start",
|
|
@@ -131,8 +174,11 @@ function* mapMessage(message, expectStructuredOutput) {
|
|
|
131
174
|
}
|
|
132
175
|
case "assistant": {
|
|
133
176
|
const assistant = message;
|
|
177
|
+
if (assistant.error !== void 0) ctx.lastFailure = classifyFailure(assistant.error);
|
|
134
178
|
for (const block of assistant.message.content) {
|
|
135
179
|
if (block.type === "text") {
|
|
180
|
+
ctx.partialText += ctx.partialText ? `
|
|
181
|
+
${block.text}` : block.text;
|
|
136
182
|
yield { type: "text", text: block.text };
|
|
137
183
|
} else if (block.type === "thinking") {
|
|
138
184
|
yield { type: "thinking", text: block.thinking };
|
|
@@ -160,20 +206,37 @@ function* mapMessage(message, expectStructuredOutput) {
|
|
|
160
206
|
}
|
|
161
207
|
case "result": {
|
|
162
208
|
if (message.subtype === "success") {
|
|
163
|
-
const missingStructured = expectStructuredOutput && message.structured_output === void 0;
|
|
209
|
+
const missingStructured = ctx.expectStructuredOutput && message.structured_output === void 0;
|
|
210
|
+
const isError = message.is_error || missingStructured;
|
|
164
211
|
yield {
|
|
165
212
|
type: "result",
|
|
166
213
|
text: message.result,
|
|
167
214
|
costUsd: message.total_cost_usd,
|
|
168
|
-
isError
|
|
215
|
+
isError,
|
|
216
|
+
outcome: isError ? "failed" : "answered",
|
|
217
|
+
turns: message.num_turns,
|
|
218
|
+
...isError && ctx.lastFailure ? { failure: ctx.lastFailure } : {},
|
|
169
219
|
...message.structured_output !== void 0 ? { structuredOutput: message.structured_output } : {}
|
|
170
220
|
};
|
|
221
|
+
} else if (message.subtype === "error_max_turns") {
|
|
222
|
+
yield {
|
|
223
|
+
type: "result",
|
|
224
|
+
text: ctx.partialText || message.errors.join("\n"),
|
|
225
|
+
costUsd: message.total_cost_usd,
|
|
226
|
+
isError: true,
|
|
227
|
+
outcome: "cut_short",
|
|
228
|
+
turns: message.num_turns,
|
|
229
|
+
...ctx.lastFailure ? { failure: ctx.lastFailure } : {}
|
|
230
|
+
};
|
|
171
231
|
} else {
|
|
172
232
|
yield {
|
|
173
233
|
type: "result",
|
|
174
234
|
text: message.errors.join("\n"),
|
|
175
235
|
costUsd: message.total_cost_usd,
|
|
176
|
-
isError: true
|
|
236
|
+
isError: true,
|
|
237
|
+
outcome: "failed",
|
|
238
|
+
turns: message.num_turns,
|
|
239
|
+
...ctx.lastFailure ? { failure: ctx.lastFailure } : {}
|
|
177
240
|
};
|
|
178
241
|
}
|
|
179
242
|
return;
|
|
@@ -206,6 +269,12 @@ function buildOptions(input, resolved) {
|
|
|
206
269
|
// path, so the subprocess cannot prefer an API key over the login.
|
|
207
270
|
env: resolved.subprocessEnv
|
|
208
271
|
};
|
|
272
|
+
if (input.systemPrompt !== void 0) {
|
|
273
|
+
options.systemPrompt = input.systemPrompt;
|
|
274
|
+
}
|
|
275
|
+
if (input.maxTurns !== void 0) {
|
|
276
|
+
options.maxTurns = input.maxTurns;
|
|
277
|
+
}
|
|
209
278
|
if (profile.disallowedTools) {
|
|
210
279
|
options.disallowedTools = profile.disallowedTools;
|
|
211
280
|
}
|
|
@@ -261,14 +330,17 @@ function createClaudeHarness(deps) {
|
|
|
261
330
|
const notify = input.onNotice ?? ((message) => console.error(message));
|
|
262
331
|
notify(resolved.notice);
|
|
263
332
|
const queryFn = deps?.queryFn ?? await loadSdkQuery(deps?.sdkLoader);
|
|
264
|
-
const
|
|
333
|
+
const ctx = {
|
|
334
|
+
expectStructuredOutput: input.outputSchema !== void 0,
|
|
335
|
+
partialText: ""
|
|
336
|
+
};
|
|
265
337
|
try {
|
|
266
338
|
const stream = queryFn({ prompt: input.prompt, options: buildOptions(input, resolved) });
|
|
267
339
|
for await (const message of stream) {
|
|
268
340
|
if (message.type === "system" && message.subtype === "init") {
|
|
269
341
|
verifyBilling(resolved, message.apiKeySource, notify);
|
|
270
342
|
}
|
|
271
|
-
yield* mapMessage(message,
|
|
343
|
+
yield* mapMessage(message, ctx);
|
|
272
344
|
}
|
|
273
345
|
} catch (error) {
|
|
274
346
|
if (isSdkAbsence(error)) throw new Error(SDK_UNAVAILABLE_MESSAGE, { cause: error });
|
|
@@ -334,6 +406,17 @@ function lifeOSProfile(identity, mcpServers) {
|
|
|
334
406
|
}
|
|
335
407
|
return profile;
|
|
336
408
|
}
|
|
409
|
+
function readMostlyProfile(identity) {
|
|
410
|
+
return {
|
|
411
|
+
allowedTools: ["Read", "Grep", "Glob", "WebFetch", "WebSearch"],
|
|
412
|
+
gatedTools: [...SIDE_EFFECT_GATED_TOOLS],
|
|
413
|
+
allowGated: false,
|
|
414
|
+
blockedBashPatterns: blockedBashPatterns(),
|
|
415
|
+
permissionMode: "default",
|
|
416
|
+
settingSources: [],
|
|
417
|
+
identity
|
|
418
|
+
};
|
|
419
|
+
}
|
|
337
420
|
|
|
338
421
|
// server/cli.ts
|
|
339
422
|
import "ws";
|
|
@@ -964,9 +1047,12 @@ ${refContext}` : "");
|
|
|
964
1047
|
let resultError = false;
|
|
965
1048
|
try {
|
|
966
1049
|
for await (const ev of harness.run({
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1050
|
+
// System instructions ride their OWN channel — the harness
|
|
1051
|
+
// forwards them to the SDK's system-prompt option. They are
|
|
1052
|
+
// never joined to the prompt (they used to be, because
|
|
1053
|
+
// HarnessRunInput had nowhere else to put them).
|
|
1054
|
+
systemPrompt: system,
|
|
1055
|
+
prompt,
|
|
970
1056
|
cwd: refRoot ?? process.cwd(),
|
|
971
1057
|
session: workspace?.changeName ?? "specsketch-generate",
|
|
972
1058
|
profile: generateProfile,
|
|
@@ -1040,9 +1126,7 @@ ${prompt}`,
|
|
|
1040
1126
|
const refContext = renderReferenceContext(refs);
|
|
1041
1127
|
const history = conversation.map((m) => `[${m.role === "user" ? "user" : "assistant"}]
|
|
1042
1128
|
${m.text}`).join("\n\n");
|
|
1043
|
-
const prompt = `${
|
|
1044
|
-
|
|
1045
|
-
${renderBundleContext(bundle)}` + (history ? `
|
|
1129
|
+
const prompt = `${renderBundleContext(bundle)}` + (history ? `
|
|
1046
1130
|
|
|
1047
1131
|
Conversation so far:
|
|
1048
1132
|
|
|
@@ -1059,6 +1143,7 @@ ${refContext}` : "");
|
|
|
1059
1143
|
let resultError = false;
|
|
1060
1144
|
try {
|
|
1061
1145
|
for await (const ev of harness.run({
|
|
1146
|
+
systemPrompt: CHAT_SYSTEM,
|
|
1062
1147
|
prompt,
|
|
1063
1148
|
cwd: refRoot ?? process.cwd(),
|
|
1064
1149
|
session: workspace?.changeName ?? bundle.changeName ?? "specsketch-review",
|
|
@@ -2150,9 +2235,8 @@ var ChangeWorkspace = class {
|
|
|
2150
2235
|
};
|
|
2151
2236
|
|
|
2152
2237
|
// server/workbench-host.ts
|
|
2153
|
-
import { existsSync as
|
|
2154
|
-
import {
|
|
2155
|
-
import { dirname as dirname5, join as join10, resolve as resolve3 } from "node:path";
|
|
2238
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
2239
|
+
import { dirname as dirname5, join as join11, resolve as resolve3 } from "node:path";
|
|
2156
2240
|
|
|
2157
2241
|
// src/workbench/core/capability-store.ts
|
|
2158
2242
|
import { readFileSync as readFileSync4, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
@@ -2285,7 +2369,7 @@ var SPEC_FILE = "spec.md";
|
|
|
2285
2369
|
function validateCapabilityName(name) {
|
|
2286
2370
|
if (name.length === 0) return "a capability name cannot be empty";
|
|
2287
2371
|
if (!CAPABILITY_NAME.test(name)) {
|
|
2288
|
-
return `\`${name}\` is not a capability name: expected lowercase kebab-case, e.g. \`specsketch-
|
|
2372
|
+
return `\`${name}\` is not a capability name: expected lowercase kebab-case, e.g. \`specsketch-workbench-host\``;
|
|
2289
2373
|
}
|
|
2290
2374
|
return null;
|
|
2291
2375
|
}
|
|
@@ -2332,1567 +2416,892 @@ function createFoldedSpecReader(repoRoot) {
|
|
|
2332
2416
|
return Object.freeze({ specsDir, listCapabilities, readCapability, openCapability });
|
|
2333
2417
|
}
|
|
2334
2418
|
|
|
2335
|
-
// src/workbench/
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2419
|
+
// src/workbench/core/citation.ts
|
|
2420
|
+
var REQUIREMENT_HEADING2 = /^###[ \t]+Requirement:[ \t]*(.+?)[ \t]*$/gm;
|
|
2421
|
+
function citableRequirements(specs) {
|
|
2422
|
+
return specs.flatMap(
|
|
2423
|
+
(spec) => [...spec.content.matchAll(REQUIREMENT_HEADING2)].map((match) => ({
|
|
2424
|
+
capability: spec.capability,
|
|
2425
|
+
key: match[1]
|
|
2426
|
+
}))
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2340
2429
|
|
|
2341
|
-
// src/workbench/
|
|
2342
|
-
var
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2430
|
+
// src/workbench/agent/credentials.ts
|
|
2431
|
+
var CREDENTIAL_SOURCES = [
|
|
2432
|
+
"subscription",
|
|
2433
|
+
"api-key",
|
|
2434
|
+
"ant-auth"
|
|
2435
|
+
];
|
|
2436
|
+
var CREDENTIAL_LABELS = {
|
|
2437
|
+
subscription: "Claude Code subscription login",
|
|
2438
|
+
"api-key": "ANTHROPIC_API_KEY",
|
|
2439
|
+
"ant-auth": "ant auth token"
|
|
2440
|
+
};
|
|
2441
|
+
var CREDENTIAL_BILLS = {
|
|
2442
|
+
subscription: "bills your Max/Pro subscription",
|
|
2443
|
+
"api-key": "bills API credits",
|
|
2444
|
+
"ant-auth": "bills API credits, NOT your subscription"
|
|
2445
|
+
};
|
|
2446
|
+
function unavailableDetail(id, reason) {
|
|
2447
|
+
switch (reason) {
|
|
2448
|
+
case "not-installed":
|
|
2449
|
+
return id === "subscription" ? "Claude Code is not installed on this machine" : id === "api-key" ? "ANTHROPIC_API_KEY is not set in this environment" : "no ant auth token or credentials profile is present";
|
|
2450
|
+
case "installed-not-authenticated":
|
|
2451
|
+
return id === "subscription" ? "Claude Code is installed but not logged in \u2014 run `claude login`" : id === "api-key" ? "ANTHROPIC_API_KEY is set but empty" : "an ant auth profile exists but holds no usable token \u2014 run `ant auth login`";
|
|
2452
|
+
case "present-but-rejected":
|
|
2453
|
+
return "a credential is present but the last run was rejected \u2014 investigate before reusing it";
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
function sourceRow(id, status) {
|
|
2352
2457
|
return {
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
components: { state: "empty" },
|
|
2358
|
-
code: { state: "empty" },
|
|
2359
|
-
impact: { state: "empty" }
|
|
2458
|
+
id,
|
|
2459
|
+
label: CREDENTIAL_LABELS[id],
|
|
2460
|
+
status,
|
|
2461
|
+
detail: status.kind === "available" ? CREDENTIAL_BILLS[id] : unavailableDetail(id, status.reason)
|
|
2360
2462
|
};
|
|
2361
2463
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
return groups.map((group, g) => ({
|
|
2368
|
-
...group,
|
|
2369
|
-
number: `${g + 1}`,
|
|
2370
|
-
title: group.title,
|
|
2371
|
-
tasks: group.tasks.map((task, t) => ({ ...task, number: `${g + 1}.${t + 1}` }))
|
|
2372
|
-
}));
|
|
2373
|
-
}
|
|
2374
|
-
var SCENARIO_HEADING = /^####[ \t]+Scenario:[ \t]*(.*)$/;
|
|
2375
|
-
var ANY_HEADING = /^#{1,6}[ \t]/;
|
|
2376
|
-
function probe2(line) {
|
|
2377
|
-
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
2378
|
-
}
|
|
2379
|
-
function parseScenarios(block) {
|
|
2380
|
-
const scenarios = [];
|
|
2381
|
-
let open = null;
|
|
2382
|
-
for (const line of block.bodyLines) {
|
|
2383
|
-
const p = probe2(line);
|
|
2384
|
-
const heading = SCENARIO_HEADING.exec(p);
|
|
2385
|
-
if (heading) {
|
|
2386
|
-
if (open) scenarios.push(open);
|
|
2387
|
-
open = { name: (heading[1] ?? "").trim(), steps: [] };
|
|
2388
|
-
continue;
|
|
2464
|
+
function resolveRailCredential(machine, selection) {
|
|
2465
|
+
if (selection !== null) {
|
|
2466
|
+
const row = machine.rows.find((r) => r.id === selection);
|
|
2467
|
+
if (row && row.status.kind === "available") {
|
|
2468
|
+
return { kind: "in-use", source: selection, reason: { kind: "explicit-selection" } };
|
|
2389
2469
|
}
|
|
2390
|
-
if (
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2470
|
+
if (machine.defaultPick !== null) {
|
|
2471
|
+
return {
|
|
2472
|
+
kind: "in-use",
|
|
2473
|
+
source: machine.defaultPick,
|
|
2474
|
+
reason: { kind: "selection-unavailable", selected: selection }
|
|
2475
|
+
};
|
|
2394
2476
|
}
|
|
2395
|
-
|
|
2477
|
+
return { kind: "no-model", droppedSelection: selection };
|
|
2478
|
+
}
|
|
2479
|
+
if (machine.defaultPick !== null) {
|
|
2480
|
+
return { kind: "in-use", source: machine.defaultPick, reason: { kind: "default-precedence" } };
|
|
2396
2481
|
}
|
|
2397
|
-
|
|
2398
|
-
return scenarios;
|
|
2482
|
+
return { kind: "no-model", droppedSelection: null };
|
|
2399
2483
|
}
|
|
2400
|
-
function
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
if (SCENARIO_HEADING.test(p) || ANY_HEADING.test(p)) break;
|
|
2405
|
-
prose.push(p);
|
|
2406
|
-
}
|
|
2407
|
-
return prose.join("\n").trim();
|
|
2484
|
+
function sourceFromApiKeySource(apiKeySource) {
|
|
2485
|
+
if (apiKeySource === "none") return "subscription";
|
|
2486
|
+
if (apiKeySource === "oauth") return "ant-auth";
|
|
2487
|
+
return "api-key";
|
|
2408
2488
|
}
|
|
2409
|
-
function
|
|
2410
|
-
|
|
2489
|
+
function servedSourceFromRun(selected, apiKeySource) {
|
|
2490
|
+
if (apiKeySource === void 0) return { kind: "unobserved" };
|
|
2491
|
+
const serving = sourceFromApiKeySource(apiKeySource);
|
|
2492
|
+
return serving === selected ? { kind: "served", source: serving } : { kind: "diverged", selected, serving };
|
|
2411
2493
|
}
|
|
2412
|
-
function
|
|
2413
|
-
|
|
2494
|
+
function servedSentence(served) {
|
|
2495
|
+
switch (served.kind) {
|
|
2496
|
+
case "served":
|
|
2497
|
+
return `served by ${CREDENTIAL_LABELS[served.source]} \u2014 ${CREDENTIAL_BILLS[served.source]}`;
|
|
2498
|
+
case "diverged":
|
|
2499
|
+
return `served by ${CREDENTIAL_LABELS[served.serving]}, NOT the selected ${CREDENTIAL_LABELS[served.selected]} \u2014 ${CREDENTIAL_BILLS[served.serving]}`;
|
|
2500
|
+
case "unobserved":
|
|
2501
|
+
return "the run carried no billing signal \u2014 which credential served this turn is unknown";
|
|
2502
|
+
case "fixture":
|
|
2503
|
+
return "answered by the workbench fixture \u2014 no model served this turn";
|
|
2504
|
+
}
|
|
2414
2505
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2506
|
+
|
|
2507
|
+
// src/workbench/agent/profile.ts
|
|
2508
|
+
var AGENT_RAIL_IDENTITY = "specsketch-workbench-agent";
|
|
2509
|
+
function agentRailProfile() {
|
|
2510
|
+
return readMostlyProfile(AGENT_RAIL_IDENTITY);
|
|
2417
2511
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
}
|
|
2512
|
+
|
|
2513
|
+
// src/workbench/agent/credential-probe.ts
|
|
2514
|
+
import { existsSync as existsSync5, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
2515
|
+
import { homedir as homedir3 } from "node:os";
|
|
2516
|
+
import { delimiter, join as join7 } from "node:path";
|
|
2517
|
+
function claudeOnPath(env) {
|
|
2518
|
+
const path = env["PATH"];
|
|
2519
|
+
if (path === void 0 || path === "") return false;
|
|
2520
|
+
for (const dir of path.split(delimiter)) {
|
|
2521
|
+
if (dir === "") continue;
|
|
2522
|
+
try {
|
|
2523
|
+
if (statSync3(join7(dir, "claude")).isFile()) return true;
|
|
2524
|
+
} catch {
|
|
2432
2525
|
}
|
|
2433
2526
|
}
|
|
2434
|
-
return
|
|
2527
|
+
return false;
|
|
2435
2528
|
}
|
|
2436
|
-
function
|
|
2437
|
-
const
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2529
|
+
function antAuthProfile(home) {
|
|
2530
|
+
const dir = join7(home, ".config", "anthropic", "credentials");
|
|
2531
|
+
try {
|
|
2532
|
+
return readdirSync6(dir).some((f) => f.endsWith(".json")) ? "token" : "empty-profile";
|
|
2533
|
+
} catch {
|
|
2534
|
+
return "absent";
|
|
2442
2535
|
}
|
|
2443
|
-
return citable;
|
|
2444
2536
|
}
|
|
2445
|
-
function
|
|
2446
|
-
if (
|
|
2447
|
-
|
|
2448
|
-
|
|
2537
|
+
function subscriptionStatus(home, installed) {
|
|
2538
|
+
if (existsSync5(join7(home, ".claude", ".credentials.json"))) return { kind: "available" };
|
|
2539
|
+
return {
|
|
2540
|
+
kind: "unavailable",
|
|
2541
|
+
reason: installed ? "installed-not-authenticated" : "not-installed"
|
|
2542
|
+
};
|
|
2449
2543
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2544
|
+
function apiKeyStatus(env) {
|
|
2545
|
+
const value = env["ANTHROPIC_API_KEY"];
|
|
2546
|
+
if (value === void 0) return { kind: "unavailable", reason: "not-installed" };
|
|
2547
|
+
if (value === "") return { kind: "unavailable", reason: "installed-not-authenticated" };
|
|
2548
|
+
return { kind: "available" };
|
|
2549
|
+
}
|
|
2550
|
+
function antAuthStatus(env, home) {
|
|
2551
|
+
const token = env["ANTHROPIC_AUTH_TOKEN"];
|
|
2552
|
+
if (token !== void 0 && token !== "") return { kind: "available" };
|
|
2553
|
+
switch (antAuthProfile(home)) {
|
|
2554
|
+
case "token":
|
|
2555
|
+
return { kind: "available" };
|
|
2556
|
+
case "empty-profile":
|
|
2557
|
+
return { kind: "unavailable", reason: "installed-not-authenticated" };
|
|
2558
|
+
case "absent":
|
|
2559
|
+
return {
|
|
2560
|
+
kind: "unavailable",
|
|
2561
|
+
reason: token === "" ? "installed-not-authenticated" : "not-installed"
|
|
2562
|
+
};
|
|
2462
2563
|
}
|
|
2463
|
-
const text = body.join("\n").trim();
|
|
2464
|
-
return text === "" ? null : text;
|
|
2465
2564
|
}
|
|
2466
|
-
function
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
// body edit the author made; refusing would make a legitimate edit and a
|
|
2476
|
-
// legitimate rename mutually exclusive. See `docs/specsketch-workbench-issues.md` #8.
|
|
2477
|
-
modified: deltas.filter((d) => d.operation === "MODIFIED" || d.operation === "RENAMED" && d.bodyChanged).map((d) => blockText(d)),
|
|
2478
|
-
removed: deltas.filter((d) => d.operation === "REMOVED").map((d) => d.base.name),
|
|
2479
|
-
renamed: deltas.filter((d) => d.operation === "RENAMED").map((d) => ({ from: d.base.name, to: d.block.name }))
|
|
2565
|
+
function probeMachineCredentials(input = {}) {
|
|
2566
|
+
const env = input.env ?? process.env;
|
|
2567
|
+
const home = input.home ?? homedir3();
|
|
2568
|
+
const installed = input.claudeInstalled ?? claudeOnPath(env);
|
|
2569
|
+
const rejected = new Set(input.rejected ?? []);
|
|
2570
|
+
const statuses = {
|
|
2571
|
+
subscription: subscriptionStatus(home, installed),
|
|
2572
|
+
"api-key": apiKeyStatus(env),
|
|
2573
|
+
"ant-auth": antAuthStatus(env, home)
|
|
2480
2574
|
};
|
|
2575
|
+
for (const id of CREDENTIAL_SOURCES) {
|
|
2576
|
+
if (rejected.has(id) && statuses[id].kind === "available") {
|
|
2577
|
+
statuses[id] = { kind: "unavailable", reason: "present-but-rejected" };
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
const rows = CREDENTIAL_SOURCES.map((id) => sourceRow(id, statuses[id]));
|
|
2581
|
+
return { rows, defaultPick: defaultPick(env, home, statuses) };
|
|
2582
|
+
}
|
|
2583
|
+
function defaultPick(env, home, statuses) {
|
|
2584
|
+
try {
|
|
2585
|
+
const pick = resolveModelCredential({ env, home }).path;
|
|
2586
|
+
return statuses[pick].kind === "available" ? pick : null;
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
if (error instanceof ModelAuthError) return null;
|
|
2589
|
+
throw error;
|
|
2590
|
+
}
|
|
2481
2591
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
return
|
|
2592
|
+
|
|
2593
|
+
// src/workbench/agent/live-run.ts
|
|
2594
|
+
function harnessInputsFor(source, env) {
|
|
2595
|
+
if (source === "subscription") return { env, useApi: false };
|
|
2596
|
+
if (source === "api-key") return { env, useApi: true };
|
|
2597
|
+
const withheld = { ...env };
|
|
2598
|
+
delete withheld["ANTHROPIC_API_KEY"];
|
|
2599
|
+
return { env: withheld, useApi: true };
|
|
2486
2600
|
}
|
|
2487
|
-
function
|
|
2601
|
+
async function runRailTurn(input, deps = {}) {
|
|
2602
|
+
const env = deps.env ?? process.env;
|
|
2603
|
+
const machine = deps.machine ?? probeMachineCredentials({
|
|
2604
|
+
env,
|
|
2605
|
+
...deps.home !== void 0 ? { home: deps.home } : {},
|
|
2606
|
+
...deps.probe ?? {}
|
|
2607
|
+
});
|
|
2608
|
+
const credential = resolveRailCredential(machine, input.selection ?? null);
|
|
2609
|
+
if (credential.kind === "no-model") return { kind: "no-model", credential };
|
|
2610
|
+
const inputs = harnessInputsFor(credential.source, env);
|
|
2611
|
+
const harness = (deps.createHarness ?? ((d) => createClaudeHarness(d)))({
|
|
2612
|
+
env: inputs.env,
|
|
2613
|
+
home: deps.home
|
|
2614
|
+
});
|
|
2615
|
+
const runInput = {
|
|
2616
|
+
prompt: input.prompt,
|
|
2617
|
+
cwd: input.cwd,
|
|
2618
|
+
session: input.session,
|
|
2619
|
+
// Read-only by construction — the live run carries the SAME profile the
|
|
2620
|
+
// canned rail always declared (issue #53).
|
|
2621
|
+
profile: agentRailProfile(),
|
|
2622
|
+
useApi: inputs.useApi,
|
|
2623
|
+
...input.systemPrompt !== void 0 ? { systemPrompt: input.systemPrompt } : {},
|
|
2624
|
+
...input.maxTurns !== void 0 ? { maxTurns: input.maxTurns } : {},
|
|
2625
|
+
...input.outputSchema !== void 0 ? { outputSchema: input.outputSchema } : {},
|
|
2626
|
+
...deps.onNotice ? { onNotice: deps.onNotice } : {}
|
|
2627
|
+
};
|
|
2628
|
+
let apiKeySource;
|
|
2629
|
+
let answer = "";
|
|
2630
|
+
let isError = false;
|
|
2631
|
+
let outcome;
|
|
2632
|
+
let turns;
|
|
2633
|
+
let failure;
|
|
2634
|
+
let structuredOutput;
|
|
2635
|
+
let sawStructuredOutput = false;
|
|
2636
|
+
for await (const event of harness.run(runInput)) {
|
|
2637
|
+
if (event.type === "start") {
|
|
2638
|
+
apiKeySource = event.apiKeySource;
|
|
2639
|
+
} else if (event.type === "result") {
|
|
2640
|
+
answer = event.text;
|
|
2641
|
+
isError = event.isError;
|
|
2642
|
+
outcome = event.outcome;
|
|
2643
|
+
turns = event.turns;
|
|
2644
|
+
failure = event.failure;
|
|
2645
|
+
if ("structuredOutput" in event) {
|
|
2646
|
+
structuredOutput = event.structuredOutput;
|
|
2647
|
+
sawStructuredOutput = true;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2488
2651
|
return {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2652
|
+
kind: "turn",
|
|
2653
|
+
turn: {
|
|
2654
|
+
answer,
|
|
2655
|
+
served: servedSourceFromRun(credential.source, apiKeySource),
|
|
2656
|
+
selected: credential.source,
|
|
2657
|
+
isError,
|
|
2658
|
+
...outcome !== void 0 ? { outcome } : {},
|
|
2659
|
+
...turns !== void 0 ? { turns } : {},
|
|
2660
|
+
...failure !== void 0 ? { failure } : {},
|
|
2661
|
+
...sawStructuredOutput ? { structuredOutput } : {}
|
|
2662
|
+
}
|
|
2496
2663
|
};
|
|
2497
2664
|
}
|
|
2498
|
-
var SKIPPED_MARKER = "_Skipped._";
|
|
2499
|
-
var UNSATISFIED_MARKER = "_Unsatisfied \u2014 this section has no body and no recorded skip._";
|
|
2500
|
-
function renderProposalSection(spec, section2) {
|
|
2501
|
-
const heading = `${"#".repeat(spec.level)} ${spec.title}`;
|
|
2502
|
-
switch (section2.state) {
|
|
2503
|
-
case "authored":
|
|
2504
|
-
return `${heading}
|
|
2505
|
-
|
|
2506
|
-
${section2.body}
|
|
2507
|
-
`;
|
|
2508
|
-
case "skipped":
|
|
2509
|
-
return `${heading}
|
|
2510
|
-
|
|
2511
|
-
${SKIPPED_MARKER} ${section2.reason.trim()}
|
|
2512
|
-
`;
|
|
2513
|
-
case "empty":
|
|
2514
|
-
return `${heading}
|
|
2515
2665
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2666
|
+
// src/workbench/agent/tools.ts
|
|
2667
|
+
import { z as z5 } from "zod";
|
|
2668
|
+
var ROOT_ARTIFACTS = ["proposal.md", "design.md", "tasks.md"];
|
|
2669
|
+
var SPEC_ARTIFACT = /^specs\/([a-z0-9]+(?:-[a-z0-9]+)*)\/spec\.md$/;
|
|
2670
|
+
var AGENT_PROPOSAL_TOOLS = [
|
|
2671
|
+
{
|
|
2672
|
+
name: "edit_artifact",
|
|
2673
|
+
description: "Propose a markdown edit to one artifact of this change. `artifact` is one of `proposal.md`, `design.md`, `tasks.md`, or `specs/<capability>/spec.md` \u2014 it must be an artifact the change already has. `text` is the markdown, exactly as it should sit in the file. `mode` is `replace` for the whole file or `append` to add at the end."
|
|
2674
|
+
},
|
|
2675
|
+
{
|
|
2676
|
+
name: "add_task_group",
|
|
2677
|
+
description: "Propose a group of implementation tasks, appended to `tasks.md`. `title` and `tasks` are required; one group per coherent area, each task a single concrete action. `capability` + `requirement` are optional and go together \u2014 name both to cite the requirement the group implements, or neither for a chore group."
|
|
2678
|
+
},
|
|
2679
|
+
{
|
|
2680
|
+
name: "add_requirement",
|
|
2681
|
+
description: "Propose a requirement, appended to a delta spec. `capability` must be one the change already has a delta spec for. `block` must be full markdown: a `### Requirement:` heading, RFC-2119 SHALL text, and at least one `#### Scenario:` with WHEN/THEN steps."
|
|
2518
2682
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
`)
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2683
|
+
];
|
|
2684
|
+
var proposalCall = z5.object({
|
|
2685
|
+
tool: z5.enum(AGENT_PROPOSAL_TOOLS.map((t) => t.name)).describe("Which kind of edit to propose."),
|
|
2686
|
+
artifact: z5.string().optional().describe("edit_artifact: the artifact this edit is against."),
|
|
2687
|
+
mode: z5.string().optional().describe("edit_artifact: `replace` or `append`. Defaults to append."),
|
|
2688
|
+
text: z5.string().optional().describe("edit_artifact: the markdown, exactly as it should read."),
|
|
2689
|
+
title: z5.string().optional().describe("add_task_group: the group heading."),
|
|
2690
|
+
tasks: z5.array(z5.string()).optional().describe("add_task_group: task titles, one per task, each one concrete action."),
|
|
2691
|
+
capability: z5.string().optional().describe("add_requirement: the delta spec\u2019s capability. add_task_group: the citation\u2019s."),
|
|
2692
|
+
requirement: z5.string().optional().describe("add_task_group: the exact requirement name, as it appears after `### Requirement:`."),
|
|
2693
|
+
block: z5.string().optional().describe("add_requirement: the complete requirement block, in markdown.")
|
|
2694
|
+
});
|
|
2695
|
+
var agentTurnResult = z5.object({
|
|
2696
|
+
answer: z5.string().describe("Brief prose for the author: what you proposed and why, or your answer if nothing."),
|
|
2697
|
+
proposals: z5.array(proposalCall).describe("The edits to propose. Empty when the turn only answers.")
|
|
2698
|
+
});
|
|
2699
|
+
var AGENT_TURN_SCHEMA = z5.toJSONSchema(agentTurnResult, {
|
|
2700
|
+
target: "draft-7"
|
|
2701
|
+
});
|
|
2702
|
+
function knownArtifacts(shape) {
|
|
2703
|
+
if (shape.artifacts !== void 0) return shape.artifacts;
|
|
2704
|
+
if (shape.buffers !== void 0) return Object.keys(shape.buffers);
|
|
2705
|
+
if (shape.openCapabilities !== void 0) {
|
|
2706
|
+
return [...ROOT_ARTIFACTS, ...shape.openCapabilities.map((c) => `specs/${c}/spec.md`)];
|
|
2529
2707
|
}
|
|
2530
|
-
return
|
|
2708
|
+
return null;
|
|
2531
2709
|
}
|
|
2532
|
-
function
|
|
2533
|
-
const
|
|
2534
|
-
|
|
2535
|
-
for (const block of doc.design) {
|
|
2536
|
-
const title = (block.title ?? "").trim();
|
|
2537
|
-
const heading = title === "" ? "" : `## ${title}
|
|
2538
|
-
|
|
2539
|
-
`;
|
|
2540
|
-
if (block.kind === "markdown") {
|
|
2541
|
-
parts.push(`${heading}${block.text.trim()}
|
|
2542
|
-
`);
|
|
2543
|
-
continue;
|
|
2544
|
-
}
|
|
2545
|
-
const info = block.kind === "mermaid" ? DESIGN_FENCES.mermaid : DESIGN_FENCES.sketch;
|
|
2546
|
-
parts.push(`${heading}\`\`\`${info}
|
|
2547
|
-
${block.text.trim()}
|
|
2548
|
-
\`\`\`
|
|
2549
|
-
`);
|
|
2550
|
-
}
|
|
2551
|
-
return parts.join("\n");
|
|
2710
|
+
function changeHas(shape, artifact) {
|
|
2711
|
+
const known = knownArtifacts(shape);
|
|
2712
|
+
return known === null ? true : known.includes(artifact);
|
|
2552
2713
|
}
|
|
2553
|
-
function
|
|
2554
|
-
return text.replace(
|
|
2714
|
+
function stripLeadingNumber(text) {
|
|
2715
|
+
return text.replace(/^\s*\d+(?:\.\d+)*[.)]?\s+/, "").trim();
|
|
2555
2716
|
}
|
|
2556
|
-
function
|
|
2557
|
-
|
|
2717
|
+
function renderTaskGroup(title, tasks, cites) {
|
|
2718
|
+
const heading = cites ? `## ${title} \u2014 implements \`${cites.capability}\`: \`${cites.key}\`` : `## ${title}`;
|
|
2719
|
+
return [heading, "", ...tasks.map((task) => `- [ ] ${task}`)].join("\n");
|
|
2558
2720
|
}
|
|
2559
|
-
function
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2721
|
+
function proposalFromToolCall(name, args, shape) {
|
|
2722
|
+
switch (name) {
|
|
2723
|
+
case "edit_artifact": {
|
|
2724
|
+
const artifact = String(args["artifact"] ?? "").trim();
|
|
2725
|
+
const shaped = ROOT_ARTIFACTS.includes(artifact) || SPEC_ARTIFACT.test(artifact);
|
|
2726
|
+
if (!shaped) {
|
|
2727
|
+
return {
|
|
2728
|
+
kind: "refused",
|
|
2729
|
+
tool: name,
|
|
2730
|
+
missing: `\`${artifact}\` is not an artifact of an OpenSpec change`
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
if (!changeHas(shape, artifact)) {
|
|
2734
|
+
return {
|
|
2735
|
+
kind: "refused",
|
|
2736
|
+
tool: name,
|
|
2737
|
+
missing: `this change has no \`${artifact}\``
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
const text = String(args["text"] ?? "");
|
|
2741
|
+
if (text.trim() === "") {
|
|
2742
|
+
return { kind: "refused", tool: name, missing: "the markdown to write" };
|
|
2743
|
+
}
|
|
2744
|
+
const mode = String(args["mode"] ?? "").trim() === "replace" ? "replace" : "append";
|
|
2745
|
+
return { kind: "artifact-edit", artifact, mode, text, tool: name };
|
|
2570
2746
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2747
|
+
case "add_task_group": {
|
|
2748
|
+
if (!changeHas(shape, "tasks.md")) {
|
|
2749
|
+
return { kind: "refused", tool: name, missing: "this change has no `tasks.md`" };
|
|
2750
|
+
}
|
|
2751
|
+
const title = stripLeadingNumber(String(args["title"] ?? ""));
|
|
2752
|
+
const tasks = Array.isArray(args["tasks"]) ? args["tasks"].map((t) => stripLeadingNumber(String(t))).filter((t) => t !== "") : [];
|
|
2753
|
+
if (title === "" || tasks.length === 0) {
|
|
2754
|
+
return { kind: "refused", tool: name, missing: "a title and at least one task" };
|
|
2755
|
+
}
|
|
2756
|
+
const capability = String(args["capability"] ?? "").trim();
|
|
2757
|
+
const key = String(args["requirement"] ?? "").trim();
|
|
2758
|
+
if (capability === "" && key === "") {
|
|
2759
|
+
return {
|
|
2760
|
+
kind: "artifact-edit",
|
|
2761
|
+
artifact: "tasks.md",
|
|
2762
|
+
mode: "append",
|
|
2763
|
+
text: renderTaskGroup(title, tasks, null),
|
|
2764
|
+
tool: name
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
if (capability === "" || key === "") {
|
|
2768
|
+
return {
|
|
2769
|
+
kind: "refused",
|
|
2770
|
+
tool: name,
|
|
2771
|
+
missing: "both a capability and a requirement name, or neither"
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
if (shape.citable !== void 0 && !shape.citable.some((c) => c.capability === capability && c.key === key)) {
|
|
2775
|
+
return {
|
|
2776
|
+
kind: "refused",
|
|
2777
|
+
tool: name,
|
|
2778
|
+
missing: `\`${key}\` in \`${capability}\` is not a requirement this change declares`
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
const cites = { capability, key };
|
|
2782
|
+
return {
|
|
2783
|
+
kind: "artifact-edit",
|
|
2784
|
+
artifact: "tasks.md",
|
|
2785
|
+
mode: "append",
|
|
2786
|
+
text: renderTaskGroup(title, tasks, cites),
|
|
2787
|
+
tool: name,
|
|
2788
|
+
cites
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
case "add_requirement": {
|
|
2792
|
+
const capability = String(args["capability"] ?? "").trim();
|
|
2793
|
+
const artifact = `specs/${capability}/spec.md`;
|
|
2794
|
+
if (capability === "" || !SPEC_ARTIFACT.test(artifact)) {
|
|
2795
|
+
return { kind: "refused", tool: name, missing: "a capability name" };
|
|
2796
|
+
}
|
|
2797
|
+
if (!changeHas(shape, artifact)) {
|
|
2798
|
+
return {
|
|
2799
|
+
kind: "refused",
|
|
2800
|
+
tool: name,
|
|
2801
|
+
missing: `this change has no delta spec for \`${capability}\``
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
const block = String(args["block"] ?? "").trim();
|
|
2805
|
+
if (!/^###[ \t]+Requirement:/m.test(block)) {
|
|
2806
|
+
return { kind: "refused", tool: name, missing: "a `### Requirement:` heading" };
|
|
2807
|
+
}
|
|
2808
|
+
return { kind: "artifact-edit", artifact, mode: "append", text: block, tool: name };
|
|
2809
|
+
}
|
|
2810
|
+
default:
|
|
2811
|
+
return { kind: "refused", tool: name, missing: `no such tool \`${name}\`` };
|
|
2573
2812
|
}
|
|
2574
|
-
parts.push(renderTestingTable(doc.testing));
|
|
2575
|
-
return parts.join("\n");
|
|
2576
2813
|
}
|
|
2577
|
-
function
|
|
2578
|
-
const
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
for (const row of rows) {
|
|
2588
|
-
lines.push(
|
|
2589
|
-
`| ${row.index} | \`${cell(row.capability)}\` | ${cell(row.requirement)} | ${cell(row.scenario)} |`
|
|
2590
|
-
);
|
|
2591
|
-
}
|
|
2592
|
-
if (rows.length === 0) lines.push("| \u2014 | \u2014 | \u2014 | \u2014 |");
|
|
2593
|
-
return `${lines.join("\n")}
|
|
2594
|
-
`;
|
|
2814
|
+
function proposalsFromStructuredOutput(value, shape) {
|
|
2815
|
+
const parsed = agentTurnResult.safeParse(value);
|
|
2816
|
+
if (!parsed.success) return null;
|
|
2817
|
+
return {
|
|
2818
|
+
answer: parsed.data.answer,
|
|
2819
|
+
proposals: parsed.data.proposals.map((call) => {
|
|
2820
|
+
const { tool, ...args } = call;
|
|
2821
|
+
return proposalFromToolCall(tool, args, shape);
|
|
2822
|
+
})
|
|
2823
|
+
};
|
|
2595
2824
|
}
|
|
2596
|
-
function renderDeltaSpec(doc, changeId) {
|
|
2597
|
-
const parts = [`# ${doc.capability} (delta) \u2014 ${changeId}
|
|
2598
|
-
`];
|
|
2599
|
-
if (doc.purpose !== null) parts.push(`## Purpose
|
|
2600
2825
|
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
if (
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2826
|
+
// src/workbench/agent/agent-turn.ts
|
|
2827
|
+
var AGENT_TURN_MAX_TURNS = 12;
|
|
2828
|
+
function noCredentialReason(credential) {
|
|
2829
|
+
if (credential.kind !== "no-model") return "no model is available";
|
|
2830
|
+
return credential.droppedSelection === null ? "no model is available \u2014 no credential source on this machine is usable" : `no model is available \u2014 the selected ${credential.droppedSelection} source is not usable, and nothing else resolved`;
|
|
2831
|
+
}
|
|
2832
|
+
function servedLabel(served) {
|
|
2833
|
+
return servedSentence(served);
|
|
2834
|
+
}
|
|
2835
|
+
async function runAgentTurn(input, deps = {}) {
|
|
2836
|
+
const run = await runRailTurn(
|
|
2837
|
+
{
|
|
2838
|
+
prompt: input.prompt,
|
|
2839
|
+
systemPrompt: input.systemPrompt,
|
|
2840
|
+
cwd: input.cwd,
|
|
2841
|
+
session: input.session,
|
|
2842
|
+
maxTurns: AGENT_TURN_MAX_TURNS,
|
|
2843
|
+
// The turn needs structure, so it asks for it. The harness validates against
|
|
2844
|
+
// this schema and treats a "successful" run without conforming output as a
|
|
2845
|
+
// failure, which is why nothing below needs a prose fallback.
|
|
2846
|
+
outputSchema: AGENT_TURN_SCHEMA,
|
|
2847
|
+
...input.selection !== void 0 ? { selection: input.selection } : {}
|
|
2848
|
+
},
|
|
2849
|
+
deps
|
|
2850
|
+
);
|
|
2851
|
+
if (run.kind === "no-model") {
|
|
2852
|
+
return { outcome: { kind: "no-credential", reason: noCredentialReason(run.credential) }, proposals: [] };
|
|
2612
2853
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2854
|
+
const turn = run.turn;
|
|
2855
|
+
const served = servedLabel(turn.served);
|
|
2856
|
+
if (turn.outcome === "failed" || turn.outcome === void 0 && turn.isError) {
|
|
2857
|
+
return {
|
|
2858
|
+
outcome: {
|
|
2859
|
+
kind: "failed",
|
|
2860
|
+
reason: turn.failure !== void 0 ? `${turn.failure.category}${turn.failure.recognised ? "" : " (unrecognised category)"}: ${turn.answer || "the model returned an error"}` : turn.answer || "the model returned an error",
|
|
2861
|
+
served
|
|
2862
|
+
},
|
|
2863
|
+
proposals: []
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
const decoded = proposalsFromStructuredOutput(turn.structuredOutput, input.shape);
|
|
2867
|
+
if (turn.outcome === "cut_short") {
|
|
2868
|
+
return {
|
|
2869
|
+
outcome: {
|
|
2870
|
+
kind: "cut-short",
|
|
2871
|
+
answer: decoded?.answer ?? turn.answer,
|
|
2872
|
+
served,
|
|
2873
|
+
selected: turn.selected
|
|
2874
|
+
},
|
|
2875
|
+
proposals: decoded?.proposals ?? []
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
if (decoded === null) {
|
|
2879
|
+
return {
|
|
2880
|
+
outcome: {
|
|
2881
|
+
kind: "failed",
|
|
2882
|
+
reason: "the turn returned no structured output to read proposals from",
|
|
2883
|
+
served
|
|
2884
|
+
},
|
|
2885
|
+
proposals: []
|
|
2886
|
+
};
|
|
2622
2887
|
}
|
|
2623
|
-
return
|
|
2888
|
+
return {
|
|
2889
|
+
outcome: { kind: "answered", answer: decoded.answer, served, selected: turn.selected },
|
|
2890
|
+
proposals: decoded.proposals
|
|
2891
|
+
};
|
|
2624
2892
|
}
|
|
2625
|
-
function section(heading, blocks) {
|
|
2626
|
-
return `${heading}
|
|
2627
2893
|
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2894
|
+
// server/change-files.ts
|
|
2895
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2896
|
+
import {
|
|
2897
|
+
existsSync as existsSync6,
|
|
2898
|
+
mkdirSync as mkdirSync3,
|
|
2899
|
+
readFileSync as readFileSync5,
|
|
2900
|
+
readdirSync as readdirSync7,
|
|
2901
|
+
statSync as statSync4,
|
|
2902
|
+
writeFileSync as writeFileSync3
|
|
2903
|
+
} from "node:fs";
|
|
2904
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
2905
|
+
|
|
2906
|
+
// src/workbench/shell/lifecycle.ts
|
|
2907
|
+
var SPEC_DRIVEN = Object.freeze({
|
|
2908
|
+
name: "spec-driven",
|
|
2909
|
+
artifacts: Object.freeze([
|
|
2910
|
+
Object.freeze({ id: "proposal", generates: "proposal.md", requires: Object.freeze([]) }),
|
|
2911
|
+
Object.freeze({
|
|
2912
|
+
id: "specs",
|
|
2913
|
+
generates: "specs/**/*.md",
|
|
2914
|
+
requires: Object.freeze(["proposal"])
|
|
2915
|
+
}),
|
|
2916
|
+
Object.freeze({ id: "design", generates: "design.md", requires: Object.freeze(["proposal"]) }),
|
|
2917
|
+
Object.freeze({
|
|
2918
|
+
id: "tasks",
|
|
2919
|
+
generates: "tasks.md",
|
|
2920
|
+
requires: Object.freeze(["specs", "design"])
|
|
2921
|
+
})
|
|
2922
|
+
])
|
|
2923
|
+
});
|
|
2924
|
+
function parseSchemaName(yamlText) {
|
|
2925
|
+
for (const raw of yamlText.split("\n")) {
|
|
2926
|
+
const line = raw.trim();
|
|
2927
|
+
if (line.startsWith("#")) continue;
|
|
2928
|
+
const match = /^schema:\s*(?:"([^"]*)"|'([^']*)'|([^\s#]+))\s*(?:#.*)?$/.exec(line);
|
|
2929
|
+
if (!match) continue;
|
|
2930
|
+
const value = (match[1] ?? match[2] ?? match[3] ?? "").trim();
|
|
2931
|
+
return value === "" ? null : value;
|
|
2642
2932
|
}
|
|
2643
|
-
return
|
|
2933
|
+
return null;
|
|
2644
2934
|
}
|
|
2645
|
-
function
|
|
2646
|
-
|
|
2935
|
+
function resolveSchema(yamlText, known = [SPEC_DRIVEN]) {
|
|
2936
|
+
const name = parseSchemaName(yamlText);
|
|
2937
|
+
if (name === null) return { kind: "unknown-schema", name: "" };
|
|
2938
|
+
const schema = known.find((candidate) => candidate.name === name);
|
|
2939
|
+
return schema ? { kind: "resolved", schema } : { kind: "unknown-schema", name };
|
|
2647
2940
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
var GROUP_HEADING = /^## (\d+)\. (.*)$/;
|
|
2653
|
-
var FENCE = /^```(\S*)\s*$/;
|
|
2654
|
-
function parseChangeDocument(emitted) {
|
|
2655
|
-
const byPath = new Map(emitted.files.map((f) => [f.path, f.content]));
|
|
2656
|
-
const proposalSrc = byPath.get("proposal.md") ?? "";
|
|
2657
|
-
const designSrc = byPath.get("design.md") ?? "";
|
|
2658
|
-
const tasksSrc = byPath.get("tasks.md") ?? "";
|
|
2659
|
-
const parsedProposal = parseProposalFile(proposalSrc);
|
|
2660
|
-
const groups = parseTasksFile(tasksSrc);
|
|
2661
|
-
const deltaSpecs = emitted.files.filter((f) => f.path.startsWith("specs/") && f.path.endsWith("/spec.md")).map((f) => parseDeltaSpecFile(capabilityOfPath(f.path), f.content));
|
|
2662
|
-
return {
|
|
2663
|
-
id: emitted.id,
|
|
2664
|
-
proposal: parsedProposal.draft,
|
|
2665
|
-
proposalExtras: parsedProposal.extras,
|
|
2666
|
-
design: parseDesignFile(designSrc),
|
|
2667
|
-
groups: numberTasks(groups),
|
|
2668
|
-
testing: parseTestingTable(tasksSrc),
|
|
2669
|
-
deltaSpecs
|
|
2670
|
-
};
|
|
2941
|
+
|
|
2942
|
+
// server/change-files.ts
|
|
2943
|
+
function changeDirectory(repoRoot, changeId) {
|
|
2944
|
+
return join8(repoRoot, "openspec", "changes", changeId);
|
|
2671
2945
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2946
|
+
var ROOT_ARTIFACTS2 = Object.freeze(["proposal.md", "design.md", "tasks.md"]);
|
|
2947
|
+
var CAPABILITY = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2948
|
+
var SPEC_ARTIFACT2 = /^specs\/([^/]+)\/spec\.md$/;
|
|
2949
|
+
function resolveArtifact(name) {
|
|
2950
|
+
if (name === "" || name !== name.trim()) return null;
|
|
2951
|
+
if (name.includes("\\") || name.includes("\0")) return null;
|
|
2952
|
+
if (name.startsWith("/") || /^[A-Za-z]:/.test(name)) return null;
|
|
2953
|
+
if (name.split("/").some((part) => part === ".." || part === "." || part === "")) return null;
|
|
2954
|
+
if (ROOT_ARTIFACTS2.includes(name)) return name;
|
|
2955
|
+
const spec = SPEC_ARTIFACT2.exec(name);
|
|
2956
|
+
if (spec && CAPABILITY.test(spec[1])) return name;
|
|
2957
|
+
return null;
|
|
2675
2958
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
const
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
const flushExtra = () => {
|
|
2684
|
-
if (!extra) return;
|
|
2685
|
-
const text = extra.lines.join("\n").trimEnd();
|
|
2686
|
-
if (text !== "") extras.push({ afterSection: extra.afterSection, text });
|
|
2687
|
-
extra = null;
|
|
2959
|
+
var REAL_IO = { readText: (path) => readFileSync5(path, "utf8") };
|
|
2960
|
+
function stateOf(path, content) {
|
|
2961
|
+
const stat = statSync4(path);
|
|
2962
|
+
return {
|
|
2963
|
+
size: stat.size,
|
|
2964
|
+
mtimeMs: stat.mtimeMs,
|
|
2965
|
+
hash: createHash3("sha256").update(Buffer.from(content, "utf8")).digest("hex")
|
|
2688
2966
|
};
|
|
2689
|
-
for (const line of lines) {
|
|
2690
|
-
const p = probe2(line);
|
|
2691
|
-
const spec = PROPOSAL_SECTIONS.find(
|
|
2692
|
-
(s) => p === `${"#".repeat(s.level)} ${s.title}`
|
|
2693
|
-
);
|
|
2694
|
-
if (spec) {
|
|
2695
|
-
flushExtra();
|
|
2696
|
-
current = spec.id;
|
|
2697
|
-
lastFixed = spec.id;
|
|
2698
|
-
bodies.set(spec.id, []);
|
|
2699
|
-
continue;
|
|
2700
|
-
}
|
|
2701
|
-
if (ANY_HEADING.test(p)) {
|
|
2702
|
-
flushExtra();
|
|
2703
|
-
current = null;
|
|
2704
|
-
if (/^#[ \t]/.test(p) && !/^##/.test(p)) continue;
|
|
2705
|
-
extra = { afterSection: lastFixed, lines: [p] };
|
|
2706
|
-
continue;
|
|
2707
|
-
}
|
|
2708
|
-
if (current) bodies.get(current).push(p);
|
|
2709
|
-
else if (extra) extra.lines.push(p);
|
|
2710
|
-
}
|
|
2711
|
-
flushExtra();
|
|
2712
|
-
const draft = emptyProposal();
|
|
2713
|
-
for (const spec of PROPOSAL_SECTIONS) {
|
|
2714
|
-
const collected = [...bodies.get(spec.id) ?? []];
|
|
2715
|
-
if (collected[0] === "") collected.shift();
|
|
2716
|
-
if (collected.length > 0 && collected[collected.length - 1] === "") collected.pop();
|
|
2717
|
-
const body = collected.join("\n");
|
|
2718
|
-
if (body === "" || body === UNSATISFIED_MARKER) {
|
|
2719
|
-
draft[spec.id] = { state: "empty" };
|
|
2720
|
-
continue;
|
|
2721
|
-
}
|
|
2722
|
-
if (body.startsWith(SKIPPED_MARKER)) {
|
|
2723
|
-
draft[spec.id] = { state: "skipped", reason: body.slice(SKIPPED_MARKER.length).trim() };
|
|
2724
|
-
continue;
|
|
2725
|
-
}
|
|
2726
|
-
draft[spec.id] = { state: "authored", body };
|
|
2727
|
-
}
|
|
2728
|
-
return { draft, extras };
|
|
2729
2967
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
let title = null;
|
|
2736
|
-
let fence = null;
|
|
2737
|
-
const withTitle = (block) => {
|
|
2738
|
-
if (title === null) return block;
|
|
2739
|
-
const titled = { ...block, title };
|
|
2740
|
-
title = null;
|
|
2741
|
-
return titled;
|
|
2742
|
-
};
|
|
2743
|
-
const flushMarkdown = () => {
|
|
2744
|
-
const text = markdown.join("\n").trim();
|
|
2745
|
-
if (text !== "" || title !== null) blocks.push(withTitle({ kind: "markdown", text }));
|
|
2746
|
-
markdown = [];
|
|
2747
|
-
};
|
|
2748
|
-
for (const [i, line] of lines.entries()) {
|
|
2749
|
-
const p = probe2(line);
|
|
2750
|
-
const fenceMatch = FENCE.exec(p);
|
|
2751
|
-
if (fence) {
|
|
2752
|
-
if (p === "```") {
|
|
2753
|
-
const kind = fence.info === DESIGN_FENCES.mermaid ? "mermaid" : "sketch";
|
|
2754
|
-
blocks.push(withTitle({ kind, text: fence.lines.join("\n").trim() }));
|
|
2755
|
-
fence = null;
|
|
2756
|
-
continue;
|
|
2757
|
-
}
|
|
2758
|
-
fence.lines.push(p);
|
|
2759
|
-
continue;
|
|
2760
|
-
}
|
|
2761
|
-
if (fenceMatch && isDesignFence(fenceMatch[1] ?? "")) {
|
|
2762
|
-
if (markdown.join("\n").trim() !== "") flushMarkdown();
|
|
2763
|
-
markdown = [];
|
|
2764
|
-
fence = { info: fenceMatch[1], lines: [] };
|
|
2765
|
-
continue;
|
|
2766
|
-
}
|
|
2767
|
-
const heading = DESIGN_HEADING.exec(p);
|
|
2768
|
-
if (heading) {
|
|
2769
|
-
flushMarkdown();
|
|
2770
|
-
title = (heading[1] ?? "").trim();
|
|
2771
|
-
continue;
|
|
2772
|
-
}
|
|
2773
|
-
if (i === 0 && p.startsWith("# Design \u2014 ")) continue;
|
|
2774
|
-
markdown.push(p);
|
|
2968
|
+
function currentState(path) {
|
|
2969
|
+
try {
|
|
2970
|
+
return stateOf(path, readFileSync5(path, "utf8"));
|
|
2971
|
+
} catch {
|
|
2972
|
+
return null;
|
|
2775
2973
|
}
|
|
2776
|
-
flushMarkdown();
|
|
2777
|
-
return blocks;
|
|
2778
|
-
}
|
|
2779
|
-
function isDesignFence(info) {
|
|
2780
|
-
return info === DESIGN_FENCES.mermaid || info === DESIGN_FENCES.sketch;
|
|
2781
2974
|
}
|
|
2782
|
-
function
|
|
2783
|
-
const
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
} : { title: text, tasks: [] };
|
|
2797
|
-
continue;
|
|
2798
|
-
}
|
|
2799
|
-
if (/^##[ \t]/.test(p)) {
|
|
2800
|
-
if (open) groups.push(open);
|
|
2801
|
-
open = null;
|
|
2802
|
-
continue;
|
|
2803
|
-
}
|
|
2804
|
-
const task = TASK_LINE.exec(p);
|
|
2805
|
-
if (task && open) {
|
|
2806
|
-
const done = (task[1] ?? " ").toLowerCase() === "x";
|
|
2807
|
-
open.tasks.push({ title: task[3] ?? "", done });
|
|
2808
|
-
continue;
|
|
2809
|
-
}
|
|
2810
|
-
const continuation = TASK_CONTINUATION_LINE.exec(p);
|
|
2811
|
-
if (continuation && open && open.tasks.length > 0) {
|
|
2812
|
-
const last = open.tasks[open.tasks.length - 1];
|
|
2813
|
-
open.tasks[open.tasks.length - 1] = {
|
|
2814
|
-
...last,
|
|
2815
|
-
title: `${last.title}
|
|
2816
|
-
${continuation[1] ?? ""}`
|
|
2975
|
+
function readArtifact(changeDir, name, io = REAL_IO) {
|
|
2976
|
+
const artifact = resolveArtifact(name);
|
|
2977
|
+
if (artifact === null) {
|
|
2978
|
+
return { outcome: "refused", artifact: name, reason: notAnArtifact(name) };
|
|
2979
|
+
}
|
|
2980
|
+
const path = join8(changeDir, artifact);
|
|
2981
|
+
let size;
|
|
2982
|
+
try {
|
|
2983
|
+
const stat = statSync4(path);
|
|
2984
|
+
if (!stat.isFile()) {
|
|
2985
|
+
return {
|
|
2986
|
+
outcome: "unreadable",
|
|
2987
|
+
artifact,
|
|
2988
|
+
reason: `\`${artifact}\` is not a file \u2014 the change directory holds something else under that name`
|
|
2817
2989
|
};
|
|
2818
2990
|
}
|
|
2991
|
+
size = stat.size;
|
|
2992
|
+
} catch (err) {
|
|
2993
|
+
if (err.code === "ENOENT") return { outcome: "absent", artifact };
|
|
2994
|
+
return { outcome: "unreadable", artifact, reason: describeErrno(artifact, err) };
|
|
2819
2995
|
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
let inTable = false;
|
|
2826
|
-
for (const line of src.split("\n")) {
|
|
2827
|
-
const p = probe2(line);
|
|
2828
|
-
if (p.startsWith("| # | Capability |")) {
|
|
2829
|
-
inTable = true;
|
|
2830
|
-
continue;
|
|
2831
|
-
}
|
|
2832
|
-
if (!inTable) continue;
|
|
2833
|
-
if (!p.startsWith("|")) break;
|
|
2834
|
-
if (/^\|[-: |]+\|$/.test(p)) continue;
|
|
2835
|
-
const cells = splitRow(p);
|
|
2836
|
-
if (cells.length !== 4) continue;
|
|
2837
|
-
const index = Number.parseInt(cells[0], 10);
|
|
2838
|
-
if (!Number.isFinite(index)) continue;
|
|
2839
|
-
rows.push({
|
|
2840
|
-
index,
|
|
2841
|
-
capability: cells[1].replace(/^`|`$/g, ""),
|
|
2842
|
-
requirement: cells[2],
|
|
2843
|
-
scenario: cells[3]
|
|
2844
|
-
});
|
|
2845
|
-
}
|
|
2846
|
-
return rows;
|
|
2847
|
-
}
|
|
2848
|
-
function splitRow(line) {
|
|
2849
|
-
const cells = [];
|
|
2850
|
-
let buffer = "";
|
|
2851
|
-
let escaped = false;
|
|
2852
|
-
for (const char of line.slice(1, line.lastIndexOf("|"))) {
|
|
2853
|
-
if (escaped) {
|
|
2854
|
-
buffer += `\\${char}`;
|
|
2855
|
-
escaped = false;
|
|
2856
|
-
continue;
|
|
2857
|
-
}
|
|
2858
|
-
if (char === "\\") {
|
|
2859
|
-
escaped = true;
|
|
2860
|
-
continue;
|
|
2861
|
-
}
|
|
2862
|
-
if (char === "|") {
|
|
2863
|
-
cells.push(uncell(buffer.trim()));
|
|
2864
|
-
buffer = "";
|
|
2865
|
-
continue;
|
|
2866
|
-
}
|
|
2867
|
-
buffer += char;
|
|
2868
|
-
}
|
|
2869
|
-
cells.push(uncell(buffer.trim()));
|
|
2870
|
-
return cells;
|
|
2871
|
-
}
|
|
2872
|
-
var DELTA_SECTION = /^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements[ \t]*$/;
|
|
2873
|
-
var REQUIREMENT_HEADING2 = /^###[ \t]+Requirement:[ \t]*(.*)$/;
|
|
2874
|
-
var RENAME_FROM = /^-[ \t]+FROM:[ \t]*`###[ \t]*Requirement:[ \t]*(.+?)`[ \t]*$/;
|
|
2875
|
-
var RENAME_TO = /^-[ \t]+TO:[ \t]*`###[ \t]*Requirement:[ \t]*(.+?)`[ \t]*$/;
|
|
2876
|
-
function parseDeltaSpecFile(capability, src) {
|
|
2877
|
-
const added = [];
|
|
2878
|
-
const modified = [];
|
|
2879
|
-
const removed = [];
|
|
2880
|
-
const renamed = [];
|
|
2881
|
-
const purposeLines = [];
|
|
2882
|
-
let operation = null;
|
|
2883
|
-
let block = null;
|
|
2884
|
-
let pendingFrom = null;
|
|
2885
|
-
const flushBlock = () => {
|
|
2886
|
-
if (!block) return;
|
|
2887
|
-
const text = block.join("\n").trimEnd();
|
|
2888
|
-
if (operation === "ADDED") added.push(text);
|
|
2889
|
-
if (operation === "MODIFIED") modified.push(text);
|
|
2890
|
-
block = null;
|
|
2891
|
-
};
|
|
2892
|
-
for (const line of src.split("\n")) {
|
|
2893
|
-
const p = probe2(line);
|
|
2894
|
-
const sectionHeading = DELTA_SECTION.exec(p);
|
|
2895
|
-
if (sectionHeading) {
|
|
2896
|
-
flushBlock();
|
|
2897
|
-
operation = sectionHeading[1];
|
|
2898
|
-
continue;
|
|
2899
|
-
}
|
|
2900
|
-
if (PURPOSE_HEADING.test(p)) {
|
|
2901
|
-
flushBlock();
|
|
2902
|
-
operation = "PURPOSE";
|
|
2903
|
-
continue;
|
|
2904
|
-
}
|
|
2905
|
-
if (/^##(?!#)[ \t]/.test(p)) {
|
|
2906
|
-
flushBlock();
|
|
2907
|
-
operation = null;
|
|
2908
|
-
continue;
|
|
2909
|
-
}
|
|
2910
|
-
if (operation === "PURPOSE") {
|
|
2911
|
-
purposeLines.push(p);
|
|
2912
|
-
continue;
|
|
2913
|
-
}
|
|
2914
|
-
const requirementHeading = REQUIREMENT_HEADING2.exec(p);
|
|
2915
|
-
if (requirementHeading) {
|
|
2916
|
-
flushBlock();
|
|
2917
|
-
if (operation === "REMOVED") {
|
|
2918
|
-
removed.push((requirementHeading[1] ?? "").trim());
|
|
2919
|
-
continue;
|
|
2920
|
-
}
|
|
2921
|
-
if (operation === "ADDED" || operation === "MODIFIED") block = [p];
|
|
2922
|
-
continue;
|
|
2923
|
-
}
|
|
2924
|
-
if (operation === "RENAMED") {
|
|
2925
|
-
const from = RENAME_FROM.exec(p);
|
|
2926
|
-
if (from) {
|
|
2927
|
-
pendingFrom = (from[1] ?? "").trim();
|
|
2928
|
-
continue;
|
|
2929
|
-
}
|
|
2930
|
-
const to = RENAME_TO.exec(p);
|
|
2931
|
-
if (to && pendingFrom !== null) {
|
|
2932
|
-
renamed.push({ from: pendingFrom, to: (to[1] ?? "").trim() });
|
|
2933
|
-
pendingFrom = null;
|
|
2934
|
-
}
|
|
2935
|
-
continue;
|
|
2936
|
-
}
|
|
2937
|
-
if (block) block.push(p);
|
|
2996
|
+
let content;
|
|
2997
|
+
try {
|
|
2998
|
+
content = io.readText(path);
|
|
2999
|
+
} catch (err) {
|
|
3000
|
+
return { outcome: "unreadable", artifact, reason: describeErrno(artifact, err) };
|
|
2938
3001
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
added,
|
|
2945
|
-
modified,
|
|
2946
|
-
removed,
|
|
2947
|
-
renamed
|
|
2948
|
-
};
|
|
2949
|
-
}
|
|
2950
|
-
|
|
2951
|
-
// src/workbench/core/validate.ts
|
|
2952
|
-
var SHALL_OR_MUST = /\b(SHALL|MUST)\b/;
|
|
2953
|
-
function validateChange(change) {
|
|
2954
|
-
const findings = [
|
|
2955
|
-
...validateProposal(change),
|
|
2956
|
-
...validateDesign(change),
|
|
2957
|
-
...validateCapabilities(change),
|
|
2958
|
-
...validateTasks(change)
|
|
2959
|
-
];
|
|
2960
|
-
return report(findings);
|
|
2961
|
-
}
|
|
2962
|
-
function report(findings) {
|
|
2963
|
-
const errors = findings.filter((f) => f.severity === "error");
|
|
2964
|
-
const warnings = findings.filter((f) => f.severity === "warning");
|
|
2965
|
-
return { findings, errors, warnings, valid: errors.length === 0 };
|
|
2966
|
-
}
|
|
2967
|
-
function validateProposal(change) {
|
|
2968
|
-
const findings = [];
|
|
2969
|
-
for (const spec of PROPOSAL_SECTIONS) {
|
|
2970
|
-
const section2 = change.proposal[spec.id];
|
|
2971
|
-
const subject = {
|
|
2972
|
-
kind: "proposal-section",
|
|
2973
|
-
section: spec.id,
|
|
2974
|
-
title: spec.title
|
|
3002
|
+
if (content === "" && size > 0) {
|
|
3003
|
+
return {
|
|
3004
|
+
outcome: "unreadable",
|
|
3005
|
+
artifact,
|
|
3006
|
+
reason: `\`${artifact}\` holds ${size} bytes on disk but read back empty \u2014 this is a read failure, not an empty artifact. Nothing has been shown to you, so do not save over it.`
|
|
2975
3007
|
};
|
|
2976
|
-
if (section2.state === "skipped") {
|
|
2977
|
-
findings.push(...validateSkip(spec, section2.reason, subject));
|
|
2978
|
-
continue;
|
|
2979
|
-
}
|
|
2980
|
-
if (section2.state === "empty" && spec.required) {
|
|
2981
|
-
findings.push({
|
|
2982
|
-
severity: "error",
|
|
2983
|
-
kind: "proposal-section-unsatisfied",
|
|
2984
|
-
subject,
|
|
2985
|
-
message: `\`${spec.title}\` has no body and no recorded skip, and is required.`
|
|
2986
|
-
});
|
|
2987
|
-
}
|
|
2988
3008
|
}
|
|
2989
|
-
return
|
|
3009
|
+
return { outcome: "read", artifact, content, state: stateOf(path, content) };
|
|
2990
3010
|
}
|
|
2991
|
-
function
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
severity: "error",
|
|
2996
|
-
kind: "proposal-section-not-skippable",
|
|
2997
|
-
subject,
|
|
2998
|
-
message: `\`${spec.title}\` is not skippable \u2014 only the C4 levels are.`
|
|
2999
|
-
}
|
|
3000
|
-
];
|
|
3011
|
+
function listArtifacts(changeDir) {
|
|
3012
|
+
const found = [];
|
|
3013
|
+
for (const name of ROOT_ARTIFACTS2) {
|
|
3014
|
+
if (existsSync6(join8(changeDir, name))) found.push(name);
|
|
3001
3015
|
}
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
subject,
|
|
3008
|
-
message: `\`${spec.title}\` is skipped with no recorded reason; a skip requires one.`
|
|
3009
|
-
}
|
|
3010
|
-
];
|
|
3016
|
+
let capabilities = [];
|
|
3017
|
+
try {
|
|
3018
|
+
capabilities = readdirSync7(join8(changeDir, "specs"), { withFileTypes: true }).filter((e) => e.isDirectory() && CAPABILITY.test(e.name)).map((e) => e.name).sort();
|
|
3019
|
+
} catch {
|
|
3020
|
+
capabilities = [];
|
|
3011
3021
|
}
|
|
3012
|
-
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
if (change.design.length === 0) {
|
|
3016
|
-
return [
|
|
3017
|
-
{
|
|
3018
|
-
severity: "warning",
|
|
3019
|
-
kind: "design-empty",
|
|
3020
|
-
subject: { kind: "change", change: change.id },
|
|
3021
|
-
message: "`design.md` carries no blocks; the change will emit a title and nothing else."
|
|
3022
|
-
}
|
|
3023
|
-
];
|
|
3022
|
+
for (const capability of capabilities) {
|
|
3023
|
+
const artifact = `specs/${capability}/spec.md`;
|
|
3024
|
+
if (existsSync6(join8(changeDir, artifact))) found.push(artifact);
|
|
3024
3025
|
}
|
|
3025
|
-
|
|
3026
|
-
change.design.forEach((block, index) => {
|
|
3027
|
-
if ((block.title ?? "").trim() !== "") return;
|
|
3028
|
-
findings.push({
|
|
3029
|
-
severity: "error",
|
|
3030
|
-
kind: "design-block-untitled",
|
|
3031
|
-
// An untitled block has no name to be reported under, so it is named by where
|
|
3032
|
-
// it is and what it is — the two things the author can find it by.
|
|
3033
|
-
subject: { kind: "design-block", index, blockKind: block.kind },
|
|
3034
|
-
message: `Design block ${index + 1} (\`${block.kind}\`) carries no title, and a block's title is emitted as its \`##\` heading.`
|
|
3035
|
-
});
|
|
3036
|
-
});
|
|
3037
|
-
return findings;
|
|
3038
|
-
}
|
|
3039
|
-
function draftIsWholeAgain(entry) {
|
|
3040
|
-
const declared = entry.declaredRequirements ?? [];
|
|
3041
|
-
if (declared.length === 0) return false;
|
|
3042
|
-
return declared.every(
|
|
3043
|
-
(name) => new RegExp(`^###[ \\t]+Requirement:[ \\t]*${escapeForRegExp(name)}\\s*$`, "m").test(entry.draft)
|
|
3044
|
-
);
|
|
3045
|
-
}
|
|
3046
|
-
function escapeForRegExp(text) {
|
|
3047
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3026
|
+
return found;
|
|
3048
3027
|
}
|
|
3049
|
-
function
|
|
3050
|
-
const
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
if (entry.reconstructed === false && !draftIsWholeAgain(entry)) {
|
|
3054
|
-
findings.push({
|
|
3055
|
-
severity: "error",
|
|
3056
|
-
kind: "capability-draft-unreconstructed",
|
|
3057
|
-
subject: { kind: "capability", capability: entry.capability },
|
|
3058
|
-
message: `\`${entry.capability}\`'s draft could not be rebuilt from this change's delta and opened from the folded text instead${entry.reconstructionReason ? ` (${entry.reconstructionReason})` : ""}. Emitting from it would derive a delta without whatever this change had already added. Restore the draft before emitting.`
|
|
3059
|
-
});
|
|
3060
|
-
continue;
|
|
3061
|
-
}
|
|
3062
|
-
if (entry.derivation.outcome === "refused") {
|
|
3063
|
-
findings.push(...refusalFindings(entry));
|
|
3064
|
-
continue;
|
|
3065
|
-
}
|
|
3066
|
-
const deltas = deltasOf(entry);
|
|
3067
|
-
derivedDeltas += deltas.length;
|
|
3068
|
-
findings.push(...emptyDerivationFindings(entry));
|
|
3069
|
-
findings.push(...conditionFindings(entry));
|
|
3070
|
-
findings.push(...requirementFindings(entry));
|
|
3071
|
-
findings.push(...duplicateIdentityFindings(entry));
|
|
3072
|
-
}
|
|
3073
|
-
if (derivedDeltas === 0) {
|
|
3074
|
-
findings.push({
|
|
3075
|
-
severity: "error",
|
|
3076
|
-
kind: "no-delta-derived",
|
|
3077
|
-
subject: { kind: "change", change: change.id },
|
|
3078
|
-
message: "the change derives no delta at all, so it alters nothing \u2014 there is no operation to fold."
|
|
3079
|
-
});
|
|
3028
|
+
function writeArtifact(changeDir, name, content, options = {}) {
|
|
3029
|
+
const artifact = resolveArtifact(name);
|
|
3030
|
+
if (artifact === null) {
|
|
3031
|
+
return { outcome: "refused", artifact: name, reason: notAnArtifact(name) };
|
|
3080
3032
|
}
|
|
3081
|
-
|
|
3033
|
+
const path = join8(changeDir, artifact);
|
|
3034
|
+
if (options.expected !== void 0 && options.overwrite !== true) {
|
|
3035
|
+
const conflict = conflictWith(path, artifact, options.expected);
|
|
3036
|
+
if (conflict) return conflict;
|
|
3037
|
+
}
|
|
3038
|
+
const materialised = !existsSync6(changeDir);
|
|
3039
|
+
try {
|
|
3040
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
3041
|
+
writeFileSync3(path, Buffer.from(content, "utf8"));
|
|
3042
|
+
writeSchemaFile(changeDir);
|
|
3043
|
+
} catch (err) {
|
|
3044
|
+
return { outcome: "refused", artifact, reason: describeErrno(artifact, err) };
|
|
3045
|
+
}
|
|
3046
|
+
return { outcome: "written", artifact, path, state: stateOf(path, content), materialised };
|
|
3082
3047
|
}
|
|
3083
|
-
function
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
if (
|
|
3087
|
-
const keys = refusal.collisions.map((c) => `\`${c.key}\``).join(", ");
|
|
3088
|
-
return {
|
|
3089
|
-
severity: "error",
|
|
3090
|
-
kind: "duplicate-requirement-identity",
|
|
3091
|
-
subject: { kind: "capability", capability: entry.capability },
|
|
3092
|
-
message: `two requirements in the ${refusal.side} \`${entry.capability}\` claim one identity: ${keys}. One of them would derive nothing.`
|
|
3093
|
-
};
|
|
3094
|
-
}
|
|
3095
|
-
if (refusal.kind === "unterminated-fence") {
|
|
3096
|
-
return {
|
|
3097
|
-
severity: "error",
|
|
3098
|
-
kind: "capability-derivation-refused",
|
|
3099
|
-
subject: { kind: "capability", capability: entry.capability },
|
|
3100
|
-
message: `the ${refusal.side} \`${entry.capability}\` opens a fence on line ${refusal.openLine + 1} (\`${refusal.openText.trim()}\`) and never closes it, so everything after it sits inside the requirement above.`
|
|
3101
|
-
};
|
|
3102
|
-
}
|
|
3048
|
+
function conflictWith(path, artifact, expected) {
|
|
3049
|
+
const current = currentState(path);
|
|
3050
|
+
if (expected === null) {
|
|
3051
|
+
if (current === null) return null;
|
|
3103
3052
|
return {
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3053
|
+
outcome: "changed-underneath",
|
|
3054
|
+
artifact,
|
|
3055
|
+
reason: `\`${artifact}\` was created underneath the buffer \u2014 the buffer was opened on a change that did not have this file, and something has written one since. Nothing was written.`,
|
|
3056
|
+
current
|
|
3108
3057
|
};
|
|
3109
|
-
});
|
|
3110
|
-
}
|
|
3111
|
-
function emptyDerivationFindings(entry) {
|
|
3112
|
-
if (entry.derivation.outcome !== "derived") return [];
|
|
3113
|
-
if (entry.derivation.deltas.length > 0) return [];
|
|
3114
|
-
const subject = { kind: "capability", capability: entry.capability };
|
|
3115
|
-
switch (entry.derivation.foldedComparison.kind) {
|
|
3116
|
-
case "identical":
|
|
3117
|
-
return [
|
|
3118
|
-
{
|
|
3119
|
-
severity: "warning",
|
|
3120
|
-
kind: "capability-unchanged",
|
|
3121
|
-
subject,
|
|
3122
|
-
message: `\`${entry.capability}\` is open but byte-identical to its folded file; no delta spec is emitted for it.`
|
|
3123
|
-
}
|
|
3124
|
-
];
|
|
3125
|
-
case "differs-outside-requirements":
|
|
3126
|
-
return [
|
|
3127
|
-
{
|
|
3128
|
-
severity: "error",
|
|
3129
|
-
kind: "capability-changed-outside-requirements",
|
|
3130
|
-
subject,
|
|
3131
|
-
message: `\`${entry.capability}\` differs from its folded file only outside its requirements (its \`## Purpose\` or head). OpenSpec has no operation for that edit \u2014 a delta file carrying no operation cannot fold. Edit \`openspec/specs/${entry.capability}/spec.md\` directly, or revert the edit.`
|
|
3132
|
-
}
|
|
3133
|
-
];
|
|
3134
|
-
case "differs-in-requirement-order":
|
|
3135
|
-
return [
|
|
3136
|
-
{
|
|
3137
|
-
severity: "error",
|
|
3138
|
-
kind: "capability-requirements-reordered",
|
|
3139
|
-
subject,
|
|
3140
|
-
message: `\`${entry.capability}\`'s requirements are reordered and otherwise unchanged. OpenSpec has no move operation, so the reordering cannot be folded and would be lost.`
|
|
3141
|
-
}
|
|
3142
|
-
];
|
|
3143
|
-
case "no-folded-counterpart":
|
|
3144
|
-
return [
|
|
3145
|
-
{
|
|
3146
|
-
severity: "error",
|
|
3147
|
-
kind: "new-capability-declares-no-requirement",
|
|
3148
|
-
subject,
|
|
3149
|
-
message: `\`${entry.capability}\` is a new capability that declares no requirement; there is nothing to add.`
|
|
3150
|
-
}
|
|
3151
|
-
];
|
|
3152
|
-
case "differs":
|
|
3153
|
-
return [
|
|
3154
|
-
{
|
|
3155
|
-
severity: "error",
|
|
3156
|
-
kind: "capability-differs-without-delta",
|
|
3157
|
-
subject,
|
|
3158
|
-
message: `\`${entry.capability}\` differs from its folded file but derives no delta; the derivation and the comparison disagree about the same two documents.`
|
|
3159
|
-
}
|
|
3160
|
-
];
|
|
3161
|
-
}
|
|
3162
|
-
}
|
|
3163
|
-
function conditionFindings(entry) {
|
|
3164
|
-
if (entry.derivation.outcome !== "derived") return [];
|
|
3165
|
-
const findings = [];
|
|
3166
|
-
const isNew = entry.derivation.foldedComparison.kind === "no-folded-counterpart";
|
|
3167
|
-
const subject = { kind: "capability", capability: entry.capability };
|
|
3168
|
-
for (const condition of entry.derivation.conditions) {
|
|
3169
|
-
if (isNew && (condition.kind === "purpose-empty" || condition.kind === "purpose-missing")) {
|
|
3170
|
-
findings.push({
|
|
3171
|
-
severity: "error",
|
|
3172
|
-
kind: "new-capability-purpose-empty",
|
|
3173
|
-
subject,
|
|
3174
|
-
message: `new capability \`${entry.capability}\` has ${condition.kind === "purpose-missing" ? "no `## Purpose` heading" : "an empty `## Purpose`"}; it seeds the folded spec's Purpose, which cannot be left blank.`
|
|
3175
|
-
});
|
|
3176
|
-
}
|
|
3177
|
-
if (condition.kind === "identity-scheme-changed") {
|
|
3178
|
-
findings.push({
|
|
3179
|
-
severity: "warning",
|
|
3180
|
-
kind: "identity-scheme-changed",
|
|
3181
|
-
subject,
|
|
3182
|
-
message: `\`${entry.capability}\`'s folded file resolves on ${condition.foldedScheme} identity and its draft on ${condition.draftScheme}, so no key matches across them: every requirement derives as a removal plus an addition. Declare the renames if that is not what you meant.`
|
|
3183
|
-
});
|
|
3184
|
-
}
|
|
3185
3058
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
if (!block) continue;
|
|
3193
|
-
const subject = {
|
|
3194
|
-
kind: "requirement",
|
|
3195
|
-
capability: entry.capability,
|
|
3196
|
-
requirement: block.name
|
|
3059
|
+
if (current === null) {
|
|
3060
|
+
return {
|
|
3061
|
+
outcome: "changed-underneath",
|
|
3062
|
+
artifact,
|
|
3063
|
+
reason: `\`${artifact}\` has gone from disk underneath the buffer \u2014 it was deleted or the change directory was moved (\`openspec archive\` moves the whole directory). Nothing was written.`,
|
|
3064
|
+
current: null
|
|
3197
3065
|
};
|
|
3198
|
-
if (!SHALL_OR_MUST.test(requirementProse(block))) {
|
|
3199
|
-
findings.push({
|
|
3200
|
-
severity: "error",
|
|
3201
|
-
kind: "requirement-without-shall",
|
|
3202
|
-
subject,
|
|
3203
|
-
message: `\`${block.name}\` in \`${entry.capability}\` states no obligation: its text carries neither SHALL nor MUST.`
|
|
3204
|
-
});
|
|
3205
|
-
}
|
|
3206
|
-
const scenarios = parseScenarios(block);
|
|
3207
|
-
if (scenarios.length === 0) {
|
|
3208
|
-
findings.push({
|
|
3209
|
-
severity: "error",
|
|
3210
|
-
kind: "requirement-without-scenario",
|
|
3211
|
-
subject,
|
|
3212
|
-
message: `\`${block.name}\` in \`${entry.capability}\` has no \`#### Scenario:\`; a requirement is not testable without one.`
|
|
3213
|
-
});
|
|
3214
|
-
}
|
|
3215
|
-
for (const scenario of scenarios) {
|
|
3216
|
-
if (scenario.steps.length > 0) continue;
|
|
3217
|
-
findings.push({
|
|
3218
|
-
severity: "error",
|
|
3219
|
-
kind: "scenario-without-steps",
|
|
3220
|
-
subject: {
|
|
3221
|
-
kind: "scenario",
|
|
3222
|
-
capability: entry.capability,
|
|
3223
|
-
requirement: block.name,
|
|
3224
|
-
scenario: scenario.name
|
|
3225
|
-
},
|
|
3226
|
-
message: `scenario \`${scenario.name}\` of \`${block.name}\` in \`${entry.capability}\` has no steps.`
|
|
3227
|
-
});
|
|
3228
|
-
}
|
|
3229
3066
|
}
|
|
3230
|
-
return
|
|
3067
|
+
if (current.hash === expected.hash) return null;
|
|
3068
|
+
return {
|
|
3069
|
+
outcome: "changed-underneath",
|
|
3070
|
+
artifact,
|
|
3071
|
+
reason: `\`${artifact}\` changed underneath the buffer \u2014 the file on disk is no longer the one this buffer was read from. Nothing was written, and your edits are still here: choose whether to overwrite the file with the buffer or discard the buffer and reload the file.`,
|
|
3072
|
+
current
|
|
3073
|
+
};
|
|
3231
3074
|
}
|
|
3232
|
-
function
|
|
3233
|
-
const
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
const name = block ? block.name : delta.operation === "REMOVED" ? delta.base.name : delta.key;
|
|
3238
|
-
const section2 = delta.operation === "RENAMED" ? "MODIFIED" : delta.operation;
|
|
3239
|
-
const slot = `${section2} ${name}`;
|
|
3240
|
-
seen.set(slot, { name, count: (seen.get(slot)?.count ?? 0) + 1 });
|
|
3241
|
-
}
|
|
3242
|
-
for (const { name, count } of seen.values()) {
|
|
3243
|
-
if (count < 2) continue;
|
|
3244
|
-
findings.push({
|
|
3245
|
-
severity: "error",
|
|
3246
|
-
kind: "duplicate-requirement-identity",
|
|
3247
|
-
subject: { kind: "requirement", capability: entry.capability, requirement: name },
|
|
3248
|
-
message: `\`${name}\` appears ${count} times in \`${entry.capability}\`'s deltas; one of them would be invisible after the fold.`
|
|
3249
|
-
});
|
|
3250
|
-
}
|
|
3251
|
-
return findings;
|
|
3075
|
+
function writeSchemaFile(changeDir) {
|
|
3076
|
+
const path = join8(changeDir, ".openspec.yaml");
|
|
3077
|
+
if (existsSync6(path)) return;
|
|
3078
|
+
writeFileSync3(path, `schema: ${SPEC_DRIVEN.name}
|
|
3079
|
+
`, "utf8");
|
|
3252
3080
|
}
|
|
3253
|
-
function
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
const subject = {
|
|
3270
|
-
kind: "task-group",
|
|
3271
|
-
number: group.number,
|
|
3272
|
-
title: group.title
|
|
3273
|
-
};
|
|
3274
|
-
if (cites.capability.includes("`") || cites.key.includes("`") || group.title.includes("`")) {
|
|
3275
|
-
findings.push({
|
|
3276
|
-
severity: "error",
|
|
3277
|
-
kind: "task-citation-unrepresentable",
|
|
3278
|
-
subject,
|
|
3279
|
-
message: `task group ${group.number} carries a backtick in its title or citation; the emitted citation could not be read back unambiguously.`
|
|
3280
|
-
});
|
|
3281
|
-
} else if (!citable.has(`${cites.capability} ${cites.key}`)) {
|
|
3282
|
-
findings.push({
|
|
3283
|
-
severity: "error",
|
|
3284
|
-
kind: "task-cites-absent-requirement",
|
|
3285
|
-
subject,
|
|
3286
|
-
message: `task group ${group.number} cites \`${cites.key}\` in \`${cites.capability}\`, which this change no longer declares.`
|
|
3287
|
-
});
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
return findings;
|
|
3081
|
+
function notAnArtifact(name) {
|
|
3082
|
+
return `\`${name}\` is not an artifact of this change. The workbench writes exactly the OpenSpec layout \u2014 \`proposal.md\`, \`design.md\`, \`tasks.md\` and \`specs/<capability>/spec.md\` \u2014 within the one open change, and reaches nothing else.`;
|
|
3083
|
+
}
|
|
3084
|
+
function describeErrno(artifact, err) {
|
|
3085
|
+
const e = err;
|
|
3086
|
+
const code = e.code ?? "unknown";
|
|
3087
|
+
const why = {
|
|
3088
|
+
EACCES: "permission denied",
|
|
3089
|
+
EPERM: "the operation is not permitted",
|
|
3090
|
+
EROFS: "the filesystem is read-only",
|
|
3091
|
+
ENOSPC: "the filesystem is full",
|
|
3092
|
+
ENOENT: "the change directory has gone",
|
|
3093
|
+
EISDIR: "a directory is in the way",
|
|
3094
|
+
ENOTDIR: "a file is in the way of the directory it needs"
|
|
3095
|
+
};
|
|
3096
|
+
return `\`${artifact}\`: ${why[code] ?? e.message ?? String(err)} (${code})`;
|
|
3292
3097
|
}
|
|
3293
3098
|
|
|
3294
|
-
//
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
}
|
|
3305
|
-
function
|
|
3306
|
-
|
|
3099
|
+
// server/openspec-validate.ts
|
|
3100
|
+
import { spawnSync } from "node:child_process";
|
|
3101
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
3102
|
+
import { join as join9 } from "node:path";
|
|
3103
|
+
|
|
3104
|
+
// src/workbench/core/validate.ts
|
|
3105
|
+
var QUOTED_SUBJECT = /"([^"]+)"/;
|
|
3106
|
+
var SCENARIO_IN_MESSAGE = /scenario\s+"([^"]+)"/i;
|
|
3107
|
+
function severityOf(level) {
|
|
3108
|
+
return level.trim().toUpperCase() === "WARNING" ? "warning" : "error";
|
|
3109
|
+
}
|
|
3110
|
+
function capabilityOfIssuePath(path) {
|
|
3111
|
+
if (path === void 0) return null;
|
|
3112
|
+
return /(?:^|\/)([^/]+)\/spec\.md$/.exec(path)?.[1] ?? null;
|
|
3113
|
+
}
|
|
3114
|
+
function openSpecFinding(issue, changeId = "") {
|
|
3115
|
+
const capability = capabilityOfIssuePath(issue.path);
|
|
3116
|
+
const named = QUOTED_SUBJECT.exec(issue.message)?.[1];
|
|
3117
|
+
const scenario = SCENARIO_IN_MESSAGE.exec(issue.message)?.[1];
|
|
3118
|
+
let subject = { kind: "change", change: changeId };
|
|
3119
|
+
if (capability !== null && named !== void 0 && scenario !== void 0) {
|
|
3120
|
+
subject = { kind: "scenario", capability, requirement: named, scenario };
|
|
3121
|
+
} else if (capability !== null && named !== void 0) {
|
|
3122
|
+
subject = { kind: "requirement", capability, requirement: named };
|
|
3123
|
+
} else if (capability !== null) {
|
|
3124
|
+
subject = { kind: "capability", capability };
|
|
3125
|
+
}
|
|
3307
3126
|
return {
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
)
|
|
3127
|
+
severity: severityOf(issue.level),
|
|
3128
|
+
kind: "openspec-reported",
|
|
3129
|
+
subject,
|
|
3130
|
+
message: issue.message
|
|
3313
3131
|
};
|
|
3314
3132
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
var ARCHIVE_FLAGS = Object.freeze(["--json", "--yes"]);
|
|
3318
|
-
var FLAGS_APPROVAL_NEVER_PASSES = Object.freeze([
|
|
3319
|
-
"--skip-specs",
|
|
3320
|
-
"--no-validate"
|
|
3321
|
-
]);
|
|
3322
|
-
function archiveArgv(changeId) {
|
|
3323
|
-
return ["archive", changeId, ...ARCHIVE_FLAGS];
|
|
3324
|
-
}
|
|
3325
|
-
function changeDirectoryOf(repoRoot, changeId) {
|
|
3326
|
-
return join7(repoRoot, "openspec", "changes", changeId);
|
|
3133
|
+
function openSpecFindings(issues, changeId = "") {
|
|
3134
|
+
return issues.map((issue) => openSpecFinding(issue, changeId));
|
|
3327
3135
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
try {
|
|
3341
|
-
const archive = runArchive(repoRoot, change.id, options);
|
|
3342
|
-
if (archive.kind === "failed") {
|
|
3343
|
-
return {
|
|
3344
|
-
outcome: "refused-fold-failed",
|
|
3345
|
-
code: archive.code,
|
|
3346
|
-
message: archive.message,
|
|
3347
|
-
rollback: restore(repoRoot, snapshot)
|
|
3348
|
-
};
|
|
3349
|
-
}
|
|
3350
|
-
const incomplete = incompleteness(repoRoot, change.id, archive, foldedCapabilities);
|
|
3351
|
-
if (incomplete) {
|
|
3352
|
-
return {
|
|
3353
|
-
outcome: "refused-fold-incomplete",
|
|
3354
|
-
reason: incomplete,
|
|
3355
|
-
rollback: restore(repoRoot, snapshot)
|
|
3356
|
-
};
|
|
3357
|
-
}
|
|
3136
|
+
|
|
3137
|
+
// server/openspec-validate.ts
|
|
3138
|
+
var runOpenSpecValidate = (repoRoot, changeId) => {
|
|
3139
|
+
const local = join9(repoRoot, "node_modules", ".bin", "openspec");
|
|
3140
|
+
const command = existsSync7(local) ? local : "openspec";
|
|
3141
|
+
const run = spawnSync(command, ["validate", changeId, "--strict", "--json", "--no-interactive"], {
|
|
3142
|
+
cwd: repoRoot,
|
|
3143
|
+
encoding: "utf8",
|
|
3144
|
+
// A validator is not allowed to hang the surface that recomputes on every edit.
|
|
3145
|
+
timeout: 2e4
|
|
3146
|
+
});
|
|
3147
|
+
if (run.error) {
|
|
3358
3148
|
return {
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
foldedCapabilities,
|
|
3363
|
-
totals: archive.totals,
|
|
3364
|
-
warnings: report2.warnings
|
|
3149
|
+
stdout: "",
|
|
3150
|
+
stderr: "",
|
|
3151
|
+
failed: run.error.code === "ENOENT" ? `\`openspec\` could not be run from ${repoRoot} \u2014 the tool this workbench validates through is not installed there` : run.error.message
|
|
3365
3152
|
};
|
|
3366
|
-
} finally {
|
|
3367
|
-
discardSnapshot(snapshot);
|
|
3368
|
-
}
|
|
3369
|
-
}
|
|
3370
|
-
function emittedDifferences(directory, emitted) {
|
|
3371
|
-
const differing = [];
|
|
3372
|
-
for (const file of emitted.files) {
|
|
3373
|
-
const target = join7(directory, file.path);
|
|
3374
|
-
let onDisk;
|
|
3375
|
-
try {
|
|
3376
|
-
onDisk = readFileSync5(target, "utf8");
|
|
3377
|
-
} catch {
|
|
3378
|
-
differing.push(file.path);
|
|
3379
|
-
continue;
|
|
3380
|
-
}
|
|
3381
|
-
if (onDisk !== file.content) differing.push(file.path);
|
|
3382
|
-
}
|
|
3383
|
-
return differing;
|
|
3384
|
-
}
|
|
3385
|
-
var SNAPSHOT_TREES = Object.freeze(["specs", "changes"]);
|
|
3386
|
-
function takeSnapshot(repoRoot) {
|
|
3387
|
-
const directory = mkdtempSync(join7(tmpdir(), "specsketch-approve-"));
|
|
3388
|
-
const present = [];
|
|
3389
|
-
for (const tree of SNAPSHOT_TREES) {
|
|
3390
|
-
const source = join7(repoRoot, "openspec", tree);
|
|
3391
|
-
if (!existsSync5(source)) continue;
|
|
3392
|
-
cpSync(source, join7(directory, tree), { recursive: true });
|
|
3393
|
-
present.push(tree);
|
|
3394
|
-
}
|
|
3395
|
-
return { root: repoRoot, present, directory };
|
|
3396
|
-
}
|
|
3397
|
-
function restore(repoRoot, snapshot) {
|
|
3398
|
-
try {
|
|
3399
|
-
for (const tree of SNAPSHOT_TREES) {
|
|
3400
|
-
const target = join7(repoRoot, "openspec", tree);
|
|
3401
|
-
rmSync(target, { recursive: true, force: true });
|
|
3402
|
-
if (!snapshot.present.includes(tree)) continue;
|
|
3403
|
-
mkdirSync3(join7(repoRoot, "openspec"), { recursive: true });
|
|
3404
|
-
cpSync(join7(snapshot.directory, tree), target, { recursive: true });
|
|
3405
|
-
}
|
|
3406
|
-
return { kind: "restored" };
|
|
3407
|
-
} catch (error) {
|
|
3408
|
-
return { kind: "restore-failed", error: error instanceof Error ? error.message : String(error) };
|
|
3409
3153
|
}
|
|
3410
|
-
}
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
const local = join7(repoRoot, "node_modules", ".bin", "openspec");
|
|
3416
|
-
return existsSync5(local) ? local : "openspec";
|
|
3417
|
-
}
|
|
3418
|
-
var NO_TOTALS = Object.freeze({ added: 0, modified: 0, removed: 0, renamed: 0 });
|
|
3419
|
-
function runArchive(repoRoot, changeId, options) {
|
|
3420
|
-
const bin = options.openspecBin ?? resolveOpenspecBinary(repoRoot);
|
|
3421
|
-
let stdout;
|
|
3422
|
-
try {
|
|
3423
|
-
stdout = execFileSync(bin, [...archiveArgv(changeId)], {
|
|
3424
|
-
cwd: repoRoot,
|
|
3425
|
-
encoding: "utf8"
|
|
3426
|
-
});
|
|
3427
|
-
} catch (error) {
|
|
3428
|
-
const captured = error.stdout;
|
|
3429
|
-
if (typeof captured === "string" && captured.trim() !== "") stdout = captured;
|
|
3430
|
-
else {
|
|
3431
|
-
return {
|
|
3432
|
-
kind: "failed",
|
|
3433
|
-
code: "archive_not_run",
|
|
3434
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3435
|
-
};
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
return readArchiveJson(stdout);
|
|
3439
|
-
}
|
|
3440
|
-
function readArchiveJson(stdout) {
|
|
3154
|
+
return { stdout: run.stdout ?? "", stderr: run.stderr ?? "" };
|
|
3155
|
+
};
|
|
3156
|
+
function validateChangeOnDisk(repoRoot, changeId, run = runOpenSpecValidate) {
|
|
3157
|
+
const result = run(repoRoot, changeId);
|
|
3158
|
+
if (result.failed !== void 0) return { outcome: "unavailable", reason: result.failed };
|
|
3441
3159
|
let parsed;
|
|
3442
3160
|
try {
|
|
3443
|
-
parsed = JSON.parse(stdout);
|
|
3161
|
+
parsed = JSON.parse(result.stdout);
|
|
3444
3162
|
} catch {
|
|
3445
|
-
|
|
3163
|
+
const detail = (result.stderr || result.stdout).trim().split("\n")[0] ?? "";
|
|
3164
|
+
return {
|
|
3165
|
+
outcome: "unavailable",
|
|
3166
|
+
reason: `\`openspec validate\` did not answer with JSON${detail === "" ? "" : `: ${detail}`}`
|
|
3167
|
+
};
|
|
3446
3168
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3169
|
+
const item = parsed.items?.find((i) => i.id === changeId) ?? parsed.items?.[0];
|
|
3170
|
+
if (!item) {
|
|
3449
3171
|
return {
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
message: diagnostic?.message ?? "openspec archive reported no result and no diagnostic."
|
|
3172
|
+
outcome: "unavailable",
|
|
3173
|
+
reason: `\`openspec validate\` reported nothing for \`${changeId}\``
|
|
3453
3174
|
};
|
|
3454
3175
|
}
|
|
3455
|
-
const
|
|
3176
|
+
const findings = openSpecFindings(item.issues ?? []);
|
|
3177
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
3178
|
+
const warnings = findings.filter((f) => f.severity === "warning");
|
|
3456
3179
|
return {
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
removed: totals.removed ?? 0,
|
|
3465
|
-
renamed: totals.renamed ?? 0
|
|
3466
|
-
} : NO_TOTALS
|
|
3180
|
+
outcome: "validated",
|
|
3181
|
+
// The tool's own verdict, not one recomputed from the issue list — under
|
|
3182
|
+
// `--strict` a warning fails, and re-deriving that rule here would be the second
|
|
3183
|
+
// implementation this module exists to avoid.
|
|
3184
|
+
valid: item.valid === true,
|
|
3185
|
+
errors,
|
|
3186
|
+
warnings
|
|
3467
3187
|
};
|
|
3468
3188
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3189
|
+
|
|
3190
|
+
// server/approvals.ts
|
|
3191
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3192
|
+
import { join as join10 } from "node:path";
|
|
3193
|
+
var SIDECAR = ".specsketch.json";
|
|
3194
|
+
var OVERLAY_DECLARATION = "authoring-overlay \u2014 specsketch only, never lifecycle truth";
|
|
3195
|
+
var STAGE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3196
|
+
var REAL_IO2 = { readText: (path) => readFileSync6(path, "utf8") };
|
|
3197
|
+
function readApprovals(changeDir, io = REAL_IO2) {
|
|
3198
|
+
const path = join10(changeDir, SIDECAR);
|
|
3199
|
+
let size;
|
|
3200
|
+
try {
|
|
3201
|
+
const stat = statSync5(path);
|
|
3202
|
+
if (!stat.isFile()) {
|
|
3203
|
+
return { outcome: "unreadable", reason: `\`${SIDECAR}\` is not a file` };
|
|
3476
3204
|
}
|
|
3205
|
+
size = stat.size;
|
|
3206
|
+
} catch (err) {
|
|
3207
|
+
if (err.code === "ENOENT") return { outcome: "absent" };
|
|
3208
|
+
return { outcome: "unreadable", reason: describe(err) };
|
|
3477
3209
|
}
|
|
3478
|
-
|
|
3479
|
-
const archived = join7(repoRoot, "openspec", "changes", "archive", archive.archivedAs);
|
|
3480
|
-
if (archive.archivedAs === "" || !isDirectory(archived)) {
|
|
3481
|
-
return { kind: "archive-directory-missing", expected: archived };
|
|
3482
|
-
}
|
|
3483
|
-
return null;
|
|
3484
|
-
}
|
|
3485
|
-
function isDirectory(path) {
|
|
3210
|
+
let text;
|
|
3486
3211
|
try {
|
|
3487
|
-
|
|
3488
|
-
} catch {
|
|
3489
|
-
return
|
|
3212
|
+
text = io.readText(path);
|
|
3213
|
+
} catch (err) {
|
|
3214
|
+
return { outcome: "unreadable", reason: describe(err) };
|
|
3490
3215
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
// src/workbench/write/write-change.ts
|
|
3494
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync6, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3495
|
-
import { dirname as dirname4, join as join8 } from "node:path";
|
|
3496
|
-
|
|
3497
|
-
// src/workbench/shell/lifecycle.ts
|
|
3498
|
-
var SPEC_DRIVEN = Object.freeze({
|
|
3499
|
-
name: "spec-driven",
|
|
3500
|
-
artifacts: Object.freeze(["proposal", "specs", "design", "tasks"])
|
|
3501
|
-
});
|
|
3502
|
-
|
|
3503
|
-
// src/workbench/write/write-change.ts
|
|
3504
|
-
function changeDirectory(repoRoot, changeId) {
|
|
3505
|
-
return join8(repoRoot, "openspec", "changes", changeId);
|
|
3506
|
-
}
|
|
3507
|
-
function writeChange(repoRoot, change, provenance, options = {}) {
|
|
3508
|
-
const directory = changeDirectory(repoRoot, change.id);
|
|
3509
|
-
const report2 = validateChange(change);
|
|
3510
|
-
if (!report2.valid) {
|
|
3216
|
+
if (text === "" && size > 0) {
|
|
3511
3217
|
return {
|
|
3512
|
-
outcome: "
|
|
3513
|
-
|
|
3514
|
-
errors: report2.errors,
|
|
3515
|
-
warnings: report2.warnings
|
|
3218
|
+
outcome: "unreadable",
|
|
3219
|
+
reason: `\`${SIDECAR}\` holds ${size} bytes on disk but read back empty \u2014 this is a read failure, not an absence of approvals`
|
|
3516
3220
|
};
|
|
3517
3221
|
}
|
|
3518
|
-
|
|
3519
|
-
return { outcome: "directory-exists", directory };
|
|
3520
|
-
}
|
|
3521
|
-
const emitted = emitChangeWithProvenance(change, provenance);
|
|
3522
|
-
writeEmitted(directory, emitted);
|
|
3523
|
-
writeSchemaFile(directory);
|
|
3524
|
-
return {
|
|
3525
|
-
outcome: "written",
|
|
3526
|
-
directory,
|
|
3527
|
-
files: emitted.files.map((f) => f.path),
|
|
3528
|
-
warnings: report2.warnings
|
|
3529
|
-
};
|
|
3530
|
-
}
|
|
3531
|
-
function isNonEmptyDirectory(directory) {
|
|
3532
|
-
try {
|
|
3533
|
-
if (!statSync4(directory).isDirectory()) return true;
|
|
3534
|
-
return readdirSync6(directory).length > 0;
|
|
3535
|
-
} catch {
|
|
3536
|
-
return false;
|
|
3537
|
-
}
|
|
3538
|
-
}
|
|
3539
|
-
function readEmittedChange(directory, id) {
|
|
3540
|
-
const files = [];
|
|
3541
|
-
for (const name of ["proposal.md", "design.md", "tasks.md"]) {
|
|
3542
|
-
files.push({ path: name, content: readFileSync6(join8(directory, name), "utf8") });
|
|
3543
|
-
}
|
|
3544
|
-
const specsDir = join8(directory, "specs");
|
|
3545
|
-
let capabilities = [];
|
|
3222
|
+
let parsed;
|
|
3546
3223
|
try {
|
|
3547
|
-
|
|
3548
|
-
} catch {
|
|
3549
|
-
capabilities = [];
|
|
3550
|
-
}
|
|
3551
|
-
for (const capability of capabilities) {
|
|
3552
|
-
files.push({
|
|
3553
|
-
path: `specs/${capability}/spec.md`,
|
|
3554
|
-
content: readFileSync6(join8(specsDir, capability, "spec.md"), "utf8")
|
|
3555
|
-
});
|
|
3556
|
-
}
|
|
3557
|
-
return { id, files };
|
|
3558
|
-
}
|
|
3559
|
-
function writeSchemaFile(directory) {
|
|
3560
|
-
const path = join8(directory, ".openspec.yaml");
|
|
3561
|
-
if (existsSync6(path)) return;
|
|
3562
|
-
writeFileSync3(path, `schema: ${SPEC_DRIVEN.name}
|
|
3563
|
-
`, "utf8");
|
|
3564
|
-
}
|
|
3565
|
-
function writeEmitted(directory, emitted) {
|
|
3566
|
-
mkdirSync4(directory, { recursive: true });
|
|
3567
|
-
for (const file of emitted.files) {
|
|
3568
|
-
const target = join8(directory, file.path);
|
|
3569
|
-
mkdirSync4(dirname4(target), { recursive: true });
|
|
3570
|
-
writeFileSync3(target, file.content, "utf8");
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
|
|
3574
|
-
// src/workbench/core/reconstruct.ts
|
|
3575
|
-
function headingName(block) {
|
|
3576
|
-
const match = /^###[ \t]+Requirement:[ \t]*(.*)$/m.exec(block);
|
|
3577
|
-
return match?.[1]?.trim() ?? null;
|
|
3578
|
-
}
|
|
3579
|
-
function reconstructDraft(folded, delta) {
|
|
3580
|
-
const capability = delta.capability;
|
|
3581
|
-
if (folded.trim() === "") {
|
|
3582
|
-
const purpose = delta.purpose === null ? "" : `
|
|
3583
|
-
## Purpose
|
|
3584
|
-
${delta.purpose}
|
|
3585
|
-
`;
|
|
3586
|
-
const head = `# ${capability} Specification
|
|
3587
|
-
${purpose}
|
|
3588
|
-
## Requirements
|
|
3589
|
-
|
|
3590
|
-
`;
|
|
3224
|
+
parsed = JSON.parse(text);
|
|
3225
|
+
} catch (err) {
|
|
3591
3226
|
return {
|
|
3592
|
-
outcome: "
|
|
3593
|
-
|
|
3594
|
-
draft: `${head}${delta.added.map((b) => b.trimEnd()).join("\n\n")}
|
|
3595
|
-
`
|
|
3227
|
+
outcome: "unreadable",
|
|
3228
|
+
reason: `\`${SIDECAR}\` is not valid JSON (${err instanceof Error ? err.message : String(err)}) \u2014 the approvals it holds cannot be read, and this is not an absence of approvals`
|
|
3596
3229
|
};
|
|
3597
3230
|
}
|
|
3598
|
-
|
|
3599
|
-
|
|
3231
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3232
|
+
return { outcome: "unreadable", reason: `\`${SIDECAR}\` is not an approval store` };
|
|
3233
|
+
}
|
|
3234
|
+
const raw = parsed.approvals;
|
|
3235
|
+
if (raw === void 0) {
|
|
3600
3236
|
return {
|
|
3601
|
-
outcome: "
|
|
3602
|
-
|
|
3603
|
-
reason: "the folded spec carries content outside its requirement blocks, which a rebuild would drop",
|
|
3604
|
-
draft: folded
|
|
3237
|
+
outcome: "unreadable",
|
|
3238
|
+
reason: `\`${SIDECAR}\` carries no \`approvals\` map \u2014 it is not the file this workbench wrote`
|
|
3605
3239
|
};
|
|
3606
3240
|
}
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
for (const requirement of parsed.requirements) {
|
|
3610
|
-
blocks.set(requirement.name, requirement.text);
|
|
3611
|
-
order.push(requirement.name);
|
|
3241
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
3242
|
+
return { outcome: "unreadable", reason: `\`${SIDECAR}\`'s \`approvals\` is not a map of stages` };
|
|
3612
3243
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
if (
|
|
3616
|
-
return {
|
|
3617
|
-
outcome: "unreconstructable",
|
|
3618
|
-
capability,
|
|
3619
|
-
reason: `the delta renames \`${pair.from}\`, which the folded spec does not declare`,
|
|
3620
|
-
draft: folded
|
|
3621
|
-
};
|
|
3244
|
+
const approvals = {};
|
|
3245
|
+
for (const [stage, value] of Object.entries(raw)) {
|
|
3246
|
+
if (!STAGE.test(stage)) {
|
|
3247
|
+
return { outcome: "unreadable", reason: `\`${SIDECAR}\` records \`${stage}\`, which is not a stage` };
|
|
3622
3248
|
}
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
order[order.indexOf(pair.from)] = pair.to;
|
|
3626
|
-
}
|
|
3627
|
-
for (const name of delta.removed) {
|
|
3628
|
-
if (!blocks.delete(name)) {
|
|
3249
|
+
const approvedAt = value?.approvedAt;
|
|
3250
|
+
if (typeof approvedAt !== "string" || approvedAt === "") {
|
|
3629
3251
|
return {
|
|
3630
|
-
outcome: "
|
|
3631
|
-
|
|
3632
|
-
reason: `the delta removes \`${name}\`, which the folded spec does not declare`,
|
|
3633
|
-
draft: folded
|
|
3252
|
+
outcome: "unreadable",
|
|
3253
|
+
reason: `\`${SIDECAR}\`'s entry for \`${stage}\` carries no \`approvedAt\` \u2014 the store is corrupt, not empty`
|
|
3634
3254
|
};
|
|
3635
3255
|
}
|
|
3636
|
-
|
|
3256
|
+
approvals[stage] = { approvedAt };
|
|
3637
3257
|
}
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
capability,
|
|
3644
|
-
reason: name === null ? "the delta modifies a block with no readable heading" : `the delta modifies \`${name}\`, which is not in the folded spec after renames and removals`,
|
|
3645
|
-
draft: folded
|
|
3646
|
-
};
|
|
3647
|
-
}
|
|
3648
|
-
blocks.set(name, block.trimEnd());
|
|
3258
|
+
return { outcome: "read", approvals };
|
|
3259
|
+
}
|
|
3260
|
+
function setApproval(changeDir, stage, approved, now = /* @__PURE__ */ new Date()) {
|
|
3261
|
+
if (!STAGE.test(stage)) {
|
|
3262
|
+
return { outcome: "failed", reason: `\`${stage}\` is not a stage of this change` };
|
|
3649
3263
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
capability,
|
|
3656
|
-
reason: "the delta adds a block with no readable heading",
|
|
3657
|
-
draft: folded
|
|
3658
|
-
};
|
|
3659
|
-
}
|
|
3660
|
-
if (!blocks.has(name)) order.push(name);
|
|
3661
|
-
blocks.set(name, block.trimEnd());
|
|
3264
|
+
if (!existsSync8(changeDir)) {
|
|
3265
|
+
return {
|
|
3266
|
+
outcome: "failed",
|
|
3267
|
+
reason: `the change directory does not exist yet \u2014 save an artifact before approving a stage`
|
|
3268
|
+
};
|
|
3662
3269
|
}
|
|
3663
|
-
const
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3270
|
+
const current = readApprovals(changeDir);
|
|
3271
|
+
if (current.outcome === "unreadable") {
|
|
3272
|
+
return {
|
|
3273
|
+
outcome: "failed",
|
|
3274
|
+
reason: `the approval store could not be read, so it was not written: ${current.reason}`
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
const next = {
|
|
3278
|
+
...current.outcome === "read" ? current.approvals : {}
|
|
3671
3279
|
};
|
|
3672
|
-
}
|
|
3673
|
-
|
|
3674
|
-
// src/workbench/agent/credential-probe.ts
|
|
3675
|
-
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
|
|
3676
|
-
import { delimiter, join as join9 } from "node:path";
|
|
3677
|
-
function readSubscriptionToken(home) {
|
|
3678
|
-
const path = join9(home, ".claude", ".credentials.json");
|
|
3679
|
-
if (!existsSync7(path)) return null;
|
|
3280
|
+
if (approved) next[stage] = { approvedAt: now.toISOString() };
|
|
3281
|
+
else delete next[stage];
|
|
3680
3282
|
try {
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
} catch {
|
|
3688
|
-
return
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
if (deps.oauthToken) process.env.ANTHROPIC_OAUTH_TOKEN = deps.oauthToken;
|
|
3702
|
-
else if (deps.apiKey) process.env.ANTHROPIC_API_KEY = deps.apiKey;
|
|
3703
|
-
const models = deps.models ?? builtinModels();
|
|
3704
|
-
const model = models.getModel(provider, modelId);
|
|
3705
|
-
if (!model) return { kind: "failed", reason: `no such model: ${provider}/${modelId}` };
|
|
3706
|
-
const messages = [
|
|
3707
|
-
{ role: "user", content: input.prompt, timestamp: Date.now() }
|
|
3708
|
-
];
|
|
3709
|
-
const context = {
|
|
3710
|
-
systemPrompt: input.systemPrompt,
|
|
3711
|
-
messages,
|
|
3712
|
-
tools: [...input.tools]
|
|
3283
|
+
writeFileSync4(
|
|
3284
|
+
join10(changeDir, SIDECAR),
|
|
3285
|
+
`${JSON.stringify({ overlay: OVERLAY_DECLARATION, approvals: next }, null, " ")}
|
|
3286
|
+
`,
|
|
3287
|
+
"utf8"
|
|
3288
|
+
);
|
|
3289
|
+
} catch (err) {
|
|
3290
|
+
return { outcome: "failed", reason: `the approval could not be stored \u2014 ${describe(err)}` };
|
|
3291
|
+
}
|
|
3292
|
+
return { outcome: "written", approvals: next };
|
|
3293
|
+
}
|
|
3294
|
+
function describe(err) {
|
|
3295
|
+
const e = err;
|
|
3296
|
+
const why = {
|
|
3297
|
+
EACCES: "permission denied",
|
|
3298
|
+
EPERM: "the operation is not permitted",
|
|
3299
|
+
EROFS: "the filesystem is read-only",
|
|
3300
|
+
ENOSPC: "the filesystem is full",
|
|
3301
|
+
ENOENT: "the change directory has gone",
|
|
3302
|
+
EISDIR: "a directory is in the way"
|
|
3713
3303
|
};
|
|
3714
|
-
|
|
3715
|
-
const served = `${provider}/${modelId}`;
|
|
3716
|
-
let answer = "";
|
|
3717
|
-
for (let round = 0; round < maxRounds; round += 1) {
|
|
3718
|
-
const stream = models.stream(model, context);
|
|
3719
|
-
const pending = [];
|
|
3720
|
-
let message;
|
|
3721
|
-
try {
|
|
3722
|
-
for await (const event of stream) {
|
|
3723
|
-
if (event.type === "toolcall_end") pending.push(event.toolCall);
|
|
3724
|
-
}
|
|
3725
|
-
message = await stream.result();
|
|
3726
|
-
} catch (err) {
|
|
3727
|
-
return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
|
|
3728
|
-
}
|
|
3729
|
-
if (message.stopReason === "error") {
|
|
3730
|
-
return { kind: "failed", reason: message.errorMessage ?? "the model returned an error" };
|
|
3731
|
-
}
|
|
3732
|
-
answer = textOf2(message);
|
|
3733
|
-
context.messages.push(message);
|
|
3734
|
-
if (pending.length === 0) return { kind: "answered", answer, calls, served };
|
|
3735
|
-
for (const call of pending) {
|
|
3736
|
-
const result = await input.execute(call.name, call.arguments);
|
|
3737
|
-
calls.push({ name: call.name, arguments: call.arguments, result });
|
|
3738
|
-
context.messages.push({
|
|
3739
|
-
role: "toolResult",
|
|
3740
|
-
toolCallId: call.id,
|
|
3741
|
-
toolName: call.name,
|
|
3742
|
-
content: [{ type: "text", text: result }],
|
|
3743
|
-
// The executor reports a refusal as ordinary text — a refused proposal is a
|
|
3744
|
-
// normal outcome the model should reason about, not a transport error.
|
|
3745
|
-
isError: false,
|
|
3746
|
-
timestamp: Date.now()
|
|
3747
|
-
});
|
|
3748
|
-
}
|
|
3749
|
-
}
|
|
3750
|
-
return { kind: "cut-short", answer, calls, served, rounds: maxRounds };
|
|
3751
|
-
}
|
|
3752
|
-
function textOf2(message) {
|
|
3753
|
-
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
|
|
3754
|
-
}
|
|
3755
|
-
|
|
3756
|
-
// src/workbench/agent/tools.ts
|
|
3757
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
3758
|
-
var AGENT_TOOLS = [
|
|
3759
|
-
{
|
|
3760
|
-
name: "add_task_group",
|
|
3761
|
-
description: "Propose a group of implementation tasks for this change. Use for planning the work; one group per coherent area, each task a single concrete action.",
|
|
3762
|
-
parameters: Type.Object({
|
|
3763
|
-
title: Type.String({ description: 'Short group title, e.g. "Search" or "Byte-safe writes"' }),
|
|
3764
|
-
tasks: Type.Array(Type.String(), {
|
|
3765
|
-
description: "Task titles, one per task. Each states one concrete action."
|
|
3766
|
-
}),
|
|
3767
|
-
capability: Type.Optional(
|
|
3768
|
-
Type.String({
|
|
3769
|
-
description: "Optional. The capability of the requirement this group implements \u2014 omit for a chore group that implements none."
|
|
3770
|
-
})
|
|
3771
|
-
),
|
|
3772
|
-
requirement: Type.Optional(
|
|
3773
|
-
Type.String({
|
|
3774
|
-
description: "Optional. The exact requirement name this group implements, as it appears after `### Requirement:`."
|
|
3775
|
-
})
|
|
3776
|
-
)
|
|
3777
|
-
})
|
|
3778
|
-
},
|
|
3779
|
-
{
|
|
3780
|
-
name: "add_requirement",
|
|
3781
|
-
description: "Propose a requirement to draft into an open capability. The block must be full markdown: a `### Requirement:` heading, RFC-2119 SHALL text, and at least one `#### Scenario:` with WHEN/THEN steps.",
|
|
3782
|
-
parameters: Type.Object({
|
|
3783
|
-
capability: Type.String({ description: "A capability already open in this change" }),
|
|
3784
|
-
block: Type.String({ description: "The complete requirement block, in markdown" })
|
|
3785
|
-
})
|
|
3786
|
-
},
|
|
3787
|
-
{
|
|
3788
|
-
name: "set_proposal_section",
|
|
3789
|
-
description: "Propose the body of one proposal section.",
|
|
3790
|
-
parameters: Type.Object({
|
|
3791
|
-
section: Type.String({
|
|
3792
|
-
description: `One of: ${PROPOSAL_SECTIONS.map((s) => s.id).join(", ")}`
|
|
3793
|
-
}),
|
|
3794
|
-
body: Type.String({ description: "The section body, in markdown" })
|
|
3795
|
-
})
|
|
3796
|
-
},
|
|
3797
|
-
{
|
|
3798
|
-
name: "add_design_block",
|
|
3799
|
-
description: "Propose a design block. Use `mermaid` for a diagram (C4Context / C4Container / C4Component / C4Dynamic render as diagrams), `markdown` for prose.",
|
|
3800
|
-
parameters: Type.Object({
|
|
3801
|
-
kind: Type.String({ description: "markdown | mermaid | sketch" }),
|
|
3802
|
-
title: Type.String({ description: "The block's heading" }),
|
|
3803
|
-
text: Type.String({ description: "The block body" })
|
|
3804
|
-
})
|
|
3805
|
-
}
|
|
3806
|
-
];
|
|
3807
|
-
var DESIGN_KINDS = /* @__PURE__ */ new Set(["markdown", "mermaid", "sketch"]);
|
|
3808
|
-
function stripLeadingNumber(text) {
|
|
3809
|
-
return text.replace(/^\s*\d+(?:\.\d+)*[.)]?\s+/, "").trim();
|
|
3810
|
-
}
|
|
3811
|
-
function proposalFromToolCall(name, args, shape) {
|
|
3812
|
-
switch (name) {
|
|
3813
|
-
case "add_task_group": {
|
|
3814
|
-
const title = stripLeadingNumber(String(args["title"] ?? ""));
|
|
3815
|
-
const tasks = Array.isArray(args["tasks"]) ? args["tasks"].map((t) => stripLeadingNumber(String(t))).filter((t) => t !== "") : [];
|
|
3816
|
-
if (title === "" || tasks.length === 0) {
|
|
3817
|
-
return { kind: "refused", tool: name, missing: "a title and at least one task" };
|
|
3818
|
-
}
|
|
3819
|
-
const capability = String(args["capability"] ?? "").trim();
|
|
3820
|
-
const key = String(args["requirement"] ?? "").trim();
|
|
3821
|
-
if (capability === "" && key === "") return { kind: "task-group", title, tasks };
|
|
3822
|
-
if (capability === "" || key === "") {
|
|
3823
|
-
return {
|
|
3824
|
-
kind: "refused",
|
|
3825
|
-
tool: name,
|
|
3826
|
-
missing: "both a capability and a requirement name, or neither"
|
|
3827
|
-
};
|
|
3828
|
-
}
|
|
3829
|
-
if (shape.citable !== void 0 && !shape.citable.some((c) => c.capability === capability && c.key === key)) {
|
|
3830
|
-
return {
|
|
3831
|
-
kind: "refused",
|
|
3832
|
-
tool: name,
|
|
3833
|
-
missing: `\`${key}\` in \`${capability}\` is not a requirement this change declares`
|
|
3834
|
-
};
|
|
3835
|
-
}
|
|
3836
|
-
return { kind: "task-group", title, tasks, cites: { capability, key } };
|
|
3837
|
-
}
|
|
3838
|
-
case "add_requirement": {
|
|
3839
|
-
const capability = String(args["capability"] ?? "").trim();
|
|
3840
|
-
const block = String(args["block"] ?? "").trim();
|
|
3841
|
-
if (!shape.openCapabilities.includes(capability)) {
|
|
3842
|
-
return {
|
|
3843
|
-
kind: "refused",
|
|
3844
|
-
tool: name,
|
|
3845
|
-
missing: `capability \`${capability}\` is not open in this change`
|
|
3846
|
-
};
|
|
3847
|
-
}
|
|
3848
|
-
if (!/^###[ \t]+Requirement:/m.test(block)) {
|
|
3849
|
-
return { kind: "refused", tool: name, missing: "a `### Requirement:` heading" };
|
|
3850
|
-
}
|
|
3851
|
-
return { kind: "requirement", capability, block };
|
|
3852
|
-
}
|
|
3853
|
-
case "set_proposal_section": {
|
|
3854
|
-
const section2 = String(args["section"] ?? "").trim();
|
|
3855
|
-
const known = PROPOSAL_SECTIONS.find((s) => s.id === section2);
|
|
3856
|
-
if (!known) {
|
|
3857
|
-
return { kind: "refused", tool: name, missing: `no proposal section \`${section2}\`` };
|
|
3858
|
-
}
|
|
3859
|
-
return {
|
|
3860
|
-
kind: "proposal-section",
|
|
3861
|
-
section: known.id,
|
|
3862
|
-
body: String(args["body"] ?? "")
|
|
3863
|
-
};
|
|
3864
|
-
}
|
|
3865
|
-
case "add_design_block": {
|
|
3866
|
-
const blockKind = String(args["kind"] ?? "").trim();
|
|
3867
|
-
if (!DESIGN_KINDS.has(blockKind)) {
|
|
3868
|
-
return { kind: "refused", tool: name, missing: `no design block kind \`${blockKind}\`` };
|
|
3869
|
-
}
|
|
3870
|
-
const title = String(args["title"] ?? "").trim();
|
|
3871
|
-
if (title === "") return { kind: "refused", tool: name, missing: "a block title" };
|
|
3872
|
-
return {
|
|
3873
|
-
kind: "design-block",
|
|
3874
|
-
blockKind,
|
|
3875
|
-
title,
|
|
3876
|
-
text: String(args["text"] ?? "")
|
|
3877
|
-
};
|
|
3878
|
-
}
|
|
3879
|
-
default:
|
|
3880
|
-
return { kind: "refused", tool: name, missing: `no such tool \`${name}\`` };
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
function toolResultFor(proposal) {
|
|
3884
|
-
switch (proposal.kind) {
|
|
3885
|
-
case "task-group":
|
|
3886
|
-
return `Proposed task group "${proposal.title}" with ${proposal.tasks.length} task(s)${proposal.cites ? `, implementing \`${proposal.cites.key}\`` : ", implementing no requirement"}. Awaiting the author's apply; nothing is written yet.`;
|
|
3887
|
-
case "requirement":
|
|
3888
|
-
return `Proposed a requirement for \`${proposal.capability}\`. Awaiting the author's apply; nothing is written yet.`;
|
|
3889
|
-
case "proposal-section":
|
|
3890
|
-
return `Proposed a body for the \`${proposal.section}\` section. Awaiting the author's apply; nothing is written yet.`;
|
|
3891
|
-
case "design-block":
|
|
3892
|
-
return `Proposed a ${proposal.blockKind} design block "${proposal.title}". Awaiting the author's apply; nothing is written yet.`;
|
|
3893
|
-
case "refused":
|
|
3894
|
-
return `Refused: ${proposal.tool} needs ${proposal.missing}. Nothing was proposed.`;
|
|
3895
|
-
}
|
|
3304
|
+
return `\`${SIDECAR}\`: ${why[e.code ?? ""] ?? e.message ?? String(err)} (${e.code ?? "unknown"})`;
|
|
3896
3305
|
}
|
|
3897
3306
|
|
|
3898
3307
|
// server/workbench-host.ts
|
|
@@ -3900,44 +3309,45 @@ var CHANGE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
3900
3309
|
function changeContextFor(target, source) {
|
|
3901
3310
|
const read = [];
|
|
3902
3311
|
let changeText = "";
|
|
3903
|
-
if (source.change) {
|
|
3904
|
-
for (const file of emitChange({ ...source.change, id: target.changeId }).files) {
|
|
3905
|
-
if (file.content.replace(/^#[ \t].*$/m, "").trim() === "") continue;
|
|
3906
|
-
changeText += `
|
|
3907
|
-
|
|
3908
|
-
===== ${file.path} (unsaved draft \u2014 what the author is looking at) =====
|
|
3909
|
-
${file.content}`;
|
|
3910
|
-
read.push(`${file.path} (draft)`);
|
|
3911
|
-
}
|
|
3912
|
-
return { read, changeText };
|
|
3913
|
-
}
|
|
3914
3312
|
if (!target.resuming) return { read, changeText };
|
|
3915
3313
|
for (const name of ["proposal.md", "tasks.md"]) {
|
|
3916
|
-
const file =
|
|
3917
|
-
if (!
|
|
3314
|
+
const file = join11(target.changeDir, name);
|
|
3315
|
+
if (!existsSync9(file)) continue;
|
|
3918
3316
|
changeText += `
|
|
3919
3317
|
|
|
3920
3318
|
===== ${name} =====
|
|
3921
|
-
${
|
|
3319
|
+
${readFileSync7(file, "utf8")}`;
|
|
3922
3320
|
read.push(name);
|
|
3923
3321
|
}
|
|
3924
3322
|
for (const capability of source.openCapabilities ?? []) {
|
|
3925
|
-
const file =
|
|
3926
|
-
if (!
|
|
3323
|
+
const file = join11(target.changeDir, "specs", capability, "spec.md");
|
|
3324
|
+
if (!existsSync9(file)) continue;
|
|
3927
3325
|
changeText += `
|
|
3928
3326
|
|
|
3929
3327
|
===== specs/${capability}/spec.md (the change's deltas) =====
|
|
3930
|
-
${
|
|
3328
|
+
${readFileSync7(file, "utf8")}`;
|
|
3931
3329
|
read.push(`specs/${capability}/spec.md`);
|
|
3932
3330
|
}
|
|
3933
3331
|
return { read, changeText };
|
|
3934
3332
|
}
|
|
3333
|
+
function citableOnDisk(target) {
|
|
3334
|
+
if (!target.resuming) return [];
|
|
3335
|
+
const specs = [];
|
|
3336
|
+
for (const artifact of listArtifacts(target.changeDir)) {
|
|
3337
|
+
const capability = /^specs\/([^/]+)\/spec\.md$/.exec(artifact)?.[1];
|
|
3338
|
+
if (capability === void 0) continue;
|
|
3339
|
+
const read = readArtifact(target.changeDir, artifact);
|
|
3340
|
+
if (read.outcome !== "read") continue;
|
|
3341
|
+
specs.push({ capability, content: read.content });
|
|
3342
|
+
}
|
|
3343
|
+
return citableRequirements(specs);
|
|
3344
|
+
}
|
|
3935
3345
|
var WorkbenchHostError = class extends Error {
|
|
3936
3346
|
};
|
|
3937
3347
|
function findRepoRoot(start) {
|
|
3938
3348
|
let dir = resolve3(start);
|
|
3939
3349
|
for (; ; ) {
|
|
3940
|
-
if (
|
|
3350
|
+
if (existsSync9(join11(dir, "openspec"))) return dir;
|
|
3941
3351
|
const parent = dirname5(dir);
|
|
3942
3352
|
if (parent === dir) return null;
|
|
3943
3353
|
dir = parent;
|
|
@@ -3959,41 +3369,20 @@ function resolveTarget(cwd, changeId) {
|
|
|
3959
3369
|
);
|
|
3960
3370
|
}
|
|
3961
3371
|
const changeDir = changeDirectory(repoRoot, changeId);
|
|
3962
|
-
return { repoRoot, changeId, changeDir, resuming:
|
|
3372
|
+
return { repoRoot, changeId, changeDir, resuming: existsSync9(changeDir) };
|
|
3373
|
+
}
|
|
3374
|
+
function readSchema(target) {
|
|
3375
|
+
try {
|
|
3376
|
+
return resolveSchema(readFileSync7(join11(target.changeDir, ".openspec.yaml"), "utf8"));
|
|
3377
|
+
} catch {
|
|
3378
|
+
return { kind: "unknown-schema", name: "" };
|
|
3379
|
+
}
|
|
3963
3380
|
}
|
|
3964
3381
|
function mountWorkbenchHost(app, options) {
|
|
3965
3382
|
const { target } = options;
|
|
3966
3383
|
const reader = createFoldedSpecReader(target.repoRoot);
|
|
3967
3384
|
app.get("/api/workbench/context", (c) => {
|
|
3968
|
-
|
|
3969
|
-
let deltaCapabilities = [];
|
|
3970
|
-
const capabilities = [];
|
|
3971
|
-
if (target.resuming) {
|
|
3972
|
-
try {
|
|
3973
|
-
const emitted = readEmittedChange(target.changeDir, target.changeId);
|
|
3974
|
-
document = parseChangeDocument(emitted);
|
|
3975
|
-
deltaCapabilities = emitted.files.map((f) => /^specs\/([^/]+)\/spec\.md$/.exec(f.path)?.[1]).filter((n) => n !== void 0);
|
|
3976
|
-
for (const spec of document.deltaSpecs) {
|
|
3977
|
-
const folded = reader.readCapability(spec.capability);
|
|
3978
|
-
const rebuilt = reconstructDraft(folded ?? "", spec);
|
|
3979
|
-
capabilities.push({
|
|
3980
|
-
capability: spec.capability,
|
|
3981
|
-
folded,
|
|
3982
|
-
draft: rebuilt.draft,
|
|
3983
|
-
reconstructed: rebuilt.outcome === "reconstructed",
|
|
3984
|
-
...rebuilt.outcome === "unreconstructable" ? { reason: rebuilt.reason } : {},
|
|
3985
|
-
// The requirement names the EMITTED delta declares. Carried so the
|
|
3986
|
-
// emission gate can ask whether the draft is whole NOW, rather than
|
|
3987
|
-
// only whether it loaded whole — otherwise an author who restores a
|
|
3988
|
-
// requirement by hand stays blocked with no way forward inside the
|
|
3989
|
-
// tool, which is a lock rather than a guard.
|
|
3990
|
-
declared: [...spec.added, ...spec.modified].map((b) => /^###[ \t]+Requirement:[ \t]*(.*)$/m.exec(b)?.[1]?.trim()).filter((n) => n !== void 0 && n !== "")
|
|
3991
|
-
});
|
|
3992
|
-
}
|
|
3993
|
-
} catch {
|
|
3994
|
-
document = null;
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
3385
|
+
const artifacts = target.resuming ? listArtifacts(target.changeDir).map((name) => readArtifact(target.changeDir, name)) : [];
|
|
3997
3386
|
return c.json({
|
|
3998
3387
|
repoRoot: target.repoRoot,
|
|
3999
3388
|
changeId: target.changeId,
|
|
@@ -4001,12 +3390,16 @@ function mountWorkbenchHost(app, options) {
|
|
|
4001
3390
|
resuming: target.resuming,
|
|
4002
3391
|
fixture: options.fixture === true,
|
|
4003
3392
|
specsDir: reader.specsDir,
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
3393
|
+
// The schema the change's own `.openspec.yaml` names — the rail's stages come
|
|
3394
|
+
// from it, never from a default this host picked. A change with no such file
|
|
3395
|
+
// resolves to `unknown-schema`, which is a fact to report, not one to paper over.
|
|
3396
|
+
schema: readSchema(target),
|
|
3397
|
+
artifacts,
|
|
3398
|
+
// The approval overlay, so a reopened change comes back with the stages the
|
|
3399
|
+
// author already gated still gated. Carried as its own outcome: an
|
|
3400
|
+
// unreadable store must reach the surface as unreadable, never as an
|
|
3401
|
+
// absence of approvals that would silently re-lock every stage.
|
|
3402
|
+
approvals: readApprovals(target.changeDir)
|
|
4010
3403
|
});
|
|
4011
3404
|
});
|
|
4012
3405
|
app.get("/api/workbench/capabilities", (c) => {
|
|
@@ -4025,52 +3418,69 @@ function mountWorkbenchHost(app, options) {
|
|
|
4025
3418
|
throw err;
|
|
4026
3419
|
}
|
|
4027
3420
|
});
|
|
4028
|
-
app.
|
|
3421
|
+
app.get(
|
|
3422
|
+
"/api/workbench/artifacts",
|
|
3423
|
+
(c) => c.json({
|
|
3424
|
+
changeDir: target.changeDir,
|
|
3425
|
+
exists: existsSync9(target.changeDir),
|
|
3426
|
+
artifacts: listArtifacts(target.changeDir)
|
|
3427
|
+
})
|
|
3428
|
+
);
|
|
3429
|
+
app.get("/api/workbench/artifact", (c) => {
|
|
3430
|
+
const name = c.req.query("artifact") ?? "";
|
|
3431
|
+
const result = readArtifact(target.changeDir, name);
|
|
3432
|
+
return c.json({ result }, result.outcome === "refused" ? 400 : 200);
|
|
3433
|
+
});
|
|
3434
|
+
app.post("/api/workbench/artifact", async (c) => {
|
|
4029
3435
|
const body = await c.req.json();
|
|
4030
|
-
if (
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
3436
|
+
if (typeof body.artifact !== "string" || typeof body.content !== "string") {
|
|
3437
|
+
return c.json({ error: "missing-artifact" }, 400);
|
|
3438
|
+
}
|
|
3439
|
+
const result = writeArtifact(target.changeDir, body.artifact, body.content, {
|
|
3440
|
+
...body.expected === void 0 ? {} : { expected: body.expected },
|
|
3441
|
+
// Never a default. An overwrite after a changed-underneath refusal is the
|
|
3442
|
+
// author's explicit answer, and taking it unasked is the whole of what that
|
|
3443
|
+
// refusal exists to prevent.
|
|
3444
|
+
...body.overwrite === true ? { overwrite: true } : {}
|
|
3445
|
+
});
|
|
4037
3446
|
return c.json({ result }, result.outcome === "written" ? 200 : 409);
|
|
4038
3447
|
});
|
|
4039
|
-
app.
|
|
3448
|
+
app.get(
|
|
3449
|
+
"/api/workbench/validity",
|
|
3450
|
+
(c) => c.json({ result: validateChangeOnDisk(target.repoRoot, target.changeId) })
|
|
3451
|
+
);
|
|
3452
|
+
app.get("/api/workbench/approvals", (c) => c.json({ result: readApprovals(target.changeDir) }));
|
|
3453
|
+
app.post("/api/workbench/approvals", async (c) => {
|
|
4040
3454
|
const body = await c.req.json();
|
|
4041
|
-
if (
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
);
|
|
4047
|
-
return c.json({ result }, result.outcome === "approved" ? 200 : 409);
|
|
3455
|
+
if (typeof body.stage !== "string" || typeof body.approved !== "boolean") {
|
|
3456
|
+
return c.json({ error: "missing-stage" }, 400);
|
|
3457
|
+
}
|
|
3458
|
+
const result = setApproval(target.changeDir, body.stage, body.approved);
|
|
3459
|
+
return c.json({ result }, result.outcome === "written" ? 200 : 409);
|
|
4048
3460
|
});
|
|
4049
3461
|
app.post("/api/workbench/agent/turn", async (c) => {
|
|
4050
3462
|
const body = await c.req.json();
|
|
4051
3463
|
const prompt = (body.prompt ?? "").trim();
|
|
4052
3464
|
if (prompt === "") return c.json({ error: "missing-prompt" }, 400);
|
|
4053
|
-
const
|
|
4054
|
-
if (!token && !process.env["ANTHROPIC_API_KEY"]) {
|
|
4055
|
-
return c.json({ outcome: { kind: "no-credential" } }, 200);
|
|
4056
|
-
}
|
|
3465
|
+
const citable = citableOnDisk(target);
|
|
4057
3466
|
const shape = {
|
|
4058
3467
|
openCapabilities: body.openCapabilities ?? [],
|
|
4059
|
-
...
|
|
3468
|
+
...citable.length === 0 ? {} : { citable }
|
|
4060
3469
|
};
|
|
4061
3470
|
const { read, changeText } = changeContextFor(target, body);
|
|
4062
|
-
const proposals =
|
|
4063
|
-
const outcome = await runTurn(
|
|
3471
|
+
const { outcome, proposals } = await runAgentTurn(
|
|
4064
3472
|
{
|
|
3473
|
+
// Standing instructions on their OWN channel, forwarded to the harness's
|
|
3474
|
+
// system-prompt option — never joined to the prompt.
|
|
4065
3475
|
systemPrompt: [
|
|
4066
3476
|
"You are helping author an OpenSpec change in the SpecSketch workbench.",
|
|
4067
3477
|
`The change is \`${target.changeId}\`, in the repository at ${target.repoRoot}.`,
|
|
4068
3478
|
shape.openCapabilities.length > 0 ? `Capabilities open in this change: ${shape.openCapabilities.join(", ")}.` : "No capability is open in this change yet.",
|
|
4069
3479
|
body.scope ? `The author is looking at ${body.scope}.` : "",
|
|
4070
3480
|
"",
|
|
4071
|
-
"
|
|
4072
|
-
"applied by the author \u2014 you never write to the change yourself, so
|
|
4073
|
-
"and say briefly what you proposed and why.",
|
|
3481
|
+
"Return your proposals as the structured result. Everything you propose is",
|
|
3482
|
+
"reviewed and applied by the author \u2014 you never write to the change yourself, so",
|
|
3483
|
+
"propose freely and say briefly in `answer` what you proposed and why.",
|
|
4074
3484
|
"",
|
|
4075
3485
|
"Ground every proposal in the change below. Use its own vocabulary: if it says",
|
|
4076
3486
|
'"changes", do not write "plans". Do not invent states, sections or files it does',
|
|
@@ -4079,21 +3489,18 @@ function mountWorkbenchHost(app, options) {
|
|
|
4079
3489
|
changeText === "" ? "(This change has nothing written yet.)" : changeText
|
|
4080
3490
|
].filter((line) => line !== "").join("\n"),
|
|
4081
3491
|
prompt,
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
proposals.push(proposal);
|
|
4086
|
-
return toolResultFor(proposal);
|
|
4087
|
-
}
|
|
3492
|
+
cwd: target.repoRoot,
|
|
3493
|
+
session: `specsketch-workbench-${target.changeId}`,
|
|
3494
|
+
shape
|
|
4088
3495
|
},
|
|
4089
|
-
{
|
|
3496
|
+
options.home !== void 0 ? { home: options.home } : {}
|
|
4090
3497
|
);
|
|
4091
3498
|
return c.json({ outcome, proposals, read });
|
|
4092
3499
|
});
|
|
4093
3500
|
}
|
|
4094
3501
|
|
|
4095
3502
|
// server/cli.ts
|
|
4096
|
-
var CLI_VERSION = true ? "2026.08.
|
|
3503
|
+
var CLI_VERSION = true ? "2026.08.31.1" : "dev";
|
|
4097
3504
|
var USAGE = `Usage: specsketch <change-id> [--port <n>] [--fixture]
|
|
4098
3505
|
specsketch login [--profile <name>] [--web-url <url>]
|
|
4099
3506
|
specsketch --version
|
|
@@ -4168,13 +3575,13 @@ var MIME = {
|
|
|
4168
3575
|
function resolveClientDir(moduleUrl) {
|
|
4169
3576
|
const here = dirname6(fileURLToPath(moduleUrl));
|
|
4170
3577
|
const candidates = [
|
|
4171
|
-
|
|
3578
|
+
join12(here, "..", "client"),
|
|
4172
3579
|
// packed: dist/cli -> dist/client
|
|
4173
|
-
|
|
3580
|
+
join12(here, "..", "dist", "client")
|
|
4174
3581
|
// repo: server/ -> dist/client
|
|
4175
3582
|
];
|
|
4176
3583
|
for (const dir of candidates) {
|
|
4177
|
-
if (
|
|
3584
|
+
if (existsSync10(join12(dir, "index.html"))) return dir;
|
|
4178
3585
|
}
|
|
4179
3586
|
return null;
|
|
4180
3587
|
}
|
|
@@ -4182,9 +3589,9 @@ function staticResponse(clientDir, pathname) {
|
|
|
4182
3589
|
const rel = normalize2(decodeURIComponent(pathname)).replace(/^\/+/, "");
|
|
4183
3590
|
const target = resolve4(clientDir, rel === "" ? "index.html" : rel);
|
|
4184
3591
|
if (!target.startsWith(resolve4(clientDir))) return null;
|
|
4185
|
-
const file =
|
|
4186
|
-
if (!
|
|
4187
|
-
return new Response(
|
|
3592
|
+
const file = existsSync10(target) && extname(target) ? target : join12(clientDir, "index.html");
|
|
3593
|
+
if (!existsSync10(file)) return null;
|
|
3594
|
+
return new Response(readFileSync8(file), {
|
|
4188
3595
|
headers: { "Content-Type": MIME[extname(file)] ?? "application/octet-stream" }
|
|
4189
3596
|
});
|
|
4190
3597
|
}
|
|
@@ -4200,7 +3607,7 @@ function detectCredentialSource(env = process.env, home = homedir4()) {
|
|
|
4200
3607
|
if (env.ANTHROPIC_API_KEY) return "ANTHROPIC_API_KEY";
|
|
4201
3608
|
if (env.ANTHROPIC_AUTH_TOKEN) return "ANTHROPIC_AUTH_TOKEN";
|
|
4202
3609
|
try {
|
|
4203
|
-
const dir =
|
|
3610
|
+
const dir = join12(home, ".config", "anthropic", "credentials");
|
|
4204
3611
|
if (readdirSync8(dir).some((f) => f.endsWith(".json"))) return "anthropic profile";
|
|
4205
3612
|
} catch {
|
|
4206
3613
|
}
|