gentle-pi 2.1.2 → 2.2.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 (171) hide show
  1. package/README.md +70 -12
  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 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  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 +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -19,14 +19,14 @@ import {
19
19
  trustedSystemExtractor,
20
20
  } from "../scripts/gentle-ai-installer.mjs";
21
21
 
22
- // v2.2.2 digests pinned from the published release: archive sha256 values match
22
+ // v2.4.0 digests pinned from the published release: archive sha256 values match
23
23
  // checksums.txt and freshly computed hashes; binary sha256 values were computed
24
24
  // from the extracted executables of each verified archive.
25
25
  const EXPECTED_ASSETS = {
26
- "darwin/amd64": { name: "gentle-ai_2.2.2_darwin_amd64.tar.gz", sha256: "5ca67829903bf4c6b14665664f80f9d8216c84b10c8e50870d297f452cefb9dc", binarySha256: "9b239423450562d026384f482bbd2f1e3f2820431a84f0921743ac3df9d632de" },
27
- "darwin/arm64": { name: "gentle-ai_2.2.2_darwin_arm64.tar.gz", sha256: "0193e1a284444dccee2863d31b8dbb76a982e8f9111955908d6a9131c1a5490e", binarySha256: "149b97248552c5e03ebc4d991f86b1360fb847a40fc315555a8aa256f95baca0" },
28
- "linux/amd64": { name: "gentle-ai_2.2.2_linux_amd64.tar.gz", sha256: "b85bbb20eb2236de97b261df16cfc8d8394dfd07a137e885c4889b62d0c20fa1", binarySha256: "00d5732e8dd3945956800217a4f60213c2d9ca63351092a2cb7f4e5f9ece54f9" },
29
- "linux/arm64": { name: "gentle-ai_2.2.2_linux_arm64.tar.gz", sha256: "61e7077342448273f0c43af49ce4d182594bab5e4f86f812975af2fbe69e3b0b", binarySha256: "a92685aa7dbea0cc4297d016569b3defe9bb30a7374a620021e245b74f50eb68" },
26
+ "darwin/amd64": { name: "gentle-ai_2.4.0_darwin_amd64.tar.gz", sha256: "c3f5a4251e927c41864e7a9a5670ac3fce97c6282755ec8e9b2e7e064067f750", binarySha256: "c4d25b98bae0d06b33baa23ba997cb64ab63c2e93e97983bcb9c239139bceaba" },
27
+ "darwin/arm64": { name: "gentle-ai_2.4.0_darwin_arm64.tar.gz", sha256: "2240ae797119dd1dc5a0dd2fd2711077ce6712e66ab0b67461344c983478f7f4", binarySha256: "54a8a5957ce4f10f36b46ef818c888f484a3e213a71faca6b7783af6c029eb77" },
28
+ "linux/amd64": { name: "gentle-ai_2.4.0_linux_amd64.tar.gz", sha256: "517eb8420cf966b74be1413a7ea1036119a5ccbb376e0778aa9f4d4d283e2e9a", binarySha256: "0be446737f8f1a9458fd2891d488beb2d72786578afead9bbf2d6ddddc9ec113" },
29
+ "linux/arm64": { name: "gentle-ai_2.4.0_linux_arm64.tar.gz", sha256: "1badec9cad1f2bbeb22b5c6804c3a88c601d844abae231264e416e8095f2a15f", binarySha256: "cdd8fa43c3417e844cc11d2e436d4667edb0a54f086e0eb1d6cd3f7493bfee5a" },
30
30
  } as const;
31
31
 
