patchrelay 0.26.0 → 0.29.1

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 (57) hide show
  1. package/README.md +83 -31
  2. package/dist/agent-session-plan.js +0 -7
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli/args.js +22 -18
  5. package/dist/cli/commands/feed.js +1 -1
  6. package/dist/cli/commands/issues.js +44 -4
  7. package/dist/cli/commands/linear.js +67 -0
  8. package/dist/cli/commands/repo.js +213 -0
  9. package/dist/cli/commands/setup.js +140 -21
  10. package/dist/cli/connect-flow.js +5 -3
  11. package/dist/cli/formatters/text.js +1 -1
  12. package/dist/cli/help.js +134 -63
  13. package/dist/cli/index.js +166 -188
  14. package/dist/cli/interactive.js +25 -0
  15. package/dist/cli/operator-client.js +11 -0
  16. package/dist/cli/service-commands.js +11 -4
  17. package/dist/cli/watch/App.js +1 -1
  18. package/dist/cli/watch/FactoryStateGraph.js +31 -0
  19. package/dist/cli/watch/FeedView.js +3 -2
  20. package/dist/cli/watch/FreshnessBadge.js +13 -0
  21. package/dist/cli/watch/IssueDetailView.js +9 -2
  22. package/dist/cli/watch/IssueListView.js +2 -2
  23. package/dist/cli/watch/IssueRow.js +9 -11
  24. package/dist/cli/watch/QueueObservationView.js +15 -0
  25. package/dist/cli/watch/StateHistoryView.js +0 -1
  26. package/dist/cli/watch/StatusBar.js +5 -2
  27. package/dist/cli/watch/format-utils.js +7 -0
  28. package/dist/cli/watch/freshness.js +30 -0
  29. package/dist/cli/watch/state-visualization.js +147 -0
  30. package/dist/cli/watch/theme.js +6 -7
  31. package/dist/cli/watch/use-watch-stream.js +5 -2
  32. package/dist/cli/watch/watch-state.js +9 -5
  33. package/dist/config.js +129 -36
  34. package/dist/db/linear-installation-store.js +23 -0
  35. package/dist/db/migrations.js +42 -0
  36. package/dist/db/repository-link-store.js +103 -0
  37. package/dist/db.js +61 -11
  38. package/dist/factory-state.js +1 -5
  39. package/dist/github-webhook-handler.js +115 -46
  40. package/dist/github-webhooks.js +4 -0
  41. package/dist/http.js +162 -0
  42. package/dist/install.js +93 -13
  43. package/dist/issue-query-service.js +34 -1
  44. package/dist/linear-client.js +80 -25
  45. package/dist/merge-queue-incident.js +104 -0
  46. package/dist/merge-queue-protocol.js +54 -0
  47. package/dist/preflight.js +28 -1
  48. package/dist/repository-linking.js +42 -0
  49. package/dist/run-orchestrator.js +197 -21
  50. package/dist/runtime-paths.js +0 -8
  51. package/dist/service.js +94 -49
  52. package/package.json +8 -7
  53. package/dist/cli/commands/connect.js +0 -54
  54. package/dist/cli/commands/project.js +0 -146
  55. package/dist/merge-queue.js +0 -200
  56. package/infra/patchrelay-reload.service +0 -6
  57. package/infra/patchrelay.path +0 -13
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  PatchRelay is a self-hosted harness for running a controlled coding loop per Linear issue on your own machine.
4
4
 
5
- It receives Linear webhooks, routes issues to the right local repository, prepares durable issue worktrees, runs Codex sessions through `codex app-server`, and keeps the whole issue loop observable and resumable from the CLI. GitHub webhooks drive reactive loops for CI repair, review fixes, and merge queue failures.
5
+ It receives Linear webhooks, routes issues to the right local repository, prepares durable issue worktrees, runs Codex sessions through `codex app-server`, and keeps the whole issue loop observable and resumable from the CLI. GitHub webhooks drive reactive loops for CI repair and review fixes. A separate [Merge Steward](./packages/merge-steward) service handles serial queue integration and landing.
6
6
 
