@astrosheep/keiyaku 4.5.4 → 4.5.6

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.
@@ -70,30 +70,26 @@ Relations are explicit facts: `needs` orders work, `parent` groups it,
70
70
  ## Batch Create Or Modify
71
71
 
72
72
  Use `task compose -` to create and modify multiple Tasks in one planning
73
- document:
73
+ document. Its input grammar is documented by the Task CLI owner:
74
74
 
75
75
  ```bash
76
76
  keiyaku task compose - <<'EOF'
77
77
  ns=feature
78
- + Parent pri=0
78
+
79
+ + Parent
80
+ as = parent
81
+ pri = 0
82
+ body <<BODY
79
83
  Parent body.
80
- + Child needs=@task/existing
81
- @task/existing pri=1 relates+=@task/other
82
- Replacement body.
84
+ BODY
85
+
86
+ + Child
87
+ parent = ^parent
88
+ needs = @task/existing
83
89
  EOF
84
90
  ```
85
91
 
86
- - Optional first line `ns=<segment/...>` selects the allocation namespace;
87
- `ns=` selects root.
88
- - `+ <title>` creates a Task. `@task/<id>` modifies an existing Task.
89
- - Two spaces of indentation assign the preceding shallower Task as `parent`.
90
- - Inline assignments are `pri=`, `parent=`, `needs=`, `supersedes=`, and
91
- `relates=`. Relation values are comma-separated `@task/...` references.
92
- - `=` replaces or clears a field. `+=` appends only to `needs`, `supersedes`,
93
- or `relates`.
94
- - Prose after a node replaces its body; bare `body=` clears it. Prefix body
95
- lines beginning with `+ `, `@task/`, or `\` with one `\`.
96
-
97
- Compose does not change Task lifecycle state. Each changed Task is admitted
98
- independently; an incomplete result returns a canonical draft for the remaining
99
- batch.
92
+ Use `--plan` to inspect aliases, admission order, and body byte previews without
93
+ writing. Compose does not change Task lifecycle state. Each changed Task is
94
+ admitted independently; an incomplete result returns a reusable draft for the
95
+ remaining batch.
@@ -47,6 +47,7 @@ export function renderInstallHelp() {
47
47
  const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
48
48
  const ASSET_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../../integrations/marketplace");
49
49
  const PLUGIN_ROOT = resolve(ASSET_ROOT, "plugins/keiyaku");
50
+ const PI_PACKAGE_SOURCE = "npm:@astrosheep/keiyaku";
50
51
  function recipes(harness) {
51
52
  switch (harness) {
52
53
  case "codex":
@@ -63,7 +64,7 @@ function recipes(harness) {
63
64
  case "opencode":
64
65
  return [["opencode", "plugin", PLUGIN_ROOT, "--global", "--force"]];
65
66
  case "pi":
66
- return [["pi", "install", PLUGIN_ROOT]];
67
+ return [["pi", "install", PI_PACKAGE_SOURCE]];
67
68
  }
68
69
  }
69
70
  function diagnostic(argv, outcome) {
@@ -294,15 +294,8 @@ function missingWorld(command) {
294
294
  });
295
295
  if (isWorldObservation(command))
296
296
  return { kind: "absent" };
297
- if (command.action === "compose" && command.flags.plan === true) {
298
- return {
299
- kind: "refused",
300
- refusal: {
301
- kind: "invalid-composition",
302
- diagnostics: [{ line: 1, reason: "task world has no valid planning coordinate", token: "--plan" }],
303
- },
304
- };
305
- }
297
+ if (command.action === "compose" && command.flags.plan === true)
298
+ return { kind: "absent" };
306
299
  if (command.action === "namespace")
307
300
  return { kind: "accepted", value: [] };
308
301
  if (command.action === "hold" || command.action === "done" || command.action === "drop") {
@@ -31,17 +31,17 @@ function createdTask(id, title, at, actor) {
31
31
  function aliasFor(node) {
32
32
  return node.assignments.find((item) => item.field === "as");
33
33
  }
34
- function allocateNodes(parsed, board, namespace, at, actor, diagnostics) {
35
- const ids = occupied(board, namespace);
34
+ function allocateNodes(input, diagnostics) {
35
+ const ids = occupied(input.board, input.namespace);
36
36
  const allocations = new Map();
37
- for (const node of parsed.nodes) {
37
+ for (const node of input.parsed.nodes) {
38
38
  if (node.kind !== "new")
39
39
  continue;
40
40
  try {
41
41
  const localId = allocateLocalId(deriveLocalStem(node.title), ids);
42
42
  ids.add(localId);
43
- const id = formatTaskId({ namespace, localId });
44
- allocations.set(node.index, createdTask(id, node.title, at, actor));
43
+ const id = formatTaskId({ namespace: input.namespace, localId });
44
+ allocations.set(node.index, createdTask(id, node.title, input.at, input.actor));
45
45
  }
46
46
  catch (error) {
47
47
  diagnostics.push(diagnostic(node.line, error instanceof Error ? error.message : String(error), node.title));
@@ -257,7 +257,14 @@ export function planTaskComposition(input) {
257
257
  const parsed = parseTaskComposition(input.markdown);
258
258
  const diagnostics = [...parsed.diagnostics];
259
259
  const namespace = parsed.namespace ?? input.namespace;
260
- const allocations = allocateNodes(parsed, input.board, namespace, input.at, input.actor, diagnostics);
260
+ const allocationInput = {
261
+ parsed,
262
+ board: input.board,
263
+ namespace,
264
+ at: input.at,
265
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
266
+ };
267
+ const allocations = allocateNodes(allocationInput, diagnostics);
261
268
  const aliases = collectAliases(parsed.nodes, allocations, diagnostics);
262
269
  const addressed = new Set();
263
270
  const tasks = [];
@@ -75,6 +75,41 @@ function withBody(node, body) {
75
75
  function replaceNode(nodes, node) {
76
76
  nodes[node.index] = node;
77
77
  }
78
+ function parseBodyAssignment(input, diagnostics) {
79
+ const fence = /^body\s+<<(.+)$/u.exec(input.text);
80
+ if (fence !== null) {
81
+ const token = fence[1];
82
+ if (!/^[A-Z][A-Z0-9_]{2,31}$/u.test(token)) {
83
+ diagnostics.push(diagnostic(input.line.number, "body token must match [A-Z][A-Z0-9_]* with length 3..32", token));
84
+ return { handled: true };
85
+ }
86
+ const close = input.lines.findIndex((candidate, candidateIndex) => candidateIndex > input.index && candidate.text === token);
87
+ if (close < 0) {
88
+ diagnostics.push(diagnostic(input.line.number, "body fence is not closed", token));
89
+ return { handled: true, stop: true };
90
+ }
91
+ const body = withoutFenceSeparator(input.source.slice(input.line.end, input.lines[close].start));
92
+ if (input.current.body !== undefined) {
93
+ diagnostics.push(diagnostic(input.line.number, "body is assigned more than once", input.text));
94
+ return { handled: true, close };
95
+ }
96
+ return {
97
+ handled: true,
98
+ node: withBody(input.current, { kind: "replace", value: body, line: input.line.number, token }),
99
+ close,
100
+ };
101
+ }
102
+ if (!/^body\s*=\s*$/u.test(input.text))
103
+ return { handled: false };
104
+ if (input.current.body !== undefined) {
105
+ diagnostics.push(diagnostic(input.line.number, "body is assigned more than once", input.text));
106
+ return { handled: true };
107
+ }
108
+ return {
109
+ handled: true,
110
+ node: withBody(input.current, { kind: "clear", value: "", line: input.line.number, token: input.text }),
111
+ };
112
+ }
78
113
  export function parseTaskComposition(source) {
79
114
  const lines = sourceLines(source);
80
115
  const diagnostics = [];
@@ -119,35 +154,16 @@ export function parseTaskComposition(source) {
119
154
  diagnostics.push(diagnostic(line.number, "property must follow a task node", text));
120
155
  continue;
121
156
  }
122
- const fence = /^body\s+<<(.+)$/u.exec(text);
123
- if (fence !== null) {
124
- const token = fence[1];
125
- if (!/^[A-Z][A-Z0-9_]{2,31}$/u.test(token)) {
126
- diagnostics.push(diagnostic(line.number, "body token must match [A-Z][A-Z0-9_]* with length 3..32", token));
127
- continue;
128
- }
129
- const close = lines.findIndex((candidate, candidateIndex) => candidateIndex > index && candidate.text === token);
130
- if (close < 0) {
131
- diagnostics.push(diagnostic(line.number, "body fence is not closed", token));
132
- break;
133
- }
134
- const body = withoutFenceSeparator(source.slice(line.end, lines[close].start));
135
- if (current.body !== undefined)
136
- diagnostics.push(diagnostic(line.number, "body is assigned more than once", text));
137
- else {
138
- current = withBody(current, { kind: "replace", value: body, line: line.number, token });
139
- replaceNode(nodes, current);
140
- }
141
- index = close;
142
- continue;
143
- }
144
- if (/^body\s*=\s*$/u.test(text)) {
145
- if (current.body !== undefined)
146
- diagnostics.push(diagnostic(line.number, "body is assigned more than once", text));
147
- else {
148
- current = withBody(current, { kind: "clear", value: "", line: line.number, token: text });
157
+ const body = parseBodyAssignment({ text, line, index, lines, source, current }, diagnostics);
158
+ if (body.handled) {
159
+ if (body.node !== undefined) {
160
+ current = body.node;
149
161
  replaceNode(nodes, current);
150
162
  }
163
+ if (body.close !== undefined)
164
+ index = body.close;
165
+ if (body.stop === true)
166
+ break;
151
167
  continue;
152
168
  }
153
169
  const assignment = parseAssignment(text, line.number);
@@ -2,6 +2,7 @@ import type { WorldRoot } from "../world.js";
2
2
  import { type TaskCompositionAlias, type TaskCompositionBodyPreview } from "./compose-language.js";
3
3
  import type { TaskId } from "./identity.js";
4
4
  import type { TaskRefusal, TaskRetry } from "./operations.js";
5
+ export type { TaskCompositionAlias, TaskCompositionBodyPreview } from "./compose-language.js";
5
6
  export type TaskDocumentChange = Readonly<{
6
7
  taskId: TaskId;
7
8
  kind: "created" | "updated";
@@ -33,4 +34,11 @@ export type TaskCompositionResult = Readonly<{
33
34
  }>;
34
35
  draft: string;
35
36
  }> & TaskCompositionFacts);
36
- export declare function composeTasks(world: WorldRoot, markdown: string, signal?: AbortSignal, actor?: string, defaultNamespace?: readonly string[], planOnly?: boolean): Promise<TaskCompositionResult>;
37
+ export declare function composeTasks(input: Readonly<{
38
+ world: WorldRoot;
39
+ markdown: string;
40
+ signal?: AbortSignal;
41
+ actor?: string;
42
+ defaultNamespace?: readonly string[];
43
+ planOnly?: boolean;
44
+ }>): Promise<TaskCompositionResult>;
@@ -107,17 +107,11 @@ async function composeUnderLocks(input) {
107
107
  return await admitPlan(input.world, fresh, replanned.plan, input.signal);
108
108
  });
109
109
  }
110
- function composeInput(world, markdown, namespace, at, actor, signal) {
111
- return {
112
- world,
113
- markdown,
114
- namespace,
115
- at,
116
- ...(actor === undefined ? {} : { actor }),
117
- ...(signal === undefined ? {} : { signal }),
118
- };
110
+ function composeInput(input) {
111
+ return input;
119
112
  }
120
- export async function composeTasks(world, markdown, signal, actor, defaultNamespace = [], planOnly = false) {
113
+ export async function composeTasks(input) {
114
+ const { world, markdown, signal, actor, defaultNamespace = [], planOnly = false } = input;
121
115
  const at = currentTimestamp();
122
116
  const initial = await readBoard(world);
123
117
  const initialPlan = planAgainst(markdown, initial.board, defaultNamespace, at, actor);
@@ -125,11 +119,18 @@ export async function composeTasks(world, markdown, signal, actor, defaultNamesp
125
119
  return refusal(initialPlan.diagnostics);
126
120
  if (planOnly)
127
121
  return plannedResult(initialPlan.plan);
128
- const input = composeInput(world, markdown, defaultNamespace, at, actor, signal);
122
+ const request = composeInput({
123
+ world,
124
+ markdown,
125
+ namespace: defaultNamespace,
126
+ at,
127
+ ...(actor === undefined ? {} : { actor }),
128
+ ...(signal === undefined ? {} : { signal }),
129
+ });
129
130
  const allocation = initialPlan.plan.tasks.some((task) => task.kind === "new");
130
131
  const admitted = allocation
131
- ? await withTaskLocks({ world, allocation: true, ids: [], ...(signal === undefined ? {} : { signal }) }, async () => await composeUnderLocks(input))
132
- : await composeUnderLocks(input);
132
+ ? await withTaskLocks({ world, allocation: true, ids: [], ...(signal === undefined ? {} : { signal }) }, async () => await composeUnderLocks(request))
133
+ : await composeUnderLocks(request);
133
134
  if (admitted !== "busy")
134
135
  return admitted;
135
136
  return {
@@ -1,7 +1,7 @@
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
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";
@@ -17,7 +17,7 @@ export type TaskDoctorReport = Readonly<{
17
17
  issues: readonly TaskDoctorIssue[];
18
18
  }>;
19
19
  export type TaskDecompositionTree = TaskOutcome<TaskTreeNode>;
20
- export type { AddTaskDocumentInput, AddTaskInput, BlockedTaskRow, TaskBatchResult, TaskCompositionDiagnostic, TaskCompositionResult, TaskDoctorIssue, TaskId, TaskMutationResult, TaskOutcome, TaskPriority, TaskRef, TaskRefusal, TaskRetry, TaskRow, TaskState, TaskTreeNode, TaskUpdateResult, TaskView, UpdateTaskInput, TaskPage, TaskQueryExpression, TaskQueryPredicate, TaskQueryRow, TaskQuerySort, TaskRelationPredicateField, };
20
+ 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
21
  export { TaskAuthorityCorruptionError, TASK_RELATION_PREDICATE_FIELDS };
22
22
  declare class TaskHandle {
23
23
  readonly id: TaskId;
@@ -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";
@@ -156,7 +156,16 @@ class TasksHandle {
156
156
  if (v.plan !== undefined && typeof v.plan !== "boolean")
157
157
  throw new TypeError("plan must be a boolean");
158
158
  const selected = namespace(v.namespace);
159
- return composeTasks(this.world, markdown, signal(v.signal), actor(v.actor), selected, v.plan === true);
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
+ });
160
169
  }
161
170
  }
162
171
  export const Tasks = Object.freeze({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "4.5.4",
3
+ "version": "4.5.6",
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.28",
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",