@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.
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +43 -27
- package/build/src/akuma/request-serve.js +1 -1
- package/build/src/cli/commands/contract-help.d.ts +166 -0
- package/build/src/cli/commands/contract-help.js +167 -0
- package/build/src/cli/commands/contract.d.ts +2 -167
- package/build/src/cli/commands/contract.js +3 -160
- package/build/src/contract-guidance.js +5 -1
- package/build/src/git/reconcile.js +45 -21
- package/build/src/git/repository.d.ts +1 -0
- package/build/src/git/repository.js +3 -0
- package/build/src/git/workspace.d.ts +20 -14
- package/build/src/git/workspace.js +91 -38
- package/build/src/library/contract-forwarding-result.d.ts +1230 -1424
- package/build/src/library/contract-forwarding-result.js +44 -44
- package/build/src/library/contract-operations.d.ts +21 -1
- package/build/src/library/contract-operations.js +65 -5
- package/build/src/library/reconcile.js +15 -2
- package/build/src/protocol/deliver.js +21 -2
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { archetypeName } from "../../akuma/identity.js";
|
|
2
2
|
import { parseTaskNamespaceSelector } from "../../task/catalog.js";
|
|
3
|
-
import { CliUsageError
|
|
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
|
-
"
|
|
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,
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
+
}
|