@catalyst-cloud/catalyst-skills 0.1.0 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +133 -36
- package/dist/args.js +237 -0
- package/dist/ask.js +137 -0
- package/dist/cli.js +165 -279
- package/dist/config.js +106 -0
- package/dist/contract-types.js +4 -0
- package/dist/contract.js +178 -0
- package/dist/detach.js +10 -0
- package/dist/errors.js +27 -0
- package/dist/execution.js +156 -0
- package/dist/http.js +142 -0
- package/dist/prompt.js +35 -0
- package/dist/query.js +187 -0
- package/dist/ready.js +123 -0
- package/dist/replica.js +318 -0
- package/dist/sdk.js +26 -0
- package/dist/skill-shape.js +179 -0
- package/dist/skills.js +74 -0
- package/dist/ts-deps-loader.js +68 -0
- package/dist/watch/consumer.js +141 -0
- package/dist/watch/cursor-file.js +62 -0
- package/dist/watch.js +173 -0
- package/dist/write.js +224 -0
- package/package.json +18 -6
- package/skills/catalyst-github/SKILL.md +35 -0
- package/skills/catalyst-github/agents/openai.yaml +6 -0
- package/skills/catalyst-github/agents/portability.yaml +4 -0
- package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
- package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
- package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
- package/skills/catalyst-linear/SKILL.md +43 -0
- package/skills/catalyst-linear/agents/openai.yaml +6 -0
- package/skills/catalyst-linear/agents/portability.yaml +5 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
- package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
- package/skills/catalyst-linear/scripts/comment.mjs +59 -0
- package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
- package/skills/catalyst-linear/scripts/label.mjs +48 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
- package/skills/catalyst-linear/scripts/move.mjs +41 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
- package/skills/catalyst-linear/scripts/search.mjs +49 -0
- package/skills/catalyst-setup/SKILL.md +32 -0
- package/skills/catalyst-setup/agents/openai.yaml +6 -0
- package/skills/catalyst-setup/agents/portability.yaml +4 -0
- package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
- package/skills/catalyst-setup/scripts/check.mjs +75 -0
- package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
- package/skills/connect-me/SKILL.md +55 -0
- package/skills/connect-me/agents/openai.yaml +6 -0
- package/skills/connect-me/agents/portability.yaml +5 -0
- package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
- package/skills/connect-me/scripts/lib/cli.mjs +184 -0
- package/skills/connect-me/scripts/verify-connection.mjs +68 -0
- package/skills/how-catalyst-works/SKILL.md +42 -0
- package/skills/how-catalyst-works/agents/openai.yaml +6 -0
- package/skills/how-catalyst-works/agents/portability.yaml +4 -0
- package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
- package/skills/how-catalyst-works/references/the-ladder.md +41 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
- package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
- package/skills/run-this-project/SKILL.md +45 -0
- package/skills/run-this-project/agents/openai.yaml +6 -0
- package/skills/run-this-project/agents/portability.yaml +5 -0
- package/skills/run-this-project/assets/stall-policy.json +15 -0
- package/skills/run-this-project/references/making-work-ready.md +60 -0
- package/skills/run-this-project/references/reacting-to-events.md +76 -0
- package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
- package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
- package/skills/run-this-project/scripts/make-ready.mjs +64 -0
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
- package/skills/what-needs-me/SKILL.md +41 -0
- package/skills/what-needs-me/agents/openai.yaml +6 -0
- package/skills/what-needs-me/agents/portability.yaml +5 -0
- package/skills/what-needs-me/references/raising-a-decision.md +41 -0
- package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
- package/skills/what-needs-me/references/settling-an-answer.md +37 -0
- package/skills/what-needs-me/scripts/inbox.mjs +47 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
- package/skills/what-needs-me/scripts/raise.mjs +53 -0
- package/skills/what-needs-me/scripts/settle.mjs +73 -0
- package/skills/whats-happening/SKILL.md +43 -0
- package/skills/whats-happening/agents/openai.yaml +6 -0
- package/skills/whats-happening/agents/portability.yaml +4 -0
- package/skills/whats-happening/assets/status-reply.json +77 -0
- package/skills/whats-happening/references/reading-the-board.md +43 -0
- package/skills/whats-happening/references/reprioritising.md +37 -0
- package/skills/whats-happening/references/routing-work.md +36 -0
- package/skills/whats-happening/references/status-reply.md +34 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
- package/skills/whats-happening/scripts/explain.mjs +28 -0
- package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
- package/skills/whats-happening/scripts/snapshot.mjs +145 -0
- package/skills/ask/SKILL.md +0 -37
- package/skills/concierge/SKILL.md +0 -41
- package/skills/join/SKILL.md +0 -30
- package/skills/linearis/SKILL.md +0 -40
- package/skills/setup/SKILL.md +0 -27
- package/skills/steward/SKILL.md +0 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
7
|
+
## 0.1.1
|
|
8
|
+
|
|
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.
|
|
10
|
+
|
|
3
11
|
## 0.1.0
|
|
4
12
|
|
|
5
13
|
First published customer skill bundle: vendored customer editions of concierge, steward, ask and linearis plus the new join and setup skills, one-command tenant discovery via GET /api/v1/me, config written to ~/.config/catalyst-cloud/customer.json, and a placeholder 0.x tenant contract range (CTC-1924 in flight).
|
package/README.md
CHANGED
|
@@ -1,70 +1,167 @@
|
|
|
1
1
|
# @catalyst-cloud/catalyst-skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://skills.sh/coalesce-labs/catalyst-cloud-skills)
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
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.
|
|
8
|
+
|
|
9
|
+
<details><summary><strong>Claude Code</strong></summary>
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
```
|
|
12
|
+
/plugin marketplace add coalesce-labs/catalyst-cloud-skills
|
|
13
|
+
/plugin install catalyst@catalyst-cloud
|
|
14
|
+
```
|
|
15
|
+
</details>
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
<details><summary><strong>Codex</strong></summary>
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
Run this command in your project:
|
|
16
20
|
|
|
17
21
|
```sh
|
|
18
|
-
npx @
|
|
22
|
+
npx skills@latest add coalesce-labs/catalyst-cloud-skills -a codex
|
|
19
23
|
```
|
|
24
|
+
</details>
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
<details><summary><strong>Cursor</strong></summary>
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
2. **Discovers your tenant from the key** — it calls `GET /api/v1/me` on the cloud, which answers `{account, slug, name, permissions, principal}` for exactly whoever the key already proves you to be. No tenant is ever typed, guessed, or defaulted.
|
|
25
|
-
3. **Writes config** to `~/.config/catalyst-cloud/customer.json` (mode `0600` — it holds your key; nothing else ever reads or copies it).
|
|
26
|
-
4. Prints the tenant it joined (`Joined <name> (<slug>)`), so you can see you landed in the right place.
|
|
28
|
+
Run this command in your project:
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
```sh
|
|
31
|
+
npx skills@latest add coalesce-labs/catalyst-cloud-skills -a cursor
|
|
32
|
+
```
|
|
33
|
+
</details>
|
|
34
|
+
|
|
35
|
+
<details><summary><strong>OpenCode, Amp, Windsurf and the rest</strong></summary>
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
Run this command in your project:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx skills@latest add coalesce-labs/catalyst-cloud-skills
|
|
41
|
+
```
|
|
42
|
+
|
|
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:
|
|
31
49
|
|
|
32
50
|
```sh
|
|
33
51
|
npm install -g @catalyst-cloud/catalyst-skills
|
|
34
|
-
|
|
52
|
+
CATALYST_CLOUD_TOKEN=<your-account-key> catalyst-skills login
|
|
53
|
+
catalyst-skills ready
|
|
35
54
|
```
|
|
36
55
|
|
|
37
|
-
|
|
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
|
|
65
|
+
|
|
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.
|
|
67
|
+
|
|
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.
|
|
69
|
+
|
|
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.
|
|
38
71
|
|
|
39
|
-
|
|
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`.
|
|
78
|
+
|
|
79
|
+
## First use
|
|
80
|
+
|
|
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 |
|
|
97
|
+
|
|
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.
|
|
101
|
+
|
|
102
|
+
## The skills
|
|
40
103
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
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) |
|
|
44
114
|
|
|
45
|
-
|
|
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.
|
|
46
116
|
|
|
47
|
-
|
|
117
|
+
## What a key cannot see yet
|
|
48
118
|
|
|
49
|
-
|
|
50
|
-
- **`join` and `setup`** are written here, against the public cloud API surface (`GET /api/v1/me`).
|
|
119
|
+
Two facts have no tenant-facing route today, and the skills say so by name rather than guess:
|
|
51
120
|
|
|
52
|
-
|
|
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.
|
|
53
123
|
|
|
54
|
-
|
|
124
|
+
## Versions and origins
|
|
55
125
|
|
|
56
|
-
|
|
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.
|
|
57
127
|
|
|
58
|
-
|
|
128
|
+
## What it writes on your machine
|
|
59
129
|
|
|
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`.
|
|
133
|
+
|
|
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.
|
|
135
|
+
|
|
136
|
+
## Updating
|
|
137
|
+
|
|
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:
|
|
139
|
+
|
|
140
|
+
```
|
|
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)
|
|
60
142
|
```
|
|
61
|
-
|
|
143
|
+
|
|
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`.
|
|
145
|
+
|
|
146
|
+
## Uninstalling
|
|
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
|
+
|
|
150
|
+
```sh
|
|
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
|
|
62
155
|
```
|
|
63
156
|
|
|
64
|
-
|
|
157
|
+
## If login fails
|
|
65
158
|
|
|
66
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
67
164
|
|
|
68
|
-
##
|
|
165
|
+
## License
|
|
69
166
|
|
|
70
|
-
|
|
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
|
+
}
|