@astrosheep/keiyaku 1.0.0 → 1.0.2

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 (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -1,12 +1,12 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { FlowError } from "../flow-error.js";
4
- import { createGit, wrapGitError } from "../git/core.js";
4
+ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
5
5
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
6
- import { dequeueContract, queuePositionFromEntries, queuedContracts, readQueue } from "./queue.js";
6
+ import { assertClaimQueueHead, readQueueReadModel } from "./queue_v2.js";
7
7
  import { releaseScope } from "./registry.js";
8
8
  import { shortCommitHash } from "./render.js";
9
- import { deriveContractState, latestBase, resolveDefaultBranch } from "./status.js";
9
+ import { commissionTarget, deriveContractState, latestBase } from "./status.js";
10
10
  import { cleanupContractWorkspace, findContractWorktreePath, stableRepoRoot } from "./worktree-path.js";
11
11
  const CLAIM_RECEIPTS_FILE = "claim-receipts.jsonl";
12
12
  async function appendClaimReceipt(cwd, receipt) {
@@ -44,6 +44,17 @@ async function resolveCommit(cwd, rev) {
44
44
  throw wrapGitError(`rev-parse ${rev}`, error, cwd);
45
45
  }
46
46
  }
47
+ async function resolveCommitOrNull(cwd, rev) {
48
+ const git = createGit(cwd);
49
+ try {
50
+ return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
51
+ }
52
+ catch (error) {
53
+ if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
54
+ return null;
55
+ throw wrapGitError(`rev-parse ${rev}`, error, cwd);
56
+ }
57
+ }
47
58
  async function isAncestor(cwd, ancestor, descendant) {
48
59
  const git = createGit(cwd);
49
60
  try {
@@ -174,21 +185,30 @@ async function reconcileAfterCas(input) {
174
185
  }
175
186
  }
176
187
  async function defaultMergeDelivery(input) {
177
- const target = await resolveDefaultBranch(input.cwd);
178
- if (!target)
179
- throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: no default branch`);
188
+ const ledger = await readLedger(input.cwd, input.contractId);
189
+ if (!ledger) {
190
+ throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: ledger missing`);
191
+ }
192
+ const commission = commissionTarget(ledger.entries);
193
+ if (!commission) {
194
+ throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: missing commission target`);
195
+ }
196
+ const targetHead = await resolveCommitOrNull(input.cwd, commission.ref);
197
+ if (!targetHead) {
198
+ throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: commission target ${commission.ref} does not resolve`);
199
+ }
180
200
  const branch = `keiyaku/${input.contractId}`;
181
201
  const deliveryHead = await resolveCommit(input.cwd, branch);
182
- if (deliveryHead !== target.head && await isAncestor(input.cwd, deliveryHead, target.head)) {
183
- return { mergeCommit: target.head };
202
+ if (deliveryHead !== targetHead && await isAncestor(input.cwd, deliveryHead, targetHead)) {
203
+ return { mergeCommit: targetHead };
184
204
  }
185
205
  const git = createGit(input.cwd);
186
206
  let tree;
187
207
  try {
188
- tree = (await git.raw(["merge-tree", "--write-tree", target.head, deliveryHead])).trim();
208
+ tree = (await git.raw(["merge-tree", "--write-tree", targetHead, deliveryHead])).trim();
189
209
  }
190
210
  catch (error) {
191
- throw new FlowError("CLOSE_MERGE_CONFLICT", `claim merge conflict for ${input.contractId} into ${target.branch}`, { cause: error });
211
+ throw new FlowError("CLOSE_MERGE_CONFLICT", `claim merge conflict for ${input.contractId} into ${commission.branch}`, { cause: error });
192
212
  }
193
213
  let mergeCommit;
194
214
  try {
@@ -196,13 +216,13 @@ async function defaultMergeDelivery(input) {
196
216
  "commit-tree",
197
217
  tree,
198
218
  "-p",
199
- target.head,
219
+ targetHead,
200
220
  "-p",
201
221
  deliveryHead,
202
222
  "-m",
203
223
  `keiyaku: claim ${input.contractId}`,
204
224
  "-m",
205
- `Merge contract ${input.contractId} into ${target.branch}.`,
225
+ `Merge contract ${input.contractId} into ${commission.branch}.`,
206
226
  ])).trim();
