@ianwremmel/dispatch 0.32.1-bootstrap.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/.claude-plugin/plugin.json +59 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/agents/.gitkeep +0 -0
- package/agents/build-graph.md +99 -0
- package/agents/milestone-reviewer.md +50 -0
- package/agents/pr-worker.md +172 -0
- package/agents/ticket-worker.md +97 -0
- package/bin/dispatch +101 -0
- package/bin/dispatch-mcp +19 -0
- package/bin/pr-status +931 -0
- package/commands/.gitkeep +0 -0
- package/commands/orchestrate.md +6 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/claim-guard.mts +98 -0
- package/hooks/hooks.json +15 -0
- package/package.json +46 -0
- package/skills/.gitkeep +0 -0
- package/skills/land/SKILL.md +238 -0
- package/skills/land/credentials-dedicated.md +33 -0
- package/skills/land/credentials-shared.md +76 -0
- package/skills/land/mode-solo.md +76 -0
- package/skills/land/mode-team.md +103 -0
- package/skills/land/reference.md +152 -0
- package/skills/land/ticket.md +94 -0
- package/skills/orchestrate/SKILL.md +87 -0
- package/skills/tracker-adapter-linear/SKILL.md +142 -0
- package/src/commands/CLAUDE.md +12 -0
- package/src/commands/claim/check.mts +88 -0
- package/src/commands/claim/guard.mts +95 -0
- package/src/commands/claim/status.mts +49 -0
- package/src/commands/edge/add.mts +44 -0
- package/src/commands/edge/rm.mts +44 -0
- package/src/commands/edge/set.mts +56 -0
- package/src/commands/greet.mts +34 -0
- package/src/commands/mcp/ack.mts +43 -0
- package/src/commands/mcp/ping.mts +61 -0
- package/src/commands/mcp/status.mts +89 -0
- package/src/commands/mcp.mts +155 -0
- package/src/commands/milestone/rm.mts +35 -0
- package/src/commands/milestone/set.mts +49 -0
- package/src/commands/outcome/rm.mts +36 -0
- package/src/commands/outcome/set.mts +86 -0
- package/src/commands/pr/rm.mts +33 -0
- package/src/commands/pr/set.mts +110 -0
- package/src/commands/pr/yield.mts +114 -0
- package/src/commands/project/rm.mts +33 -0
- package/src/commands/project/set.mts +50 -0
- package/src/commands/queue.mts +41 -0
- package/src/commands/refresh/done.mts +42 -0
- package/src/commands/refresh/status.mts +40 -0
- package/src/commands/refresh.mts +56 -0
- package/src/commands/review/record.mts +46 -0
- package/src/commands/review/release.mts +49 -0
- package/src/commands/status.mts +85 -0
- package/src/commands/ticket/missing.mts +31 -0
- package/src/commands/ticket/rm.mts +33 -0
- package/src/commands/ticket/set.mts +134 -0
- package/src/commands/worker/rm.mts +46 -0
- package/src/commands/worker/set.mts +63 -0
- package/src/lib/cli/CLAUDE.md +13 -0
- package/src/lib/cli/cli.mts +226 -0
- package/src/lib/cli/index.mts +1 -0
- package/src/lib/command/CLAUDE.md +26 -0
- package/src/lib/command/__fixtures__/bad-export/oops.mts +1 -0
- package/src/lib/command/__fixtures__/bad-name/mismatch.mts +19 -0
- package/src/lib/command/__fixtures__/commands/cli-only.mts +20 -0
- package/src/lib/command/__fixtures__/commands/greet.mts +39 -0
- package/src/lib/command/__fixtures__/commands/math/add.mts +32 -0
- package/src/lib/command/__fixtures__/commands/mcp-only.mts +20 -0
- package/src/lib/command/__fixtures__/commands/needs-token.mts +19 -0
- package/src/lib/command/__fixtures__/commands/store/get.mts +26 -0
- package/src/lib/command/__fixtures__/commands/store.mts +26 -0
- package/src/lib/command/abstract-command.mts +104 -0
- package/src/lib/command/discovery.mts +100 -0
- package/src/lib/command/env.mts +19 -0
- package/src/lib/command/index.mts +6 -0
- package/src/lib/command/parse.mts +64 -0
- package/src/lib/command/test-support.mts +81 -0
- package/src/lib/command/transports.mts +17 -0
- package/src/lib/command/types.mts +53 -0
- package/src/lib/db/CLAUDE.md +13 -0
- package/src/lib/db/database.mts +160 -0
- package/src/lib/db/index.mts +4 -0
- package/src/lib/db/schema.mts +195 -0
- package/src/lib/db/time.mts +24 -0
- package/src/lib/db/with-database.mts +56 -0
- package/src/lib/errors/CLAUDE.md +18 -0
- package/src/lib/errors/command-error.mts +13 -0
- package/src/lib/errors/data-error.mts +12 -0
- package/src/lib/errors/definition-error.mts +6 -0
- package/src/lib/errors/dispatch-error.mts +27 -0
- package/src/lib/errors/ensure.mts +22 -0
- package/src/lib/errors/environment-error.mts +7 -0
- package/src/lib/errors/index.mts +8 -0
- package/src/lib/errors/json-rpc-error.mts +18 -0
- package/src/lib/errors/usage-error.mts +7 -0
- package/src/lib/graph/CLAUDE.md +17 -0
- package/src/lib/graph/anomalies.mts +110 -0
- package/src/lib/graph/derive.mts +96 -0
- package/src/lib/graph/index.mts +26 -0
- package/src/lib/graph/pipeline.mts +410 -0
- package/src/lib/graph/queries.mts +207 -0
- package/src/lib/graph/rows.mts +99 -0
- package/src/lib/graph/types.mts +137 -0
- package/src/lib/liveness/CLAUDE.md +14 -0
- package/src/lib/liveness/index.mts +10 -0
- package/src/lib/liveness/liveness.mts +147 -0
- package/src/lib/liveness/retire.mts +63 -0
- package/src/lib/logger/CLAUDE.md +12 -0
- package/src/lib/logger/index.mts +2 -0
- package/src/lib/logger/logger.mts +58 -0
- package/src/lib/logger/stream-sink.mts +23 -0
- package/src/lib/mcp/CLAUDE.md +21 -0
- package/src/lib/mcp/channel.mts +41 -0
- package/src/lib/mcp/dispatch.mts +60 -0
- package/src/lib/mcp/drain.mts +83 -0
- package/src/lib/mcp/index.mts +5 -0
- package/src/lib/mcp/mcp.mts +267 -0
- package/src/lib/mcp/tools.mts +77 -0
- package/src/lib/model/CLAUDE.md +8 -0
- package/src/lib/model/index.mts +3 -0
- package/src/lib/model/repo-caps.mts +95 -0
- package/src/lib/model/status.mts +91 -0
- package/src/lib/model/types.mts +83 -0
- package/src/lib/refresh/index.mts +2 -0
- package/src/lib/refresh/placeholders.mts +43 -0
- package/src/lib/refresh/refresh-service.mts +203 -0
- package/src/lib/schedule/CLAUDE.md +18 -0
- package/src/lib/schedule/caps.mts +113 -0
- package/src/lib/schedule/correlate.mts +69 -0
- package/src/lib/schedule/index.mts +7 -0
- package/src/lib/schedule/scheduler.mts +355 -0
- package/src/lib/schedule/tick.mts +266 -0
- package/src/lib/stores/CLAUDE.md +24 -0
- package/src/lib/stores/coordination.mts +359 -0
- package/src/lib/stores/cursor.mts +41 -0
- package/src/lib/stores/edge.mts +138 -0
- package/src/lib/stores/fetch-request.mts +346 -0
- package/src/lib/stores/index.mts +19 -0
- package/src/lib/stores/materialize.mts +69 -0
- package/src/lib/stores/milestone.mts +74 -0
- package/src/lib/stores/notice.mts +57 -0
- package/src/lib/stores/policy.mts +48 -0
- package/src/lib/stores/pr-event.mts +94 -0
- package/src/lib/stores/pr.mts +167 -0
- package/src/lib/stores/project.mts +79 -0
- package/src/lib/stores/refresh.mts +197 -0
- package/src/lib/stores/review.mts +113 -0
- package/src/lib/stores/session.mts +170 -0
- package/src/lib/stores/ticket.mts +246 -0
- package/src/lib/stores/watch.mts +360 -0
- package/src/lib/stores/worker.mts +121 -0
- package/src/lib/watch/adopt.mts +151 -0
- package/src/lib/watch/arm.mts +48 -0
- package/src/lib/watch/cadence.mts +45 -0
- package/src/lib/watch/diff.mts +274 -0
- package/src/lib/watch/index.mts +11 -0
- package/src/lib/watch/marker.mts +24 -0
- package/src/lib/watch/payload.mts +56 -0
- package/src/lib/watch/poll.mts +87 -0
- package/src/lib/watch/render.mts +61 -0
- package/src/lib/watch/snapshot.mts +312 -0
- package/src/main.mts +18 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bumped on any change an existing database file cannot absorb. `Database.open`
|
|
3
|
+
* refuses a file whose recorded version differs. The database is a rebuildable
|
|
4
|
+
* cache (the graph re-derives from a full sync; sessions and claims are pure
|
|
5
|
+
* runtime state), so a bump's recovery is "delete the file and re-sync" — there
|
|
6
|
+
* is no migration machinery.
|
|
7
|
+
*/
|
|
8
|
+
export const SCHEMA_VERSION = 6;
|
|
9
|
+
|
|
10
|
+
export const SCHEMA = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
12
|
+
key TEXT PRIMARY KEY,
|
|
13
|
+
value TEXT NOT NULL
|
|
14
|
+
) STRICT;
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS node (
|
|
17
|
+
id INTEGER PRIMARY KEY,
|
|
18
|
+
external_id TEXT NOT NULL UNIQUE,
|
|
19
|
+
kind TEXT NOT NULL CHECK (kind IN ('project','milestone','ticket','pr','unknown'))
|
|
20
|
+
) STRICT;
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS project (
|
|
23
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
source TEXT
|
|
26
|
+
) STRICT;
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS milestone (
|
|
29
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
30
|
+
project_id INTEGER NOT NULL REFERENCES node(id),
|
|
31
|
+
name TEXT NOT NULL
|
|
32
|
+
) STRICT;
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS ticket (
|
|
35
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
36
|
+
project_id INTEGER NOT NULL REFERENCES node(id),
|
|
37
|
+
url TEXT NOT NULL,
|
|
38
|
+
title TEXT NOT NULL,
|
|
39
|
+
status TEXT NOT NULL CHECK (status IN (
|
|
40
|
+
'backlog','paused','awaiting-external','available','in-progress',
|
|
41
|
+
'in-review','finished','delivered','verified','canceled')),
|
|
42
|
+
target_kind TEXT NOT NULL CHECK (target_kind IN ('pr','verification','human-only')),
|
|
43
|
+
requires_human INTEGER NOT NULL CHECK (requires_human IN (0,1)),
|
|
44
|
+
injected INTEGER NOT NULL CHECK (injected IN (0,1)),
|
|
45
|
+
priority REAL,
|
|
46
|
+
branch_hint TEXT,
|
|
47
|
+
labels TEXT NOT NULL,
|
|
48
|
+
updated_at TEXT
|
|
49
|
+
) STRICT;
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS pr (
|
|
52
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
53
|
+
ticket_id INTEGER REFERENCES node(id),
|
|
54
|
+
origin TEXT NOT NULL CHECK (origin IN ('prompt','ticket','adopted','resumed')),
|
|
55
|
+
repo TEXT,
|
|
56
|
+
pr_number INTEGER,
|
|
57
|
+
url TEXT,
|
|
58
|
+
branch TEXT,
|
|
59
|
+
title TEXT NOT NULL,
|
|
60
|
+
injected INTEGER NOT NULL CHECK (injected IN (0,1)),
|
|
61
|
+
priority REAL,
|
|
62
|
+
updated_at TEXT
|
|
63
|
+
) STRICT;
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS edge (
|
|
66
|
+
blocker INTEGER NOT NULL REFERENCES node(id) ON DELETE CASCADE,
|
|
67
|
+
blocked INTEGER NOT NULL REFERENCES node(id) ON DELETE CASCADE,
|
|
68
|
+
PRIMARY KEY (blocker, blocked),
|
|
69
|
+
CHECK (blocker <> blocked)
|
|
70
|
+
) STRICT;
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS session (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
host TEXT,
|
|
75
|
+
pid INTEGER,
|
|
76
|
+
claude_session_id TEXT,
|
|
77
|
+
acked_at TEXT,
|
|
78
|
+
started_at TEXT NOT NULL,
|
|
79
|
+
heartbeat_at TEXT NOT NULL
|
|
80
|
+
) STRICT;
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS claim (
|
|
83
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
84
|
+
session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE,
|
|
85
|
+
actor TEXT,
|
|
86
|
+
worktree TEXT,
|
|
87
|
+
branch TEXT,
|
|
88
|
+
claimed_at TEXT NOT NULL
|
|
89
|
+
) STRICT;
|
|
90
|
+
|
|
91
|
+
/* A worker's PR wait, handed to the server. The snapshot column holds the last
|
|
92
|
+
observation the poll took; diffing the next one against it is what turns
|
|
93
|
+
"something changed" into named events. */
|
|
94
|
+
CREATE TABLE IF NOT EXISTS watch (
|
|
95
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
96
|
+
state TEXT NOT NULL CHECK (state IN ('watching','fired')),
|
|
97
|
+
snapshot TEXT,
|
|
98
|
+
interval_s INTEGER NOT NULL,
|
|
99
|
+
session_id TEXT,
|
|
100
|
+
created_at TEXT NOT NULL,
|
|
101
|
+
checked_at TEXT,
|
|
102
|
+
expires_at TEXT NOT NULL
|
|
103
|
+
) STRICT;
|
|
104
|
+
|
|
105
|
+
/* Observations owed to a session, oldest first. Delivery is recorded so a
|
|
106
|
+
server restart re-pushes what nobody heard rather than assuming it landed.
|
|
107
|
+
session_id is the session whose worker armed the wait: with a shared graph
|
|
108
|
+
DB several servers drain this table, and only that one can route the event
|
|
109
|
+
to the worker holding the PR. A null session_id is drainable by any. */
|
|
110
|
+
CREATE TABLE IF NOT EXISTS pr_event (
|
|
111
|
+
id INTEGER PRIMARY KEY,
|
|
112
|
+
node_id INTEGER NOT NULL REFERENCES node(id) ON DELETE CASCADE,
|
|
113
|
+
kind TEXT NOT NULL,
|
|
114
|
+
summary TEXT NOT NULL,
|
|
115
|
+
meta TEXT NOT NULL,
|
|
116
|
+
session_id TEXT,
|
|
117
|
+
observed_at TEXT NOT NULL,
|
|
118
|
+
delivered_at TEXT
|
|
119
|
+
) STRICT;
|
|
120
|
+
|
|
121
|
+
/* A live worker's address: the agent ref its launcher got back, recorded so
|
|
122
|
+
an event for the node can be relayed to the worker that already holds its
|
|
123
|
+
context. Cleared with the outcome — not the yield — so a yielded worker
|
|
124
|
+
stays resumable until it reports. */
|
|
125
|
+
CREATE TABLE IF NOT EXISTS worker (
|
|
126
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
127
|
+
session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE,
|
|
128
|
+
agent_ref TEXT NOT NULL,
|
|
129
|
+
launched_at TEXT NOT NULL
|
|
130
|
+
) STRICT;
|
|
131
|
+
|
|
132
|
+
CREATE TABLE IF NOT EXISTS outcome (
|
|
133
|
+
node_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
134
|
+
outcome TEXT NOT NULL CHECK (outcome IN
|
|
135
|
+
('verified','canceled','delivered','human-blocked','decomposed','failed')),
|
|
136
|
+
retryable INTEGER CHECK (retryable IN (0,1)),
|
|
137
|
+
detail TEXT,
|
|
138
|
+
recorded_at TEXT NOT NULL
|
|
139
|
+
) STRICT;
|
|
140
|
+
|
|
141
|
+
CREATE TABLE IF NOT EXISTS review (
|
|
142
|
+
milestone_id INTEGER PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
|
|
143
|
+
recorded_at TEXT NOT NULL
|
|
144
|
+
) STRICT;
|
|
145
|
+
|
|
146
|
+
CREATE TABLE IF NOT EXISTS review_member (
|
|
147
|
+
milestone_id INTEGER NOT NULL REFERENCES review(milestone_id) ON DELETE CASCADE,
|
|
148
|
+
member_external_id TEXT NOT NULL,
|
|
149
|
+
PRIMARY KEY (milestone_id, member_external_id)
|
|
150
|
+
) STRICT;
|
|
151
|
+
|
|
152
|
+
CREATE TABLE IF NOT EXISTS cursor (
|
|
153
|
+
source TEXT PRIMARY KEY,
|
|
154
|
+
value TEXT NOT NULL
|
|
155
|
+
) STRICT;
|
|
156
|
+
|
|
157
|
+
/* A refresh must outlive the staleness sweep that reaps its session, which is the case takeover exists for. */
|
|
158
|
+
CREATE TABLE IF NOT EXISTS refresh (
|
|
159
|
+
source TEXT PRIMARY KEY,
|
|
160
|
+
state TEXT NOT NULL CHECK (state IN ('scanning','resolving','idle')),
|
|
161
|
+
session_id TEXT,
|
|
162
|
+
projects TEXT NOT NULL,
|
|
163
|
+
pending_cursor TEXT,
|
|
164
|
+
started_at TEXT NOT NULL,
|
|
165
|
+
completed_at TEXT,
|
|
166
|
+
completion_emitted_at TEXT
|
|
167
|
+
) STRICT;
|
|
168
|
+
|
|
169
|
+
/* One row per condition order in flight: fires once while its condition holds,
|
|
170
|
+
cleared when the condition lapses so a new episode can fire again. */
|
|
171
|
+
CREATE TABLE IF NOT EXISTS notice (
|
|
172
|
+
kind TEXT NOT NULL CHECK (kind IN ('park_human_blocked','alert_failure','project_complete')),
|
|
173
|
+
node TEXT NOT NULL,
|
|
174
|
+
emitted_at TEXT NOT NULL,
|
|
175
|
+
PRIMARY KEY (kind, node)
|
|
176
|
+
) STRICT;
|
|
177
|
+
|
|
178
|
+
CREATE TABLE IF NOT EXISTS fetch_request (
|
|
179
|
+
id INTEGER PRIMARY KEY,
|
|
180
|
+
source TEXT NOT NULL,
|
|
181
|
+
kind TEXT NOT NULL CHECK (kind IN ('scan_project','fetch_ticket','refresh_ticket')),
|
|
182
|
+
payload TEXT NOT NULL,
|
|
183
|
+
created_at TEXT NOT NULL,
|
|
184
|
+
delivered_at TEXT,
|
|
185
|
+
resolution TEXT CHECK (resolution IN ('materialized','missing'))
|
|
186
|
+
) STRICT;
|
|
187
|
+
|
|
188
|
+
CREATE INDEX IF NOT EXISTS ticket_project ON ticket (project_id);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS milestone_project ON milestone (project_id);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS pr_ticket ON pr (ticket_id);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS edge_blocked ON edge (blocked);
|
|
192
|
+
CREATE INDEX IF NOT EXISTS claim_session ON claim (session_id);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS fetch_request_open ON fetch_request (source, resolution);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS pr_event_undelivered ON pr_event (delivered_at, session_id, id);
|
|
195
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {DataError, ensure} from '../errors/index.mts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RFC 3339 shape, checked before Date.parse: V8 also accepts local formats like
|
|
5
|
+
* "07/31/2026", which would record an instant the caller never meant.
|
|
6
|
+
*/
|
|
7
|
+
const RFC3339_RE =
|
|
8
|
+
/^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}:\d{2})$/u;
|
|
9
|
+
|
|
10
|
+
/** The current instant as a Zulu ISO-8601 string — what callers stamp rows with. */
|
|
11
|
+
export function nowIso(): string {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Reject a timestamp that is not RFC 3339, where the fix is naming the field. */
|
|
16
|
+
export function assertInstant(value: string, field: string): void {
|
|
17
|
+
ensure(
|
|
18
|
+
RFC3339_RE.test(value) && !Number.isNaN(Date.parse(value)),
|
|
19
|
+
() =>
|
|
20
|
+
new DataError(`${field} is not an RFC 3339 timestamp: "${value}"`, {
|
|
21
|
+
hint: `pass an instant like 2026-07-31T12:00:00Z, or omit ${field}.`,
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {homedir} from 'node:os';
|
|
2
|
+
import {join} from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type {Option} from '../command/index.mts';
|
|
5
|
+
import {Database} from './database.mts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `--db` flag every graph-writing command carries. Declared `as const
|
|
9
|
+
* satisfies Option` rather than typed `Option`: `ParsedOptions` reads the
|
|
10
|
+
* literal `type` to decide what `run` receives, and a widened type resolves to
|
|
11
|
+
* `never`.
|
|
12
|
+
*/
|
|
13
|
+
export const DB_OPTION = {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description:
|
|
16
|
+
'Graph database path. Defaults to $DISPATCH_DB, else $XDG_STATE_HOME/dispatch/graph-v2.db.',
|
|
17
|
+
positional: false,
|
|
18
|
+
required: false,
|
|
19
|
+
} as const satisfies Option;
|
|
20
|
+
|
|
21
|
+
export function resolveDbPath(
|
|
22
|
+
flag: string | undefined,
|
|
23
|
+
env: NodeJS.ProcessEnv
|
|
24
|
+
): string {
|
|
25
|
+
if (flag !== undefined && flag !== '') return flag;
|
|
26
|
+
if (env.DISPATCH_DB !== undefined && env.DISPATCH_DB !== '')
|
|
27
|
+
return env.DISPATCH_DB;
|
|
28
|
+
const state =
|
|
29
|
+
env.XDG_STATE_HOME !== undefined && env.XDG_STATE_HOME !== ''
|
|
30
|
+
? env.XDG_STATE_HOME
|
|
31
|
+
: join(homedir(), '.local', 'state');
|
|
32
|
+
// `graph-v2.db`, not `graph.db`: installations that ran the retired
|
|
33
|
+
// `dispatch graph …` CLI still have a `graph.db` in this directory at an
|
|
34
|
+
// incompatible schema, and `Database.open` refuses a file recorded at a
|
|
35
|
+
// foreign version. The suffix names the CLI generation, not the schema
|
|
36
|
+
// version.
|
|
37
|
+
return join(state, 'dispatch', 'graph-v2.db');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Open the graph, run `body`, and close it even if `body` throws. Several
|
|
42
|
+
* agents share one file, so a command that leaks its handle holds a lock the
|
|
43
|
+
* next one has to wait out.
|
|
44
|
+
*/
|
|
45
|
+
export async function withDatabase<T>(
|
|
46
|
+
flag: string | undefined,
|
|
47
|
+
env: NodeJS.ProcessEnv,
|
|
48
|
+
body: (db: Database) => Promise<T> | T
|
|
49
|
+
): Promise<T> {
|
|
50
|
+
const db = await Database.open(resolveDbPath(flag, env));
|
|
51
|
+
try {
|
|
52
|
+
return await body(db);
|
|
53
|
+
} finally {
|
|
54
|
+
await db.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
The failure taxonomy the CLI and MCP server throw. Every deliberate error extends
|
|
4
|
+
`DispatchError`, so one `instanceof` separates an intended failure from a crash.
|
|
5
|
+
The caller is usually an agent, so errors carry an optional `hint` for whoever has
|
|
6
|
+
to fix it. One class per file; `index.mts` re-exports all of them.
|
|
7
|
+
|
|
8
|
+
`dispatch-error.mts` is the root: `message`, `hint`, `toString()` rendering
|
|
9
|
+
message + hint, and no transport-specific field. `command-error.mts` —
|
|
10
|
+
`CommandError` extends it for validate/discover/run failures, adding the CLI
|
|
11
|
+
`exitCode`; `usage-error.mts` (2), `environment-error.mts` (3), and
|
|
12
|
+
`definition-error.mts` (1) specialize it. `json-rpc-error.mts` — `JsonRpcError`
|
|
13
|
+
also extends `DispatchError`, but as the MCP server's protocol fault it carries a
|
|
14
|
+
JSON-RPC `code`, not an `exitCode` — a sibling of `CommandError`, not a command
|
|
15
|
+
failure. `ensure.mts` holds `assertUsage` and the lazy `ensure`.
|
|
16
|
+
|
|
17
|
+
Add a class only when a caller must branch on a failure the existing ones don't
|
|
18
|
+
cover; give it its own file under the right parent.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {DispatchError} from './dispatch-error.mts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A failure from validating, discovering, or running a command. The CLI returns
|
|
5
|
+
* its `exitCode`; the MCP transport renders it as an `isError` tool result. This
|
|
6
|
+
* is the base for the command-facing taxonomy — a transport-protocol failure
|
|
7
|
+
* (the MCP server's `JsonRpcError`) extends `DispatchError` directly instead, as
|
|
8
|
+
* a sibling of this, because it carries a protocol code rather than an exit code.
|
|
9
|
+
*/
|
|
10
|
+
export class CommandError extends DispatchError {
|
|
11
|
+
override readonly name: string = 'CommandError';
|
|
12
|
+
readonly exitCode: number = 1;
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {CommandError} from './command-error.mts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The caller's input was well-formed but describes invalid data: a dependency
|
|
5
|
+
* cycle, an id used as two kinds, an unknown status. Distinct from `UsageError`
|
|
6
|
+
* (a malformed invocation) so a caller can branch on "fix the data" vs "fix the
|
|
7
|
+
* command line".
|
|
8
|
+
*/
|
|
9
|
+
export class DataError extends CommandError {
|
|
10
|
+
override readonly name: string = 'DataError';
|
|
11
|
+
override readonly exitCode = 4;
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import {CommandError} from './command-error.mts';
|
|
2
|
+
|
|
3
|
+
/** A command is defined or registered wrong — a bug in the plugin, fixed by editing the command file. */
|
|
4
|
+
export class DefinitionError extends CommandError {
|
|
5
|
+
override readonly name: string = 'DefinitionError';
|
|
6
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DispatchErrorOptions extends ErrorOptions {
|
|
2
|
+
/** What to do about it, written for the agent that ran the command. */
|
|
3
|
+
readonly hint?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An intended, caller-actionable failure, as opposed to a crash — the root of
|
|
8
|
+
* every deliberate error the plugin throws, so one `instanceof` tells the two
|
|
9
|
+
* apart. Command failures (`CommandError`) and transport-protocol failures
|
|
10
|
+
* (`JsonRpcError`) both extend it; the root itself holds no transport-specific
|
|
11
|
+
* field, only the `hint` and rendering common to all of them.
|
|
12
|
+
*/
|
|
13
|
+
export class DispatchError extends Error {
|
|
14
|
+
override readonly name: string = 'DispatchError';
|
|
15
|
+
readonly hint: string | undefined;
|
|
16
|
+
|
|
17
|
+
constructor(message: string, options: DispatchErrorOptions = {}) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.hint = options.hint;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override toString(): string {
|
|
23
|
+
return this.hint === undefined
|
|
24
|
+
? `${this.name}: ${this.message}`
|
|
25
|
+
: `${this.name}: ${this.message}\nhint: ${this.hint}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {DispatchError} from './dispatch-error.mts';
|
|
2
|
+
import {UsageError} from './usage-error.mts';
|
|
3
|
+
|
|
4
|
+
/** `assert` for caller input: a failed check is a usage error, not a crash. */
|
|
5
|
+
export function assertUsage(
|
|
6
|
+
condition: unknown,
|
|
7
|
+
message: string
|
|
8
|
+
): asserts condition {
|
|
9
|
+
if (!condition) {
|
|
10
|
+
throw new UsageError(message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `assert` against a taxonomy error, built lazily so the passing path never constructs it. */
|
|
15
|
+
export function ensure(
|
|
16
|
+
condition: unknown,
|
|
17
|
+
error: () => DispatchError
|
|
18
|
+
): asserts condition {
|
|
19
|
+
if (!condition) {
|
|
20
|
+
throw error();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {CommandError} from './command-error.mts';
|
|
2
|
+
|
|
3
|
+
/** A variable the command declared in `env` is missing. The command was right; the environment was not. */
|
|
4
|
+
export class EnvironmentError extends CommandError {
|
|
5
|
+
override readonly name: string = 'EnvironmentError';
|
|
6
|
+
override readonly exitCode = 3;
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './dispatch-error.mts';
|
|
2
|
+
export * from './command-error.mts';
|
|
3
|
+
export * from './data-error.mts';
|
|
4
|
+
export * from './usage-error.mts';
|
|
5
|
+
export * from './environment-error.mts';
|
|
6
|
+
export * from './definition-error.mts';
|
|
7
|
+
export * from './json-rpc-error.mts';
|
|
8
|
+
export * from './ensure.mts';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {DispatchError} from './dispatch-error.mts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A JSON-RPC protocol failure (bad method, malformed request, unknown tool).
|
|
5
|
+
* The server loop renders it into an `error` response. A sibling of
|
|
6
|
+
* `CommandError` under `DispatchError`: it carries a protocol `code` rather than
|
|
7
|
+
* an exit code, and unlike a command failure — which becomes a successful result
|
|
8
|
+
* with `isError: true` — it is a wire-level fault, not a tool result.
|
|
9
|
+
*/
|
|
10
|
+
export class JsonRpcError extends DispatchError {
|
|
11
|
+
override readonly name = 'JsonRpcError';
|
|
12
|
+
readonly code: number;
|
|
13
|
+
|
|
14
|
+
constructor(code: number, message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {CommandError} from './command-error.mts';
|
|
2
|
+
|
|
3
|
+
/** The caller invoked the command wrong: an unknown flag, a missing argument, a bad choice. */
|
|
4
|
+
export class UsageError extends CommandError {
|
|
5
|
+
override readonly name: string = 'UsageError';
|
|
6
|
+
override readonly exitCode = 2;
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# graph
|
|
2
|
+
|
|
3
|
+
The derived read-model: every scheduling decision, as SQL over the stores'
|
|
4
|
+
tables. Consumers read results; nothing re-derives.
|
|
5
|
+
|
|
6
|
+
- `pipeline.mts` — the shared CTE prefix (edge semantics, blocking closure,
|
|
7
|
+
claim liveness, milestone gating, classification, the dispatch queue) plus
|
|
8
|
+
the rank/queue orderings and parameter plumbing. Read its doc comment first.
|
|
9
|
+
- `queries.mts` — `classifiedItems`, `frontier`, `dispatchQueue`,
|
|
10
|
+
`milestoneStates` over the prefix, plus `repoPrLoad`: per-repo open and
|
|
11
|
+
building PR counts for the admission caps, read from the stored watch
|
|
12
|
+
snapshots rather than a fetch of its own.
|
|
13
|
+
- `anomalies.mts` — global structure writes could not refuse: dangling
|
|
14
|
+
placeholder endpoints, mutually blocking projects, cycles as a safety net.
|
|
15
|
+
- `derive.mts` — assembles counts and the terminal verdict from the above.
|
|
16
|
+
- `rows.mts` — row → model narrowing shared by the queries.
|
|
17
|
+
- `types.mts` — the result types and `DeriveOptions`.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await --
|
|
2
|
+
* `node:sqlite` is synchronous; the async facade keeps call sites stable. */
|
|
3
|
+
import type {Database} from '../db/database.mts';
|
|
4
|
+
import {integer, splitList, text} from './rows.mts';
|
|
5
|
+
import type {Anomaly} from './types.mts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What the writes could not prevent. Kind conflicts, self-edges, cycles, and
|
|
9
|
+
* malformed timestamps are rejected at write time; what remains is global
|
|
10
|
+
* structure no single write can judge:
|
|
11
|
+
*
|
|
12
|
+
* - a placeholder edge endpoint — the node was never written (a fetch that
|
|
13
|
+
* resolved `missing`, or an ingest that never completed);
|
|
14
|
+
* - two projects that block each other, so neither can finish first;
|
|
15
|
+
* - a cycle, as a safety net for a hand-edited or imported database.
|
|
16
|
+
*/
|
|
17
|
+
export async function anomalies(db: Database): Promise<Anomaly[]> {
|
|
18
|
+
const out: Anomaly[] = [];
|
|
19
|
+
|
|
20
|
+
const cycleNodes = db
|
|
21
|
+
.all(
|
|
22
|
+
`WITH RECURSIVE reach(target, id) AS (
|
|
23
|
+
SELECT blocker, blocked FROM edge
|
|
24
|
+
UNION
|
|
25
|
+
SELECT r.target, e.blocked FROM reach r JOIN edge e ON e.blocker = r.id
|
|
26
|
+
)
|
|
27
|
+
SELECT DISTINCT n.external_id AS id
|
|
28
|
+
FROM reach r JOIN node n ON n.id = r.target
|
|
29
|
+
WHERE r.target = r.id
|
|
30
|
+
ORDER BY id`
|
|
31
|
+
)
|
|
32
|
+
.map((row) => text(row.id) ?? '');
|
|
33
|
+
if (cycleNodes.length > 0) {
|
|
34
|
+
out.push({
|
|
35
|
+
kind: 'cycle',
|
|
36
|
+
nodes: cycleNodes,
|
|
37
|
+
detail: `dependency cycle through ${cycleNodes.join(', ')}; the CLI refuses cycle-closing edges, so this database was edited by something else — remove an edge to break the loop`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const row of db.all(
|
|
42
|
+
`SELECT nb.external_id AS blocker, nb.kind AS blocker_kind,
|
|
43
|
+
nd.external_id AS blocked
|
|
44
|
+
FROM edge e
|
|
45
|
+
JOIN node nb ON nb.id = e.blocker
|
|
46
|
+
JOIN node nd ON nd.id = e.blocked
|
|
47
|
+
WHERE nb.kind = 'unknown' OR nd.kind = 'unknown'
|
|
48
|
+
ORDER BY blocker, blocked`
|
|
49
|
+
)) {
|
|
50
|
+
const blocker = text(row.blocker) ?? '';
|
|
51
|
+
const blocked = text(row.blocked) ?? '';
|
|
52
|
+
out.push({
|
|
53
|
+
kind: 'dangling-edge',
|
|
54
|
+
nodes: [blocker, blocked],
|
|
55
|
+
detail:
|
|
56
|
+
text(row.blocker_kind) === 'unknown'
|
|
57
|
+
? `blocker ${blocker} of ${blocked} is not in the graph; ${blocked} is held blocked until ${blocker} is written or the edge is removed`
|
|
58
|
+
: `${blocker} blocks ${blocked}, which is not in the graph; the edge schedules nothing — write ${blocked} if it is in scope`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pairs = db.all(
|
|
63
|
+
`WITH pe(a, b) AS (
|
|
64
|
+
SELECT DISTINCT tb.project_id, td.project_id
|
|
65
|
+
FROM edge e
|
|
66
|
+
JOIN ticket tb ON tb.node_id = e.blocker
|
|
67
|
+
JOIN ticket td ON td.node_id = e.blocked
|
|
68
|
+
WHERE tb.project_id <> td.project_id
|
|
69
|
+
)
|
|
70
|
+
SELECT x.a AS a_id, x.b AS b_id,
|
|
71
|
+
pa.external_id AS a, pb.external_id AS b
|
|
72
|
+
FROM pe x
|
|
73
|
+
JOIN pe y ON y.a = x.b AND y.b = x.a
|
|
74
|
+
JOIN node pa ON pa.id = x.a
|
|
75
|
+
JOIN node pb ON pb.id = x.b
|
|
76
|
+
WHERE x.a < x.b
|
|
77
|
+
ORDER BY a, b`
|
|
78
|
+
);
|
|
79
|
+
for (const pair of pairs) {
|
|
80
|
+
const involved = db
|
|
81
|
+
.all(
|
|
82
|
+
`SELECT group_concat(id, ',') AS ids FROM (
|
|
83
|
+
SELECT DISTINCT n.external_id AS id
|
|
84
|
+
FROM edge e
|
|
85
|
+
JOIN ticket tb ON tb.node_id = e.blocker
|
|
86
|
+
JOIN ticket td ON td.node_id = e.blocked
|
|
87
|
+
JOIN node n ON n.id = e.blocker OR n.id = e.blocked
|
|
88
|
+
WHERE (tb.project_id = ? AND td.project_id = ?)
|
|
89
|
+
OR (tb.project_id = ? AND td.project_id = ?)
|
|
90
|
+
ORDER BY id
|
|
91
|
+
)`,
|
|
92
|
+
[
|
|
93
|
+
integer(pair.a_id),
|
|
94
|
+
integer(pair.b_id),
|
|
95
|
+
integer(pair.b_id),
|
|
96
|
+
integer(pair.a_id),
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
.flatMap((row) => splitList(row.ids));
|
|
100
|
+
out.push({
|
|
101
|
+
kind: 'cross-project-reverse',
|
|
102
|
+
nodes: involved,
|
|
103
|
+
detail: `projects ${text(pair.a) ?? ''} and ${text(pair.b) ?? ''} block each other; neither can be completed first`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* eslint-enable @typescript-eslint/require-await */
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {Database} from '../db/database.mts';
|
|
2
|
+
import {anomalies} from './anomalies.mts';
|
|
3
|
+
import {classifiedItems, milestoneStates} from './queries.mts';
|
|
4
|
+
import {text} from './rows.mts';
|
|
5
|
+
import type {
|
|
6
|
+
Classification,
|
|
7
|
+
ClassificationCounts,
|
|
8
|
+
ClassifiedItem,
|
|
9
|
+
DeriveOptions,
|
|
10
|
+
DerivedGraph,
|
|
11
|
+
ProjectCounts,
|
|
12
|
+
} from './types.mts';
|
|
13
|
+
|
|
14
|
+
const OPEN: readonly Classification[] = [
|
|
15
|
+
'available',
|
|
16
|
+
'blocked',
|
|
17
|
+
'human-blocked',
|
|
18
|
+
'in-flight',
|
|
19
|
+
'dormant',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Assemble the derived graph. The reasoning — blocking, ranking, gating,
|
|
24
|
+
* classification, anomalies — is the SQL in `pipeline.mts`; this only reads
|
|
25
|
+
* those results and tallies counts, so no consumer re-derives anything.
|
|
26
|
+
*/
|
|
27
|
+
export async function derive(
|
|
28
|
+
db: Database,
|
|
29
|
+
options: DeriveOptions = {}
|
|
30
|
+
): Promise<DerivedGraph> {
|
|
31
|
+
const items = await classifiedItems(db, options);
|
|
32
|
+
const tickets = items.filter((entry) => entry.item.kind === 'ticket');
|
|
33
|
+
const prs = items.filter((entry) => entry.item.kind === 'pr');
|
|
34
|
+
const milestones = await milestoneStates(db, options);
|
|
35
|
+
|
|
36
|
+
const allProjects = db
|
|
37
|
+
.all(
|
|
38
|
+
`SELECT n.external_id AS id, p.name FROM project p
|
|
39
|
+
JOIN node n ON n.id = p.node_id
|
|
40
|
+
WHERE (? IS NULL OR n.external_id = ?)
|
|
41
|
+
ORDER BY id`,
|
|
42
|
+
[options.project ?? null, options.project ?? null]
|
|
43
|
+
)
|
|
44
|
+
.map((row) => ({id: text(row.id) ?? '', name: text(row.name) ?? ''}));
|
|
45
|
+
|
|
46
|
+
// A project with an owed milestone review is not done: the review is work.
|
|
47
|
+
const isTerminal = (project: string): boolean =>
|
|
48
|
+
!tickets.some(
|
|
49
|
+
(entry) =>
|
|
50
|
+
entry.item.project === project && OPEN.includes(entry.classification)
|
|
51
|
+
) &&
|
|
52
|
+
!milestones.some(
|
|
53
|
+
(milestone) =>
|
|
54
|
+
milestone.project === project &&
|
|
55
|
+
milestone.readyForReview &&
|
|
56
|
+
!milestone.reviewRecorded
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const projects = allProjects.map((project) => ({
|
|
60
|
+
...project,
|
|
61
|
+
terminal: isTerminal(project.id),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
projects,
|
|
66
|
+
items: tickets,
|
|
67
|
+
milestones,
|
|
68
|
+
counts: allProjects.map((project): ProjectCounts => ({
|
|
69
|
+
project: project.id,
|
|
70
|
+
total: tickets.filter((entry) => entry.item.project === project.id)
|
|
71
|
+
.length,
|
|
72
|
+
...tally(tickets.filter((entry) => entry.item.project === project.id)),
|
|
73
|
+
terminal: isTerminal(project.id),
|
|
74
|
+
})),
|
|
75
|
+
prs,
|
|
76
|
+
anomalies: await anomalies(db),
|
|
77
|
+
terminal:
|
|
78
|
+
projects.every((project) => project.terminal) &&
|
|
79
|
+
!prs.some((entry) => OPEN.includes(entry.classification)),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function tally(entries: readonly ClassifiedItem[]): ClassificationCounts {
|
|
84
|
+
const count = (kind: Classification): number =>
|
|
85
|
+
entries.filter((entry) => entry.classification === kind).length;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
available: count('available'),
|
|
89
|
+
blocked: count('blocked'),
|
|
90
|
+
humanBlocked: count('human-blocked'),
|
|
91
|
+
inFlight: count('in-flight'),
|
|
92
|
+
dormant: count('dormant'),
|
|
93
|
+
verified: count('verified'),
|
|
94
|
+
canceled: count('canceled'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {anomalies} from './anomalies.mts';
|
|
2
|
+
export {derive} from './derive.mts';
|
|
3
|
+
export {DEFAULT_STALE_AFTER_SECONDS} from './pipeline.mts';
|
|
4
|
+
export {
|
|
5
|
+
classifiedItems,
|
|
6
|
+
dispatchQueue,
|
|
7
|
+
frontier,
|
|
8
|
+
milestoneStates,
|
|
9
|
+
repoPrLoad,
|
|
10
|
+
} from './queries.mts';
|
|
11
|
+
export type {RepoPrLoad} from './queries.mts';
|
|
12
|
+
export type {
|
|
13
|
+
Anomaly,
|
|
14
|
+
Classification,
|
|
15
|
+
ClassificationCounts,
|
|
16
|
+
ClassifiedItem,
|
|
17
|
+
ClaimView,
|
|
18
|
+
DerivedGraph,
|
|
19
|
+
DeriveOptions,
|
|
20
|
+
MilestoneState,
|
|
21
|
+
OutcomeView,
|
|
22
|
+
Pass,
|
|
23
|
+
ProjectCounts,
|
|
24
|
+
QueueEntry,
|
|
25
|
+
WorkItem,
|
|
26
|
+
} from './types.mts';
|