@astrosheep/keiyaku 4.0.1 → 4.0.2

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 (245) hide show
  1. package/README.md +137 -5
  2. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +3 -3
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +91 -35
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +98 -58
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +152 -53
  9. package/build/src/akuma/abort.d.ts +2 -0
  10. package/build/src/akuma/abort.js +50 -0
  11. package/build/src/akuma/akuma.d.ts +23 -14
  12. package/build/src/akuma/akuma.js +120 -104
  13. package/build/src/akuma/archetype.d.ts +7 -6
  14. package/build/src/akuma/archetype.js +68 -41
  15. package/build/src/akuma/body.d.ts +3 -5
  16. package/build/src/akuma/body.js +408 -213
  17. package/build/src/akuma/heart/facts.d.ts +25 -34
  18. package/build/src/akuma/heart/facts.js +18 -7
  19. package/build/src/akuma/heart/index.d.ts +39 -42
  20. package/build/src/akuma/heart/index.js +134 -93
  21. package/build/src/akuma/heart/rows.d.ts +9 -31
  22. package/build/src/akuma/heart/rows.js +26 -44
  23. package/build/src/akuma/heart/schema.d.ts +1 -1
  24. package/build/src/akuma/heart/schema.js +7 -15
  25. package/build/src/akuma/heart/soul.d.ts +8 -0
  26. package/build/src/akuma/heart/soul.js +136 -0
  27. package/build/src/akuma/heart/storage.d.ts +28 -14
  28. package/build/src/akuma/heart/storage.js +86 -26
  29. package/build/src/akuma/identity.d.ts +2 -2
  30. package/build/src/akuma/identity.js +9 -9
  31. package/build/src/akuma/index.d.ts +6 -1
  32. package/build/src/akuma/index.js +6 -0
  33. package/build/src/akuma/projection.d.ts +117 -38
  34. package/build/src/akuma/projection.js +208 -97
  35. package/build/src/akuma/provider-recipe.d.ts +24 -0
  36. package/build/src/akuma/provider-recipe.js +100 -0
  37. package/build/src/akuma/provider.d.ts +12 -4
  38. package/build/src/akuma/provider.js +117 -70
  39. package/build/src/akuma/providers/acp/core.d.ts +19 -0
  40. package/build/src/akuma/providers/acp/core.js +159 -0
  41. package/build/src/akuma/providers/acp/events.d.ts +20 -0
  42. package/build/src/akuma/providers/acp/events.js +73 -0
  43. package/build/src/akuma/providers/acp/index.d.ts +12 -0
  44. package/build/src/akuma/providers/acp/index.js +94 -0
  45. package/build/src/akuma/providers/claude/events.js +66 -13
  46. package/build/src/akuma/providers/claude/index.d.ts +2 -1
  47. package/build/src/akuma/providers/claude/index.js +35 -21
  48. package/build/src/akuma/providers/codex-app-server/events.js +4 -1
  49. package/build/src/akuma/providers/codex-app-server/index.d.ts +3 -3
  50. package/build/src/akuma/providers/codex-app-server/index.js +24 -31
  51. package/build/src/akuma/providers/grok-build/index.d.ts +4 -0
  52. package/build/src/akuma/providers/grok-build/index.js +70 -0
  53. package/build/src/akuma/providers/index.d.ts +5 -3
  54. package/build/src/akuma/providers/index.js +24 -59
  55. package/build/src/akuma/providers/opencode-sdk/events.d.ts +40 -34
  56. package/build/src/akuma/providers/opencode-sdk/events.js +220 -101
  57. package/build/src/akuma/providers/opencode-sdk/index.d.ts +3 -2
  58. package/build/src/akuma/providers/opencode-sdk/index.js +233 -128
  59. package/build/src/akuma/providers/opencode-sdk/session.d.ts +11 -26
  60. package/build/src/akuma/providers/opencode-sdk/session.js +10 -9
  61. package/build/src/akuma/providers/pi/events.js +49 -11
  62. package/build/src/akuma/providers/pi/index.d.ts +1 -1
  63. package/build/src/akuma/providers/pi/index.js +50 -21
  64. package/build/src/akuma/publication.d.ts +3 -3
  65. package/build/src/akuma/publication.js +28 -40
  66. package/build/src/akuma/requests.d.ts +9 -7
  67. package/build/src/akuma/requests.js +92 -65
  68. package/build/src/alias/index.d.ts +2 -2
  69. package/build/src/alias/index.js +10 -10
  70. package/build/src/body/decode.js +7 -1
  71. package/build/src/body/region.d.ts +2 -0
  72. package/build/src/body/region.js +77 -7
  73. package/build/src/cli/accepted.d.ts +17 -13
  74. package/build/src/cli/accepted.js +72 -34
  75. package/build/src/cli/actor.js +3 -3
  76. package/build/src/cli/commands/akuma-invoke.d.ts +4 -3
  77. package/build/src/cli/commands/akuma-invoke.js +14 -9
  78. package/build/src/cli/commands/akuma.d.ts +13 -3
  79. package/build/src/cli/commands/akuma.js +67 -34
  80. package/build/src/cli/commands/contract.d.ts +18 -5
  81. package/build/src/cli/commands/contract.js +9 -3
  82. package/build/src/cli/commands/task-invoke.d.ts +17 -4
  83. package/build/src/cli/commands/task-invoke.js +43 -20
  84. package/build/src/cli/commands/task-query.d.ts +5 -0
  85. package/build/src/cli/commands/task-query.js +247 -0
  86. package/build/src/cli/commands/task.d.ts +3 -1
  87. package/build/src/cli/commands/task.js +51 -13
  88. package/build/src/cli/coordinates.d.ts +18 -0
  89. package/build/src/cli/coordinates.js +120 -0
  90. package/build/src/cli/draft.d.ts +8 -0
  91. package/build/src/cli/draft.js +96 -0
  92. package/build/src/cli/invoke.d.ts +4 -3
  93. package/build/src/cli/invoke.js +160 -179
  94. package/build/src/cli/main.js +34 -6
  95. package/build/src/cli/parse.d.ts +9 -2
  96. package/build/src/cli/parse.js +29 -7
  97. package/build/src/cli/render/akuma-tool-command.d.ts +6 -0
  98. package/build/src/cli/render/akuma-tool-command.js +144 -0
  99. package/build/src/cli/render/akuma-tool.d.ts +2 -2
  100. package/build/src/cli/render/akuma-tool.js +30 -4
  101. package/build/src/cli/render/akuma.d.ts +1 -0
  102. package/build/src/cli/render/akuma.js +211 -116
  103. package/build/src/cli/render/audit.d.ts +3 -0
  104. package/build/src/cli/render/audit.js +104 -0
  105. package/build/src/cli/render/contract.d.ts +3 -3
  106. package/build/src/cli/render/contract.js +192 -79
  107. package/build/src/cli/render/kanshi.js +263 -168
  108. package/build/src/cli/render/receipt.d.ts +19 -0
  109. package/build/src/cli/render/receipt.js +106 -0
  110. package/build/src/cli/render/refusal.d.ts +16 -2
  111. package/build/src/cli/render/refusal.js +88 -20
  112. package/build/src/cli/render/region.d.ts +2 -0
  113. package/build/src/cli/render/region.js +17 -0
  114. package/build/src/cli/render/task.d.ts +2 -1
  115. package/build/src/cli/render/task.js +223 -60
  116. package/build/src/cli/render/terminal.d.ts +7 -0
  117. package/build/src/cli/render/terminal.js +46 -0
  118. package/build/src/cli/render/text.js +6 -3
  119. package/build/src/cli/result.d.ts +115 -20
  120. package/build/src/cli/usage.d.ts +1 -0
  121. package/build/src/cli/usage.js +3 -0
  122. package/build/src/contract-worktree.d.ts +6 -5
  123. package/build/src/contract-worktree.js +95 -57
  124. package/build/src/coordination/durable-file.d.ts +4 -3
  125. package/build/src/coordination/durable-file.js +29 -25
  126. package/build/src/coordination/sqlite-transaction-lock.d.ts +4 -0
  127. package/build/src/coordination/sqlite-transaction-lock.js +22 -5
  128. package/build/src/core/facts/fold.js +1 -6
  129. package/build/src/core/facts/gate.d.ts +1 -0
  130. package/build/src/core/facts/gate.js +1 -0
  131. package/build/src/core/verbs/amend.d.ts +1 -1
  132. package/build/src/core/verbs/amend.js +0 -11
  133. package/build/src/core/verbs/deliver.d.ts +1 -1
  134. package/build/src/core/verbs/deliver.js +1 -7
  135. package/build/src/core/verbs/placement.d.ts +1 -1
  136. package/build/src/core/verbs/placement.js +4 -1
  137. package/build/src/dispatch/index.d.ts +2 -2
  138. package/build/src/dispatch/index.js +17 -17
  139. package/build/src/git/admission.d.ts +1 -1
  140. package/build/src/git/admission.js +11 -11
  141. package/build/src/git/hooks.js +13 -13
  142. package/build/src/git/integration.d.ts +14 -15
  143. package/build/src/git/integration.js +80 -44
  144. package/build/src/git/observe.d.ts +5 -3
  145. package/build/src/git/observe.js +18 -12
  146. package/build/src/git/read-observation.js +9 -9
  147. package/build/src/git/reconcile.d.ts +2 -0
  148. package/build/src/git/reconcile.js +138 -95
  149. package/build/src/git/repository.d.ts +27 -19
  150. package/build/src/git/repository.js +155 -83
  151. package/build/src/git/scratch.d.ts +7 -3
  152. package/build/src/git/scratch.js +44 -37
  153. package/build/src/git/target-placement.d.ts +40 -4
  154. package/build/src/git/target-placement.js +221 -101
  155. package/build/src/git/tender.d.ts +6 -5
  156. package/build/src/git/tender.js +24 -43
  157. package/build/src/git/terminal-seal.d.ts +1 -1
  158. package/build/src/git/terminal-seal.js +7 -5
  159. package/build/src/git/workspace.d.ts +36 -4
  160. package/build/src/git/workspace.js +53 -18
  161. package/build/src/identity/selector.js +1 -1
  162. package/build/src/index.d.ts +3 -3
  163. package/build/src/index.js +1 -1
  164. package/build/src/kanshi/index.d.ts +2 -2
  165. package/build/src/kanshi/index.js +1 -1
  166. package/build/src/kanshi/read.d.ts +2 -1
  167. package/build/src/kanshi/read.js +118 -27
  168. package/build/src/kanshi/report.d.ts +50 -0
  169. package/build/src/kanshi/select.d.ts +5 -0
  170. package/build/src/kanshi/select.js +32 -0
  171. package/build/src/library/address.d.ts +12 -3
  172. package/build/src/library/address.js +77 -46
  173. package/build/src/library/akuma-creation.d.ts +6 -1
  174. package/build/src/library/akuma-creation.js +86 -13
  175. package/build/src/library/audit.d.ts +18 -0
  176. package/build/src/library/audit.js +44 -0
  177. package/build/src/library/bind.js +4 -4
  178. package/build/src/library/catalog.d.ts +1 -1
  179. package/build/src/library/catalog.js +5 -8
  180. package/build/src/library/contract.d.ts +5 -6
  181. package/build/src/library/contract.js +11 -35
  182. package/build/src/library/delivery.d.ts +2 -1
  183. package/build/src/library/fleet.d.ts +2 -2
  184. package/build/src/library/fleet.js +35 -71
  185. package/build/src/library/keiyaku.d.ts +3 -2
  186. package/build/src/library/keiyaku.js +1 -0
  187. package/build/src/library/mutation.d.ts +7 -6
  188. package/build/src/library/mutation.js +23 -25
  189. package/build/src/library/reconcile.d.ts +29 -0
  190. package/build/src/library/reconcile.js +179 -0
  191. package/build/src/library/region.d.ts +12 -0
  192. package/build/src/library/region.js +15 -1
  193. package/build/src/library/repo.d.ts +4 -16
  194. package/build/src/library/repo.js +13 -38
  195. package/build/src/protocol/attempt.js +6 -6
  196. package/build/src/protocol/bind.js +4 -4
  197. package/build/src/protocol/intent.d.ts +15 -2
  198. package/build/src/protocol/intent.js +21 -6
  199. package/build/src/protocol/operations.d.ts +73 -22
  200. package/build/src/protocol/operations.js +292 -88
  201. package/build/src/protocol/outcome.d.ts +9 -3
  202. package/build/src/protocol/outcome.js +3 -1
  203. package/build/src/protocol/placement.d.ts +15 -4
  204. package/build/src/protocol/placement.js +58 -25
  205. package/build/src/protocol/read/status.d.ts +8 -1
  206. package/build/src/protocol/read/status.js +61 -14
  207. package/build/src/runtime/proc/line-rpc.d.ts +3 -8
  208. package/build/src/runtime/proc/line-rpc.js +16 -43
  209. package/build/src/runtime/proc/run.d.ts +15 -29
  210. package/build/src/runtime/proc/run.js +113 -105
  211. package/build/src/runtime/proc/stdio.d.ts +18 -0
  212. package/build/src/runtime/proc/stdio.js +58 -0
  213. package/build/src/settings.d.ts +2 -2
  214. package/build/src/settings.js +8 -8
  215. package/build/src/settlement/fence.d.ts +0 -4
  216. package/build/src/settlement/fence.js +0 -9
  217. package/build/src/settlement/holder.d.ts +11 -1
  218. package/build/src/settlement/holder.js +25 -0
  219. package/build/src/settlement/settle.d.ts +1 -1
  220. package/build/src/settlement/settle.js +52 -89
  221. package/build/src/task/board.d.ts +3 -3
  222. package/build/src/task/board.js +6 -9
  223. package/build/src/task/compose.d.ts +1 -1
  224. package/build/src/task/compose.js +18 -10
  225. package/build/src/task/context.d.ts +3 -3
  226. package/build/src/task/context.js +14 -14
  227. package/build/src/task/document.d.ts +2 -1
  228. package/build/src/task/document.js +15 -5
  229. package/build/src/task/index.d.ts +19 -7
  230. package/build/src/task/index.js +42 -23
  231. package/build/src/task/operations.d.ts +15 -9
  232. package/build/src/task/operations.js +78 -48
  233. package/build/src/task/query.d.ts +67 -0
  234. package/build/src/task/query.js +279 -0
  235. package/build/src/task/store.d.ts +2 -2
  236. package/build/src/task/store.js +40 -25
  237. package/build/src/verification/execution.d.ts +4 -2
  238. package/build/src/verification/execution.js +9 -5
  239. package/build/src/workspace-place.d.ts +41 -0
  240. package/build/src/workspace-place.js +386 -0
  241. package/build/src/world.d.ts +13 -2
  242. package/build/src/world.js +81 -48
  243. package/package.json +3 -2
  244. package/build/src/protocol/read/audit.d.ts +0 -31
  245. package/build/src/protocol/read/audit.js +0 -55