32
32
  test("default installer package root is the package containing scripts, not its parent", () => {
@@ -37,15 +37,15 @@ test("default installer package root is the package containing scripts, not its
37
37
  assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
38
38
  });
39
39
 
40
- test("release mapping selects only the supported official v2.2.2 archive and pinned digests", () => {
40
+ test("release mapping selects only the supported official v2.4.0 archive and pinned digests", () => {
41
41
  assert.deepEqual(
42
42
  Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
43
43
  EXPECTED_ASSETS,
44
44
  );
45
- assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.2.2_linux_amd64.tar.gz");
46
- assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.2.2_darwin_arm64.tar.gz");
45
+ assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.4.0_linux_amd64.tar.gz");
46
+ assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.4.0_darwin_arm64.tar.gz");
47
47
  for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
48
- assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.2\.2\//);
48
+ assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.4\.0\//);
49
49
  }
50
50
  });
51
51
 
@@ -66,7 +66,7 @@ test("release digests are all-or-none and install fails closed while any digest
66
66
  }),
67
67
  /checksum mismatch/,
68
68
  );
69
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
69
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
70
70
  }
71
71
  });
72
72
 
@@ -95,7 +95,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
95
95
  const metadata = [
96
96
  "gentle-ai.exe: go1.25.10",
97
97
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
98
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.2.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
98
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.4.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
99
99
  "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
100
100
  ].join("\n");
101
101
  const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
@@ -113,7 +113,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
113
113
  return { stdout: "", stderr: "" };
114
114
  }
115
115
  if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
116
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.2.2\n", stderr: "" };
116
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.4.0\n", stderr: "" };
117
117
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
118
118
  };
119
119
  return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
@@ -129,7 +129,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
129
129
  const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
130
130
  assert.equal(result.installed, true);
131
131
  assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
132
- ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.2.2"], ["version", "-m"],
132
+ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.4.0"], ["version", "-m"],
133
133
  ]);
134
134
  }
135
135
  });
@@ -148,7 +148,7 @@ test("Windows source install reports missing or too-old Go without publishing a
148
148
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
149
149
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
150
150
  );
151
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai.exe")), false);
151
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai.exe")), false);
152
152
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
153
153
  }
154
154
  });
@@ -167,7 +167,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
167
167
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
168
168
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
169
169
  );
170
- const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.2.2");
170
+ const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.4.0");
171
171
  assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
172
172
  assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
173
173
  assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
@@ -188,7 +188,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
188
188
  assert.equal(reused.installed, false);
189
189
  assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
190
190
 
191
- await writeFile(join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json"), "{}\n");
191
+ await writeFile(join(packageRoot, ".gentle-ai", "v2.4.0", "integrity.json"), "{}\n");
192
192
  const repaired = windowsGoFixture();
193
193
  repaired.setGoExecutable(goPath);
194
194
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
@@ -230,7 +230,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
230
230
  const metadata = fixtureOptions.metadataOverride ?? [
231
231
  "gentle-ai.exe: go1.25.10",
232
232
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
233
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.2.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
233
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.4.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
234
234
  "\tbuild\t-buildmode=exe",
235
235
  "\tbuild\t-compiler=gc",
236
236
  "\tbuild\tCGO_ENABLED=0",
@@ -250,7 +250,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
250
250
  return { stdout: "", stderr: "" };
251
251
  }
252
252
  if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
253
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.2.2\n", stderr: "" };
253
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.4.0\n", stderr: "" };
254
254
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
255
255
  };
256
256
  return {
@@ -292,7 +292,7 @@ test("Windows source installation resolves one validated Go executable and seals
292
292
  const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
293
293
  assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
294
294
  ["version"],
295
- ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.2.2"],
295
+ ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.4.0"],
296
296
  ["version", "-m"],
297
297
  ]);
