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