@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
File without changes
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Drive a tracker project to completion under CLI-issued work orders.
3
+ argument-hint: <project name or id>
4
+ ---
5
+
6
+ Use the `orchestrate` skill to drive: $ARGUMENTS
package/hooks/.gitkeep ADDED
File without changes
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse hook: deny launching a dispatch worker agent unless the session
3
+ // holds a live claim on a node the launch prompt names.
4
+ //
5
+ // The harness executes this before the tool call runs, so no instruction to
6
+ // the model — from a supervisor, an operator note, or a future skill edit —
7
+ // can skip it. That is the property the in-agent `claim check` cannot have:
8
+ // it only runs if the worker chooses to run it.
9
+ //
10
+ // All judgment lives in `dispatch claim guard`; this file only parses the
11
+ // hook payload, scopes the rule to the plugin's own worker agents, and
12
+ // relays the verdict.
13
+ import {spawnSync} from 'node:child_process';
14
+ import {fileURLToPath} from 'node:url';
15
+ import {dirname, join} from 'node:path';
16
+
17
+ /** The agent types the scheduler dispatches; nothing else is gated. */
18
+ const WORKERS = new Set([
19
+ 'dispatch:pr-worker',
20
+ 'dispatch:ticket-worker',
21
+ 'dispatch:milestone-reviewer',
22
+ // Bare names, as a harness may strip the plugin prefix.
23
+ 'pr-worker',
24
+ 'ticket-worker',
25
+ 'milestone-reviewer',
26
+ ]);
27
+
28
+ const deny = (reason: string): void => {
29
+ process.stdout.write(
30
+ JSON.stringify({
31
+ hookSpecificOutput: {
32
+ hookEventName: 'PreToolUse',
33
+ permissionDecision: 'deny',
34
+ permissionDecisionReason: reason,
35
+ },
36
+ })
37
+ );
38
+ process.exit(0);
39
+ };
40
+
41
+ interface HookPayload {
42
+ tool_input?: {
43
+ subagent_type?: string;
44
+ agent_type?: string;
45
+ prompt?: string;
46
+ };
47
+ }
48
+
49
+ let payload: unknown;
50
+ try {
51
+ const chunks: Buffer[] = [];
52
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
53
+ payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
54
+ } catch {
55
+ // An unreadable payload gives no way to tell a worker launch from anything
56
+ // else. Failing closed here would break every tool call in the session, so
57
+ // the gate only fails closed once it knows a worker is being launched.
58
+ process.exit(0);
59
+ }
60
+
61
+ // Valid JSON of the wrong shape (null, a number) is as unreadable as a
62
+ // parse failure and gets the same treatment.
63
+
64
+ const input =
65
+ payload !== null && typeof payload === 'object'
66
+ ? ((payload as HookPayload).tool_input ?? {})
67
+ : {};
68
+ const agentType = input.subagent_type ?? input.agent_type ?? '';
69
+ if (!WORKERS.has(agentType)) process.exit(0);
70
+
71
+ const prompt = typeof input.prompt === 'string' ? input.prompt : '';
72
+ // From here on the launch is a worker launch, and failure is denial: a guard
73
+ // that fails open for the case it exists for is not a guard.
74
+ const bin = join(
75
+ dirname(fileURLToPath(import.meta.url)),
76
+ '..',
77
+ 'bin',
78
+ 'dispatch'
79
+ );
80
+ const result = spawnSync(bin, ['claim', 'guard'], {
81
+ input: prompt,
82
+ encoding: 'utf8',
83
+ timeout: 10_000,
84
+ });
85
+ if (result.status === 0) process.exit(0);
86
+ deny(
87
+ `dispatch refused this ${agentType} launch: ${(
88
+ result.stderr ||
89
+ result.stdout ||
90
+ 'claim guard did not run'
91
+ )
92
+ .trim()
93
+ .split('\n')
94
+ .slice(0, 3)
95
+ .join(
96
+ ' '
97
+ )} Workers are launched from work orders, which name a node the scheduler already claimed.`
98
+ );
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Task|Agent",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/claim-guard.mts\""
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ianwremmel/dispatch",
3
+ "version": "0.32.1-bootstrap.0",
4
+ "description": "Dispatch engineering work end-to-end: pull request lifecycle (drafting, review, CI triage, merge) and ticket-tracker project orchestration (triage, planning, status, cross-team sync) — Linear bundled, other trackers added by writing an adapter skill.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude-code-plugin",
8
+ "github",
9
+ "pull-requests",
10
+ "ci",
11
+ "code-review",
12
+ "linear",
13
+ "project-management",
14
+ "issues",
15
+ "planning"
16
+ ],
17
+ "homepage": "https://github.com/ianwremmel/agentic/tree/main/plugins/dispatch#readme",
18
+ "bugs": "https://github.com/ianwremmel/agentic/issues",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ianwremmel/agentic.git",
22
+ "directory": "plugins/dispatch"
23
+ },
24
+ "license": "MIT",
25
+ "author": {
26
+ "name": "Ian Remmel"
27
+ },
28
+ "type": "module",
29
+ "files": [
30
+ ".claude-plugin/",
31
+ ".mcp.json",
32
+ "agents/",
33
+ "bin/",
34
+ "commands/",
35
+ "hooks/",
36
+ "skills/",
37
+ "src/",
38
+ "!**/*.test.mts"
39
+ ],
40
+ "engines": {
41
+ "node": ">=24.18.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
File without changes
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: land
3
+ description: Take a single pull request to completion — draft it, drive CI and reviews, iterate, and monitor until it merges or closes. Starts from a PR URL, a ticket URL, or a freeform prompt. Stateless, one PR per run. Use whenever the unit of work is "land this change" and nothing broader.
4
+ ---
5
+
6
+ # land
7
+
8
+ Each tick: run `pr-status <pr>`, address every actionable concern, then
9
+ evaluate the gates to decide whether to transition.
10
+
11
+ **Operator** = the one human directing this agent; the only human with stop
12
+ authority. Role glossary in [`reference.md`](./reference.md#roles).
13
+
14
+ ## Environment
15
+
16
+ Fixed by plugin config. State these in your first status output; never infer
17
+ any of them:
18
+
19
+ - Operator: `${user_config.operator_login}`
20
+ - Operator mode: `${user_config.operator_mode}`
21
+ - Credential mode: `${user_config.credential_mode}`
22
+ - Copilot review available: `${user_config.copilot_available}`
23
+ - Worktree base: `${user_config.worktree_base}`
24
+
25
+ Before any other work, read these two files — and only these variants, not the
26
+ other mode or credentials files:
27
+
28
+ - `mode-${user_config.operator_mode}.md` — the lifecycle, states, and review
29
+ gates for this operator mode.
30
+ - `credentials-${user_config.credential_mode}.md` — the wire format,
31
+ notification venue, and review rules for this credential mode.
32
+
33
+ If either file does not exist, the config value is invalid — stop and ask the
34
+ operator to set `operator_mode` (`solo` or `team`) and `credential_mode`
35
+ (`dedicated` or `shared`) in the dispatch plugin config.
36
+
37
+ ## Intake
38
+
39
+ One run drives exactly one PR. Resolve the input before Setup:
40
+
41
+ - **PR URL or number** — that PR. The brief is its body plus its plan comment;
42
+ a ticket link in the body makes the run ticket-backed. The snapshot carries
43
+ neither, so read them from the API once, here at intake.
44
+ - **Ticket URL or bare id** — the ticket is the brief and the run is
45
+ ticket-backed ([`ticket.md`](./ticket.md)).
46
+ - **Freeform prompt** — no ticket. The prompt is the brief.
47
+
48
+ Then, before Setup, find the PR: a PR input names it; for a ticket or a prompt,
49
+ search the ticket's linked PRs and the branch. **Never assume there is no PR** —
50
+ a killed session may already have opened one. If one exists, run `pr-status` on
51
+ it to establish where it stands, then take Setup's Resume path. If none exists,
52
+ go to Setup and open one.
53
+
54
+ Gates, lifecycle, and ending are the same for all three. A ticket-backed run
55
+ also keeps the ticket's role in sync and claims it before the first push; a run
56
+ with no ticket skips every ticket step.
57
+
58
+ Stop and ask the operator when the input names more than one PR, or when the
59
+ brief is too thin to tell when the change is done. Never invent scope.
60
+
61
+ ## Setup
62
+
63
+ 1. **Worktree.** Work in `${user_config.worktree_base}/<owner>/<repo>/<branch>`.
64
+ Locate via `git worktree list` — never guess. Reuse if present.
65
+ 2. **Open PR** (skip if one already exists for the branch):
66
+ - `git commit --allow-empty -m "chore: open PR [skip ci]"` — never amend or
67
+ squash this commit.
68
+ - Push; open a **draft** PR. Body: Motivation, and the ticket link when
69
+ there is one (full URL, never a bare id). **No execution plan in the
70
+ body.**
71
+ - Post the plan as a top-level comment in the wire-format body. Put
72
+ `<!-- agent-plan:<agent-id> -->` on its own line, after the machine marker
73
+ (which stays first — see
74
+ [`reference.md`](./reference.md#wire-format)). Pin if supported.
75
+ 3. **Resume.** PR exists → reuse worktree, skip the open sequence, find the plan
76
+ comment by its `agent-plan` marker (post one if missing). Never open a second
77
+ PR or rewrite the body.
78
+
79
+ ## Gates
80
+
81
+ Seven binary signals. Gates 1–5 and most gate 6/7 signals come from the
82
+ `pr-status` XML — an `approved` review, a `+1` reaction on the engagement
83
+ comment, `<terminal state>` leaving `draft`; your credentials and operator-mode
84
+ files name which ones count here. An approval given on the ticket or out of
85
+ band never reaches the snapshot — accept it when you see it:
86
+
87
+ 1. **CI** — `<checks state="passing">` (rollup treats neutral/success as
88
+ passing; repo can suppress non-blocking checks via `informational="true"`).
89
+ 2. **No conflicts** — `<merge-conflicts present="false"/>`.
90
+ 3. **No actionable annotations** — zero `<annotation actionable="true">`.
91
+ 4. **No actionable comments or review bodies** — zero `<comment
92
+ actionable="true">` and zero `<review actionable="true">`. A review's own
93
+ prose is a work item like any other; the inline threads it came with are
94
+ gate 5's business, not this one's.
95
+ 5. **No actionable threads** — zero `<thread actionable="true">`.
96
+ 6. **Operator-approved** (always required). Your credentials file lists the
97
+ signal forms that exist in this environment; your operator-mode file names
98
+ the stage that satisfies it.
99
+ 7. **Any second approval your operator-mode file requires.** That file says
100
+ whether one exists here and what satisfies it.
101
+
102
+ Gates 1–5 are evaluated every tick outside `starting`/`done`. **Gate failures
103
+ are fixed in place — they don't change state.** Only the conditions on a
104
+ transition edge change state.
105
+
106
+ ## Lifecycle
107
+
108
+ **Coding only happens in `draft`** — and in any other state only as the fix to
109
+ a gate-1–5 failure (CI broke, conflict, new actionable item). That fix is
110
+ "addressing concerns in place," not advancing the lifecycle.
111
+
112
+ ### Ending the run
113
+
114
+ Two things end a run: the PR closes, or the operator says stop. Nothing else —
115
+ not a finished plan, not green CI, not a review request, not "there's nobody
116
+ left to ask." Re-decide this from the current `pr-status` every tick; never
117
+ carry a "stop when X" rule forward from an earlier one.
118
+
119
+ On either, read `<terminal state>` and do exactly what its row says:
120
+
121
+ | `<terminal state>` | What happened | Do |
122
+ | ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
123
+ | `shipped` | The change is in the base branch. | React `rocket` and reply `Shipped.`; run the ticket's final transition ([`ticket.md`](./ticket.md)); delete the worktree you created. |
124
+ | `abandoned` | PR closed, change not in base. | React `-1` and reply `Declined.`; report the closure on the ticket but don't advance its role; quote any `error=` attribute verbatim; delete the worktree you created. |
125
+ | `open` / `draft` | PR still live — the operator stopped you. | Post what you finished and what is left; leave the PR open, the ticket untouched, and the worktree in place for a resumed run. Never close the PR. |
126
+
127
+ `shipped` covers a squash or rebase landing, not just a merge commit. Never
128
+ call a change delivered on any signal other than this attribute.
129
+
130
+ ## Reading PR state
131
+
132
+ **Every gate and actionability decision comes from `pr-status` XML and the cache
133
+ files it writes** — never from `gh pr view`, `gh pr checks`, `gh api
134
+ …/comments|/reviews`, or an MCP PR read. Read full text from the cache. Direct
135
+ reads are allowed only for a field the snapshot omits: a thread's file and line,
136
+ the PR body and plan comment at intake, and an approval given on the ticket or
137
+ out of band. `gh` and MCP are otherwise for **writes** — reply, react,
138
+ request review, mark ready.
139
+
140
+ A review still being drafted is invisible here by design. Don't chase it; wait
141
+ for `pr-status` to surface it.
142
+
143
+ ## Per-concern handling
144
+
145
+ Address **every** actionable item, not just the first. `actionable="true"` is
146
+ your only task source: work the item, then reply and apply a terminal signal so
147
+ it settles. An item marked `actionable="false"` carries a `reason=` (`resolved`,
148
+ `agent-artifact`, `agent-terminal-reply`, `acked`, `no-body`, `dismissed`) —
149
+ leave it alone. `<summary>`
150
+ recaps what an item *says*, never whether it's resolved, so an item you already
151
+ settled still reads as open. That is expected, and never grounds to reopen it.
152
+
153
+ | XML signal | Action |
154
+ | ----------------------------------------------------- | ------------------------------------------------------------------------- |
155
+ | `<merge-conflicts present="true"/>` (gate 2) | Rebase or merge the target branch; resolve. |
156
+ | `<checks state="failing">` (gate 1) | Diagnose root cause; fix. |
157
+ | Actionable `<comment>` or `<thread>` (gates 4–5) | Reply (commit link **or** one-line dismissal naming what's dismissed) and apply a terminal signal. Dismissing a bot's point needs only that line; dismissing a human's needs you to say why their concern doesn't apply — if you can't say it, do what they asked. **Never resolve the thread** — even your own; that's a human's call, and the terminal signal already suppresses re-evaluation. |
158
+ | Actionable `<annotation>` (gate 3) | Fix the code, OR dismiss it: write the rationale to the path in `cache=` with `.md` swapped for `.ack` (a sibling file, not a child), and record it in the plan comment or commit body. |
159
+ | Actionable `<review>` (gate 4) | Read the body from `cache=` — it is prose the reviewer wrote outside any thread. Act on it and reply in a top-level comment saying what you did; a body that asks for nothing (a bot's overview) needs no reply. Either way, settle it by writing the `cache=` path with `.md` swapped for `.ack` (a sibling file), rationale inside — the review object itself takes no reply or reaction, so the `.ack` is what clears the gate. |
160
+
161
+ ## Cross-cutting behaviors
162
+
163
+ Apply in every state.
164
+
165
+ - **Pre-push review.** Before every significant push, run two adversarial
166
+ passes:
167
+ 1. *Spec-aware* — spec/docs + PR contents: find every drift from the spec
168
+ (missing, extra, or conflicting behavior).
169
+ 2. *Spec-blind* — PR contents only: find every bug, inconsistency, or
170
+ claim-vs-implementation gap (judged against the PR's own commit
171
+ messages/identifiers/comments).
172
+
173
+ Run both passes on a model family distinct from the authoring one (e.g. Codex
174
+ `codex:adversarial-review`/`codex:rescue` when Claude authored). A subagent on
175
+ the authoring model counts only when the install has no distinct family —
176
+ weaker, so take extra caution. Triage every finding (act, or one-line
177
+ dismissal naming it). Skip pre-push review only for non-significant pushes
178
+ (the empty open commit, whitespace/format-only, trivial typo/lint); if unsure,
179
+ treat as significant.
180
+ - **Plan comment is the living plan.** Edit in place: check off done steps,
181
+ strike abandoned ones with a one-line rationale (don't delete), append new
182
+ ones. The PR body stays stable.
183
+ - **First green.** Gate 1 needs a green rollup on the **current head commit** —
184
+ the commit you intend to leave `draft` with. A green from an earlier push
185
+ doesn't count.
186
+
187
+ ## Polling
188
+
189
+ Adaptive, not fixed. **Never poll faster than once per minute.** Emit an INFO
190
+ heartbeat each time round ([`reference.md`](./reference.md#operational-logging);
191
+ `ticket=-` when there's no ticket).
192
+
193
+ A `pending` review is in-flight, not absent. Each reviewer appears once under
194
+ `<reviews>`, walking `pending → commented | changes_requested | approved |
195
+ dismissed`; a fresh request overrides any earlier verdict back to `pending`.
196
+ While anyone is `pending` — a `mode="bot"` reviewer especially — inline threads
197
+ can still land minutes later, so an unchanging thread set is **not**
198
+ convergence. Keep polling until `pending` clears.
199
+
200
+ | Waiting on | Schedule |
201
+ | -------------------------------------- | ---------------------------------------------------------------------------------------- |
202
+ | CI (`<checks state="pending">`) | 60 s; lengthen to ~5 min once past the project's typical CI duration. |
203
+ | Reviewer reply after a request | 5 min for the first hour; then 30 min. |
204
+ | Merge after `ready_for_merge` | 5 min for the first hour; then 30 min. |
205
+
206
+ ### Mechanism
207
+
208
+ The agent **is** the poll loop — inline, sequential foreground tool calls
209
+ (`Bash` `sleep`, then a `pr-status` re-read and any reactive work). Stay
210
+ continuously active in the current turn until the run ends; never yield
211
+ the turn, hand off to a wakeup, or expect re-prodding. Holds whether `land`
212
+ is invoked directly or dispatched as a subagent.
213
+
214
+ For waits past the Bash timeout (~10 min), split into shorter intervals (a
215
+ 30-min wait ≈ 5×6-min `sleep`s, each followed by a cheap `pr-status` check).
216
+ The table is an upper bound; re-checking sooner is fine, never under a minute.
217
+
218
+ **Forbidden** (each has stranded a PR):
219
+
220
+ - **Detached background poll loops** — any `run_in_background` Bash repeating
221
+ `touch <lock>; sleep; poll` (`while`, `until`, `nohup`, `disown`, …). The OS
222
+ process polls forever while the agent is reaped; the PR sits orphaned.
223
+ - **`Monitor` as the poll vehicle** — the armed-monitor wake observably fails on
224
+ long polls. Use foreground `sleep`.
225
+ - **Ending the turn while the run is still going** — no reasoning licenses it:
226
+ not "no work right now," not who has seen what, not where a notification was
227
+ sent or whether anyone is around to read it. The poll loop is the only thing
228
+ that observes a reply, so returning early orphans the PR. Don't design a
229
+ caller around mid-lifecycle re-dispatch.
230
+
231
+ Tune the schedule within the run from what you observe: once you've watched this
232
+ repo's CI finish twice, poll on that duration rather than the table's head.
233
+
234
+ ## References
235
+
236
+ Machine marker, terminal signals, engagement mechanics, actionability,
237
+ log-line format: [`reference.md`](./reference.md). Ticket
238
+ resolution, claiming, role sync: [`ticket.md`](./ticket.md).
@@ -0,0 +1,33 @@
1
+ # land — dedicated credentials
2
+
3
+ The agent has its own account, so its posts are already distinguishable by
4
+ author. The wire format is the one in
5
+ [`reference.md`](./reference.md#wire-format), unchanged.
6
+
7
+ ## Operator notification
8
+
9
+ When an engagement edge fires (your operator-mode file says which), request
10
+ review from `operator_login` and post the engagement comment — marker +
11
+ `agent-engagement` sentinel
12
+ ([mechanics](./reference.md#operator-engagement)).
13
+
14
+ ## Gate 6 signals
15
+
16
+ All four forms exist here; any one satisfies the gate:
17
+
18
+ - `<review mode="human" role="operator" state="approved">` — the formal
19
+ approval, and the one to expect;
20
+ - `<reaction emoji="+1">` from the operator on the engagement comment;
21
+ - a "go ahead" / "lgtm" / "ready" reply from the operator, on the engagement
22
+ comment, the ticket, or out of band;
23
+ - a ticket-side approval, such as an operator status transition.
24
+
25
+ ## Review rules
26
+
27
+ GitHub may refuse a Copilot review request from a bot account. Local
28
+ instructions may provide a way around it — commonly a second GitHub token to
29
+ request with. Use it if one is available.
30
+
31
+ Otherwise, or if the request is refused anyway, log `ERROR`, post a PR comment
32
+ saying it was refused, and take your operator-mode file's *Copilot unavailable*
33
+ branch. A refused Copilot request is not a `BLOCK`.
@@ -0,0 +1,76 @@
1
+ # land — shared credentials
2
+
3
+ The agent posts with the operator's own account, so `operator_login` is also the
4
+ authenticated account. Consequences: author doesn't distinguish
5
+ agent posts from operator posts; GitHub refuses a review request aimed at the
6
+ operator, and refuses operator `approved` / `changes_requested` reviews on this
7
+ PR — `commented` only.
8
+
9
+ ## Wire format
10
+
11
+ The body inside the marker ([wire format](./reference.md#wire-format)) is
12
+ wrapped in a sparkle block:
13
+
14
+ ```text
15
+ <!-- agent-reply:<agent-id> -->
16
+
17
+
18
+ Fixed in abc1234.
19
+
20
+
21
+
22
+ Done.
23
+ ```
24
+
25
+ The sparkle (U+2728) sits alone on its line, one blank line in from the body on
26
+ each side. **A terminal token goes after the closing sparkle**, as shown:
27
+ `pr-status` reads the last non-empty line and nothing else, so a token inside
28
+ the block is never seen — the closing sparkle is that line — and the item stays
29
+ actionable forever. Reactions are unaffected; a sentinel is matched anywhere in
30
+ the comment as long as it is alone on its line, so it may stay inside the
31
+ block.
32
+
33
+ ## Operator notification
34
+
35
+ When an engagement edge fires (your operator-mode file says which), post the
36
+ engagement comment — marker + `agent-engagement` sentinel
37
+ ([mechanics](./reference.md#operator-engagement)) — then notify the operator
38
+ separately, since a review request can't reach them:
39
+
40
+ 1. a ticket comment tagging the operator, when the work has a ticket;
41
+ 2. otherwise, in the session: say you are waiting on the operator's approval and
42
+ what for.
43
+
44
+ Notifying is not waiting. Whichever venue you used, keep polling in the same
45
+ turn, watching that venue as well as `pr-status` — nothing else observes the
46
+ reply.
47
+
48
+ The engagement comment never notifies anyone — it posts under the operator's
49
+ own account — but it still anchors the reaction- and reply-based Gate 6
50
+ signals.
51
+
52
+ ## Gate 6 signals
53
+
54
+ `pr-status` will never report `<review role="operator" state="approved">` here —
55
+ the platform refuses self-review, so that element cannot exist on this PR. Don't
56
+ wait for it. Any one of these satisfies the gate instead:
57
+
58
+ - `<reaction emoji="+1">` on the engagement comment. **Never react to your own
59
+ engagement comment in this mode** — you and the operator post under one login,
60
+ so a `+1` you added there is indistinguishable from their approval and you
61
+ would clear your own gate. The sentinel already keeps that comment
62
+ non-actionable, so it never needs a terminal signal from you;
63
+ - a "go ahead" / "lgtm" / "ready" reply from the operator, on the engagement
64
+ comment, the ticket, or out of band;
65
+ - a ticket-side approval, such as an operator status transition.
66
+
67
+ ## Reading reviews — the inverse rule
68
+
69
+ The absence of a formal `changes_requested` never means "no changes
70
+ requested." Every operator comment — review comment, inline comment, or
71
+ top-level PR comment — is either a question to answer or an implicit change
72
+ request.
73
+
74
+ ## Review rules
75
+
76
+ Requests to reviewers other than the authenticated account work normally.
@@ -0,0 +1,76 @@
1
+ # land — solo operator mode
2
+
3
+ The operator is the only human in the loop: after Copilot review (where
4
+ available), the agent clears draft and engages the operator as the public
5
+ reviewer.
6
+
7
+ ## Gates 6–7 in solo
8
+
9
+ - **Gate 6 (operator-approved)** is satisfied during `public_review_*`.
10
+ - **Gate 7** — there is no second approver in this mode. Never evaluated.
11
+
12
+ ## Draft clearing
13
+
14
+ The agent clears draft on the edge into `ready_for_public_review`. If the
15
+ operator clears draft first, just proceed.
16
+
17
+ ## Lifecycle
18
+
19
+ ```mermaid
20
+ stateDiagram-v2
21
+ [*] --> starting
22
+
23
+ starting --> draft: worktree + empty commit + draft PR + plan comment
24
+
25
+ draft --> ready_for_copilot_review: ready · gates 1-5 · Copilot available
26
+ draft --> ready_for_public_review: ready · gates 1-5 · Copilot unavailable (clear draft)
27
+
28
+ ready_for_copilot_review --> copilot_review_requested: review requested
29
+
30
+ copilot_review_requested --> copilot_commented: Copilot left actionable items
31
+ copilot_review_requested --> ready_for_public_review: Copilot reviewed · zero actionable (clear draft)
32
+
33
+ copilot_commented --> ready_for_copilot_review: addressed · gates 1-5 · re-request
34
+
35
+ ready_for_public_review --> public_review_requested: operator engaged (engagement comment + notification)
36
+
37
+ public_review_requested --> public_review_commented: operator commented (no formal verdict)
38
+ public_review_requested --> public_review_requested_changes: operator changes_requested
39
+ public_review_requested --> public_review_approved: gate 6 satisfied
40
+
41
+ public_review_commented --> ready_for_public_review: addressed · gates 1-5 · re-engage
42
+ public_review_requested_changes --> ready_for_public_review: addressed · gates 1-5 · re-engage (required to unblock merge)
43
+
44
+ public_review_approved --> ready_for_merge: gates 1-5 still hold
45
+
46
+ ready_for_merge --> merged: PR closed (terminal resolved by pr-status)
47
+
48
+ merged --> done: worktree removed
49
+
50
+ done --> [*]
51
+ ```
52
+
53
+ ## States
54
+
55
+ | State | Do | Poll? |
56
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------- |
57
+ | `starting` | Create or locate the worktree (see Setup in `SKILL.md`). | no |
58
+ | `draft` | **Coding happens here.** Edit; pre-push review; push. When ready, check gates 1–5. | no |
59
+ | `ready_for_copilot_review` | Request Copilot review. | no |
60
+ | `copilot_review_requested` | Await Copilot's review. | CI |
61
+ | `copilot_commented` | Address each actionable Copilot item; push fix(es). | no |
62
+ | `ready_for_public_review` | Clear draft (if still draft). Post the engagement comment (agent-reply marker + `<!-- agent-engagement:<agent-id> -->` sentinel) and notify the operator via your credentials file's venue. Never self-request. | no |
63
+ | `public_review_requested` | Await the operator's signal. | reviewer |
64
+ | `public_review_commented` | Address each item; push; re-engage. | no |
65
+ | `public_review_requested_changes` | Address; push; **re-engage required** — blocks merge. | no |
66
+ | `public_review_approved` | Confirm gates 1–5 still hold; else fix in place. | no |
67
+ | `ready_for_merge` | Await merge. **Don't self-merge unless instructed.** | merge |
68
+ | `merged` | Handle per **Ending the run** in `SKILL.md`. | no |
69
+ | `done` | Terminal. | — |
70
+
71
+ ## No formal review
72
+
73
+ A formal review may never arrive — whether one is even possible is your
74
+ credentials file's concern. The engagement comment anchors the reaction- and
75
+ reply-based Gate 6 signals; keep polling on the reviewer cadence. "Nobody to
76
+ ask" never terminates.