@astrosheep/keiyaku 4.5.6 → 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 (36) 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/src/akuma/body.d.ts +1 -1
  5. package/build/src/akuma/body.js +20 -33
  6. package/build/src/akuma/heart/index.d.ts +2 -2
  7. package/build/src/akuma/heart/index.js +2 -2
  8. package/build/src/akuma/heart/request-authority.d.ts +1 -0
  9. package/build/src/akuma/heart/request-authority.js +11 -1
  10. package/build/src/akuma/heart/storage.d.ts +0 -1
  11. package/build/src/akuma/heart/storage.js +0 -54
  12. package/build/src/akuma/publication.js +75 -13
  13. package/build/src/akuma/request-lifecycle.js +9 -22
  14. package/build/src/akuma/request-serve.js +17 -2
  15. package/build/src/cli/commands/task-invoke.d.ts +4 -2
  16. package/build/src/cli/commands/task-invoke.js +48 -22
  17. package/build/src/cli/commands/task.d.ts +1 -1
  18. package/build/src/cli/commands/task.js +7 -3
  19. package/build/src/cli/coordinates.d.ts +2 -0
  20. package/build/src/cli/coordinates.js +18 -2
  21. package/build/src/cli/render/task.js +6 -5
  22. package/build/src/cli/square-edge.js +2 -2
  23. package/build/src/git/repository.d.ts +2 -0
  24. package/build/src/git/repository.js +34 -21
  25. package/build/src/git/target-placement.js +0 -6
  26. package/build/src/protocol/attempt.js +9 -4
  27. package/build/src/task/compose-language.d.ts +4 -0
  28. package/build/src/task/compose-language.js +7 -0
  29. package/build/src/task/compose-parser.d.ts +1 -0
  30. package/build/src/task/compose-parser.js +4 -2
  31. package/build/src/task/compose.d.ts +1 -0
  32. package/build/src/task/compose.js +2 -1
  33. package/build/src/task/context.d.ts +7 -1
  34. package/build/src/task/context.js +25 -6
  35. package/build/src/task/index.d.ts +6 -1
  36. 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";
@@ -191,11 +193,11 @@ function lifecycleMutationRequest(command) {
191
193
  throw new Error(`task action cannot forward: ${command.action}`);
192
194
  }
193
195
  }
