@astrosheep/keiyaku 4.0.6 → 4.1.0

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 (65) hide show
  1. package/build/src/akuma/archetype.js +8 -1
  2. package/build/src/akuma/provider-recipe.d.ts +2 -0
  3. package/build/src/akuma/provider-recipe.js +10 -0
  4. package/build/src/akuma/providers/acp/config.d.ts +2 -0
  5. package/build/src/akuma/providers/acp/config.js +9 -1
  6. package/build/src/akuma/providers/acp/core.d.ts +5 -2
  7. package/build/src/akuma/providers/acp/core.js +29 -16
  8. package/build/src/akuma/providers/acp/index.js +8 -2
  9. package/build/src/akuma/providers/claude/index.d.ts +1 -1
  10. package/build/src/akuma/providers/claude/index.js +9 -5
  11. package/build/src/akuma/providers/codex-app-server/index.js +4 -1
  12. package/build/src/akuma/providers/grok-build/index.js +15 -1
  13. package/build/src/akuma/providers/index.js +1 -1
  14. package/build/src/akuma/providers/opencode-sdk/index.js +3 -0
  15. package/build/src/akuma/providers/pi/index.js +3 -1
  16. package/build/src/body/amend.js +0 -9
  17. package/build/src/body/region.js +2 -2
  18. package/build/src/cli/accepted.js +2 -0
  19. package/build/src/cli/commands/contract.d.ts +5 -3
  20. package/build/src/cli/commands/contract.js +23 -13
  21. package/build/src/cli/commands/task-invoke.d.ts +4 -1
  22. package/build/src/cli/commands/task-invoke.js +6 -3
  23. package/build/src/cli/commands/task.js +10 -2
  24. package/build/src/cli/parse.js +2 -2
  25. package/build/src/cli/render/akuma.js +42 -11
  26. package/build/src/cli/render/catalog.js +5 -1
  27. package/build/src/cli/render/contract.js +25 -3
  28. package/build/src/cli/render/kanshi.js +142 -94
  29. package/build/src/cli/render/receipt.d.ts +1 -0
  30. package/build/src/cli/render/receipt.js +18 -14
  31. package/build/src/cli/render/refusal.js +6 -0
  32. package/build/src/cli/render/task.js +63 -11
  33. package/build/src/cli/result.d.ts +8 -1
  34. package/build/src/contract-worktree.d.ts +0 -2
  35. package/build/src/contract-worktree.js +13 -24
  36. package/build/src/git/nuke.js +1 -6
  37. package/build/src/git/reconcile.d.ts +0 -2
  38. package/build/src/git/reconcile.js +2 -33
  39. package/build/src/git/tender.d.ts +13 -2
  40. package/build/src/git/tender.js +43 -4
  41. package/build/src/kanshi/read.js +32 -12
  42. package/build/src/kanshi/report.d.ts +2 -1
  43. package/build/src/library/continuation.d.ts +26 -0
  44. package/build/src/library/continuation.js +78 -0
  45. package/build/src/library/contract.d.ts +9 -5
  46. package/build/src/library/contract.js +27 -2
  47. package/build/src/library/delivery.d.ts +18 -13
  48. package/build/src/library/keiyaku.d.ts +1 -1
  49. package/build/src/library/mutation.js +11 -4
  50. package/build/src/protocol/deliver.d.ts +12 -2
  51. package/build/src/protocol/deliver.js +52 -19
  52. package/build/src/protocol/operations.d.ts +7 -2
  53. package/build/src/settlement/fence.d.ts +0 -2
  54. package/build/src/settlement/fence.js +0 -24
  55. package/build/src/task/board.d.ts +21 -2
  56. package/build/src/task/board.js +25 -12
  57. package/build/src/task/identity.js +14 -4
  58. package/build/src/task/index.d.ts +1 -0
  59. package/build/src/task/index.js +7 -1
  60. package/build/src/task/operations.d.ts +2 -1
  61. package/build/src/task/operations.js +15 -4
  62. package/build/src/task/query.js +2 -2
  63. package/build/src/task/store.d.ts +2 -0
  64. package/build/src/task/store.js +12 -83
  65. package/package.json +1 -1
@@ -25,10 +25,10 @@ function priorityText(priority) {
25
25
  return priority === null ? "P?" : `P${priority}`;
26
26
  }
27
27
  function scanUnit(id, priority, word) {
28
- return `${markFor(word)} ${id} · ${priorityText(priority)} ${word}`;
28
+ return word === undefined ? `${markFor("ready")} ${id} · ${priorityText(priority)}` : `${markFor(word)} ${id} · ${priorityText(priority)} ${word}`;
29
29
  }
