@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.
Files changed (165) hide show
  1. package/.claude-plugin/plugin.json +59 -0
  2. package/.mcp.json +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +93 -0
  5. package/agents/.gitkeep +0 -0
  6. package/agents/build-graph.md +99 -0
  7. package/agents/milestone-reviewer.md +50 -0
  8. package/agents/pr-worker.md +172 -0
  9. package/agents/ticket-worker.md +97 -0
  10. package/bin/dispatch +101 -0
  11. package/bin/dispatch-mcp +19 -0
  12. package/bin/pr-status +931 -0
  13. package/commands/.gitkeep +0 -0
  14. package/commands/orchestrate.md +6 -0
  15. package/hooks/.gitkeep +0 -0
  16. package/hooks/claim-guard.mts +98 -0
  17. package/hooks/hooks.json +15 -0
  18. package/package.json +46 -0
  19. package/skills/.gitkeep +0 -0
  20. package/skills/land/SKILL.md +238 -0
  21. package/skills/land/credentials-dedicated.md +33 -0
  22. package/skills/land/credentials-shared.md +76 -0
  23. package/skills/land/mode-solo.md +76 -0
  24. package/skills/land/mode-team.md +103 -0
  25. package/skills/land/reference.md +152 -0
  26. package/skills/land/ticket.md +94 -0
  27. package/skills/orchestrate/SKILL.md +87 -0
  28. package/skills/tracker-adapter-linear/SKILL.md +142 -0
  29. package/src/commands/CLAUDE.md +12 -0
  30. package/src/commands/claim/check.mts +88 -0
  31. package/src/commands/claim/guard.mts +95 -0
  32. package/src/commands/claim/status.mts +49 -0
  33. package/src/commands/edge/add.mts +44 -0
  34. package/src/commands/edge/rm.mts +44 -0
  35. package/src/commands/edge/set.mts +56 -0
  36. package/src/commands/greet.mts +34 -0
  37. package/src/commands/mcp/ack.mts +43 -0
  38. package/src/commands/mcp/ping.mts +61 -0
  39. package/src/commands/mcp/status.mts +89 -0
  40. package/src/commands/mcp.mts +155 -0
  41. package/src/commands/milestone/rm.mts +35 -0
  42. package/src/commands/milestone/set.mts +49 -0
  43. package/src/commands/outcome/rm.mts +36 -0
  44. package/src/commands/outcome/set.mts +86 -0
  45. package/src/commands/pr/rm.mts +33 -0
  46. package/src/commands/pr/set.mts +110 -0
  47. package/src/commands/pr/yield.mts +114 -0
  48. package/src/commands/project/rm.mts +33 -0
  49. package/src/commands/project/set.mts +50 -0
  50. package/src/commands/queue.mts +41 -0
  51. package/src/commands/refresh/done.mts +42 -0
  52. package/src/commands/refresh/status.mts +40 -0
  53. package/src/commands/refresh.mts +56 -0
  54. package/src/commands/review/record.mts +46 -0
  55. package/src/commands/review/release.mts +49 -0
  56. package/src/commands/status.mts +85 -0
  57. package/src/commands/ticket/missing.mts +31 -0
  58. package/src/commands/ticket/rm.mts +33 -0
  59. package/src/commands/ticket/set.mts +134 -0
  60. package/src/commands/worker/rm.mts +46 -0
  61. package/src/commands/worker/set.mts +63 -0
  62. package/src/lib/cli/CLAUDE.md +13 -0
  63. package/src/lib/cli/cli.mts +226 -0
  64. package/src/lib/cli/index.mts +1 -0
  65. package/src/lib/command/CLAUDE.md +26 -0
  66. package/src/lib/command/__fixtures__/bad-export/oops.mts +1 -0
  67. package/src/lib/command/__fixtures__/bad-name/mismatch.mts +19 -0
  68. package/src/lib/command/__fixtures__/commands/cli-only.mts +20 -0
  69. package/src/lib/command/__fixtures__/commands/greet.mts +39 -0
  70. package/src/lib/command/__fixtures__/commands/math/add.mts +32 -0
  71. package/src/lib/command/__fixtures__/commands/mcp-only.mts +20 -0
  72. package/src/lib/command/__fixtures__/commands/needs-token.mts +19 -0
  73. package/src/lib/command/__fixtures__/commands/store/get.mts +26 -0
  74. package/src/lib/command/__fixtures__/commands/store.mts +26 -0
  75. package/src/lib/command/abstract-command.mts +104 -0
  76. package/src/lib/command/discovery.mts +100 -0
  77. package/src/lib/command/env.mts +19 -0
  78. package/src/lib/command/index.mts +6 -0
  79. package/src/lib/command/parse.mts +64 -0
  80. package/src/lib/command/test-support.mts +81 -0
  81. package/src/lib/command/transports.mts +17 -0
  82. package/src/lib/command/types.mts +53 -0
  83. package/src/lib/db/CLAUDE.md +13 -0
  84. package/src/lib/db/database.mts +160 -0
  85. package/src/lib/db/index.mts +4 -0
  86. package/src/lib/db/schema.mts +195 -0
  87. package/src/lib/db/time.mts +24 -0
  88. package/src/lib/db/with-database.mts +56 -0
  89. package/src/lib/errors/CLAUDE.md +18 -0
  90. package/src/lib/errors/command-error.mts +13 -0
  91. package/src/lib/errors/data-error.mts +12 -0
  92. package/src/lib/errors/definition-error.mts +6 -0
  93. package/src/lib/errors/dispatch-error.mts +27 -0
  94. package/src/lib/errors/ensure.mts +22 -0
  95. package/src/lib/errors/environment-error.mts +7 -0
  96. package/src/lib/errors/index.mts +8 -0
  97. package/src/lib/errors/json-rpc-error.mts +18 -0
  98. package/src/lib/errors/usage-error.mts +7 -0
  99. package/src/lib/graph/CLAUDE.md +17 -0
  100. package/src/lib/graph/anomalies.mts +110 -0
  101. package/src/lib/graph/derive.mts +96 -0
  102. package/src/lib/graph/index.mts +26 -0
  103. package/src/lib/graph/pipeline.mts +410 -0
  104. package/src/lib/graph/queries.mts +207 -0
  105. package/src/lib/graph/rows.mts +99 -0
  106. package/src/lib/graph/types.mts +137 -0
  107. package/src/lib/liveness/CLAUDE.md +14 -0
  108. package/src/lib/liveness/index.mts +10 -0
  109. package/src/lib/liveness/liveness.mts +147 -0
  110. package/src/lib/liveness/retire.mts +63 -0
  111. package/src/lib/logger/CLAUDE.md +12 -0
  112. package/src/lib/logger/index.mts +2 -0
  113. package/src/lib/logger/logger.mts +58 -0
  114. package/src/lib/logger/stream-sink.mts +23 -0
  115. package/src/lib/mcp/CLAUDE.md +21 -0
  116. package/src/lib/mcp/channel.mts +41 -0
  117. package/src/lib/mcp/dispatch.mts +60 -0
  118. package/src/lib/mcp/drain.mts +83 -0
  119. package/src/lib/mcp/index.mts +5 -0
  120. package/src/lib/mcp/mcp.mts +267 -0
  121. package/src/lib/mcp/tools.mts +77 -0
  122. package/src/lib/model/CLAUDE.md +8 -0
  123. package/src/lib/model/index.mts +3 -0
  124. package/src/lib/model/repo-caps.mts +95 -0
  125. package/src/lib/model/status.mts +91 -0
  126. package/src/lib/model/types.mts +83 -0
  127. package/src/lib/refresh/index.mts +2 -0
  128. package/src/lib/refresh/placeholders.mts +43 -0
  129. package/src/lib/refresh/refresh-service.mts +203 -0
  130. package/src/lib/schedule/CLAUDE.md +18 -0
  131. package/src/lib/schedule/caps.mts +113 -0
  132. package/src/lib/schedule/correlate.mts +69 -0
  133. package/src/lib/schedule/index.mts +7 -0
  134. package/src/lib/schedule/scheduler.mts +355 -0
  135. package/src/lib/schedule/tick.mts +266 -0
  136. package/src/lib/stores/CLAUDE.md +24 -0
  137. package/src/lib/stores/coordination.mts +359 -0
  138. package/src/lib/stores/cursor.mts +41 -0
  139. package/src/lib/stores/edge.mts +138 -0
  140. package/src/lib/stores/fetch-request.mts +346 -0
  141. package/src/lib/stores/index.mts +19 -0
  142. package/src/lib/stores/materialize.mts +69 -0
  143. package/src/lib/stores/milestone.mts +74 -0
  144. package/src/lib/stores/notice.mts +57 -0
  145. package/src/lib/stores/policy.mts +48 -0
  146. package/src/lib/stores/pr-event.mts +94 -0
  147. package/src/lib/stores/pr.mts +167 -0
  148. package/src/lib/stores/project.mts +79 -0
  149. package/src/lib/stores/refresh.mts +197 -0
  150. package/src/lib/stores/review.mts +113 -0
  151. package/src/lib/stores/session.mts +170 -0
  152. package/src/lib/stores/ticket.mts +246 -0
  153. package/src/lib/stores/watch.mts +360 -0
  154. package/src/lib/stores/worker.mts +121 -0
  155. package/src/lib/watch/adopt.mts +151 -0
  156. package/src/lib/watch/arm.mts +48 -0
  157. package/src/lib/watch/cadence.mts +45 -0
  158. package/src/lib/watch/diff.mts +274 -0
  159. package/src/lib/watch/index.mts +11 -0
  160. package/src/lib/watch/marker.mts +24 -0
  161. package/src/lib/watch/payload.mts +56 -0
  162. package/src/lib/watch/poll.mts +87 -0
  163. package/src/lib/watch/render.mts +61 -0
  164. package/src/lib/watch/snapshot.mts +312 -0
  165. package/src/main.mts +18 -0
