@astrosheep/keiyaku 2.9.9 → 2.9.11

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 (230) hide show
  1. package/README.md +93 -174
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/harness/control-types.js +18 -1
  4. package/build/agents/harness/index.js +2 -0
  5. package/build/agents/harness/outcome-error.js +81 -0
  6. package/build/agents/harness/projection.js +2 -1
  7. package/build/agents/harness/router.js +5 -1
  8. package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
  9. package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
  10. package/build/agents/providers/claude-agent-sdk/session.js +19 -5
  11. package/build/agents/providers/codex-app-server/adapter.js +11 -6
  12. package/build/agents/providers/codex-app-server/events.js +67 -6
  13. package/build/agents/providers/codex-app-server/registration.js +11 -1
  14. package/build/agents/providers/opencode-sdk/adapter.js +8 -1
  15. package/build/agents/providers/opencode-sdk/events.js +24 -2
  16. package/build/agents/providers/opencode-sdk/registration.js +11 -1
  17. package/build/agents/providers/pi/checkpoint.js +9 -1
  18. package/build/agents/providers/pi/events.js +2 -0
  19. package/build/agents/providers/pi/registration.js +14 -1
  20. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  21. package/build/cli/commands/akuma/akuma/meta.js +3 -3
  22. package/build/cli/commands/akuma/catalog.js +2 -2
  23. package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
  24. package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
  25. package/build/cli/commands/akuma.js +1 -1
  26. package/build/cli/commands/contract/amend/handler.js +5 -4
  27. package/build/cli/commands/contract/arc/handler.js +5 -4
  28. package/build/cli/commands/contract/audit/handler.js +2 -2
  29. package/build/cli/commands/contract/bind/handler.js +12 -3
  30. package/build/cli/commands/contract/bind/meta.js +45 -15
  31. package/build/cli/commands/contract/forfeit/handler.js +9 -5
  32. package/build/cli/commands/contract/log/handler.js +2 -2
  33. package/build/cli/commands/contract/petition/handler.js +34 -7
  34. package/build/cli/commands/contract/renew/handler.js +7 -7
  35. package/build/cli/commands/contract/renew/meta.js +3 -3
  36. package/build/cli/commands/metadata.js +10 -6
  37. package/build/cli/commands/projection/call/meta.js +1 -1
  38. package/build/cli/commands/projection/catalog.js +2 -0
  39. package/build/cli/commands/projection/history/handler.js +21 -0
  40. package/build/cli/commands/projection/history/meta.js +9 -0
  41. package/build/cli/commands/projection/shared.js +3 -4
  42. package/build/cli/commands/projection/status/handler.js +3 -3
  43. package/build/cli/commands/projection/wait/handler.js +20 -11
  44. package/build/cli/commands/task/add/handler.js +14 -5
  45. package/build/cli/commands/task/add/meta.js +2 -2
  46. package/build/cli/commands/task/catalog.js +4 -0
  47. package/build/cli/commands/task/done/handler.js +1 -4
  48. package/build/cli/commands/task/done/meta.js +1 -1
  49. package/build/cli/commands/task/drop/meta.js +1 -1
  50. package/build/cli/commands/task/hold/handler.js +3 -0
  51. package/build/cli/commands/task/hold/meta.js +8 -0
  52. package/build/cli/commands/task/log/handler.js +1 -1
  53. package/build/cli/commands/task/log/meta.js +2 -2
  54. package/build/cli/commands/task/resume/handler.js +3 -0
  55. package/build/cli/commands/task/resume/meta.js +8 -0
  56. package/build/cli/commands/task/shared.js +39 -14
  57. package/build/cli/commands/task/show/handler.js +1 -1
  58. package/build/cli/commands/task/show/meta.js +1 -1
  59. package/build/cli/commands/task/start/meta.js +1 -1
  60. package/build/cli/commands/task/stop/handler.js +1 -4
  61. package/build/cli/commands/task/stop/meta.js +1 -1
  62. package/build/cli/commands/task/update/handler.js +11 -3
  63. package/build/cli/commands/task/update/meta.js +3 -3
  64. package/build/cli/completion.js +3 -2
  65. package/build/cli/flags.js +51 -14
  66. package/build/cli/help.js +4 -1
  67. package/build/cli/index.js +40 -25
  68. package/build/cli/parse-flags.js +40 -7
  69. package/build/cli/parse-metadata.js +5 -2
  70. package/build/cli/parse-selectors.js +2 -3
  71. package/build/cli/parse.js +40 -6
  72. package/build/cli/projection-address.js +12 -10
  73. package/build/cli/render/amendment-warning.js +8 -0
  74. package/build/cli/render/arc.js +6 -2
  75. package/build/cli/render/audit.js +400 -81
  76. package/build/cli/render/kanshi.js +6 -1
  77. package/build/cli/render/lifecycle.js +56 -0
  78. package/build/cli/render/misc.js +24 -28
  79. package/build/cli/render/petition.js +5 -0
  80. package/build/cli/render/projection-activity.js +3 -2
  81. package/build/cli/render/projection-history.js +64 -0
  82. package/build/cli/render/shared.js +23 -9
  83. package/build/cli/render/status.js +9 -16
  84. package/build/cli/render/task.js +51 -26
  85. package/build/cli/render/terminal-failure.js +3 -1
  86. package/build/cli/render/wait.js +3 -5
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +2 -0
  90. package/build/config/env.js +23 -0
  91. package/build/config/settings/schema.js +3 -1
  92. package/build/core/address-carrier.js +89 -0
  93. package/build/core/addressing.js +255 -196
  94. package/build/core/amend.js +115 -38
  95. package/build/core/amendment-criteria.js +17 -0
  96. package/build/core/amendment-warning.js +10 -0
  97. package/build/core/arc.js +184 -83
  98. package/build/core/audit/candidate.js +110 -9
  99. package/build/core/audit/coordinates.js +120 -65
  100. package/build/core/audit/evidence.js +19 -23
  101. package/build/core/audit/facade.js +34 -11
  102. package/build/core/audit/report.js +5 -5
  103. package/build/core/bind-reconciliation.js +390 -0
  104. package/build/core/bind-workspace.js +12 -20
  105. package/build/core/bind.js +242 -273
  106. package/build/core/call/call.js +1 -0
  107. package/build/core/call/context.js +46 -71
  108. package/build/core/call/prompt.js +6 -3
  109. package/build/core/context.js +49 -35
  110. package/build/core/contract-carrier-runtime.js +408 -0
  111. package/build/core/contract-carrier.js +270 -0
  112. package/build/core/contract-view.js +33 -27
  113. package/build/core/contract.js +5 -1
  114. package/build/core/derived-replay.js +207 -0
  115. package/build/core/draft.js +125 -57
  116. package/build/core/forfeit.js +113 -56
  117. package/build/core/identifier-slug.js +63 -13
  118. package/build/core/ids.js +4 -4
  119. package/build/core/lifecycle-history.js +35 -0
  120. package/build/core/lifecycle-recovery.js +117 -0
  121. package/build/core/lifecycle-runner.js +377 -0
  122. package/build/core/log.js +37 -11
  123. package/build/core/outcome-base.js +181 -0
  124. package/build/core/process-group.js +139 -0
  125. package/build/core/projection/generation/database.js +82 -7
  126. package/build/core/projection/generation/model.js +9 -36
  127. package/build/core/projection/generation/projection-generation-continuation.js +10 -22
  128. package/build/core/projection/generation/projection-generation-execution.js +6 -5
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +27 -8
  131. package/build/core/projection/projection-alias.js +23 -10
  132. package/build/core/projection/projection-core.js +21 -7
  133. package/build/core/projection/projection-execution-observer.js +91 -39
  134. package/build/core/projection/projection-history.js +474 -0
  135. package/build/core/projection/projection-runner-lock.js +12 -0
  136. package/build/core/projection/projection-status-observation.js +365 -0
  137. package/build/core/projection/projection-status.js +21 -358
  138. package/build/core/projection/projection-terminal-failure.js +13 -8
  139. package/build/core/projection/projection-wait.js +7 -2
  140. package/build/core/projection/tell/launch-store.js +244 -0
  141. package/build/core/projection/tell/model.js +3 -3
  142. package/build/core/projection/tell/store.js +70 -118
  143. package/build/core/registry.js +22 -87
  144. package/build/core/renew-build.js +424 -0
  145. package/build/core/renew-plan.js +239 -0
  146. package/build/core/renew-rewrite.js +77 -0
  147. package/build/core/renew.js +295 -520
  148. package/build/core/repository-ledger/accepted-fold-read.js +19 -0
  149. package/build/core/repository-ledger/accepted-tail.js +116 -0
  150. package/build/core/repository-ledger/admission.js +38 -0
  151. package/build/core/repository-ledger/authoritative-transaction.js +1 -0
  152. package/build/core/repository-ledger/claim-evidence.js +147 -0
  153. package/build/core/repository-ledger/codec.js +297 -0
  154. package/build/core/repository-ledger/current-state-store.js +853 -0
  155. package/build/core/repository-ledger/fold-repository.js +375 -0
  156. package/build/core/repository-ledger/fold.js +248 -0
  157. package/build/core/repository-ledger/git-object-store.js +243 -0
  158. package/build/core/repository-ledger/history-read.js +15 -0
  159. package/build/core/repository-ledger/incremental-read.js +153 -0
  160. package/build/core/repository-ledger/index.js +15 -0
  161. package/build/core/repository-ledger/inventory-plan.js +75 -0
  162. package/build/core/repository-ledger/inventory.js +310 -0
  163. package/build/core/repository-ledger/publication-admission.js +125 -0
  164. package/build/core/repository-ledger/publication.js +353 -0
  165. package/build/core/repository-ledger/read-model.js +469 -0
  166. package/build/core/repository-ledger/transaction-membership.js +78 -0
  167. package/build/core/repository-ledger/traversal.js +404 -0
  168. package/build/core/repository-ledger/write-transaction.js +339 -0
  169. package/build/core/seal.js +168 -161
  170. package/build/core/settlement/claim-delivery.js +42 -215
  171. package/build/core/settlement/claim.js +235 -153
  172. package/build/core/settlement/index.js +4 -4
  173. package/build/core/settlement/petition-claim-gates.js +52 -33
  174. package/build/core/settlement/petition-forfeit.js +12 -5
  175. package/build/core/settlement/petition-head-guard.js +8 -5
  176. package/build/core/settlement/petition-preview.js +68 -56
  177. package/build/core/settlement/petition-window.js +50 -0
  178. package/build/core/settlement/petition.js +252 -131
  179. package/build/core/settlement/queue-read-model.js +72 -95
  180. package/build/core/settlement/queue-seat-allocation.js +35 -126
  181. package/build/core/settlement/queue.js +1 -1
  182. package/build/core/settlement/settlement.js +172 -80
  183. package/build/core/settlement/verdict.js +112 -66
  184. package/build/core/settlement/verification-coordination.js +305 -0
  185. package/build/core/settlement/verification-supervisor.js +275 -0
  186. package/build/core/settlement/verification.js +398 -31
  187. package/build/core/status/board.js +114 -119
  188. package/build/core/status/lifecycle.js +7 -80
  189. package/build/core/structured-query.js +23 -0
  190. package/build/core/target-ref.js +1 -1
  191. package/build/core/task/board.js +59 -24
  192. package/build/core/task/commands.js +181 -33
  193. package/build/core/task/coordinate.js +56 -0
  194. package/build/core/task/document.js +25 -9
  195. package/build/core/task/index.js +7 -4
  196. package/build/core/task/query.js +17 -27
  197. package/build/core/task/settlement-git.js +164 -179
  198. package/build/core/task/settlement-policy.js +15 -11
  199. package/build/core/task/source-board.js +31 -13
  200. package/build/core/task/task-bind-preparation.js +193 -0
  201. package/build/core/task/task-contract.js +102 -193
  202. package/build/core/task/task-file-transaction.js +171 -0
  203. package/build/core/task/task-git-runtime.js +76 -14
  204. package/build/core/task/task-git-store.js +68 -169
  205. package/build/core/task/task-store-repository.js +13 -6
  206. package/build/core/task/task-worktree.js +6 -33
  207. package/build/core/task/task.js +1 -1
  208. package/build/core/task/tree.js +3 -3
  209. package/build/core/task/validation.js +4 -3
  210. package/build/core/transcripts.js +93 -1
  211. package/build/core/worktree-bootstrap.js +44 -36
  212. package/build/core/worktree-path.js +210 -77
  213. package/build/flow-error.js +7 -7
  214. package/build/generated/version.js +2 -2
  215. package/build/git/core.js +76 -13
  216. package/build/git/refs.js +168 -46
  217. package/build/git/streaming-batch.js +73 -13
  218. package/package.json +1 -1
  219. package/skills/keiyaku/SKILL.md +1 -1
  220. package/skills/keiyaku-akuma/SKILL.md +3 -3
  221. package/skills/keiyaku-task/SKILL.md +4 -2
  222. package/skills/keiyaku-workflow/SKILL.md +88 -203
  223. package/build/core/entry.js +0 -305
  224. package/build/core/ledger-batch.js +0 -194
  225. package/build/core/ledger.js +0 -84
  226. package/build/core/projection/leash.js +0 -87
  227. package/build/core/ref-log.js +0 -119
  228. package/build/core/renew-session.js +0 -128
  229. package/build/core/status/ledger-batch.js +0 -1
  230. package/build/core/status/reconciliation.js +0 -137
