@astrosheep/keiyaku 4.5.5 → 4.5.7

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 (41) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/opencode.js +1 -1
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +1 -0
  3. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-babysit/SKILL.md +114 -0
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +15 -19
  5. package/build/src/akuma/body.d.ts +1 -1
  6. package/build/src/akuma/body.js +20 -33
  7. package/build/src/akuma/heart/index.d.ts +2 -2
  8. package/build/src/akuma/heart/index.js +2 -2
  9. package/build/src/akuma/heart/request-authority.d.ts +1 -0
  10. package/build/src/akuma/heart/request-authority.js +11 -1
  11. package/build/src/akuma/heart/storage.d.ts +0 -1
  12. package/build/src/akuma/heart/storage.js +0 -54
  13. package/build/src/akuma/publication.js +75 -13
  14. package/build/src/akuma/request-lifecycle.js +9 -22
  15. package/build/src/akuma/request-serve.js +17 -2
  16. package/build/src/cli/commands/task-invoke.d.ts +6 -2
  17. package/build/src/cli/commands/task-invoke.js +56 -24
  18. package/build/src/cli/commands/task.d.ts +1 -1
  19. package/build/src/cli/commands/task.js +28 -6
  20. package/build/src/cli/coordinates.d.ts +2 -0
  21. package/build/src/cli/coordinates.js +18 -2
  22. package/build/src/cli/invoke.js +1 -0
  23. package/build/src/cli/render/task.js +31 -6
  24. package/build/src/cli/square-edge.js +2 -2
  25. package/build/src/git/repository.d.ts +2 -0
  26. package/build/src/git/repository.js +34 -21
  27. package/build/src/git/target-placement.js +0 -6
  28. package/build/src/protocol/attempt.js +9 -4
  29. package/build/src/runtime/proc/windows-launch.exe +0 -0
  30. package/build/src/task/compose-language.d.ts +47 -0
  31. package/build/src/task/compose-language.js +328 -0
  32. package/build/src/task/compose-parser.d.ts +32 -0
  33. package/build/src/task/compose-parser.js +180 -0
  34. package/build/src/task/compose.d.ts +29 -9
  35. package/build/src/task/compose.js +126 -287
  36. package/build/src/task/context.d.ts +7 -1
  37. package/build/src/task/context.js +25 -6
  38. package/build/src/task/index.d.ts +10 -4
  39. package/build/src/task/index.js +14 -3
  40. package/build/src/task/operations.d.ts +6 -1
  41. package/package.json +2 -2
@@ -1,4 +1,6 @@
1
+ /* eslint-disable max-lines -- Task edge keeps native reads, forwarding, and local dispatch together. */
1
2
  import { Tasks, } from "../../task/index.js";
3
+ import { taskCompositionNamespaceHeader } from "../../task/compose-language.js";
2
4
  import { observeTaskDetails } from "../../task/operations.js";
3
5
  import { injectedBodyRequests, requestBodyTask } from "../../akuma/requests.js";
4
6
  import { decodeTaskMutationRequest } from "../../task/mutation.js";
