1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! Per-issue dispatch for `tm watch` — build the task, then dry-run or spawn.
//!
//! Why: once an issue is matched by the routing label, watch must turn it into
//! the SAME managed-session execution `tm ticket` performs: a Claude Code session
//! spawned via `POST /api/v1/sessions/managed` whose task instructs the driver
//! agent to implement the issue and open a PR. Factoring the per-issue step out
//! of the loops keeps the safety gate (dry-run vs execute) and the task wording
//! in one tested place that both `poll` and `listen` share.
//! What: [`build_watch_task`] builds the agent prompt for a matched issue (pure,
//! tested); [`DispatchMode`] is the dry-run/execute selector; [`dispatch_issue`]
//! either prints the would-execute plan (dry-run) or POSTs the managed-spawn
//! request (execute), reusing the exact request shape `tm ticket` uses. The
//! branch ref is the repo's resolved default branch; the agent creates the ticket
//! branch from there.
//! Test: `build_watch_task_*` in the sibling `tests.rs`; the HTTP spawn mirrors
//! `tm ticket::spawn_managed` (covered by the managed MVP integration test) and
//! is exercised end-to-end manually.
use Deserialize;
use RuntimeKind;
use MatchedIssue;
/// Whether `dispatch_issue` should actually spawn work or only describe it.
///
/// Why: `tm watch` drives real execution against real repos, so the default must
/// be safe — describe, do not execute — unless the operator explicitly opts in.
/// A two-variant selector makes the gate explicit and impossible to fat-finger
/// into mass execution.
/// What: `DryRun` (list what WOULD run, spawn nothing) or `Execute` (POST the
/// managed-spawn request).
/// Test: `dispatch_dry_run_spawns_nothing` (dry-run path), and the execute path
/// via the manual e2e + the shared `spawn_managed` shape.
pub
/// Build the agent task prompt for a watch-matched issue.
///
/// Why: the spawned managed session needs a self-contained instruction naming
/// the issue, the branch to create, the base branch to create it from, and the
/// close-on-merge convention — identical in spirit to `tm ticket`'s task so a
/// watched issue is resolved the same way a hand-run ticket is. Building it as a
/// pure function keeps the wording asserted in a test.
/// What: returns a multi-line task string embedding the issue number/title/body,
/// the derived `branch`, the `base_branch` to branch from, and the close-on-merge
/// (`Closes #<n>`) and pull-request requirements.
/// Test: `build_watch_task_includes_branch_and_close` and
/// `build_watch_task_handles_empty_body`.
pub
/// Derive the per-issue working branch name for a watched issue.
///
/// Why: every matched issue needs a stable, unique branch so concurrent watch
/// dispatches do not collide; deriving it from the issue number keeps it
/// deterministic and re-runnable.
/// What: returns `tm-watch/<number>` — a namespaced branch that groups all
/// watch-driven work and never clashes across issues.
/// Test: `watch_branch_name_is_namespaced`.
pub
/// Dispatch one matched issue: dry-run describes it, execute spawns it.
///
/// Why: the single per-issue action both loops call; centralising the safety
/// gate and the managed-spawn shape means dry-run can never accidentally spawn
/// and the execute path can never diverge from `tm ticket`.
/// What: in [`DispatchMode::DryRun`] prints the issue, derived branch, and target
/// repo/ref WITHOUT any HTTP call; in [`DispatchMode::Execute`] POSTs
/// `{repo_url, ref, task, name_hint, runtime}` to `/api/v1/sessions/managed` and
/// prints the new session id/state/attach command. Returns `Ok(true)` when the
/// issue was actually spawned (execute path succeeded) and `Ok(false)` for a
/// dry-run, so the caller's dedup set only records genuinely-dispatched issues.
/// Test: dry-run via `dispatch_dry_run_spawns_nothing`; execute via the manual
/// e2e (mirrors `tm ticket::spawn_managed`).
pub async
/// POST the managed-session spawn request that drives the issue implementation.
///
/// Why: reuses the exact session-manager spawn path (`POST
/// /api/v1/sessions/managed`) that `tm ticket` / `tm session new` use, so
/// `tm watch` plugs into the existing isolated-workspace + runtime-adapter
/// machinery (the P4 workspace root `~/trusty-mpm-projects/<owner>/<repo>/…`)
/// rather than re-implementing it. The provisioner clones the repo and the driver
/// agent creates `branch` off `base_ref` per the task.
/// What: posts repo_url/ref/task/name_hint/runtime and prints the new session id,
/// state, runtime, and attach command.
/// Test: mirrors `tm ticket::spawn_managed`; HTTP path covered by the managed MVP
/// integration test.
async