194
- async function mutationRequest(command, input, current) {
196
+ async function mutationRequest(command, input, current, composeMarkdown) {
195
197
  if (command.action === "add")
196
198
  return await addMutationRequest(command, input, current);
197
199
  if (command.action === "compose")
198
- return { action: "task.compose", markdown: await input.readStdin(), namespace: current };
200
+ return { action: "task.compose", markdown: composeMarkdown ?? (await input.readStdin()), namespace: current };
199
201
  if (command.action === "update")
200
202
  return await updateMutationRequest(command, input);
201
203
  return lifecycleMutationRequest(command);
@@ -296,22 +298,23 @@ function missingWorld(command) {
296
298
  return { kind: "absent" };
297
299
  if (command.action === "compose" && command.flags.plan === true)
298
300
  return { kind: "absent" };
299
- if (command.action === "namespace")
300
- return { kind: "accepted", value: [] };
301
+ if (command.action === "context")
302
+ return { kind: "accepted", value: { namespace: [], source: "default-root" } };
301
303
  if (command.action === "hold" || command.action === "done" || command.action === "drop") {
302
304
  return { items: command.positionals.map((id) => ({ id: id, outcome: missing(id) })) };
303
305
  }
304
306
  return missing(command.positionals[0]);
305
307
  }
306
- async function invokeLocalMutation(tasks, command, input, current) {
308
+ async function invokeLocalMutation(input) {
309
+ const { tasks, command, edge, current, composeMarkdown, composeNamespace } = input;
307
310
  if (isWorldObservation(command))
308
311
  return observeWorldRead(tasks, command, current);
309
312
  if (command.action === "show" || command.action === "tree")
310
313
  return invokeRead(tasks, command, current);
311
314
  if (command.action === "add")
312
- return await invokeAdd(tasks, command, input, current ?? []);
315
+ return await invokeAdd(tasks, command, edge, current ?? []);
313
316
  if (command.action === "update")
314
- return await invokeUpdate(tasks, command, input.readStdin);
317
+ return await invokeUpdate(tasks, command, edge.readStdin);
315
318
  const id = command.positionals[0];
316
319
  switch (command.action) {
317
320
  case "start":
@@ -330,17 +333,22 @@ async function invokeLocalMutation(tasks, command, input, current) {
330
333
  const note = value(command, "note");
331
334
  return tasks.batch({ verb: "drop", ids: command.positionals, ...(note === undefined ? {} : { note }) });
332
335
  }
333
- case "namespace": {
336
+ case "context": {
334
337
  const selected = namespace(command.positionals[0]);
335
- if (selected !== undefined)
336
- await writeTaskNamespaceContext(input.context.directory, selected);
337
- 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" } };
338
346
  }
339
347
  case "compose":
340
348
  return tasks.compose({
341
- markdown: await input.readStdin(),
342
- namespace: current ?? [],
343
- ...(input.actor === undefined ? {} : { actor: input.actor }),
349
+ markdown: composeMarkdown ?? (await edge.readStdin()),
350
+ namespace: composeNamespace ?? current ?? [],
351
+ ...(edge.actor === undefined ? {} : { actor: edge.actor }),
344
352
  ...(command.flags.plan === true ? { plan: true } : {}),
345
353
  });
346
354
  default:
@@ -350,24 +358,32 @@ async function invokeLocalMutation(tasks, command, input, current) {
350
358
  function establishesWorld(command) {
351
359
  return (command.action === "add" ||
352
360
  (command.action === "compose" && command.flags.plan !== true) ||
353
- (command.action === "namespace" && command.positionals.length > 0));
361
+ (command.action === "context" && command.positionals.length > 0));
354
362
  }
355
363
  function forwardsMutation(command) {
356
364
  return (command.action !== "show" &&
357
365
  command.action !== "tree" &&
358
- command.action !== "namespace" &&
366
+ command.action !== "context" &&
359
367
  !(command.action === "compose" && command.flags.plan === true) &&
360
368
  !isWorldObservation(command));
361
369
  }
370
+ // eslint-disable-next-line complexity -- this is the single CLI edge ordering world, context, forwarding, and local execution.
362
371
  export async function invokeTask(command, input) {
363
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;
364
380
  const world = input.world ?? (planOnly ? input.candidate : establishesWorld(command) ? await input.establish() : null);
365
381
  if (world === null)
366
382
  return missingWorld(command);
367
383
  const tasks = Tasks.of(world);
368
- const contextSensitive = command.action === "namespace" ||
369
- command.action === "add" ||
370
- command.action === "compose" ||
384
+ const contextSensitive = command.action === "context" ||
385
+ (command.action === "add" && explicitNamespace === undefined) ||
386
+ (command.action === "compose" && composition?.specified !== true) ||
371
387
  ((command.action === "ls" ||
372
388
  command.action === "ready" ||
373
389
  command.action === "blocked" ||
@@ -378,12 +394,22 @@ export async function invokeTask(command, input) {
378
394
  const resolved = await resolveTaskNamespaceContext(input.context);
379
395
  if (typeof resolved === "object" && "kind" in resolved)
380
396
  return { kind: "refused", refusal: resolved };
381
- current = resolved;
397
+ current = resolved.namespace;
398
+ if (command.action === "context" && command.positionals.length === 0) {
399
+ return { kind: "accepted", value: resolved };
400
+ }
382
401
  }
383
402
  const requests = injectedBodyRequests();
384
403
  if (requests !== null && forwardsMutation(command)) {
385
- const request = validatedMutationRequest(await mutationRequest(command, input, current ?? []));
404
+ const request = validatedMutationRequest(await mutationRequest(command, input, explicitNamespace ?? current ?? [], composeMarkdown));
386
405
  return (await requestBodyTask({ directory: requests, world, request }));
387
406
  }
388
- 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
+ });
389
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,11 +128,11 @@ 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],
@@ -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
  }
@@ -319,11 +319,12 @@ function renderTaskValue(command, result, columns) {
319
319
  }
320
320
  if (command.action === "doctor")
321
321
  return renderDoctor(result);
322
- if (command.action === "namespace") {
323
- const namespace = result;
324
- if (namespace.kind !== "accepted")
325
- return renderFailure(command.action, namespace, columns);
326
- 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}`;
327
328
  }
328
329
  if (command.action === "compose")
329
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) {
@@ -2,6 +2,10 @@ import type { TaskBoard } from "./board.js";
2
2
  import { type TaskDocument } from "./document.js";
3
3
  import { type TaskId } from "./identity.js";
4
4
  import { type TaskCompositionDiagnostic } from "./operations.js";
5
+ export declare function taskCompositionNamespaceHeader(markdown: string): Readonly<{
6
+ specified: boolean;
7
+ namespace?: readonly string[];
8
+ }>;
5
9
  export type TaskCompositionAlias = Readonly<{
6
10
  alias: string;
7
11
  taskId: TaskId;
@@ -2,6 +2,13 @@ import { serializeTaskDocument } from "./document.js";
2
2
  import { allocateLocalId, deriveLocalStem, formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
3
3
  import { parseTaskComposition } from "./compose-parser.js";
4
4
  import { advanceTaskTimestamp } from "./operations.js";
5
+ export function taskCompositionNamespaceHeader(markdown) {
6
+ const parsed = parseTaskComposition(markdown);
7
+ return {
8
+ specified: parsed.namespaceSpecified,
9
+ ...(parsed.namespace === undefined ? {} : { namespace: parsed.namespace }),
10
+ };
11
+ }
5
12
  function diagnostic(line, reason, token) {
6
13
  return { line, reason, token };
7
14
  }
@@ -25,6 +25,7 @@ export type ParsedNode = Readonly<{
25
25
  }>;
26
26
  export type ParsedComposition = Readonly<{
27
27
  namespace?: readonly string[];
28
+ namespaceSpecified: boolean;
28
29
  nodes: readonly ParsedNode[];
29
30
  diagnostics: readonly TaskCompositionDiagnostic[];
30
31
  }>;
@@ -32,8 +32,10 @@ function withoutFenceSeparator(value) {
32
32
  return value;
33
33
  }
34
34
  function parseNamespace(value) {
35
- if (value === "")
35
+ if (value === "/")
36
36
  return [];
37
+ if (value === "")
38
+ throw new Error("empty compose namespace; use ns=/ for root");
37
39
  return parseTaskId(`task/${value}/placeholder`).namespace;
38
40
  }
39
41
  function parseAssignment(text, line) {
@@ -174,5 +176,5 @@ export function parseTaskComposition(source) {
174
176
  replaceNode(nodes, current);
175
177
  }
176
178
  }
177
- return { ...(namespace === undefined ? {} : { namespace }), nodes, diagnostics };
179
+ return { ...(namespace === undefined ? {} : { namespace }), namespaceSpecified: sawNamespace, nodes, diagnostics };
178
180
  }
@@ -3,6 +3,7 @@ import { type TaskCompositionAlias, type TaskCompositionBodyPreview } from "./co
3
3
  import type { TaskId } from "./identity.js";
4
4
  import type { TaskRefusal, TaskRetry } from "./operations.js";
5
5
  export type { TaskCompositionAlias, TaskCompositionBodyPreview } from "./compose-language.js";
6
+ export { taskCompositionNamespaceHeader } from "./compose-language.js";
6
7
  export type TaskDocumentChange = Readonly<{
7
8
  taskId: TaskId;
8
9
  kind: "created" | "updated";
@@ -2,6 +2,7 @@ import { documentDiff } from "../markdown/diff.js";
2
2
  import { planTaskComposition, } from "./compose-language.js";
3
3
  import { serializeTaskDocument } from "./document.js";
4
4
  import { authorityPath, readBoard, replaceAuthority, withTaskLocks } from "./store.js";
5
+ export { taskCompositionNamespaceHeader } from "./compose-language.js";
5
6
  function refusal(diagnostics) {
6
7
  return { kind: "refused", refusal: { kind: "invalid-composition", diagnostics } };
7
8
  }
@@ -46,7 +47,7 @@ function recoveryDraft(namespace, remaining) {
46
47
  if (task.kind === "new" && task.alias !== undefined)
47
48
  aliases.set(task.after.id, task.alias);
48
49
  }
49
- const lines = [`ns=${namespace.join("/")}`];
50
+ const lines = [`ns=${namespace.length === 0 ? "/" : namespace.join("/")}`];
50
51
  for (const task of remaining)
51
52
  lines.push("", ...taskDraft(task, aliases));
52
53
  return `${lines.join("\n")}\n`;
@@ -5,11 +5,17 @@ export type NamespaceContextRead = readonly string[] | "absent" | Readonly<{
5
5
  export type NamespaceContextInput = Readonly<{
6
6
  directory: string;
7
7
  boundary: string;
8
+ managed?: boolean;
9
+ }>;
10
+ export type NamespaceContextSource = "default-root" | "contract-installed" | "local-override";
11
+ export type ResolvedNamespaceContext = Readonly<{
12
+ namespace: readonly string[];
13
+ source: NamespaceContextSource;
8
14
  }>;
9
15
  export declare function readNamespaceContext(coordinates: NamespaceContextInput): Promise<NamespaceContextRead>;
10
16
  export declare function installNamespaceContext(root: string, segments: readonly string[]): Promise<void>;
11
17
  export declare function repairNamespaceContext(root: string, segments: readonly string[]): Promise<"kept" | "installed">;
12
- export declare function resolveTaskNamespaceContext(input: NamespaceContextInput): Promise<readonly string[] | Readonly<{
18
+ export declare function resolveTaskNamespaceContext(input: NamespaceContextInput): Promise<ResolvedNamespaceContext | Readonly<{
13
19
  kind: "invalid-namespace-context";
14
20
  path: string;
15
21
  }>>;
@@ -40,6 +40,12 @@ async function readAt(root) {
40
40
  const segments = content === "" ? [] : content.split("/");
41
41
  return validNamespaceSegments(segments) ? segments : { kind: "malformed", path: currentPath(root) };
42
42
  }
43
+ async function readAtWithPath(root) {
44
+ const selected = await readAt(root);
45
+ if (selected === "absent" || (typeof selected === "object" && "kind" in selected))
46
+ return selected;
47
+ return { value: selected, path: currentPath(root) };
48
+ }
43
49
  export async function readNamespaceContext(coordinates) {
44
50
  let current = coordinates.directory;
45
51
  for (;;) {
@@ -75,12 +81,25 @@ export async function repairNamespaceContext(root, segments) {
75
81
  return "installed";
76
82
  }
77
83
  export async function resolveTaskNamespaceContext(input) {
78
- const selected = await readNamespaceContext(input);
79
- return selected === "absent"
80
- ? []
81
- : typeof selected === "object" && "kind" in selected
82
- ? { kind: "invalid-namespace-context", path: selected.path }
83
- : selected;
84
+ let current = input.directory;
85
+ for (;;) {
86
+ const selected = await readAtWithPath(current);
87
+ if (selected !== "absent") {
88
+ if (typeof selected === "object" && "kind" in selected)
89
+ return { kind: "invalid-namespace-context", path: selected.path };
90
+ return {
91
+ namespace: "value" in selected ? selected.value : selected,
92
+ source: current === input.boundary && input.managed === true ? "contract-installed" : "local-override",
93
+ };
94
+ }
95
+ if (current === input.boundary)
96
+ return { namespace: [], source: "default-root" };
97
+ const resolvedParent = dirname(resolve(current));
98
+ if (resolvedParent === current || !resolvedParent.startsWith(`${input.boundary}/`)) {
99
+ return { namespace: [], source: "default-root" };
100
+ }
101
+ current = resolvedParent;
102
+ }
84
103
  }
85
104
  export async function writeTaskNamespaceContext(directory, value) {
86
105
  await installNamespaceContext(directory, value);