207
227
  }
208
228
  catch (error) {
@@ -211,26 +231,26 @@ async function defaultMergeDelivery(input) {
211
231
  // Pre-CAS: locate target worktree and snapshot real blockers while HEAD/index still share oldHead.
212
232
  const preCasSnapshot = await snapshotPreCasBlockers({
213
233
  cwd: input.cwd,
214
- targetBranch: target.branch,
215
- oldHead: target.head,
234
+ targetBranch: commission.branch,
235
+ oldHead: targetHead,
216
236
  mergeCommit,
217
237
  });
218
238
  try {
219
- await git.raw(["update-ref", `refs/heads/${target.branch}`, mergeCommit, target.head]);
239
+ await git.raw(["update-ref", commission.ref, mergeCommit, targetHead]);
220
240
  }
221
241
  catch (error) {
222
- const currentHead = await resolveCommit(input.cwd, target.branch);
242
+ const currentHead = await resolveCommit(input.cwd, commission.ref);
223
243
  if (await isAncestor(input.cwd, deliveryHead, currentHead)) {
224
244
  return { mergeCommit: currentHead };
225
245
  }
226
- throw new FlowError("CONCURRENT_MODIFICATION", `target branch ${target.branch} moved while claiming ${input.contractId}`, { cause: error });
246
+ throw new FlowError("CONCURRENT_MODIFICATION", `target branch ${commission.branch} moved while claiming ${input.contractId}`, { cause: error });
227
247
  }
228
248
  // The target ref is authoritative. Receipt failure cannot undo its CAS.
229
249
  const receiptWarnings = [];
230
250
  try {
231
251
  await (input.writeClaimReceipt ?? appendClaimReceipt)(input.cwd, {
232
252
  kind: "receipt",
233
- oldHead: target.head,
253
+ oldHead: targetHead,
234
254
  newHead: mergeCommit,
235
255
  contract: input.contractId,
236
256
  });
@@ -240,8 +260,8 @@ async function defaultMergeDelivery(input) {
240
260
  }
241
261
  const warnings = await reconcileAfterCas({
242
262
  contractId: input.contractId,
243
- targetBranch: target.branch,
244
- oldHead: target.head,
263
+ targetBranch: commission.branch,
264
+ oldHead: targetHead,
245
265
  mergeCommit,
246
266
  snapshot: preCasSnapshot,
247
267
  });
@@ -260,33 +280,29 @@ function assertClaimableFromLedger(contractId, ledger) {
260
280
  if (claim && state.state === "claimed")
261
281
  return claim;
262
282
  if (state.state === "forfeited") {
263
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} forfeited; run forfeit to clear queue`);
283
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} forfeited`);
264
284
  }
265
285
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot claim from state ${state.state}`);
266
286
  }
267
- async function assertQueueHead(cwd, contractId) {
268
- const entries = await readQueue(cwd);
269
- const position = queuePositionFromEntries(entries, contractId);
270
- if (position !== 1) {
271
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} is not queue head; position=${position ?? "not queued"}`);
272
- }
273
- }
274
287
  export async function previewClaimContract(input) {
275
- const queueEntries = await readQueue(input.cwd);
276
- const queued = queuedContracts(queueEntries);
277
- const position = queuePositionFromEntries(queueEntries, input.contractId);
278
- if (position !== 1) {
279
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} is not queue head; position=${position ?? "not queued"}`);
280
- }
288
+ const model = await readQueueReadModel(input.cwd);
289
+ const membership = assertClaimQueueHead(model, input.contractId);
281
290
  const ledger = await readLedger(input.cwd, input.contractId);
282
291
  assertClaimableFromLedger(input.contractId, ledger);
283
292
  const entries = ledger.entries;
284
293
  const cleanupPath = await findContractWorktreePath(input.cwd, input.contractId);
285
294
  const state = deriveContractState(entries);
286
295
  const base = latestBase(entries);
287
- const target = await resolveDefaultBranch(input.cwd);
288
- const freshness = base && target
289
- ? base === target.head ? "fresh" : "stale"
296
+ const commission = commissionTarget(entries);
297
+ if (!commission) {
298
+ throw new FlowError("INTERNAL_STATE", `cannot preview claim ${input.contractId}: missing commission target`);
299
+ }
300
+ const targetHead = await resolveCommitOrNull(input.cwd, commission.ref);
301
+ if (!targetHead) {
302
+ throw new FlowError("INTERNAL_STATE", `cannot preview claim ${input.contractId}: commission target ${commission.ref} does not resolve`);
303
+ }
304
+ const freshness = base
305
+ ? base === targetHead ? "fresh" : "stale"
290
306
  : "unknown";
291
307
  const deliveryBranch = `keiyaku/${input.contractId}`;
292
308
  let deliveryHead;
@@ -298,16 +314,16 @@ export async function previewClaimContract(input) {
298
314
  }
299
315
  return {
300
316
  contractId: input.contractId,
301
- queueHead: queued[0],
302
- queuePosition: position,
317
+ queueHead: membership.queueHead,
318
+ queuePosition: membership.position,
303
319
  state: state.state,
304
320
  freshness,
305
321
  ...(base ? { base } : {}),
306
- ...(target ? { targetBranch: target.branch, targetHead: target.head } : {}),
322
+ targetBranch: commission.branch,
323
+ targetHead,
307
324
  deliveryBranch,
308
325
  ...(deliveryHead ? { deliveryHead } : {}),
309
326
  cleanupEffects: [
310
- `remove ${input.contractId} from queue`,
311
327
  `release registry scope for ${input.contractId}`,
312
328
  cleanupPath ? `remove worktree ${path.relative(input.cwd, cleanupPath) || "."} when present` : "no linked worktree to remove",
313
329
  `delete branch keiyaku/${input.contractId} when present`,
@@ -316,9 +332,21 @@ export async function previewClaimContract(input) {
316
332
  }
317
333
  export async function claimContract(input) {
318
334
  const at = new Date(input.nowMs).toISOString();
319
- await assertQueueHead(input.cwd, input.contractId);
320
335
  const existing = await readLedger(input.cwd, input.contractId);
321
336
  const existingClaim = assertClaimableFromLedger(input.contractId, existing);
337
+ const existingState = existing ? deriveContractState(existing.entries) : undefined;
338
+ // Already-claimed retry: skip queue-head gating and merge; finish cleanup.
339
+ if (existingClaim && existingState?.state === "claimed") {
340
+ const cleanup = await cleanupClaim(input.cwd, input.contractId, input.actor, at);
341
+ return {
342
+ contractId: input.contractId,
343
+ mergeCommit: existingClaim.data.mergeCommit,
344
+ cleanup,
345
+ };
346
+ }
347
+ // Still petitioned: prove target-local head before merge.
348
+ const model = await readQueueReadModel(input.cwd);
349
+ assertClaimQueueHead(model, input.contractId);
322
350
  let mergeCommit = existingClaim?.data.mergeCommit;
323
351
  let warnings;
324
352
  if (!mergeCommit) {
@@ -349,11 +377,11 @@ export async function claimContract(input) {
349
377
  throw new FlowError("INTERNAL_STATE", `failed to append claim for ${input.contractId}: ${result.reason}`);
350
378
  }
351
379
  }
352
- await dequeueContract(input.cwd, { contractId: input.contractId, actor: input.actor, at });
380
+ // Claim entry naturally removes membership; no dequeue record.
353
381
  const cleanup = await cleanupClaim(input.cwd, input.contractId, input.actor, at);
354
382
  return {
355
383
  contractId: input.contractId,
356
- mergeCommit,
384
+ mergeCommit: mergeCommit,
357
385
  cleanup,
358
386
  ...(warnings && warnings.length > 0 ? { warnings } : {}),
359
387
  };
@@ -3,6 +3,7 @@ import { FlowError } from "../flow-error.js";
3
3
  import { parseMarkdownListSection, parseMarkdownStructure } from "./markdown/sections.js";
4
4
  import { KeiyakuParseError } from "./markdown/types.js";
5
5
  import { normalizeSectionTitle } from "./markdown/titles.js";
6
+ import { isBlankScopePatternLine, normalizeScopePattern, ScopePatternError } from "./scope.js";
6
7
  function amendScopeHeading(name) {
7
8
  return `## Scope ${name[0].toUpperCase()}${name.slice(1)}`;
8
9
  }
