@astrosheep/keiyaku 2.9.4 → 2.9.5

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 (126) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +1 -0
  3. package/build/agents/harness/execution-handle.js +1 -0
  4. package/build/agents/harness/outcome.js +21 -1
  5. package/build/agents/harness/projection.js +147 -75
  6. package/build/agents/providers/codex-app-server/adapter.js +32 -0
  7. package/build/agents/providers/codex-app-server/events.js +23 -0
  8. package/build/agents/providers/pi/events.js +4 -3
  9. package/build/agents/types.js +2 -0
  10. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  11. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  12. package/build/cli/commands/akuma/view/meta.js +1 -1
  13. package/build/cli/commands/contract/amend/meta.js +4 -4
  14. package/build/cli/commands/contract/arc/handler.js +1 -1
  15. package/build/cli/commands/contract/arc/meta.js +4 -4
  16. package/build/cli/commands/contract/audit/handler.js +16 -0
  17. package/build/cli/commands/contract/audit/meta.js +16 -0
  18. package/build/cli/commands/contract/bind/meta.js +20 -11
  19. package/build/cli/commands/contract/catalog.js +2 -0
  20. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  21. package/build/cli/commands/contract/log/meta.js +1 -1
  22. package/build/cli/commands/contract/petition/handler.js +1 -2
  23. package/build/cli/commands/contract/petition/meta.js +6 -6
  24. package/build/cli/commands/contract/renew/handler.js +2 -2
  25. package/build/cli/commands/contract/renew/meta.js +4 -4
  26. package/build/cli/commands/metadata.js +3 -2
  27. package/build/cli/commands/projection/call/meta.js +4 -1
  28. package/build/cli/commands/projection/kill/meta.js +1 -1
  29. package/build/cli/commands/projection/revive/meta.js +4 -1
  30. package/build/cli/commands/projection/status/meta.js +1 -1
  31. package/build/cli/commands/projection/tell/handler.js +1 -1
  32. package/build/cli/commands/projection/tell/meta.js +4 -1
  33. package/build/cli/commands/projection/wait/meta.js +1 -1
  34. package/build/cli/commands/skills/install/meta.js +1 -1
  35. package/build/cli/commands/skills/skills/meta.js +1 -1
  36. package/build/cli/commands/system/completion/meta.js +1 -1
  37. package/build/cli/commands/system/dump-env/meta.js +1 -1
  38. package/build/cli/commands/system/guide/meta.js +1 -1
  39. package/build/cli/commands/task/add/handler.js +2 -2
  40. package/build/cli/commands/task/add/meta.js +4 -1
  41. package/build/cli/commands/task/doctor/meta.js +1 -1
  42. package/build/cli/commands/task/done/meta.js +1 -1
  43. package/build/cli/commands/task/drop/meta.js +1 -1
  44. package/build/cli/commands/task/ls/handler.js +10 -4
  45. package/build/cli/commands/task/ls/meta.js +3 -3
  46. package/build/cli/commands/task/show/meta.js +1 -1
  47. package/build/cli/commands/task/start/meta.js +1 -1
  48. package/build/cli/commands/task/stop/meta.js +1 -1
  49. package/build/cli/commands/task/task/meta.js +1 -1
  50. package/build/cli/commands/task/update/handler.js +2 -3
  51. package/build/cli/commands/task/update/meta.js +4 -1
  52. package/build/cli/flags.js +18 -3
  53. package/build/cli/help.js +3 -3
  54. package/build/cli/index.js +59 -75
  55. package/build/cli/output-stream-error-policy.js +19 -5
  56. package/build/cli/parse-flags.js +19 -2
  57. package/build/cli/parse-metadata.js +1 -7
  58. package/build/cli/parse-selectors.js +1 -0
  59. package/build/cli/parse.js +16 -7
  60. package/build/cli/projection-address.js +6 -1
  61. package/build/cli/render/audit.js +121 -0
  62. package/build/cli/render/misc.js +3 -1
  63. package/build/cli/render/projection-activity.js +12 -6
  64. package/build/cli/render/status.js +28 -18
  65. package/build/cli/render/success-response.js +6 -5
  66. package/build/cli/render/task.js +12 -43
  67. package/build/cli/render/terminal-failure.js +7 -2
  68. package/build/cli/render/wait.js +36 -8
  69. package/build/config/env-keys.js +2 -0
  70. package/build/config/env.js +3 -0
  71. package/build/core/amend.js +20 -94
  72. package/build/core/arc.js +14 -6
  73. package/build/core/audit.js +650 -0
  74. package/build/core/bind.js +1 -1
  75. package/build/core/call/execution.js +1 -0
  76. package/build/core/contract-view.js +2 -3
  77. package/build/core/draft.js +33 -20
  78. package/build/core/entry.js +3 -4
  79. package/build/core/markdown/lex.js +2 -1
  80. package/build/core/projection/generation/database.js +41 -16
  81. package/build/core/projection/generation/model.js +22 -2
  82. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  83. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  84. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  85. package/build/core/projection/generation/store.js +1 -1
  86. package/build/core/projection/generation/transitions.js +19 -3
  87. package/build/core/projection/index.js +2 -2
  88. package/build/core/projection/projection-execution-observer.js +35 -21
  89. package/build/core/projection/projection-life-observer.js +16 -2
  90. package/build/core/projection/projection-status.js +67 -22
  91. package/build/core/projection/projection-terminal-failure.js +2 -0
  92. package/build/core/projection/projection-wait.js +6 -9
  93. package/build/core/projection/projection-wake.js +30 -6
  94. package/build/core/renew.js +41 -22
  95. package/build/core/scope.js +129 -2
  96. package/build/core/seal.js +36 -15
  97. package/build/core/settlement/petition-claim-gates.js +4 -33
  98. package/build/core/settlement/petition-preview.js +1 -1
  99. package/build/core/settlement/petition.js +10 -3
  100. package/build/core/settlement/settlement.js +3 -10
  101. package/build/core/settlement/verification.js +59 -0
  102. package/build/core/status/board.js +48 -28
  103. package/build/core/stored-agent-event.js +3 -2
  104. package/build/core/task/board.js +1 -0
  105. package/build/core/task/commands.js +20 -12
  106. package/build/core/task/document.js +5 -0
  107. package/build/core/task/index.js +1 -1
  108. package/build/core/task/model.js +8 -1
  109. package/build/core/task/query.js +172 -0
  110. package/build/core/task/settlement-git.js +115 -16
  111. package/build/core/task/settlement-policy.js +17 -9
  112. package/build/core/task/source-board.js +1 -0
  113. package/build/core/task/task-contract.js +125 -51
  114. package/build/core/task/task-store-repository.js +1 -0
  115. package/build/core/task/task.js +2 -0
  116. package/build/flow-error.js +2 -4
  117. package/build/generated/version.js +2 -2
  118. package/build/git/diff/parsers.js +23 -13
  119. package/build/git/diff/structured.js +127 -0
  120. package/build/index.js +7 -7
  121. package/build/telemetry/logger.js +4 -2
  122. package/package.json +1 -1
  123. package/skills/keiyaku/SKILL.md +2 -2
  124. package/skills/keiyaku-akuma/SKILL.md +17 -6
  125. package/skills/keiyaku-task/SKILL.md +16 -7
  126. package/skills/keiyaku-workflow/SKILL.md +42 -13