7
7
  PatchRelay is the system around the model:
8
8
 
@@ -11,7 +11,8 @@ PatchRelay is the system around the model:
11
11
  - issue-to-repo routing
12
12
  - issue worktree and branch lifecycle
13
13
  - context packaging, run orchestration, and thread continuity
14
- - reactive CI repair, review fix, and merge queue repair loops
14
+ - reactive CI repair and review fix loops
15
+ - queue repair runs triggered by Merge Steward evictions
15
16
  - native Linear agent input forwarding into active runs
16
17
  - read-only inspection and run reporting
17
18
 
@@ -36,7 +37,7 @@ PatchRelay does the deterministic harness work that you do not want to re-implem
36
37
  - packages the right issue, repo, review, and failure context for each loop
37
38
  - creates and reuses one durable worktree and branch per issue lifecycle
38
39
  - starts Codex threads for implementation runs
39
- - triggers reactive runs for CI failures, review feedback, and merge queue failures
40
+ - triggers reactive runs for CI failures, review feedback, and Merge Steward evictions
40
41
  - persists enough state to correlate the Linear issue, local workspace, run, and Codex thread
41
42
  - reports progress back to Linear and forwards follow-up agent input into active runs
42
43
  - exposes CLI and optional read-only inspection surfaces so operators can understand what happened
@@ -79,8 +80,9 @@ You will also need:
79
80
  2. PatchRelay verifies the webhook, routes the issue to the right local project, and packages the issue context for the first loop.
80
81
  3. Delegated issues create or reuse the issue worktree and launch an implementation run through `codex app-server`.
81
82
  4. PatchRelay persists thread ids, run state, and observations so the work stays inspectable and resumable.
82
- 5. GitHub webhooks drive reactive verification and repair loops: CI repair on check failures, review fix on changes requested, and merge queue repair on queue failures.
83
- 6. Native agent prompts and Linear comments can steer the active run. An operator can take over from the exact same worktree when needed.
83
+ 5. GitHub webhooks drive reactive verification and repair loops: CI repair on check failures and review fix on changes requested.
84
+ 6. When the PR is approved and CI is green, PatchRelay adds the `queue` label. Merge Steward takes over rebasing, validating, and merging the PR. If the steward evicts the PR, PatchRelay triggers a queue repair run.
85
+ 7. Native agent prompts and Linear comments can steer the active run. An operator can take over from the exact same worktree when needed.
84
86
 
85
87
  ## Factory State Machine
86
88
 
@@ -127,6 +129,22 @@ PatchRelay uses repo-local workflow files as prompts for Codex runs:
127
129
 
128
130
  These files define how the agent should work in that repository. Keep them short, action-oriented, and human-authored.
129
131
 
132
+ ## Knowledge And Validation Surfaces
133
+
134
+ PatchRelay works best when repository guidance follows progressive disclosure:
135
+
136
+ - keep the root entrypoints short and navigational
137
+ - treat deeper `docs/` content as the durable system of record
138
+ - capture architecture, workflow, and product decisions in versioned files instead of chat history or operator memory
139
+
140
+ PatchRelay should also help agents validate their own work inside the issue loop:
141
+
142
+ - package the smallest useful context for the current run instead of replaying ever-growing transcript history
143
+ - preserve high-signal failure evidence such as review feedback, failing checks, and queue incidents
144
+ - make repo-local validation surfaces legible per worktree so the next run can see what passed, what failed, and what needs repair
145
+
146
+ Keeping those knowledge and validation surfaces clean is part of the harness, not optional documentation polish.
147
+
130
148
  ## Access Control
131
149
 
132
150
  PatchRelay reacts only for issues that route to a configured project.
@@ -159,8 +177,6 @@ It creates the local config, env file, and system service units:
159
177
  - `~/.config/patchrelay/service.env`
