@astrosheep/keiyaku 4.5.2 → 4.5.4
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.
- package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/package.json +4 -1
- package/build/integrations/pi/keiyaku.ts +19 -11
- package/build/src/cli/commands/task-invoke.d.ts +2 -0
- package/build/src/cli/commands/task-invoke.js +15 -2
- package/build/src/cli/commands/task.js +21 -3
- package/build/src/cli/invoke.js +1 -0
- package/build/src/cli/render/task.js +25 -1
- package/build/src/task/compose-language.d.ts +43 -0
- package/build/src/task/compose-language.js +314 -0
- package/build/src/task/compose-parser.d.ts +31 -0
- package/build/src/task/compose-parser.js +162 -0
- package/build/src/task/compose.d.ts +20 -9
- package/build/src/task/compose.js +124 -287
- package/build/src/task/index.d.ts +3 -2
- package/build/src/task/index.js +4 -2
- package/build/src/task/operations.d.ts +6 -1
- package/package.json +12 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { parseTaskId } from "./identity.js";
|
|
2
|
+
function diagnostic(line, reason, token) {
|
|
3
|
+
return { line, reason, token };
|
|
4
|
+
}
|
|
5
|
+
function sourceLines(source) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
let start = 0;
|
|
8
|
+
let number = 1;
|
|
9
|
+
while (start < source.length) {
|
|
10
|
+
let cursor = start;
|
|
11
|
+
while (cursor < source.length && source[cursor] !== "\n" && source[cursor] !== "\r")
|
|
12
|
+
cursor += 1;
|
|
13
|
+
let end = cursor;
|
|
14
|
+
if (source[cursor] === "\r" && source[cursor + 1] === "\n")
|
|
15
|
+
end += 2;
|
|
16
|
+
else if (cursor < source.length)
|
|
17
|
+
end += 1;
|
|
18
|
+
lines.push({ number, start, end, text: source.slice(start, cursor) });
|
|
19
|
+
start = end;
|
|
20
|
+
number += 1;
|
|
21
|
+
}
|
|
22
|
+
if (source.length === 0 || source.endsWith("\n") || source.endsWith("\r")) {
|
|
23
|
+
lines.push({ number, start: source.length, end: source.length, text: "" });
|
|
24
|
+
}
|
|
25
|
+
return lines;
|
|
26
|
+
}
|
|
27
|
+
function withoutFenceSeparator(value) {
|
|
28
|
+
if (value.endsWith("\r\n"))
|
|
29
|
+
return value.slice(0, -2);
|
|
30
|
+
if (value.endsWith("\n") || value.endsWith("\r"))
|
|
31
|
+
return value.slice(0, -1);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function parseNamespace(value) {
|
|
35
|
+
if (value === "")
|
|
36
|
+
return [];
|
|
37
|
+
return parseTaskId(`task/${value}/placeholder`).namespace;
|
|
38
|
+
}
|
|
39
|
+
function parseAssignment(text, line) {
|
|
40
|
+
const matched = /^(as|parent|pri|needs|supersedes|relates)\s*(=|\+=|-=)\s*(.*)$/u.exec(text);
|
|
41
|
+
if (matched === null)
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
field: matched[1],
|
|
45
|
+
operator: matched[2],
|
|
46
|
+
value: matched[3],
|
|
47
|
+
line,
|
|
48
|
+
token: text,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function parseNode(text, line, index) {
|
|
52
|
+
if (text.startsWith("+ ")) {
|
|
53
|
+
const title = text.slice(2);
|
|
54
|
+
return title.trim().length === 0
|
|
55
|
+
? diagnostic(line, "new task title must be nonblank", text)
|
|
56
|
+
: { index, line, kind: "new", title, assignments: [] };
|
|
57
|
+
}
|
|
58
|
+
if (!text.startsWith("@task/"))
|
|
59
|
+
return diagnostic(line, "node must begin with + or @task/", text);
|
|
60
|
+
try {
|
|
61
|
+
const id = text.slice(1);
|
|
62
|
+
parseTaskId(id);
|
|
63
|
+
return { index, line, kind: "existing", id, assignments: [] };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return diagnostic(line, error instanceof Error ? error.message : String(error), text);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function withAssignment(node, assignment) {
|
|
70
|
+
return { ...node, assignments: [...node.assignments, assignment] };
|
|
71
|
+
}
|
|
72
|
+
function withBody(node, body) {
|
|
73
|
+
return { ...node, body };
|
|
74
|
+
}
|
|
75
|
+
function replaceNode(nodes, node) {
|
|
76
|
+
nodes[node.index] = node;
|
|
77
|
+
}
|
|
78
|
+
export function parseTaskComposition(source) {
|
|
79
|
+
const lines = sourceLines(source);
|
|
80
|
+
const diagnostics = [];
|
|
81
|
+
const nodes = [];
|
|
82
|
+
let namespace;
|
|
83
|
+
let current;
|
|
84
|
+
let sawNode = false;
|
|
85
|
+
let sawNamespace = false;
|
|
86
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
87
|
+
const line = lines[index];
|
|
88
|
+
const text = line.text.trimStart();
|
|
89
|
+
if (text.trim().length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
if (text.startsWith("ns=")) {
|
|
92
|
+
if (sawNode || sawNamespace)
|
|
93
|
+
diagnostics.push(diagnostic(line.number, "namespace must be the first nonblank line", text));
|
|
94
|
+
else {
|
|
95
|
+
try {
|
|
96
|
+
namespace = parseNamespace(text.slice(3));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
diagnostics.push(diagnostic(line.number, error instanceof Error ? error.message : String(error), text));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
sawNamespace = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (text.startsWith("+ ") || text.startsWith("@task/")) {
|
|
106
|
+
const parsed = parseNode(text, line.number, nodes.length);
|
|
107
|
+
if ("reason" in parsed) {
|
|
108
|
+
diagnostics.push(parsed);
|
|
109
|
+
current = undefined;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
nodes.push(parsed);
|
|
113
|
+
current = parsed;
|
|
114
|
+
}
|
|
115
|
+
sawNode = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (current === undefined) {
|
|
119
|
+
diagnostics.push(diagnostic(line.number, "property must follow a task node", text));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
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 });
|
|
149
|
+
replaceNode(nodes, current);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const assignment = parseAssignment(text, line.number);
|
|
154
|
+
if (assignment === null)
|
|
155
|
+
diagnostics.push(diagnostic(line.number, "unknown compose property", text));
|
|
156
|
+
else {
|
|
157
|
+
current = withAssignment(current, assignment);
|
|
158
|
+
replaceNode(nodes, current);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { ...(namespace === undefined ? {} : { namespace }), nodes, diagnostics };
|
|
162
|
+
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
import { type TaskId } from "./identity.js";
|
|
2
1
|
import type { WorldRoot } from "../world.js";
|
|
3
|
-
import { type
|
|
4
|
-
type
|
|
2
|
+
import { type TaskCompositionAlias, type TaskCompositionBodyPreview } from "./compose-language.js";
|
|
3
|
+
import type { TaskId } from "./identity.js";
|
|
4
|
+
import type { TaskRefusal, TaskRetry } from "./operations.js";
|
|
5
|
+
export type TaskDocumentChange = Readonly<{
|
|
5
6
|
taskId: TaskId;
|
|
6
7
|
kind: "created" | "updated";
|
|
7
8
|
documentDiff: string;
|
|
8
9
|
}>;
|
|
10
|
+
export type TaskCompositionFacts = Readonly<{
|
|
11
|
+
aliases: readonly TaskCompositionAlias[];
|
|
12
|
+
admissionOrder: readonly TaskId[];
|
|
13
|
+
}>;
|
|
9
14
|
export type TaskCompositionResult = Readonly<{
|
|
15
|
+
kind: "planned";
|
|
16
|
+
aliases: readonly TaskCompositionAlias[];
|
|
17
|
+
admissionOrder: readonly TaskId[];
|
|
18
|
+
bodies: readonly TaskCompositionBodyPreview[];
|
|
19
|
+
}> | Readonly<{
|
|
10
20
|
kind: "accepted";
|
|
11
21
|
documentChanges: readonly TaskDocumentChange[];
|
|
12
|
-
}> | Readonly<{
|
|
22
|
+
} & TaskCompositionFacts> | Readonly<{
|
|
13
23
|
kind: "refused";
|
|
14
|
-
refusal: TaskRefusal
|
|
15
|
-
|
|
24
|
+
refusal: Extract<TaskRefusal, {
|
|
25
|
+
kind: "invalid-composition";
|
|
26
|
+
}>;
|
|
27
|
+
}> | (Readonly<{
|
|
16
28
|
kind: "incomplete";
|
|
17
29
|
documentChanges: readonly TaskDocumentChange[];
|
|
18
30
|
stopped: TaskRefusal | Readonly<{
|
|
@@ -20,6 +32,5 @@ export type TaskCompositionResult = Readonly<{
|
|
|
20
32
|
reason: TaskRetry;
|
|
21
33
|
}>;
|
|
22
34
|
draft: string;
|
|
23
|
-
}
|
|
24
|
-
export declare function composeTasks(world: WorldRoot, markdown: string, signal?: AbortSignal, actor?: string, defaultNamespace?: readonly string[]): Promise<TaskCompositionResult>;
|
|
25
|
-
export {};
|
|
35
|
+
}> & TaskCompositionFacts);
|
|
36
|
+
export declare function composeTasks(world: WorldRoot, markdown: string, signal?: AbortSignal, actor?: string, defaultNamespace?: readonly string[], planOnly?: boolean): Promise<TaskCompositionResult>;
|
|
@@ -1,305 +1,142 @@
|
|
|
1
1
|
import { documentDiff } from "../markdown/diff.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
return { kind: "invalid-composition", diagnostic };
|
|
5
|
+
function refusal(diagnostics) {
|
|
6
|
+
return { kind: "refused", refusal: { kind: "invalid-composition", diagnostics } };
|
|
9
7
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
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
|
-
};
|
|
8
|
+
function currentTimestamp() {
|
|
9
|
+
return new Date().toISOString();
|
|
61
10
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 };
|
|
11
|
+
function facts(plan) {
|
|
12
|
+
return { aliases: plan.aliases, admissionOrder: plan.admissionOrder };
|
|
13
|
+
}
|
|
14
|
+
function reference(id, remainingAliases) {
|
|
15
|
+
const alias = remainingAliases.get(id);
|
|
16
|
+
return alias === undefined ? `@${id}` : `^${alias}`;
|
|
17
|
+
}
|
|
18
|
+
function bodyToken(body) {
|
|
19
|
+
const lines = new Set(body.split(/\r\n|\n|\r/u));
|
|
20
|
+
for (let suffix = 0; Number.isSafeInteger(suffix); suffix += 1) {
|
|
21
|
+
const token = suffix === 0 ? "END_BODY" : `END_BODY_${suffix}`;
|
|
22
|
+
if (token.length > 32)
|
|
23
|
+
break;
|
|
24
|
+
if (!lines.has(token))
|
|
25
|
+
return token;
|
|
101
26
|
}
|
|
102
|
-
|
|
103
|
-
|
|
27
|
+
throw new Error("compose recovery body token space exhausted");
|
|
28
|
+
}
|
|
29
|
+
function taskDraft(task, remainingAliases) {
|
|
30
|
+
const document = task.after;
|
|
31
|
+
const lines = [task.kind === "new" ? `+ ${document.title}` : `@${document.id}`];
|
|
32
|
+
if (task.kind === "new" && task.alias !== undefined)
|
|
33
|
+
lines.push(`as = ${task.alias}`);
|
|
34
|
+
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(", ")}`);
|
|
35
|
+
if (document.body === "")
|
|
36
|
+
lines.push("body =");
|
|
37
|
+
else {
|
|
38
|
+
const token = bodyToken(document.body);
|
|
39
|
+
lines.push(`body <<${token}`, document.body, token);
|
|
104
40
|
}
|
|
41
|
+
return lines;
|
|
105
42
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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;
|
|
43
|
+
function recoveryDraft(namespace, remaining) {
|
|
44
|
+
const aliases = new Map();
|
|
45
|
+
for (const task of remaining) {
|
|
46
|
+
if (task.kind === "new" && task.alias !== undefined)
|
|
47
|
+
aliases.set(task.after.id, task.alias);
|
|
48
|
+
}
|
|
49
|
+
const lines = [`ns=${namespace.join("/")}`];
|
|
50
|
+
for (const task of remaining)
|
|
51
|
+
lines.push("", ...taskDraft(task, aliases));
|
|
52
|
+
return `${lines.join("\n")}\n`;
|
|
128
53
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
54
|
+
function plannedResult(plan) {
|
|
55
|
+
return { kind: "planned", ...facts(plan), bodies: plan.bodies };
|
|
56
|
+
}
|
|
57
|
+
function planAgainst(markdown, board, namespace, at, actor) {
|
|
58
|
+
return planTaskComposition({ markdown, board, namespace, at, ...(actor === undefined ? {} : { actor }) });
|
|
59
|
+
}
|
|
60
|
+
async function admitPlan(world, snapshot, plan, signal) {
|
|
61
|
+
const changes = [];
|
|
62
|
+
for (let index = 0; index < plan.tasks.length; index += 1) {
|
|
63
|
+
signal?.throwIfAborted();
|
|
64
|
+
const item = plan.tasks[index];
|
|
65
|
+
const beforeBytes = item.before === null ? null : (snapshot.bytes.get(item.after.id) ?? null);
|
|
66
|
+
const afterBytes = serializeTaskDocument(item.after);
|
|
67
|
+
const before = beforeBytes === null ? "" : Buffer.from(beforeBytes).toString("utf8");
|
|
68
|
+
const after = Buffer.from(afterBytes).toString("utf8");
|
|
69
|
+
const replaced = await replaceAuthority({
|
|
70
|
+
path: authorityPath(world, item.after.id),
|
|
71
|
+
expected: beforeBytes,
|
|
72
|
+
next: afterBytes,
|
|
73
|
+
});
|
|
74
|
+
if (replaced !== "replaced") {
|
|
75
|
+
return {
|
|
76
|
+
kind: "incomplete",
|
|
77
|
+
...facts(plan),
|
|
78
|
+
documentChanges: changes,
|
|
79
|
+
stopped: { kind: "retry", reason: "concurrent-modification" },
|
|
80
|
+
draft: recoveryDraft(plan.namespace, plan.tasks.slice(index)),
|
|
81
|
+
};
|
|
142
82
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
83
|
+
const label = `${item.after.id}.md`;
|
|
84
|
+
changes.push({
|
|
85
|
+
taskId: item.after.id,
|
|
86
|
+
kind: item.before === null ? "created" : "updated",
|
|
87
|
+
documentDiff: documentDiff(label, label, before, after),
|
|
88
|
+
});
|
|
149
89
|
}
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
function
|
|
153
|
-
|
|
90
|
+
return { kind: "accepted", ...facts(plan), documentChanges: changes };
|
|
91
|
+
}
|
|
92
|
+
async function composeUnderLocks(input) {
|
|
93
|
+
const snapshot = await readBoard(input.world);
|
|
94
|
+
const planned = planAgainst(input.markdown, snapshot.board, input.namespace, input.at, input.actor);
|
|
95
|
+
if (planned.kind === "refused")
|
|
96
|
+
return refusal(planned.diagnostics);
|
|
97
|
+
return await withTaskLocks({
|
|
98
|
+
world: input.world,
|
|
99
|
+
allocation: false,
|
|
100
|
+
ids: planned.plan.admissionOrder,
|
|
101
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
102
|
+
}, async () => {
|
|
103
|
+
const fresh = await readBoard(input.world);
|
|
104
|
+
const replanned = planAgainst(input.markdown, fresh.board, input.namespace, input.at, input.actor);
|
|
105
|
+
if (replanned.kind === "refused")
|
|
106
|
+
return refusal(replanned.diagnostics);
|
|
107
|
+
return await admitPlan(input.world, fresh, replanned.plan, input.signal);
|
|
108
|
+
});
|
|
154
109
|
}
|
|
155
|
-
function
|
|
110
|
+
function composeInput(world, markdown, namespace, at, actor, signal) {
|
|
156
111
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
supersedes: [],
|
|
164
|
-
relates: [],
|
|
165
|
-
note: "",
|
|
166
|
-
...(actor === undefined ? {} : { createdBy: actor }),
|
|
167
|
-
createdAt: at,
|
|
168
|
-
updatedAt: at,
|
|
169
|
-
body: "",
|
|
112
|
+
world,
|
|
113
|
+
markdown,
|
|
114
|
+
namespace,
|
|
115
|
+
at,
|
|
116
|
+
...(actor === undefined ? {} : { actor }),
|
|
117
|
+
...(signal === undefined ? {} : { signal }),
|
|
170
118
|
};
|
|
171
119
|
}
|
|
172
|
-
function
|
|
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);
|
|
225
|
-
}
|
|
226
|
-
return planned;
|
|
227
|
-
}
|
|
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));
|
|
237
|
-
}
|
|
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`;
|
|
254
|
-
}
|
|
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 };
|
|
120
|
+
export async function composeTasks(world, markdown, signal, actor, defaultNamespace = [], planOnly = false) {
|
|
259
121
|
const at = currentTimestamp();
|
|
260
|
-
const namespace = sketch.namespace ?? defaultNamespace ?? [];
|
|
261
122
|
const initial = await readBoard(world);
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 };
|
|
296
|
-
});
|
|
297
|
-
return result === "busy"
|
|
298
|
-
? {
|
|
299
|
-
kind: "incomplete",
|
|
300
|
-
documentChanges: [],
|
|
301
|
-
stopped: { kind: "retry", reason: "busy" },
|
|
302
|
-
draft: draft(namespace, ordered),
|
|
303
|
-
}
|
|
304
|
-
: result;
|
|
123
|
+
const initialPlan = planAgainst(markdown, initial.board, defaultNamespace, at, actor);
|
|
124
|
+
if (initialPlan.kind === "refused")
|
|
125
|
+
return refusal(initialPlan.diagnostics);
|
|
126
|
+
if (planOnly)
|
|
127
|
+
return plannedResult(initialPlan.plan);
|
|
128
|
+
const input = composeInput(world, markdown, defaultNamespace, at, actor, signal);
|
|
129
|
+
const allocation = initialPlan.plan.tasks.some((task) => task.kind === "new");
|
|
130
|
+
const admitted = allocation
|
|
131
|
+
? await withTaskLocks({ world, allocation: true, ids: [], ...(signal === undefined ? {} : { signal }) }, async () => await composeUnderLocks(input))
|
|
132
|
+
: await composeUnderLocks(input);
|
|
133
|
+
if (admitted !== "busy")
|
|
134
|
+
return admitted;
|
|
135
|
+
return {
|
|
136
|
+
kind: "incomplete",
|
|
137
|
+
...facts(initialPlan.plan),
|
|
138
|
+
documentChanges: [],
|
|
139
|
+
stopped: { kind: "retry", reason: "busy" },
|
|
140
|
+
draft: recoveryDraft(initialPlan.plan.namespace, initialPlan.plan.tasks),
|
|
141
|
+
};
|
|
305
142
|
}
|
|
@@ -4,7 +4,7 @@ import { type BlockedTaskRow, type TaskDetailFacts, type TaskDoctorIssue, type T
|
|
|
4
4
|
import { 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;
|
|
@@ -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, 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, 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;
|
|
@@ -95,6 +95,7 @@ declare class TasksHandle {
|
|
|
95
95
|
namespace?: readonly string[];
|
|
96
96
|
actor?: string;
|
|
97
97
|
signal?: AbortSignal;
|
|
98
|
+
plan?: boolean;
|
|
98
99
|
}>): Promise<TaskCompositionResult>;
|
|
99
100
|
}
|
|
100
101
|
export type Tasks = TasksHandle;
|