@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,50 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {RefreshService} from '../../lib/refresh/index.mts';
5
+ import {ProjectStore} from '../../lib/stores/index.mts';
6
+
7
+ const options = {
8
+ id: {
9
+ type: 'string',
10
+ description: 'Tracker identifier for the project.',
11
+ positional: false,
12
+ required: true,
13
+ },
14
+ name: {
15
+ type: 'string',
16
+ description: 'Human-readable project name.',
17
+ positional: false,
18
+ required: true,
19
+ },
20
+ tracker: {
21
+ type: 'string',
22
+ description:
23
+ 'Tracker the project lives on, e.g. linear. Every ticket in it inherits this.',
24
+ positional: false,
25
+ required: true,
26
+ },
27
+ db: DB_OPTION,
28
+ } as const;
29
+
30
+ export class Command extends AbstractCommand {
31
+ readonly name = 'set';
32
+ readonly summary = 'Create or update one project.';
33
+ readonly env = [];
34
+ readonly options = options;
35
+
36
+ async run(
37
+ parsed: ParsedOptions<typeof options>,
38
+ ctx: CommandContext
39
+ ): Promise<void> {
40
+ await withDatabase(parsed.db, ctx.env, async (db) => {
41
+ await new ProjectStore(db).upsertProject({
42
+ id: parsed.id,
43
+ name: parsed.name,
44
+ source: parsed.tracker,
45
+ });
46
+ await new RefreshService(db).reconcile();
47
+ ctx.io.write(`project ${parsed.id}\n`);
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,41 @@
1
+ import {AbstractCommand} from '../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../lib/db/index.mts';
4
+ import {dispatchQueue} from '../lib/graph/index.mts';
5
+
6
+ const options = {
7
+ project: {
8
+ type: 'string',
9
+ description: 'Restrict the queue to one project id.',
10
+ positional: false,
11
+ required: false,
12
+ },
13
+ db: DB_OPTION,
14
+ } as const;
15
+
16
+ export class Command extends AbstractCommand {
17
+ readonly name = 'queue';
18
+ readonly summary =
19
+ 'Print what the scheduler would hand out next, in dispatch order.';
20
+ readonly env = [];
21
+ readonly options = options;
22
+
23
+ async run(
24
+ parsed: ParsedOptions<typeof options>,
25
+ ctx: CommandContext
26
+ ): Promise<void> {
27
+ await withDatabase(parsed.db, ctx.env, async (db) => {
28
+ const entries = await dispatchQueue(db, {project: parsed.project});
29
+ for (const {entry, pass} of entries) {
30
+ ctx.io.write(
31
+ `${pass ?? 'available'} ${entry.item.id} kind=${entry.item.kind}` +
32
+ (entry.item.project === null
33
+ ? ''
34
+ : ` project=${entry.item.project}`) +
35
+ `${entry.item.injected ? ' injected' : ''}\n`
36
+ );
37
+ }
38
+ if (entries.length === 0) ctx.io.write('queue empty\n');
39
+ });
40
+ }
41
+ }
@@ -0,0 +1,42 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {RefreshService} from '../../lib/refresh/index.mts';
5
+
6
+ const options = {
7
+ tracker: {
8
+ type: 'string',
9
+ description: 'Tracker whose scan is complete.',
10
+ positional: false,
11
+ required: true,
12
+ },
13
+ cursor: {
14
+ type: 'string',
15
+ description:
16
+ 'Opaque tracker token marking how far this scan read. Recorded only when the refresh closes.',
17
+ positional: false,
18
+ required: false,
19
+ },
20
+ db: DB_OPTION,
21
+ } as const;
22
+
23
+ export class Command extends AbstractCommand {
24
+ readonly name = 'done';
25
+ readonly summary = 'Report that everything the scan found has been written.';
26
+ readonly env = [];
27
+ readonly options = options;
28
+
29
+ async run(
30
+ parsed: ParsedOptions<typeof options>,
31
+ ctx: CommandContext
32
+ ): Promise<void> {
33
+ await withDatabase(parsed.db, ctx.env, async (db) => {
34
+ const {state, pending} = await new RefreshService(db).completeScan({
35
+ source: parsed.tracker,
36
+ cursor: parsed.cursor ?? null,
37
+ });
38
+ ctx.io.write(`refresh ${parsed.tracker} ${state}\n`);
39
+ for (const id of pending) ctx.io.write(`pending ${id}\n`);
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,40 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {RefreshService} from '../../lib/refresh/index.mts';
5
+
6
+ const options = {
7
+ tracker: {
8
+ type: 'string',
9
+ description: 'Tracker to report on.',
10
+ positional: false,
11
+ required: true,
12
+ },
13
+ db: DB_OPTION,
14
+ } as const;
15
+
16
+ export class Command extends AbstractCommand {
17
+ readonly name = 'status';
18
+ readonly summary =
19
+ 'Print the refresh state and every outstanding instruction.';
20
+ readonly env = [];
21
+ readonly options = options;
22
+
23
+ async run(
24
+ parsed: ParsedOptions<typeof options>,
25
+ ctx: CommandContext
26
+ ): Promise<void> {
27
+ await withDatabase(parsed.db, ctx.env, async (db) => {
28
+ const {refresh, requests} = await new RefreshService(db).status(
29
+ parsed.tracker
30
+ );
31
+ ctx.io.write(`refresh ${parsed.tracker} ${refresh?.state ?? 'none'}\n`);
32
+ for (const request of requests) {
33
+ if (request.resolution !== null) continue;
34
+ ctx.io.write(
35
+ `${request.kind} ${JSON.stringify(request.payload)} delivered=${String(request.deliveredAt !== null)}\n`
36
+ );
37
+ }
38
+ });
39
+ }
40
+ }
@@ -0,0 +1,56 @@
1
+ import {AbstractCommand} from '../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../lib/db/index.mts';
4
+ import {RefreshService} from '../lib/refresh/index.mts';
5
+
6
+ const options = {
7
+ tracker: {
8
+ type: 'string',
9
+ description: 'Tracker to refresh, e.g. linear.',
10
+ positional: false,
11
+ required: true,
12
+ },
13
+ project: {
14
+ type: 'string',
15
+ description: 'Comma-separated project ids to scan.',
16
+ positional: false,
17
+ required: true,
18
+ },
19
+ rebuild: {
20
+ type: 'boolean',
21
+ description: 'Drop the graph and scan from scratch, ignoring the cursor.',
22
+ positional: false,
23
+ required: false,
24
+ },
25
+ db: DB_OPTION,
26
+ } as const;
27
+
28
+ export class Command extends AbstractCommand {
29
+ readonly name = 'refresh';
30
+ readonly summary =
31
+ 'Start or resume building the project graph for a tracker.';
32
+ readonly env = [];
33
+ readonly options = options;
34
+
35
+ async run(
36
+ parsed: ParsedOptions<typeof options>,
37
+ ctx: CommandContext
38
+ ): Promise<void> {
39
+ const projects = parsed.project
40
+ .split(',')
41
+ .map((id) => id.trim())
42
+ .filter((id) => id !== '');
43
+
44
+ await withDatabase(parsed.db, ctx.env, async (db) => {
45
+ const {resumed} = await new RefreshService(db).startScan({
46
+ source: parsed.tracker,
47
+ projects,
48
+ sessionId: ctx.env.CLAUDE_CODE_SESSION_ID ?? null,
49
+ rebuild: parsed.rebuild,
50
+ });
51
+ ctx.io.write(
52
+ `refresh ${parsed.tracker} ${resumed ? 'resumed' : 'opened'}\n`
53
+ );
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,46 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, nowIso, withDatabase} from '../../lib/db/index.mts';
4
+ import {RefreshService} from '../../lib/refresh/index.mts';
5
+ import {correlateSession} from '../../lib/schedule/index.mts';
6
+ import {ReviewStore} from '../../lib/stores/index.mts';
7
+
8
+ const options = {
9
+ milestone: {
10
+ type: 'string',
11
+ description: 'The milestone whose review just ran.',
12
+ positional: false,
13
+ required: true,
14
+ },
15
+ session: {
16
+ type: 'string',
17
+ description:
18
+ 'Registry id whose claim this releases; defaults to the session correlated from the environment.',
19
+ positional: false,
20
+ required: false,
21
+ },
22
+ db: DB_OPTION,
23
+ } as const;
24
+
25
+ export class Command extends AbstractCommand {
26
+ readonly name = 'record';
27
+ readonly summary =
28
+ 'Record a milestone review with a member snapshot, opening its gate.';
29
+ readonly env = [];
30
+ readonly options = options;
31
+
32
+ async run(
33
+ parsed: ParsedOptions<typeof options>,
34
+ ctx: CommandContext
35
+ ): Promise<void> {
36
+ await withDatabase(parsed.db, ctx.env, async (db) => {
37
+ // No server means no claim of this caller's to release; ambiguous
38
+ // correlation refuses rather than leave a real claim held.
39
+ const session =
40
+ (await correlateSession(db, ctx.env, parsed.session)) ?? '';
41
+ await new ReviewStore(db).record(parsed.milestone, nowIso(), session);
42
+ await new RefreshService(db).reconcile();
43
+ ctx.io.write(`review ${parsed.milestone}\n`);
44
+ });
45
+ }
46
+ }
@@ -0,0 +1,49 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {correlateSession} from '../../lib/schedule/index.mts';
5
+ import {ReviewStore} from '../../lib/stores/index.mts';
6
+
7
+ const options = {
8
+ milestone: {
9
+ type: 'string',
10
+ description: 'The milestone whose review ended without recording.',
11
+ positional: false,
12
+ required: true,
13
+ },
14
+ session: {
15
+ type: 'string',
16
+ description:
17
+ 'Registry id whose claim this releases; defaults to the session correlated from the environment.',
18
+ positional: false,
19
+ required: false,
20
+ },
21
+ db: DB_OPTION,
22
+ } as const;
23
+
24
+ export class Command extends AbstractCommand {
25
+ readonly name = 'release';
26
+ readonly summary =
27
+ 'Release a milestone-review claim without opening the gate.';
28
+ readonly env = [];
29
+ readonly options = options;
30
+
31
+ async run(
32
+ parsed: ParsedOptions<typeof options>,
33
+ ctx: CommandContext
34
+ ): Promise<void> {
35
+ await withDatabase(parsed.db, ctx.env, async (db) => {
36
+ // No server means no claim of this caller's to release; ambiguous
37
+ // correlation refuses rather than leave a real claim held.
38
+ const session =
39
+ (await correlateSession(db, ctx.env, parsed.session)) ?? '';
40
+ const released = await new ReviewStore(db).release(
41
+ parsed.milestone,
42
+ session
43
+ );
44
+ ctx.io.write(
45
+ `released review ${parsed.milestone} existed=${String(released)}\n`
46
+ );
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,85 @@
1
+ import {AbstractCommand} from '../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../lib/db/index.mts';
4
+ import {derive, dispatchQueue, repoPrLoad} from '../lib/graph/index.mts';
5
+ import {RepoAdmission} from '../lib/schedule/index.mts';
6
+ import {PolicyStore} from '../lib/stores/index.mts';
7
+
8
+ const options = {
9
+ project: {
10
+ type: 'string',
11
+ description:
12
+ 'Restrict the report to one project id. Anomalies are graph-wide and stay unfiltered.',
13
+ positional: false,
14
+ required: false,
15
+ },
16
+ db: DB_OPTION,
17
+ } as const;
18
+
19
+ export class Command extends AbstractCommand {
20
+ readonly name = 'status';
21
+ readonly summary =
22
+ 'Print per-project counts, milestone gates, anomalies, and the terminal verdict.';
23
+ readonly env = [];
24
+ readonly options = options;
25
+
26
+ async run(
27
+ parsed: ParsedOptions<typeof options>,
28
+ ctx: CommandContext
29
+ ): Promise<void> {
30
+ await withDatabase(parsed.db, ctx.env, async (db) => {
31
+ const graph = await derive(db, {project: parsed.project});
32
+
33
+ for (const counts of graph.counts) {
34
+ ctx.io.write(
35
+ `project ${counts.project} total=${String(counts.total)} ` +
36
+ `available=${String(counts.available)} blocked=${String(counts.blocked)} ` +
37
+ `human-blocked=${String(counts.humanBlocked)} in-flight=${String(counts.inFlight)} ` +
38
+ `dormant=${String(counts.dormant)} verified=${String(counts.verified)} ` +
39
+ `canceled=${String(counts.canceled)} terminal=${String(counts.terminal)}\n`
40
+ );
41
+ }
42
+ for (const milestone of graph.milestones) {
43
+ ctx.io.write(
44
+ `milestone ${milestone.id} project=${milestone.project} ` +
45
+ `members=${String(milestone.memberCount)} unresolved=${String(milestone.openCount)} ` +
46
+ `ready-for-review=${String(milestone.readyForReview)} ` +
47
+ `review-recorded=${String(milestone.reviewRecorded)} open=${String(milestone.open)}\n`
48
+ );
49
+ }
50
+ for (const entry of graph.prs) {
51
+ ctx.io.write(
52
+ `pr ${entry.item.id}` +
53
+ (entry.item.ticket === null ? '' : ` ticket=${entry.item.ticket}`) +
54
+ ` ${entry.classification}\n`
55
+ );
56
+ }
57
+ // Why the queue is idle when it is: a repo at one of its caps holds
58
+ // work back with nothing else to show for it, which otherwise reads as
59
+ // a wedged system. Deliberately no `reserve` here — this reports the
60
+ // caps holding work *now*, from load already on the forge. Simulating
61
+ // the fill pass instead would blame a cap for a queue the compute budget
62
+ // is what is actually holding, and the scheduler's own claims land in
63
+ // this reading within a tick anyway.
64
+ const admission = new RepoAdmission(
65
+ await new PolicyStore(db).getRepoCaps(),
66
+ await repoPrLoad(db)
67
+ );
68
+ for (const {entry} of await dispatchQueue(db, {
69
+ project: parsed.project,
70
+ })) {
71
+ if (entry.item.kind === 'pr') admission.admit(entry.item);
72
+ }
73
+ for (const hold of admission.holds()) {
74
+ ctx.io.write(
75
+ `cap-hold ${hold.repo} ${hold.cap}=${String(hold.observed)}/${String(hold.limit)} ` +
76
+ `waiting=${String(hold.waiting)}\n`
77
+ );
78
+ }
79
+ for (const anomaly of graph.anomalies) {
80
+ ctx.io.write(`anomaly ${anomaly.kind} ${anomaly.detail}\n`);
81
+ }
82
+ ctx.io.write(`terminal=${String(graph.terminal)}\n`);
83
+ });
84
+ }
85
+ }
@@ -0,0 +1,31 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {RefreshService} from '../../lib/refresh/index.mts';
5
+
6
+ const options = {
7
+ id: {
8
+ type: 'string',
9
+ description: 'The ticket id the tracker has no record of.',
10
+ positional: false,
11
+ required: true,
12
+ },
13
+ db: DB_OPTION,
14
+ } as const;
15
+
16
+ export class Command extends AbstractCommand {
17
+ readonly name = 'missing';
18
+ readonly summary = 'Report that a requested ticket does not exist.';
19
+ readonly env = [];
20
+ readonly options = options;
21
+
22
+ async run(
23
+ parsed: ParsedOptions<typeof options>,
24
+ ctx: CommandContext
25
+ ): Promise<void> {
26
+ await withDatabase(parsed.db, ctx.env, async (db) => {
27
+ await new RefreshService(db).markMissing(parsed.id);
28
+ ctx.io.write(`missing ticket ${parsed.id}\n`);
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,33 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {RefreshService} from '../../lib/refresh/index.mts';
5
+ import {TicketStore} from '../../lib/stores/index.mts';
6
+
7
+ const options = {
8
+ id: {
9
+ type: 'string',
10
+ description: 'Tracker identifier for the ticket.',
11
+ positional: false,
12
+ required: true,
13
+ },
14
+ db: DB_OPTION,
15
+ } as const;
16
+
17
+ export class Command extends AbstractCommand {
18
+ readonly name = 'rm';
19
+ readonly summary = 'Delete one ticket.';
20
+ readonly env = [];
21
+ readonly options = options;
22
+
23
+ async run(
24
+ parsed: ParsedOptions<typeof options>,
25
+ ctx: CommandContext
26
+ ): Promise<void> {
27
+ await withDatabase(parsed.db, ctx.env, async (db) => {
28
+ const existed = await new TicketStore(db).removeTicket(parsed.id);
29
+ await new RefreshService(db).reconcile();
30
+ ctx.io.write(`removed ticket ${parsed.id} existed=${String(existed)}\n`);
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,134 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {STATUSES, TARGET_KINDS} from '../../lib/model/status.mts';
5
+ import {RefreshService} from '../../lib/refresh/index.mts';
6
+ import {TicketStore} from '../../lib/stores/index.mts';
7
+
8
+ const options = {
9
+ id: {
10
+ type: 'string',
11
+ description: 'Tracker identifier, e.g. CLC-945.',
12
+ positional: false,
13
+ required: true,
14
+ },
15
+ project: {
16
+ type: 'string',
17
+ description: 'Project the ticket belongs to.',
18
+ positional: false,
19
+ required: true,
20
+ },
21
+ status: {
22
+ type: 'string',
23
+ description: 'Normalized lifecycle status; map the tracker state yourself.',
24
+ positional: false,
25
+ required: true,
26
+ choices: STATUSES,
27
+ },
28
+ title: {
29
+ type: 'string',
30
+ description: 'Ticket title.',
31
+ positional: false,
32
+ required: false,
33
+ },
34
+ url: {
35
+ type: 'string',
36
+ description: 'Ticket URL.',
37
+ positional: false,
38
+ required: false,
39
+ },
40
+ 'target-kind': {
41
+ type: 'string',
42
+ description: 'What finishing this ticket produces.',
43
+ positional: false,
44
+ required: false,
45
+ choices: TARGET_KINDS,
46
+ },
47
+ 'requires-human': {
48
+ type: 'boolean',
49
+ description: 'Only a human may work this ticket.',
50
+ positional: false,
51
+ required: false,
52
+ },
53
+ injected: {
54
+ type: 'boolean',
55
+ description: 'Rank this ticket to the top of the frontier.',
56
+ positional: false,
57
+ required: false,
58
+ },
59
+ priority: {
60
+ type: 'number',
61
+ description: 'Lower is more urgent; omit if the tracker has none.',
62
+ positional: false,
63
+ required: false,
64
+ },
65
+ labels: {
66
+ type: 'string',
67
+ description: 'Comma-separated tracker labels, passed through as-is.',
68
+ positional: false,
69
+ required: false,
70
+ },
71
+ 'branch-hint': {
72
+ type: 'string',
73
+ description: 'Branch-name seed the tracker suggests.',
74
+ positional: false,
75
+ required: false,
76
+ },
77
+ 'updated-at': {
78
+ type: 'string',
79
+ description: 'When the tracker last saw the ticket move (RFC 3339).',
80
+ positional: false,
81
+ required: false,
82
+ },
83
+ db: DB_OPTION,
84
+ } as const;
85
+
86
+ export class Command extends AbstractCommand {
87
+ readonly name = 'set';
88
+ readonly summary = 'Create or update one ticket.';
89
+ readonly env = [];
90
+ readonly options = options;
91
+
92
+ async run(
93
+ parsed: ParsedOptions<typeof options>,
94
+ ctx: CommandContext
95
+ ): Promise<void> {
96
+ const labels =
97
+ parsed.labels === undefined
98
+ ? undefined
99
+ : parsed.labels
100
+ .split(',')
101
+ .map((label) => label.trim())
102
+ .filter((label) => label !== '');
103
+
104
+ await withDatabase(parsed.db, ctx.env, async (db) => {
105
+ // Only what the caller named. A re-read reports the fields it was asked
106
+ // about and says nothing about the rest, so absent must mean "leave it"
107
+ // — otherwise answering a refresh blanks the ticket's title and labels.
108
+ // `injected` and `requires-human` are carried only when set: the parser
109
+ // cannot tell an absent boolean from a false one.
110
+ await new TicketStore(db).patchTicket({
111
+ id: parsed.id,
112
+ project: parsed.project,
113
+ status: parsed.status,
114
+ ...(parsed.url === undefined ? {} : {url: parsed.url}),
115
+ ...(parsed.title === undefined ? {} : {title: parsed.title}),
116
+ ...(parsed['target-kind'] === undefined
117
+ ? {}
118
+ : {targetKind: parsed['target-kind']}),
119
+ ...(parsed['requires-human'] ? {requiresHuman: true} : {}),
120
+ ...(parsed.injected ? {injected: true} : {}),
121
+ ...(parsed.priority === undefined ? {} : {priority: parsed.priority}),
122
+ ...(parsed['branch-hint'] === undefined
123
+ ? {}
124
+ : {branchHint: parsed['branch-hint']}),
125
+ ...(labels === undefined ? {} : {labels}),
126
+ ...(parsed['updated-at'] === undefined
127
+ ? {}
128
+ : {updatedAt: parsed['updated-at']}),
129
+ });
130
+ await new RefreshService(db).reconcile();
131
+ ctx.io.write(`ticket ${parsed.id}\n`);
132
+ });
133
+ }
134
+ }
@@ -0,0 +1,46 @@
1
+ import {AbstractCommand} from '../../lib/command/index.mts';
2
+ import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
3
+ import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
4
+ import {DataError, ensure} from '../../lib/errors/index.mts';
5
+ import {correlateSession} from '../../lib/schedule/index.mts';
6
+ import {WorkerStore} from '../../lib/stores/index.mts';
7
+
8
+ const options = {
9
+ node: {
10
+ type: 'string',
11
+ description: 'The node whose worker address to drop.',
12
+ positional: false,
13
+ required: true,
14
+ },
15
+ db: DB_OPTION,
16
+ } as const;
17
+
18
+ /**
19
+ * Hand a node from warm relay to cold recovery, for a relay that provably
20
+ * went nowhere: drops the caller's own address and releases its claim, so
21
+ * the scheduler re-serves the item as a `resume` pass.
22
+ */
23
+ export class Command extends AbstractCommand {
24
+ readonly name = 'rm';
25
+ readonly summary = "Drop a node's worker address.";
26
+ readonly env = [];
27
+ readonly options = options;
28
+
29
+ async run(
30
+ parsed: ParsedOptions<typeof options>,
31
+ ctx: CommandContext
32
+ ): Promise<void> {
33
+ await withDatabase(parsed.db, ctx.env, async (db) => {
34
+ const session = await correlateSession(db, ctx.env, undefined);
35
+ ensure(
36
+ session !== null,
37
+ () =>
38
+ new DataError('no live server correlates to this session', {
39
+ hint: 'only the session that recorded the address can revoke it.',
40
+ })
41
+ );
42
+ const removed = await new WorkerStore(db).remove(parsed.node, session);
43
+ ctx.io.write(`worker ${parsed.node} removed=${String(removed)}\n`);
44
+ });
45
+ }
46
+ }