@catalyst-cloud/catalyst-skills 0.2.0 → 0.3.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 (48) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +39 -29
  3. package/bin/catalyst-skills.js +42 -2
  4. package/dist/args.js +14 -8
  5. package/dist/ask.js +26 -5
  6. package/dist/cli.js +20 -8
  7. package/dist/config.js +1 -1
  8. package/dist/contract.js +2 -2
  9. package/dist/execution.js +165 -13
  10. package/dist/http.js +55 -3
  11. package/dist/prompt.js +1 -1
  12. package/dist/query.js +42 -4
  13. package/dist/ready.js +2 -2
  14. package/package.json +4 -3
  15. package/skills/catalyst-github/SKILL.md +1 -1
  16. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  17. package/skills/catalyst-github/scripts/lib/cli.mjs +2 -2
  18. package/skills/catalyst-github/scripts/read-pr.mjs +1 -1
  19. package/skills/catalyst-linear/SKILL.md +1 -1
  20. package/skills/catalyst-linear/references/reading-a-ticket.md +2 -2
  21. package/skills/catalyst-linear/references/writing-to-linear.md +1 -1
  22. package/skills/catalyst-linear/scripts/lib/cli.mjs +1 -1
  23. package/skills/catalyst-setup/SKILL.md +3 -3
  24. package/skills/catalyst-setup/references/what-each-check-means.md +6 -5
  25. package/skills/catalyst-setup/scripts/lib/cli.mjs +2 -2
  26. package/skills/catalyst-setup/scripts/replica-status.mjs +1 -1
  27. package/skills/connect-me/SKILL.md +9 -9
  28. package/skills/connect-me/agents/openai.yaml +1 -1
  29. package/skills/connect-me/scripts/lib/cli.mjs +2 -2
  30. package/skills/how-catalyst-works/SKILL.md +2 -2
  31. package/skills/how-catalyst-works/references/coding-accounts.md +2 -2
  32. package/skills/how-catalyst-works/references/when-a-phase-fails.md +2 -2
  33. package/skills/how-catalyst-works/scripts/lib/cli.mjs +1 -1
  34. package/skills/how-catalyst-works/scripts/whats-running.mjs +1 -2
  35. package/skills/run-this-project/SKILL.md +2 -2
  36. package/skills/run-this-project/references/reacting-to-events.md +1 -1
  37. package/skills/run-this-project/references/stalls-and-escalation.md +1 -1
  38. package/skills/run-this-project/scripts/lib/cli.mjs +2 -2
  39. package/skills/what-needs-me/SKILL.md +2 -2
  40. package/skills/what-needs-me/references/reading-the-inbox.md +1 -1
  41. package/skills/what-needs-me/scripts/inbox.mjs +15 -6
  42. package/skills/what-needs-me/scripts/lib/cli.mjs +2 -2
  43. package/skills/whats-happening/SKILL.md +3 -3
  44. package/skills/whats-happening/assets/status-reply.json +1 -1
  45. package/skills/whats-happening/references/reading-the-board.md +1 -1
  46. package/skills/whats-happening/references/status-reply.md +1 -1
  47. package/skills/whats-happening/references/why-is-it-stuck.md +1 -1
  48. package/skills/whats-happening/scripts/lib/cli.mjs +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ You connect with your own personal key now, not the tenant's shared account key: mint it yourself at Settings → API keys in the Catalyst Cloud app (every member can; no admin needed), and `login` prints who you connected as beside which tenant. Your agent acts as you — an ask it raises names you, and "what needs me" means you: `ask list` (and the what-needs-me skill) keeps only the asks assigned to your Linear user by default, `--anyone` lists the whole tenant, and when your Linear identity is not matched yet it says so and shows everything rather than an empty list. `customer.json` gains a `user` block (id, label, role, your Linear user id); an older config is still read unchanged. Connecting with the account key still works — it is the right credential for a host or daemon — but `login` tells you it names no person. Every skill, reference and help line that said "account key", or said execution history and coding-account status were not readable, has been rewritten: `explain --history` and `accounts` read them, and the two things your key genuinely cannot do (release a park, read PR labels or reactions) are named instead. The catalyst-setup reference now lists all eleven team checks, including `environment_declared`. Needs a cloud that admits personal keys on the agent routes; on an older cloud the contract read says so by name, and the account key still works until it is updated.
