@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
@@ -2,78 +2,25 @@ import * as crypto from "node:crypto";
2
2
  import { assertSettingsKnobUsable } from "../config/settings/disease.js";
3
3
  import { loadKeiyakuSettings } from "../config/settings/loader.js";
4
4
  import { FlowError } from "../flow-error.js";
5
- import { activeLedgerRef, appendEntry, appendWithRetry, readLedger } from "./ledger.js";
5
+ import { lookupCurrentContract, readRepositoryLifecycle, } from "./repository-ledger/read-model.js";
6
+ import { repositoryDeliveryRef, } from "./repository-ledger/codec.js";
7
+ import { publishBindTransaction, } from "./repository-ledger/write-transaction.js";
8
+ import { bindEffectiveScope } from "./repository-ledger/fold.js";
9
+ import { activeWorkspaceHereContracts } from "./repository-ledger/inventory.js";
6
10
  import { describeMissingBindSections, materializeBindDraft, mergeBindDraft, missingBindDraftSections, parseBindDraft, parsePartialBindDraft, renderBindDraft, renderMissingBindSectionsTemplate, } from "./draft.js";
7
11
  import { assertValidContractId, buildContractId, isCommissionId } from "./ids.js";
8
- import { previewScopeRegistration, registerScope, releaseScope } from "./registry.js";
12
+ import { admitScopeRegistration, } from "./registry.js";
9
13
  import { resolveDefaultBranch } from "./status/drift.js";
10
14
  import { normalizeTargetRef } from "./target-ref.js";
11
- import { previewTaskBoardForBind, reconcileTaskForfeit, settleTaskBoardForBind } from "./task/index.js";
12
- import { bootstrapEntryDataFromObservation, isBootstrapSuccess, runWorktreeBootstrap, } from "./worktree-bootstrap.js";
13
- import { contractWorktreePathFromBind, stableRepoRoot } from "./worktree-path.js";
14
- import { allocateWorktreePlace, cleanupCreatedBindResources, createContractBranch, defaultCreateWorktree, materializeContractView, resolveTreeRoot, } from "./bind-workspace.js";
15
- class BindLedgerCollisionError extends Error {
16
- contractId;
17
- constructor(contractId) {
18
- super(`contract ledger already exists for ${contractId}`);
19
- this.contractId = contractId;
20
- this.name = "BindLedgerCollisionError";
21
- }
22
- }
15
+ import { previewTaskBoardForBind, readTaskNamespaces, replayPreparedTaskBindLocked, withPreparedTaskBind, } from "./task/index.js";
16
+ import { stableRepoRoot } from "./worktree-path.js";
17
+ import { allocateWorktreePlaceFromRead, } from "./bind-workspace.js";
18
+ import { committedOrThrow, reconcileCommittedBind, revalidateBindAuthority } from "./bind-reconciliation.js";
19
+ import { allocateTaskNamespace, taskNamespaceFromScope, taskNamespaceScope } from "./task/coordinate.js";
20
+ export { reconcileBindContract } from "./bind-reconciliation.js";
23
21
  function randomBytesDefault() {
24
22
  return [...crypto.randomBytes(10)];
25
23
  }