298
298
  for (const call of goCalls) {
@@ -318,14 +318,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
318
318
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
319
319
  const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
320
320
  const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
321
- const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json"), "utf8")) as Record<string, string>;
321
+ const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.4.0", "integrity.json"), "utf8")) as Record<string, string>;
322
322
  assert.equal(result.installed, true);
323
323
  assert.deepEqual(manifest, {
324
- version: "2.2.2",
324
+ version: "2.4.0",
325
325
  method: "go-sumdb-source-build",
326
326
  package: "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
327
327
  module: "github.com/gentleman-programming/gentle-ai/v2",
328
- tag: "v2.2.2",
328
+ tag: "v2.4.0",
329
329
  architecture: "arm64",
330
330
  binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
331
331
  moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
@@ -345,12 +345,12 @@ test("Windows source installation rejects Go metadata for a different architectu
345
345
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
346
346
  (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
347
347
  );
348
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai.exe")), false);
348
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai.exe")), false);
349
349
  });
350
350
 
351
351
  test("Windows source installation treats a fresh ownerless lock as active", async () => {
352
352
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
353
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
353
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
354
354
  await mkdir(lockPath, { recursive: true });
355
355
  const fixture = await hardenedWindowsGoFixture(packageRoot);
356
356
  await assertManualLockRecoveryRequired(packageRoot, fixture);
@@ -358,7 +358,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
358
358
 
359
359
  test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
360
360
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
361
- const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock", "owner.json");
361
+ const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock", "owner.json");
362
362
  const fixture = await hardenedWindowsGoFixture(packageRoot);
363
363
  let replacedOwner = false;
364
364
  const run = async (...arguments_: Parameters<typeof fixture.run>) => {
@@ -375,7 +375,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
375
375
  });
376
376
 
377
377
  async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
378
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
378
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
379
379
  await assert.rejects(
380
380
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
381
381
  (error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
@@ -385,18 +385,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
385
385
  }
386
386
 
387
387
  function tombstonePath(packageRoot: string, nonce: string): string {
388
- return join(packageRoot, ".gentle-ai", `.v2.2.2.install.tombstone-${nonce}`);
388
+ return join(packageRoot, ".gentle-ai", `.v2.4.0.install.tombstone-${nonce}`);
389
389
  }
390
390
 
391
391
  async function tombstones(packageRoot: string): Promise<string[]> {
392
392
  const runtimeRoot = join(packageRoot, ".gentle-ai");
393
393
  return existsSync(runtimeRoot)
394
- ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.2.2.install.tombstone-"))
394
+ ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.4.0.install.tombstone-"))
395
395
  : [];
396
396
  }
397
397
 
398
398
  function backupBundlePath(packageRoot: string, nonce: string): string {
399
- return join(packageRoot, ".gentle-ai", `.v2.2.2.backup-${nonce}`);
399
+ return join(packageRoot, ".gentle-ai", `.v2.4.0.backup-${nonce}`);
400
400
  }
401
401
 
402
402
  async function copyWindowsBundle(source: string, destination: string): Promise<void> {
@@ -407,7 +407,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
407
407
 
408
408
  test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
409
409
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
410
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
410
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
411
411
  await mkdir(lockPath, { recursive: true });
412
412
  const fixture = await hardenedWindowsGoFixture(packageRoot);
413
413
  await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
@@ -415,7 +415,7 @@ test("Windows source installation fails closed for an ownerless lock even after
415
415
 
416
416
  test("Windows source installation fails closed for a stale owner lock", async () => {
417
417
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
418
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
418
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
419
419
  await mkdir(lockPath, { recursive: true });
420
420
  await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
421
421
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -467,7 +467,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
467
467
  (error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
468
468
  );
469
469
  assert.equal(existsSync(foreignTombstone), true);
470
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock")), false);
470
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock")), false);
471
471
  assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
472
472
  });
473
473
 
@@ -475,7 +475,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
475
475
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
476
476
  const fixture = await hardenedWindowsGoFixture(packageRoot);
477
477
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
478
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock")), false);
478
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock")), false);
479
479
  assert.deepEqual(await tombstones(packageRoot), []);
480
480
  const reuse = await hardenedWindowsGoFixture(packageRoot);
481
481
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
@@ -485,7 +485,7 @@ test("Windows source recovers a valid backup after a crash between publication r
485
485
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
486
486
  const initial = await hardenedWindowsGoFixture(packageRoot);
487
487
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
488
- const live = join(packageRoot, ".gentle-ai", "v2.2.2");
488
+ const live = join(packageRoot, ".gentle-ai", "v2.4.0");
489
489
  const backup = backupBundlePath(packageRoot, "crash");
490
490
  await rename(live, backup);
491
491
  const recovery = await hardenedWindowsGoFixture(packageRoot);
@@ -521,7 +521,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
521
521
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
522
522
  const initial = await hardenedWindowsGoFixture(packageRoot);
523
523
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
524
- const live = join(packageRoot, ".gentle-ai", "v2.2.2");
524
+ const live = join(packageRoot, ".gentle-ai", "v2.4.0");
525
525
  const backup = backupBundlePath(packageRoot, "valid");
526
526
  await copyWindowsBundle(live, backup);
527
527
  const reuse = await hardenedWindowsGoFixture(packageRoot);
@@ -534,7 +534,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
534
534
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
535
535
  const initial = await hardenedWindowsGoFixture(packageRoot);
536
536
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
537
- const live = join(packageRoot, ".gentle-ai", "v2.2.2");
537
+ const live = join(packageRoot, ".gentle-ai", "v2.4.0");
538
538
  const backup = backupBundlePath(packageRoot, "valid");
539
539
  await copyWindowsBundle(live, backup);
540
540
  await writeFile(join(live, "integrity.json"), "{}\n");
@@ -552,7 +552,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
552
552
  const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
553
553
  const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
554
554
  await fixture.waitForInstall();
555
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
555
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
556
556
  await assert.rejects(
557
557
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
558
558
  (error: unknown) => error instanceof Error && error.message.includes(lockPath),
@@ -566,7 +566,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
566
566
 
567
567
  test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
568
568
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
569
- const versionDirectory = join(packageRoot, ".gentle-ai", "v2.2.2");
569
+ const versionDirectory = join(packageRoot, ".gentle-ai", "v2.4.0");
570
570
  await mkdir(versionDirectory, { recursive: true });
571
571
  await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
572
572
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -590,7 +590,7 @@ test("Windows source publication rolls back a prior bundle when final directory
590
590
  test("Darwin/Linux signed bundles retain their four-field manifest and reusable compatibility", async () => {
591
591
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-signed-compatibility-"));
592
592
  const payload = Buffer.from("signed archive fixture");
593
- const asset = { name: "gentle-ai_2.2.2_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" };
593
+ const asset = { name: "gentle-ai_2.4.0_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" };
594
594
  const options = {
595
595
  packageRoot,
596
596
  platform: "linux",
@@ -603,7 +603,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
603
603
  },
604
604
  };
605
605
  await installGentleAi(options);
606
- const manifestPath = join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json");
606
+ const manifestPath = join(packageRoot, ".gentle-ai", "v2.4.0", "integrity.json");
607
607
  const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
608
608
  assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
609
609
  assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
@@ -680,7 +680,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
680
680
  }),
681
681
  /checksum mismatch/,
682
682
  );
683
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
683
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
684
684
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
685
685
  });
686
686
 
@@ -702,7 +702,7 @@ test("installer promotes only the expected regular executable with executable PO
702
702
  await chmod(extracted, 0o700);
703
703
  },
704
704
  });
705
- const binary = join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai");
705
+ const binary = join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai");
706
706
  assert.equal(existsSync(binary), true);
707
707
  assert.equal(await readFile(binary, "utf8"), "native executable");
708
708
  assert.ok(((await stat(binary)).mode & 0o111) !== 0);
@@ -733,7 +733,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
733
733
  }),
734
734
  /binary checksum mismatch/,
735
735
  );
