@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
@@ -0,0 +1,342 @@
1
+ import { createHash } from "node:crypto";
2
+ import { FlowError } from "../flow-error.js";
3
+ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
4
+ import { listContractIds, readLedger } from "./ledger.js";
5
+ import { commissionTarget, deriveContractState } from "./status.js";
6
+ const EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
7
+ const MISSING_REF_SENTINEL = "0".repeat(40);
8
+ const ISO_UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
9
+ function isPositiveSafeInteger(value) {
10
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
11
+ }
12
+ function isCurrentPetitionData(data) {
13
+ return "target" in data && "seat" in data;
14
+ }
15
+ function latestPetition(entries) {
16
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
17
+ const entry = entries[index];
18
+ if (entry.kind === "petition")
19
+ return entry;
20
+ }
21
+ return undefined;
22
+ }
23
+ export function deriveQueueReadModel(ledgers) {
24
+ const seatsByContract = new Map();
25
+ const diagnosticsByContract = new Map();
26
+ const orderedByTarget = new Map();
27
+ for (const ledger of ledgers) {
28
+ if (deriveContractState(ledger.entries).state !== "petitioned")
29
+ continue;
30
+ const petition = latestPetition(ledger.entries);
31
+ if (!petition)
32
+ continue;
33
+ const derived = commissionTarget(ledger.entries);
34
+ if (!derived)
35
+ continue;
36
+ const diagnostics = [];
37
+ let seat;
38
+ if (isCurrentPetitionData(petition.data)) {
39
+ seat = {
40
+ contract: ledger.contractId,
41
+ target: petition.data.target,
42
+ seat: petition.data.seat,
43
+ legacy: false,
44
+ };
45
+ if (petition.data.target !== derived.ref) {
46
+ diagnostics.push({
47
+ kind: "target-mismatch",
48
+ petitioned: petition.data.target,
49
+ derived: derived.ref,
50
+ });
51
+ }
52
+ const bucket = orderedByTarget.get(petition.data.target) ?? [];
53
+ bucket.push(seat);
54
+ orderedByTarget.set(petition.data.target, bucket);
55
+ }
56
+ else {
57
+ seat = {
58
+ contract: ledger.contractId,
59
+ target: derived.ref,
60
+ seat: null,
61
+ legacy: true,
62
+ };
63
+ diagnostics.push({ kind: "legacy-petition" });
64
+ }
65
+ seatsByContract.set(ledger.contractId, seat);
66
+ diagnosticsByContract.set(ledger.contractId, diagnostics);
67
+ }
68
+ for (const [target, seats] of orderedByTarget) {
69
+ const bySeat = new Map();
70
+ for (const seat of seats) {
71
+ if (seat.seat === null)
72
+ continue;
73
+ const group = bySeat.get(seat.seat) ?? [];
74
+ group.push(seat);
75
+ bySeat.set(seat.seat, group);
76
+ }
77
+ for (const [seatNumber, group] of bySeat) {
78
+ if (group.length < 2)
79
+ continue;
80
+ const sorted = [...group].sort((a, b) => a.contract.localeCompare(b.contract));
81
+ for (const member of sorted) {
82
+ const others = sorted.filter((other) => other.contract !== member.contract);
83
+ const existing = diagnosticsByContract.get(member.contract) ?? [];
84
+ for (const other of others) {
85
+ existing.push({ kind: "duplicate-seat", seat: seatNumber, with: other.contract });
86
+ }
87
+ diagnosticsByContract.set(member.contract, existing);
88
+ }
89
+ }
90
+ seats.sort((a, b) => {
91
+ const seatA = a.seat ?? Number.POSITIVE_INFINITY;
92
+ const seatB = b.seat ?? Number.POSITIVE_INFINITY;
93
+ if (seatA !== seatB)
94
+ return seatA - seatB;
95
+ return a.contract.localeCompare(b.contract);
96
+ });
97
+ orderedByTarget.set(target, seats);
98
+ }
99
+ const targetQueues = [...orderedByTarget.entries()]
100
+ .sort(([a], [b]) => a.localeCompare(b))
101
+ .map(([target, ordered]) => ({ target, ordered }));
102
+ return {
103
+ targetQueues,
104
+ seatsByContract,
105
+ diagnosticsByContract,
106
+ };
107
+ }
108
+ export async function readQueueReadModel(cwd) {
109
+ const contractIds = await listContractIds(cwd);
110
+ const ledgers = [];
111
+ for (const id of contractIds) {
112
+ const ledger = await readLedger(cwd, id);
113
+ if (ledger)
114
+ ledgers.push(ledger);
115
+ }
116
+ return deriveQueueReadModel(ledgers);
117
+ }
118
+ export function queueMembership(model, contractId) {
119
+ const seat = model.seatsByContract.get(contractId);
120
+ if (!seat)
121
+ return undefined;
122
+ const diagnostics = model.diagnosticsByContract.get(contractId) ?? [];
123
+ if (seat.legacy) {
124
+ return {
125
+ seat,
126
+ position: null,
127
+ queueLength: 0,
128
+ queueHead: null,
129
+ diagnostics,
130
+ };
131
+ }
132
+ const targetQueue = model.targetQueues.find((queue) => queue.target === seat.target);
133
+ if (!targetQueue) {
134
+ return {
135
+ seat,
136
+ position: null,
137
+ queueLength: 0,
138
+ queueHead: null,
139
+ diagnostics,
140
+ };
141
+ }
142
+ const index = targetQueue.ordered.findIndex((entry) => entry.contract === contractId);
143
+ const position = index < 0 ? null : index + 1;
144
+ return {
145
+ seat,
146
+ position,
147
+ queueLength: targetQueue.ordered.length,
148
+ queueHead: targetQueue.ordered[0]?.contract ?? null,
149
+ diagnostics,
150
+ };
151
+ }
152
+ export function assertClaimQueueHead(model, contractId) {
153
+ const membership = queueMembership(model, contractId);
154
+ if (!membership) {
155
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} is not queue head; position=not queued`);
156
+ }
157
+ if (membership.seat.legacy) {
158
+ throw new FlowError("QUEUE_LEGACY_PETITION", `contract ${contractId} petitioned before seat law took effect; reject its petition and petition again`);
159
+ }
160
+ const mismatch = membership.diagnostics.find((diagnostic) => diagnostic.kind === "target-mismatch");
161
+ if (mismatch && mismatch.kind === "target-mismatch") {
162
+ throw new FlowError("QUEUE_TARGET_MISMATCH", `contract ${contractId} petition target ${mismatch.petitioned} disagrees with derived target ${mismatch.derived}; reject its petition and petition again`);
163
+ }
164
+ if (membership.position !== 1) {
165
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} is not queue head; position=${membership.position ?? "not queued"}`);
166
+ }
167
+ return membership;
168
+ }
169
+ export function seatAllocatorRef(target) {
170
+ const digest = createHash("sha256").update(target, "utf8").digest("hex");
171
+ return `refs/keiyaku/seat/${digest}`;
172
+ }
173
+ async function resolveRefHeadRaw(cwd, ref) {
174
+ const git = createGit(cwd);
175
+ try {
176
+ // Raw observed-head read: any existing Git object id, not commit-only.
177
+ // Non-commit heads are returned so parseAllocatorChain can classify them as corrupt.
178
+ return (await git.raw(["rev-parse", "--verify", ref])).trim();
179
+ }
180
+ catch (error) {
181
+ if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
182
+ return null;
183
+ throw wrapGitError(`rev-parse ${ref}`, error, cwd);
184
+ }
185
+ }
186
+ async function readCommitMessage(cwd, commit) {
187
+ const git = createGit(cwd);
188
+ try {
189
+ return await git.raw(["show", "-s", "--format=%B", commit]);
190
+ }
191
+ catch (error) {
192
+ throw wrapGitError(`show commit message ${commit}`, error, cwd);
193
+ }
194
+ }
195
+ function parseSeatAllocatorMessage(message, expectedTarget) {
196
+ const normalized = message.replace(/\r\n/g, "\n");
197
+ const separatorIndex = normalized.indexOf("\n\n");
198
+ if (separatorIndex < 0)
199
+ throw new Error("missing blank line");
200
+ const subject = normalized.slice(0, separatorIndex).trim();
201
+ const body = normalized.slice(separatorIndex + 2).trim();
202
+ const parsed = JSON.parse(body);
203
+ const keys = Object.keys(parsed).sort();
204
+ if (keys.join(",") !== "actor,at,seat,target,v")
205
+ throw new Error("unexpected fields");
206
+ if (parsed.v !== 1)
207
+ throw new Error("bad version");
208
+ if (typeof parsed.target !== "string" || parsed.target !== expectedTarget)
209
+ throw new Error("target mismatch");
210
+ if (!isPositiveSafeInteger(parsed.seat))
211
+ throw new Error("bad seat");
212
+ if (typeof parsed.at !== "string" || !ISO_UTC_RE.test(parsed.at))
213
+ throw new Error("bad at");
214
+ if (typeof parsed.actor !== "string" || parsed.actor.trim().length === 0)
215
+ throw new Error("bad actor");
216
+ if (subject !== `keiyaku seat: ${parsed.seat}`)
217
+ throw new Error("bad subject");
218
+ return {
219
+ v: 1,
220
+ target: parsed.target,
221
+ seat: parsed.seat,
222
+ at: parsed.at,
223
+ actor: parsed.actor,
224
+ };
225
+ }
226
+ async function parseAllocatorChain(cwd, head, expectedTarget) {
227
+ const git = createGit(cwd);
228
+ let output;
229
+ try {
230
+ output = await git.raw(["rev-list", "--reverse", head]);
231
+ }
232
+ catch {
233
+ return null;
234
+ }
235
+ const commits = output
236
+ .trim()
237
+ .split("\n")
238
+ .map((line) => line.trim())
239
+ .filter(Boolean);
240
+ if (commits.length === 0)
241
+ return null;
242
+ const entries = [];
243
+ let previousSeat = 0;
244
+ for (const commit of commits) {
245
+ try {
246
+ const entry = parseSeatAllocatorMessage(await readCommitMessage(cwd, commit), expectedTarget);
247
+ if (entry.seat <= previousSeat)
248
+ return null;
249
+ previousSeat = entry.seat;
250
+ entries.push(entry);
251
+ }
252
+ catch {
253
+ return null;
254
+ }
255
+ }
256
+ return { entries, lastSeat: previousSeat };
257
+ }
258
+ async function ledgerSeatFloor(cwd, target) {
259
+ const contractIds = await listContractIds(cwd);
260
+ let maxSeat = 0;
261
+ for (const id of contractIds) {
262
+ const ledger = await readLedger(cwd, id);
263
+ if (!ledger)
264
+ continue;
265
+ for (const entry of ledger.entries) {
266
+ if (entry.kind !== "petition")
267
+ continue;
268
+ if (!isCurrentPetitionData(entry.data))
269
+ continue;
270
+ if (entry.data.target !== target)
271
+ continue;
272
+ if (entry.data.seat > maxSeat)
273
+ maxSeat = entry.data.seat;
274
+ }
275
+ }
276
+ return maxSeat;
277
+ }
278
+ async function createSeatCommit(cwd, entry, parentHead) {
279
+ const git = createGit(cwd);
280
+ const subject = `keiyaku seat: ${entry.seat}`;
281
+ const args = ["commit-tree", EMPTY_TREE_HASH];
282
+ if (parentHead)
283
+ args.push("-p", parentHead);
284
+ args.push("-m", subject, "-m", JSON.stringify(entry, null, 2));
285
+ try {
286
+ return (await git.raw(args)).trim();
287
+ }
288
+ catch (error) {
289
+ throw wrapGitError(`commit-tree seat ${entry.seat}`, error, cwd);
290
+ }
291
+ }
292
+ async function casUpdateRef(cwd, ref, newHead, expectedOld) {
293
+ const git = createGit(cwd);
294
+ const oldValue = expectedOld ?? MISSING_REF_SENTINEL;
295
+ try {
296
+ await git.raw(["update-ref", ref, newHead, oldValue]);
297
+ return true;
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ }
303
+ export async function allocateSettlementSeat(cwd, input) {
304
+ const ref = seatAllocatorRef(input.target);
305
+ const maxRetries = 3;
306
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
307
+ const observedHead = await resolveRefHeadRaw(cwd, ref);
308
+ const floor = await ledgerSeatFloor(cwd, input.target);
309
+ const parsed = observedHead ? await parseAllocatorChain(cwd, observedHead, input.target) : null;
310
+ const healthy = parsed !== null && parsed.lastSeat >= floor;
311
+ let seat;
312
+ let parent;
313
+ let expectedOld;
314
+ if (healthy && parsed) {
315
+ seat = parsed.lastSeat + 1;
316
+ parent = observedHead;
317
+ expectedOld = observedHead;
318
+ }
319
+ else {
320
+ // Missing, parse failure, target mismatch, non-monotonic, or stale-behind-ledger:
321
+ // mint a detached no-parent root at floor+1. Leave the bad chain for Git GC.
322
+ seat = floor + 1;
323
+ parent = null;
324
+ expectedOld = observedHead;
325
+ }
326
+ if (!isPositiveSafeInteger(seat)) {
327
+ throw new FlowError("INTERNAL_STATE", `settlement seats exhausted for ${input.target}`);
328
+ }
329
+ const entry = {
330
+ v: 1,
331
+ target: input.target,
332
+ seat,
333
+ at: input.at,
334
+ actor: input.actor,
335
+ };
336
+ const newHead = await createSeatCommit(cwd, entry, parent);
337
+ const casOk = await casUpdateRef(cwd, ref, newHead, expectedOld);
338
+ if (casOk)
339
+ return { head: newHead, seat };
340
+ }
341
+ throw new FlowError("INTERNAL_STATE", `failed to allocate settlement seat for ${input.target}`);
342
+ }
@@ -1,7 +1,7 @@
1
1
  import { FlowError } from "../flow-error.js";