26
- async function appendBindFailedForfeit(cwd, id, actor, at) {
27
- const forfeit = {
28
- v: 1,
29
- kind: "forfeit",
30
- contract: id,
31
- at,
32
- actor,
33
- data: {
34
- reason: "bind-failed",
35
- by: actor,
36
- },
37
- };
38
- const stableRoot = await stableRepoRoot(cwd);
39
- await reconcileTaskForfeit({
40
- cwd: stableRoot,
41
- contractId: id,
42
- publishLedger: async () => {
43
- const result = await appendWithRetry(stableRoot, activeLedgerRef(id), () => forfeit, { maxRetries: 1 });
44
- if (!result.ok) {
45
- throw new FlowError("INTERNAL_STATE", `BIND_FAILURE_FORFEIT_FAILED: failed to append bind-failed forfeit for ${id}: ${result.reason}`);
46
- }
47
- return result;
48
- },
49
- });
50
- }
51
- async function appendBootstrapEntry(cwd, id, actor, at, data) {
52
- const entry = {
53
- v: 1,
54
- kind: "bootstrap",
55
- contract: id,
56
- at,
57
- actor,
58
- data,
59
- };
60
- // Bootstrap is legal only at index 1 immediately after the planned bind.
61
- // If another entry advanced during bootstrap execution, abort rather than
62
- // CAS-retrying into a late (corrupt) position.
63
- const result = await appendWithRetry(cwd, activeLedgerRef(id), (_currentHead, currentEntries) => {
64
- if (currentEntries.length !== 1)
65
- return null;
66
- const only = currentEntries[0];
67
- if (!only || only.kind !== "bind" || only.contract !== id || !only.data.bootstrap) {
68
- return null;
69
- }
70
- return entry;
71
- });
72
- if (!result.ok) {
73
- throw new FlowError("INTERNAL_STATE", `BIND_BOOTSTRAP_APPEND_FAILED: failed to append bootstrap entry for ${id}: ${result.reason}`);
74
- }
75
- return result.head;
76
- }
77
24
  function resolvedBootstrapPlan(settings) {
78
25
  if (!settings)
79
26
  return undefined;
@@ -82,33 +29,55 @@ function resolvedBootstrapPlan(settings) {
82
29
  timeoutMs: settings.timeoutMs,
83
30
  };
84
31
  }