@@ -0,0 +1,312 @@
1
+ import {execFile} from 'node:child_process';
2
+ import {promisify} from 'node:util';
3
+
4
+ import {DataError, EnvironmentError, ensure} from '../errors/index.mts';
5
+ import {writtenByThisAgent} from './marker.mts';
6
+
7
+ const run = promisify(execFile);
8
+
9
+ /**
10
+ * Everything one poll needs to decide what changed and say so specifically.
11
+ *
12
+ * `updatedAt` is deliberately absent: it moves on every write, including the
13
+ * agent's own, so a snapshot carrying it would report a change every time the
14
+ * worker touched its own PR. Each field here is instead something a waiting
15
+ * worker would act on.
16
+ */
17
+ export const SNAPSHOT_QUERY = `query($owner: String!, $name: String!, $number: Int!) {
18
+ repository(owner: $owner, name: $name) {
19
+ pullRequest(number: $number) {
20
+ headRefOid
21
+ state
22
+ isDraft
23
+ merged
24
+ mergeable
25
+ mergeStateStatus
26
+ reviewDecision
27
+ reviews(last: 20) {
28
+ totalCount
29
+ nodes { author { login } state submittedAt body }
30
+ }
31
+ reviewThreads(last: 50) {
32
+ totalCount
33
+ nodes {
34
+ id
35
+ isResolved
36
+ isOutdated
37
+ comments(last: 1) { nodes { author { login } createdAt body } }
38
+ }
39
+ }
40
+ comments(last: 30) {
41
+ totalCount
42
+ nodes { id author { login } createdAt body }
43
+ }
44
+ commits(last: 1) {
45
+ nodes {
46
+ commit {
47
+ statusCheckRollup {
48
+ state
49
+ contexts(last: 50) {
50
+ nodes {
51
+ __typename
52
+ ... on CheckRun {
53
+ name
54
+ conclusion
55
+ status
56
+ detailsUrl
57
+ }
58
+ ... on StatusContext {
59
+ context
60
+ state
61
+ targetUrl
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }`;
72
+
73
+ export interface CheckState {
74
+ readonly name: string;
75
+ /** `null` while the check is still running. */
76
+ readonly conclusion: string | null;
77
+ readonly url: string | null;
78
+ }
79
+
80
+ export interface ReviewState {
81
+ readonly author: string;
82
+ readonly state: string;
83
+ readonly submittedAt: string | null;
84
+ /** Carries this agent's machine marker; see `marker.mts`. */
85
+ readonly mine: boolean;
86
+ }
87
+
88
+ export interface ThreadState {
89
+ readonly id: string;
90
+ readonly resolved: boolean;
91
+ readonly outdated: boolean;
92
+ readonly lastAuthor: string | null;
93
+ readonly lastAt: string | null;
94
+ readonly mine: boolean;
95
+ }
96
+
97
+ export interface CommentState {
98
+ readonly id: string;
99
+ readonly author: string;
100
+ readonly createdAt: string;
101
+ readonly mine: boolean;
102
+ }
103
+
104
+ export interface PrSnapshot {
105
+ readonly head: string | null;
106
+ readonly state: string | null;
107
+ readonly draft: boolean;
108
+ readonly merged: boolean;
109
+ readonly mergeable: string | null;
110
+ readonly mergeState: string | null;
111
+ readonly reviewDecision: string | null;
112
+ readonly rollup: string | null;
113
+ readonly checks: readonly CheckState[];
114
+ readonly reviews: readonly ReviewState[];
115
+ readonly threads: readonly ThreadState[];
116
+ readonly comments: readonly CommentState[];
117
+ /**
118
+ * Totals as the forge reports them, against the windows the query asks for.
119
+ * A total past its window means the snapshot is missing older entries, and
120
+ * the diff must not read "absent from my window" as "deleted".
121
+ */
122
+ readonly totals: {
123
+ readonly reviews: number;
124
+ readonly threads: number;
125
+ readonly comments: number;
126
+ };
127
+ }
128
+
129
+ interface RawContext {
130
+ __typename?: string;
131
+ name?: string;
132
+ conclusion?: string | null;
133
+ status?: string | null;
134
+ detailsUrl?: string | null;
135
+ context?: string;
136
+ state?: string | null;
137
+ targetUrl?: string | null;
138
+ }
139
+
140
+ interface RawPr {
141
+ headRefOid?: string;
142
+ state?: string;
143
+ isDraft?: boolean;
144
+ merged?: boolean;
145
+ mergeable?: string | null;
146
+ mergeStateStatus?: string | null;
147
+ reviewDecision?: string | null;
148
+ reviews?: {
149
+ totalCount?: number;
150
+ nodes?: {
151
+ author?: {login?: string} | null;
152
+ state?: string;
153
+ submittedAt?: string | null;
154
+ body?: string;
155
+ }[];
156
+ };
157
+ reviewThreads?: {
158
+ totalCount?: number;
159
+ nodes?: {
160
+ id?: string;
161
+ isResolved?: boolean;
162
+ isOutdated?: boolean;
163
+ comments?: {
164
+ nodes?: {
165
+ author?: {login?: string} | null;
166
+ createdAt?: string;
167
+ body?: string;
168
+ }[];
169
+ };
170
+ }[];
171
+ };
172
+ comments?: {
173
+ totalCount?: number;
174
+ nodes?: {
175
+ id?: string;
176
+ author?: {login?: string} | null;
177
+ createdAt?: string;
178
+ body?: string;
179
+ }[];
180
+ };
181
+ commits?: {
182
+ nodes?: {
183
+ commit?: {
184
+ statusCheckRollup?: {
185
+ state?: string;
186
+ contexts?: {nodes?: RawContext[]};
187
+ } | null;
188
+ };
189
+ }[];
190
+ };
191
+ }
192
+
193
+ /**
194
+ * A status context reports `state`; a check run reports `conclusion` once it
195
+ * finishes and `status` while it runs. Normalizing both onto "conclusion or
196
+ * null-while-running" is what lets the diff talk about checks without caring
197
+ * which kind produced them.
198
+ */
199
+ function toCheck(context: RawContext): CheckState | null {
200
+ if (typeof context.name === 'string') {
201
+ const running = context.status !== 'COMPLETED';
202
+ return {
203
+ name: context.name,
204
+ conclusion: running ? null : (context.conclusion ?? null),
205
+ url: context.detailsUrl ?? null,
206
+ };
207
+ }
208
+ if (typeof context.context === 'string') {
209
+ const state = context.state ?? null;
210
+ return {
211
+ name: context.context,
212
+ conclusion: state === 'PENDING' || state === null ? null : state,
213
+ url: context.targetUrl ?? null,
214
+ };
215
+ }
216
+ return null;
217
+ }
218
+
219
+ function parse(pr: RawPr): PrSnapshot {
220
+ const rollupNode = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup ?? null;
221
+ return {
222
+ head: pr.headRefOid ?? null,
223
+ state: pr.state ?? null,
224
+ draft: pr.isDraft === true,
225
+ merged: pr.merged === true,
226
+ mergeable: pr.mergeable ?? null,
227
+ mergeState: pr.mergeStateStatus ?? null,
228
+ reviewDecision: pr.reviewDecision ?? null,
229
+ rollup: rollupNode?.state ?? null,
230
+ checks: (rollupNode?.contexts?.nodes ?? [])
231
+ .map(toCheck)
232
+ .filter((check): check is CheckState => check !== null)
233
+ .sort((a, b) => a.name.localeCompare(b.name)),
234
+ reviews: (pr.reviews?.nodes ?? []).map((review) => ({
235
+ author: review.author?.login ?? '',
236
+ state: review.state ?? '',
237
+ submittedAt: review.submittedAt ?? null,
238
+ mine: writtenByThisAgent(review.body),
239
+ })),
240
+ threads: (pr.reviewThreads?.nodes ?? []).map((thread) => {
241
+ const last = thread.comments?.nodes?.at(-1);
242
+ return {
243
+ id: thread.id ?? '',
244
+ resolved: thread.isResolved === true,
245
+ outdated: thread.isOutdated === true,
246
+ lastAuthor: last?.author?.login ?? null,
247
+ lastAt: last?.createdAt ?? null,
248
+ mine: writtenByThisAgent(last?.body),
249
+ };
250
+ }),
251
+ comments: (pr.comments?.nodes ?? []).map((comment) => ({
252
+ id: comment.id ?? '',
253
+ author: comment.author?.login ?? '',
254
+ createdAt: comment.createdAt ?? '',
255
+ mine: writtenByThisAgent(comment.body),
256
+ })),
257
+ totals: {
258
+ reviews: pr.reviews?.totalCount ?? 0,
259
+ threads: pr.reviewThreads?.totalCount ?? 0,
260
+ comments: pr.comments?.totalCount ?? 0,
261
+ },
262
+ };
263
+ }
264
+
265
+ export type Snapshotter = (
266
+ repo: string,
267
+ prNumber: number
268
+ ) => Promise<PrSnapshot>;
269
+
270
+ /** Snapshot a PR through `gh api graphql`: one call per poll. */
271
+ export const githubSnapshot: Snapshotter = async (repo, prNumber) => {
272
+ ensure(
273
+ /^[^/\s]+\/[^/\s]+$/u.test(repo),
274
+ () =>
275
+ new DataError(`"${repo}" is not an owner/repo`, {
276
+ hint: 'record the PR item with --repo <owner>/<name> before watching it.',
277
+ })
278
+ );
279
+ const [owner, name] = repo.split('/') as [string, string];
280
+ let stdout: string;
281
+ try {
282
+ ({stdout} = await run('gh', [
283
+ 'api',
284
+ 'graphql',
285
+ '-f',
286
+ `query=${SNAPSHOT_QUERY}`,
287
+ '-F',
288
+ `owner=${owner}`,
289
+ '-F',
290
+ `name=${name}`,
291
+ '-F',
292
+ `number=${String(prNumber)}`,
293
+ ]));
294
+ } catch (error) {
295
+ throw new EnvironmentError(
296
+ `gh could not read ${repo}#${String(prNumber)}: ${error instanceof Error ? error.message : String(error)}`,
297
+ {hint: 'check gh auth status and that the PR still exists.'}
298
+ );
299
+ }
300
+ const parsed = JSON.parse(stdout) as {
301
+ data?: {repository?: {pullRequest?: RawPr | null} | null};
302
+ };
303
+ const pr = parsed.data?.repository?.pullRequest;
304
+ ensure(
305
+ pr !== undefined && pr !== null,
306
+ () =>
307
+ new DataError(`no pull request ${repo}#${String(prNumber)}`, {
308
+ hint: 'the PR item names a PR the forge does not have; fix its --repo/--pr-number.',
309
+ })
310
+ );
311
+ return parse(pr);
312
+ };
package/src/main.mts ADDED
@@ -0,0 +1,18 @@
1
+ import {discover} from './lib/command/index.mts';
2
+ import {runCli} from './lib/cli/index.mts';
3
+ import {createLogger, streamSink} from './lib/logger/index.mts';
4
+
5
+ const tree = await discover(new URL('./commands/', import.meta.url));
6
+
7
+ // Diagnostics go to stderr on every path, because one of them is `dispatch mcp`:
8
+ // it serves JSON-RPC on stdout, and the default `console` sink would put
9
+ // `log`/`info`/`debug` into that stream. Command output rides `io`, not the
10
+ // logger, so nothing a caller reads moves.
11
+ process.exitCode = await runCli({
12
+ argv: process.argv.slice(2),
13
+ tree,
14
+ log: createLogger(streamSink(process.stderr)),
15
+ env: process.env,
16
+ stdout: process.stdout,
17
+ stderr: process.stderr,
18
+ });