@astrosheep/keiyaku 4.5.7 → 4.5.9
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 +10 -0
- 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/opencode.js +3 -2
- package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +12 -11
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +33 -12
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-babysit/SKILL.md +35 -96
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +28 -49
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +21 -15
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +148 -89
- package/build/src/akuma/abort.d.ts +2 -1
- package/build/src/akuma/abort.js +6 -23
- package/build/src/akuma/akuma-handle.js +4 -2
- package/build/src/akuma/akuma-observe.d.ts +2 -0
- package/build/src/akuma/akuma-observe.js +1 -0
- package/build/src/akuma/akuma-product.js +9 -7
- package/build/src/akuma/akuma.d.ts +392 -9
- package/build/src/akuma/akuma.js +31 -1
- package/build/src/akuma/allowed.d.ts +1 -1
- package/build/src/akuma/allowed.js +1 -0
- package/build/src/akuma/archetype.js +1 -1
- package/build/src/akuma/body.d.ts +8 -5
- package/build/src/akuma/body.js +12 -5
- package/build/src/akuma/call-request.d.ts +80 -0
- package/build/src/akuma/call-request.js +211 -0
- package/build/src/akuma/heart/facts.d.ts +16 -78
- package/build/src/akuma/heart/facts.js +11 -0
- package/build/src/akuma/heart/index.d.ts +3 -2
- package/build/src/akuma/heart/index.js +4 -1
- package/build/src/akuma/heart/request-authority.d.ts +5 -4
- package/build/src/akuma/heart/request-authority.js +20 -34
- package/build/src/akuma/heart/request-rows.js +12 -23
- package/build/src/akuma/heart/schema.d.ts +1 -1
- package/build/src/akuma/heart/schema.js +5 -13
- package/build/src/akuma/index.d.ts +1 -1
- package/build/src/akuma/index.js +1 -1
- package/build/src/akuma/projection-read.d.ts +1 -0
- package/build/src/akuma/projection-read.js +8 -1
- package/build/src/akuma/projection.d.ts +907 -137
- package/build/src/akuma/projection.js +193 -9
- package/build/src/akuma/provider.d.ts +30 -3
- package/build/src/akuma/provider.js +111 -0
- package/build/src/akuma/providers/acp/core.d.ts +2 -2
- package/build/src/akuma/providers/acp/core.js +13 -6
- package/build/src/akuma/providers/acp/index.js +5 -4
- package/build/src/akuma/providers/claude/events.d.ts +2 -2
- package/build/src/akuma/providers/claude/events.js +16 -3
- package/build/src/akuma/providers/claude/index.d.ts +3 -3
- package/build/src/akuma/providers/claude/index.js +16 -5
- package/build/src/akuma/providers/codex-app-server/events.d.ts +2 -2
- package/build/src/akuma/providers/codex-app-server/events.js +25 -4
- package/build/src/akuma/providers/codex-app-server/index.js +18 -6
- package/build/src/akuma/providers/grok-build/index.js +5 -4
- package/build/src/akuma/providers/opencode-sdk/index.js +66 -25
- package/build/src/akuma/providers/opencode-sdk/session.d.ts +3 -2
- package/build/src/akuma/providers/opencode-sdk/session.js +28 -11
- package/build/src/akuma/providers/pi/index.js +72 -58
- package/build/src/akuma/request-lifecycle.d.ts +4 -10
- package/build/src/akuma/request-lifecycle.js +7 -5
- package/build/src/akuma/request-rendezvous.d.ts +26 -0
- package/build/src/akuma/request-rendezvous.js +100 -0
- package/build/src/akuma/request-serve.d.ts +6 -66
- package/build/src/akuma/request-serve.js +83 -210
- package/build/src/akuma/request-wire.d.ts +109 -115
- package/build/src/akuma/request-wire.js +63 -318
- package/build/src/akuma/requests.d.ts +19 -47
- package/build/src/akuma/requests.js +51 -107
- package/build/src/akuma/turn-drive.d.ts +5 -3
- package/build/src/akuma/turn-drive.js +39 -11
- package/build/src/akuma-body.d.ts +7 -2
- package/build/src/akuma-body.js +68 -46
- package/build/src/body/amend.js +18 -19
- package/build/src/cli/accepted.d.ts +1 -1
- package/build/src/cli/accepted.js +2 -2
- package/build/src/cli/commands/akuma-invoke.d.ts +2 -0
- package/build/src/cli/commands/akuma-invoke.js +17 -9
- package/build/src/cli/commands/akuma.js +8 -8
- package/build/src/cli/commands/contract-invoke.d.ts +3 -2
- package/build/src/cli/commands/contract-invoke.js +42 -30
- package/build/src/cli/commands/contract.d.ts +16 -20
- package/build/src/cli/commands/contract.js +58 -25
- package/build/src/cli/commands/install.d.ts +2 -1
- package/build/src/cli/commands/install.js +3 -2
- package/build/src/cli/commands/task-invoke.d.ts +3 -0
- package/build/src/cli/commands/task-invoke.js +23 -140
- package/build/src/cli/commands/task.js +54 -20
- package/build/src/cli/invoke.js +39 -25
- package/build/src/cli/main.js +2 -1
- package/build/src/cli/parse.d.ts +2 -2
- package/build/src/cli/parse.js +56 -27
- package/build/src/cli/render/akuma-activity.d.ts +3 -0
- package/build/src/cli/render/akuma-activity.js +45 -29
- package/build/src/cli/render/akuma-tool.d.ts +5 -1
- package/build/src/cli/render/akuma.js +4 -8
- package/build/src/cli/render/audit.js +1 -1
- package/build/src/cli/render/catalog.js +80 -21
- package/build/src/cli/render/contract-observation.d.ts +2 -0
- package/build/src/cli/render/contract-observation.js +11 -0
- package/build/src/cli/render/contract.js +35 -62
- package/build/src/cli/render/kanshi-akuma.d.ts +8 -0
- package/build/src/cli/render/kanshi-akuma.js +132 -0
- package/build/src/cli/render/kanshi.js +131 -213
- package/build/src/cli/render/receipt.d.ts +1 -2
- package/build/src/cli/render/receipt.js +76 -26
- package/build/src/cli/render/refusal.js +2 -10
- package/build/src/cli/render/region.js +5 -2
- package/build/src/cli/render/settings.d.ts +1 -1
- package/build/src/cli/render/settings.js +27 -8
- package/build/src/cli/render/task.js +22 -5
- package/build/src/cli/render/terminal.d.ts +32 -1
- package/build/src/cli/render/terminal.js +77 -1
- package/build/src/cli/result.d.ts +3 -3
- package/build/src/cli/runtime.js +4 -2
- package/build/src/cli/selectors.d.ts +2 -1
- package/build/src/cli/selectors.js +4 -3
- package/build/src/cli/square-edge.js +120 -36
- package/build/src/cli/usage.js +2 -1
- package/build/src/contract-guidance.d.ts +5 -0
- package/build/src/contract-guidance.js +121 -0
- package/build/src/contract-worktree.d.ts +0 -1
- package/build/src/contract-worktree.js +32 -83
- package/build/src/coordination/sqlite-transaction-lock.d.ts +1 -0
- package/build/src/coordination/sqlite-transaction-lock.js +5 -0
- package/build/src/core/facts/codec.d.ts +2 -0
- package/build/src/core/facts/codec.js +4 -0
- package/build/src/core/verbs/placement.d.ts +6 -1
- package/build/src/core/verbs/placement.js +5 -3
- package/build/src/dispatch/index.d.ts +0 -2
- package/build/src/dispatch/index.js +54 -55
- package/build/src/git/admission.js +2 -2
- package/build/src/git/hooks.d.ts +9 -15
- package/build/src/git/hooks.js +23 -317
- package/build/src/git/nuke.d.ts +2 -1
- package/build/src/git/nuke.js +39 -21
- package/build/src/git/private-state-seat.d.ts +12 -0
- package/build/src/git/private-state-seat.js +39 -0
- package/build/src/git/process.d.ts +6 -0
- package/build/src/git/process.js +26 -11
- package/build/src/git/read-observation.js +29 -20
- package/build/src/git/reconcile.d.ts +13 -1
- package/build/src/git/reconcile.js +70 -11
- package/build/src/git/ref-migration.d.ts +22 -0
- package/build/src/git/ref-migration.js +55 -0
- package/build/src/git/repository.d.ts +10 -6
- package/build/src/git/repository.js +16 -8
- package/build/src/git/target-placement.d.ts +1 -0
- package/build/src/git/target-placement.js +5 -0
- package/build/src/git/tender.d.ts +1 -0
- package/build/src/git/terminal-reconcile.d.ts +1 -1
- package/build/src/git/terminal-reconcile.js +22 -20
- package/build/src/git/workspace.d.ts +14 -0
- package/build/src/git/workspace.js +44 -0
- package/build/src/kanshi/index.d.ts +1 -1
- package/build/src/kanshi/read.js +16 -16
- package/build/src/kanshi/report.d.ts +2 -2
- package/build/src/library/address.d.ts +9 -3
- package/build/src/library/address.js +3 -2
- package/build/src/library/akuma-creation.d.ts +4 -3
- package/build/src/library/akuma-creation.js +19 -13
- package/build/src/library/audit.d.ts +14 -6
- package/build/src/library/audit.js +13 -23
- package/build/src/library/catalog.d.ts +3 -1
- package/build/src/library/catalog.js +20 -6
- package/build/src/library/composition.d.ts +385 -0
- package/build/src/library/composition.js +28 -0
- package/build/src/library/configuration.js +8 -2
- package/build/src/library/continuation.js +66 -30
- package/build/src/library/contract-bind.js +10 -2
- package/build/src/library/contract-forwarding-reconciliation-result.d.ts +102 -0
- package/build/src/library/contract-forwarding-reconciliation-result.js +92 -0
- package/build/src/library/contract-forwarding-result.d.ts +4464 -0
- package/build/src/library/contract-forwarding-result.js +521 -0
- package/build/src/library/contract-forwarding.d.ts +71 -0
- package/build/src/library/contract-forwarding.js +114 -0
- package/build/src/library/contract-handle.d.ts +38 -0
- package/build/src/library/contract-handle.js +314 -0
- package/build/src/library/contract-operations.d.ts +214 -69
- package/build/src/library/contract-operations.js +189 -118
- package/build/src/library/contract-types.d.ts +7 -3
- package/build/src/library/contract.d.ts +21 -34
- package/build/src/library/contract.js +29 -287
- package/build/src/library/fleet-result.d.ts +1580 -0
- package/build/src/library/fleet-result.js +98 -0
- package/build/src/library/fleet.d.ts +112 -54
- package/build/src/library/fleet.js +211 -71
- package/build/src/library/input.d.ts +3 -1
- package/build/src/library/input.js +11 -1
- package/build/src/library/keiyaku.d.ts +388 -20
- package/build/src/library/keiyaku.js +13 -21
- package/build/src/library/mutation.d.ts +3 -3
- package/build/src/library/mutation.js +5 -9
- package/build/src/library/nuke.d.ts +1 -1
- package/build/src/library/nuke.js +4 -3
- package/build/src/library/reconcile.d.ts +4 -0
- package/build/src/library/reconcile.js +2 -0
- package/build/src/library/refusal.d.ts +0 -9
- package/build/src/library/refusal.js +0 -7
- package/build/src/protocol/amend.js +56 -50
- package/build/src/protocol/attempt.d.ts +2 -0
- package/build/src/protocol/attempt.js +8 -1
- package/build/src/protocol/completion.js +7 -0
- package/build/src/protocol/deliver.js +6 -2
- package/build/src/protocol/operations.d.ts +4 -1
- package/build/src/protocol/operations.js +5 -2
- package/build/src/protocol/placement.d.ts +1 -0
- package/build/src/protocol/placement.js +52 -38
- package/build/src/protocol/read/observation.d.ts +1 -4
- package/build/src/protocol/read/observation.js +2 -15
- package/build/src/protocol/reintegrate.js +62 -58
- package/build/src/protocol/review.js +5 -0
- package/build/src/protocol/run.js +17 -13
- package/build/src/runtime/proc/run.d.ts +9 -0
- package/build/src/runtime/proc/run.js +51 -4
- package/build/src/runtime/proc/termination.js +26 -22
- package/build/src/runtime/proc/windows-launch.exe +0 -0
- package/build/src/settlement/holder.d.ts +2 -1
- package/build/src/settlement/holder.js +15 -4
- package/build/src/settlement/settle.js +99 -33
- package/build/src/task/board.d.ts +68 -27
- package/build/src/task/board.js +31 -1
- package/build/src/task/catalog.d.ts +4 -0
- package/build/src/task/catalog.js +23 -0
- package/build/src/task/compose-language.js +33 -7
- package/build/src/task/compose-parser.d.ts +1 -1
- package/build/src/task/compose-parser.js +1 -1
- package/build/src/task/compose.js +2 -0
- package/build/src/task/index.d.ts +12 -4
- package/build/src/task/index.js +97 -16
- package/build/src/task/mutation-result.d.ts +923 -0
- package/build/src/task/mutation-result.js +140 -0
- package/build/src/task/mutation.d.ts +552 -28
- package/build/src/task/mutation.js +304 -97
- package/build/src/task/operations.d.ts +1 -4
- package/build/src/task/operations.js +0 -6
- package/build/src/task/public.d.ts +2 -0
- package/build/src/task/public.js +1 -0
- package/build/src/workspace-place.d.ts +7 -2
- package/build/src/workspace-place.js +34 -24
- package/build/src/world.d.ts +1 -0
- package/build/src/world.js +11 -0
- package/package.json +8 -6
- package/build/src/akuma/request-execution.d.ts +0 -8
- package/build/src/akuma/request-execution.js +0 -98
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extendAdmissionPathsAt, observeContractsForAdmissionAt } from "../git/observe.js";
|
|
2
|
+
import { withPrivateStatePublicationSeat } from "../git/private-state-seat.js";
|
|
2
3
|
import { admitDecidedOffer } from "./attempt.js";
|
|
3
4
|
/** Form one decided offer without admitting or reinterpreting it. */
|
|
4
5
|
export async function prepareProtocolAttempt(input, baseAttempt) {
|
|
@@ -41,20 +42,23 @@ export async function prepareProtocolAttempt(input, baseAttempt) {
|
|
|
41
42
|
export async function runProtocol(input) {
|
|
42
43
|
const attempts = input.attempts;
|
|
43
44
|
for (let index = 0; index < attempts.length; index += 1) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
const result = await withPrivateStatePublicationSeat(input.repository, async (seat) => {
|
|
46
|
+
const prepared = await prepareProtocolAttempt(input, attempts[index]);
|
|
47
|
+
if (prepared.kind === "refused")
|
|
48
|
+
return prepared;
|
|
49
|
+
return await admitDecidedOffer({
|
|
50
|
+
channel: input.channel,
|
|
51
|
+
repository: input.repository,
|
|
52
|
+
seat,
|
|
53
|
+
decisionObservation: prepared.observation,
|
|
54
|
+
attempt: prepared.attempt,
|
|
55
|
+
offer: prepared.offer,
|
|
56
|
+
primaryContract: input.input.contractId,
|
|
57
|
+
assertions: prepared.assertions,
|
|
58
|
+
...(input.validateAdmission === undefined ? {} : { validateAdmission: input.validateAdmission }),
|
|
59
|
+
});
|
|
56
60
|
});
|
|
57
|
-
if (result.kind === "
|
|
61
|
+
if (result.kind === "refused" || result.kind === "accepted" || result.kind === "publication-failed")
|
|
58
62
|
return result;
|
|
59
63
|
if (result.kind === "collision" && index + 1 === attempts.length)
|
|
60
64
|
return result;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
1
2
|
import { type DetachedProcessInput } from "./launch.js";
|
|
2
3
|
import type { OwnedProcess } from "./types.js";
|
|
3
4
|
export type { DetachedProcessInput } from "./launch.js";
|
|
@@ -8,6 +9,14 @@ type ProcessLaunch = Readonly<{
|
|
|
8
9
|
readonly env?: NodeJS.ProcessEnv;
|
|
9
10
|
readonly signal?: AbortSignal;
|
|
10
11
|
}>;
|
|
12
|
+
export type CancellableProcess = Readonly<{
|
|
13
|
+
child: ChildProcess;
|
|
14
|
+
cancelled(): boolean;
|
|
15
|
+
terminationFailure: Promise<never>;
|
|
16
|
+
terminate(force?: boolean): Promise<void>;
|
|
17
|
+
waitTermination(): Promise<void>;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function spawnCancellableProcess(input: ProcessLaunch): CancellableProcess;
|
|
11
20
|
export type ProcessInput = ProcessLaunch & Readonly<{
|
|
12
21
|
readonly timeoutMs?: number;
|
|
13
22
|
}>;
|
|
@@ -7,6 +7,49 @@ import { createProcessLifecycle } from "./lifecycle.js";
|
|
|
7
7
|
import { terminateOwnedProcess } from "./termination.js";
|
|
8
8
|
import { spawnWindowsRetainedProcess } from "./windows-run.js";
|
|
9
9
|
const STREAM_TAIL_BYTES = 16 * 1024;
|
|
10
|
+
function waitForProcessEnd(child, terminationFailure) {
|
|
11
|
+
const childEnd = new Promise((resolve) => {
|
|
12
|
+
child.once("close", (code) => resolve({ kind: "closed", code }));
|
|
13
|
+
child.once("error", (error) => resolve({ kind: "spawn-error", error }));
|
|
14
|
+
});
|
|
15
|
+
return Promise.race([childEnd, terminationFailure]);
|
|
16
|
+
}
|
|
17
|
+
export function spawnCancellableProcess(input) {
|
|
18
|
+
const child = spawn(input.argv[0], input.argv.slice(1), spawnOptionsFor(input, ["pipe", "pipe", "pipe"]));
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
let termination;
|
|
21
|
+
let reportTerminationFailure;
|
|
22
|
+
const terminationFailure = new Promise((_resolve, reject) => {
|
|
23
|
+
reportTerminationFailure = reject;
|
|
24
|
+
});
|
|
25
|
+
void terminationFailure.catch(() => undefined);
|
|
26
|
+
const terminate = (force = false) => {
|
|
27
|
+
if (termination === undefined) {
|
|
28
|
+
termination = terminateOwnedProcess(child, force);
|
|
29
|
+
void termination.catch(reportTerminationFailure);
|
|
30
|
+
}
|
|
31
|
+
return termination;
|
|
32
|
+
};
|
|
33
|
+
const cancel = () => {
|
|
34
|
+
if (cancelled)
|
|
35
|
+
return;
|
|
36
|
+
cancelled = true;
|
|
37
|
+
void terminate();
|
|
38
|
+
};
|
|
39
|
+
input.signal?.addEventListener("abort", cancel, { once: true });
|
|
40
|
+
if (input.signal?.aborted === true)
|
|
41
|
+
cancel();
|
|
42
|
+
return {
|
|
43
|
+
child,
|
|
44
|
+
cancelled: () => cancelled,
|
|
45
|
+
terminationFailure,
|
|
46
|
+
terminate,
|
|
47
|
+
async waitTermination() {
|
|
48
|
+
input.signal?.removeEventListener("abort", cancel);
|
|
49
|
+
await termination;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
10
53
|
function tailCapture(limit) {
|
|
11
54
|
const bytes = Buffer.allocUnsafe(limit);
|
|
12
55
|
let total = 0;
|
|
@@ -116,11 +159,16 @@ async function executeProcess(input, timeoutMs, consumeStdout, spawnProcess = sp
|
|
|
116
159
|
let stop;
|
|
117
160
|
let streamError;
|
|
118
161
|
let termination;
|
|
162
|
+
let reportTerminationFailure;
|
|
163
|
+
const terminationFailure = new Promise((resolve) => {
|
|
164
|
+
reportTerminationFailure = (error) => resolve({ kind: "termination-error", error });
|
|
165
|
+
});
|
|
119
166
|
const requestStop = (reason) => {
|
|
120
167
|
if (stop !== undefined)
|
|
121
168
|
return;
|
|
122
169
|
stop = reason;
|
|
123
170
|
termination = terminateOwnedProcess(child);
|
|
171
|
+
void termination.catch(reportTerminationFailure);
|
|
124
172
|
};
|
|
125
173
|
const failStream = (error) => {
|
|
126
174
|
if (streamError !== undefined)
|
|
@@ -148,13 +196,12 @@ async function executeProcess(input, timeoutMs, consumeStdout, spawnProcess = sp
|
|
|
148
196
|
const cancel = () => requestStop("cancelled");
|
|
149
197
|
input.signal?.addEventListener("abort", cancel, { once: true });
|
|
150
198
|
const timeout = timeoutMs === undefined ? undefined : setTimeout(() => requestStop("timeout"), timeoutMs);
|
|
151
|
-
const terminal = await
|
|
152
|
-
child.once("close", (code) => resolve({ kind: "closed", code }));
|
|
153
|
-
child.once("error", (error) => resolve({ kind: "spawn-error", error }));
|
|
154
|
-
});
|
|
199
|
+
const terminal = await waitForProcessEnd(child, terminationFailure);
|
|
155
200
|
if (timeout !== undefined)
|
|
156
201
|
clearTimeout(timeout);
|
|
157
202
|
input.signal?.removeEventListener("abort", cancel);
|
|
203
|
+
if (terminal.kind === "termination-error")
|
|
204
|
+
throw terminal.error;
|
|
158
205
|
await termination;
|
|
159
206
|
const pid = child.pid ?? null;
|
|
160
207
|
if (terminal.kind === "spawn-error") {
|
|
@@ -5,9 +5,25 @@ const TERMINATION_GRACE_MS = 250;
|
|
|
5
5
|
const WINDOWS_TERMINATION_SETTLE_MS = 250;
|
|
6
6
|
const WINDOWS_TERMINATION_TIMEOUT_MS = 1_000;
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function isMissingProcess(error) {
|
|
9
|
+
return error.code === "ESRCH";
|
|
10
|
+
}
|
|
11
|
+
async function signalOwnedProcessGroup(pid, signal, exit) {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(-pid, signal);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (isMissingProcess(error))
|
|
17
|
+
return;
|
|
18
|
+
if (error.code !== "EPERM")
|
|
19
|
+
throw error;
|
|
20
|
+
const exited = await Promise.race([
|
|
21
|
+
exit.then(() => true),
|
|
22
|
+
delay(TERMINATION_GRACE_MS, false, { ref: false }).then(() => false),
|
|
23
|
+
]);
|
|
24
|
+
if (!exited)
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
11
27
|
}
|
|
12
28
|
export async function terminateWindowsTree(pid) {
|
|
13
29
|
try {
|
|
@@ -53,6 +69,9 @@ export async function terminateOwnedProcess(child, force = false) {
|
|
|
53
69
|
resolve();
|
|
54
70
|
});
|
|
55
71
|
});
|
|
72
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
73
|
+
if (exited || child.exitCode !== null || child.signalCode !== null)
|
|
74
|
+
return;
|
|
56
75
|
if (process.platform === "win32") {
|
|
57
76
|
try {
|
|
58
77
|
await terminateWindowsTree(pid);
|
|
@@ -64,31 +83,16 @@ export async function terminateOwnedProcess(child, force = false) {
|
|
|
64
83
|
return;
|
|
65
84
|
}
|
|
66
85
|
if (force) {
|
|
67
|
-
|
|
68
|
-
process.kill(-pid, "SIGKILL");
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
ignoreMissingProcess(error);
|
|
72
|
-
}
|
|
86
|
+
await signalOwnedProcessGroup(pid, "SIGKILL", exit);
|
|
73
87
|
await exit;
|
|
74
88
|
return;
|
|
75
89
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
ignoreMissingProcess(error);
|
|
81
|
-
await exit;
|
|
90
|
+
await signalOwnedProcessGroup(pid, "SIGTERM", exit);
|
|
91
|
+
if (exited || child.exitCode !== null || child.signalCode !== null)
|
|
82
92
|
return;
|
|
83
|
-
}
|
|
84
93
|
await Promise.race([exit, delay(TERMINATION_GRACE_MS)]);
|
|
85
94
|
if (exited || child.exitCode !== null || child.signalCode !== null)
|
|
86
95
|
return;
|
|
87
|
-
|
|
88
|
-
process.kill(-pid, "SIGKILL");
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
ignoreMissingProcess(error);
|
|
92
|
-
}
|
|
96
|
+
await signalOwnedProcessGroup(pid, "SIGKILL", exit);
|
|
93
97
|
await exit;
|
|
94
98
|
}
|
|
Binary file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TreeUpdate } from "../core/facts/offer.js";
|
|
2
2
|
import { type ContractId } from "../core/facts/types.js";
|
|
3
3
|
import type { GitDecisionObservation } from "../git/observe.js";
|
|
4
|
+
import { type PrivateStatePublicationSeat } from "../git/private-state-seat.js";
|
|
4
5
|
import type { GitRepository } from "../git/process.js";
|
|
5
6
|
import { type GitDecodeChannel, type GitReadObservation, type GitTreeSelection } from "../git/read-observation.js";
|
|
6
7
|
import { type TaskId } from "../task/identity.js";
|
|
@@ -37,7 +38,7 @@ export type TaskHolderReleasePublication = Readonly<{
|
|
|
37
38
|
diagnostic: string;
|
|
38
39
|
}>;
|
|
39
40
|
/** Publish the released holder against the same frozen observation that decided the release. */
|
|
40
|
-
export declare function publishTaskHolderRelease(repository: GitRepository, channel: GitDecodeChannel, observation: GitDecisionObservation, owner: ContractId): Promise<TaskHolderReleasePublication>;
|
|
41
|
+
export declare function publishTaskHolderRelease(repository: GitRepository, channel: GitDecodeChannel, observation: GitDecisionObservation, owner: ContractId, seat: PrivateStatePublicationSeat): Promise<TaskHolderReleasePublication>;
|
|
41
42
|
export declare function readTaskHolderProjectionFromDecision(channel: GitDecodeChannel, observation: GitDecisionObservation): Promise<TaskHolderProjection>;
|
|
42
43
|
export declare function observeTaskHolderProjection(repository: GitRepository, channel: GitDecodeChannel): Promise<TaskHolderProjection>;
|
|
43
44
|
export declare function releaseTaskHolderWithFence<T extends AdmissionOutcome>(repository: GitRepository, channel: GitDecodeChannel, owner: ContractId, action: () => T | Promise<T>): Promise<TaskHolderAdmission<T>>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { AuthorityCorruptionError } from "../core/facts/errors.js";
|
|
3
3
|
import { contractId } from "../core/facts/types.js";
|
|
4
|
-
import { GIT_REF, updateGitTree, updateRefsAtomically, writeBlob,
|
|
4
|
+
import { GIT_REF, updateGitTree, updateRefsAtomically, writeBlob, writeStateCommit } from "../git/repository.js";
|
|
5
|
+
import { confirmPrivateStatePublication } from "../git/private-state-seat.js";
|
|
5
6
|
import { withGitTargetedReadObservation, } from "../git/read-observation.js";
|
|
6
7
|
import { parseTaskId } from "../task/identity.js";
|
|
7
8
|
import { acquireTaskSettlementFence } from "./fence.js";
|
|
@@ -121,13 +122,17 @@ export async function releaseTaskHolder(channel, observation, owner) {
|
|
|
121
122
|
const current = (await readTaskHolderProjectionFromDecision(channel, observation)).get(owner) ?? null;
|
|
122
123
|
return current === null || current.disposition !== "held" ? null : update({ ...current, disposition: "released" });
|
|
123
124
|
}
|
|
125
|
+
async function releasedHolderIsCurrent(repository, channel, owner, expected) {
|
|
126
|
+
const current = (await observeTaskHolderProjection(repository, channel)).get(owner) ?? null;
|
|
127
|
+
return current?.disposition === "released" && Buffer.from(canonicalBytes(current)).equals(expected);
|
|
128
|
+
}
|
|
124
129
|
/** Publish the released holder against the same frozen observation that decided the release. */
|
|
125
|
-
export async function publishTaskHolderRelease(repository, channel, observation, owner) {
|
|
130
|
+
export async function publishTaskHolderRelease(repository, channel, observation, owner, seat) {
|
|
126
131
|
const release = await releaseTaskHolder(channel, observation, owner);
|
|
127
132
|
if (release === null)
|
|
128
133
|
return { kind: "not-held" };
|
|
129
134
|
const tree = await updateGitTree(repository, observation.admission.snapshot.tree, new Map([[release.path, { oid: await writeBlob(repository, release.bytes) }]]));
|
|
130
|
-
const commit = await
|
|
135
|
+
const commit = await writeStateCommit({
|
|
131
136
|
repository,
|
|
132
137
|
tree,
|
|
133
138
|
parent: observation.admission.snapshot.commit,
|
|
@@ -140,8 +145,14 @@ export async function publishTaskHolderRelease(repository, channel, observation,
|
|
|
140
145
|
expectedOid: observation.admission.snapshot.commit,
|
|
141
146
|
},
|
|
142
147
|
]);
|
|
143
|
-
if (publication.kind === "published")
|
|
148
|
+
if (publication.kind === "published") {
|
|
149
|
+
confirmPrivateStatePublication(seat);
|
|
150
|
+
return { kind: "released" };
|
|
151
|
+
}
|
|
152
|
+
if (publication.kind === "unknown" && (await releasedHolderIsCurrent(repository, channel, owner, release.bytes))) {
|
|
153
|
+
confirmPrivateStatePublication(seat);
|
|
144
154
|
return { kind: "released" };
|
|
155
|
+
}
|
|
145
156
|
return {
|
|
146
157
|
kind: "non-published",
|
|
147
158
|
diagnostic: publication.kind === "non-published"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { contractSegment } from "../core/facts/types.js";
|
|
2
2
|
import { observeContractsForAdmissionAt } from "../git/observe.js";
|
|
3
|
+
import { withPrivateStatePublicationSeat } from "../git/private-state-seat.js";
|
|
3
4
|
import { repairNamespaceContext } from "../task/context.js";
|
|
4
5
|
import { settleTask } from "../task/operations.js";
|
|
5
6
|
import { publishTaskHolderRelease, readTaskHolderProjectionFromDecision, taskHolderObservationSelection, } from "./holder.js";
|
|
7
|
+
import { acquireTaskSettlementFence } from "./fence.js";
|
|
6
8
|
import { World } from "../world.js";
|
|
7
9
|
export function deferredTaskHolderSettlement(input) {
|
|
8
10
|
return { actions: [], lags: [{ kind: "settlement-failed", surface: "task-holder", ...input }] };
|
|
@@ -18,11 +20,16 @@ function taskFailure(result) {
|
|
|
18
20
|
? `Task settlement requires retry: ${result.reason}`
|
|
19
21
|
: `Task settlement refused: ${JSON.stringify(result.refusal)}`;
|
|
20
22
|
}
|
|
21
|
-
async function
|
|
22
|
-
const { repository, channel,
|
|
23
|
+
async function observeApplicableHolder(input) {
|
|
24
|
+
const { repository, channel, candidate, lags } = input;
|
|
23
25
|
let observation;
|
|
24
26
|
try {
|
|
25
27
|
observation = await observeContractsForAdmissionAt(repository, channel, [candidate.id], taskHolderObservationSelection());
|
|
28
|
+
const state = observation.journals.get(candidate.id)?.state ?? null;
|
|
29
|
+
if (state === null || state.terminal?.kind !== "claimed")
|
|
30
|
+
return null;
|
|
31
|
+
const holder = (await readTaskHolderProjectionFromDecision(channel, observation)).get(candidate.id) ?? null;
|
|
32
|
+
return holder?.disposition === "held" ? holder : null;
|
|
26
33
|
}
|
|
27
34
|
catch (error) {
|
|
28
35
|
lags.push({
|
|
@@ -31,27 +38,25 @@ async function settleTasks(input) {
|
|
|
31
38
|
contractId: candidate.id,
|
|
32
39
|
diagnostic: diagnostic(error),
|
|
33
40
|
});
|
|
34
|
-
return;
|
|
41
|
+
return null;
|
|
35
42
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let
|
|
43
|
+
}
|
|
44
|
+
async function completeHeldTask(input) {
|
|
45
|
+
const { repository, candidate, actions, lags, taskId } = input;
|
|
46
|
+
let world;
|
|
40
47
|
try {
|
|
41
|
-
|
|
48
|
+
world = await World.at(repository.primaryWorktree);
|
|
42
49
|
}
|
|
43
50
|
catch (error) {
|
|
44
51
|
lags.push({
|
|
45
52
|
kind: "settlement-failed",
|
|
46
|
-
surface: "task
|
|
53
|
+
surface: "task",
|
|
47
54
|
contractId: candidate.id,
|
|
55
|
+
taskId,
|
|
48
56
|
diagnostic: diagnostic(error),
|
|
49
57
|
});
|
|
50
|
-
return;
|
|
58
|
+
return false;
|
|
51
59
|
}
|
|
52
|
-
if (holder === null || holder.disposition !== "held")
|
|
53
|
-
return;
|
|
54
|
-
const taskId = holder.taskId;
|
|
55
60
|
let result;
|
|
56
61
|
try {
|
|
57
62
|
result = await settleTask(world, taskId);
|
|
@@ -64,44 +69,104 @@ async function settleTasks(input) {
|
|
|
64
69
|
taskId,
|
|
65
70
|
diagnostic: diagnostic(error),
|
|
66
71
|
});
|
|
67
|
-
return;
|
|
72
|
+
return false;
|
|
68
73
|
}
|
|
69
74
|
if (result.kind === "changed")
|
|
70
75
|
actions.push({ kind: "task", taskId, action: "done" });
|
|
71
|
-
|
|
76
|
+
if (result.kind === "changed" || result.kind === "unchanged")
|
|
77
|
+
return true;
|
|
78
|
+
lags.push({
|
|
79
|
+
kind: "settlement-failed",
|
|
80
|
+
surface: "task",
|
|
81
|
+
contractId: candidate.id,
|
|
82
|
+
taskId,
|
|
83
|
+
diagnostic: taskFailure(result),
|
|
84
|
+
});
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
async function releaseHeldTaskHolder(input) {
|
|
88
|
+
const { repository, channel, candidate, lags, taskId } = input;
|
|
89
|
+
try {
|
|
90
|
+
await withPrivateStatePublicationSeat(repository, async (seat) => {
|
|
91
|
+
const observation = await observeContractsForAdmissionAt(repository, channel, [candidate.id], taskHolderObservationSelection());
|
|
92
|
+
const state = observation.journals.get(candidate.id)?.state ?? null;
|
|
93
|
+
if (state === null || state.terminal?.kind !== "claimed")
|
|
94
|
+
return;
|
|
95
|
+
const holder = (await readTaskHolderProjectionFromDecision(channel, observation)).get(candidate.id) ?? null;
|
|
96
|
+
if (holder === null || holder.disposition !== "held" || holder.taskId !== taskId)
|
|
97
|
+
return;
|
|
98
|
+
const publication = await publishTaskHolderRelease(repository, channel, observation, candidate.id, seat);
|
|
99
|
+
if (publication.kind === "non-published") {
|
|
100
|
+
lags.push({
|
|
101
|
+
kind: "settlement-failed",
|
|
102
|
+
surface: "task-holder",
|
|
103
|
+
contractId: candidate.id,
|
|
104
|
+
taskId,
|
|
105
|
+
diagnostic: `Task holder release requires retry: ${publication.diagnostic}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
72
111
|
lags.push({
|
|
73
112
|
kind: "settlement-failed",
|
|
74
|
-
surface: "task",
|
|
113
|
+
surface: "task-holder",
|
|
75
114
|
contractId: candidate.id,
|
|
76
115
|
taskId,
|
|
77
|
-
diagnostic:
|
|
116
|
+
diagnostic: diagnostic(error),
|
|
78
117
|
});
|
|
79
|
-
return;
|
|
80
118
|
}
|
|
119
|
+
}
|
|
120
|
+
async function settleTasks(input) {
|
|
121
|
+
const { repository, channel, candidate, actions, lags } = input;
|
|
122
|
+
const hint = await observeApplicableHolder({ repository, channel, candidate, lags });
|
|
123
|
+
if (hint === null)
|
|
124
|
+
return false;
|
|
125
|
+
const taskId = hint.taskId;
|
|
126
|
+
let fence;
|
|
81
127
|
try {
|
|
82
|
-
|
|
83
|
-
if (publication.kind === "non-published") {
|
|
84
|
-
lags.push({
|
|
85
|
-
kind: "settlement-failed",
|
|
86
|
-
surface: "task-holder",
|
|
87
|
-
contractId: candidate.id,
|
|
88
|
-
taskId,
|
|
89
|
-
diagnostic: `Task holder release requires retry: ${publication.diagnostic}`,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
128
|
+
fence = await acquireTaskSettlementFence(repository, taskId);
|
|
92
129
|
}
|
|
93
130
|
catch (error) {
|
|
94
131
|
lags.push({
|
|
95
132
|
kind: "settlement-failed",
|
|
96
|
-
surface: "task
|
|
133
|
+
surface: "task",
|
|
97
134
|
contractId: candidate.id,
|
|
98
135
|
taskId,
|
|
99
136
|
diagnostic: diagnostic(error),
|
|
100
137
|
});
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
let taskSettled = false;
|
|
141
|
+
try {
|
|
142
|
+
const holder = await observeApplicableHolder({ repository, channel, candidate, lags });
|
|
143
|
+
if (holder === null || holder.taskId !== taskId)
|
|
144
|
+
return false;
|
|
145
|
+
if (!(await completeHeldTask({ repository, candidate, actions, lags, taskId })))
|
|
146
|
+
return true;
|
|
147
|
+
taskSettled = true;
|
|
148
|
+
await releaseHeldTaskHolder({ repository, channel, candidate, taskId, lags });
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
try {
|
|
153
|
+
await fence.close();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (!taskSettled)
|
|
157
|
+
throw error;
|
|
158
|
+
lags.push({
|
|
159
|
+
kind: "settlement-failed",
|
|
160
|
+
surface: "task-holder",
|
|
161
|
+
contractId: candidate.id,
|
|
162
|
+
taskId,
|
|
163
|
+
diagnostic: diagnostic(error),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
101
166
|
}
|
|
102
167
|
}
|
|
103
168
|
async function settleNamespace(state, effects, actions, lags) {
|
|
104
|
-
if (state.
|
|
169
|
+
if (state.coordinates.workspace !== "worktree")
|
|
105
170
|
return;
|
|
106
171
|
const worktrees = effects.filter((effect) => effect.kind === "worktree" && effect.action !== "removed");
|
|
107
172
|
for (const effect of worktrees) {
|
|
@@ -127,12 +192,12 @@ async function settleObserved(input) {
|
|
|
127
192
|
if (input.state === null)
|
|
128
193
|
return { actions: [], lags: [] };
|
|
129
194
|
const actions = [], lags = [];
|
|
195
|
+
let holderApplies = false;
|
|
130
196
|
if (input.state.terminal?.kind === "claimed") {
|
|
131
197
|
try {
|
|
132
|
-
await settleTasks({
|
|
198
|
+
holderApplies = await settleTasks({
|
|
133
199
|
repository: input.repository,
|
|
134
200
|
channel: input.channel,
|
|
135
|
-
world: await World.at(input.repository.primaryWorktree),
|
|
136
201
|
candidate: input.state,
|
|
137
202
|
actions,
|
|
138
203
|
lags,
|
|
@@ -147,7 +212,8 @@ async function settleObserved(input) {
|
|
|
147
212
|
});
|
|
148
213
|
}
|
|
149
214
|
}
|
|
150
|
-
|
|
215
|
+
if (holderApplies)
|
|
216
|
+
await settleNamespace(input.state, input.effects, actions, lags);
|
|
151
217
|
return { actions, lags };
|
|
152
218
|
}
|
|
153
219
|
function onPrimaryWorktree(repository) {
|
|
@@ -1,24 +1,65 @@
|
|
|
1
1
|
import type { TaskDocument, TaskPriority, TaskState } from "./document.js";
|
|
2
2
|
import { type TaskId } from "./identity.js";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
export type TaskRef = Readonly<{
|
|
4
5
|
id: TaskId;
|
|
5
6
|
title: string | null;
|
|
6
7
|
state: TaskState | "missing";
|
|
7
8
|
}>;
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
children?: Readonly<{
|
|
18
|
-
live: number;
|
|
19
|
-
total: number;
|
|
9
|
+
export declare const taskRowSchema: z.ZodObject<{
|
|
10
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<`task/${string}`, string>>;
|
|
11
|
+
title: z.ZodString;
|
|
12
|
+
state: z.ZodEnum<{
|
|
13
|
+
open: "open";
|
|
14
|
+
in_progress: "in_progress";
|
|
15
|
+
on_hold: "on_hold";
|
|
16
|
+
done: "done";
|
|
17
|
+
drop: "drop";
|
|
20
18
|
}>;
|
|
21
|
-
|
|
19
|
+
priority: z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>;
|
|
20
|
+
disposition: z.ZodEnum<{
|
|
21
|
+
in_progress: "in_progress";
|
|
22
|
+
on_hold: "on_hold";
|
|
23
|
+
done: "done";
|
|
24
|
+
drop: "drop";
|
|
25
|
+
ready: "ready";
|
|
26
|
+
blocked: "blocked";
|
|
27
|
+
}>;
|
|
28
|
+
updatedAt: z.ZodString;
|
|
29
|
+
bodyPresent: z.ZodBoolean;
|
|
30
|
+
children: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
live: z.ZodNumber;
|
|
32
|
+
total: z.ZodNumber;
|
|
33
|
+
}, z.core.$strict>>;
|
|
34
|
+
}, z.core.$strict>;
|
|
35
|
+
export declare const taskRowsSchema: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
36
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<`task/${string}`, string>>;
|
|
37
|
+
title: z.ZodString;
|
|
38
|
+
state: z.ZodEnum<{
|
|
39
|
+
open: "open";
|
|
40
|
+
in_progress: "in_progress";
|
|
41
|
+
on_hold: "on_hold";
|
|
42
|
+
done: "done";
|
|
43
|
+
drop: "drop";
|
|
44
|
+
}>;
|
|
45
|
+
priority: z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>;
|
|
46
|
+
disposition: z.ZodEnum<{
|
|
47
|
+
in_progress: "in_progress";
|
|
48
|
+
on_hold: "on_hold";
|
|
49
|
+
done: "done";
|
|
50
|
+
drop: "drop";
|
|
51
|
+
ready: "ready";
|
|
52
|
+
blocked: "blocked";
|
|
53
|
+
}>;
|
|
54
|
+
updatedAt: z.ZodString;
|
|
55
|
+
bodyPresent: z.ZodBoolean;
|
|
56
|
+
children: z.ZodOptional<z.ZodObject<{
|
|
57
|
+
live: z.ZodNumber;
|
|
58
|
+
total: z.ZodNumber;
|
|
59
|
+
}, z.core.$strict>>;
|
|
60
|
+
}, z.core.$strict>>>;
|
|
61
|
+
export type TaskDisposition = z.infer<typeof taskRowSchema>["disposition"];
|
|
62
|
+
export type TaskRow = z.infer<typeof taskRowSchema>;
|
|
22
63
|
export type BlockedTaskRow = TaskRow & Readonly<{
|
|
23
64
|
blockers: readonly TaskRef[];
|
|
24
65
|
}>;
|
|
@@ -74,35 +115,35 @@ export declare function projectDetailFacts(board: TaskBoard, id: TaskId, relatio
|
|
|
74
115
|
export declare function projectTaskRow(board: TaskBoard, relations: TaskRelationProjection, task: TaskDocument): TaskRow;
|
|
75
116
|
export declare function projectRows(board: TaskBoard, relations: TaskRelationProjection, scope: readonly string[] | null, selection: "active" | "closed" | "all"): readonly TaskRow[];
|
|
76
117
|
export declare function projectBlocked(board: TaskBoard, scope: readonly string[] | null, relations: TaskRelationProjection): readonly BlockedTaskRow[];
|
|
77
|
-
export declare function projectStatusRows(board: TaskBoard, relations: TaskRelationProjection, scope: readonly string[] | null): (
|
|
78
|
-
id:
|
|
118
|
+
export declare function projectStatusRows(board: TaskBoard, relations: TaskRelationProjection, scope: readonly string[] | null): ({
|
|
119
|
+
id: `task/${string}`;
|
|
79
120
|
title: string;
|
|
80
|
-
state:
|
|
81
|
-
priority:
|
|
82
|
-
disposition:
|
|
121
|
+
state: "open" | "in_progress" | "on_hold" | "done" | "drop";
|
|
122
|
+
priority: 0 | 1 | 2 | 3;
|
|
123
|
+
disposition: "in_progress" | "on_hold" | "done" | "drop" | "ready" | "blocked";
|
|
83
124
|
updatedAt: string;
|
|
84
125
|
bodyPresent: boolean;
|
|
85
|
-
children?:
|
|
126
|
+
children?: {
|
|
86
127
|
live: number;
|
|
87
128
|
total: number;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
129
|
+
} | undefined;
|
|
130
|
+
} | {
|
|
90
131
|
blockers: readonly Readonly<{
|
|
91
132
|
id: TaskId;
|
|
92
133
|
title: string | null;
|
|
93
134
|
state: TaskState | "missing";
|
|
94
135
|
}>[];
|
|
95
|
-
id:
|
|
136
|
+
id: `task/${string}`;
|
|
96
137
|
title: string;
|
|
97
|
-
state:
|
|
98
|
-
priority:
|
|
99
|
-
disposition:
|
|
138
|
+
state: "open" | "in_progress" | "on_hold" | "done" | "drop";
|
|
139
|
+
priority: 0 | 1 | 2 | 3;
|
|
140
|
+
disposition: "in_progress" | "on_hold" | "done" | "drop" | "ready" | "blocked";
|
|
100
141
|
updatedAt: string;
|
|
101
142
|
bodyPresent: boolean;
|
|
102
|
-
children?:
|
|
143
|
+
children?: {
|
|
103
144
|
live: number;
|
|
104
145
|
total: number;
|
|
105
|
-
}
|
|
146
|
+
} | undefined;
|
|
106
147
|
})[];
|
|
107
148
|
export type TaskBoardObservation = Readonly<{
|
|
108
149
|
statusRows: ReturnType<typeof projectStatusRows>;
|
package/build/src/task/board.js
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
|
-
import { parseTaskId, sameNamespace } from "./identity.js";
|
|
1
|
+
import { formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const taskRowIdSchema = z.string().transform((value, context) => {
|
|
4
|
+
try {
|
|
5
|
+
const id = formatTaskId(parseTaskId(value));
|
|
6
|
+
if (id !== value)
|
|
7
|
+
throw new Error("not canonical");
|
|
8
|
+
return id;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
context.addIssue({ code: "custom", message: "expected canonical TaskId" });
|
|
12
|
+
return z.NEVER;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
const taskRowTimestampSchema = z.string().refine((value) => Number.isFinite(Date.parse(value)), "expected timestamp");
|
|
16
|
+
export const taskRowSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
id: taskRowIdSchema,
|
|
19
|
+
title: z.string().refine((value) => value.trim() !== ""),
|
|
20
|
+
state: z.enum(["open", "in_progress", "on_hold", "done", "drop"]),
|
|
21
|
+
priority: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]),
|
|
22
|
+
disposition: z.enum(["ready", "blocked", "in_progress", "on_hold", "done", "drop"]),
|
|
23
|
+
updatedAt: taskRowTimestampSchema,
|
|
24
|
+
bodyPresent: z.boolean(),
|
|
25
|
+
children: z
|
|
26
|
+
.object({ live: z.number().int().nonnegative(), total: z.number().int().nonnegative() })
|
|
27
|
+
.strict()
|
|
28
|
+
.optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
export const taskRowsSchema = z.array(taskRowSchema).readonly();
|
|
2
32
|
const terminal = (state) => state === "done" || state === "drop";
|
|
3
33
|
const none = Object.freeze([]);
|
|
4
34
|
const sorted = (values) => [...values].sort((a, b) => Buffer.compare(Buffer.from(a.id), Buffer.from(b.id)));
|