@catalyst-cloud/cli 0.16.25 → 0.16.28

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.28
4
+
5
+ `catalyst events wait-for` can now wait on a repository (`--repo owner/name`), a branch (`--ref owner/name:branch`), a pull request (`--pr owner/name#n`), a work session (`--session KEY-123/phase#n`), events addressed to you (`--to me`) or a family of event types (`--type pr.*`). An agent can wait hours for a review or a conflict on its own branch: the wait holds one connection, prints each matching event as one JSON line, and after a dropped connection picks up where it left off without missing or repeating an event. Two new exit codes say why a wait ended without a match. 2 means the cloud refused the wait, for example on a ticket outside your teams, or the wait failed. 3 means `--from-cache` found no local event cache. A wait still exits 1 on a timeout and 4 on an outage, as before, and on a timeout it names the `--after` to resume from. New: `catalyst messages read --ticket KEY-123` prints the messages sent to a ticket on your teams, with each sender and time.
6
+
7
+ ## 0.16.27
8
+
9
+ A large workspace's local replica now finishes its first download instead of stopping after 30 seconds: it waits as long as data keeps arriving, and gives up only after 150 seconds with no progress. New commands: `catalyst repo status` lists your repositories and whether each is paused, and `catalyst repo pause|resume` pauses or resumes one (owners and admins). `catalyst runner status|stop|start|uninstall` stops a self-hosted machine taking work, starts it again with the same registration, or removes it and revokes its registration. Setup with local sync off now lists an old local replica's files with their sizes and removes them only after you say yes. Setup with local sync on keeps using the replica you already have. On a Mac where Docker Desktop does not share your home folder, setup now says which setting to change (Settings, then Resources, then File sharing).
10
+
11
+ ## 0.16.26
12
+
13
+ Not published. Its changes ship in 0.16.27.
14
+
3
15
  ## 0.16.25
4
16
 
5
17
  A self-hosted machine that runs several teams' work from one `compose.json` can now take each approved release by itself. Run `catalyst runner update --fleet --dir <the folder that holds compose.json>`, by hand or on a schedule. It switches only the supervisor to the approved images and leaves work that is already running alone. If the machine's teams are not ready again within four minutes, it puts the previous images back. `--check` shows what would change without changing anything.
package/README.md CHANGED
@@ -166,6 +166,19 @@ With a person at the terminal, `catalyst onboard` starts each of these approvals
166
166
 
167
167
  With `--runner yes`, setup starts the self-hosted runner and verifies enrollment, advertised capacity and team admission. This needs Docker with Compose, pinned host images and an organization key file (`CATALYST_RUNNER_ORG_KEY_FILE`). When images, a key or admission are missing, exit 11 names that gate. A failed setup step exits 10. Use `--runner no` to skip the machine's runner. Unlike the interactive optional runner step, an explicitly requested headless runner must finish before exit 0.
168
168
 
169
+ ### Stopping or removing this machine's runner
170
+
171
+ Run these on the machine itself:
172
+
173
+ | command | what it does |
174
+ | -- | -- |
175
+ | `catalyst runner stop` | The machine takes no new work. Work it already started finishes or reaches its deadline. Its registration, settings and checkouts are kept, and `catalyst hosts` shows it as `stopped`. |
176
+ | `catalyst runner start` | Starts it again with the same registration. |
177
+ | `catalyst runner status` | Says whether it is running, stopped or not installed. |
178
+ | `catalyst runner uninstall` | Asks first (`--yes` confirms without a terminal; an owner or admin login is needed). It stops new work, revokes the machine's registration and the runner key setup issued, and switches the project's runner admission off when no other machine runs it. Then it removes the runner's containers, their volumes and the session network, and the setup files in the runner folder. On macOS it also removes the storage LaunchAgent and its folders. On Linux it also removes the session-egress units, their script and state, and the `inet catalyst_session` nftables table, through `sudo`. |
179
+
180
+ Uninstall removes only what setup created. Anything that does not match what setup wrote, such as a network with another label or a changed unit file, is left in place. Your repository and thoughts checkouts are kept unless you add `--delete-checkouts`. The command ends with a "Left behind" list naming everything it did not remove. When a step fails, such as a cloud call while you are offline, it says which one and exits 1. Run it again: it picks up where the last run stopped. Do not delete the runner folder by hand while the runner is installed. That deletes its settings while its containers keep running.
181
+
169
182
  With `--json`, stdout carries exactly one document: the onboarding receipt plus a `headless` block (`schema: "catalyst-onboard-headless/1"`). The block holds the resolved `inputs` (the key appears only as its source: `env`, `file` or `saved`), and lists of items under `missing`, `refused`, `failed`, `deferred` and `warnings`. Each item has `id`, `kind` (`input`, `grant` or `step`), `reason` and a one-line `text`, plus `flag`, `env` and `url` where they apply. Everything else goes to stderr.
