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
@@ -1,802 +0,0 @@
1
- // Generated by scripts/build-git-commit-transaction-runner.mjs. Do not edit.
2
- import { spawn } from "node:child_process";
3
- import { createHash, randomUUID } from "node:crypto";
4
- import {
5
- chmodSync,
6
- closeSync,
7
- existsSync,
8
- fsyncSync,
9
- mkdirSync,
10
- openSync,
11
- readFileSync,
12
- realpathSync,
13
- renameSync,
14
- rmSync,
15
- statSync,
16
- unlinkSync,
17
- writeFileSync,
18
- } from "node:fs";
19
- import { hostname } from "node:os";
20
- import { dirname, isAbsolute, join, resolve } from "node:path";
21
- import { fileURLToPath } from "node:url";
22
- import { execFileSync } from "node:child_process";
23
- import {
24
- createNativeReviewCli,
25
-
26
-
27
- } from "./native-review-cli.mjs";
28
-
29
- const TRANSACTION_SCHEMA = "gentle-pi.git-commit-transaction/v1";
30
- const INVOCATION_SCHEMA = "gentle-pi.git-commit-transaction-invocation/v1";
31
- const INDEX_ASSERTION_SCHEMA = "gentle-pi.git-commit-index-assertion/v1";
32
- const COMMIT_CAPTURE_SCHEMA = "gentle-pi.git-commit-capture/v1";
33
- const GIT_TIMEOUT_MS = 10_000;
34
-
35
- export const COMMIT_TRANSACTION_STATE = {
36
- PREPARED: "prepared",
37
- HOOK_RUNNING: "hook-running",
38
- AWAITING_NATIVE: "awaiting-native",
39
- AWAITING_REVIEW: "awaiting-review",
40
- VALIDATION_FAILED: "validation-failed",
41
- VALIDATED: "validated",
42
- COMMIT_RUNNING: "commit-running",
43
- COMMIT_FAILED: "commit-failed",
44
- COMMITTED: "committed",
45
- HOOK_FAILED: "hook-failed",
46
- INTERRUPTED: "interrupted",
47
- INCIDENT: "incident",
48
- ABANDONED: "abandoned",
49
- } ;
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
- function sha256(value ) {
155
- return `sha256:${createHash("sha256").update(value).digest("hex")}`;
156
- }
157
-
158
- function canonicalJson(value ) {
159
- if (value === null || typeof value !== "object") return JSON.stringify(value);
160
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
161
- const object = value ;
162
- return `{${Object.keys(object).filter((key) => object[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
163
- }
164
-
165
- function recordHash(body ) {
166
- return sha256(canonicalJson(body));
167
- }
168
-
169
- function git(cwd , args ) {
170
- return execFileSync("git", args, {
171
- cwd,
172
- encoding: "utf8",
173
- stdio: ["ignore", "pipe", "pipe"],
174
- timeout: GIT_TIMEOUT_MS,
175
- windowsHide: true,
176
- }).trim();
177
- }
178
-
179
- function absoluteGitPath(cwd , name ) {
180
- const value = git(cwd, ["rev-parse", "--path-format=absolute", "--git-path", name]);
181
- return isAbsolute(value) ? value : resolve(cwd, value);
182
- }
183
-
184
- function repositoryBinding(cwd ) {
185
- const root = realpathSync(git(cwd, ["rev-parse", "--show-toplevel"]));
186
- const commonDirValue = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
187
- const gitDirValue = git(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
188
- const commonDir = realpathSync(commonDirValue);
189
- const gitDir = realpathSync(gitDirValue);
190
- const roots = git(root, ["rev-list", "--max-parents=0", "HEAD"]).split(/\r?\n/).filter(Boolean).sort();
191
- const repositoryId = sha256(canonicalJson({ common_directory: commonDir, roots }));
192
- const worktreeKey = sha256(gitDir).slice("sha256:".length, "sha256:".length + 24);
193
- const stateDir = join(commonDir, "gentle-pi", "commit-transactions", worktreeKey);
194
- return {
195
- root,
196
- commonDir,
197
- gitDir,
198
- repositoryId,
199
- stateDir,
200
- activePath: join(stateDir, "active.json"),
201
- lockPath: join(stateDir, "lock.json"),
202
- historyDir: join(stateDir, "history"),
203
- };
204
- }
205
-
206
- function ensureStateDirectories(binding ) {
207
- mkdirSync(binding.historyDir, { recursive: true, mode: 0o700 });
208
- chmodSync(dirname(binding.stateDir), 0o700);
209
- chmodSync(binding.stateDir, 0o700);
210
- chmodSync(binding.historyDir, 0o700);
211
- }
212
-
213
- function atomicWrite(path , value ) {
214
- const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
215
- const descriptor = openSync(temporary, "wx", 0o600);
216
- try {
217
- writeFileSync(descriptor, value, "utf8");
218
- fsyncSync(descriptor);
219
- } finally {
220
- closeSync(descriptor);
221
- }
222
- renameSync(temporary, path);
223
- if (process.platform !== "win32") {
224
- const directory = openSync(dirname(path), "r");
225
- try { fsyncSync(directory); } finally { closeSync(directory); }
226
- }
227
- }
228
-
229
- function writeRecord(path , body ) {
230
- const record = { ...body, record_hash: recordHash(body) };
231
- atomicWrite(path, `${canonicalJson(record)}\n`);
232
- return record;
233
- }
234
-
235
- function isStringArray(value ) {
236
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
237
- }
238
-
239
- function decodeRecord(value ) {
240
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("commit transaction record is not an object");
241
- const record = value ;
242
- if (record.schema !== TRANSACTION_SCHEMA) throw new Error("commit transaction record schema is incompatible");
243
- for (const field of [
244
- "transaction_id", "repository_id", "repository_root", "common_directory", "git_directory",
245
- "command", "command_hash", "original_head", "original_head_tree", "original_index_tree",
246
- "original_index_hash", "authorized_pre_hook_tree", "state", "created_at", "updated_at", "record_hash",
247
- ]) if (typeof record[field] !== "string" || (record[field] ).length === 0) throw new Error(`commit transaction record ${field} is invalid`);
248
- if (!isStringArray(record.arguments) || !isStringArray(record.invocation_ids) || !isStringArray(record.lineage_history)) throw new Error("commit transaction record arrays are invalid");
249
- for (const field of ["post_hook_tree", "post_hook_index_hash", "authorized_tree", "authority_revision", "gate_context_hash", "committed_head", "committed_tree", "git_created_head", "git_created_tree", "error"] ) {
250
- if (record[field] !== undefined && typeof record[field] !== "string") throw new Error(`commit transaction record ${field} is invalid`);
251
- }
252
- if (!Number.isSafeInteger(record.hook_runs) || (record.hook_runs ) < 0) throw new Error("commit transaction hook count is invalid");
253
- if (!(Object.values(COMMIT_TRANSACTION_STATE) ).includes(record.state)) throw new Error("commit transaction state is invalid");
254
- const { record_hash: hash, ...body } = record;
255
- if (recordHash(body ) !== hash) throw new Error("commit transaction record integrity check failed");
256
- return record ;
257
- }
258
-
259
- function readRecord(path ) {
260
- if (!existsSync(path)) return undefined;
261
- return decodeRecord(JSON.parse(readFileSync(path, "utf8")));
262
- }
263
-
264
- function bodyOf(record ) {
265
- const { record_hash: _hash, ...body } = record;
266
- return body;
267
- }
268
-
269
- function transition(
270
- binding ,
271
- record ,
272
- state ,
273
- patch = {},
274
- now = () => new Date(),
275
- ) {
276
- return writeRecord(binding.activePath, {
277
- ...bodyOf(record),
278
- ...patch,
279
- state,
280
- updated_at: now().toISOString(),
281
- });
282
- }
283
-
284
- function archive(binding , record ) {
285
- const archived = writeRecord(join(binding.historyDir, `${record.transaction_id}.json`), bodyOf(record));
286
- if (existsSync(binding.activePath)) unlinkSync(binding.activePath);
287
- return archived;
288
- }
289
-
290
- function processIsAlive(pid ) {
291
- try { process.kill(pid, 0); return true; } catch (error) {
292
- return (error ).code !== "ESRCH";
293
- }
294
- }
295
-
296
- function acquireLock(binding , transactionId , now ) {
297
- ensureStateDirectories(binding);
298
- const body = {
299
- schema: "gentle-pi.git-commit-transaction-lock/v1",
300
- pid: process.pid,
301
- host: hostname(),
302
- transaction_id: transactionId,
303
- created_at: now().toISOString(),
304
- };
305
- for (let attempt = 0; attempt < 2; attempt += 1) {
306
- try {
307
- const descriptor = openSync(binding.lockPath, "wx", 0o600);
308
- try { writeFileSync(descriptor, `${canonicalJson(body)}\n`, "utf8"); fsyncSync(descriptor); }
309
- finally { closeSync(descriptor); }
310
- return () => { try { unlinkSync(binding.lockPath); } catch (error) { if ((error ).code !== "ENOENT") throw error; } };
311
- } catch (error) {
312
- if ((error ).code !== "EEXIST") throw error;
313
- let existing = {};
314
- try { existing = JSON.parse(readFileSync(binding.lockPath, "utf8")) ; } catch { /* fail closed below */ }
315
- if (existing.host === hostname() && typeof existing.pid === "number" && !processIsAlive(existing.pid)) {
316
- unlinkSync(binding.lockPath);
317
- continue;
318
- }
319
- throw new Error(`commit transaction lock is active${typeof existing.transaction_id === "string" ? ` for ${existing.transaction_id}` : ""}`);
320
- }
321
- }
322
- throw new Error("commit transaction stale lock could not be reconciled");
323
- }
324
-
325
- function indexFingerprint(cwd ) {
326
- const indexPath = absoluteGitPath(cwd, "index");
327
- if (!existsSync(indexPath)) return sha256("missing-index");
328
- const before = statSync(indexPath);
329
- const bytes = readFileSync(indexPath);
330
- const after = statSync(indexPath);
331
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
332
- throw new Error("Git index changed while its transaction fingerprint was captured");
333
- }
334
- return sha256(bytes);
335
- }
336
-
337
- function assertSafeCommitArguments(arguments_ ) {
338
- const booleanOptions = new Set([
339
- "--allow-empty", "--allow-empty-message", "--amend", "--edit", "--no-edit", "--no-gpg-sign",
340
- "--no-post-rewrite", "--no-signoff", "--no-status", "--no-verify", "--quiet", "--short",
341
- "--signoff", "--status", "--verbose",
342
- ]);
343
- const valueOptions = new Set([
344
- "--author", "--cleanup", "--date", "--file", "--fixup", "--message", "--reedit-message",
345
- "--reuse-message", "--squash", "-C", "-F", "-c", "-m",
346
- ]);
347
- const unsupportedTreeOptions = /^(?:--all|--include|--interactive|--only|--patch|--pathspec-from-file|--pathspec-file-nul|-a|-i|-o|-p)$/;
348
- for (let index = 0; index < arguments_.length; index += 1) {
349
- const argument = arguments_[index] ;
350
- if (argument === "--") {
351
- if (index !== arguments_.length - 1) throw new Error("commit pathspecs are unsupported by the transaction runner");
352
- continue;
353
- }
354
- if (argument === "--dry-run") throw new Error("commit --dry-run does not create a transaction");
355
- if (unsupportedTreeOptions.test(argument) || unsupportedTreeOptions.test(argument.split("=")[0] )) throw new Error(`unsupported commit tree semantics: ${argument}`);
356
- if (argument === "-S" || argument === "--gpg-sign") continue;
357
- if (/^-S.+/.test(argument) || argument.startsWith("--gpg-sign=")) continue;
358
- if (/^-[^-]+$/.test(argument) && argument.length > 2) {
359
- const flags = argument.slice(1);
360
- if (/[^emnsqv]/.test(flags)) throw new Error(`unsupported combined commit option: ${argument}`);
361
- if (flags.includes("m")) {
362
- index += 1;
363
- if (arguments_[index] === undefined) throw new Error("commit message option is missing its value");
364
- }
365
- continue;
366
- }
367
- if (booleanOptions.has(argument)) continue;
368
- if ([...valueOptions].some((option) => argument.startsWith(`${option}=`))) continue;
369
- if (valueOptions.has(argument)) {
370
- index += 1;
371
- if (arguments_[index] === undefined) throw new Error(`commit option ${argument} is missing its value`);
372
- continue;
373
- }
374
- if (!argument.startsWith("-")) throw new Error("commit pathspecs are unsupported by the transaction runner");
375
- throw new Error(`unsupported commit option: ${argument}`);
376
- }
377
- }
378
-
379
- function shellQuote(value ) {
380
- if (process.platform === "win32") return `\"${value.replaceAll("\"", "\\\"")}\"`;
381
- return `'${value.replaceAll("'", `'\\''`)}'`;
382
- }
383
-
384
- function invocationBody(invocation ) {
385
- return {
386
- schema: invocation.schema,
387
- transaction_id: invocation.transactionId,
388
- command: invocation.command,
389
- command_hash: invocation.commandHash,
390
- cwd: invocation.cwd,
391
- arguments: invocation.arguments,
392
- authorization: {
393
- lineage_id: invocation.authorization.lineageId,
394
- store_revision: invocation.authorization.storeRevision,
395
- fingerprint: invocation.authorization.fingerprint,
396
- intended_tree: invocation.authorization.intendedTree,
397
- },
398
- };
399
- }
400
-
401
- export function encodeCommitTransactionInvocation(invocation ) {
402
- return Buffer.from(canonicalJson(invocationBody(invocation)), "utf8").toString("base64url");
403
- }
404
-
405
- export function decodeCommitTransactionInvocation(encoded ) {
406
- let value ;
407
- try { value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); }
408
- catch { throw new Error("commit transaction invocation is malformed"); }
409
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("commit transaction invocation is invalid");
410
- const body = value ;
411
- const authorization = body.authorization;
412
- if (body.schema !== INVOCATION_SCHEMA || typeof body.transaction_id !== "string" || typeof body.command !== "string" || typeof body.command_hash !== "string" || typeof body.cwd !== "string" || !isStringArray(body.arguments) || typeof authorization !== "object" || authorization === null || Array.isArray(authorization)) throw new Error("commit transaction invocation is incompatible");
413
- const native = authorization ;
414
- for (const field of ["lineage_id", "store_revision", "fingerprint", "intended_tree"]) if (typeof native[field] !== "string" || (native[field] ).length === 0) throw new Error(`commit transaction authorization ${field} is invalid`);
415
- if (sha256(body.command) !== body.command_hash) throw new Error("commit transaction command hash is invalid");
416
- assertSafeCommitArguments(body.arguments);
417
- return {
418
- schema: INVOCATION_SCHEMA,
419
- transactionId: body.transaction_id,
420
- command: body.command,
421
- commandHash: body.command_hash,
422
- cwd: body.cwd,
423
- arguments: body.arguments,
424
- authorization: {
425
- lineageId: native.lineage_id ,
426
- storeRevision: native.store_revision ,
427
- fingerprint: native.fingerprint ,
428
- intendedTree: native.intended_tree ,
429
- },
430
- };
431
- }
432
-
433
- export function prepareCommitTransactionInvocation(input
434
-
435
-
436
-
437
-
438
- ) {
439
- assertSafeCommitArguments(input.arguments);
440
- const binding = repositoryBinding(input.cwd);
441
- const head = git(binding.root, ["rev-parse", "--verify", "HEAD"]);
442
- const currentTree = git(binding.root, ["write-tree"]);
443
- if (currentTree !== input.authorization.intendedTree) throw new Error("commit transaction pre-hook index no longer matches its controller authorization");
444
- let transactionId = randomUUID();
445
- const active = readRecord(binding.activePath);
446
- const commandHash = sha256(input.command);
447
- if (active !== undefined && active.command_hash === commandHash && active.original_head === head) transactionId = active.transaction_id;
448
- return {
449
- schema: INVOCATION_SCHEMA,
450
- transactionId,
451
- command: input.command,
452
- commandHash,
453
- cwd: binding.root,
454
- arguments: [...input.arguments],
455
- authorization: { ...input.authorization },
456
- };
457
- }
458
-
459
- export function commitTransactionRunnerPath() {
460
- return fileURLToPath(new URL("../scripts/run-git-commit-transaction.mjs", import.meta.url));
461
- }
462
-
463
- export function buildCommitTransactionShellCommand(invocation ) {
464
- return [
465
- shellQuote(process.execPath),
466
- shellQuote(commitTransactionRunnerPath()),
467
- "run",
468
- shellQuote(encodeCommitTransactionInvocation(invocation)),
469
- ].join(" ");
470
- }
471
-
472
- function runProcess(file , arguments_ , cwd , signal ) {
473
- return new Promise((resolvePromise, reject) => {
474
- const child = spawn(file, [...arguments_], { cwd, env: process.env, stdio: "inherit", windowsHide: true, signal });
475
- child.once("error", reject);
476
- child.once("close", (code, processSignal) => resolvePromise({ code: code ?? 1, signal: processSignal }));
477
- });
478
- }
479
-
480
- function assertionPayload(binding , record ) {
481
- return Buffer.from(canonicalJson({
482
- schema: INDEX_ASSERTION_SCHEMA,
483
- cwd: binding.root,
484
- transaction_id: record.transaction_id,
485
- authorized_tree: record.authorized_tree,
486
- }), "utf8").toString("base64url");
487
- }
488
-
489
- function capturePayload(binding , record ) {
490
- return Buffer.from(canonicalJson({
491
- schema: COMMIT_CAPTURE_SCHEMA,
492
- cwd: binding.root,
493
- transaction_id: record.transaction_id,
494
- authorized_tree: record.authorized_tree,
495
- }), "utf8").toString("base64url");
496
- }
497
-
498
- function writeHook(path , lines ) {
499
- writeFileSync(path, `#!/bin/sh\nset -eu\n${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o700 });
500
- chmodSync(path, 0o700);
501
- }
502
-
503
- function createHookProxy(binding , record , runnerPath ) {
504
- if (record.authorized_tree === undefined) throw new Error("commit transaction cannot create hooks before native authorization");
505
- const originalHooks = absoluteGitPath(binding.root, "hooks");
506
- const proxy = join(binding.stateDir, `hooks-${record.transaction_id}`);
507
- rmSync(proxy, { recursive: true, force: true });
508
- mkdirSync(proxy, { recursive: true, mode: 0o700 });
509
- const assertion = `${shellQuote(process.execPath)} ${shellQuote(runnerPath)} assert-index ${shellQuote(assertionPayload(binding, record))}`;
510
- const capture = `${shellQuote(process.execPath)} ${shellQuote(runnerPath)} capture-commit ${shellQuote(capturePayload(binding, record))}`;
511
- writeHook(join(proxy, "pre-commit"), [`exec ${assertion}`]);
512
- for (const name of ["prepare-commit-msg", "commit-msg"]) {
513
- const original = join(originalHooks, name);
514
- const lines = existsSync(original) && (statSync(original).mode & 0o111) !== 0
515
- ? [`${shellQuote(original)} \"$@\"`, `exec ${assertion}`]
516
- : [`exec ${assertion}`];
517
- writeHook(join(proxy, name), lines);
518
- }
519
- const postCommit = join(originalHooks, "post-commit");
520
- writeHook(join(proxy, "post-commit"), [capture, ...(existsSync(postCommit) && (statSync(postCommit).mode & 0o111) !== 0 ? [`exec ${shellQuote(postCommit)} \"$@\"`] : [])]);
521
- const postRewrite = join(originalHooks, "post-rewrite");
522
- if (existsSync(postRewrite) && (statSync(postRewrite).mode & 0o111) !== 0) writeHook(join(proxy, "post-rewrite"), [`exec ${shellQuote(postRewrite)} \"$@\"`]);
523
- return proxy;
524
- }
525
-
526
- function nativeResultDocument(result ) {
527
- return {
528
- allowed: result.allowed,
529
- result: result.result,
530
- action: result.action,
531
- reason: result.reason,
532
- context: result.gateContext.raw,
533
- };
534
- }
535
-
536
- function validateNativeTree(result , lineageId , tree ) {
537
- if (!result.allowed || result.result !== "allow") throw new Error(`native pre-commit validation denied the post-hook tree: ${result.result}; ${result.action}; ${result.reason}`);
538
- if (result.gateContext.lineageId !== lineageId) throw new Error("native pre-commit validation returned a different lineage");
539
- if (result.gateContext.raw.gate !== "pre-commit") throw new Error("native pre-commit validation returned a different gate");
540
- if (result.gateContext.raw.candidate_tree !== tree) throw new Error("native pre-commit validation did not authorize the exact post-hook tree");
541
- }
542
-
543
- function createRecord(binding , invocation , now ) {
544
- const head = git(binding.root, ["rev-parse", "--verify", "HEAD"]);
545
- const tree = git(binding.root, ["write-tree"]);
546
- if (tree !== invocation.authorization.intendedTree) throw new Error("commit transaction index changed after controller authorization");
547
- const timestamp = now().toISOString();
548
- return writeRecord(binding.activePath, {
549
- schema: TRANSACTION_SCHEMA,
550
- transaction_id: invocation.transactionId,
551
- repository_id: binding.repositoryId,
552
- repository_root: binding.root,
553
- common_directory: binding.commonDir,
554
- git_directory: binding.gitDir,
555
- command: invocation.command,
556
- command_hash: invocation.commandHash,
557
- arguments: [...invocation.arguments],
558
- original_head: head,
559
- original_head_tree: git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]),
560
- original_index_tree: tree,
561
- original_index_hash: indexFingerprint(binding.root),
562
- authorized_pre_hook_tree: invocation.authorization.intendedTree,
563
- state: COMMIT_TRANSACTION_STATE.PREPARED,
564
- created_at: timestamp,
565
- updated_at: timestamp,
566
- hook_runs: 0,
567
- invocation_ids: [invocation.transactionId],
568
- lineage_history: [invocation.authorization.lineageId],
569
- });
570
- }
571
-
572
- function assertInvocationMatches(binding , record , invocation ) {
573
- if (record.repository_id !== binding.repositoryId || record.repository_root !== binding.root || record.common_directory !== binding.commonDir || record.git_directory !== binding.gitDir) throw new Error("commit transaction repository identity changed");
574
- if (record.transaction_id !== invocation.transactionId || record.command_hash !== invocation.commandHash || record.command !== invocation.command || canonicalJson(record.arguments) !== canonicalJson(invocation.arguments)) throw new Error("commit transaction exact retry does not match the durable command intent");
575
- if (git(binding.root, ["rev-parse", "--verify", "HEAD"]) !== record.original_head) throw new Error("commit transaction HEAD changed before reconciliation");
576
- }
577
-
578
- function recoverCompletedCommit(binding , record , now ) {
579
- if (record.state !== COMMIT_TRANSACTION_STATE.COMMIT_RUNNING && record.state !== COMMIT_TRANSACTION_STATE.COMMITTED) return undefined;
580
- const head = git(binding.root, ["rev-parse", "--verify", "HEAD"]);
581
- if (head === record.original_head) return undefined;
582
- const tree = git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]);
583
- if (record.git_created_head === undefined || record.git_created_tree === undefined || head !== record.git_created_head || tree !== record.git_created_tree || tree !== record.authorized_tree) {
584
- transition(binding, record, COMMIT_TRANSACTION_STATE.INCIDENT, { committed_head: head, committed_tree: tree, error: "HEAD identity differs from the exact Git-created authorized commit" }, now);
585
- throw new Error("commit transaction incident: HEAD identity differs from the exact Git-created authorized commit; push, PR, and release remain blocked");
586
- }
587
- const committed = transition(binding, record, COMMIT_TRANSACTION_STATE.COMMITTED, { committed_head: head, committed_tree: tree }, now);
588
- archive(binding, committed);
589
- return { transactionId: record.transaction_id, status: "recovered", head, tree };
590
- }
591
-
592
- function appendInvocation(record , invocation ) {
593
- return {
594
- invocation_ids: [...new Set([...record.invocation_ids, invocation.transactionId])],
595
- lineage_history: [...new Set([...record.lineage_history, invocation.authorization.lineageId])],
596
- };
597
- }
598
-
599
- export async function runGitCommitTransaction(
600
- invocation ,
601
- dependencies = {},
602
- ) {
603
- assertSafeCommitArguments(invocation.arguments);
604
- if (sha256(invocation.command) !== invocation.commandHash) throw new Error("commit transaction command identity is invalid");
605
- const now = dependencies.now ?? (() => new Date());
606
- const binding = repositoryBinding(invocation.cwd);
607
- const releaseLock = acquireLock(binding, invocation.transactionId, now);
608
- let record ;
609
- try {
610
- record = readRecord(binding.activePath);
611
- if (record !== undefined) {
612
- assertInvocationMatches(binding, record, invocation);
613
- const recovered = recoverCompletedCommit(binding, record, now);
614
- if (recovered !== undefined) return recovered;
615
- if ([COMMIT_TRANSACTION_STATE.HOOK_FAILED, COMMIT_TRANSACTION_STATE.VALIDATION_FAILED, COMMIT_TRANSACTION_STATE.COMMIT_FAILED, COMMIT_TRANSACTION_STATE.INTERRUPTED, COMMIT_TRANSACTION_STATE.INCIDENT, COMMIT_TRANSACTION_STATE.HOOK_RUNNING, COMMIT_TRANSACTION_STATE.COMMIT_RUNNING].includes(record.state )) {
616
- throw new Error(`commit transaction ${record.transaction_id} requires explicit recovery from state ${record.state}; no Git state was rolled back`);
617
- }
618
- if (record.state !== COMMIT_TRANSACTION_STATE.AWAITING_REVIEW && record.state !== COMMIT_TRANSACTION_STATE.AWAITING_NATIVE && record.state !== COMMIT_TRANSACTION_STATE.VALIDATED && record.state !== COMMIT_TRANSACTION_STATE.PREPARED) throw new Error(`commit transaction cannot resume from ${record.state}`);
619
- record = transition(binding, record, record.state, appendInvocation(record, invocation), now);
620
- } else {
621
- record = createRecord(binding, invocation, now);
622
- }
623
-
624
- const noVerify = invocation.arguments.includes("--no-verify") || invocation.arguments.some((argument) => /^-[^-]*n/.test(argument));
625
- if (record.state === COMMIT_TRANSACTION_STATE.PREPARED) {
626
- if (!noVerify) {
627
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.HOOK_RUNNING, { hook_runs: record.hook_runs + 1 }, now);
628
- const hook = await runProcess("git", ["hook", "run", "--ignore-missing", "pre-commit"], binding.root, dependencies.signal);
629
- if (hook.code !== 0 || hook.signal !== null) {
630
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.HOOK_FAILED, { error: `pre-commit hook failed with ${hook.signal ?? `exit ${hook.code}`}` }, now);
631
- throw new Error(`pre-commit hook failed; transaction ${record.transaction_id} created no commit and requires explicit recovery`);
632
- }
633
- }
634
- const postHookTree = git(binding.root, ["write-tree"]);
635
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.AWAITING_NATIVE, {
636
- post_hook_tree: postHookTree,
637
- post_hook_index_hash: indexFingerprint(binding.root),
638
- }, now);
639
- }
640
-
641
- if (record.state === COMMIT_TRANSACTION_STATE.AWAITING_REVIEW) {
642
- const tree = git(binding.root, ["write-tree"]);
643
- if (tree !== record.post_hook_tree || indexFingerprint(binding.root) !== record.post_hook_index_hash) throw new Error("post-hook index changed while the commit transaction awaited review");
644
- }
645
-
646
- if (record.state === COMMIT_TRANSACTION_STATE.AWAITING_NATIVE || record.state === COMMIT_TRANSACTION_STATE.AWAITING_REVIEW || record.state === COMMIT_TRANSACTION_STATE.VALIDATED) {
647
- const currentTree = git(binding.root, ["write-tree"]);
648
- if (record.post_hook_tree !== currentTree) throw new Error("commit transaction post-hook tree changed before native validation");
649
- if (currentTree !== invocation.authorization.intendedTree) {
650
- const mutation = `pre-commit hook mutated the staged candidate: post-hook tree ${currentTree} is not the authorized tree ${invocation.authorization.intendedTree}; the content-bound receipt no longer covers this index; normalize sources and re-run review explicitly, or make the hook convergent, then retry the exact command; transaction ${record.transaction_id} created no commit`;
651
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.AWAITING_REVIEW, { error: mutation }, now);
652
- throw new Error(mutation);
653
- }
654
- const nativeReviewCli = dependencies.nativeReviewCli ?? createNativeReviewCli();
655
- let nativeResult ;
656
- try {
657
- nativeResult = await nativeReviewCli.validate({ cwd: binding.root, gate: "pre-commit", lineageId: invocation.authorization.lineageId, ...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }) });
658
- } catch (error) {
659
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.VALIDATION_FAILED, { error: error instanceof Error ? error.message : String(error) }, now);
660
- throw error;
661
- }
662
- if (!nativeResult.allowed || nativeResult.result !== "allow") {
663
- const state = nativeResult.result === "scope-changed" ? COMMIT_TRANSACTION_STATE.AWAITING_REVIEW : COMMIT_TRANSACTION_STATE.VALIDATION_FAILED;
664
- record = transition(binding, record, state, { native_result: nativeResultDocument(nativeResult), error: nativeResult.reason }, now);
665
- throw new Error(`native pre-commit validation denied the post-hook tree: ${nativeResult.result}; ${nativeResult.action}; ${nativeResult.reason}`);
666
- }
667
- try { validateNativeTree(nativeResult, invocation.authorization.lineageId, currentTree); }
668
- catch (error) {
669
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.INCIDENT, { native_result: nativeResultDocument(nativeResult), error: error instanceof Error ? error.message : String(error) }, now);
670
- throw error;
671
- }
672
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.VALIDATED, {
673
- authorized_tree: currentTree,
674
- authority_revision: nativeResult.gateContext.storeRevision,
675
- gate_context_hash: sha256(canonicalJson(nativeResult.gateContext.raw)),
676
- native_result: nativeResultDocument(nativeResult),
677
- error: undefined,
678
- }, now);
679
- }
680
-
681
- const runnerPath = dependencies.runnerPath ?? commitTransactionRunnerPath();
682
- const proxy = createHookProxy(binding, record, runnerPath);
683
- dependencies.signal?.throwIfAborted();
684
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.COMMIT_RUNNING, {}, now);
685
- const commit = await runProcess("git", ["-c", `core.hooksPath=${proxy}`, "commit", ...invocation.arguments], binding.root);
686
- if (dependencies.failpoint === "after-commit-before-proof") throw new Error("commit transaction test interruption after Git returned");
687
- record = readRecord(binding.activePath) ?? record;
688
- const head = git(binding.root, ["rev-parse", "--verify", "HEAD"]);
689
- const headTree = git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]);
690
- if (head !== record.original_head && head === record.git_created_head && headTree === record.git_created_tree && headTree === record.authorized_tree) {
691
- const committed = transition(binding, record, COMMIT_TRANSACTION_STATE.COMMITTED, { committed_head: head, committed_tree: headTree, ...(commit.code === 0 && commit.signal === null ? {} : { error: `Git returned ${commit.signal ?? `exit ${commit.code}`} after creating the authorized commit` }) }, now);
692
- archive(binding, committed);
693
- return { transactionId: record.transaction_id, status: "committed", head, tree: headTree };
694
- }
695
- if (head !== record.original_head) {
696
- transition(binding, record, COMMIT_TRANSACTION_STATE.INCIDENT, { committed_head: head, committed_tree: headTree, error: "HEAD identity differs from the exact Git-created authorized commit" }, now);
697
- throw new Error("commit transaction incident: HEAD identity changed after Git created the authorized commit; publication remains blocked");
698
- }
699
- record = transition(binding, record, COMMIT_TRANSACTION_STATE.COMMIT_FAILED, { error: `Git commit failed with ${commit.signal ?? `exit ${commit.code}`}` }, now);
700
- throw new Error(`Git commit failed; transaction ${record.transaction_id} created no commit and requires explicit recovery`);
701
- } catch (error) {
702
- if (record !== undefined && dependencies.signal?.aborted === true && existsSync(binding.activePath)) {
703
- try {
704
- const active = readRecord(binding.activePath) ?? record;
705
- if (git(binding.root, ["rev-parse", "--verify", "HEAD"]) === active.original_head) transition(binding, active, COMMIT_TRANSACTION_STATE.INTERRUPTED, { error: "commit transaction was cancelled" }, now);
706
- } catch { /* retain the earlier durable state */ }
707
- }
708
- throw error;
709
- } finally {
710
- releaseLock();
711
- }
712
- }
713
-
714
- export function assertCommitTransactionIndex(encoded ) {
715
- let value ;
716
- try { value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); }
717
- catch { throw new Error("commit transaction index assertion is malformed"); }
718
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("commit transaction index assertion is invalid");
719
- const body = value ;
720
- if (body.schema !== INDEX_ASSERTION_SCHEMA || typeof body.cwd !== "string" || typeof body.transaction_id !== "string" || typeof body.authorized_tree !== "string") throw new Error("commit transaction index assertion is incompatible");
721
- const binding = repositoryBinding(body.cwd);
722
- const record = readRecord(binding.activePath);
723
- if (record === undefined || record.transaction_id !== body.transaction_id || record.state !== COMMIT_TRANSACTION_STATE.COMMIT_RUNNING || record.authorized_tree !== body.authorized_tree) throw new Error("commit transaction index assertion has no matching active authorization");
724
- if (git(binding.root, ["write-tree"]) !== body.authorized_tree) throw new Error("Git hook changed the index after native pre-commit authorization");
725
- }
726
-
727
- export function captureCommitTransactionHead(encoded ) {
728
- let value ;
729
- try { value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); }
730
- catch { throw new Error("commit transaction capture is malformed"); }
731
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("commit transaction capture is invalid");
732
- const body = value ;
733
- if (body.schema !== COMMIT_CAPTURE_SCHEMA || typeof body.cwd !== "string" || typeof body.transaction_id !== "string" || typeof body.authorized_tree !== "string") throw new Error("commit transaction capture is incompatible");
734
- const binding = repositoryBinding(body.cwd);
735
- const record = readRecord(binding.activePath);
736
- if (record === undefined || record.transaction_id !== body.transaction_id || record.state !== COMMIT_TRANSACTION_STATE.COMMIT_RUNNING || record.authorized_tree !== body.authorized_tree) throw new Error("commit transaction capture has no matching active authorization");
737
- const head = git(binding.root, ["rev-parse", "--verify", "HEAD"]);
738
- const tree = git(binding.root, ["rev-parse", "--verify", "HEAD^{tree}"]);
739
- if (head === record.original_head || tree !== record.authorized_tree) throw new Error("commit transaction capture does not match a new authorized commit");
740
- if (record.git_created_head !== undefined && (record.git_created_head !== head || record.git_created_tree !== tree)) throw new Error("commit transaction capture conflicts with the recorded Git commit");
741
- transition(binding, record, COMMIT_TRANSACTION_STATE.COMMIT_RUNNING, { git_created_head: head, git_created_tree: tree });
742
- }
743
-
744
- export function inspectCommitTransaction(cwd ) {
745
- let binding ;
746
- try { binding = repositoryBinding(cwd); }
747
- catch (error) { return { status: "corrupted", reason: error instanceof Error ? error.message : String(error) }; }
748
- try {
749
- const record = readRecord(binding.activePath);
750
- return record === undefined ? { status: "clean" } : { status: "active", record };
751
- } catch (error) {
752
- return { status: "corrupted", reason: error instanceof Error ? error.message : String(error) };
753
- }
754
- }
755
-
756
- export function reconcileCommitTransaction(cwd ) {
757
- const binding = repositoryBinding(cwd);
758
- const active = readRecord(binding.activePath);
759
- if (active === undefined) return { status: "clean" };
760
- const now = () => new Date();
761
- const releaseLock = acquireLock(binding, active.transaction_id, now);
762
- try {
763
- const record = readRecord(binding.activePath);
764
- if (record === undefined) return { status: "clean" };
765
- const recovered = recoverCompletedCommit(binding, record, now);
766
- return recovered === undefined ? { status: "active", record } : { status: "clean" };
767
- } finally { releaseLock(); }
768
- }
769
-
770
- export function assertNoUnresolvedCommitTransaction(cwd ) {
771
- const inspection = inspectCommitTransaction(cwd);
772
- if (inspection.status === "clean") return;
773
- if (inspection.status === "corrupted") throw new Error(`commit transaction recovery state is corrupted: ${inspection.reason}`);
774
- throw new Error(`commit transaction ${inspection.record .transaction_id} is unresolved in state ${inspection.record .state}; publication is blocked until deterministic recovery completes`);
775
- }
776
-
777
- export function abandonCommitTransaction(cwd ) {
778
- const binding = repositoryBinding(cwd);
779
- const now = () => new Date();
780
- const active = readRecord(binding.activePath);
781
- if (active === undefined) throw new Error("no active commit transaction exists");
782
- const releaseLock = acquireLock(binding, active.transaction_id, now);
783
- try {
784
- const record = readRecord(binding.activePath);
785
- if (record === undefined) throw new Error("active commit transaction disappeared during recovery");
786
- if (git(binding.root, ["rev-parse", "--verify", "HEAD"]) !== record.original_head) throw new Error("cannot abandon a commit transaction after HEAD changed; reconcile the committed tree instead");
787
- const abandoned = transition(binding, record, COMMIT_TRANSACTION_STATE.ABANDONED, { error: "explicitly abandoned without changing HEAD or index" }, now);
788
- return archive(binding, abandoned);
789
- } finally { releaseLock(); }
790
- }
791
-
792
- export function verifyCommitTransactionResult(cwd , transactionId ) {
793
- const binding = repositoryBinding(cwd);
794
- const active = readRecord(binding.activePath);
795
- if (active?.transaction_id === transactionId) throw new Error(`commit transaction ${transactionId} remains unresolved in state ${active.state}`);
796
- const history = readRecord(join(binding.historyDir, `${transactionId}.json`));
797
- if (history === undefined || history.state !== COMMIT_TRANSACTION_STATE.COMMITTED || history.committed_head === undefined || history.committed_tree === undefined || history.git_created_head !== history.committed_head || history.git_created_tree !== history.committed_tree || history.authorized_tree !== history.committed_tree) throw new Error(`commit transaction ${transactionId} has no durable verified commit result`);
798
- const head = git(binding.root, ["rev-parse", "--verify", "HEAD"]);
799
- const tree = git(binding.root, ["rev-parse", "--verify", `${history.committed_head}^{tree}`]);
800
- if (head !== history.committed_head || tree !== history.committed_tree) throw new Error(`commit transaction ${transactionId} HEAD proof changed before tool_result reconciliation`);
801
- return { transactionId, status: "committed", head, tree };
802
- }