@astrosheep/keiyaku 2.9.4 → 2.9.6

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 (143) 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 +31 -5
  5. package/build/agents/harness/projection.js +151 -78
  6. package/build/agents/harness/router.js +2 -1
  7. package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
  8. package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
  9. package/build/agents/providers/codex-app-server/adapter.js +43 -10
  10. package/build/agents/providers/codex-app-server/events.js +23 -0
  11. package/build/agents/providers/codex-app-server/registration.js +2 -1
  12. package/build/agents/providers/opencode-sdk/adapter.js +7 -0
  13. package/build/agents/providers/opencode-sdk/registration.js +2 -1
  14. package/build/agents/providers/pi/adapter.js +10 -0
  15. package/build/agents/providers/pi/events.js +4 -3
  16. package/build/agents/providers/pi/registration.js +3 -1
  17. package/build/agents/types.js +2 -0
  18. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  19. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  20. package/build/cli/commands/akuma/view/meta.js +1 -1
  21. package/build/cli/commands/contract/amend/meta.js +4 -4
  22. package/build/cli/commands/contract/arc/handler.js +1 -1
  23. package/build/cli/commands/contract/arc/meta.js +4 -4
  24. package/build/cli/commands/contract/audit/handler.js +16 -0
  25. package/build/cli/commands/contract/audit/meta.js +16 -0
  26. package/build/cli/commands/contract/bind/meta.js +20 -11
  27. package/build/cli/commands/contract/catalog.js +2 -0
  28. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  29. package/build/cli/commands/contract/log/meta.js +1 -1
  30. package/build/cli/commands/contract/petition/handler.js +1 -2
  31. package/build/cli/commands/contract/petition/meta.js +6 -6
  32. package/build/cli/commands/contract/renew/handler.js +2 -2
  33. package/build/cli/commands/contract/renew/meta.js +4 -4
  34. package/build/cli/commands/metadata.js +3 -2
  35. package/build/cli/commands/projection/call/meta.js +4 -1
  36. package/build/cli/commands/projection/kill/meta.js +1 -1
  37. package/build/cli/commands/projection/revive/meta.js +4 -1
  38. package/build/cli/commands/projection/status/meta.js +1 -1
  39. package/build/cli/commands/projection/tell/handler.js +1 -1
  40. package/build/cli/commands/projection/tell/meta.js +4 -1
  41. package/build/cli/commands/projection/wait/meta.js +1 -1
  42. package/build/cli/commands/skills/install/meta.js +1 -1
  43. package/build/cli/commands/skills/skills/meta.js +1 -1
  44. package/build/cli/commands/system/completion/meta.js +1 -1
  45. package/build/cli/commands/system/dump-env/meta.js +1 -1
  46. package/build/cli/commands/system/guide/meta.js +1 -1
  47. package/build/cli/commands/task/add/handler.js +2 -2
  48. package/build/cli/commands/task/add/meta.js +4 -1
  49. package/build/cli/commands/task/doctor/meta.js +1 -1
  50. package/build/cli/commands/task/done/meta.js +1 -1
  51. package/build/cli/commands/task/drop/meta.js +1 -1
  52. package/build/cli/commands/task/ls/handler.js +10 -4
  53. package/build/cli/commands/task/ls/meta.js +3 -3
  54. package/build/cli/commands/task/show/meta.js +1 -1
  55. package/build/cli/commands/task/start/meta.js +1 -1
  56. package/build/cli/commands/task/stop/meta.js +1 -1
  57. package/build/cli/commands/task/task/meta.js +1 -1
  58. package/build/cli/commands/task/update/handler.js +2 -3
  59. package/build/cli/commands/task/update/meta.js +4 -1
  60. package/build/cli/completion.js +9 -2
  61. package/build/cli/flags.js +19 -3
  62. package/build/cli/help.js +3 -3
  63. package/build/cli/index.js +59 -75
  64. package/build/cli/output-stream-error-policy.js +19 -5
  65. package/build/cli/parse-flags.js +19 -2
  66. package/build/cli/parse-metadata.js +1 -7
  67. package/build/cli/parse-selectors.js +1 -0
  68. package/build/cli/parse.js +16 -7
  69. package/build/cli/projection-address.js +6 -1
  70. package/build/cli/render/audit.js +121 -0
  71. package/build/cli/render/errors.js +18 -1
  72. package/build/cli/render/format.js +5 -2
  73. package/build/cli/render/misc.js +3 -1
  74. package/build/cli/render/projection-activity.js +12 -6
  75. package/build/cli/render/status.js +28 -18
  76. package/build/cli/render/success-response.js +6 -5
  77. package/build/cli/render/task.js +12 -43
  78. package/build/cli/render/terminal-failure.js +7 -2
  79. package/build/cli/render/wait.js +36 -8
  80. package/build/config/env-keys.js +2 -0
  81. package/build/config/env.js +3 -0
  82. package/build/core/amend.js +20 -94
  83. package/build/core/arc.js +14 -6
  84. package/build/core/audit/candidate.js +48 -0
  85. package/build/core/audit/coordinates.js +180 -0
  86. package/build/core/audit/evidence.js +158 -0
  87. package/build/core/audit/facade.js +42 -0
  88. package/build/core/audit/readiness.js +4 -0
  89. package/build/core/audit/report.js +38 -0
  90. package/build/core/audit.js +1 -0
  91. package/build/core/bind.js +1 -1
  92. package/build/core/call/execution.js +1 -0
  93. package/build/core/contract-view.js +2 -3
  94. package/build/core/draft.js +33 -20
  95. package/build/core/entry.js +3 -4
  96. package/build/core/markdown/lex.js +2 -1
  97. package/build/core/projection/generation/database.js +41 -16
  98. package/build/core/projection/generation/model.js +22 -2
  99. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  100. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  101. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  102. package/build/core/projection/generation/store.js +1 -1
  103. package/build/core/projection/generation/transitions.js +19 -3
  104. package/build/core/projection/index.js +2 -2
  105. package/build/core/projection/projection-execution-observer.js +35 -21
  106. package/build/core/projection/projection-life-observer.js +16 -2
  107. package/build/core/projection/projection-status.js +67 -22
  108. package/build/core/projection/projection-terminal-failure.js +2 -0
  109. package/build/core/projection/projection-wait.js +6 -9
  110. package/build/core/projection/projection-wake.js +30 -6
  111. package/build/core/renew.js +41 -22
  112. package/build/core/scope.js +129 -2
  113. package/build/core/seal.js +36 -15
  114. package/build/core/settlement/petition-claim-gates.js +4 -33
  115. package/build/core/settlement/petition-preview.js +1 -1
  116. package/build/core/settlement/petition.js +10 -3
  117. package/build/core/settlement/settlement.js +3 -10
  118. package/build/core/settlement/verification.js +59 -0
  119. package/build/core/status/board.js +48 -28
  120. package/build/core/stored-agent-event.js +3 -2
  121. package/build/core/task/board.js +1 -0
  122. package/build/core/task/commands.js +20 -12
  123. package/build/core/task/document.js +5 -0
  124. package/build/core/task/index.js +1 -1
  125. package/build/core/task/model.js +8 -1
  126. package/build/core/task/query.js +214 -0
  127. package/build/core/task/settlement-git.js +115 -16
  128. package/build/core/task/settlement-policy.js +17 -9
  129. package/build/core/task/source-board.js +1 -0
  130. package/build/core/task/task-contract.js +125 -51
  131. package/build/core/task/task-store-repository.js +1 -0
  132. package/build/core/task/task.js +2 -0
  133. package/build/flow-error.js +2 -4
  134. package/build/generated/version.js +2 -2
  135. package/build/git/diff/parsers.js +23 -13
  136. package/build/git/diff/structured.js +127 -0
  137. package/build/index.js +7 -7
  138. package/build/telemetry/logger.js +4 -2
  139. package/package.json +1 -1
  140. package/skills/keiyaku/SKILL.md +2 -2
  141. package/skills/keiyaku-akuma/SKILL.md +17 -6
  142. package/skills/keiyaku-task/SKILL.md +16 -7
  143. package/skills/keiyaku-workflow/SKILL.md +48 -13