@@ -29,8 +30,6 @@ function amendHeading(line) {
29
30
  return { level };
30
31
  if (title === "scope add")
31
32
  return { level, name: "add" };
32
- if (title === "scope remove")
33
- return { level, name: "remove" };
34
33
  return { level };
35
34
  }
36
35
  function amendFence(line) {
@@ -97,28 +96,36 @@ function scopePatterns(markdown, section) {
97
96
  const patterns = markdown
98
97
  .slice(section.contentStart, section.end)
99
98
  .split(/\r?\n/)
100
- .map((line) => line.trim())
101
- .filter(Boolean);
99
+ .filter((line) => !isBlankScopePatternLine(line))
100
+ .map((pattern) => {
101
+ try {
102
+ // Do not trim() here: gitignore treats trailing `\ ` as a significant space.
103
+ return normalizeScopePattern(pattern);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof ScopePatternError) {
107
+ throw new FlowError("INVALID_KEIYAKU_DRAFT", error.message);
108
+ }
109
+ throw error;
110
+ }
111
+ });
102
112
  if (patterns.length === 0) {
103
113
  throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend input has an empty ${amendScopeHeading(section.name)} section`);
104
114
  }
105
- return [...new Set(patterns)];
115
+ return patterns;
106
116
  }
107
- export function buildAmendInput(markdown, cwd) {
117
+ export function buildAmendInput(markdown, cwd, contractId, contractAddressSource) {
108
118
  const { sections, prose } = parseAmendScopeSections(markdown);
109
119
  const add = sections.add ? scopePatterns(markdown, sections.add) : [];
110
- const remove = sections.remove ? scopePatterns(markdown, sections.remove) : [];
111
- const conflict = add.find((pattern) => remove.includes(pattern));
112
- if (conflict) {
113
- throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend scope pattern appears in both ${amendScopeHeading("add")} and ${amendScopeHeading("remove")}: ${conflict}`);
114
- }
115
120
  if (!prose.trim()) {
116
121
  throw new FlowError("EMPTY_PARAM", "amendment cannot be empty");
117
122
  }