170
183
 
171
184
  | Exit | Meaning |
@@ -201,6 +214,24 @@ Open a new session and ask about your own cloud account. The skills read the con
201
214
 
202
215
  `catalyst team list` inventories visible teams without running readiness checks. `team check ENG` prints readiness for one team; `team check --all` runs checks for every team. `team map ENG`, `team adopt ENG`, and `team migrate ENG` print a plan and exit 3 before changing configuration; review the plan and rerun the chosen command with `--yes --plan-hash <hash>` from that preview. `team adopt ENG --undo` previews the exact previously created stages, and `team migrate ENG --retire` requires its own confirmation after the ticket move. `team checklist ENG` prints the same manual setup lines as the browser. These commands use the member's own login; an admin or owner seat is required for setup, and Adopt and Migrate also need that member's personal Linear connection.
203
216
 
217
+ ## Repository status and pauses
218
+
219
+ Use your existing login to list the repositories visible to you, their status and attached projects:
220
+
221
+ ```sh
222
+ catalyst repo status
223
+ catalyst repo status --json
224
+ catalyst repo pause acme/app --reason "flaky deploy"
225
+ catalyst repo resume acme/app --reason "deploy fixed"
226
+ catalyst repo resume acme/app
227
+ ```
228
+
229
+ Any workspace member can read status. Pausing or resuming requires a workspace owner or admin. Pause requires a reason; resume accepts an optional reason. A supplied reason must contain 1 to 500 characters after trimming. These commands work outside a local checkout.
230
+
231
+ Paused rows show who paused the repository (their email, or the key or operator that paused it), when and why, when those details are available. An empty list means no repositories are visible to your login. After a write, the command reads and prints the repository's current state. If that read fails, it reports that the write succeeded and status could not be read; check `catalyst repo status` before repeating a write.
232
+
233
+ `--json` preserves the list response for status and prints the refreshed repository row for a write. An older cloud reports that repository status is not served yet and needs a newer cloud. Agents can use `catalyst capabilities --json` to check route availability against the cached cloud contract.
234
+
204
235
  ## What has to be running
205
236
 
206
237
  Nothing, by default. After `login`, every read, write, ask and explanation goes to the cloud's origin-fresh API with the config file and the cached contract on disk. Two optional processes exist for people who want them:
@@ -215,7 +246,22 @@ CLI and skill reads use the cloud API by default, including on a machine with a
215
246
 
216
247
  For a local view, `catalyst replica status` checks the writer, heartbeat and cursor. It exits `0` for fresh, `1` for present but stale, `2` for not connected, `3` for absent. `replica status --probe` compares its cursor with the cloud head. These diagnostics do not change the default read source, and an absent replica does not make `catalyst ready` fail.
217
248
 
218
- `catalyst events tail`, `wait-for`, `query` and `status` read from the cloud by default, with no local file. `events tail` prints one JSON line per new event and stays in the foreground; `events wait-for --ticket ... --timeout ...` prints the first match and exits 0, or exits 1 when none arrives in time; `events query` reads one page of filtered history (`--ticket`, `--type`, `--limit` up to 200, `--order desc|asc`, `--before`, `--after`); `events status` names the cloud head. Add `--from-cache` to read this machine's local event cache instead, which pays off when several agents on one machine share one cloud connection. `catalyst events status --from-cache --probe` checks the cache's own cursor against the cloud head; `replica status --probe` checks the separate entity-replica cursor. A fresh replica does not prove event freshness. The replica process is the single writer and reports an event-sync failure without terminating a healthy entity replica. The event cache keeps closed daily segments for at most seven days or 256 MiB per account and reports an explicit gap when a requested sequence has retired.
249
+ `catalyst events tail`, `wait-for`, `query` and `status` read from the cloud by default, with no local file. `events tail` prints one JSON line per new event and stays in the foreground; `events wait-for --ticket ... --timeout ...` prints the first match and exits 0, or exits 1 when none arrives in time; `events query` reads one page of filtered history (`--ticket`, `--type`, `--limit` up to 200, `--order desc|asc`, `--before`, `--after`); `events status` names the cloud head.
250
+
251
+ `events wait-for` can also wait on a repository (`--repo owner/name`), a branch (`--ref owner/name:branch`), a pull request (`--pr owner/name#n`), a phase session (`--session TICKET/phase#n`), events addressed to you (`--to me`), or a family of types (`--type pr.*`). Values of one flag are OR'ed and different flags are AND'ed, and `--repo`, `--ref`, `--pr` and `--session` can repeat. Such a wait holds one connection and prints nothing until a match, so a long `--timeout` such as 14400 costs nothing while it waits. It prints every matching event as one JSON line. After a dropped connection it replays from its cursor, so no event is missed or printed twice.
252
+
253
+ | `wait-for` exit | Meaning |
254
+ | --- | --- |
255
+ | 0 | a match, printed on stdout |
256
+ | 1 | none arrived in time; with a scope flag, stderr names the `--after` to resume from |
257
+ | 2 | the cloud refused the wait (for example a ticket outside your teams), or the wait failed; stderr gives the reason |
258
+ | 3 | `--from-cache` and the local event cache is absent or its writer stopped |
259
+ | 4 | the cloud was unreachable for five attempts in a row: an outage, not a timeout |
260
+ | 130 | interrupted |
261
+
262
+ `catalyst messages read --ticket KEY-123 [--since SEQUENCE]` prints the messages sent to a ticket on your teams, with each sender and time, for example after a wait on `--type comms.message.posted` returns.
263
+
264
+ Add `--from-cache` to read this machine's local event cache instead, which pays off when several agents on one machine share one cloud connection. `catalyst events status --from-cache --probe` checks the cache's own cursor against the cloud head; `replica status --probe` checks the separate entity-replica cursor. A fresh replica does not prove event freshness. The replica process is the single writer and reports an event-sync failure without terminating a healthy entity replica. The event cache keeps closed daily segments for at most seven days or 256 MiB per account and reports an explicit gap when a requested sequence has retired.
219
265
 