6
+
7
+ ## 0.2.1
8
+
9
+ Fixes five defects found by running 0.2.0's verbs against a live tenant. Three skills (catalyst-github, catalyst-setup, how-catalyst-works) carried a colon-and-space inside an unquoted description, which YAML reads as a nested map rather than text, so `npx skills add` skipped them and still reported success — every Codex, Cursor and OpenCode install of 0.2.0 quietly got five of the eight skills, and reinstalling with this version gives you all eight. `running` no longer asks for lease attributions it has no coordinates for, so the headline "what's happening?" verb works instead of failing for every tenant; the lease read is still available as `running --ticket T --phase P`. The CLI no longer truncates its own output at 64KB when a skill reads it through a pipe, which made `query issues --json` on a tenant past roughly eighty tickets return invalid JSON with a success code. `accounts` now actually reads your coding-account slots and renders them, `history <ticket>` (also `explain --history`) shows a ticket's execution history, and both say plainly when your cloud is older than this bundle rather than printing an empty success. `queue` with no `--team` reads every team on your tenant instead of being refused, as its help always promised. `query changes` works at all now: it reads the feed's NDJSON, where before it called any successful response a non-JSON body — nothing noticed because the documented `--since 0` is refused on any tenant whose feed has rotated, so a success was never reached. That refusal now names a cursor you can actually use, and `--since head` starts from now.
10
+
3
11
  ## 0.2.0
4
12
 
5
13
  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.
package/README.md CHANGED
@@ -4,9 +4,25 @@
4
4
 
5
5
  ## Install
6
6
 
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.
7
+ Start by pasting one sentence into the coding agent you already use:
8
8
 
9
- <details><summary><strong>Claude Code</strong></summary>
9
+ ```
10
+ Help me understand and set up Catalyst Cloud. Read https://staging.catalystcloud.dev/agent-guide.md first, then walk me through it step by step.
11
+ ```
12
+
13
+ Your agent reads the guide, explains Catalyst Cloud in terms of your own repositories and tickets, finds out where it is running and what is already installed, and does the setup below itself — asking once before it writes to your machine. Everything else on this page is the reference it follows.
14
+
15
+ Or by hand. One command, for every coding agent on the machine:
16
+
17
+ ```sh
18
+ npx skills@latest add coalesce-labs/catalyst-cloud-skills --all
19
+ ```
20
+
21
+ It installs all eight skills for each agent it detects (Claude Code, Codex, Cursor, OpenCode and the rest). 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.
22
+
23
+ <details><summary><strong>Alternative for Claude Code: the plugin marketplace</strong></summary>
24
+
25
+ The plugin installs the set as a managed bundle that updates when we ship. It needs a GitHub SSH key, and it does not load into the session you are already in — run `/reload-plugins` or restart afterwards. Pick one rail; installing both leaves you with every skill twice.
10
26
 
11
27
  ```
12
28
  /plugin marketplace add coalesce-labs/catalyst-cloud-skills
@@ -14,65 +30,59 @@ Two ways in. The plugin installs the set as a managed bundle that updates when w
14
30
  ```
15
31
  </details>
16
32
 
17
- <details><summary><strong>Codex</strong></summary>
33
+ <details><summary><strong>One agent at a time</strong></summary>
18
34
 
19
- Run this command in your project:
35
+ Codex:
20
36
 
21
37
  ```sh
22
38
  npx skills@latest add coalesce-labs/catalyst-cloud-skills -a codex
23
39
  ```
24
- </details>
25
40
 
26
- <details><summary><strong>Cursor</strong></summary>
27
-
28
- Run this command in your project:
41
+ Cursor:
29
42
 
30
43
  ```sh
31
44
  npx skills@latest add coalesce-labs/catalyst-cloud-skills -a cursor
32
45
  ```
33
- </details>
34
-
35
- <details><summary><strong>OpenCode, Amp, Windsurf and the rest</strong></summary>
36
46
 
37
- Run this command in your project:
47
+ OpenCode, Amp, Windsurf and the rest:
38
48
 