160
178
  - `~/.config/patchrelay/patchrelay.json`
161
179
  - `/etc/systemd/system/patchrelay.service`
162
- - `/etc/systemd/system/patchrelay-reload.service`
163
- - `/etc/systemd/system/patchrelay.path`
164
180
 
165
181
  The generated `patchrelay.json` is intentionally minimal, and `patchrelay init` prints the webhook URL, OAuth callback URL, and the Linear app values you need next.
166
182
 
@@ -177,23 +193,37 @@ LINEAR_OAUTH_CLIENT_SECRET=replace-with-linear-oauth-client-secret
177
193
 
178
194
  Keep service secrets in `service.env`. `runtime.env` is for non-secret overrides such as `PATCHRELAY_DB_PATH` or `PATCHRELAY_LOG_FILE`. Everyday local inspection commands do not require exporting these values in your shell.
179
195
 
180
- ### 4. Configure a project
196
+ ### 4. Connect PatchRelay to Linear
197
+
198
+ Connect PatchRelay to one Linear workspace:
199
+
200
+ ```bash
201
+ patchrelay linear connect
202
+ patchrelay linear sync
203
+ ```
204
+
205
+ This authorizes the workspace once, then caches its teams and projects locally. Workspace auth is separate from repo linking.
206
+
207
+ ### 5. Link a GitHub repo
181
208
 
182
- Add repositories after `patchrelay init` with `patchrelay project apply <id> <repo-path>`.
209
+ Link repos by GitHub identity, not by local path:
183
210
 
184
- For a single project, that is usually enough. For multiple projects, add routing with `--issue-prefix APP` or `--team-id <linear-team-id>`.
211
+ ```bash
212
+ patchrelay repo link krasnoperov/usertold --workspace usertold --team USE
213
+ ```
214
+
215
+ PatchRelay treats the GitHub repo as the source of truth. It reuses an existing local clone under the managed repo root when `origin` already matches, or clones it automatically when missing. Use `--path <path>` only when you want a non-default local location.
185
216
 
186
- The generated `~/.config/patchrelay/patchrelay.json` is machine-level service config only. Project entries should be created with the CLI, not by hand-editing a placeholder template.
217
+ The generated `~/.config/patchrelay/patchrelay.json` stays machine-level service config. Repo links should be created with the CLI, not by hand-editing the file.
187
218
 
188
- `patchrelay project apply` is idempotent:
219
+ `patchrelay repo link` is idempotent:
189
220
 
190
- - it creates or updates the local project entry
191
- - it checks whether PatchRelay is ready
221
+ - it creates or updates the linked repo entry
222
+ - it refreshes the selected Linear workspace catalog before resolving teams/projects
192
223
  - it reloads the service when it can
193
- - it reuses or starts the Linear connect flow when the local setup is ready
194
224
  - if workflow files or secrets are still missing, it tells you exactly what to fix and can be rerun safely
195
225
 
196
- ### 5. Add workflow docs to the repo
226
+ ### 6. Add workflow docs to the repo
197
227
 
198
228
  PatchRelay looks for:
199
229
 
@@ -204,21 +234,21 @@ REVIEW_WORKFLOW.md
204
234
 
205
235
  These files define how the agent should work in that repo.
206
236
 
207
- ### 6. Validate
237
+ ### 7. Validate
208
238
 
209
239
  ```bash
210
240
  patchrelay doctor
241
+ patchrelay service status
211
242
  ```
212
243
 
213
- ### 7. Check the installation
244
+ ### 8. Check linked workspaces and repos
214
245
 
215
246
  ```bash
216
- patchrelay installations
247
+ patchrelay linear list
248
+ patchrelay repo list
217
249
  ```
218
250
 
