@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
|
File without changes
|
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
|
+
);
|
package/hooks/hooks.json
ADDED
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
|
+
}
|
package/skills/.gitkeep
ADDED
|
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.
|