gentle-pi 2.2.0 → 2.4.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 (160) hide show
  1. package/README.md +76 -79
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-explore.md +1 -0
  8. package/assets/agents/sdd-proposal.md +2 -0
  9. package/assets/agents/sdd-research.md +54 -0
  10. package/assets/agents/sdd-status.md +4 -4
  11. package/assets/agents/sdd-tasks.md +2 -3
  12. package/assets/agents/sdd-verify.md +21 -1
  13. package/assets/chains/sdd-full.chain.md +1 -1
  14. package/assets/chains/sdd-verify.chain.md +1 -1
  15. package/assets/orchestrator-delegation.md +56 -249
  16. package/assets/orchestrator-memory.md +2 -0
  17. package/assets/orchestrator.md +20 -40
  18. package/assets/sdd-orchestrator-workflow.md +30 -37
  19. package/assets/support/sdd-status-contract.md +7 -7
  20. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  21. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  22. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  23. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  24. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  25. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  26. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  27. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  28. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  29. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  30. package/docs/native-authority-architecture.md +9 -11
  31. package/docs/review-integration.md +27 -373
  32. package/extensions/ask-user-choice.ts +151 -0
  33. package/extensions/gentle-ai.ts +2349 -3785
  34. package/extensions/quiet-tools.ts +515 -32
  35. package/extensions/sdd-init.ts +4 -8
  36. package/lib/agent-home.ts +8 -0
  37. package/lib/gentle-ai-renderer.ts +70 -0
  38. package/lib/model-routing-authority.ts +133 -0
  39. package/lib/native-review-cli.ts +387 -940
  40. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  41. package/lib/provider-contract-bundle.ts +88 -6
  42. package/lib/review-candidate-view.ts +341 -132
  43. package/lib/review-host-relay.ts +324 -96
  44. package/lib/review-integration-v2.ts +868 -307
  45. package/lib/review-last-event-controller.ts +35 -0
  46. package/lib/sdd-preflight.ts +179 -68
  47. package/lib/sdd-status.ts +66 -111
  48. package/lib/terminal-theme.ts +1 -1
  49. package/package.json +83 -82
  50. package/runtime/gentle-ai-binary.mjs +1 -1
  51. package/runtime/native-review-cli.mjs +337 -890
  52. package/runtime/review-integration-v2.mjs +816 -255
  53. package/runtime/review-relay-contract.mjs +1 -1
  54. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  55. package/scripts/gentle-ai-installer.mjs +75 -22
  56. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  57. package/scripts/test-packed-runner.mjs +4 -7
  58. package/scripts/verify-package-files.mjs +23 -23
  59. package/skills/_shared/review-ledger-contract.md +14 -14
  60. package/skills/chained-pr/SKILL.md +3 -0
  61. package/skills/cognitive-doc-design/SKILL.md +1 -1
  62. package/skills/comment-writer/SKILL.md +1 -1
  63. package/skills/gentle-ai/SKILL.md +7 -74
  64. package/skills/issue-creation/SKILL.md +53 -93
  65. package/skills/judgment-day/SKILL.md +5 -7
  66. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  67. package/skills/release/SKILL.md +3 -3
  68. package/skills/skill-registry/SKILL.md +1 -1
  69. package/skills/work-unit-commits/SKILL.md +3 -1
  70. package/tests/artifact-language.test.ts +24 -11
  71. package/tests/ask-user-choice.test.ts +264 -0
  72. package/tests/codegraph-tools.test.ts +3 -3
  73. package/tests/crosslane/cross-lane.mjs +15 -1168
  74. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  75. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  76. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  77. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  78. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  79. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  80. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  81. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  82. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  83. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  84. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  85. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  86. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  87. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  89. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  92. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  93. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  94. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  95. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  96. package/tests/gentle-ai-binary.test.ts +83 -13
  97. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  98. package/tests/gentle-ai-installer.test.ts +137 -47
  99. package/tests/gentle-ai.test.ts +821 -131
  100. package/tests/gentle-theme.test.ts +133 -0
  101. package/tests/issue-creation-skill.test.ts +103 -0
  102. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  103. package/tests/model-routing-authority.test.ts +257 -0
  104. package/tests/native-review-capability-contract.test.ts +45 -1
  105. package/tests/native-review-cli.test.ts +586 -962
  106. package/tests/native-review-consent.test.ts +150 -66
  107. package/tests/native-review-parity-runtime.test.ts +80 -359
  108. package/tests/native-review-parity.test.ts +674 -925
  109. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  110. package/tests/orchestrator-budget.test.ts +132 -88
  111. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  112. package/tests/package-manifest.test.ts +152 -36
  113. package/tests/provider-contract-bundle.test.ts +76 -0
  114. package/tests/provider-contract-mirror.test.ts +19 -0
  115. package/tests/provider-defect-handoff.test.ts +15 -118
  116. package/tests/quiet-tool-rendering.test.ts +1055 -28
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-authority-recovery-docs.test.ts +1 -2
  119. package/tests/review-candidate-view.test.ts +409 -3
  120. package/tests/review-compact-contract.test.ts +4 -30
  121. package/tests/review-contract-prompt.test.ts +139 -0
  122. package/tests/review-controller-lock-status.test.ts +2 -2
  123. package/tests/review-controller-native-recovery.test.ts +363 -1450
  124. package/tests/review-controller-native-routing.test.ts +1251 -5642
  125. package/tests/review-controller-retired-ops.test.ts +1 -1
  126. package/tests/review-controller-workspace-root.test.ts +222 -71
  127. package/tests/review-controller.test.ts +26 -816
  128. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  129. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  130. package/tests/review-gate.test.ts +0 -45
  131. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  132. package/tests/review-host-relay-routing.test.ts +328 -91
  133. package/tests/review-host-relay.test.ts +436 -14
  134. package/tests/review-integration-v2-forward.test.ts +623 -553
  135. package/tests/review-integration-v2.test.ts +239 -165
  136. package/tests/review-last-event-closure.test.ts +408 -0
  137. package/tests/review-ledger-contract.test.ts +97 -35
  138. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  139. package/tests/review-relay-transport-agent.test.ts +119 -46
  140. package/tests/review-snapshot.test.ts +3 -2
  141. package/tests/runtime-harness.mjs +362 -196
  142. package/tests/sdd-agent-tools.test.ts +36 -0
  143. package/tests/sdd-preflight.test.ts +81 -15
  144. package/tests/sdd-status.test.ts +109 -110
  145. package/tests/skill-collision-prefixes.test.ts +5 -8
  146. package/tests/writer-edit-surface-scope.test.ts +366 -0
  147. package/themes/Gentleman-Cute.json +94 -0
  148. package/themes/Gentleman-Sexy.json +92 -0
  149. package/lib/git-commit-transaction.ts +0 -861
  150. package/runtime/git-commit-transaction.mjs +0 -862
  151. package/scripts/run-git-commit-transaction.mjs +0 -35
  152. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  153. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  154. package/tests/git-commit-transaction.test.ts +0 -530
  155. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  156. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  157. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  158. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  159. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  160. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -67,6 +67,7 @@ const requiredToolsByAgent: Record<string, string[]> = {
67
67
  "sdd-init.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
68
68
  "sdd-onboard.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
69
69
  "sdd-proposal.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
70
+ "sdd-research.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
70
71
  "sdd-spec.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
71
72
  "sdd-status.md": ["read", "grep", "find", "bash", "mem_search", "mem_get_observation"],
72
73
  "sdd-sync.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
@@ -122,6 +123,41 @@ test("generic non-SDD agents declare exact role tool allowlists", () => {
122
123
  }
123
124
  });