118
123
  return {
119
124
  cwd,
125
+ ...(contractId ? { contractId } : {}),
126
+ ...(contractAddressSource ? { contractAddressSource } : {}),
120
127
  amendment: prose,
121
- ...(sections.add || sections.remove ? { scopeDelta: { add, remove } } : {}),
128
+ ...(sections.add ? { scopeDelta: { add } } : {}),
122
129
  };
123
130
  }
124
131
  export function defaultActorClock(nowMs = Date.now()) {
@@ -165,10 +172,3 @@ export function requireMarkdownTitle(parts) {
165
172
  }
166
173
  return title;
167
174
  }
168
- export function assertKnownMarkdownSections(parts, command, allowed) {
169
- const allowedSet = new Set(allowed.map(normalizeSectionTitle));
170
- const unknown = [...parts.sections.keys()].filter((section) => !allowedSet.has(section));
171
- if (unknown.length > 0) {
172
- throw new FlowError("INVALID_KEIYAKU_DRAFT", `${command} input has unknown section header(s): ${unknown.map((section) => `'${section}'`).join(", ")}`);
173
- }
174
- }
@@ -4,16 +4,20 @@ import { FlowError } from "../flow-error.js";
4
4
  import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
5
5
  import { getCurrentBranch, listLocalBranches } from "../git/branches.js";
