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.
- package/README.md +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- 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
|
-
}
|