@@ -3,7 +3,7 @@ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, parseRevList
3
3
  import { readLedger } from "./ledger.js";
4
4
  import { hasStrandableProjectionGeneration } from "./projection/index.js";
5
5
  import { refreshContractView } from "./contract-view.js";
6
- import { appendRenewSettlement, captureRenewSeal, captureSeal, findOpenArc, getFence, getMaxArc, receiptsAfterFence } from "./seal.js";
6
+ import { appendRenewSettlement, appendScopeTerms, captureRenewSeal, captureSeal, findOpenArc, getFence, getMaxArc, receiptsAfterFence } from "./seal.js";
7
7
  import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
8
8
  import { commissionTarget, latestBase } from "./status/drift.js";
9
9
  import { findContractWorktreePath, findRegisteredContractWorktreePath, stableRepoRoot } from "./worktree-path.js";
@@ -13,7 +13,7 @@ import { createRecoveryRef, deleteRecoveryRef } from "../git/refs.js";
13
13
  import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
14
14
  import { ACTOR_IDENTITY } from "../keiyaku.js";
15
15
  import { persistResponseHistory } from "./transcripts.js";
16
- import { convergeRenewRecoverySession, readRenewRecoverySession, removeRenewRecoverySession, writeRenewRecoverySession, } from "./renew-session.js";
16
+ import { convergeRenewRecoverySession, assertNoRenewRecoverySession, removeRenewRecoverySession, writeRenewRecoverySession, } from "./renew-session.js";
17
17
  function assertOperable(contractId, entries) {
18
18
  const state = deriveContractState(entries);
19
19
  if (state.state === "corrupt") {
@@ -47,7 +47,7 @@ const RENEW_RECOVERY_REF_PREFIX = "refs/keiyaku/recovery/renew";
47
47
  function renewRecoveryRef(contractId, oldHead) {
48
48
  return `${RENEW_RECOVERY_REF_PREFIX}/${contractId}/${oldHead}`;
49
49
  }
50
- async function rewriteBranch(input) {
50
+ export async function rewriteRenewBranch(input) {
51
51
  const { cwd, contractId, oldHead, recoveryRef, newBase, oldBase, branch, preserveConflict } = input;
52
52
  const git = createGit(cwd);
53
53
  const originalBranch = branch ? await getCurrentBranch(cwd) : undefined;
@@ -160,7 +160,7 @@ async function assertRenewCoordinates(input) {
160
160
  }
161
161
  async function finishRenew(input) {
162
162
  const capture = input.capture === undefined
163
- ? await captureSeal(input.repositoryRoot, input.entries, input.oldHead, input.waivePaths)
163
+ ? await captureSeal(input.repositoryRoot, input.entries, input.oldHead)
164
164
  : input.capture;
165
165
  const settlement = await appendRenewSettlement({
166
166
  cwd: input.repositoryRoot,
@@ -198,11 +198,7 @@ export async function renewContract(input) {
198
198
  const repositoryRoot = await stableRepoRoot(input.cwd);
199
199
  const worktreePath = await findContractWorktreePath(repositoryRoot, input.contractId);
200
200
  const rewriteCwd = worktreePath ?? input.cwd;
201
- if (await readRenewRecoverySession(repositoryRoot, input.contractId)) {
202
- throw new FlowError("RENEW_SESSION_OPEN", `renew recovery session is open for ${input.contractId}`, {
203
- facts: { kind: "renew_session", commission: input.contractId, nextAction: "continue-or-abort" },
204
- });
205
- }
201
+ await assertNoRenewRecoverySession(repositoryRoot, input.contractId);
206
202
  if (input.resolveConflicts && !worktreePath) {
207
203
  throw new FlowError("INTERNAL_STATE", `renew --resolve requires ${input.contractId}'s registered contract worktree`);
208
204
  }
@@ -233,12 +229,27 @@ export async function renewContract(input) {
233
229
  targetHead,
234
230
  entries: ledger.entries,
235
231
  });
236
- const capture = await captureSeal(input.cwd, ledger.entries, oldHead, input.waivePaths);
237
232
  if (getMaxArc(ledger.entries) > 0 && !findOpenArc(ledger.entries)) {
238
233
  const loose = await receiptsAfterFence(input.cwd, ledger.entries, oldHead);
239
234
  if (loose.length > 0)
240
235
  throw looseCommitError(input.contractId, loose);
241
236
  }
237
+ if (oldBase !== targetHead) {
238
+ const deliveryWorktree = worktreePath ?? repositoryRoot;
239
+ await assertNoInFlightProjection(repositoryRoot, input.contractId, deliveryWorktree, input.nowMs);
240
+ }
241
+ await appendScopeTerms({
242
+ cwd: repositoryRoot,
243
+ renderCwd: rewriteCwd,
244
+ contractId: input.contractId,
245
+ patterns: input.appendScope,
246
+ actor: input.actor,
247
+ nowMs: input.nowMs,
248
+ });
249
+ const currentLedger = input.appendScope?.length ? await readLedger(repositoryRoot, input.contractId) : ledger;
250
+ if (!currentLedger)
251
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} disappeared while appending scope`);
252
+ const capture = await captureSeal(input.cwd, currentLedger.entries, oldHead);
242
253
  if (oldBase === targetHead) {
243
254
  return {
244
255
  contractId: input.contractId,
@@ -246,14 +257,12 @@ export async function renewContract(input) {
246
257
  newBase: targetHead,
247
258
  oldHead,
248
259
  newHead: oldHead,
249
- ledgerHead: ledger.head,
260
+ ledgerHead: currentLedger.head,
250
261
  alreadyFresh: true,
251
262
  sealed: null,
252
263
  recoveryCleanup: null,
253
264
  };
254
265
  }
255
- const deliveryWorktree = worktreePath ?? repositoryRoot;
256
- await assertNoInFlightProjection(repositoryRoot, input.contractId, deliveryWorktree, input.nowMs);
257
266
  const recoveryRef = renewRecoveryRef(input.contractId, oldHead);
258
267
  await createRecoveryRef(input.cwd, recoveryRef, oldHead);
259
268
  if (input.resolveConflicts) {
@@ -266,7 +275,7 @@ export async function renewContract(input) {
266
275
  });
267
276
  }
268
277
  try {
269
- await rewriteBranch({
278
+ await rewriteRenewBranch({
270
279
  cwd: rewriteCwd,
271
280
  contractId: input.contractId,
272
281
  oldHead,
@@ -287,8 +296,8 @@ export async function renewContract(input) {
287
296
  return await finishRenew({
288
297
  repositoryRoot,
289
298
  rewriteCwd,
290
- ledgerHead: ledger.head,
291
- entries: ledger.entries,
299
+ ledgerHead: currentLedger.head,
300
+ entries: currentLedger.entries,
292
301
  contractId: input.contractId,
293
302
  oldBase,
294
303
  oldHead,
@@ -390,19 +399,29 @@ export async function continueRenewContract(input) {
390
399
  facts: { kind: "renew_session", commission: input.contractId, nextAction: "abort" },
391
400
  });
392
401
  }
402
+ await appendScopeTerms({
403
+ cwd: repositoryRoot,
404
+ renderCwd: rewriteCwd,
405
+ contractId: input.contractId,
406
+ patterns: input.appendScope,
407
+ actor: input.actor,
408
+ nowMs: input.nowMs,
409
+ });
410
+ const currentLedger = input.appendScope?.length ? await readLedger(repositoryRoot, input.contractId) : ledger;
411
+ if (!currentLedger)
412
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} disappeared while appending scope`);
393
413
  const capture = await captureRenewSeal({
394
414
  cwd: repositoryRoot,
395
- entries: ledger.entries,
415
+ entries: currentLedger.entries,
396
416
  receiptHead: session.oldHead,
397
417
  contentBase: session.newBase,
398
418
  contentHead: newHead,
399
- waivePaths: input.waivePaths,
400
419
  });
401
420
  return await finishRenew({
402
421
  repositoryRoot,
403
422
  rewriteCwd,
404
- ledgerHead: ledger.head,
405
- entries: ledger.entries,
423
+ ledgerHead: currentLedger.head,
424
+ entries: currentLedger.entries,
406
425
  contractId: input.contractId,
407
426
  oldBase: session.oldBase,
408
427
  oldHead: session.oldHead,
@@ -469,7 +488,7 @@ export async function renewKeiyaku(input) {
469
488
  contractId,
470
489
  actor: ACTOR_IDENTITY,
471
490
  nowMs: Date.now(),
472
- waivePaths: input.waivePaths,
491
+ appendScope: input.appendScope,
473
492
  resolveConflicts: input.resolveConflicts,
474
493
  });
475
494
  return await buildRenewOutcome(input.cwd, address, renewed);
@@ -530,7 +549,7 @@ export async function continueRenewKeiyaku(input) {
530
549
  contractId: input.address.binding.commissionId,
531
550
  actor: ACTOR_IDENTITY,
532
551
  nowMs: Date.now(),
533
- waivePaths: input.waivePaths,
552
+ appendScope: input.appendScope,
534
553
  })));
535
554
  }
536
555
  export async function abortRenewKeiyaku(input) {
@@ -1,3 +1,4 @@
1
+ import { FlowError } from "../flow-error.js";
1
2
  export class ScopePatternError extends Error {
2
3
  code = "INVALID_SCOPE_PATTERN";
3
4
  constructor(message) {
@@ -31,10 +32,136 @@ function stripUnescapedTrailingSpaces(value) {
31
32
  function canonicalizeScopePatternInput(pattern) {
32
33
  return stripUnescapedTrailingSpaces(pattern.replace(/^\s+/, ""));
33
34
  }
34
- /** True when a raw Scope Add / list line has no pattern after gitignore blank rules. */
35
+ /** True when a raw scope line has no pattern after gitignore blank rules. */
35
36
  export function isBlankScopePatternLine(line) {
36
37
  return canonicalizeScopePatternInput(line).length === 0;
37
38
  }
39
+ /** Parse Keiyaku's source-line Scope opener, not a normalized Markdown node. */
40
+ export function parseScopeFenceOpening(line) {
41
+ const match = line.match(/^(`{3,})$/);
42
+ return match ? { length: match[1].length } : null;
43
+ }
44
+ /** Keiyaku closer: column-zero backticks, at least opener length, then whitespace. */
45
+ export function isScopeFenceClosing(line, fence) {
46
+ const match = line.match(/^(`+)[ \t]*$/);
47
+ return Boolean(match && match[1].length >= fence.length);
48
+ }
49
+ function fencedScopeError(heading, detail) {
50
+ return new FlowError("INVALID_KEIYAKU_DRAFT", `${heading} must contain exactly one fenced gitignore-subset block; ${detail}. ` +
51
+ `Paste-ready canonical example:\n\n${heading}\n\`\`\`\nfoo\nbar\n\`\`\``);
52
+ }
53
+ /** Validate and normalize the one fenced block that constitutes a Scope body. */
54
+ export function validateScopeFragmentLines(lines, options) {
55
+ const first = lines.findIndex((line) => line.trim().length > 0);
56
+ if (first === -1)
57
+ throw fencedScopeError(options.heading, "found no fenced block");
58
+ const fence = parseScopeFenceOpening(lines[first]);
59
+ if (!fence)
60
+ throw fencedScopeError(options.heading, `offending line '${lines[first]}'`);
61
+ let close = -1;
62
+ for (let index = first + 1; index < lines.length; index += 1) {
63
+ if (isScopeFenceClosing(lines[index], fence)) {
64
+ close = index;
65
+ break;
66
+ }
67
+ }
68
+ if (close === -1)
69
+ throw fencedScopeError(options.heading, "the fenced block is not closed");
70
+ const outside = lines.slice(close + 1).find((line) => line.trim().length > 0);
71
+ if (outside !== undefined) {
72
+ throw fencedScopeError(options.heading, `offending line '${outside}' appears outside the fenced block`);
73
+ }
74
+ const patterns = [];
75
+ for (const line of lines.slice(first + 1, close)) {
76
+ if (isBlankScopePatternLine(line))
77
+ continue;
78
+ try {
79
+ patterns.push(normalizeScopePattern(line));
80
+ }
81
+ catch (error) {
82
+ if (error instanceof ScopePatternError) {
83
+ throw new FlowError("INVALID_KEIYAKU_DRAFT", error.message);
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+ if (patterns.length === 0)
89
+ throw new FlowError("INVALID_KEIYAKU_DRAFT", options.emptyMessage);
90
+ return patterns;
91
+ }
92
+ function sourceFenceOpening(line) {
93
+ const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
94
+ if (!match)
95
+ return null;
96
+ const marker = match[1];
97
+ if (marker[0] === "`" && match[2].includes("`"))
98
+ return null;
99
+ return { delimiter: marker[0], length: marker.length };
100
+ }
101
+ function sourceFenceClosing(line, fence) {
102
+ const match = line.match(/^ {0,3}(`+|~+)[ \t]*$/);
103
+ return Boolean(match && match[1][0] === fence.delimiter && match[1].length >= fence.length);
104
+ }
105
+ /** Locate structural H1/H2 source ranges while leaving fenced headings inert. */
106
+ export function markdownSourceSections(markdown) {
107
+ const lines = [];
108
+ let start = 0;
109
+ while (start < markdown.length) {
110
+ const newline = markdown.indexOf("\n", start);
111
+ const end = newline === -1 ? markdown.length : newline;
112
+ const next = newline === -1 ? markdown.length : newline + 1;
113
+ const raw = markdown.slice(start, end);
114
+ lines.push({ text: raw.endsWith("\r") ? raw.slice(0, -1) : raw, start, next });
115
+ start = next;
116
+ }
117
+ let frontMatterEnd = -1;
118
+ if (lines[0]?.text.trim() === "---") {
119
+ frontMatterEnd = lines.findIndex((line, index) => index > 0 && line.text.trim() === "---");
120
+ }
121
+ const headings = [];
122
+ let fence = null;
123
+ for (let index = 0; index < lines.length; index += 1) {
124
+ const line = lines[index];
125
+ if (index <= frontMatterEnd)
126
+ continue;
127
+ if (fence) {
128
+ if (sourceFenceClosing(line.text, fence))
129
+ fence = null;
130
+ continue;
131
+ }
132
+ const opening = sourceFenceOpening(line.text);
133
+ if (opening) {
134
+ fence = opening;
135
+ continue;
136
+ }
137
+ // Column-0 only: indented ATX-looking lines inside list items remain content.
138
+ const heading = line.text.match(/^(#{1,2})(?!#)[ \t]+(.+)$/);
139
+ if (!heading)
140
+ continue;
141
+ headings.push({
142
+ level: heading[1].length,
143
+ title: heading[2],
144
+ start: line.start,
145
+ contentStart: line.next,
146
+ });
147
+ }
148
+ return headings.map((heading, index) => {
149
+ const next = heading.level === 2
150
+ ? headings.slice(index + 1).find((candidate) => candidate.level === 2)
151
+ : headings[index + 1];
152
+ return { ...heading, end: next?.start ?? markdown.length };
153
+ });
154
+ }
155
+ /** Render canonical Scope bytes with a fence that content cannot close. */
156
+ export function renderScopeFragment(patterns) {
157
+ let longestRun = 0;
158
+ for (const pattern of patterns) {
159
+ for (const run of pattern.match(/`+/g) ?? [])
160
+ longestRun = Math.max(longestRun, run.length);
161
+ }
162
+ const fence = "`".repeat(Math.max(3, longestRun + 1));
163
+ return `${fence}\n${patterns.join("\n")}\n${fence}`;
164
+ }
38
165
  /**
39
166
  * Canonicalize a scope Gitignore-like pattern for matching and persistence.
40
167
  * Strips only a cosmetic leading `./` (including after `!`); preserves meaningful
@@ -57,7 +184,7 @@ export function normalizeScopePattern(pattern) {
57
184
  return negative ? `!${body}` : body;
58
185
  }
59
186
  /**
60
- * Normalize a path to Git diff form for exact waiver comparison:
187
+ * Normalize a path to Git diff form for matcher comparison:
61
188
  * either slash convention, collapse separators, and strip cosmetic `./` or `/`.
62
189
  * Leading and trailing spaces are significant path bytes and must remain unchanged.
63
190
  */
@@ -4,7 +4,7 @@ import { KEIYAKU_FILE } from "../keiyaku.js";
4
4
  import { formatLedgerEntryCommitMessage } from "./entry.js";
5
5
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
6
6
  import { refreshContractView } from "./contract-view.js";
7
- import { normalizeScopePath, scopePathEvidence, } from "./scope.js";
7
+ import { normalizeScopePattern, scopePathEvidence, } from "./scope.js";
8
8
  import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
9
9
  import { casUpdateRefLog, createRefLogCommit } from "./ref-log.js";
10
10
  function findBind(entries) {
@@ -103,20 +103,41 @@ async function scopeViolations(cwd, paths, scope) {
103
103
  const auditedPaths = paths.filter((changedPath) => changedPath !== KEIYAKU_FILE);
104
104
  return (await scopePathEvidence(cwd, scope, auditedPaths)).filter((evidence) => !evidence.matched);
105
105
  }
106
- async function auditScope(cwd, entries, paths, waivePaths) {
106
+ async function auditScope(cwd, entries, paths) {
107
107
  const scope = effectiveScope(entries);
108
108
  const scopeViolationEvidence = await scopeViolations(cwd, paths, scope);
109
- const waivedPaths = new Set(waivePaths.map(normalizeScopePath));
110
- const unwaived = scopeViolationEvidence.filter((violation) => !waivedPaths.has(violation.path));
111
- if (unwaived.length > 0) {
112
- throw new ScopeAuditError(findBind(entries).contract, scope, unwaived);
109
+ if (scopeViolationEvidence.length > 0) {
110
+ throw new ScopeAuditError(findBind(entries).contract, scope, scopeViolationEvidence);
113
111
  }
114
112
  const violations = scopeViolationEvidence.map((violation) => violation.path);
115
113
  return {
116
- scopeAudit: { violations, waived: violations.length > 0 },
114
+ scopeAudit: { violations },
117
115
  scopeViolationEvidence,
118
116
  };
119
117
  }
118
+ const APPEND_SCOPE_AMENDMENT = "Scope extended by lifecycle command.";
119
+ export async function appendScopeTerms(input) {
120
+ if (!input.patterns || input.patterns.length === 0)
121
+ return null;
122
+ const patterns = input.patterns.map(normalizeScopePattern);
123
+ const at = new Date(input.nowMs).toISOString();
124
+ const result = await appendWithRetry(input.cwd, activeLedgerRef(input.contractId), (_head, entries) => {
125
+ assertOperable(input.contractId, entries);
126
+ return {
127
+ v: 1,
128
+ kind: "amend",
129
+ contract: input.contractId,
130
+ at,
131
+ actor: input.actor,
132
+ data: { amendment: APPEND_SCOPE_AMENDMENT, scopeDelta: { add: patterns } },
133
+ };
134
+ });
135
+ if (!result.ok) {
136
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} active ledger disappeared while appending scope`);
137
+ }
138
+ await refreshContractView({ ledgerCwd: input.cwd, renderCwd: input.renderCwd, contractId: input.contractId });
139
+ return result.head;
140
+ }
120
141
  export class ScopeAuditError extends FlowError {
121
142
  scopeViolationEvidence;
122
143
  constructor(_contractId, scope, evidence) {
@@ -159,7 +180,7 @@ export function petitionSealSource(entries) {
159
180
  };
160
181
  }
161
182
  /** Shared read-only owner for the seal capture and its compact changed-path evidence. */
162
- export async function previewSeal(cwd, entries, head = "HEAD", waivePaths = []) {
183
+ export async function previewSeal(cwd, entries, head = "HEAD") {
163
184
  const arc = currentSealArc(entries);
164
185
  if (arc === null)
165
186
  return { capture: null, paths: [] };
@@ -170,7 +191,7 @@ export async function previewSeal(cwd, entries, head = "HEAD", waivePaths = [])
170
191
  changedPaths(cwd, oldFence, newFence),
171
192
  diffstat(cwd, oldFence, newFence),
172
193
  ]);
173
- const scopeAudit = await auditScope(cwd, entries, paths, waivePaths);
194
+ const scopeAudit = await auditScope(cwd, entries, paths);
174
195
  return {
175
196
  capture: {
176
197
  arc,
@@ -183,8 +204,8 @@ export async function previewSeal(cwd, entries, head = "HEAD", waivePaths = [])
183
204
  paths,
184
205
  };
185
206
  }
186
- export async function captureSeal(cwd, entries, head = "HEAD", waivePaths = []) {
187
- return (await previewSeal(cwd, entries, head, waivePaths)).capture;
207
+ export async function captureSeal(cwd, entries, head = "HEAD") {
208
+ return (await previewSeal(cwd, entries, head)).capture;
188
209
  }
189
210
  /** Preserve original receipt hashes while auditing the rebased delivery content. */
190
211
  export async function captureRenewSeal(input) {
@@ -208,7 +229,7 @@ export async function captureRenewSeal(input) {
208
229
  newFence: receiptHead,
209
230
  receipts,
210
231
  diffstat: stat,
211
- ...await auditScope(input.cwd, input.entries, paths, input.waivePaths ?? []),
232
+ ...await auditScope(input.cwd, input.entries, paths),
212
233
  };
213
234
  }
214
235
  export async function sealCurrentArc(input) {
@@ -217,7 +238,7 @@ export async function sealCurrentArc(input) {
217
238
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
218
239
  }
219
240
  assertOperable(input.contractId, ledger.entries);
220
- const capture = input.capture ?? await captureSeal(input.cwd, ledger.entries, "HEAD", input.waivePaths);
241
+ const capture = input.capture ?? await captureSeal(input.cwd, ledger.entries, "HEAD");
221
242
  if (!capture)
222
243
  return null;
223
244
  const at = new Date(input.nowMs).toISOString();
@@ -235,7 +256,7 @@ export async function sealCurrentArc(input) {
235
256
  }
236
257
  const { scopeViolationEvidence: _scopeViolationEvidence, ...durableCapture } = capture;
237
258
  return {
238
- v: 1,
259
+ v: 2,
239
260
  kind: "seal",
240
261
  contract: input.contractId,
241
262
  at,
@@ -277,7 +298,7 @@ export async function appendRenewSettlement(input) {
277
298
  if (input.capture) {
278
299
  const { scopeViolationEvidence: _evidence, ...durableCapture } = input.capture;
279
300
  const sealEntry = {
280
- v: 1,
301
+ v: 2,
281
302
  kind: "seal",
282
303
  contract: input.contractId,
283
304
  at,
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import { getConfig } from "../../config/env.js";
3
2
  import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
4
3
  import { resolveReviewerAgentName } from "../../config/settings/knobs.js";
@@ -15,6 +14,7 @@ import { persistResponseHistory } from "../transcripts.js";
15
14
  import { logInfo } from "../../telemetry/logger.js";
16
15
  import { execSubagent } from "../call/call.js";
17
16
  import { readContractLog } from "../log.js";
17
+ import { runVerificationCommand } from "./verification.js";
18
18
  const CLAIM_VERIFICATION_OUTPUT_MAX_CHARS = 1200;
19
19
  const REVIEW_PASS_TOKEN = "PASS";
20
20
  const REVIEW_REJECTION_MESSAGE = "CLAIM rejected by independent reviewer.";
@@ -48,31 +48,6 @@ function buildReviewAttemptBlock(reviewNumber) {
48
48
  "Focus on whether the previous concerns have been addressed.",
49
49
  ];
50
50
  }
51
- async function runCommand(options) {
52
- return await new Promise((resolve, reject) => {
53
- const child = spawn(options.command, options.args, {
54
- cwd: options.cwd,
55
- stdio: ["ignore", "pipe", "pipe"],
56
- env: getConfig().runtimeEnv,
57
- });
58
- let stdout = "";
59
- let stderr = "";
60
- child.stdout.on("data", (chunk) => {
61
- stdout += String(chunk);
62
- });
63
- child.stderr.on("data", (chunk) => {
64
- stderr += String(chunk);
65
- });
66
- child.on("error", reject);
67
- child.on("close", (code) => {
68
- resolve({
69
- code: typeof code === "number" ? code : -1,
70
- stdout,
71
- stderr,
72
- });
73
- });
74
- });
75
- }
76
51
  function clipCommandOutput(value, maxChars = CLAIM_VERIFICATION_OUTPUT_MAX_CHARS) {
77
52
  const trimmed = value.trim();
78
53
  if (!trimmed)
@@ -128,11 +103,7 @@ export function createCommandVerificationGate() {
128
103
  async evaluate({ cwd, verificationCommands }) {
129
104
  for (const verification of verificationCommands) {
130
105
  logInfo(`[CLAIM] Running verification: ${verification.label}`, { cwd, section: "script", progressOnly: true });
131
- const result = await runCommand({
132
- cwd,
133
- command: verification.command,
134
- args: verification.args,
135
- });
106
+ const result = await runVerificationCommand(cwd, verification);
136
107
  if (result.code !== 0) {
137
108
  return {
138
109
  pass: false,
@@ -141,8 +112,8 @@ export function createCommandVerificationGate() {
141
112
  facts: {
142
113
  kind: "verification_failure",
143
114
  label: verification.label,
144
- stdout: clipCommandOutput(result.stdout),
145
- stderr: clipCommandOutput(result.stderr),
115
+ stdout: clipCommandOutput(result.stdout.tail),
116
+ stderr: clipCommandOutput(result.stderr.tail),
146
117
  },
147
118
  };
148
119
  }
@@ -55,7 +55,7 @@ export async function previewPetition(input) {
55
55
  let seal;
56
56
  if (freshness.result === "pass" && arc.result === "pass" && state.result === "pass") {
57
57
  try {
58
- const sealPreview = await previewSeal(context.deliveryCwd, context.ledger.entries, "HEAD", input.waivePaths);
58
+ const sealPreview = await previewSeal(context.deliveryCwd, context.ledger.entries, "HEAD");
59
59
  seal = { name: "seal", result: "pass", detail: sealPathDetail(sealPreview.paths) };
60
60
  }
61
61
  catch (error) {
@@ -3,7 +3,7 @@ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError
3
3
  import { parseCommandMarkdown } from "../command-markdown.js";
4
4
  import { activeLedgerRef, appendWithRetry, readLedger } from "../ledger.js";
5
5
  import { allocateSettlementSeat, queueMembership, readQueueReadModel } from "./queue.js";
6
- import { findOpenArc, getMaxArc, receiptsAfterFence, sealCurrentArc } from "../seal.js";
6
+ import { appendScopeTerms, findOpenArc, getMaxArc, receiptsAfterFence, sealCurrentArc } from "../seal.js";
7
7
  import { deriveContractState } from "../status/lifecycle.js";
8
8
  import { commissionTarget, latestBase } from "../status/drift.js";
9
9
  import { findContractWorktreePath } from "../worktree-path.js";
@@ -73,13 +73,20 @@ export async function petitionContract(input) {
73
73
  await assertPetitionFresh(input.cwd, input.contractId, ledger.entries, base);
74
74
  await assertPetitionArcReady(deliveryCwd, input.contractId, ledger.entries);
75
75
  assertPetitionable(input.contractId, ledger.entries);
76
+ await appendScopeTerms({
77
+ cwd: input.cwd,
78
+ renderCwd: deliveryCwd,
79
+ contractId: input.contractId,
80
+ patterns: input.intent === "claim" ? input.appendScope : undefined,
81
+ actor: input.actor,
82
+ nowMs: input.nowMs,
83
+ });
76
84
  const sealed = await sealCurrentArc({
77
85
  cwd: deliveryCwd,
78
86
  renderCwd: deliveryCwd,
79
87
  contractId: input.contractId,
80
88
  sealedBy: "petition",
81
89
  nowMs: input.nowMs,
82
- waivePaths: input.intent === "claim" ? input.waivePaths : undefined,
83
90
  });
84
91
  const allocated = await allocateSettlementSeat(input.cwd, {
85
92
  target: target.ref,
@@ -148,6 +155,6 @@ export function buildPetitionClaimInput(input) {
148
155
  plea: `Settle contract ${input.contractId} through the petition pipeline.`,
149
156
  oath,
150
157
  signal: input.signal,
151
- waivePaths: input.waivePaths,
158
+ appendScope: input.appendScope,
152
159
  };
153
160
  }
@@ -1,5 +1,3 @@
1
- import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
2
- import { loadKeiyakuSettings } from "../../config/settings/loader.js";
3
1
  import { FlowError } from "../../flow-error.js";
4
2
  import { readCurrentContractContext } from "../context.js";
5
3
  import { readLedger } from "../ledger.js";
@@ -13,6 +11,7 @@ import { scopePathEvidence } from "../scope.js";
13
11
  import { nextReviewResponseNumber } from "../transcripts.js";
14
12
  import { claimContract } from "./claim.js";
15
13
  import { buildClaimGatePipeline, } from "./petition-claim-gates.js";
14
+ import { resolveVerificationCommands } from "./verification.js";
16
15
  import { forfeitKeiyaku } from "./petition-forfeit.js";
17
16
  import { assertHeadUnchanged } from "./petition-head-guard.js";
18
17
  import { petitionContract } from "./petition.js";
@@ -23,13 +22,7 @@ async function resolveClaimGateConfig(cwd) {
23
22
  verificationCommands: [],
24
23
  };
25
24
  try {
26
- const loaded = await loadKeiyakuSettings(cwd);
27
- assertSettingsKnobUsable(loaded, "gate");
28
- const gate = loaded.knobs?.gate;
29
- if (!gate) {
30
- return merged;
31
- }
32
- merged.verificationCommands = gate.commands ?? merged.verificationCommands;
25
+ merged.verificationCommands = await resolveVerificationCommands(cwd);
33
26
  return merged;
34
27
  }
35
28
  catch (error) {
@@ -255,7 +248,7 @@ export async function claimKeiyaku(input, context, dependencies) {
255
248
  nowMs: Date.now(),
256
249
  intent: "claim",
257
250
  oath,
258
- waivePaths: input.waivePaths,
251
+ appendScope: input.appendScope,
259
252
  });
260
253
  // 2. Pure verdict: judgment facts only; no claim execution.
261
254
  // Capture one clock for verdict + claim so ledger timestamps match the prior
@@ -0,0 +1,59 @@
1
+ import { spawn } from "node:child_process";
2
+ import { getConfig } from "../../config/env.js";
3
+ import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
4
+ import { loadKeiyakuSettings } from "../../config/settings/loader.js";
5
+ import { FlowError } from "../../flow-error.js";
6
+ const AUDIT_OUTPUT_TAIL_BYTES = 8_192;
7
+ function tail(value, limit = AUDIT_OUTPUT_TAIL_BYTES) {
8
+ if (value.byteLength <= limit) {
9
+ return { tail: value.toString("utf8"), truncated: false, omittedBytes: 0 };
10
+ }
11
+ return {
12
+ tail: value.subarray(value.byteLength - limit).toString("utf8"),
13
+ truncated: true,
14
+ omittedBytes: value.byteLength - limit,
15
+ };
16
+ }
17
+ export function verificationExecutor(command) {
18
+ const basename = command.command.split(/[\\/]/).pop() ?? "";
19
+ const executor = basename.toLowerCase().replace(/\.exe$/, "");
20
+ if (executor !== "bash" && executor !== "zsh" && executor !== "pwsh") {
21
+ return { cause: "unsupported_executor" };
22
+ }
23
+ const bodyIndex = command.args.findIndex((arg) => arg === "-c" || arg === "-lc" || arg === "-Command");
24
+ const body = bodyIndex < 0 ? undefined : command.args[bodyIndex + 1];
25
+ if (body === undefined)
26
+ return { cause: "missing_command_body" };
27
+ return { executor, body };
28
+ }
29
+ export async function resolveVerificationCommands(cwd) {
30
+ const loaded = await loadKeiyakuSettings(cwd);
31
+ assertSettingsKnobUsable(loaded, "gate");
32
+ return loaded.knobs?.gate?.commands ?? [];
33
+ }
34
+ export async function runVerificationCommand(cwd, verification) {
35
+ const started = process.hrtime.bigint();
36
+ return await new Promise((resolve, reject) => {
37
+ const child = spawn(verification.command, verification.args, {
38
+ cwd,
39
+ stdio: ["ignore", "pipe", "pipe"],
40
+ env: getConfig().runtimeEnv,
41
+ });
42
+ const stdout = [];
43
+ const stderr = [];
44
+ child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
45
+ child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
46
+ child.on("error", reject);
47
+ child.on("close", (code) => {
48
+ const durationMs = Number((process.hrtime.bigint() - started) / 1000000n);
49
+ resolve({
50
+ code: typeof code === "number" ? code : -1,
51
+ durationMs,
52
+ stdout: tail(Buffer.concat(stdout)),
53
+ stderr: tail(Buffer.concat(stderr)),
54
+ });
55
+ });
56
+ }).catch((error) => {
57
+ throw new FlowError("CLOSE_QUALITY_GATE_FAILED", `verification '${verification.label}' could not start`, { cause: error });
58
+ });
59
+ }