@@ -1,11 +1,12 @@
1
+ import { lstat } from "node:fs/promises";
1
2
  import { resolve } from "node:path";
2
3
  import { acquireSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
3
4
  import { gitObjectId, gitObjectIdForSnapshot, gitRefLocator, mintSnapshotId } from "./identity.js";
4
5
  import { currentBranch } from "./observe.js";
5
- import { commonGitDirectory, GitPlumbingError, readRef, registeredWorktrees, runGit, } from "./repository.js";
6
- import { captureWorkspaceTree, withPrivateGitIndex } from "./workspace.js";
7
- export function observeTargetHead(repository, target) {
8
- const value = readRef(repository, target);
6
+ import { commonGitDirectory, consumeGitStdout, GitPlumbingError, readRef, registeredWorktrees, runGit, } from "./repository.js";
7
+ import { captureWorkspaceTree } from "./workspace.js";
8
+ export async function observeTargetHead(repository, target) {
9
+ const value = await readRef(repository, target);
9
10
  return value === null ? null : mintSnapshotId(value);
10
11
  }
11
12
  function diagnostic(error) {
@@ -17,115 +18,198 @@ function nulPaths(bytes) {
17
18
  throw new Error("Git path output is not NUL terminated");
18
19
  return [...new Set(fields.slice(0, -1))].sort();
19
20
  }
20
- function gitPaths(repository, path, args) {
21
- return nulPaths(runGit(repository, ["-C", path, ...args]));
21
+ async function gitPaths(repository, path, args) {
22
+ return nulPaths(await runGit(repository, ["-C", path, ...args]));
22
23
  }
23
- function commitTree(repository, snapshot) {
24
- return gitObjectId(runGit(repository, ["show", "-s", "--format=%T", gitObjectIdForSnapshot(snapshot)])
24
+ function literalPath(path) {
25
+ return `:(literal)${path}`;
26
+ }
27
+ async function commitTree(repository, snapshot) {
28
+ return gitObjectId((await runGit(repository, ["show", "-s", "--format=%T", gitObjectIdForSnapshot(snapshot)]))
25
29
  .toString("utf8")
26
30
  .trim(), "commit tree");
27
31
  }
28
- function pathsOverlap(left, right) {
29
- return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
32
+ function checkoutRefusal(contractId, target, path, reason, paths) {
33
+ return { kind: "checkout-not-followable", contractId, target: target.target, path, reason, paths };
30
34
  }
31
- function intersection(left, right) {
32
- return left.filter((candidate) => right.some((path) => pathsOverlap(candidate, path))).sort();
35
+ async function changedPaths(repository, path, predecessor, candidate, filter) {
36
+ return await gitPaths(repository, path, [
37
+ "diff",
38
+ "--name-only",
39
+ "--no-renames",
40
+ ...(filter === undefined ? [] : [`--diff-filter=${filter}`]),
41
+ "-z",
42
+ predecessor,
43
+ candidate,
44
+ ]);
33
45
  }
34
- function failurePaths(error) {
35
- if (!(error instanceof GitPlumbingError))
36
- return [];
37
- const paths = new Set();
38
- for (const match of error.stderr.toString("utf8").matchAll(/(?:Entry|Untracked working tree file) '([^']+)'/gu)) {
39
- paths.add(match[1]);
40
- }
41
- return [...paths].sort();
46
+ async function dryRunRefusal(input, scopes) {
47
+ const { repository, contractId, target, path, predecessor, candidate } = input;
48
+ const changed = await changedPaths(repository, path, predecessor, candidate);
49
+ const pathspecs = changed.map(literalPath);
50
+ if (pathspecs.length === 0)
51
+ return checkoutRefusal(contractId, target, path, "conflict", []);
52
+ const staged = await gitPaths(repository, path, [
53
+ "diff",
54
+ "--cached",
55
+ "--name-only",
56
+ "-z",
57
+ predecessor,
58
+ "--",
59
+ ...pathspecs,
60
+ ]);
61
+ if (staged.length > 0)
62
+ return checkoutRefusal(contractId, target, path, "staged", staged);
63
+ const dirty = await gitPaths(repository, path, ["diff-files", "--name-only", "-z", "--", ...pathspecs]);
64
+ if (dirty.length > 0)
65
+ return checkoutRefusal(contractId, target, path, "conflict", dirty);
66
+ const unmerged = await gitPaths(repository, path, ["diff", "--name-only", "--diff-filter=U", "-z", "--", ...pathspecs]);
67
+ if (unmerged.length > 0)
68
+ return checkoutRefusal(contractId, target, path, "conflict", unmerged);
69
+ return await untrackedRefusalWithinScopes(input, scopes, false)
70
+ ?? checkoutRefusal(contractId, target, path, "conflict", []);
42
71
  }
43
- function checkoutRefusal(contractId, target, path, reason, error) {
44
- const paths = failurePaths(error);
45
- if (paths.length === 0)
46
- throw error;
47
- return { kind: "checkout-not-followable", contractId, target: target.target, path, reason, paths };
72
+ async function physicalScope(worktree, candidatePath) {
73
+ const components = candidatePath.split("/");
74
+ for (let index = 0; index < components.length; index += 1) {
75
+ const scope = components.slice(0, index + 1).join("/");
76
+ let stat;
77
+ try {
78
+ stat = await lstat(resolve(worktree, ...components.slice(0, index + 1)));
79
+ }
80
+ catch (error) {
81
+ const code = error.code;
82
+ if (code === "ENOENT" || code === "ENOTDIR")
83
+ return null;
84
+ throw error;
85
+ }
86
+ if (!stat.isDirectory())
87
+ return { path: scope, kind: "leaf" };
88
+ if (index + 1 === components.length)
89
+ return { path: scope, kind: "directory" };
90
+ }
91
+ return null;
48
92
  }
49
- function indexMatchesTreeOnPaths(repository, path, tree, paths) {
50
- return gitPaths(repository, path, ["diff-index", "--cached", "--name-only", "-z", tree, "--", ...paths]).length === 0;
93
+ async function candidateEntryIsBlob(repository, path, candidate, candidatePath) {
94
+ const output = await runGit(repository, ["-C", path, "ls-tree", "-z", candidate, "--", literalPath(candidatePath)]);
95
+ const records = output.toString("utf8").split("\0").filter((record) => record.length > 0);
96
+ if (records.length !== 1)
97
+ throw new Error(`candidate path has no unique Git entry: ${candidatePath}`);
98
+ const separator = records[0].indexOf("\t");
99
+ if (separator < 0)
100
+ throw new Error(`candidate path has a malformed Git entry: ${candidatePath}`);
101
+ const fields = records[0].slice(0, separator).split(" ");
102
+ return fields[1] === "blob";
51
103
  }
52
- function workspaceMatchesTreeOnPaths(repository, path, tree, workspaceTree, paths) {
53
- return gitPaths(repository, path, ["diff", "--name-only", "-z", tree, workspaceTree, "--", ...paths]).length === 0;
104
+ async function destructionScopes(repository, path, candidate, writes) {
105
+ const scopes = new Map();
106
+ for (const write of writes) {
107
+ const scope = await physicalScope(path, write);
108
+ if (scope === null)
109
+ continue;
110
+ if (scope.kind === "directory" && !(await candidateEntryIsBlob(repository, path, candidate, write)))
111
+ continue;
112
+ scopes.set(scope.path, scope);
113
+ }
114
+ return [...scopes.values()].sort((left, right) => left.path.localeCompare(right.path));
54
115
  }
55
- function sourceWorktree(repository) {
56
- return resolve(runGit(repository, ["rev-parse", "--show-toplevel"]).toString("utf8").trim());
116
+ function untrackedArgs(ignored) {
117
+ return [
118
+ "ls-files",
119
+ "--others",
120
+ ...(ignored ? ["--ignored"] : []),
121
+ "--exclude-standard",
122
+ "--directory",
123
+ "--no-empty",
124
+ "-z",
125
+ ];
57
126
  }
58
- function ordinaryPrecheck(repository, contractId, target, path) {
59
- const predecessor = gitObjectIdForSnapshot(target.expectedOid);
60
- const candidate = gitObjectIdForSnapshot(target.newOid);
61
- try {
62
- withPrivateGitIndex((environment) => {
63
- runGit(repository, ["-C", path, "read-tree", "-i", `--index-output=${environment.GIT_INDEX_FILE}`, "-m", predecessor, candidate]);
64
- });
127
+ async function untrackedRefusalWithinScopes(input, scopes, ignored) {
128
+ const { repository, contractId, target, path } = input;
129
+ const args = untrackedArgs(ignored);
130
+ const leaves = scopes.filter((scope) => scope.kind === "leaf");
131
+ if (leaves.length > 0) {
132
+ const collisions = await gitPaths(repository, path, [
133
+ ...args,
134
+ "--",
135
+ ...leaves.map((scope) => literalPath(scope.path)),
136
+ ]);
137
+ if (collisions.length > 0)
138
+ return checkoutRefusal(contractId, target, path, "untracked", collisions);
65
139
  }
66
- catch (error) {
67
- return checkoutRefusal(contractId, target, path, "staged", error);
68
- }
69
- const changed = gitPaths(repository, path, ["diff", "--name-only", "-z", predecessor, candidate]);
70
- const dirty = gitPaths(repository, path, ["diff-files", "--name-only", "-z"]);
71
- const conflicts = intersection(changed, dirty);
72
- if (conflicts.length > 0) {
73
- return {
74
- kind: "checkout-not-followable",
75
- contractId,
76
- target: target.target,
77
- path,
78
- reason: "conflict",
79
- paths: conflicts,
80
- };
81
- }
82
- const additions = gitPaths(repository, path, ["diff", "--name-only", "--diff-filter=A", "-z", predecessor, candidate]);
83
- const untracked = gitPaths(repository, path, ["ls-files", "--others", "-z"]);
84
- const collisions = intersection(additions, untracked);
85
- if (collisions.length > 0) {
86
- return {
87
- kind: "checkout-not-followable",
88
- contractId,
89
- target: target.target,
140
+ for (const scope of scopes) {
141
+ if (scope.kind !== "directory")
142
+ continue;
143
+ let found = false;
144
+ await consumeGitStdout(repository, [
145
+ "-C",
90
146
  path,
91
- reason: "untracked",
92
- paths: collisions,
93
- };
147
+ ...args,
148
+ "--",
149
+ literalPath(scope.path),
150
+ ], (chunk) => {
151
+ if (chunk.length > 0)
152
+ found = true;
153
+ });
154
+ if (found)
155
+ return checkoutRefusal(contractId, target, path, "untracked", [scope.path]);
94
156
  }
157
+ return null;
158
+ }
159
+ async function indexMatchesTreeOnPaths(repository, path, tree, paths) {
160
+ return (await gitPaths(repository, path, ["diff-index", "--cached", "--name-only", "-z", tree, "--", ...paths])).length === 0;
161
+ }
162
+ async function workspaceMatchesTreeOnPaths(repository, path, tree, workspaceTree, paths) {
163
+ return (await gitPaths(repository, path, ["diff", "--name-only", "-z", tree, workspaceTree, "--", ...paths])).length === 0;
164
+ }
165
+ async function sourceWorktree(repository) {
166
+ return resolve((await runGit(repository, ["rev-parse", "--show-toplevel"])).toString("utf8").trim());
167
+ }
168
+ async function ordinaryPrecheck(repository, contractId, target, path) {
169
+ const predecessor = gitObjectIdForSnapshot(target.expectedOid);
170
+ const candidate = gitObjectIdForSnapshot(target.newOid);
171
+ const observation = { repository, contractId, target, path, predecessor, candidate };
172
+ let dryRunFailed = false;
95
173
  try {
96
- runGit(repository, ["-C", path, "read-tree", "--dry-run", "-m", "-u", predecessor, candidate]);
174
+ await runGit(repository, ["-C", path, "read-tree", "--dry-run", "-m", "-u", predecessor, candidate]);
97
175
  }
98
176
  catch (error) {
99
- const reason = error instanceof GitPlumbingError && /untracked working tree file/iu.test(error.stderr.toString("utf8"))
100
- ? "untracked"
101
- : "conflict";
102
- return checkoutRefusal(contractId, target, path, reason, error);
177
+ if (!(error instanceof GitPlumbingError))
178
+ throw error;
179
+ dryRunFailed = true;
103
180
  }
104
- return null;
181
+ const writes = await changedPaths(repository, path, predecessor, candidate, "ACMRT");
182
+ const scopes = await destructionScopes(repository, path, candidate, writes);
183
+ if (dryRunFailed)
184
+ return await dryRunRefusal(observation, scopes);
185
+ return await untrackedRefusalWithinScopes(observation, scopes, true);
105
186
  }
106
187
  export async function acquireTargetPlacementFence(repository, target) {
107
188
  const locator = gitRefLocator(target);
108
- return acquireSqliteTransactionLock({
189
+ return await acquireSqliteTransactionLock({
109
190
  path: resolve(commonGitDirectory(repository), "keiyaku", "locks", "target-placement", `${locator}.sqlite`),
110
191
  mode: "immediate",
111
192
  });
112
193
  }
113
- export function prepareTargetPlacement(repository, state, target) {
114
- if (state.coordinates.target !== target.target || state.delivery?.data.integration.snapshot !== target.newOid) {
115
- throw new Error("placement state does not match its offered target movement");
116
- }
117
- const worktrees = registeredWorktrees(repository)
194
+ /** Observe whether registered target checkouts can follow a predecessor-to-candidate movement. */
195
+ export async function observeTargetPlacement(repository, input) {
196
+ const target = {
197
+ target: input.coordinates.target,
198
+ expectedOid: input.predecessor,
199
+ newOid: input.candidate,
200
+ };
201
+ const worktrees = (await registeredWorktrees(repository))
118
202
  .filter((worktree) => worktree.branch === target.target)
119
203
  .sort((left, right) => left.path.localeCompare(right.path));
120
- const hereSource = state.coordinates.workspace === "here" ? sourceWorktree(repository) : null;
204
+ const hereSource = input.coordinates.workspace === "here" ? await sourceWorktree(repository) : null;
121
205
  if (hereSource !== null) {
122
- const branch = currentBranch(repository, hereSource);
206
+ const branch = await currentBranch(repository, hereSource);
123
207
  if (branch !== target.target) {
124
208
  return {
125
209
  kind: "refused",
126
210
  refusal: {
127
211
  kind: "workspace-not-on-target",
128
- contractId: state.id,
212
+ contractId: input.contractId,
129
213
  target: target.target,
130
214
  branch,
131
215
  },
@@ -136,7 +220,7 @@ export function prepareTargetPlacement(repository, state, target) {
136
220
  for (const worktree of worktrees) {
137
221
  const kind = hereSource !== null && resolve(worktree.path) === hereSource ? "here" : "ordinary";
138
222
  if (kind === "ordinary") {
139
- const refusal = ordinaryPrecheck(repository, state.id, target, worktree.path);
223
+ const refusal = await ordinaryPrecheck(repository, input.contractId, target, worktree.path);
140
224
  if (refusal !== null)
141
225
  return { kind: "refused", refusal };
142
226
  }
@@ -145,9 +229,45 @@ export function prepareTargetPlacement(repository, state, target) {
145
229
  if (hereSource !== null && !arms.some((arm) => arm.kind === "here")) {
146
230
  throw new Error("targeted here workspace is not a registered checkout of its target");
147
231
  }
148
- return { kind: "prepared", placement: { target, arms } };
232
+ return { kind: "ready", arms };
233
+ }
234
+ /** Adjudicate the complete post-Verification audit target answer without placing. */
235
+ export async function adjudicateAuditTarget(repository, input) {
236
+ const ref = input.coordinates.target;
237
+ const expected = input.predecessor;
238
+ try {
239
+ const head = await observeTargetHead(repository, ref);
240
+ if (head !== expected)
241
+ return { kind: "moved", ref, expected, observed: head };
242
+ const follow = await observeTargetPlacement(repository, input);
243
+ const observed = await observeTargetHead(repository, ref);
244
+ if (observed !== expected)
245
+ return { kind: "moved", ref, expected, observed };
246
+ return follow.kind === "refused"
247
+ ? { kind: "refused", refusal: follow.refusal }
248
+ : { kind: "placeable", ref, head: expected };
249
+ }
250
+ catch (error) {
251
+ if (!(error instanceof GitPlumbingError))
252
+ throw error;
253
+ return { kind: "failed", diagnostic: error.message };
254
+ }
255
+ }
256
+ export async function prepareTargetPlacement(repository, state, target) {
257
+ if (state.coordinates.target !== target.target || state.delivery?.data.integration.snapshot !== target.newOid) {
258
+ throw new Error("placement state does not match its offered target movement");
259
+ }
260
+ const observation = await observeTargetPlacement(repository, {
261
+ contractId: state.id,
262
+ coordinates: { ...state.coordinates, target: target.target },
263
+ predecessor: target.expectedOid,
264
+ candidate: target.newOid,
265
+ });
266
+ return observation.kind === "refused"
267
+ ? observation
268
+ : { kind: "prepared", placement: { target, arms: observation.arms } };
149
269
  }
150
- export function followTargetPlacement(repository, prepared) {
270
+ export async function followTargetPlacement(repository, prepared) {
151
271
  const effects = [];
152
272
  const lag = [];
153
273
  const predecessor = gitObjectIdForSnapshot(prepared.target.expectedOid);
@@ -155,9 +275,9 @@ export function followTargetPlacement(repository, prepared) {
155
275
  for (const arm of prepared.arms) {
156
276
  try {
157
277
  if (arm.kind === "here")
158
- runGit(repository, ["-C", arm.path, "read-tree", candidate]);
278
+ await runGit(repository, ["-C", arm.path, "read-tree", candidate]);
159
279
  else
160
- runGit(repository, ["-C", arm.path, "read-tree", "-m", "-u", predecessor, candidate]);
280
+ await runGit(repository, ["-C", arm.path, "read-tree", "-m", "-u", predecessor, candidate]);
161
281
  effects.push({ kind: "target-checkout", path: arm.path, target: prepared.target.target, action: "followed" });
162
282
  }
163
283
  catch (error) {
@@ -174,23 +294,23 @@ export function followTargetPlacement(repository, prepared) {
174
294
  function recoveryLag(path, target, detail) {
175
295
  return { kind: "target-checkout-retained", path, target, diagnostic: detail };
176
296
  }
177
- function recoverCheckout(input) {
297
+ async function recoverCheckout(input) {
178
298
  const { repository, path, predecessor, candidate, predecessorTree, candidateTree } = input;
179
- const changedPaths = gitPaths(repository, path, ["diff", "--name-only", "-z", predecessorTree, candidateTree]);
299
+ const changedPaths = await gitPaths(repository, path, ["diff", "--name-only", "-z", predecessorTree, candidateTree]);
180
300
  if (changedPaths.length === 0)
181
301
  return "complete";
182
- const workspaceTree = captureWorkspaceTree(repository, path).tree;
183
- const candidateIndex = indexMatchesTreeOnPaths(repository, path, candidateTree, changedPaths);
184
- const candidateWorkspace = workspaceMatchesTreeOnPaths(repository, path, candidateTree, workspaceTree, changedPaths);
302
+ const workspaceTree = (await captureWorkspaceTree(repository, path)).tree;
303
+ const candidateIndex = await indexMatchesTreeOnPaths(repository, path, candidateTree, changedPaths);
304
+ const candidateWorkspace = await workspaceMatchesTreeOnPaths(repository, path, candidateTree, workspaceTree, changedPaths);
185
305
  if (candidateIndex && candidateWorkspace)
186
306
  return "complete";
187
307
  if (workspaceTree === candidateTree) {
188
- runGit(repository, ["-C", path, "read-tree", gitObjectIdForSnapshot(candidate)]);
308
+ await runGit(repository, ["-C", path, "read-tree", gitObjectIdForSnapshot(candidate)]);
189
309
  return "recovered";
190
310
  }
191
- const predecessorIndex = indexMatchesTreeOnPaths(repository, path, predecessorTree, changedPaths);
311
+ const predecessorIndex = await indexMatchesTreeOnPaths(repository, path, predecessorTree, changedPaths);
192
312
  if (predecessorIndex && candidateWorkspace) {
193
- runGit(repository, [
313
+ await runGit(repository, [
194
314
  "-C",
195
315
  path,
196
316
  "read-tree",
@@ -201,10 +321,10 @@ function recoverCheckout(input) {
201
321
  ]);
202
322
  return "recovered";
203
323
  }
204
- const predecessorWorkspace = workspaceMatchesTreeOnPaths(repository, path, predecessorTree, workspaceTree, changedPaths);
324
+ const predecessorWorkspace = await workspaceMatchesTreeOnPaths(repository, path, predecessorTree, workspaceTree, changedPaths);
205
325
  if (!predecessorIndex || !predecessorWorkspace)
206
326
  return "retained";
207
- runGit(repository, [
327
+ await runGit(repository, [
208
328
  "-C",
209
329
  path,
210
330
  "read-tree",
@@ -215,23 +335,23 @@ function recoverCheckout(input) {
215
335
  ]);
216
336
  return "recovered";
217
337
  }
218
- export function recoverTargetPlacement(repository, state) {
338
+ export async function recoverTargetPlacement(repository, state) {
219
339
  const target = state.coordinates.target;
220
340
  const delivery = state.delivery;
221
341
  if (state.terminal?.kind !== "claimed" || target === undefined || delivery === null)
222
342
  return { effects: [], lag: [] };
223
- if (readRef(repository, target) !== delivery.data.integration.snapshot)
343
+ if (await readRef(repository, target) !== delivery.data.integration.snapshot)
224
344
  return { effects: [], lag: [] };
225
- const candidateTree = commitTree(repository, delivery.data.integration.snapshot);
226
- const predecessorTree = commitTree(repository, delivery.data.integration.predecessor);
227
- const worktrees = registeredWorktrees(repository)
345
+ const candidateTree = await commitTree(repository, delivery.data.integration.snapshot);
346
+ const predecessorTree = await commitTree(repository, delivery.data.integration.predecessor);
347
+ const worktrees = (await registeredWorktrees(repository))
228
348
  .filter((worktree) => worktree.branch === target)
229
349
  .sort((left, right) => left.path.localeCompare(right.path));
230
350
  const effects = [];
231
351
  const lag = [];
232
352
  for (const worktree of worktrees) {
233
353
  try {
234
- const recovery = recoverCheckout({
354
+ const recovery = await recoverCheckout({
235
355
  repository,
236
356
  path: worktree.path,
237
357
  predecessor: delivery.data.integration.predecessor,
@@ -6,6 +6,7 @@ import { captureWorkspaceTree } from "./workspace.js";
6
6
  export type TenderCaptureCoordinates = Readonly<{
7
7
  contractId: ContractId;
8
8
  coordinates: ContractCoordinates;
9
+ place?: string;
9
10
  }>;
10
11
  export type TenderCaptureRefusal = Readonly<{
11
12
  kind: "worktree-missing";
@@ -34,15 +35,15 @@ export type TenderCapture = Readonly<{
34
35
  tree: GitObjectId;
35
36
  head: SnapshotId;
36
37
  dirty: boolean;
37
- changes: ReturnType<typeof captureWorkspaceTree>["changes"];
38
+ changes: Awaited<ReturnType<typeof captureWorkspaceTree>>["changes"];
38
39
  }>;
39
40
  /** Capture the complete workspace tree through Git's private index mechanics. */
40
- export declare function captureTender(repository: GitRepository, input: TenderCaptureCoordinates): Preparation<TenderCapture, TenderCaptureRefusal>;
41
- export declare function dirtyTenderRefusal(repository: GitRepository, contractId: ContractId, tender: TenderCapture): DirtyWorkspaceRefusal;
42
- export declare function dirtyTenderDelta(repository: GitRepository, tender: TenderCapture): WorkspaceDirtyDelta | undefined;
41
+ export declare function captureTender(repository: GitRepository, input: TenderCaptureCoordinates): Promise<Preparation<TenderCapture, TenderCaptureRefusal>>;
42
+ export declare function dirtyTenderRefusal(repository: GitRepository, contractId: ContractId, tender: TenderCapture): Promise<DirtyWorkspaceRefusal>;
43
+ export declare function dirtyTenderDelta(repository: GitRepository, tender: TenderCapture): Promise<WorkspaceDirtyDelta | undefined>;
43
44
  /** Materialize the tender tree when it contains admitted dirty workspace bytes. */
44
45
  export declare function materializeTenderSnapshot(repository: GitRepository, tender: TenderCapture, input: Readonly<{
45
46
  contractId: ContractId;
46
47
  title: string;
47
48
  message?: string;
48
- }>): SnapshotId;
49
+ }>): Promise<SnapshotId>;
@@ -1,82 +1,63 @@
1
- import { existsSync } from "node:fs";
1
+ import { access } from "node:fs/promises";
2
2
  import { gitObjectIdForSnapshot, mintSnapshotId } from "./identity.js";
3
- import { registeredWorktreePaths, runGit, runGitWithEnvironment } from "./repository.js";
4
- import { captureWorkspaceTree, deliveryWorktreePath } from "./workspace.js";
5
- function workspaceExists(repository, workspace, path) {
3
+ import { decodeGitNumstat, registeredWorktreePaths, runGit, runGitWithEnvironment } from "./repository.js";
4
+ import { captureWorkspaceTree, worktreePath } from "./workspace.js";
5
+ async function workspaceExists(repository, workspace, path) {
6
+ const exists = await access(path).then(() => true, () => false);
6
7
  return workspace === "here"
7
- || (existsSync(path) && registeredWorktreePaths(repository).includes(path));
8
+ || (exists && (await registeredWorktreePaths(repository)).includes(path));
8
9
  }
9
- function workspaceFor(repository, id, workspace) {
10
- return workspace === "worktree" ? deliveryWorktreePath(repository, id) : repository.effectiveCwd;
10
+ function workspaceFor(repository, input) {
11
+ if (input.coordinates.workspace === "here")
12
+ return repository.effectiveCwd;
13
+ return input.place === undefined ? undefined : worktreePath(repository, input.place);
11
14
  }
12
15
  /** Capture the complete workspace tree through Git's private index mechanics. */
13
- export function captureTender(repository, input) {
14
- const workspace = workspaceFor(repository, input.contractId, input.coordinates.workspace);
15
- if (!workspaceExists(repository, input.coordinates.workspace, workspace)) {
16
+ export async function captureTender(repository, input) {
17
+ const workspace = workspaceFor(repository, input);
18
+ if (workspace === undefined || !(await workspaceExists(repository, input.coordinates.workspace, workspace))) {
16
19
  return { kind: "refused", refusal: { kind: "worktree-missing", contractId: input.contractId } };
17
20
  }
18
- return { kind: "prepared", data: captureWorkspaceTree(repository, workspace) };
21
+ return { kind: "prepared", data: await captureWorkspaceTree(repository, workspace) };
19
22
  }
20
- function dirtyShortStat(repository, tender) {
21
- const fields = runGit(repository, [
23
+ async function dirtyShortStat(repository, tender) {
24
+ return decodeGitNumstat(await runGit(repository, [
22
25
  "diff",
23
26
  "--numstat",
24
27
  "-z",
25
28
  gitObjectIdForSnapshot(tender.head),
26
29
  tender.tree,
27
- ]).toString("utf8").split("\0");
28
- if (fields.at(-1) !== "")
29
- throw new Error("Git numstat output is not NUL terminated");
30
- let filesChanged = 0;
31
- let insertions = 0;
32
- let deletions = 0;
33
- for (let index = 0; index < fields.length - 1; index += 1) {
34
- const field = fields[index];
35
- const [added, deleted, path, extra] = field.split("\t");
36
- if (added === undefined || deleted === undefined || path === undefined || extra !== undefined) {
37
- throw new Error("Git numstat output is malformed");
38
- }
39
- if (path.length === 0) {
40
- if (fields[index + 1] === undefined || fields[index + 2] === undefined) {
41
- throw new Error("Git rename numstat output is missing paths");
42
- }
43
- index += 2;
44
- }
45
- filesChanged += 1;
46
- insertions += added === "-" ? 0 : Number.parseInt(added, 10);
47
- deletions += deleted === "-" ? 0 : Number.parseInt(deleted, 10);
48
- }
49
- return { filesChanged, insertions, deletions };
30
+ ]));
50
31
  }
51
- export function dirtyTenderRefusal(repository, contractId, tender) {
32
+ export async function dirtyTenderRefusal(repository, contractId, tender) {
52
33
  return {
53
34
  kind: "dirty-workspace",
54
35
  contractId,
55
36
  ...tender.changes,
56
- shortStat: dirtyShortStat(repository, tender),
37
+ shortStat: await dirtyShortStat(repository, tender),
57
38
  };
58
39
  }
59
- export function dirtyTenderDelta(repository, tender) {
40
+ export async function dirtyTenderDelta(repository, tender) {
60
41
  if (!tender.dirty)
61
42
  return undefined;
62
43
  const { staged, unstaged, untracked } = tender.changes;
63
- return { staged, unstaged, untracked, shortStat: dirtyShortStat(repository, tender) };
44
+ return { staged, unstaged, untracked, shortStat: await dirtyShortStat(repository, tender) };
64
45
  }
65
46
  function commitMessage(contractId, title, message) {
66
47
  const subject = message ?? `${contractId}: ${title}`;
67
48
  return `${subject}\n\nKeiyaku-Contract: ${contractId}\n`;
68
49
  }
69
50
  /** Materialize the tender tree when it contains admitted dirty workspace bytes. */
70
- export function materializeTenderSnapshot(repository, tender, input) {
51
+ export async function materializeTenderSnapshot(repository, tender, input) {
71
52
  if (!tender.dirty)
72
53
  return tender.head;
73
- const commit = runGitWithEnvironment(repository, ["commit-tree", tender.tree, "-p", gitObjectIdForSnapshot(tender.head)], commitMessage(input.contractId, input.title, input.message), {
54
+ const commit = (await runGitWithEnvironment(repository, ["commit-tree", tender.tree, "-p", gitObjectIdForSnapshot(tender.head)], commitMessage(input.contractId, input.title, input.message), {
74
55
  GIT_AUTHOR_NAME: "Keiyaku",
75
56
  GIT_AUTHOR_EMAIL: "keiyaku@localhost",
76
57
  GIT_COMMITTER_NAME: "Keiyaku",
77
58
  GIT_COMMITTER_EMAIL: "keiyaku@localhost",
78
59
  GIT_AUTHOR_DATE: "Thu, 01 Jan 1970 00:00:00 +0000",
79
60
  GIT_COMMITTER_DATE: "Thu, 01 Jan 1970 00:00:00 +0000",
80
- }).toString("utf8").trim();
61
+ })).toString("utf8").trim();
81
62
  return mintSnapshotId(commit);
82
63
  }
@@ -21,4 +21,4 @@ export type TerminalSealObject = Readonly<{
21
21
  }>;
22
22
  export declare function terminalSealSnapshots(state: ContractState): readonly SnapshotId[];
23
23
  export declare function terminalSealExpectations(state: ContractState, objects: ReadonlyMap<GitOid, TerminalSealObject>): TerminalSealExpectations;
24
- export declare function unsealedBytes(repository: GitRepository, path: string, expected: TerminalSealExpectations): UnsealedBytes | null;
24
+ export declare function unsealedBytes(repository: GitRepository, path: string, expected: TerminalSealExpectations): Promise<UnsealedBytes | null>;
@@ -57,14 +57,14 @@ export function terminalSealExpectations(state, objects) {
57
57
  treeBySnapshot,
58
58
  };
59
59
  }
60
- function changedPaths(repository, left, right) {
61
- const fields = runGit(repository, ["diff", "--name-only", "-z", left, right]).toString("utf8").split("\0");
60
+ async function changedPaths(repository, left, right) {
61
+ const fields = (await runGit(repository, ["diff", "--name-only", "-z", left, right])).toString("utf8").split("\0");
62
62
  if (fields.at(-1) !== "")
63
63
  throw new Error("Git tree diff paths are not NUL terminated");
64
64
  return fields.slice(0, -1).sort();
65
65
  }
66
- export function unsealedBytes(repository, path, expected) {
67
- const workspace = captureWorkspaceTree(repository, path);
66
+ export async function unsealedBytes(repository, path, expected) {
67
+ const workspace = await captureWorkspaceTree(repository, path);
68
68
  const headIsSealed = expected.heads.includes(workspace.head);
69
69
  if (workspace.changes.submodules.length > 0) {
70
70
  return {
@@ -76,7 +76,9 @@ export function unsealedBytes(repository, path, expected) {
76
76
  }
77
77
  if (headIsSealed && expected.trees.includes(workspace.tree))
78
78
  return null;
79
- const alternatives = expected.trees.map((tree) => changedPaths(repository, tree, workspace.tree));
79
+ const alternatives = [];
80
+ for (const tree of expected.trees)
81
+ alternatives.push(await changedPaths(repository, tree, workspace.tree));
80
82
  alternatives.sort((left, right) => left.length - right.length || left.join("\0").localeCompare(right.join("\0")));
81
83
  return {
82
84
  kind: "unsealed-bytes",