124
125
 
126
+ test("sdd-verify phase text carries the verify-result envelope and validate-before-persist rule", () => {
127
+ // gentle-pi#535 row 5: the phase must produce a natively admissible report
128
+ // on its first persistence attempt without hunting the format elsewhere.
129
+ const envelopeFields = [
130
+ "schema: gentle-ai.verify-result/v1",
131
+ "evidence_revision: sha256:",
132
+ "verdict:",
133
+ "blockers:",
134
+ "critical_findings:",
135
+ "requirements:",
136
+ "scenarios:",
137
+ "test_command:",
138
+ "test_exit_code:",
139
+ "test_output_hash: sha256:",
140
+ "build_command:",
141
+ "build_exit_code:",
142
+ "build_output_hash: sha256:",
143
+ ];
144
+
145
+ const agentSource = readFileSync(join(assetsAgentsDir, "sdd-verify.md"), "utf8");
146
+ assert.match(agentSource, /```yaml\nschema: gentle-ai\.verify-result\/v1\n/);
147
+ for (const field of envelopeFields) {
148
+ assert.ok(agentSource.includes(field), `sdd-verify.md envelope must carry \`${field}\``);
149
+ }
150
+ assert.match(agentSource, /first non-empty content/);
151
+ assert.match(
152
+ agentSource,
153
+ /gentle-ai sdd-verify-validate --input <path\|-> --requirements <n> --scenarios <n>/,
154
+ );
155
+
156
+ const chainSource = readFileSync(join(repoRoot, "assets", "chains", "sdd-verify.chain.md"), "utf8");
157
+ assert.match(chainSource, /gentle-ai\.verify-result\/v1/);
158
+ assert.match(chainSource, /sdd-verify-validate/);
159
+ });
160
+
125
161
  test("the retired Pi adversarial role agents are not packaged", () => {
126
162
  // gentle-pi#311 P5: the refuter and targeted validator verdicts execute
127
163
  // through Go-owned pi processes via provider-rendered self-contained
@@ -13,7 +13,10 @@ import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import test from "node:test";
15
15
  import {
16
+ collectSddPreflightPreferences,
17
+ DEFAULT_SDD_PREFLIGHT,
16
18
  installSddAssets,
19
+ isSddPreflightTrigger,
17
20
  readSddPreflightFromDisk,
18
21
  sddPreflightDiskPath,
19
22
  writeSddPreflightToDisk,
@@ -27,12 +30,44 @@ async function workspace(): Promise<string> {
27
30
  const SAMPLE_PREFS: SddPreflightPreferences = {
28
31
  executionMode: "auto",
29
32
  artifactStore: "engram",
30
- chainedPrStrategy: "auto-forecast",
33
+ chainedPrStrategy: "auto-chain",
31
34
  reviewBudgetLines: 400,
32
35
  engramAvailable: true,
33
36
  prompted: true,
34
37
  };
35
38
 
39
+ function preflightContext(cwd: string, hasUI: boolean, calls: string[] = [], answers: Record<string, string> = {}) {
40
+ return { cwd, hasUI, ui: { select: async (title: string) => (calls.push(`select:${title}`), answers[title]), input: async (title: string) => (calls.push(`input:${title}`), answers[title]), notify: () => {} } } as Parameters<typeof collectSddPreflightPreferences>[0];
41
+ }
42
+ function writeRawPreflight(cwd: string, chainedPrStrategy: string, prompted = true): string {
43
+ const path = sddPreflightDiskPath(cwd); mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true }); writeFileSync(path, JSON.stringify({ executionMode: "auto", artifactStore: "openspec", chainedPrStrategy, reviewBudgetLines: 400, engramAvailable: false, prompted })); return path;
44
+ }
45
+ test("production callers distinguish automatic defaults from explicit preflight prompts", () => {
46
+ const root = join(import.meta.dirname, ".."), gentleAi = readFileSync(join(root, "extensions", "gentle-ai.ts"), "utf8"), sddInit = readFileSync(join(root, "extensions", "sdd-init.ts"), "utf8");
47
+ assert.match(gentleAi, /function runSddPreflight\(\s*ctx: ExtensionContext,\s*promptFields: readonly SddPreflightField\[\] = \[\]\s*\)/s); assert.match(gentleAi, /if \(isSddAgent && !getSddPreflightPreferences\(ctx\)\) \{\s*await runSddPreflight\(ctx\);/s); assert.match(gentleAi, /applyModelConfig: async \(\) => applySavedModelConfig\(ctx\)\s*\},\s*\{\s*promptFields\s*\}\s*\);/s); assert.match(gentleAi, /handler: async \(_args, ctx\) => \{\s*await runSddPreflight\(ctx, SDD_PREFLIGHT_FIELDS\);/s); assert.match(sddInit, /applyModelConfig: \(\) => applySavedModelConfig\(ctx\)\s*\},\s*\{\s*promptFields: \[\]\s*\}\s*\);/s);
48
+ });
49
+ test("capability-constrained artifact selector elision", async () => {
50
+ const calls: string[] = [], prefs = await collectSddPreflightPreferences(preflightContext(await workspace(), true, calls), false, { promptFields: ["artifactStore"] });
51
+ assert.deepEqual(prefs, DEFAULT_SDD_PREFLIGHT); assert.deepEqual(calls, []);
52
+ });
53
+ test("headless/UI canonical-domain parity", async () => {
54
+ const calls: string[] = [], ui = await collectSddPreflightPreferences(preflightContext(await workspace(), true, calls), false), headless = await collectSddPreflightPreferences(preflightContext(await workspace(), false), false);
55
+ assert.deepEqual(ui, DEFAULT_SDD_PREFLIGHT); assert.deepEqual(headless, ui); assert.deepEqual(calls, []);
56
+ });
57
+ test("explicit UI selections override defaults when a field is genuinely unresolved", async () => {
58
+ const calls: string[] = [], prefs = await collectSddPreflightPreferences(preflightContext(await workspace(), true, calls, { "SDD execution mode": "interactive", "SDD artifact store": "engram", "SDD delivery strategy": "auto-chain", "SDD review budget lines": "700" }), true, { persisted: DEFAULT_SDD_PREFLIGHT, promptFields: ["executionMode", "artifactStore", "chainedPrStrategy", "reviewBudgetLines"] });
59
+ assert.deepEqual({ executionMode: prefs.executionMode, artifactStore: prefs.artifactStore, chainedPrStrategy: prefs.chainedPrStrategy, reviewBudgetLines: prefs.reviewBudgetLines }, { executionMode: "interactive", artifactStore: "engram", chainedPrStrategy: "auto-chain", reviewBudgetLines: 700 }); assert.equal(prefs.prompted, true); assert.equal(calls.length, 4);
60
+ });
61
+ test("legacy persisted strategies normalize to canonical values", async () => {
62
+ const mappings = { "auto-forecast": "ask-on-risk", "ask-always": "ask-on-risk", "single-pr-default": "single-pr", "force-chained": "auto-chain" } as const;
63
+ for (const [legacy, canonical] of Object.entries(mappings)) { const cwd = await workspace(); writeRawPreflight(cwd, legacy); assert.equal(readSddPreflightFromDisk(cwd)?.chainedPrStrategy, canonical); }
64
+ });
65
+ test("exception-ok requires narrow delivery-gate provenance", async () => {
66
+ for (const prompted of [false, true]) { const cwd = await workspace(); writeRawPreflight(cwd, "exception-ok", prompted); assert.equal(readSddPreflightFromDisk(cwd)?.chainedPrStrategy, "ask-on-risk"); }
67
+ const accepted = await collectSddPreflightPreferences(preflightContext(await workspace(), false), false, { persisted: { ...DEFAULT_SDD_PREFLIGHT, chainedPrStrategy: "exception-ok" }, acceptSizeException: true }); assert.equal(accepted.chainedPrStrategy, "exception-ok"); assert.equal(accepted.sizeExceptionAccepted, true);
68
+ const durable = await workspace(); writeSddPreflightToDisk(durable, accepted); assert.equal(readSddPreflightFromDisk(durable)?.chainedPrStrategy, "ask-on-risk");
69
+ });
70
+
36
71
  test("sddPreflightDiskPath returns project-local .pi/gentle-ai/sdd-preflight.json", async () => {
37
72
  const cwd = await workspace();
38
73
  const path = sddPreflightDiskPath(cwd);
@@ -62,18 +97,11 @@ test("readSddPreflightFromDisk returns persisted prefs after write", async () =>
62
97
  assert.deepEqual(loaded, SAMPLE_PREFS);
63
98
  });
64
99
 
65
- test("persisted store survives a simulated cache miss (write then cold read)", async () => {
66
- const cwd = await workspace();
67
-
68
- // Simulate ensureSddPreflight writing to disk
69
- writeSddPreflightToDisk(cwd, SAMPLE_PREFS);
70
-
71
- // Simulate a fresh process where in-memory Map is empty — only disk store exists
72
- const loaded = readSddPreflightFromDisk(cwd);
73
- assert.ok(loaded !== undefined);
74
- assert.equal(loaded.artifactStore, "engram");
75
- assert.equal(loaded.executionMode, "auto");
76
- assert.equal(loaded.prompted, true);
100
+ test("persisted preferences are reused with zero prompts", async () => {
101
+ const cwd = await workspace(); writeSddPreflightToDisk(cwd, SAMPLE_PREFS);
102
+ const loaded = readSddPreflightFromDisk(cwd); assert.ok(loaded);
103
+ const calls: string[] = [], reused = await collectSddPreflightPreferences(preflightContext(cwd, true, calls), loaded!.engramAvailable, { persisted: loaded });
104
+ assert.deepEqual(reused, loaded); assert.deepEqual(calls, []);
77
105
  });
78
106
 
79
107
  test("readSddPreflightFromDisk returns undefined for corrupt JSON", async () => {
@@ -95,7 +123,7 @@ test("readSddPreflightFromDisk returns undefined for JSON with invalid fields",
95
123
  assert.equal(readSddPreflightFromDisk(cwd), undefined);
96
124
  });
97
125
 
98
- test("readSddPreflightFromDisk normalizes unknown chainedPrStrategy to auto-forecast", async () => {
126
+ test("readSddPreflightFromDisk normalizes unknown chainedPrStrategy to ask-on-risk", async () => {
99
127
  const cwd = await workspace();
100
128
  const path = sddPreflightDiskPath(cwd);
101
129
  mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true });
@@ -110,7 +138,7 @@ test("readSddPreflightFromDisk normalizes unknown chainedPrStrategy to auto-fore
110
138
 
111
139
  const loaded = readSddPreflightFromDisk(cwd);
112
140
  assert.ok(loaded !== undefined);
113
- assert.equal(loaded.chainedPrStrategy, "auto-forecast");
141
+ assert.equal(loaded.chainedPrStrategy, "ask-on-risk");
114
142
  });
115
143
 
116
144
  test("writeSddPreflightToDisk is non-fatal when directory is not writable (no throw)", async () => {
@@ -224,3 +252,41 @@ test("forced asset refresh migrates only untouched v0.14 package contracts and p
224
252
  rmSync(temporaryAgentHome, { recursive: true, force: true });
225
253
  }
226
254
  });
255
+
256
+ // gentle-ai calls the dual-store mode "hybrid"; this repo called the same
257
+ // operator-facing choice "both". Two names for one concept is how a caller ends
258
+ // up mapping between them by hand. gentle-ai owns the contract, so "hybrid" is
259
+ // canonical here too — but "both" is already persisted in operator preflight
260
+ // files on disk, so it must keep loading rather than fall back to the default.
261
+ test("a persisted legacy 'both' artifact store loads as hybrid", async () => {
262
+ const cwd = await mkdtemp(join(tmpdir(), "sdd-preflight-legacy-"));
263
+ mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true });
264
+ writeFileSync(
265
+ sddPreflightDiskPath(cwd),
266
+ JSON.stringify({ executionMode: "auto", artifactStore: "hybrid", chainedPrStrategy: "ask-on-risk", reviewBudgetLines: 400, engramAvailable: true, prompted: true }),
267
+ );
268
+
269
+ const loaded = readSddPreflightFromDisk(cwd, true);
270
+ assert.equal(loaded?.artifactStore, "hybrid", "legacy 'both' must normalize to the canonical name, not be discarded");
271
+ });
272
+
273
+ test("a persisted canonical 'hybrid' artifact store loads unchanged", async () => {
274
+ const cwd = await mkdtemp(join(tmpdir(), "sdd-preflight-hybrid-"));
275
+ mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true });
276
+ writeFileSync(
277
+ sddPreflightDiskPath(cwd),
278
+ JSON.stringify({ executionMode: "auto", artifactStore: "hybrid", chainedPrStrategy: "ask-on-risk", reviewBudgetLines: 400, engramAvailable: true, prompted: true }),
279
+ );
280
+
281
+ const loaded = readSddPreflightFromDisk(cwd, true);
282
+ assert.equal(loaded?.artifactStore, "hybrid");
283
+ });
284
+
285
+ test("slash SDD preflight trigger accepts the gentle-sdd command prefix", () => {
286
+ for (const text of ["/gentle-sdd-init", "/gentle-sdd-continue", "/gentle-sdd-status fix-rose --json", "/sdd", "/sdd:plan", "/sdd-plan this change"]) {
287
+ assert.equal(isSddPreflightTrigger(text), true, text);
288
+ }
289
+ for (const text of ["/gentle-sddx", "/gentle:sdd-preflight", "/gentle-status", "gentle-sdd-init"]) {
290
+ assert.equal(isSddPreflightTrigger(text), false, text);
291
+ }
292
+ });
@@ -92,69 +92,24 @@ test("resolveSddStatus selects the only active change and counts task progress",
92
92
  assert.match(status.instructions?.apply.join("\n") ?? "", /persisted task checkboxes/);
93
93
  });
