@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,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`.
|