@astrosheep/keiyaku 2.9.9 → 2.9.10

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 +50 -182
  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
package/build/git/refs.js CHANGED
@@ -1,11 +1,22 @@
1
- import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, runGitProcess, wrapGitError, } from "./core.js";
2
- const ZERO_OBJECT_ID = "0000000000000000000000000000000000000000";
1
+ import { isFullGitObjectId, runGitProcess, wrapGitError, } from "./core.js";
3
2
  /** Normalize a configured branch to the sole local-branch ref spelling. */
4
3
  export function normalizeLocalBranchRef(branch) {
5
4
  return branch.startsWith("refs/heads/") ? branch : `refs/heads/${branch}`;
6
5
  }
6
+ export const REF_TRANSACTION_CAS_MISMATCH = "REF_TRANSACTION_CAS_MISMATCH";
7
+ export const REF_TRANSACTION_NOT_COMMITTED = "REF_TRANSACTION_NOT_COMMITTED";
8
+ export const REF_TRANSACTION_INDETERMINATE = "REF_TRANSACTION_INDETERMINATE";
9
+ export class RefTransactionError extends Error {
10
+ code = "REF_TRANSACTION_FAILED";
11
+ constructor(message, options) {
12
+ super(message, options);
13
+ this.name = "RefTransactionError";
14
+ }
15
+ }
7
16
  function isMissingExactRef(result) {
8
- return result.status === 1 && result.stdout === "";
17
+ return !result.error
18
+ && result.stdout === ""
19
+ && result.status === 1;
9
20
  }
