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
@@ -0,0 +1,17 @@
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
+ // gentle-pi.review-relay/v1 — the Pi host relay handshake (gentle-pi#311 P4,
3
+ // provider side gentle-ai#3249/#3254).
4
+ //
5
+ // gentle-ai admits `pi` as a host-mediated runtime identity only when the
6
+ // launcher that relays the invocation declares this exact contract through
7
+ // this exact environment variable. Any other value (or none) keeps Pi
8
+ // fail-closed at admission, before any repository, target, or authority work.
9
+ // That refusal is correct provider behavior and is never worked around here;
10
+ // the single central gentle-ai process runner injects this declaration on
11
+ // every invocation the extension makes.
12
+ //
13
+ // Both values are compiled constants: the contract identity is versioned by
14
+ // release, never by configuration.
15
+
16
+ export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
17
+ export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
@@ -8,11 +8,11 @@ import { fileURLToPath } from "node:url";
8
8
  const root = fileURLToPath(new URL("..", import.meta.url));
9
9
  const sources = [
10
10
  "gentle-ai-binary",
11
+ "review-relay-contract",
11
12
  "review-integration-v2",
12
13
  "native-review-cli",
13
- "git-commit-transaction",
14
14
  ];
15
- const header = "// Generated by scripts/build-git-commit-transaction-runner.mjs. Do not edit.\n";
15
+ const header = "// Generated by scripts/build-runtime-modules.mjs. Do not edit.\n";
16
16
 