219
- In the normal happy path, the earlier `patchrelay project apply <id> <repo-path>` command already handles the connect step for you. `patchrelay connect --project <id>` still exists as the advanced/manual command when you want to retry or debug only the Linear authorization layer.
220
-
221
- If you later add another local repo that should use the same Linear installation, run `patchrelay project apply <id> <repo-path>` for that repo too. PatchRelay now reuses the single saved installation automatically when there is no ambiguity, so you usually will not need another browser approval.
251
+ If you later add another local repo from the same workspace, run `patchrelay repo link <owner/repo> --workspace <workspace> --team <team>` again. PatchRelay reuses the existing workspace installation instead of opening a new OAuth flow.
222
252
 
223
253
  Important:
224
254
 
@@ -236,16 +266,18 @@ Important:
236
266
 
237
267
  Useful commands:
238
268
 
239
- - `patchrelay list --active`
240
- - `patchrelay inspect APP-123`
241
- - `patchrelay live APP-123 --watch`
242
- - `patchrelay report APP-123`
243
- - `patchrelay events APP-123 --follow`
244
- - `patchrelay worktree APP-123 --cd`
245
- - `patchrelay open APP-123`
246
- - `patchrelay retry APP-123`
269
+ - `patchrelay issue list --active`
270
+ - `patchrelay issue show APP-123`
271
+ - `patchrelay issue watch APP-123`
272
+ - `patchrelay dashboard`
273
+ - `patchrelay issue report APP-123`
274
+ - `patchrelay issue events APP-123 --follow`
275
+ - `patchrelay issue path APP-123 --cd`
276
+ - `patchrelay issue open APP-123`
277
+ - `patchrelay issue retry APP-123`
278
+ - `patchrelay service logs --lines 100`
247
279
 
248
- `patchrelay open` is the handoff bridge: it opens Codex in the issue worktree and resumes the existing thread when PatchRelay has one.
280
+ `patchrelay issue open` is the handoff bridge: it opens Codex in the issue worktree and resumes the existing thread when PatchRelay has one.
249
281
 
250
282
  Today that takeover path is intentionally YOLO mode: it launches Codex with `--dangerously-bypass-approvals-and-sandbox`.
251
283
 
@@ -261,12 +293,32 @@ PatchRelay keeps enough durable state to answer the questions that matter during
261
293
  - which files it changed
262
294
  - whether the run completed, failed, or needs handoff
263
295
 
296
+ ## Merge Steward
297
+
298
+ [Merge Steward](./packages/merge-steward) is a separate service that owns serial merge queue integration. PatchRelay develops code and produces pull requests. Merge Steward delivers those PRs into production — rebasing onto main, waiting for CI, and merging when green.
299
+
300
+ The two services communicate through GitHub. PatchRelay adds a `queue` label when a PR is ready. The steward processes the queue. On failure, the steward evicts the PR with a check run report, and PatchRelay can trigger a queue repair run in response.
301
+
302
+ The steward now has its own bootstrap flow:
303
+
304
+ ```bash
305
+ merge-steward init https://queue.example.com
306
+ merge-steward attach app owner/repo --base-branch main --required-check test,lint
307
+ merge-steward doctor --repo app
308
+ merge-steward service status app
309
+ merge-steward queue status --repo app
310
+ ```
311
+
312
+ See [Merge queue](./docs/merge-queue.md) for the full two-service overview and [Merge Steward README](./packages/merge-steward/README.md) for operational details.
313
+
264
314
  ## Docs
265
315
 
266
316
  Use the README for the product overview and quick start. Use the docs for operating details:
267
317
 
318
+ - [Merge queue and delivery](./docs/merge-queue.md)
268
319
  - [Self-hosting and deployment](./docs/self-hosting.md)
269
320
  - [Architecture](./docs/architecture.md)
321
+ - [Design docs index](./docs/design-docs/index.md)
270
322
  - [Design principles](./docs/design-docs/core-beliefs.md)
271
323
  - [External reference patterns](./docs/references/external-patterns.md)
272
324
  - [Security policy](./SECURITY.md)
