@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,305 +1,144 @@
1
1
  import { documentDiff } from "../markdown/diff.js";
2
- import { relationProblem } from "./board.js";
2
+ import { planTaskComposition, } from "./compose-language.js";
3
3
  import { serializeTaskDocument } from "./document.js";
4
- import { allocateLocalId, deriveLocalStem, formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
5
4
  import { authorityPath, readBoard, replaceAuthority, withTaskLocks } from "./store.js";
6
- import { advanceTaskTimestamp } from "./operations.js";
7
- function failure(diagnostic) {
8
- return { kind: "invalid-composition", diagnostic };
5
+ export { taskCompositionNamespaceHeader } from "./compose-language.js";
6
+ function refusal(diagnostics) {
7
+ return { kind: "refused", refusal: { kind: "invalid-composition", diagnostics } };
9
8
  }
10
- function parseNamespace(value) {
11
- if (value === "")
12
- return [];
13
- const coordinate = parseTaskId(`task/${value}/placeholder`);
14
- return coordinate.namespace;
15
- }
16
- function assignment(token) {
17
- const matched = /^(parent|needs|supersedes|relates|pri|body)(\+=|=)(.*)$/u.exec(token);
18
- if (matched === null)
19
- return null;
20
- const field = matched[1], append = matched[2] === "+=", value = matched[3];
21
- if (append && (field === "parent" || field === "pri" || field === "body"))
22
- throw new TypeError(`${field} does not accept +=`);
23
- if (field === "body" && (append || value !== ""))
24
- throw new TypeError("body accepts only bare body=");
25
- return { field, append, value };
26
- }
27
- function parseNode(text, depth, index) {
28
- const tokens = text.split(/ +/u);
29
- if (text.startsWith("+ ")) {
30
- const values = tokens.slice(1);
31
- const at = values.findIndex((token) => assignment(token) !== null);
32
- const title = values.slice(0, at < 0 ? values.length : at).join(" ");
33
- if (title.trim().length === 0)
34
- throw new TypeError("compose + node requires a title");
35
- return {
36
- index,
37
- depth,
38
- kind: "new",
39
- title,
40
- assignments: (at < 0 ? [] : values.slice(at)).map((token) => assignment(token) ??
41
- (() => {
42
- throw new TypeError(`invalid assignment token: ${token}`);
43
- })()),
44
- };
45
- }
46
- const [rawId, ...rest] = tokens;
47
- if (rawId === undefined || !rawId.startsWith("@task/"))
48
- throw new TypeError("compose node must begin with + or @task/");
49
- const id = `task/${rawId.slice(1).slice(5)}`;
50
- parseTaskId(id);
51
- return {
52
- index,
53
- depth,
54
- kind: "existing",
55
- id,
56
- assignments: rest.map((token) => assignment(token) ??
57
- (() => {
58
- throw new TypeError(`invalid assignment token: ${token}`);
59
- })()),
60
- };
9
+ function currentTimestamp() {
10
+ return new Date().toISOString();
61
11
  }
62
- function parseSketch(markdown) {
63
- try {
64
- const lines = markdown.replace(/\r\n?/gu, "\n").split("\n");
65
- let namespace;
66
- let start = 0;
67
- if (lines[0]?.startsWith("ns=")) {
68
- namespace = parseNamespace(lines[0].slice(3));
69
- start = 1;
70
- }
71
- const mutable = [];
72
- for (let line = start; line < lines.length; line += 1) {
73
- const raw = lines[line];
74
- if (line === lines.length - 1 && raw === "")
75
- continue;
76
- const leading = /^(?:\t| )*/u.exec(raw)[0].replace(/\t/gu, " ");
77
- const text = raw.slice(/^(?:\t| )*/u.exec(raw)[0].length);
78
- const nodeLine = text.startsWith("+ ") || text.startsWith("@task/");
79
- if (nodeLine) {
80
- if (leading.length % 2 !== 0)
81
- throw new TypeError(`compose line ${line + 1} has invalid indentation`);
82
- const depth = leading.length / 2;
83
- if (depth > 0 && !mutable.some((entry) => entry.node.depth === depth - 1))
84
- throw new TypeError(`compose line ${line + 1} skips a parent depth`);
85
- mutable.push({ node: parseNode(text, depth, mutable.length), body: [] });
86
- continue;
87
- }
88
- const current = mutable.at(-1);
89
- if (current === undefined) {
90
- if (text.trim() === "")
91
- continue;
92
- throw new TypeError(`compose line ${line + 1} has body before a node`);
93
- }
94
- current.body.push(text.startsWith("\\") ? text.slice(1) : text);
95
- }
96
- const nodes = mutable.map(({ node, body }) => ({
97
- ...node,
98
- ...(body.length === 0 ? {} : { body: body.join("\n") }),
99
- }));
100
- return { ...(namespace === undefined ? {} : { namespace }), nodes };
12
+ function facts(plan) {
13
+ return { aliases: plan.aliases, admissionOrder: plan.admissionOrder };
14
+ }
15
+ function reference(id, remainingAliases) {
16
+ const alias = remainingAliases.get(id);
17
+ return alias === undefined ? `@${id}` : `^${alias}`;
18
+ }
19
+ function bodyToken(body) {
20
+ const lines = new Set(body.split(/\r\n|\n|\r/u));
21
+ for (let suffix = 0; Number.isSafeInteger(suffix); suffix += 1) {
22
+ const token = suffix === 0 ? "END_BODY" : `END_BODY_${suffix}`;
23
+ if (token.length > 32)
24
+ break;
25
+ if (!lines.has(token))
26
+ return token;
101
27
  }
102
- catch (error) {
103
- return failure(error instanceof Error ? error.message : String(error));
28
+ throw new Error("compose recovery body token space exhausted");
29
+ }
30
+ function taskDraft(task, remainingAliases) {
31
+ const document = task.after;
32
+ const lines = [task.kind === "new" ? `+ ${document.title}` : `@${document.id}`];
33
+ if (task.kind === "new" && task.alias !== undefined)
34
+ lines.push(`as = ${task.alias}`);
35
+ lines.push(`pri = ${document.priority}`, `needs = ${document.needs.map((id) => reference(id, remainingAliases)).join(", ")}`, `parent = ${document.parent === null ? "" : reference(document.parent, remainingAliases)}`, `supersedes = ${document.supersedes.map((id) => reference(id, remainingAliases)).join(", ")}`, `relates = ${document.relates.map((id) => reference(id, remainingAliases)).join(", ")}`);
36
+ if (document.body === "")
37
+ lines.push("body =");
38
+ else {
39
+ const token = bodyToken(document.body);
40
+ lines.push(`body <<${token}`, document.body, token);
104
41
  }
42
+ return lines;
105
43
  }
106
- function ids(value) {
107
- if (value === "")
108
- return [];
109
- const parsed = value.split(",").map((raw) => {
110
- if (!raw.startsWith("@task/"))
111
- throw new TypeError("relation values must be @TaskId");
112
- const id = raw.slice(1);
113
- parseTaskId(id);
114
- return id;
115
- });
116
- if (new Set(parsed).size !== parsed.length)
117
- throw new TypeError("relation values must not contain duplicates");
118
- return parsed;
119
- }
120
- function scalarId(value) {
121
- const values = ids(value);
122
- if (values.length > 1)
123
- throw new TypeError("parent accepts at most one TaskId");
124
- return values[0] ?? null;
125
- }
126
- function changed(current, value, append) {
127
- return append ? [...current, ...value.filter((id) => !current.includes(id))] : value;
128
- }
129
- function applyAssignments(document, assignments, body) {
130
- let next = document;
131
- const seen = new Set();
132
- for (const item of assignments) {
133
- const mode = `${item.field}:${item.append}`;
134
- if (seen.has(mode) || [...seen].some((value) => value.startsWith(`${item.field}:`) && value !== mode))
135
- throw new TypeError(`duplicate compose assignment: ${item.field}`);
136
- seen.add(mode);
137
- if (item.field === "pri") {
138
- const priority = Number(item.value);
139
- if (!Number.isInteger(priority) || priority < 0 || priority > 3)
140
- throw new TypeError("pri must be 0..3");
141
- next = { ...next, priority: priority };
142
- }
143
- else if (item.field === "parent")
144
- next = { ...next, parent: scalarId(item.value) };
145
- else if (item.field === "body")
146
- next = { ...next, body: "" };
147
- else
148
- next = { ...next, [item.field]: changed(next[item.field], ids(item.value), item.append) };
44
+ function recoveryDraft(namespace, remaining) {
45
+ const aliases = new Map();
46
+ for (const task of remaining) {
47
+ if (task.kind === "new" && task.alias !== undefined)
48
+ aliases.set(task.after.id, task.alias);
149
49
  }
150
- return body === undefined ? next : { ...next, body };
151
- }
152
- function currentTimestamp() {
153
- return new Date().toISOString();
154
- }
155
- function createdTask(id, title, at, actor) {
156
- return {
157
- id,
158
- title,
159
- state: "open",
160
- priority: 2,
161
- needs: [],
162
- parent: null,
163
- supersedes: [],
164
- relates: [],
165
- note: "",
166
- ...(actor === undefined ? {} : { createdBy: actor }),
167
- createdAt: at,
168
- updatedAt: at,
169
- body: "",
170
- };
50
+ const lines = [`ns=${namespace.length === 0 ? "/" : namespace.join("/")}`];
51
+ for (const task of remaining)
52
+ lines.push("", ...taskDraft(task, aliases));
53
+ return `${lines.join("\n")}\n`;
171
54
  }
172
- function plan(sketch, board, defaultNamespace, at, actor) {
173
- try {
174
- const namespace = sketch.namespace ?? defaultNamespace;
175
- const occupied = new Set([...board.tasks.values()].flatMap((task) => {
176
- const coordinate = parseTaskId(task.id);
177
- return sameNamespace(coordinate.namespace, namespace) ? [coordinate.localId] : [];
178
- }));
179
- const allocations = new Map();
180
- for (const node of sketch.nodes)
181
- if (node.kind === "new") {
182
- const localId = allocateLocalId(deriveLocalStem(node.title), occupied);
183
- occupied.add(localId);
184
- const coordinate = { namespace, localId };
185
- allocations.set(node.index, createdTask(formatTaskId(coordinate), node.title, at, actor));
186
- }
187
- const all = new Map(board.tasks);
188
- for (const allocated of allocations.values())
189
- all.set(allocated.id, allocated);
190
- const byDepth = [], addressed = new Set();
191
- const planned = [];
192
- for (const node of sketch.nodes) {
193
- const before = node.kind === "new" ? null : (board.tasks.get(node.id) ?? null);
194
- let current = node.kind === "new" ? allocations.get(node.index) : all.get(node.id);
195
- if (current === undefined)
196
- throw new TypeError(`compose task does not exist: ${node.id}`);
197
- if (addressed.has(current.id))
198
- throw new TypeError(`compose addresses ${current.id} more than once`);
199
- addressed.add(current.id);
200
- if (node.depth > 0) {
201
- const parent = byDepth[node.depth - 1];
202
- if (parent === undefined)
203
- throw new TypeError("compose parent is unavailable");
204
- if (node.assignments.some((item) => item.field === "parent"))
205
- throw new TypeError("indented node cannot also assign parent");
206
- current = { ...current, parent };
207
- }
208
- current = applyAssignments(current, node.assignments, node.body);
209
- if (before !== null &&
210
- !Buffer.from(serializeTaskDocument(current)).equals(Buffer.from(serializeTaskDocument(before)))) {
211
- current = {
212
- ...current,
213
- updatedAt: advanceTaskTimestamp(before.updatedAt, at),
214
- };
215
- }
216
- byDepth[node.depth] = current.id;
217
- byDepth.length = node.depth + 1;
218
- all.set(current.id, current);
219
- planned.push({ node, before, after: current });
220
- }
221
- for (const item of planned) {
222
- const problem = relationProblem({ tasks: all }, item.before, item.after);
223
- if (problem !== null)
224
- return failure(problem);
55
+ function plannedResult(plan) {
56
+ return { kind: "planned", ...facts(plan), bodies: plan.bodies };
57
+ }
58
+ function planAgainst(markdown, board, namespace, at, actor) {
59
+ return planTaskComposition({ markdown, board, namespace, at, ...(actor === undefined ? {} : { actor }) });
60
+ }
61
+ async function admitPlan(world, snapshot, plan, signal) {
62
+ const changes = [];
63
+ for (let index = 0; index < plan.tasks.length; index += 1) {
64
+ signal?.throwIfAborted();
65
+ const item = plan.tasks[index];
66
+ const beforeBytes = item.before === null ? null : (snapshot.bytes.get(item.after.id) ?? null);
67
+ const afterBytes = serializeTaskDocument(item.after);
68
+ const before = beforeBytes === null ? "" : Buffer.from(beforeBytes).toString("utf8");
69
+ const after = Buffer.from(afterBytes).toString("utf8");
70
+ const replaced = await replaceAuthority({
71
+ path: authorityPath(world, item.after.id),
72
+ expected: beforeBytes,
73
+ next: afterBytes,
74
+ });
75
+ if (replaced !== "replaced") {
76
+ return {
77
+ kind: "incomplete",
78
+ ...facts(plan),
79
+ documentChanges: changes,
80
+ stopped: { kind: "retry", reason: "concurrent-modification" },
81
+ draft: recoveryDraft(plan.namespace, plan.tasks.slice(index)),
82
+ };
225
83
  }
226
- return planned;
84
+ const label = `${item.after.id}.md`;
85
+ changes.push({
86
+ taskId: item.after.id,
87
+ kind: item.before === null ? "created" : "updated",
88
+ documentDiff: documentDiff(label, label, before, after),
89
+ });
227
90
  }
228
- catch (error) {
229
- return failure(error instanceof Error ? error.message : String(error));
230
- }
231
- }
232
- function ref(id) {
233
- return `@${id}`;
234
- }
235
- function bodyLines(body) {
236
- return body.split("\n").map((line) => (/^[\\]|^\+ |^@task\//u.test(line) ? `\\${line}` : line));
91
+ return { kind: "accepted", ...facts(plan), documentChanges: changes };
92
+ }
93
+ async function composeUnderLocks(input) {
94
+ const snapshot = await readBoard(input.world);
95
+ const planned = planAgainst(input.markdown, snapshot.board, input.namespace, input.at, input.actor);
96
+ if (planned.kind === "refused")
97
+ return refusal(planned.diagnostics);
98
+ return await withTaskLocks({
99
+ world: input.world,
100
+ allocation: false,
101
+ ids: planned.plan.admissionOrder,
102
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
103
+ }, async () => {
104
+ const fresh = await readBoard(input.world);
105
+ const replanned = planAgainst(input.markdown, fresh.board, input.namespace, input.at, input.actor);
106
+ if (replanned.kind === "refused")
107
+ return refusal(replanned.diagnostics);
108
+ return await admitPlan(input.world, fresh, replanned.plan, input.signal);
109
+ });
237
110
  }
238
- function draft(namespace, remaining) {
239
- const lines = [`ns=${namespace.join("/")}`];
240
- for (const item of remaining) {
241
- const task = item.after;
242
- const assignments = [
243
- `pri=${task.priority}`,
244
- `needs=${task.needs.map(ref).join(",")}`,
245
- `parent=${task.parent === null ? "" : ref(task.parent)}`,
246
- `supersedes=${task.supersedes.map(ref).join(",")}`,
247
- `relates=${task.relates.map(ref).join(",")}`,
248
- ];
249
- lines.push(`${item.node.kind === "new" ? `+ ${task.title}` : ref(task.id)} ${assignments.join(" ")}${task.body === "" ? " body=" : ""}`);
250
- if (task.body !== "")
251
- lines.push(...bodyLines(task.body));
252
- }
253
- return `${lines.join("\n")}\n`;
111
+ function composeInput(input) {
112
+ return input;
254
113
  }
255
- export async function composeTasks(world, markdown, signal, actor, defaultNamespace) {
256
- const sketch = parseSketch(markdown);
257
- if ("kind" in sketch)
258
- return { kind: "refused", refusal: sketch };
114
+ export async function composeTasks(input) {
115
+ const { world, markdown, signal, actor, defaultNamespace = [], planOnly = false } = input;
259
116
  const at = currentTimestamp();
260
- const namespace = sketch.namespace ?? defaultNamespace ?? [];
261
117
  const initial = await readBoard(world);
262
- const planned = plan(sketch, initial.board, namespace, at, actor);
263
- if ("kind" in planned)
264
- return { kind: "refused", refusal: planned };
265
- const ordered = [...planned].sort((a, b) => Buffer.compare(Buffer.from(a.after.id), Buffer.from(b.after.id)));
266
- const allocation = sketch.nodes.some((node) => node.kind === "new");
267
- const result = await withTaskLocks({ world, allocation, ids: ordered.map((item) => item.after.id), ...(signal === undefined ? {} : { signal }) }, async () => {
268
- const fresh = await readBoard(world);
269
- const replanned = plan(sketch, fresh.board, namespace, at, actor);
270
- if ("kind" in replanned)
271
- return { kind: "refused", refusal: replanned };
272
- const queue = [...replanned].sort((a, b) => Buffer.compare(Buffer.from(a.after.id), Buffer.from(b.after.id))), changes = [];
273
- for (let index = 0; index < queue.length; index += 1) {
274
- signal?.throwIfAborted();
275
- const item = queue[index], path = authorityPath(world, item.after.id);
276
- const beforeBytes = item.before === null ? null : (fresh.bytes.get(item.after.id) ?? null);
277
- const afterBytes = serializeTaskDocument(item.after);
278
- const before = beforeBytes === null ? "" : Buffer.from(beforeBytes).toString("utf8"), after = Buffer.from(afterBytes).toString("utf8");
279
- if (before === after)
280
- continue;
281
- if ((await replaceAuthority({ path, expected: beforeBytes, next: afterBytes })) !== "replaced")
282
- return {
283
- kind: "incomplete",
284
- documentChanges: changes,
285
- stopped: { kind: "retry", reason: "concurrent-modification" },
286
- draft: draft(namespace, queue.slice(index)),
287
- };
288
- const label = `${item.after.id}.md`;
289
- changes.push({
290
- taskId: item.after.id,
291
- kind: item.before === null ? "created" : "updated",
292
- documentDiff: documentDiff(label, label, before, after),
293
- });
294
- }
295
- return { kind: "accepted", documentChanges: changes };
118
+ const initialPlan = planAgainst(markdown, initial.board, defaultNamespace, at, actor);
119
+ if (initialPlan.kind === "refused")
120
+ return refusal(initialPlan.diagnostics);
121
+ if (planOnly)
122
+ return plannedResult(initialPlan.plan);
123
+ const request = composeInput({
124
+ world,
125
+ markdown,
126
+ namespace: defaultNamespace,
127
+ at,
128
+ ...(actor === undefined ? {} : { actor }),
129
+ ...(signal === undefined ? {} : { signal }),
296
130
  });
297
- return result === "busy"
298
- ? {
299
- kind: "incomplete",
300
- documentChanges: [],
301
- stopped: { kind: "retry", reason: "busy" },
302
- draft: draft(namespace, ordered),
303
- }
304
- : result;
131
+ const allocation = initialPlan.plan.tasks.some((task) => task.kind === "new");
132
+ const admitted = allocation
133
+ ? await withTaskLocks({ world, allocation: true, ids: [], ...(signal === undefined ? {} : { signal }) }, async () => await composeUnderLocks(request))
134
+ : await composeUnderLocks(request);
135
+ if (admitted !== "busy")
136
+ return admitted;
137
+ return {
138
+ kind: "incomplete",
139
+ ...facts(initialPlan.plan),
140
+ documentChanges: [],
141
+ stopped: { kind: "retry", reason: "busy" },
142
+ draft: recoveryDraft(initialPlan.plan.namespace, initialPlan.plan.tasks),
143
+ };
305
144
  }
@@ -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);
@@ -1,10 +1,10 @@
1
1
  import type { WorldRoot } from "../world.js";
2
2
  export type { WorldRoot } from "../world.js";
3
3
  import { type BlockedTaskRow, type TaskDetailFacts, type TaskDoctorIssue, type TaskRef, type TaskRow, type TaskTreeNode } from "./board.js";
4
- import { type TaskCompositionResult } from "./compose.js";
4
+ import { type TaskCompositionAlias, type TaskCompositionBodyPreview, type TaskCompositionResult } from "./compose.js";
5
5
  import { TaskAuthorityCorruptionError, type TaskPriority, type TaskState } from "./document.js";
6
6
  import type { TaskId } from "./identity.js";
7
- import { type AddTaskDocumentInput, type AddTaskInput, type TaskBatchResult, type TaskMutationResult, type TaskOutcome, type TaskRefusal, type TaskRetry, type TaskUpdateResult, type TaskView, type UpdateTaskInput } from "./operations.js";
7
+ import { type AddTaskDocumentInput, type AddTaskInput, type TaskBatchResult, type TaskCompositionDiagnostic, type TaskMutationResult, type TaskOutcome, type TaskRefusal, type TaskRetry, type TaskUpdateResult, type TaskView, type UpdateTaskInput } from "./operations.js";
8
8
  import { TASK_RELATION_PREDICATE_FIELDS, type TaskPage, type TaskQueryExpression, type TaskQueryPredicate, type TaskQueryRow, type TaskQuerySort, type TaskRelationPredicateField } from "./query.js";
9
9
  export type TaskDetail = Omit<TaskDetailFacts, "task"> & Readonly<{
10
10
  task: TaskView;
@@ -12,12 +12,17 @@ export type TaskDetail = Omit<TaskDetailFacts, "task"> & Readonly<{
12
12
  export type TaskList = TaskOutcome<TaskPage<TaskRow>>;
13
13
  export type BlockedTaskList = TaskOutcome<TaskPage<BlockedTaskRow>>;
14
14
  export type TaskQueryResult = TaskOutcome<TaskPage<TaskQueryRow>>;
15
- export type TaskNamespaceResult = TaskOutcome<readonly string[]>;
15
+ export type TaskContextSource = "default-root" | "contract-installed" | "local-override";
16
+ export type ResolvedNamespaceContext = Readonly<{
17
+ namespace: readonly string[];
18
+ source: TaskContextSource;
19
+ }>;
20
+ export type TaskContextResult = TaskOutcome<ResolvedNamespaceContext>;
16
21
  export type TaskDoctorReport = Readonly<{
17
22
  issues: readonly TaskDoctorIssue[];
18
23
  }>;
19
24
  export type TaskDecompositionTree = TaskOutcome<TaskTreeNode>;
20
- export type { AddTaskDocumentInput, AddTaskInput, BlockedTaskRow, TaskBatchResult, TaskCompositionResult, TaskDoctorIssue, TaskId, TaskMutationResult, TaskOutcome, TaskPriority, TaskRef, TaskRefusal, TaskRetry, TaskRow, TaskState, TaskTreeNode, TaskUpdateResult, TaskView, UpdateTaskInput, TaskPage, TaskQueryExpression, TaskQueryPredicate, TaskQueryRow, TaskQuerySort, TaskRelationPredicateField, };
25
+ export type { AddTaskDocumentInput, AddTaskInput, BlockedTaskRow, TaskBatchResult, TaskCompositionDiagnostic, TaskCompositionAlias, TaskCompositionBodyPreview, TaskCompositionResult, TaskDoctorIssue, TaskId, TaskMutationResult, TaskOutcome, TaskPriority, TaskRef, TaskRefusal, TaskRetry, TaskRow, TaskState, TaskTreeNode, TaskUpdateResult, TaskView, UpdateTaskInput, TaskPage, TaskQueryExpression, TaskQueryPredicate, TaskQueryRow, TaskQuerySort, TaskRelationPredicateField, };
21
26
  export { TaskAuthorityCorruptionError, TASK_RELATION_PREDICATE_FIELDS };
22
27
  declare class TaskHandle {
23
28
  readonly id: TaskId;
@@ -95,6 +100,7 @@ declare class TasksHandle {
95
100
  namespace?: readonly string[];
96
101
  actor?: string;
97
102
  signal?: AbortSignal;
103
+ plan?: boolean;
98
104
  }>): Promise<TaskCompositionResult>;
99
105
  }
100
106
  export type Tasks = TasksHandle;
@@ -1,5 +1,5 @@
1
1
  import { buildTree, createTaskRelations, diagnoseBoard, projectDetailFacts, } from "./board.js";
2
- import { composeTasks } from "./compose.js";
2
+ import { composeTasks, } from "./compose.js";
3
3
  import { TaskAuthorityCorruptionError } from "./document.js";
4
4
  import { addTask, addTaskDocument, batchTasks, blockedTasks, lifecycleTask, listTasks, queryTasks, readyTasks, taskView, updateTask, } from "./operations.js";
5
5
  import { readBoard } from "./store.js";
@@ -149,12 +149,23 @@ class TasksHandle {
149
149
  }
150
150
  compose(input) {
151
151
  const v = record(input, "compose input");
152
- closed(v, ["markdown", "namespace", "actor", "signal"], "compose input");
152
+ closed(v, ["markdown", "namespace", "actor", "signal", "plan"], "compose input");
153
153
  const markdown = text(v.markdown, "markdown");
154
154
  if (markdown === undefined)
155
155
  throw new TypeError("markdown is required");
156
+ if (v.plan !== undefined && typeof v.plan !== "boolean")
157
+ throw new TypeError("plan must be a boolean");
156
158
  const selected = namespace(v.namespace);
157
- return composeTasks(this.world, markdown, signal(v.signal), actor(v.actor), selected);
159
+ const selectedSignal = signal(v.signal);
160
+ const selectedActor = actor(v.actor);
161
+ return composeTasks({
162
+ world: this.world,
163
+ markdown,
164
+ ...(selectedSignal === undefined ? {} : { signal: selectedSignal }),
165
+ ...(selectedActor === undefined ? {} : { actor: selectedActor }),
166
+ ...(selected === undefined ? {} : { defaultNamespace: selected }),
167
+ planOnly: v.plan === true,
168
+ });
158
169
  }
159
170
  }
160
171
  export const Tasks = Object.freeze({
@@ -9,6 +9,11 @@ export type TaskView = Readonly<TaskDocument & {
9
9
  export declare function nukeTask(world: WorldRoot, options?: Readonly<{
10
10
  timeoutMs?: number;
11
11
  }>): Promise<void>;
12
+ export type TaskCompositionDiagnostic = Readonly<{
13
+ line: number;
14
+ reason: string;
15
+ token: string;
16
+ }>;
12
17
  export type TaskRefusal = Readonly<{
13
18
  kind: "task-missing";
14
19
  taskId: TaskId;
@@ -30,7 +35,7 @@ export type TaskRefusal = Readonly<{
30
35
  declaringTask: TaskId;
31
36
  }> | Readonly<{
32
37
  kind: "invalid-composition";
33
- diagnostic: string;
38
+ diagnostics: readonly TaskCompositionDiagnostic[];
34
39
  }>;
35
40
  export type TaskRetry = "busy" | "concurrent-modification";
36
41
  export type TaskOutcome<A> = Readonly<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "4.5.5",
3
+ "version": "4.5.7",
4
4
  "files": [
5
5
  "build"
6
6
  ],
@@ -64,7 +64,7 @@
64
64
  "dependencies": {
65
65
  "@agentclientprotocol/sdk": "^1.3.0",
66
66
  "@anthropic-ai/claude-agent-sdk": "^0.3.226",
67
- "@astrosheep/square": "0.3.24",
67
+ "@astrosheep/square": "0.3.30",
68
68
  "@earendil-works/pi-coding-agent": "0.80.10",
69
69
  "@opencode-ai/sdk": "1.18.3",
70
70
  "cross-spawn": "^7.0.6",