@astrosheep/keiyaku 1.0.1 → 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.
- package/README.md +5 -7
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms_v2.js +103 -0
- package/build/agents/harness/activity-values.js +54 -0
- package/build/agents/harness/event-channel.js +55 -20
- package/build/agents/harness/events.js +47 -1
- package/build/agents/harness/execution-handle.js +292 -61
- package/build/agents/harness/index.js +103 -121
- package/build/agents/harness/pump.js +36 -47
- package/build/agents/harness/runtime.js +3 -0
- package/build/agents/launch-snapshot_v2.js +348 -0
- package/build/agents/opencode-sdk.js +42 -169
- package/build/agents/providers/claude-agent-sdk.js +240 -79
- package/build/agents/providers/codex-app-server.js +280 -72
- package/build/agents/providers/codex-sdk.js +159 -41
- package/build/agents/providers/event-type-tail.js +16 -0
- package/build/agents/providers/opencode-sdk.js +165 -59
- package/build/agents/providers/pi.js +254 -42
- package/build/agents/selector_v2.js +122 -0
- package/build/cli/commands/akuma.js +120 -42
- package/build/cli/completion.js +6 -7
- package/build/cli/flags.js +36 -10
- package/build/cli/help.js +29 -24
- package/build/cli/index.js +225 -93
- package/build/cli/parse.js +194 -75
- package/build/cli/projection-address.js +11 -0
- package/build/cli/render/address.js +88 -0
- package/build/cli/render/arc.js +10 -10
- package/build/cli/render/call.js +59 -0
- package/build/cli/render/compact-text.js +3 -0
- package/build/cli/render/errors.js +4 -2
- package/build/cli/render/format.js +6 -0
- package/build/cli/render/line-width.js +70 -0
- package/build/cli/render/misc.js +7 -4
- package/build/cli/render/petition.js +4 -3
- package/build/cli/render/projection-activity.js +201 -0
- package/build/cli/render/shared.js +46 -12
- package/build/cli/render/status.js +59 -10
- package/build/cli/render/tool-ledger-rollup.js +74 -0
- package/build/cli/render/tool-presentation.js +97 -0
- package/build/cli/render/wait.js +229 -0
- package/build/cli/skills-install.js +42 -24
- package/build/cli/subagent-guard.js +1 -1
- package/build/config/akuma-loader_v2.js +222 -0
- package/build/config/env-keys.js +0 -4
- package/build/config/env.js +0 -6
- package/build/config/settings.js +325 -12
- package/build/core/addressing.js +224 -0
- package/build/core/amend.js +30 -47
- package/build/core/arc.js +68 -60
- package/build/core/bind.js +105 -25
- package/build/core/call-persist.js +69 -0
- package/build/core/call.js +658 -0
- package/build/core/claim.js +72 -44
- package/build/core/command-io.js +19 -19
- package/build/core/context.js +11 -36
- package/build/core/draft.js +2 -3
- package/build/core/entry.js +101 -6
- package/build/core/execution-coordinate.js +62 -0
- package/build/core/execution-pact.js +141 -0
- package/build/core/forfeit.js +2 -3
- package/build/core/hints.js +10 -10
- package/build/core/ids.js +6 -0
- package/build/core/ledger.js +16 -1
- package/build/core/petition-claim-gates.js +14 -12
- package/build/core/petition-claim.js +1 -0
- package/build/core/petition-run.js +6 -2
- package/build/core/petition.js +63 -21
- package/build/core/places.js +106 -13
- package/build/core/projection/generation.js +412 -0
- package/build/core/projection/heart.js +316 -0
- package/build/core/projection/identity.js +81 -0
- package/build/core/projection/leash.js +87 -0
- package/build/core/projection/mint.js +154 -0
- package/build/core/projection-activity.js +219 -0
- package/build/core/projection-coordinate.js +35 -0
- package/build/core/projection-core.js +69 -463
- package/build/core/projection-generation-continuation.js +31 -0
- package/build/core/projection-generation-execution.js +148 -0
- package/build/core/projection-generation-identity.js +17 -0
- package/build/core/projection-generation-launcher.js +90 -0
- package/build/core/projection-generation-process.js +43 -0
- package/build/core/projection-generation-runner.js +144 -0
- package/build/core/projection-generation-runtime.js +15 -0
- package/build/core/projection-generation-store.js +707 -0
- package/build/core/projection-identity.js +11 -0
- package/build/core/projection-life-observer.js +87 -0
- package/build/core/projection-life-protocol.js +95 -0
- package/build/core/projection-mint.js +96 -24
- package/build/core/projection-runner-lock.js +230 -0
- package/build/core/projection-status.js +207 -107
- package/build/core/projection-tells.js +401 -0
- package/build/core/projection-wait.js +155 -0
- package/build/core/projection-wake.js +109 -231
- package/build/core/queue_v2.js +342 -0
- package/build/core/registry.js +6 -3
- package/build/core/render.js +63 -1
- package/build/core/renew.js +80 -53
- package/build/core/scope.js +186 -137
- package/build/core/seal.js +16 -19
- package/build/core/status.js +88 -25
- package/build/core/stored-agent-event.js +101 -0
- package/build/core/transcripts.js +214 -228
- package/build/core/verdict.js +27 -15
- package/build/core/worktree-bootstrap.js +187 -0
- package/build/core/worktree-path.js +72 -18
- package/build/flow-error.js +4 -6
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +6 -1
- package/build/index.js +8 -5
- package/build/keiyaku.js +1 -3
- package/package.json +6 -5
- package/skills/keiyaku/SKILL.md +6 -47
- package/skills/keiyaku-akuma/SKILL.md +75 -0
- package/build/agents/effective-policy.js +0 -32
- package/build/agents/index.js +0 -114
- package/build/agents/selector.js +0 -28
- package/build/cli/render/summon.js +0 -17
- package/build/cli/render/tell.js +0 -11
- package/build/config/akuma-loader.js +0 -268
- package/build/core/queue.js +0 -73
- package/build/core/summon-persist.js +0 -73
- 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
|
+
}
|
package/build/core/registry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/build/core/render.js
CHANGED
|
@@ -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 =
|
|
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
|
}
|
package/build/core/renew.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
80
|
-
if (!
|
|
81
|
-
throw new FlowError("INTERNAL_STATE", `cannot renew ${input.contractId}:
|
|
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 ===
|
|
111
|
+
if (oldBase === targetHead) {
|
|
95
112
|
return {
|
|
96
113
|
contractId: input.contractId,
|
|
97
114
|
oldBase,
|
|
98
|
-
newBase:
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
}
|