gentle-pi 0.4.2 → 0.4.4
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/assets/agents/sdd-apply.md +39 -5
- package/assets/agents/sdd-archive.md +39 -2
- package/assets/agents/sdd-spec.md +7 -0
- package/assets/agents/sdd-status.md +100 -0
- package/assets/agents/sdd-sync.md +14 -1
- package/assets/agents/sdd-verify.md +32 -2
- package/assets/orchestrator.md +37 -2
- package/assets/support/sdd-status-contract.md +100 -0
- package/extensions/gentle-ai.ts +89 -1
- package/lib/sdd-status.ts +529 -0
- package/package.json +1 -1
- package/scripts/verify-package-files.mjs +2 -0
- package/tests/runtime-harness.mjs +40 -0
- package/tests/sdd-status.test.ts +291 -0
|
@@ -31,6 +31,10 @@ const EXPECTED_COMMANDS = [
|
|
|
31
31
|
"gentle-ai:install-sdd",
|
|
32
32
|
"gentle-ai:sdd-preflight",
|
|
33
33
|
"gentle:sdd-preflight",
|
|
34
|
+
"sdd-status",
|
|
35
|
+
"gentle-ai:sdd-status",
|
|
36
|
+
"sdd-continue",
|
|
37
|
+
"gentle-ai:sdd-continue",
|
|
34
38
|
"gentle:models",
|
|
35
39
|
"gentle-ai:models",
|
|
36
40
|
"gentleman:models",
|
|
@@ -246,6 +250,25 @@ async function run() {
|
|
|
246
250
|
assert.match(onboardPromptResult.systemPrompt, /onboard base/);
|
|
247
251
|
assert.match(onboardPromptResult.systemPrompt, /## SDD Session Preflight/);
|
|
248
252
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-onboard.md")), true);
|
|
253
|
+
await mkdir(join(promptCwd, "openspec", "changes", "status-demo", "specs", "demo"), { recursive: true });
|
|
254
|
+
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "proposal.md"), "# Proposal\n");
|
|
255
|
+
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "specs", "demo", "spec.md"), "# Spec\n");
|
|
256
|
+
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "design.md"), "# Design\n");
|
|
257
|
+
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "tasks.md"), "# Tasks\n\n- [ ] 1.1 Implement demo\n");
|
|
258
|
+
const applyPromptResult = await promptHook(
|
|
259
|
+
{ agentName: "sdd-apply", systemPrompt: "apply base" },
|
|
260
|
+
createCtx(promptCwd, true, "sdd-apply-session"),
|
|
261
|
+
);
|
|
262
|
+
assert.match(applyPromptResult.systemPrompt, /## Native SDD Status Engine/);
|
|
263
|
+
assert.match(applyPromptResult.systemPrompt, /"changeName": "status-demo"/);
|
|
264
|
+
assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
|
|
265
|
+
const statusCtx = createCtx(promptCwd, true);
|
|
266
|
+
await commands.get("sdd-status").handler("status-demo --json", statusCtx);
|
|
267
|
+
assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-pi\.sdd-status"/);
|
|
268
|
+
const continueCtx = createCtx(promptCwd, true);
|
|
269
|
+
await commands.get("sdd-continue").handler("status-demo", continueCtx);
|
|
270
|
+
assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Dispatcher/);
|
|
271
|
+
assert.match(continueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
|
|
249
272
|
} finally {
|
|
250
273
|
await rm(promptCwd, { recursive: true, force: true });
|
|
251
274
|
}
|
|
@@ -311,8 +334,10 @@ async function run() {
|
|
|
311
334
|
);
|
|
312
335
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
313
336
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
337
|
+
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
314
338
|
await writeFile(join(globalAgentHome, "agents", "sdd-apply.md"), "stale global apply\n");
|
|
315
339
|
await writeFile(join(globalAgentHome, "chains", "sdd-full.chain.md"), "stale global chain\n");
|
|
340
|
+
await writeFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), "stale global status contract\n");
|
|
316
341
|
await mkdir(join(noUiCwd, ".pi", "agents"), { recursive: true });
|
|
317
342
|
await writeFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "project override must stay\n");
|
|
318
343
|
for (const handler of hooks.get("session_start")) {
|
|
@@ -328,6 +353,11 @@ async function run() {
|
|
|
328
353
|
"stale global chain\n",
|
|
329
354
|
"session_start must refresh stale global SDD chains",
|
|
330
355
|
);
|
|
356
|
+
assert.notEqual(
|
|
357
|
+
await readFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), "utf8"),
|
|
358
|
+
"stale global status contract\n",
|
|
359
|
+
"session_start must refresh stale global SDD support files",
|
|
360
|
+
);
|
|
331
361
|
assert.equal(
|
|
332
362
|
await readFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "utf8"),
|
|
333
363
|
"project override must stay\n",
|
|
@@ -402,7 +432,9 @@ async function run() {
|
|
|
402
432
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
403
433
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
404
434
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
435
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
405
436
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
437
|
+
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
406
438
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
407
439
|
assert.equal(ctx.ui.selections.length, 3);
|
|
408
440
|
assert.equal(ctx.ui.selections[0].label, "SDD execution mode");
|
|
@@ -436,7 +468,9 @@ async function run() {
|
|
|
436
468
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
437
469
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
438
470
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
471
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
439
472
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
473
|
+
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
440
474
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
441
475
|
const globalSddApply = await readFile(
|
|
442
476
|
join(globalAgentHome, "agents", "sdd-apply.md"),
|
|
@@ -605,6 +639,8 @@ async function run() {
|
|
|
605
639
|
assert.match(ctx.ui.notifications.at(-1).message, /Global Gentle AI SDD assets installed/);
|
|
606
640
|
assert.equal(existsSync(join(installCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
607
641
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
642
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
643
|
+
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
608
644
|
} finally {
|
|
609
645
|
await rm(installCwd, { recursive: true, force: true });
|
|
610
646
|
}
|
|
@@ -613,9 +649,11 @@ async function run() {
|
|
|
613
649
|
try {
|
|
614
650
|
await mkdir(join(staleAssetsCwd, ".pi", "agents"), { recursive: true });
|
|
615
651
|
await mkdir(join(staleAssetsCwd, ".pi", "chains"), { recursive: true });
|
|
652
|
+
await mkdir(join(staleAssetsCwd, ".pi", "gentle-ai", "support"), { recursive: true });
|
|
616
653
|
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-apply.md"), "stale apply\n");
|
|
617
654
|
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-spec.md"), "stale spec\n");
|
|
618
655
|
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
656
|
+
await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
|
|
619
657
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
620
658
|
await commands.get("gentle-ai:status").handler("", ctx);
|
|
621
659
|
assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD override drift: \d+ file\(s\)/);
|
|
@@ -638,7 +676,9 @@ async function run() {
|
|
|
638
676
|
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
639
677
|
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
640
678
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
679
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
641
680
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
681
|
+
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
642
682
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
643
683
|
assert.equal(ctx.ui.selections.length, 3);
|
|
644
684
|
assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtemp } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import {
|
|
8
|
+
listActiveOpenSpecChanges,
|
|
9
|
+
parseSddStatusCommandArgs,
|
|
10
|
+
renderPhaseInstructions,
|
|
11
|
+
renderSddStatusMarkdown,
|
|
12
|
+
resolveSddStatus,
|
|
13
|
+
} from "../lib/sdd-status.ts";
|
|
14
|
+
|
|
15
|
+
async function workspace(): Promise<string> {
|
|
16
|
+
return mkdtemp(join(tmpdir(), "gentle-pi-sdd-status-"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function write(path: string, content: string): void {
|
|
20
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
21
|
+
writeFileSync(path, content);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function seedChange(cwd: string, change = "add-auth"): string {
|
|
25
|
+
const root = join(cwd, "openspec", "changes", change);
|
|
26
|
+
write(join(root, "proposal.md"), "# Proposal\n");
|
|
27
|
+
write(join(root, "specs", "auth", "spec.md"), "# Auth Spec\n");
|
|
28
|
+
write(join(root, "design.md"), "# Design\n");
|
|
29
|
+
write(
|
|
30
|
+
join(root, "tasks.md"),
|
|
31
|
+
`# Tasks
|
|
32
|
+
|
|
33
|
+
- [x] 1.1 Build foundation
|
|
34
|
+
- [ ] 1.2 Wire routes
|
|
35
|
+
`,
|
|
36
|
+
);
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("listActiveOpenSpecChanges excludes archive and sorts active changes", async () => {
|
|
41
|
+
const cwd = await workspace();
|
|
42
|
+
mkdirSync(join(cwd, "openspec", "changes", "b-change"), { recursive: true });
|
|
43
|
+
mkdirSync(join(cwd, "openspec", "changes", "a-change"), { recursive: true });
|
|
44
|
+
mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-old"), { recursive: true });
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(listActiveOpenSpecChanges(cwd), ["a-change", "b-change"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("resolveSddStatus blocks when there are no active changes", async () => {
|
|
50
|
+
const cwd = await workspace();
|
|
51
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
52
|
+
|
|
53
|
+
const status = resolveSddStatus({ cwd });
|
|
54
|
+
|
|
55
|
+
assert.equal(status.changeName, null);
|
|
56
|
+
assert.match(status.blockedReasons[0], /No active SDD changes/);
|
|
57
|
+
assert.equal(status.dependencies.apply, "blocked");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("resolveSddStatus blocks when change selection is ambiguous", async () => {
|
|
61
|
+
const cwd = await workspace();
|
|
62
|
+
mkdirSync(join(cwd, "openspec", "changes", "first"), { recursive: true });
|
|
63
|
+
mkdirSync(join(cwd, "openspec", "changes", "second"), { recursive: true });
|
|
64
|
+
|
|
65
|
+
const status = resolveSddStatus({ cwd });
|
|
66
|
+
|
|
67
|
+
assert.equal(status.changeName, null);
|
|
68
|
+
assert.match(status.blockedReasons[0], /ambiguous/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("resolveSddStatus selects the only active change and counts task progress", async () => {
|
|
72
|
+
const cwd = await workspace();
|
|
73
|
+
const root = seedChange(cwd);
|
|
74
|
+
|
|
75
|
+
const status = resolveSddStatus({ cwd, includeInstructions: true });
|
|
76
|
+
|
|
77
|
+
assert.equal(status.changeName, "add-auth");
|
|
78
|
+
assert.equal(status.changeRoot, root);
|
|
79
|
+
assert.equal(status.artifacts.proposal, "done");
|
|
80
|
+
assert.equal(status.artifacts.specs, "done");
|
|
81
|
+
assert.deepEqual(status.taskProgress, {
|
|
82
|
+
total: 2,
|
|
83
|
+
complete: 1,
|
|
84
|
+
remaining: 1,
|
|
85
|
+
unchecked: ["- [ ] 1.2 Wire routes"],
|
|
86
|
+
});
|
|
87
|
+
assert.equal(status.applyState, "ready");
|
|
88
|
+
assert.equal(status.dependencies.apply, "ready");
|
|
89
|
+
assert.match(status.instructions?.apply.join("\n") ?? "", /persisted task checkboxes/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("resolveSddStatus marks apply all_done and verify ready when tasks are checked", async () => {
|
|
93
|
+
const cwd = await workspace();
|
|
94
|
+
const root = seedChange(cwd);
|
|
95
|
+
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Build foundation\n");
|
|
96
|
+
|
|
97
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
98
|
+
|
|
99
|
+
assert.equal(status.applyState, "all_done");
|
|
100
|
+
assert.equal(status.dependencies.apply, "all_done");
|
|
101
|
+
assert.equal(status.dependencies.verify, "ready");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("resolveSddStatus blocks sync when verify report is not clearly passing", async () => {
|
|
105
|
+
const cwd = await workspace();
|
|
106
|
+
const root = seedChange(cwd);
|
|
107
|
+
write(join(root, "apply-progress.md"), "# Apply\n\nSome work completed.\n");
|
|
108
|
+
write(join(root, "verify-report.md"), "# Verify\n\nTODO: tests not run yet\n");
|
|
109
|
+
|
|
110
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
111
|
+
|
|
112
|
+
assert.equal(status.dependencies.verify, "ready");
|
|
113
|
+
assert.equal(status.dependencies.sync, "blocked");
|
|
114
|
+
assert.equal(status.dependencies.archive, "blocked");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("resolveSddStatus rejects negated pass and sync-complete phrases", async () => {
|
|
118
|
+
const cwd = await workspace();
|
|
119
|
+
const root = seedChange(cwd);
|
|
120
|
+
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
121
|
+
write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
|
|
122
|
+
write(join(root, "sync-report.md"), "# Sync\n\nSync complete: no\n");
|
|
123
|
+
|
|
124
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
125
|
+
|
|
126
|
+
assert.equal(status.dependencies.verify, "ready");
|
|
127
|
+
assert.equal(status.dependencies.sync, "blocked");
|
|
128
|
+
assert.equal(status.dependencies.archive, "blocked");
|
|
129
|
+
assert.notEqual(status.nextRecommended, "sdd-archive");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("resolveSddStatus blocks sync when verify report contains critical text", async () => {
|
|
133
|
+
const cwd = await workspace();
|
|
134
|
+
const root = seedChange(cwd);
|
|
135
|
+
write(join(root, "verify-report.md"), "# Verify\n\nCRITICAL: missing tests\n");
|
|
136
|
+
|
|
137
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
138
|
+
|
|
139
|
+
assert.equal(status.dependencies.sync, "blocked");
|
|
140
|
+
assert.equal(status.dependencies.archive, "blocked");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("resolveSddStatus reports same-domain collisions", async () => {
|
|
144
|
+
const cwd = await workspace();
|
|
145
|
+
const root = seedChange(cwd, "current");
|
|
146
|
+
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
147
|
+
write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
|
|
148
|
+
write(join(cwd, "openspec", "changes", "other", "specs", "auth", "spec.md"), "# Other\n");
|
|
149
|
+
|
|
150
|
+
const status = resolveSddStatus({ cwd, changeName: "current" });
|
|
151
|
+
|
|
152
|
+
assert.deepEqual(status.collisions.map((collision) => collision.domain), ["auth"]);
|
|
153
|
+
assert.equal(status.collisions[0].changes[0].change, "other");
|
|
154
|
+
assert.equal(status.dependencies.sync, "blocked");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("resolveSddStatus blocks apply when tasks has no checkboxes", async () => {
|
|
158
|
+
const cwd = await workspace();
|
|
159
|
+
const root = seedChange(cwd);
|
|
160
|
+
write(join(root, "tasks.md"), "# Tasks\n\nImplementation notes only.\n");
|
|
161
|
+
|
|
162
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
163
|
+
|
|
164
|
+
assert.equal(status.taskProgress.total, 0);
|
|
165
|
+
assert.equal(status.applyState, "blocked");
|
|
166
|
+
assert.equal(status.dependencies.apply, "blocked");
|
|
167
|
+
assert.match(status.blockedReasons.join("\n"), /no implementation task checkboxes/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("resolveSddStatus marks legacy flat specs partial and blocks sync", async () => {
|
|
171
|
+
const cwd = await workspace();
|
|
172
|
+
write(join(cwd, "openspec", "changes", "legacy", "proposal.md"), "# Proposal\n");
|
|
173
|
+
write(join(cwd, "openspec", "changes", "legacy", "spec.md"), "# Flat\n");
|
|
174
|
+
write(join(cwd, "openspec", "changes", "legacy", "design.md"), "# Design\n");
|
|
175
|
+
write(join(cwd, "openspec", "changes", "legacy", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
176
|
+
write(join(cwd, "openspec", "changes", "legacy", "verify-report.md"), "# Verify\n\nPASS\n");
|
|
177
|
+
|
|
178
|
+
const status = resolveSddStatus({ cwd, changeName: "legacy" });
|
|
179
|
+
|
|
180
|
+
assert.equal(status.artifacts.specs, "partial");
|
|
181
|
+
assert.match(status.blockedReasons.join("\n"), /Legacy flat spec/);
|
|
182
|
+
assert.equal(status.dependencies.sync, "blocked");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("resolveSddStatus accepts nested domain specs even when a legacy flat spec also exists", async () => {
|
|
186
|
+
const cwd = await workspace();
|
|
187
|
+
write(join(cwd, "openspec", "changes", "mixed", "proposal.md"), "# Proposal\n");
|
|
188
|
+
write(join(cwd, "openspec", "changes", "mixed", "spec.md"), "# Flat\n");
|
|
189
|
+
write(join(cwd, "openspec", "changes", "mixed", "specs", "parent", "child", "spec.md"), "# Nested\n");
|
|
190
|
+
write(join(cwd, "openspec", "changes", "mixed", "design.md"), "# Design\n");
|
|
191
|
+
write(join(cwd, "openspec", "changes", "mixed", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
192
|
+
|
|
193
|
+
const status = resolveSddStatus({ cwd, changeName: "mixed" });
|
|
194
|
+
|
|
195
|
+
assert.equal(status.artifacts.specs, "done");
|
|
196
|
+
assert.equal(status.legacyFlatSpec?.hasDomainSpecs, true);
|
|
197
|
+
assert.doesNotMatch(status.blockedReasons.join("\n"), /Legacy flat spec/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("resolveSddStatus blocks sync when core artifacts are missing even with clean verify", async () => {
|
|
201
|
+
const cwd = await workspace();
|
|
202
|
+
write(join(cwd, "openspec", "changes", "thin", "proposal.md"), "# Proposal\n");
|
|
203
|
+
write(join(cwd, "openspec", "changes", "thin", "design.md"), "# Design\n");
|
|
204
|
+
write(join(cwd, "openspec", "changes", "thin", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
205
|
+
write(join(cwd, "openspec", "changes", "thin", "verify-report.md"), "# Verify\n\nPASS\n");
|
|
206
|
+
|
|
207
|
+
const status = resolveSddStatus({ cwd, changeName: "thin" });
|
|
208
|
+
|
|
209
|
+
assert.match(status.blockedReasons.join("\n"), /domain specs are missing or partial/);
|
|
210
|
+
assert.equal(status.dependencies.sync, "blocked");
|
|
211
|
+
assert.notEqual(status.nextRecommended, "sdd-sync");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("resolveSddStatus blocks stale sync report when current verify is not passing", async () => {
|
|
215
|
+
const cwd = await workspace();
|
|
216
|
+
const root = seedChange(cwd);
|
|
217
|
+
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
218
|
+
write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
|
|
219
|
+
write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
|
|
220
|
+
|
|
221
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
222
|
+
|
|
223
|
+
assert.equal(status.dependencies.verify, "ready");
|
|
224
|
+
assert.equal(status.dependencies.sync, "blocked");
|
|
225
|
+
assert.equal(status.dependencies.archive, "blocked");
|
|
226
|
+
assert.notEqual(status.nextRecommended, "sdd-archive");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("resolveSddStatus blocks archive when required artifacts are missing", async () => {
|
|
230
|
+
const cwd = await workspace();
|
|
231
|
+
write(join(cwd, "openspec", "changes", "thin", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
232
|
+
write(join(cwd, "openspec", "changes", "thin", "verify-report.md"), "# Verify\n\nPASS\n");
|
|
233
|
+
write(join(cwd, "openspec", "changes", "thin", "sync-report.md"), "# Sync\n\nPASS\n");
|
|
234
|
+
|
|
235
|
+
const status = resolveSddStatus({ cwd, changeName: "thin" });
|
|
236
|
+
|
|
237
|
+
assert.match(status.blockedReasons.join("\n"), /proposal\.md is missing/);
|
|
238
|
+
assert.equal(status.dependencies.archive, "blocked");
|
|
239
|
+
assert.notEqual(status.nextRecommended, "sdd-archive");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("resolveSddStatus reports partial core artifacts as blockers", async () => {
|
|
243
|
+
const cwd = await workspace();
|
|
244
|
+
const root = seedChange(cwd);
|
|
245
|
+
write(join(root, "proposal.md"), "");
|
|
246
|
+
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
247
|
+
write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
|
|
248
|
+
write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
|
|
249
|
+
|
|
250
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
251
|
+
|
|
252
|
+
assert.equal(status.artifacts.proposal, "partial");
|
|
253
|
+
assert.match(status.blockedReasons.join("\n"), /proposal\.md is empty or partial/);
|
|
254
|
+
assert.equal(status.dependencies.archive, "blocked");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("resolveSddStatus marks archive ready only after clean verify, sync, and complete tasks", async () => {
|
|
258
|
+
const cwd = await workspace();
|
|
259
|
+
const root = seedChange(cwd);
|
|
260
|
+
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
261
|
+
write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
|
|
262
|
+
write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
|
|
263
|
+
|
|
264
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
265
|
+
|
|
266
|
+
assert.equal(status.dependencies.archive, "ready");
|
|
267
|
+
assert.equal(status.nextRecommended, "sdd-archive");
|
|
268
|
+
assert.match(renderPhaseInstructions(status).archive.join("\n"), /CRITICAL verification issues have no override/);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("renderSddStatusMarkdown includes structured JSON", async () => {
|
|
272
|
+
const cwd = await workspace();
|
|
273
|
+
seedChange(cwd);
|
|
274
|
+
|
|
275
|
+
const markdown = renderSddStatusMarkdown(resolveSddStatus({ cwd }));
|
|
276
|
+
|
|
277
|
+
assert.match(markdown, /## SDD Status: add-auth/);
|
|
278
|
+
assert.match(markdown, /```json/);
|
|
279
|
+
assert.match(markdown, /"schemaName": "gentle-pi.sdd-status"/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("parseSddStatusCommandArgs extracts change and json flag", () => {
|
|
283
|
+
assert.deepEqual(parseSddStatusCommandArgs("add-auth --json"), {
|
|
284
|
+
changeName: "add-auth",
|
|
285
|
+
json: true,
|
|
286
|
+
});
|
|
287
|
+
assert.deepEqual(parseSddStatusCommandArgs("--json"), {
|
|
288
|
+
changeName: undefined,
|
|
289
|
+
json: true,
|
|
290
|
+
});
|
|
291
|
+
});
|