@@ -0,0 +1,650 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { Buffer } from "node:buffer";
6
+ import { FlowError, asMessage } from "../flow-error.js";
7
+ import { createGit, wrapGitError } from "../git/core.js";
8
+ import { readStructuredDiff } from "../git/diff/structured.js";
9
+ import { readLedger } from "./ledger.js";
10
+ import { hasStrandableProjectionGeneration } from "./projection/index.js";
11
+ import { readRenewRecoverySession } from "./renew-session.js";
12
+ import { rewriteRenewBranch } from "./renew.js";
13
+ import { effectiveScope, findOpenArc, getFence, getMaxArc, receiptsAfterFence } from "./seal.js";
14
+ import { queueMembership, readQueueReadModel } from "./settlement/queue.js";
15
+ import { resolveVerificationCommands, runVerificationCommand, verificationExecutor, } from "./settlement/verification.js";
16
+ import { markdownSourceSections, scopePathEvidence } from "./scope.js";
17
+ import { normalizeSectionTitle } from "./markdown/titles.js";
18
+ import { commissionTarget, latestBase } from "./status/drift.js";
19
+ import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
20
+ import { resolveContractDeliveryCwd } from "./worktree-path.js";
21
+ import { KEIYAKU_FILE } from "../keiyaku.js";
22
+ async function resolveObject(cwd, revision, kind) {
23
+ const git = createGit(cwd);
24
+ try {
25
+ return (await git.raw(["rev-parse", "--verify", `${revision}^{${kind}}`])).trim();
26
+ }
27
+ catch (error) {
28
+ throw wrapGitError(`rev-parse ${revision}^{${kind}}`, error, cwd);
29
+ }
30
+ }
31
+ async function pinCoordinates(input) {
32
+ const ledger = await readLedger(input.cwd, input.contractId);
33
+ if (!ledger)
34
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
35
+ const bind = ledger.entries.find((entry) => entry.kind === "bind");
36
+ const target = commissionTarget(ledger.entries);
37
+ const base = latestBase(ledger.entries);
38
+ if (!bind || !target || !base)
39
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} is missing pinned audit coordinates`);
40
+ const [deliveryHead, deliveryTree, targetHead, targetTree] = await Promise.all([
41
+ resolveObject(input.cwd, input.deliveryRef, "commit"),
42
+ resolveObject(input.cwd, input.deliveryRef, "tree"),
43
+ resolveObject(input.cwd, target.ref, "commit"),
44
+ resolveObject(input.cwd, target.ref, "tree"),
45
+ ]);
46
+ return {
47
+ criteria: effectiveCriteria(ledger.entries),
48
+ base,
49
+ entries: ledger.entries,
50
+ coordinates: {
51
+ ledger: { head: ledger.head },
52
+ delivery: { ref: input.deliveryRef, head: deliveryHead, tree: deliveryTree },
53
+ target: { ref: target.ref, head: targetHead, tree: targetTree },
54
+ },
55
+ };
56
+ }
57
+ async function createCandidate(sourceRoot, pinned, contractId) {
58
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "keiyaku-audit-"));
59
+ const git = createGit(sourceRoot);
60
+ try {
61
+ await git.raw(["clone", "--quiet", "--no-hardlinks", "--no-checkout", sourceRoot, cwd]);
62
+ const candidateGit = createGit(cwd);
63
+ await candidateGit.raw(["config", "user.name", "Keiyaku Audit"]);
64
+ await candidateGit.raw(["config", "user.email", "audit@keiyaku.invalid"]);
65
+ await candidateGit.raw(["checkout", "--quiet", "-B", `keiyaku/${contractId}`, pinned.coordinates.delivery.head]);
66
+ if (pinned.base === pinned.coordinates.target.head) {
67
+ return {
68
+ cwd,
69
+ rehearsal: { status: "fresh" },
70
+ candidate: {
71
+ base: pinned.coordinates.target.head,
72
+ head: pinned.coordinates.delivery.head,
73
+ tree: pinned.coordinates.delivery.tree,
74
+ rewritten: false,
75
+ },
76
+ };
77
+ }
78
+ try {
79
+ await rewriteRenewBranch({
80
+ cwd,
81
+ contractId,
82
+ oldHead: pinned.coordinates.delivery.head,
83
+ recoveryRef: "refs/keiyaku/audit/disposable",
84
+ newBase: pinned.coordinates.target.head,
85
+ oldBase: pinned.base,
86
+ });
87
+ }
88
+ catch (error) {
89
+ if (error instanceof FlowError && error.code === "CLOSE_MERGE_CONFLICT" && error.facts?.kind === "renew_conflict") {
90
+ return {
91
+ cwd,
92
+ rehearsal: {
93
+ status: "conflict",
94
+ paths: [...error.facts.conflictPaths],
95
+ pathCapture: error.facts.conflictPathCapture.status === "captured" ? "captured" : "failed",
96
+ ...(error.facts.conflictPathCapture.status === "failed" ? { diagnostic: error.facts.conflictPathCapture.diagnostic } : {}),
97
+ },
98
+ candidate: null,
99
+ };
100
+ }
101
+ return { cwd, rehearsal: { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) } }, candidate: null };
102
+ }
103
+ const [head, tree] = await Promise.all([resolveObject(cwd, "HEAD", "commit"), resolveObject(cwd, "HEAD", "tree")]);
104
+ return {
105
+ cwd,
106
+ rehearsal: { status: "clean", oldBase: pinned.base, newBase: pinned.coordinates.target.head, oldHead: pinned.coordinates.delivery.head },
107
+ candidate: { base: pinned.coordinates.target.head, head, tree, rewritten: true },
108
+ };
109
+ }
110
+ catch (error) {
111
+ return { cwd, rehearsal: { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) } }, candidate: null };
112
+ }
113
+ }
114
+ function importLine(line) {
115
+ const text = line.content.trim();
116
+ return /^(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+\s*=\s*require\s*\(|[^=]+\brequire\s*\(|[^=]+\bimport\s*\()/.test(text);
117
+ }
118
+ function identifierRename(before, after) {
119
+ const left = before.match(/[A-Za-z_$][\w$]*/g) ?? [];
120
+ const right = after.match(/[A-Za-z_$][\w$]*/g) ?? [];
121
+ if (left.length !== right.length)
122
+ return null;
123
+ const differences = left.flatMap((value, index) => value === right[index] ? [] : [{ from: value, to: right[index] }]);
124
+ if (differences.length !== 1)
125
+ return null;
126
+ const rename = differences[0];
127
+ const expression = new RegExp(`\\b${rename.from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g");
128
+ return before.replace(expression, rename.to) === after ? rename : null;
129
+ }
130
+ function explainFile(file, matcher) {
131
+ const explanations = [];
132
+ const explained = new Set();
133
+ if (file.status === "R" && file.oldPath) {
134
+ explanations.push({ kind: "file-rename", from: file.oldPath, to: file.path, similarity: file.similarity ?? 0 });
135
+ }
136
+ for (const hunk of file.hunks) {
137
+ const changed = hunk.lines.filter((line) => line.kind !== "context");
138
+ for (const line of changed) {
139
+ if (!importLine(line))
140
+ continue;
141
+ explanations.push({ kind: "import", hunk: hunk.index, oldLine: line.oldLine, newLine: line.newLine });
142
+ explained.add(`${hunk.index}:${line.kind}:${line.oldLine}:${line.newLine}`);
143
+ }
144
+ const deletions = changed.filter((line) => line.kind === "deletion" && !importLine(line));
145
+ const additions = changed.filter((line) => line.kind === "addition" && !importLine(line));
146
+ if (deletions.length === additions.length) {
147
+ deletions.forEach((deletion, index) => {
148
+ const addition = additions[index];
149
+ const rename = identifierRename(deletion.content, addition.content);
150
+ if (!rename)
151
+ return;
152
+ explanations.push({ kind: "symbol-rename", ...rename, hunk: hunk.index, oldLine: deletion.oldLine, newLine: addition.newLine });
153
+ explained.add(`${hunk.index}:${deletion.kind}:${deletion.oldLine}:${deletion.newLine}`);
154
+ explained.add(`${hunk.index}:${addition.kind}:${addition.oldLine}:${addition.newLine}`);
155
+ });
156
+ }
157
+ }
158
+ const residue = file.binary ? [{ hunk: -1, oldLine: null, newLine: null, kind: "binary", content: "binary content changed" }] : [];
159
+ for (const hunk of file.hunks) {
160
+ for (const line of hunk.lines) {
161
+ if (line.kind === "context")
162
+ continue;
163
+ const key = `${hunk.index}:${line.kind}:${line.oldLine}:${line.newLine}`;
164
+ if (!explained.has(key))
165
+ residue.push({ hunk: hunk.index, oldLine: line.oldLine, newLine: line.newLine, kind: line.kind, content: line.content });
166
+ }
167
+ }
168
+ const klass = matcher.matched ? "in-scope" : residue.length === 0 && explanations.length > 0 ? "downgraded" : "violated";
169
+ return { path: file.path, ...(file.oldPath ? { oldPath: file.oldPath } : {}), matcher, explanations, residue, class: klass };
170
+ }
171
+ async function evaluateScope(cwd, entries, diff) {
172
+ const matchers = await scopePathEvidence(cwd, effectiveScope(entries), diff.files.map((file) => file.path));
173
+ return diff.files.map((file, index) => explainFile(file, matchers[index]));
174
+ }
175
+ function rawHunk(file, hunk) {
176
+ const lines = [
177
+ `${file.status} ${file.oldPath ? `${file.oldPath} -> ` : ""}${file.path} · +${file.insertions} -${file.deletions}`,
178
+ hunk.header,
179
+ ...hunk.lines.map((line) => `${line.kind === "addition" ? "+" : line.kind === "deletion" ? "-" : " "}${line.content}`),
180
+ ];
181
+ return lines.join("\n");
182
+ }
183
+ function rawFile(file) {
184
+ return `${file.status} ${file.oldPath ? `${file.oldPath} -> ` : ""}${file.path} · +${file.insertions} -${file.deletions}`;
185
+ }
186
+ function moduleFor(pathname) {
187
+ const segments = pathname.split("/");
188
+ return segments.length > 1 ? segments.slice(0, -1).join("/") : ".";
189
+ }
190
+ function isAuthorityBoundary(pathname) {
191
+ return /(^|\/)(auth(?:entication|orization)?|schema|schemas|migrations?|api|config|ci|\.github|permissions?|concurrency)(\/|$)|(^|\/)(package|tsconfig|vite|webpack|eslint|prettier)\.(json|ya?ml|[cm]?[jt]s)$/.test(pathname);
192
+ }
193
+ function isGeneratedOrLowPriority(pathname, file) {
194
+ return file.binary || /(^|\/)(vendor|generated|dist|build)(\/|$)|(^|\/)([^/]+\.)?(lock|min)\.(json|ya?ml|js|css)$|(?:^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$/.test(pathname);
195
+ }
196
+ function isSource(pathname) {
197
+ return /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|rb|php|cs|swift)$/.test(pathname) && !/(^|\/)(test|tests|__tests__|spec)(\/|\.)/.test(pathname);
198
+ }
199
+ function hasRelatedTest(file, all) {
200
+ const stem = file.path.replace(/\.[^.]+$/, "").replace(/\/index$/, "");
201
+ return all.some((candidate) => candidate.path !== file.path && /(test|tests|__tests__|spec)/.test(candidate.path) && candidate.path.replace(/\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|rb|php|cs|swift)$/, "").includes(stem.split("/").at(-1) ?? ""));
202
+ }
203
+ function portfolioTier(file, scope, all) {
204
+ if (scope.class === "violated")
205
+ return { tier: 0, mandatory: true };
206
+ if (isAuthorityBoundary(file.path))
207
+ return { tier: 1, mandatory: true };
208
+ if (/(^|\/)(package\.json|workspace|pnpm-workspace|turbo|nx)\b/.test(file.path))
209
+ return { tier: 2, mandatory: false };
210
+ if (isGeneratedOrLowPriority(file.path, file))
211
+ return { tier: 6, mandatory: false };
212
+ if (isSource(file.path) && !hasRelatedTest(file, all))
213
+ return { tier: 3, mandatory: false };
214
+ if (file.status === "R" || scope.class === "downgraded")
215
+ return { tier: 5, mandatory: false };
216
+ return { tier: 4, mandatory: false };
217
+ }
218
+ function stablePortfolioOrder(left, right) {
219
+ if (left.tier !== right.tier)
220
+ return left.tier - right.tier;
221
+ const leftValue = 1000 - left.tier * 100;
222
+ const rightValue = 1000 - right.tier * 100;
223
+ const ratio = rightValue / Math.max(right.bytes, 1) - leftValue / Math.max(left.bytes, 1);
224
+ if (ratio !== 0)
225
+ return ratio;
226
+ return left.path.localeCompare(right.path) || (left.hunk ?? -1) - (right.hunk ?? -1);
227
+ }
228
+ async function writeDiffArtifact(input) {
229
+ const body = JSON.stringify({ version: 1, base: input.base, deliveryHead: input.deliveryHead, targetHead: input.targetHead, diff: input.diff });
230
+ const digest = createHash("sha256").update(body).digest("hex");
231
+ const directory = path.join(os.tmpdir(), "keiyaku-audit-artifacts");
232
+ const artifact = path.join(directory, `${digest}.json`);
233
+ await fs.mkdir(directory, { recursive: true });
234
+ try {
235
+ await fs.writeFile(artifact, body, { encoding: "utf8", flag: "wx", mode: 0o444 });
236
+ }
237
+ catch (error) {
238
+ if (error.code !== "EEXIST")
239
+ throw error;
240
+ }
241
+ return artifact;
242
+ }
243
+ function retrieval(artifact, pathname, hunk) {
244
+ return { kind: "immutable-artifact", artifact, argv: ["cat", artifact], path: pathname, hunk };
245
+ }
246
+ function presentDiff(diff, scope, budget, artifact) {
247
+ const candidates = [];
248
+ for (let fileIndex = 0; fileIndex < diff.files.length; fileIndex += 1) {
249
+ const file = diff.files[fileIndex];
250
+ const row = scope[fileIndex];
251
+ const policy = portfolioTier(file, row, diff.files);
252
+ const chunks = file.hunks.length === 0
253
+ ? [{ hunk: null, content: rawFile(file) }]
254
+ : file.hunks.map((hunk) => ({ hunk: hunk.index, content: rawHunk(file, hunk) }));
255
+ for (const chunk of chunks) {
256
+ const bytes = Buffer.byteLength(chunk.content);
257
+ candidates.push({ path: file.path, hunk: chunk.hunk, content: chunk.content, bytes, cost: bytes + 1, tier: policy.tier, mandatory: policy.mandatory, module: moduleFor(file.path) });
258
+ }
259
+ }
260
+ const selected = [];
261
+ const selectedKeys = new Set();
262
+ let used = 0;
263
+ const take = (candidate) => {
264
+ const key = `${candidate.path}\0${candidate.hunk ?? "file"}`;
265
+ if (selectedKeys.has(key))
266
+ return;
267
+ selectedKeys.add(key);
268
+ selected.push(candidate);
269
+ used += candidate.cost;
270
+ };
271
+ for (const candidate of [...candidates].filter((item) => item.mandatory).sort(stablePortfolioOrder)) {
272
+ if (used + candidate.cost <= budget)
273
+ take(candidate);
274
+ }
275
+ const modules = [...new Set(candidates.map((candidate) => candidate.module))].sort();
276
+ for (const module of modules) {
277
+ const representative = candidates.filter((candidate) => candidate.module === module && !selectedKeys.has(`${candidate.path}\0${candidate.hunk ?? "file"}`)).sort(stablePortfolioOrder)[0];
278
+ if (representative && used + representative.cost <= budget)
279
+ take(representative);
280
+ }
281
+ for (const candidate of [...candidates].sort(stablePortfolioOrder)) {
282
+ if (selectedKeys.has(`${candidate.path}\0${candidate.hunk ?? "file"}`))
283
+ continue;
284
+ if (used + candidate.cost <= budget)
285
+ take(candidate);
286
+ }
287
+ const omissions = candidates
288
+ .filter((candidate) => !selectedKeys.has(`${candidate.path}\0${candidate.hunk ?? "file"}`))
289
+ .sort((left, right) => left.path.localeCompare(right.path) || (left.hunk ?? -1) - (right.hunk ?? -1))
290
+ .map((candidate) => ({
291
+ path: candidate.path,
292
+ hunk: candidate.hunk,
293
+ bytes: candidate.bytes,
294
+ reason: candidate.hunk === null && candidate.content === rawFile(diff.files.find((file) => file.path === candidate.path)) ? "no-raw-hunks" : used + candidate.cost > budget ? "budget" : "not-selected",
295
+ retrieval: retrieval(artifact, candidate.path, candidate.hunk),
296
+ }));
297
+ return { budget: { limit: budget, used }, selected: selected.sort((left, right) => left.path.localeCompare(right.path) || (left.hunk ?? -1) - (right.hunk ?? -1)), omissions };
298
+ }
299
+ function fileFacts(diff, scope) {
300
+ return diff.files.map((file, index) => {
301
+ const row = scope[index];
302
+ return {
303
+ path: file.path,
304
+ ...(file.oldPath ? { oldPath: file.oldPath } : {}),
305
+ status: file.status,
306
+ stats: { insertions: file.insertions, deletions: file.deletions, binary: file.binary },
307
+ matcher: row.matcher,
308
+ explanations: row.explanations,
309
+ residue: row.residue,
310
+ class: row.class,
311
+ };
312
+ });
313
+ }
314
+ function appendedScopeTerms(entries) {
315
+ return entries.flatMap((entry) => entry.kind === "amend" && entry.data.scopeDelta ? entry.data.scopeDelta.add : []);
316
+ }
317
+ function effectiveCriteria(entries) {
318
+ const bind = entries.find((entry) => entry.kind === "bind");
319
+ if (!bind)
320
+ return [];
321
+ const criteria = [...bind.data.checks];
322
+ for (const entry of entries) {
323
+ if (entry.kind !== "amend")
324
+ continue;
325
+ for (const section of markdownSourceSections(entry.data.amendment)) {
326
+ if (section.level !== 2 || normalizeSectionTitle(section.title) !== "checks")
327
+ continue;
328
+ const content = entry.data.amendment.slice(section.contentStart, section.end);
329
+ for (const line of content.split(/\r?\n/)) {
330
+ const match = line.match(/^\s*-\s+(.+)$/);
331
+ if (match)
332
+ criteria.push(match[1].trim());
333
+ }
334
+ }
335
+ }
336
+ return criteria;
337
+ }
338
+ async function evaluateVerification(cwd, head) {
339
+ let commands;
340
+ try {
341
+ commands = await resolveVerificationCommands(cwd);
342
+ }
343
+ catch (error) {
344
+ return { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) }, results: [] };
345
+ }
346
+ if (commands.length === 0)
347
+ return { status: "not_configured", results: [] };
348
+ const results = [];
349
+ for (const command of commands) {
350
+ const argv = [command.command, ...command.args];
351
+ const known = verificationExecutor(command);
352
+ if ("cause" in known) {
353
+ results.push({ status: "unverifiable", label: command.label, command: command.command, argv, head, cause: known.cause });
354
+ continue;
355
+ }
356
+ let execution;
357
+ try {
358
+ execution = await runVerificationCommand(cwd, command);
359
+ }
360
+ catch (error) {
361
+ return { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) }, results: [] };
362
+ }
363
+ results.push({ status: "executed", label: command.label, executor: known.executor, body: known.body, command: command.command, argv, head, exitCode: execution.code, durationMs: execution.durationMs, stdout: execution.stdout, stderr: execution.stderr });
364
+ }
365
+ return { status: "evaluated", results };
366
+ }
367
+ async function deliveryResidue(cwd) {
368
+ const git = createGit(cwd);
369
+ const fields = (await git.raw(["status", "--porcelain=v1", "-z", "--untracked-files=all"])).split("\0");
370
+ const staged = new Set();
371
+ const unstaged = new Set();
372
+ const untracked = new Set();
373
+ for (let index = 0; index < fields.length; index += 1) {
374
+ const field = fields[index];
375
+ if (!field)
376
+ continue;
377
+ const xy = field.slice(0, 2);
378
+ const filePath = field.slice(3);
379
+ if (filePath === KEIYAKU_FILE || filePath.startsWith(".keiyaku/recovery/")) {
380
+ if (xy[0] === "R" || xy[0] === "C")
381
+ index += 1;
382
+ continue;
383
+ }
384
+ if (xy === "??")
385
+ untracked.add(filePath);
386
+ else {
387
+ if (xy[0] !== " ")
388
+ staged.add(filePath);
389
+ if (xy[1] !== " ")
390
+ unstaged.add(filePath);
391
+ if (xy[0] === "R" || xy[0] === "C")
392
+ index += 1;
393
+ }
394
+ }
395
+ const tracked = new Set([...staged, ...unstaged]);
396
+ return { tracked: [...tracked], staged: [...staged], unstaged: [...unstaged], untracked: [...untracked] };
397
+ }
398
+ async function deliveryCoordinate(cwd) {
399
+ const [head, tree, residue] = await Promise.all([
400
+ resolveObject(cwd, "HEAD", "commit"),
401
+ resolveObject(cwd, "HEAD", "tree"),
402
+ deliveryResidue(cwd),
403
+ ]);
404
+ return { head, tree, residue, residueFingerprint: await residueFingerprint(cwd, residue) };
405
+ }
406
+ function residueKey(residue) {
407
+ return JSON.stringify({
408
+ tracked: [...residue.tracked].sort(),
409
+ staged: [...residue.staged].sort(),
410
+ unstaged: [...residue.unstaged].sort(),
411
+ untracked: [...residue.untracked].sort(),
412
+ });
413
+ }
414
+ async function residueFingerprint(cwd, residue) {
415
+ const git = createGit(cwd);
416
+ const digest = createHash("sha256").update(residueKey(residue));
417
+ const tracked = [...residue.tracked].sort();
418
+ if (tracked.length > 0) {
419
+ digest.update(await git.raw(["diff", "--no-color", "--no-ext-diff", "--binary", "HEAD", "--", ...tracked]));
420
+ }
421
+ for (const pathname of [...residue.untracked].sort()) {
422
+ const absolute = path.join(cwd, pathname);
423
+ const stat = await fs.lstat(absolute);
424
+ digest.update(pathname).update("\0").update(String(stat.mode)).update("\0");
425
+ if (stat.isSymbolicLink())
426
+ digest.update(await fs.readlink(absolute));
427
+ else
428
+ digest.update(await fs.readFile(absolute));
429
+ digest.update("\0");
430
+ }
431
+ return digest.digest("hex");
432
+ }
433
+ async function evaluateReadiness(input) {
434
+ const bind = input.pinned.entries.find((entry) => entry.kind === "bind");
435
+ const lifecycle = deriveContractState(input.pinned.entries);
436
+ const [residue, looseCommits, session, queue, liveProjection, dependencies] = await Promise.all([
437
+ deliveryResidue(input.deliveryCwd),
438
+ receiptsAfterFence(input.root, input.pinned.entries, input.pinned.coordinates.delivery.head),
439
+ readRenewRecoverySession(input.root, input.contractId),
440
+ readQueueReadModel(input.root),
441
+ hasStrandableProjectionGeneration({ cwd: input.root, commissionId: input.contractId, deliveryWorktree: input.deliveryCwd, nowMs: Date.now() }),
442
+ Promise.all((bind.data.after ?? []).map(async (id) => {
443
+ const ledger = await readLedger(input.root, id);
444
+ const state = ledger ? deriveContractState(ledger.entries) : { state: "missing" };
445
+ return { id, satisfied: ledger !== null && isTerminalState(state), state: state.state };
446
+ })),
447
+ ]);
448
+ const membership = queueMembership(queue, input.contractId);
449
+ return {
450
+ deliveryResidue: residue,
451
+ arc: { open: findOpenArc(input.pinned.entries)?.data.arc ?? (getMaxArc(input.pinned.entries) === 0 ? 0 : null), max: getMaxArc(input.pinned.entries), fence: getFence(input.pinned.entries), looseCommits },
452
+ renewSession: { open: session !== null },
453
+ rewriteGuard: { liveProjection },
454
+ dependencies,
455
+ petition: {
456
+ state: lifecycle.state,
457
+ queue: membership ? { position: membership.position, length: membership.queueLength, head: membership.queueHead, diagnostics: membership.diagnostics.map((item) => item.kind) } : null,
458
+ },
459
+ };
460
+ }
461
+ function coordinateChanges(before, after) {
462
+ const changes = [];
463
+ if (before.ledger.head !== after.ledger.head)
464
+ changes.push(`ledger ${before.ledger.head} -> ${after.ledger.head}`);
465
+ if (before.delivery.head !== after.delivery.head || before.delivery.tree !== after.delivery.tree)
466
+ changes.push(`delivery ${before.delivery.head}/${before.delivery.tree} -> ${after.delivery.head}/${after.delivery.tree}`);
467
+ if (before.target.head !== after.target.head || before.target.tree !== after.target.tree)
468
+ changes.push(`target ${before.target.head}/${before.target.tree} -> ${after.target.head}/${after.target.tree}`);
469
+ return changes;
470
+ }
471
+ function quote(value) {
472
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
473
+ }
474
+ function collectFindings(input) {
475
+ const findings = [];
476
+ const selector = `keiyaku @${input.contractId}`;
477
+ if (input.rehearsal.status === "clean")
478
+ findings.push({ code: "BASE_DRIFT", detail: "delivery is stale; disposable renew rehearsal was clean", nextCommand: `${selector} renew` });
479
+ if (input.rehearsal.status === "conflict")
480
+ findings.push({ code: "RENEW_CONFLICT", detail: `renew rehearsal conflicts at ${input.rehearsal.paths.join(", ") || "unknown paths"}`, nextCommand: `${selector} renew --resolve` });
481
+ if (input.rehearsal.status === "unknown")
482
+ findings.push({ code: "RENEW_UNKNOWN", detail: input.rehearsal.cause.message, nextCommand: `${selector} audit` });
483
+ const violated = input.scope?.filter((row) => row.class === "violated") ?? [];
484
+ if (violated.length > 0)
485
+ findings.push({ code: "SCOPE_RESIDUE", detail: `${violated.length} changed paths contain unexplained out-of-scope residue`, nextCommand: `${selector} renew --append-scope ${quote(violated[0].path)}` });
486
+ if (input.verification.status === "evaluated") {
487
+ for (const result of input.verification.results) {
488
+ if (result.status === "unverifiable")
489
+ findings.push({ code: "VERIFICATION_UNVERIFIABLE", detail: `${result.label}: ${result.cause}`, nextCommand: `${selector} audit` });
490
+ else if (result.exitCode !== 0)
491
+ findings.push({ code: "VERIFICATION_FAILED", detail: `${result.label} exited ${result.exitCode}`, nextCommand: `cd ${quote(input.deliveryCwd)} && ${result.argv.map(quote).join(" ")}` });
492
+ }
493
+ }
494
+ else if (input.verification.status === "unknown") {
495
+ findings.push({ code: "VERIFICATION_UNVERIFIABLE", detail: input.verification.cause.message, nextCommand: `${selector} audit` });
496
+ }
497
+ const residue = input.readiness.deliveryResidue;
498
+ if (residue.tracked.length + residue.untracked.length > 0)
499
+ findings.push({ code: "DELIVERY_RESIDUE", detail: `${residue.tracked.length} tracked and ${residue.untracked.length} untracked delivery paths`, nextCommand: `git -C ${quote(input.deliveryCwd)} status --short` });
500
+ if (input.readiness.arc.max > 0 && input.readiness.arc.open === null && input.readiness.arc.looseCommits.length > 0)
501
+ findings.push({ code: "LOOSE_COMMITS", detail: `${input.readiness.arc.looseCommits.length} loose commits after fence`, nextCommand: `printf '# Continue\\n\\n## Objective\\nReceipt loose commits.\\n\\n## Brief\\nContinue delivery.\\n' | ${selector} arc` });
502
+ if (input.readiness.renewSession.open)
503
+ findings.push({ code: "RENEW_SESSION_OPEN", detail: "renew recovery session is open", nextCommand: `${selector} renew --continue` });
504
+ if (input.readiness.rewriteGuard.liveProjection)
505
+ findings.push({ code: "LIVE_REWRITE_GUARD", detail: "a live projection owns the delivery worktree", nextCommand: "keiyaku status" });
506
+ for (const dependency of input.readiness.dependencies.filter((item) => !item.satisfied))
507
+ findings.push({ code: "DEPENDENCY_OPEN", detail: `dependency ${dependency.id} is ${dependency.state}`, nextCommand: `keiyaku @${dependency.id} log` });
508
+ if (input.readiness.petition.state === "petitioned" && input.readiness.petition.queue?.position !== 1)
509
+ findings.push({ code: "PETITION_QUEUE", detail: `petition queue position ${input.readiness.petition.queue?.position ?? "unknown"}`, nextCommand: "keiyaku status" });
510
+ return findings;
511
+ }
512
+ function coordinateRaceReport(input) {
513
+ const findings = [{
514
+ code: "COORDINATE_RACE",
515
+ detail: input.detail,
516
+ nextCommand: `keiyaku @${input.contractId} audit`,
517
+ }];
518
+ return {
519
+ contract: input.contractId,
520
+ coordinates: input.pinned.coordinates,
521
+ terms: { criteria: input.pinned.criteria, scope: effectiveScope(input.pinned.entries), appendScope: appendedScopeTerms(input.pinned.entries) },
522
+ rehearsal: { status: "unknown", cause: { code: "COORDINATE_RACE", message: input.detail } },
523
+ candidate: null,
524
+ diff: null,
525
+ presentation: null,
526
+ files: null,
527
+ scope: null,
528
+ verification: { status: "not_evaluated", reason: "coordinate_race", results: [] },
529
+ readiness: null,
530
+ findings,
531
+ counts: { files: 0, selected: 0, omitted: 0, inScope: 0, downgraded: 0, violated: 0, findings: findings.length },
532
+ };
533
+ }
534
+ export async function auditContract(input) {
535
+ const contractId = input.address.binding.commissionId;
536
+ const root = input.address.repo.stableRoot;
537
+ const deliveryRef = `refs/heads/keiyaku/${contractId}`;
538
+ const pinned = await pinCoordinates({ cwd: root, contractId, deliveryRef });
539
+ let deliveryCwd;
540
+ let pinnedDeliveryCoordinate;
541
+ try {
542
+ deliveryCwd = await resolveContractDeliveryCwd(input.cwd, contractId);
543
+ pinnedDeliveryCoordinate = await deliveryCoordinate(deliveryCwd);
544
+ }
545
+ catch (error) {
546
+ return coordinateRaceReport({ contractId, pinned, detail: `registered delivery worktree could not be read at pinned coordinate: ${asMessage(error)}` });
547
+ }
548
+ if (pinnedDeliveryCoordinate.head !== pinned.coordinates.delivery.head || pinnedDeliveryCoordinate.tree !== pinned.coordinates.delivery.tree) {
549
+ return coordinateRaceReport({
550
+ contractId,
551
+ pinned,
552
+ detail: `registered delivery worktree ${pinnedDeliveryCoordinate.head}/${pinnedDeliveryCoordinate.tree} does not match pinned delivery ref ${pinned.coordinates.delivery.head}/${pinned.coordinates.delivery.tree}`,
553
+ });
554
+ }
555
+ const readiness = await evaluateReadiness({ root, deliveryCwd, contractId, pinned });
556
+ const disposable = await createCandidate(root, pinned, contractId);
557
+ let diff = null;
558
+ let scope = null;
559
+ let presentation = null;
560
+ let files = null;
561
+ let reportCandidate = null;
562
+ let verification;
563
+ try {
564
+ if (disposable.candidate) {
565
+ const rawDiff = await readStructuredDiff(disposable.cwd, disposable.candidate.base, disposable.candidate.head, { excludePaths: [path.join(".keiyaku", "KEIYAKU.md")] });
566
+ const artifactDiff = { ...rawDiff, ...(disposable.candidate.rewritten ? { to: "renew-artifact" } : {}) };
567
+ const artifact = await writeDiffArtifact({
568
+ base: disposable.candidate.base,
569
+ deliveryHead: pinned.coordinates.delivery.head,
570
+ targetHead: pinned.coordinates.target.head,
571
+ diff: artifactDiff,
572
+ });
573
+ const stamp = path.basename(artifact, ".json");
574
+ reportCandidate = disposable.candidate.rewritten
575
+ ? { kind: "renew-artifact", base: disposable.candidate.base, deliveryHead: pinned.coordinates.delivery.head, targetHead: pinned.coordinates.target.head, artifact, stamp }
576
+ : { kind: "delivery", base: disposable.candidate.base, head: disposable.candidate.head, tree: disposable.candidate.tree };
577
+ diff = { ...rawDiff, ...(disposable.candidate.rewritten ? { to: `artifact:${stamp}` } : {}) };
578
+ scope = await evaluateScope(disposable.cwd, pinned.entries, diff);
579
+ presentation = presentDiff(diff, scope, input.diffBudgetBytes ?? 32_000, artifact);
580
+ files = fileFacts(diff, scope);
581
+ verification = await evaluateVerification(disposable.cwd, disposable.candidate.rewritten ? `artifact:${stamp}` : disposable.candidate.head);
582
+ }
583
+ else {
584
+ verification = { status: "not_evaluated", reason: disposable.rehearsal.status === "conflict" ? "renew_conflict" : "renew_unknown", results: [] };
585
+ }
586
+ await input.afterEvidence?.();
587
+ }
588
+ finally {
589
+ await fs.rm(disposable.cwd, { recursive: true, force: true });
590
+ }
591
+ let rechecked = null;
592
+ let finalCoordinateReadFailure = null;
593
+ try {
594
+ rechecked = await pinCoordinates({ cwd: root, contractId, deliveryRef });
595
+ }
596
+ catch (error) {
597
+ finalCoordinateReadFailure = asMessage(error);
598
+ }
599
+ let deliveryCoordinateChanged = false;
600
+ let deliveryPathChanged = false;
601
+ try {
602
+ const recheckedDeliveryCwd = await resolveContractDeliveryCwd(input.cwd, contractId);
603
+ deliveryPathChanged = path.resolve(deliveryCwd) !== path.resolve(recheckedDeliveryCwd);
604
+ }
605
+ catch {
606
+ deliveryPathChanged = true;
607
+ }
608
+ try {
609
+ const recheckedDeliveryCoordinate = await deliveryCoordinate(deliveryCwd);
610
+ deliveryCoordinateChanged =
611
+ pinnedDeliveryCoordinate.head !== recheckedDeliveryCoordinate.head
612
+ || pinnedDeliveryCoordinate.tree !== recheckedDeliveryCoordinate.tree
613
+ || pinnedDeliveryCoordinate.residueFingerprint !== recheckedDeliveryCoordinate.residueFingerprint;
614
+ }
615
+ catch {
616
+ deliveryCoordinateChanged = true;
617
+ }
618
+ const findings = collectFindings({ contractId, deliveryCwd, rehearsal: disposable.rehearsal, scope, verification, readiness });
619
+ const changes = rechecked ? coordinateChanges(pinned.coordinates, rechecked.coordinates) : [`final coordinate reread failed: ${finalCoordinateReadFailure}`];
620
+ if (deliveryPathChanged)
621
+ changes.push("delivery worktree coordinate changed");
622
+ if (deliveryCoordinateChanged)
623
+ changes.push("delivery worktree bytes or HEAD changed");
624
+ if (changes.length > 0)
625
+ findings.push({ code: "COORDINATE_RACE", detail: `audit coordinates changed while evidence was collected: ${changes.join("; ")}`, nextCommand: `keiyaku @${contractId} audit` });
626
+ const countScope = (klass) => scope?.filter((row) => row.class === klass).length ?? 0;
627
+ return {
628
+ contract: contractId,
629
+ coordinates: pinned.coordinates,
630
+ terms: { criteria: pinned.criteria, scope: effectiveScope(pinned.entries), appendScope: appendedScopeTerms(pinned.entries) },
631
+ rehearsal: disposable.rehearsal,
632
+ candidate: reportCandidate,
633
+ diff,
634
+ presentation,
635
+ files,
636
+ scope,
637
+ verification,
638
+ readiness,
639
+ findings,
640
+ counts: {
641
+ files: files?.length ?? 0,
642
+ selected: presentation?.selected.length ?? 0,
643
+ omitted: presentation?.omissions.length ?? 0,
644
+ inScope: countScope("in-scope"),
645
+ downgraded: countScope("downgraded"),
646
+ violated: countScope("violated"),
647
+ findings: findings.length,
648
+ },
649
+ };
650
+ }
@@ -119,7 +119,7 @@ function incompleteTaskBindError(taskId, missing) {
119
119
  const template = renderMissingBindSectionsTemplate(missing);
120
120
  const message = [
121
121
  `task ${coordinate} carries no ${describeMissingBindSections(missing)}`,
122
- `» keiyaku bind --task ${coordinate} <<'KEIYAKU'`,
122
+ `» keiyaku bind --task ${coordinate} - <<'KEIYAKU'`,
123
123
  template,
124
124
  "KEIYAKU",
125
125
  ].join("\n");
@@ -159,6 +159,7 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
159
159
  task: options.task ?? prompt,
160
160
  launchSnapshot,
161
161
  idleTimeoutMs: resolvedTerms.idleTimeoutMs,
162
+ executionTimeoutMs: resolvedTerms.executionTimeoutMs,
162
163
  cwd,
163
164
  historyCwd: options.historyStorageCwd ?? cwd,
164
165
  ...(options.evidenceCwd !== undefined ? { evidenceCwd: options.evidenceCwd } : {}),