39
49
  ```sh
40
50
  npx skills@latest add coalesce-labs/catalyst-cloud-skills
41
51
  ```
42
52
 
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.
53
+ Without `--all` the installer asks which skills to take and which agents to install them on.
44
54
  </details>
45
55
 
46
56
  ### Then connect to your tenant
47
57
 
48
- The skills call one CLI, and the CLI holds your credential. Install it once and connect this machine with your account key:
58
+ The skills call one CLI, and the CLI holds your credential. Install it once and connect this machine with your own **personal key** — mint it at Settings → API keys in the Catalyst Cloud app (every member can; no admin needed):
49
59
 
50
60
  ```sh
51
61
  npm install -g @catalyst-cloud/catalyst-skills
52
- CATALYST_CLOUD_TOKEN=<your-account-key> catalyst-skills login
62
+ CATALYST_CLOUD_TOKEN=<your-personal-key> catalyst-skills login
53
63
  catalyst-skills ready
54
64
  ```
55
65
 
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>`.
66
+ 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-personal-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
67
 
58
68
  That is the whole setup. Everything below explains what you just installed.
59
69
 
60
70
  ## What this is
61
71
 
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.
72
+ Eight skills that let your coding agent run your own Catalyst Cloud tenant (https://staging.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 your own personal key is the only credential. Your agent acts as you: the asks it raises and the comments it posts carry your name, and "what needs me" means you.
63
73
 
64
74
  ## What connecting does
65
75
 
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.
76
+ `login` does three things: it calls `GET /api/v1/me` with your key, which discovers your tenant and who you are from the key alone; it writes `~/.config/catalyst-cloud/customer.json` with file mode `0600`, holding your key, who you are (id, label, role, your Linear user id) 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.
67
77
 
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.
78
+ `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 and as whom, `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.
69
79
 
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.
80
+ 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. The tenant's **account key** (Settings → Account keys, admin-minted) is a different credential — for a host or daemon that runs unattended — and is not what a person connects their own agent with: it would strip your name from everything your agent writes, and `login` says so if you use one.
71
81
 
72
82
  ## Requirements
73
83
 
74
84
  - 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.
85
+ - Your personal key, minted by you at Settings → API keys. The key is the only tenant selector: you never type a tenant or account id. If your Linear identity is not matched yet, `login` says so; an admin matches it in Settings → Members, and until then "what needs me" shows everyone's asks.
76
86
  - 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
87
  - Bun is optional, only if you prefer `bunx` over `npx`.
78
88
 
@@ -110,16 +120,16 @@ Nothing rotates. The replica is upserts and deletes into one file, the cursor is
110
120
  | `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
121
  | `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
122
  | `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) |
123
+ | `connect-me` | "Connect this machine to my tenant." | Connects the machine to the tenant with your own personal key, caches the tenant contract, verifies, and offers to start the replica. | [`SKILL.md`](skills/connect-me/SKILL.md) |
114
124
 
115
125
  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
126
 
117
127
  ## What a key cannot see yet
118
128
 
119
- Two facts have no tenant-facing route today, and the skills say so by name rather than guess:
129
+ Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst-skills explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst-skills accounts`: provider, declared and observed state, window usage, walls, quarantine — never a credential; enrolling or pausing one is `<your cloud>/settings/coding-accounts`). Two things it cannot do, and the skills say so by name rather than guess:
120
130
 
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.
131
+ - Release a park. When a ticket is parked after repeated failures, an operator releases it; the skill names the park and points at `<your cloud>/settings`.
132
+ - Read pull-request labels or the reviewer's reaction. The mirror does not carry them; GitHub's own page does.
123
133
 
124
134
  ## Versions and origins
125
135
 
@@ -127,11 +137,11 @@ The package pins the tenant contract range `1.x`, recorded in `package.json` und
127
137
 
128
138
  ## What it writes on your machine
129
139
 
130
- - `~/.config/catalyst-cloud/customer.json`, written with mode `0600`, holding your account key and the CLI path.
140
+ - `~/.config/catalyst-cloud/customer.json`, written with mode `0600`, holding your personal key, who you are, and the CLI path.
131
141
  - `~/.config/catalyst-cloud/contract.json`, the cached tenant contract.
132
142
  - Only if you start them: `~/.config/catalyst-cloud/replica.db` with its `.pid` and `.writer.lock` sidecars, and `~/.config/catalyst-cloud/watch-cursor.json`.
133
143
 
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.
144
+ The skill files themselves are written by whichever install command you ran, in that tool's own location. Your personal key goes into that one config file and nowhere else.
135
145
 
136
146
  ## Updating
137
147
 
@@ -156,11 +166,11 @@ npm uninstall -g @catalyst-cloud/catalyst-skills
156
166
 
157
167
  ## If login fails
158
168
 
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.
169
+ - `catalyst-skills: GET /me failed (401): credential not accepted — mint a personal key at Settings → API keys and log in again` — the key is stale, mistyped or revoked. Mint a new one, then run `login` again.
160
170
  - `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.
