@catalyst-cloud/catalyst-skills 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +109 -53
  3. package/dist/args.js +237 -0
  4. package/dist/ask.js +137 -0
  5. package/dist/cli.js +165 -279
  6. package/dist/config.js +106 -0
  7. package/dist/contract-types.js +4 -0
  8. package/dist/contract.js +178 -0
  9. package/dist/detach.js +10 -0
  10. package/dist/errors.js +27 -0
  11. package/dist/execution.js +156 -0
  12. package/dist/http.js +142 -0
  13. package/dist/prompt.js +35 -0
  14. package/dist/query.js +187 -0
  15. package/dist/ready.js +123 -0
  16. package/dist/replica.js +318 -0
  17. package/dist/sdk.js +26 -0
  18. package/dist/skill-shape.js +179 -0
  19. package/dist/skills.js +74 -0
  20. package/dist/ts-deps-loader.js +68 -0
  21. package/dist/watch/consumer.js +141 -0
  22. package/dist/watch/cursor-file.js +62 -0
  23. package/dist/watch.js +173 -0
  24. package/dist/write.js +224 -0
  25. package/package.json +9 -5
  26. package/skills/catalyst-github/SKILL.md +35 -0
  27. package/skills/catalyst-github/agents/openai.yaml +6 -0
  28. package/skills/catalyst-github/agents/portability.yaml +4 -0
  29. package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
  30. package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
  31. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
  32. package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
  33. package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
  34. package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
  35. package/skills/catalyst-linear/SKILL.md +43 -0
  36. package/skills/catalyst-linear/agents/openai.yaml +6 -0
  37. package/skills/catalyst-linear/agents/portability.yaml +5 -0
  38. package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
  39. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
  40. package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
  41. package/skills/catalyst-linear/scripts/comment.mjs +59 -0
  42. package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
  43. package/skills/catalyst-linear/scripts/label.mjs +48 -0
  44. package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
  45. package/skills/catalyst-linear/scripts/move.mjs +41 -0
  46. package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
  47. package/skills/catalyst-linear/scripts/search.mjs +49 -0
  48. package/skills/catalyst-setup/SKILL.md +32 -0
  49. package/skills/catalyst-setup/agents/openai.yaml +6 -0
  50. package/skills/catalyst-setup/agents/portability.yaml +4 -0
  51. package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
  52. package/skills/catalyst-setup/scripts/check.mjs +75 -0
  53. package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
  54. package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
  55. package/skills/connect-me/SKILL.md +55 -0
  56. package/skills/connect-me/agents/openai.yaml +6 -0
  57. package/skills/connect-me/agents/portability.yaml +5 -0
  58. package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
  59. package/skills/connect-me/scripts/lib/cli.mjs +184 -0
  60. package/skills/connect-me/scripts/verify-connection.mjs +68 -0
  61. package/skills/how-catalyst-works/SKILL.md +42 -0
  62. package/skills/how-catalyst-works/agents/openai.yaml +6 -0
  63. package/skills/how-catalyst-works/agents/portability.yaml +4 -0
  64. package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
  65. package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
  66. package/skills/how-catalyst-works/references/the-ladder.md +41 -0
  67. package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
  68. package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
  69. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
  70. package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
  71. package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
  72. package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
  73. package/skills/run-this-project/SKILL.md +45 -0
  74. package/skills/run-this-project/agents/openai.yaml +6 -0
  75. package/skills/run-this-project/agents/portability.yaml +5 -0
  76. package/skills/run-this-project/assets/stall-policy.json +15 -0
  77. package/skills/run-this-project/references/making-work-ready.md +60 -0
  78. package/skills/run-this-project/references/reacting-to-events.md +76 -0
  79. package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
  80. package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
  81. package/skills/run-this-project/scripts/make-ready.mjs +64 -0
  82. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  83. package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
  84. package/skills/what-needs-me/SKILL.md +41 -0
  85. package/skills/what-needs-me/agents/openai.yaml +6 -0
  86. package/skills/what-needs-me/agents/portability.yaml +5 -0
  87. package/skills/what-needs-me/references/raising-a-decision.md +41 -0
  88. package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
  89. package/skills/what-needs-me/references/settling-an-answer.md +37 -0
  90. package/skills/what-needs-me/scripts/inbox.mjs +47 -0
  91. package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
  92. package/skills/what-needs-me/scripts/raise.mjs +53 -0
  93. package/skills/what-needs-me/scripts/settle.mjs +73 -0
  94. package/skills/whats-happening/SKILL.md +43 -0
  95. package/skills/whats-happening/agents/openai.yaml +6 -0
  96. package/skills/whats-happening/agents/portability.yaml +4 -0
  97. package/skills/whats-happening/assets/status-reply.json +77 -0
  98. package/skills/whats-happening/references/reading-the-board.md +43 -0
  99. package/skills/whats-happening/references/reprioritising.md +37 -0
  100. package/skills/whats-happening/references/routing-work.md +36 -0
  101. package/skills/whats-happening/references/status-reply.md +34 -0
  102. package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
  103. package/skills/whats-happening/scripts/explain.mjs +28 -0
  104. package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
  105. package/skills/whats-happening/scripts/snapshot.mjs +145 -0
  106. package/skills/ask/SKILL.md +0 -37
  107. package/skills/concierge/SKILL.md +0 -41
  108. package/skills/join/SKILL.md +0 -30
  109. package/skills/linearis/SKILL.md +0 -40
  110. package/skills/setup/SKILL.md +0 -27
  111. package/skills/steward/SKILL.md +0 -30
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ Eight skills named from the executive's seat (whats-happening, what-needs-me, run-this-project, catalyst-setup, catalyst-linear, catalyst-github, how-catalyst-works, connect-me) replace the six; reads and the live event stream go through the Catalyst Cloud SDK and writes through the tenant's agent proxy, all behind catalyst-skills verbs (me, contract, query, replica, explain, running, queue, watch, write, ask, ready, accounts), with every stage id, label id, threshold and route read from the tenant contract; Node 22 is now required; installing the skills has moved to your agent's own command (the Claude Code plugin marketplace in this repository, or npx skills add for Codex, Cursor, OpenCode and the rest) and the CLI no longer copies them, leaving install only as a repair path; catalyst-skills login replaces join, which stays as a deprecated alias for one minor version, and login now prompts for the key without echoing it when no CATALYST_CLOUD_TOKEN is set; every SKILL.md declares allowed-tools scoped to this package's binary, so no skill needs a blanket shell grant; your existing customer.json is still read unchanged and gains the CLI path the next time you run catalyst-skills login, which also caches the tenant contract; run catalyst-skills ready to confirm.
6
+
3
7
  ## 0.1.1