@@ -1,14 +1,61 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs/promises";
3
3
  import { FlowError } from "../flow-error.js";
4
- import { createGit, wrapGitError } from "../git/core.js";
5
- import { readLedger } from "./ledger.js";
6
- import { readLedgerSnapshot } from "./ledger-batch.js";
7
- import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
4
+ import { createGit, runGitProcess, wrapGitError } from "../git/core.js";
5
+ import { resolveLocalBranchHead } from "../git/refs.js";
6
+ import { readCurrentContractLifecycle } from "./repository-ledger/read-model.js";
8
7
  import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
9
8
  import { gitCommonRootPath, resolveGitCommonRoot } from "../git/path-coordinate.js";
10
- export function findBindEntry(entries) {
11
- return entries.find((entry) => entry.kind === "bind");
9
+ function record(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value)
11
+ ? value
12
+ : undefined;
13
+ }
14
+ /** Reconstruct the public terminal-cleanup fact from its independent replay receipts. */
15
+ export function terminalCleanupFacts(worktreeValue, deliveryRefValue) {
16
+ const worktree = record(worktreeValue);
17
+ const delivery = record(deliveryRefValue);
18
+ if (!worktree || !Array.isArray(worktree.absent) || !delivery)
19
+ return undefined;
20
+ const absent = worktree.absent.filter((item) => item === "worktree");
21
+ const base = {
22
+ absent,
23
+ ...(typeof worktree.removedWorktreePath === "string" ? { removedWorktreePath: worktree.removedWorktreePath } : {}),
24
+ };
25
+ const failure = record(worktree.worktreeRemoveFailure);
26
+ if (failure && typeof failure.path === "string" && typeof failure.reason === "string") {
27
+ base.worktreeRemoveFailure = { path: failure.path, reason: failure.reason };
28
+ }
29
+ if (delivery.status === "aligned"
30
+ && typeof delivery.ref === "string"
31
+ && delivery.expected === "absent"
32
+ && delivery.observed === "absent") {
33
+ return { ...base, deliveryRef: { status: "aligned", ref: delivery.ref, expected: "absent", observed: "absent" } };
34
+ }
35
+ const mismatch = record(delivery.mismatch);
36
+ const observed = record(mismatch?.observed);
37
+ if (delivery.status === "mismatch"
38
+ && mismatch?.kind === "projected-surface-mismatch"
39
+ && mismatch.surface === "delivery-ref"
40
+ && typeof mismatch.ref === "string"
41
+ && mismatch.expected === "absent"
42
+ && observed?.state === "present"
43
+ && typeof observed.head === "string") {
44
+ return {
45
+ ...base,
46
+ deliveryRef: {
47
+ status: "mismatch",
48
+ mismatch: {
49
+ kind: "projected-surface-mismatch",
50
+ surface: "delivery-ref",
51
+ ref: mismatch.ref,
52
+ expected: "absent",
53
+ observed: { state: "present", head: observed.head },
54
+ },
55
+ },
56
+ };
57
+ }
58
+ return undefined;
12
59
  }
