@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,103 @@
1
+ # land — team operator mode
2
+
3
+ The operator directs the agent but is one of several humans. Review happens in
4
+ two stages. First the operator reviews alone, while the PR is still a draft.
5
+ Then the operator — never the agent — clears draft, and the rest of the team
6
+ reviews. The team stage is a plain review request, not an engagement.
7
+
8
+ ## Gates 6–7 in team
9
+
10
+ - **Gate 6 (operator-approved)** is satisfied during `private_review_*`. Draft
11
+ clearing is one of its signals here: it is the operator's alone to do, so
12
+ `<terminal state>` moving off `draft` is an approval in its own right. The
13
+ remaining signals are the ones your credentials file lists.
14
+ - **Gate 7 (team-approved)** is satisfied during `public_review_*`: at least
15
+ one `<review mode="human" role="team" state="approved">` from a non-self,
16
+ non-operator reviewer and no current `changes_requested`.
17
+
18
+ ## Draft clearing
19
+
20
+ The agent **never** clears draft; the operator does. Poll on the reviewer
21
+ cadence until the PR is no longer a draft, then proceed to
22
+ `ready_for_public_review`.
23
+
24
+ ## Lifecycle
25
+
26
+ ```mermaid
27
+ stateDiagram-v2
28
+ [*] --> starting
29
+
30
+ starting --> draft: worktree + empty commit + draft PR + plan comment
31
+
32
+ draft --> ready_for_copilot_review: ready · gates 1-5 · Copilot available
33
+ draft --> ready_for_private_review: ready · gates 1-5 · Copilot unavailable
34
+
35
+ ready_for_copilot_review --> copilot_review_requested: review requested
36
+
37
+ copilot_review_requested --> copilot_commented: Copilot left actionable items
38
+ copilot_review_requested --> ready_for_private_review: Copilot reviewed · zero actionable
39
+
40
+ copilot_commented --> ready_for_copilot_review: addressed · gates 1-5 · re-request
41
+
42
+ ready_for_private_review --> private_review_requested: operator engagement sent
43
+
44
+ private_review_requested --> private_review_commented: operator commented (no formal verdict)
45
+ private_review_requested --> private_review_requested_changes: operator changes_requested
46
+ private_review_requested --> private_review_approved: gate 6 satisfied
47
+
48
+ private_review_commented --> ready_for_private_review: addressed · gates 1-5 · re-engage
49
+ private_review_requested_changes --> ready_for_private_review: addressed · gates 1-5 · re-engage (required to unblock)
50
+
51
+ private_review_approved --> ready_for_public_review: operator cleared draft
52
+
53
+ ready_for_public_review --> public_review_requested: team review requested (operator excluded)
54
+ ready_for_public_review --> public_review_requested: no eligible reviewer (skip request)
55
+
56
+ public_review_requested --> public_review_commented: reviewer commented (no formal verdict)
57
+ public_review_requested --> public_review_requested_changes: reviewer changes_requested
58
+ public_review_requested --> public_review_approved: gate 7 satisfied
59
+
60
+ public_review_commented --> ready_for_public_review: addressed · gates 1-5 · re-request
61
+ public_review_requested_changes --> ready_for_public_review: addressed · gates 1-5 · re-request (required to unblock merge)
62
+
63
+ public_review_approved --> ready_for_merge: gates 1-5 still hold
64
+
65
+ ready_for_merge --> merged: PR closed (terminal resolved by pr-status)
66
+
67
+ merged --> done: worktree removed
68
+
69
+ done --> [*]
70
+ ```
71
+
72
+ ## States
73
+
74
+ | State | Do | Poll? |
75
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------- |
76
+ | `starting` | Create or locate the worktree (see Setup in `SKILL.md`). | no |
77
+ | `draft` | **Coding happens here.** Edit; pre-push review; push. When ready, check gates 1–5. | no |
78
+ | `ready_for_copilot_review` | Request Copilot review. | no |
79
+ | `copilot_review_requested` | Await Copilot's review. | CI |
80
+ | `copilot_commented` | Address each actionable Copilot item; push fix(es). | no |
81
+ | `ready_for_private_review` | Engage the operator while in draft: post the engagement comment (agent-reply marker + `<!-- agent-engagement:<agent-id> -->` sentinel) and notify via your credentials file's venue. | no |
82
+ | `private_review_requested` | Await the operator's signal. | reviewer |
83
+ | `private_review_commented` | Address each item; push; re-engage. | no |
84
+ | `private_review_requested_changes` | Address; push; **re-engage required** — blocks public review. | no |
85
+ | `private_review_approved` | Poll until the PR is no longer a draft (see Draft clearing), then → `ready_for_public_review`. | reviewer |
86
+ | `ready_for_public_review` | Request review from team reviewer(s), **excluding the operator**. Never self-request. | no |
87
+ | `public_review_requested` | Await the public reviewer. | reviewer |
88
+ | `public_review_commented` | Address each item; push; re-request. | no |
89
+ | `public_review_requested_changes` | Address; push; **re-request required** — blocks merge. | no |
90
+ | `public_review_approved` | Confirm gates 1–5 still hold; else fix in place. | no |
91
+ | `ready_for_merge` | Await merge. **Don't self-merge unless instructed.** | merge |
92
+ | `merged` | Handle per **Ending the run** in `SKILL.md`. | no |
93
+ | `done` | Terminal. | — |
94
+
95
+ ## No eligible reviewer
96
+
97
+ If no non-self, non-operator reviewer exists in `ready_for_public_review`,
98
+ skip the request but still transition to `public_review_requested` and keep
99
+ polling on the reviewer cadence. Gate 7 is then unreachable — the PR merges
100
+ out-of-band, the agent observes closure on a poll, and `merged → done` fires.
101
+ Same for the sole-reviewer case: the operator approves privately, clears draft,
102
+ and merges. A closed PR ends the run from any state — see **Ending the run** in
103
+ `SKILL.md`. "Nobody to ask" never terminates.
@@ -0,0 +1,152 @@
1
+ # land — protocol reference
2
+
3
+ ## Roles
4
+
5
+ - **Agent** — the agentic coding assistant doing the work (this skill).
6
+ - **Operator** — the one individual directing the agent; almost certainly
7
+ human; the only human with stop authority.
8
+ - **Reviewer** — any participant leaving review feedback (Copilot, another agent,
9
+ or a human). The operator may also be a reviewer.
10
+
11
+ The credential mode is plugin config, stated in the skill's Environment
12
+ section and described by your credentials file. Never infer it from account
13
+ names.
14
+
15
+ ## Wire format
16
+
17
+ Every agent-authored post (new post or thread reply — not reactions) carries one
18
+ machine marker as its **first line**, alone, no leading whitespace:
19
+
20
+ ```text
21
+ <!-- agent-reply:<agent-id> -->
22
+ ```
23
+
24
+ That marker is the whole of the universal format. Your credentials file gives
25
+ the body format for this environment — follow it exactly, and add nothing it
26
+ doesn't call for.
27
+
28
+ The plan-comment sentinel `<!-- agent-plan:<agent-id> -->` goes **inside** the
29
+ body, **alone on its own line** — that is what `pr-status` matches, and a
30
+ sentinel sharing a line with prose is not seen at all, leaving the comment
31
+ actionable forever.
32
+
33
+ ## Terminal signals
34
+
35
+ A terminal signal means "finished with this item"; it suppresses re-evaluation
36
+ next poll. Anything else means "still working." The agent signals finished
37
+ **only** via a terminal signal — it MUST NOT resolve the thread, even one it
38
+ opened. Platform-resolved threads are read (see
39
+ [Actionability](#actionability)) but never written by the agent.
40
+
41
+ Reactions settle a **top-level comment** and are preferred there. They never
42
+ settle a thread — `pr-status` doesn't read them on threads; use a
43
+ terminal-tagged reply instead.
44
+
45
+ | Reaction | Meaning |
46
+ | -------- | ------------------------------------- |
47
+ | `+1` | Terminal — addressed / agreed |
48
+ | `-1` | Terminal — rejected (with a reply) |
49
+ | `rocket` | Terminal — shipped / merged / applied |
50
+ | `eyes` | Non-terminal — seen, in progress |
51
+
52
+ Text tokens are the terminal mechanism on threads, and on any platform without
53
+ reactions. When you emit one it must be the **last non-empty line of the whole
54
+ comment** — after any closing wrapper your credentials file adds, not inside it. Emit only
55
+ these three. The reader is more lenient
56
+ than the writer: `pr-status` also accepts `✓`, `✅`, `acknowledged`, `wontfix`,
57
+ `dismissed`, and `resolved`, so a reviewer's stray "resolved" can settle an
58
+ item — don't rely on it, and don't add to this set:
59
+
60
+ | Token | Meaning |
61
+ | ----------- | --------------------- |
62
+ | `Done.` | Terminal (≡ `+1`) |
63
+ | `Declined.` | Terminal (≡ `-1`) |
64
+ | `Shipped.` | Terminal (≡ `rocket`) |
65
+
66
+ ## Review rules
67
+
68
+ - An agent MUST NOT request review from the account it is authenticated as.
69
+ - Per-credential-mode rules are in your credentials file.
70
+ - The self-request prohibition constrains the *request*, not the loop — the
71
+ no-eligible-reviewer handling in the operator-mode files (skip the request,
72
+ keep polling) still applies.
73
+
74
+ ### Operator engagement
75
+
76
+ `land` engages the operator on the edge your operator-mode file marks. Each
77
+ engagement is two parts:
78
+
79
+ 1. **Notification** — the venue your credentials file prescribes.
80
+ 2. **Engagement comment** — a top-level PR comment with the
81
+ `<!-- agent-reply:<agent-id> -->` marker on its first line and the
82
+ engagement sentinel `<!-- agent-engagement:<agent-id> -->` alone on a line of
83
+ its own anywhere after it. Post it in both credential modes; it anchors the
84
+ reaction- and reply-based Gate 6 signals.
85
+
86
+ The sentinel makes the comment classify **non-actionable** (like the plan
87
+ comment) — without it, the agent's own soliciting comment stays actionable
88
+ forever, failing Gate 4 and blocking draft-clear/merge. Do **not** terminal-tag
89
+ it instead: the agent is awaiting approval, not finished.
90
+
91
+ ## Actionability
92
+
93
+ `pr-status` classifies each item `actionable="true|false"` by the rules below.
94
+
95
+ A comment or thread is **non-actionable** iff any of:
96
+
97
+ - it's one of the calling agent's artifact comments — an `agent-plan` or
98
+ `agent-engagement` sentinel AND author = the calling gh identity (the author
99
+ match keeps a human quoting a marker actionable).
100
+ - the newest comment was written by the calling agent (author = calling
101
+ identity) AND carries an `agent-reply` marker AND its last non-empty line is a
102
+ terminal token from the table above (case-insensitive, trailing period
103
+ optional). The author match keys on the gh-authenticated login.
104
+ - the calling agent reacted to it with a terminal reaction (`+1`/`-1`/`rocket`;
105
+ comments only). Top-level comments have no reply threading, so this is the
106
+ only signal that can settle a comment someone else authored.
107
+ - the platform has explicitly resolved the thread (threads only).
108
+
109
+ A reviewer reply after the agent's last turn re-actionables the item. An
110
+ annotation is actionable unless `<cache>/<id>.ack` exists.
111
+
112
+ A `<review>` carries the review's own prose, separate from its inline threads.
113
+ It is actionable unless the body is empty (`reason="no-body"` — a bare verdict,
114
+ or a reviewer who hasn't reviewed yet), the review it came from was dismissed,
115
+ or `<cache>/<id>.ack` exists. A bare verdict over an earlier substantive review
116
+ keeps the earlier body, so `state` and the body can come from different reviews.
117
+ A review body has no reply thread and no reactions, so `.ack` is the only way to
118
+ settle one. Answer first — reply in a top-level comment — unless the body asks
119
+ for nothing, in which case the `.ack` rationale is the whole answer.
120
+
121
+ ## Operational logging
122
+
123
+ One line per entry:
124
+
125
+ ```text
126
+ <timestamp> <kind> ticket=<ticket-url> pr=<pr-url> ticket-role=<role> pr-state=<state> | <message>
127
+ ```
128
+
129
+ - `<timestamp>` — RFC 3339 with offset, second precision.
130
+ - `ticket=`/`pr=` — full URLs, never bare IDs; `-` when absent.
131
+ - `ticket-role=` — a role name from [`ticket.md`](./ticket.md); `-` on
132
+ PR-only runs.
133
+ - `<pr-state>` — `draft` | `open` | `shipped` | `abandoned`; `-` when no PR.
134
+
135
+ Kinds `land` emits:
136
+
137
+ | Kind | When |
138
+ | ------------ | --------------------------------------------------------------------------------------- |
139
+ | `INFO` | Heartbeats while polling; substantive non-state events. |
140
+ | `WAIT` | Entering a poll; message names the venue and awaited outcome. |
141
+ | `RESUME` | The awaited condition is met and work resumes. |
142
+ | `ERROR` | Errors surfaced but not immediately fatal. |
143
+ | `BLOCK` | A "report and stop" path — an unclaimable ticket, a refused required request. |
144
+ | `TRANSITION` | (Ticket-backed runs) a ticket role change. |
145
+
146
+ Each ticket role change also gets a state-change comment on the ticket, in wire
147
+ format. Body exactly:
148
+
149
+ ```text
150
+ State: <prev-role> → <new-role>
151
+ Rationale: <one line>
152
+ ```
@@ -0,0 +1,94 @@
1
+ # land — ticket-backed runs
2
+
3
+ Applies only when Intake resolved a ticket.
4
+
5
+ ## Resolving the tracker
6
+
7
+ A ticket URL names its own tracker (`linear.app/<workspace>/issue/DEV-123` →
8
+ `linear`); a bare id (`DEV-123`) uses `${user_config.tracker}`. Linear bindings
9
+ are below. For any other tracker, map its states onto these roles yourself
10
+ through its MCP server. A native state whose role is ambiguous maps to no
11
+ role — see Claim.
12
+
13
+ ## Roles
14
+
15
+ Speak these role names, never a tracker's own state names.
16
+
17
+ | Role | Meaning |
18
+ | ------------- | ------------------------------------------------ |
19
+ | `available` | Eligible to be picked up. |
20
+ | `in-progress` | Actively being worked. |
21
+ | `in-review` | Primary work complete; iterating with reviewers. |
22
+ | `delivered` | Merged or deployed; not yet verified. |
23
+ | `verified` | Validated against the ticket's aims. Read-only. |
24
+ | `canceled` | Will not be done. |
25
+
26
+ Forward path — `available → in-progress → in-review → delivered`. Never invent
27
+ a native state. When the tracker can't express the target role, stop at the
28
+ last role it can, say so, and let the operator close it: with no `delivered`
29
+ state, the ticket stays `in-review` when the PR ships.
30
+
31
+ ## Transitions
32
+
33
+ | Ticket edge | Fires when |
34
+ | ------------------------- | ---------------------------------------------------------------------- |
35
+ | `available → in-progress` | Claiming, before the first push (below). |
36
+ | `in-progress → in-review` | The run reaches its first `*_review_requested` state. |
37
+ | `→ delivered` | `<terminal state="shipped">`, **only if this PR completes the ticket**. |
38
+
39
+ A ticket that needs more than one PR stays `in-review` when this one lands:
40
+ record the shipped PR in a ticket comment and say which aims remain. Never
41
+ write `verified`; a run ends at `delivered`.
42
+
43
+ `<terminal state="abandoned">` transitions nothing. Report the closure on the
44
+ ticket and stop.
45
+
46
+ Every transition emits a `TRANSITION` log line and a state-change comment on
47
+ the ticket ([format](./reference.md#operational-logging)).
48
+
49
+ ## Claim
50
+
51
+ Steps 1–3 run before the first push:
52
+
53
+ 1. Resolve the current role and act on it:
54
+ - `available` — claimable; continue.
55
+ - `in-progress` or `in-review` assigned to **you** — a resumed run. Skip
56
+ steps 2–3.
57
+ - `in-progress` or `in-review` **unassigned** — claimable; do step 2, skip
58
+ step 3.
59
+ - `in-progress` or `in-review` assigned to **anyone else** — they are on it.
60
+ Report and stop.
61
+ - anything else, including a native state that maps to no role — not
62
+ claimable. Report and stop; moving it is the operator's call.
63
+ 2. Assign the ticket to yourself.
64
+ 3. Transition the ticket `available → in-progress`.
65
+ 4. Once the PR exists, comment its URL on the ticket unless it is already
66
+ there, and put the ticket's full URL (never a bare id) in the PR body.
67
+
68
+ ## Linear bindings
69
+
70
+ | Operation | Call |
71
+ | -------------- | ------------------------------------------------------------------------------------------------------------- |
72
+ | fetch brief | `get_issue(id, includeRelations=true)`; `list_comments(issueId)` when the acceptance criteria live in comments |
73
+ | resolve role | `get_issue(id).state` → `list_issue_statuses(team)` → the role map below |
74
+ | own identity | `get_user("me")` |
75
+ | assign self | `save_issue(id, assignee="me")` |
76
+ | transition | `save_issue(id, state=<substate mapping to the target role>)` |
77
+ | ticket comment | `save_comment(issueId, body)` |
78
+ | react | `unsupported` — use the text tokens |
79
+
80
+ Match `list_issue_statuses(team)` names case-insensitively:
81
+
82
+ | Native substate | Role |
83
+ | --------------- | ------------- |
84
+ | Todo | `available` |
85
+ | In Progress | `in-progress` |
86
+ | In Review | `in-review` |
87
+ | Delivered | `delivered` |
88
+ | Done | `verified` |
89
+ | Canceled | `canceled` |
90
+
91
+ `Delivered` is a custom substate. A substate this table doesn't name maps to no role. A team's custom `Blocked` sits in Linear's `Unstarted` group and is
92
+ not `available`.
93
+
94
+ Read the ticket's team before writing a state; substates are per-team.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: orchestrate
3
+ description: Drive one or more tracker projects to completion — build the dependency graph, then execute the CLI's work orders as they arrive, launching ticket-worker, pr-worker, and milestone-reviewer agents. Use when the unit of work is a whole project, not one ticket.
4
+ ---
5
+
6
+ # orchestrate
7
+
8
+ **The CLI decides; you execute.** Never work out which tickets are missing,
9
+ what to dispatch next, or whether anything is complete — every decision
10
+ arrives as an instruction, and you answer it. You never read ticket bodies or
11
+ judge CI state; workers do.
12
+
13
+ The `dispatch` commands below are also tools on the plugin's MCP server
14
+ (`mcp ack` → the `mcp_ack` tool). When the server is attached, call the tools:
15
+ the server pushes queued instructions after each tool call.
16
+
17
+ **In plan mode, decline** and ask the operator to re-invoke outside it — this
18
+ skill launches agents and writes state.
19
+
20
+ ## Start
21
+
22
+ 1. Resolve each project name the operator gave to its project id. Load
23
+ `tracker-adapter-${user_config.tracker}` and use its lookup; without an
24
+ adapter, drive the tracker's MCP server directly.
25
+ 2. Run `dispatch refresh --tracker <tracker> --project <ids>` — one
26
+ comma-separated value, not repeated flags.
27
+ 3. Stop and wait — work arrives as instructions, each handled per the table
28
+ below, until `project_complete` covers every project the operator named or
29
+ the operator says stop.
30
+
31
+ Add `--rebuild` only when the operator asks for a rebuild from scratch.
32
+
33
+ ## Answering instructions
34
+
35
+ | Instruction | Do this |
36
+ | -------------------------- | ------------------------------------------------------------------------------------------------ |
37
+ | `probe` | Run `dispatch mcp ack --server <id>` with the id the event carries. Work orders wait on it. |
38
+ | `scan_project` | Launch a background `build-graph` agent, passing the event's projects and cursor. |
39
+ | `fetch_ticket` | Launch a background `build-graph` agent, passing the event's ticket. |
40
+ | `refresh_ticket` | Launch a background `build-graph` agent, passing the event's ticket. |
41
+ | `refresh_complete` | Report the graph is built. Stay resident — dispatch begins. |
42
+ | `dispatch_ticket` | Launch a background `ticket-worker` agent, passing the event's ticket, project, and pass. Then record its address: `dispatch worker set --node <ticket> --agent <ref>` with the ref the launch returned. |
43
+ | `dispatch_pr` | Launch a background `pr-worker` agent, passing the event's PR item id, pass, and (when the item is ticket-backed) its ticket. Then record its address: `dispatch worker set --node <item-id> --agent <ref>`. |
44
+ | `perform_milestone_review` | Launch a background `milestone-reviewer` agent, passing the milestone and project. |
45
+ | `park_human_blocked` | Park the ticket yourself via the adapter (awaiting-external, else paused) and post the handoff. |
46
+ | `alert_failure` | Alert the operator where the order body says — the PR when one exists, else the ticket. |
47
+ | `project_complete` | Announce it. Stop once every project the operator named is complete. |
48
+
49
+ **Relay events.** Some events carry an `agent` meta key instead of an
50
+ instruction from the table: SendMessage the event verbatim to that ref and
51
+ stop. If the relay fails, run `dispatch worker rm --node <id>` and move on. A
52
+ non-instruction event with no `agent` key needs nothing from you.
53
+
54
+ Return to waiting after each launch. Give each worker only what the event
55
+ carries; never ticket content. Launch every order you receive; the CLI claims
56
+ and rate-limits before it emits.
57
+
58
+ Never ask the session for input (`AskUserQuestion` or any blocking prompt) —
59
+ a headless run has no operator, and an unanswered question stalls every
60
+ project you drive. Human input routes through the tracker (alerts on tickets,
61
+ questions on review artifacts); status reports to the session are fine. When
62
+ an order's premise looks wrong or the CLI misbehaves, alert the operator on
63
+ the tracker as for `alert_failure`, then keep executing orders as issued.
64
+
65
+ ## Injection
66
+
67
+ When the operator hands you new work mid-run — and only then. A new ticket:
68
+ run `dispatch refresh` again and let the scan fetch it, or write it directly:
69
+
70
+ ```shell
71
+ dispatch ticket set --id <ticket> --project <project> --status available --injected
72
+ dispatch pr set --id <owner/repo>#<n> --repo <owner/repo> --pr-number <n> --injected
73
+ ```
74
+
75
+ The second form is a ticketless PR or prompt item. Both rank to the head of
76
+ the queue; the next tick dispatches them.
77
+
78
+ ## If nothing arrives
79
+
80
+ Run `dispatch mcp status`. `active <id>` means the channel works — keep
81
+ waiting; silence means there is nothing to do yet, which is a normal state.
82
+
83
+ Anything else names why the channel does not work. **Report that verdict to
84
+ the operator and stop.**
85
+
86
+ A work order is the only thing that authorizes launching an agent.
87
+ `dispatch queue` and `dispatch status` are read-only diagnostics.
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: tracker-adapter-linear
3
+ description: Linear tracker adapter for dispatch — binds the workers' statuses and ticket operations to Linear and supplies build-graph's fetch calls, field mapping, and cursor. Use whenever the ticket or project lives on Linear.
4
+ ---
5
+
6
+ # tracker-adapter-linear
7
+
8
+ ## Identity
9
+
10
+ | Field | Value |
11
+ | ------------ | ------------------------------------- |
12
+ | tracker id | `linear` |
13
+ | ticket URLs | `linear.app/<workspace>/issue/<ID>` |
14
+ | ticket ids | `<TEAM-KEY>-<number>`, e.g. `DEV-123` |
15
+ | access | Linear MCP server |
16
+ | own identity | `get_user("me")` |
17
+
18
+ ## Role map
19
+
20
+ Read the team's substates with `list_issue_statuses(team)` and match by name,
21
+ case-insensitively; each substate carries the Linear group shown here.
22
+
23
+ | Native substate | Group | Role |
24
+ | --------------- | ----------- | ------------- |
25
+ | Triage | `backlog` | `backlog` |
26
+ | Backlog | `backlog` | `backlog` |
27
+ | Todo | `unstarted` | `available` |
28
+ | In Progress | `started` | `in-progress` |
29
+ | In Review | `started` | `in-review` |
30
+ | Finished | `started` | `finished` |
31
+ | Delivered | `started` | `delivered` |
32
+ | Done | `completed` | `verified` |
33
+ | Canceled | `canceled` | `canceled` |
34
+ | Duplicate | `canceled` | `canceled` |
35
+
36
+ `Finished` and `Delivered` are custom substates; a team without them collapses
37
+ the forward path (`in-review → delivered`, or `in-review → verified` where
38
+ neither exists). `paused` and `awaiting-external` are unmapped by default: a
39
+ team that needs them adds Backlog substates and maps them in its own copy of
40
+ this adapter. Until then a park has no substate to land on and is an `ERROR` —
41
+ moving a ticket to plain `Backlog` is not a park.
42
+
43
+ A substate this table doesn't name is handled per consumer:
44
+
45
+ - **a worker** (transitioning the acting ticket): an `ERROR`, not a guess —
46
+ its Linear group narrows the role but doesn't pick it (a team's custom
47
+ `Blocked` substate sits in `Unstarted` and is not `available`).
48
+ Map it in your own copy.
49
+ - **build-graph** (sweeping whole projects, foreign teams included): map it
50
+ only when its lifecycle meaning is unambiguous (a `Merged` substate is
51
+ `delivered`); otherwise escalate to the operator — a wrong role silently
52
+ dispatches, or strands, real work.
53
+
54
+ ## Operations
55
+
56
+ | Operation | Binding |
57
+ | ------------------ | -------------------------------------------------------------------------------------------------------------- |
58
+ | fetch brief | `get_issue(id, includeRelations=true)`; `list_comments(issueId)` when the acceptance criteria live in comments |
59
+ | resolve role | `get_issue(id).state` → `list_issue_statuses(team)` → the role map above |
60
+ | own identity | `get_user("me")` |
61
+ | claim guard | `get_issue(id).assignee` |
62
+ | assign self | `save_issue(id, assignee="me")` |
63
+ | transition | `save_issue(id, state=<substate mapping to the target role>)` |
64
+ | ticket comment | `save_comment(issueId, body)` |
65
+ | read comments | `list_comments(issueId)` — match the alert sentinel; replies carry `parentId` |
66
+ | react | `unsupported` — no reaction call in the Linear MCP server; use the text tokens |
67
+ | file ticket | `save_issue(title, team, description)` — same team as the ticket unless the brief says otherwise |
68
+ | subtask | `save_issue(title, team, parentId=<parent>)` |
69
+ | blocks edge | `save_issue(id=<blocker>, blocks=[<blocked>])` (append-only) |
70
+ | one-edge neighbors | `get_issue(id, includeRelations=true)` → `blockedBy` / `blocks` |
71
+
72
+ ## Quirks
73
+
74
+ - Linear tickets are per-team: read the acting ticket's team before writing a
75
+ state or filing into it, and don't reuse another team's substate names.
76
+ - Linear archives completed work; an archived task's `Done`/`Canceled` status
77
+ still counts toward its milestone, so `build-graph` must not `task rm` it.
78
+
79
+ ## Review artifact (milestone-review)
80
+
81
+ The review artifact is a **project status update**. Status updates are
82
+ project-scoped, not per-milestone, so the body must carry the milestone id
83
+ (the episode sentinel does) to keep concurrent milestones' reviews distinct.
84
+
85
+ | Operation | Binding |
86
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | milestone brief | `get_milestone(project, query)` — the goal lives in its description |
88
+ | find review artifact | `get_status_updates(type="project", project)` → the newest update whose body carries the episode sentinel with this milestone's id |
89
+ | post review artifact | `save_status_update(type="project", project, body, health)` — `onTrack` when the goal is achieved, `atRisk` otherwise |
90
+ | update review artifact | `save_status_update(id, body, health)` — same update; the pending→outcome edit when human input resolved |
91
+ | artifact thread | `list_comments(statusUpdateId)` / `save_comment(statusUpdateId)`; one thread per update — reply via `parentId`; tag with `@displayName` |
92
+ | file follow-up | `save_issue(title, team, description, project, milestone)` — `milestone` takes a name or id; pick the team per Quirks |
93
+
94
+ Member DoD comments and canceled-member rationales are ticket reads — use the
95
+ Operations bindings above (`fetch brief`, `read comments`).
96
+
97
+ ## Graph fetch (build-graph)
98
+
99
+ `build-graph` owns the loop and the CLI; this section supplies the Linear side.
100
+
101
+ Per selected project:
102
+
103
+ | Step | Call | Take |
104
+ | ------------ | ---------------------------------------------------- | --------------------------------------------------- |
105
+ | Project | `list_projects` (`query`) | `id`, `name` |
106
+ | Milestones | `list_milestones` (`project`) | `id`, `name`, `sortOrder` |
107
+ | Tasks | `list_issues` (`project`, `limit: 250`, `updatedAt`) | see the mapping below |
108
+ | Dependencies | `get_issue` (`id`, `includeRelations: true`) | `relations.blocks[].id`, `relations.blockedBy[].id` |
109
+
110
+ - `list_issues` does not return relations. `get_issue` every task in the delta,
111
+ in parallel batches.
112
+ - Page on `hasNextPage` / `cursor` (that `cursor` is pagination, not the sync
113
+ cursor).
114
+ - A `blockedBy` id outside the delta still gets its edge. Do not chase it —
115
+ the CLI records a placeholder and sends a `fetch_ticket` instruction if it
116
+ wants the ticket.
117
+
118
+ ### Map to CLI flags
119
+
120
+ | CLI | Linear |
121
+ | ----------------------------------------- | --------------------------------------------------- |
122
+ | `ticket set --id` | `id` (the identifier `CLC-945`, not the UUID) |
123
+ | `ticket set --project` | `projectId` |
124
+ | `ticket set --status` | `status`, mapped by the Role map above |
125
+ | `ticket set --priority` | `priority.value`; omit when `0` (`0` = no priority) |
126
+ | `ticket set --url / --title` | `url` / `title` |
127
+ | `ticket set --branch-hint` | `gitBranchName` |
128
+ | `ticket set --labels` | `labels`, comma-joined |
129
+ | `ticket set --updated-at` | `updatedAt` |
130
+ | `edge add --blocker <t> --blocked <m>` | membership: `projectMilestone.id` per ticket |
131
+ | `edge set --node --direction blockers` | `relations.blockedBy[].id`, comma-joined |
132
+
133
+ **Milestone order.** Sort milestones by `sortOrder` and chain adjacent pairs:
134
+ `edge add --blocker <prev> --blocked <next>`.
135
+
136
+ ### Cursor
137
+
138
+ The sync cursor is the latest `updatedAt` you fetched. A `scan_project`
139
+ instruction hands it to you as `list_issues`' `updatedAt` filter ("updated
140
+ after"); report it back with `dispatch refresh done --tracker linear
141
+ --cursor <ts>`. Both a status change and a relation change bump `updatedAt`,
142
+ so one delta sees both.
@@ -0,0 +1,12 @@
1
+ # Commands
2
+
3
+ One file per command, discovered automatically — no registry. The folder path is
4
+ the invocation path: `foo/bar.mts` → `dispatch foo bar`, and a `foo.mts` beside a
5
+ `foo/` directory makes that node both runnable and a namespace.
6
+
7
+ A command subclasses `AbstractCommand` (`../lib/command`); that contract — the
8
+ `Command` export, the `name` matching the file basename, `options`, `env`, `run` —
9
+ lives there and is enforced by `discover`. `greet.mts` is the worked example.
10
+
11
+ Skill-invoked commands must resolve inside the plugin directory, so keep
12
+ everything a command needs under `plugins/dispatch`.