4
8
 
5
9
  The bundle now lives and publishes from its own public repository, coalesce-labs/catalyst-cloud-skills, so every skill can be read before it is installed; the README leads with the CATALYST_CLOUD_TOKEN form of join, and join's output no longer names an internal ticket.
package/README.md CHANGED
@@ -1,111 +1,167 @@
1
1
  # @catalyst-cloud/catalyst-skills
2
2
 
3
- The customer skill bundle for Catalyst Cloud (https://catalystcloud.dev). One command installs six Claude Code skills — `concierge`, `steward`, `ask`, `linearis`, `join`, and `setup` — into `~/.claude/skills` and joins your machine to your own tenant. It is for catalyst-cloud customers: every skill is plain Markdown you can read in this repository before you install it, the installer is this npm package, and the account key from your tenant admin is the only credential.
3
+ [![skills.sh](https://skills.sh/b/coalesce-labs/catalyst-cloud-skills)](https://skills.sh/coalesce-labs/catalyst-cloud-skills)
4
4
 
5
- ## Read the skills first
5
+ ## Install
6
6
 
7
- Every skill is a plain Markdown file under `skills/<name>/SKILL.md` in this repository. Read them before you install anything:
7
+ Two ways in. The plugin installs the set as a managed bundle that updates when we ship. `npx skills` copies editable files into your project. Pick one; installing both leaves you with every skill twice.
8
8
 
9
- - [`skills/ask/SKILL.md`](skills/ask/SKILL.md)
10
- - [`skills/concierge/SKILL.md`](skills/concierge/SKILL.md)
11
- - [`skills/join/SKILL.md`](skills/join/SKILL.md)
12
- - [`skills/linearis/SKILL.md`](skills/linearis/SKILL.md)
13
- - [`skills/setup/SKILL.md`](skills/setup/SKILL.md)
14
- - [`skills/steward/SKILL.md`](skills/steward/SKILL.md)
9
+ <details><summary><strong>Claude Code</strong></summary>
15
10
 
16
- ## Requirements
11
+ ```
12
+ /plugin marketplace add coalesce-labs/catalyst-cloud-skills
13
+ /plugin install catalyst@catalyst-cloud
14
+ ```
15
+ </details>
17
16
 
18
- - Claude Code 2.0 or newer, which discovers skills in `~/.claude/skills`.
19
- - Node 18.17 or newer (20 or newer recommended), which supplies the `npx` used below.
20
- - Bun 1.0 or newer, optional, only if you prefer `bunx` over `npx`.
21
- - An account key from your tenant admin. The key is the only tenant selector: you never type a tenant or account id.
17
+ <details><summary><strong>Codex</strong></summary>
18
+
19
+ Run this command in your project:
20
+
21
+ ```sh
22
+ npx skills@latest add coalesce-labs/catalyst-cloud-skills -a codex
23
+ ```
24
+ </details>
22
25
 
23
- ## Install and join
26
+ <details><summary><strong>Cursor</strong></summary>
24
27
 
25
- The recommended form passes the key as an environment variable, because a key typed into a command line lands in your shell history:
28
+ Run this command in your project:
26
29
 
27
30
  ```sh
28
- CATALYST_CLOUD_TOKEN=<your-account-key> npx @catalyst-cloud/catalyst-skills join
31
+ npx skills@latest add coalesce-labs/catalyst-cloud-skills -a cursor
29
32
  ```
33
+ </details>
30
34
 
31
- If you accept the shell-history trade-off, the `--key` form is equivalent:
35
+ <details><summary><strong>OpenCode, Amp, Windsurf and the rest</strong></summary>
36
+
37
+ Run this command in your project:
32
38
 
33
39
  ```sh
34
- npx @catalyst-cloud/catalyst-skills join --key <your-account-key>
40
+ npx skills@latest add coalesce-labs/catalyst-cloud-skills
35
41
  ```
36
42
 
37
- Or install the package globally once and run the command directly:
43
+ The installer asks which skills to take and which agents to install them on. Add `-g` to install into your home directory instead of the project. Skills installed this way do not auto-update; run `npx skills update -y` to refresh them.
44
+ </details>
45
+
46
+ ### Then connect to your tenant
47
+
48
+ The skills call one CLI, and the CLI holds your credential. Install it once and connect this machine with your account key:
38
49
 
39
50
  ```sh
40
51
  npm install -g @catalyst-cloud/catalyst-skills
41
- CATALYST_CLOUD_TOKEN=<your-account-key> catalyst-skills join
52
+ CATALYST_CLOUD_TOKEN=<your-account-key> catalyst-skills login
53
+ catalyst-skills ready
42
54
  ```
43
55
 
44
- `bunx @catalyst-cloud/catalyst-skills join` works in place of the `npx` form. A non-default cloud is set with `CATALYST_CLOUD_BASE_URL` or `--base-url <url>`.
56
+ Passing the key as an environment variable is the recommended form, because a key typed into a command line lands in your shell history. With no key in the environment and a terminal attached, `catalyst-skills login` prompts for it without echoing it; `--key <your-account-key>` is the third form, for a script. `npx @catalyst-cloud/catalyst-skills login` works without the global install, and `bunx` works in place of `npx`. A non-default cloud is set with `CATALYST_CLOUD_BASE_URL` or `--base-url <url>`.
57
+
58
+ That is the whole setup. Everything below explains what you just installed.
59
+
60
+ ## What this is
61
+
62
+ Eight skills that let your coding agent run your own Catalyst Cloud tenant (https://catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your tenant through the Catalyst Cloud SDK, write to it through the tenant's agent proxy, and never compose a URL or run a tool of their own; every read, write and subscription is a `catalyst-skills` verb with `--help`. Every skill is plain Markdown under `skills/<name>/SKILL.md` in this repository, and the account key from your tenant admin is the only credential.
63
+
64
+ ## What connecting does
45
65
 
46
- `join` does all of this in one run:
66
+ `login` does three things: it calls `GET /api/v1/me` with your key, which discovers your tenant from the key alone; it writes `~/.config/catalyst-cloud/customer.json` with file mode `0600`, holding your key and the absolute path of the CLI so every skill script can spawn the same binary; and it fetches the tenant contract (`GET /api/v1/agent/contract`) and caches it at `~/.config/catalyst-cloud/contract.json` with its ETag, so stage names and ids, label ids, the ask template, thresholds and the route table come from your tenant, live, and no skill restates them.
47
67
 
48
- 1. Installs the six skills into your skills directory, `~/.claude/skills` by default (`--skills-dir <dir>` overrides it).
49
- 2. Calls `GET /api/v1/me` on the cloud with your key, which discovers your tenant from the key alone.
50
- 3. Writes `~/.config/catalyst-cloud/customer.json` with file mode `0600`; that file holds your key.
51
- 4. Prints what it did: the tenant it joined (`Joined <name> (<slug>) — account <account>`), the config path and mode, the skills installed, and the tenant contract range the package pins.
68
+ `login` does not install skills; the install command above did that. Re-running `login` after a key rotation rewrites the config. `catalyst-skills status` prints which tenant this machine is connected to, `catalyst-skills ready` prints one READY or NOT READY verdict with the fix for each failure and who can apply it, and `catalyst-skills --version` prints the package version and its pinned tenant contract range.
52
69
 
53
- Re-running `join` after a key rotation rewrites the config. `catalyst-skills status` prints which tenant this machine is joined to; `catalyst-skills --version` prints the package version and its pinned tenant contract range.
70
+ The setup skill Catalyst seeds into your repository ends by pointing at this same command. That served skill lives in the Catalyst Cloud application, not here; this is the only connect step a customer runs, and the credential is called the account key in both places.
71
+
72
+ ## Requirements
73
+
74
+ - Node 22 or newer. The bundle uses Node's built-in SQLite module for the optional local replica, so there is no native dependency to build; if `better-sqlite3` resolves on the machine it is used instead. `catalyst-skills ready` names the exact reason when an older Node is found.
75
+ - An account key from your tenant admin. The key is the only tenant selector: you never type a tenant or account id.
76
+ - An agent that discovers skills. Claude Code loads the plugin; Codex, Cursor, OpenCode and the rest read the `skills/<name>/SKILL.md` files the `npx skills` installer writes.
77
+ - Bun is optional, only if you prefer `bunx` over `npx`.
54
78
 
55
79
  ## First use
56
80
 
57
- Open a new Claude Code session and talk to the concierge about your own tenant — it reads the config `join` wrote. For example:
81
+ Open a new session and ask about your own tenant. The skills read the config `login` wrote. For example:
82
+
83
+ - What's happening? Where are we, why is that stuck, what closed, what's next?
84
+ - What needs me?
85
+ - Run this project for me until it closes.
86
+ - Am I set up?
87
+
88
+ ## What has to be running
89
+
90
+ 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:
91
+
92
+ | process | needed for | what it holds on disk | lifetime |
93
+ | --- | --- | --- | --- |
94
+ | none | every read, write, ask and explain | `customer.json` and `contract.json` | the default after login |
95
+ | `catalyst-skills replica start` | local SQL, cheap repeated reads, `replica sql` and `replica schema` | one SQLite file (`~/.config/catalyst-cloud/replica.db`), a writer lock with a heartbeat beside it, and a cursor row inside the database | long-running; foreground by default, `--detach` writes a pidfile beside the database and returns; `login --start-replica` does the same at the end of login |
96
+ | `catalyst-skills watch` | a project owner reacting to its scope | one cursor file (`~/.config/catalyst-cloud/watch-cursor.json`) stamped with the tenant | lives inside the session that armed it; exits with it |
58
97
 
59
- - Which tenant is this machine joined to, and who am I on it?
60
- - Give me a status summary of my projects this cycle.
98
+ The check every skill runs first is `catalyst-skills replica status`, which needs no network: is the pidfile's process alive, is the writer-lock heartbeat younger than the staleness threshold, and is the cursor non-empty. It exits `0` for fresh, `1` for present but stale, `2` for not connected, `3` for absent, and prints one line either way (`--json` for scripts). A fresh replica is used; anything else falls back to the API and the skill says so in its answer. A skill never refuses to work because the replica is down and never silently reads a stale one. `replica status --probe` compares the local cursor against the cloud's head for the honest "how far behind" number; that is the only form that touches the network. `catalyst-skills replica stop` stops a detached writer.
99
+
100
+ Nothing rotates. The replica is upserts and deletes into one file, the cursor is a row, and the watch cursor is a few bytes; there is no directory of old files to clean. It is a Node process, not a service: the supported path is the plain command, and `skills/connect-me/references/keeping-the-replica-running.md` gives launchd and systemd examples for people who want the writer to survive a reboot.
61
101
 
62
102
  ## The skills
63
103
 
64
- | skill | purpose | file |
65
- | --- | --- | --- |
66
- | `concierge` | The one agent a human talks to on their tenant: status summaries, routing work to stewards, filing new work, and questions their tenant data answers. | [`SKILL.md`](skills/concierge/SKILL.md) |
67
- | `steward` | The long-running owner of one initiative or project: makes work ready and visible, watches tickets until they close, and never writes product code itself. | [`SKILL.md`](skills/steward/SKILL.md) |
68
- | `ask` | Records a decision request when active work is gated on a decision or action only the human can make. | [`SKILL.md`](skills/ask/SKILL.md) |
69
- | `linearis` | Reads and writes Linear tickets for your tenant through its catalyst-cloud mirror, with freshness-gated reads. | [`SKILL.md`](skills/linearis/SKILL.md) |
70
- | `join` | Joins this machine to your tenant in one step: installs the bundle and discovers the tenant from the account key. | [`SKILL.md`](skills/join/SKILL.md) |
71
- | `setup` | Runs readiness checks before first real use of the bundle, and confirms everything lines up right after `join`. | [`SKILL.md`](skills/setup/SKILL.md) |
104
+ | skill | what the person says | what it does | file |
105
+ | --- | --- | --- | --- |
106
+ | `whats-happening` | "What's happening? Where are we? Why is that stuck? What's next?" | The desk for a tenant: reads the contract, what is running and queued, the eligibility explainer and the open asks, and answers in one reply with ticket ids. Routes work to a project owner and decisions to `what-needs-me`. | [`SKILL.md`](skills/whats-happening/SKILL.md) |
107
+ | `what-needs-me` | "What needs me? What am I blocking?" | The human's decision inbox, ranked by what each answer releases, and the one way an agent raises a decision on their behalf: files an ask through the cloud's ask route with the tenant's own template and records the answer so the held work releases. | [`SKILL.md`](skills/what-needs-me/SKILL.md) |
108
+ | `run-this-project` | "Run this project for me. Own it until it closes." | Single-threaded owner of one project: subscribes to the tenant stream for its scope, reacts to each change in the same turn, makes tickets ready and moves them to dispatch, parks what should stop, chases stalls, escalates inward, and keeps one status summary current. Never polls. | [`SKILL.md`](skills/run-this-project/SKILL.md) |
109
+ | `catalyst-setup` | "Am I set up? What is missing?" | Machine readiness plus tenant readiness from the contract's per-team checks, in one verdict: what passes, what is blocked, what is merely waiting, and who can click what. Reports; never repairs. | [`SKILL.md`](skills/catalyst-setup/SKILL.md) |
110
+ | `catalyst-linear` | "Show me the ticket, the history, what Catalyst wrote on it." | Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the replica when fresh and the API otherwise, always naming the source; writes comments, card moves, labels and new tickets as the app actor; knows what a ticket accumulates as Catalyst works it. | [`SKILL.md`](skills/catalyst-linear/SKILL.md) |
111
+ | `catalyst-github` | "Show me the PR, the checks, the review, the queue." | A ticket's pull request with its checks, reviews and review threads; whether it is mergeable under the repository's policy; what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). | [`SKILL.md`](skills/catalyst-github/SKILL.md) |
112
+ | `how-catalyst-works` | "How does this work? Why did it do that? How does it prioritise?" | The execution model as references loaded on demand: the eight-phase ladder, the eleven board slots and this team's live stage map, what happens when a phase fails, how the queue is ordered and routed, every exclusion reason, and the coding-account model. Scripts explain one ticket's eligibility in plain English. | [`SKILL.md`](skills/how-catalyst-works/SKILL.md) |
113
+ | `connect-me` | "Connect this machine to my tenant." | Connects the machine to the tenant with the account key, caches the tenant contract, verifies, and offers to start the replica. | [`SKILL.md`](skills/connect-me/SKILL.md) |
114
+
115
+ Four skills only read (`whats-happening`, `catalyst-setup`, `catalyst-github`, `how-catalyst-works`). The four that write anything (`catalyst-linear`, `what-needs-me`, `run-this-project`, `connect-me`) are marked so an agent cannot invoke them on its own; the person asks for them. Every skill declares `allowed-tools` scoped to this package's own binary, so none of them needs a blanket shell grant.
116
+
117
+ ## What a key cannot see yet
118
+
119
+ Two facts have no tenant-facing route today, and the skills say so by name rather than guess:
120
+
121
+ - Coding-account status (provider, declared and observed state, window usage, walls, quarantine). `catalyst-skills accounts` prints "not visible to an account key yet" and points at `<your cloud>/settings/coding-accounts`, where the tenant's settings page shows it.
122
+ - Per-ticket execution history (phase attempts, remediation rounds, park state). `catalyst-skills explain --history <ticket>` prints the same kind of line and points at `<your cloud>/settings`. What a key can see is the eligibility explainer, the dispatch queue, fleet activity, agent sessions and lease attributions, which is what `explain`, `running` and `queue` read.
72
123
 
73
124
  ## Versions and origins
74
125
 
75
- The package pins the tenant contract range `0.x`, recorded in `package.json` under `catalystCloud.tenantContractRange`, and reports it in `--version`, `status`, and `join`. The four vendored skills (`concierge`, `steward`, `ask`, `linearis`) carry a `vendored-from:` line naming their origin, the catalyst repository's catalyst-dev plugin; `join` and `setup` are written in this repository.
126
+ The package pins the tenant contract range `1.x`, recorded in `package.json` under `catalystCloud.tenantContractRange`, and reports it in `--version`, `status` and `login`. A tenant whose contract version falls outside that range is refused with one line naming both versions; update the bundle. Every skill carries a `vendored-from:` line naming this package as its origin; all eight are written in this repository for customer tenants.
76
127
 
77
128
  ## What it writes on your machine
78
129
 
79
- - Six skill directories under `~/.claude/skills/`: `ask`, `concierge`, `join`, `linearis`, `setup`, `steward`.
80
- - One config file, `~/.config/catalyst-cloud/customer.json`, written with mode `0600`.
130
+ - `~/.config/catalyst-cloud/customer.json`, written with mode `0600`, holding your account key and the CLI path.
131
+ - `~/.config/catalyst-cloud/contract.json`, the cached tenant contract.
132
+ - Only if you start them: `~/.config/catalyst-cloud/replica.db` with its `.pid` and `.writer.lock` sidecars, and `~/.config/catalyst-cloud/watch-cursor.json`.
81
133
 
82
- Nothing else. Your account key goes into that one config file and nowhere else.
134
+ The skill files themselves are written by whichever install command you ran, in that tool's own location. Your account key goes into that one config file and nowhere else.
83
135
 
84
136
  ## Updating
85
137
 
86
- Update the package with `npm update -g @catalyst-cloud/catalyst-skills`, or run any command against the latest publish with `npx @catalyst-cloud/catalyst-skills@latest join`. The next `catalyst-skills` run prints a one-line notice:
138
+ A plugin install updates when we ship. Skills copied by `npx skills add` do not; run `npx skills update -y` to refresh them. Update the CLI with `npm update -g @catalyst-cloud/catalyst-skills`, or run any command against the latest publish with `npx @catalyst-cloud/catalyst-skills@latest login`. The next `catalyst-skills` run prints a one-line notice:
87
139
 
88
140
  ```
89
- [catalyst-skills] updated 0.1.0 → 0.2.0: <that version's CHANGELOG.md summary> · update with: npm update -g @catalyst-cloud/catalyst-skills (or: npx @catalyst-cloud/catalyst-skills@latest join)
141
+ [catalyst-skills] updated 0.1.1 → 0.2.0: <that version's CHANGELOG.md summary> · update with: npm update -g @catalyst-cloud/catalyst-skills (or: npx @catalyst-cloud/catalyst-skills@latest login)
90
142
  ```
91
143
 
92
- The same run refreshes the installed skill copies to the new bundle before it records the new version, so a published skill fix reaches your machine. A skill directory you hand-edited is left alone and named in the output; `catalyst-skills install --force` replaces it. If the refresh fails, the old version stays recorded and the next command tries again and tells you to run `catalyst-skills install`.
144
+ A `customer.json` written by an older bundle is still read unchanged; it gains the CLI path and the cached contract the next time you run `catalyst-skills login`.
93
145
 
94
146
  ## Uninstalling
95
147
 
148
+ Remove the skills the way you installed them: `/plugin uninstall catalyst@catalyst-cloud` in Claude Code, or delete the eight directories (`catalyst-github`, `catalyst-linear`, `catalyst-setup`, `connect-me`, `how-catalyst-works`, `run-this-project`, `what-needs-me`, `whats-happening`) from wherever `npx skills add` wrote them. Then remove what the CLI wrote:
149
+
96
150
  ```sh
97
- rm -rf ~/.claude/skills/ask ~/.claude/skills/concierge ~/.claude/skills/join ~/.claude/skills/linearis ~/.claude/skills/setup ~/.claude/skills/steward
98
- rm ~/.config/catalyst-cloud/customer.json
151
+ catalyst-skills replica stop
152
+ rm -f ~/.config/catalyst-cloud/customer.json ~/.config/catalyst-cloud/contract.json ~/.config/catalyst-cloud/watch-cursor.json
153
+ rm -f ~/.config/catalyst-cloud/replica.db ~/.config/catalyst-cloud/replica.db.pid ~/.config/catalyst-cloud/replica.db.writer.lock
154
+ npm uninstall -g @catalyst-cloud/catalyst-skills
99
155
  ```
100
156
 
101
- If you joined with `--skills-dir <dir>`, remove the six directories there instead. If you installed the package globally, also run `npm uninstall -g @catalyst-cloud/catalyst-skills`.
102
-
103
- ## If join fails
157
+ ## If login fails
104
158
 
105
- - `catalyst-skills: GET /me failed (401): credential not accepted — ask your tenant admin for a valid account key` — the key is stale or mistyped. Ask your tenant admin for a valid account key, then run `join` again.
159
+ - `catalyst-skills: GET /me failed (401): credential not accepted — ask your tenant admin for a valid account key` — the key is stale or mistyped. Ask your tenant admin for a valid account key, then run `login` again.
106
160
  - `catalyst-skills: GET /me failed (403): account-not-operational` — the tenant is suspended. This is an admin conversation on the tenant, not a local fix.
107
161
  - `catalyst-skills: could not reach <url>: <detail>` — the machine cannot reach the cloud. The URL is named in the message; check `CATALYST_CLOUD_BASE_URL` or `--base-url`.
162
+ - A line naming two contract versions after `Connected to` — the tenant serves a contract outside this bundle's `1.x` range. The config is written; update the bundle before using the other skills.
163
+ - `[catalyst-skills] GET /api/v1/agent/contract refused (403): the contract needs an account key` on stderr — the key is a workstation key, which connects but cannot read the contract or the machine-only routes. Ask your tenant admin for an account key for the full set.
108
164
 
109
165
  ## License
110
166
 
111
- MIT — see [LICENSE](LICENSE). How to contribute and how releases happen are described in [CONTRIBUTING.md](CONTRIBUTING.md).
167
+ MIT — see [LICENSE](LICENSE). How to contribute and how releases happen are described in [CONTRIBUTING.md](CONTRIBUTING.md). The install commands above are one canonical block kept in [`.agents/install-block.md`](.agents/install-block.md); change them there first.
package/dist/args.js ADDED
@@ -0,0 +1,237 @@
1
+ // args.ts — argument parsing for every verb. Positional 0 is the command, positional 1 the
2
+ // subcommand, the rest are positionals; flags are per-verb tables so an unknown flag is a usage
3
+ // error naming the verb, and every verb's --help is generated from its own table.
4
+ import { UsageError } from "./errors.js";
5
+ const GLOBAL_FLAGS = {
6
+ key: { value: true, help: "account key (or CATALYST_CLOUD_TOKEN)" },
7
+ "base-url": { value: true, help: "Catalyst Cloud origin (or CATALYST_CLOUD_BASE_URL)" },
8
+ "skills-dir": { value: true, help: "where the skills are copied (default ~/.claude/skills)" },
9
+ force: { value: false, help: "replace a skill directory this package did not install" },
10
+ json: { value: false, help: "machine-readable output" },
11
+ };
12
+ /** Per-verb flag tables. A verb absent here accepts only the global flags. */
13
+ export const FLAG_TABLES = {
14
+ login: {
15
+ "start-replica": { value: false, help: "run `replica start --detach` after connecting" },
16
+ },
17
+ install: {},
18
+ status: {},
19
+ notice: {},
20
+ me: {},
21
+ contract: {
22
+ refresh: { value: false, help: "ignore the cache age and revalidate now" },
23
+ path: { value: true, help: "print a sub-document, e.g. teams.0.stages" },
24
+ },
25
+ query: {
26
+ team: { value: true, help: "team key filter" },
27
+ project: { value: true, help: "project id filter" },
28
+ state: { value: true, help: "state name filter" },
29
+ limit: { value: true, help: "max rows (default 50)" },
30
+ since: { value: true, help: "changes: the cursor to read after" },
31
+ source: { value: true, help: "replica | api (default: replica when fresh, else api)" },
32
+ ticket: { value: true, help: "pulls: only PRs linked to this ticket" },
33
+ },
34
+ replica: {
35
+ detach: { value: false, help: "start: run the writer in the background and write a pidfile" },
36
+ db: { value: true, help: "replica file path (default from customer.json)" },
37
+ probe: { value: false, help: "status: also fetch the cloud head and print the lag" },
38
+ "stale-ms": { value: true, help: "status: heartbeat age that counts as stale (default 15000)" },
39
+ },
40
+ explain: {
41
+ history: { value: false, help: "per-ticket execution history (not visible to an account key yet)" },
42
+ },
43
+ running: {},
44
+ queue: {
45
+ team: { value: true, help: "team key" },
46
+ },
47
+ watch: {
48
+ team: { value: true, help: "scope: a team key" },
49
+ ticket: { value: true, repeat: true, help: "scope: a ticket identifier (repeatable)" },
50
+ project: { value: true, help: "scope: a project id" },
51
+ exec: { value: true, help: "run this shell command per frame with the frame on stdin" },
52
+ "cursor-file": { value: true, help: "cursor file path (default ~/.config/catalyst-cloud/watch-cursor.json)" },
53
+ from: { value: true, help: "cursor | head (default cursor)" },
54
+ },
55
+ write: {
56
+ body: { value: true, help: "comment: the body text" },
57
+ stdin: { value: false, help: "comment: read the body from stdin" },
58
+ parent: { value: true, help: "comment: reply under this comment id" },
59
+ bookkeeping: { value: false, help: "comment: prefix the contract's bookkeeping marker" },
60
+ "as-user": { value: false, help: "post with the personal identity instead of the app actor" },
61
+ slot: { value: true, help: "state: the workflow slot to move to" },
62
+ "state-id": { value: true, help: "state: an explicit Linear state id" },
63
+ "state-type": { value: true, help: "state: the first state of this type on the ticket's team (e.g. backlog)" },
64
+ add: { value: true, repeat: true, help: "label: label name or id to add (repeatable)" },
65
+ remove: { value: true, repeat: true, help: "label: label name or id to remove (repeatable)" },
66
+ team: { value: true, help: "create: team key" },
67
+ title: { value: true, help: "create/attachment/session: title" },
68
+ label: { value: true, repeat: true, help: "create: label name or id (repeatable)" },
69
+ priority: { value: true, help: "create: Linear priority 0-4" },
70
+ comment: { value: true, help: "reaction: react to this comment id instead of the ticket" },
71
+ emoji: { value: true, help: "reaction: the emoji" },
72
+ url: { value: true, help: "attachment/session: the URL" },
73
+ "plan-file": { value: true, help: "session: a JSON file holding the plan" },
74
+ activity: { value: true, help: "session: one activity line" },
75
+ },
76
+ ask: {
77
+ team: { value: true, help: "raise: team key" },
78
+ title: { value: true, help: "raise: the question" },
79
+ context: { value: true, help: "raise: context paragraph" },
80
+ option: { value: true, repeat: true, help: "raise: an option (repeatable)" },
81
+ default: { value: true, help: "raise: the default if silent" },
82
+ blocks: { value: true, repeat: true, help: "raise: a ticket this decision blocks (repeatable)" },
83
+ "nothing-to-block": { value: false, help: "raise: declare that nothing is blocked" },
84
+ "ask-key": { value: true, help: "raise: idempotency key" },
85
+ answer: { value: true, help: "accept: the answering comment id" },
86
+ role: { value: true, help: "accept: the role recording the answer" },
87
+ },
88
+ ready: {},
89
+ accounts: {},
90
+ };
91
+ export const VERB_USAGE = {
92
+ login: "login [--key <account-key>] [--base-url <url>] [--start-replica]",
93
+ install: "install [--skills-dir <dir>] [--force]",
94
+ status: "status",
95
+ notice: "notice",
96
+ me: "me [--json]",
97
+ contract: "contract [--refresh] [--path <a.b.c>] [--json]",
98
+ query: "query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <n>> [--team K] [--project P] [--state S] [--limit N] [--source replica|api] [--json]",
99
+ replica: "replica <start [--detach]|stop|status [--probe] [--json]|sql \"<select>\"|schema [table]> [--db <path>]",
100
+ explain: "explain <ticket> [--history] [--json]",
101
+ running: "running [--json]",
102
+ queue: "queue [--team K] [--json]",
103
+ watch: "watch [--team K] [--ticket T]... [--project P] [--exec CMD] [--cursor-file <path>] [--from cursor|head]",
104
+ write: "write <comment <ticket> --body|--stdin [--parent] [--bookkeeping] [--as-user] | state <ticket> --slot|--state-id|--state-type | label <ticket> --add... --remove... | create --team --title [--label] [--priority] | reaction <ticket>|--comment <id> --emoji <e> | attachment <ticket> --title --url | session <ticket> [--title] [--plan-file] [--activity]>",
105
+ ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] | accept <askTicket> --answer <commentId> --role <role> | list [--json]>",
106
+ ready: "ready [--json]",
107
+ accounts: "accounts",
108
+ };
109
+ export function parseArgs(argv) {
110
+ const out = {
111
+ command: null,
112
+ subcommand: null,
113
+ rest: [],
114
+ flags: {},
115
+ force: false,
116
+ help: false,
117
+ version: false,
118
+ json: false,
119
+ };
120
+ const positionals = [];
121
+ // The per-verb table is switched in the moment the command positional is seen, so a verb's own
122
+ // flags are accepted after it and a global flag is accepted anywhere.
123
+ let table = { ...GLOBAL_FLAGS };
124
+ for (let i = 0; i < argv.length; i++) {
125
+ const a = argv[i];
126
+ if (a === "-h" || a === "--help") {
127
+ out.help = true;
128
+ continue;
129
+ }
130
+ if (a === "-V" || a === "--version") {
131
+ out.version = true;
132
+ continue;
133
+ }
134
+ if (a === "--") {
135
+ positionals.push(...argv.slice(i + 1));
136
+ break;
137
+ }
138
+ if (a.startsWith("--")) {
139
+ let name = a.slice(2);
140
+ let inlineValue;
141
+ const eq = name.indexOf("=");
142
+ if (eq !== -1) {
143
+ inlineValue = name.slice(eq + 1);
144
+ name = name.slice(0, eq);
145
+ }
146
+ const spec = table[name];
147
+ if (!spec) {
148
+ throw new UsageError(out.command ? `unknown option for ${out.command}: --${name}` : `unknown option: --${name}`);
149
+ }
150
+ if (spec.value) {
151
+ const v = inlineValue ?? requireValue(argv, ++i, `--${name}`);
152
+ if (spec.repeat) {
153
+ const prev = out.flags[name];
154
+ out.flags[name] = Array.isArray(prev) ? [...prev, v] : [v];
155
+ }
156
+ else {
157
+ out.flags[name] = v;
158
+ }
159
+ }
160
+ else {
161
+ if (inlineValue !== undefined)
162
+ throw new UsageError(`--${name} takes no value`);
163
+ out.flags[name] = true;
164
+ }
165
+ continue;
166
+ }
167
+ if (a.startsWith("-") && a.length > 1)
168
+ throw new UsageError(`unknown option: ${a}`);
169
+ positionals.push(a);
170
+ if (positionals.length === 1) {
171
+ out.command = a;
172
+ table = { ...GLOBAL_FLAGS, ...(FLAG_TABLES[a] ?? {}) };
173
+ }
174
+ }
175
+ out.command = positionals[0] ?? null;
176
+ out.subcommand = positionals[1] ?? null;
177
+ out.rest = positionals.slice(2);
178
+ if (typeof out.flags.key === "string")
179
+ out.key = out.flags.key;
180
+ if (typeof out.flags["base-url"] === "string")
181
+ out.baseUrl = out.flags["base-url"];
182
+ if (typeof out.flags["skills-dir"] === "string")
183
+ out.skillsDir = out.flags["skills-dir"];
184
+ out.force = out.flags.force === true;
185
+ out.json = out.flags.json === true;
186
+ return out;
187
+ }
188
+ function requireValue(argv, i, flag) {
189
+ const v = argv[i];
190
+ if (v === undefined || v === "")
191
+ throw new UsageError(`${flag} requires a value`);
192
+ return v;
193
+ }
194
+ /** The positional arguments after the command: `[subcommand, ...rest]` with nulls dropped. */
195
+ export function positionals(args) {
196
+ return args.subcommand === null ? [] : [args.subcommand, ...args.rest];
197
+ }
198
+ export function flagString(args, name) {
199
+ const v = args.flags[name];
200
+ return typeof v === "string" ? v : undefined;
201
+ }
202
+ export function flagList(args, name) {
203
+ const v = args.flags[name];
204
+ if (Array.isArray(v))
205
+ return v;
206
+ return typeof v === "string" ? [v] : [];
207
+ }
208
+ export function flagBool(args, name) {
209
+ return args.flags[name] === true;
210
+ }
211
+ export function flagInt(args, name, fallback) {
212
+ const v = flagString(args, name);
213
+ if (v === undefined)
214
+ return fallback;
215
+ const n = Number(v);
216
+ if (!Number.isInteger(n))
217
+ throw new UsageError(`--${name} must be an integer`);
218
+ return n;
219
+ }
220
+ /** The help text for one verb: usage line, then every flag in its table and the global ones. */
221
+ export function verbHelp(verb) {
222
+ const lines = [`Usage: catalyst-skills ${VERB_USAGE[verb] ?? verb}`, ""];
223
+ const table = FLAG_TABLES[verb] ?? {};
224
+ const names = Object.keys(table);
225
+ if (names.length > 0) {
226
+ lines.push("Options:");
227
+ for (const name of names)
228
+ lines.push(` --${name}${table[name].value ? " <value>" : ""} ${table[name].help}`);
229
+ lines.push("");
230
+ }
231
+ lines.push("Global options:");
232
+ for (const name of Object.keys(GLOBAL_FLAGS)) {
233
+ lines.push(` --${name}${GLOBAL_FLAGS[name].value ? " <value>" : ""} ${GLOBAL_FLAGS[name].help}`);
234
+ }
235
+ lines.push(" -h, --help this text", " -V, --version the bundle version");
236
+ return lines.join("\n");
237
+ }
package/dist/ask.js ADDED
@@ -0,0 +1,137 @@
1
+ // ask.ts — `ask raise|accept|list`: raise a decision through the contract's ask route (the cloud
2
+ // renders the body from its own template, so this passes fields, never headings), record an answer
3
+ // through ask-accept, and list open asks ranked by the work each one holds.
4
+ import { flagBool, flagList, flagString, positionals } from "./args.js";
5
+ import { requireConfig } from "./config.js";
6
+ import { loadContract, teamByKey } from "./contract.js";
7
+ import { UsageError } from "./errors.js";
8
+ import { apiClient } from "./http.js";
9
+ import { rowsOf } from "./query.js";
10
+ import { fetchWorkflowStates, postAgent, resolveIssue } from "./write.js";
11
+ export async function cmdAsk(args, ctx) {
12
+ const [sub, ...rest] = positionals(args);
13
+ if (!sub)
14
+ throw new UsageError("ask needs a subcommand: raise | accept | list");
15
+ if (sub === "raise" && !flagBool(args, "nothing-to-block") && flagList(args, "blocks").length === 0) {
16
+ throw new UsageError("ask raise needs --blocks <ticket>... (what this decision holds) or --nothing-to-block");
17
+ }
18
+ const cfg = requireConfig(ctx);
19
+ const { doc } = await loadContract(ctx, cfg);
20
+ const api = apiClient(cfg, ctx);
21
+ switch (sub) {
22
+ case "raise":
23
+ return raise(args, ctx, doc, api);
24
+ case "accept":
25
+ return accept(args, ctx, doc, api, rest);
26
+ case "list":
27
+ return list(args, ctx, doc, api);
28
+ default:
29
+ throw new UsageError(`unknown ask subcommand: ${sub}`);
30
+ }
31
+ }
32
+ async function raise(args, ctx, doc, api) {
33
+ const teamKey = flagString(args, "team");
34
+ const title = flagString(args, "title");
35
+ if (!teamKey || !title)
36
+ throw new UsageError("ask raise needs --team <key> and --title <question>");
37
+ const team = teamByKey(doc, teamKey);
38
+ const options = flagList(args, "option");
39
+ if (options.length > doc.askTemplate.maxLetteredOptions) {
40
+ throw new UsageError(`ask raise takes at most ${doc.askTemplate.maxLetteredOptions} options`);
41
+ }
42
+ const blocks = [];
43
+ for (const t of flagList(args, "blocks"))
44
+ blocks.push((await resolveIssue(api, t)).id);
45
+ const body = { title, teamId: team.id };
46
+ if (flagString(args, "context"))
47
+ body.context = flagString(args, "context");
48
+ if (options.length)
49
+ body.options = options;
50
+ if (flagString(args, "default"))
51
+ body.defaultIfSilent = flagString(args, "default");
52
+ if (blocks.length)
53
+ body.blocks = blocks;
54
+ if (flagBool(args, "nothing-to-block"))
55
+ body.nothingToBlock = true;
56
+ if (flagString(args, "ask-key"))
57
+ body.askKey = flagString(args, "ask-key");
58
+ const result = await postAgent(api, doc, "ask", body);
59
+ ctx.stdout(args.json ? JSON.stringify(result) : `ask raised: ${String(result.identifier ?? result.id ?? "ok")}${blocks.length ? ` (blocks ${blocks.length})` : ""}`);
60
+ return 0;
61
+ }
62
+ async function accept(args, ctx, doc, api, rest) {
63
+ const askTicket = rest[0];
64
+ const answer = flagString(args, "answer");
65
+ const role = flagString(args, "role");
66
+ if (!askTicket || !answer || !role)
67
+ throw new UsageError("ask accept needs <askTicket> --answer <commentId> --role <role>");
68
+ const ask = await resolveIssue(api, askTicket);
69
+ const result = await postAgent(api, doc, "ask-accept", {
70
+ askIssueId: ask.id,
71
+ answerCommentId: answer,
72
+ acceptedByRole: role,
73
+ });
74
+ ctx.stdout(args.json ? JSON.stringify(result) : `ask ${ask.identifier}: answer ${answer} recorded by ${role}`);
75
+ return 0;
76
+ }
77
+ /** Rank open asks by the priority-weighted count of open tickets each one blocks. Priority 1 (urgent)
78
+ * weighs 4, 4 (low) weighs 1, 0 (none) weighs 1. */
79
+ export function rankAsks(issues, doc, openState) {
80
+ const askIds = new Set();
81
+ for (const team of doc.teams)
82
+ for (const l of team.labels.ask)
83
+ for (const id of [l.unscopedId, l.teamScopedId, l.preferredId])
84
+ if (id)
85
+ askIds.add(id);
86
+ const isAsk = (issue) => {
87
+ const labels = Array.isArray(issue.labels) ? issue.labels : [];
88
+ return labels.some((l) => (typeof l.id === "string" && askIds.has(l.id)) || (typeof l.name === "string" && (l.name === doc.vocabulary.askMarkerLabel || l.name.startsWith(doc.vocabulary.askLabelPrefix))));
89
+ };
90
+ const byIdentifier = new Map();
91
+ for (const i of issues)
92
+ if (typeof i.identifier === "string")
93
+ byIdentifier.set(i.identifier, i);
94
+ const weight = (issue) => {
95
+ const p = typeof issue?.priority === "number" ? issue.priority : 0;
96
+ return p >= 1 && p <= 4 ? 5 - p : 1;
97
+ };
98
+ const out = [];
99
+ for (const issue of issues) {
100
+ if (!isAsk(issue) || !openState(issue))
101
+ continue;
102
+ const relations = Array.isArray(issue.relations) ? issue.relations : [];
103
+ const blocked = relations
104
+ .filter((r) => r.type === "blocks" && r.issue_identifier === issue.identifier && typeof r.related_identifier === "string")
105
+ .map((r) => r.related_identifier)
106
+ .filter((id) => {
107
+ const t = byIdentifier.get(id);
108
+ return !t || openState(t);
109
+ });
110
+ out.push({
111
+ identifier: String(issue.identifier),
112
+ title: String(issue.title ?? ""),
113
+ state: String(issue.state ?? ""),
114
+ blocks: blocked,
115
+ score: blocked.reduce((sum, id) => sum + weight(byIdentifier.get(id)), 0),
116
+ });
117
+ }
118
+ return out.sort((a, b) => b.score - a.score || a.identifier.localeCompare(b.identifier));
119
+ }
120
+ async function list(args, ctx, doc, api) {
121
+ const [issuesRes, states] = await Promise.all([api.getJson("/api/v1/issues", { query: { limit: 500 } }), fetchWorkflowStates(api)]);
122
+ const issues = rowsOf(issuesRes.body);
123
+ const terminal = new Set(states.filter((s) => ["completed", "canceled", "cancelled"].includes(s.type.toLowerCase())).map((s) => s.name.toLowerCase()));
124
+ const openState = (issue) => !terminal.has(String(issue.state ?? "").toLowerCase());
125
+ const ranked = rankAsks(issues, doc, openState);
126
+ if (args.json) {
127
+ ctx.stdout(JSON.stringify(ranked));
128
+ return 0;
129
+ }
130
+ if (ranked.length === 0) {
131
+ ctx.stdout("no open asks");
132
+ return 0;
133
+ }
134
+ for (const a of ranked)
135
+ ctx.stdout(`${a.identifier} holds ${a.blocks.length} ticket${a.blocks.length === 1 ? "" : "s"} (weight ${a.score})${a.blocks.length ? `: ${a.blocks.join(", ")}` : ""} ${a.title}`);
136
+ return 0;
137
+ }