@coopcli/specsketch 2026.817.2 → 2026.825.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 +99 -20
- package/dist/client/assets/MarkdownEditorImpl-B7fANsTl.js +16 -0
- package/dist/client/assets/index-CLLS_dcT.css +1 -0
- package/dist/client/assets/index-DzRQXC_d.js +197 -0
- package/dist/client/index.html +2 -2
- package/package.json +8 -1
- package/dist/client/assets/index-DyUTkivM.css +0 -1
- package/dist/client/assets/index-Tyf33hWt.js +0 -196
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// server/cli.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync8 } from "node:fs";
|
|
5
5
|
import { homedir as homedir4 } from "node:os";
|
|
6
6
|
import { dirname as dirname6, extname, join as join11, normalize as normalize2, resolve as resolve4 } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -2150,7 +2150,7 @@ var ChangeWorkspace = class {
|
|
|
2150
2150
|
};
|
|
2151
2151
|
|
|
2152
2152
|
// server/workbench-host.ts
|
|
2153
|
-
import { existsSync as
|
|
2153
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
|
|
2154
2154
|
import { homedir as homedir3 } from "node:os";
|
|
2155
2155
|
import { dirname as dirname5, join as join10, resolve as resolve3 } from "node:path";
|
|
2156
2156
|
|
|
@@ -2491,6 +2491,7 @@ function changeDocument(change) {
|
|
|
2491
2491
|
design: change.design,
|
|
2492
2492
|
groups: numberTasks(change.tasks),
|
|
2493
2493
|
testing: testingRows(change.capabilities),
|
|
2494
|
+
proposalExtras: change.proposalExtras,
|
|
2494
2495
|
deltaSpecs: change.capabilities.filter(emitsDeltaSpec).map(deltaSpecDocument).sort((a, b) => a.capability < b.capability ? -1 : a.capability > b.capability ? 1 : 0)
|
|
2495
2496
|
};
|
|
2496
2497
|
}
|
|
@@ -2517,10 +2518,14 @@ ${UNSATISFIED_MARKER}
|
|
|
2517
2518
|
}
|
|
2518
2519
|
}
|
|
2519
2520
|
function renderProposal(doc) {
|
|
2521
|
+
const extras = doc.proposalExtras ?? [];
|
|
2522
|
+
const after = (id) => extras.filter((e) => e.afterSection === id).map((e) => `${e.text.trimEnd()}
|
|
2523
|
+
`);
|
|
2520
2524
|
const parts = [`# ${doc.id}
|
|
2521
|
-
|
|
2525
|
+
`, ...after(null)];
|
|
2522
2526
|
for (const spec of PROPOSAL_SECTIONS) {
|
|
2523
2527
|
parts.push(renderProposalSection(spec, doc.proposal[spec.id]));
|
|
2528
|
+
parts.push(...after(spec.id));
|
|
2524
2529
|
}
|
|
2525
2530
|
return `${parts.join("\n")}`;
|
|
2526
2531
|
}
|
|
@@ -2560,7 +2565,8 @@ function renderTasks(doc) {
|
|
|
2560
2565
|
`];
|
|
2561
2566
|
for (const task of group.tasks) {
|
|
2562
2567
|
const box = task.done ? "x" : " ";
|
|
2563
|
-
lines.push(`- [${box}] ${task.number} ${task.title
|
|
2568
|
+
lines.push(`- [${box}] ${task.number} ${task.title.split("\n").join(`
|
|
2569
|
+
${TASK_CONTINUATION}`)}`);
|
|
2564
2570
|
}
|
|
2565
2571
|
parts.push(`${lines.join("\n")}
|
|
2566
2572
|
`);
|
|
@@ -2640,6 +2646,8 @@ function emitChange(change) {
|
|
|
2640
2646
|
return renderChangeDocument(changeDocument(change));
|
|
2641
2647
|
}
|
|
2642
2648
|
var TASK_LINE = /^- \[([ xX])\] (\d+\.\d+) (.*)$/;
|
|
2649
|
+
var TASK_CONTINUATION = " ";
|
|
2650
|
+
var TASK_CONTINUATION_LINE = /^ {2}(.*)$/;
|
|
2643
2651
|
var GROUP_CITATION = /^(.*) — implements `([^`]+)`: `([^`]+)`$/;
|
|
2644
2652
|
var GROUP_HEADING = /^## (\d+)\. (.*)$/;
|
|
2645
2653
|
var FENCE = /^```(\S*)\s*$/;
|
|
@@ -2648,11 +2656,13 @@ function parseChangeDocument(emitted) {
|
|
|
2648
2656
|
const proposalSrc = byPath.get("proposal.md") ?? "";
|
|
2649
2657
|
const designSrc = byPath.get("design.md") ?? "";
|
|
2650
2658
|
const tasksSrc = byPath.get("tasks.md") ?? "";
|
|
2659
|
+
const parsedProposal = parseProposalFile(proposalSrc);
|
|
2651
2660
|
const groups = parseTasksFile(tasksSrc);
|
|
2652
2661
|
const deltaSpecs = emitted.files.filter((f) => f.path.startsWith("specs/") && f.path.endsWith("/spec.md")).map((f) => parseDeltaSpecFile(capabilityOfPath(f.path), f.content));
|
|
2653
2662
|
return {
|
|
2654
2663
|
id: emitted.id,
|
|
2655
|
-
proposal:
|
|
2664
|
+
proposal: parsedProposal.draft,
|
|
2665
|
+
proposalExtras: parsedProposal.extras,
|
|
2656
2666
|
design: parseDesignFile(designSrc),
|
|
2657
2667
|
groups: numberTasks(groups),
|
|
2658
2668
|
testing: parseTestingTable(tasksSrc),
|
|
@@ -2667,25 +2677,44 @@ function parseProposalFile(src) {
|
|
|
2667
2677
|
const lines = src.split("\n");
|
|
2668
2678
|
const bodies = /* @__PURE__ */ new Map();
|
|
2669
2679
|
let current = null;
|
|
2680
|
+
const extras = [];
|
|
2681
|
+
let extra = null;
|
|
2682
|
+
let lastFixed = null;
|
|
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;
|
|
2688
|
+
};
|
|
2670
2689
|
for (const line of lines) {
|
|
2671
2690
|
const p = probe2(line);
|
|
2672
2691
|
const spec = PROPOSAL_SECTIONS.find(
|
|
2673
2692
|
(s) => p === `${"#".repeat(s.level)} ${s.title}`
|
|
2674
2693
|
);
|
|
2675
2694
|
if (spec) {
|
|
2695
|
+
flushExtra();
|
|
2676
2696
|
current = spec.id;
|
|
2697
|
+
lastFixed = spec.id;
|
|
2677
2698
|
bodies.set(spec.id, []);
|
|
2678
2699
|
continue;
|
|
2679
2700
|
}
|
|
2680
2701
|
if (ANY_HEADING.test(p)) {
|
|
2702
|
+
flushExtra();
|
|
2681
2703
|
current = null;
|
|
2704
|
+
if (/^#[ \t]/.test(p) && !/^##/.test(p)) continue;
|
|
2705
|
+
extra = { afterSection: lastFixed, lines: [p] };
|
|
2682
2706
|
continue;
|
|
2683
2707
|
}
|
|
2684
2708
|
if (current) bodies.get(current).push(p);
|
|
2709
|
+
else if (extra) extra.lines.push(p);
|
|
2685
2710
|
}
|
|
2711
|
+
flushExtra();
|
|
2686
2712
|
const draft = emptyProposal();
|
|
2687
2713
|
for (const spec of PROPOSAL_SECTIONS) {
|
|
2688
|
-
const
|
|
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");
|
|
2689
2718
|
if (body === "" || body === UNSATISFIED_MARKER) {
|
|
2690
2719
|
draft[spec.id] = { state: "empty" };
|
|
2691
2720
|
continue;
|
|
@@ -2696,7 +2725,7 @@ function parseProposalFile(src) {
|
|
|
2696
2725
|
}
|
|
2697
2726
|
draft[spec.id] = { state: "authored", body };
|
|
2698
2727
|
}
|
|
2699
|
-
return draft;
|
|
2728
|
+
return { draft, extras };
|
|
2700
2729
|
}
|
|
2701
2730
|
var DESIGN_HEADING = /^##[ \t]+(.*)$/;
|
|
2702
2731
|
function parseDesignFile(src) {
|
|
@@ -2775,7 +2804,17 @@ function parseTasksFile(src) {
|
|
|
2775
2804
|
const task = TASK_LINE.exec(p);
|
|
2776
2805
|
if (task && open) {
|
|
2777
2806
|
const done = (task[1] ?? " ").toLowerCase() === "x";
|
|
2778
|
-
open.tasks.push({ title:
|
|
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] ?? ""}`
|
|
2817
|
+
};
|
|
2779
2818
|
}
|
|
2780
2819
|
}
|
|
2781
2820
|
if (open) groups.push(open);
|
|
@@ -2997,10 +3036,29 @@ function validateDesign(change) {
|
|
|
2997
3036
|
});
|
|
2998
3037
|
return findings;
|
|
2999
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, "\\$&");
|
|
3048
|
+
}
|
|
3000
3049
|
function validateCapabilities(change) {
|
|
3001
3050
|
const findings = [];
|
|
3002
3051
|
let derivedDeltas = 0;
|
|
3003
3052
|
for (const entry of change.capabilities) {
|
|
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
|
+
}
|
|
3004
3062
|
if (entry.derivation.outcome === "refused") {
|
|
3005
3063
|
findings.push(...refusalFindings(entry));
|
|
3006
3064
|
continue;
|
|
@@ -3433,8 +3491,16 @@ function isDirectory(path) {
|
|
|
3433
3491
|
}
|
|
3434
3492
|
|
|
3435
3493
|
// src/workbench/write/write-change.ts
|
|
3436
|
-
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync6, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3494
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync6, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3437
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
|
|
3438
3504
|
function changeDirectory(repoRoot, changeId) {
|
|
3439
3505
|
return join8(repoRoot, "openspec", "changes", changeId);
|
|
3440
3506
|
}
|
|
@@ -3454,6 +3520,7 @@ function writeChange(repoRoot, change, provenance, options = {}) {
|
|
|
3454
3520
|
}
|
|
3455
3521
|
const emitted = emitChangeWithProvenance(change, provenance);
|
|
3456
3522
|
writeEmitted(directory, emitted);
|
|
3523
|
+
writeSchemaFile(directory);
|
|
3457
3524
|
return {
|
|
3458
3525
|
outcome: "written",
|
|
3459
3526
|
directory,
|
|
@@ -3489,6 +3556,12 @@ function readEmittedChange(directory, id) {
|
|
|
3489
3556
|
}
|
|
3490
3557
|
return { id, files };
|
|
3491
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
|
+
}
|
|
3492
3565
|
function writeEmitted(directory, emitted) {
|
|
3493
3566
|
mkdirSync4(directory, { recursive: true });
|
|
3494
3567
|
for (const file of emitted.files) {
|
|
@@ -3599,11 +3672,11 @@ ${body}
|
|
|
3599
3672
|
}
|
|
3600
3673
|
|
|
3601
3674
|
// src/workbench/agent/credential-probe.ts
|
|
3602
|
-
import { existsSync as
|
|
3675
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
|
|
3603
3676
|
import { delimiter, join as join9 } from "node:path";
|
|
3604
3677
|
function readSubscriptionToken(home) {
|
|
3605
3678
|
const path = join9(home, ".claude", ".credentials.json");
|
|
3606
|
-
if (!
|
|
3679
|
+
if (!existsSync7(path)) return null;
|
|
3607
3680
|
try {
|
|
3608
3681
|
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3609
3682
|
const token = parsed.claudeAiOauth?.accessToken;
|
|
@@ -3841,7 +3914,7 @@ ${file.content}`;
|
|
|
3841
3914
|
if (!target.resuming) return { read, changeText };
|
|
3842
3915
|
for (const name of ["proposal.md", "tasks.md"]) {
|
|
3843
3916
|
const file = join10(target.changeDir, name);
|
|
3844
|
-
if (!
|
|
3917
|
+
if (!existsSync8(file)) continue;
|
|
3845
3918
|
changeText += `
|
|
3846
3919
|
|
|
3847
3920
|
===== ${name} =====
|
|
@@ -3850,7 +3923,7 @@ ${readFileSync8(file, "utf8")}`;
|
|
|
3850
3923
|
}
|
|
3851
3924
|
for (const capability of source.openCapabilities ?? []) {
|
|
3852
3925
|
const file = join10(target.changeDir, "specs", capability, "spec.md");
|
|
3853
|
-
if (!
|
|
3926
|
+
if (!existsSync8(file)) continue;
|
|
3854
3927
|
changeText += `
|
|
3855
3928
|
|
|
3856
3929
|
===== specs/${capability}/spec.md (the change's deltas) =====
|
|
@@ -3864,7 +3937,7 @@ var WorkbenchHostError = class extends Error {
|
|
|
3864
3937
|
function findRepoRoot(start) {
|
|
3865
3938
|
let dir = resolve3(start);
|
|
3866
3939
|
for (; ; ) {
|
|
3867
|
-
if (
|
|
3940
|
+
if (existsSync8(join10(dir, "openspec"))) return dir;
|
|
3868
3941
|
const parent = dirname5(dir);
|
|
3869
3942
|
if (parent === dir) return null;
|
|
3870
3943
|
dir = parent;
|
|
@@ -3886,7 +3959,7 @@ function resolveTarget(cwd, changeId) {
|
|
|
3886
3959
|
);
|
|
3887
3960
|
}
|
|
3888
3961
|
const changeDir = changeDirectory(repoRoot, changeId);
|
|
3889
|
-
return { repoRoot, changeId, changeDir, resuming:
|
|
3962
|
+
return { repoRoot, changeId, changeDir, resuming: existsSync8(changeDir) };
|
|
3890
3963
|
}
|
|
3891
3964
|
function mountWorkbenchHost(app, options) {
|
|
3892
3965
|
const { target } = options;
|
|
@@ -3908,7 +3981,13 @@ function mountWorkbenchHost(app, options) {
|
|
|
3908
3981
|
folded,
|
|
3909
3982
|
draft: rebuilt.draft,
|
|
3910
3983
|
reconstructed: rebuilt.outcome === "reconstructed",
|
|
3911
|
-
...rebuilt.outcome === "unreconstructable" ? { reason: rebuilt.reason } : {}
|
|
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 !== "")
|
|
3912
3991
|
});
|
|
3913
3992
|
}
|
|
3914
3993
|
} catch {
|
|
@@ -4014,7 +4093,7 @@ function mountWorkbenchHost(app, options) {
|
|
|
4014
4093
|
}
|
|
4015
4094
|
|
|
4016
4095
|
// server/cli.ts
|
|
4017
|
-
var CLI_VERSION = true ? "2026.08.
|
|
4096
|
+
var CLI_VERSION = true ? "2026.08.25.1" : "dev";
|
|
4018
4097
|
var USAGE = `Usage: specsketch <change-id> [--port <n>] [--fixture]
|
|
4019
4098
|
specsketch login [--profile <name>] [--web-url <url>]
|
|
4020
4099
|
specsketch --version
|
|
@@ -4095,7 +4174,7 @@ function resolveClientDir(moduleUrl) {
|
|
|
4095
4174
|
// repo: server/ -> dist/client
|
|
4096
4175
|
];
|
|
4097
4176
|
for (const dir of candidates) {
|
|
4098
|
-
if (
|
|
4177
|
+
if (existsSync9(join11(dir, "index.html"))) return dir;
|
|
4099
4178
|
}
|
|
4100
4179
|
return null;
|
|
4101
4180
|
}
|
|
@@ -4103,8 +4182,8 @@ function staticResponse(clientDir, pathname) {
|
|
|
4103
4182
|
const rel = normalize2(decodeURIComponent(pathname)).replace(/^\/+/, "");
|
|
4104
4183
|
const target = resolve4(clientDir, rel === "" ? "index.html" : rel);
|
|
4105
4184
|
if (!target.startsWith(resolve4(clientDir))) return null;
|
|
4106
|
-
const file =
|
|
4107
|
-
if (!
|
|
4185
|
+
const file = existsSync9(target) && extname(target) ? target : join11(clientDir, "index.html");
|
|
4186
|
+
if (!existsSync9(file)) return null;
|
|
4108
4187
|
return new Response(readFileSync9(file), {
|
|
4109
4188
|
headers: { "Content-Type": MIME[extname(file)] ?? "application/octet-stream" }
|
|
4110
4189
|
});
|