6
6
  import { readLedger } from "./ledger.js";
7
- import { deriveContractState, isTerminalState, latestBase, listContractIds, resolveDefaultBranch } from "./status.js";
7
+ import { listContractIds } from "./ledger.js";
8
+ import { deriveContractState, isTerminalState, latestBase } from "./status.js";
8
9
  import { activeContractIdForWorktreeDir } from "./worktree-path.js";
9
- async function operableContractIds(cwd) {
10
+ function isOperableLedger(entries) {
11
+ const state = deriveContractState(entries);
12
+ return state.state !== "corrupt" && !isTerminalState(state);
13
+ }
14
+ export async function operableContractIds(cwd) {
10
15
  const ids = await listContractIds(cwd);
11
16
  const ledgers = await Promise.all(ids.map(async (id) => ({ id, ledger: await readLedger(cwd, id) })));
12
17
  return ledgers.flatMap(({ id, ledger }) => {
13
18
  if (!ledger)
14
19
  return [];
15
- const state = deriveContractState(ledger.entries);
16
- return state.state !== "corrupt" && !isTerminalState(state) ? [id] : [];
20
+ return isOperableLedger(ledger.entries) ? [id] : [];
17
21
  });
18
22
  }
19
23
  async function isOperableContract(cwd, contractId) {
@@ -64,11 +68,12 @@ export async function readCurrentContractContext(cwd) {
64
68
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found or not active`);
65
69
  }
66
70
  const base = latestBase(ledger.entries);
67
- if (!base) {
71
+ const bind = ledger.entries.find((entry) => entry.kind === "bind");
72
+ if (!base || !bind) {
68
73
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} active ledger is missing bind entry`);
69
74
  }
70
75
  const currentBranch = await getCurrentBranch(cwd);
71
- const baseBranch = (await resolveDefaultBranch(cwd))?.branch ?? currentBranch;
76
+ const baseBranch = bind.data.target.slice("refs/heads/".length);
72
77
  return {
73
78
  contractId,
74
79
  currentBranch,
@@ -76,33 +81,3 @@ export async function readCurrentContractContext(cwd) {
76
81
  base,
77
82
  };
78
83
  }
79
- function normalizeAddressQuery(raw) {
80
- const value = raw.startsWith("@") ? raw.slice(1) : raw;
81
- if (!value.trim()) {
82
- throw new FlowError("EMPTY_PARAM", "contract address cannot be empty");
83
- }
84
- return value.trim();
85
- }
86
- export async function resolveContractAddress(cwd, raw) {
87
- if (!raw) {
88
- const current = await resolveImplicitCurrentContractId(cwd);
89
- if (current)
90
- return current;
91
- const operable = await operableContractIds(cwd);
92
- if (operable.length === 1)
93
- return operable[0];
94
- throw new FlowError("EMPTY_PARAM", operable.length === 0
95
- ? "no active contract found; pass @<id>"
96
- : "multiple active contracts found; pass @<id>");
97
- }
98
- const query = normalizeAddressQuery(raw);
99
- const ids = await listContractIds(cwd);
100
- if (ids.includes(query))
101
- return query;
102
- const matches = ids.filter((id) => id.startsWith(query));
103
- if (matches.length === 1)
104
- return matches[0];
105
- if (matches.length === 0)
106
- throw new FlowError("EMPTY_PARAM", `unknown contract address @${query}`);
107
- throw new FlowError("EMPTY_PARAM", `ambiguous contract address @${query}: ${matches.join(", ")}`);
108
- }
@@ -41,9 +41,8 @@ export function parseBindDraft(content) {
41
41
  if (node.level !== 2)
42
42
  continue;
43
43
  const normalized = normalizeSectionName(node.title);
44
- if (!KNOWN_SECTIONS.has(normalized)) {
45
- invalidBindDraft(`unknown section '${node.title}' in bind draft`);
46
- }
44
+ if (!KNOWN_SECTIONS.has(normalized))
45
+ continue;
47
46
  if (sections.has(normalized)) {
48
47
  invalidBindDraft(`duplicate section '${node.title}' in bind draft`);
49
48
  }
@@ -2,12 +2,17 @@ import { z } from "zod";
2
2
  const SHA_RE = /^[0-9a-f]{7,40}$/i;
3
3
  const ISO_UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
4
4
  const CONTRACT_ID_RE = /^\S+$/;
5
- const SUBJECT_RE = /^keiyaku: (bind|arc|seal|amend|renew|petition|verdict|claim|forfeit) ([^\s]+)$/;
5
+ const SUBJECT_RE = /^keiyaku: (bind|arc|seal|amend|renew|petition|verdict|claim|forfeit|bootstrap) ([^\s]+)$/;
6
6
  const shaSchema = z.string().regex(SHA_RE, "expected git sha");
7
7
  const isoUtcSchema = z.string().regex(ISO_UTC_RE, "expected ISO 8601 UTC timestamp");
8
8
  const nonEmptyString = z.string().trim().min(1);
9
9
  const nonBlankString = z.string().refine((value) => value.trim().length > 0, "expected non-empty string");
10
- const placeSchema = z.string().regex(/^[a-z]{3,12}$/);
10
+ // Scope patterns are gitignore text: do not trim, or escaped trailing spaces (`foo\ `) are destroyed.
11
+ const scopePatternString = z
12
+ .string()
13
+ .refine((value) => value.trim().length > 0, "expected non-empty scope pattern");
14
+ // Catalog base (2-13 lowercase letters) or generation overflow base2, base3, ...
15
+ const placeSchema = z.string().regex(/^[a-z]{2,13}(?:[2-9]|[1-9][0-9]+)?$/);
11
16
  export const entryKindSchema = z.enum([
12
17
  "bind",
13
18
  "arc",
@@ -18,6 +23,7 @@ export const entryKindSchema = z.enum([
18
23
  "verdict",
19
24
  "claim",
20
25
  "forfeit",
26
+ "bootstrap",
21
27
  ]);
22
28
  const diffstatSchema = z
23
29
  .object({
@@ -32,15 +38,67 @@ const scopeAuditSchema = z
32
38
  waived: z.boolean(),
33
39
  })
34
40
  .strict();
41
+ export const fullBranchRefSchema = z
42
+ .string()
43
+ .regex(/^refs\/heads\/\S+$/, "expected full branch ref");
44
+ export const bindBootstrapPlanSchema = z
45
+ .object({
46
+ command: z.array(nonEmptyString).min(1),
47
+ timeoutMs: z.number().int().min(1).max(3_600_000),
48
+ })
49
+ .strict();
35
50
  export const bindEntryDataSchema = z
36
51
  .object({
37
52
  name: nonEmptyString,
38
53
  objective: nonEmptyString,
39
- scope: z.array(nonEmptyString).min(1),
54
+ scope: z.array(scopePatternString).min(1),
40
55
  base: shaSchema,
56
+ target: fullBranchRefSchema,
41
57
  workspace: z.enum(["worktree", "here"]),
42
58
  after: z.array(nonEmptyString).optional(),
43
59
  place: placeSchema.nullable().optional(),
60
+ bootstrap: bindBootstrapPlanSchema.optional(),
61
+ })
62
+ .strict();
63
+ const bootstrapExitResultSchema = z
64
+ .object({
65
+ kind: z.literal("exit"),
66
+ code: z.number().int().min(0).max(255),
67
+ })
68
+ .strict();
69
+ const bootstrapSignalResultSchema = z
70
+ .object({
71
+ kind: z.literal("signal"),
72
+ signal: z.string().min(1),
73
+ })
74
+ .strict();
75
+ const bootstrapTimeoutResultSchema = z
76
+ .object({
77
+ kind: z.literal("timeout"),
78
+ })
79
+ .strict();
80
+ const bootstrapSpawnResultSchema = z
81
+ .object({
82
+ kind: z.literal("spawn"),
83
+ message: z.string(),
84
+ })
85
+ .strict();
86
+ export const bootstrapResultSchema = z.discriminatedUnion("kind", [
87
+ bootstrapExitResultSchema,
88
+ bootstrapSignalResultSchema,
89
+ bootstrapTimeoutResultSchema,
90
+ bootstrapSpawnResultSchema,
91
+ ]);
92
+ export const bootstrapEntryDataSchema = z
93
+ .object({
94
+ result: bootstrapResultSchema,
95
+ durationMs: z
96
+ .number()
97
+ .int()
98
+ .nonnegative()
99
+ .refine((value) => Number.isSafeInteger(value), "durationMs must be a safe integer"),
100
+ stderrTail: z.string().optional(),
101
+ stderrTruncated: z.boolean().optional(),
44
102
  })
45
103
  .strict();
46
104
  export const arcEntryDataSchema = z
@@ -67,8 +125,7 @@ export const amendEntryDataSchema = z
67
125
  amendment: nonBlankString,
68
126
  scopeDelta: z
69
127
  .object({
70
- add: z.array(nonEmptyString),
71
- remove: z.array(nonEmptyString),
128
+ add: z.array(scopePatternString),
72
129
  })
73
130
  .strict()
74
131
  .optional(),
@@ -82,7 +139,29 @@ export const renewEntryDataSchema = z
82
139
  newHead: shaSchema,
83
140
  })
84
141
  .strict();
85
- export const petitionEntryDataSchema = z.discriminatedUnion("intent", [
142
+ const positiveSafeSeatSchema = z
143
+ .number()
144
+ .int()
145
+ .positive()
146
+ .refine((value) => Number.isSafeInteger(value), "seat must be a positive safe integer");
147
+ // Current petitions always carry target + seat. Legacy residuals omit both and
148
+ // exist only for diagnostic/rejection paths — never for production writers.
149
+ export const petitionEntryDataSchema = z.union([
150
+ z
151
+ .object({
152
+ intent: z.literal("claim"),
153
+ oath: nonEmptyString,
154
+ target: fullBranchRefSchema,
155
+ seat: positiveSafeSeatSchema,
156
+ })
157
+ .strict(),
158
+ z
159
+ .object({
160
+ intent: z.literal("forfeit"),
161
+ target: fullBranchRefSchema,
162
+ seat: positiveSafeSeatSchema,
163
+ })
164
+ .strict(),
86
165
  z
87
166
  .object({
88
167
  intent: z.literal("claim"),
@@ -134,6 +213,7 @@ export const petitionEntrySchema = makeEntrySchema("petition", petitionEntryData
134
213
  export const verdictEntrySchema = makeEntrySchema("verdict", verdictEntryDataSchema);
135
214
  export const claimEntrySchema = makeEntrySchema("claim", claimEntryDataSchema);
136
215
  export const forfeitEntrySchema = makeEntrySchema("forfeit", forfeitEntryDataSchema);
216
+ export const bootstrapEntrySchema = makeEntrySchema("bootstrap", bootstrapEntryDataSchema);
137
217
  export const ledgerEntrySchema = z.discriminatedUnion("kind", [
138
218
  bindEntrySchema,
139
219
  arcEntrySchema,
@@ -144,6 +224,7 @@ export const ledgerEntrySchema = z.discriminatedUnion("kind", [
144
224
  verdictEntrySchema,
145
225
  claimEntrySchema,
146
226
  forfeitEntrySchema,
227
+ bootstrapEntrySchema,
147
228
  ]);
148
229
  export class LedgerEntryFormatError extends Error {
149
230
  code = "INVALID_LEDGER_ENTRY_FORMAT";
@@ -190,6 +271,20 @@ export function parseLedgerEntryCommitMessage(message) {
190
271
  catch (error) {
191
272
  throw new LedgerEntryFormatError("ledger entry JSON body is not valid JSON", { cause: error });
192
273
  }
274
+ if (typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "repoId")) {
275
+ const { repoId: _legacyRepoId, ...current } = parsed;
276
+ parsed = current;
277
+ }
278
+ if (typeof parsed === "object" && parsed !== null &&
279
+ parsed.kind === "amend") {
280
+ const data = parsed.data;
281
+ const scopeDelta = typeof data === "object" && data !== null
282
+ ? data.scopeDelta
283
+ : undefined;
284
+ if (typeof scopeDelta === "object" && scopeDelta !== null && Object.hasOwn(scopeDelta, "remove")) {
285
+ throw new LedgerEntrySchemaError("ledger entry schema validation failed: scopeDelta.remove is obsolete; ledger row is corrupt");
286
+ }
287
+ }
193
288
  const result = ledgerEntrySchema.safeParse(parsed);
194
289
  if (!result.success) {
195
290
  throw new LedgerEntrySchemaError(`ledger entry schema validation failed: ${result.error.message}`, {
@@ -0,0 +1,62 @@
1
+ import { isCommissionId } from "./ids.js";
2
+ function isRecord(value) {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function assertExactFields(value, required, optional, prefix, invalid) {
6
+ for (const field of required) {
7
+ if (!(field in value))
8
+ throw invalid(prefix ? `${prefix}.${field}` : field, "is missing");
9
+ }
10
+ const allowed = new Set([...required, ...optional]);
11
+ const unknown = Object.keys(value).filter((field) => !allowed.has(field)).sort()[0];
12
+ if (unknown !== undefined)
13
+ throw invalid(prefix ? `${prefix}.${unknown}` : unknown, "is unknown");
14
+ }
15
+ function requireNonBlank(value, field, invalid) {
16
+ if (typeof value !== "string" || value.trim() === "")
17
+ throw invalid(field, "must be a non-blank string");
18
+ }
19
+ export function parseExecutionAuthority(value, invalid) {
20
+ if (!isRecord(value))
21
+ throw invalid("authority", "must be an object");
22
+ if (!("kind" in value))
23
+ throw invalid("authority.kind", "is missing");
24
+ if (value.kind === "repository") {
25
+ // repoId is the sole legacy coordinate field accepted on read. It is inert
26
+ // and deliberately discarded; current writers emit the unit variant.
27
+ assertExactFields(value, ["kind"], ["repoId"], "authority", invalid);
28
+ return { kind: "repository" };
29
+ }
30
+ if (value.kind === "user") {
31
+ assertExactFields(value, ["kind"], [], "authority", invalid);
32
+ return { kind: "user" };
33
+ }
34
+ throw invalid("authority.kind", "must be 'repository' or 'user'");
35
+ }
36
+ export function parseExecutionBinding(value, invalid) {
37
+ if (!isRecord(value))
38
+ throw invalid("binding", "must be an object");
39
+ if (!("kind" in value))
40
+ throw invalid("binding.kind", "is missing");
41
+ if (value.kind === "commission") {
42
+ assertExactFields(value, ["kind", "commissionId"], [], "binding", invalid);
43
+ requireNonBlank(value.commissionId, "binding.commissionId", invalid);
44
+ if (!isCommissionId(value.commissionId))
45
+ throw invalid("binding.commissionId", "must be a full commission ID");
46
+ return { kind: "commission", commissionId: value.commissionId };
47
+ }
48
+ if (value.kind === "bare" || value.kind === "unbound") {
49
+ assertExactFields(value, ["kind"], [], "binding", invalid);
50
+ return { kind: value.kind };
51
+ }
52
+ throw invalid("binding.kind", "must be 'commission', 'bare', or 'unbound'");
53
+ }
54
+ /** The one durable authority/binding schema used by pacts and response artifacts. */
55
+ export function parseExecutionCoordinates(authorityValue, bindingValue, invalid) {
56
+ const authority = parseExecutionAuthority(authorityValue, invalid);
57
+ const binding = parseExecutionBinding(bindingValue, invalid);
58
+ if (authority.kind === "user" && binding.kind === "commission") {
59
+ throw invalid("binding.kind", "cannot be 'commission' when authority.kind is 'user'");
60
+ }
61
+ return { authority, binding };
62
+ }