17
17
  function assertNoTrailingWhitespace(content, label) {
18
18
  const line = content.split("\n").findIndex((value) => /[ \t]+$/.test(value));
@@ -34,7 +34,7 @@ async function generatedBytes(name) {
34
34
  async function main() {
35
35
  const mode = process.argv[2];
36
36
  if ((mode !== "--write" && mode !== "--check") || process.argv.length !== 3) {
37
- throw new Error("usage: build-git-commit-transaction-runner.mjs --write|--check");
37
+ throw new Error("usage: build-runtime-modules.mjs --write|--check");
38
38
  }
39
39
  const runtime = join(root, "runtime");
40
40
  if (mode === "--write") await mkdir(runtime, { recursive: true });
@@ -55,9 +55,9 @@ async function main() {
55
55
  if (actual !== expected) drift.push(destination);
56
56
  }
57
57
  if (drift.length > 0) {
58
- throw new Error(`generated commit transaction runtime is stale: ${drift.join(", ")}`);
58
+ throw new Error(`generated runtime is stale: ${drift.join(", ")}`);
59
59
  }
60
- process.stdout.write(`commit transaction runtime ${mode === "--write" ? "generated" : "matches TypeScript sources"} (${sources.length} modules)\n`);
60
+ process.stdout.write(`runtime ${mode === "--write" ? "generated" : "matches TypeScript sources"} (${sources.length} modules)\n`);
61
61
  }
62
62
 
63
63
  main().catch((error) => {
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // Offline drift check for the mirrored gentle-ai provider contract bundle
3
+ // (gentle-pi#311 P2).
4
+ //
5
+ // Fails when the mirror, the lock record, the schemas, the vectors, or the
6
+ // generated consumer baselines disagree in any direction. Runs completely
7
+ // offline against `contracts/review-provider-contract-mirror/` and never
8
+ // touches the network or the Gentle AI release pin.
9
+
10
+ import { createHash } from "node:crypto";
11
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import {
15
+ generateProviderContractBaselines,
16
+ piRuntimeRegistration,
17
+ verifyProviderContractBundleTree,
18
+ } from "../lib/provider-contract-bundle.ts";
19
+ import {
20
+ PROVIDER_CONTRACT_ACQUISITION,
21
+ PROVIDER_CONTRACT_LOCK_FILE,
22
+ PROVIDER_CONTRACT_LOCK_SCHEMA,
23
+ PROVIDER_CONTRACT_MIRROR_ROOT,
24
+ } from "./mirror-provider-contract.mjs";
25
+
26
+ function sha256Hex(bytes) {
27
+ return createHash("sha256").update(bytes).digest("hex");
28
+ }
29
+
30
+ function recordEquals(problems, label, expected, actual) {
31
+ const expectedKeys = Object.keys(expected).sort();
32
+ const actualKeys = Object.keys(actual).sort();
33
+ for (const key of expectedKeys) {
34
+ if (!(key in actual)) problems.push(`${label}: ${key} is recorded in the lock but absent from the mirror`);
35
+ else if (actual[key] !== expected[key]) problems.push(`${label}: ${key} drifted (lock ${expected[key]}, mirror ${actual[key]})`);
36
+ }
37
+ for (const key of actualKeys) {
38
+ if (!(key in expected)) problems.push(`${label}: ${key} exists in the mirror but is not recorded in the lock`);
39
+ }
40
+ }
41
+
42
+ // Returns a list of drift problems; an empty list means the mirror, lock,
43
+ // schemas, vectors, and generated baselines all agree.
44
+ export function checkProviderContractMirror(packageRoot) {
45
+ const problems = [];
46
+ const mirrorRoot = join(packageRoot, ...PROVIDER_CONTRACT_MIRROR_ROOT.split("/"));
47
+ const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
48
+ if (!existsSync(lockPath)) {
49
+ return [`${PROVIDER_CONTRACT_MIRROR_ROOT}/${PROVIDER_CONTRACT_LOCK_FILE} is missing; run scripts/mirror-provider-contract.mjs against a verified local bundle`];
50
+ }
51
+
52
+ let lock;
53
+ try {
54
+ lock = JSON.parse(readFileSync(lockPath, "utf8"));
55
+ } catch (error) {
56
+ return [`lock record is not valid JSON: ${error instanceof Error ? error.message : String(error)}`];
57
+ }
58
+ if (lock === null || typeof lock !== "object" || Array.isArray(lock)) return ["lock record is not a JSON object"];
59
+ if (lock.schema !== PROVIDER_CONTRACT_LOCK_SCHEMA) problems.push(`lock schema is ${JSON.stringify(lock.schema)}; expected ${JSON.stringify(PROVIDER_CONTRACT_LOCK_SCHEMA)}`);
60
+ if (lock.acquisition !== PROVIDER_CONTRACT_ACQUISITION) problems.push(`lock acquisition is ${JSON.stringify(lock.acquisition)}; expected ${JSON.stringify(PROVIDER_CONTRACT_ACQUISITION)}`);
61
+ if (typeof lock.contract_semver !== "string") return [...problems, "lock contract_semver is not a string"];
62
+ if (lock.pi_registered !== true) problems.push("lock does not record pi as a registered runtime identity");
63
+
64
+ // The mirror directory must contain exactly the lock and the locked version.
65
+ const expectedVersionDirectory = `v${lock.contract_semver}`;
66
+ for (const entry of readdirSync(mirrorRoot)) {
67
+ if (entry !== PROVIDER_CONTRACT_LOCK_FILE && entry !== expectedVersionDirectory) {
68
+ problems.push(`mirror has unexpected entry ${JSON.stringify(entry)}; only ${PROVIDER_CONTRACT_LOCK_FILE} and ${expectedVersionDirectory} belong there`);
69
+ }
70
+ }
71
+
72
+ const versionRoot = join(mirrorRoot, expectedVersionDirectory);
73
+ let bundle;
74
+ try {
75
+ bundle = verifyProviderContractBundleTree(join(versionRoot, "bundle"));
76
+ } catch (error) {
77
+ return [...problems, `mirrored bundle failed verification: ${error instanceof Error ? error.message : String(error)}`];
78
+ }
79
+
80
+ if (bundle.contractSemver !== lock.contract_semver) {
81
+ problems.push(`mirrored bundle declares contract ${bundle.contractSemver}; lock records ${lock.contract_semver}`);
82
+ }
83
+ if (bundle.treeSha256 !== lock.tree_sha256) {
84
+ problems.push(`mirrored bundle tree digest ${bundle.treeSha256} does not match lock tree_sha256 ${lock.tree_sha256}`);
85
+ }
86
+ recordEquals(problems, "bundle entries", lock.entries ?? {}, Object.fromEntries(bundle.entrySha256));
87
+ const lockRuntimes = Array.isArray(lock.runtimes) ? lock.runtimes : [];
88
+ const bundleRuntimes = [...(bundle.runtimes ?? [])];
89
+ if (JSON.stringify(lockRuntimes) !== JSON.stringify(bundleRuntimes)) {
90
+ problems.push(`lock runtimes ${JSON.stringify(lockRuntimes)} do not match mirrored manifest runtimes ${JSON.stringify(bundleRuntimes)}`);
91
+ }
92
+ if (!piRuntimeRegistration(bundle).registered) {
93
+ problems.push("mirrored bundle does not register the pi runtime identity");
94
+ }
95
+
96
+ // Regenerate the consumer contract surface from the verified mirror and
97
+ // require byte-identical generated files plus matching lock digests.
98
+ const baselines = generateProviderContractBaselines(bundle);
99
+ const generatedRoot = join(versionRoot, "generated");
100
+ const onDisk = existsSync(generatedRoot) ? readdirSync(generatedRoot).sort() : [];
101
+ const expectedGenerated = [...baselines.keys()].sort();
102
+ for (const name of onDisk) {
103
+ if (!baselines.has(name)) problems.push(`generated/${name} exists on disk but is not a generated provider baseline`);
104
+ }
105
+ const actualGeneratedSha256 = {};
106
+ for (const name of expectedGenerated) {
107
+ const path = join(generatedRoot, name);
108
+ if (!existsSync(path)) {
109
+ problems.push(`generated/${name} is missing from the mirror`);
110
+ continue;
111
+ }
112
+ const actual = readFileSync(path);
113
+ const expected = Buffer.from(baselines.get(name), "utf8");
114
+ if (!actual.equals(expected)) problems.push(`generated/${name} is not byte-identical to the baseline derived from the verified mirror`);
115
+ actualGeneratedSha256[`generated/${name}`] = sha256Hex(actual);
116
+ }
117
+ recordEquals(problems, "generated baselines", lock.generated ?? {}, actualGeneratedSha256);
118
+
119
+ return problems;
120
+ }
121
+
122
+ async function main() {
123
+ const packageRoot = join(fileURLToPath(new URL("..", import.meta.url)));
124
+ const problems = checkProviderContractMirror(packageRoot);
125
+ if (problems.length > 0) {
126
+ console.error("gentle-pi provider contract mirror has drifted:");
127
+ for (const problem of problems) console.error(`- ${problem}`);
128
+ console.error("\nRe-run scripts/mirror-provider-contract.mjs against a verified local bundle, or restore the mirrored bytes.");
129
+ process.exit(1);
130
+ }
131
+ const lock = JSON.parse(readFileSync(join(packageRoot, ...PROVIDER_CONTRACT_MIRROR_ROOT.split("/"), PROVIDER_CONTRACT_LOCK_FILE), "utf8"));
132
+ console.log(`gentle-pi provider contract mirror check passed (contract ${lock.contract_semver}, ${Object.keys(lock.entries).length} bundle entries, ${Object.keys(lock.generated).length} generated baselines, acquisition ${lock.acquisition}).`);
133
+ }
134
+
135
+ const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
136
+ if (isMainModule) {
137
+ await main();
138
+ }
@@ -19,24 +19,36 @@ import { execFile } from "node:child_process";
19
19
  import { promisify } from "node:util";
20
20
 
21
21
  const execFileAsync = promisify(execFile);
22
- const RELEASE_BASE_URL = "https://github.com/Gentleman-Programming/gentle-ai/releases/download/v2.2.2/";
23
22
  const MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
24
23
  const MAX_REDIRECTS = 3;
25
24
  const DOWNLOAD_TIMEOUTS = { headers: 10_000, body: 30_000, attempts: 2, retryDelay: 100 };
26
25
  const GO_COMMAND_TIMEOUT_MS = 120_000;
26
+ // `go install` compiles the pinned module from source on Windows. A clean
27
+ // build on a cold module cache measured about 232 s (#400), so the 120 s
28
+ // probe bound killed a healthy build; fifteen minutes leaves close to a 4x
29
+ // margin for slower disks and real-time scanners while still bounding a hung
30
+ // toolchain. The `go version` and `where.exe` probes keep the 120 s bound.
31
+ const GO_INSTALL_TIMEOUT_MS = 15 * 60_000;
27
32
  const GO_COMMAND_MAX_BUFFER = 1024 * 1024;
28
33
  const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
29
- export const INSTALLER_VERSION = "2.2.2";
34
+ // The one authoritative pinned Gentle AI version. Every other location in this
35
+ // module (the release download URL, the Windows source tag, and the reported
36
+ // version check below) derives from this constant instead of repeating the
37
+ // literal, so a pin bump cannot leave a stale copy behind. See
38
+ // scripts/install-gentle-ai.mjs for the incident that motivated this.
39
+ export const INSTALLER_VERSION = "2.5.0";
40
+ export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
30
41
  export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
31
42
  SIGNED_RELEASE_ASSET: "signed-release-asset",
32
43
  GO_SUMDB_SOURCE_BUILD: "go-sumdb-source-build",
33
44
  });
34
45
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai";
35
46
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v2";
36
- export const GENTLE_AI_WINDOWS_SOURCE_TAG = "v2.2.2";
37
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.2.2`
38
- // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum.
39
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:YZcI5dRvoHm82I2CULvgBkB2M3UQQGarYO/u/Nt5LSc=";
47
+ export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.5.0`
49
+ // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
+ // tag resolves to commit f5dd1a6c, the published v2.5.0 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:0nvTLJFAf9ruBDwTvi+aDrWt/dt/GKvUP2cFVB6c2DI=";
40
52
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
41
53
  export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
42
54
  export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
@@ -55,28 +67,73 @@ export class GentleAiInstallerError extends Error {
55
67
  // Sentinel used while a re-pinned gentle-ai release is not yet published. A
56
68
  // sentinel digest can never match a real SHA-256, so installation fails closed,
57
69
  // and verify-package-files.mjs refuses to pack/publish while any digest below
58
- // still holds it. The v2.2.2 digests are pinned from the published release:
70
+ // still holds it. The v2.5.0 digests are pinned from the published release:
59
71
  // archive sha256 values verified against the minisign-signed checksums.txt and
60
72
  // freshly computed hashes; binary sha256 values computed from the extracted
61
73
  // executables.
62
74
  export const GENTLE_AI_PENDING_DIGEST = "PENDING-GENTLE-AI-RELEASE-DIGEST";
63
75
 
76
+ // A raw prerelease asset is less durable than a signed stable archive, and the
77
+ // install path has no other Darwin/Linux source, so the gentle-pi repository
78
+ // hosts a byte-identical mirror of the pinned prerelease assets under its own
79
+ // release tag. The mirror is only a second download source: every byte from
80
+ // either source must still match the same pinned SHA-256 digests, and a
81
+ // checksum mismatch never falls through to the other source.
82
+ export const GENTLE_AI_MIRROR_RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-pi/releases/download/gentle-ai-mirror-v${INSTALLER_VERSION}/`;
83
+ export const GENTLE_AI_ASSET_UNAVAILABLE_CODE = "GENTLE_AI_ASSET_UNAVAILABLE";
84
+
64
85
  function asset(name, sha256, binarySha256, executable) {
65
- return Object.freeze({ name, sha256, binarySha256, executable, url: `${RELEASE_BASE_URL}${name}` });
86
+ const mirrorUrl = INSTALLER_VERSION.includes("-") ? `${GENTLE_AI_MIRROR_RELEASE_BASE_URL}${name}` : undefined;
87
+ return Object.freeze({ name, sha256, binarySha256, executable, url: `${RELEASE_BASE_URL}${name}`, ...(mirrorUrl === undefined ? {} : { mirrorUrl }) });
88
+ }
89
+
90
+ // Download failure (not integrity failure) is the only condition that falls
91
+ // through to the mirror; when both sources are unavailable the error names the
92
+ // documented recovery instead of leaving a dead end until the next repin.
93
+ async function downloadPinnedGentleAiAsset(asset, destination, options) {
94
+ const download = options.download ?? downloadGentleAiAsset;
95
+ try {
96
+ await download(asset.url, destination);
97
+ } catch (primaryError) {
98
+ if (asset.mirrorUrl === undefined) throw primaryError;
99
+ try {
100
+ await download(asset.mirrorUrl, destination);
101
+ } catch (mirrorError) {
102
+ throw new GentleAiInstallerError(
103
+ GENTLE_AI_ASSET_UNAVAILABLE_CODE,
104
+ `Gentle AI ${asset.name} is unavailable from the pinned release and its gentle-pi mirror. Retry later, or set GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1 and register a locally built v${INSTALLER_VERSION} binary through ~/.pi/gentle-ai/dev-binary.json.`,
105
+ mirrorError,
106
+ );
107
+ }
108
+ }
66
109
  }
67
110
 
68
111
  // Windows is absent from signed release archives on purpose. gentle-ai stopped
69
- // distributing unsigned Windows builds in c4b764d0, so v2.2.2 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v2.5.0 publishes signed
70
113
  // Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
71
114
  // exact-tag Go SumDB source-build path below; restore archive rows only when
72
115
  // upstream ships signed Windows assets.
73
116
  export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
74
- "darwin/amd64": asset("gentle-ai_2.2.2_darwin_amd64.tar.gz", "5ca67829903bf4c6b14665664f80f9d8216c84b10c8e50870d297f452cefb9dc", "9b239423450562d026384f482bbd2f1e3f2820431a84f0921743ac3df9d632de", "gentle-ai"),
75
- "darwin/arm64": asset("gentle-ai_2.2.2_darwin_arm64.tar.gz", "0193e1a284444dccee2863d31b8dbb76a982e8f9111955908d6a9131c1a5490e", "149b97248552c5e03ebc4d991f86b1360fb847a40fc315555a8aa256f95baca0", "gentle-ai"),
76
- "linux/amd64": asset("gentle-ai_2.2.2_linux_amd64.tar.gz", "b85bbb20eb2236de97b261df16cfc8d8394dfd07a137e885c4889b62d0c20fa1", "00d5732e8dd3945956800217a4f60213c2d9ca63351092a2cb7f4e5f9ece54f9", "gentle-ai"),
77
- "linux/arm64": asset("gentle-ai_2.2.2_linux_arm64.tar.gz", "61e7077342448273f0c43af49ce4d182594bab5e4f86f812975af2fbe69e3b0b", "a92685aa7dbea0cc4297d016569b3defe9bb30a7374a620021e245b74f50eb68", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_2.5.0_darwin_amd64.tar.gz", "8204ed692d7ff16fae2eb8bd7b3132b5e4e4dd7b9ea40e93bfced57394da27c0", "929817d320e94ad62be7ed546ad8746e62bd63ab97e3f38c94456f221f163eca", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_2.5.0_darwin_arm64.tar.gz", "ce06831cea9fd2ac7b2b36d9e98e63988246e0fbafbbd2cc7c003ec2f1463a23", "c859cd88138eb41e83ef9b75309697aaff2acb9f720c9a029b432095f66aea02", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_2.5.0_linux_amd64.tar.gz", "2ba84a3a7ba2b1193019bde2acb05b02cbf222b667568c919686352b3caab113", "5acedd14b7927a7665636bf728c14a614597ce85dd91313bc50cbc96881c6cab", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_2.5.0_linux_arm64.tar.gz", "16a6243d17c146e3fc0024f6f005c5bd141fbb2dda4b56e269318e499ddc170b", "ce75ba1174955813ef462f4e7a50dd59a8e8167575033f6e17aa8d8488630cb8", "gentle-ai"),
78
121
  });
79
122
 
123
+ // A pinned asset is either a signed archive or, for a prerelease pin only,
124
+ // the raw executable itself: prereleases are hand-built and ship raw binaries,
125
+ // while every stable release publishes signed archives, so a raw asset under a
126
+ // stable pin means the pin itself is wrong. Anything else fails closed before
127
+ // a byte is downloaded.
128
+ export function gentleAiAssetForm(name, installerVersion = INSTALLER_VERSION) {
129
+ if (name.endsWith(".tar.gz") || name.endsWith(".zip")) return "archive";
130
+ if (/^gentle-ai_[0-9A-Za-z.+~-]+_(darwin|linux|windows)_(amd64|arm64)(\.exe)?$/.test(name)) {
131
+ if (!installerVersion.includes("-")) throw new Error(`raw Gentle AI release asset ${name} is only admitted for a prerelease pin; stable pin ${installerVersion} requires a signed archive`);
132
+ return "raw-binary";
133
+ }
134
+ throw new Error(`unsupported Gentle AI release asset form: ${name}`);
135
+ }
136
+
80
137
  function upstreamArchitecture(architecture) {
81
138
  return architecture === "x64" ? "amd64" : architecture;
82
139
  }
@@ -96,7 +153,7 @@ export function resolveGentleAiReleaseAsset(platform = process.platform, archite
96
153
  const windowsSource = isWindowsGoSumdbSourceTarget(platform, architecture)
97
154
  ? "; Windows x64/arm64 use Go SumDB source installation because no signed Windows archive is published"
98
155
  : "";
99
- throw new Error(`unsupported Gentle AI platform/architecture: ${platform}/${architecture}; signed archive pairs are darwin/x64, darwin/arm64, linux/x64, and linux/arm64${windowsSource}`);
156
+ throw new Error(`unsupported Gentle AI platform/architecture: ${platform}/${architecture}; pinned release asset pairs are darwin/x64, darwin/arm64, linux/x64, and linux/arm64${windowsSource}`);
100
157
  }
101
158
  return resolved;
102
159
  }
@@ -241,8 +298,8 @@ function isCanonicalManifest(contents, parsed, expected) {
241
298
 
242
299
  function commandOutput(result) { return typeof result?.stdout === "string" ? result.stdout : ""; }
243
300
 
244
- function commandOptions(env, cwd) {
245
- return { cwd, env, shell: false, windowsHide: true, timeout: GO_COMMAND_TIMEOUT_MS, maxBuffer: GO_COMMAND_MAX_BUFFER };
301
+ function commandOptions(env, cwd, timeout = GO_COMMAND_TIMEOUT_MS) {
302
+ return { cwd, env, shell: false, windowsHide: true, timeout, maxBuffer: GO_COMMAND_MAX_BUFFER };
246
303
  }
247
304
 
248
305
  function outputVersion(output) {
@@ -330,11 +387,13 @@ async function verifyGoBuildMetadata(execute, goPath, binaryPath, environment, c
330
387
  catch (error) { if (error instanceof GentleAiInstallerError) throw error; throw new GentleAiInstallerError(GENTLE_AI_GO_INSTALL_FAILED_CODE, "Gentle AI source build metadata could not be verified.", error); }
331
388
  }
332
389
 
390
+ function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
391
+
333
392
  async function assertExactGentleAiVersion(execute, binaryPath, environment, cwd) {
334
393
  let result;
335
394
  try { result = await runCommand(execute, binaryPath, ["version"], commandOptions(environment, cwd)); }
336
395
  catch (error) { throw new GentleAiInstallerError(GENTLE_AI_VERSION_MISMATCH_CODE, `Gentle AI source build at ${binaryPath} could not report its version.`, error); }
337
- if (!/^gentle-ai 2\.2\.2\r?\n?$/.test(commandOutput(result))) throw new GentleAiInstallerError(GENTLE_AI_VERSION_MISMATCH_CODE, `Gentle AI source build reported ${JSON.stringify(commandOutput(result).trim())}; expected gentle-ai ${INSTALLER_VERSION}.`);
396
+ if (!new RegExp(`^gentle-ai ${escapeRegExp(INSTALLER_VERSION)}\\r?\\n?$`).test(commandOutput(result))) throw new GentleAiInstallerError(GENTLE_AI_VERSION_MISMATCH_CODE, `Gentle AI source build reported ${JSON.stringify(commandOutput(result).trim())}; expected gentle-ai ${INSTALLER_VERSION}.`);
338
397
  }
339
398
 
340
399
  function sameFile(before, after) { return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs; }
@@ -574,7 +633,7 @@ async function installWindowsGentleAiFromGoSumdb(options, packageRoot, architect
574
633
  const existing = versionBundlePath(runtimeRoot);
575
634
  if (await existingWindowsSourceBundleMatches(existing, execute, goPath, environment, architecture)) return { installed: false, binaryPath: join(existing, "gentle-ai.exe"), method: GENTLE_AI_INSTALL_METHOD.GO_SUMDB_SOURCE_BUILD };
576
635
  await assertGoToolchain(execute, goPath, environment, stagingDirectory);
577
- try { await runCommand(execute, goPath, ["install", GENTLE_AI_WINDOWS_SOURCE_PACKAGE], commandOptions(environment, stagingDirectory)); }
636
+ try { await runCommand(execute, goPath, ["install", GENTLE_AI_WINDOWS_SOURCE_PACKAGE], commandOptions(environment, stagingDirectory, GO_INSTALL_TIMEOUT_MS)); }
578
637
  catch (error) { throw new GentleAiInstallerError(GENTLE_AI_GO_INSTALL_FAILED_CODE, `Gentle AI Go SumDB source installation failed for ${GENTLE_AI_WINDOWS_SOURCE_PACKAGE}.`, error); }
579
638
  const builtBinary = join(environment.GOBIN, "gentle-ai.exe"), binaryPath = join(stagingDirectory, "gentle-ai.exe");
580
639
  const details = await lstat(builtBinary);
@@ -599,18 +658,22 @@ async function installSignedRelease(options, packageRoot, platform, arch, asset)
599
658
  if (await existingSignedBundleMatches(existing, asset, platform)) return { installed: false, binaryPath: join(existing, asset.executable), asset };
600
659
  const stagingDirectory = await mkdtemp(join(runtimeRoot, `.v${INSTALLER_VERSION}.staging-`));
601
660
  try {
661
+ const form = gentleAiAssetForm(asset.name);
602
662
  const archive = join(stagingDirectory, asset.name);
603
- await (options.download ?? downloadGentleAiAsset)(asset.url, archive);
663
+ await downloadPinnedGentleAiAsset(asset, archive, options);
604
664
  if ((await sha256File(archive)) !== asset.sha256) throw new Error(`Gentle AI archive checksum mismatch for ${asset.name}`);
665
+ let source = archive;
605
666
  const extracted = join(stagingDirectory, "extracted");
606
- await (options.extractArchive ?? extractGentleAiArchive)(archive, extracted);
607
- const source = await expectedRegularFile(extracted, asset.executable);
667
+ if (form === "archive") {
668
+ await (options.extractArchive ?? extractGentleAiArchive)(archive, extracted);
669
+ source = await expectedRegularFile(extracted, asset.executable);
670
+ }
608
671
  if (asset.binarySha256 && (await sha256File(source)) !== asset.binarySha256) throw new Error(`Gentle AI binary checksum mismatch for ${asset.name}`);
609
672
  const binaryPath = join(stagingDirectory, asset.executable);
610
673
  await copyFile(source, binaryPath);
611
674
  if (platform !== "win32") await chmod(binaryPath, 0o700);
612
675
  await writeFile(join(stagingDirectory, "integrity.json"), canonicalManifest(signedReleaseManifest(asset, await sha256File(binaryPath))), { mode: 0o600 });
613
- await safeRemoveDirectory(extracted);
676
+ if (form === "archive") await safeRemoveDirectory(extracted);
614
677
  await rm(archive, { force: true });
615
678
  const published = await publishBundle(runtimeRoot, stagingDirectory, options);
616
679
  return { installed: true, binaryPath: join(published, asset.executable), asset };