@astrosheep/keiyaku 2.9.12 → 2.9.13

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 (212) hide show
  1. package/README.md +22 -18
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms.js +1 -0
  4. package/build/agents/providers/claude-agent-sdk/adapter.js +2 -2
  5. package/build/cli/commands/akuma/akuma/handler.js +2 -1
  6. package/build/cli/commands/akuma/akuma/meta.js +4 -3
  7. package/build/cli/commands/akuma/catalog.js +2 -0
  8. package/build/cli/commands/akuma/list/handler.js +1 -1
  9. package/build/cli/commands/akuma/list/meta-list.js +12 -0
  10. package/build/cli/commands/akuma/list/meta-ls.js +2 -2
  11. package/build/cli/commands/akuma.js +5 -0
  12. package/build/cli/commands/contract/amend/meta.js +4 -3
  13. package/build/cli/commands/contract/arc/meta.js +2 -0
  14. package/build/cli/commands/contract/audit/handler.js +28 -2
  15. package/build/cli/commands/contract/audit/meta.js +4 -3
  16. package/build/cli/commands/contract/petition/handler.js +2 -1
  17. package/build/cli/commands/contract/petition/meta.js +2 -2
  18. package/build/cli/commands/contract/renew/handler.js +44 -3
  19. package/build/cli/commands/contract/renew/meta.js +5 -4
  20. package/build/cli/commands/metadata.js +12 -9
  21. package/build/cli/commands/projection/call/handler.js +1 -1
  22. package/build/cli/commands/projection/call/meta.js +1 -1
  23. package/build/cli/commands/projection/catalog.js +2 -2
  24. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  25. package/build/cli/commands/projection/fork/meta.js +12 -0
  26. package/build/cli/commands/projection/history/handler.js +46 -2
  27. package/build/cli/commands/projection/history/meta.js +3 -3
  28. package/build/cli/commands/projection/kill/meta.js +2 -2
  29. package/build/cli/commands/projection/status/handler.js +29 -13
  30. package/build/cli/commands/projection/status/meta.js +14 -3
  31. package/build/cli/commands/projection/tell/handler.js +8 -4
  32. package/build/cli/commands/projection/tell/meta.js +1 -1
  33. package/build/cli/commands/projection/wait/handler.js +45 -20
  34. package/build/cli/commands/projection/wait/meta.js +1 -1
  35. package/build/cli/commands/shared.js +1 -1
  36. package/build/cli/commands/system/catalog.js +2 -0
  37. package/build/cli/commands/task/add/handler.js +28 -12
  38. package/build/cli/commands/task/add/meta.js +18 -6
  39. package/build/cli/commands/task/catalog.js +2 -0
  40. package/build/cli/commands/task/compose/handler.js +2 -2
  41. package/build/cli/commands/task/compose/meta.js +2 -2
  42. package/build/cli/commands/task/doctor/handler.js +2 -3
  43. package/build/cli/commands/task/doctor/meta.js +2 -2
  44. package/build/cli/commands/task/done/meta.js +2 -2
  45. package/build/cli/commands/task/drop/handler.js +7 -2
  46. package/build/cli/commands/task/drop/meta.js +6 -3
  47. package/build/cli/commands/task/hold/meta.js +2 -2
  48. package/build/cli/commands/task/log/meta.js +2 -2
  49. package/build/cli/commands/task/ls/meta.js +2 -2
  50. package/build/cli/commands/task/note/handler.js +6 -0
  51. package/build/cli/commands/task/note/meta.js +11 -0
  52. package/build/cli/commands/task/resume/meta.js +2 -2
  53. package/build/cli/commands/task/shared.js +29 -15
  54. package/build/cli/commands/task/show/meta.js +2 -2
  55. package/build/cli/commands/task/start/meta.js +2 -2
  56. package/build/cli/commands/task/stop/meta.js +2 -2
  57. package/build/cli/commands/task/update/meta.js +3 -2
  58. package/build/cli/commands/verification/handler.js +43 -0
  59. package/build/cli/commands/verification/meta.js +13 -0
  60. package/build/cli/completion.js +16 -13
  61. package/build/cli/flags.js +43 -11
  62. package/build/cli/help.js +29 -30
  63. package/build/cli/index.js +6 -5
  64. package/build/cli/parse-flags.js +39 -0
  65. package/build/cli/parse-metadata.js +5 -2
  66. package/build/cli/parse-selectors.js +18 -0
  67. package/build/cli/parse.js +21 -8
  68. package/build/cli/render/arc.js +12 -3
  69. package/build/cli/render/audit.js +2 -2
  70. package/build/cli/render/call.js +4 -4
  71. package/build/cli/render/kanshi.js +122 -85
  72. package/build/cli/render/misc.js +2 -2
  73. package/build/cli/render/petition.js +16 -1
  74. package/build/cli/render/projection-history.js +17 -4
  75. package/build/cli/render/shared.js +5 -5
  76. package/build/cli/render/status-indicator.js +47 -0
  77. package/build/cli/render/status.js +106 -76
  78. package/build/cli/render/success-response.js +20 -5
  79. package/build/cli/render/task.js +6 -5
  80. package/build/cli/render/tell.js +10 -4
  81. package/build/cli/render/tool-command-normalization.js +137 -0
  82. package/build/cli/render/tool-presentation.js +2 -1
  83. package/build/cli/render/verification.js +23 -0
  84. package/build/cli/render/wait.js +101 -33
  85. package/build/cli/types.js +2 -1
  86. package/build/config/akuma-loader.js +21 -1
  87. package/build/config/settings/knobs.js +7 -1
  88. package/build/config/settings/schema.js +10 -5
  89. package/build/core/amend.js +102 -17
  90. package/build/core/arc.js +4 -4
  91. package/build/core/audit/candidate.js +10 -18
  92. package/build/core/audit/coordinates.js +12 -17
  93. package/build/core/audit/evidence.js +6 -4
  94. package/build/core/audit/facade.js +3 -3
  95. package/build/core/audit/report.js +1 -1
  96. package/build/core/bind-reconciliation.js +42 -23
  97. package/build/core/bind.js +182 -81
  98. package/build/core/call/call.js +6 -7
  99. package/build/core/call/context.js +43 -30
  100. package/build/core/call/execution.js +21 -6
  101. package/build/core/call/prompt.js +11 -42
  102. package/build/core/contract-carrier-runtime.js +209 -105
  103. package/build/core/contract-carrier.js +156 -73
  104. package/build/core/contract-view.js +42 -5
  105. package/build/core/contract.js +2 -1
  106. package/build/core/derived-replay.js +168 -35
  107. package/build/core/draft.js +9 -5
  108. package/build/core/forfeit.js +1 -1
  109. package/build/core/lifecycle-history.js +13 -0
  110. package/build/core/lifecycle-recovery.js +32 -27
  111. package/build/core/lifecycle-runner.js +14 -14
  112. package/build/core/log.js +1 -1
  113. package/build/core/outcome-base.js +2 -2
  114. package/build/core/projection/akuma-name.js +43 -0
  115. package/build/core/projection/generation/database.js +141 -79
  116. package/build/core/projection/generation/ledger.js +91 -0
  117. package/build/core/projection/generation/model.js +18 -13
  118. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  119. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  120. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  121. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  122. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  123. package/build/core/projection/generation/projection-generation-process.js +16 -15
  124. package/build/core/projection/generation/projection-generation-runner.js +125 -79
  125. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  126. package/build/core/projection/generation/protocol.js +68 -0
  127. package/build/core/projection/generation/store.js +17 -7
  128. package/build/core/projection/generation/transitions.js +14 -14
  129. package/build/core/projection/index.js +5 -5
  130. package/build/core/projection/projection-core.js +6 -2
  131. package/build/core/projection/projection-execution-observer.js +1 -1
  132. package/build/core/projection/projection-history.js +7 -7
  133. package/build/core/projection/projection-kill.js +15 -6
  134. package/build/core/projection/projection-life-observer.js +8 -8
  135. package/build/core/projection/projection-mint.js +6 -8
  136. package/build/core/projection/projection-status-observation.js +77 -8
  137. package/build/core/projection/projection-status.js +151 -4
  138. package/build/core/projection/projection-wait.js +46 -27
  139. package/build/core/projection/projection-wake.js +27 -89
  140. package/build/core/projection/tell/launch-store.js +1 -1
  141. package/build/core/projection/tell/store.js +1 -1
  142. package/build/core/renew-build.js +204 -74
  143. package/build/core/renew-plan.js +34 -9
  144. package/build/core/renew-rewrite.js +56 -15
  145. package/build/core/renew.js +39 -35
  146. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  147. package/build/core/repository-ledger/codec.js +11 -5
  148. package/build/core/repository-ledger/current-state-store.js +13 -16
  149. package/build/core/repository-ledger/fold-repository.js +5 -6
  150. package/build/core/repository-ledger/identity.js +60 -0
  151. package/build/core/repository-ledger/inventory.js +3 -3
  152. package/build/core/repository-ledger/read-model.js +54 -6
  153. package/build/core/repository-ledger/write-transaction.js +9 -4
  154. package/build/core/run-control/connection-bound-close.js +41 -0
  155. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  156. package/build/core/run-control/detached-process.js +14 -0
  157. package/build/core/run-control/index.js +6 -0
  158. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  159. package/build/core/run-control/runner-lease.js +172 -0
  160. package/build/core/{sqlite-process-lifetime-lock.js → run-control/sqlite-process-lifetime-lock.js} +4 -24
  161. package/build/core/seal.js +44 -32
  162. package/build/core/settlement/claim-delivery.js +22 -9
  163. package/build/core/settlement/claim.js +8 -6
  164. package/build/core/settlement/petition-claim-gates.js +9 -8
  165. package/build/core/settlement/petition-head-guard.js +5 -6
  166. package/build/core/settlement/petition-preview.js +21 -5
  167. package/build/core/settlement/petition.js +51 -17
  168. package/build/core/settlement/settlement.js +20 -15
  169. package/build/core/settlement/verdict.js +2 -2
  170. package/build/core/settlement/verification.js +492 -364
  171. package/build/core/status/board.js +184 -75
  172. package/build/core/status/drift.js +8 -10
  173. package/build/core/task/board.js +1 -0
  174. package/build/core/task/commands.js +25 -0
  175. package/build/core/task/compose.js +3 -2
  176. package/build/core/task/document.js +102 -39
  177. package/build/core/task/index.js +3 -3
  178. package/build/core/task/settlement-git.js +1 -4
  179. package/build/core/task/settlement-policy.js +27 -14
  180. package/build/core/task/source-board.js +1 -0
  181. package/build/core/task/task-bind-preparation.js +59 -9
  182. package/build/core/task/task-contract.js +2 -2
  183. package/build/core/task/task-store-repository.js +1 -0
  184. package/build/core/task/task.js +2 -2
  185. package/build/core/transcripts.js +4 -4
  186. package/build/core/verification-declaration.js +95 -0
  187. package/build/core/worktree-bootstrap.js +1 -1
  188. package/build/core/worktree-path.js +7 -11
  189. package/build/flow-error.js +9 -1
  190. package/build/generated/version.js +2 -2
  191. package/build/git/branches.js +41 -70
  192. package/build/git/commits.js +92 -21
  193. package/build/git/core.js +9 -161
  194. package/build/git/diff/preview.js +4 -4
  195. package/build/git/diff/read.js +4 -4
  196. package/build/git/diff/structured.js +5 -5
  197. package/build/git/process.js +229 -0
  198. package/build/git/refs.js +30 -5
  199. package/build/git/staging.js +39 -115
  200. package/build/git/streaming-batch.js +9 -16
  201. package/build/git/worktree.js +104 -91
  202. package/build/index.js +7 -0
  203. package/package.json +1 -1
  204. package/skills/keiyaku-akuma/SKILL.md +21 -20
  205. package/skills/keiyaku-task/SKILL.md +38 -4
  206. package/skills/keiyaku-workflow/SKILL.md +51 -11
  207. package/build/cli/commands/projection/revive/meta.js +0 -12
  208. package/build/core/projection/projection-life-protocol.js +0 -115
  209. package/build/core/projection/projection-runner-lock.js +0 -211
  210. package/build/core/projection/tell/database.js +0 -127
  211. package/build/core/settlement/verification-coordination.js +0 -305
  212. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -55,6 +55,12 @@ export function resolveFlowErrorHints(code, context, facts) {
55
55
  if (facts?.kind === "projection_wake_failure") {
56
56
  return [];
57
57
  }
58
+ if (facts?.kind === "task_contract_workspace") {
59
+ return ["Select an active contract with a materialized workspace, or retry from the desired checkout with -C."];
60
+ }
61
+ if (facts?.kind === "carrier_dispatch_refusal") {
62
+ return [facts.recoveryCommand];
63
+ }
58
64
  switch (code) {
59
65
  case "NOT_GIT_REPO":
60
66
  return ["The provided `cwd` is not a git repository."];
@@ -64,6 +70,8 @@ export function resolveFlowErrorHints(code, context, facts) {
64
70
  return [`Next: open an arc with \`${context.arcToolName}\`, then use \`${context.renewToolName}\` when delivery must follow the current base, or \`${context.closeToolName}\` to settle it.`];
65
71
  case "CONTRACT_AMBIGUOUS":
66
72
  return ["Address a specific contract workspace, or use `call --bare` to run without contract attachment."];
73
+ case "CONTRACT_WORKSPACE_UNAVAILABLE":
74
+ return [];
67
75
  case "TASK_AMBIGUOUS":
68
76
  return ["Pass one complete task coordinate from the listed candidates."];
69
77
  case "UNKNOWN_COMMAND":
@@ -108,7 +116,7 @@ export function resolveFlowErrorHints(code, context, facts) {
108
116
  case "DETACHED_HEAD":
109
117
  return ["Repository is in detached HEAD state. Address an active contract workspace before retrying."];
110
118
  case "INVALID_RESPONSE_PATH":
111
- return ["`keiyaku revive ARTIFACT_ID` needs a valid previous Keiyaku response artifact that includes complete provenance plus `session` and `subagent_name`."];
119
+ return ["`keiyaku fork ARTIFACT_ID` needs a valid previous Keiyaku response artifact that includes complete provenance plus `session` and `subagent_name`."];
112
120
  case "INVALID_CLOSE_INTENT":
113
121
  return ["Invalid close intent. Valid values are `CLAIM` and `FORFEIT`."];
114
122
  case "INVALID_SETTINGS":
@@ -1,3 +1,3 @@
1
1
  // Generated into build output by scripts/build.mjs
2
- export const VERSION = "2.9.12";
3
- export const GIT_HASH = "25d9570";
2
+ export const VERSION = "2.9.13";
3
+ export const GIT_HASH = "0971934";
@@ -1,29 +1,37 @@
1
- import { simpleGit } from "simple-git";
2
1
  import { FlowError } from "../flow-error.js";
3
- import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
4
- import { createGit, errorContainsAnyPattern, isGitExecutableUnavailable, NOT_GIT_REPOSITORY_PATTERNS, wrapGitError, } from "./core.js";
2
+ import { errorContainsAnyPattern, isGitExecutableUnavailable, NOT_GIT_REPOSITORY_PATTERNS, wrapGitError, } from "./core.js";
3
+ import { runGitOrThrow, runGitProcess } from "./process.js";
4
+ async function gitOutput(cwd, args, label) {
5
+ try {
6
+ return (await runGitOrThrow(cwd, args)).stdout;
7
+ }
8
+ catch (error) {
9
+ throw wrapGitError(label, error, cwd);
10
+ }
11
+ }
12
+ function processFailure(result) {
13
+ return result.error ?? Object.assign(new Error(`git exited with status ${result.status ?? "unknown"}`), {
14
+ stdout: result.stdout,
15
+ stderr: result.stderr,
16
+ status: result.status,
17
+ });
18
+ }
5
19
  export async function observeGitRepository(cwd) {
6
20
  try {
7
- const git = simpleGit(normalizeFilesystemCoordinate(cwd));
8
- git.env("GIT_TERMINAL_PROMPT", "0");
9
- git.env("GCM_INTERACTIVE", "Never");
10
- git.env("GIT_MERGE_AUTOEDIT", "no");
11
- git.env("LC_ALL", "C");
12
- git.env("PATH", process.env.PATH ?? "");
13
- const output = await git.raw(["rev-parse", "--is-inside-work-tree", "--is-bare-repository"]);
14
- const [insideWorktree, bare] = output.trim().split(/\r?\n/);
21
+ const output = await runGitOrThrow(cwd, ["rev-parse", "--is-inside-work-tree", "--is-bare-repository"]);
22
+ const [insideWorktree, bare] = output.stdout.trim().split(/\r?\n/);
15
23
  return {
16
24
  capability: "available",
17
25
  coordinate: insideWorktree === "true" && bare !== "true" ? "repository" : "non-repository",
18
26
  };
19
27
  }
20
- catch (err) {
21
- if (isGitExecutableUnavailable(err))
28
+ catch (error) {
29
+ if (isGitExecutableUnavailable(error))
22
30
  return { capability: "unavailable" };
23
- if (errorContainsAnyPattern(err, NOT_GIT_REPOSITORY_PATTERNS)) {
31
+ if (errorContainsAnyPattern(error, NOT_GIT_REPOSITORY_PATTERNS)) {
24
32
  return { capability: "available", coordinate: "non-repository" };
25
33
  }
26
- return { capability: "unknown", error: wrapGitError("check-is-repo", err, cwd) };
34
+ return { capability: "unknown", error: wrapGitError("check-is-repo", error, cwd) };
27
35
  }
28
36
  }
29
37
  export function assertGitObservation(observation, operation) {
@@ -40,74 +48,37 @@ export async function requireGitRepository(cwd, operation) {
40
48
  }
41
49
  }
42
50
  export async function getCurrentBranch(cwd) {
43
- const git = createGit(cwd);
44
- let status;
45
- try {
46
- status = await git.status();
47
- }
48
- catch (err) {
49
- throw wrapGitError("status", err, cwd);
51
+ const symbolic = await runGitProcess(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
52
+ if (symbolic.error || (symbolic.status !== 0 && symbolic.status !== 1)) {
53
+ throw wrapGitError("symbolic-ref --quiet --short HEAD", processFailure(symbolic), cwd);
50
54
  }
51
- if (!status.current) {
55
+ if (symbolic.status === 1 || !symbolic.stdout.trim()) {
52
56
  throw new FlowError("DETACHED_HEAD", "Detached HEAD or no branch found");
53
57
  }
54
- return status.current;
58
+ const head = await runGitProcess(cwd, ["rev-parse", "--verify", "--quiet", "HEAD"]);
59
+ if (head.error || (head.status !== 0 && head.status !== 1)) {
60
+ throw wrapGitError("rev-parse --verify HEAD", processFailure(head), cwd);
61
+ }
62
+ if (head.status === 0)
63
+ return symbolic.stdout.trim();
64
+ throw new FlowError("DETACHED_HEAD", "Detached HEAD or no branch found");
55
65
  }
56
66
  export async function listLocalBranches(cwd) {
57
- const git = createGit(cwd);
58
- let branches;
59
- try {
60
- branches = await git.branchLocal();
61
- }
62
- catch (err) {
63
- throw wrapGitError("branch --list", err, cwd);
64
- }
65
- return branches.all;
67
+ const output = await gitOutput(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads"], "branch --list");
68
+ return output.split(/\r?\n/).filter((branch) => branch.length > 0);
66
69
  }
67
70
  export async function createAndSwitchBranch(cwd, branchName) {
68
- const git = createGit(cwd);
69
- try {
70
- await git.raw(["switch", "-c", branchName]);
71
- }
72
- catch (err) {
73
- throw wrapGitError(`switch -c ${branchName}`, err, cwd);
74
- }
71
+ await gitOutput(cwd, ["switch", "-c", branchName], `switch -c ${branchName}`);
75
72
  }
76
73
  export async function switchBranch(cwd, branchName) {
77
- const git = createGit(cwd);
78
- try {
79
- await git.raw(["switch", branchName]);
80
- }
81
- catch (err) {
82
- throw wrapGitError(`switch ${branchName}`, err, cwd);
83
- }
74
+ await gitOutput(cwd, ["switch", branchName], `switch ${branchName}`);
84
75
  }
85
76
  export async function deleteBranch(cwd, branchName, force = false) {
86
- const git = createGit(cwd);
87
- try {
88
- await git.deleteLocalBranch(branchName, force);
89
- }
90
- catch (err) {
91
- throw wrapGitError(`branch -d${force ? " -D" : ""} ${branchName}`, err, cwd);
92
- }
77
+ await gitOutput(cwd, ["branch", force ? "-D" : "-d", branchName], `branch -d${force ? " -D" : ""} ${branchName}`);
93
78
  }
94
79
  export async function hasLocalBranch(cwd, branchName) {
95
- const git = createGit(cwd);
96
- let branches;
97
- try {
98
- branches = await git.branchLocal();
99
- }
100
- catch (err) {
101
- throw wrapGitError("branch --list", err, cwd);
102
- }
103
- return branches.all.includes(branchName);
80
+ return (await listLocalBranches(cwd)).includes(branchName);
104
81
  }
105
82
  export async function assertValidBranchName(cwd, branchName) {
106
- const git = createGit(cwd);
107
- try {
108
- await git.raw(["check-ref-format", "--branch", branchName]);
109
- }
110
- catch (err) {
111
- throw wrapGitError(`check-ref-format --branch ${branchName}`, err, cwd);
112
- }
83
+ await gitOutput(cwd, ["check-ref-format", "--branch", branchName], `check-ref-format --branch ${branchName}`);
113
84
  }
@@ -1,35 +1,107 @@
1
1
  import { FlowError } from "../flow-error.js";
2
- import { createGit, wrapGitError } from "./core.js";
2
+ import { isFullGitObjectId, runGitProcess, wrapGitError, } from "./core.js";
3
+ import { runGitOrThrow } from "./process.js";
4
+ const COMMIT_COORDINATE_FORMAT = "format:%H%x00%T%x00%P";
5
+ /**
6
+ * Resolve one revision to a peeled commit coordinate through a single argv-safe,
7
+ * non-walking Git query. Ordinary commits and annotated tags share the peeled tip.
8
+ * Missing revisions, non-commit terminal objects, malformed output, and process
9
+ * failures stay distinguishable without matching localized presentation text.
10
+ */
11
+ export async function readCommitCoordinate(cwd, revision) {
12
+ const result = await runGitProcess(cwd, [
13
+ "log",
14
+ "--no-walk",
15
+ "-1",
16
+ `--format=${COMMIT_COORDINATE_FORMAT}`,
17
+ "--end-of-options",
18
+ revision,
19
+ "--",
20
+ ]);
21
+ if (result.error) {
22
+ return { status: "failed", revision, error: result.error, result };
23
+ }
24
+ if (result.status !== 0) {
25
+ return { status: "missing", revision };
26
+ }
27
+ const raw = result.stdout.replace(/(?:\r?\n)+$/, "");
28
+ if (raw.length === 0) {
29
+ return { status: "non-commit", revision };
30
+ }
31
+ const fields = raw.split("\0");
32
+ if (fields.length !== 3) {
33
+ return {
34
+ status: "malformed",
35
+ revision,
36
+ detail: `expected 3 NUL fields, received ${fields.length}`,
37
+ };
38
+ }
39
+ const [commit, tree, parentsField] = fields;
40
+ if (!isFullGitObjectId(commit) || !isFullGitObjectId(tree)) {
41
+ return {
42
+ status: "malformed",
43
+ revision,
44
+ detail: "commit or tree field is not a full object id",
45
+ };
46
+ }
47
+ const parents = parentsField.trim().length === 0
48
+ ? []
49
+ : parentsField.trim().split(/\s+/).filter((part) => part.length > 0);
50
+ if (parents.some((parent) => !isFullGitObjectId(parent))) {
51
+ return {
52
+ status: "malformed",
53
+ revision,
54
+ detail: "parent field contains a non-object id",
55
+ };
56
+ }
57
+ return { status: "ok", coordinate: { commit, tree, parents } };
58
+ }
59
+ /** Map a failed coordinate read into a wrapped Git error for throw-style callers. */
60
+ export function commitCoordinateFailureError(read, cwd) {
61
+ if (read.status === "failed") {
62
+ const failure = read.error ?? Object.assign(new Error(read.result.stderr || `git log --no-walk exited ${read.result.status}`), {
63
+ stderr: read.result.stderr,
64
+ stdout: read.result.stdout,
65
+ });
66
+ return wrapGitError(`log --no-walk coordinate ${read.revision}`, failure, cwd);
67
+ }
68
+ if (read.status === "missing") {
69
+ return wrapGitError(`log --no-walk coordinate ${read.revision}`, Object.assign(new Error(`revision '${read.revision}' does not resolve`), { stderr: "" }), cwd);
70
+ }
71
+ if (read.status === "non-commit") {
72
+ return wrapGitError(`log --no-walk coordinate ${read.revision}`, Object.assign(new Error(`revision '${read.revision}' does not resolve to a commit`), { stderr: "" }), cwd);
73
+ }
74
+ return wrapGitError(`log --no-walk coordinate ${read.revision}`, Object.assign(new Error(`malformed commit coordinate for '${read.revision}': ${read.detail}`), { stderr: "" }), cwd);
75
+ }
76
+ /** Require a resolved coordinate or throw a wrapped Git error. */
77
+ export async function requireCommitCoordinate(cwd, revision) {
78
+ const read = await readCommitCoordinate(cwd, revision);
79
+ if (read.status === "ok")
80
+ return read.coordinate;
81
+ throw commitCoordinateFailureError(read, cwd);
82
+ }
3
83
  export function shortCommitHash(hash) {
4
84
  return hash.slice(0, 12);
5
85
  }
6
86
  export async function resolveCommitRef(cwd, ref) {
7
- const git = createGit(cwd);
8
- try {
9
- const output = await git.raw(["rev-parse", "--verify", `${ref}^{commit}`]);
10
- return output.trim();
11
- }
12
- catch (error) {
13
- throw new FlowError("INVALID_COMMIT", `commit '${ref}' does not resolve to a commit`, { cause: error });
87
+ const read = await readCommitCoordinate(cwd, ref);
88
+ if (read.status !== "ok") {
89
+ throw new FlowError("INVALID_COMMIT", `commit '${ref}' does not resolve to a commit`, {
90
+ cause: read.status === "failed" ? read.error : undefined,
91
+ });
14
92
  }
93
+ return read.coordinate.commit;
15
94
  }
16
95
  export async function isCommitReachableFromHead(cwd, commit) {
17
96
  return isCommitAncestor(cwd, commit, "HEAD");
18
97
  }
19
98
  export async function isCommitAncestor(cwd, ancestor, descendant) {
20
- const git = createGit(cwd);
21
- try {
22
- await git.raw(["merge-base", "--is-ancestor", ancestor, descendant]);
23
- return true;
24
- }
25
- catch {
26
- return false;
27
- }
99
+ const result = await runGitProcess(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]);
100
+ return result.status === 0 && result.error === undefined;
28
101
  }
29
102
  export async function getHeadCommitHash(cwd) {
30
- const git = createGit(cwd);
31
103
  try {
32
- const output = await git.raw(["rev-parse", "HEAD"]);
104
+ const output = (await runGitOrThrow(cwd, ["rev-parse", "HEAD"])).stdout;
33
105
  return output.trim();
34
106
  }
35
107
  catch (error) {
@@ -62,16 +134,15 @@ export async function resolveCommitRefs(cwd, refs) {
62
134
  export async function renderCommitStatPreview(cwd, commits) {
63
135
  if (commits.length === 0)
64
136
  return "No delivery commits recorded.";
65
- const git = createGit(cwd);
66
137
  try {
67
- const chunks = await Promise.all(commits.map(async (commit) => (await git.raw([
138
+ const chunks = await Promise.all(commits.map(async (commit) => (await runGitOrThrow(cwd, [
68
139
  "show",
69
140
  "--stat",
70
141
  "--summary",
71
142
  "--format=commit %H%n%s",
72
143
  "--no-renames",
73
144
  commit,
74
- ])).trim()));
145
+ ])).stdout.trim()));
75
146
  return chunks.filter((chunk) => chunk.length > 0).join("\n\n");
76
147
  }
77
148
  catch (error) {
package/build/git/core.js CHANGED
@@ -1,31 +1,10 @@
1
- import { spawn } from "node:child_process";
2
1
  import { simpleGit } from "simple-git";
2
+ import { existsSync } from "node:fs";
3
3
  import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
4
- import { getConfig } from "../config/env.js";
5
4
  import { appendDebugBlock } from "../telemetry/debug-log.js";
5
+ import { gitProcessEnv, resolveGitExecutable, resolveGitTimeoutMs } from "./process.js";
6
+ export { GIT_ABORT_FORCE_KILL_MS, gitProcessEnv, openGitProcess, resolveGitExecutable, resolveGitTimeoutMs, runGitOrThrow, runGitProcess, runGitProcessBytes, terminateGitChild, } from "./process.js";
6
7
  const GIT_ERROR_COMPACT_MAX_CHARS = 4000;
7
- export const GIT_ABORT_FORCE_KILL_MS = 250;
8
- /** Terminate one non-detached Git child and wait until Node drains its stdio close. */
9
- export async function terminateGitChild(child, closed, graceMs = GIT_ABORT_FORCE_KILL_MS) {
10
- if (child.exitCode === null && child.signalCode === null)
11
- child.kill("SIGTERM");
12
- let grace;
13
- const exited = await Promise.race([
14
- closed.then(() => true),
15
- new Promise((resolve) => {
16
- grace = setTimeout(() => resolve(false), graceMs);
17
- grace.unref();
18
- }),
19
- ]);
20
- if (grace)
21
- clearTimeout(grace);
22
- // Only the close event proves process exit and stdio drain. `killed` and
23
- // `signalCode` merely report signal delivery and can be set while a handler
24
- // keeps the child alive.
25
- if (!exited)
26
- child.kill("SIGKILL");
27
- await closed;
28
- }
29
8
  export const MISSING_GIT_CONFIG_PATTERNS = ["No such section or key"];
30
9
  export const NOT_GIT_REPOSITORY_PATTERNS = ["not a git repository"];
31
10
  export const MISSING_HEAD_PATTERNS = [
@@ -41,8 +20,9 @@ export function isFullGitObjectId(value) {
41
20
  /** Git is an optional adapter for directory-local product surfaces. */
42
21
  export function isGitExecutableUnavailable(error) {
43
22
  const source = (error ?? {});
44
- if (source.code === "ENOENT")
45
- return true;
23
+ if (source.code === "ENOENT") {
24
+ return typeof source.gitCwd !== "string" || existsSync(source.gitCwd);
25
+ }
46
26
  const message = typeof source.message === "string" ? source.message : String(error);
47
27
  return /spawn\s+git\s+ENOENT|git(?:\.exe)?["']?\s*:\s*(?:not found|no such file)/i.test(message);
48
28
  }
@@ -93,146 +73,14 @@ export function wrapGitError(commandLabel, err, cwd) {
93
73
  }
94
74
  return wrapped;
95
75
  }
96
- /** Configured Git timeout, with a bootstrap-safe default before env loads. */
97
- export function resolveGitTimeoutMs() {
98
- try {
99
- return getConfig().git.timeoutMs;
100
- }
101
- catch {
102
- // Git coordinates and early probes remain usable before config bootstrap.
103
- return 60_000;
104
- }
105
- }
106
- /** Non-interactive Git process environment shared by one-shot and streaming gates. */
107
- export function gitProcessEnv(extra) {
108
- return {
109
- ...process.env,
110
- ...extra,
111
- GIT_TERMINAL_PROMPT: "0",
112
- GCM_INTERACTIVE: "Never",
113
- GIT_MERGE_AUTOEDIT: "no",
114
- LC_ALL: "C",
115
- PATH: extra?.PATH ?? process.env.PATH ?? "",
116
- };
117
- }
118
76
  export function createGit(cwd) {
119
77
  const timeoutMs = resolveGitTimeoutMs();
120
78
  const git = simpleGit(normalizeFilesystemCoordinate(cwd), {
79
+ binary: resolveGitExecutable(),
121
80
  timeout: { block: timeoutMs, stdErr: true, stdOut: true },
81
+ unsafe: { allowUnsafeCustomBinary: true },
122
82
  });
123
83
  // simple-git replaces its child environment when configured through env().
124
- // Preserve trace controls explicitly without admitting unrelated ambient Git
125
- // behavior such as a pager into command execution.
126
- const traceEnvironment = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("GIT_TRACE2")));
127
- git.env({
128
- ...traceEnvironment,
129
- GIT_TERMINAL_PROMPT: "0",
130
- GCM_INTERACTIVE: "Never",
131
- GIT_MERGE_AUTOEDIT: "no",
132
- LC_ALL: "C",
133
- PATH: process.env.PATH ?? "",
134
- });
84
+ git.env(gitProcessEnv());
135
85
  return git;
136
86
  }
137
- /**
138
- * Asynchronous one-shot Git process boundary that preserves stdout/stderr/status facts.
139
- * Prefer `createGit` when non-zero exits are fatal; use this when callers need
140
- * soft status codes or stdin (simple-git drops empty-stderr non-zero exits).
141
- * Long-lived multi-request stdin/stdout sessions use `openGitStreamingBatch`.
142
- */
143
- export async function runGitProcessBytes(cwd, args, options) {
144
- options?.signal?.throwIfAborted();
145
- const timeoutMs = resolveGitTimeoutMs();
146
- const env = gitProcessEnv(options?.env);
147
- return await new Promise((resolve) => {
148
- const child = spawn("git", [...args], {
149
- cwd: normalizeFilesystemCoordinate(cwd),
150
- env,
151
- shell: false,
152
- stdio: ["pipe", "pipe", "pipe"],
153
- });
154
- const stdoutChunks = [];
155
- const stderrChunks = [];
156
- let settled = false;
157
- let timedOut = false;
158
- let aborting = false;
159
- let terminationPromise;
160
- const closed = new Promise((resolveClosed) => child.once("close", () => resolveClosed()));
161
- const finish = (result) => {
162
- if (settled)
163
- return;
164
- settled = true;
165
- clearTimeout(timeout);
166
- options?.signal?.removeEventListener("abort", abort);
167
- resolve(result);
168
- };
169
- const abort = () => {
170
- if (aborting)
171
- return;
172
- aborting = true;
173
- terminationPromise = terminateGitChild(child, closed);
174
- };
175
- options?.signal?.addEventListener("abort", abort, { once: true });
176
- if (options?.signal?.aborted)
177
- abort();
178
- const timeout = setTimeout(() => {
179
- timedOut = true;
180
- child.kill("SIGKILL");
181
- }, timeoutMs);
182
- if (options?.input !== undefined) {
183
- if (typeof options.input === "string")
184
- child.stdin.end(options.input, "utf8");
185
- else
186
- child.stdin.end(options.input);
187
- }
188
- else {
189
- child.stdin.end();
190
- }
191
- child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
192
- child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
193
- child.on("error", (error) => {
194
- finish({
195
- stdout: Buffer.concat(stdoutChunks),
196
- stderr: Buffer.concat(stderrChunks),
197
- status: null,
198
- error,
199
- });
200
- });
201
- child.on("close", async (code, signal) => {
202
- if (terminationPromise)
203
- await terminationPromise;
204
- const stdout = Buffer.concat(stdoutChunks);
205
- const stderr = Buffer.concat(stderrChunks);
206
- if (timedOut) {
207
- finish({
208
- stdout,
209
- stderr,
210
- status: null,
211
- error: new Error("git timed out"),
212
- });
213
- return;
214
- }
215
- if (signal) {
216
- finish({
217
- stdout,
218
- stderr,
219
- status: null,
220
- error: options?.signal?.aborted
221
- ? options.signal.reason instanceof Error ? options.signal.reason : new DOMException("The operation was aborted", "AbortError")
222
- : new Error(`git exited from signal ${signal}`),
223
- });
224
- return;
225
- }
226
- finish({ stdout, stderr, status: code });
227
- });
228
- });
229
- }
230
- export async function runGitProcess(cwd, args, options) {
231
- const result = await runGitProcessBytes(cwd, args, options);
232
- return {
233
- stdout: result.stdout.toString("utf8"),
234
- stderr: result.stderr.toString("utf8"),
235
- status: result.status,
236
- ...(result.error ? { error: result.error } : {}),
237
- };
238
- }
@@ -1,15 +1,15 @@
1
- import { createGit, wrapGitError } from "../core.js";
1
+ import { wrapGitError } from "../core.js";
2
+ import { runGitOrThrow } from "../process.js";
2
3
  import { diffPathspecArgs } from "./pathspec.js";
3
4
  export async function getDiffStats(cwd, baseBranch, options = {}) {
4
- const git = createGit(cwd);
5
5
  let rawNumStat;
6
6
  try {
7
- rawNumStat = await git.raw([
7
+ rawNumStat = (await runGitOrThrow(cwd, [
8
8
  "diff",
9
9
  "--numstat",
10
10
  `${baseBranch}...HEAD`,
11
11
  ...diffPathspecArgs({ excludePaths: options.excludePaths }),
12
- ]);
12
+ ])).stdout;
13
13
  }
14
14
  catch (err) {
15
15
  throw wrapGitError(`diff --numstat ${baseBranch}...HEAD`, err, cwd);
@@ -1,4 +1,5 @@
1
- import { createGit, wrapGitError } from "../core.js";
1
+ import { wrapGitError } from "../core.js";
2
+ import { runGitOrThrow } from "../process.js";
2
3
  import { diffPathspecArgs } from "./pathspec.js";
3
4
  import { isMissingBaseRevisionError, splitDiffByFile, totalJoinedLength, truncateDiffBlockToCharBudget, truncateDiffSectionByLineCount, truncateTextWithFooter, } from "./parsers.js";
4
5
  function normalizeDiffTarget(target) {
@@ -66,7 +67,6 @@ function renderPatchDiff(output, options) {
66
67
  return renderedSections.join("\n\n").trim() || "No diff.";
67
68
  }
68
69
  export async function readDiff(cwd, target, format, options = {}) {
69
- const git = createGit(cwd);
70
70
  const formattedTarget = formatDiffTarget(target);
71
71
  try {
72
72
  if (format === "stat") {
@@ -75,7 +75,7 @@ export async function readDiff(cwd, target, format, options = {}) {
75
75
  const args = statVariant === "numstat"
76
76
  ? ["diff", "--numstat", ...normalizeDiffTarget(target), ...pathspec]
77
77
  : ["diff", "--stat=120,80", "--compact-summary", ...normalizeDiffTarget(target), ...pathspec];
78
- const output = await git.raw(args);
78
+ const output = (await runGitOrThrow(cwd, args)).stdout;
79
79
  if (statVariant !== "summary") {
80
80
  return output.trim() || "No diff.";
81
81
  }
@@ -88,7 +88,7 @@ export async function readDiff(cwd, target, format, options = {}) {
88
88
  }
89
89
  return truncateTextWithFooter(trimmed, maxChars, trimmed.length - maxChars);
90
90
  }
91
- const output = await git.raw(buildPatchCommand(target, options));
91
+ const output = (await runGitOrThrow(cwd, buildPatchCommand(target, options))).stdout;
92
92
  return renderPatchDiff(output, options);
93
93
  }
94
94
  catch (error) {
@@ -1,4 +1,5 @@
1
- import { createGit, wrapGitError } from "../core.js";
1
+ import { wrapGitError } from "../core.js";
2
+ import { runGitOrThrow } from "../process.js";
2
3
  import { diffPathspecArgs } from "./pathspec.js";
3
4
  import { parseHunkRange, parseUnifiedHunks, splitDiffByFile } from "./parsers.js";
4
5
  function parseRawDiff(output) {
@@ -88,14 +89,13 @@ function parsePatchHunks(section) {
88
89
  });
89
90
  }
90
91
  export async function readStructuredDiff(cwd, from, to, options = {}) {
91
- const git = createGit(cwd);
92
92
  const range = `${from}..${to}`;
93
93
  const pathspec = diffPathspecArgs({ excludePaths: options.excludePaths });
94
94
  try {
95
95
  const [raw, numstat, patch] = await Promise.all([
96
- git.raw(["diff", "--raw", "-z", "--find-renames", range, ...pathspec]),
97
- git.raw(["diff", "--numstat", "-z", "--find-renames", range, ...pathspec]),
98
- git.raw(["diff", "--no-color", "--no-ext-diff", "--unified=3", "--find-renames", range, ...pathspec]),
96
+ runGitOrThrow(cwd, ["diff", "--raw", "-z", "--find-renames", range, ...pathspec]).then((result) => result.stdout),
97
+ runGitOrThrow(cwd, ["diff", "--numstat", "-z", "--find-renames", range, ...pathspec]).then((result) => result.stdout),
98
+ runGitOrThrow(cwd, ["diff", "--no-color", "--no-ext-diff", "--unified=3", "--find-renames", range, ...pathspec]).then((result) => result.stdout),
99
99
  ]);
100
100
  const rawFiles = parseRawDiff(raw);
101
101
  const stats = parseNumstat(numstat);