@catalyst-cloud/cli 0.16.14 → 0.16.16
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/dist/args.js +15 -0
- package/dist/cli.js +8 -0
- package/dist/onboard-next.js +21 -1
- package/dist/onboard-runner-custody.js +48 -11
- package/dist/onboard-runner.js +65 -6
- package/dist/seat.js +45 -0
- package/dist/stage.js +824 -0
- package/dist/transport.js +13 -2
- package/dist/write.js +49 -7
- package/package.json +4 -4
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-linear/references/writing-to-linear.md +2 -2
- package/skills/catalyst-linear/scripts/label.mjs +6 -5
- package/skills/catalyst-onboard/SKILL.md +7 -6
- package/skills/catalyst-onboard/references/connecting-this-machine.md +1 -0
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +2 -2
- package/skills/whats-happening/scripts/snapshot.mjs +128 -34
package/dist/transport.js
CHANGED
|
@@ -2,6 +2,10 @@ import { normalizeBaseUrl } from "./config.js";
|
|
|
2
2
|
import { CliError, MeError } from "./errors.js";
|
|
3
3
|
import { bearerFor } from "./oauth.js";
|
|
4
4
|
export const REQUEST_TIMEOUT_MS = 15_000;
|
|
5
|
+
// CTC-5399 — a WorkOS sign-in with no organization behind it is refused with this reason on every
|
|
6
|
+
// route. The person needs the invitation next step, never the raw reason.
|
|
7
|
+
const NO_WORKSPACE_REASON = "jwt-org-unmapped";
|
|
8
|
+
const NO_WORKSPACE_NEXT_STEP = "This sign-in has no Catalyst workspace yet. Ask your admin for an invitation, or accept the one in your email, then run the install command again.";
|
|
5
9
|
export class ApiClient {
|
|
6
10
|
baseUrl;
|
|
7
11
|
credential;
|
|
@@ -161,7 +165,12 @@ export class ApiClient {
|
|
|
161
165
|
throw new CliError(`${what} failed (401): credential not accepted. Run catalyst login to sign in again; an unattended machine needs a new personal key from Settings → API keys`, "unauthorized", 2, 401);
|
|
162
166
|
}
|
|
163
167
|
if (res.status === 403) {
|
|
164
|
-
throw new CliError(
|
|
168
|
+
throw new CliError(
|
|
169
|
+
// CTC-5399: a no-workspace sign-in is not a route-permission problem; it maps to the
|
|
170
|
+
// invitation next step. Every other reason keeps the route line naming its own reason.
|
|
171
|
+
reason === NO_WORKSPACE_REASON
|
|
172
|
+
? `${what} failed (403): ${NO_WORKSPACE_NEXT_STEP}`
|
|
173
|
+
: `${what} failed (403): this key may not reach that route${reason ? ` (${reason})` : ""} — a route that acts as a host (claiming work, publishing artifacts) needs the tenant's account key; every read and write a person makes takes your personal key`, "forbidden", 2, 403);
|
|
165
174
|
}
|
|
166
175
|
if (res.status === 429) {
|
|
167
176
|
const retryAfter = res.headers.get("retry-after");
|
|
@@ -200,7 +209,9 @@ export async function fetchMe(baseUrl, key, fetchImpl, timeoutMs = REQUEST_TIMEO
|
|
|
200
209
|
}
|
|
201
210
|
const detail = res.status === 401
|
|
202
211
|
? "credential not accepted. Run catalyst login to sign in again; an unattended machine needs a new personal key from Settings → API keys"
|
|
203
|
-
: reason
|
|
212
|
+
: reason === NO_WORKSPACE_REASON
|
|
213
|
+
? NO_WORKSPACE_NEXT_STEP
|
|
214
|
+
: reason || `HTTP ${res.status}`;
|
|
204
215
|
throw new MeError(`GET /me failed (${res.status}): ${detail}`, "http", res.status);
|
|
205
216
|
}
|
|
206
217
|
let body;
|
package/dist/write.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// write.ts — writes to Linear as the app actor through the agent proxy. Every route path comes from
|
|
2
2
|
// the contract's route table (never a literal); every stage id from `teams[].stages` by slot; every
|
|
3
3
|
// label id from `teams[].labels`; the bookkeeping marker from `vocabulary`. `delegate` is never offered.
|
|
4
|
+
// A label goes by its name in `labelNames` unless the value itself is a UUID (CTC-5871).
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
import { flagBool, flagInt, flagList, flagString, positionals } from "./args.js";
|
|
6
7
|
import { requireConfig } from "./config.js";
|
|
7
8
|
import { bookkeepingPrefix, labelId, loadContract, routePath, stageIdForSlot, teamByKey, teamForTicket } from "./contract.js";
|
|
8
|
-
import { CliError, UsageError } from "./errors.js";
|
|
9
|
+
import { CliError, MeError, UsageError } from "./errors.js";
|
|
9
10
|
import { apiClient } from "./transport.js";
|
|
10
11
|
/** A ticket identifier → its Linear id (and team/project) through GET /api/v1/issues/:identifier. */
|
|
11
12
|
export async function resolveIssue(api, ticket) {
|
|
@@ -67,10 +68,34 @@ export function firstStateOfType(states, teamId, type) {
|
|
|
67
68
|
}
|
|
68
69
|
return hit;
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
+
// The shape the mirror refuses in `labelIds` (CTC-5566): Linear label ids are UUIDs.
|
|
72
|
+
const LABEL_ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
73
|
+
/** CTC-5871 — `--label`/`--add`/`--remove` values split for the route: a UUID goes in labelIds and every
|
|
74
|
+
* other value, a contract-known name included, goes in labelNames for the mirror to resolve. A Catalyst
|
|
75
|
+
* label the contract lists as absent from the workspace still refuses before any write. */
|
|
76
|
+
export function labelFields(team, values) {
|
|
77
|
+
const ids = [];
|
|
78
|
+
const names = [];
|
|
79
|
+
for (const value of values) {
|
|
80
|
+
if (LABEL_ID_SHAPE.test(value)) {
|
|
81
|
+
ids.push(value);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
labelId(team, value); // throws label-absent for a Catalyst label this workspace lacks
|
|
85
|
+
names.push(value);
|
|
86
|
+
}
|
|
87
|
+
return { ...(ids.length ? { labelIds: ids } : {}), ...(names.length ? { labelNames: names } : {}) };
|
|
88
|
+
}
|
|
89
|
+
/** Post to a contract route by name; a budget refusal names the contract's daily budget. A 2xx whose body
|
|
90
|
+
* says the write failed (issue-label answers a batch of failed items with 200) is a failure too. */
|
|
71
91
|
export async function postAgent(api, doc, routeName, body) {
|
|
72
92
|
try {
|
|
73
93
|
const res = await api.postJson(routePath(doc, routeName), body);
|
|
94
|
+
const outcome = res.body && typeof res.body === "object" ? res.body.outcome : undefined;
|
|
95
|
+
if (outcome === "failed" || outcome === "rejected" || outcome === "exhausted") {
|
|
96
|
+
const reason = res.body.reason;
|
|
97
|
+
throw new CliError(`POST ${routePath(doc, routeName)} answered ${res.status} with outcome "${outcome}"${typeof reason === "string" && reason ? `: ${reason}` : ""}`, "write-failed", 2, res.status);
|
|
98
|
+
}
|
|
74
99
|
return res.body;
|
|
75
100
|
}
|
|
76
101
|
catch (err) {
|
|
@@ -81,6 +106,23 @@ export async function postAgent(api, doc, routeName, body) {
|
|
|
81
106
|
}
|
|
82
107
|
}
|
|
83
108
|
export async function cmdWrite(args, ctx, deps = {}) {
|
|
109
|
+
try {
|
|
110
|
+
return await runWrite(args, ctx, deps);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
// CTC-5871 — a script reading --json stdout learns the write failed; the dispatcher still prints
|
|
114
|
+
// the reason on stderr and returns the error's exit code.
|
|
115
|
+
if (args.json && (err instanceof CliError || err instanceof MeError)) {
|
|
116
|
+
ctx.stdout(JSON.stringify({
|
|
117
|
+
ok: false,
|
|
118
|
+
command: `write ${positionals(args)[0] ?? ""}`.trim(),
|
|
119
|
+
error: { code: err instanceof CliError ? err.code : err.kind, message: err.message, ...(err.status !== undefined ? { status: err.status } : {}) },
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function runWrite(args, ctx, deps) {
|
|
84
126
|
const [sub, ...rest] = positionals(args);
|
|
85
127
|
if (!sub)
|
|
86
128
|
throw new UsageError("write needs a subcommand: comment | state | label | create | reaction | attachment | session");
|
|
@@ -138,9 +180,9 @@ export async function cmdWrite(args, ctx, deps = {}) {
|
|
|
138
180
|
const team = teamForTicket(doc, issue.identifier);
|
|
139
181
|
const results = [];
|
|
140
182
|
if (add.length)
|
|
141
|
-
results.push(await postAgent(api, doc, "issue-label", { issueId: issue.id,
|
|
183
|
+
results.push(await postAgent(api, doc, "issue-label", { issueId: issue.id, ...labelFields(team, add), mode: "add" }));
|
|
142
184
|
if (remove.length)
|
|
143
|
-
results.push(await postAgent(api, doc, "issue-label", { issueId: issue.id,
|
|
185
|
+
results.push(await postAgent(api, doc, "issue-label", { issueId: issue.id, ...labelFields(team, remove), mode: "remove" }));
|
|
144
186
|
result = results.length === 1 ? results[0] : results;
|
|
145
187
|
break;
|
|
146
188
|
}
|
|
@@ -158,13 +200,13 @@ export async function cmdWrite(args, ctx, deps = {}) {
|
|
|
158
200
|
throw new UsageError("write create --stdin read an empty description");
|
|
159
201
|
}
|
|
160
202
|
const team = teamByKey(doc, teamKey);
|
|
161
|
-
const labels = flagList(args, "label")
|
|
203
|
+
const labels = labelFields(team, flagList(args, "label"));
|
|
162
204
|
const priority = flagString(args, "priority") !== undefined ? flagInt(args, "priority", 0) : undefined;
|
|
163
205
|
result = await postAgent(api, doc, "issue-create", {
|
|
164
206
|
teamId: team.id,
|
|
165
207
|
title,
|
|
166
208
|
...(description ? { description } : {}),
|
|
167
|
-
...
|
|
209
|
+
...labels,
|
|
168
210
|
...(priority !== undefined ? { priority } : {}),
|
|
169
211
|
...(asUser ? { createAsUser: true } : {}),
|
|
170
212
|
});
|
|
@@ -190,7 +232,7 @@ export async function cmdWrite(args, ctx, deps = {}) {
|
|
|
190
232
|
if (!title || !url)
|
|
191
233
|
throw new UsageError("write attachment needs --title <t> and --url <u>");
|
|
192
234
|
const issue = await resolveIssue(api, ticket);
|
|
193
|
-
result = await postAgent(api, doc, "attachment", { issueId: issue.id, title, url });
|
|
235
|
+
result = await postAgent(api, doc, "attachment", { issueId: issue.id, title, url, metadata: {} });
|
|
194
236
|
break;
|
|
195
237
|
}
|
|
196
238
|
case "session": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.16",
|
|
4
4
|
"description": "Set up Catalyst Cloud, check your tickets and manage your account from the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -53,16 +53,16 @@
|
|
|
53
53
|
"prepublishOnly": "node scripts/release-train-check.mjs --publish cli"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@catalyst-cloud/sdk": "0.16.
|
|
56
|
+
"@catalyst-cloud/sdk": "0.16.6",
|
|
57
57
|
"@clack/prompts": "1.8.1",
|
|
58
58
|
"fast-string-width": "3.0.2",
|
|
59
59
|
"fast-wrap-ansi": "0.2.2",
|
|
60
60
|
"smol-toml": "^1.7.1",
|
|
61
61
|
"yaml": "^2.9.0",
|
|
62
|
-
"@catalyst-cloud/sdk-replica-node": "0.16.
|
|
62
|
+
"@catalyst-cloud/sdk-replica-node": "0.16.6"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@catalyst-cloud/schema": "0.16.
|
|
65
|
+
"@catalyst-cloud/schema": "0.16.5",
|
|
66
66
|
"@types/node": "^26.3.0",
|
|
67
67
|
"@vitest/coverage-v8": "^4.1.11",
|
|
68
68
|
"typescript": "^7.0.2",
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst's GitHub
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Catalyst's view of Linear on the customer's own cloud account. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the cloud API by default, with local replica reads available as an explicit opt-in, always naming the source; searches tickets, PRs, projects and initiatives; writes comments, card moves, labels and new tickets through the account's agent proxy as the app actor, with every route, stage id, label id and marker read from the account's contract. Knows what a ticket accumulates as Catalyst works it (phase-outcome comments, the document per phase, the agent session, the labels, the single blocks relation, the bookkeeping marker, the eyes acknowledgement). Use when a person says "show me the ticket", "what did Catalyst write on it", "comment on it", "move it", "label it" or "file a ticket". Not for raising a decision for a human (what-needs-me) and not for pull requests (catalyst-github).
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst Linear
|
|
10
10
|
|
|
@@ -26,9 +26,9 @@ Every write spends one unit of the per-host daily budget (`thresholds.hostDailyW
|
|
|
26
26
|
|
|
27
27
|
`--slot <slot>` resolves to the team's live state id from the contract, and refuses when the slot is unmapped or its state is gone (re-map first: `catalyst team map <KEY>`). Moving to `dispatch` dispatches work. `--state-type backlog` resolves the team's first backlog-type state and parks the card, stopping remediation rounds. Names are display fields that survive re-imports while ids do not, so there is no move by name.
|
|
28
28
|
|
|
29
|
-
## Labels are by
|
|
29
|
+
## Labels are by name or UUID
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
`--label`, `--add` and `--remove` take a label's name or its UUID. A UUID is sent as a label id; every other value is sent as a name, and the proxy resolves it in the ticket's team. When the contract lists one of Catalyst's own labels (the ask marker, the hold label, the release label) as absent from the workspace, the CLI refuses rather than inventing it.
|
|
32
32
|
|
|
33
33
|
Two labels are a human's, not yours: the release label (which frees a ticket the shape detector held) is applied by a human; the hold labels on a pull request are `catalyst-github`'s subject.
|
|
34
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// label.mjs — add or remove labels on a ticket. A
|
|
3
|
-
//
|
|
2
|
+
// label.mjs — add or remove labels on a ticket. A value shaped like a UUID goes as a label id; any other
|
|
3
|
+
// value goes as a label name for the proxy to resolve in the ticket's team.
|
|
4
4
|
import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
|
|
5
5
|
|
|
6
6
|
const HELP = `Usage: node scripts/label.mjs <ticket> [--add <name|id>]... [--remove <name|id>]... [--json]
|
|
@@ -13,9 +13,10 @@ direction (add, remove), whatever the label count.
|
|
|
13
13
|
--remove <name|id> a label to remove (repeatable)
|
|
14
14
|
--json print the CLI's JSON result
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
label
|
|
16
|
+
A value shaped like a UUID is sent as a label id. Any other value, Catalyst's own labels (the ask
|
|
17
|
+
marker, the hold label, the release label) and the workspace's custom labels alike, is sent as a
|
|
18
|
+
label name, and the proxy resolves it in the ticket's team. When the contract lists one of
|
|
19
|
+
Catalyst's own labels as absent from the workspace, the CLI refuses before any write.
|
|
19
20
|
|
|
20
21
|
Exit 0 written, 1 on a usage error, 2 when this machine is not connected to a tenant or the write
|
|
21
22
|
was refused (a Catalyst label the workspace lacks, the daily budget spent; the line says which).`;
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Set up Catalyst Cloud and say whether setup is working. Walks a person from nothing to their first ticket running, one step at a time: the coding account, the project, the integrations and connected accounts, each repository's settings file, then one real ticket moving. Also logs this machine in, reads the readiness verdict with who can fix each failure, and checks the optional local replica. Use when someone says "set me up", "onboard me", "I just signed up", "get me started", "what do I do first", "log me in", "which account is this machine on", "am I set up", "what is missing", "why does nothing happen" or "is the replica running", when a login expired, or when a skill script exits 2 saying this machine is not connected. Does every step a key can do through the catalyst CLI, hands over the exact page for the steps only a browser can do, and never claims a step it did not watch succeed.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Onboard me
|
|
10
10
|
|
|
@@ -12,11 +12,12 @@ Guide setup through the same `catalyst onboard` flow that the installer runs. Th
|
|
|
12
12
|
|
|
13
13
|
## Use the onboarding command
|
|
14
14
|
|
|
15
|
-
1.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
4.
|
|
19
|
-
5.
|
|
15
|
+
1. Accept the workspace invitation first. Catalyst has no self-serve sign-up: a person joins by accepting the invitation email their workspace admin sent. Until it is accepted, sign-in is refused with "This sign-in has no Catalyst workspace yet"; if no invitation ever arrived, asking that admin is the next step, and nothing below runs before it lands.
|
|
16
|
+
2. Run `catalyst capabilities --json`. If its `onboard` entry is available and advertises `bootstrapHandoff: true`, use this path. If absent, use the older step-by-step references below and name the CLI update needed for the continuous flow.
|
|
17
|
+
3. For a requested setup, run `catalyst onboard`. To inspect first, run `catalyst onboard --dry-run --json`. Resume with `catalyst onboard`; the command rechecks saved results. Do not ask the person to confirm completed browser steps or add a separate approval before the command's plan question.
|
|
18
|
+
4. In a terminal, the command shows the verified person, workspace and plan before approval. Without terminal input, or with `--json`, use `--yes` only when the person has accepted the displayed defaults; otherwise setup waits with exit 11 and keeps the saved connection and receipt. JSON keeps progress, identity and the plan on stderr and returns one result on stdout. A supplied key needs `catalyst login` before onboarding; browser sign-in does not replace it.
|
|
19
|
+
5. For the current onboarding checks, run `catalyst ready --onboarding --json`. A failed check needs attention. An unknown check needs evidence. Report observed project work separately from incomplete setup; an unknown setup check does not prove work stopped.
|
|
20
|
+
6. Exit 0 completes the requested scope. For a full run, exit 0 means work can start. If the receipt's `complete` is false, a repository value or the first ticket is still open, and the command lists it under "Next, when you want". The daily update never keeps `complete` false, and neither do repository settings this computer cannot review (no checkout here, or not yet approved); a settings review that timed out or could not keep its draft does. Only a person at a terminal picks the first ticket; `--yes`, `--json` and headless runs leave it waiting with an action. Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. A successful `--only` step does not mean onboarding is complete. Keep the command's safe fix and supported resume command in your reply.
|
|
20
21
|
|
|
21
22
|
Use cloud reads for ordinary workstation setup checks. Offer local sync for local SQL or sustained local reads inside the plan review. Local sync does not promise offline browsing. It is optional; do not make an absent replica a setup failure. Browser approval belongs to the person. Missing bearer capabilities stay unfinished; never call a session-only route from the CLI or invent a command.
|
|
22
23
|
|
|
@@ -31,5 +31,6 @@ An unmatched Linear identity is the person's to match: `catalyst identity linear
|
|
|
31
31
|
- **A 403 on the contract naming an older cloud.** The cloud lacks personal-key access: update the cloud, or use the account key until then.
|
|
32
32
|
- **A line naming two contract versions.** `npm install -g @catalyst-cloud/cli@latest && catalyst login` before using the other skills.
|
|
33
33
|
- **A refusal naming `jwt-no-membership`.** The cloud learns of a person at their first sign-in to the app. Hand over the app's sign-in link once, then offer to run `catalyst login` again. If it repeats, their seat is inactive and an owner or admin activates it.
|
|
34
|
+
- **A refusal naming `jwt-org-unmapped`, or "no Catalyst workspace yet".** The sign-in reached Catalyst with no workspace behind it: Catalyst is joined by invitation, not sign-up. The invitation email (or the workspace admin who sends one) is the missing step; once it is accepted, run the install command or `catalyst login` again.
|
|
34
35
|
|
|
35
36
|
Retry a failed login once, by hand, after the fix. A key goes into the login command or `CATALYST_CLOUD_TOKEN` and nowhere else: never a ticket, a transcript, or a file you write. After a bundle update, the next command prints a one-line notice; read it to the person if they are watching.
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Own one Catalyst Cloud project end to end until it closes. Use when the person says "run this project for me", "own this until it ships", "keep this moving", or hands you a project id or a set of tickets to drive. Subscribes to the account's event stream for the scope through the catalyst CLI, 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. Writes to Linear as the app actor; never polls.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Run this project
|
|
10
10
|
|
package/skills/unstick/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Get a stuck Catalyst Cloud ticket moving again. Use when the person asks "why is this parked and can you release it?", "unpark this", "get things flowing again", "the outage is over, retry what failed", or hands you a ticket that is not moving. Reads why nothing runs and what holds the ticket through the catalyst CLI, decides whether the recorded cause is fixed, previews the release, releases every governor holding the ticket the right way (or one failure class across a team), and raises an ask only for what a person has to do. Never releases a cause it cannot show changed without saying so.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Unstick
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
The human's decision inbox on Catalyst Cloud, and the one way an agent raises a decision on their behalf. Use when the person asks "what needs me?", "what am I blocking?", or when active work is gated on a choice only they can make. Lists open asks ranked by how much open work each one holds, files an ask through the cloud's ask route with the account's own template, and records the answer so the held work releases. Never answers as the human, never duplicates an open ask.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What needs me
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
What environment variable names does this repository need, and where does each one come from? Scans the repository offline (no login, no network) and lists the names in three groups (build/test, deploy-only, bindings), each with where it was found, what uses it, and where a local value would come from. Never reads a value or prints one. Also checks the environment variable table in `.catalyst/catalyst.toml` offline. Use when someone asks "what does catalyst.env.json mean", "what env vars does this repo need", "why does the container need this", or before reviewing or writing the declaration.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What this repository needs
|
|
10
10
|
|
|
@@ -4,13 +4,13 @@ description: >-
|
|
|
4
4
|
Use for "what's happening?", "what machines do we have?", "how much capacity do we have?", "where are we?", "why is that stuck?", "what closed?", "what's next?", "how does this work?", "why did it do that?", "how does it prioritise?" and "what does this setting do, where is it set?" in Catalyst Cloud, or asks for something to be done rather than known. Reads the contract via catalyst CLI. Read-only: route work/decisions, never answer as the human.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.16 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|
|
11
11
|
Use one snapshot and the person's login. Run scripts, never read them. --help explains usage; exit 2 needs onboard, exit 1 means failure.
|
|
12
12
|
|
|
13
|
-
- `node scripts/snapshot.mjs` reads contract, running work, queue and ranked asks from the cloud. --board adds
|
|
13
|
+
- `node scripts/snapshot.mjs` reads contract, running work, queue and ranked asks from the cloud. --board adds every open ticket's count per stage with up to 50 cards each; when boardTruncated is present, say how many it left out. --accounts adds coding accounts, --team K narrows. --replica adds optional local diagnostics.
|
|
14
14
|
- `node scripts/explain.mjs <ticket>` explains eligibility; --history adds attempts, rounds, holds and releases.
|
|
15
15
|
- `node scripts/show-my-map.mjs [--team K]` prints stages, labels, ladder and thresholds.
|
|
16
16
|
|
|
@@ -10,13 +10,22 @@ import {
|
|
|
10
10
|
runCli,
|
|
11
11
|
} from "./lib/cli.mjs";
|
|
12
12
|
|
|
13
|
+
/** Cards listed per board column by default. Counts are never capped by this; the cards are. */
|
|
14
|
+
const BOARD_CARDS_PER_COLUMN = 50;
|
|
15
|
+
/** One page of `query issues` when the whole-scope read fails: the server's page cap. */
|
|
16
|
+
const BOARD_FALLBACK_PAGE = 500;
|
|
17
|
+
const TERMINAL_TYPES = new Set(["completed", "canceled", "cancelled"]);
|
|
18
|
+
|
|
13
19
|
const SPEC = {
|
|
14
20
|
team: { value: true, help: "limit the queue and the board to this team key" },
|
|
15
21
|
board: {
|
|
16
22
|
value: false,
|
|
17
23
|
help: "also include open tickets grouped by stage, in the contract's slot order",
|
|
18
24
|
},
|
|
19
|
-
limit: {
|
|
25
|
+
limit: {
|
|
26
|
+
value: true,
|
|
27
|
+
help: `board: max cards listed per column (default ${BOARD_CARDS_PER_COLUMN}); column counts always cover every ticket read`,
|
|
28
|
+
},
|
|
20
29
|
"full-contract": {
|
|
21
30
|
value: false,
|
|
22
31
|
help: "include the whole contract instead of the trimmed `tenant` block",
|
|
@@ -39,7 +48,8 @@ if (help) {
|
|
|
39
48
|
[
|
|
40
49
|
"Reads catalyst contract, running, queue and ask list from the cloud (query issues with --board,",
|
|
41
50
|
"accounts with --accounts, optional local diagnostics with --replica).",
|
|
42
|
-
"Output: one JSON document {takenAt, source, tenant, running, queue, waitingOnHuman, board?, accounts?, errors?}.",
|
|
51
|
+
"Output: one JSON document {takenAt, source, tenant, running, queue, waitingOnHuman, board?, boardTruncated?, accounts?, errors?}.",
|
|
52
|
+
"board.counts counts every open ticket in scope; boardTruncated is present whenever a card list or the read itself left tickets out.",
|
|
43
53
|
"A section the cloud refused is reported under errors and the rest still prints; exit 1 in that case.",
|
|
44
54
|
"Every account fact (stage names, thresholds, teams) comes from the contract in this output, never from prose.",
|
|
45
55
|
],
|
|
@@ -47,6 +57,12 @@ if (help) {
|
|
|
47
57
|
process.exit(0);
|
|
48
58
|
}
|
|
49
59
|
|
|
60
|
+
const perColumn = flags.limit === undefined ? BOARD_CARDS_PER_COLUMN : Number(flags.limit);
|
|
61
|
+
if (!Number.isInteger(perColumn) || perColumn < 1) {
|
|
62
|
+
process.stderr.write(`--limit must be a positive whole number, got ${flags.limit}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
const errors = {};
|
|
51
67
|
const section = (name, args) => {
|
|
52
68
|
const r = runCli(args);
|
|
@@ -106,28 +122,14 @@ const out = {
|
|
|
106
122
|
if (flags.accounts) out.accounts = section("accounts", ["accounts", "--json"]);
|
|
107
123
|
|
|
108
124
|
if (flags.board) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
"issues",
|
|
112
|
-
"--source",
|
|
113
|
-
"api",
|
|
114
|
-
"--limit",
|
|
115
|
-
String(flags.limit ?? 200),
|
|
116
|
-
"--json",
|
|
117
|
-
];
|
|
118
|
-
if (flags.team) args.push("--team", flags.team);
|
|
119
|
-
const r = runCli(args);
|
|
120
|
-
const sourceLine = (r.stderr.match(/^source: .*$/m) ?? [null])[0];
|
|
121
|
-
// A board read is a window (`--limit` stays the default), and a window the cloud cut short must
|
|
122
|
-
// say so rather than reading as the whole team.
|
|
123
|
-
const truncatedLine = (r.stderr.match(/^truncated at .*$/m) ?? [null])[0];
|
|
124
|
-
if (r.code !== 0)
|
|
125
|
-
errors.board = (r.stderr || `exit ${r.code}`).trim().split("\n").at(-1);
|
|
125
|
+
const read = readBoard(flags.team);
|
|
126
|
+
if (read.error) errors.board = read.error;
|
|
126
127
|
else {
|
|
127
|
-
|
|
128
|
-
out.board =
|
|
129
|
-
if (
|
|
130
|
-
|
|
128
|
+
out.board = groupByStage(read.rows, contract, flags.team, perColumn);
|
|
129
|
+
if (read.sourceLine) out.source.board = read.sourceLine.replace(/^source: /, "");
|
|
130
|
+
if (read.truncatedLine) out.source.boardTruncated = read.truncatedLine;
|
|
131
|
+
const truncated = boardTruncation(out.board, read, perColumn);
|
|
132
|
+
if (truncated) out.boardTruncated = truncated;
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
|
|
@@ -135,8 +137,10 @@ if (Object.keys(errors).length) out.errors = errors;
|
|
|
135
137
|
process.stderr.write(
|
|
136
138
|
`source: api for running/queue/asks; replica ${replica.verdict}${replica.cursor != null ? ` (cursor ${replica.cursor})` : ""}${out.source.board ? `; board from ${out.source.board}` : ""}\n`,
|
|
137
139
|
);
|
|
140
|
+
if (out.boardTruncated) process.stderr.write(`board truncated: ${out.boardTruncated.message}\n`);
|
|
138
141
|
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
139
|
-
|
|
142
|
+
// exitCode, not exit(): a whole board is well past a pipe's buffer, and exit() would cut it off.
|
|
143
|
+
process.exitCode = Object.keys(errors).length ? 1 : 0;
|
|
140
144
|
|
|
141
145
|
/** The contract facts a status reply needs, and nothing a reply should not paste. */
|
|
142
146
|
function trimContract(doc, teamKey) {
|
|
@@ -181,37 +185,125 @@ function trimContract(doc, teamKey) {
|
|
|
181
185
|
};
|
|
182
186
|
}
|
|
183
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Every ticket in scope, so the board counts are whole. The cloud caps one page at 500
|
|
190
|
+
* rows, so a single `--limit` read is a window over the most recently updated tickets, and the
|
|
191
|
+
* stages it happens to cover are not the board. `--all` follows the cloud's page cursor to the end.
|
|
192
|
+
* If that read is refused (a scope past the CLI's page ceiling, say), one full page is read instead
|
|
193
|
+
* and the result says exactly how much of the scope it covers.
|
|
194
|
+
*/
|
|
195
|
+
function readBoard(teamKey) {
|
|
196
|
+
const base = ["query", "issues", "--source", "api"];
|
|
197
|
+
const team = teamKey ? ["--team", teamKey] : [];
|
|
198
|
+
const lineOf = (stderr, re) => (stderr.match(re) ?? [null])[0];
|
|
199
|
+
const lastLine = (r) => (r.stderr || r.stdout || `exit ${r.code}`).trim().split("\n").at(-1);
|
|
200
|
+
|
|
201
|
+
const all = runCli([...base, "--all", "--json", ...team]);
|
|
202
|
+
if (all.code === 0) {
|
|
203
|
+
const rows = parseJson(all.stdout, "issues");
|
|
204
|
+
return {
|
|
205
|
+
rows,
|
|
206
|
+
complete: true,
|
|
207
|
+
read: { returned: rows.length, total: rows.length },
|
|
208
|
+
sourceLine: lineOf(all.stderr, /^source: .*$/m),
|
|
209
|
+
truncatedLine: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const allError = lastLine(all);
|
|
213
|
+
const page = runCli([...base, "--limit", String(BOARD_FALLBACK_PAGE), "--json", ...team]);
|
|
214
|
+
if (page.code !== 0) return { error: `${allError}; one page: ${lastLine(page)}` };
|
|
215
|
+
const rows = parseJson(page.stdout, "issues");
|
|
216
|
+
const truncatedLine = lineOf(page.stderr, /^truncated at .*$/m);
|
|
217
|
+
// The CLI's line: "truncated at N of M — …", where M is a count or "an unknown total (…)".
|
|
218
|
+
const m = truncatedLine?.match(/^truncated at (\d+) of (\d+)?/);
|
|
219
|
+
return {
|
|
220
|
+
rows,
|
|
221
|
+
complete: !truncatedLine,
|
|
222
|
+
read: {
|
|
223
|
+
returned: m ? Number(m[1]) : rows.length,
|
|
224
|
+
total: truncatedLine ? (m?.[2] ? Number(m[2]) : null) : rows.length,
|
|
225
|
+
},
|
|
226
|
+
readError: allError,
|
|
227
|
+
sourceLine: lineOf(page.stderr, /^source: .*$/m),
|
|
228
|
+
truncatedLine,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The top-level truncation report, or null when the board lists every open ticket in scope. */
|
|
233
|
+
function boardTruncation(board, read, perColumn) {
|
|
234
|
+
const total = Object.values(board.counts).reduce((a, b) => a + b, 0);
|
|
235
|
+
const returned = Object.values(board.byStage).reduce((a, cards) => a + cards.length, 0);
|
|
236
|
+
if (read.complete && returned === total) return null;
|
|
237
|
+
const n = (v) => (v === null ? "an unknown number of" : v.toLocaleString("en-US"));
|
|
238
|
+
const parts = [];
|
|
239
|
+
if (returned < total)
|
|
240
|
+
parts.push(`lists ${n(returned)} of ${n(total)} open tickets (at most ${perColumn} per column; raise --limit for more)`);
|
|
241
|
+
parts.push(
|
|
242
|
+
read.complete
|
|
243
|
+
? "column counts cover every ticket"
|
|
244
|
+
: `column counts cover only the ${n(read.read.returned)} most recently updated of ${n(read.read.total)} tickets read, so they are low`,
|
|
245
|
+
);
|
|
246
|
+
return {
|
|
247
|
+
returned,
|
|
248
|
+
total,
|
|
249
|
+
perColumn,
|
|
250
|
+
countsComplete: read.complete,
|
|
251
|
+
...(read.complete ? {} : { read: read.read, reason: read.readError ?? null }),
|
|
252
|
+
message: `board ${parts.join("; ")}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
184
256
|
/** Open tickets by stage name, ordered by the contract's slot order for the ticket's team; unmapped
|
|
185
|
-
* state names follow, alphabetically. Terminal
|
|
186
|
-
|
|
257
|
+
* state names follow, alphabetically. Terminal stages are dropped so the board is work only: a stage
|
|
258
|
+
* in the done or canceled slot, a stage the contract types completed or canceled, and any state the
|
|
259
|
+
* ticket itself carries a completed or canceled `state_type` for (Duplicate is canceled-typed and
|
|
260
|
+
* sits in no slot). Terminal is judged per ticket, from the ticket's own `state_type` or its OWN
|
|
261
|
+
* team's contract, never from a name blacklist: team A's open `Review` must survive team B's
|
|
262
|
+
* completed `Review`. `counts` covers every row; `byStage` lists the first `perColumn` per stage. */
|
|
263
|
+
function groupByStage(rows, doc, teamKey, perColumn) {
|
|
187
264
|
const order = [];
|
|
188
265
|
const seen = new Set();
|
|
189
266
|
const terminal = new Set();
|
|
267
|
+
const terminalByTeam = new Map();
|
|
190
268
|
for (const t of doc.teams ?? []) {
|
|
191
269
|
if (teamKey && String(t.key ?? "").toUpperCase() !== teamKey.toUpperCase())
|
|
192
270
|
continue;
|
|
271
|
+
const names = new Set();
|
|
193
272
|
for (const slot of doc.slots ?? []) {
|
|
194
273
|
const s = t.stages?.[slot];
|
|
195
274
|
if (!s?.name) continue;
|
|
196
275
|
if (
|
|
197
276
|
slot === "done" ||
|
|
198
277
|
slot === "canceled" ||
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
202
|
-
)
|
|
278
|
+
TERMINAL_TYPES.has(String(s.type ?? "").toLowerCase())
|
|
279
|
+
) {
|
|
280
|
+
names.add(s.name);
|
|
203
281
|
terminal.add(s.name);
|
|
204
|
-
else if (!seen.has(s.name)) {
|
|
282
|
+
} else if (!seen.has(s.name)) {
|
|
205
283
|
seen.add(s.name);
|
|
206
284
|
order.push(s.name);
|
|
207
285
|
}
|
|
208
286
|
}
|
|
287
|
+
terminalByTeam.set(String(t.key ?? "").toUpperCase(), names);
|
|
209
288
|
}
|
|
289
|
+
for (const r of rows)
|
|
290
|
+
if (TERMINAL_TYPES.has(String(r.state_type ?? "").toLowerCase()))
|
|
291
|
+
terminal.add(String(r.state ?? "unknown"));
|
|
292
|
+
const rowIsTerminal = (r) =>
|
|
293
|
+
TERMINAL_TYPES.has(String(r.state_type ?? "").toLowerCase()) ||
|
|
294
|
+
(terminalByTeam
|
|
295
|
+
.get(String(r.team_key ?? "").toUpperCase())
|
|
296
|
+
?.has(String(r.state ?? "unknown")) ??
|
|
297
|
+
false);
|
|
298
|
+
const counts = {};
|
|
210
299
|
const byStage = {};
|
|
211
300
|
for (const r of rows) {
|
|
212
301
|
const state = String(r.state ?? "unknown");
|
|
213
|
-
if (
|
|
214
|
-
|
|
302
|
+
if (rowIsTerminal(r)) continue;
|
|
303
|
+
counts[state] = (counts[state] ?? 0) + 1;
|
|
304
|
+
const cards = (byStage[state] ??= []);
|
|
305
|
+
if (cards.length >= perColumn) continue;
|
|
306
|
+
cards.push({
|
|
215
307
|
identifier: r.identifier,
|
|
216
308
|
title: r.title,
|
|
217
309
|
priority: r.priority ?? null,
|
|
@@ -223,8 +315,10 @@ function groupByStage(rows, doc, teamKey) {
|
|
|
223
315
|
const extra = Object.keys(byStage)
|
|
224
316
|
.filter((s) => !seen.has(s))
|
|
225
317
|
.sort();
|
|
318
|
+
const stageOrder = [...order, ...extra];
|
|
226
319
|
return {
|
|
227
|
-
stageOrder
|
|
320
|
+
stageOrder,
|
|
321
|
+
counts: Object.fromEntries(stageOrder.map((s) => [s, counts[s] ?? 0])),
|
|
228
322
|
byStage,
|
|
229
323
|
terminalStagesDropped: [...terminal],
|
|
230
324
|
};
|