736
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
736
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
737
737
  });
738
738
 
739
739
  test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
@@ -756,7 +756,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
756
756
  },
757
757
  };
758
758
  await installGentleAi(options);
759
- const binary = join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai");
759
+ const binary = join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai");
760
760
  await chmod(binary, 0o600);
761
761
  const repaired = await installGentleAi(options);
762
762
  assert.equal(repaired.installed, true);
@@ -793,7 +793,7 @@ test("installer rejects archives with multiple expected executable entries", asy
793
793
  }),
794
794
  /exactly one regular gentle-ai/,
795
795
  );
796
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
796
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
797
797
  });
798
798
 
799
799
  test("installer rejects an archive without the expected regular executable", async () => {
@@ -811,5 +811,5 @@ test("installer rejects an archive without the expected regular executable", asy
811
811
  }),
812
812
  /non-regular gentle-ai/,
813
813
  );
814
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
814
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
815
815
  });
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
7
  import test from "node:test";
@@ -8,12 +9,14 @@ import { setTimeout as delay } from "node:timers/promises";
8
9
  import {
9
10
  COMMIT_TRANSACTION_STATE,
10
11
  assertNoUnresolvedCommitTransaction,
12
+ abandonCommitTransaction,
11
13
  inspectCommitTransaction,
12
14
  prepareCommitTransactionInvocation,
13
15
  reconcileCommitTransaction,
14
16
  runGitCommitTransaction,
15
17
  verifyCommitTransactionResult,
16
18
  } from "../lib/git-commit-transaction.ts";