161
171
  - `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
172
  - 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.
173
+ - `[catalyst-skills] GET /api/v1/agent/contract refused (403): this cloud is older than the bundle …` on stderr — the cloud has not yet deployed personal-key access to the contract. Update the cloud, or connect with the tenant's account key until it has.
164
174
 
165
175
  ## License
166
176
 
@@ -1,10 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
  // Thin ESM launcher for @catalyst-cloud/catalyst-skills (CTC-1926). tsc does not carry a shebang
3
3
  // through emit, so the bin is this two-line wrapper and the program lives in dist/cli.js.
4
+ //
5
+ // ⛔ NEVER `process.exit(code)` STRAIGHT AFTER A WRITE. When stdout is a PIPE — which is how every
6
+ // skill script spawns this CLI — writes are asynchronous, so `process.exit` severs whatever is
7
+ // still in flight AND STILL REPORTS THE ORIGINAL EXIT CODE. The caller then reads a truncated body
8
+ // with a success code and has no way to tell it from a complete one: `query issues --json` on a
9
+ // tenant with ~80+ tickets came back as 65,528 bytes of invalid JSON, exit 0 (0.2.0). Interactive
10
+ // use hid it, because a tty gives stdout a synchronous write path.
11
+ //
12
+ // So: park the code on `process.exitCode`, wait for both streams to drain, and only then exit. The
13
+ // explicit exit stays — `fetch`'s keep-alive sockets can hold the event loop open for seconds after
14
+ // the work is done, and a CLI that lingers is its own defect — but by then nothing is buffered.
4
15
  import("../dist/cli.js")
5
16
  .then((m) => m.main(process.argv.slice(2)))
6
- .then((code) => process.exit(code))
17
+ .then((code) => finish(code))
7
18
  .catch((err) => {
8
19
  console.error(`catalyst-skills: failed to load: ${err instanceof Error ? err.message : String(err)}`);
9
- process.exit(1);
20
+ finish(1);
10
21
  });
22
+
23
+ /**
24
+ * Resolve once every byte already handed to `stream` has reached the other end. The empty write is
25
+ * ordered behind the real ones, so its callback is the drain signal; a destroyed or errored stream
26
+ * (a reader that hung up) resolves immediately rather than hanging the process forever.
27
+ */
28
+ function drained(stream) {
29
+ return new Promise((resolve) => {
30
+ if (!stream || stream.destroyed || stream.writableEnded) return resolve();
31
+ let settled = false;
32
+ const done = () => {
33
+ if (settled) return;
34
+ settled = true;
35
+ resolve();
36
+ };
37
+ stream.once("error", done);
38
+ try {
39
+ stream.write("", done);
40
+ } catch {
41
+ done();
42
+ }
43
+ });
44
+ }
45
+
46
+ async function finish(code) {
47
+ process.exitCode = code;
48
+ await Promise.all([drained(process.stdout), drained(process.stderr)]);
49
+ process.exit(code);
50
+ }
package/dist/args.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // error naming the verb, and every verb's --help is generated from its own table.
4
4
  import { UsageError } from "./errors.js";
5
5
  const GLOBAL_FLAGS = {
6
- key: { value: true, help: "account key (or CATALYST_CLOUD_TOKEN)" },
6
+ key: { value: true, help: "your personal key (or CATALYST_CLOUD_TOKEN)" },
7
7
  "base-url": { value: true, help: "Catalyst Cloud origin (or CATALYST_CLOUD_BASE_URL)" },
8
8
  "skills-dir": { value: true, help: "where the skills are copied (default ~/.claude/skills)" },
9
9
  force: { value: false, help: "replace a skill directory this package did not install" },
@@ -38,11 +38,15 @@ export const FLAG_TABLES = {
38
38
  "stale-ms": { value: true, help: "status: heartbeat age that counts as stale (default 15000)" },
39
39
  },
40
40
  explain: {
41
- history: { value: false, help: "per-ticket execution history (not visible to an account key yet)" },
41
+ history: { value: false, help: "per-ticket execution history instead of the eligibility reason" },
42
+ },
43
+ history: {},
44
+ running: {
45
+ ticket: { value: true, help: "with --phase: also the lease attributions for this (ticket, phase)" },
46
+ phase: { value: true, help: "with --ticket: the phase whose lease attributions to read" },
42
47
  },
43
- running: {},
44
48
  queue: {
45
- team: { value: true, help: "team key" },
49
+ team: { value: true, help: "team key (default: every team the tenant contract names)" },
46
50
  },
47
51
  watch: {
48
52
  team: { value: true, help: "scope: a team key" },
@@ -81,6 +85,7 @@ export const FLAG_TABLES = {
81
85
  default: { value: true, help: "raise: the default if silent" },
82
86
  blocks: { value: true, repeat: true, help: "raise: a ticket this decision blocks (repeatable)" },
83
87
  "nothing-to-block": { value: false, help: "raise: declare that nothing is blocked" },
88
+ anyone: { value: false, help: "list: every open ask in the tenant, not only the ones assigned to you" },
84
89
  "ask-key": { value: true, help: "raise: idempotency key" },
85
90
  answer: { value: true, help: "accept: the answering comment id" },
86
91
  role: { value: true, help: "accept: the role recording the answer" },
@@ -95,16 +100,17 @@ export const VERB_USAGE = {
95
100
  notice: "notice",
96
101
  me: "me [--json]",
97
102
  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]",
103
+ query: "query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>> [--team K] [--project P] [--state S] [--limit N] [--source replica|api] [--json]",
99
104
  replica: "replica <start [--detach]|stop|status [--probe] [--json]|sql \"<select>\"|schema [table]> [--db <path>]",
100
105
  explain: "explain <ticket> [--history] [--json]",
101
- running: "running [--json]",
106
+ history: "history <ticket> [--json]",
107
+ running: "running [--ticket T --phase P] [--json]",
102
108
  queue: "queue [--team K] [--json]",
103
109
  watch: "watch [--team K] [--ticket T]... [--project P] [--exec CMD] [--cursor-file <path>] [--from cursor|head]",
104
110
  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]>",
111
+ ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] | accept <askTicket> --answer <commentId> --role <role> | list [--anyone] [--json]>",
106
112
  ready: "ready [--json]",
107
- accounts: "accounts",
113
+ accounts: "accounts [--json]",
108
114
  };
109
115
  export function parseArgs(argv) {
110
116
  const out = {
package/dist/ask.js CHANGED
@@ -24,7 +24,7 @@ export async function cmdAsk(args, ctx) {
24
24
  case "accept":
25
25
  return accept(args, ctx, doc, api, rest);
26
26
  case "list":
27
- return list(args, ctx, doc, api);
27
+ return list(args, ctx, doc, api, cfg);
28
28
  default:
29
29
  throw new UsageError(`unknown ask subcommand: ${sub}`);
30
30
  }
@@ -74,6 +74,18 @@ async function accept(args, ctx, doc, api, rest) {
74
74
  ctx.stdout(args.json ? JSON.stringify(result) : `ask ${ask.identifier}: answer ${answer} recorded by ${role}`);
75
75
  return 0;
76
76
  }
77
+ /** Decide the inbox scope from the connected identity and the `--anyone` flag. A personal key with a
78
+ * matched Linear identity reads "mine" by default; `--anyone` widens; an unmatched identity or a
79
+ * host key cannot mean "me" and says so rather than filtering to nothing. */
80
+ export function inboxScope(cfg, anyone) {
81
+ if (anyone)
82
+ return { kind: "anyone" };
83
+ if (!cfg.user)
84
+ return { kind: "no-person" };
85
+ if (!cfg.user.linearUserId)
86
+ return { kind: "unmatched", label: cfg.user.label };
87
+ return { kind: "mine", linearUserId: cfg.user.linearUserId, label: cfg.user.label };
88
+ }
77
89
  /** Rank open asks by the priority-weighted count of open tickets each one blocks. Priority 1 (urgent)
78
90
  * weighs 4, 4 (low) weighs 1, 0 (none) weighs 1. */
79
91
  export function rankAsks(issues, doc, openState) {
@@ -113,22 +125,31 @@ export function rankAsks(issues, doc, openState) {
113
125
  state: String(issue.state ?? ""),
114
126
  blocks: blocked,
115
127
  score: blocked.reduce((sum, id) => sum + weight(byIdentifier.get(id)), 0),
128
+ assigneeId: typeof issue.assignee_id === "string" && issue.assignee_id !== "" ? issue.assignee_id : null,
116
129
  });
117
130
  }
118
131
  return out.sort((a, b) => b.score - a.score || a.identifier.localeCompare(b.identifier));
119
132
  }
120
- async function list(args, ctx, doc, api) {
133
+ async function list(args, ctx, doc, api, cfg) {
121
134
  const [issuesRes, states] = await Promise.all([api.getJson("/api/v1/issues", { query: { limit: 500 } }), fetchWorkflowStates(api)]);
122
135
  const issues = rowsOf(issuesRes.body);
123
136
  const terminal = new Set(states.filter((s) => ["completed", "canceled", "cancelled"].includes(s.type.toLowerCase())).map((s) => s.name.toLowerCase()));
124
137
  const openState = (issue) => !terminal.has(String(issue.state ?? "").toLowerCase());
125
- const ranked = rankAsks(issues, doc, openState);
138
+ const all = rankAsks(issues, doc, openState);
139
+ const scope = inboxScope(cfg, flagBool(args, "anyone"));
140
+ const ranked = scope.kind === "mine" ? all.filter((a) => a.assigneeId === scope.linearUserId) : all;
126
141
  if (args.json) {
127
- ctx.stdout(JSON.stringify(ranked));
142
+ ctx.stdout(JSON.stringify({ scope, asks: ranked }));
128
143
  return 0;
129
144
  }
145
+ if (scope.kind === "unmatched") {
146
+ ctx.stderr(`[catalyst-skills] your Linear identity is not matched yet (an admin matches it in Settings → Members), so this is every open ask, not only yours`);
147
+ }
148
+ else if (scope.kind === "no-person") {
149
+ ctx.stderr(`[catalyst-skills] connected with the tenant's account key, which names no person — this is every open ask; log in with your personal key to see only yours`);
150
+ }
130
151
  if (ranked.length === 0) {
131
- ctx.stdout("no open asks");
152
+ ctx.stdout(scope.kind === "mine" ? `no open asks assigned to ${scope.label} (${all.length} open in the tenant — add --anyone to see them)` : "no open asks");
132
153
  return 0;
133
154
  }