2
2
  import { readLedger } from "./ledger.js";
3
3
  import { appendRefLogWithRetry } from "./ref-log.js";
4
- import { patternsOverlap } from "./scope.js";
4
+ import { isNegativeScopePattern, patternsOverlap } from "./scope.js";
5
5
  import { deriveContractState, isTerminalState } from "./status.js";
6
6
  export const REGISTRY_REF = "refs/keiyaku/registry";
7
7
  function parseRegistryEntry(message) {
@@ -52,12 +52,15 @@ export async function registerScope(cwd, input) {
52
52
  if (existing.tree === input.tree) {
53
53
  throw new FlowError("ACTIVE_KEIYAKU_EXISTS", [
54
54
  `tree is already occupied by active contract ${existing.contract}`,
55
- "Use `keiyaku bind --worktree ...` to allocate a separate workspace.",
56
- `Or run \`keiyaku forfeit @${existing.contract}\` to free this tree.`,
55
+ `Run \`keiyaku forfeit @${existing.contract}\` to free this tree.`,
57
56
  ].join(" "));
58
57
  }
59
58
  for (const nextPattern of input.scope) {
59
+ if (isNegativeScopePattern(nextPattern))
60
+ continue;
60
61
  for (const existingPattern of existing.scope) {
62
+ if (isNegativeScopePattern(existingPattern))
63
+ continue;
61
64
  if (patternsOverlap(nextPattern, existingPattern)) {
62
65
  nextWarnings.push(`▲ scope overlaps registered active contract ${existing.contract}: ${nextPattern} overlaps ${existingPattern}; consider --after ${existing.contract}`);
63
66
  }
@@ -69,6 +69,43 @@ function renderSealFacts(entry) {
69
69
  ]),
70
70
  ];
71
71
  }
72
+ function bootstrapTimeoutMs(entries) {
73
+ const bind = entries.find((entry) => entry.kind === "bind");
74
+ return bind?.data.bootstrap?.timeoutMs;
75
+ }
76
+ function renderBootstrapFacts(entry, entries) {
77
+ const duration = `duration ${entry.data.durationMs}ms`;
78
+ let first;
79
+ switch (entry.data.result.kind) {
80
+ case "exit":
81
+ first = joinFactClusters([`exit ${entry.data.result.code}`, duration]);
82
+ break;
83
+ case "signal":
84
+ first = joinFactClusters([`signal ${entry.data.result.signal}`, duration]);
85
+ break;
86
+ case "timeout": {
87
+ const timeoutMs = bootstrapTimeoutMs(entries);
88
+ first =
89
+ timeoutMs === undefined
90
+ ? joinFactClusters([`timeout`, duration])
91
+ : joinFactClusters([`timeout ${timeoutMs}ms`, duration]);
92
+ break;
93
+ }
94
+ case "spawn":
95
+ first = joinFactClusters([
96
+ "spawn error",
97
+ compactWhitespace(entry.data.result.message),
98
+ duration,
99
+ ]);
100
+ break;
101
+ }
102
+ const facts = [first];
103
+ if (entry.data.stderrTail !== undefined) {
104
+ const tail = compactWhitespace(entry.data.stderrTail);
105
+ facts.push(entry.data.stderrTruncated ? `┆ stderr …${tail}` : `┆ stderr ${tail}`);
106
+ }
107
+ return facts;
108
+ }
72
109
  function renderEventFacts(entry, entries) {
73
110
  switch (entry.kind) {
74
111
  case "bind":
@@ -81,6 +118,8 @@ function renderEventFacts(entry, entries) {
81
118
  entry.data.after && entry.data.after.length > 0 ? `after ${entry.data.after.length}` : null,
82
119
  ]),
83
120
  ];
121
+ case "bootstrap":
122
+ return renderBootstrapFacts(entry, entries);
84
123
  case "arc":
85
124
  return [renderOpenArcFact(entry)];
86
125
  case "seal":
@@ -261,6 +300,10 @@ export function projectionGlyph(state) {
261
300
  return "✕";
262
301
  case "minting":
263
302
  return "…";
303
+ case "startup-timeout":
304
+ return "!";
305
+ case "unknown":
306
+ return "?";
264
307
  }
265
308
  }