10
21
  function parseShowRefRows(output) {
11
22
  const lines = output.split(/\r?\n/);
@@ -15,14 +26,18 @@ function parseShowRefRows(output) {
15
26
  return null;
16
27
  const rows = [];
17
28
  for (const line of lines) {
18
- const match = /^(?<objectId>[0-9a-fA-F]{40}|[0-9a-fA-F]{64}) (?<ref>refs\/\S+)$/.exec(line);
19
- if (!match?.groups)
29
+ const separator = line.indexOf(" ");
30
+ const objectId = separator < 0 ? "" : line.slice(0, separator);
31
+ const ref = separator < 0 ? "" : line.slice(separator + 1);
32
+ if (!isFullGitObjectId(objectId) || !/^refs\/\S+$/.test(ref))
20
33
  return null;
21
- rows.push({ objectId: match.groups.objectId, ref: match.groups.ref });
34
+ rows.push({ objectId, ref });
22
35
  }
23
36
  return rows;
24
37
  }
25
38
  function throwGitProcessFailure(commandLabel, result, cwd) {
39
+ if (result.error?.name === "AbortError")
40
+ throw result.error;
26
41
  const failure = result.error ?? Object.assign(new Error(result.stderr || `git ${commandLabel} exited ${result.status}`), {
27
42
  stderr: result.stderr,
28
43
  stdout: result.stdout,
@@ -46,58 +61,165 @@ export async function resolveLocalBranchHead(cwd, branch, run = runGitProcess) {
46
61
  }
47
62
  return throwGitProcessFailure(`show-ref ${ref}`, result, cwd);
48
63
  }
49
- async function resolveRefOrNull(cwd, ref) {
50
- const git = createGit(cwd);
51
- try {
52
- return (await git.raw(["rev-parse", "--verify", ref])).trim();
64
+ async function validateTransactionOperation(cwd, operation, objectIdLength) {
65
+ const refCheck = await runGitProcess(cwd, ["check-ref-format", operation.ref]);
66
+ if (refCheck.status !== 0 || refCheck.error) {
67
+ throw new RefTransactionError(`invalid transaction ref ${JSON.stringify(operation.ref)}`, { cause: refCheck.error });
53
68
  }
54
- catch (error) {
55
- if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
56
- return null;
57
- throw wrapGitError(`rev-parse ${ref}`, error, cwd);
69
+ const objectIds = operation.kind === "update"
70
+ ? [["new", operation.newOid], ["old", operation.expectedOldOid]]
71
+ : [["old", operation.expectedOldOid]];
72
+ for (const [label, oid] of objectIds) {
73
+ if (oid !== null && (oid.length !== objectIdLength || !isFullGitObjectId(oid))) {
74
+ throw new RefTransactionError(`invalid ${label} object ID for ${operation.ref}`);
75
+ }
58
76
  }
59
77
  }
60
- /** Create an immutable recovery ref, accepting an earlier identical attempt. */
61
- export async function createRecoveryRef(cwd, ref, objectId) {
62
- const existing = await resolveRefOrNull(cwd, ref);
63
- if (existing === objectId)
64
- return;
65
- if (existing !== null) {
66
- throw new Error(`recovery ref ${ref} already resolves to ${existing}, not ${objectId}`);
67
- }
68
- const git = createGit(cwd);
78
+ function formatTransactionOperation(operation, zero) {
79
+ if (operation.kind === "delete")
80
+ return `delete ${operation.ref} ${operation.expectedOldOid}`;
81
+ if (operation.kind === "verify")
82
+ return `verify ${operation.ref} ${operation.expectedOldOid ?? zero}`;
83
+ return `update ${operation.ref} ${operation.newOid} ${operation.expectedOldOid ?? zero}`;
84
+ }
85
+ function isUnambiguousCasRefusal(result) {
86
+ if (result.status === null || result.status === 0 || result.error)
87
+ return false;
88
+ return /cannot lock ref '[^']+': (?:is at [0-9a-f]+ but expected [0-9a-f]+|reference already exists|reference is missing but expected [0-9a-f]+)/i.test(result.stderr);
89
+ }
90
+ function isUnableToResolveReference(result) {
91
+ return result.status !== null
92
+ && result.status !== 0
93
+ && !result.error
94
+ && /cannot lock ref '[^']+': unable to resolve reference/i.test(result.stderr);
95
+ }
96
+ function hasConclusiveMissingExpectedReference(observations) {
97
+ return observations.some((observation) => (observation.expectedOldOid !== null
98
+ && observation.observed.status === "absent"));
99
+ }
100
+ function transactionFailureCause(result) {
101
+ return result.stderr.trim() || result.error?.message || `exit ${String(result.status)}`;
102
+ }
103
+ function requestedNewOid(operation) {
104
+ if (operation.kind === "update")
105
+ return operation.newOid;
106
+ if (operation.kind === "delete")
107
+ return null;
108
+ return operation.expectedOldOid;
109
+ }
110
+ function observationMatches(observation, expectedOid) {
111
+ if (observation.observed.status === "unknown")
112
+ return false;
113
+ if (expectedOid === null)
114
+ return observation.observed.status === "absent";
115
+ return observation.observed.status === "present" && observation.observed.objectId === expectedOid;
116
+ }
117
+ async function observeExactRef(cwd, ref) {
69
118
  try {
70
- await git.raw(["update-ref", ref, objectId, ZERO_OBJECT_ID]);
119
+ const symbolic = await runGitProcess(cwd, ["symbolic-ref", "--quiet", ref]);
120
+ if (symbolic.status === 0 && !symbolic.error) {
121
+ const target = symbolic.stdout.trim();
122
+ return { status: "unknown", cause: `symbolic ref targets ${target || "an unreported ref"}` };
123
+ }
124
+ if (symbolic.error || (symbolic.status !== 1 && symbolic.status !== 0)) {
125
+ const cause = symbolic.stderr.trim() || symbolic.error?.message || `symbolic-ref exited ${String(symbolic.status)}`;
126
+ return { status: "unknown", cause };
127
+ }
128
+ const exact = await runGitProcess(cwd, ["show-ref", ref]);
129
+ if (exact.status === 0 && !exact.error) {
130
+ const exactRows = parseShowRefRows(exact.stdout)?.filter((row) => row.ref === ref);
131
+ if (exactRows?.length === 1) {
132
+ return { status: "present", objectId: exactRows[0].objectId };
133
+ }
134
+ return { status: "unknown", cause: `show-ref returned an invalid exact value for ${ref}` };
135
+ }
136
+ if (!exact.error && exact.status === 1 && exact.stdout === "")
137
+ return { status: "absent" };
138
+ const cause = exact.stderr.trim() || exact.error?.message || `show-ref exited ${String(exact.status)}`;
139
+ return { status: "unknown", cause };
71
140
  }
72
141
  catch (error) {
73
- // Another identical attempt may have won the create race.
74
- if (await resolveRefOrNull(cwd, ref) === objectId)
75
- return;
76
- throw wrapGitError(`update-ref ${ref}`, error, cwd);
142
+ return { status: "unknown", cause: error instanceof Error ? error.message : String(error) };
77
143
  }
78
144
  }
79
- /** Remove only the exact attempt ref, returning truthful non-fatal cleanup facts. */
80
- export async function deleteRecoveryRef(cwd, ref, objectId) {
145
+ async function observeTransactionRefs(cwd, operations) {
146
+ return await Promise.all(operations.map(async (operation) => ({
147
+ ref: operation.ref,
148
+ expectedOldOid: operation.expectedOldOid,
149
+ requestedNewOid: requestedNewOid(operation),
150
+ observed: await observeExactRef(cwd, operation.ref),
151
+ })));
152
+ }
153
+ /**
154
+ * Atomically apply a closed set of ref operations. Every old-OID assertion is
155
+ * submitted inside the same update-ref transaction as its operation; no
156
+ * preflight read is used as authority.
157
+ */
158
+ export async function commitRefTransaction(cwd, operations, objectIdLength, options = {}) {
159
+ if (operations.length === 0)
160
+ throw new RefTransactionError("a ref transaction requires at least one operation");
161
+ const seen = new Set();
162
+ for (const operation of operations) {
163
+ await validateTransactionOperation(cwd, operation, objectIdLength);
164
+ if (seen.has(operation.ref))
165
+ throw new RefTransactionError(`duplicate transaction ref ${operation.ref}`);
166
+ seen.add(operation.ref);
167
+ }
168
+ const zero = "0".repeat(objectIdLength);
169
+ const input = [
170
+ "start",
171
+ ...operations.map((operation) => formatTransactionOperation(operation, zero)),
172
+ "prepare",
173
+ "commit",
174
+ "",
175
+ ].join("\n");
176
+ let result;
81
177
  try {
82
- const existing = await resolveRefOrNull(cwd, ref);
83
- if (existing === null)
84
- return { ref, status: "already-absent" };
85
- if (existing !== objectId) {
86
- return {
87
- ref,
88
- status: "retained",
89
- diagnostic: `ref resolves to ${existing}, not expected object ${objectId}`,
90
- };
91
- }
92
- const git = createGit(cwd);
93
- await git.raw(["update-ref", "-d", ref, objectId]);
94
- return { ref, status: "removed" };
178
+ result = await (options.run ?? runGitProcess)(cwd, ["update-ref", "--no-deref", "--stdin"], { input });
95
179
  }
96
180
  catch (error) {
181
+ result = {
182
+ stdout: "",
183
+ stderr: "",
184
+ status: null,
185
+ error: error instanceof Error ? error : new Error(String(error)),
186
+ };
187
+ }
188
+ if (result.status === 0 && !result.error) {
189
+ return { status: "committed", proof: "process-success", operations, observations: [] };
190
+ }
191
+ const observations = await observeTransactionRefs(cwd, operations);
192
+ const allPre = observations.every((observation) => observationMatches(observation, observation.expectedOldOid));
193
+ const allPost = observations.every((observation) => observationMatches(observation, observation.requestedNewOid));
194
+ const changesAuthority = operations.some((operation) => requestedNewOid(operation) !== operation.expectedOldOid);
195
+ const cause = transactionFailureCause(result);
196
+ if (allPost && changesAuthority) {
197
+ return { status: "committed", proof: "observed-post-state", operations, observations };
198
+ }
199
+ if (isUnambiguousCasRefusal(result)
200
+ || (isUnableToResolveReference(result) && hasConclusiveMissingExpectedReference(observations))) {
201
+ return {
202
+ status: "refused",
203
+ code: REF_TRANSACTION_CAS_MISMATCH,
204
+ operations,
205
+ observations,
206
+ cause,
207
+ };
208
+ }
209
+ if (allPre && changesAuthority) {
97
210
  return {
98
- ref,
99
- status: "retained",
100
- diagnostic: wrapGitError(`update-ref -d ${ref}`, error, cwd).message,
211
+ status: "refused",
212
+ code: REF_TRANSACTION_NOT_COMMITTED,
213
+ operations,
214
+ observations,
215
+ cause,
101
216
  };
102
217
  }
218
+ return {
219
+ status: "indeterminate",
220
+ code: REF_TRANSACTION_INDETERMINATE,
221
+ operations,
222
+ observations,
223
+ cause,
224
+ };
103
225
  }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { isFullGitObjectId } from "./core.js";
2
3
  import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
3
- import { gitProcessEnv, resolveGitTimeoutMs, } from "./core.js";
4
+ import { gitProcessEnv, resolveGitTimeoutMs, terminateGitChild, } from "./core.js";
4
5
  /** Default long-lived batch argv. One-shot calls must not use this gate. */
5
6
  export const GIT_STREAMING_BATCH_DEFAULT_ARGS = ["cat-file", "--batch"];
6
7
  /** Safety cap for a single response header line before the body frame. */
@@ -15,10 +16,12 @@ export class GitStreamingBatchResponseEndedError extends Error {
15
16
  }
16
17
  class BufferedReader {
17
18
  #iterator;
19
+ #onProgress;
18
20
  #buffer = Buffer.alloc(0);
19
21
  #ended = false;
20
- constructor(stream) {
22
+ constructor(stream, onProgress) {
21
23
  this.#iterator = stream[Symbol.asyncIterator]();
24
+ this.#onProgress = onProgress;
22
25
  }
23
26
  async #fill() {
24
27
  if (this.#ended) {
@@ -30,6 +33,7 @@ class BufferedReader {
30
33
  throw new GitStreamingBatchResponseEndedError("git streaming batch response ended unexpectedly");
31
34
  }
32
35
  const chunk = Buffer.isBuffer(next.value) ? next.value : Buffer.from(next.value);
36
+ this.#onProgress();
33
37
  this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);
34
38
  }
35
39
  async readLine(maxBytes) {
@@ -84,20 +88,36 @@ export class GitStreamingBatchSession {
84
88
  #child;
85
89
  #reader;
86
90
  #stderr = [];
87
- #timeout;
91
+ #timeoutMs;
88
92
  #closed;
89
93
  #maxHeaderBytes;
94
+ #signal;
95
+ #abortListener;
96
+ #timeout = null;
90
97
  #state = { kind: "open" };
91
98
  #timedOut = false;
92
99
  #aborted = false;
93
100
  #inputError = null;
94
101
  #requestInFlight = false;
102
+ #requestDone = null;
103
+ #finishRequest = null;
104
+ #requestTimeoutOwner = null;
95
105
  #closePromise = null;
96
- constructor(cwd, child, timeoutMs, maxHeaderBytes) {
106
+ #terminationPromise = null;
107
+ constructor(cwd, child, timeoutMs, maxHeaderBytes, signal) {
97
108
  this.cwd = cwd;
98
109
  this.#child = child;
99
- this.#reader = new BufferedReader(this.#child.stdout);
110
+ this.#timeoutMs = timeoutMs;
111
+ this.#reader = new BufferedReader(this.#child.stdout, () => {
112
+ if (this.#requestTimeoutOwner)
113
+ this.#armTimeout(this.#requestTimeoutOwner);
114
+ });
100
115
  this.#maxHeaderBytes = maxHeaderBytes;
116
+ this.#signal = signal;
117
+ this.#abortListener = () => this.abort();
118
+ signal?.addEventListener("abort", this.#abortListener, { once: true });
119
+ if (signal?.aborted)
120
+ this.abort();
101
121
  this.#child.stderr.on("data", (chunk) => this.#stderr.push(chunk));
102
122
  this.#child.stdin.on("error", (error) => {
103
123
  this.#inputError = error;
@@ -110,10 +130,27 @@ export class GitStreamingBatchSession {
110
130
  resolve({ code, signal });
111
131
  });
112
132
  });
113
- this.#timeout = setTimeout(() => {
133
+ }
134
+ #armTimeout(owner) {
135
+ this.#clearTimeout();
136
+ const handle = setTimeout(() => {
137
+ if (this.#timeout?.owner !== owner)
138
+ return;
139
+ this.#timeout = null;
114
140
  this.#timedOut = true;
141
+ if (this.#state.kind === "open")
142
+ this.#state = { kind: "closing" };
115
143
  this.#child.kill("SIGKILL");
116
- }, timeoutMs);
144
+ }, this.#timeoutMs);
145
+ this.#timeout = { owner, handle };
146
+ }
147
+ #clearTimeout(owner) {
148
+ if (this.#timeout === null)
149
+ return;
150
+ if (owner !== undefined && this.#timeout.owner !== owner)
151
+ return;
152
+ clearTimeout(this.#timeout.handle);
153
+ this.#timeout = null;
117
154
  }
118
155
  /**
119
156
  * Open the sole long-lived Git batch process for `cwd`.
@@ -125,11 +162,11 @@ export class GitStreamingBatchSession {
125
162
  const maxHeaderBytes = options?.maxHeaderBytes ?? GIT_STREAMING_BATCH_MAX_HEADER_BYTES;
126
163
  const child = spawn("git", [...args], {
127
164
  cwd: normalizeFilesystemCoordinate(cwd),
128
- env: gitProcessEnv(),
165
+ env: gitProcessEnv(options?.env),
129
166
  shell: false,
130
167
  stdio: ["pipe", "pipe", "pipe"],
131
168
  });
132
- return new GitStreamingBatchSession(cwd, child, timeoutMs, maxHeaderBytes);
169
+ return new GitStreamingBatchSession(cwd, child, timeoutMs, maxHeaderBytes, options?.signal);
133
170
  }
134
171
  /**
135
172
  * Write one request line and read one framed response record.
@@ -142,7 +179,13 @@ export class GitStreamingBatchSession {
142
179
  if (this.#requestInFlight) {
143
180
  throw new Error("git streaming batch rejects concurrent requests");
144
181
  }
182
+ const timeoutOwner = Symbol("git-streaming-batch-request");
145
183
  this.#requestInFlight = true;
184
+ this.#requestTimeoutOwner = timeoutOwner;
185
+ this.#requestDone = new Promise((resolve) => {
186
+ this.#finishRequest = resolve;
187
+ });
188
+ this.#armTimeout(timeoutOwner);
146
189
  try {
147
190
  if (this.#inputError)
148
191
  throw this.#inputError;
@@ -168,7 +211,8 @@ export class GitStreamingBatchSession {
168
211
  catch (error) {
169
212
  if (error instanceof GitStreamingBatchFramingError
170
213
  || error instanceof GitStreamingBatchResponseEndedError) {
171
- this.abort();
214
+ if (!this.#timedOut)
215
+ this.abort();
172
216
  }
173
217
  throw error;
174
218
  }
@@ -183,7 +227,7 @@ export class GitStreamingBatchSession {
183
227
  const size = Number(sizeText);
184
228
  if (unexpected.length > 0
185
229
  || !oid
186
- || !/^[0-9a-f]{40,64}$/.test(oid)
230
+ || !isFullGitObjectId(oid)
187
231
  || !type
188
232
  || !Number.isSafeInteger(size)
189
233
  || size < 0) {
@@ -221,7 +265,13 @@ export class GitStreamingBatchSession {
221
265
  };
222
266
  }
223
267
  finally {
268
+ this.#clearTimeout(timeoutOwner);
269
+ this.#requestTimeoutOwner = null;
224
270
  this.#requestInFlight = false;
271
+ const finishRequest = this.#finishRequest;
272
+ this.#finishRequest = null;
273
+ this.#requestDone = null;
274
+ finishRequest?.();
225
275
  }
226
276
  }
227
277
  /**
@@ -232,11 +282,12 @@ export class GitStreamingBatchSession {
232
282
  if (this.#aborted)
233
283
  return;
234
284
  this.#aborted = true;
285
+ this.#clearTimeout();
235
286
  if (this.#state.kind === "open") {
236
287
  this.#state = { kind: "closing" };
237
288
  }
238
289
  this.#child.stdin.destroy();
239
- this.#child.kill("SIGKILL");
290
+ this.#terminationPromise ??= terminateGitChild(this.#child, this.#closed);
240
291
  }
241
292
  /**
242
293
  * End stdin, await process exit, and return structured close facts.
@@ -263,7 +314,15 @@ export class GitStreamingBatchSession {
263
314
  return this.#closePromise;
264
315
  }
265
316
  async #closeOnce() {
317
+ const pendingRequest = this.#requestDone;
318
+ if (pendingRequest)
319
+ await pendingRequest;
320
+ if (this.#terminationPromise)
321
+ await this.#terminationPromise;
322
+ const timeoutOwner = Symbol("git-streaming-batch-close");
266
323
  if (!this.#aborted && this.#child.stdin?.writable) {
324
+ if (!this.#timedOut)
325
+ this.#armTimeout(timeoutOwner);
267
326
  this.#child.stdin.end();
268
327
  }
269
328
  let exit;
@@ -271,8 +330,9 @@ export class GitStreamingBatchSession {
271
330
  exit = await this.#closed;
272
331
  }
273
332
  finally {
274
- clearTimeout(this.#timeout);
333
+ this.#clearTimeout(timeoutOwner);
275
334
  }
335
+ this.#signal?.removeEventListener("abort", this.#abortListener);
276
336
  if (exit.processError && !this.#aborted) {
277
337
  const fact = {
278
338
  code: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "2.9.9",
3
+ "version": "2.9.10",
4
4
  "description": "CLI for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,7 @@ EOF
33
33
  ```
34
34
 
35
35
  That's the whole loop. Skip the task when the work needs no planning:
36
- `keiyaku bind --place NAME --objective TEXT --scope GLOB --checks CHECK` (or the same as Markdown through `keiyaku bind -` — `bind --help` shows the shape).
36
+ `keiyaku bind --title TEXT --context TEXT --objective TEXT --design TEXT --scope GLOB --criteria TEXT [--verification COMMAND]` (or the same settled document through `keiyaku bind -` — `bind --help` shows the shape).
37
37
 
38
38
  ## Getting oriented
39
39
 
@@ -41,10 +41,10 @@ addressable. `revive` starts a new projection and does not inherit an alias.
41
41
 
42
42
  ```bash
43
43
  keiyaku akuma ls # who's callable
44
- keiyaku akuma view NAME # resolved profile + live projections
44
+ keiyaku akuma show NAME # resolved profile + live projections
45
45
  keiyaku call NAME "do the thing" # run in foreground until it finishes
46
46
  keiyaku call NAME --wait 10m "do the thing" # bounded: after 10m you get a snapshot, it keeps running
47
- keiyaku call NAME --detach "do the thing" # fire and forget; note the projection id
47
+ keiyaku call NAME -d "do the thing" # short for --detach; note the projection id
48
48
  keiyaku call NAME - < prompt.md # long prompt: literal `-` reads stdin
49
49
  keiyaku wait <proj-id> # join a running one; prints its activity
50
50
  keiyaku wait <proj-id> --timeout 5m # peek for 5m; timeout = snapshot, not death
@@ -63,7 +63,7 @@ keiyaku status # forgot an id? it's on the board
63
63
  <KEIYAKU_HOME>/akuma/<name>.md user profile (normally ~/.keiyaku)
64
64
  ```
65
65
 
66
- Use `keiyaku akuma view NAME` to inspect the resolved source and configuration.
66
+ Use `keiyaku akuma show NAME` to inspect the resolved source and configuration.
67
67
 
68
68
  ## Expectations
69
69
 
@@ -52,8 +52,10 @@ keiyaku bind --task fix-parser --task fix-parser-help - < contract.md
52
52
  Only ready tasks bind. Multi-bind only when the tasks describe one delivery
53
53
  intent, share a coherent write surface, and should settle together; otherwise
54
54
  keep separate contracts and express order with task `needs` or contract
55
- `--after`. The first task supplies name/objective defaults; provide a partial
56
- bind document for any missing `Scope` or `Checks`.
55
+ `--after`. The first task supplies title/objective defaults; provide a partial
56
+ bind document for every remaining resolved section. Final bind admission always
57
+ requires Title, Context, Objective, Design, Scope, and Criteria; Verification
58
+ is optional.
57
59
 
58
60
  ## Gotcha
59
61