13
60
  function parseWorktreeListPorcelainZ(output) {
14
61
  const listings = [];
@@ -28,7 +75,16 @@ function parseWorktreeListPorcelainZ(output) {
28
75
  listings.push(current);
29
76
  return listings;
30
77
  }
31
- async function listWorktrees(cwd) {
78
+ async function listWorktrees(cwd, signal) {
79
+ if (signal) {
80
+ const result = await runGitProcess(cwd, ["worktree", "list", "--porcelain", "-z"], { signal });
81
+ if (result.error || result.status !== 0) {
82
+ if (result.error?.name === "AbortError")
83
+ throw result.error;
84
+ throw wrapGitError("worktree list --porcelain -z", result.error ?? new Error(result.stderr || `exit ${String(result.status)}`), cwd);
85
+ }
86
+ return parseWorktreeListPorcelainZ(result.stdout);
87
+ }
32
88
  const git = createGit(cwd);
33
89
  try {
34
90
  return parseWorktreeListPorcelainZ(await git.raw(["worktree", "list", "--porcelain", "-z"]));
@@ -80,16 +136,19 @@ function contractWorktreePathFromStableRoot(stableRoot, contractId, bind) {
80
136
  return path.join(stableRoot, ".keiyaku", "wt", bind?.data.place ?? contractId);
81
137
  }
82
138
  export async function contractWorktreePath(cwd, contractId) {
83
- const ledger = await readLedger(cwd, contractId);
84
- const bind = ledger ? findBindEntry(ledger.entries) : undefined;
85
- return await contractWorktreePathFromBind(cwd, contractId, bind);
139
+ const stableRoot = await stableRepoRoot(cwd);
140
+ const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
141
+ return path.join(stableRoot, ".keiyaku", "wt", outcome.status === "healthy" || outcome.status === "diseased"
142
+ ? outcome.model.resources.worktree?.place ?? contractId
143
+ : contractId);
86
144
  }
87
145
  export async function requireContractWorktreePath(cwd, contractId) {
88
- const ledger = await readLedger(cwd, contractId);
89
- if (!ledger) {
146
+ const stableRoot = await stableRepoRoot(cwd);
147
+ const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
148
+ if (outcome.status !== "healthy" || !outcome.model.resources.worktree) {
90
149
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found`);
91
150
  }
92
- return await contractWorktreePathFromBind(cwd, contractId, findBindEntry(ledger.entries));
151
+ return path.join(stableRoot, ".keiyaku", "wt", outcome.model.resources.worktree.place);
93
152
  }
94
153
  async function findContractWorktree(cwd, contractId) {
95
154
  const expectedBranch = `refs/heads/keiyaku/${contractId}`;
@@ -108,21 +167,53 @@ export async function findContractWorktreePath(cwd, contractId) {
108
167
  }
109
168
  export async function findRegisteredContractWorktreePath(cwd, contractId) {
110
169
  const stableRoot = await stableRepoRoot(cwd);
111
- const ledger = await readLedger(stableRoot, contractId);
112
- const bind = ledger ? findBindEntry(ledger.entries) : undefined;
113
- if (!bind || bind.data.workspace !== "worktree")
170
+ const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
171
+ if (outcome.status !== "healthy" || outcome.model.resources.worktree?.state !== "held")
114
172
  return undefined;
115
- let expected;
116
- try {
117
- expected = await fs.realpath(contractWorktreePathFromStableRoot(stableRoot, contractId, bind));
118
- }
119
- catch {
173
+ return findRegisteredContractWorktreePathFromModel(stableRoot, outcome.model);
174
+ }
175
+ export async function findRegisteredContractWorktreePathFromModel(stableRoot, model) {
176
+ const worktree = model.resources.worktree;
177
+ if (!worktree)
120
178
  return undefined;
179
+ const expected = path.join(stableRoot, ".keiyaku", "wt", worktree.place);
180
+ for (const registered of await listWorktrees(stableRoot)) {
181
+ if (registered.branch !== model.resources.deliveryRef)
182
+ continue;
183
+ try {
184
+ if (normalizeFilesystemCoordinate(await fs.realpath(registered.path)) === normalizeFilesystemCoordinate(await fs.realpath(expected))) {
185
+ return await fs.realpath(registered.path);
186
+ }
187
+ }
188
+ catch { /* stale registration is not lifecycle evidence */ }
121
189
  }
190
+ return undefined;
191
+ }
192
+ async function findRegisteredContractWorktreePathFromBind(stableRoot, contractId, _bind) {
193
+ const expectedBranch = `refs/heads/keiyaku/${contractId}`;
122
194
  for (const worktree of await listWorktrees(stableRoot)) {
195
+ let ownsBranch = worktree.branch === expectedBranch;
196
+ if (!ownsBranch && !worktree.branch) {
197
+ for (const statePath of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
198
+ try {
199
+ const gitPath = (await createGit(worktree.path).raw(["rev-parse", "--git-path", statePath])).trim();
200
+ if ((await fs.readFile(gitPath, "utf8")).trim() === expectedBranch) {
201
+ ownsBranch = true;
202
+ break;
203
+ }
204
+ }
205
+ catch {
206
+ // No owned rebase coordinate at this registered worktree.
207
+ }
208
+ }
209
+ }
210
+ if (!ownsBranch)
211
+ continue;
123
212
  try {
124
- if (await fs.realpath(worktree.path) === expected)
125
- return worktree.path;
213
+ const dotGit = await fs.stat(path.join(worktree.path, ".git"));
214
+ if (dotGit.isDirectory())
215
+ continue;
216
+ return await fs.realpath(worktree.path);
126
217
  }
127
218
  catch {
128
219
  // A stale Git registration is not an engine-owned worktree fact.
@@ -130,12 +221,21 @@ export async function findRegisteredContractWorktreePath(cwd, contractId) {
130
221
  }
131
222
  return undefined;
132
223
  }
133
- /**
134
- * Delivery coordinate for dirty admission and KEIYAKU.md render: the linked
135
- * contract worktree when Git registers one, otherwise cwd (workspace here).
136
- */
137
- export async function resolveContractDeliveryCwd(cwd, contractId) {
138
- return (await findContractWorktreePath(cwd, contractId)) ?? cwd;
224
+ /** Prove the sole carrier target from stable-root, bind, and Git registration facts. */
225
+ export async function resolveContractCarrierTarget(stableRoot, contractId, bind) {
226
+ if (bind.data.workspace === "here") {
227
+ const root = await fs.realpath(stableRoot);
228
+ return { status: "available", root, path: path.join(root, ".keiyaku", "KEIYAKU.md"), workspace: "here" };
229
+ }
230
+ const root = await findRegisteredContractWorktreePathFromBind(stableRoot, contractId, bind);
231
+ if (!root) {
232
+ return {
233
+ status: "unavailable",
234
+ workspace: "worktree",
235
+ reason: `contract ${contractId} has no verified registered engine worktree`,
236
+ };
237
+ }
238
+ return { status: "available", root, path: path.join(root, ".keiyaku", "KEIYAKU.md"), workspace: "worktree" };
139
239
  }
140
240
  /**
141
241
  * Resolve the contract id for an engine-owned bound worktree containing cwd.
@@ -144,10 +244,29 @@ export async function resolveContractDeliveryCwd(cwd, contractId) {
144
244
  */
145
245
  async function contractIdForEngineOwnedWorktree(cwd, options) {
146
246
  const stableRoot = options.stableRoot ?? await stableRepoRoot(cwd);
247
+ const candidate = await contractIdentityFromWorktreeWitness(cwd, stableRoot);
248
+ if (!candidate)
249
+ return undefined;
250
+ const outcome = await readCurrentContractLifecycle(stableRoot, candidate.contractId);
251
+ if (outcome.status !== "healthy" || !outcome.model.resources.worktree)
252
+ return undefined;
253
+ if (options.requireActive && ["claimed", "forfeited"].includes(outcome.model.phase))
254
+ return undefined;
255
+ let expectedWorktree;
256
+ try {
257
+ expectedWorktree = await fs.realpath(path.join(stableRoot, ".keiyaku", "wt", outcome.model.resources.worktree.place));
258
+ }
259
+ catch {
260
+ return undefined;
261
+ }
262
+ return candidate.path === expectedWorktree ? candidate.contractId : undefined;
263
+ }
264
+ /** Resolve only Git's local registered-worktree witness; lifecycle facts must verify it separately. */
265
+ export async function contractIdentityFromWorktreeWitness(cwd, stableRoot, signal) {
147
266
  const canonicalCwd = await fs.realpath(cwd);
267
+ const branchPrefix = "refs/heads/keiyaku/";
148
268
  const candidates = [];
149
- for (const worktree of await listWorktrees(stableRoot)) {
150
- const branchPrefix = "refs/heads/keiyaku/";
269
+ for (const worktree of await listWorktrees(stableRoot, signal)) {
151
270
  if (!worktree.branch?.startsWith(branchPrefix))
152
271
  continue;
153
272
  let canonicalWorktree;
@@ -162,27 +281,10 @@ async function contractIdForEngineOwnedWorktree(cwd, options) {
162
281
  candidates.push({
163
282
  contractId: worktree.branch.slice(branchPrefix.length),
164
283
  path: canonicalWorktree,
284
+ branch: worktree.branch,
165
285
  });
166
286
  }
167
- const candidate = candidates.sort((left, right) => right.path.length - left.path.length)[0];
168
- if (!candidate)
169
- return undefined;
170
- const ledger = await readLedgerSnapshot(stableRoot, candidate.contractId);
171
- if (!ledger)
172
- return undefined;
173
- if (options.requireActive && isTerminalState(deriveContractState(ledger.entries)))
174
- return undefined;
175
- const bind = findBindEntry(ledger.entries);
176
- if (!bind || bind.data.workspace !== "worktree")
177
- return undefined;
178
- let expectedWorktree;
179
- try {
180
- expectedWorktree = await fs.realpath(contractWorktreePathFromStableRoot(stableRoot, candidate.contractId, bind));
181
- }
182
- catch {
183
- return undefined;
184
- }
185
- return candidate.path === expectedWorktree ? candidate.contractId : undefined;
287
+ return candidates.sort((left, right) => right.path.length - left.path.length || left.contractId.localeCompare(right.contractId))[0];
186
288
  }
187
289
  /**
188
290
  * Resolve the contract bound by an engine-owned worktree containing cwd,
@@ -196,12 +298,48 @@ export async function contractBoundByEngineWorktree(cwd, stableRoot) {
196
298
  export async function activeContractBoundByEngineWorktree(cwd, stableRoot) {
197
299
  return contractIdForEngineOwnedWorktree(cwd, { requireActive: true, stableRoot });
198
300
  }
199
- export async function cleanupContractWorkspace(cwd, contractId) {
301
+ /**
302
+ * Verify a worktree binding against a folded current-ledger model. Git's
303
+ * registration is only a physical witness here; it cannot create lifecycle
304
+ * state or select a contract absent from the supplied read model.
305
+ */
306
+ export async function currentContractBoundByWorktreeWitness(input) {
307
+ let canonicalCwd;
308
+ try {
309
+ canonicalCwd = await fs.realpath(input.cwd);
310
+ }
311
+ catch {
312
+ return undefined;
313
+ }
314
+ const registeredWorktrees = await listWorktrees(input.stableRoot);
315
+ const candidates = [];
316
+ for (const model of input.contracts) {
317
+ const worktree = model.resources.worktree;
318
+ if (!worktree || worktree.state !== "held")
319
+ continue;
320
+ const expectedPath = path.join(input.stableRoot, ".keiyaku", "wt", worktree.place);
321
+ let canonicalExpectedPath;
322
+ try {
323
+ canonicalExpectedPath = await fs.realpath(expectedPath);
324
+ }
325
+ catch {
326
+ continue;
327
+ }
328
+ if (!worktreeContainsCoordinate(canonicalExpectedPath, canonicalCwd))
329
+ continue;
330
+ const registered = registeredWorktrees.some((candidate) => candidate.branch === model.resources.deliveryRef &&
331
+ normalizeFilesystemCoordinate(candidate.path) === normalizeFilesystemCoordinate(canonicalExpectedPath));
332
+ if (registered)
333
+ candidates.push({ contract: model.contract, path: canonicalExpectedPath });
334
+ }
335
+ candidates.sort((left, right) => right.path.length - left.path.length || left.contract.localeCompare(right.contract));
336
+ return candidates[0]?.contract;
337
+ }
338
+ /** Remove only the derived linked worktree. Authoritative refs are outside this adapter. */
339
+ export async function cleanupContractWorkspace(cwd, lifecycle) {
200
340
  const stableRoot = await stableRepoRoot(cwd);
201
341
  const git = createGit(stableRoot);
202
- const branch = `keiyaku/${contractId}`;
203
- const worktree = await findContractWorktree(stableRoot, contractId);
204
- const worktreePath = worktree.path;
342
+ const worktreePath = await findRegisteredContractWorktreePathFromModel(stableRoot, lifecycle);
205
343
  const facts = { absent: [] };
206
344
  if (worktreePath) {
207
345
  try {
@@ -215,28 +353,23 @@ export async function cleanupContractWorkspace(cwd, contractId) {
215
353
  else {
216
354
  facts.absent.push("worktree");
217
355
  }
218
- if (worktree.primary) {
219
- facts.absent.push("branch");
220
- return facts;
221
- }
222
- let branchRef;
223
- try {
224
- branchRef = (await git.raw(["for-each-ref", "--format=%(refname)", `refs/heads/${branch}`])).trim();
225
- }
226
- catch (error) {
227
- throw wrapGitError(`for-each-ref refs/heads/${branch}`, error, stableRoot);
228
- }
229
- if (branchRef !== `refs/heads/${branch}`) {
230
- facts.absent.push("branch");
231
- }
232
- else {
233
- try {
234
- await git.raw(["branch", "-D", branch]);
235
- facts.deletedBranch = branch;
236
- }
237
- catch (error) {
238
- facts.branchDeleteFailure = { name: branch, reason: wrapGitError(`branch -D ${branch}`, error, stableRoot).message };
239
- }
240
- }
241
356
  return facts;
242
357
  }
358
+ /** Observe the terminal transaction's expected delivery-ref absence without reconciling it. */
359
+ export async function observeTerminalDeliveryRef(cwd, contractId) {
360
+ const stableRoot = await stableRepoRoot(cwd);
361
+ const ref = `refs/heads/keiyaku/${contractId}`;
362
+ const observed = await resolveLocalBranchHead(stableRoot, ref);
363
+ if (!observed)
364
+ return { status: "aligned", ref, expected: "absent", observed: "absent" };
365
+ return {
366
+ status: "mismatch",
367
+ mismatch: {
368
+ kind: "projected-surface-mismatch",
369
+ surface: "delivery-ref",
370
+ ref,
371
+ expected: "absent",
372
+ observed: { state: "present", head: observed.head },
373
+ },
374
+ };
375
+ }
@@ -64,6 +64,8 @@ export function resolveFlowErrorHints(code, context, facts) {
64
64
  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
65
  case "CONTRACT_AMBIGUOUS":
66
66
  return ["Address a specific contract workspace, or use `call --bare` to run without contract attachment."];
67
+ case "TASK_AMBIGUOUS":
68
+ return ["Pass one complete task coordinate from the listed candidates."];
67
69
  case "UNKNOWN_COMMAND":
68
70
  return ["Run `keiyaku --help` to inspect available commands."];
69
71
  case "EMPTY_PARAM":
@@ -77,13 +79,11 @@ export function resolveFlowErrorHints(code, context, facts) {
77
79
  case "MISSING_PROTOCOL_FILES":
78
80
  return ["Required protocol file is missing (`KEIYAKU.md`)."];
79
81
  case "CLOSE_MERGE_CONFLICT":
80
- return [`Open a Keiyaku recovery session with \`keiyaku ${context.renewToolName} --resolve\`.`];
82
+ return [];
81
83
  case "RENEW_COORDINATE_DRIFT":
82
84
  return [`Restore the delivery to its ledger-recorded history with Git reflog, then retry \`keiyaku ${context.renewToolName}\`.`];
83
- case "RENEW_SESSION_OPEN":
84
- return [`Continue the owned rewrite with \`keiyaku ${context.renewToolName} --continue\`, or restore it with \`keiyaku ${context.renewToolName} --abort\`.`];
85
- case "RENEW_SESSION_NOT_FOUND":
86
- return [`No recovery session is open; run \`keiyaku ${context.renewToolName}\` normally.`];
85
+ case "RENEW_SCRATCH_NOT_FOUND":
86
+ return [`No renew conflict scratch is open; run \`keiyaku ${context.renewToolName}\` normally.`];
87
87
  case "CLOSE_QUALITY_GATE_FAILED":
88
88
  return [`CLAIM denied: a configured verification command failed. Use \`${context.arcToolName}\` after addressing the failure before petitioning again.`];
89
89
  case "INVALID_BRANCH_TITLE":
@@ -95,9 +95,9 @@ export function resolveFlowErrorHints(code, context, facts) {
95
95
  case "FROM_DRAFT_NOT_FOUND":
96
96
  return ["The path in `from_draft` does not exist. `from_draft` must reference an existing file path (absolute or relative to `cwd`)."];
97
97
  case "INVALID_KEIYAKU_DRAFT":
98
- return ["Invalid draft/input. Use a `# <title>` heading with non-empty `## Objective` and `## Scope` sections (or the equivalent shape in `from_draft`)."];
98
+ return ["Invalid draft/input. Use a `# <title>` heading with non-empty `## Context`, `## Objective`, `## Design`, `## Scope`, `## Criteria`, and `## Verification` sections (or the equivalent shape in `from_draft`)."];
99
99
  case "BIND_TASK_INCOMPLETE":
100
- return ["Task supplies name/objective defaults; pipe only the missing sections as partial contract Markdown on stdin."];
100
+ return ["Task supplies title/objective defaults; pipe only the missing sections as partial contract Markdown on stdin."];
101
101
  case "KEIYAKU_FILE_EXISTS":
102
102
  return [`\`${context.bindToolName}\` requires a clean slate. \`KEIYAKU.md\` must be removed before starting.`];
103
103
  case "DETACHED_HEAD":
@@ -1,3 +1,3 @@
1
1
  // Generated into build output by scripts/build.mjs
2
- export const VERSION = "2.9.9";
3
- export const GIT_HASH = "79df272";
2
+ export const VERSION = "2.9.11";
3
+ export const GIT_HASH = "4384d75";
package/build/git/core.js CHANGED
@@ -4,6 +4,28 @@ import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
4
4
  import { getConfig } from "../config/env.js";
5
5
  import { appendDebugBlock } from "../telemetry/debug-log.js";
6
6
  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
+ }
7
29
  export const MISSING_GIT_CONFIG_PATTERNS = ["No such section or key"];
8
30
  export const NOT_GIT_REPOSITORY_PATTERNS = ["not a git repository"];
9
31
  export const MISSING_HEAD_PATTERNS = [
@@ -12,6 +34,10 @@ export const MISSING_HEAD_PATTERNS = [
12
34
  "unknown revision or path not in the working tree",
13
35
  "bad revision 'HEAD'",
14
36
  ];
37
+ /** Accept only a complete object id from either supported Git object format. */
38
+ export function isFullGitObjectId(value) {
39
+ return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value);
40
+ }
15
41
  /** Git is an optional adapter for directory-local product surfaces. */
16
42
  export function isGitExecutableUnavailable(error) {
17
43
  const source = (error ?? {});
@@ -94,11 +120,18 @@ export function createGit(cwd) {
94
120
  const git = simpleGit(normalizeFilesystemCoordinate(cwd), {
95
121
  timeout: { block: timeoutMs, stdErr: true, stdOut: true },
96
122
  });
97
- git.env("GIT_TERMINAL_PROMPT", "0");
98
- git.env("GCM_INTERACTIVE", "Never");
99
- git.env("GIT_MERGE_AUTOEDIT", "no");
100
- git.env("LC_ALL", "C");
101
- git.env("PATH", process.env.PATH ?? "");
123
+ // 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
+ });
102
135
  return git;
103
136
  }
104
137
  /**
@@ -107,7 +140,8 @@ export function createGit(cwd) {
107
140
  * soft status codes or stdin (simple-git drops empty-stderr non-zero exits).
108
141
  * Long-lived multi-request stdin/stdout sessions use `openGitStreamingBatch`.
109
142
  */
110
- export async function runGitProcess(cwd, args, options) {
143
+ export async function runGitProcessBytes(cwd, args, options) {
144
+ options?.signal?.throwIfAborted();
111
145
  const timeoutMs = resolveGitTimeoutMs();
112
146
  const env = gitProcessEnv(options?.env);
113
147
  return await new Promise((resolve) => {
@@ -121,19 +155,35 @@ export async function runGitProcess(cwd, args, options) {
121
155
  const stderrChunks = [];
122
156
  let settled = false;
123
157
  let timedOut = false;
158
+ let aborting = false;
159
+ let terminationPromise;
160
+ const closed = new Promise((resolveClosed) => child.once("close", () => resolveClosed()));
124
161
  const finish = (result) => {
125
162
  if (settled)
126
163
  return;
127
164
  settled = true;
128
165
  clearTimeout(timeout);
166
+ options?.signal?.removeEventListener("abort", abort);
129
167
  resolve(result);
130
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();
131
178
  const timeout = setTimeout(() => {
132
179
  timedOut = true;
133
180
  child.kill("SIGKILL");
134
181
  }, timeoutMs);
135
182
  if (options?.input !== undefined) {
136
- child.stdin.end(options.input, "utf8");
183
+ if (typeof options.input === "string")
184
+ child.stdin.end(options.input, "utf8");
185
+ else
186
+ child.stdin.end(options.input);
137
187
  }
138
188
  else {
139
189
  child.stdin.end();
@@ -142,15 +192,17 @@ export async function runGitProcess(cwd, args, options) {
142
192
  child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
143
193
  child.on("error", (error) => {
144
194
  finish({
145
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
146
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
195
+ stdout: Buffer.concat(stdoutChunks),
196
+ stderr: Buffer.concat(stderrChunks),
147
197
  status: null,
148
198
  error,
149
199
  });
150
200
  });
151
- child.on("close", (code, signal) => {
152
- const stdout = Buffer.concat(stdoutChunks).toString("utf8");
153
- const stderr = Buffer.concat(stderrChunks).toString("utf8");
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);
154
206
  if (timedOut) {
155
207
  finish({
156
208
  stdout,
@@ -165,7 +217,9 @@ export async function runGitProcess(cwd, args, options) {
165
217
  stdout,
166
218
  stderr,
167
219
  status: null,
168
- error: new Error(`git exited from signal ${signal}`),
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}`),
169
223
  });
170
224
  return;
171
225
  }
@@ -173,3 +227,12 @@ export async function runGitProcess(cwd, args, options) {
173
227
  });
174
228
  });
175
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
+ }