19
+ import type { CommitTransactionInvocation } from "../lib/git-commit-transaction.ts";
17
20
  import type { NativeReviewCli, NativeValidateResult } from "../lib/native-review-cli.ts";
18
21
 
19
22
  function git(cwd: string, ...arguments_: string[]): string {
@@ -300,3 +303,228 @@ test("cancellation cannot strand a commit after HEAD advances", async (t) => {
300
303
  assert.equal(result.status, "committed");
301
304
  assert.deepEqual(inspectCommitTransaction(cwd), { status: "clean" });
302
305
  });
306
+
307
+ function unbornRepository(t: test.TestContext): string {
308
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-commit-transaction-unborn-"));
309
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
310
+ git(cwd, "init", "-b", "main");
311
+ git(cwd, "config", "user.name", "Commit Transaction Test");
312
+ git(cwd, "config", "user.email", "commit-transaction@example.invalid");
313
+ writeFileSync(join(cwd, "initial.txt"), "first commit\n");
314
+ git(cwd, "add", "initial.txt");
315
+ return cwd;
316
+ }
317
+
318
+ test("an unborn repository creates its first reviewed commit without a prior HEAD", async (t) => {
319
+ const cwd = unbornRepository(t);
320
+ const intendedTree = git(cwd, "write-tree");
321
+ const command = `git commit ${JSON.stringify("-m")} ${JSON.stringify("initial")}`;
322
+ const invocation = prepareCommitTransactionInvocation({
323
+ command,
324
+ cwd,
325
+ arguments: ["-m", "initial"],
326
+ authorization: {
327
+ lineageId: "unborn-first",
328
+ storeRevision: "sha256:" + "a".repeat(64),
329
+ fingerprint: "sha256:" + "b".repeat(64),
330
+ intendedTree,
331
+ },
332
+ });
333
+ const result = await runGitCommitTransaction(invocation, { nativeReviewCli: native(cwd, "unborn-first") });
334
+ assert.equal(result.status, "committed");
335
+ assert.equal(result.tree, intendedTree);
336
+ assert.equal(result.head, git(cwd, "rev-parse", "HEAD"));
337
+ assert.equal(git(cwd, "rev-parse", "HEAD^{tree}"), intendedTree);
338
+ assert.deepEqual(inspectCommitTransaction(cwd), { status: "clean" });
339
+ const verified = verifyCommitTransactionResult(cwd, result.transactionId);
340
+ assert.equal(verified.tree, intendedTree);
341
+ });
342
+
343
+ test("an unborn repository that fails to commit leaves no HEAD and allows exact retry", async (t) => {
344
+ const cwd = unbornRepository(t);
345
+ const intendedTree = git(cwd, "write-tree");
346
+ const command = `git commit ${JSON.stringify("-m")} ${JSON.stringify("initial")}`;
347
+ const invocation = prepareCommitTransactionInvocation({
348
+ command,
349
+ cwd,
350
+ arguments: ["-m", "initial"],
351
+ authorization: {
352
+ lineageId: "unborn-retry",
353
+ storeRevision: "sha256:" + "a".repeat(64),
354
+ fingerprint: "sha256:" + "b".repeat(64),
355
+ intendedTree,
356
+ },
357
+ });
358
+ await assert.rejects(
359
+ runGitCommitTransaction(invocation, { nativeReviewCli: native(cwd, "unborn-retry", "invalidated") }),
360
+ /native pre-commit validation denied/,
361
+ );
362
+ assert.throws(() => git(cwd, "rev-parse", "--verify", "HEAD"), /fatal|Needed/i);
363
+ assert.equal(inspectCommitTransaction(cwd).record?.state, COMMIT_TRANSACTION_STATE.VALIDATION_FAILED);
364
+ // A denied attempt leaves the index intact and HEAD unborn, so the exact
365
+ // commit command can be retried. The failed transaction requires explicit
366
+ // recovery: abandon archives it (HEAD never moved, so abandon is allowed),
367
+ // then a fresh invocation authorizes the same tree and commits successfully.
368
+ abandonCommitTransaction(cwd);
369
+ assert.equal(inspectCommitTransaction(cwd).status, "clean");
370
+ const retry = prepareCommitTransactionInvocation({
371
+ command,
372
+ cwd,
373
+ arguments: ["-m", "initial"],
374
+ authorization: {
375
+ lineageId: "unborn-retry",
376
+ storeRevision: "sha256:" + "a".repeat(64),
377
+ fingerprint: "sha256:" + "b".repeat(64),
378
+ intendedTree,
379
+ },
380
+ });
381
+ const result = await runGitCommitTransaction(retry, { nativeReviewCli: native(cwd, "unborn-retry") });
382
+ assert.equal(result.status, "committed");
383
+ assert.equal(result.tree, intendedTree);
384
+ assert.equal(result.head, git(cwd, "rev-parse", "HEAD"));
385
+ assert.equal(git(cwd, "rev-parse", "HEAD^{tree}"), intendedTree);
386
+ assert.deepEqual(inspectCommitTransaction(cwd), { status: "clean" });
387
+ const verified = verifyCommitTransactionResult(cwd, result.transactionId);
388
+ assert.equal(verified.tree, intendedTree);
389
+ });
390
+
391
+ function unbornInvocation(cwd: string, lineageId: string): CommitTransactionInvocation {
392
+ return prepareCommitTransactionInvocation({
393
+ command: "git commit -m initial",
394
+ cwd,
395
+ arguments: ["-m", "initial"],
396
+ authorization: {
397
+ lineageId,
398
+ storeRevision: "sha256:" + "a".repeat(64),
399
+ fingerprint: "sha256:" + "b".repeat(64),
400
+ intendedTree: git(cwd, "write-tree"),
401
+ },
402
+ });
403
+ }
404
+
405
+ // Mirrors lib/git-commit-transaction.ts canonicalJson + sha256 so tests can
406
+ // prove the durable repository identity is byte-for-byte compatible with the
407
+ // previous formula `sha256(canonicalJson({ common_directory: commonDir, roots }))`.
408
+ function canonicalJson(value: unknown): string {
409
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
410
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
411
+ const object = value as Record<string, unknown>;
412
+ return `{${Object.keys(object).filter((key) => object[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
413
+ }
414
+ function identitySha256(value: string): string {
415
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
416
+ }
417
+
418
+ test("a born repository keeps the durable transaction identity formula with sorted root commits across the unborn-handling upgrade", async (t) => {
419
+ const cwd = repository(t);
420
+ stage(cwd);
421
+ installHook(cwd, "pre-commit", "exit 23");
422
+ const commonDir = realpathSync(git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"));
423
+ const roots = git(cwd, "rev-list", "--max-parents=0", "HEAD").split(/\r?\n/).filter(Boolean).sort();
424
+ const expected = identitySha256(canonicalJson({ common_directory: commonDir, roots }));
425
+ await assert.rejects(
426
+ runGitCommitTransaction(invocation(cwd, "born-identity"), { nativeReviewCli: native(cwd, "born-identity") }),
427
+ /pre-commit hook failed/,
428
+ );
429
+ const record = inspectCommitTransaction(cwd).record;
430
+ assert.ok(record, "a failing hook leaves an active transaction record carrying the repository identity");
431
+ assert.equal(record!.repository_id, expected);
432
+ assert.equal(record!.common_directory, commonDir);
433
+ abandonCommitTransaction(cwd);
434
+ assert.equal(inspectCommitTransaction(cwd).status, "clean");
435
+ });
436
+
437
+ test("an unborn repository derives a deterministic repository identity without rev-list HEAD and without masking errors", async (t) => {
438
+ const cwd = unbornRepository(t);
439
+ const commonDir = realpathSync(git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"));
440
+ // Unborn has no root commits reachable from HEAD; the identity uses the
441
+ // empty roots set, which is deterministic and avoids `rev-list HEAD`.
442
+ const expected = identitySha256(canonicalJson({ common_directory: commonDir, roots: [] }));
443
+ const intendedTree = git(cwd, "write-tree");
444
+ const command = `git commit ${JSON.stringify("-m")} ${JSON.stringify("initial")}`;
445
+ const invocation = prepareCommitTransactionInvocation({
446
+ command,
447
+ cwd,
448
+ arguments: ["-m", "initial"],
449
+ authorization: {
450
+ lineageId: "unborn-identity",
451
+ storeRevision: "sha256:" + "a".repeat(64),
452
+ fingerprint: "sha256:" + "b".repeat(64),
453
+ intendedTree,
454
+ },
455
+ });
456
+ await assert.rejects(
457
+ runGitCommitTransaction(invocation, { nativeReviewCli: native(cwd, "unborn-identity", "invalidated") }),
458
+ /native pre-commit validation denied/,
459
+ );
460
+ const record = inspectCommitTransaction(cwd).record;
461
+ assert.ok(record, "a denied unborn attempt leaves an active transaction record carrying the repository identity");
462
+ assert.equal(record!.repository_id, expected);
463
+ assert.equal(record!.common_directory, commonDir);
464
+ abandonCommitTransaction(cwd);
465
+ assert.equal(inspectCommitTransaction(cwd).status, "clean");
466
+ });
467
+
468
+ // Resolves the canonical absolute git common directory for asserting the
469
+ // absence of the transaction state root without assuming a literal `.git`.
470
+ function transactionStateRoot(cwd: string): string {
471
+ const commonDir = realpathSync(git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"));
472
+ return join(commonDir, "gentle-pi", "commit-transactions");
473
+ }
474
+
475
+ test("preparation fails closed for a corrupt HEAD ref without writing transaction state and reports corruption", (t) => {
476
+ const cwd = unbornRepository(t);
477
+ // A garbage ref (not a valid SHA) makes HEAD unresolvable. Resolving root
478
+ // commits during repository binding rethrows, so preparation must throw,
479
+ // no transaction state may be written, and inspection reports corruption
480
+ // (repository identity itself cannot be resolved while HEAD is corrupt).
481
+ mkdirSync(join(cwd, ".git", "refs", "heads"), { recursive: true });
482
+ writeFileSync(join(cwd, ".git", "refs", "heads", "main"), "z".repeat(40));
483
+ assert.throws(() => unbornInvocation(cwd, "corrupt-head"));
484
+ assert.equal(existsSync(transactionStateRoot(cwd)), false, "no transaction state root is written for a corrupt HEAD");
485
+ const inspection = inspectCommitTransaction(cwd);
486
+ assert.equal(inspection.status, "corrupted", "repository corruption makes inspection corrupted even with no transaction state written");
487
+ assert.ok(typeof inspection.reason === "string" && inspection.reason.length > 0, "inspection reason is present and actionable");
488
+ });
489
+
490
+ test("preparation fails closed for a symbolic HEAD pointing at a missing object instead of classifying it as unborn", (t) => {
491
+ const cwd = unbornRepository(t);
492
+ // A valid-format SHA with no object: rev-parse --verify HEAD succeeds, so
493
+ // resolveHead returns a commit id rather than classifying HEAD as unborn.
494
+ // Resolving root commits then fails on the missing object, so preparation
495
+ // must throw, no transaction state may be written, and inspection reports
496
+ // corruption. This must never be classified as an unborn empty-roots repo.
497
+ mkdirSync(join(cwd, ".git", "refs", "heads"), { recursive: true });
498
+ writeFileSync(join(cwd, ".git", "refs", "heads", "main"), "0123456789abcdef0123456789abcdef01234567\n");
499
+ assert.throws(() => unbornInvocation(cwd, "missing-object-head"));
500
+ assert.equal(existsSync(transactionStateRoot(cwd)), false, "no transaction state root is written for a missing-object HEAD");
501
+ const inspection = inspectCommitTransaction(cwd);
502
+ assert.equal(inspection.status, "corrupted", "a missing-object HEAD is repository corruption, not a clean or unborn repo");
503
+ assert.ok(typeof inspection.reason === "string" && inspection.reason.length > 0, "inspection reason is present and actionable");
504
+ });
505
+
506
+ test("resolveHead propagates a probe timeout instead of masking it as an unborn HEAD", (t) => {
507
+ const cwd = unbornRepository(t);
508
+ const wrapperDir = mkdtempSync(join(tmpdir(), "gentle-pi-commit-transaction-timeout-"));
509
+ t.after(() => rmSync(wrapperDir, { recursive: true, force: true }));
510
+ const realGit = execFileSync("which", ["git"], { encoding: "utf8" }).trim();
511
+ writeFileSync(join(wrapperDir, "git"), `#!/bin/sh\nif [ "$1" = "rev-parse" ] && [ "$2" = "--verify" ] && [ "$3" = "HEAD" ] && [ $# -eq 3 ]; then\nsleep 30\nelse\nexec ${realGit} "$@"\nfi\n`);
512
+ chmodSync(join(wrapperDir, "git"), 0o755);
513
+ const originalPath = process.env.PATH;
514
+ process.env.PATH = `${wrapperDir}:${originalPath}`;
515
+ try {
516
+ // The probe wrapper sleeps past GIT_TIMEOUT_MS (10s), so execFileSync
517
+ // kills the process and resolveHead rethrows the raw timeout error
518
+ // rather than classifying it as an unborn HEAD. Accept only a failure
519
+ // that genuinely represents a timeout, not any arbitrary error.
520
+ assert.throws(
521
+ () => unbornInvocation(cwd, "timeout-head"),
522
+ (error: unknown) => error !== null && typeof error === "object"
523
+ && ((error as { code?: unknown }).code === "ETIMEDOUT" || (error as { killed?: unknown }).killed === true),
524
+ "resolveHead must propagate a probe timeout instead of masking it as an unborn HEAD",
525
+ );
526
+ } finally {
527
+ process.env.PATH = originalPath;
528
+ }
529
+ assert.equal(inspectCommitTransaction(cwd).status, "clean");
530
+ });