220
266
  The replica is a Node process, not a service: the supported path is the plain command, and `skills/catalyst-onboard/references/local-sync.md` gives launchd and systemd examples for people who want the writer to survive a reboot.
221
267
 
@@ -270,6 +316,8 @@ A `customer.json` written by an older bundle is still read unchanged; it gains t
270
316
 
271
317
  ## Uninstalling
272
318
 
319
+ If this machine runs Catalyst's work, run `catalyst runner uninstall` first (see "Stopping or removing this machine's runner").
320
+
273
321
  Remove the skills the way you installed them: `/plugin uninstall catalyst@catalyst-cloud` in Claude Code, or delete the skill directories (`catalyst-github`, `catalyst-linear`, `catalyst-onboard`, `run-this-project`, `unstick`, `what-needs-me`, `what-this-repo-needs`, `whats-happening`) from wherever `npx skills add` wrote them. Skills an earlier release shipped (`catalyst-setup`, `connect-me`, `how-catalyst-works`) go the same way; the CLI removes its own stamped copies of them when it installs or refreshes skills. Then remove what the CLI wrote:
274
322
 
275
323
  ```sh
package/dist/args.js CHANGED
@@ -26,6 +26,8 @@ export const FLAG_TABLES = {
26
26
  auto: { value: false, help: "one-line output for the scheduled run" },
27
27
  dir: { value: true, help: "the runner directory (default: the one setup created)" },
28
28
  fleet: { value: false, help: "update a multi-team supervisor fleet's compose.json in --dir" },
29
+ yes: { value: false, help: "uninstall: confirm without a terminal" },
30
+ "delete-checkouts": { value: false, help: "uninstall: also delete the repository and thoughts checkouts" },
29
31
  },
30
32
  login: {
31
33
  "start-replica": {
@@ -66,6 +68,10 @@ export const FLAG_TABLES = {
66
68
  value: false,
67
69
  help: "include the optional local sync setup in this plan",
68
70
  },
71
+ "remove-local-data": {
72
+ value: false,
73
+ help: "with local sync off, stop existing local writers and delete the listed replica and event-cache files; keep login, contract and settings",
74
+ },
69
75
  runner: {
70
76
  value: false,
71
77
  optionalValue: true,
@@ -152,11 +158,16 @@ export const FLAG_TABLES = {
152
158
  value: false,
153
159
  help: "status --from-cache: compare the local event cursor with the cloud event head",
154
160
  },
155
- type: { value: true, help: "exact event type" },
161
+ type: { value: true, help: "exact event type, or for wait-for a family such as pr.*" },
156
162
  ticket: {
157
163
  value: true,
158
164
  help: "ticket identifier found in the event",
159
165
  },
166
+ repo: { value: true, repeat: true, help: "wait-for: repository as owner/name (repeatable)" },
167
+ ref: { value: true, repeat: true, help: "wait-for: branch as owner/name:branch (repeatable)" },
168
+ pr: { value: true, repeat: true, help: "wait-for: pull request as owner/name#number (repeatable)" },
169
+ session: { value: true, repeat: true, help: "wait-for: phase session as TICKET/phase#attempt (repeatable)" },
170
+ to: { value: true, help: "wait-for: me, for events addressed to this login" },
160
171
  after: {
161
172
  value: true,
162
173
  help: "event sequence to read after (tail and wait-for default to new events only)",
@@ -472,6 +483,10 @@ export const FLAG_TABLES = {
472
483
  },
473
484
  },
474
485
  identity: {},
486
+ messages: {
487
+ ticket: { value: true, help: "read: the ticket whose messages to read" },
488
+ since: { value: true, help: "read: the first message sequence to read (inclusive)" },
489
+ },
475
490
  capabilities: {},
476
491
  project: {
477
492
  team: {
@@ -486,7 +501,7 @@ export const FLAG_TABLES = {
486
501
  },
487
502
  data: {
488
503
  value: false,
489
- help: "historical flag; shared data folders are always kept",
504
+ help: "include the listed replica DB, sidecars and local event cache; shared data folders, login, contract and settings are kept",
490
505
  },
491
506
  yes: {
492
507
  value: false,
@@ -494,6 +509,7 @@ export const FLAG_TABLES = {
494
509
  },
495
510
  },
496
511
  repo: {
512
+ reason: { value: true, help: "pause: required; resume: optional; 1 to 500 characters after trimming (owner or admin)" },
497
513
  write: {
498
514
  value: false,
499
515
  help: "agents-block: write or update the block in AGENTS.md (working tree only)",
@@ -545,10 +561,10 @@ export const FLAG_TABLES = {
545
561
  };
546
562
  export const VERB_USAGE = {
547
563
  hosts: "hosts [list] | hosts remove <name-or-id> [--yes] | hosts rename <name-or-id> <new-name> [--json]",
548
- runner: "runner update [--check] [--dir <runner directory>] [--fleet] | runner auto-update [status|on|off]",
564
+ runner: "runner [status] | runner stop | runner start | runner uninstall [--yes] [--delete-checkouts] | runner update [--check] [--dir <runner directory>] [--fleet] | runner auto-update [status|on|off] [--json]",
549
565
  setup: "setup [onboard options] (sets up Catalyst step by step; same as catalyst onboard) | setup --engine <file> --engine-sha256 <hex> [-- <install options>] (run by install.sh; install.sh --help lists the options)",
550
566
  login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
551
- onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--runner|--no-runner] [--no-color] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--json]",
567
+ onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--remove-local-data] [--runner|--no-runner] [--no-color] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--remove-local-data] [--json]",
552
568
  install: "install [--skills-dir <dir>] [--force]",
553
569
  status: "status",
554
570
  notice: "notice",
@@ -557,7 +573,7 @@ export const VERB_USAGE = {
557
573
  query: "query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>> [--team K] [--project P] [--state S] [--limit N] [--all] [--source replica|api] [--json]",
558
574
  replica: 'replica <start [--detach]|stop|status [--probe] [--json]|sql "<select>"|schema [table]> [--db <path>]',
559
575
  runtime: "runtime <status [--json]|install|path|uninstall>",
560
- events: "events <tail|wait-for|query|status [--probe] [--json]> [--type NAME] [--ticket CTC-N] [--after SEQUENCE] [--limit N] [--timeout SECONDS] [--directory PATH]",
576
+ events: "events <tail|wait-for|query|status [--probe] [--json]> [--type NAME] [--ticket CTC-N] [--repo O/N] [--ref O/N:BRANCH] [--pr O/N#N] [--session T/PHASE#N] [--to me] [--after SEQUENCE] [--limit N] [--timeout SECONDS] [--directory PATH]",
561
577
  explain: "explain <ticket> [--history] [--json]",
562
578
  history: "history <ticket> [--json]",
563
579
  running: "running [--ticket T --phase P] [--json]",
@@ -575,9 +591,10 @@ export const VERB_USAGE = {
575
591
  secret: "secret set <NAME> --repo <owner/name> [--command '<cmd>'] [--json] (value from --command, stdin, or a hidden prompt) | secret import <file> --repo <owner/name> [--names NAME[,NAME...]] [--rotate NAME]... [--json]",
576
592
  var: "var <set NAME|import <file>> [--repo <owner/name>] [--names NAME[,NAME...]] [--json] (plain environment variables; set reads stdin or a hidden prompt)",
577
593
  identity: "identity linear <status|options|set> [<linearUserId>] [--json]",
594
+ messages: "messages read --ticket CTC-N [--since SEQUENCE] [--json]",
578
595
  capabilities: "capabilities [--json]",
579
596
  project: "project <list [--json] | wip-limit <get|set <n>|set default> [--team K] [--json]>",
580
- repo: "repo <agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> [--json]",
597
+ repo: "repo <status|pause <owner/name> --reason <text>|resume <owner/name> [--reason <text>]|agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> [--json] (status: visible workspace repositories; agents-block/agent-setup: local checkout)",
581
598
  legacy: "legacy [--remove [--data] [--yes]] [--json]",
582
599
  connections: "connections personal <linear|github> <start|status> [--wait <seconds>] [--json]",
583
600
  release: "release <ticket> --because <what changed> [--retry-unchanged] [--dry-run] [--json] | release --class <failure-class> --team <K> --because <what changed> [--retry-unchanged] [--dry-run] [--limit N] [--json]",
@@ -8,6 +8,8 @@ export const CAPABILITIES = [
8
8
  { verb: "hosts", does: "list this account's machines, current use and live capacity", needs: "member", routes: [], since: "0.16.0" },
9
9
  { verb: "hosts remove|rename", does: "remove or rename a self-hosted machine in this account", needs: "admin", routes: [], since: "0.16.0" },
10
10
  { verb: "runner update|auto-update", does: "move this machine's self-hosted runner to the release Catalyst approved, and check for one every 15 minutes", needs: "member", routes: [], since: "0.16.16" },
11
+ { verb: "runner status|stop|start", does: "stop this machine's self-hosted runner taking work, or start it again with the same registration", needs: "member", routes: [], since: "0.16.27" },
12
+ { verb: "runner uninstall", does: "remove this machine's self-hosted runner and revoke its registration and runner key", needs: "admin", routes: [], since: "0.16.27" },
11
13
  // CTC-4680: plain `catalyst setup` runs the onboard flow, so install.sh may hand off to it. install.sh
12
14
  // reads this flag rather than `setup --help`, which already exits 0 on 0.15.x.
13
15
  { verb: "setup", does: "set up Catalyst step by step, or show install.sh's plan, progress and sign-in in this terminal", needs: "member", routes: [], since: "0.15.0", engineProtocol: 1, bootstrapHandoff: true, bootstrapHandoffVersion: 2 },
@@ -28,6 +30,8 @@ export const CAPABILITIES = [
28
30
  { verb: "team checklist", does: "print what adopting the workflow would do to a team", needs: "admin", routes: [agent("GET", "team-workflow")], since: "0.9.5" },
29
31
  { verb: "project wip-limit get", does: "read a project's new-start WIP limit, where it comes from, and its work in progress now", needs: "member", routes: [agent("GET", "team-wip-limit")], since: "0.13.1" },
30
32
  { verb: "project wip-limit set", does: "set a project's new-start WIP limit (0 to 9999, or back to the workspace default)", needs: "admin", routes: [agent("POST", "team-wip-limit")], since: "0.13.1" },
33
+ { verb: "repo status", does: "list visible repositories with their state, pause details and projects", needs: "member", routes: [agent("GET", "repos")], since: "0.16.27" },
34
+ { verb: "repo pause|resume", does: "pause or resume a repository as a workspace owner or admin", needs: "admin", routes: [agent("GET", "repos"), agent("POST", "repos/pause"), agent("POST", "repos/resume")], since: "0.16.27" },
31
35
  { verb: "repo agents-block", does: "add or refresh the one Catalyst block in a checkout's AGENTS.md, in the working tree, for the person to commit and open as a pull request", needs: "member", routes: [], since: "0.13.1" },
32
36
  { verb: "repo agent-setup", does: "read a checkout's agent setup (AGENTS.md, CLAUDE.md, .agents and .claude skills and rules) and, on request, make it portable in the working tree", needs: "member", routes: [], since: "0.13.1" },
33
37
  { verb: "env shell", does: "open this repository's environment on this computer: Catalyst's image, the declared toolchain and variables (needs Docker; no login for Catalyst's image)", needs: "member", routes: [], since: "0.16.20" },
package/dist/cli.js CHANGED
@@ -47,8 +47,9 @@ import { cmdProject } from "./project.js";
47
47
  import { cmdRepo } from "./repo.js";
48
48
  import { cmdLegacy } from "./legacy.js";
49
49
  import { cmdIdentity } from "./identity.js";
50
+ import { cmdMessages } from "./messages.js";
50
51
  import { cmdConnections } from "./connections.js";
51
- import { cmdOnboard, onboardErrorJournal, } from "./onboard.js";
52
+ import { cmdOnboard, onboardErrorJournal, onboardStatePath, readOnboardJournal, } from "./onboard.js";
52
53
  import { createClackOnboardUi, shouldUseOnboardUi, } from "./onboard-ui.js";
53
54
  import { createOnboardRuntime } from "./onboard-runtime.js";
54
55
  import { argvRequestsHeadless, headlessOnboardDeps, headlessUsageRefusal, onboardHeadlessRequested, runOnboardHeadless, } from "./onboard-headless.js";
@@ -85,6 +86,7 @@ export function usageText() {
85
86
  " catalyst status | notice | me | ready | accounts",
86
87
  " catalyst seat checkout --size big|med --harness claude,codex,opencode [--session-id ID] [--stage STAGE]",
87
88
  " catalyst hosts [list] | hosts remove <name-or-id> [--yes] | hosts rename <name-or-id> <new-name>",
89
+ " catalyst runner [status] | runner stop | runner start | runner uninstall [--yes] [--delete-checkouts] (this machine's self-hosted runner)",
88
90
  " catalyst runner update [--check] | runner auto-update [status|on|off] (move this machine's runner to the approved release)",
89
91
  " catalyst mcp add|list|remove (vault references only) | mcp status <name> (a server's state, grants and last 50 tool calls)",
90
92
  " catalyst contract [--refresh] [--path <a.b.c>]",
@@ -92,6 +94,7 @@ export function usageText() {
92
94
  ' catalyst replica <start [--detach]|stop|status [--probe]|sql "<select>"|schema [table]>',
93
95
  " catalyst runtime <status [--json]|install|path|uninstall> (a pinned Node this CLI manages itself)",
94
96
  " catalyst events <tail|wait-for|query|status> [--type NAME] [--ticket CTC-N] [--after SEQUENCE] [--from-cache]",
97
+ " catalyst events wait-for [--repo O/N] [--ref O/N:BRANCH] [--pr O/N#N] [--session T/PHASE#N] [--to me] [--type pr.*] [--timeout SECONDS] (one connection; nothing printed until a match)",
95
98
  " catalyst explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
96
99
  " catalyst watch [--team K] [--ticket T]... [--project P] [--exec CMD]",
97
100
  " catalyst write <comment|state|label|create|reaction|attachment|session> ...",
@@ -106,9 +109,12 @@ export function usageText() {
106
109
  " catalyst project list [--json] (all workspace projects, including those with no stage mapping)",
107
110
  " catalyst stage <start|renew|done|fail> <ticket> <phase> (a cloud-claimed local phase)",
108
111
  " catalyst project wip-limit <get|set <n>|set default> [--team K] (a project's new-start WIP limit; members read, owners and admins set)",
112
+ " catalyst repo pause <owner/name> --reason <text> | repo resume <owner/name> [--reason <text>] (owner or admin; reason: 1 to 500 trimmed characters)",
113
+ " catalyst repo status [--json] (visible workspace repositories and their paused state; needs login)",
109
114
  " catalyst repo <agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> (a checkout's AGENTS.md block and portable agent layout; working tree only)",
110
115
  " catalyst legacy [--remove [--data] [--yes]] (leftovers of the old local Catalyst runtime: list them; remove them only on a yes)",
111
116
  " catalyst identity linear <status|options|set> [<linearUserId>] [--json]",
117
+ " catalyst messages read --ticket CTC-N [--since SEQUENCE] [--json] (messages sent to a ticket on your teams)",
112
118
  " catalyst connections personal <linear|github> <start|status> [--wait <seconds>] [--json]",
113
119
  "",
114
120
  "Every verb takes --help. --json makes the output machine-readable.",
@@ -131,6 +137,15 @@ export function usageText() {
131
137
  * the `catalyst` launcher beside it, once, on the next run of either name. Read and write back to
132
138
  * back, so the window for racing a concurrent OAuth refresh is as small as the update notice's.
133
139
  */
140
+ /** Setup's saved local sync choice; an unreadable record reads as off, which only adds a hint. */
141
+ function savedLocalSync(ctx) {
142
+ try {
143
+ return readOnboardJournal(onboardStatePath(ctx.home, ctx.env))?.localSync === true;
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
134
149
  function migrateLegacyCliPath(ctx) {
135
150
  let cfg;
136
151
  try {
@@ -520,9 +535,11 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
520
535
  case "repo":
521
536
  return await cmdRepo(args, ctx);
522
537
  case "legacy":
523
- return await cmdLegacy(args, ctx, deps.legacy ?? {});
538
+ return await cmdLegacy(args, ctx, { localSyncSelected: savedLocalSync(ctx), ...deps.legacy });
524
539
  case "identity":
525
540
  return await cmdIdentity(args, ctx, deps.identity ?? {});
541
+ case "messages":
542
+ return await cmdMessages(args, ctx, deps.messages ?? {});
526
543
  case "hosts":
527
544
  return await cmdHosts(args, ctx, deps.hosts);
528
545
  case "runner":
@@ -587,6 +604,7 @@ const VERB_HELP_KNOWN = Object.fromEntries([
587
604
  "environment",
588
605
  "connections",
589
606
  "identity",
607
+ "messages",
590
608
  "mcp",
591
609
  "team",
592
610
  "capabilities",
package/dist/events.js CHANGED
@@ -1,4 +1,4 @@
1
- import { flagInt, flagString, positionals } from "./args.js";
1
+ import { flagInt, flagList, flagString, positionals } from "./args.js";
2
2
  import { normalizeBaseUrl, requireConfig } from "./config.js";
3
3
  import { CliError, UsageError } from "./errors.js";
4
4
  import { authStrategyFor } from "./oauth.js";
@@ -39,6 +39,13 @@ export async function cmdEvents(args, ctx, deps = {}) {
39
39
  throw new UsageError("events needs a subcommand: tail | wait-for | query | status");
40
40
  if (!["tail", "wait-for", "query", "status"].includes(sub))
41
41
  throw new UsageError(`unknown events subcommand: ${sub}`);
42
+ // CTC-6650 — a scope key or a type family runs on the SDK's wait; the --type/--ticket path is unchanged.
43
+ const scope = scopeFilter(args);
44
+ if (scope) {
45
+ if (sub !== "wait-for")
46
+ throw new UsageError("--repo, --ref, --pr, --session, --to and a type family such as pr.* work with wait-for only");
47
+ return cmdScopedWait(args, ctx, deps, scope);
48
+ }
42
49
  // CTC-4554 — the cloud is the default source; the local event file is the opt-in.
43
50
  if (args.flags["from-cache"] !== true) {
44
51
  if (flagString(args, "directory") !== undefined)
@@ -118,6 +125,144 @@ function startingCursor(args, name = "after") {
118
125
  throw new UsageError(`--${name} must be a non-negative event sequence`);
119
126
  return parsed;
120
127
  }
128
+ const SCOPE_KEYS = ["repo", "ref", "pr", "session"];
129
+ /** The SDK filter when the wait names a scope key or a type family, or null for the original path. */
130
+ function scopeFilter(args) {
131
+ const filter = {};
132
+ for (const key of SCOPE_KEYS) {
133
+ const values = flagList(args, key);
134
+ if (values.length)
135
+ filter[key] = values;
136
+ }
137
+ const to = flagString(args, "to");
138
+ if (to !== undefined) {
139
+ if (to !== "me")
140
+ throw new UsageError("--to accepts only me");
141
+ filter.to = "me";
142
+ }
143
+ const type = flagString(args, "type");
144
+ if (Object.keys(filter).length === 0 && !type?.endsWith(".*"))
145
+ return null;
146
+ const ticket = flagString(args, "ticket");
147
+ return { ...filter, ...(type ? { type } : {}), ...(ticket ? { ticket } : {}) };
148
+ }
149
+ /** Counts consecutive failed requests, so a wait that cannot reach the cloud ends as an outage. */
150
+ function outageFetch(base, signal, onOutage, now) {
151
+ let failures = 0;
152
+ let sinceMs = 0;
153
+ const trouble = () => {
154
+ if (failures === 0)
155
+ sinceMs = now();
156
+ failures += 1;
157
+ if (failures >= OUTAGE_ATTEMPTS)
158
+ onOutage(failures, sinceMs);
159
+ };
160
+ return async (input, init) => {
161
+ try {
162
+ const response = await base(input, init);
163
+ if (response.status >= 500)
164
+ trouble();
165
+ else
166
+ failures = 0;
167
+ return response;
168
+ }
169
+ catch (error) {
170
+ // A request the SDK timed out counts; one cancelled because this wait ended does not.
171
+ if (!signal.aborted)
172
+ trouble();
173
+ throw error;
174
+ }
175
+ };
176
+ }
177
+ /** `wait-for` with scope keys: the SDK's wait, under the CLI's documented exit codes. */
178
+ async function cmdScopedWait(args, ctx, deps, filter) {
179
+ const cfg = requireConfig(ctx);
180
+ const timeout = flagInt(args, "timeout", 300);
181
+ if (timeout < 0)
182
+ throw new UsageError("--timeout must be zero or more seconds");
183
+ const after = startingCursor(args);
184
+ const fromCache = args.flags["from-cache"] === true;
185
+ if (!fromCache && flagString(args, "directory") !== undefined)
186
+ throw new UsageError("--directory names the local event cache; add --from-cache to read it");
187
+ const sdk = await (deps.loadSdk ?? loadEventsSdk)();
188
+ const { waitForEvents, waitForCachedEvents } = sdk;
189
+ if (!waitForEvents || !waitForCachedEvents)
190
+ throw new CliError("the installed SDK cannot wait on scope keys; upgrade the catalyst CLI", "sdk-upgrade-required");
191
+ const controller = new AbortController();
192
+ const abort = () => controller.abort(deps.signal?.reason);
193
+ if (deps.signal?.aborted)
194
+ abort();
195
+ else
196
+ deps.signal?.addEventListener("abort", abort, { once: true });
197
+ let interrupted = false;
198
+ let outage;
199
+ const removeInterrupt = (deps.onInterrupt ?? processInterrupts)(() => {
200
+ interrupted = true;
201
+ controller.abort(new Error("interrupted"));
202
+ });
203
+ try {
204
+ let result;
205
+ if (fromCache) {
206
+ result = await waitForCachedEvents({
207
+ tenantId: cfg.account,
208
+ directory: flagString(args, "directory") ?? sdk.defaultEventCacheDirectory(cfg.account),
209
+ filter,
210
+ after,
211
+ timeoutMs: timeout * 1_000,
212
+ signal: controller.signal,
213
+ });
214
+ }
215
+ else {
216
+ const auth = authStrategyFor(ctx, cfg);
217
+ if (auth.kind === "cookie")
218
+ throw new CliError("event waits require a token or OAuth session", "events-auth");
219
+ result = await waitForEvents({
220
+ baseUrl: normalizeBaseUrl(cfg.baseUrl),
221
+ auth,
222
+ tenantId: cfg.account,
223
+ filter,
224
+ after,
225
+ timeoutMs: timeout * 1_000,
226
+ signal: controller.signal,
227
+ fetch: outageFetch(ctx.fetch, controller.signal, (failures, sinceMs) => {
228
+ if (outage)
229
+ return;
230
+ outage = { failures, sinceMs };
231
+ controller.abort(new Error("cloud unreachable"));
232
+ }, () => ctx.now().getTime()),
233
+ });
234
+ }
235
+ const resume = (cursor) => (cursor === null || cursor === undefined ? "" : `; resume with --after ${cursor}`);
236
+ switch (result.outcome) {
237
+ case "matched":
238
+ for (const event of result.events)
239
+ ctx.stdout(JSON.stringify(event));
240
+ return 0;
241
+ case "timeout":
242
+ ctx.stderr(`events: no matching event within ${timeout} s${resume(result.cursor)}`);
243
+ return 1;
244
+ case "aborted":
245
+ if (interrupted)
246
+ return 130;
247
+ if (outage) {
248
+ ctx.stderr(`events: the cloud event service was unreachable for ${outage.failures} attempts over ${Math.round((ctx.now().getTime() - outage.sinceMs) / 1_000)} s; this is an outage, not a timeout${resume(result.cursor)}`);
249
+ return 4;
250
+ }
251
+ return 1;
252
+ case "refused":
253
+ throw new CliError(`events: ${result.source === "filter" ? "the filter was refused" : `the cloud refused the wait${result.status === null ? "" : ` (${result.status})`}`}${result.key ? ` on --${result.key}${result.value === null ? "" : ` ${result.value}`}` : ""}: ${result.reason}`, "events-refused");
254
+ default:
255
+ // The remaining outcome is "failed"; a default keeps every path ending in a return or throw.
256
+ if (result.code === "cache-missing" || result.code === "cache-writer-stopped")
257
+ throw new CliError(`events: ${result.reason}. Start the local writer with: catalyst replica start --detach, or drop --from-cache to read from the cloud`, "events-absent", 3);
258
+ throw new CliError(`events: the wait failed (${result.code}): ${result.reason}${resume(result.resumeFrom ?? result.cursor)}`, "events-failed");
259
+ }
260
+ }
261
+ finally {
262
+ removeInterrupt();
263
+ deps.signal?.removeEventListener("abort", abort);
264
+ }
265
+ }
121
266
  function eventFilter(args) {
122
267
  const type = flagString(args, "type");
123
268
  const ticket = flagString(args, "ticket")?.toUpperCase();
package/dist/hosts.js CHANGED
@@ -8,7 +8,7 @@ async function confirmRemove(name) {
8
8
  return confirmYesNo(`Remove ${name} from this account? It will stop receiving work. [y/N] `);
9
9
  }
10
10
  export function machineLines(inventory) {
11
- const lines = inventory.machines.map((m) => `${m.name} | ${m.ownership === "self_hosted" ? "Self-hosted" : "Catalyst's"} | ${m.teams.join(", ")} | ${m.slots} slots, ${m.inUse} in use, ${m.free} free | ${m.status} | last check-in ${m.lastCheckInAtMs === null ? "never" : new Date(m.lastCheckInAtMs).toISOString()}`);
11
+ const lines = inventory.machines.map((m) => `${m.name} | ${m.ownership === "self_hosted" ? "Self-hosted" : "Catalyst's"} | ${m.teams.join(", ")} | ${m.slots} slots, ${m.inUse} in use, ${m.free} free | ${m.stopped === true ? "stopped" : m.status} | last check-in ${m.lastCheckInAtMs === null ? "never" : new Date(m.lastCheckInAtMs).toISOString()}`);
12
12
  if (lines.length === 0)
13
13
  lines.push("No machines are enrolled for this account.");
14
14
  const c = inventory.capacity;