gentle-pi 2.1.2 → 2.3.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 (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -5,7 +5,6 @@ import test from "node:test";
5
5
 
6
6
  const repoRoot = process.cwd();
7
7
  const assetsAgentsDir = join(repoRoot, "assets", "agents");
8
- const REVIEW_REFUTER_TOOLS = ["read", "grep", "find"];
9
8
  const GENERIC_ROLE_TOOLS: Record<string, string[]> = {
10
9
  "gentle-ai-explore.md": ["read", "grep", "find", "codegraph"],
11
10
  "gentle-ai-worker.md": ["read", "grep", "find", "edit", "write", "bash", "mem_save"],
@@ -61,18 +60,19 @@ function assertGenericRoleBody(fileName: string, source: string): void {
61
60
  }
62
61
 
63
62
  const requiredToolsByAgent: Record<string, string[]> = {
64
- "sdd-apply.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
65
- "sdd-archive.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
66
- "sdd-design.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
67
- "sdd-explore.md": ["read", "grep", "glob", "edit", "write", "mem_save"],
68
- "sdd-init.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
69
- "sdd-onboard.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
70
- "sdd-proposal.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
71
- "sdd-spec.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
72
- "sdd-status.md": ["read", "grep", "glob", "bash", "mem_search", "mem_get_observation"],
73
- "sdd-sync.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
74
- "sdd-tasks.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
75
- "sdd-verify.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
63
+ "sdd-apply.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
64
+ "sdd-archive.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
65
+ "sdd-design.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
66
+ "sdd-explore.md": ["read", "grep", "find", "edit", "write", "mem_save"],
67
+ "sdd-init.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
68
+ "sdd-onboard.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
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"],
71
+ "sdd-spec.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
72
+ "sdd-status.md": ["read", "grep", "find", "bash", "mem_search", "mem_get_observation"],
73
+ "sdd-sync.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
74
+ "sdd-tasks.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
75
+ "sdd-verify.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
76
76
  };
77
77
 
78
78
  test("SDD package agents declare role-appropriate tools as YAML arrays", () => {
@@ -123,25 +123,46 @@ test("generic non-SDD agents declare exact role tool allowlists", () => {
123
123
  }
124
124
  });
125
125
 
126
- test("review-refuter exposes only complete-list inspection tools", () => {
127
- const path = join(assetsAgentsDir, "review-refuter.md");
128
- assert.ok(existsSync(path), "review-refuter.md must exist");
129
- const entries = readTools(path);
130
- assert.equal(entries[0], '"*": false', "review-refuter must lead with the deny-all rule");
131
- assert.deepEqual(entries.slice(1), REVIEW_REFUTER_TOOLS);
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
+ });
132
160
 
133
- const frontmatter = readFrontmatter(path);
134
- assert.match(frontmatter, /^name:\s*review-refuter$/m);
135
- for (const forbidden of [
136
- "bash",
137
- "edit",
138
- "write",
139
- "task",
140
- "subagent",
141
- "subagent_run",
142
- "mem_save",
143
- "mem_update",
144
- ]) {
145
- assert.doesNotMatch(frontmatter, new RegExp(`^ - ${forbidden}$`, "m"));
161
+ test("the retired Pi adversarial role agents are not packaged", () => {
162
+ // gentle-pi#311 P5: the refuter and targeted validator verdicts execute
163
+ // through Go-owned pi processes via provider-rendered self-contained
164
+ // vectors; no Pi agent definition may reintroduce a Pi-authored verdict.
165
+ for (const retired of ["review-refuter.md", "review-validator.md"]) {
166
+ assert.ok(!existsSync(join(assetsAgentsDir, retired)), `${retired} must stay deleted`);
146
167
  }
147
168
  });
@@ -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
+ });