94
94
 
95
- test("resolveSddStatus keeps planning-only changes read-only and blocks only an expected invalid review authority", async () => {
95
+ test("resolveSddStatus routes completed implementation through archive without RDD authority", async () => {
96
96
  const cwd = await workspace();
97
97
  const root = seedChange(cwd);
98
98
  write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
99
99
  write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
100
100
  write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
101
- let resolverCalls = 0;
102
- const planning = resolveSddStatus({
103
- cwd,
104
- changeName: "add-auth",
105
- reviewAuthority: {
106
- expected: false,
107
- resolve: () => {
108
- resolverCalls += 1;
109
- return { activeAuthorityId: "must-not-run" };
110
- },
111
- },
112
- });
113
- assert.equal(resolverCalls, 0);
114
- assert.equal(planning.blockedReasons.some((reason) => reason.startsWith("resolve-review:")), false);
115
-
116
- const invalid = resolveSddStatus({
117
- cwd,
118
- changeName: "add-auth",
119
- reviewAuthority: {
120
- expected: true,
121
- resolve: () => {
122
- throw new Error("stale compact successor");
123
- },
124
- },
125
- });
126
- assert.equal(invalid.applyState, "blocked");
127
- assert.equal(invalid.dependencies.archive, "blocked");
128
- assert.equal(invalid.nextRecommended, "resolve-review");
129
- assert.match(invalid.blockedReasons.join("\n"), /^resolve-review: stale compact successor/m);
130
101
 
131
- const valid = resolveSddStatus({
132
- cwd,
133
- changeName: "add-auth",
134
- reviewAuthority: {
135
- expected: true,
136
- resolve: () => ({ activeAuthorityId: "validated-compact-authority" }),
137
- },
138
- });
139
- assert.equal(valid.applyState, "all_done");
140
- assert.equal(valid.dependencies.archive, "ready");
141
- assert.equal(valid.blockedReasons.some((reason) => reason.startsWith("resolve-review:")), false);
142
- });
102
+ const status = resolveSddStatus({ cwd, changeName: "add-auth" });
143
103
 
144
- test("resolveSddStatus blocks an expected missing native bound readiness without inferring approval", async () => {
145
- const cwd = await workspace();
146
- seedChange(cwd);
147
- const status = resolveSddStatus({
148
- cwd,
149
- changeName: "add-auth",
150
- nativeReviewReadiness: { expected: true, ready: false, reason: "binding is missing" },
151
- });
152
- assert.equal(status.applyState, "blocked");
153
- assert.equal(status.nextRecommended, "resolve-review");
154
- assert.deepEqual(status.blockedReasons, ["resolve-review: binding is missing"]);
104
+ assert.equal(status.applyState, "all_done");
105
+ assert.equal(status.dependencies.verify, "all_done");
106
+ assert.equal(status.dependencies.sync, "all_done");
107
+ assert.equal(status.dependencies.archive, "ready");
108
+ assert.equal(status.nextRecommended, "sdd-archive");
109
+ assert.equal(status.blockedReasons.some((reason) => reason.startsWith("resolve-review:")), false);
155
110
  });
156
111
 
157
- test("resolveSddStatus routes completed implementation to parent lifecycle and separates parent actions", async () => {
112
+ test("resolveSddStatus ignores legacy parent review actions after implementation", async () => {
158
113
  const cwd = await workspace();
159
114
  const root = seedChange(cwd);
160
115
  write(
@@ -174,8 +129,8 @@ test("resolveSddStatus routes completed implementation to parent lifecycle and s
174
129
  assert.deepEqual(status.taskArtifactErrors, []);
175
130
  assert.equal(status.applyState, "all_done");
176
131
  assert.equal(status.dependencies.apply, "all_done");
177
- assert.equal(status.nextRecommended, "parent-lifecycle");
178
- assert.match(status.instructions?.apply.join("\n") ?? "", /Deferred parent lifecycle actions/);
132
+ assert.equal(status.dependencies.verify, "ready");
133
+ assert.equal(status.nextRecommended, "sdd-verify");
179
134
  });
180
135
 
181
136
  test("resolveSddStatus treats malformed ownership as unresolved implementation work", async () => {
@@ -197,7 +152,7 @@ test("resolveSddStatus treats malformed ownership as unresolved implementation w
197
152
  assert.match(status.blockedReasons.join("\n"), /task ownership marker/i);
198
153
  });
199
154
 
200
- test("resolveSddStatus routes completed legacy implementation to parent lifecycle", async () => {
155
+ test("resolveSddStatus routes completed legacy implementation directly to verify", async () => {
201
156
  const cwd = await workspace();
202
157
  const root = seedChange(cwd);
203
158
  write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Build foundation\n");
@@ -206,7 +161,8 @@ test("resolveSddStatus routes completed legacy implementation to parent lifecycl
206
161
 
207
162
  assert.equal(status.applyState, "all_done");
208
163
  assert.equal(status.dependencies.apply, "all_done");
209
- assert.equal(status.nextRecommended, "parent-lifecycle");
164
+ assert.equal(status.dependencies.verify, "ready");
165
+ assert.equal(status.nextRecommended, "sdd-verify");
210
166
  });
211
167
 
212
168
  test("resolveSddStatus blocks sync when verify report is not clearly passing", async () => {
@@ -215,11 +171,7 @@ test("resolveSddStatus blocks sync when verify report is not clearly passing", a
215
171
  write(join(root, "apply-progress.md"), "# Apply\n\nSome work completed.\n");
216
172
  write(join(root, "verify-report.md"), "# Verify\n\nTODO: tests not run yet\n");
217
173
 
218
- const status = resolveSddStatus({
219
- cwd,
220
- changeName: "add-auth",
221
- reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
222
- });
174
+ const status = resolveSddStatus({ cwd, changeName: "add-auth" });
223
175
 
224
176
  assert.equal(status.dependencies.verify, "ready");
225
177
  assert.equal(status.dependencies.sync, "blocked");
@@ -233,11 +185,7 @@ test("resolveSddStatus rejects negated pass and sync-complete phrases", async ()
233
185
  write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
234
186
  write(join(root, "sync-report.md"), "# Sync\n\nSync complete: no\n");
235
187
 
236
- const status = resolveSddStatus({
237
- cwd,
238
- changeName: "add-auth",
239
- reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
240
- });
188
+ const status = resolveSddStatus({ cwd, changeName: "add-auth" });
241
189
 
242
190
  assert.equal(status.dependencies.verify, "ready");
243
191
  assert.equal(status.dependencies.sync, "blocked");
@@ -334,11 +282,7 @@ test("resolveSddStatus blocks stale sync report when current verify is not passi
334
282
  write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
335
283
  write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
336
284
 
337
- const status = resolveSddStatus({
338
- cwd,
339
- changeName: "add-auth",
340
- reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
341
- });
285
+ const status = resolveSddStatus({ cwd, changeName: "add-auth" });
342
286
 
343
287
  assert.equal(status.dependencies.verify, "ready");
344
288
  assert.equal(status.dependencies.sync, "blocked");
@@ -381,11 +325,7 @@ test("resolveSddStatus marks archive ready only after clean verify, sync, and co
381
325
  write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
382
326
  write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
383
327
 
384
- const status = resolveSddStatus({
385
- cwd,
386
- changeName: "add-auth",
387
- reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
388
- });
328
+ const status = resolveSddStatus({ cwd, changeName: "add-auth" });
389
329
 
390
330
  assert.equal(status.dependencies.archive, "ready");
391
331
  assert.equal(status.nextRecommended, "sdd-archive");
@@ -428,13 +368,13 @@ test("resolveSddStatus with artifactStore none returns non-authoritative status
428
368
  assert.equal(status.nextRecommended, "resolve-via-engram");
429
369
  });
430
370
 
431
- test("resolveSddStatus with artifactStore both uses disk scan and reflects store", async () => {
371
+ test("resolveSddStatus with artifactStore hybrid uses disk scan and reflects store", async () => {
432
372
  const cwd = await workspace();
433
373
  seedChange(cwd);
434
374
 
435
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
375
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
436
376
 
437
- assert.equal(status.artifactStore, "both");
377
+ assert.equal(status.artifactStore, "hybrid");
438
378
  assert.equal(status.changeName, "add-auth");
439
379
  assert.notEqual(status.nextRecommended, "resolve-via-engram");
440
380
  });
@@ -484,13 +424,13 @@ test("parseSddStatusCommandArgs extracts change and json flag", () => {
484
424
  });
485
425
  });
486
426
 
487
- test("resolveSddStatus with artifactStore both and NO openspec dir returns non-authoritative status", async () => {
427
+ test("resolveSddStatus with artifactStore hybrid and NO openspec dir returns non-authoritative status", async () => {
488
428
  const cwd = await workspace();
489
- // No openspec directory — both store without disk backing is non-authoritative
429
+ // No openspec directory — the hybrid store without disk backing is non-authoritative
490
430
 
491
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "my-change" });
431
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "my-change" });
492
432
 
493
- assert.equal(status.artifactStore, "both");
433
+ assert.equal(status.artifactStore, "hybrid");
494
434
  assert.equal(status.changeName, "my-change");
495
435
  assert.deepEqual(status.blockedReasons, []);
496
436
  assert.equal(status.nextRecommended, "resolve-via-engram");
@@ -499,13 +439,13 @@ test("resolveSddStatus with artifactStore both and NO openspec dir returns non-a
499
439
  assert.equal(status.dependencies.archive, "not_applicable");
500
440
  });
501
441
 
502
- test("resolveSddStatus with artifactStore both and existing openspec dir runs authoritative disk scan", async () => {
442
+ test("resolveSddStatus with artifactStore hybrid and existing openspec dir runs authoritative disk scan", async () => {
503
443
  const cwd = await workspace();
504
444
  seedChange(cwd);
505
445
 
506
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
446
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
507
447
 
508
- assert.equal(status.artifactStore, "both");
448
+ assert.equal(status.artifactStore, "hybrid");
509
449
  assert.equal(status.changeName, "add-auth");
510
450
  assert.notEqual(status.nextRecommended, "resolve-via-engram");
511
451
  assert.equal(status.artifacts.proposal, "done");
@@ -526,13 +466,13 @@ test("isNonAuthoritativeStatus reads typed isNonAuthoritative field and matches
526
466
  assert.equal(isNonAuthoritativeStatus(none), true);
527
467
  assert.equal(none.nextRecommended, "resolve-via-engram");
528
468
 
529
- const bothWithoutOpenspec = resolveSddStatus({ cwd, artifactStore: "both", changeName: "x" });
469
+ const bothWithoutOpenspec = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "x" });
530
470
  assert.equal(bothWithoutOpenspec.isNonAuthoritative, true);
531
471
  assert.equal(isNonAuthoritativeStatus(bothWithoutOpenspec), true);
532
472
  assert.equal(bothWithoutOpenspec.nextRecommended, "resolve-via-engram");
533
473
 
534
474
  seedChange(cwd);
535
- const bothWithOpenspec = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
475
+ const bothWithOpenspec = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
536
476
  assert.equal(bothWithOpenspec.isNonAuthoritative, false);
537
477
  assert.equal(isNonAuthoritativeStatus(bothWithOpenspec), false);
538
478
  assert.notEqual(bothWithOpenspec.nextRecommended, "resolve-via-engram");
@@ -551,12 +491,12 @@ test("isNonAuthoritative boolean field is set correctly across all store/disk co
551
491
  assert.equal(none.isNonAuthoritative, true);
552
492
 
553
493
  // both without openspec/ → non-authoritative
554
- const bothWithout = resolveSddStatus({ cwd, artifactStore: "both", changeName: "x" });
494
+ const bothWithout = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "x" });
555
495
  assert.equal(bothWithout.isNonAuthoritative, true);
556
496
 
557
497
  // both WITH openspec/ and seeded change → authoritative
558
498
  seedChange(cwd);
559
- const bothWith = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
499
+ const bothWith = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
560
500
  assert.equal(bothWith.isNonAuthoritative, false);
561
501
 
562
502
  // openspec (default disk scan, seeded) → authoritative
@@ -565,30 +505,30 @@ test("isNonAuthoritative boolean field is set correctly across all store/disk co
565
505
  });
566
506
 
567
507
  // Fix 4 item 1 — both + openspec/ dir present + change NOT on disk → non-authoritative
568
- test("resolveSddStatus with artifactStore both, openspec dir present but change not on disk returns non-authoritative", async () => {
508
+ test("resolveSddStatus with artifactStore hybrid, openspec dir present but change not on disk returns non-authoritative", async () => {
569
509
  const cwd = await workspace();
570
510
  // Create an openspec/changes dir with a different change — not the requested one
571
511
  mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
572
512
 
573
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "missing-change" });
513
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "missing-change" });
574
514
 
575
515
  assert.equal(status.isNonAuthoritative, true);
576
516
  assert.equal(status.nextRecommended, "resolve-via-engram");
577
517
  assert.deepEqual(status.blockedReasons, []);
578
518
  assert.equal(status.applyState, "not_applicable");
579
- assert.equal(status.artifactStore, "both");
519
+ assert.equal(status.artifactStore, "hybrid");
580
520
  // Must NOT be treated as blocked
581
521
  assert.notEqual(status.applyState, "blocked");
582
522
  });
583
523
 
584
524
  // Fix 4 item 2 — strengthen existing both-with-openspec-and-seeded-change test
585
- test("resolveSddStatus with artifactStore both, openspec dir present and change on disk is authoritative", async () => {
525
+ test("resolveSddStatus with artifactStore hybrid, openspec dir present and change on disk is authoritative", async () => {
586
526
  const cwd = await workspace();
587
527
  seedChange(cwd);
588
528
 
589
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
529
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
590
530
 
591
- assert.equal(status.artifactStore, "both");
531
+ assert.equal(status.artifactStore, "hybrid");
592
532
  assert.equal(status.changeName, "add-auth");
593
533
  // Must be authoritative
594
534
  assert.equal(isNonAuthoritativeStatus(status), false);
@@ -618,7 +558,7 @@ test("renderSddDispatcherMarkdown for both-without-openspec does NOT render Read
618
558
  const cwd = await workspace();
619
559
  // No openspec directory — both store is non-authoritative
620
560
 
621
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
561
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
622
562
  const markdown = renderSddDispatcherMarkdown(status);
623
563
 
624
564
  assert.doesNotMatch(markdown, /### Ready/);
@@ -628,7 +568,7 @@ test("renderSddDispatcherMarkdown for both-without-openspec does NOT render Read
628
568
  test("renderNativeSddPhasePrompt for both-without-openspec emits non-authoritative line", async () => {
629
569
  const cwd = await workspace();
630
570
 
631
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
571
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
632
572
  const prompt = renderNativeSddPhasePrompt(status, "apply");
633
573
 
634
574
  assert.match(prompt, /non-authoritative/);
@@ -646,17 +586,17 @@ test("renderPhaseInstructions for not_applicable applyState emits neutral line",
646
586
  });
647
587
 
648
588
  // Fix 4 item 1 — both + openspec/ + ZERO changes + no changeName → non-authoritative
649
- test("resolveSddStatus both + openspec/ dir + zero active changes + no changeName returns non-authoritative", async () => {
589
+ test("resolveSddStatus hybrid + openspec/ dir + zero active changes + no changeName returns non-authoritative", async () => {
650
590
  const cwd = await workspace();
651
591
  // openspec/ dir exists but holds no active changes (only the changes/ subdir)
652
592
  mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
653
593
 
654
- const status = resolveSddStatus({ cwd, artifactStore: "both" });
594
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
655
595
 
656
596
  assert.equal(status.isNonAuthoritative, true);
657
597
  assert.equal(status.nextRecommended, "resolve-via-engram");
658
598
  assert.deepEqual(status.blockedReasons, []);
659
- assert.equal(status.artifactStore, "both");
599
+ assert.equal(status.artifactStore, "hybrid");
660
600
  assert.equal(status.applyState, "not_applicable");
661
601
  assert.equal(status.dependencies.apply, "not_applicable");
662
602
  assert.equal(status.dependencies.archive, "not_applicable");
@@ -665,12 +605,12 @@ test("resolveSddStatus both + openspec/ dir + zero active changes + no changeNam
665
605
  });
666
606
 
667
607
  // Fix 4 item 2 — both + openspec/ + MULTIPLE changes + no changeName → authoritative select-change
668
- test("resolveSddStatus both + openspec/ dir + multiple active changes + no changeName stays authoritative", async () => {
608
+ test("resolveSddStatus hybrid + openspec/ dir + multiple active changes + no changeName stays authoritative", async () => {
669
609
  const cwd = await workspace();
670
610
  mkdirSync(join(cwd, "openspec", "changes", "alpha"), { recursive: true });
671
611
  mkdirSync(join(cwd, "openspec", "changes", "beta"), { recursive: true });
672
612
 
673
- const status = resolveSddStatus({ cwd, artifactStore: "both" });
613
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
674
614
 
675
615
  // Authoritative ambiguous-selection behavior must be preserved
676
616
  assert.equal(status.isNonAuthoritative, false);
@@ -679,16 +619,16 @@ test("resolveSddStatus both + openspec/ dir + multiple active changes + no chang
679
619
  });
680
620
 
681
621
  // Fix 4 item 3 — both + openspec/ + ONE resolvable change → authoritative
682
- test("resolveSddStatus both + openspec/ dir + exactly one active change is authoritative", async () => {
622
+ test("resolveSddStatus hybrid + openspec/ dir + exactly one active change is authoritative", async () => {
683
623
  const cwd = await workspace();
684
624
  seedChange(cwd);
685
625
 
686
626
  // No changeName supplied — should auto-select the single change
687
- const status = resolveSddStatus({ cwd, artifactStore: "both" });
627
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
688
628
 
689
629
  assert.equal(status.isNonAuthoritative, false);
690
630
  assert.equal(status.changeName, "add-auth");
691
- assert.equal(status.artifactStore, "both");
631
+ assert.equal(status.artifactStore, "hybrid");
692
632
  assert.notEqual(status.applyState, "not_applicable");
693
633
  assert.notEqual(status.nextRecommended, "resolve-via-engram");
694
634
  assert.equal(status.artifacts.proposal, "done");
@@ -719,14 +659,73 @@ test("resolveSddStatus openspec + named change missing still blocks", async () =
719
659
  assert.notEqual(status.nextRecommended, "resolve-via-engram");
720
660
  });
721
661
 
662
+ // Issue #535 — post-archive status exposes explicit completion and the archived artifact location
663
+ test("resolveSddStatus projects archived terminal state when the change lives in the archive", async () => {
664
+ const cwd = await workspace();
665
+ mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
666
+ write(
667
+ join(cwd, "openspec", "changes", "archive", "2026-01-02-add-auth", "proposal.md"),
668
+ "# Proposal\n",
669
+ );
670
+
671
+ const status = resolveSddStatus({ cwd, changeName: "add-auth" });
672
+
673
+ assert.equal(status.changeName, "add-auth");
674
+ assert.equal(status.nextRecommended, "archived");
675
+ assert.deepEqual(status.archived, {
676
+ path: join("openspec", "changes", "archive", "2026-01-02-add-auth"),
677
+ });
678
+ assert.deepEqual(status.blockedReasons, []);
679
+ assert.equal(status.dependencies.archive, "all_done");
680
+ assert.doesNotMatch(status.nextRecommended, /sdd-new|Start an SDD change|not found/i);
681
+ });
682
+
683
+ test("resolveSddStatus archived projection prefers the newest archive date", async () => {
684
+ const cwd = await workspace();
685
+ mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
686
+ mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-add-auth"), { recursive: true });
687
+ mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-03-04-add-auth"), { recursive: true });
688
+
689
+ const status = resolveSddStatus({ cwd, changeName: "add-auth" });
690
+
691
+ assert.equal(status.nextRecommended, "archived");
692
+ assert.deepEqual(status.archived, {
693
+ path: join("openspec", "changes", "archive", "2026-03-04-add-auth"),
694
+ });
695
+ });
696
+
697
+ test("resolveSddStatus still blocks a change that never existed and has no archive entry", async () => {
698
+ const cwd = await workspace();
699
+ mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
700
+ mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-unrelated"), { recursive: true });
701
+
702
+ const status = resolveSddStatus({ cwd, changeName: "ghost" });
703
+
704
+ assert.equal(status.archived, undefined);
705
+ assert.match(status.blockedReasons.join("\n"), /Active change not found/);
706
+ assert.notEqual(status.nextRecommended, "archived");
707
+ });
708
+
709
+ test("resolveSddStatus archive matching requires the exact change-name suffix", async () => {
710
+ const cwd = await workspace();
711
+ mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
712
+ mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-foo-bar-baz"), { recursive: true });
713
+
714
+ const status = resolveSddStatus({ cwd, changeName: "foo-bar" });
715
+
716
+ assert.equal(status.archived, undefined);
717
+ assert.match(status.blockedReasons.join("\n"), /Active change not found/);
718
+ assert.notEqual(status.nextRecommended, "archived");
719
+ });
720
+
722
721
  // Fix 4 render test — non-authoritative both status → dispatcher shows "both" not "Engram or none"
723
722
  test("renderSddDispatcherMarkdown for non-authoritative both status shows artifact store 'both'", async () => {
724
723
  const cwd = await workspace();
725
724
 
726
- const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
725
+ const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
727
726
  const markdown = renderSddDispatcherMarkdown(status);
728
727
 
729
- assert.match(markdown, /artifact store: both/);
728
+ assert.match(markdown, /artifact store: hybrid/);
730
729
  assert.doesNotMatch(markdown, /Engram or none/);
731
730
  assert.match(markdown, /resolve via Engram/i);
732
731
  });
@@ -14,21 +14,18 @@ function readSkillName(dir: string): string | undefined {
14
14
  const PREFIXED_NAMES: Record<string, string> = {
15
15
  "branch-pr": "gentle-ai-branch-pr",
16
16
  "chained-pr": "gentle-ai-chained-pr",
17
+ "cognitive-doc-design": "gentle-ai-cognitive-doc-design",
18
+ "comment-writer": "gentle-ai-comment-writer",
17
19
  "issue-creation": "gentle-ai-issue-creation",
18
20
  "judgment-day": "gentle-ai-judgment-day",
19
21
  "rdd-defect-workflow": "gentle-ai-rdd-defect-workflow",
20
22
  "skill-creator": "gentle-ai-skill-creator",
21
23
  "skill-improver": "gentle-ai-skill-improver",
24
+ "skill-registry": "gentle-ai-skill-registry",
25
+ "work-unit-commits": "gentle-ai-work-unit-commits",
22
26
  };
23
27
 
24
- const UNPREFIXED_DIRS = [
25
- "skill-registry",
26
- "release",
27
- "work-unit-commits",
28
- "gentle-ai",
29
- "comment-writer",
30
- "cognitive-doc-design",
31
- ];
28
+ const UNPREFIXED_DIRS = ["gentle-ai", "release"];
32
29
 
33
30
  for (const [dir, expectedName] of Object.entries(PREFIXED_NAMES)) {
34
31
  test(`skills/${dir}/SKILL.md frontmatter name is prefixed`, () => {