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
@@ -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,20 +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",
21
+ "rdd-defect-workflow": "gentle-ai-rdd-defect-workflow",
19
22
  "skill-creator": "gentle-ai-skill-creator",
20
23
  "skill-improver": "gentle-ai-skill-improver",
24
+ "skill-registry": "gentle-ai-skill-registry",
25
+ "work-unit-commits": "gentle-ai-work-unit-commits",
21
26
  };
22
27
 
23
- const UNPREFIXED_DIRS = [
24
- "skill-registry",
25
- "release",
26
- "work-unit-commits",
27
- "gentle-ai",
28
- "comment-writer",
29
- "cognitive-doc-design",
30
- ];
28
+ const UNPREFIXED_DIRS = ["gentle-ai", "release"];
31
29
 
32
30
  for (const [dir, expectedName] of Object.entries(PREFIXED_NAMES)) {
33
31
  test(`skills/${dir}/SKILL.md frontmatter name is prefixed`, () => {
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, readFileSync, statSync, symlinkSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import test from "node:test";
@@ -267,3 +267,52 @@ test("orchestrator documents path injection protocol", () => {
267
267
  assert.match(source, /paths-injected/);
268
268
  assert.doesNotMatch(source, /Use matching compact rules based on code context and task intent/);
269
269
  });
270
+
271
+ test("non-forced regeneration invalidates cache when skill bytes change but path, size, and mtime are restored", async () => {
272
+ const cwd = join(tmpdir(), `gentle-pi-fingerprint-${Date.now()}`);
273
+ const skillPath = join(cwd, "skills", "alpha", "SKILL.md");
274
+ mkdirSync(dirname(skillPath), { recursive: true });
275
+
276
+ const contentV1 =
277
+ '---\nname: alpha\ndescription: "Trigger: alpha skill. Variant one. Body A."\n---\n\n## Rules\n\n- Rule A.\n';
278
+ const contentV2 =
279
+ '---\nname: alpha\ndescription: "Trigger: alpha skill. Variant two. Body B."\n---\n\n## Rules\n\n- Rule B.\n';
280
+ assert.equal(
281
+ Buffer.byteLength(contentV1),
282
+ Buffer.byteLength(contentV2),
283
+ "test fixtures must have identical byte length",
284
+ );
285
+
286
+ const fixedMtimeSeconds = 1_000_000_000;
287
+ writeFileSync(skillPath, contentV1);
288
+ utimesSync(skillPath, fixedMtimeSeconds, fixedMtimeSeconds);
289
+ const beforeStat = statSync(skillPath);
290
+ const beforeMtimeMs = beforeStat.mtimeMs;
291
+ const beforeSize = beforeStat.size;
292
+
293
+ const first = await __testing.regenerateRegistry(cwd, false);
294
+ assert.equal(first.regenerated, true, "initial non-forced regeneration writes the registry");
295
+ assert.equal(first.reason, "fingerprint-changed");
296
+
297
+ const registryPath = join(cwd, ".atl", "skill-registry.md");
298
+ const firstRegistry = readFileSync(registryPath, "utf8");
299
+ assert.match(firstRegistry, /Variant one\. Body A\./);
300
+
301
+ writeFileSync(skillPath, contentV2);
302
+ utimesSync(skillPath, fixedMtimeSeconds, fixedMtimeSeconds);
303
+ const midStat = statSync(skillPath);
304
+ assert.equal(midStat.size, beforeSize, "byte size must be unchanged after rewrite");
305
+ assert.equal(midStat.mtimeMs, beforeMtimeMs, "mtime must be restored exactly");
306
+
307
+ const second = await __testing.regenerateRegistry(cwd, false);
308
+ assert.equal(
309
+ second.regenerated,
310
+ true,
311
+ "non-forced regeneration must invalidate cache when content bytes changed",
312
+ );
313
+ assert.equal(second.reason, "fingerprint-changed");
314
+
315
+ const secondRegistry = readFileSync(registryPath, "utf8");
316
+ assert.match(secondRegistry, /Variant two\. Body B\./);
317
+ assert.doesNotMatch(secondRegistry, /Variant one\. Body A\./);
318
+ });
@@ -4,9 +4,12 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
6
6
  import {
7
+ gentleAiVersionPinMismatches,
7
8
  reconcileContractsOnDisk,
8
9
  reconcileGeneratedRuntimeSources,
9
10
  } from "../scripts/verify-package-files.mjs";
11
+ import { INSTALLER_VERSION, RELEASE_BASE_URL, GENTLE_AI_WINDOWS_SOURCE_TAG } from "../scripts/gentle-ai-installer.mjs";
12
+ import { GENTLE_AI_VERSION } from "../lib/gentle-ai-binary.ts";
10
13
 
11
14
  function makeFixtureRoot(): string {
12
15
  return mkdtempSync(join(tmpdir(), "gentle-pi-verify-package-files-"));
@@ -49,6 +52,22 @@ test("contracts/ walk fails on a contractHashes entry that is missing from disk"
49
52
  }
50
53
  });
51
54
 
55
+ test("contracts/ walk excludes the provider contract mirror subtree, whose bytes are pinned by its own lock", () => {
56
+ const fixtureRoot = makeFixtureRoot();
57
+ try {
58
+ mkdirSync(join(fixtureRoot, "contracts/review-provider-contract-mirror/v1.1.0/bundle"), { recursive: true });
59
+ writeFileSync(join(fixtureRoot, "contracts/review-provider-contract-mirror/provider-contract.lock.json"), "{}\n");
60
+ writeFileSync(join(fixtureRoot, "contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md"), "mirror\n");
61
+
62
+ const { unlistedOnDisk, listedButMissing } = reconcileContractsOnDisk(fixtureRoot, {});
63
+
64
+ assert.deepEqual(unlistedOnDisk, []);
65
+ assert.deepEqual(listedButMissing, []);
66
+ } finally {
67
+ rmSync(fixtureRoot, { recursive: true, force: true });
68
+ }
69
+ });
70
+
52
71
  test("sources <-> runtime/*.mjs walk fails when a generated runtime file is absent from the generator's sources array", () => {
53
72
  const fixtureRoot = makeFixtureRoot();
54
73
  try {
@@ -91,6 +110,49 @@ test("sources <-> runtime/*.mjs walk fails when a sources entry is absent from r
91
110
  }
92
111
  });
93
112
 
113
+ // Regression test for the documented incident in scripts/install-gentle-ai.mjs:
114
+ // the installer once reported installing v2.1.11 while writing v2.2.0 to disk
115
+ // because two hardcoded version copies had drifted apart. The textual
116
+ // `.includes(...)` grep this replaces could not have caught this, because it
117
+ // only verifies a string appears in a file, not that two values agree.
118
+ test("Gentle AI version pin mismatch is reported with a specific message when a derived location disagrees with the authoritative constant", () => {
119
+ const mismatches = gentleAiVersionPinMismatches({
120
+ installerVersion: "2.2.3",
121
+ releaseBaseUrl: "https://github.com/Gentleman-Programming/gentle-ai/releases/download/v2.2.3/",
122
+ windowsSourceTag: "v2.2.3",
123
+ libGentleAiVersion: "2.2.0",
124
+ });
125
+
126
+ assert.deepEqual(mismatches, [
127
+ 'lib/gentle-ai-binary.ts GENTLE_AI_VERSION ("2.2.0") does not match the authoritative scripts/gentle-ai-installer.mjs INSTALLER_VERSION ("2.2.3")',
128
+ ]);
129
+ });
130
+
131
+ test("Gentle AI version pin mismatch also flags a drifted release URL and a drifted Windows source tag", () => {
132
+ const mismatches = gentleAiVersionPinMismatches({
133
+ installerVersion: "2.2.3",
134
+ releaseBaseUrl: "https://github.com/Gentleman-Programming/gentle-ai/releases/download/v2.2.0/",
135
+ windowsSourceTag: "v2.1.11",
136
+ libGentleAiVersion: "2.2.3",
137
+ });
138
+
139
+ assert.deepEqual(mismatches, [
140
+ 'RELEASE_BASE_URL ("https://github.com/Gentleman-Programming/gentle-ai/releases/download/v2.2.0/") does not pin the authoritative v2.2.3',
141
+ 'GENTLE_AI_WINDOWS_SOURCE_TAG ("v2.1.11") does not match the authoritative v2.2.3',
142
+ ]);
143
+ });
144
+
145
+ test("Gentle AI version pin agrees across the installer version, the release URL, the Windows source tag, and lib/gentle-ai-binary.ts", () => {
146
+ const mismatches = gentleAiVersionPinMismatches({
147
+ installerVersion: INSTALLER_VERSION,
148
+ releaseBaseUrl: RELEASE_BASE_URL,
149
+ windowsSourceTag: GENTLE_AI_WINDOWS_SOURCE_TAG,
150
+ libGentleAiVersion: GENTLE_AI_VERSION,
151
+ });
152
+
153
+ assert.deepEqual(mismatches, []);
154
+ });
155
+
94
156
  test("both walks report no drift when sources, runtime/*.mjs, requiredPaths, and contractHashes fully agree", () => {
95
157
  const fixtureRoot = makeFixtureRoot();
96
158
  try {