@@ -8,6 +10,7 @@ export async function invokeTaskFromEdge(input) {
8
10
  try {
9
11
  return await invokeTask(input.parsed, {
10
12
  world: input.world,
13
+ candidate: input.candidate,
11
14
  context: input.context,
12
15
  establish: input.establish,
13
16
  readStdin: input.readStdin,
@@ -190,11 +193,11 @@ function lifecycleMutationRequest(command) {
190
193
  throw new Error(`task action cannot forward: ${command.action}`);
191
194
  }
192
195
  }
193
- async function mutationRequest(command, input, current) {
196
+ async function mutationRequest(command, input, current, composeMarkdown) {
194
197
  if (command.action === "add")
195
198
  return await addMutationRequest(command, input, current);
196
199
  if (command.action === "compose")
197
- return { action: "task.compose", markdown: await input.readStdin(), namespace: current };
200
+ return { action: "task.compose", markdown: composeMarkdown ?? (await input.readStdin()), namespace: current };
198
201
  if (command.action === "update")
199
202
  return await updateMutationRequest(command, input);
200
203
  return lifecycleMutationRequest(command);
@@ -293,22 +296,25 @@ function missingWorld(command) {
293
296
  });
294
297
  if (isWorldObservation(command))
295
298
  return { kind: "absent" };
296
- if (command.action === "namespace")
297
- return { kind: "accepted", value: [] };
299
+ if (command.action === "compose" && command.flags.plan === true)
300
+ return { kind: "absent" };
301
+ if (command.action === "context")
302
+ return { kind: "accepted", value: { namespace: [], source: "default-root" } };
298
303
  if (command.action === "hold" || command.action === "done" || command.action === "drop") {
299
304
  return { items: command.positionals.map((id) => ({ id: id, outcome: missing(id) })) };
300
305
  }
301
306
  return missing(command.positionals[0]);
302
307
  }
303
- async function invokeLocalMutation(tasks, command, input, current) {
308
+ async function invokeLocalMutation(input) {
309
+ const { tasks, command, edge, current, composeMarkdown, composeNamespace } = input;
304
310
  if (isWorldObservation(command))
305
311
  return observeWorldRead(tasks, command, current);
306
312
  if (command.action === "show" || command.action === "tree")
307
313
  return invokeRead(tasks, command, current);
308
314
  if (command.action === "add")
309
- return await invokeAdd(tasks, command, input, current ?? []);
315
+ return await invokeAdd(tasks, command, edge, current ?? []);
310
316
  if (command.action === "update")
311
- return await invokeUpdate(tasks, command, input.readStdin);
317
+ return await invokeUpdate(tasks, command, edge.readStdin);
312
318
  const id = command.positionals[0];
313
319
  switch (command.action) {
314
320
  case "start":
@@ -327,17 +333,23 @@ async function invokeLocalMutation(tasks, command, input, current) {
327
333
  const note = value(command, "note");
328
334
  return tasks.batch({ verb: "drop", ids: command.positionals, ...(note === undefined ? {} : { note }) });
329
335
  }
330
- case "namespace": {
336
+ case "context": {
331
337
  const selected = namespace(command.positionals[0]);
332
- if (selected !== undefined)
333
- await writeTaskNamespaceContext(input.context.directory, selected);
334
- return { kind: "accepted", value: selected ?? current ?? [] };
338
+ if (selected !== undefined) {
339
+ await writeTaskNamespaceContext(edge.context.writeRoot ?? edge.context.directory, selected);
340
+ const resolved = await resolveTaskNamespaceContext(edge.context);
341
+ if (typeof resolved === "object" && "kind" in resolved)
342
+ return { kind: "refused", refusal: resolved };
343
+ return { kind: "accepted", value: resolved };
344
+ }
345
+ return { kind: "accepted", value: { namespace: current ?? [], source: "default-root" } };
335
346
  }
336
347
  case "compose":
337
348
  return tasks.compose({
338
- markdown: await input.readStdin(),
339
- namespace: current ?? [],
340
- ...(input.actor === undefined ? {} : { actor: input.actor }),
349
+ markdown: composeMarkdown ?? (await edge.readStdin()),
350
+ namespace: composeNamespace ?? current ?? [],
351
+ ...(edge.actor === undefined ? {} : { actor: edge.actor }),
352
+ ...(command.flags.plan === true ? { plan: true } : {}),
341
353
  });
342
354
  default:
343
355
  throw new Error(`task action has no invocation: ${command.action}`);
@@ -345,23 +357,33 @@ async function invokeLocalMutation(tasks, command, input, current) {
345
357
  }
346
358
  function establishesWorld(command) {
347
359
  return (command.action === "add" ||
348
- command.action === "compose" ||
349
- (command.action === "namespace" && command.positionals.length > 0));
360
+ (command.action === "compose" && command.flags.plan !== true) ||
361
+ (command.action === "context" && command.positionals.length > 0));
350
362
  }
351
363
  function forwardsMutation(command) {
352
364
  return (command.action !== "show" &&
353
365
  command.action !== "tree" &&
354
- command.action !== "namespace" &&
366
+ command.action !== "context" &&
367
+ !(command.action === "compose" && command.flags.plan === true) &&
355
368
  !isWorldObservation(command));
356
369
  }
370
+ // eslint-disable-next-line complexity -- this is the single CLI edge ordering world, context, forwarding, and local execution.
357
371
  export async function invokeTask(command, input) {
358
- const world = input.world ?? (establishesWorld(command) ? await input.establish() : null);
372
+ const planOnly = command.action === "compose" && command.flags.plan === true;
373
+ const composeMarkdown = command.action === "compose" ? await input.readStdin() : undefined;
374
+ const composition = composeMarkdown === undefined ? undefined : taskCompositionNamespaceHeader(composeMarkdown);
375
+ const explicitNamespace = command.action === "add"
376
+ ? namespace(value(command, "namespace"))
377
+ : command.action === "compose" && composition?.specified
378
+ ? composition.namespace
379
+ : undefined;
380
+ const world = input.world ?? (planOnly ? input.candidate : establishesWorld(command) ? await input.establish() : null);
359
381
  if (world === null)
360
382
  return missingWorld(command);
361
383
  const tasks = Tasks.of(world);
362
- const contextSensitive = command.action === "namespace" ||
363
- command.action === "add" ||
364
- command.action === "compose" ||
384
+ const contextSensitive = command.action === "context" ||
385
+ (command.action === "add" && explicitNamespace === undefined) ||
386
+ (command.action === "compose" && composition?.specified !== true) ||
365
387
  ((command.action === "ls" ||
366
388
  command.action === "ready" ||
367
389
  command.action === "blocked" ||
@@ -372,12 +394,22 @@ export async function invokeTask(command, input) {
372
394
  const resolved = await resolveTaskNamespaceContext(input.context);
373
395
  if (typeof resolved === "object" && "kind" in resolved)
374
396
  return { kind: "refused", refusal: resolved };
375
- current = resolved;
397
+ current = resolved.namespace;
398
+ if (command.action === "context" && command.positionals.length === 0) {
399
+ return { kind: "accepted", value: resolved };
400
+ }
376
401
  }
377
402
  const requests = injectedBodyRequests();
378
403
  if (requests !== null && forwardsMutation(command)) {
379
- const request = validatedMutationRequest(await mutationRequest(command, input, current ?? []));
404
+ const request = validatedMutationRequest(await mutationRequest(command, input, explicitNamespace ?? current ?? [], composeMarkdown));
380
405
  return (await requestBodyTask({ directory: requests, world, request }));
381
406
  }
382
- return await invokeLocalMutation(tasks, command, input, current);
407
+ return await invokeLocalMutation({
408
+ tasks,
409
+ command,
410
+ edge: input,
411
+ ...(current === undefined ? {} : { current }),
412
+ ...(composeMarkdown === undefined ? {} : { composeMarkdown }),
413
+ ...(explicitNamespace === undefined ? {} : { composeNamespace: explicitNamespace }),
414
+ });
383
415
  }
@@ -1,5 +1,5 @@
1
1
  import { type TaskQueryExpression } from "./task-query.js";
2
- export type TaskAction = "add" | "show" | "ls" | "ready" | "blocked" | "tree" | "doctor" | "update" | "query" | "start" | "stop" | "hold" | "resume" | "done" | "drop" | "namespace" | "compose";
2
+ export type TaskAction = "add" | "show" | "ls" | "ready" | "blocked" | "tree" | "doctor" | "update" | "query" | "start" | "stop" | "hold" | "resume" | "done" | "drop" | "context" | "compose";
3
3
  type TaskFlagValue = string | true | readonly string[];
4
4
  type TaskStdin = "document" | "body" | "append" | "note" | "compose";
5
5
  export type ParsedTaskCommand = Readonly<{
@@ -38,18 +38,21 @@ task add [--namespace <ns>] [--actor <actor>] -`,
38
38
  flags: { ...COMMON, closed: "boolean", all: "boolean", world: "boolean", limit: "value" },
39
39
  usage: "task ls [--closed | --all] [--world] [--limit <n>]",
40
40
  purpose: "List Tasks in the selected scope.",
41
+ details: "--world lists every namespace in the current Task world.",
41
42
  },
42
43
  ready: {
43
44
  arity: [0, 0],
44
45
  flags: { ...COMMON, world: "boolean", parent: "value", limit: "value" },
45
46
  usage: "task ready [--world] [--parent <TaskId>] [--limit <n>]",
46
47
  purpose: "List open Tasks whose every need is terminal.",
48
+ details: "--world lists every namespace in the current Task world.",
47
49
  },
48
50
  blocked: {
49
51
  arity: [0, 0],
50
52
  flags: { ...COMMON, world: "boolean", parent: "value", limit: "value" },
51
53
  usage: "task blocked [--world] [--parent <TaskId>] [--limit <n>]",
52
54
  purpose: "List Tasks blocked by dependencies.",
55
+ details: "--world lists every namespace in the current Task world.",
53
56
  },
54
57
  query: {
55
58
  arity: [0, 0],
@@ -60,6 +63,7 @@ task add [--namespace <ns>] [--actor <actor>] -`,
60
63
  details: [
61
64
  "fields: state priority title id parent under needs blocks ready blocked created updated",
62
65
  "operators: = != < > <= >= ~ and or not ( )",
66
+ "--world lists every namespace in the current Task world.",
63
67
  "examples:",
64
68
  " keiyaku task query --where 'priority <= 1 and ready' --world",
65
69
  " keiyaku task query --where 'updated < 2026-08-06T00:00:00.000Z' --world",
@@ -124,18 +128,36 @@ task add [--namespace <ns>] [--actor <actor>] -`,
124
128
  usage: "task drop <TaskId>... [--note <text>]",
125
129
  purpose: "Drop one or more Tasks.",
126
130
  },
127
- namespace: {
131
+ context: {
128
132
  arity: [0, 1],
129
133
  flags: COMMON,
130
- usage: "task namespace [<namespace>]",
131
- purpose: "Read or replace the current Task namespace.",
134
+ usage: "task context [<namespace>]",
135
+ purpose: "Read or replace the directory Task context.",
132
136
  },
133
137
  compose: {
134
138
  arity: [0, 0],
135
139
  stdin: "compose",
136
- flags: { ...COMMON, actor: "value" },
137
- usage: "task compose [--actor <actor>] -",
138
- purpose: "Admit Task documents independently; partial admission has no cross-file atomicity or rollback.",
140
+ flags: { ...COMMON, actor: "value", plan: "boolean" },
141
+ usage: "task compose [--actor <actor>] [--plan] -",
142
+ purpose: "Plan or admit one explicit Task composition.",
143
+ details: [
144
+ "nodes: + <Title> creates; @task/<id> modifies a pre-existing Task",
145
+ "properties: as = <alias>; pri = 0..3; parent = <ref>|empty",
146
+ "relations: needs|supersedes|relates with =, +=, or -=",
147
+ "references: @task/... is pre-existing; ^alias is new in this document",
148
+ "body: body = clears; body <<TOKEN reads exact content to TOKEN",
149
+ "TOKEN: [A-Z][A-Z0-9_]* with length 3..32",
150
+ "example:",
151
+ " ns=feature",
152
+ " + Parent",
153
+ " as = parent",
154
+ " pri = 1",
155
+ " body <<BODY",
156
+ " Exact body bytes.",
157
+ " BODY",
158
+ " + Child",
159
+ " parent = ^parent",
160
+ ].join("\n"),
139
161
  },
140
162
  };
141
163
  export function isTaskAction(value) {
@@ -10,6 +10,8 @@ export type CliCoordinates = Readonly<{
10
10
  taskContext: Readonly<{
11
11
  directory: string;
12
12
  boundary: string;
13
+ writeRoot: string;
14
+ managed: boolean;
13
15
  }>;
14
16
  establishWorld: () => Promise<WorldRoot>;
15
17
  }>;
@@ -1,4 +1,4 @@
1
- import { realpath } from "node:fs/promises";
1
+ import { lstat, realpath } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { NoGitWorldError, Repo } from "../library/repo.js";
4
4
  import { World, WorldError } from "../world.js";
@@ -24,6 +24,17 @@ async function discoverRepoAt(coordinate, gitPath) {
24
24
  throw error;
25
25
  }
26
26
  }
27
+ async function managedContractWorktree(root) {
28
+ try {
29
+ const stat = await lstat(resolve(root, ".keiyaku", "KEIYAKU.md"));
30
+ return stat.isFile() && !stat.isSymbolicLink();
31
+ }
32
+ catch (error) {
33
+ if (error.code === "ENOENT")
34
+ return false;
35
+ throw error;
36
+ }
37
+ }
27
38
  function repoFor(use, discovery) {
28
39
  if (use === "none")
29
40
  return undefined;
@@ -72,7 +83,12 @@ export async function resolveCliCoordinates(input) {
72
83
  ...(repo === undefined ? {} : { repo }),
73
84
  world: world.root,
74
85
  candidateWorld: world.candidate,
75
- taskContext: { directory: cwd, boundary },
86
+ taskContext: {
87
+ directory: cwd,
88
+ boundary,
89
+ writeRoot: worldRepo === undefined ? boundary : cwd,
90
+ managed: await managedContractWorktree(boundary),
91
+ },
76
92
  establishWorld: world.establish,
77
93
  };
78
94
  }
@@ -318,6 +318,7 @@ async function invokeParsed(invocation, runtime) {
318
318
  return await (await import("./commands/task-invoke.js")).invokeTaskFromEdge({
319
319
  parsed,
320
320
  world,
321
+ candidate: candidateWorld,
321
322
  context: taskContext,
322
323
  establish: coordinates.establishWorld,
323
324
  readStdin: edge.readStdin,
@@ -59,6 +59,9 @@ function projectRefusal(refusal) {
59
59
  if (refusal.kind === "relation-owned-by-other") {
60
60
  return { line: `relation-owned-by-other ${refusal.taskId} ${refusal.related} ${refusal.declaringTask}` };
61
61
  }
62
+ if (refusal.kind === "invalid-composition") {
63
+ return { line: refusal.kind, compositionDiagnostics: refusal.diagnostics };
64
+ }
62
65
  return { line: refusal.kind, diagnostic: refusal.diagnostic };
63
66
  }
64
67
  function appendDiagnostic(lines, diagnostic) {
@@ -73,6 +76,9 @@ function renderFailure(verb, result, columns) {
73
76
  const facts = projectRefusal(result.refusal);
74
77
  lines.push(facts.line);
75
78
  appendDiagnostic(lines, facts.diagnostic);
79
+ for (const item of facts.compositionDiagnostics ?? []) {
80
+ lines.push(`line ${item.line} · ${safeText(item.reason)} · ${safeText(item.token)}`);
81
+ }
76
82
  return lines.join("\n");
77
83
  }
78
84
  function edge(label, ref, mark) {
@@ -256,12 +262,30 @@ function stoppedLines(stopped) {
256
262
  appendDiagnostic(lines, facts.diagnostic);
257
263
  return lines;
258
264
  }
265
+ function aliasLines(aliases) {
266
+ return aliases.map((binding) => `alias ^${binding.alias} ${binding.taskId}`);
267
+ }
268
+ function renderPlan(result) {
269
+ const lines = [
270
+ `compose plan · ${result.admissionOrder.length} documents`,
271
+ ...aliasLines(result.aliases),
272
+ ...result.admissionOrder.map((id, index) => `admit ${index + 1} ${id}`),
273
+ ];
274
+ for (const body of result.bodies) {
275
+ lines.push(`body ${body.taskId} · ${body.bytes} bytes`);
276
+ lines.push(` first ${safeText(body.firstLine)}`);
277
+ lines.push(` last ${safeText(body.lastLine)}`);
278
+ }
279
+ return lines.join("\n");
280
+ }
259
281
  function renderCompose(result, columns) {
260
282
  if (result.kind === "incomplete")
261
283
  return "";
284
+ if (result.kind === "planned")
285
+ return renderPlan(result);
262
286
  if (result.kind !== "accepted")
263
287
  return renderFailure("compose", result, columns);
264
- const lines = [`✓ compose accepted · ${result.documentChanges.length} changed`];
288
+ const lines = [`✓ compose accepted · ${result.documentChanges.length} changed`, ...aliasLines(result.aliases)];
265
289
  composeDiffs(lines, result.documentChanges);
266
290
  return lines.join("\n");
267
291
  }
@@ -295,11 +319,12 @@ function renderTaskValue(command, result, columns) {
295
319
  }
296
320
  if (command.action === "doctor")
297
321
  return renderDoctor(result);
298
- if (command.action === "namespace") {
299
- const namespace = result;
300
- if (namespace.kind !== "accepted")
301
- return renderFailure(command.action, namespace, columns);
302
- return `namespace ${namespace.value.length === 0 ? "root" : namespace.value.join("/")}`;
322
+ if (command.action === "context") {
323
+ const context = result;
324
+ if (context.kind !== "accepted")
325
+ return renderFailure(command.action, context, columns);
326
+ const value = context.value.namespace.length === 0 ? "root" : context.value.namespace.join("/");
327
+ return `context ${value} · ${context.value.source}`;
303
328
  }
304
329
  if (command.action === "compose")
305
330
  return renderCompose(result, columns);
@@ -44,13 +44,13 @@ export async function recognizeAndListen(worldRoot, environment, allocated) {
44
44
  }
45
45
  }
46
46
  if (bound)
47
- unbindCurrentParticipant(path, name, environment);
47
+ await unbindCurrentParticipant(path, name, environment);
48
48
  };
49
49
  try {
50
50
  const joinedResult = await square.joinWithActivity(name);
51
51
  joined = joinedResult.activity !== null;
52
52
  const participant = joinedResult.participant;
53
- bound = bindCurrentParticipant(path, name, environment).created;
53
+ bound = (await bindCurrentParticipant(path, name, environment)).created;
54
54
  const listener = participant;
55
55
  const change = await listener.listen(allocated.id);
56
56
  listening = change.activity !== null;
@@ -42,6 +42,8 @@ export declare function commonGitDirectory(repository: GitRepository): string;
42
42
  export declare function worktreeGitDirectory(repository: GitRepository, worktree: string): Promise<string>;
43
43
  export declare function worktreeRoot(repository: GitRepository): Promise<string>;
44
44
  export declare function readRef(repository: GitRepository, ref: string): Promise<GitOid | null>;
45
+ /** Read an explicit set of asserted refs from one fresh Git observation. */
46
+ export declare function readRefs(repository: GitRepository, refs: readonly string[]): Promise<Map<any, any>>;
45
47
  export declare function readTreeEntries(repository: GitRepository, tree: GitOid): Promise<Map<string, TreeEntry>>;
46
48
  export declare function readGit(repository: GitRepository): Promise<GitSnapshot>;
47
49
  /** Read only the requested private Git paths from one immutable Git tree. */
@@ -113,9 +113,6 @@ export async function worktreeGitDirectory(repository, worktree) {
113
113
  export async function worktreeRoot(repository) {
114
114
  return await absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
115
115
  }
116
- function assertOid(oid, label) {
117
- gitObjectId(oid, label);
118
- }
119
116
  function assertRef(ref) {
120
117
  if (!ref.startsWith("refs/") || /[\s\0]/.test(ref) || ref.endsWith("/") || ref.includes("..")) {
121
118
  throw new Error(`invalid Git ref: ${ref}`);
@@ -129,10 +126,7 @@ export async function readRef(repository, ref) {
129
126
  assertAssertionRef(ref);
130
127
  try {
131
128
  const oid = (await runGit(repository, ["rev-parse", "--verify", "--quiet", ref])).toString("utf8").trim();
132
- if (oid.length === 0)
133
- return null;
134
- assertOid(oid, `ref ${ref}`);
135
- return oid;
129
+ return oid.length === 0 ? null : gitObjectId(oid, `ref ${ref}`);
136
130
  }
137
131
  catch (error) {
138
132
  if (error instanceof GitPlumbingError && error.status === 1)
@@ -140,10 +134,29 @@ export async function readRef(repository, ref) {
140
134
  throw error;
141
135
  }
142
136
  }
137
+ /** Read an explicit set of asserted refs from one fresh Git observation. */
138
+ export async function readRefs(repository, refs) {
139
+ const refsToRead = [...new Set(refs)];
140
+ for (const ref of refsToRead)
141
+ assertAssertionRef(ref);
142
+ if (refsToRead.length === 0)
143
+ return new Map();
144
+ const output = (await runGit(repository, ["cat-file", "--batch-check=%(objectname)"], `${refsToRead.join("\n")}\n`)).toString("utf8");
145
+ const records = output.split("\n");
146
+ if (records.pop() !== "" || records.length !== refsToRead.length)
147
+ throw new Error("Git ref batch output is malformed");
148
+ return new Map(refsToRead.map((ref, index) => {
149
+ const oid = records[index];
150
+ if (oid === `${ref} missing`)
151
+ return [ref, null];
152
+ gitObjectId(oid, `ref ${ref}`);
153
+ return [ref, oid];
154
+ }));
155
+ }
143
156
  async function readTreeForCommit(repository, commit) {
144
- assertOid(commit, "Git commit");
157
+ gitObjectId(commit, "Git commit");
145
158
  const tree = (await runGit(repository, ["show", "-s", "--format=%T", commit])).toString("utf8").trim();
146
- assertOid(tree, "Git tree");
159
+ gitObjectId(tree, "Git tree");
147
160
  return tree;
148
161
  }
149
162
  function parseTreeEntries(output) {
@@ -159,13 +172,13 @@ function parseTreeEntries(output) {
159
172
  if (mode === undefined || type === undefined || oid === undefined || path.length === 0) {
160
173
  throw new GitPlumbingError({ stderr: record, status: null, message: `malformed ls-tree record: ${record}` });
161
174
  }
162
- assertOid(oid, `tree entry ${path}`);
175
+ gitObjectId(oid, `tree entry ${path}`);
163
176
  entries.set(path, { mode, type, oid });
164
177
  }
165
178
  return entries;
166
179
  }
167
180
  export async function readTreeEntries(repository, tree) {
168
- assertOid(tree, "tree");
181
+ gitObjectId(tree, "tree");
169
182
  const output = await runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
170
183
  return parseTreeEntries(output);
171
184
  }
@@ -193,11 +206,11 @@ export async function readGitPaths(repository, requestedPaths) {
193
206
  }
194
207
  export async function writeBlob(repository, bytes) {
195
208
  const oid = (await runGit(repository, ["hash-object", "-w", "--stdin"], bytes)).toString("utf8").trim();
196
- assertOid(oid, "written blob");
209
+ gitObjectId(oid, "written blob");
197
210
  return oid;
198
211
  }
199
212
  export async function readBlob(repository, oid) {
200
- assertOid(oid, "blob");
213
+ gitObjectId(oid, "blob");
201
214
  return await runGit(repository, ["cat-file", "blob", oid]);
202
215
  }
203
216
  function malformedBatchOutput(detail) {
@@ -210,7 +223,7 @@ function malformedBatchOutput(detail) {
210
223
  async function readObjects(repository, oids) {
211
224
  const unique = [...new Set(oids)];
212
225
  for (const oid of unique)
213
- assertOid(oid, "Git object");
226
+ gitObjectId(oid, "Git object");
214
227
  if (unique.length === 0)
215
228
  return new Map();
216
229
  const output = await runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
@@ -305,7 +318,7 @@ async function writePreparedTrees(repository, prepared) {
305
318
  for (let index = 0; index < prepared.length; index += 1) {
306
319
  const expected = prepared[index].oid;
307
320
  const actual = written[index];
308
- assertOid(actual, "written tree");
321
+ gitObjectId(actual, "written tree");
309
322
  if (actual !== expected) {
310
323
  throw new GitPlumbingError({ stderr: output, status: null, message: `mktree wrote unexpected tree: ${actual}` });
311
324
  }
@@ -338,9 +351,9 @@ export async function updateGitTreeFromFrozenDirectories(repository, baseTree, d
338
351
  }
339
352
  async function writeCommitObject(input) {
340
353
  const { repository, tree, parents, message, actor, email, at } = input;
341
- assertOid(tree, "commit tree");
354
+ gitObjectId(tree, "commit tree");
342
355
  for (const parent of parents)
343
- assertOid(parent, "commit parent");
356
+ gitObjectId(parent, "commit parent");
344
357
  const args = ["commit-tree", tree];
345
358
  for (const parent of parents)
346
359
  args.push("-p", parent);
@@ -353,7 +366,7 @@ async function writeCommitObject(input) {
353
366
  }))
354
367
  .toString("utf8")
355
368
  .trim();
356
- assertOid(commit, "written Git commit");
369
+ gitObjectId(commit, "written Git commit");
357
370
  return commit;
358
371
  }
359
372
  export async function writeCommit(input) {
@@ -369,7 +382,7 @@ export async function writeCommit(input) {
369
382
  }
370
383
  function refAssertionLine(assertion) {
371
384
  assertAssertionRef(assertion.ref);
372
- assertOid(assertion.oid, `expected oid for ${assertion.ref}`);
385
+ gitObjectId(assertion.oid, `expected oid for ${assertion.ref}`);
373
386
  return `${assertion.ref === "HEAD" ? "" : "option no-deref\n"}verify ${assertion.ref} ${assertion.oid}`;
374
387
  }
375
388
  function refUpdateLine(update, index) {
@@ -378,9 +391,9 @@ function refUpdateLine(update, index) {
378
391
  throw new Error(`duplicate ref update: ${update.ref}`);
379
392
  if (index > 0 && update.expectedOid === null)
380
393
  throw new Error("target ref updates require non-null OIDs");
381
- assertOid(update.newOid, `new oid for ${update.ref}`);
394
+ gitObjectId(update.newOid, `new oid for ${update.ref}`);
382
395
  if (update.expectedOid !== null)
383
- assertOid(update.expectedOid, `expected oid for ${update.ref}`);
396
+ gitObjectId(update.expectedOid, `expected oid for ${update.ref}`);
384
397
  return `option no-deref\nupdate ${update.ref} ${update.newOid} ${update.expectedOid ?? "0".repeat(update.newOid.length)}`;
385
398
  }
386
399
  export async function updateRefsAtomically(repository, updates, assertions = []) {
@@ -280,8 +280,6 @@ async function classifyCheckout(input) {
280
280
  const candidateWorkspace = await workspaceMatchesTreeOnPaths(repository, path, candidateTree, workspaceTree, changedPaths);
281
281
  if (candidateIndex && candidateWorkspace)
282
282
  return { kind: "complete" };
283
- if (workspaceTree === candidateTree)
284
- return { kind: "recoverable", action: "candidate-index" };
285
283
  const predecessorIndex = await indexMatchesTreeOnPaths(repository, path, predecessorTree, changedPaths);
286
284
  if (predecessorIndex && candidateWorkspace)
287
285
  return { kind: "recoverable", action: "index-merge" };
@@ -305,10 +303,6 @@ async function recoverCheckout(input) {
305
303
  if (classified.kind === "retained")
306
304
  return "retained";
307
305
  const { repository, path, predecessor, candidate } = input;
308
- if (classified.action === "candidate-index") {
309
- await runGit(repository, ["-C", path, "read-tree", gitObjectIdForSnapshot(candidate)]);
310
- return "recovered";
311
- }
312
306
  if (classified.action === "index-merge") {
313
307
  await runGit(repository, [
314
308
  "-C",
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { encodeEntry } from "../core/facts/codec.js";
3
3
  import { admit } from "../git/admission.js";
4
4
  import { observeContractsForAdmissionAt } from "../git/observe.js";
5
- import { GIT_REF, readRef } from "../git/repository.js";
5
+ import { GIT_REF, readRefs } from "../git/repository.js";
6
6
  import { foldJournal } from "../core/facts/fold.js";
7
7
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
8
8
  import { entryUlid, } from "../core/facts/types.js";
@@ -78,12 +78,17 @@ function recoveredAcceptance(observation, offer, primaryContract) {
78
78
  };
79
79
  }
80
80
  async function publicationPremiseMoved(repository, observation, offer, assertions) {
81
- if ((await readRef(repository, GIT_REF)) !== observation.admission.snapshot.commit)
81
+ const refs = await readRefs(repository, [
82
+ GIT_REF,
83
+ ...assertions.map((assertion) => assertion.ref),
84
+ ...(offer.target === undefined ? [] : [offer.target.target]),
85
+ ]);
86
+ if (refs.get(GIT_REF) !== observation.admission.snapshot.commit)
82
87
  return true;
83
88
  for (const assertion of assertions)
84
- if ((await readRef(repository, assertion.ref)) !== assertion.oid)
89
+ if (refs.get(assertion.ref) !== assertion.oid)
85
90
  return true;
86
- return offer.target !== undefined && (await readRef(repository, offer.target.target)) !== offer.target.expectedOid;
91
+ return offer.target !== undefined && refs.get(offer.target.target) !== offer.target.expectedOid;
87
92
  }
88
93
  /** Admit one decided offer without making another legal decision. */
89
94
  export async function admitDecidedOffer(input) {
@@ -0,0 +1,47 @@
1
+ import type { TaskBoard } from "./board.js";
2
+ import { type TaskDocument } from "./document.js";
3
+ import { type TaskId } from "./identity.js";
4
+ import { type TaskCompositionDiagnostic } from "./operations.js";
5
+ export declare function taskCompositionNamespaceHeader(markdown: string): Readonly<{
6
+ specified: boolean;
7
+ namespace?: readonly string[];
8
+ }>;
9
+ export type TaskCompositionAlias = Readonly<{
10
+ alias: string;
11
+ taskId: TaskId;
12
+ }>;
13
+ export type TaskCompositionBodyPreview = Readonly<{
14
+ taskId: TaskId;
15
+ bytes: number;
16
+ firstLine: string;
17
+ lastLine: string;
18
+ }>;
19
+ export type PlannedTask = Readonly<{
20
+ index: number;
21
+ line: number;
22
+ kind: "new" | "existing";
23
+ alias?: string;
24
+ before: TaskDocument | null;
25
+ after: TaskDocument;
26
+ }>;
27
+ export type TaskCompositionPlan = Readonly<{
28
+ namespace: readonly string[];
29
+ aliases: readonly TaskCompositionAlias[];
30
+ admissionOrder: readonly TaskId[];
31
+ bodies: readonly TaskCompositionBodyPreview[];
32
+ tasks: readonly PlannedTask[];
33
+ }>;
34
+ export type TaskCompositionPlanning = Readonly<{
35
+ kind: "planned";
36
+ plan: TaskCompositionPlan;
37
+ }> | Readonly<{
38
+ kind: "refused";
39
+ diagnostics: readonly TaskCompositionDiagnostic[];
40
+ }>;
41
+ export declare function planTaskComposition(input: Readonly<{
42
+ markdown: string;
43
+ board: TaskBoard;
44
+ namespace: readonly string[];
45
+ at: string;
46
+ actor?: string;
47
+ }>): TaskCompositionPlanning;