@astrosheep/keiyaku 2.8.4 → 2.9.0
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/README.md +35 -15
- package/build/.tsbuildinfo +1 -1
- package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
- package/build/agents/harness/control-types.js +1 -0
- package/build/agents/harness/execution-handle.js +23 -3
- package/build/agents/harness/index.js +6 -607
- package/build/agents/harness/outcome.js +141 -0
- package/build/agents/harness/projection.js +310 -0
- package/build/agents/harness/router.js +201 -0
- package/build/agents/harness/runtime.js +2 -1
- package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
- package/build/agents/launch-snapshot/resolve.js +72 -0
- package/build/agents/opencode-sdk.js +24 -3
- package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
- package/build/agents/providers/claude-agent-sdk/events.js +218 -0
- package/build/agents/providers/claude-agent-sdk/session.js +158 -0
- package/build/agents/providers/codex-app-server/adapter.js +347 -0
- package/build/agents/providers/codex-app-server/events.js +352 -0
- package/build/agents/providers/codex-app-server/session.js +266 -0
- package/build/agents/providers/codex-sdk.js +23 -5
- package/build/agents/providers/opencode-sdk/adapter.js +344 -0
- package/build/agents/providers/opencode-sdk/events.js +354 -0
- package/build/agents/providers/opencode-sdk/session.js +180 -0
- package/build/agents/providers/pi/adapter.js +269 -0
- package/build/agents/providers/pi/checkpoint.js +41 -0
- package/build/agents/providers/pi/events.js +282 -0
- package/build/agents/{selector_v2.js → selector.js} +8 -6
- package/build/cli/commands/akuma/akuma/handler.js +4 -0
- package/build/cli/commands/akuma/akuma/meta.js +16 -0
- package/build/cli/commands/akuma/catalog.js +8 -0
- package/build/cli/commands/akuma/list/handler.js +15 -0
- package/build/cli/commands/akuma/list/meta-ls.js +9 -0
- package/build/cli/commands/akuma/view/handler.js +34 -0
- package/build/cli/commands/akuma/view/meta.js +8 -0
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +10 -0
- package/build/cli/commands/contract/amend/meta.js +12 -0
- package/build/cli/commands/contract/arc/handler.js +9 -0
- package/build/cli/commands/contract/arc/meta.js +26 -0
- package/build/cli/commands/contract/bind/handler.js +23 -0
- package/build/cli/commands/contract/bind/meta.js +30 -0
- package/build/cli/commands/contract/catalog.js +16 -0
- package/build/cli/commands/contract/forfeit/handler.js +13 -0
- package/build/cli/commands/contract/forfeit/meta.js +12 -0
- package/build/cli/commands/contract/log/handler.js +12 -0
- package/build/cli/commands/contract/log/meta.js +12 -0
- package/build/cli/commands/contract/petition/handler.js +26 -0
- package/build/cli/commands/contract/petition/meta.js +23 -0
- package/build/cli/commands/contract/renew/handler.js +10 -0
- package/build/cli/commands/contract/renew/meta.js +17 -0
- package/build/cli/commands/projection/call/handler.js +39 -0
- package/build/cli/commands/projection/call/meta.js +9 -0
- package/build/cli/commands/projection/catalog.js +14 -0
- package/build/cli/commands/projection/kill/handler.js +35 -0
- package/build/cli/commands/projection/kill/meta.js +11 -0
- package/build/cli/commands/projection/revive/handler.js +53 -0
- package/build/cli/commands/projection/revive/meta.js +9 -0
- package/build/cli/commands/projection/status/handler.js +23 -0
- package/build/cli/commands/projection/status/meta.js +9 -0
- package/build/cli/commands/projection/tell/handler.js +34 -0
- package/build/cli/commands/projection/tell/meta.js +9 -0
- package/build/cli/commands/projection/wait/handler.js +21 -0
- package/build/cli/commands/projection/wait/meta.js +11 -0
- package/build/cli/commands/registry.js +100 -0
- package/build/cli/commands/shared.js +71 -0
- package/build/cli/commands/skills/catalog.js +6 -0
- package/build/cli/commands/skills/install/handler.js +10 -0
- package/build/cli/commands/skills/install/meta.js +9 -0
- package/build/cli/commands/skills/skills/handler.js +4 -0
- package/build/cli/commands/skills/skills/meta.js +10 -0
- package/build/cli/commands/system/catalog.js +8 -0
- package/build/cli/commands/system/completion/handler.js +10 -0
- package/build/cli/commands/system/completion/meta.js +9 -0
- package/build/cli/commands/system/dump-env/handler.js +3 -0
- package/build/cli/commands/system/dump-env/meta.js +10 -0
- package/build/cli/commands/system/guide/handler.js +4 -0
- package/build/cli/commands/system/guide/meta.js +9 -0
- package/build/cli/commands/task/add/handler.js +21 -0
- package/build/cli/commands/task/add/meta.js +8 -0
- package/build/cli/commands/task/catalog.js +22 -0
- package/build/cli/commands/task/doctor/handler.js +4 -0
- package/build/cli/commands/task/doctor/meta.js +8 -0
- package/build/cli/commands/task/done/handler.js +17 -0
- package/build/cli/commands/task/done/meta.js +8 -0
- package/build/cli/commands/task/drop/handler.js +6 -0
- package/build/cli/commands/task/drop/meta.js +8 -0
- package/build/cli/commands/task/ls/handler.js +13 -0
- package/build/cli/commands/task/ls/meta.js +8 -0
- package/build/cli/commands/task/shared.js +131 -0
- package/build/cli/commands/task/start/handler.js +6 -0
- package/build/cli/commands/task/start/meta.js +8 -0
- package/build/cli/commands/task/stop/handler.js +17 -0
- package/build/cli/commands/task/stop/meta.js +8 -0
- package/build/cli/commands/task/task/handler.js +5 -0
- package/build/cli/commands/task/task/meta.js +11 -0
- package/build/cli/commands/task/update/handler.js +18 -0
- package/build/cli/commands/task/update/meta.js +8 -0
- package/build/cli/commands/task/view/handler.js +14 -0
- package/build/cli/commands/task/view/meta.js +8 -0
- package/build/cli/completion.js +26 -27
- package/build/cli/flags.js +79 -2
- package/build/cli/help.js +27 -49
- package/build/cli/index.js +121 -374
- package/build/cli/output-stream-error-policy.js +17 -0
- package/build/cli/parse.js +159 -72
- package/build/cli/projection-address.js +5 -0
- package/build/cli/render/arc.js +1 -1
- package/build/cli/render/errors.js +4 -3
- package/build/cli/render/format.js +14 -0
- package/build/cli/render/kanshi.js +106 -0
- package/build/cli/render/petition.js +24 -1
- package/build/cli/render/projection-activity.js +5 -1
- package/build/cli/render/shared.js +2 -2
- package/build/cli/render/status.js +79 -13
- package/build/cli/render/task.js +113 -0
- package/build/cli/render/wait.js +23 -3
- package/build/cli/types.js +1 -1
- package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
- package/build/config/env.js +9 -4
- package/build/config/settings/disease.js +26 -0
- package/build/config/settings/knobs.js +8 -0
- package/build/config/settings/loader.js +244 -0
- package/build/config/settings/schema.js +276 -0
- package/build/core/addressing.js +1 -1
- package/build/core/amend.js +3 -3
- package/build/core/arc.js +5 -4
- package/build/core/bind-workspace.js +123 -0
- package/build/core/bind.js +75 -155
- package/build/core/call/call.js +224 -0
- package/build/core/call/context.js +238 -0
- package/build/core/call/execution.js +217 -0
- package/build/core/call/prompt.js +42 -0
- package/build/core/call/reference.js +24 -0
- package/build/core/call-persist.js +1 -1
- package/build/core/claim-delivery.js +247 -0
- package/build/core/claim.js +39 -277
- package/build/core/context.js +2 -1
- package/build/core/{render.js → contract-view.js} +12 -140
- package/build/core/draft.js +107 -9
- package/build/core/entry.js +9 -1
- package/build/core/execution-coordinate.js +4 -20
- package/build/core/execution-pact.js +16 -31
- package/build/core/forfeit.js +35 -22
- package/build/core/hints.js +2 -2
- package/build/core/log.js +2 -2
- package/build/core/petition-claim-gates.js +8 -6
- package/build/core/petition-preview.js +97 -0
- package/build/core/petition-run.js +3 -2
- package/build/core/petition.js +39 -26
- package/build/core/projection/generation/database.js +306 -0
- package/build/core/projection/generation/identity.js +84 -0
- package/build/core/projection/generation/model.js +201 -0
- package/build/core/projection/generation/store.js +73 -0
- package/build/core/projection/generation/transitions.js +258 -0
- package/build/core/projection/heart.js +0 -307
- package/build/core/projection/tell/database.js +109 -0
- package/build/core/projection/tell/model.js +97 -0
- package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
- package/build/core/projection-activity.js +23 -2
- package/build/core/projection-core.js +2 -1
- package/build/core/projection-execution-observer.js +259 -0
- package/build/core/projection-generation-continuation.js +1 -1
- package/build/core/projection-generation-execution.js +90 -20
- package/build/core/projection-generation-launcher.js +1 -1
- package/build/core/projection-generation-runner.js +198 -4
- package/build/core/projection-identity.js +1 -1
- package/build/core/projection-kill.js +50 -17
- package/build/core/projection-life-observer.js +1 -1
- package/build/core/projection-life-protocol.js +10 -0
- package/build/core/projection-mint.js +1 -1
- package/build/core/projection-status.js +45 -32
- package/build/core/projection-wait.js +12 -2
- package/build/core/projection-wake.js +5 -2
- package/build/core/{queue_v2.js → queue.js} +2 -1
- package/build/core/registry.js +1 -1
- package/build/core/renew.js +7 -7
- package/build/core/response-artifact-catalog-sqlite.js +112 -0
- package/build/core/response-artifact-catalog.js +7 -0
- package/build/core/seal.js +44 -16
- package/build/core/status/board.js +286 -0
- package/build/core/status/drift.js +99 -0
- package/build/core/status/lifecycle.js +84 -0
- package/build/core/status/reconciliation.js +131 -0
- package/build/core/stored-agent-event.js +28 -4
- package/build/core/target-ref.js +10 -0
- package/build/core/task/board.js +175 -0
- package/build/core/task/commands.js +211 -0
- package/build/core/task/document.js +189 -0
- package/build/core/task/model.js +1 -0
- package/build/core/task/settlement-git.js +207 -0
- package/build/core/task/settlement-policy.js +82 -0
- package/build/core/task/source-board.js +84 -0
- package/build/core/task/source-model.js +1 -0
- package/build/core/task/validation.js +44 -0
- package/build/core/task-contract.js +224 -0
- package/build/core/task-doctor.js +14 -0
- package/build/core/task-git-actor.js +38 -0
- package/build/core/task-git-runtime.js +34 -0
- package/build/core/task-git-store.js +375 -0
- package/build/core/task-repository.js +1 -0
- package/build/core/task-store-repository.js +86 -0
- package/build/core/task-store.js +1 -0
- package/build/core/task-worktree.js +51 -0
- package/build/core/task.js +4 -0
- package/build/core/transcripts.js +138 -56
- package/build/core/validation.js +17 -0
- package/build/core/verdict.js +2 -2
- package/build/core/worktree-path.js +24 -6
- package/build/flow-error.js +3 -2
- package/build/generated/version.js +1 -1
- package/build/git/commits.js +3 -0
- package/build/git/diff/parsers.js +0 -94
- package/build/index.js +2 -0
- package/build/keiyaku-constants.js +2 -0
- package/build/keiyaku.js +4 -3
- package/package.json +3 -2
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +11 -6
- package/build/agents/harness/pump.js +0 -158
- package/build/agents/providers/codex-app-server.js +0 -864
- package/build/agents/providers/opencode-sdk.js +0 -363
- package/build/agents/providers/pi.js +0 -557
- package/build/config/settings.js +0 -533
- package/build/core/call.js +0 -686
- package/build/core/projection/generation.js +0 -412
- package/build/core/projection/identity.js +0 -81
- package/build/core/projection/mint.js +0 -174
- package/build/core/projection-generation-store.js +0 -707
- package/build/core/status.js +0 -539
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { isAlias, isMap, isScalar, isSeq, parseDocument, stringify, } from "yaml";
|
|
2
|
+
import { canonicalTimestamp, fullId, invalid, nonblank, TASK_STATES, } from "./validation.js";
|
|
3
|
+
const FIELD_ORDER = [
|
|
4
|
+
"id",
|
|
5
|
+
"title",
|
|
6
|
+
"state",
|
|
7
|
+
"pri",
|
|
8
|
+
"needs",
|
|
9
|
+
"parent",
|
|
10
|
+
"from",
|
|
11
|
+
"createdAt",
|
|
12
|
+
"creator",
|
|
13
|
+
"startedAt",
|
|
14
|
+
"contractId",
|
|
15
|
+
];
|
|
16
|
+
const FIELD_SET = new Set(FIELD_ORDER);
|
|
17
|
+
function validateNode(node) {
|
|
18
|
+
if (node == null)
|
|
19
|
+
return;
|
|
20
|
+
if (isAlias(node))
|
|
21
|
+
invalid("task front matter must not contain aliases");
|
|
22
|
+
if ("anchor" in node && node.anchor)
|
|
23
|
+
invalid("task front matter must not contain anchors");
|
|
24
|
+
if ("tag" in node && node.tag)
|
|
25
|
+
invalid("task front matter must not contain explicit tags");
|
|
26
|
+
if (isMap(node)) {
|
|
27
|
+
for (const item of node.items) {
|
|
28
|
+
const pair = item;
|
|
29
|
+
validateNode(pair.key);
|
|
30
|
+
validateNode(pair.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (isSeq(node)) {
|
|
34
|
+
for (const item of node.items)
|
|
35
|
+
validateNode(item);
|
|
36
|
+
}
|
|
37
|
+
else if (!isScalar(node)) {
|
|
38
|
+
invalid("task front matter contains an unsupported YAML node");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function frontMatter(source) {
|
|
42
|
+
if (!source.text.startsWith("---\n") && !source.text.startsWith("---\r\n")) {
|
|
43
|
+
invalid("task document must begin with exactly one '---' front matter block");
|
|
44
|
+
}
|
|
45
|
+
const lines = source.text.split(/\r?\n/);
|
|
46
|
+
let closing = -1;
|
|
47
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
48
|
+
if (lines[index] === "---") {
|
|
49
|
+
closing = index;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (closing < 0)
|
|
54
|
+
invalid("task document front matter is not closed");
|
|
55
|
+
return {
|
|
56
|
+
yaml: lines.slice(1, closing).join("\n"),
|
|
57
|
+
body: lines.slice(closing + 1).join("\n"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function parseTaskDocument(source) {
|
|
61
|
+
const split = frontMatter(source);
|
|
62
|
+
const parsed = parseDocument(split.yaml, {
|
|
63
|
+
merge: false,
|
|
64
|
+
prettyErrors: false,
|
|
65
|
+
strict: true,
|
|
66
|
+
uniqueKeys: true,
|
|
67
|
+
});
|
|
68
|
+
if (parsed.errors.length > 0) {
|
|
69
|
+
invalid(`invalid task YAML: ${parsed.errors[0].message}`);
|
|
70
|
+
}
|
|
71
|
+
validateNode(parsed.contents);
|
|
72
|
+
if (!isMap(parsed.contents))
|
|
73
|
+
invalid("task front matter must be a YAML mapping");
|
|
74
|
+
const values = new Map();
|
|
75
|
+
for (const item of parsed.contents.items) {
|
|
76
|
+
if (!isScalar(item.key) || typeof item.key.value !== "string") {
|
|
77
|
+
invalid("task front matter keys must be strings");
|
|
78
|
+
}
|
|
79
|
+
const key = item.key.value;
|
|
80
|
+
if (!FIELD_SET.has(key))
|
|
81
|
+
invalid(`unknown task front matter key '${key}'`);
|
|
82
|
+
const value = item.value;
|
|
83
|
+
if (isSeq(value)) {
|
|
84
|
+
const items = value.items.map((entry) => {
|
|
85
|
+
if (!isScalar(entry) || typeof entry.value !== "string") {
|
|
86
|
+
return invalid(`task field '${key}' must contain only strings`);
|
|
87
|
+
}
|
|
88
|
+
return entry.value;
|
|
89
|
+
});
|
|
90
|
+
values.set(key, items);
|
|
91
|
+
}
|
|
92
|
+
else if (isScalar(value)) {
|
|
93
|
+
values.set(key, value.value);
|
|
94
|
+
}
|
|
95
|
+
else if (value === null) {
|
|
96
|
+
values.set(key, null);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
invalid(`task field '${key}' must be a scalar or string array`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const id = fullId("id", values.get("id"));
|
|
103
|
+
const title = nonblank("title", values.get("title"));
|
|
104
|
+
const state = values.get("state");
|
|
105
|
+
if (typeof state !== "string" || !TASK_STATES.has(state)) {
|
|
106
|
+
invalid("task field 'state' must be open, in_progress, done, or drop");
|
|
107
|
+
}
|
|
108
|
+
const pri = values.get("pri");
|
|
109
|
+
if (typeof pri !== "number" || !Number.isInteger(pri) || pri < 0 || pri > 3) {
|
|
110
|
+
invalid("task field 'pri' must be an integer from 0 through 3");
|
|
111
|
+
}
|
|
112
|
+
const needsValue = values.get("needs");
|
|
113
|
+
if (!Array.isArray(needsValue))
|
|
114
|
+
invalid("task field 'needs' must be a string array");
|
|
115
|
+
const needs = needsValue.map((need) => fullId("needs", need));
|
|
116
|
+
if (new Set(needs).size !== needs.length)
|
|
117
|
+
invalid("task field 'needs' must be unique");
|
|
118
|
+
const relationship = (field) => {
|
|
119
|
+
const value = values.get(field);
|
|
120
|
+
if (value === null)
|
|
121
|
+
return null;
|
|
122
|
+
const related = fullId(field, value);
|
|
123
|
+
if (related === id)
|
|
124
|
+
invalid(`task field '${field}' must not reference itself`);
|
|
125
|
+
return related;
|
|
126
|
+
};
|
|
127
|
+
const parent = relationship("parent");
|
|
128
|
+
const fromValue = values.get("from");
|
|
129
|
+
if (!Array.isArray(fromValue))
|
|
130
|
+
invalid("task field 'from' must be a string array");
|
|
131
|
+
const from = fromValue.map((related) => fullId("from", related));
|
|
132
|
+
if (new Set(from).size !== from.length)
|
|
133
|
+
invalid("task field 'from' must be unique");
|
|
134
|
+
if (from.includes(id))
|
|
135
|
+
invalid("task field 'from' must not reference itself");
|
|
136
|
+
const createdAt = canonicalTimestamp("createdAt", values.get("createdAt"));
|
|
137
|
+
const creator = nonblank("creator", values.get("creator"));
|
|
138
|
+
const startedValue = values.get("startedAt");
|
|
139
|
+
const contractValue = values.get("contractId");
|
|
140
|
+
const startedAt = startedValue === undefined ? undefined : canonicalTimestamp("startedAt", startedValue);
|
|
141
|
+
const contractId = contractValue === undefined ? undefined : nonblank("contractId", contractValue);
|
|
142
|
+
if (state === "in_progress" && startedAt === undefined) {
|
|
143
|
+
invalid("task field 'startedAt' is required when state is in_progress");
|
|
144
|
+
}
|
|
145
|
+
if (state !== "in_progress" && startedAt !== undefined) {
|
|
146
|
+
invalid("task field 'startedAt' is forbidden unless state is in_progress");
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
id,
|
|
150
|
+
title,
|
|
151
|
+
state: state,
|
|
152
|
+
pri,
|
|
153
|
+
needs,
|
|
154
|
+
parent,
|
|
155
|
+
from,
|
|
156
|
+
createdAt,
|
|
157
|
+
creator,
|
|
158
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
159
|
+
...(contractId === undefined ? {} : { contractId }),
|
|
160
|
+
body: split.body,
|
|
161
|
+
source,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function validateDocument(document) {
|
|
165
|
+
return parseTaskDocument({ path: `${document.id}.md`, text: serializeUnchecked(document) });
|
|
166
|
+
}
|
|
167
|
+
function serializeUnchecked(document) {
|
|
168
|
+
const data = {
|
|
169
|
+
id: document.id,
|
|
170
|
+
title: document.title,
|
|
171
|
+
state: document.state,
|
|
172
|
+
pri: document.pri,
|
|
173
|
+
needs: [...document.needs],
|
|
174
|
+
parent: document.parent,
|
|
175
|
+
from: document.from,
|
|
176
|
+
createdAt: document.createdAt,
|
|
177
|
+
creator: document.creator,
|
|
178
|
+
};
|
|
179
|
+
if (document.startedAt !== undefined)
|
|
180
|
+
data.startedAt = document.startedAt;
|
|
181
|
+
if (document.contractId !== undefined)
|
|
182
|
+
data.contractId = document.contractId;
|
|
183
|
+
const yaml = stringify(data, { lineWidth: 0 }).trimEnd();
|
|
184
|
+
return `---\n${yaml}\n---\n${document.body}`;
|
|
185
|
+
}
|
|
186
|
+
export function serializeTaskDocument(document) {
|
|
187
|
+
validateDocument(document);
|
|
188
|
+
return serializeUnchecked(document);
|
|
189
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { FlowError } from "../../flow-error.js";
|
|
5
|
+
import { parseTaskDocument } from "../task.js";
|
|
6
|
+
function gitResult(cwd, args, input, env) {
|
|
7
|
+
return spawnSync("git", [...args], {
|
|
8
|
+
cwd,
|
|
9
|
+
input,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
env: { ...process.env, ...env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function git(cwd, args, input, env) {
|
|
16
|
+
const result = gitResult(cwd, args, input, env);
|
|
17
|
+
if (result.error) {
|
|
18
|
+
throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${result.error.message}`, { cause: result.error });
|
|
19
|
+
}
|
|
20
|
+
if (result.status !== 0) {
|
|
21
|
+
throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${result.stderr.trim() || `exit ${String(result.status)}`}`);
|
|
22
|
+
}
|
|
23
|
+
return result.stdout;
|
|
24
|
+
}
|
|
25
|
+
function boardPath(root, source) {
|
|
26
|
+
const relative = path.relative(root, source.path).split(path.sep).join("/");
|
|
27
|
+
if (!relative.startsWith(".keiyaku/tasks/") || relative.includes("..")) {
|
|
28
|
+
throw new FlowError("INTERNAL_STATE", `task source is outside the board: ${source.path}`);
|
|
29
|
+
}
|
|
30
|
+
return relative;
|
|
31
|
+
}
|
|
32
|
+
export function inspectCurrentBranch(cwd) {
|
|
33
|
+
const branch = gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
34
|
+
if (branch.error) {
|
|
35
|
+
throw new FlowError("INTERNAL_STATE", `cannot inspect HEAD: ${branch.error.message}`, { cause: branch.error });
|
|
36
|
+
}
|
|
37
|
+
if (branch.status !== 0 || branch.stdout.trim() === "") {
|
|
38
|
+
throw new FlowError("DETACHED_HEAD", "cannot publish task board from detached HEAD");
|
|
39
|
+
}
|
|
40
|
+
return { ref: branch.stdout.trim(), head: git(cwd, ["rev-parse", "HEAD"]).trim() };
|
|
41
|
+
}
|
|
42
|
+
export function createTaskBoardCommit(input) {
|
|
43
|
+
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
|
|
44
|
+
const indexPath = path.join(temporaryDirectory, "index");
|
|
45
|
+
const env = { GIT_INDEX_FILE: indexPath };
|
|
46
|
+
try {
|
|
47
|
+
git(input.cwd, ["read-tree", input.parent], undefined, env);
|
|
48
|
+
git(input.cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
|
|
49
|
+
for (const source of input.sources) {
|
|
50
|
+
const oid = git(input.cwd, ["hash-object", "-w", "--stdin"], source.text).trim();
|
|
51
|
+
git(input.cwd, ["update-index", "--add", "--cacheinfo", `100644,${oid},${boardPath(input.root, source)}`], undefined, env);
|
|
52
|
+
}
|
|
53
|
+
const tree = git(input.cwd, ["write-tree"], undefined, env).trim();
|
|
54
|
+
return git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject]).trim();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function taskTreeEntries(cwd, commit) {
|
|
61
|
+
return git(cwd, ["ls-tree", "-r", "-z", commit, "--", ".keiyaku/tasks"])
|
|
62
|
+
.split("\0")
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.map((record) => {
|
|
65
|
+
const tab = record.indexOf("\t");
|
|
66
|
+
const [mode, type, oid] = record.slice(0, tab).split(" ");
|
|
67
|
+
if (tab < 0 || type !== "blob" || !mode || !oid) {
|
|
68
|
+
throw new FlowError("INTERNAL_STATE", `invalid task tree entry in ${commit}`);
|
|
69
|
+
}
|
|
70
|
+
return { mode, oid, path: record.slice(tab + 1) };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function reconcileTaskIndex(cwd, commit) {
|
|
74
|
+
const existing = git(cwd, ["ls-files", "-z", "--", ".keiyaku/tasks"])
|
|
75
|
+
.split("\0")
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
for (const file of existing)
|
|
78
|
+
git(cwd, ["update-index", "--force-remove", "--", file]);
|
|
79
|
+
for (const entry of taskTreeEntries(cwd, commit)) {
|
|
80
|
+
git(cwd, ["update-index", "--add", "--cacheinfo", `${entry.mode},${entry.oid},${entry.path}`]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function refHead(cwd, ref) {
|
|
84
|
+
return git(cwd, ["rev-parse", ref]).trim();
|
|
85
|
+
}
|
|
86
|
+
export function readableRefHead(cwd, ref) {
|
|
87
|
+
const result = gitResult(cwd, ["rev-parse", ref]);
|
|
88
|
+
if (result.status !== 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
return result.stdout.trim();
|
|
91
|
+
}
|
|
92
|
+
export function taskContractAtCommit(cwd, commit, id) {
|
|
93
|
+
const sourcePath = `.keiyaku/tasks/${id}.md`;
|
|
94
|
+
const result = gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
|
|
95
|
+
if (result.status !== 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
return parseTaskDocument({ path: sourcePath, text: result.stdout }).contractId;
|
|
98
|
+
}
|
|
99
|
+
export async function updateBranch(input, deps) {
|
|
100
|
+
if (deps.updateBranch)
|
|
101
|
+
await deps.updateBranch(input);
|
|
102
|
+
else
|
|
103
|
+
git(input.cwd, ["update-ref", input.ref, input.next, input.expected]);
|
|
104
|
+
}
|
|
105
|
+
export async function publishForwardTaskBoardCommit(input, deps) {
|
|
106
|
+
let lastError;
|
|
107
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
108
|
+
const parent = refHead(input.cwd, input.ref);
|
|
109
|
+
const commit = createTaskBoardCommit({ ...input, parent });
|
|
110
|
+
try {
|
|
111
|
+
await updateBranch({ cwd: input.cwd, ref: input.ref, next: commit, expected: parent }, deps);
|
|
112
|
+
try {
|
|
113
|
+
reconcileTaskIndex(input.cwd, commit);
|
|
114
|
+
}
|
|
115
|
+
catch { /* the forward branch fact remains authoritative */ }
|
|
116
|
+
return commit;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
lastError = error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw lastError;
|
|
123
|
+
}
|
|
124
|
+
function treeWithoutTasks(cwd, commit, gitDir) {
|
|
125
|
+
const temporaryDirectory = fs.mkdtempSync(path.join(gitDir, "keiyaku-landing-index-"));
|
|
126
|
+
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
127
|
+
try {
|
|
128
|
+
git(cwd, ["read-tree", commit], undefined, env);
|
|
129
|
+
git(cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
|
|
130
|
+
return git(cwd, ["write-tree"], undefined, env).trim();
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function composeLandingTree(input) {
|
|
137
|
+
const baseTree = treeWithoutTasks(input.cwd, input.base, input.gitDir);
|
|
138
|
+
const targetTree = treeWithoutTasks(input.cwd, input.target, input.gitDir);
|
|
139
|
+
const deliveryTree = treeWithoutTasks(input.cwd, input.delivery, input.gitDir);
|
|
140
|
+
let mergedTree;
|
|
141
|
+
try {
|
|
142
|
+
mergedTree = git(input.cwd, [
|
|
143
|
+
"merge-tree",
|
|
144
|
+
"--write-tree",
|
|
145
|
+
"--merge-base",
|
|
146
|
+
baseTree,
|
|
147
|
+
targetTree,
|
|
148
|
+
deliveryTree,
|
|
149
|
+
]).trim();
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw new FlowError("CLOSE_MERGE_CONFLICT", "claim merge conflict outside task board", { cause: error });
|
|
153
|
+
}
|
|
154
|
+
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-landing-index-"));
|
|
155
|
+
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
156
|
+
try {
|
|
157
|
+
git(input.cwd, ["read-tree", mergedTree], undefined, env);
|
|
158
|
+
git(input.cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
|
|
159
|
+
for (const source of input.sources) {
|
|
160
|
+
const oid = git(input.cwd, ["hash-object", "-w", "--stdin"], source.text).trim();
|
|
161
|
+
git(input.cwd, [
|
|
162
|
+
"update-index",
|
|
163
|
+
"--add",
|
|
164
|
+
"--cacheinfo",
|
|
165
|
+
`100644,${oid},${boardPath(input.root, source)}`,
|
|
166
|
+
], undefined, env);
|
|
167
|
+
}
|
|
168
|
+
return git(input.cwd, ["write-tree"], undefined, env).trim();
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function createLandingCommit(input) {
|
|
175
|
+
const tree = composeLandingTree(input);
|
|
176
|
+
return git(input.cwd, [
|
|
177
|
+
"commit-tree",
|
|
178
|
+
tree,
|
|
179
|
+
"-p",
|
|
180
|
+
input.target,
|
|
181
|
+
"-p",
|
|
182
|
+
input.delivery,
|
|
183
|
+
"-m",
|
|
184
|
+
`keiyaku: claim ${input.contractId}`,
|
|
185
|
+
"-m",
|
|
186
|
+
`Merge contract ${input.contractId} into ${input.targetBranch}.`,
|
|
187
|
+
]).trim();
|
|
188
|
+
}
|
|
189
|
+
export function containsCommit(cwd, ancestor, descendant) {
|
|
190
|
+
return gitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
|
|
191
|
+
}
|
|
192
|
+
export function updateTargetRef(cwd, ref, next, expected) {
|
|
193
|
+
git(cwd, ["update-ref", ref, next, expected]);
|
|
194
|
+
}
|
|
195
|
+
export function sameTaskSourceBytes(root, left, right) {
|
|
196
|
+
const snapshot = (sources) => new Map(sources.map((source) => [boardPath(root, source), source.text]));
|
|
197
|
+
const leftSnapshot = snapshot(left);
|
|
198
|
+
const rightSnapshot = snapshot(right);
|
|
199
|
+
return leftSnapshot.size === rightSnapshot.size
|
|
200
|
+
&& [...leftSnapshot].every(([sourcePath, sourceText]) => rightSnapshot.get(sourcePath) === sourceText);
|
|
201
|
+
}
|
|
202
|
+
export function taskWritesFromSources(sources) {
|
|
203
|
+
return sources.map((source) => ({
|
|
204
|
+
id: path.basename(source.path, ".md"),
|
|
205
|
+
text: source.text,
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { resolveTaskReadId } from "./board.js";
|
|
3
|
+
export function requireHealthySettlementBoard(board) {
|
|
4
|
+
if (board.diseases.length > 0) {
|
|
5
|
+
throw new FlowError("INTERNAL_STATE", `authoritative task board is diseased: ${board.diseases.map((disease) => disease.message).join("; ")}`);
|
|
6
|
+
}
|
|
7
|
+
return board;
|
|
8
|
+
}
|
|
9
|
+
function settlementDocument(task, contractId) {
|
|
10
|
+
return {
|
|
11
|
+
id: task.id,
|
|
12
|
+
title: task.title,
|
|
13
|
+
state: task.state,
|
|
14
|
+
pri: task.pri,
|
|
15
|
+
needs: [...task.needs],
|
|
16
|
+
parent: task.parent,
|
|
17
|
+
from: [...task.from],
|
|
18
|
+
createdAt: task.createdAt,
|
|
19
|
+
creator: task.creator,
|
|
20
|
+
...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
|
|
21
|
+
...(contractId === undefined ? {} : { contractId }),
|
|
22
|
+
body: task.body,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function contractPointerDocument(task, contractId) {
|
|
26
|
+
return settlementDocument(task, contractId);
|
|
27
|
+
}
|
|
28
|
+
export function reopenedTaskDocument(task) {
|
|
29
|
+
const document = settlementDocument(task, undefined);
|
|
30
|
+
document.state = "open";
|
|
31
|
+
delete document.startedAt;
|
|
32
|
+
return document;
|
|
33
|
+
}
|
|
34
|
+
export function selectReadyTaskForBind(board, taskId) {
|
|
35
|
+
requireHealthySettlementBoard(board);
|
|
36
|
+
const id = resolveTaskReadId(board, taskId);
|
|
37
|
+
const task = board.byId.get(id);
|
|
38
|
+
if (task.state === "in_progress") {
|
|
39
|
+
throw new FlowError("INVALID_TARGET", `task ${id} is in_progress; stop it before bind`);
|
|
40
|
+
}
|
|
41
|
+
if (task.state !== "open") {
|
|
42
|
+
throw new FlowError("INVALID_TARGET", `task ${id} is ${task.state}, not ready for bind`);
|
|
43
|
+
}
|
|
44
|
+
const unsatisfied = task.needs
|
|
45
|
+
.filter((need) => {
|
|
46
|
+
const dependency = board.byId.get(need);
|
|
47
|
+
return dependency.state === "open" || dependency.state === "in_progress";
|
|
48
|
+
})
|
|
49
|
+
.sort();
|
|
50
|
+
if (unsatisfied.length > 0) {
|
|
51
|
+
throw new FlowError("INVALID_TARGET", `task ${id} is blocked by: ${unsatisfied.join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
if (task.contractId !== undefined) {
|
|
54
|
+
throw new FlowError("INVALID_TARGET", `task ${id} is already bound to contract ${task.contractId}`);
|
|
55
|
+
}
|
|
56
|
+
return task;
|
|
57
|
+
}
|
|
58
|
+
export function taskForContract(board, contractId) {
|
|
59
|
+
requireHealthySettlementBoard(board);
|
|
60
|
+
const matches = board.tasks
|
|
61
|
+
.filter((task) => task.contractId === contractId)
|
|
62
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
63
|
+
if (matches.length > 1) {
|
|
64
|
+
throw new FlowError("INTERNAL_STATE", `contract ${contractId} is referenced by multiple tasks: ${matches.map((task) => task.id).join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
return matches[0];
|
|
67
|
+
}
|
|
68
|
+
export function requireSettledTask(board, id) {
|
|
69
|
+
const task = requireHealthySettlementBoard(board).byId.get(id);
|
|
70
|
+
if (!task)
|
|
71
|
+
throw new FlowError("INTERNAL_STATE", `task ${id} disappeared from the board`);
|
|
72
|
+
return task;
|
|
73
|
+
}
|
|
74
|
+
export function doneTaskReplacement(board, contractId) {
|
|
75
|
+
const bound = taskForContract(board, contractId);
|
|
76
|
+
if (!bound)
|
|
77
|
+
return undefined;
|
|
78
|
+
const settled = settlementDocument(bound, undefined);
|
|
79
|
+
settled.state = "done";
|
|
80
|
+
delete settled.startedAt;
|
|
81
|
+
return settled;
|
|
82
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { buildTaskBoardFromDocuments } from "./board.js";
|
|
2
|
+
import { parseTaskDocument } from "./document.js";
|
|
3
|
+
import { invalid } from "./validation.js";
|
|
4
|
+
function compareText(left, right) {
|
|
5
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
6
|
+
}
|
|
7
|
+
function diseaseSort(left, right) {
|
|
8
|
+
return compareText(left.path, right.path)
|
|
9
|
+
|| compareText(left.code, right.code)
|
|
10
|
+
|| compareText(left.id ?? "", right.id ?? "")
|
|
11
|
+
|| compareText(left.message, right.message);
|
|
12
|
+
}
|
|
13
|
+
function sourceBasename(sourcePath) {
|
|
14
|
+
return sourcePath.split(/[\\/]/).at(-1) ?? "";
|
|
15
|
+
}
|
|
16
|
+
function taskDocument(parsed) {
|
|
17
|
+
return {
|
|
18
|
+
id: parsed.id,
|
|
19
|
+
title: parsed.title,
|
|
20
|
+
state: parsed.state,
|
|
21
|
+
pri: parsed.pri,
|
|
22
|
+
needs: [...parsed.needs],
|
|
23
|
+
parent: parsed.parent,
|
|
24
|
+
from: [...parsed.from],
|
|
25
|
+
createdAt: parsed.createdAt,
|
|
26
|
+
creator: parsed.creator,
|
|
27
|
+
...(parsed.startedAt === undefined ? {} : { startedAt: parsed.startedAt }),
|
|
28
|
+
...(parsed.contractId === undefined ? {} : { contractId: parsed.contractId }),
|
|
29
|
+
body: parsed.body,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function buildTaskSourceBoard(sources) {
|
|
33
|
+
const sourceDiseases = [];
|
|
34
|
+
const authoritative = [];
|
|
35
|
+
for (const source of [...sources].sort((a, b) => compareText(a.path, b.path))) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = parseTaskDocument(source);
|
|
38
|
+
if (sourceBasename(source.path) !== `${parsed.id}.md`) {
|
|
39
|
+
invalid(`task source basename must be ${parsed.id}.md`);
|
|
40
|
+
}
|
|
41
|
+
authoritative.push(parsed);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
sourceDiseases.push({
|
|
45
|
+
code: "invalid_document",
|
|
46
|
+
path: source.path,
|
|
47
|
+
message: error instanceof Error ? error.message : String(error),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const counts = new Map();
|
|
52
|
+
for (const task of authoritative)
|
|
53
|
+
counts.set(task.id, (counts.get(task.id) ?? 0) + 1);
|
|
54
|
+
const candidates = new Map();
|
|
55
|
+
for (const task of authoritative) {
|
|
56
|
+
if (counts.get(task.id) > 1) {
|
|
57
|
+
sourceDiseases.push({
|
|
58
|
+
code: "duplicate_id",
|
|
59
|
+
path: task.source.path,
|
|
60
|
+
message: `duplicate authoritative task id ${task.id}`,
|
|
61
|
+
id: task.id,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
candidates.set(task.id, task);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const domainBoard = buildTaskBoardFromDocuments([...candidates.values()].map(taskDocument));
|
|
69
|
+
const graphDiseases = domainBoard.diseases.map((disease) => ({
|
|
70
|
+
...disease,
|
|
71
|
+
path: disease.id === undefined ? "" : candidates.get(disease.id)?.source.path ?? "",
|
|
72
|
+
}));
|
|
73
|
+
const tasks = domainBoard.tasks.map((task) => ({
|
|
74
|
+
...task,
|
|
75
|
+
path: candidates.get(task.id).source.path,
|
|
76
|
+
}));
|
|
77
|
+
return {
|
|
78
|
+
tasks,
|
|
79
|
+
diseases: [...sourceDiseases, ...graphDiseases].sort(diseaseSort),
|
|
80
|
+
byId: new Map(tasks.map((task) => [task.id, task])),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Compatibility name for source-backed board consumers. */
|
|
84
|
+
export const buildTaskBoard = buildTaskSourceBoard;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
export const TASK_ID = /^k-[0-9a-f]{12}$/;
|
|
3
|
+
export const TASK_STATES = new Set(["open", "in_progress", "done", "drop"]);
|
|
4
|
+
const CANONICAL_ISO_UTC_MS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
5
|
+
export function invalid(message) {
|
|
6
|
+
throw new FlowError("INVALID_TARGET", message);
|
|
7
|
+
}
|
|
8
|
+
export function nonblank(field, value) {
|
|
9
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
10
|
+
return invalid(`task field '${field}' must be a nonblank string`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function fullId(field, value) {
|
|
15
|
+
const id = nonblank(field, value);
|
|
16
|
+
if (!TASK_ID.test(id))
|
|
17
|
+
invalid(`task field '${field}' must be a canonical full task id`);
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
20
|
+
export function canonicalTimestamp(field, value) {
|
|
21
|
+
const timestamp = nonblank(field, value);
|
|
22
|
+
if (!CANONICAL_ISO_UTC_MS.test(timestamp)) {
|
|
23
|
+
invalid(`task field '${field}' must be a canonical ISO UTC millisecond timestamp`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
if (new Date(timestamp).toISOString() !== timestamp) {
|
|
27
|
+
invalid(`task field '${field}' must be a canonical ISO UTC millisecond timestamp`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
invalid(`task field '${field}' must be a canonical ISO UTC millisecond timestamp`);
|
|
32
|
+
}
|
|
33
|
+
return timestamp;
|
|
34
|
+
}
|
|
35
|
+
export function dateTimestamp(field, value) {
|
|
36
|
+
try {
|
|
37
|
+
return canonicalTimestamp(field, value.toISOString());
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof FlowError)
|
|
41
|
+
throw error;
|
|
42
|
+
return invalid(`task field '${field}' must be a valid date`);
|
|
43
|
+
}
|
|
44
|
+
}
|