gentle-pi 2.6.4 → 3.0.0

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.
Files changed (128) hide show
  1. package/README.md +25 -7
  2. package/assets/agents/gentle-ai-worker.md +5 -1
  3. package/assets/agents/sdd-apply.md +9 -7
  4. package/assets/agents/sdd-archive.md +42 -23
  5. package/assets/agents/sdd-proposal.md +2 -2
  6. package/assets/agents/sdd-remediate.md +4 -4
  7. package/assets/agents/sdd-research.md +20 -48
  8. package/assets/agents/sdd-tasks.md +5 -5
  9. package/assets/agents/sdd-verify.md +6 -28
  10. package/assets/chains/sdd-full.chain.md +4 -22
  11. package/assets/chains/sdd-verify.chain.md +3 -12
  12. package/assets/orchestrator-delegation.md +33 -3
  13. package/assets/orchestrator-memory.md +20 -7
  14. package/assets/orchestrator.md +5 -3
  15. package/assets/sdd-orchestrator-workflow.md +25 -58
  16. package/assets/support/sdd-status-contract.md +9 -12
  17. package/docs/gentle-shell.md +41 -19
  18. package/docs/readme-reference.md +175 -36
  19. package/extensions/codegraph-tools.ts +2 -0
  20. package/extensions/gentle-agents.ts +289 -361
  21. package/extensions/gentle-ai.ts +588 -117
  22. package/extensions/gentle-shell.ts +123 -97
  23. package/extensions/pi-pretty.ts +63 -14
  24. package/extensions/quiet-tools.ts +1 -2
  25. package/extensions/startup-banner.ts +10 -9
  26. package/lib/agent-home.ts +8 -0
  27. package/lib/agent-profile-pin.ts +336 -0
  28. package/lib/agent-profiles.ts +28 -8
  29. package/lib/agents-config.ts +24 -2
  30. package/lib/agents-history.ts +3 -97
  31. package/lib/agents-keys.ts +27 -0
  32. package/lib/agents-protocol.ts +2 -15
  33. package/lib/agents-runner.ts +74 -113
  34. package/lib/agents-session-transport.ts +691 -0
  35. package/lib/command-palette-catalog.ts +87 -0
  36. package/lib/command-palette.ts +346 -0
  37. package/lib/native-choice-list.ts +5 -0
  38. package/lib/native-review-cli.ts +19 -97
  39. package/lib/review-publication-gate.ts +11 -1
  40. package/lib/review-repository.ts +1 -1
  41. package/lib/review-snapshot.ts +1 -0
  42. package/lib/review-transaction.ts +4 -2
  43. package/lib/sdd-preflight.ts +2 -1
  44. package/lib/sdd-research-capabilities.ts +18 -152
  45. package/lib/sdd-status.ts +7 -779
  46. package/lib/session-change-capture.ts +88 -0
  47. package/lib/session-changes.ts +147 -0
  48. package/lib/shell-bar.ts +24 -10
  49. package/lib/shell-card.ts +8 -12
  50. package/lib/shell-changes-view.ts +2 -1
  51. package/lib/shell-changes.ts +5 -2
  52. package/lib/shell-prompt.ts +25 -8
  53. package/lib/shell-sidebar-banner.ts +2 -2
  54. package/lib/shell-sidebar-layout.ts +5 -2
  55. package/lib/windows-session-transport.ts +877 -0
  56. package/package.json +3 -3
  57. package/runtime/native-review-cli.mjs +18 -96
  58. package/runtime/windows-session-transport.ps1 +791 -0
  59. package/scripts/gentle-ai-installer.mjs +10 -10
  60. package/scripts/test-packed-runner.mjs +1668 -20
  61. package/scripts/verify-package-files.mjs +2 -3
  62. package/tests/agent-home.test.ts +52 -0
  63. package/tests/agent-profiles.test.ts +30 -1
  64. package/tests/agents-config.test.ts +44 -0
  65. package/tests/agents-history.test.ts +12 -24
  66. package/tests/agents-runner.test.ts +321 -58
  67. package/tests/agents-session-transport-process.test.ts +249 -0
  68. package/tests/agents-session-transport.test.ts +823 -0
  69. package/tests/artifact-language.test.ts +10 -7
  70. package/tests/command-palette.test.ts +378 -0
  71. package/tests/delegated-key-learnings-contract.test.ts +2 -2
  72. package/tests/fixtures/agents-session-transport-process.mjs +108 -0
  73. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
  74. package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
  75. package/tests/fixtures/windows-session-compile.ps1 +110 -0
  76. package/tests/gentle-agents.test.ts +883 -356
  77. package/tests/gentle-ai-binary.test.ts +1 -1
  78. package/tests/gentle-ai-installer.test.ts +47 -47
  79. package/tests/gentle-ai.test.ts +472 -4
  80. package/tests/gentle-shell.test.ts +338 -205
  81. package/tests/native-choice-list.test.ts +13 -0
  82. package/tests/native-review-capability-contract.test.ts +15 -1
  83. package/tests/native-review-cli.test.ts +0 -33
  84. package/tests/odd-routing-contract.test.ts +208 -0
  85. package/tests/orchestrator-budget.test.ts +17 -2
  86. package/tests/package-manifest.test.ts +119 -31
  87. package/tests/persona-single-channel.test.ts +3 -3
  88. package/tests/pi-pretty.test.ts +45 -0
  89. package/tests/profile-pin.test.ts +370 -0
  90. package/tests/quiet-tool-rendering.test.ts +32 -5
  91. package/tests/review-contract-prompt.test.ts +9 -0
  92. package/tests/review-controller.test.ts +0 -44
  93. package/tests/review-session-standing-permission-ipc.test.ts +427 -13
  94. package/tests/runtime-harness.mjs +4 -4
  95. package/tests/sdd-agent-tools.test.ts +15 -36
  96. package/tests/sdd-archive-replay.test.ts +82 -0
  97. package/tests/sdd-classical-continuation.test.ts +74 -0
  98. package/tests/sdd-execution-routing-contract.test.ts +18 -2
  99. package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
  100. package/tests/sdd-native-managed-uptake.test.ts +11 -21
  101. package/tests/sdd-no-attempts-contract.test.ts +15 -0
  102. package/tests/sdd-odd-integration.test.ts +33 -0
  103. package/tests/sdd-optional-research.test.ts +124 -0
  104. package/tests/sdd-planning-routing-contract.test.ts +1 -1
  105. package/tests/sdd-preflight-rpc-input.test.ts +125 -0
  106. package/tests/sdd-preflight.test.ts +1 -1
  107. package/tests/sdd-research-capabilities.test.ts +20 -162
  108. package/tests/sdd-selection-transport.test.ts +180 -88
  109. package/tests/sdd-status.test.ts +5 -778
  110. package/tests/sdd-task-truth.test.ts +43 -0
  111. package/tests/session-change-capture.test.ts +86 -0
  112. package/tests/session-changes-shell.test.ts +38 -0
  113. package/tests/session-changes.test.ts +114 -0
  114. package/tests/shell-bar.test.ts +35 -0
  115. package/tests/shell-card.test.ts +8 -6
  116. package/tests/shell-changes.test.ts +8 -0
  117. package/tests/shell-prompt.test.ts +41 -7
  118. package/tests/shell-sidebar-banner.test.ts +4 -4
  119. package/tests/shell-sidebar-layout.test.ts +97 -13
  120. package/tests/startup-banner.test.ts +55 -2
  121. package/tests/windows-hidden-processes.test.ts +303 -0
  122. package/tests/windows-session-bootstrap.test.ts +1772 -0
  123. package/tests/windows-session-compile.test.ts +170 -0
  124. package/tests/windows-session-transport.test.ts +754 -0
  125. package/assets/agents/sdd-sync.md +0 -146
  126. package/lib/openspec-guardrails.ts +0 -99
  127. package/tests/native-sdd-attempt-authority.test.ts +0 -240
  128. package/tests/openspec-guardrails.test.ts +0 -71