266
309
  function renderDriftMark(drift) {
@@ -273,9 +316,14 @@ function renderDriftMark(drift) {
273
316
  export function renderKanshiBoard(status) {
274
317
  const head = shortHead(status.defaultHead);
275
318
  if (status.contracts.length === 0) {
319
+ if (status.targetFilter !== null) {
320
+ return `現世 ${head} · target ${status.targetFilter}\nno commissions on target ${status.targetFilter}\n`;
321
+ }
276
322
  return `現世 ${head} 无在飞契约\n`;
277
323
  }
278
- const header = `現世 ${head}`;
324
+ const header = status.targetFilter !== null
325
+ ? `現世 ${head} · target ${status.targetFilter}`
326
+ : `現世 ${head}`;
279
327
  const lines = [header, ""];
280
328
  for (const contract of status.contracts) {
281
329
  const glyph = kanshiGlyph(contract);
@@ -321,6 +369,20 @@ export function renderKanshiBoard(status) {
321
369
  const sparkCol = spark.padEnd(14);
322
370
  const factText = facts.join(" · ");
323
371
  lines.push(`${glyph} ${name.padEnd(18)} ${sparkCol} ${factText}`.trimEnd());
372
+ if (contract.bootstrapDiagnostics.some((diagnostic) => diagnostic.kind === "plan-without-result")) {
373
+ lines.push(" ! bootstrap · no result recorded · forfeit and re-bind");
374
+ }
375
+ for (const diagnostic of contract.queueDiagnostics) {
376
+ if (diagnostic.kind === "legacy-petition") {
377
+ lines.push(" ! legacy petition · no seat · reject then re-petition");
378
+ continue;
379
+ }
380
+ if (diagnostic.kind === "duplicate-seat") {
381
+ lines.push(` ! seat ${diagnostic.seat} duplicated with ${diagnostic.with} · ordered by contract id`);
382
+ continue;
383
+ }
384
+ lines.push(` ! target mismatch · petitioned ${diagnostic.petitioned} · derived ${diagnostic.derived}`);
385
+ }
324
386
  }
325
387
  return `${lines.join("\n")}\n`;
326
388
  }
@@ -1,16 +1,16 @@
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, errorContainsAnyPattern, wrapGitError } from "../git/core.js";
4
+ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
5
5
  import { KEIYAKU_FILE } from "../keiyaku.js";
6
6
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
7
7
  import { readProjectionStatusBoard } from "./projection-status.js";
8
8
  import { renderContractView } from "./render.js";
9
9
  import { captureSeal, findOpenArc, receiptsAfterFence, sealOpenArc } from "./seal.js";
10
- import { deriveContractState, isTerminalState, latestBase, resolveDefaultBranch } from "./status.js";
10
+ import { commissionTarget, deriveContractState, isTerminalState, latestBase } from "./status.js";
11
11
  import { findContractWorktreePath } from "./worktree-path.js";
12
12
  import { getCurrentBranch, isGitRepo } from "../git/branches.js";
13
- import { resolveCurrentContractId } from "./context.js";
13
+ import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
14
14
  import { ACTOR_IDENTITY } from "../keiyaku.js";
15
15
  import { persistResponseHistory } from "./transcripts.js";
16
16
  function assertOperable(contractId, entries) {
@@ -31,6 +31,17 @@ async function resolveCommit(cwd, rev) {
31
31
  throw wrapGitError(`rev-parse ${rev}`, error, cwd);
32
32
  }
33
33
  }
34
+ async function resolveCommitOrNull(cwd, rev) {
35
+ const git = createGit(cwd);
36
+ try {
37
+ return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
38
+ }
39
+ catch (error) {
40
+ if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
41
+ return null;
42
+ throw wrapGitError(`rev-parse ${rev}`, error, cwd);
43
+ }
44
+ }
34
45
  async function renderUpdatedContract(ledgerCwd, renderCwd, contractId) {
35
46
  const ledger = await readLedger(ledgerCwd, contractId);
36
47
  if (!ledger) {
@@ -61,7 +72,8 @@ async function rewriteBranch(cwd, newBase, oldBase, branch) {
61
72
  }
62
73
  function assertNoInFlightProjection(cwd, contractId, nowMs) {
63
74
  const board = readProjectionStatusBoard(cwd, nowMs);
64
- if (!board?.rows.some((row) => row.state === "out"))
75
+ const unsafe = new Set(["out", "minting", "startup-timeout", "unknown"]);
76
+ if (!board?.rows.some((row) => unsafe.has(row.state)))
65
77
  return;
66
78
  throw new FlowError("INTERNAL_STATE", `akuma still out on ${contractId}'s worktree; rewriting under it would strand its work`, { hints: ["keiyaku status — watch for the akuma to land, then renew"] });
67
79
  }
@@ -76,26 +88,31 @@ export async function renewContract(input) {
76
88
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
77
89
  }
78
90
  assertOperable(input.contractId, ledger.entries);
79
- const target = await resolveDefaultBranch(input.cwd);
80
- if (!target)
81
- throw new FlowError("INTERNAL_STATE", `cannot renew ${input.contractId}: no default branch`);
91
+ const commission = commissionTarget(ledger.entries);
92
+ if (!commission) {
93
+ throw new FlowError("INTERNAL_STATE", `cannot renew ${input.contractId}: missing commission target`);
94
+ }
95
+ const targetHead = await resolveCommitOrNull(input.cwd, commission.ref);
96
+ if (!targetHead) {
97
+ throw new FlowError("INTERNAL_STATE", `cannot renew ${input.contractId}: commission target ${commission.ref} does not resolve`);
98
+ }
82
99
  const oldBase = latestBase(ledger.entries);
83
100
  if (!oldBase)
84
101
  throw new FlowError("INTERNAL_STATE", "ledger must contain bind entry");
85
102
  const branch = `keiyaku/${input.contractId}`;
86
103
  const oldHead = await resolveCommit(input.cwd, branch);
87
104
  const open = findOpenArc(ledger.entries);
88
- const capture = open ? await captureSeal(input.cwd, ledger.entries, oldHead) : null;
105
+ const capture = open ? await captureSeal(input.cwd, ledger.entries, oldHead, input.waivePaths) : null;
89
106
  if (!open) {
90
107
  const loose = await receiptsAfterFence(input.cwd, ledger.entries, oldHead);
91
108
  if (loose.length > 0)
92
109
  throw looseCommitError(input.contractId, loose);
93
110
  }
94
- if (oldBase === target.head) {
111
+ if (oldBase === targetHead) {
95
112
  return {
96
113
  contractId: input.contractId,
97
114
  oldBase,
98
- newBase: target.head,
115
+ newBase: targetHead,
99
116
  oldHead,
100
117
  newHead: oldHead,
101
118
  ledgerHead: ledger.head,
@@ -106,7 +123,7 @@ export async function renewContract(input) {
106
123
  const worktreePath = await findContractWorktreePath(input.cwd, input.contractId);
107
124
  const rewriteCwd = worktreePath ?? input.cwd;
108
125
  assertNoInFlightProjection(rewriteCwd, input.contractId, input.nowMs);
109
- await rewriteBranch(rewriteCwd, target.head, oldBase, worktreePath ? undefined : branch);
126
+ await rewriteBranch(rewriteCwd, targetHead, oldBase, worktreePath ? undefined : branch);
110
127
  const newHead = await resolveCommit(input.cwd, branch);
111
128
  const sealed = await sealOpenArc({
112
129
  cwd: input.cwd,
@@ -128,7 +145,7 @@ export async function renewContract(input) {
128
145
  contract: input.contractId,
129
146
  at,
130
147
  actor: input.actor,
131
- data: { oldBase, newBase: target.head, oldHead, newHead },
148
+ data: { oldBase, newBase: targetHead, oldHead, newHead },
132
149
  };
133
150
  });
134
151
  if (!result.ok) {
@@ -138,7 +155,7 @@ export async function renewContract(input) {
138
155
  return {
139
156
  contractId: input.contractId,
140
157
  oldBase,
141
- newBase: target.head,
158
+ newBase: targetHead,
142
159
  oldHead,
143
160
  newHead,
144
161
  ledgerHead: result.head,
@@ -150,46 +167,56 @@ export async function renewKeiyaku(input) {
150
167
  if (!(await isGitRepo(input.cwd))) {
151
168
  throw new FlowError("NOT_GIT_REPO", `${input.cwd} is not a git repository`);
152
169
  }
153
- const resolution = input.contractId ? undefined : await resolveCurrentContractId(input.cwd);
154
- if (resolution?.status === "none")
155
- throw new FlowError("EMPTY_PARAM", "no active contract found");
156
- const contractId = input.contractId ?? resolution.contractId;
157
- const renewed = await renewContract({ cwd: input.cwd, contractId, ...{ actor: ACTOR_IDENTITY, nowMs: Date.now() } });
158
- const currentBranch = await getCurrentBranch(input.cwd);
159
- const target = await resolveDefaultBranch(input.cwd);
160
- const diff = renewed.sealed?.incrementalDiff ?? "nothing sealed";
161
- const summary = renewed.alreadyFresh
162
- ? `Contract ${contractId} is already fresh; nothing touched.`
163
- : `Contract ${contractId} renewed onto ${renewed.newBase}.`;
164
- let responsePath;
165
- try {
166
- responsePath = await persistResponseHistory({
170
+ const address = input.address ?? await resolveContractAddress(input.cwd, input.contractId, input.contractAddressSource);
171
+ return await withResolvedAddress(address, async () => {
172
+ const contractId = address.binding.commissionId;
173
+ const renewed = await renewContract({
167
174
  cwd: input.cwd,
168
- tool: "renew",
169
- title: contractId,
170
- branch: currentBranch,
171
- commit: renewed.ledgerHead,
172
- oldBase: renewed.oldBase,
173
- newBase: renewed.newBase,
174
- oldHead: renewed.oldHead,
175
- newHead: renewed.newHead,
176
- ledgerCommit: renewed.ledgerHead,
177
- summary,
178
- diff,
179
- ...(renewed.sealed ? { sealed: renewed.sealed } : {}),
175
+ contractId,
176
+ actor: ACTOR_IDENTITY,
177
+ nowMs: Date.now(),
178
+ waivePaths: input.waivePaths,
180
179
  });
181
- }
182
- catch {
183
- responsePath = undefined;
184
- }
185
- return {
186
- status: "success",
187
- ...renewed,
188
- currentBranch,
189
- baseBranch: target?.branch ?? currentBranch,
190
- diff,
191
- sealed: renewed.sealed ?? undefined,
192
- summary,
193
- responsePath,
194
- };
180
+ const currentBranch = await getCurrentBranch(input.cwd);
181
+ const baseBranch = address.target.branch.slice("refs/heads/".length);
182
+ const diff = renewed.sealed?.incrementalDiff ?? "nothing sealed";
183
+ const summary = renewed.alreadyFresh
184
+ ? `Contract ${contractId} is already fresh; nothing touched.`
185
+ : `Contract ${contractId} renewed onto ${renewed.newBase}.`;
186
+ let responsePath;
187
+ let artifactId;
188
+ try {
189
+ const persisted = await persistResponseHistory({
190
+ cwd: input.cwd,
191
+ tool: "renew",
192
+ provenance: { authority: { kind: "repository" }, binding: address.binding, evidenceCwd: input.cwd },
193
+ title: contractId,
194
+ oldBase: renewed.oldBase,
195
+ newBase: renewed.newBase,
196
+ oldHead: renewed.oldHead,
197
+ newHead: renewed.newHead,
198
+ ledgerCommit: renewed.ledgerHead,
199
+ summary,
200
+ diff,
201
+ ...(renewed.sealed ? { sealed: renewed.sealed } : {}),
202
+ });
203
+ responsePath = persisted.responsePath;
204
+ artifactId = persisted.artifactId;
205
+ }
206
+ catch {
207
+ responsePath = undefined;
208
+ }
209
+ return {
210
+ status: "success",
211
+ ...renewed,
212
+ currentBranch,
213
+ baseBranch,
214
+ diff,
215
+ sealed: renewed.sealed ?? undefined,
216
+ summary,
217
+ responsePath,
218
+ artifactId,
219
+ address,
220
+ };
221
+ });
195
222
  }