@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
@@ -1,12 +1,13 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
2
  import { resolveTaskReadId } from "./board.js";
3
+ import { advancedTaskUpdatedAt } from "./model.js";
3
4
  export function requireHealthySettlementBoard(board) {
4
5
  if (board.diseases.length > 0) {
5
6
  throw new FlowError("INTERNAL_STATE", `authoritative task board is diseased: ${board.diseases.map((disease) => disease.message).join("; ")}`);
6
7
  }
7
8
  return board;
8
9
  }
9
- function settlementDocument(task, contractId) {
10
+ function settlementDocument(task, contractId, updatedAt) {
10
11
  return {
11
12
  id: task.id,
12
13
  title: task.title,
@@ -16,17 +17,18 @@ function settlementDocument(task, contractId) {
16
17
  parent: task.parent,
17
18
  from: [...task.from],
18
19
  createdAt: task.createdAt,
20
+ updatedAt: advancedTaskUpdatedAt(task, updatedAt),
19
21
  creator: task.creator,
20
22
  ...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
21
23
  ...(contractId === undefined ? {} : { contractId }),
22
24
  body: task.body,
23
25
  };
24
26
  }
25
- export function contractPointerDocument(task, contractId) {
26
- return settlementDocument(task, contractId);
27
+ export function contractPointerDocument(task, contractId, updatedAt) {
28
+ return settlementDocument(task, contractId, updatedAt);
27
29
  }
28
- export function reopenedTaskDocument(task) {
29
- const document = settlementDocument(task, undefined);
30
+ export function reopenedTaskDocument(task, updatedAt) {
31
+ const document = settlementDocument(task, undefined, updatedAt);
30
32
  document.state = "open";
31
33
  delete document.startedAt;
32
34
  return document;
@@ -96,12 +98,18 @@ export function tasksForContract(board, contractId) {
96
98
  .filter((task) => task.contractId === contractId)
97
99
  .sort((left, right) => left.id.localeCompare(right.id));
98
100
  }
99
- export function doneTaskReplacements(board, contractId) {
100
- return tasksForContract(board, contractId).map((task) => {
101
- const settled = settlementDocument(task, undefined);
101
+ export function doneTaskReplacements(board, contractId, taskIds, updatedAt) {
102
+ return taskIds.flatMap((taskId) => {
103
+ const task = requireSettledTask(board, taskId);
104
+ if (task.state === "done" && task.contractId === undefined)
105
+ return [];
106
+ if (task.contractId !== contractId) {
107
+ throw new FlowError("INTERNAL_STATE", `task ${task.id} no longer points to contract ${contractId} while settling its immutable bind-base ownership`);
108
+ }
109
+ const settled = settlementDocument(task, undefined, updatedAt);
102
110
  settled.state = "done";
103
111
  delete settled.startedAt;
104
- return settled;
112
+ return [settled];
105
113
  });
106
114
  }
107
115
  export function requireSettledTask(board, id) {
@@ -23,6 +23,7 @@ function taskDocument(parsed) {
23
23
  parent: parsed.parent,
24
24
  from: [...parsed.from],
25
25
  createdAt: parsed.createdAt,
26
+ ...(parsed.updatedAt === undefined ? {} : { updatedAt: parsed.updatedAt }),
26
27
  creator: parsed.creator,
27
28
  ...(parsed.startedAt === undefined ? {} : { startedAt: parsed.startedAt }),
28
29
  ...(parsed.contractId === undefined ? {} : { contractId: parsed.contractId }),
@@ -4,7 +4,7 @@ import { withLockHeldTaskBoard } from "./task-git-store.js";
4
4
  import { prepareTaskDocumentReplacements, taskBoardFromSourceBoard, taskRepositoryFromLockedBoard } from "./task-store-repository.js";
5
5
  import { serializeTaskDocument } from "./document.js";
6
6
  import { buildTaskSourceBoard } from "./source-board.js";
7
- import { containsCommit, createLandingCommit, createTaskBoardCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, readableRefHead, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, updateTargetRef, } from "./settlement-git.js";
7
+ import { composeLandingTree, containsCommit, createLandingCommit, createTaskBoardCommit, findAcceptedLandingCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskSettlementOwnershipAtCommit, taskSourcesAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, updateTargetRef, } from "./settlement-git.js";
8
8
  import { contractPointerDocument, doneTaskReplacements, reopenedTaskDocument, requireHealthySettlementBoard, requireSettledTask, selectReadyTasksForBind, tasksForContract, } from "./settlement-policy.js";
9
9
  import { stableRepoRoot } from "../worktree-path.js";
10
10
  function sourcePath(root, source) {
@@ -66,7 +66,9 @@ async function clearAttemptedPointers(input) {
66
66
  return input.repository.mutate((board) => {
67
67
  const replacements = input.tasks.flatMap((task) => {
68
68
  const current = requireSettledTask(board, task.id);
69
- return current.contractId === input.contractId ? [contractPointerDocument(current, undefined)] : [];
69
+ return current.contractId === input.contractId
70
+ ? [contractPointerDocument(current, undefined, input.updatedAt)]
71
+ : [];
70
72
  });
71
73
  return { replacements, select: () => replacements.map((replacement) => replacement.id) };
72
74
  });
@@ -86,6 +88,7 @@ async function rollForwardClearAttemptedPointer(input) {
86
88
  repository: input.repository,
87
89
  tasks: input.selected,
88
90
  contractId: input.contractId,
91
+ updatedAt: input.updatedAt,
89
92
  });
90
93
  if (clearedIds.length === 0)
91
94
  return;
@@ -107,6 +110,7 @@ async function rollForwardClearAttemptedPointer(input) {
107
110
  }, input.deps);
108
111
  }
109
112
  export async function settleTaskBoardForBind(input, deps = {}) {
113
+ const updatedAt = new Date().toISOString();
110
114
  await requireGitRepository(input.cwd, "task bind");
111
115
  return withLockHeldTaskBoard(input.cwd, async (transaction) => {
112
116
  if (transaction.location.mode !== "git") {
@@ -125,7 +129,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
125
129
  const prepared = taskBound ? await input.prepare(selectedTasks) : input;
126
130
  const contractId = prepared.contractId;
127
131
  if (taskBound && selectedTasks.length > 0) {
128
- await replaceTaskDocuments(repository, selectedTasks.map((task) => contractPointerDocument(task, contractId)));
132
+ await replaceTaskDocuments(repository, selectedTasks.map((task) => contractPointerDocument(task, contractId, updatedAt)));
129
133
  }
130
134
  const taskIds = taskBound ? selectedTasks.map((t) => t.id) : undefined;
131
135
  const selectedPaths = ownedTaskPaths(await repository.read(), taskIds);
@@ -176,7 +180,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
176
180
  repository, selected: selectedTasks, contractId, hubCwd,
177
181
  branchRef: branch.ref, trackedHead: branch.head,
178
182
  gitDir: transaction.location.gitDir, root: transaction.location.root,
179
- selectedPaths, deps,
183
+ selectedPaths, deps, updatedAt,
180
184
  });
181
185
  }
182
186
  else {
@@ -194,7 +198,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
194
198
  repository, selected: selectedTasks, contractId, hubCwd,
195
199
  branchRef: branch.ref, trackedHead: branch.head,
196
200
  gitDir: transaction.location.gitDir, root: transaction.location.root,
197
- selectedPaths, deps,
201
+ selectedPaths, deps, updatedAt,
198
202
  });
199
203
  }
200
204
  throw indexError;
@@ -217,13 +221,14 @@ export async function settleTaskBoardForBind(input, deps = {}) {
217
221
  repository, selected: selectedTasks, contractId, hubCwd,
218
222
  branchRef: branch.ref, trackedHead: branch.head,
219
223
  gitDir: transaction.location.gitDir, root: transaction.location.root,
220
- selectedPaths, deps,
224
+ selectedPaths, deps, updatedAt,
221
225
  });
222
226
  throw publishError;
223
227
  }
224
228
  }, { signal: input.signal });
225
229
  }
226
230
  export async function reconcileTaskForfeit(input) {
231
+ const updatedAt = new Date().toISOString();
227
232
  await requireGitRepository(input.cwd, "task forfeit");
228
233
  const hubRoot = await stableRepoRoot(input.cwd);
229
234
  return withLockHeldTaskBoard(hubRoot, async (transaction) => {
@@ -238,7 +243,7 @@ export async function reconcileTaskForfeit(input) {
238
243
  const ledger = await input.publishLedger();
239
244
  if (matched.length === 0)
240
245
  return { ledger };
241
- const reopenedDocuments = matched.map(reopenedTaskDocument);
246
+ const reopenedDocuments = matched.map((task) => reopenedTaskDocument(task, updatedAt));
242
247
  const reopened = await replaceTaskDocuments(repository, reopenedDocuments);
243
248
  const taskLabel = matched.length === 1 ? `task ${matched[0].id}` : `${matched.length} tasks`;
244
249
  await publishTaskMutationCommit({
@@ -255,11 +260,14 @@ export async function reconcileTaskForfeit(input) {
255
260
  * task trees are fossils: the candidate is the code-only three-way merge plus
256
261
  * one complete snapshot of the common hub board held under its repository lock.
257
262
  */
258
- export async function settleTaskBoardForLanding(input) {
263
+ export async function settleTaskBoardForLanding(input, deps = {}) {
264
+ const updatedAt = new Date().toISOString();
259
265
  await requireGitRepository(input.cwd, "task landing");
260
266
  const hubRoot = await stableRepoRoot(input.cwd);
267
+ const ownership = await taskSettlementOwnershipAtCommit(input.cwd, input.baseCommit, input.contractId);
268
+ const reconcileIndex = deps.reconcileTaskPathsetIndex ?? reconcileTaskPathsetIndex;
261
269
  let lastTargetHead = input.targetHead;
262
- let lastPublishError;
270
+ let lastPostCasError;
263
271
  let acceptedMovement;
264
272
  for (let attempt = 0; attempt < 4; attempt += 1) {
265
273
  const result = await withLockHeldTaskBoard(hubRoot, async (transaction) => {
@@ -273,77 +281,143 @@ export async function settleTaskBoardForLanding(input) {
273
281
  // common task lock. A losing CAS exits before the next attempt.
274
282
  const targetHead = await refHead(input.cwd, input.targetRef);
275
283
  lastTargetHead = targetHead;
284
+ const selectedPaths = ownership.paths;
285
+ const acceptedLanding = acceptedMovement?.mergeCommit === targetHead
286
+ ? targetHead
287
+ : await findAcceptedLandingCommit({
288
+ cwd: input.cwd,
289
+ gitDir: transaction.location.gitDir,
290
+ root: transaction.location.root,
291
+ base: input.baseCommit,
292
+ target: targetHead,
293
+ delivery: input.deliveryHead,
294
+ contractId: input.contractId,
295
+ targetBranch: input.targetBranch,
296
+ ownedPaths: selectedPaths,
297
+ settledPaths: ownership.taskIds.map(taskPath),
298
+ });
299
+ const deliveryAlreadyAccepted = ownership.taskIds.length === 0
300
+ && await containsCommit(input.cwd, input.deliveryHead, targetHead);
301
+ if (acceptedLanding !== undefined || deliveryAlreadyAccepted) {
302
+ const mergeCommit = acceptedLanding ?? targetHead;
303
+ const movement = acceptedMovement?.mergeCommit === targetHead ? acceptedMovement : undefined;
304
+ const authoritativeSources = selectedPaths.length > 0
305
+ ? await taskSourcesAtCommit(input.cwd, transaction.location.root, targetHead, selectedPaths)
306
+ : [];
307
+ if (!authoritativeSources) {
308
+ throw new FlowError("INTERNAL_STATE", `accepted landing ${mergeCommit} lost an owned task path`);
309
+ }
310
+ const selection = selectSettlementSources({
311
+ root: transaction.location.root,
312
+ sources: authoritativeSources,
313
+ trackedPaths: await trackedTaskPaths(input.cwd, targetHead),
314
+ ownedPaths: selectedPaths,
315
+ mutationPaths: selectedPaths,
316
+ });
317
+ const currentText = new Map(transaction.sources.map((source) => [sourcePath(transaction.location.root, source), source.text]));
318
+ const writes = authoritativeSources.filter((source) => currentText.get(sourcePath(transaction.location.root, source)) !== source.text);
319
+ if (writes.length > 0)
320
+ await transaction.replace(taskWritesFromSources(writes));
321
+ try {
322
+ await reconcileIndex(transaction.location.root, targetHead, selectedPaths);
323
+ }
324
+ catch (error) {
325
+ lastPostCasError = error;
326
+ return { kind: "retry" };
327
+ }
328
+ try {
329
+ await input.publishLedger?.(mergeCommit);
330
+ }
331
+ catch (error) {
332
+ lastPostCasError = error;
333
+ return { kind: "retry" };
334
+ }
335
+ const reconcileWarnings = movement
336
+ ? await input.afterCas?.(mergeCommit, movement.oldHead)
337
+ : undefined;
338
+ return {
339
+ kind: movement ? "accepted" : "idempotent",
340
+ mergeCommit,
341
+ ...(movement ? { oldHead: movement.oldHead } : {}),
342
+ ...(reconcileWarnings && reconcileWarnings.length > 0 ? { warnings: reconcileWarnings } : {}),
343
+ evidence: selection.evidence,
344
+ };
345
+ }
346
+ // No durable receipt exists, so this remains a first-publication attempt:
347
+ // validate mutable hub authority before composing or publishing anything.
276
348
  const repository = repositoryForSettlement(transaction, transaction.sources);
277
349
  const board = requireHealthySettlementBoard(await repository.read());
278
- const replacements = doneTaskReplacements(board, input.contractId);
350
+ const replacements = doneTaskReplacements(board, input.contractId, ownership.taskIds, updatedAt);
279
351
  const prepared = replacements.length > 0
280
352
  ? prepareTaskDocumentReplacements(transaction.sources, replacements)
281
353
  : { sources: transaction.sources, board };
282
354
  const finalSources = prepared.sources;
283
355
  requireHealthySettlementBoard(prepared.board);
284
- const replacementIds = replacements.map((r) => r.id);
285
- const selectedPaths = ownedTaskPaths(prepared.board, replacementIds.length > 0 ? replacementIds : undefined);
286
356
  const selection = selectSettlementSources({
287
357
  root: transaction.location.root,
288
358
  sources: finalSources,
289
359
  trackedPaths: await trackedTaskPaths(input.cwd, targetHead),
290
360
  ownedPaths: selectedPaths,
291
- mutationPaths: replacements.map((r) => taskPath(r.id)),
361
+ // Bind-base ownership, unlike incidental hub bytes, authorizes a path
362
+ // even when a concurrent target commit removed it.
363
+ mutationPaths: selectedPaths,
292
364
  });
293
- let mergeCommit = targetHead;
294
- let movement = acceptedMovement?.mergeCommit === targetHead ? acceptedMovement : undefined;
295
- if (!(await containsCommit(input.cwd, input.deliveryHead, targetHead))) {
296
- mergeCommit = await createLandingCommit({
297
- cwd: input.cwd,
298
- gitDir: transaction.location.gitDir,
299
- root: transaction.location.root,
300
- base: input.baseCommit,
301
- target: targetHead,
302
- delivery: input.deliveryHead,
303
- sources: selection.sources,
304
- contractId: input.contractId,
305
- targetBranch: input.targetBranch,
306
- });
307
- await input.beforeCas?.(mergeCommit, targetHead);
308
- try {
309
- await updateTargetRef(input.cwd, input.targetRef, mergeCommit, targetHead);
310
- movement = { mergeCommit, oldHead: targetHead };
311
- acceptedMovement = movement;
312
- }
313
- catch {
314
- const current = await readableRefHead(input.cwd, input.targetRef);
315
- if (!current || !(await containsCommit(input.cwd, input.deliveryHead, current))) {
316
- return { kind: "retry" };
317
- }
318
- mergeCommit = current;
319
- movement = acceptedMovement?.mergeCommit === mergeCommit ? acceptedMovement : undefined;
320
- }
365
+ const selected = new Set(selection.evidence.selectedPaths);
366
+ const missingOwnedPaths = selectedPaths.filter((ownedPath) => !selected.has(ownedPath));
367
+ if (missingOwnedPaths.length > 0) {
368
+ throw new FlowError("INTERNAL_STATE", `hub board lost bind-base-owned task paths: ${missingOwnedPaths.join(", ")}`);
369
+ }
370
+ const candidateTree = await composeLandingTree({
371
+ cwd: input.cwd,
372
+ gitDir: transaction.location.gitDir,
373
+ root: transaction.location.root,
374
+ base: input.baseCommit,
375
+ target: targetHead,
376
+ delivery: input.deliveryHead,
377
+ sources: selection.sources,
378
+ });
379
+ const mergeCommit = await createLandingCommit({
380
+ cwd: input.cwd,
381
+ target: targetHead,
382
+ delivery: input.deliveryHead,
383
+ tree: candidateTree,
384
+ contractId: input.contractId,
385
+ targetBranch: input.targetBranch,
386
+ });
387
+ await input.beforeCas?.(mergeCommit, targetHead);
388
+ let movement;
389
+ try {
390
+ await updateTargetRef(input.cwd, input.targetRef, mergeCommit, targetHead);
391
+ movement = { mergeCommit, oldHead: targetHead };
392
+ acceptedMovement = movement;
393
+ }
394
+ catch {
395
+ return { kind: "retry" };
321
396
  }
322
397
  const warnings = [];
323
398
  if (replacements.length > 0) {
324
399
  await transaction.replace(taskWritesFromSources(selection.sources));
325
400
  }
326
401
  try {
327
- await reconcileTaskPathsetIndex(transaction.location.root, mergeCommit, selection.evidence.selectedPaths);
402
+ await reconcileIndex(transaction.location.root, mergeCommit, selectedPaths);
328
403
  }
329
404
  catch (error) {
330
- warnings.push(`Accepted landing ${mergeCommit}, but task index reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
405
+ lastPostCasError = error;
406
+ return { kind: "retry" };
331
407
  }
332
408
  try {
333
409
  await input.publishLedger?.(mergeCommit);
334
410
  }
335
411
  catch (error) {
336
- lastPublishError = error;
412
+ lastPostCasError = error;
337
413
  return { kind: "retry" };
338
414
  }
339
- const reconcileWarnings = movement
340
- ? await input.afterCas?.(mergeCommit, movement.oldHead)
341
- : undefined;
415
+ const reconcileWarnings = await input.afterCas?.(mergeCommit, movement.oldHead);
342
416
  warnings.push(...(reconcileWarnings ?? []));
343
417
  return {
344
- kind: movement ? "accepted" : "idempotent",
418
+ kind: "accepted",
345
419
  mergeCommit,
346
- ...(movement ? { oldHead: movement.oldHead } : {}),
420
+ oldHead: movement.oldHead,
347
421
  ...(warnings.length > 0 ? { warnings } : {}),
348
422
  evidence: selection.evidence,
349
423
  };
@@ -364,7 +438,7 @@ export async function settleTaskBoardForLanding(input) {
364
438
  ...(result.evidence ? { evidence: result.evidence } : {}),
365
439
  };
366
440
  }
367
- if (lastPublishError !== undefined)
368
- throw lastPublishError;
441
+ if (lastPostCasError !== undefined)
442
+ throw lastPostCasError;
369
443
  throw new FlowError("CONCURRENT_MODIFICATION", `target branch kept moving while claiming ${input.contractId}; last observed ${lastTargetHead}`);
370
444
  }
@@ -11,6 +11,7 @@ export function taskBoardFromSourceBoard(sourceBoard) {
11
11
  parent: task.parent,
12
12
  from: [...task.from],
13
13
  createdAt: task.createdAt,
14
+ ...(task.updatedAt === undefined ? {} : { updatedAt: task.updatedAt }),
14
15
  creator: task.creator,
15
16
  ...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
16
17
  ...(task.contractId === undefined ? {} : { contractId: task.contractId }),
@@ -1,3 +1,5 @@
1
+ export { deriveTaskDisplay, parseTaskFilter, parseTaskSort, selectTaskList } from "./query.js";
2
+ export { advancedTaskUpdatedAt, taskUpdatedAt } from "./model.js";
1
3
  export { parseTaskDocument, serializeTaskDocument, } from "./document.js";
2
4
  export { resolveTaskReadId, } from "./board.js";
3
5
  export { buildTaskBoard } from "./source-board.js";
@@ -53,9 +53,7 @@ export function resolveFlowErrorHints(code, context, facts) {
53
53
  return ["keiyaku status — watch for the akuma to land, then renew"];
54
54
  }
55
55
  if (facts?.kind === "projection_wake_failure") {
56
- return [
57
- `keiyaku tell ${facts.projectionId} [--wait DURATION] "retry the saved tell" — retries the same-projection wake; the saved tell remains queued`,
58
- ];
56
+ return [];
59
57
  }
60
58
  switch (code) {
61
59
  case "NOT_GIT_REPO":
@@ -119,7 +117,7 @@ export function resolveFlowErrorHints(code, context, facts) {
119
117
  case "INTERNAL_STATE":
120
118
  return ["An internal state error occurred. Please verify your repository status."];
121
119
  case "SCOPE_VIOLATION":
122
- return ["Files outside the contract's declared scope were modified. Widen the scope (amend), waive the violations, or move the changes back."];
120
+ return ["Files outside the contract's declared scope were modified. Append ordered scope terms with `--append-scope`, amend the contract, or move the changes back."];
123
121
  case "NO_OPEN_ARC":
124
122
  return ["Open an arc to receipt delivery commits after the fence, then retry."];
125
123
  case "REVIEW_REJECTED":
@@ -1,3 +1,3 @@
1
1
  // Generated into build output by scripts/build.mjs
2
- export const VERSION = "2.9.4";
3
- export const GIT_HASH = "0aa48ab";
2
+ export const VERSION = "2.9.5";
3
+ export const GIT_HASH = "ade1510";
@@ -32,8 +32,10 @@ export function parseUnifiedHunks(section) {
32
32
  hunks.push({
33
33
  header: current[0] ?? "",
34
34
  lines: current.slice(1),
35
- newStart: parsed.start,
36
- newEnd: parsed.end,
35
+ oldStart: parsed.oldStart,
36
+ oldEnd: parsed.oldEnd,
37
+ newStart: parsed.newStart,
38
+ newEnd: parsed.newEnd,
37
39
  });
38
40
  }
39
41
  }
@@ -49,25 +51,33 @@ export function parseUnifiedHunks(section) {
49
51
  hunks.push({
50
52
  header: current[0] ?? "",
51
53
  lines: current.slice(1),
52
- newStart: parsed.start,
53
- newEnd: parsed.end,
54
+ oldStart: parsed.oldStart,
55
+ oldEnd: parsed.oldEnd,
56
+ newStart: parsed.newStart,
57
+ newEnd: parsed.newEnd,
54
58
  });
55
59
  }
56
60
  }
57
61
  return { prelude, hunks };
58
62
  }
59
- function parseHunkRange(hunkHeader) {
60
- const match = hunkHeader.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
63
+ export function parseHunkRange(hunkHeader) {
64
+ const match = hunkHeader.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
61
65
  if (!match)
62
66
  return null;
63
- const start = Number.parseInt(match[1] ?? "", 10);
64
- const countRaw = match[2];
65
- const count = countRaw ? Number.parseInt(countRaw, 10) : 1;
66
- if (!Number.isFinite(start) || !Number.isFinite(count))
67
+ const oldStart = Number.parseInt(match[1] ?? "", 10);
68
+ const oldLines = match[2] === undefined ? 1 : Number.parseInt(match[2], 10);
69
+ const newStart = Number.parseInt(match[3] ?? "", 10);
70
+ const newLines = match[4] === undefined ? 1 : Number.parseInt(match[4], 10);
71
+ if (![oldStart, oldLines, newStart, newLines].every(Number.isFinite))
67
72
  return null;
68
- if (count <= 0)
69
- return { start, end: start - 1 };
70
- return { start, end: start + count - 1 };
73
+ return {
74
+ oldStart,
75
+ oldLines,
76
+ oldEnd: oldLines <= 0 ? oldStart - 1 : oldStart + oldLines - 1,
77
+ newStart,
78
+ newLines,
79
+ newEnd: newLines <= 0 ? newStart - 1 : newStart + newLines - 1,
80
+ };
71
81
  }
72
82
  export function isDeletedDiffSection(section) {
73
83
  return section.some((line) => line.startsWith("deleted file mode ") || line.startsWith("+++ /dev/null"));
@@ -0,0 +1,127 @@
1
+ import { createGit, wrapGitError } from "../core.js";
2
+ import { diffPathspecArgs } from "./pathspec.js";
3
+ import { parseHunkRange, parseUnifiedHunks, splitDiffByFile } from "./parsers.js";
4
+ function parseRawDiff(output) {
5
+ const fields = output.split("\0");
6
+ const files = [];
7
+ let index = 0;
8
+ while (index < fields.length) {
9
+ const header = fields[index++];
10
+ if (!header)
11
+ continue;
12
+ const statusToken = header.trim().split(/\s+/).at(-1) ?? "";
13
+ const status = statusToken[0];
14
+ if (status !== "A" && status !== "M" && status !== "D" && status !== "R" && status !== "C" && status !== "T" && status !== "U")
15
+ continue;
16
+ const firstPath = fields[index++] ?? "";
17
+ if (status === "R" || status === "C") {
18
+ const path = fields[index++] ?? "";
19
+ files.push({
20
+ oldPath: firstPath,
21
+ path,
22
+ status,
23
+ similarity: Number.parseInt(statusToken.slice(1), 10) || 0,
24
+ });
25
+ }
26
+ else {
27
+ files.push({ path: firstPath, status });
28
+ }
29
+ }
30
+ return files;
31
+ }
32
+ function withoutTerminalPatchDelimiter(patch) {
33
+ return patch.endsWith("\n") ? patch.slice(0, -1) : patch;
34
+ }
35
+ function parseNumstat(output) {
36
+ const fields = output.split("\0");
37
+ const stats = [];
38
+ let index = 0;
39
+ while (index < fields.length) {
40
+ const record = fields[index++];
41
+ if (!record)
42
+ continue;
43
+ const [added = "0", removed = "0", path = ""] = record.split("\t");
44
+ const binary = added === "-" || removed === "-";
45
+ if (path) {
46
+ stats.push({ path, insertions: binary ? 0 : Number.parseInt(added, 10) || 0, deletions: binary ? 0 : Number.parseInt(removed, 10) || 0, binary });
47
+ continue;
48
+ }
49
+ const oldPath = fields[index++] ?? "";
50
+ const newPath = fields[index++] ?? "";
51
+ stats.push({ path: newPath, oldPath, insertions: binary ? 0 : Number.parseInt(added, 10) || 0, deletions: binary ? 0 : Number.parseInt(removed, 10) || 0, binary });
52
+ }
53
+ return stats;
54
+ }
55
+ function structuredLines(raw, oldStart, newStart) {
56
+ let oldLine = oldStart;
57
+ let newLine = newStart;
58
+ const lines = [];
59
+ for (const line of raw) {
60
+ if (line.startsWith("\"))
61
+ continue;
62
+ if (line.startsWith("+")) {
63
+ lines.push({ kind: "addition", content: line.slice(1), oldLine: null, newLine: newLine++ });
64
+ }
65
+ else if (line.startsWith("-")) {
66
+ lines.push({ kind: "deletion", content: line.slice(1), oldLine: oldLine++, newLine: null });
67
+ }
68
+ else {
69
+ lines.push({ kind: "context", content: line.startsWith(" ") ? line.slice(1) : line, oldLine: oldLine++, newLine: newLine++ });
70
+ }
71
+ }
72
+ return lines;
73
+ }
74
+ function parsePatchHunks(section) {
75
+ return parseUnifiedHunks([...section]).hunks.flatMap((hunk, index) => {
76
+ const range = parseHunkRange(hunk.header);
77
+ if (!range)
78
+ return [];
79
+ return [{
80
+ index,
81
+ header: hunk.header,
82
+ oldStart: range.oldStart,
83
+ oldLines: range.oldLines,
84
+ newStart: range.newStart,
85
+ newLines: range.newLines,
86
+ lines: structuredLines(hunk.lines, range.oldStart, range.newStart),
87
+ }];
88
+ });
89
+ }
90
+ export async function readStructuredDiff(cwd, from, to, options = {}) {
91
+ const git = createGit(cwd);
92
+ const range = `${from}..${to}`;
93
+ const pathspec = diffPathspecArgs({ excludePaths: options.excludePaths });
94
+ try {
95
+ const [raw, numstat, patch] = await Promise.all([
96
+ git.raw(["diff", "--raw", "-z", "--find-renames", range, ...pathspec]),
97
+ git.raw(["diff", "--numstat", "-z", "--find-renames", range, ...pathspec]),
98
+ git.raw(["diff", "--no-color", "--no-ext-diff", "--unified=3", "--find-renames", range, ...pathspec]),
99
+ ]);
100
+ const rawFiles = parseRawDiff(raw);
101
+ const stats = parseNumstat(numstat);
102
+ const sections = splitDiffByFile(withoutTerminalPatchDelimiter(patch));
103
+ const files = rawFiles.map((file, index) => {
104
+ const stat = stats[index];
105
+ return {
106
+ ...file,
107
+ insertions: stat?.insertions ?? 0,
108
+ deletions: stat?.deletions ?? 0,
109
+ binary: stat?.binary ?? false,
110
+ hunks: parsePatchHunks(sections[index] ?? []),
111
+ };
112
+ });
113
+ return {
114
+ from,
115
+ to,
116
+ files,
117
+ stat: {
118
+ filesChanged: files.length,
119
+ insertions: files.reduce((sum, file) => sum + file.insertions, 0),
120
+ deletions: files.reduce((sum, file) => sum + file.deletions, 0),
121
+ },
122
+ };
123
+ }
124
+ catch (error) {
125
+ throw wrapGitError(`diff --find-renames ${range}`, error, cwd);
126
+ }
127
+ }
package/build/index.js CHANGED
@@ -3,10 +3,10 @@ import { VERSION, GIT_HASH } from "./generated/version.js";
3
3
  import * as fs from "node:fs/promises";
4
4
  import { getConfig, initializeConfig } from "./config/env.js";
5
5
  import { ENV_KEYS } from "./config/env-keys.js";
6
- import { logErrorWithCause } from "./telemetry/logger.js";
6
+ import { formatErrorWithCause } from "./telemetry/logger.js";
7
7
  import { runProjectionGenerationRuntime } from "./core/projection/index.js";
8
8
  import { renderCliHelp, runCliCommand } from "./cli/index.js";
9
- import { installCliOutputStreamErrorPolicy } from "./cli/output-stream-error-policy.js";
9
+ import { installCliOutputStreamErrorPolicy, writeCliOutput } from "./cli/output-stream-error-policy.js";
10
10
  import { assertSubagentCliCommandAllowed } from "./cli/subagent-guard.js";
11
11
  installCliOutputStreamErrorPolicy(process.stdout, process.stderr);
12
12
  const packageVersion = VERSION;
@@ -45,16 +45,16 @@ async function main() {
45
45
  args: cliArgs,
46
46
  });
47
47
  if (cliArgs.length === 1 && cliArgs[0] === "--version") {
48
- console.log(GIT_HASH ? `${packageVersion} (${GIT_HASH})` : packageVersion);
48
+ await writeCliOutput(process.stdout, `${GIT_HASH ? `${packageVersion} (${GIT_HASH})` : packageVersion}\n`);
49
49
  return;
50
50
  }
51
51
  if (cliArgs.length === 0 || (cliArgs.length === 1 && (cliArgs[0] === "--help" || cliArgs[0] === "-h"))) {
52
- console.log(renderCliHelp(packageVersion));
52
+ await writeCliOutput(process.stdout, `${renderCliHelp(packageVersion)}\n`);
53
53
  return;
54
54
  }
55
55
  process.exitCode = await runCliCommand(cliArgs);
56
56
  }
57
- main().catch((err) => {
58
- logErrorWithCause("Fatal error:", err, { mirrorToDebugLog: false });
59
- process.exit(1);
57
+ await main().catch(async (err) => {
58
+ await writeCliOutput(process.stderr, `${formatErrorWithCause("Fatal error:", err)}\n`);
59
+ process.exitCode = 1;
60
60
  });
@@ -31,6 +31,9 @@ function stringifyUnknown(error) {
31
31
  return String(error);
32
32
  }
33
33
  }
34
+ export function formatErrorWithCause(message, error) {
35
+ return `${message} ${stringifyUnknown(error)}`;
36
+ }
34
37
  export function logInfo(message, options = {}) {
35
38
  log("info", message, options);
36
39
  }
@@ -41,6 +44,5 @@ export function logError(message, options = {}) {
41
44
  log("error", message, options);
42
45
  }
43
46
  export function logErrorWithCause(message, error, options = {}) {
44
- const details = stringifyUnknown(error);
45
- logError(`${message} ${details}`, options);
47
+ logError(formatErrorWithCause(message, error), options);
46
48
  }