@@ -1,782 +1,9 @@
1
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
2
  import test from "node:test";
7
- import {
8
- isNonAuthoritativeStatus,
9
- listActiveOpenSpecChanges,
10
- parseSddStatusCommandArgs,
11
- renderNativeSddPhasePrompt,
12
- renderPhaseInstructions,
13
- renderSddDispatcherMarkdown,
14
- renderSddStatusMarkdown,
15
- resolveSddStatus,
16
- } from "../lib/sdd-status.ts";
3
+ import { parseSddStatusCommandArgs } from "../lib/sdd-status.ts";
17
4
 
18
- async function workspace(): Promise<string> {
19
- return mkdtemp(join(tmpdir(), "gentle-pi-sdd-status-"));
20
- }
21
-
22
- function write(path: string, content: string): void {
23
- mkdirSync(dirname(path), { recursive: true });
24
- writeFileSync(path, content);
25
- }
26
-
27
- function seedChange(cwd: string, change = "add-auth"): string {
28
- const root = join(cwd, "openspec", "changes", change);
29
- write(join(root, "proposal.md"), "# Proposal\n");
30
- write(join(root, "specs", "auth", "spec.md"), "# Auth Spec\n");
31
- write(join(root, "design.md"), "# Design\n");
32
- write(
33
- join(root, "tasks.md"),
34
- `# Tasks
35
-
36
- - [x] 1.1 Build foundation
37
- - [ ] 1.2 Wire routes
38
- `,
39
- );
40
- return root;
41
- }
42
-
43
- test("planning recommendations are executable tokens with separate diagnostics", async () => {
44
- const cwd = await workspace();
45
- const root = join(cwd, "openspec", "changes", "planning");
46
- mkdirSync(root, { recursive: true });
47
- for (const [route, diagnostic, artifact] of [
48
- ["sdd-propose", "proposal.md is missing.", "proposal.md"],
49
- ["sdd-spec", "domain specs are missing or partial.", "specs/auth/spec.md"],
50
- ["sdd-design", "design.md is missing.", "design.md"],
51
- ["sdd-tasks", "tasks.md is missing.", "tasks.md"],
52
- ] as const) {
53
- const status = resolveSddStatus({ cwd });
54
- assert.equal(status.nextRecommended, route);
55
- assert.ok(status.blockedReasons.includes(diagnostic));
56
- assert.equal(status.dependencies.apply, "blocked");
57
- write(join(root, artifact), "# Artifact\n- [ ] Implement\n");
58
- }
59
- assert.equal(resolveSddStatus({ cwd }).nextRecommended, "sdd-apply");
60
- });
61
-
62
- test("partial planning artifacts route to their repair phase", async () => {
63
- for (const [artifact, route] of [
64
- ["proposal.md", "sdd-propose"],
65
- ["specs/auth/spec.md", "sdd-spec"],
66
- ["design.md", "sdd-design"],
67
- ["tasks.md", "sdd-tasks"],
68
- ] as const) {
69
- const cwd = await workspace();
70
- const root = seedChange(cwd);
71
- write(join(root, artifact), " \n");
72
- const status = resolveSddStatus({ cwd });
73
- assert.equal(status.nextRecommended, route);
74
- assert.ok(status.blockedReasons.length > 0);
75
- assert.equal(status.applyState, "blocked");
76
- assert.match(renderSddDispatcherMarkdown(status), new RegExp(`nextPhase: ${route}`));
77
- }
78
- });
79
-
80
- test("unresolved change selection returns blocked rather than diagnostic prose", async () => {
81
- const cwd = await workspace();
82
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
83
- assert.equal(resolveSddStatus({ cwd }).nextRecommended, "blocked");
84
- assert.equal(resolveSddStatus({ cwd, changeName: "absent" }).nextRecommended, "blocked");
85
- seedChange(cwd, "first");
86
- seedChange(cwd, "second");
87
- const ambiguous = resolveSddStatus({ cwd });
88
- assert.equal(ambiguous.nextRecommended, "blocked");
89
- assert.match(ambiguous.blockedReasons[0], /ambiguous/);
90
- });
91
-
92
- test("listActiveOpenSpecChanges excludes archive and sorts active changes", async () => {
93
- const cwd = await workspace();
94
- mkdirSync(join(cwd, "openspec", "changes", "b-change"), { recursive: true });
95
- mkdirSync(join(cwd, "openspec", "changes", "a-change"), { recursive: true });
96
- mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-old"), { recursive: true });
97
-
98
- assert.deepEqual(listActiveOpenSpecChanges(cwd), ["a-change", "b-change"]);
99
- });
100
-
101
- test("resolveSddStatus blocks when there are no active changes", async () => {
102
- const cwd = await workspace();
103
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
104
-
105
- const status = resolveSddStatus({ cwd });
106
-
107
- assert.equal(status.changeName, null);
108
- assert.match(status.blockedReasons[0], /No active SDD changes/);
109
- assert.equal(status.dependencies.apply, "blocked");
110
- });
111
-
112
- test("resolveSddStatus blocks when change selection is ambiguous", async () => {
113
- const cwd = await workspace();
114
- mkdirSync(join(cwd, "openspec", "changes", "first"), { recursive: true });
115
- mkdirSync(join(cwd, "openspec", "changes", "second"), { recursive: true });
116
-
117
- const status = resolveSddStatus({ cwd });
118
-
119
- assert.equal(status.changeName, null);
120
- assert.match(status.blockedReasons[0], /ambiguous/);
121
- });
122
-
123
- test("resolveSddStatus selects the only active change and counts task progress", async () => {
124
- const cwd = await workspace();
125
- const root = seedChange(cwd);
126
-
127
- const status = resolveSddStatus({ cwd, includeInstructions: true });
128
-
129
- assert.equal(status.changeName, "add-auth");
130
- assert.equal(status.changeRoot, root);
131
- assert.equal(status.artifacts.proposal, "done");
132
- assert.equal(status.artifacts.specs, "done");
133
- assert.deepEqual(status.taskProgress, {
134
- total: 2,
135
- complete: 1,
136
- remaining: 1,
137
- unchecked: ["- [ ] 1.2 Wire routes"],
138
- });
139
- assert.equal(status.applyState, "ready");
140
- assert.equal(status.dependencies.apply, "ready");
141
- assert.match(status.instructions?.apply.join("\n") ?? "", /persisted task checkboxes/);
142
- });
143
-
144
- test("resolveSddStatus routes completed implementation through archive without RDD authority", async () => {
145
- const cwd = await workspace();
146
- const root = seedChange(cwd);
147
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
148
- write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
149
- write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
150
-
151
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
152
-
153
- assert.equal(status.applyState, "all_done");
154
- assert.equal(status.dependencies.verify, "all_done");
155
- assert.equal(status.dependencies.sync, "all_done");
156
- assert.equal(status.dependencies.archive, "ready");
157
- assert.equal(status.nextRecommended, "sdd-archive");
158
- assert.equal(status.blockedReasons.some((reason) => reason.startsWith("resolve-review:")), false);
159
- });
160
-
161
- test("resolveSddStatus ignores legacy parent review actions after implementation", async () => {
162
- const cwd = await workspace();
163
- const root = seedChange(cwd);
164
- write(
165
- join(root, "tasks.md"),
166
- "# Tasks\n\n- [x] 1.1 Build foundation <!-- sdd-owner: implementation -->\n- [ ] Start bounded review. <!-- sdd-owner: parent -->\n",
167
- );
168
-
169
- const status = resolveSddStatus({ cwd, changeName: "add-auth", includeInstructions: true });
170
-
171
- assert.deepEqual(status.taskProgress, { total: 1, complete: 1, remaining: 0, unchecked: [] });
172
- assert.deepEqual(status.deferredParentActions, {
173
- total: 1,
174
- complete: 0,
175
- remaining: 1,
176
- unchecked: ["- [ ] Start bounded review. <!-- sdd-owner: parent -->"],
177
- });
178
- assert.deepEqual(status.taskArtifactErrors, []);
179
- assert.equal(status.applyState, "all_done");
180
- assert.equal(status.dependencies.apply, "all_done");
181
- assert.equal(status.dependencies.verify, "ready");
182
- assert.equal(status.nextRecommended, "sdd-verify");
183
- });
184
-
185
- test("resolveSddStatus treats malformed ownership as unresolved implementation work", async () => {
186
- const cwd = await workspace();
187
- const root = seedChange(cwd);
188
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] Completed but malformed. <!-- sdd-owner: Parent -->\n");
189
-
190
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
191
-
192
- assert.deepEqual(status.taskProgress, {
193
- total: 1,
194
- complete: 0,
195
- remaining: 1,
196
- unchecked: ["- [x] Completed but malformed. <!-- sdd-owner: Parent -->"],
197
- });
198
- assert.equal(status.applyState, "blocked");
199
- assert.equal(status.nextRecommended, "fix-task-ownership-marker");
200
- assert.match(status.taskArtifactErrors.join("\n"), /Completed but malformed/);
201
- assert.match(status.blockedReasons.join("\n"), /task ownership marker/i);
202
- });
203
-
204
- test("resolveSddStatus routes completed legacy implementation directly to verify", async () => {
205
- const cwd = await workspace();
206
- const root = seedChange(cwd);
207
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Build foundation\n");
208
-
209
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
210
-
211
- assert.equal(status.applyState, "all_done");
212
- assert.equal(status.dependencies.apply, "all_done");
213
- assert.equal(status.dependencies.verify, "ready");
214
- assert.equal(status.nextRecommended, "sdd-verify");
215
- });
216
-
217
- test("resolveSddStatus blocks sync when verify report is not clearly passing", async () => {
218
- const cwd = await workspace();
219
- const root = seedChange(cwd);
220
- write(join(root, "apply-progress.md"), "# Apply\n\nSome work completed.\n");
221
- write(join(root, "verify-report.md"), "# Verify\n\nTODO: tests not run yet\n");
222
-
223
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
224
-
225
- assert.equal(status.dependencies.verify, "ready");
226
- assert.equal(status.dependencies.sync, "blocked");
227
- assert.equal(status.dependencies.archive, "blocked");
228
- });
229
-
230
- test("resolveSddStatus rejects negated pass and sync-complete phrases", async () => {
231
- const cwd = await workspace();
232
- const root = seedChange(cwd);
233
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
234
- write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
235
- write(join(root, "sync-report.md"), "# Sync\n\nSync complete: no\n");
236
-
237
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
238
-
239
- assert.equal(status.dependencies.verify, "ready");
240
- assert.equal(status.dependencies.sync, "blocked");
241
- assert.equal(status.dependencies.archive, "blocked");
242
- assert.notEqual(status.nextRecommended, "sdd-archive");
243
- });
244
-
245
- test("resolveSddStatus blocks sync when verify report contains critical text", async () => {
246
- const cwd = await workspace();
247
- const root = seedChange(cwd);
248
- write(join(root, "verify-report.md"), "# Verify\n\nCRITICAL: missing tests\n");
249
-
250
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
251
-
252
- assert.equal(status.dependencies.sync, "blocked");
253
- assert.equal(status.dependencies.archive, "blocked");
254
- });
255
-
256
- test("resolveSddStatus reports same-domain collisions", async () => {
257
- const cwd = await workspace();
258
- const root = seedChange(cwd, "current");
259
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
260
- write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
261
- write(join(cwd, "openspec", "changes", "other", "specs", "auth", "spec.md"), "# Other\n");
262
-
263
- const status = resolveSddStatus({ cwd, changeName: "current" });
264
-
265
- assert.deepEqual(status.collisions.map((collision) => collision.domain), ["auth"]);
266
- assert.equal(status.collisions[0].changes[0].change, "other");
267
- assert.equal(status.dependencies.sync, "blocked");
268
- });
269
-
270
- test("resolveSddStatus blocks apply when tasks has no checkboxes", async () => {
271
- const cwd = await workspace();
272
- const root = seedChange(cwd);
273
- write(join(root, "tasks.md"), "# Tasks\n\nImplementation notes only.\n");
274
-
275
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
276
-
277
- assert.equal(status.taskProgress.total, 0);
278
- assert.equal(status.applyState, "blocked");
279
- assert.equal(status.dependencies.apply, "blocked");
280
- assert.match(status.blockedReasons.join("\n"), /no implementation task checkboxes/);
281
- assert.equal(status.nextRecommended, "sdd-tasks");
282
- });
283
-
284
- test("resolveSddStatus marks legacy flat specs partial and blocks sync", async () => {
285
- const cwd = await workspace();
286
- write(join(cwd, "openspec", "changes", "legacy", "proposal.md"), "# Proposal\n");
287
- write(join(cwd, "openspec", "changes", "legacy", "spec.md"), "# Flat\n");
288
- write(join(cwd, "openspec", "changes", "legacy", "design.md"), "# Design\n");
289
- write(join(cwd, "openspec", "changes", "legacy", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
290
- write(join(cwd, "openspec", "changes", "legacy", "verify-report.md"), "# Verify\n\nPASS\n");
291
-
292
- const status = resolveSddStatus({ cwd, changeName: "legacy" });
293
-
294
- assert.equal(status.artifacts.specs, "partial");
295
- assert.equal(status.nextRecommended, "sdd-spec");
296
- assert.match(status.blockedReasons.join("\n"), /Legacy flat spec/);
297
- assert.equal(status.dependencies.sync, "blocked");
298
- });
299
-
300
- test("resolveSddStatus accepts nested domain specs even when a legacy flat spec also exists", async () => {
301
- const cwd = await workspace();
302
- write(join(cwd, "openspec", "changes", "mixed", "proposal.md"), "# Proposal\n");
303
- write(join(cwd, "openspec", "changes", "mixed", "spec.md"), "# Flat\n");
304
- write(join(cwd, "openspec", "changes", "mixed", "specs", "parent", "child", "spec.md"), "# Nested\n");
305
- write(join(cwd, "openspec", "changes", "mixed", "design.md"), "# Design\n");
306
- write(join(cwd, "openspec", "changes", "mixed", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
307
-
308
- const status = resolveSddStatus({ cwd, changeName: "mixed" });
309
-
310
- assert.equal(status.artifacts.specs, "done");
311
- assert.equal(status.legacyFlatSpec?.hasDomainSpecs, true);
312
- assert.doesNotMatch(status.blockedReasons.join("\n"), /Legacy flat spec/);
313
- });
314
-
315
- test("resolveSddStatus blocks sync when core artifacts are missing even with clean verify", async () => {
316
- const cwd = await workspace();
317
- write(join(cwd, "openspec", "changes", "thin", "proposal.md"), "# Proposal\n");
318
- write(join(cwd, "openspec", "changes", "thin", "design.md"), "# Design\n");
319
- write(join(cwd, "openspec", "changes", "thin", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
320
- write(join(cwd, "openspec", "changes", "thin", "verify-report.md"), "# Verify\n\nPASS\n");
321
-
322
- const status = resolveSddStatus({ cwd, changeName: "thin" });
323
-
324
- assert.match(status.blockedReasons.join("\n"), /domain specs are missing or partial/);
325
- assert.equal(status.dependencies.sync, "blocked");
326
- assert.notEqual(status.nextRecommended, "sdd-sync");
327
- });
328
-
329
- test("resolveSddStatus blocks stale sync report when current verify is not passing", async () => {
330
- const cwd = await workspace();
331
- const root = seedChange(cwd);
332
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
333
- write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
334
- write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
335
-
336
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
337
-
338
- assert.equal(status.dependencies.verify, "ready");
339
- assert.equal(status.dependencies.sync, "blocked");
340
- assert.equal(status.dependencies.archive, "blocked");
341
- assert.notEqual(status.nextRecommended, "sdd-archive");
342
- });
343
-
344
- test("resolveSddStatus blocks archive when required artifacts are missing", async () => {
345
- const cwd = await workspace();
346
- write(join(cwd, "openspec", "changes", "thin", "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
347
- write(join(cwd, "openspec", "changes", "thin", "verify-report.md"), "# Verify\n\nPASS\n");
348
- write(join(cwd, "openspec", "changes", "thin", "sync-report.md"), "# Sync\n\nPASS\n");
349
-
350
- const status = resolveSddStatus({ cwd, changeName: "thin" });
351
-
352
- assert.match(status.blockedReasons.join("\n"), /proposal\.md is missing/);
353
- assert.equal(status.dependencies.archive, "blocked");
354
- assert.notEqual(status.nextRecommended, "sdd-archive");
355
- });
356
-
357
- test("resolveSddStatus reports partial core artifacts as blockers", async () => {
358
- const cwd = await workspace();
359
- const root = seedChange(cwd);
360
- write(join(root, "proposal.md"), "");
361
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
362
- write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
363
- write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
364
-
365
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
366
-
367
- assert.equal(status.artifacts.proposal, "partial");
368
- assert.match(status.blockedReasons.join("\n"), /proposal\.md is empty or partial/);
369
- assert.equal(status.dependencies.archive, "blocked");
370
- });
371
-
372
- test("resolveSddStatus marks archive ready only after clean verify, sync, and complete tasks", async () => {
373
- const cwd = await workspace();
374
- const root = seedChange(cwd);
375
- write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
376
- write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
377
- write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
378
-
379
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
380
-
381
- assert.equal(status.dependencies.archive, "ready");
382
- assert.equal(status.nextRecommended, "sdd-archive");
383
- assert.match(renderPhaseInstructions(status).archive.join("\n"), /CRITICAL verification issues have no override/);
384
- });
385
-
386
- test("renderSddStatusMarkdown includes structured JSON", async () => {
387
- const cwd = await workspace();
388
- seedChange(cwd);
389
-
390
- const markdown = renderSddStatusMarkdown(resolveSddStatus({ cwd }));
391
-
392
- assert.match(markdown, /## SDD Status: add-auth/);
393
- assert.match(markdown, /```json/);
394
- assert.match(markdown, /"schemaName": "gentle-pi.sdd-status"/);
395
- });
396
-
397
- test("resolveSddStatus with artifactStore engram returns non-authoritative status without disk scan", async () => {
398
- const cwd = await workspace();
399
- // No openspec directory — simulates an engram-only session
400
-
401
- const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "my-change" });
402
-
403
- assert.equal(status.artifactStore, "engram");
404
- assert.equal(status.changeName, "my-change");
405
- assert.deepEqual(status.blockedReasons, []);
406
- assert.equal(status.nextRecommended, "resolve-via-engram");
407
- assert.equal(status.dependencies.apply, "not_applicable");
408
- assert.equal(status.applyState, "not_applicable");
409
- });
410
-
411
- test("resolveSddStatus with artifactStore none returns non-authoritative status without disk scan", async () => {
412
- const cwd = await workspace();
413
- // No openspec directory
414
-
415
- const status = resolveSddStatus({ cwd, artifactStore: "none", changeName: "my-change" });
416
-
417
- assert.equal(status.artifactStore, "none");
418
- assert.deepEqual(status.blockedReasons, []);
419
- assert.equal(status.nextRecommended, "resolve-via-engram");
420
- });
421
-
422
- test("resolveSddStatus with artifactStore hybrid uses disk scan and reflects store", async () => {
423
- const cwd = await workspace();
424
- seedChange(cwd);
425
-
426
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
427
-
428
- assert.equal(status.artifactStore, "hybrid");
429
- assert.equal(status.changeName, "add-auth");
430
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
431
- });
432
-
433
- test("resolveSddStatus with undefined store and existing openspec dir defaults to openspec and blocks", async () => {
434
- const cwd = await workspace();
435
- // Create openspec/ directory to signal an openspec workspace
436
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
437
-
438
- const status = resolveSddStatus({ cwd });
439
-
440
- // openspec workspace with no active changes → blocked (back-compat)
441
- assert.equal(status.artifactStore, "openspec");
442
- assert.match(status.blockedReasons[0] ?? "", /No active SDD changes/);
443
- });
444
-
445
- test("resolveSddStatus with undefined store and NO openspec dir returns non-authoritative status", async () => {
446
- const cwd = await workspace();
447
- // No openspec directory at all — unknown store, no disk evidence
448
-
449
- const status = resolveSddStatus({ cwd });
450
-
451
- // Safety net: should not emit the openspec false-block
452
- assert.equal(status.artifactStore, "none");
453
- assert.deepEqual(status.blockedReasons, []);
454
- assert.equal(status.nextRecommended, "resolve-via-engram");
455
- assert.equal(status.applyState, "not_applicable");
456
- });
457
-
458
- test("resolveSddStatus non-authoritative status has neutral planningHome (no misleading openspec path)", async () => {
459
- const cwd = await workspace();
460
-
461
- const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "fix-auth" });
462
-
463
- assert.equal(status.planningHome.changesDir, "");
464
- assert.equal(status.planningHome.root, status.actionContext.workspaceRoot);
465
- });
466
-
467
- test("parseSddStatusCommandArgs extracts change and json flag", () => {
468
- assert.deepEqual(parseSddStatusCommandArgs("add-auth --json"), {
469
- changeName: "add-auth",
470
- json: true,
471
- });
472
- assert.deepEqual(parseSddStatusCommandArgs("--json"), {
473
- changeName: undefined,
474
- json: true,
475
- });
476
- });
477
-
478
- test("resolveSddStatus with artifactStore hybrid and NO openspec dir returns non-authoritative status", async () => {
479
- const cwd = await workspace();
480
- // No openspec directory — the hybrid store without disk backing is non-authoritative
481
-
482
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "my-change" });
483
-
484
- assert.equal(status.artifactStore, "hybrid");
485
- assert.equal(status.changeName, "my-change");
486
- assert.deepEqual(status.blockedReasons, []);
487
- assert.equal(status.nextRecommended, "resolve-via-engram");
488
- assert.equal(status.applyState, "not_applicable");
489
- assert.equal(status.dependencies.apply, "not_applicable");
490
- assert.equal(status.dependencies.archive, "not_applicable");
491
- });
492
-
493
- test("resolveSddStatus with artifactStore hybrid and existing openspec dir runs authoritative disk scan", async () => {
494
- const cwd = await workspace();
495
- seedChange(cwd);
496
-
497
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
498
-
499
- assert.equal(status.artifactStore, "hybrid");
500
- assert.equal(status.changeName, "add-auth");
501
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
502
- assert.equal(status.artifacts.proposal, "done");
503
- });
504
-
505
- // Renamed: previously "returns true only when nextRecommended is resolve-via-engram" — now
506
- // explicitly asserts BOTH the typed isNonAuthoritative field and the sentinel together.
507
- test("isNonAuthoritativeStatus reads typed isNonAuthoritative field and matches resolve-via-engram sentinel", async () => {
508
- const cwd = await workspace();
509
-
510
- const engram = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "x" });
511
- assert.equal(engram.isNonAuthoritative, true);
512
- assert.equal(isNonAuthoritativeStatus(engram), true);
513
- assert.equal(engram.nextRecommended, "resolve-via-engram");
514
-
515
- const none = resolveSddStatus({ cwd, artifactStore: "none", changeName: "x" });
516
- assert.equal(none.isNonAuthoritative, true);
517
- assert.equal(isNonAuthoritativeStatus(none), true);
518
- assert.equal(none.nextRecommended, "resolve-via-engram");
519
-
520
- const bothWithoutOpenspec = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "x" });
521
- assert.equal(bothWithoutOpenspec.isNonAuthoritative, true);
522
- assert.equal(isNonAuthoritativeStatus(bothWithoutOpenspec), true);
523
- assert.equal(bothWithoutOpenspec.nextRecommended, "resolve-via-engram");
524
-
525
- seedChange(cwd);
526
- const bothWithOpenspec = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
527
- assert.equal(bothWithOpenspec.isNonAuthoritative, false);
528
- assert.equal(isNonAuthoritativeStatus(bothWithOpenspec), false);
529
- assert.notEqual(bothWithOpenspec.nextRecommended, "resolve-via-engram");
530
- });
531
-
532
- // Fix 4 item 4 — isNonAuthoritative boolean is set correctly on the typed field
533
- test("isNonAuthoritative boolean field is set correctly across all store/disk combinations", async () => {
534
- const cwd = await workspace();
535
-
536
- // engram → non-authoritative
537
- const engram = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "x" });
538
- assert.equal(engram.isNonAuthoritative, true);
539
-
540
- // none → non-authoritative
541
- const none = resolveSddStatus({ cwd, artifactStore: "none", changeName: "x" });
542
- assert.equal(none.isNonAuthoritative, true);
543
-
544
- // both without openspec/ → non-authoritative
545
- const bothWithout = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "x" });
546
- assert.equal(bothWithout.isNonAuthoritative, true);
547
-
548
- // both WITH openspec/ and seeded change → authoritative
549
- seedChange(cwd);
550
- const bothWith = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
551
- assert.equal(bothWith.isNonAuthoritative, false);
552
-
553
- // openspec (default disk scan, seeded) → authoritative
554
- const openspec = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "add-auth" });
555
- assert.equal(openspec.isNonAuthoritative, false);
556
- });
557
-
558
- // Fix 4 item 1 — both + openspec/ dir present + change NOT on disk → non-authoritative
559
- test("resolveSddStatus with artifactStore hybrid, openspec dir present but change not on disk returns non-authoritative", async () => {
560
- const cwd = await workspace();
561
- // Create an openspec/changes dir with a different change — not the requested one
562
- mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
563
-
564
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "missing-change" });
565
-
566
- assert.equal(status.isNonAuthoritative, true);
567
- assert.equal(status.nextRecommended, "resolve-via-engram");
568
- assert.deepEqual(status.blockedReasons, []);
569
- assert.equal(status.applyState, "not_applicable");
570
- assert.equal(status.artifactStore, "hybrid");
571
- // Must NOT be treated as blocked
572
- assert.notEqual(status.applyState, "blocked");
573
- });
574
-
575
- // Fix 4 item 2 — strengthen existing both-with-openspec-and-seeded-change test
576
- test("resolveSddStatus with artifactStore hybrid, openspec dir present and change on disk is authoritative", async () => {
577
- const cwd = await workspace();
578
- seedChange(cwd);
579
-
580
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
581
-
582
- assert.equal(status.artifactStore, "hybrid");
583
- assert.equal(status.changeName, "add-auth");
584
- // Must be authoritative
585
- assert.equal(isNonAuthoritativeStatus(status), false);
586
- assert.equal(status.isNonAuthoritative, false);
587
- // Must not be not_applicable — real disk scan ran
588
- assert.notEqual(status.applyState, "not_applicable");
589
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
590
- assert.equal(status.artifacts.proposal, "done");
591
- });
592
-
593
- // Fix 4 item 3 — pure openspec store + change not found STILL blocks (guard against over-broadening Fix 2)
594
- test("resolveSddStatus with artifactStore openspec and change not found still blocks", async () => {
595
- const cwd = await workspace();
596
- // Create openspec dir with a different change — simulate openspec store with no matching change
597
- mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
598
-
599
- const status = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "nonexistent" });
600
-
601
- // Must block, not return non-authoritative
602
- assert.equal(status.isNonAuthoritative, false);
603
- assert.match(status.blockedReasons.join("\n"), /Active change not found/);
604
- assert.equal(status.applyState, "blocked");
605
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
606
- });
607
-
608
- test("renderSddDispatcherMarkdown for both-without-openspec does NOT render Ready", async () => {
609
- const cwd = await workspace();
610
- // No openspec directory — both store is non-authoritative
611
-
612
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
613
- const markdown = renderSddDispatcherMarkdown(status);
614
-
615
- assert.doesNotMatch(markdown, /### Ready/);
616
- assert.match(markdown, /resolve via Engram/i);
617
- });
618
-
619
- test("renderNativeSddPhasePrompt for both-without-openspec emits non-authoritative line", async () => {
620
- const cwd = await workspace();
621
-
622
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
623
- const prompt = renderNativeSddPhasePrompt(status, "apply");
624
-
625
- assert.match(prompt, /non-authoritative/);
626
- assert.doesNotMatch(prompt, /deterministically/);
627
- });
628
-
629
- test("renderPhaseInstructions for not_applicable applyState emits neutral line", async () => {
630
- const cwd = await workspace();
631
-
632
- const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "fix-x" });
633
- const instructions = renderPhaseInstructions(status);
634
-
635
- assert.match(instructions.apply.join("\n"), /Readiness is resolved from Engram/);
636
- assert.match(instructions.archive.join("\n"), /Readiness is resolved from Engram/);
637
- });
638
-
639
- // Fix 4 item 1 — both + openspec/ + ZERO changes + no changeName → non-authoritative
640
- test("resolveSddStatus hybrid + openspec/ dir + zero active changes + no changeName returns non-authoritative", async () => {
641
- const cwd = await workspace();
642
- // openspec/ dir exists but holds no active changes (only the changes/ subdir)
643
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
644
-
645
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
646
-
647
- assert.equal(status.isNonAuthoritative, true);
648
- assert.equal(status.nextRecommended, "resolve-via-engram");
649
- assert.deepEqual(status.blockedReasons, []);
650
- assert.equal(status.artifactStore, "hybrid");
651
- assert.equal(status.applyState, "not_applicable");
652
- assert.equal(status.dependencies.apply, "not_applicable");
653
- assert.equal(status.dependencies.archive, "not_applicable");
654
- // Must NOT be treated as blocked
655
- assert.notEqual(status.applyState, "blocked");
656
- });
657
-
658
- // Fix 4 item 2 — both + openspec/ + MULTIPLE changes + no changeName → authoritative select-change
659
- test("resolveSddStatus hybrid + openspec/ dir + multiple active changes + no changeName stays authoritative", async () => {
660
- const cwd = await workspace();
661
- mkdirSync(join(cwd, "openspec", "changes", "alpha"), { recursive: true });
662
- mkdirSync(join(cwd, "openspec", "changes", "beta"), { recursive: true });
663
-
664
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
665
-
666
- // Authoritative ambiguous-selection behavior must be preserved
667
- assert.equal(status.isNonAuthoritative, false);
668
- assert.match(status.blockedReasons.join("\n"), /ambiguous/);
669
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
670
- });
671
-
672
- // Fix 4 item 3 — both + openspec/ + ONE resolvable change → authoritative
673
- test("resolveSddStatus hybrid + openspec/ dir + exactly one active change is authoritative", async () => {
674
- const cwd = await workspace();
675
- seedChange(cwd);
676
-
677
- // No changeName supplied — should auto-select the single change
678
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
679
-
680
- assert.equal(status.isNonAuthoritative, false);
681
- assert.equal(status.changeName, "add-auth");
682
- assert.equal(status.artifactStore, "hybrid");
683
- assert.notEqual(status.applyState, "not_applicable");
684
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
685
- assert.equal(status.artifacts.proposal, "done");
686
- });
687
-
688
- // Fix 4 item 4 — pure openspec + zero/missing change STILL blocks (guard against over-broadening)
689
- test("resolveSddStatus openspec + zero active changes still blocks", async () => {
690
- const cwd = await workspace();
691
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
692
-
693
- const status = resolveSddStatus({ cwd, artifactStore: "openspec" });
694
-
695
- assert.equal(status.isNonAuthoritative, false);
696
- assert.match(status.blockedReasons.join("\n"), /No active SDD changes/);
697
- assert.equal(status.applyState, "blocked");
698
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
699
- });
700
-
701
- test("resolveSddStatus openspec + named change missing still blocks", async () => {
702
- const cwd = await workspace();
703
- mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
704
-
705
- const status = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "nonexistent" });
706
-
707
- assert.equal(status.isNonAuthoritative, false);
708
- assert.match(status.blockedReasons.join("\n"), /Active change not found/);
709
- assert.equal(status.applyState, "blocked");
710
- assert.notEqual(status.nextRecommended, "resolve-via-engram");
711
- });
712
-
713
- // Issue #535 — post-archive status exposes explicit completion and the archived artifact location
714
- test("resolveSddStatus projects archived terminal state when the change lives in the archive", async () => {
715
- const cwd = await workspace();
716
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
717
- write(
718
- join(cwd, "openspec", "changes", "archive", "2026-01-02-add-auth", "proposal.md"),
719
- "# Proposal\n",
720
- );
721
-
722
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
723
-
724
- assert.equal(status.changeName, "add-auth");
725
- assert.equal(status.nextRecommended, "archived");
726
- assert.deepEqual(status.archived, {
727
- path: join("openspec", "changes", "archive", "2026-01-02-add-auth"),
728
- });
729
- assert.deepEqual(status.blockedReasons, []);
730
- assert.equal(status.dependencies.archive, "all_done");
731
- assert.doesNotMatch(status.nextRecommended, /sdd-new|Start an SDD change|not found/i);
732
- });
733
-
734
- test("resolveSddStatus archived projection prefers the newest archive date", async () => {
735
- const cwd = await workspace();
736
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
737
- mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-add-auth"), { recursive: true });
738
- mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-03-04-add-auth"), { recursive: true });
739
-
740
- const status = resolveSddStatus({ cwd, changeName: "add-auth" });
741
-
742
- assert.equal(status.nextRecommended, "archived");
743
- assert.deepEqual(status.archived, {
744
- path: join("openspec", "changes", "archive", "2026-03-04-add-auth"),
745
- });
746
- });
747
-
748
- test("resolveSddStatus still blocks a change that never existed and has no archive entry", async () => {
749
- const cwd = await workspace();
750
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
751
- mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-unrelated"), { recursive: true });
752
-
753
- const status = resolveSddStatus({ cwd, changeName: "ghost" });
754
-
755
- assert.equal(status.archived, undefined);
756
- assert.match(status.blockedReasons.join("\n"), /Active change not found/);
757
- assert.notEqual(status.nextRecommended, "archived");
758
- });
759
-
760
- test("resolveSddStatus archive matching requires the exact change-name suffix", async () => {
761
- const cwd = await workspace();
762
- mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
763
- mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-foo-bar-baz"), { recursive: true });
764
-
765
- const status = resolveSddStatus({ cwd, changeName: "foo-bar" });
766
-
767
- assert.equal(status.archived, undefined);
768
- assert.match(status.blockedReasons.join("\n"), /Active change not found/);
769
- assert.notEqual(status.nextRecommended, "archived");
770
- });
771
-
772
- // Fix 4 render test — non-authoritative both status → dispatcher shows "both" not "Engram or none"
773
- test("renderSddDispatcherMarkdown for non-authoritative both status shows artifact store 'both'", async () => {
774
- const cwd = await workspace();
775
-
776
- const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
777
- const markdown = renderSddDispatcherMarkdown(status);
778
-
779
- assert.match(markdown, /artifact store: hybrid/);
780
- assert.doesNotMatch(markdown, /Engram or none/);
781
- assert.match(markdown, /resolve via Engram/i);
5
+ test("SDD status arguments preserve selected identity and JSON preference", () => {
6
+ assert.deepEqual(parseSddStatusCommandArgs("alpha --json"), { changeName: "alpha", json: true });
7
+ assert.deepEqual(parseSddStatusCommandArgs(" --json "), { changeName: undefined, json: true });
8
+ assert.deepEqual(parseSddStatusCommandArgs(""), { changeName: undefined, json: false });
782
9
  });