30
30
  function entityLines(entity, columns, indent = "") {
31
- const scan = `${indent}${scanUnit(entity.id, entity.priority, entity.word)}`;
31
+ const scan = `${indent}${scanUnit(entity.id, entity.priority, entity.word)}${entity.facts === undefined ? "" : ` · ${entity.facts}`}`;
32
32
  if (entity.title === null || entity.title.length === 0)
33
33
  return [scan];
34
34
  const title = safeText(entity.title);
@@ -37,9 +37,6 @@ function entityLines(entity, columns, indent = "") {
37
37
  return [inline];
38
38
  return [`${scan} —`, ...renderTextBlock(title, `${indent} `, columns)];
39
39
  }
40
- function listEntity(item) {
41
- return { id: item.id, priority: item.priority, word: item.disposition, title: item.title };
42
- }
43
40
  function stateEntity(task) {
44
41
  return { id: task.id, priority: "priority" in task && task.priority !== undefined ? task.priority : null, word: task.state, title: task.title };
45
42
  }
@@ -79,23 +76,71 @@ function pageHeading(view, page) {
79
76
  return `${view} ${page.returned} of ${page.total} · limit ${page.returned}`;
80
77
  return `${view} ${page.returned}`;
81
78
  }
82
- function renderListRow(item, columns) {
83
- const lines = [...entityLines(listEntity(item), columns)];
79
+ function updatedAge(updatedAt) {
80
+ const elapsed = Math.max(0, performance.timeOrigin + performance.now() - Date.parse(updatedAt));
81
+ const minutes = Math.floor(elapsed / 60_000);
82
+ if (minutes < 60)
83
+ return `${minutes}m`;
84
+ const hours = Math.floor(minutes / 60);
85
+ if (hours < 24)
86
+ return `${hours}h`;
87
+ return `${Math.floor(hours / 24)}d`;
88
+ }
89
+ function compactFacts(item) {
90
+ return [
91
+ `updated ${updatedAge(item.updatedAt)}`,
92
+ ...(item.bodyPresent ? [] : ["no body"]),
93
+ ...(item.children === undefined ? [] : [`children ${item.children.live}/${item.children.total} live`]),
94
+ ].join(" · ");
95
+ }
96
+ function listEntity(item, omitDisposition) {
97
+ return {
98
+ id: item.id,
99
+ priority: item.priority,
100
+ ...(omitDisposition ? {} : { word: item.disposition }),
101
+ facts: compactFacts(item),
102
+ title: item.title,
103
+ };
104
+ }
105
+ function renderListRow(item, columns, omitDisposition) {
106
+ const lines = [...entityLines(listEntity(item, omitDisposition), columns)];
84
107
  if ("blockers" in item) {
85
108
  for (const blocker of item.blockers)
86
109
  lines.push(` needs ${blocker.id} · ${blocker.state}`);
87
110
  }
88
111
  return lines;
89
112
  }
113
+ function shellQuote(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
114
+ function recoveryCommand(command, total) {
115
+ const parts = ["keiyaku", "task", command.action];
116
+ if (command.action === "ls") {
117
+ if (command.flags.closed === true)
118
+ parts.push("--closed");
119
+ if (command.flags.all === true)
120
+ parts.push("--all");
121
+ }
122
+ if (command.flags.world === true)
123
+ parts.push("--world");
124
+ if ((command.action === "ready" || command.action === "blocked") && typeof command.flags.parent === "string") {
125
+ parts.push("--parent", command.flags.parent);
126
+ }
127
+ if (command.action === "query" && typeof command.flags.where === "string")
128
+ parts.push("--where", shellQuote(command.flags.where));
129
+ if (command.action === "query" && typeof command.flags.sort === "string")
130
+ parts.push("--sort", command.flags.sort);
131
+ parts.push("--limit", String(total));
132
+ return parts.join(" ");
133
+ }
90
134
  function renderRows(command, result, columns) {
91
135
  if (result.kind !== "accepted")
92
136
  return renderFailure(command.action, result, columns);
93
137
  const view = command.action === "ls" ? "tasks" : command.action;
94
- return [pageHeading(view, result.value), ...result.value.rows.flatMap((item) => renderListRow(item, columns))].join("\n");
138
+ const footer = result.value.truncated
139
+ ? ["", ` + ${result.value.total - result.value.returned} more not shown`, ` ${recoveryCommand(command, result.value.total)}`]
140
+ : [];
141
+ return [pageHeading(view, result.value), ...result.value.rows.flatMap((item) => renderListRow(item, columns, command.action === "ready")), ...footer].join("\n");
95
142
  }
96
- function renderShow(result, columns) {
97
- if ("kind" in result)
98
- return result.kind === "accepted" ? entityLines(stateEntity(result.value), columns).join("\n") : renderFailure("show", result, columns);
143
+ function renderShowDetail(result, columns) {
99
144
  const task = result.task;
100
145
  const lines = [
101
146
  ...entityLines(stateEntity(task), columns),
@@ -124,6 +169,13 @@ function renderShow(result, columns) {
124
169
  receiptPayload(lines, "body", task.body);
125
170
  return lines.join("\n");
126
171
  }
172
+ function renderShow(result, columns) {
173
+ if (Array.isArray(result))
174
+ return result.map((detail) => renderShowDetail(detail, columns)).join("\n\n");
175
+ if ("kind" in result)
176
+ return renderFailure("show", result, columns);
177
+ return renderShowDetail(result, columns);
178
+ }
127
179
  function treeLines(node, columns, depth = 0) {
128
180
  const indent = " ".repeat(depth);
129
181
  if (node.cycle === true)
@@ -1,4 +1,4 @@
1
- import type { AuditReport, ContractHistory, ContractId, Delivery, Fact, IntegrationConflictMaterialized, MutationResult, PlacementStop, ReconcileReport, Review, VerificationReuse, VerificationStop } from "../index.js";
1
+ import type { AuditReport, ContractHistory, ContractId, ContinuationReport, Delivery, Fact, IntegrationConflictMaterialized, MutationResult, PlacementStop, ReconcileReport, Review, VerificationReuse, VerificationStop } from "../index.js";
2
2
  import type { KanshiReport } from "../kanshi/index.js";
3
3
  import type { RegionRead, Section } from "../kanshi/index.js";
4
4
  import type { Catalog } from "../index.js";
@@ -45,6 +45,7 @@ export type AcceptedBindResult = AcceptedEnvelope & Readonly<{
45
45
  verificationReuse?: never;
46
46
  verificationSummary?: never;
47
47
  placement?: never;
48
+ continuation?: never;
48
49
  cleanup?: never;
49
50
  leak?: never;
50
51
  report?: never;
@@ -63,6 +64,7 @@ export type AcceptedAmendResult = AcceptedEnvelope & Readonly<{
63
64
  verificationReuse?: never;
64
65
  verificationSummary?: never;
65
66
  placement?: never;
67
+ continuation?: never;
66
68
  cleanup?: never;
67
69
  leak?: never;
68
70
  report?: never;
@@ -80,6 +82,7 @@ export type AcceptedDeliverResult = AcceptedEnvelope & Readonly<{
80
82
  verificationReuse?: VerificationReuse;
81
83
  verificationSummary?: string;
82
84
  placement?: PlacementStop;
85
+ continuation?: ContinuationReport;
83
86
  cleanup?: MutationResult<unknown>["cleanup"];
84
87
  leak?: MutationResult<unknown>["leak"];
85
88
  target?: never;
@@ -101,6 +104,7 @@ export type AcceptedReviewResult = AcceptedEnvelope & Readonly<{
101
104
  verificationReuse?: VerificationReuse;
102
105
  verificationSummary?: string;
103
106
  placement?: PlacementStop;
107
+ continuation?: ContinuationReport;
104
108
  workspace?: Review["workspace"];
105
109
  cleanup?: MutationResult<unknown>["cleanup"];
106
110
  leak?: MutationResult<unknown>["leak"];
@@ -124,6 +128,7 @@ export type AcceptedArcResult = AcceptedEnvelope & Readonly<{
124
128
  verificationReuse?: never;
125
129
  verificationSummary?: never;
126
130
  placement?: never;
131
+ continuation?: never;
127
132
  cleanup?: never;
128
133
  leak?: never;
129
134
  overlaps?: never;
@@ -144,6 +149,7 @@ export type AcceptedAbandonResult = AcceptedEnvelope & Readonly<{
144
149
  verificationReuse?: never;
145
150
  verificationSummary?: never;
146
151
  placement?: never;
152
+ continuation?: never;
147
153
  cleanup?: never;
148
154
  leak?: never;
149
155
  overlaps?: never;
@@ -166,6 +172,7 @@ export type AcceptedAuditResult = AcceptedEnvelope & Readonly<{
166
172
  verificationReuse?: never;
167
173
  verificationSummary?: never;
168
174
  placement?: never;
175
+ continuation?: never;
169
176
  overlaps?: never;
170
177
  overlapFailure?: never;
171
178
  workspace?: never;
@@ -54,6 +54,4 @@ export declare function withContractWorktreeAppointment<T>(repository: GitReposi
54
54
  export declare function releaseContractWorktree(repository: GitRepository, contract: ContractId): Promise<void>;
55
55
  /** Remove matching here appointments and their exact generated support from registered worktrees. */
56
56
  export declare function nukeHereAppointments(repository: GitRepository): Promise<void>;
57
- /** Validate the existing Contract-worktree coordination lock before releasing it. */
58
- export declare function nukeContractWorktreeLock(repository: GitRepository): Promise<void>;
59
57
  export declare function projectContractWorktree(repository: GitRepository, state: ContractState | null, register?: PlaceRegister, onConflict?: (diagnostic: string) => never): Promise<ContractWorktreeResult>;
@@ -30,7 +30,14 @@ const FULFILLMENT = [
30
30
  "For work requiring three or more steps, prefer `keiyaku task -C <worktree>` "
31
31
  + "to organize and manage Tasks.",
32
32
  "Promptly update progress for Tasks already present in the current worktree.",
33
- "Leave lifecycle decisions to the caller; report the candidate, verification performed, and any unmet term.", "",
33
+ "Leave lifecycle decisions to the caller; report the candidate, verification performed, and any unmet term.",
34
+ "Deliver from this worktree. A clean worktree delivers HEAD; uncommitted work",
35
+ "needs `deliver --include-dirty`, which captures the final non-ignored tree and",
36
+ "stages or commits nothing. If deliver reports a conflict, run",
37
+ "`deliver --materialize-conflict`, resolve the conflicted files, stage them,",
38
+ "and deliver again; while the merge stays uncommitted that continuation uses",
39
+ "`--include-dirty`. Unresolved paths always refuse.",
40
+ "",
34
41
  "### Reviewer", "",
35
42
  "Review this Contract worktree against the complete Contract.",
36
43
  "Review the complete current worktree snapshot, not a named candidate commit.",
@@ -177,30 +184,12 @@ async function removeOwnedHereSupport(worktree) {
177
184
  export async function nukeHereAppointments(repository) {
178
185
  for (const registered of await registeredWorktrees(repository)) {
179
186
  const scoped = { ...repository, effectiveCwd: registered.path };
180
- await withContractWorktreeAppointment(scoped, async () => {
181
- const appointment = await readContractAppointment(scoped);
182
- if (appointment.kind === "appointed") {
183
- await unlink(appointment.path);
184
- await removeOwnedHereSupport(registered.path);
185
- }
186
- });
187
- }
188
- }
189
- /** Validate the existing Contract-worktree coordination lock before releasing it. */
190
- export async function nukeContractWorktreeLock(repository) {
191
- const path = join(repository.commonDirectory, "keiyaku", "contract-worktree.sqlite");
192
- try {
193
- const value = await lstat(path);
194
- if (!value.isFile() || value.isSymbolicLink())
195
- throw new Error(`Contract worktree lock custody is not a regular file: ${path}`);
196
- }
197
- catch (error) {
198
- if (error.code === "ENOENT")
199
- return;
200
- throw error;
187
+ const appointment = await readContractAppointment(scoped);
188
+ if (appointment.kind === "appointed") {
189
+ await unlink(appointment.path);
190
+ await removeOwnedHereSupport(registered.path);
191
+ }
201
192
  }
202
- const held = await acquireSqliteTransactionLock({ path, mode: "immediate" });
203
- held.close();
204
193
  }
205
194
  async function repair(repository, worktree, relativePath, bytes) {
206
195
  const path = join(worktree, relativePath);
@@ -1,9 +1,7 @@
1
1
  import { access } from "node:fs/promises";
2
- import { nukeContractWorktreeLock, nukeHereAppointments } from "../contract-worktree.js";
3
- import { nukeSettlementFences } from "../settlement/fence.js";
2
+ import { nukeHereAppointments } from "../contract-worktree.js";
4
3
  import { nukeEmptyPlaceAuthority, readPlaceRegister, releaseManagedWorktrees, } from "../workspace-place.js";
5
4
  import { nukeWorktreeHookResidue } from "./hooks.js";
6
- import { nukeReconcileLocks } from "./reconcile.js";
7
5
  import { runGit } from "./process.js";
8
6
  import { NoGitWorldError, registeredWorktrees, repositoryAt, worktreeGitDirectory } from "./repository.js";
9
7
  import { worktreePath } from "./workspace.js";
@@ -86,9 +84,6 @@ export async function nukeGit(world) {
86
84
  }
87
85
  await nukeHereAppointments(repository);
88
86
  await nukeEmptyPlaceAuthority(repository);
89
- await nukeContractWorktreeLock(repository);
90
- await nukeReconcileLocks(repository);
91
- await nukeSettlementFences(repository);
92
87
  await removeOwnedRefs(repository, custody.refs);
93
88
  if (custody.state) {
94
89
  const oid = (await runGit(repository, ["rev-parse", "--verify", "refs/heads/keiyaku-state"])).toString("utf8").trim();
@@ -64,6 +64,4 @@ type ReconcileBatchOptions = Readonly<{
64
64
  }>;
65
65
  /** Reconcile each discovered Contract through its own serialized, fresh observation. */
66
66
  export declare function reconcileBatch(repository: GitRepository, channel: GitDecodeChannel, contracts: Iterable<ContractId>, options: ReconcileBatchOptions): Promise<readonly ReconcileBatchItem[]>;
67
- /** Remove only validated Contract reconciliation lock residue. */
68
- export declare function nukeReconcileLocks(repository: GitRepository): Promise<void>;
69
67
  export {};
@@ -1,5 +1,5 @@
1
- import { access, mkdir, readdir } from "node:fs/promises";
2
- import { dirname, join, resolve } from "node:path";
1
+ import { access, mkdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
3
  import { acquireSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
4
4
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
5
5
  import { CANDIDATE_PIN_REF_NAMESPACE, commonGitDirectory, DELIVERY_REF_NAMESPACE, isKeiyakuOwnedRef, readRef, registeredWorktreePaths, writeCommit, worktreeGitDirectory, } from "./repository.js";
@@ -466,34 +466,3 @@ export async function reconcileBatch(repository, channel, contracts, options) {
466
466
  }
467
467
  return items;
468
468
  }
469
- /** Remove only validated Contract reconciliation lock residue. */
470
- export async function nukeReconcileLocks(repository) {
471
- const root = join(commonGitDirectory(repository), "keiyaku", "locks", "reconcile");
472
- let first;
473
- try {
474
- first = await readdir(root, { withFileTypes: true });
475
- }
476
- catch (error) {
477
- if (error.code === "ENOENT")
478
- return;
479
- throw error;
480
- }
481
- let paths = [];
482
- for (const directory of first) {
483
- if (!/^[0-9a-f]{2}$/u.test(directory.name) || !directory.isDirectory() || directory.isSymbolicLink()) {
484
- throw new Error(`reconciliation lock custody is invalid: ${resolve(root, directory.name)}`);
485
- }
486
- const nested = resolve(root, directory.name);
487
- const files = await readdir(nested, { withFileTypes: true });
488
- for (const file of files) {
489
- if (!/^[0-9a-f]{62}\.sqlite$/u.test(file.name) || !file.isFile() || file.isSymbolicLink()) {
490
- throw new Error(`reconciliation lock custody is invalid: ${resolve(nested, file.name)}`);
491
- }
492
- paths.push(resolve(nested, file.name));
493
- }
494
- }
495
- for (const path of paths) {
496
- const held = await acquireSqliteTransactionLock({ path, mode: "immediate" });
497
- held.close();
498
- }
499
- }
@@ -8,11 +8,18 @@ export type TenderCaptureCoordinates = Readonly<{
8
8
  coordinates: ContractCoordinates;
9
9
  place?: string;
10
10
  workspacePath?: string;
11
+ captureMergeState?: boolean;
12
+ rejectUnmerged?: boolean;
11
13
  }>;
12
- export type TenderCaptureRefusal = Readonly<{
14
+ export type WorktreeMissingRefusal = Readonly<{
13
15
  kind: "worktree-missing";
14
16
  contractId: ContractId;
15
17
  }>;
18
+ export type TenderCaptureRefusal = WorktreeMissingRefusal | Readonly<{
19
+ kind: "unmerged-paths";
20
+ contractId: ContractId;
21
+ paths: readonly string[];
22
+ }>;
16
23
  export type WorkspaceDirtyDelta = Readonly<{
17
24
  staged: readonly string[];
18
25
  unstaged: readonly string[];
@@ -35,12 +42,16 @@ export type DirtyWorkspaceRefusal = Readonly<{
35
42
  export type TenderCapture = Readonly<{
36
43
  tree: GitObjectId;
37
44
  head: SnapshotId;
45
+ mergeHead?: SnapshotId;
38
46
  at: string;
39
47
  dirty: boolean;
40
48
  changes: Awaited<ReturnType<typeof captureWorkspaceTree>>["changes"];
41
49
  }>;
42
50
  /** Capture the complete workspace tree through Git's private index mechanics. */
43
- export declare function captureTender(repository: GitRepository, input: TenderCaptureCoordinates): Promise<Preparation<TenderCapture, TenderCaptureRefusal>>;
51
+ export declare function captureTender(repository: GitRepository, input: TenderCaptureCoordinates & Readonly<{
52
+ rejectUnmerged: true;
53
+ }>): Promise<Preparation<TenderCapture, TenderCaptureRefusal>>;
54
+ export declare function captureTender(repository: GitRepository, input: TenderCaptureCoordinates): Promise<Preparation<TenderCapture, WorktreeMissingRefusal>>;
44
55
  export declare function dirtyTenderRefusal(repository: GitRepository, contractId: ContractId, tender: TenderCapture): Promise<DirtyWorkspaceRefusal>;
45
56
  export declare function dirtyTenderDelta(repository: GitRepository, tender: TenderCapture): Promise<WorkspaceDirtyDelta | undefined>;
46
57
  type GitCommitIdentity = Readonly<{
@@ -13,13 +13,49 @@ function workspaceFor(repository, input) {
13
13
  return input.workspacePath;
14
14
  return input.place === undefined ? undefined : worktreePath(repository, input.place);
15
15
  }
16
- /** Capture the complete workspace tree through Git's private index mechanics. */
16
+ async function workspaceMergeHead(repository, workspace) {
17
+ try {
18
+ return mintSnapshotId((await runGit(repository, ["-C", workspace, "rev-parse", "-q", "--verify", "MERGE_HEAD"]))
19
+ .toString("utf8")
20
+ .trim());
21
+ }
22
+ catch (error) {
23
+ if (error instanceof GitPlumbingError && error.status === 1)
24
+ return undefined;
25
+ throw error;
26
+ }
27
+ }
28
+ async function unmergedWorkspacePaths(repository, workspace) {
29
+ const records = (await runGit(repository, ["-C", workspace, "ls-files", "--unmerged", "-z"]))
30
+ .toString("utf8")
31
+ .split("\0");
32
+ const paths = new Set();
33
+ for (const record of records.slice(0, -1)) {
34
+ const separator = record.indexOf("\t");
35
+ if (separator < 0)
36
+ throw new Error(`malformed unmerged index record: ${record}`);
37
+ paths.add(record.slice(separator + 1));
38
+ }
39
+ return [...paths].sort();
40
+ }
17
41
  export async function captureTender(repository, input) {
18
42
  const workspace = workspaceFor(repository, input);
19
43
  if (workspace === undefined || !(await workspaceExists(repository, input.coordinates.workspace, workspace))) {
20
44
  return { kind: "refused", refusal: { kind: "worktree-missing", contractId: input.contractId } };
21
45
  }
22
- return { kind: "prepared", data: await captureWorkspaceTree(repository, workspace) };
46
+ if (input.rejectUnmerged === true) {
47
+ const paths = await unmergedWorkspacePaths(repository, workspace);
48
+ if (paths.length > 0)
49
+ return { kind: "refused", refusal: { kind: "unmerged-paths", contractId: input.contractId, paths } };
50
+ }
51
+ const captured = await captureWorkspaceTree(repository, workspace);
52
+ const mergeHead = input.captureMergeState === true
53
+ ? await workspaceMergeHead(repository, workspace)
54
+ : undefined;
55
+ return {
56
+ kind: "prepared",
57
+ data: { ...captured, ...(mergeHead === undefined ? {} : { mergeHead }) },
58
+ };
23
59
  }
24
60
  async function dirtyShortStat(repository, tender) {
25
61
  return decodeGitNumstat(await runGit(repository, [
@@ -81,9 +117,12 @@ export async function prepareDeliveryCommitMetadata(repository, input) {
81
117
  }
82
118
  /** Materialize the tender tree when it contains admitted dirty workspace bytes. */
83
119
  export async function materializeTenderSnapshot(repository, tender, commit) {
84
- if (!tender.dirty)
120
+ if (!tender.dirty && tender.mergeHead === undefined)
85
121
  return tender.head;
86
- const oid = (await runGitWithEnvironment(repository, ["commit-tree", tender.tree, "-p", gitObjectIdForSnapshot(tender.head)], commit.message, {
122
+ const parents = ["-p", gitObjectIdForSnapshot(tender.head)];
123
+ if (tender.mergeHead !== undefined)
124
+ parents.push("-p", gitObjectIdForSnapshot(tender.mergeHead));
125
+ const oid = (await runGitWithEnvironment(repository, ["commit-tree", tender.tree, ...parents], commit.message, {
87
126
  GIT_AUTHOR_NAME: commit.identity.name,
88
127
  GIT_AUTHOR_EMAIL: commit.identity.email,
89
128
  GIT_COMMITTER_NAME: commit.identity.name,
@@ -244,22 +244,42 @@ async function joinAkuma(path, observeContract, dispatches, aliases) {
244
244
  for (const binding of aliases.value)
245
245
  aliasById.set(binding.akuId, [...(aliasById.get(binding.akuId) ?? []), binding]);
246
246
  const dispatchById = new Map(dispatches.map((dispatch) => [dispatch.akuId, dispatch]));
247
+ const rows = source.rows.map((row) => {
248
+ const dispatch = dispatchById.get(row.id);
249
+ return {
250
+ ...row,
251
+ aliases: (aliasById.get(row.id) ?? []).map((binding) => binding.alias),
252
+ ...(dispatch === undefined ? {} : {
253
+ contract: {
254
+ id: dispatch.contractId,
255
+ observed: observeContract(dispatch.contractId),
256
+ },
257
+ }),
258
+ };
259
+ });
260
+ const running = rows
261
+ .filter((row) => row.life === "running")
262
+ .sort((left, right) => {
263
+ const l = "lastActivityAt" in left && left.lastActivityAt !== null ? Date.parse(left.lastActivityAt) : -Infinity;
264
+ const r = "lastActivityAt" in right && right.lastActivityAt !== null ? Date.parse(right.lastActivityAt) : -Infinity;
265
+ return r - l;
266
+ })
267
+ .slice(0, 3);
268
+ const snapshots = new Map(await Promise.all(running.map(async (row) => {
269
+ try {
270
+ return [row.id, (await Akuma.of(path).of({ id: row.id }).status()).timeline];
271
+ }
272
+ catch {
273
+ return [row.id, undefined];
274
+ }
275
+ })));
247
276
  return {
248
277
  kind: "present",
249
278
  value: {
250
279
  ...source,
251
- rows: source.rows.map((row) => {
252
- const dispatch = dispatchById.get(row.id);
253
- return {
254
- ...row,
255
- aliases: (aliasById.get(row.id) ?? []).map((binding) => binding.alias),
256
- ...(dispatch === undefined ? {} : {
257
- contract: {
258
- id: dispatch.contractId,
259
- observed: observeContract(dispatch.contractId),
260
- },
261
- }),
262
- };
280
+ rows: rows.map((row) => {
281
+ const snapshot = snapshots.get(row.id);
282
+ return snapshot === undefined ? row : { ...row, snapshot };
263
283
  }),
264
284
  },
265
285
  };
@@ -1,6 +1,6 @@
1
1
  import type { ContractBoard, ContractDisposition } from "../library/contract.js";
2
2
  import type { TaskId, TaskRef, TaskRow } from "../task/index.js";
3
- import type { AkumaList, AkumaListRow, UnbornAkumaListRow } from "../akuma/index.js";
3
+ import type { AkumaList, AkumaListRow, UnbornAkumaListRow, ActivitySnapshot } from "../akuma/index.js";
4
4
  import type { AkumaAlias } from "../identity/selector.js";
5
5
  import type { WorldRoot } from "../world.js";
6
6
  import type { ContractId } from "../library/contract.js";
@@ -53,6 +53,7 @@ export type AkumaKanshiRow = (AkumaListRow | UnbornAkumaListRow) & Readonly<{
53
53
  id: string;
54
54
  observed: ContractEndpointObservation;
55
55
  }>;
56
+ snapshot?: ActivitySnapshot;
56
57
  }>;
57
58
  export type AkumaKanshiWorld = Omit<AkumaList, "rows"> & Readonly<{
58
59
  rows: readonly AkumaKanshiRow[];
@@ -0,0 +1,26 @@
1
+ import type { ContractId, ContractState } from "../core/facts/types.js";
2
+ import { type GitDecodeChannel } from "../git/read-observation.js";
3
+ import type { DocumentDerivation, HereWorkspaceResolver, PlacementStop, RepositoryScope } from "../protocol/operations.js";
4
+ import type { AcceptedIntent } from "./mutation.js";
5
+ export type ContinuationReport = Readonly<{
6
+ attempted: number;
7
+ claimed: readonly ContractId[];
8
+ stopped: readonly Readonly<{
9
+ contractId: ContractId;
10
+ stop: PlacementStop | Readonly<{
11
+ kind: "already-terminal" | "delivery-missing";
12
+ }>;
13
+ }>[];
14
+ }>;
15
+ export declare function continueDeliveredDependents<Value extends object>(input: Readonly<{
16
+ scope: RepositoryScope;
17
+ channel: GitDecodeChannel;
18
+ contractId: ContractId;
19
+ accepted: AcceptedIntent<Value>;
20
+ deriveDocument: (state: ContractState) => DocumentDerivation;
21
+ resolveHereWorkspace?: HereWorkspaceResolver;
22
+ actor?: import("../core/facts/types.js").ActorId;
23
+ signal?: AbortSignal;
24
+ }>): Promise<AcceptedIntent<Value & Readonly<{
25
+ continuation?: ContinuationReport;
26
+ }>>>;
@@ -0,0 +1,78 @@
1
+ import { observeActiveContractWorld } from "../git/observe.js";
2
+ import { withGitReadObservation } from "../git/read-observation.js";
3
+ import { continueDeliveryOperation } from "../protocol/deliver.js";
4
+ function directDependents(world, claimed, attempted, newlyClaimed) {
5
+ const dependents = [];
6
+ for (const [contractId, record] of world.contracts) {
7
+ const state = record.state;
8
+ if (state === null
9
+ || attempted.has(contractId)
10
+ || !state.terms.after.includes(claimed)
11
+ || state.delivery === null
12
+ || !state.terms.after.every((dependency) => newlyClaimed.has(dependency) || world.eligibility.get(dependency)?.terminal?.kind === "claimed"))
13
+ continue;
14
+ dependents.push({ state, journal: record.entries });
15
+ }
16
+ return dependents.sort((left, right) => left.state.id.localeCompare(right.state.id));
17
+ }
18
+ function appendAccepted(primary, child) {
19
+ const effects = [...(primary.physical?.effects ?? []), ...(child.physical?.effects ?? [])];
20
+ const lag = [...(primary.physical?.lag ?? []), ...(child.physical?.lag ?? [])];
21
+ return {
22
+ ...primary,
23
+ facts: [...primary.facts, ...child.facts],
24
+ ...(effects.length === 0 && lag.length === 0 ? {} : { physical: { effects, lag } }),
25
+ };
26
+ }
27
+ export async function continueDeliveredDependents(input) {
28
+ const world = await withGitReadObservation(input.scope, input.channel, async (observation) => await observeActiveContractWorld(observation));
29
+ const pending = [input.contractId];
30
+ const attempted = new Set();
31
+ const newlyClaimed = new Set(pending);
32
+ const claimed = [];
33
+ const stopped = [];
34
+ let accepted = input.accepted;
35
+ while (pending.length > 0) {
36
+ const parent = pending.shift();
37
+ for (const candidate of directDependents(world, parent, attempted, newlyClaimed)) {
38
+ const contractId = candidate.state.id;
39
+ attempted.add(contractId);
40
+ const child = await continueDeliveryOperation({
41
+ scope: input.scope,
42
+ channel: input.channel,
43
+ state: candidate.state,
44
+ journal: candidate.journal,
45
+ deriveDocument: input.deriveDocument,
46
+ ...(input.resolveHereWorkspace === undefined ? {} : { resolveHereWorkspace: input.resolveHereWorkspace }),
47
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
48
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
49
+ });
50
+ accepted = appendAccepted(accepted, child.admission);
51
+ if (child.evidence.completion !== undefined) {
52
+ claimed.push(contractId);
53
+ newlyClaimed.add(contractId);
54
+ pending.push(contractId);
55
+ continue;
56
+ }
57
+ const stop = child.evidence.placement;
58
+ if (stop === undefined)
59
+ throw new Error("incomplete continuation is missing its placement stop");
60
+ if ("refusal" in stop && stop.refusal.kind === "terminal") {
61
+ stopped.push({ contractId, stop: { kind: "already-terminal" } });
62
+ }
63
+ else if ("refusal" in stop && stop.refusal.kind === "delivery-missing") {
64
+ stopped.push({ contractId, stop: { kind: "delivery-missing" } });
65
+ }
66
+ else {
67
+ stopped.push({ contractId, stop });
68
+ }
69
+ }
70
+ }
71
+ return {
72
+ ...accepted,
73
+ value: {
74
+ ...accepted.value,
75
+ ...(attempted.size === 0 ? {} : { continuation: { attempted: attempted.size, claimed, stopped } }),
76
+ },
77
+ };
78
+ }
@@ -6,7 +6,7 @@ export { AuthorityCorruptionError } from "../core/facts/errors.js";
6
6
  import { type PlacementStop, type RepositoryScope, type VerificationStop, type DeliveryPreparationRefusal } from "../protocol/operations.js";
7
7
  import type { ContractBoard, ContractDisposition, ContractGateCurrent, ContractGateReport, ContractObservation, ContractPhase, ContractRow } from "../protocol/read/status.js";
8
8
  import type { AuditReport } from "../protocol/audit.js";
9
- import { type DeliverValue, type IntegrationConflictMaterialized, type VerificationReuse } from "../protocol/deliver.js";
9
+ import { type IntegrationConflictMaterialized, type VerificationReuse } from "../protocol/deliver.js";
10
10
  import type { ReconcileReport as ProtocolReconcileReport } from "../protocol/reconcile.js";
11
11
  import { type ReviewValue } from "../protocol/review.js";
12
12
  import type { FactKind } from "../core/facts/types.js";
@@ -15,7 +15,8 @@ import { type SettlementReport } from "../settlement/settle.js";
15
15
  import { type TaskId } from "../task/identity.js";
16
16
  import { Repo, type ReconcileInput } from "./repo.js";
17
17
  import { type AuditInput } from "./audit.js";
18
- import { Delivery } from "./delivery.js";
18
+ import { Delivery, type DeliveryValue } from "./delivery.js";
19
+ import { type ContinuationReport } from "./continuation.js";
19
20
  import { type MutationResult } from "./mutation.js";
20
21
  import { KeiyakuRetry, type KeiyakuRefusal, type KeiyakuRetryReason } from "./refusal.js";
21
22
  export { KeiyakuRefused, type ContractAppointmentRefusal, } from "./refusal.js";
@@ -41,8 +42,11 @@ export type ContractHistory = Readonly<{
41
42
  }>;
42
43
  export type ActorId = string;
43
44
  export type AttestationVerdict = "satisfied" | "unsatisfied";
44
- export type Review = ReviewValue;
45
+ export type Review = ReviewValue & Readonly<{
46
+ continuation?: ContinuationReport;
47
+ }>;
45
48
  export type { PlacementStop, VerificationReuse, VerificationStop, IntegrationConflictMaterialized };
49
+ export type { ContinuationReport };
46
50
  export type TopologyEffect = ProtocolReconcileReport["effects"][number] | ContractFileEffect;
47
51
  export type Lag = ProtocolReconcileReport["lag"][number] | ContractFileLag;
48
52
  export type { MutationResult };
@@ -115,7 +119,7 @@ export type DeliverInput = ActorOptions & Readonly<{
115
119
  }>;
116
120
  type ForwardedDeliveryReceipt = Readonly<{
117
121
  kind: "accepted";
118
- result: MutationResult<DeliverValue>;
122
+ result: MutationResult<DeliveryValue>;
119
123
  }> | Readonly<{
120
124
  kind: "refused";
121
125
  refusal: KeiyakuRefusal;
@@ -125,7 +129,7 @@ type ForwardedDeliveryReceipt = Readonly<{
125
129
  }> | IntegrationConflictMaterialized;
126
130
  type ForwardedReviewReceipt = Readonly<{
127
131
  kind: "accepted";
128
- result: MutationResult<ReviewValue>;
132
+ result: MutationResult<Review>;
129
133
  }> | Readonly<{
130
134
  kind: "refused";
131
135
  refusal: KeiyakuRefusal;