134
155
  for (const a of ranked)
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { CONFIG_MODE, DEFAULT_BASE_URL, PACKAGE_NAME, cliPath, configPathFor, co
6
6
  import { loadContract, pickPath } from "./contract.js";
7
7
  import { CliError, MeError, UsageError } from "./errors.js";
8
8
  import { fetchMe } from "./http.js";
9
- import { cmdAccounts, cmdExplain, cmdQueue, cmdRunning } from "./execution.js";
9
+ import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning } from "./execution.js";
10
10
  import { cmdQuery } from "./query.js";
11
11
  import { cmdReady } from "./ready.js";
12
12
  import { cmdReplica } from "./replica.js";
@@ -37,14 +37,14 @@ export function usageText() {
37
37
  `${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your tenant, read through the SDK, write through the agent proxy`,
38
38
  "",
39
39
  "Usage:",
40
- " catalyst-skills login [--key <account-key>] [--base-url <url>] [--start-replica]",
40
+ " catalyst-skills login [--key <personal-key>] [--base-url <url>] [--start-replica]",
41
41
  " catalyst-skills join ... (deprecated alias of login; removed in the next minor version)",
42
42
  " catalyst-skills install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
43
43
  " catalyst-skills status | notice | me | ready | accounts",
44
44
  " catalyst-skills contract [--refresh] [--path <a.b.c>]",
45
- " catalyst-skills query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <n>>",
45
+ " catalyst-skills query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
46
46
  " catalyst-skills replica <start [--detach]|stop|status [--probe]|sql \"<select>\"|schema [table]>",
47
- " catalyst-skills explain <ticket> | running | queue [--team K]",
47
+ " catalyst-skills explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
48
48
  " catalyst-skills watch [--team K] [--ticket T]... [--project P] [--exec CMD]",
49
49
  " catalyst-skills write <comment|state|label|create|reaction|attachment|session> ...",
50
50
  " catalyst-skills ask <raise|accept|list> ...",
@@ -148,6 +148,8 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
148
148
  return await cmdReplica(args, ctx, deps.replica);
149
149
  case "explain":
150
150
  return await cmdExplain(args, ctx);
151
+ case "history":
152
+ return await cmdHistory(args, ctx);
151
153
  case "running":
152
154
  return await cmdRunning(args, ctx);
153
155
  case "queue":
@@ -161,7 +163,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
161
163
  case "ready":
162
164
  return await cmdReady(args, ctx, { skillNames: CUSTOMER_SKILLS, loadSdk: deps.loadSdk });
163
165
  case "accounts":
164
- return cmdAccounts(ctx);
166
+ return await cmdAccounts(args, ctx);
165
167
  default:
166
168
  ctx.stderr(`unknown command: ${args.command}`);
167
169
  ctx.stderr(usageText());
@@ -190,10 +192,10 @@ async function cmdLogin(args, ctx, deps) {
190
192
  const manifest = readManifest();
191
193
  let key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
192
194
  if (!key && (deps.isTty ?? stdinIsTty)()) {
193
- key = (await (deps.promptSecret ?? promptSecret)("Account key (not echoed): ")).trim();
195
+ key = (await (deps.promptSecret ?? promptSecret)("Personal key (not echoed): ")).trim();
194
196
  }
195
197
  if (!key)
196
- throw new UsageError("login needs an account key: pass --key <account-key> or set CATALYST_CLOUD_TOKEN");
198
+ throw new UsageError("login needs your personal key: pass --key <personal-key> or set CATALYST_CLOUD_TOKEN");
197
199
  const baseUrl = normalizeBaseUrl(args.baseUrl ?? ctx.env.CATALYST_CLOUD_BASE_URL ?? DEFAULT_BASE_URL);
198
200
  const me = await fetchMe(baseUrl, key, ctx.fetch);
199
201
  let existing;
@@ -213,6 +215,7 @@ async function cmdLogin(args, ctx, deps) {
213
215
  name: me.name,
214
216
  permissions: me.permissions,
215
217
  principal: me.principal,
218
+ ...(me.user ? { user: me.user } : {}),
216
219
  joinedAt: ctx.now().toISOString(),
217
220
  lastSkillBundleVersion: manifest.version,
218
221
  skillsDir,
@@ -221,6 +224,12 @@ async function cmdLogin(args, ctx, deps) {
221
224
  };
222
225
  const written = writeConfig(ctx.home, config);
223
226
  ctx.stdout(`Connected to ${me.name} (${me.slug}) — account ${me.account}`);
227
+ if (me.user) {
228
+ ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId ? "" : " — your Linear identity is not matched yet, so \"what needs me\" will show everyone's asks until an admin matches it in Settings"}`);
229
+ }
230
+ else {
231
+ ctx.stderr(`[catalyst-skills] this is the tenant's account key (a host credential), not your own — the skills work, but nothing your agent writes will carry your name and "what needs me" cannot mean you. Mint a personal key at Settings → API keys and log in with that.`);
232
+ }
224
233
  ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your key and the CLI path)${written.mode === CONFIG_MODE ? "" : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
225
234
  ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
226
235
  try {
@@ -271,11 +280,12 @@ function cmdStatus(ctx) {
271
280
  const manifest = readManifest();
272
281
  const cfg = loadConfig(ctx.home);
273
282
  if (!cfg) {
274
- ctx.stdout(`Not connected yet — run: CATALYST_CLOUD_TOKEN=<your account key> npx ${PACKAGE_NAME} login`);
283
+ ctx.stdout(`Not connected yet — run: CATALYST_CLOUD_TOKEN=<your personal key> npx ${PACKAGE_NAME} login`);
275
284
  return 0;
276
285
  }
277
286
  ctx.stdout(`Tenant: ${cfg.name} (${cfg.slug}) — account ${cfg.account}`);
278
287
  ctx.stdout(`API: ${cfg.baseUrl} (principal: ${cfg.principal})`);
288
+ ctx.stdout(cfg.user ? `As: ${cfg.user.label} (${cfg.user.role})` : "As: the tenant's account key (a host credential — no person)");
279
289
  ctx.stdout(`Bundle: ${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
280
290
  if (cfg.cliPath)
281
291
  ctx.stdout(`CLI: ${cfg.cliPath}${existsSync(cfg.cliPath) ? "" : " (missing — re-run login)"}`);
@@ -290,6 +300,8 @@ async function cmdMe(args, ctx) {
290
300
  else {
291
301
  ctx.stdout(`${me.name} (${me.slug}) — account ${me.account}`);
292
302
  ctx.stdout(`principal: ${me.principal}; permissions: ${me.permissions ? me.permissions.join(", ") : "unrestricted"}`);
303
+ if (me.user)
304
+ ctx.stdout(`user: ${me.user.label} (${me.user.role}); linear: ${me.user.linearUserId ?? "unmatched"}`);
293
305
  }
294
306
  return 0;
295
307
  }
package/dist/config.js CHANGED
@@ -70,7 +70,7 @@ export function loadConfig(home) {
70
70
  export function requireConfig(ctx) {
71
71
  const cfg = loadConfig(ctx.home);
72
72
  if (!cfg) {
73
- throw new CliError(`not connected yet — run: CATALYST_CLOUD_TOKEN=<account key> npx ${PACKAGE_NAME} login`, "not-configured");
73
+ throw new CliError(`not connected yet — run: CATALYST_CLOUD_TOKEN=<your personal key> npx ${PACKAGE_NAME} login`, "not-configured");
74
74
  }
75
75
  return cfg;
76
76
  }
package/dist/contract.js CHANGED
@@ -52,7 +52,7 @@ export function assertContractRange(version, range) {
52
52
  * Load the contract per the cloud's own cache policy: a cache younger than `doc.cache.maxAgeSeconds`
53
53
  * is used as-is; otherwise a conditional GET revalidates it (304 refreshes `fetchedAt`, 200 replaces
54
54
  * the document). A network failure on a cache older than `staleRefusalSeconds` refuses; a 403 (a
55
- * workstation key) refuses naming the account key; a major version outside the bundle's range refuses
55
+ * personal key on an older cloud) refuses naming the cloud version; a major version outside the bundle's range refuses
56
56
  * naming both versions.
57
57
  */
58
58
  export async function loadContract(ctx, cfg, opts = {}) {
@@ -79,7 +79,7 @@ export async function loadContract(ctx, cfg, opts = {}) {
79
79
  }
80
80
  catch (err) {
81
81
  if (err instanceof CliError && err.status === 403) {
82
- throw new CliError(`GET ${CONTRACT_ROUTE} refused (403): the contract needs an account key (ctc_acct_…); this key is a workstation key — ask your tenant admin for an account key`, "contract-forbidden", 2, 403);
82
+ throw new CliError(`GET ${CONTRACT_ROUTE} refused (403): this cloud is older than the bundle and does not yet admit a personal key on the contract — update the cloud, or log in with the tenant's account key until it is`, "contract-forbidden", 2, 403);
83
83
  }
84
84
  if (err instanceof MeError && err.kind === "network" && cached) {
85
85
  if (ageSeconds >= cached.doc.cache.staleRefusalSeconds) {