@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
@@ -6,15 +6,10 @@ import { KEIYAKU_FILE } from "../keiyaku.js";
6
6
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
7
7
  import { renderContractView } from "./render.js";
8
8
  import { deriveContractState, isTerminalState } from "./status.js";
9
- import { resolveCurrentContractId } from "./context.js";
9
+ import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
10
10
  import { resolveDefaultBranch } from "./status.js";
11
11
  import { assertCleanWorkingTree } from "./arc.js";
12
- function requireText(name, value) {
13
- const normalized = value.trim();
14
- if (!normalized)
15
- throw new FlowError("EMPTY_PARAM", `${name} cannot be empty`);
16
- return normalized;
17
- }
12
+ import { normalizeScopePattern } from "./scope.js";
18
13
  function requireAmendment(value) {
19
14
  if (!value.trim())
20
15
  throw new FlowError("EMPTY_PARAM", "amendment cannot be empty");
@@ -39,15 +34,11 @@ async function renderUpdatedContract(cwd, contractId) {
39
34
  function normalizeScopeDelta(scopeDelta) {
40
35
  if (!scopeDelta)
41
36
  return undefined;
42
- const add = [...new Set(scopeDelta.add.map((pattern) => requireText("scope add", pattern)))];
43
- const remove = [...new Set(scopeDelta.remove.map((pattern) => requireText("scope remove", pattern)))];
44
- const conflict = add.find((pattern) => remove.includes(pattern));
45
- if (conflict) {
46
- throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend scope pattern appears in both ## Scope Add and ## Scope Remove: ${conflict}`);
47
- }
48
- if (add.length === 0 && remove.length === 0)
37
+ // Scope patterns are gitignore text; do not requireText/trim, or escaped trailing spaces die.
38
+ const add = scopeDelta.add.map((pattern) => normalizeScopePattern(pattern));
39
+ if (add.length === 0)
49
40
  return undefined;
50
- return { add, remove };
41
+ return { add };
51
42
  }
52
43
  export async function amendContract(input) {
53
44
  const amendment = requireAmendment(input.amendment);
@@ -84,37 +75,29 @@ export async function amendKeiyaku(input) {
84
75
  if (!isRepo) {
85
76
  throw new FlowError("NOT_GIT_REPO", `${input.cwd} is not a git repository`);
86
77
  }
87
- let contractId;
88
- try {
89
- const resolution = await resolveCurrentContractId(input.cwd);
90
- if (resolution.status === "none")
91
- throw new FlowError("EMPTY_PARAM", "no active contract found");
92
- contractId = resolution.contractId;
93
- }
94
- catch (error) {
95
- if (!(error instanceof FlowError) || error.code !== "EMPTY_PARAM") {
96
- throw error;
97
- }
98
- throw new FlowError("NOT_ACTIVE_KEIYAKU_BRANCH", "Current workspace is not addressing an active Keiyaku contract.", { hints: ["keiyaku bind — opens a new contract"] });
99
- }
100
- await assertCleanWorkingTree(input.cwd, []);
101
- const amendment = requireAmendment(input.amendment);
102
- const currentBranch = await getCurrentBranch(input.cwd);
103
- const baseBranch = (await resolveDefaultBranch(input.cwd))?.branch ?? currentBranch;
104
- const amended = await amendContract({
105
- cwd: input.cwd,
106
- contractId,
107
- actor: "keiyaku",
108
- nowMs: Date.now(),
109
- amendment,
110
- scopeDelta: input.scopeDelta,
78
+ const address = await resolveContractAddress(input.cwd, input.contractId, input.contractAddressSource);
79
+ return await withResolvedAddress(address, async () => {
80
+ const contractId = address.binding.commissionId;
81
+ await assertCleanWorkingTree(input.cwd, []);
82
+ const amendment = requireAmendment(input.amendment);
83
+ const currentBranch = await getCurrentBranch(input.cwd);
84
+ const baseBranch = (await resolveDefaultBranch(input.cwd))?.branch ?? currentBranch;
85
+ const amended = await amendContract({
86
+ cwd: input.cwd,
87
+ contractId,
88
+ actor: "keiyaku",
89
+ nowMs: Date.now(),
90
+ amendment,
91
+ scopeDelta: input.scopeDelta,
92
+ });
93
+ return {
94
+ status: "success",
95
+ amendmentNumber: amended.amendmentNumber,
96
+ currentBranch,
97
+ baseBranch,
98
+ ledgerCommit: amended.ledgerHead,
99
+ summary: `Appended Amendment ${amended.amendmentNumber} to contract ledger ${contractId}.`,
100
+ address,
101
+ };
111
102
  });
112
- return {
113
- status: "success",
114
- amendmentNumber: amended.amendmentNumber,
115
- currentBranch,
116
- baseBranch,
117
- ledgerCommit: amended.ledgerHead,
118
- summary: `Appended Amendment ${amended.amendmentNumber} to contract ledger ${contractId}.`,
119
- };
120
103
  }
package/build/core/arc.js CHANGED
@@ -6,8 +6,8 @@ import { DIRTY_FILE_CATEGORY, getDirtyFiles, renderDirtyFileStatusLine } from ".
6
6
  import { KEIYAKU_FILE, ACTOR_IDENTITY } from "../keiyaku.js";
7
7
  import { buildDirtyWorktreeArtifactHint } from "./hints.js";
8
8
  import { normalizeInlineTitle } from "./markdown/titles.js";
9
- import { assertKnownMarkdownSections, parseCommandMarkdown, requireMarkdownTextSection, requireMarkdownTitle, } from "./command-io.js";
10
- import { resolveCurrentContractId } from "./context.js";
9
+ import { parseCommandMarkdown, requireMarkdownTextSection, requireMarkdownTitle, } from "./command-io.js";
10
+ import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
11
11
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
12
12
  import { renderContractView } from "./render.js";
13
13
  import { findOpenArc, getMaxArc, sealOpenArc } from "./seal.js";
@@ -117,6 +117,7 @@ export async function openArc(input) {
117
117
  contractId: input.contractId,
118
118
  sealedBy: "arc",
119
119
  nowMs: input.nowMs,
120
+ waivePaths: input.waivePaths,
120
121
  });
121
122
  const at = new Date(input.nowMs).toISOString();
122
123
  let arcNumber = 0;
@@ -141,14 +142,16 @@ export async function openArc(input) {
141
142
  await renderUpdatedContract(input.cwd, input.contractId);
142
143
  return { contractId: input.contractId, arc: arcNumber, ledgerHead: result.head, sealed };
143
144
  }
144
- export function buildArcInput(content, cwd) {
145
+ export function buildArcInput(content, cwd, waivePaths = [], contractId, contractAddressSource) {
145
146
  const parts = parseCommandMarkdown(content);
146
- assertKnownMarkdownSections(parts, "arc", ["Objective", "Brief"]);
147
147
  return {
148
148
  cwd,
149
+ ...(contractId ? { contractId } : {}),
150
+ ...(contractAddressSource ? { contractAddressSource } : {}),
149
151
  title: requireMarkdownTitle(parts),
150
152
  objective: requireMarkdownTextSection(parts, "Objective"),
151
153
  brief: requireMarkdownTextSection(parts, "Brief"),
154
+ waivePaths,
152
155
  };
153
156
  }
154
157
  async function readBranchLabels(cwd) {
@@ -160,66 +163,71 @@ export async function arcKeiyaku(input) {
160
163
  if (!(await isGitRepo(input.cwd))) {
161
164
  throw new FlowError("NOT_GIT_REPO", `${input.cwd} is not a git repository`);
162
165
  }
163
- await assertCleanWorkingTree(input.cwd, []);
164
- try {
165
- await fs.access(path.join(input.cwd, KEIYAKU_FILE));
166
- }
167
- catch {
168
- throw new FlowError("MISSING_PROTOCOL_FILES", `missing protocol file for current keiyaku (${KEIYAKU_FILE})`);
169
- }
170
- const title = normalizeInlineTitle(requireText("title", input.title));
171
- const objective = requireText("objective", input.objective);
172
- const brief = requireText("brief", input.brief);
173
- const resolution = await resolveCurrentContractId(input.cwd);
174
- if (resolution.status === "none")
175
- throw new FlowError("EMPTY_PARAM", "no active contract found");
176
- const contractId = resolution.contractId;
177
- const opened = await openArc({
178
- cwd: input.cwd,
179
- contractId,
180
- actor: ACTOR_IDENTITY,
181
- nowMs: Date.now(),
182
- title,
183
- objective,
184
- brief,
185
- });
186
- const { currentBranch, baseBranch } = await readBranchLabels(input.cwd);
187
- const summary = opened.sealed
188
- ? `a${opened.sealed.arc} sealed; a${opened.arc} open: ${title}.`
189
- : `a${opened.arc} open: ${title}.`;
190
- const diff = opened.sealed?.incrementalDiff ?? "working tree untouched";
191
- let responsePath;
192
- try {
193
- responsePath = await persistResponseHistory({
166
+ const address = await resolveContractAddress(input.cwd, input.contractId, input.contractAddressSource);
167
+ return await withResolvedAddress(address, async () => {
168
+ await assertCleanWorkingTree(input.cwd, []);
169
+ try {
170
+ await fs.access(path.join(input.cwd, KEIYAKU_FILE));
171
+ }
172
+ catch {
173
+ throw new FlowError("MISSING_PROTOCOL_FILES", `missing protocol file for current keiyaku (${KEIYAKU_FILE})`);
174
+ }
175
+ const title = normalizeInlineTitle(requireText("title", input.title));
176
+ const objective = requireText("objective", input.objective);
177
+ const brief = requireText("brief", input.brief);
178
+ const contractId = address.binding.commissionId;
179
+ const opened = await openArc({
194
180
  cwd: input.cwd,
195
- tool: "arc",
181
+ contractId,
182
+ actor: ACTOR_IDENTITY,
183
+ nowMs: Date.now(),
196
184
  title,
197
- branch: currentBranch,
185
+ objective,
186
+ brief,
187
+ waivePaths: input.waivePaths,
188
+ });
189
+ const { currentBranch, baseBranch } = await readBranchLabels(input.cwd);
190
+ const summary = opened.sealed
191
+ ? `a${opened.sealed.arc} sealed; a${opened.arc} open: ${title}.`
192
+ : `a${opened.arc} open: ${title}.`;
193
+ const diff = opened.sealed?.incrementalDiff ?? "working tree untouched";
194
+ let responsePath;
195
+ let artifactId;
196
+ try {
197
+ const persisted = await persistResponseHistory({
198
+ cwd: input.cwd,
199
+ tool: "arc",
200
+ provenance: { authority: { kind: "repository" }, binding: address.binding, evidenceCwd: input.cwd },
201
+ title,
202
+ objective,
203
+ brief,
204
+ ledgerCommit: opened.ledgerHead,
205
+ summary,
206
+ diff,
207
+ ...(opened.sealed ? { sealed: opened.sealed } : {}),
208
+ });
209
+ responsePath = persisted.responsePath;
210
+ artifactId = persisted.artifactId;
211
+ }
212
+ catch {
213
+ responsePath = undefined;
214
+ }
215
+ return {
216
+ status: "success",
217
+ arc: opened.arc,
218
+ currentBranch,
219
+ baseBranch,
198
220
  commit: opened.ledgerHead,
221
+ diff,
222
+ summary,
223
+ title,
199
224
  objective,
200
225
  brief,
201
226
  ledgerCommit: opened.ledgerHead,
202
- summary,
203
- diff,
204
- ...(opened.sealed ? { sealed: opened.sealed } : {}),
205
- });
206
- }
207
- catch {
208
- responsePath = undefined;
209
- }
210
- return {
211
- status: "success",
212
- arc: opened.arc,
213
- currentBranch,
214
- baseBranch,
215
- commit: opened.ledgerHead,
216
- diff,
217
- summary,
218
- title,
219
- objective,
220
- brief,
221
- ledgerCommit: opened.ledgerHead,
222
- sealed: opened.sealed ?? undefined,
223
- responsePath,
224
- };
227
+ sealed: opened.sealed ?? undefined,
228
+ responsePath,
229
+ artifactId,
230
+ address,
231
+ };
232
+ });
225
233
  }
@@ -1,17 +1,19 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import { assertSettingsKnobUsable, loadKeiyakuSettings } from "../config/settings.js";
4
5
  import { FlowError } from "../flow-error.js";
5
6
  import { KEIYAKU_FILE } from "../keiyaku.js";
6
7
  import { createGit, wrapGitError } from "../git/core.js";
7
- import { activeLedgerRef, appendEntry, appendWithRetry, readLedger } from "./ledger.js";
8
+ import { activeLedgerRef, appendEntry, appendWithRetry, listContractIds, readLedger } from "./ledger.js";
8
9
  import { parseBindDraft } from "./draft.js";
9
10
  import { assertValidContractId, buildContractId } from "./ids.js";
10
11
  import { CONTRACT_PLACES } from "./places.js";
11
12
  import { renderContractView } from "./render.js";
12
13
  import { registerScope, releaseScope } from "./registry.js";
13
- import { deriveContractState, isTerminalState, listContractIds } from "./status.js";
14
- import { contractWorktreePathFromBind, findBindEntry, stableRepoRoot } from "./worktree-path.js";
14
+ import { deriveContractState, isTerminalState, normalizeTargetRef, resolveDefaultBranch } from "./status.js";
15
+ import { bootstrapEntryDataFromObservation, isBootstrapSuccess, runWorktreeBootstrap, } from "./worktree-bootstrap.js";
16
+ import { contractWorktreePathFromBind, findBindEntry } from "./worktree-path.js";
15
17
  function randomBytesDefault() {
16
18
  return [...crypto.randomBytes(10)];
17
19
  }
@@ -112,25 +114,50 @@ async function appendBindFailedForfeit(cwd, id, actor, at) {
112
114
  throw new FlowError("INTERNAL_STATE", `BIND_FAILURE_FORFEIT_FAILED: failed to append bind-failed forfeit for ${id}: ${result.reason}`);
113
115
  }
114
116
  }
115
- async function occupiedWorktreePlaces(cwd) {
116
- const occupied = new Set();
117
- const stableRoot = await stableRepoRoot(cwd);
118
- try {
119
- const dirents = await fs.readdir(path.join(stableRoot, ".keiyaku", "wt"), { withFileTypes: true });
120
- for (const dirent of dirents) {
121
- if (dirent.isDirectory())
122
- occupied.add(dirent.name);
117
+ async function appendBootstrapEntry(cwd, id, actor, at, data) {
118
+ const entry = {
119
+ v: 1,
120
+ kind: "bootstrap",
121
+ contract: id,
122
+ at,
123
+ actor,
124
+ data,
125
+ };
126
+ // Bootstrap is legal only at index 1 immediately after the planned bind.
127
+ // If another entry advanced during bootstrap execution, abort rather than
128
+ // CAS-retrying into a late (corrupt) position.
129
+ const result = await appendWithRetry(cwd, activeLedgerRef(id), (_currentHead, currentEntries) => {
130
+ if (currentEntries.length !== 1)
131
+ return null;
132
+ const only = currentEntries[0];
133
+ if (!only || only.kind !== "bind" || only.contract !== id || !only.data.bootstrap) {
134
+ return null;
123
135
  }
136
+ return entry;
137
+ });
138
+ if (!result.ok) {
139
+ throw new FlowError("INTERNAL_STATE", `BIND_BOOTSTRAP_APPEND_FAILED: failed to append bootstrap entry for ${id}: ${result.reason}`);
124
140
  }
125
- catch (error) {
126
- if (error.code !== "ENOENT")
127
- throw error;
128
- }
141
+ return result.head;
142
+ }
143
+ function resolvedBootstrapPlan(settings) {
144
+ if (!settings)
145
+ return undefined;
146
+ return {
147
+ command: [...settings.command],
148
+ timeoutMs: settings.timeoutMs,
149
+ };
150
+ }
151
+ function bootstrapProgressLine(command) {
152
+ return `bootstrap » ${command.join(" ")}`;
153
+ }
154
+ async function occupiedWorktreePlaces(cwd) {
155
+ const occupied = new Set();
129
156
  for (const id of await listContractIds(cwd)) {
130
157
  const ledger = await readLedger(cwd, id);
131
158
  if (!ledger || isTerminalState(deriveContractState(ledger.entries)))
132
159
  continue;
133
- const place = ledger ? findBindEntry(ledger.entries)?.data.place : undefined;
160
+ const place = findBindEntry(ledger.entries)?.data.place;
134
161
  if (place)
135
162
  occupied.add(place);
136
163
  }
@@ -143,12 +170,21 @@ function randomIndex(randomBytes, size) {
143
170
  }
144
171
  return value;
145
172
  }
173
+ function nextGenerationPlace(base, occupied) {
174
+ for (let generation = 2;; generation += 1) {
175
+ const candidate = `${base}${generation}`;
176
+ if (!occupied.has(candidate))
177
+ return candidate;
178
+ }
179
+ }
146
180
  async function allocateWorktreePlace(cwd, randomBytes) {
147
181
  const occupied = await occupiedWorktreePlaces(cwd);
148
182
  const candidates = CONTRACT_PLACES.filter((place) => !occupied.has(place));
149
- if (candidates.length === 0)
150
- return null;
151
- return candidates[randomIndex(randomBytes, candidates.length)];
183
+ if (candidates.length > 0) {
184
+ return candidates[randomIndex(randomBytes, candidates.length)];
185
+ }
186
+ const base = CONTRACT_PLACES[randomIndex(randomBytes, CONTRACT_PLACES.length)];
187
+ return nextGenerationPlace(base, occupied);
152
188
  }
153
189
  function makeBindEntry(input) {
154
190
  return {
@@ -162,9 +198,11 @@ function makeBindEntry(input) {
162
198
  objective: input.draft.objective,
163
199
  scope: input.draft.scope,
164
200
  base: input.base,
201
+ target: input.target,
165
202
  workspace: input.workspace,
166
203
  after: input.after,
167
204
  ...(input.workspace === "worktree" ? { place: input.place ?? null } : {}),
205
+ ...(input.bootstrap ? { bootstrap: input.bootstrap } : {}),
168
206
  },
169
207
  };
170
208
  }
@@ -173,6 +211,17 @@ export async function bindContract(input) {
173
211
  const workspace = input.workspace ?? "here";
174
212
  const at = new Date(input.nowMs).toISOString();
175
213
  const base = await getHead(input.cwd);
214
+ const defaultBranch = await resolveDefaultBranch(input.cwd);
215
+ if (!defaultBranch) {
216
+ throw new FlowError("INTERNAL_STATE", "cannot bind: no default branch");
217
+ }
218
+ const target = normalizeTargetRef(defaultBranch.branch);
219
+ const loadedSettings = await loadKeiyakuSettings(input.cwd);
220
+ let bootstrapPlan;
221
+ if (workspace === "worktree") {
222
+ assertSettingsKnobUsable(loadedSettings, "worktreeBootstrap");
223
+ bootstrapPlan = resolvedBootstrapPlan(loadedSettings.knobs?.worktreeBootstrap);
224
+ }
176
225
  const collidedIds = [];
177
226
  for (let attempt = 0; attempt < 2; attempt += 1) {
178
227
  const randomBytes = input.randomBytes?.(attempt) ?? randomBytesDefault();
@@ -189,9 +238,11 @@ export async function bindContract(input) {
189
238
  actor: input.actor,
190
239
  draft,
191
240
  base,
241
+ target,
192
242
  after: input.after ?? [],
193
243
  workspace,
194
244
  place,
245
+ bootstrap: bootstrapPlan,
195
246
  });
196
247
  const ref = activeLedgerRef(id);
197
248
  const appended = await appendEntry(input.cwd, ref, entry, null);
@@ -203,6 +254,8 @@ export async function bindContract(input) {
203
254
  const branch = `keiyaku/${id}`;
204
255
  const worktreePath = await contractWorktreePathFromBind(input.cwd, id, entry);
205
256
  const targetRoot = workspace === "here" ? input.cwd : worktreePath;
257
+ let ledgerHead = appended.head;
258
+ let bootstrapRecorded = false;
206
259
  try {
207
260
  const tree = await resolveTreeRoot(input.cwd, workspace, id, entry);
208
261
  const registration = await registerScope(input.cwd, {
@@ -226,6 +279,16 @@ export async function bindContract(input) {
226
279
  });
227
280
  resources.worktreePath = worktreePath;
228
281
  }
282
+ if (bootstrapPlan) {
283
+ input.onProgress?.(bootstrapProgressLine(bootstrapPlan.command));
284
+ const runner = input.bootstrapRunner ?? runWorktreeBootstrap;
285
+ const observation = await runner({ cwd: worktreePath, plan: bootstrapPlan }, input.bootstrapDependencies);
286
+ ledgerHead = await appendBootstrapEntry(input.cwd, id, input.actor, at, bootstrapEntryDataFromObservation(observation));
287
+ bootstrapRecorded = true;
288
+ if (!isBootstrapSuccess(observation)) {
289
+ throw new FlowError("INTERNAL_STATE", `BIND_BOOTSTRAP_FAILED: worktree bootstrap failed with ${observation.result.kind}`);
290
+ }
291
+ }
229
292
  const ledger = await readLedger(input.cwd, id);
230
293
  if (!ledger)
231
294
  throw new FlowError("INTERNAL_STATE", `LEDGER_MISSING: ledger missing immediately after bind for ${id}`);
@@ -239,17 +302,34 @@ export async function bindContract(input) {
239
302
  branch: workspace === "worktree" ? branch : undefined,
240
303
  worktreePath: targetRoot,
241
304
  workspace,
242
- ledgerHead: appended.head,
305
+ ledgerHead,
243
306
  warnings: registration.warnings,
244
307
  };
245
308
  }
246
309
  catch (error) {
247
- const cleanupFailures = await cleanupCreatedResources(input.cwd, resources);
248
- try {
249
- await appendBindFailedForfeit(input.cwd, id, input.actor, at);
310
+ // Bootstrap failure order is strict: bootstrap entry (already appended)
311
+ // forfeit(bind-failed) → cleanup. Cleanup cold facts cannot convert success.
312
+ const cleanupFailures = [];
313
+ if (!bootstrapRecorded) {
314
+ // Non-bootstrap failure: cleanup first remains compatible with prior path,
315
+ // then forfeit. When bootstrap already recorded its failed fact, forfeit
316
+ // must precede cleanup.
317
+ cleanupFailures.push(...(await cleanupCreatedResources(input.cwd, resources)));
318
+ try {
319
+ await appendBindFailedForfeit(input.cwd, id, input.actor, at);
320
+ }
321
+ catch (forfeitError) {
322
+ cleanupFailures.push(forfeitError instanceof Error ? forfeitError.message : String(forfeitError));
323
+ }
250
324
  }
251
- catch (forfeitError) {
252
- cleanupFailures.push(forfeitError instanceof Error ? forfeitError.message : String(forfeitError));
325
+ else {
326
+ try {
327
+ await appendBindFailedForfeit(input.cwd, id, input.actor, at);
328
+ }
329
+ catch (forfeitError) {
330
+ cleanupFailures.push(forfeitError instanceof Error ? forfeitError.message : String(forfeitError));
331
+ }
332
+ cleanupFailures.push(...(await cleanupCreatedResources(input.cwd, resources)));
253
333
  }
254
334
  if (resources.registry) {
255
335
  try {
@@ -0,0 +1,69 @@
1
+ import { appendDebugLog } from "../telemetry/debug-log.js";
2
+ import { logWarn } from "../telemetry/logger.js";
3
+ import { runCall } from "./call.js";
4
+ import { persistResponseHistory } from "./transcripts.js";
5
+ import { isGitRepo } from "../git/branches.js";
6
+ import * as path from "node:path";
7
+ import * as fs from "node:fs/promises";
8
+ import { stableRepoRoot } from "./worktree-path.js";
9
+ function responsePathFromPrintingCwd(historyStorageCwd, responsePath) {
10
+ const printingCwd = process.cwd();
11
+ const absoluteHistoryCwd = path.resolve(printingCwd, historyStorageCwd);
12
+ return path.relative(printingCwd, path.resolve(absoluteHistoryCwd, responsePath));
13
+ }
14
+ export async function runCallAndPersist(input) {
15
+ const historyCandidate = input.repo ?? input.cwd;
16
+ const canonicalHistoryCandidate = await fs.realpath(historyCandidate);
17
+ const historyStorageCwd = (await isGitRepo(historyCandidate))
18
+ ? await stableRepoRoot(historyCandidate)
19
+ : canonicalHistoryCandidate;
20
+ const historyDisplayCwd = path.resolve(historyCandidate, path.relative(canonicalHistoryCandidate, historyStorageCwd));
21
+ const result = await runCall(input);
22
+ if (input.incognito) {
23
+ return { result };
24
+ }
25
+ if (result.projectionOwnsResponsePersistence === true) {
26
+ return {
27
+ result,
28
+ ...(result.responsePath ? { responsePath: responsePathFromPrintingCwd(historyDisplayCwd, result.responsePath) } : {}),
29
+ ...(result.artifactId ? { artifactId: result.artifactId } : {}),
30
+ };
31
+ }
32
+ let responsePath;
33
+ let artifactId;
34
+ try {
35
+ if (!result.evidenceCwd)
36
+ throw new Error("call response persistence is missing evidenceCwd");
37
+ const authority = result.address.repo.kind === "repository"
38
+ ? { kind: "repository" }
39
+ : { kind: "user" };
40
+ const persisted = await persistResponseHistory({
41
+ cwd: historyStorageCwd,
42
+ tool: "call",
43
+ provenance: { authority, binding: result.address.binding, evidenceCwd: result.evidenceCwd },
44
+ title: input.title,
45
+ task: input.task,
46
+ context: input.context,
47
+ response: result.summary,
48
+ session: result.session,
49
+ subagentName: result.agent,
50
+ launchSnapshot: result.launchSnapshot,
51
+ effectivePolicy: result.meta?.effectivePolicy,
52
+ });
53
+ responsePath = persisted.responsePath;
54
+ artifactId = persisted.artifactId;
55
+ }
56
+ catch (historyError) {
57
+ const historyErrorMessage = historyError instanceof Error ? historyError.message : String(historyError);
58
+ logWarn(`Failed to persist call response: ${historyErrorMessage}`, { cwd: historyStorageCwd, section: "script" });
59
+ appendDebugLog(`tool call response persistence failed: ${historyErrorMessage}`, {
60
+ cwd: historyStorageCwd,
61
+ section: "script",
62
+ });
63
+ }
64
+ return {
65
+ result: artifactId ? { ...result, artifactId } : result,
66
+ ...(responsePath ? { responsePath: responsePathFromPrintingCwd(historyDisplayCwd, responsePath) } : {}),
67
+ ...(artifactId ? { artifactId } : {}),
68
+ };
69
+ }