85
- function bootstrapProgressLine(command) {
86
- return `bootstrap » ${command.join(" ")}`;
32
+ function bindDraftWithTaskNamespace(draft, read, persistedNamespaces) {
33
+ const declared = draft.scope.flatMap((term) => {
34
+ const namespace = taskNamespaceFromScope(term);
35
+ return namespace ? [namespace] : [];
36
+ });
37
+ if (declared.length > 1)
38
+ throw new FlowError("INVALID_TARGET", "bind scope must carry exactly one contract task namespace");
39
+ const occupied = new Set(read.contracts.flatMap((contract) => contract.effectiveScope.flatMap((term) => {
40
+ const namespace = taskNamespaceFromScope(term);
41
+ return namespace ? [namespace] : [];
42
+ })));
43
+ for (const namespace of persistedNamespaces)
44
+ occupied.add(namespace);
45
+ if (declared.length === 1) {
46
+ if (occupied.has(declared[0]))
47
+ throw new FlowError("INVALID_TARGET", `task namespace ${declared[0]} is already occupied`);
48
+ return draft;
49
+ }
50
+ const namespace = allocateTaskNamespace(draft.title, occupied);
51
+ return { ...draft, scope: [...draft.scope, taskNamespaceScope(namespace)] };
87
52
  }
88
53
  function makeBindEntry(input) {
54
+ const common = {
55
+ title: input.draft.title,
56
+ context: input.draft.context,
57
+ objective: input.draft.objective,
58
+ design: input.draft.design,
59
+ scope: input.draft.scope,
60
+ criteria: input.draft.criteria,
61
+ verification: input.draft.verification,
62
+ extensions: input.draft.extensions,
63
+ base: input.base,
64
+ target: input.target,
65
+ ...(input.after.length > 0 ? { after: input.after } : {}),
66
+ ...(input.taskIds ? { taskIds: [...input.taskIds] } : {}),
67
+ };
89
68
  return {
90
- v: 1,
69
+ v: 2,
91
70
  kind: "bind",
92
71
  contract: input.id,
93
72
  at: input.at,
94
73
  actor: input.actor,
95
- data: {
96
- name: input.draft.name,
97
- objective: input.draft.objective,
98
- scope: input.draft.scope,
99
- checks: input.draft.checks,
100
- extensions: input.draft.extensions,
101
- base: input.base,
102
- target: input.target,
103
- workspace: input.workspace,
104
- after: input.after,
105
- ...(input.workspace === "worktree" ? { place: input.place ?? null } : {}),
106
- ...(input.bootstrap ? { bootstrap: input.bootstrap } : {}),
107
- },
74
+ data: input.workspace === "worktree"
75
+ ? { ...common, workspace: "worktree", place: input.place, ...(input.bootstrap ? { bootstrap: input.bootstrap } : {}) }
76
+ : { ...common, workspace: "here" },
108
77
  };
109
78
  }
110
79
  function taskBindDefaults(task) {
111
- const defaults = { name: task.title };
80
+ const defaults = { title: task.title };
112
81
  const objective = task.body.trim();
113
82
  if (objective.length > 0)
114
83
  defaults.objective = objective;
@@ -126,7 +95,7 @@ function incompleteTaskBindError(taskId, missing) {
126
95
  return new FlowError("BIND_TASK_INCOMPLETE", message);
127
96
  }
128
97
  /**
129
- * Section-level merge for `bind --task`: task title/body are name/objective
98
+ * Section-level merge for `bind --task`: task title/body are title/objective
130
99
  * defaults; stdin partial markdown sections override. Missing required sections
131
100
  * fail with a paste-ready partial heredoc template.
132
101
  */
@@ -138,7 +107,7 @@ export function composeTaskBindDraft(input) {
138
107
  }
139
108
  return materializeBindDraft(merged);
140
109
  }
141
- async function validateBindAfter(cwd, after) {
110
+ function validateBindAfter(read, after) {
142
111
  const unique = new Set();
143
112
  for (const contractId of after) {
144
113
  if (unique.has(contractId)) {
@@ -148,20 +117,44 @@ async function validateBindAfter(cwd, after) {
148
117
  if (!isCommissionId(contractId)) {
149
118
  throw new FlowError("INVALID_TARGET", `after contract ${contractId} is not a valid contract id`);
150
119
  }
151
- if (!await readLedger(cwd, contractId)) {
152
- throw new FlowError("INVALID_TARGET", `after contract ${contractId} does not exist`);
120
+ const outcome = lookupCurrentContract(read, contractId);
121
+ if (outcome.status !== "healthy") {
122
+ throw new FlowError("INVALID_TARGET", `after contract ${contractId} is not a healthy terminal contract`);
153
123
  }
124
+ if (!["claimed", "forfeited"].includes(outcome.model.phase)) {
125
+ throw new FlowError("INVALID_TARGET", `after contract ${contractId} is not terminal`);
126
+ }
127
+ }
128
+ }
129
+ function assertContractAbsent(read, contractId) {
130
+ const outcome = lookupCurrentContract(read, contractId);
131
+ if (outcome.status === "absent")
132
+ return;
133
+ if (outcome.status === "indeterminate") {
134
+ throw new FlowError("INTERNAL_STATE", `BIND_ADMISSION_INDETERMINATE: cannot prove contract ${contractId} absent`);
154
135
  }
136
+ throw new FlowError("CONCURRENT_MODIFICATION", `CONTRACT_ID_COLLISION: contract ${contractId} already exists`);
137
+ }
138
+ function admitWorkspaceOccupancy(read, workspace) {
139
+ if (workspace === "worktree")
140
+ return;
141
+ const occupants = activeWorkspaceHereContracts(read);
142
+ if (occupants.length === 0)
143
+ return;
144
+ throw new FlowError("INVALID_TARGET", `workspace here is occupied by nonterminal contract ${occupants.join(", ")}`);
155
145
  }
156
146
  /**
157
147
  * The common read-only bind admission candidate. Mutation uses this candidate
158
148
  * before task settlement; preview stops after it has observed all admission
159
149
  * facts and rendered the canonical draft.
160
150
  */
161
- async function admitBindCandidate(input, draft) {
162
- const id = buildContractId({ name: draft.name, nowMs: input.nowMs, randomBytes: input.randomBytes });
151
+ async function admitBindCandidate(input, draft, read) {
152
+ draft = bindDraftWithTaskNamespace(draft, read, await readTaskNamespaces(input.cwd));
153
+ admitWorkspaceOccupancy(read, input.workspace);
154
+ const id = buildContractId({ title: draft.title, nowMs: input.nowMs, randomBytes: input.randomBytes });
163
155
  await assertValidContractId(input.cwd, id);
164
- await validateBindAfter(input.cwd, input.after);
156
+ assertContractAbsent(read, id);
157
+ validateBindAfter(read, input.after);
165
158
  const provisionalEntry = makeBindEntry({
166
159
  id,
167
160
  at: new Date(input.nowMs).toISOString(),
@@ -175,14 +168,14 @@ async function admitBindCandidate(input, draft) {
175
168
  workspace: input.workspace,
176
169
  place: input.place,
177
170
  bootstrap: input.bootstrapPlan,
171
+ taskIds: input.taskIds ? [...input.taskIds] : undefined,
178
172
  });
179
- const tree = await resolveTreeRoot(input.cwd, input.workspace, id, provisionalEntry);
180
- const scopeOverlaps = await previewScopeRegistration(input.cwd, {
173
+ const scopeOverlaps = await admitScopeRegistration(input.cwd, {
181
174
  contractId: id,
182
- scope: draft.scope,
183
- tree,
175
+ scope: bindEffectiveScope(provisionalEntry),
176
+ tree: input.base,
184
177
  exclusive: input.exclusive,
185
- });
178
+ }, read);
186
179
  return {
187
180
  id,
188
181
  draft,
@@ -209,8 +202,9 @@ export async function previewBindContract(input) {
209
202
  assertSettingsKnobUsable(loadedSettings, "worktreeBootstrap");
210
203
  bootstrapPlan = resolvedBootstrapPlan(loadedSettings.knobs?.worktreeBootstrap);
211
204
  }
205
+ const read = await readRepositoryLifecycle(input.cwd);
212
206
  const randomBytes = input.randomBytes?.(0) ?? randomBytesDefault();
213
- const place = workspace === "worktree" ? await allocateWorktreePlace(input.cwd, randomBytes) : undefined;
207
+ const place = workspace === "worktree" ? allocateWorktreePlaceFromRead(read, randomBytes) : undefined;
214
208
  const taskAdmission = isTaskBound
215
209
  ? await previewTaskBoardForBind({ cwd: input.cwd, taskIds: input.taskIds })
216
210
  : { tasks: [] };
@@ -229,205 +223,180 @@ export async function previewBindContract(input) {
229
223
  target,
230
224
  bootstrapPlan,
231
225
  place,
232
- }, draft);
226
+ ...(isTaskBound ? { taskIds: input.taskIds } : {}),
227
+ }, draft, read);
233
228
  }
234
229
  export async function bindContract(input) {
230
+ const stableRoot = await stableRepoRoot(input.cwd);
235
231
  const isTaskBound = input.taskIds !== undefined && input.taskIds.length > 0;
236
232
  const ordinaryDraft = !isTaskBound ? parseBindDraft(input.draft) : undefined;
237
233
  const workspace = input.workspace ?? "here";
238
234
  const at = new Date(input.nowMs).toISOString();
239
- const defaultBranch = await resolveDefaultBranch(input.cwd);
240
- if (!defaultBranch) {
235
+ const after = input.after ?? [];
236
+ const defaultBranch = await resolveDefaultBranch(stableRoot);
237
+ if (!defaultBranch)
241
238
  throw new FlowError("INTERNAL_STATE", "cannot bind: no default branch");
242
- }
243
239
  const target = normalizeTargetRef(defaultBranch.branch);
244
- const loadedSettings = await loadKeiyakuSettings(input.cwd);
240
+ const publishBind = input.dependencies?.publishBind ?? publishBindTransaction;
241
+ const persistedTaskNamespaces = await readTaskNamespaces(stableRoot);
242
+ const loadedSettings = await loadKeiyakuSettings(stableRoot);
245
243
  let bootstrapPlan;
246
244
  if (workspace === "worktree") {
247
245
  assertSettingsKnobUsable(loadedSettings, "worktreeBootstrap");
248
246
  bootstrapPlan = resolvedBootstrapPlan(loadedSettings.knobs?.worktreeBootstrap);
249
247
  }
250
- const collidedIds = [];
251
- for (let attempt = 0; attempt < 2; attempt += 1) {
252
- const randomBytes = input.randomBytes?.(attempt) ?? randomBytesDefault();
253
- const place = workspace === "worktree" ? await allocateWorktreePlace(input.cwd, randomBytes) : undefined;
254
- let settlement;
255
- try {
256
- const prepare = async (draft) => {
257
- const candidate = await admitBindCandidate({
258
- cwd: input.cwd,
259
- actor: input.actor,
260
- nowMs: input.nowMs,
261
- after: input.after ?? [],
262
- exclusive: input.exclusive,
263
- workspace,
264
- randomBytes,
265
- base: defaultBranch.head,
266
- target,
267
- bootstrapPlan,
268
- place,
269
- }, draft);
270
- const id = candidate.id;
271
- const ref = activeLedgerRef(id);
272
- return {
273
- contractId: id,
274
- publishLedger: async (base) => {
275
- const entry = makeBindEntry({
276
- id,
277
- at,
278
- actor: input.actor,
279
- draft,
280
- base,
281
- target,
282
- after: input.after ?? [],
283
- workspace,
284
- place,
285
- bootstrap: bootstrapPlan,
286
- });
287
- const appended = await appendEntry(input.cwd, ref, entry, null);
288
- if (!appended.ok)
289
- throw new BindLedgerCollisionError(id);
290
- return { entry, appended };
291
- },
292
- };
293
- };
294
- settlement = !isTaskBound
295
- ? await (async () => {
296
- const prepared = await prepare(ordinaryDraft);
297
- return settleTaskBoardForBind({ cwd: input.cwd, signal: input.signal, ...prepared });
298
- })()
299
- : await (async () => {
300
- return settleTaskBoardForBind({
301
- cwd: input.cwd,
302
- taskIds: input.taskIds,
303
- signal: input.signal,
304
- prepare: async (tasks) => prepare(composeTaskBindDraft({ task: tasks[0], draft: input.draft })),
305
- });
306
- })();
307
- }
308
- catch (error) {
309
- if (error instanceof BindLedgerCollisionError) {
310
- // The selected task's locked snapshot chose this attempted identity.
311
- // Record the concrete id only when preparation reached publication.
312
- collidedIds.push(error.contractId);
248
+ let selectedDraft;
249
+ let selectedPlace;
250
+ let selectedId;
251
+ const selectCandidate = async (draft, read) => {
252
+ admitWorkspaceOccupancy(read, workspace);
253
+ const collided = [];
254
+ for (let attempt = 0; attempt < 2; attempt += 1) {
255
+ const randomBytes = input.randomBytes?.(attempt) ?? randomBytesDefault();
256
+ const place = workspace === "worktree" ? allocateWorktreePlaceFromRead(read, randomBytes) : undefined;
257
+ const id = buildContractId({ title: draft.title, nowMs: input.nowMs, randomBytes });
258
+ const outcome = lookupCurrentContract(read, id);
259
+ if (outcome.status === "indeterminate")
260
+ assertContractAbsent(read, id);
261
+ if (outcome.status !== "absent") {
262
+ collided.push(id);
313
263
  continue;
314
264
  }
315
- throw error;
265
+ await assertValidContractId(stableRoot, id);
266
+ selectedDraft = bindDraftWithTaskNamespace(draft, read, persistedTaskNamespaces);
267
+ selectedPlace = place;
268
+ selectedId = id;
269
+ return { contractId: id, at };
316
270
  }
317
- const { entry, appended } = settlement.ledger;
318
- const id = settlement.contractId;
319
- const ref = activeLedgerRef(id);
320
- const base = settlement.boardCommit;
321
- const resources = { id };
322
- const branch = `keiyaku/${id}`;
323
- const worktreePath = await contractWorktreePathFromBind(input.cwd, id, entry);
324
- const targetRoot = workspace === "here" ? input.cwd : worktreePath;
325
- let ledgerHead = appended.head;
326
- let bootstrapRecorded = false;
327
- let registryRegistered = false;
328
- try {
329
- const tree = await resolveTreeRoot(input.cwd, workspace, id, entry);
330
- const registration = await registerScope(input.cwd, {
331
- contractId: id,
332
- scope: entry.data.scope,
333
- tree,
334
- actor: input.actor,
335
- at,
336
- exclusive: input.exclusive,
337
- });
338
- registryRegistered = true;
339
- if (workspace === "worktree") {
340
- await createContractBranch(input.cwd, branch, base);
341
- resources.branch = branch;
342
- await (input.createWorktree ?? defaultCreateWorktree)({
343
- cwd: input.cwd,
344
- id,
345
- branch,
346
- base,
347
- worktreePath,
348
- });
349
- resources.worktreePath = worktreePath;
350
- }
351
- if (bootstrapPlan) {
352
- input.onProgress?.(bootstrapProgressLine(bootstrapPlan.command));
353
- const runner = input.bootstrapRunner ?? runWorktreeBootstrap;
354
- const observation = await runner({ cwd: worktreePath, plan: bootstrapPlan }, input.bootstrapDependencies);
355
- ledgerHead = await appendBootstrapEntry(input.cwd, id, input.actor, at, bootstrapEntryDataFromObservation(observation));
356
- bootstrapRecorded = true;
357
- if (!isBootstrapSuccess(observation)) {
358
- throw new FlowError("INTERNAL_STATE", `BIND_BOOTSTRAP_FAILED: worktree bootstrap failed with ${observation.result.kind}`);
271
+ throw new FlowError("CONCURRENT_MODIFICATION", `CONTRACT_ID_COLLISION: contract id collision after two attempts: ${collided.join(", ")}`);
272
+ };
273
+ let entry;
274
+ let admittedScopeOverlaps = [];
275
+ let preparedTask;
276
+ if (isTaskBound) {
277
+ let lockedRead;
278
+ const settled = await withPreparedTaskBind({
279
+ cwd: stableRoot,
280
+ taskIds: input.taskIds,
281
+ signal: input.signal,
282
+ prepare: async (tasks) => {
283
+ lockedRead = await readRepositoryLifecycle(stableRoot);
284
+ validateBindAfter(lockedRead, after);
285
+ return await selectCandidate(composeTaskBindDraft({ task: tasks[0], draft: input.draft }), lockedRead);
286
+ },
287
+ transact: async (prepared, taskTransaction) => {
288
+ if (prepared.boardRef !== target) {
289
+ throw new FlowError("CONCURRENT_MODIFICATION", "locked task-board ref does not match the selected bind target");
359
290
  }
291
+ let attemptRead = lockedRead;
292
+ let lastRefusal;
293
+ for (let attempt = 0; attempt < 4; attempt += 1) {
294
+ admitWorkspaceOccupancy(attemptRead, workspace);
295
+ validateBindAfter(attemptRead, after);
296
+ assertContractAbsent(attemptRead, selectedId);
297
+ selectedPlace = workspace === "worktree"
298
+ ? allocateWorktreePlaceFromRead(attemptRead, input.randomBytes?.(attempt) ?? randomBytesDefault())
299
+ : undefined;
300
+ entry = makeBindEntry({
301
+ id: selectedId, at, actor: input.actor, draft: selectedDraft, base: prepared.entryBase,
302
+ target, after, workspace, place: selectedPlace, bootstrap: bootstrapPlan, taskIds: prepared.taskIds,
303
+ });
304
+ admittedScopeOverlaps = await admitScopeRegistration(stableRoot, {
305
+ contractId: entry.contract,
306
+ scope: bindEffectiveScope(entry),
307
+ tree: entry.data.base,
308
+ ...(input.exclusive === undefined ? {} : { exclusive: input.exclusive }),
309
+ }, attemptRead);
310
+ await revalidateBindAuthority({
311
+ cwd: stableRoot,
312
+ target,
313
+ targetHead: prepared.expectedHead,
314
+ deliveryRef: repositoryDeliveryRef(entry.contract),
315
+ });
316
+ const written = await publishBind({
317
+ cwd: stableRoot,
318
+ entry,
319
+ expectedLedgerHead: attemptRead.observedLedgerHead,
320
+ delivery: { ref: repositoryDeliveryRef(entry.contract), head: prepared.entryBase },
321
+ taskBoard: prepared.taskBoard,
322
+ });
323
+ if (written.family === "committed") {
324
+ try {
325
+ await replayPreparedTaskBindLocked(taskTransaction, prepared);
326
+ }
327
+ catch {
328
+ // Authority is already committed. The ordinary derived replay
329
+ // below owns truthful retry/contained classification.
330
+ }
331
+ return written;
332
+ }
333
+ if (written.family === "indeterminate")
334
+ committedOrThrow(written, "BIND_TRANSACTION");
335
+ lastRefusal = written;
336
+ attemptRead = await readRepositoryLifecycle(stableRoot);
337
+ }
338
+ return lastRefusal;
339
+ },
340
+ });
341
+ preparedTask = settled.preparation;
342
+ committedOrThrow(settled.result, "BIND_TRANSACTION");
343
+ }
344
+ else {
345
+ let committed;
346
+ for (let attempt = 0; attempt < 4; attempt += 1) {
347
+ const [read, branch] = await Promise.all([
348
+ readRepositoryLifecycle(stableRoot),
349
+ resolveDefaultBranch(stableRoot),
350
+ ]);
351
+ if (!branch || normalizeTargetRef(branch.branch) !== target) {
352
+ throw new FlowError("CONCURRENT_MODIFICATION", "bind target changed during admission");
360
353
  }
361
- const ledger = await readLedger(input.cwd, id);
362
- if (!ledger)
363
- throw new FlowError("INTERNAL_STATE", `LEDGER_MISSING: ledger missing immediately after bind for ${id}`);
364
- resources.keiyakuPath = await materializeContractView({
365
- targetRoot,
366
- id,
367
- entries: ledger.entries,
354
+ validateBindAfter(read, after);
355
+ await selectCandidate(ordinaryDraft, read);
356
+ entry = makeBindEntry({
357
+ id: selectedId, at, actor: input.actor, draft: selectedDraft, base: branch.head,
358
+ target, after, workspace, place: selectedPlace, bootstrap: bootstrapPlan,
368
359
  });
369
- return {
370
- id,
371
- ref,
360
+ admittedScopeOverlaps = await admitScopeRegistration(stableRoot, {
361
+ contractId: entry.contract,
362
+ scope: bindEffectiveScope(entry),
363
+ tree: entry.data.base,
364
+ ...(input.exclusive === undefined ? {} : { exclusive: input.exclusive }),
365
+ }, read);
366
+ await revalidateBindAuthority({
367
+ cwd: stableRoot,
372
368
  target,
373
- place: workspace === "worktree" ? entry.data.place ?? null : null,
374
- branch: workspace === "worktree" ? branch : undefined,
375
- worktreePath: targetRoot,
376
- workspace,
377
- ledgerHead,
378
- scopeOverlaps: registration.overlaps,
379
- };
380
- }
381
- catch (error) {
382
- // Bootstrap failure order is strict: bootstrap entry (already appended) →
383
- // forfeit(bind-failed) → cleanup. Cleanup cold facts cannot convert success.
384
- const cleanupFailures = [];
385
- if (!bootstrapRecorded) {
386
- // Non-bootstrap failure: cleanup first remains compatible with prior path,
387
- // then forfeit. When bootstrap already recorded its failed fact, forfeit
388
- // must precede cleanup.
389
- cleanupFailures.push(...(await cleanupCreatedBindResources(input.cwd, resources)));
390
- try {
391
- await appendBindFailedForfeit(input.cwd, id, input.actor, at);
392
- }
393
- catch (forfeitError) {
394
- cleanupFailures.push(forfeitError instanceof Error ? forfeitError.message : String(forfeitError));
395
- }
396
- }
397
- else {
398
- try {
399
- await appendBindFailedForfeit(input.cwd, id, input.actor, at);
400
- }
401
- catch (forfeitError) {
402
- cleanupFailures.push(forfeitError instanceof Error ? forfeitError.message : String(forfeitError));
403
- }
404
- cleanupFailures.push(...(await cleanupCreatedBindResources(input.cwd, resources)));
405
- }
406
- if (registryRegistered) {
407
- try {
408
- await releaseScope(input.cwd, { contractId: id, actor: input.actor, at });
409
- }
410
- catch (releaseError) {
411
- cleanupFailures.push(releaseError instanceof Error ? releaseError.message : String(releaseError));
412
- }
413
- }
414
- if (error instanceof FlowError && error.code === "ACTIVE_KEIYAKU_EXISTS") {
415
- const message = [
416
- `bind rejected and forfeited as ${id}: ${error.message}`,
417
- cleanupFailures.length > 0 ? `cleanup failed: ${cleanupFailures.join("; ")}` : "",
418
- ]
419
- .filter(Boolean)
420
- .join("; ");
421
- throw new FlowError("ACTIVE_KEIYAKU_EXISTS", message, { cause: error });
369
+ targetHead: branch.head,
370
+ deliveryRef: repositoryDeliveryRef(entry.contract),
371
+ });
372
+ const bindWrite = await publishBind({
373
+ cwd: stableRoot,
374
+ entry,
375
+ expectedLedgerHead: read.observedLedgerHead,
376
+ delivery: { ref: repositoryDeliveryRef(entry.contract), head: entry.data.base },
377
+ });
378
+ if (bindWrite.family === "committed") {
379
+ committed = bindWrite;
380
+ break;
422
381
  }
423
- const message = [
424
- `bind failed for ${id}: ${error instanceof Error ? error.message : String(error)}`,
425
- cleanupFailures.length > 0 ? `cleanup failed: ${cleanupFailures.join("; ")}` : "",
426
- ]
427
- .filter(Boolean)
428
- .join("; ");
429
- throw new FlowError("INTERNAL_STATE", `BIND_WORKSPACE_FAILED: ${message}`, { cause: error });
382
+ if (bindWrite.family === "indeterminate")
383
+ committedOrThrow(bindWrite, "BIND_TRANSACTION");
384
+ }
385
+ if (!committed) {
386
+ throw new FlowError("CONCURRENT_MODIFICATION", "BIND_TRANSACTION_REFUSED: repository authority changed during four bind attempts");
430
387
  }
431
388
  }
432
- throw new FlowError("CONCURRENT_MODIFICATION", `CONTRACT_ID_COLLISION: contract id collision after two attempts: ${collidedIds.join(", ")}`);
389
+ return await reconcileCommittedBind({
390
+ cwd: stableRoot,
391
+ contractId: entry.contract,
392
+ actor: input.actor,
393
+ nowMs: input.nowMs,
394
+ ...(input.signal ? { signal: input.signal } : {}),
395
+ ...(input.createWorktree ? { createWorktree: input.createWorktree } : {}),
396
+ ...(input.onProgress ? { onProgress: input.onProgress } : {}),
397
+ ...(input.bootstrapRunner ? { bootstrapRunner: input.bootstrapRunner } : {}),
398
+ ...(input.bootstrapDependencies ? { bootstrapDependencies: input.bootstrapDependencies } : {}),
399
+ ...(preparedTask ? { preparedTask } : {}),
400
+ dependencies: input.dependencies,
401
+ }, admittedScopeOverlaps);
433
402
  }
@@ -83,6 +83,7 @@ export async function runCall(input) {
83
83
  mode: input.mode ?? "call",
84
84
  repoCwd,
85
85
  reviveBinding: reviveArtifact?.provenance.binding,
86
+ signal: input.signal,
86
87
  });
87
88
  return await withResolvedAddress(keiyakuContext.address, async () => {
88
89
  const executionCwd = keiyakuContext.effectiveCwd;