@@ -88,7 +88,6 @@ export function buildAgentSessionPlan(params) {
88
88
  const runType = resolvePlanRunType(params);
89
89
  switch (params.factoryState) {
90
90
  case "delegated":
91
- case "preparing":
92
91
  return setStatuses(planForRunType(runType, params), ["inProgress", "pending", "pending", "pending"]);
93
92
  case "implementing":
94
93
  return setStatuses(planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
@@ -144,12 +143,6 @@ export function buildAgentSessionPlanForIssue(issue, options) {
144
143
  ...(options?.activeRunType ? { activeRunType: options.activeRunType } : {}),
145
144
  });
146
145
  }
147
- export function buildPreparingSessionPlan(runType) {
148
- return buildAgentSessionPlan({
149
- factoryState: "preparing",
150
- pendingRunType: runType,
151
- });
152
- }
153
146
  export function buildRunningSessionPlan(runType) {
154
147
  return buildAgentSessionPlan({
155
148
  factoryState: runType === "ci_repair" ? "repairing_ci"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.26.0",
4
- "commit": "a2509ed10bce",
5
- "builtAt": "2026-03-27T10:15:02.197Z"
3
+ "version": "0.29.1",
4
+ "commit": "a5915b7ea36c",
5
+ "builtAt": "2026-03-31T23:14:36.879Z"
6
6
  }
package/dist/cli/args.js CHANGED
@@ -2,26 +2,22 @@ import { UnknownCommandError, UnknownFlagsError } from "./errors.js";
2
2
  export const KNOWN_COMMANDS = new Set([
3
3
  "version",
4
4
  "serve",
5
- "inspect",
6
- "live",
7
- "report",
8
- "events",
9
- "worktree",
10
- "open",
11
- "retry",
12
- "list",
5
+ "issue",
13
6
  "doctor",
14
7
  "init",
15
- "project",
8
+ "attach",
9
+ "repos",
10
+ "linear",
11
+ "repo",
12
+ "dashboard",
13
+ "dash",
14
+ "d",
15
+ "service",
16
16
  "connect",
17
17
  "installations",
18
18
  "feed",
19
- "watch",
20
- "install-service",
21
- "restart-service",
22
19
  "help",
23
20
  ]);
24
- const ISSUE_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
25
21
  export function parseArgs(argv) {
26
22
  const positionals = [];
27
23
  const flags = new Map();
@@ -60,10 +56,10 @@ export function resolveCommand(parsed) {
60
56
  return { command: "help", commandArgs: [] };
61
57
  }
62
58
  if (KNOWN_COMMANDS.has(requestedCommand)) {
63
- return { command: requestedCommand, commandArgs: parsed.positionals.slice(1) };
64
- }
65
- if (ISSUE_KEY_PATTERN.test(requestedCommand)) {
66
- return { command: "inspect", commandArgs: parsed.positionals };
59
+ const command = requestedCommand === "dash" || requestedCommand === "d"
60
+ ? "dashboard"
61
+ : requestedCommand;
62
+ return { command, commandArgs: parsed.positionals.slice(1) };
67
63
  }
68
64
  throw new UnknownCommandError(requestedCommand);
69
65
  }
@@ -92,7 +88,15 @@ export function assertKnownFlags(parsed, command, allowedFlags) {
92
88
  if (unknownFlags.length === 0) {
93
89
  return;
94
90
  }
95
- throw new UnknownFlagsError(unknownFlags, command === "project" || command === "project apply" ? "project" : "root");
91
+ throw new UnknownFlagsError(unknownFlags, command === "repo"
92
+ ? "repo"
93
+ : command === "linear"
94
+ ? "linear"
95
+ : command === "issue"
96
+ ? "issue"
97
+ : command === "service"
98
+ ? "service"
99
+ : "root");
96
100
  }
97
101
  export function parsePositiveIntegerFlag(value, flagName) {
98
102
  if (typeof value !== "string") {
@@ -26,7 +26,7 @@ export async function handleFeedCommand(params) {
26
26
  const limit = parseLimit(params.parsed.flags.get("limit"));
27
27
  const follow = params.parsed.flags.get("follow") === true;
28
28
  const issueKey = readOptionalStringFlag(params.parsed, "issue");
29
- const projectId = readOptionalStringFlag(params.parsed, "project");
29
+ const projectId = readOptionalStringFlag(params.parsed, "repo");
30
30
  const kind = readOptionalStringFlag(params.parsed, "kind");
31
31
  const stage = readOptionalStringFlag(params.parsed, "stage");
32
32
  const status = readOptionalStringFlag(params.parsed, "status");
@@ -1,13 +1,53 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
2
  import { getRunTypeFlag, parsePositiveIntegerFlag } from "../args.js";
3
+ import { CliUsageError } from "../errors.js";
3
4
  import { formatJson } from "../formatters/json.js";
4
5
  import { formatEvents, formatInspect, formatList, formatLive, formatOpen, formatReport, formatRetry, formatWorktree } from "../formatters/text.js";
5
6
  import { buildOpenCommand } from "../interactive.js";
6
7
  import { writeOutput } from "../output.js";
8
+ export async function handleIssueCommand(params) {
9
+ const subcommand = params.commandArgs[0];
10
+ if (!subcommand) {
11
+ throw new CliUsageError("patchrelay issue requires a subcommand.", "issue");
12
+ }
13
+ const nested = {
14
+ ...params,
15
+ commandArgs: params.commandArgs.slice(1),
16
+ };
17
+ switch (subcommand) {
18
+ case "show":
19
+ return await handleInspectCommand(nested);
20
+ case "list":
21
+ return await handleListCommand(nested);
22
+ case "watch": {
23
+ const flags = new Map(params.parsed.flags);
24
+ flags.set("watch", true);
25
+ return await handleLiveCommand({
26
+ ...nested,
27
+ parsed: {
28
+ ...params.parsed,
29
+ flags,
30
+ },
31
+ });
32
+ }
33
+ case "report":
34
+ return await handleReportCommand(nested);
35
+ case "events":
36
+ return await handleEventsCommand(nested);
37
+ case "path":
38
+ return await handleWorktreeCommand(nested);
39
+ case "open":
40
+ return await handleOpenCommand(nested);
41
+ case "retry":
42
+ return await handleRetryCommand(nested);
43
+ default:
44
+ throw new CliUsageError(`Unknown issue command: ${subcommand}`, "issue");
45
+ }
46
+ }
7
47
  export async function handleInspectCommand(params) {
8
48
  const issueKey = params.commandArgs[0];
9
49
  if (!issueKey) {
10
- throw new Error("inspect requires <issueKey>.");
50
+ throw new Error("show requires <issueKey>.");
11
51
  }
12
52
  const result = await params.data.inspect(issueKey);
13
53
  if (!result) {
@@ -19,7 +59,7 @@ export async function handleInspectCommand(params) {
19
59
  export async function handleLiveCommand(params) {
20
60
  const issueKey = params.commandArgs[0];
21
61
  if (!issueKey) {
22
- throw new Error("live requires <issueKey>.");
62
+ throw new Error(`${params.parsed.flags.get("watch") === true ? "watch" : "status"} requires <issueKey>.`);
23
63
  }
24
64
  const watch = params.parsed.flags.get("watch") === true;
25
65
  do {
@@ -88,7 +128,7 @@ export async function handleEventsCommand(params) {
88
128
  export async function handleWorktreeCommand(params) {
89
129
  const issueKey = params.commandArgs[0];
90
130
  if (!issueKey) {
91
- throw new Error("worktree requires <issueKey>.");
131
+ throw new Error("path requires <issueKey>.");
92
132
  }
93
133
  const result = params.data.worktree(issueKey);
94
134
  if (!result) {
@@ -150,7 +190,7 @@ export async function handleListCommand(params) {
150
190
  const result = params.data.list({
151
191
  active: params.parsed.flags.get("active") === true,
152
192
  failed: params.parsed.flags.get("failed") === true,
153
- ...(typeof params.parsed.flags.get("project") === "string" ? { project: String(params.parsed.flags.get("project")) } : {}),
193
+ ...(typeof params.parsed.flags.get("repo") === "string" ? { project: String(params.parsed.flags.get("repo")) } : {}),
154
194
  });
155
195
  writeOutput(params.stdout, params.json ? formatJson(result) : formatList(result));
156
196
  return 0;
@@ -0,0 +1,67 @@
1
+ import { loadConfig } from "../../config.js";
2
+ import { runConnectFlow, parseTimeoutSeconds } from "../connect-flow.js";
3
+ import { formatJson } from "../formatters/json.js";
4
+ import { writeOutput } from "../output.js";
5
+ import { openExternalUrl } from "../interactive.js";
6
+ export async function handleLinearCommand(params) {
7
+ const subcommand = params.commandArgs[0] ?? "list";
8
+ const config = params.options?.config ?? loadConfig(undefined, { profile: "operator_cli" });
9
+ const data = params.options?.data ?? (await createCliOperatorDataAccess(config));
10
+ try {
11
+ switch (subcommand) {
12
+ case "connect":
13
+ return await runConnectFlow({
14
+ config,
15
+ data,
16
+ stdout: params.stdout,
17
+ noOpen: params.parsed.flags.get("no-open") === true,
18
+ timeoutSeconds: parseTimeoutSeconds(params.parsed.flags.get("timeout"), "linear connect"),
19
+ json: params.json,
20
+ openExternal: params.options?.openExternal ?? openExternalUrl,
21
+ ...(params.options?.connectPollIntervalMs !== undefined ? { connectPollIntervalMs: params.options.connectPollIntervalMs } : {}),
22
+ });
23
+ case "list": {
24
+ const result = await data.listLinearWorkspaces();
25
+ writeOutput(params.stdout, params.json
26
+ ? formatJson({ ok: true, ...result })
27
+ : result.workspaces.length === 0
28
+ ? "No Linear workspaces connected.\n"
29
+ : `${result.workspaces.map((workspace) => {
30
+ const name = workspace.installation.workspaceKey ?? workspace.installation.workspaceName ?? `installation-${workspace.installation.id}`;
31
+ return `${name} repos=${workspace.linkedRepos.length} teams=${workspace.teams.length} projects=${workspace.projects.length}`;
32
+ }).join("\n")}\n`);
33
+ return 0;
34
+ }
35
+ case "sync": {
36
+ const workspace = params.commandArgs[1];
37
+ const result = await data.syncLinearWorkspace(workspace);
38
+ writeOutput(params.stdout, params.json
39
+ ? formatJson({ ok: true, ...result })
40
+ : `Synced ${result.installation.workspaceKey ?? result.installation.workspaceName ?? result.installation.id}: ${result.teams.length} teams, ${result.projects.length} projects\n`);
41
+ return 0;
42
+ }
43
+ case "disconnect": {
44
+ const workspace = params.commandArgs[1];
45
+ if (!workspace) {
46
+ throw new Error("patchrelay linear disconnect requires <workspace>.");
47
+ }
48
+ const result = await data.disconnectLinearWorkspace(workspace);
49
+ writeOutput(params.stdout, params.json
50
+ ? formatJson({ ok: true, ...result })
51
+ : `Disconnected ${result.installation.workspaceKey ?? result.installation.workspaceName ?? result.installation.id}.\n`);
52
+ return 0;
53
+ }
54
+ default:
55
+ throw new Error(`Unknown linear subcommand: ${subcommand}`);
56
+ }
57
+ }
58
+ finally {
59
+ if (!params.options?.data) {
60
+ data.close();
61
+ }
62
+ }
63
+ }
64
+ async function createCliOperatorDataAccess(config) {
65
+ const { CliOperatorApiClient } = await import("../operator-client.js");
66
+ return new CliOperatorApiClient(config);
67
+ }