@astrosheep/keiyaku 4.5.9 → 4.5.11

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.
@@ -1,6 +1,8 @@
1
1
  import { archetypeName } from "../../akuma/identity.js";
2
2
  import { parseTaskNamespaceSelector } from "../../task/catalog.js";
3
- import { CliUsageError, usageLine } from "../usage.js";
3
+ import { CliUsageError } from "../usage.js";
4
+ import { renderContractUsage } from "./contract-help.js";
5
+ export { CONTRACT_COMMAND_SPECS, renderContractHelp, renderContractUsage, } from "./contract-help.js";
4
6
  function parseAkumaCatalogPath(value) {
5
7
  if (value === "aku" || value === "aku/")
6
8
  return { kind: "archetypes" };
@@ -11,135 +13,6 @@ function parseAkumaCatalogPath(value) {
11
13
  return null;
12
14
  return { kind: "akuma", archetype: archetypeName(match[1]) };
13
15
  }
14
- export const CONTRACT_COMMAND_SPECS = {
15
- bind: {
16
- positional: "none",
17
- stdin: "optional",
18
- flags: {
19
- actor: "value",
20
- task: "value",
21
- target: "value",
22
- after: "repeat-value",
23
- gates: "raw-value",
24
- "fork-of": "value",
25
- json: "boolean",
26
- },
27
- usage: "bind [--task <task/...>] [--target <ref>] [--after <kei/...> ...] [--gates <name,...>] [--actor <actor>] [--json] - | bind --fork-of <kei/...> [--target <ref>] [--actor <actor>] [--json]",
28
- purpose: "Create one Contract from stdin Markdown or a sibling fork.",
29
- },
30
- amend: {
31
- positional: "optional",
32
- stdin: "optional",
33
- flags: { actor: "value", after: "repeat-value", "clear-after": "boolean", gates: "raw-value", json: "boolean" },
34
- usage: "amend [<contract>|@<contract>] [--after <kei/...> ... | --clear-after] [--gates <name,...>] [--actor <actor>] [--json] [-]",
35
- purpose: "Amend one Contract's document operations or structured terms.",
36
- details: [
37
- " ## Context|Objective|Design|Region|Criteria|Verification|<extension> (replace, or add new extension)",
38
- " ## Replace: Context|Objective|Design|Region|Criteria|Verification|<extension>",
39
- " ## Append: Context|Objective|Design|Criteria|<extension>",
40
- " ## Add: Criteria|<new-extension-title>",
41
- " ## Update: <existing-extension-title>",
42
- " ## Remove: <existing-extension-title>",
43
- ].join("\n"),
44
- },
45
- deliver: {
46
- positional: "optional",
47
- stdin: "none",
48
- flags: {
49
- message: "value",
50
- "include-dirty": "boolean",
51
- "materialize-conflict": "boolean",
52
- json: "boolean",
53
- },
54
- usage: "deliver [<contract>|@<contract>] [--message <text>] [--include-dirty] [--materialize-conflict] [--json]",
55
- purpose: "Deliver one Contract candidate from the appointed worktree.",
56
- details: [
57
- " --include-dirty Capture the complete non-ignored worktree tree as the",
58
- " candidate; stages nothing, commits nothing, including",
59
- " an unmerged shared index.",
60
- " --materialize-conflict After a conflict result, project the judged targetHead",
61
- " into the worktree as an uncommitted merge. Not a",
62
- " delivery: resolve, then deliver with --include-dirty.",
63
- ].join("\n"),
64
- },
65
- review: {
66
- positional: "optional",
67
- stdin: "optional",
68
- flags: { satisfied: "boolean", unsatisfied: "boolean", summary: "value", json: "boolean" },
69
- usage: "review [<contract>|@<contract>] (--satisfied | --unsatisfied) (--summary <text> | -) [--json]",
70
- purpose: "Record one review verdict.",
71
- },
72
- arc: {
73
- positional: "optional",
74
- stdin: "required",
75
- flags: { actor: "value", json: "boolean" },
76
- usage: "arc [<contract>|@<contract>] [--actor <actor>] [--json] -",
77
- purpose: "Record stdin arc Markdown for one Contract.",
78
- },
79
- abandon: {
80
- positional: "optional",
81
- stdin: "none",
82
- flags: { actor: "value", note: "value", json: "boolean" },
83
- usage: "abandon [<contract>|@<contract>] [--note <text>] [--actor <actor>] [--json]",
84
- purpose: "Abandon one Contract with an optional note.",
85
- },
86
- status: {
87
- positional: "optional",
88
- stdin: "none",
89
- flags: { json: "boolean" },
90
- usage: "status [<contract>|@name|<aku/...>]... [--json]",
91
- purpose: "Read the world status board or one or more Contract and Akuma projections.",
92
- },
93
- show: {
94
- positional: "optional",
95
- stdin: "none",
96
- flags: { json: "boolean" },
97
- usage: "show [<contract>|@<contract>] [--json]",
98
- purpose: "Read one Contract guidance projection.",
99
- },
100
- ls: {
101
- positional: "optional",
102
- stdin: "none",
103
- flags: { json: "boolean" },
104
- usage: 'ls task[/] [--json]\nls kei[/] [--json]\nls aku[/] [--json]\nls aku/<akuma>[/] [--json]\nls "aku/*/*" [--json]',
105
- purpose: "List one identity directory.",
106
- },
107
- audit: {
108
- positional: "optional",
109
- stdin: "none",
110
- flags: { "include-dirty": "boolean", diff: "boolean", json: "boolean" },
111
- usage: "audit [<contract>|@<contract>] [--include-dirty] [--diff] [--json]",
112
- purpose: "Ask what candidate preparation, Verification, and target placement would do.",
113
- },
114
- reconcile: {
115
- positional: "optional",
116
- stdin: "none",
117
- flags: { "retry-hooks": "boolean", json: "boolean" },
118
- usage: "reconcile [<contract>|@<contract>] [--retry-hooks] [--json]",
119
- purpose: "Reconcile one Contract or the invocation world.",
120
- },
121
- nuke: {
122
- positional: "none",
123
- stdin: "none",
124
- flags: { confirm: "value", json: "boolean" },
125
- usage: "nuke [--confirm <WorldRoot>] [--json]",
126
- purpose: "Remove Keiyaku-owned data from one confirmed World.",
127
- },
128
- settings: {
129
- positional: "none",
130
- stdin: "none",
131
- flags: { json: "boolean" },
132
- usage: "settings [--json]",
133
- purpose: "Show effective Settings (user + project, read-only)",
134
- },
135
- region: {
136
- positional: "optional",
137
- stdin: "none",
138
- flags: { path: "repeat-value", json: "boolean" },
139
- usage: "region [<contract>] [--json]\nregion --path <pattern> [--path <pattern> ...] [--json]",
140
- purpose: "Read active declared Contract Regions.",
141
- },
142
- };
143
16
  function optionalFlag(flags, name) {
144
17
  const value = flags[name];
145
18
  return typeof value === "string" ? value : undefined;
@@ -296,36 +169,6 @@ function parseLs(parts) {
296
169
  refuse("ls", error instanceof Error ? error.message : "invalid ls directory");
297
170
  }
298
171
  }
299
- export function renderContractHelp(command) {
300
- const spec = CONTRACT_COMMAND_SPECS[command];
301
- if (command === "settings") {
302
- return [
303
- "Show effective Settings — the merged read-only view of:",
304
- " user ~/.keiyaku/settings.json",
305
- " project <WorldRoot>/.keiyaku/settings.json",
306
- "A project record wholly shadows the same-name user record.",
307
- "",
308
- "Shape: namespace -> entry -> JSON value. There is no write",
309
- "command; edit the files directly.",
310
- "",
311
- "Recognized settings:",
312
- " gates gate bundles selected by bind and amend",
313
- " worktree commands run when worktrees are created or destroyed",
314
- " git.requireBranchesToBeUpToDate whether deliver and audit require up-to-date target branches",
315
- " providers the available Akuma and how each is run",
316
- "",
317
- "Each entry is validated by the feature that reads it. A rejected",
318
- "value's diagnostic states the expected shape.",
319
- "",
320
- usageLine(spec.usage),
321
- ].join("\n");
322
- }
323
- const help = `${spec.purpose}\n\n${usageLine(spec.usage)}`;
324
- return spec.details === undefined ? help : `${help}\n\n${spec.details}`;
325
- }
326
- export function renderContractUsage(command) {
327
- return usageLine(CONTRACT_COMMAND_SPECS[command].usage);
328
- }
329
172
  export function parseContractCommand(parts) {
330
173
  switch (parts.command) {
331
174
  case "bind":
@@ -19,12 +19,16 @@ export const CONTRACT_DELIVERER_SKILL = [
19
19
  "",
20
20
  "## Deliver",
21
21
  "",
22
+ "`deliver` creates the immutable candidate commit and prepares its integration with the target as it exists for that invocation. It runs or reuses Verification, records the candidate, and requests placement. If the target moves, run `deliver` again; Keiyaku recomputes the current-target integration. A manual rebase is optional candidate shaping, not target refresh.",
23
+ "",
24
+ "When the finished worktree itself is the candidate, use `deliver --include-dirty`; Keiyaku captures its complete non-ignored state without moving this worktree's HEAD or real index.",
25
+ "",
22
26
  "```bash",
23
27
  "keiyaku deliver <contract> --include-dirty",
24
28
  "keiyaku deliver <contract> --materialize-conflict",
25
29
  "```",
26
30
  "",
27
- "Delivery submits the full current worktree state as the candidate for review and placement. Deliver when Verification passes and the evidence is in place; the Reviewer judges the rest.",
31
+ "If delivery reports an integration conflict, materialize that judged conflict, resolve it in this worktree, then deliver the resolved bytes with `--include-dirty`. Deliver when the candidate is ready and the evidence is in place; delivery records and requests placement, while the Reviewer independently judges any declared review gate.",
28
32
  ]
29
33
  .join("\n")
30
34
  .concat("\n");
@@ -8,7 +8,7 @@ import { contractLocator, contractPhysicalName, gitObjectIdForSnapshot } from ".
8
8
  import { observeContractAt } from "./observe.js";
9
9
  import { acquireTargetPlacementFence, recoverTargetPlacement, } from "./target-placement.js";
10
10
  import { runCreateHooks } from "./hooks.js";
11
- import { followDependentManagedWorktree, followManagedWorktree, worktreePath } from "./workspace.js";
11
+ import { followDependentManagedWorktree, retireConflictHandoff, worktreePath } from "./workspace.js";
12
12
  import { removeCollectableScratchWorktrees } from "./scratch.js";
13
13
  import { migrateContractCustodyRefs } from "./ref-migration.js";
14
14
  import { reconcileTerminalManagedWorktree, removeRef, updateRef } from "./terminal-reconcile.js";
@@ -111,26 +111,19 @@ async function reconcileActiveManagedWorktree({ repository, hooks, retryHooks, p
111
111
  const desired = state.delivery?.data.tenderSnapshot ?? state.coordinates.start;
112
112
  effects.push(await updateRef(repository, deliveryRefFor(state.id), desired));
113
113
  const projection = await worktree(repository, topology, path, desired);
114
- if (projection.kind === "worktree" && projection.action === "unchanged" && state.delivery !== null) {
115
- const follow = await followManagedWorktree(repository, path, state.delivery.data.tenderSnapshot);
116
- if (follow.kind === "followed") {
117
- effects.push({ kind: "worktree", path, action: "followed", before: follow.before, after: follow.after });
118
- }
119
- else {
120
- effects.push(projection);
121
- if (follow.kind === "retained") {
122
- lag.push({
123
- kind: "worktree-follow-retained",
124
- path,
125
- tender: state.delivery.data.tenderSnapshot,
126
- head: follow.head,
127
- reason: follow.reason,
128
- });
129
- }
130
- }
131
- }
132
- else {
133
- effects.push(projection);
114
+ effects.push(projection);
115
+ const handoff = await retireConflictHandoff(repository, {
116
+ contractId: state.id,
117
+ place,
118
+ workspace: path,
119
+ consume: state.delivery !== null,
120
+ });
121
+ if (state.delivery !== null && handoff.kind === "retained") {
122
+ lag.push({
123
+ kind: "reconcile-failed",
124
+ stage: "effect",
125
+ diagnostic: `conflict handoff retirement retained: ${handoff.reason}`,
126
+ });
134
127
  }
135
128
  if (projection.action === "created" || retryHooks) {
136
129
  const hookRun = await runCreateHooks(path, hooks);
@@ -143,6 +136,35 @@ async function reconcileActiveManagedWorktree({ repository, hooks, retryHooks, p
143
136
  : await removeRef(repository, candidatePinRefFor(state.id))));
144
137
  return complete(effects, lag, hookRuns);
145
138
  }
139
+ async function terminalHandoffRetained(input, state, topology, lag) {
140
+ if (input.place === undefined)
141
+ return false;
142
+ const path = worktreePath(input.repository, input.place);
143
+ if (!topology.paths.has(path) && !(await pathExists(path)))
144
+ return false;
145
+ const handoff = await retireConflictHandoff(input.repository, {
146
+ contractId: state.id,
147
+ place: input.place,
148
+ workspace: path,
149
+ consume: state.delivery !== null,
150
+ });
151
+ if (handoff.kind === "active") {
152
+ lag.push({
153
+ kind: "reconcile-failed",
154
+ stage: "effect",
155
+ diagnostic: "conflict handoff retirement retained: active",
156
+ });
157
+ return true;
158
+ }
159
+ if (handoff.kind !== "retained")
160
+ return false;
161
+ lag.push({
162
+ kind: "reconcile-failed",
163
+ stage: "effect",
164
+ diagnostic: `conflict handoff retirement retained: ${handoff.reason}`,
165
+ });
166
+ return true;
167
+ }
146
168
  async function reconcileWithTopology(input, state, topology) {
147
169
  const { repository } = input;
148
170
  const effects = [];
@@ -161,6 +183,8 @@ async function reconcileWithTopology(input, state, topology) {
161
183
  effects.push(...targetCheckouts.effects);
162
184
  lag.push(...targetCheckouts.lag);
163
185
  if (state.terminal) {
186
+ if (await terminalHandoffRetained(input, state, topology, lag))
187
+ return complete(effects, lag, hookRuns);
164
188
  return await reconcileTerminalManagedWorktree(input, state, topology, { effects, lag, hookRuns }, { ref: deliveryRefFor(state.id), pin: candidatePinRefFor(state.id) });
165
189
  }
166
190
  const result = await reconcileActiveManagedWorktree(input, state, topology, { effects, lag, hookRuns });
@@ -4,6 +4,7 @@ export type { TreeChange } from "./tree.js";
4
4
  export declare const GIT_REF = "refs/heads/keiyaku-state";
5
5
  export declare const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
6
6
  export declare const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
7
+ export declare const HANDOFF_RECEIPT_REF_NAMESPACE = "refs/keiyaku/handoff";
7
8
  export declare const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
8
9
  export declare const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
9
10
  export declare const GIT_FORMAT_PATH = "meta/format.json";
@@ -7,6 +7,7 @@ import { parseTreeObject, prepareTreeUpdate, treeUpdate, validPath, } from "./tr
7
7
  export const GIT_REF = "refs/heads/keiyaku-state";
8
8
  export const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
9
9
  export const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
10
+ export const HANDOFF_RECEIPT_REF_NAMESPACE = "refs/keiyaku/handoff";
10
11
  export const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
11
12
  export const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
12
13
  export const GIT_FORMAT_PATH = "meta/format.json";
@@ -19,6 +20,8 @@ export function isKeiyakuOwnedRef(ref) {
19
20
  ref.startsWith(`${DELIVERY_REF_NAMESPACE}/`) ||
20
21
  ref === CANDIDATE_PIN_REF_NAMESPACE ||
21
22
  ref.startsWith(`${CANDIDATE_PIN_REF_NAMESPACE}/`) ||
23
+ ref === HANDOFF_RECEIPT_REF_NAMESPACE ||
24
+ ref.startsWith(`${HANDOFF_RECEIPT_REF_NAMESPACE}/`) ||
22
25
  ref === LEGACY_DELIVERY_REF_NAMESPACE ||
23
26
  ref.startsWith(`${LEGACY_DELIVERY_REF_NAMESPACE}/`) ||
24
27
  ref === LEGACY_CANDIDATE_PIN_REF_NAMESPACE ||
@@ -1,4 +1,4 @@
1
- import type { SnapshotId } from "../core/facts/types.js";
1
+ import type { ContractId, SnapshotId } from "../core/facts/types.js";
2
2
  import { type GitObjectId } from "./identity.js";
3
3
  import { type GitRepository } from "./process.js";
4
4
  export type WorkspaceChanges = Readonly<{
@@ -15,17 +15,6 @@ export type WorkspaceTree = Readonly<{
15
15
  changes: WorkspaceChanges;
16
16
  }>;
17
17
  export declare function worktreePath(repository: GitRepository, place: string): string;
18
- export type ManagedWorktreeFollow = Readonly<{
19
- kind: "followed";
20
- before: SnapshotId;
21
- after: SnapshotId;
22
- }> | Readonly<{
23
- kind: "unchanged";
24
- }> | Readonly<{
25
- kind: "retained";
26
- head: SnapshotId;
27
- reason: "head-moved" | "head-attached" | "operation-in-progress" | "unsupported-parent-shape";
28
- }>;
29
18
  export type DependentWorktreeFollow = Readonly<{
30
19
  kind: "followed";
31
20
  before: SnapshotId;
@@ -38,8 +27,6 @@ export type DependentWorktreeFollow = Readonly<{
38
27
  reason: "head-moved" | "head-attached" | "operation-in-progress";
39
28
  paths: readonly string[];
40
29
  }>;
41
- /** Follow an accepted tender only through the native index-and-HEAD transition. */
42
- export declare function followManagedWorktree(repository: GitRepository, workspace: string, tender: SnapshotId): Promise<ManagedWorktreeFollow>;
43
30
  /** Follow a dependency baseline only through a clean, detached fast-forward. */
44
31
  export declare function followDependentManagedWorktree(repository: GitRepository, workspace: string, target: SnapshotId): Promise<DependentWorktreeFollow>;
45
32
  export declare function withPrivateGitIndex<Value>(action: (environment: Readonly<{
@@ -91,3 +78,22 @@ export declare function observeWorkspace(repository: GitRepository, location: Co
91
78
  export declare function observeTargetLag(repository: GitRepository, workspace: string, head: SnapshotId | null | undefined): Promise<ContractTargetLag>;
92
79
  /** Capture tracked and untracked workspace bytes without changing its real index. */
93
80
  export declare function captureWorkspaceTree(repository: GitRepository, workspace: string): Promise<WorkspaceTree>;
81
+ export type HandoffRetirement = Readonly<{
82
+ kind: "absent" | "active" | "orphan-retired" | "retired";
83
+ }> | Readonly<{
84
+ kind: "retained";
85
+ reason: "receipt-mismatch" | "operation-in-progress";
86
+ }>;
87
+ export declare function recordConflictHandoff(repository: GitRepository, input: Readonly<{
88
+ contractId: ContractId;
89
+ place: string;
90
+ workspace: string;
91
+ head: SnapshotId;
92
+ mergeHead: SnapshotId;
93
+ }>): Promise<void>;
94
+ export declare function retireConflictHandoff(repository: GitRepository, input: Readonly<{
95
+ contractId: ContractId;
96
+ place: string;
97
+ workspace: string;
98
+ consume: boolean;
99
+ }>): Promise<HandoffRetirement>;
@@ -1,9 +1,9 @@
1
1
  import { access, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
- import { gitObjectId, mintSnapshotId } from "./identity.js";
4
+ import { contractPhysicalName, gitObjectId, mintSnapshotId } from "./identity.js";
5
5
  import { GitPlumbingError, runGit, runGitWithEnvironment } from "./process.js";
6
- import { worktreeGitDirectory } from "./repository.js";
6
+ import { HANDOFF_RECEIPT_REF_NAMESPACE, readBlob, readRef, worktreeGitDirectory, writeBlob } from "./repository.js";
7
7
  const WORKTREE_DIRECTORY = [".keiyaku", "wt"];
8
8
  export function worktreePath(repository, place) {
9
9
  return resolve(repository.primaryWorktree, ...WORKTREE_DIRECTORY, place);
@@ -35,41 +35,6 @@ async function workspaceOperationState(repository, workspace, gitDirectory) {
35
35
  const unmerged = (await runGit(repository, ["-C", workspace, "ls-files", "--unmerged", "-z"])).length > 0;
36
36
  return { other: other.some(Boolean), unmerged };
37
37
  }
38
- /** Follow an accepted tender only through the native index-and-HEAD transition. */
39
- export async function followManagedWorktree(repository, workspace, tender) {
40
- const head = await workspaceRevision(repository, workspace, "HEAD");
41
- if (head === null)
42
- throw new Error("managed worktree HEAD is missing");
43
- const attached = await runGit(repository, ["-C", workspace, "symbolic-ref", "--quiet", "HEAD"]).then(() => true, (error) => {
44
- if (error instanceof GitPlumbingError && error.status === 1)
45
- return false;
46
- throw error;
47
- });
48
- if (attached)
49
- return { kind: "retained", head, reason: "head-attached" };
50
- if (head === tender)
51
- return { kind: "unchanged" };
52
- const parents = (await runGit(repository, ["show", "-s", "--format=%P", gitObjectId(tender)]))
53
- .toString("utf8")
54
- .trim()
55
- .split(" ")
56
- .filter((parent) => parent.length > 0)
57
- .map((parent) => mintSnapshotId(parent));
58
- if (parents.length > 0 && head !== parents[0])
59
- return { kind: "retained", head, reason: "head-moved" };
60
- if (parents.length !== 1 && parents.length !== 2)
61
- return { kind: "retained", head, reason: "unsupported-parent-shape" };
62
- const gitDirectory = await worktreeGitDirectory(repository, workspace);
63
- const mergeHead = await workspaceRevision(repository, workspace, "MERGE_HEAD");
64
- const operation = await workspaceOperationState(repository, workspace, gitDirectory);
65
- const admitted = parents.length === 1
66
- ? mergeHead === null && !operation.other && !operation.unmerged
67
- : mergeHead === parents[1] && !operation.other && !operation.unmerged;
68
- if (!admitted)
69
- return { kind: "retained", head, reason: "operation-in-progress" };
70
- await runGit(repository, ["-C", workspace, "reset", "--mixed", gitObjectId(tender)]);
71
- return { kind: "followed", before: head, after: tender };
72
- }
73
38
  /** Follow a dependency baseline only through a clean, detached fast-forward. */
74
39
  export async function followDependentManagedWorktree(repository, workspace, target) {
75
40
  const head = await workspaceRevision(repository, workspace, "HEAD");
@@ -214,7 +179,11 @@ export async function workspaceMergeHead(repository, workspace) {
214
179
  }
215
180
  /** True when the appointed workspace already has Git MERGE_HEAD. */
216
181
  export async function workspaceMergeStatePresent(repository, workspace) {
217
- return (await workspaceMergeHead(repository, workspace)) !== undefined;
182
+ if ((await workspaceMergeHead(repository, workspace)) !== undefined)
183
+ return true;
184
+ const gitDirectory = await worktreeGitDirectory(repository, workspace);
185
+ const operation = await workspaceOperationState(repository, workspace, gitDirectory);
186
+ return operation.other || operation.unmerged;
218
187
  }
219
188
  /** Sorted unique unmerged index paths from porcelain status. */
220
189
  export async function unmergedWorkspacePaths(repository, workspace) {
@@ -293,3 +262,87 @@ export async function captureWorkspaceTree(repository, workspace) {
293
262
  };
294
263
  });
295
264
  }
265
+ const handoffRefFor = (contractId, place) => `${HANDOFF_RECEIPT_REF_NAMESPACE}/${contractPhysicalName(contractId)}/${place}`;
266
+ function receiptFor(value) {
267
+ if (typeof value !== "object" || value === null || Array.isArray(value))
268
+ throw new Error("handoff receipt is malformed");
269
+ const record = value;
270
+ if (record.version !== 1 ||
271
+ typeof record.contractId !== "string" ||
272
+ typeof record.place !== "string" ||
273
+ typeof record.workspace !== "string" ||
274
+ typeof record.head !== "string" ||
275
+ typeof record.mergeHead !== "string") {
276
+ throw new Error("handoff receipt is malformed");
277
+ }
278
+ return {
279
+ version: 1,
280
+ contractId: record.contractId,
281
+ place: record.place,
282
+ workspace: resolve(record.workspace),
283
+ head: mintSnapshotId(record.head),
284
+ mergeHead: mintSnapshotId(record.mergeHead),
285
+ };
286
+ }
287
+ async function storedHandoffReceipt(repository, contractId, place) {
288
+ const ref = handoffRefFor(contractId, place);
289
+ const oid = await readRef(repository, ref);
290
+ if (oid === null)
291
+ return null;
292
+ const receipt = receiptFor(JSON.parse((await readBlob(repository, oid)).toString("utf8")));
293
+ return { receipt, ref, oid };
294
+ }
295
+ async function removeHandoffReceipt(repository, stored) {
296
+ await runGit(repository, ["update-ref", "--no-deref", "-d", stored.ref, stored.oid]);
297
+ }
298
+ export async function recordConflictHandoff(repository, input) {
299
+ const ref = handoffRefFor(input.contractId, input.place);
300
+ const existing = await readRef(repository, ref);
301
+ if (existing !== null)
302
+ throw new Error("a conflict handoff receipt is already active");
303
+ const receipt = {
304
+ version: 1,
305
+ contractId: input.contractId,
306
+ place: input.place,
307
+ workspace: resolve(input.workspace),
308
+ head: input.head,
309
+ mergeHead: input.mergeHead,
310
+ };
311
+ const oid = await writeBlob(repository, `${JSON.stringify(receipt)}\n`);
312
+ await runGit(repository, ["update-ref", "--no-deref", ref, oid, "0".repeat(oid.length)]);
313
+ }
314
+ export async function retireConflictHandoff(repository, input) {
315
+ const stored = await storedHandoffReceipt(repository, input.contractId, input.place);
316
+ if (stored === null)
317
+ return { kind: "absent" };
318
+ const { receipt } = stored;
319
+ if (receipt.contractId !== input.contractId ||
320
+ receipt.place !== input.place ||
321
+ receipt.workspace !== resolve(input.workspace)) {
322
+ return { kind: "retained", reason: "receipt-mismatch" };
323
+ }
324
+ const gitDirectory = await worktreeGitDirectory(repository, input.workspace);
325
+ const operation = await workspaceOperationState(repository, input.workspace, gitDirectory);
326
+ if (operation.other)
327
+ return { kind: "retained", reason: "operation-in-progress" };
328
+ const [head, mergeHead] = await Promise.all([
329
+ workspaceRevision(repository, input.workspace, "HEAD"),
330
+ workspaceRevision(repository, input.workspace, "MERGE_HEAD"),
331
+ ]);
332
+ if (mergeHead === null) {
333
+ if (operation.unmerged)
334
+ return { kind: "retained", reason: "operation-in-progress" };
335
+ await removeHandoffReceipt(repository, stored);
336
+ return { kind: "orphan-retired" };
337
+ }
338
+ if (head !== receipt.head)
339
+ return { kind: "retained", reason: "receipt-mismatch" };
340
+ if (mergeHead !== receipt.mergeHead) {
341
+ return { kind: "retained", reason: "receipt-mismatch" };
342
+ }
343
+ if (!input.consume)
344
+ return { kind: "active" };
345
+ await runGit(repository, ["-C", input.workspace, "merge", "--quit"]);
346
+ await removeHandoffReceipt(repository, stored);
347
+ return { kind: "retired" };
348
+ }