@catalyst-cloud/cli 0.9.5 → 0.13.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 +22 -0
- package/README.md +4 -2
- package/dist/args.js +3 -1
- package/dist/cli.js +7 -2
- package/dist/env/declaration-rules.js +163 -0
- package/dist/env/group.js +51 -0
- package/dist/env/inventory.js +102 -0
- package/dist/env/render.js +39 -0
- package/dist/env/scan-env-example.js +62 -0
- package/dist/env/scan-source.js +43 -0
- package/dist/env/scan-workflow.js +123 -0
- package/dist/env/scan-wrangler.js +120 -0
- package/dist/env/types.js +1 -0
- package/dist/env/walk.js +87 -0
- package/dist/env.js +76 -0
- package/package.json +4 -4
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +1 -1
- package/skills/catalyst-setup/SKILL.md +1 -1
- package/skills/connect-me/SKILL.md +3 -3
- package/skills/how-catalyst-works/SKILL.md +4 -2
- package/skills/how-catalyst-works/references/settings-and-where-they-live.md +20 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +8 -1
- 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-needs-me/references/reading-the-inbox.md +4 -0
- package/skills/what-this-repo-needs/SKILL.md +40 -0
- package/skills/what-this-repo-needs/agents/openai.yaml +6 -0
- package/skills/what-this-repo-needs/agents/portability.yaml +4 -0
- package/skills/what-this-repo-needs/references/what-each-group-means.md +68 -0
- package/skills/what-this-repo-needs/scripts/check.mjs +64 -0
- package/skills/what-this-repo-needs/scripts/inventory.mjs +61 -0
- package/skills/what-this-repo-needs/scripts/lib/cli.mjs +114 -0
- package/skills/what-this-repo-needs/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/SKILL.md +1 -1
- package/skills/whats-happening/references/reprioritising.md +1 -1
- package/skills/whats-happening/references/status-reply.md +2 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +41 -34
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: what-this-repo-needs
|
|
3
|
+
description: >-
|
|
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 or prints a value. Also validates a catalyst.env.json file the same way the cloud does. 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 a repository's environment declaration.
|
|
5
|
+
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
|
+
---
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
|
|
8
|
+
|
|
9
|
+
# What this repository needs
|
|
10
|
+
|
|
11
|
+
Say this first, in your own words, before you scan: the fleet builds and tests this repository in a
|
|
12
|
+
container that has only what is declared — nothing more, nothing guessed. The names below are what
|
|
13
|
+
that container needs; none of them is a value, and this tool never reads or shows one.
|
|
14
|
+
|
|
15
|
+
## Run first
|
|
16
|
+
|
|
17
|
+
- `node scripts/inventory.mjs [path] --help` — scans the repository (default: here) and prints the
|
|
18
|
+
names in three groups: build/test, deploy-only, bindings. Each name shows where it was found
|
|
19
|
+
(file:line), what uses it, and where a local value would come from — a `.env` file, your shell, a
|
|
20
|
+
CI secret, or a Cloudflare binding. `--json` gives the same data in machine form.
|
|
21
|
+
- `node scripts/check.mjs <file> --help` — validates an existing `catalyst.env.json` offline, with
|
|
22
|
+
the same rules the cloud applies, and never prints a value from it either.
|
|
23
|
+
|
|
24
|
+
Then ask the person to review the grouped list: for each name, keep it, drop it, or move it to a
|
|
25
|
+
different group. Keep your own reasoning short — the list is the point, not your commentary on it.
|
|
26
|
+
|
|
27
|
+
## Load on demand
|
|
28
|
+
|
|
29
|
+
| when | read |
|
|
30
|
+
| -- | -- |
|
|
31
|
+
| the person asks what a group means, or why a wrangler.toml entry is or is not a binding | `references/what-each-group-means.md` |
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
- Report, never repair. This tool never writes `catalyst.env.json` and never proposes anything.
|
|
36
|
+
- Never print a value. Not a `.env` value, not a secret, not a committed `wrangler.toml [vars]`
|
|
37
|
+
value — only the name, where it was found, and where a local value would come from.
|
|
38
|
+
- A Cloudflare (or other platform) binding is not an environment value; keep the two apart.
|
|
39
|
+
- This reads the repository only. It needs no login and makes no network call — unlike the
|
|
40
|
+
account-scope `catalyst-skills environment` verb, which reads your tenant's own declaration.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "What this repository needs"
|
|
3
|
+
short_description: "The environment variable names this repository needs, where each is used, and where its value comes from — never a value"
|
|
4
|
+
default_prompt: "Use $what-this-repo-needs to explain what environment variables this repository needs and where they come from."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# What each group means
|
|
2
|
+
|
|
3
|
+
`node scripts/inventory.mjs` sorts every environment variable name it finds into exactly one of
|
|
4
|
+
three groups. This page explains what each one means and why a name lands where it does — it never
|
|
5
|
+
restates a value, because the scan itself never reads one.
|
|
6
|
+
|
|
7
|
+
## build/test
|
|
8
|
+
|
|
9
|
+
Needed to install, build and run the tests in a container. This is the default group: a name lands
|
|
10
|
+
here unless something specific pulls it into `deploy-only` or `bindings`. Examples: a name read by
|
|
11
|
+
the repository's own source (`process.env.X`), a name documented in a `.env.example`-shaped file, a
|
|
12
|
+
name a non-deploy workflow job uses.
|
|
13
|
+
|
|
14
|
+
The scan does not read `package.json` scripts. A name that only a script's own shell line mentions
|
|
15
|
+
and nothing else refers to is therefore not listed — add it by hand if the container needs it.
|
|
16
|
+
|
|
17
|
+
A name referenced from **both** a deploy job and a non-deploy job (or build step) still lands here,
|
|
18
|
+
not in `deploy-only` — a container that builds and tests the repository needs it too, so the
|
|
19
|
+
stricter requirement wins.
|
|
20
|
+
|
|
21
|
+
## deploy-only
|
|
22
|
+
|
|
23
|
+
A CI secret used **only** by a job the scan classifies as deploying something: a workflow job that
|
|
24
|
+
sets `environment:`, whose id looks like `deploy`, `publish`, `release` or `cd`, or that runs a
|
|
25
|
+
recognised deploy command (`wrangler deploy`, `npm publish`, `gh release create`, and similar). If
|
|
26
|
+
the same name is referenced anywhere outside a deploy job, it moves to `build/test` instead.
|
|
27
|
+
|
|
28
|
+
Its local value comes from a CI secret — something a repository or organisation admin sets in the
|
|
29
|
+
CI provider, not something a person types into a local `.env` file.
|
|
30
|
+
|
|
31
|
+
## bindings
|
|
32
|
+
|
|
33
|
+
A Cloudflare (or other platform) binding: a name bound to a resource — a KV namespace, a D1
|
|
34
|
+
database, a Durable Object, an R2 bucket, a queue — declared in `wrangler.toml`. A binding is not an
|
|
35
|
+
environment value a process reads from `process.env`; it is wired up by the platform at deploy
|
|
36
|
+
time, which is why this scan keeps the two apart even though both are "names the container needs".
|
|
37
|
+
|
|
38
|
+
A `wrangler.toml` binding table names the binding through one attribute's value — `binding = "NAME"`
|
|
39
|
+
for most tables, `name = "NAME"` for `durable_objects.bindings` — never through the table's own key.
|
|
40
|
+
|
|
41
|
+
## wrangler.toml `[vars]`: build/test, not a binding, and nothing to declare
|
|
42
|
+
|
|
43
|
+
A `[vars]` (or `[env.<name>.vars]`) table is different from every other wrangler table: its keys ARE
|
|
44
|
+
the names, and their values are committed in `wrangler.toml` itself — checked into the repository,
|
|
45
|
+
not a secret and not something a person supplies locally. So a `[vars]` name lands in `build/test`
|
|
46
|
+
(it is a plain value a build or test step could read, not a resource binding), and the scan marks it
|
|
47
|
+
`needsDeclaration: false`: there is nothing for a customer to declare, because the value is already
|
|
48
|
+
sitting in the file everyone can already read.
|
|
49
|
+
|
|
50
|
+
## Where a local value comes from
|
|
51
|
+
|
|
52
|
+
For every name, "where the local value comes from" is one of:
|
|
53
|
+
|
|
54
|
+
- **a `.env` file or your shell** — the ordinary case for `build/test`.
|
|
55
|
+
- **a CI secret** — for `deploy-only`, and for any `build/test` name whose only non-committed source
|
|
56
|
+
is a workflow's `secrets.X` reference.
|
|
57
|
+
- **a Cloudflare binding** — for `bindings`; there is no local value to supply, the platform wires
|
|
58
|
+
it up.
|
|
59
|
+
- **value committed in wrangler.toml — nothing to declare** — for a `[vars]` entry.
|
|
60
|
+
|
|
61
|
+
None of these is the value itself — only where it would come from, so a person reviewing the list
|
|
62
|
+
knows what to check without this tool ever showing them what is actually set.
|
|
63
|
+
|
|
64
|
+
## A `wrangler.jsonc` or `wrangler.json` file
|
|
65
|
+
|
|
66
|
+
Modern Wrangler configs are often JSONC. This scan does not read them yet — `wrangler.toml` is the
|
|
67
|
+
only Cloudflare config format it understands — so a repository configured that way gets a note
|
|
68
|
+
saying so, rather than a silent, confidently wrong "no bindings found".
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check.mjs — validates a catalyst.env.json file offline, with the same rules this bundle vendors
|
|
3
|
+
// from the cloud's own validator. Never reads or prints a value. Offline: no login, no network —
|
|
4
|
+
// wraps `catalyst-skills env check`.
|
|
5
|
+
import { runCliOffline } from "./lib/cli.mjs";
|
|
6
|
+
|
|
7
|
+
const HELP = `Usage: node scripts/check.mjs <file> [--json]
|
|
8
|
+
|
|
9
|
+
Validates a catalyst.env.json file against this bundle's offline copy of the cloud's rules. Prints
|
|
10
|
+
"valid" or the refusal reasons — never a value. Exit 0 valid, 1 invalid or unreadable.
|
|
11
|
+
|
|
12
|
+
--json prints {state, errors}. This is a local, offline check: no login, no network required.`;
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
16
|
+
console.log(HELP);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
const json = args.includes("--json");
|
|
20
|
+
const file = args.find((a) => a !== "--json");
|
|
21
|
+
if (!file) {
|
|
22
|
+
console.error("env check needs a file: node scripts/check.mjs <path to catalyst.env.json>");
|
|
23
|
+
console.log(HELP);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const res = runCliOffline(["env", "check", file, ...(json ? ["--json"] : [])]);
|
|
28
|
+
if (res.stdout) process.stdout.write(res.stdout);
|
|
29
|
+
if (res.stderr) process.stderr.write(res.stderr);
|
|
30
|
+
await finish(res.code);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ⛔ NEVER `process.exit(code)` STRAIGHT AFTER A WRITE — the same rule `bin/catalyst-skills.js`
|
|
34
|
+
* states in full, reintroduced here at validate attempt 29 (M-3). When this script's own stdout is a
|
|
35
|
+
* PIPE — which is how an agent harness runs it — writes are asynchronous, so `process.exit` severs
|
|
36
|
+
* whatever is still in flight AND STILL REPORTS THE ORIGINAL EXIT CODE. Measured: a `--json` body of
|
|
37
|
+
* 385,633 bytes arrived as 65,536 bytes of invalid JSON at exit 0. Writing to a FILE hides it,
|
|
38
|
+
* because a file gives stdout a synchronous write path.
|
|
39
|
+
*
|
|
40
|
+
* So: park the code on `process.exitCode`, wait for both streams to drain, and only then exit.
|
|
41
|
+
*/
|
|
42
|
+
function drained(stream) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
if (!stream || stream.destroyed || stream.writableEnded) return resolve();
|
|
45
|
+
let settled = false;
|
|
46
|
+
const done = () => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
resolve();
|
|
50
|
+
};
|
|
51
|
+
stream.once("error", done);
|
|
52
|
+
try {
|
|
53
|
+
stream.write("", done);
|
|
54
|
+
} catch {
|
|
55
|
+
done();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function finish(code) {
|
|
61
|
+
process.exitCode = code;
|
|
62
|
+
await Promise.all([drained(process.stdout), drained(process.stderr)]);
|
|
63
|
+
process.exit(code);
|
|
64
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// inventory.mjs — lists the environment variable NAMES this repository needs, grouped build/test,
|
|
3
|
+
// deploy-only and bindings, each with where it was found, what uses it, and where a local value
|
|
4
|
+
// would come from. Never reads or prints a value. Offline: no login, no network — wraps
|
|
5
|
+
// `catalyst-skills env inventory`.
|
|
6
|
+
import { runCliOffline } from "./lib/cli.mjs";
|
|
7
|
+
|
|
8
|
+
const HELP = `Usage: node scripts/inventory.mjs [path] [--json]
|
|
9
|
+
|
|
10
|
+
Scans a repository (default: the current directory) for the environment variable names it needs and
|
|
11
|
+
prints them in three groups: build/test, deploy-only, bindings. For each name: where it was found
|
|
12
|
+
(file:line), what uses it, and where a local value would come from. Never reads or prints a value.
|
|
13
|
+
|
|
14
|
+
--json prints the same data as JSON. This is a local, offline scan: no login, no network required.`;
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
18
|
+
console.log(HELP);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
const json = args.includes("--json");
|
|
22
|
+
const path = args.find((a) => a !== "--json") ?? ".";
|
|
23
|
+
|
|
24
|
+
const res = runCliOffline(["env", "inventory", path, ...(json ? ["--json"] : [])]);
|
|
25
|
+
if (res.stdout) process.stdout.write(res.stdout);
|
|
26
|
+
if (res.stderr) process.stderr.write(res.stderr);
|
|
27
|
+
await finish(res.code);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ⛔ NEVER `process.exit(code)` STRAIGHT AFTER A WRITE — the same rule `bin/catalyst-skills.js`
|
|
31
|
+
* states in full, reintroduced here at validate attempt 29 (M-3). When this script's own stdout is a
|
|
32
|
+
* PIPE — which is how an agent harness runs it — writes are asynchronous, so `process.exit` severs
|
|
33
|
+
* whatever is still in flight AND STILL REPORTS THE ORIGINAL EXIT CODE. Measured: a `--json` body of
|
|
34
|
+
* 385,633 bytes arrived as 65,536 bytes of invalid JSON at exit 0. Writing to a FILE hides it,
|
|
35
|
+
* because a file gives stdout a synchronous write path.
|
|
36
|
+
*
|
|
37
|
+
* So: park the code on `process.exitCode`, wait for both streams to drain, and only then exit.
|
|
38
|
+
*/
|
|
39
|
+
function drained(stream) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
if (!stream || stream.destroyed || stream.writableEnded) return resolve();
|
|
42
|
+
let settled = false;
|
|
43
|
+
const done = () => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
resolve();
|
|
47
|
+
};
|
|
48
|
+
stream.once("error", done);
|
|
49
|
+
try {
|
|
50
|
+
stream.write("", done);
|
|
51
|
+
} catch {
|
|
52
|
+
done();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function finish(code) {
|
|
58
|
+
process.exitCode = code;
|
|
59
|
+
await Promise.all([drained(process.stdout), drained(process.stderr)]);
|
|
60
|
+
process.exit(code);
|
|
61
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/cli.mjs — the one way a skill script reaches the Catalyst Cloud SDK and API: by spawning the
|
|
3
|
+
// catalyst-skills CLI this bundle installed. It reads customer.json to find that CLI and nothing
|
|
4
|
+
// else; it never holds the key itself. This file is a library — run a sibling script with
|
|
5
|
+
// --help for usage. Per-skill (skills install one directory at a time, so nothing shared outside the
|
|
6
|
+
// skill would ever be installed) — see runCliOffline below for the one way this skill's copy differs.
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
12
|
+
|
|
13
|
+
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
14
|
+
export const CONNECT_HINT = `this machine is not connected to a tenant yet — run: ${CONNECT_COMMAND}`;
|
|
15
|
+
|
|
16
|
+
/** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME before HOME. */
|
|
17
|
+
export function configPath() {
|
|
18
|
+
const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
19
|
+
return join(home, ".config", "catalyst-cloud", "customer.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The customer config, or exit 2 with one line naming the connect command. Never throws. */
|
|
23
|
+
export function requireCustomerConfig() {
|
|
24
|
+
const path = configPath();
|
|
25
|
+
if (!existsSync(path)) {
|
|
26
|
+
console.error(CONNECT_HINT);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
let cfg;
|
|
30
|
+
try {
|
|
31
|
+
cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(`${path} could not be read (${err instanceof Error ? err.message : String(err)}) — ${CONNECT_HINT}`);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
if (!hasCredential(cfg) || typeof cfg.baseUrl !== "string") {
|
|
37
|
+
console.error(`${path} is missing required fields — ${CONNECT_HINT}`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
return cfg;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run one catalyst-skills verb and return {code, stdout, stderr}. Spawns the CLI whose path the
|
|
45
|
+
* connect step recorded in customer.json; falls back to `npx @catalyst-cloud/catalyst-skills` when
|
|
46
|
+
* no path is recorded or it no longer exists. Exits 2 when the machine is not connected.
|
|
47
|
+
*/
|
|
48
|
+
export function runCli(args, opts = {}) {
|
|
49
|
+
const cfg = requireCustomerConfig();
|
|
50
|
+
return spawnCli(cfg, args, opts);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run `catalyst-skills env inventory` / `env check` without requiring a credential at all. Unlike
|
|
55
|
+
* every other verb this launcher can reach, `env inventory` and `env check` are LOCAL and OFFLINE —
|
|
56
|
+
* they never call the cloud — so gating them on `requireCustomerConfig` would break the feature they
|
|
57
|
+
* exist to provide. Still resolves the recorded `cliPath` when a customer.json happens to exist (so
|
|
58
|
+
* an already-connected machine reuses the same installed CLI), and still falls back to
|
|
59
|
+
* `npx @catalyst-cloud/catalyst-skills` otherwise.
|
|
60
|
+
*/
|
|
61
|
+
export function runCliOffline(args, opts = {}) {
|
|
62
|
+
let cfg = null;
|
|
63
|
+
try {
|
|
64
|
+
const path = configPath();
|
|
65
|
+
if (existsSync(path)) cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
cfg = null;
|
|
68
|
+
}
|
|
69
|
+
return spawnCli(cfg, args, opts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function spawnCli(cfg, args, opts) {
|
|
73
|
+
const recorded = Boolean(cfg) && typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
|
|
74
|
+
const command = recorded ? process.execPath : process.platform === "win32" ? "npx.cmd" : "npx";
|
|
75
|
+
const argv = recorded ? [cfg.cliPath, ...args] : [PACKAGE, ...args];
|
|
76
|
+
const res = spawnSync(command, argv, {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
input: opts.stdin,
|
|
79
|
+
env: process.env,
|
|
80
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
81
|
+
shell: !recorded && process.platform === "win32",
|
|
82
|
+
});
|
|
83
|
+
if (res.error) {
|
|
84
|
+
console.error(`could not run ${recorded ? cfg.cliPath : `npx ${PACKAGE}`}: ${res.error.message}`);
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Run a verb that must succeed. On exit 2 (the CLI's "not configured / refused" class) the script
|
|
92
|
+
* exits 2; on any other non-zero exit it exits 1. The CLI's own stderr is forwarded either way so the
|
|
93
|
+
* reason is never lost.
|
|
94
|
+
*/
|
|
95
|
+
export function runCliOrExit(args, opts = {}) {
|
|
96
|
+
const res = runCli(args, opts);
|
|
97
|
+
if (res.code === 0) return res;
|
|
98
|
+
const why = res.stderr.trim() || res.stdout.trim() || `catalyst-skills ${args.join(" ")} exited ${res.code}`;
|
|
99
|
+
console.error(why);
|
|
100
|
+
process.exit(res.code === 2 ? 2 : 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Parse the CLI's --json stdout; null when it is not JSON (the caller decides what that means). */
|
|
104
|
+
export function parseJson(stdout) {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(stdout);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
113
|
+
console.log("lib/cli.mjs is a library used by the scripts beside it; run any of those with --help for usage.");
|
|
114
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
|
|
3
|
+
// byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
|
|
4
|
+
// (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
|
|
5
|
+
// each carries its own copy. This file is a library — run a sibling script with --help for usage.
|
|
6
|
+
//
|
|
7
|
+
// customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
|
|
8
|
+
// session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
|
|
9
|
+
// catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
|
|
10
|
+
// itself. A new credential kind lands here, once.
|
|
11
|
+
|
|
12
|
+
/** The command that connects this machine, as every not-connected line names it. */
|
|
13
|
+
export const CONNECT_COMMAND =
|
|
14
|
+
"npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
|
|
15
|
+
|
|
16
|
+
/** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
|
|
17
|
+
export function hasCredential(cfg) {
|
|
18
|
+
if (cfg === null || typeof cfg !== "object") return false;
|
|
19
|
+
const key = cfg["key"];
|
|
20
|
+
if (typeof key === "string" && key !== "") return true;
|
|
21
|
+
const login = cfg["auth"];
|
|
22
|
+
return (
|
|
23
|
+
login !== null &&
|
|
24
|
+
typeof login === "object" &&
|
|
25
|
+
login["kind"] === "oauth" &&
|
|
26
|
+
typeof login["refreshToken"] === "string" &&
|
|
27
|
+
login["refreshToken"] !== ""
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
The desk for a Catalyst Cloud tenant. Use when the person asks "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", or asks for something to be done rather than known. Reads the tenant contract, what is running and queued, the eligibility explainer and the open asks through the catalyst-skills CLI, and answers in one reply with ticket ids. Routes work to a project owner and decisions to what-needs-me. Never composes a URL, never polls, never answers as the human.
|
|
5
5
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ Each team's queue is derived by the cloud itself from the team's own tickets, so
|
|
|
12
12
|
|
|
13
13
|
Tickets that are already **mid-ladder** join the same candidate set and sort ahead by how many phases they have completed, so a ticket that is nearly done finishes before a fresh one starts. That is an ordering rule only; it never makes an ineligible ticket eligible.
|
|
14
14
|
|
|
15
|
-
Within the order, dispatch is bounded per repository (a concurrency cap the tenant
|
|
15
|
+
Within the order, dispatch is bounded per repository (a concurrency cap of 20 running phases by default that an operator raises or lowers — the tenant's own settings page displays it and does not change it — and a paused repository resolves to zero without losing the stored value) and split across teams so one team cannot starve another. Comment-wake work, where the cloud answers a human comment on a ticket, shares the same cap as relay work.
|
|
16
16
|
|
|
17
17
|
The queue is recomputed on every ingest that touches the team and on each alarm pass, so a change you make shows up within seconds, not on a schedule.
|
|
18
18
|
|
|
@@ -16,6 +16,8 @@ The person asked one question and gets one reply. If they would need a second su
|
|
|
16
16
|
|
|
17
17
|
A sixth block, **cannot see**, appears only when it is non-empty: the facts your key cannot read (PR labels and reactions, which are not mirrored; an operator-only park release), each with the URL the CLI printed.
|
|
18
18
|
|
|
19
|
+
A flow number is named in that block by name, not left out: cycle time, throughput, or how long pull requests have been open are not computed by anything a key reads today. Say they are not computed rather than counting ticket dates or open pull requests and calling it that.
|
|
20
|
+
|
|
19
21
|
## The header line
|
|
20
22
|
|
|
21
23
|
The reply opens with one clause that says when and from where: the snapshot's `takenAt` and its source line. When the replica was stale or absent, the reply says the numbers came from the API; when it was fresh, it says the cursor. This is not decoration. A stale source that goes unmentioned is the way a wrong status reply happens.
|
|
@@ -4,55 +4,62 @@ This reference restates an invariant: the vocabulary the cloud's eligibility exp
|
|
|
4
4
|
|
|
5
5
|
## How to read a reason
|
|
6
6
|
|
|
7
|
-
`explain` prints one paragraph: the queue position (or none), the status, the reason, a detail or marker when the cloud gave one, the last failure, and any advisories. Match the reason to a row below. "Releases itself" means the cloud clears it with no human action; "
|
|
7
|
+
`explain` prints one paragraph: the queue position (or none), the status, the reason, a detail or marker when the cloud gave one, the last failure, and any advisories. Match the reason to a row below. "Releases itself" means the cloud clears it with no human action; "who acts" names who does — the person, with their own login; a tenant owner or admin, in settings; an operator; or "no one acts", which is a fourth honest answer, not a gap. When a reason is not in this table, `explain` prints it as the cloud spelled it; report it verbatim and say the bundle does not know it.
|
|
8
8
|
|
|
9
9
|
## Reasons that release themselves
|
|
10
10
|
|
|
11
|
-
| reason | what it means | how it clears |
|
|
12
|
-
| -- | -- | -- |
|
|
13
|
-
| `retry_backoff` | the failed phase is retrying in place and waiting out its rung | a clock; the rungs are the contract's `thresholds.retryBackoffMs` |
|
|
14
|
-
| `lease_held` | a live container already holds this phase | the phase finishes |
|
|
15
|
-
| `intake_lease_held` | a later phase is offered while an intake container still holds the ticket | intake finishes |
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
11
|
+
| reason | what it means | how it clears | who acts |
|
|
12
|
+
| -- | -- | -- | -- |
|
|
13
|
+
| `retry_backoff` | the failed phase is retrying in place and waiting out its rung | a clock; the rungs are the contract's `thresholds.retryBackoffMs` | no one acts |
|
|
14
|
+
| `lease_held` | a live container already holds this phase | the phase finishes | no one acts |
|
|
15
|
+
| `intake_lease_held` | a later phase is offered while an intake container still holds the ticket | intake finishes | no one acts |
|
|
16
|
+
| `later_phase_lease_held` | an earlier phase is offered while a live container still holds a later phase of this ticket | the later phase finishes | no one acts |
|
|
17
|
+
| `environment_check_running` | the repository's environment check is in flight | the check finishes | no one acts |
|
|
18
|
+
| `claim_storm` | this unit was claimed too many times in the last hour | it waits the hour out; there is nothing to release | no one acts |
|
|
19
|
+
| `repo_at_capacity` | the repository's runner seats are all in use | it starts when a seat frees | no one acts |
|
|
20
|
+
| `runner_image_breaker` | fleet-wide: the live runner image fails every phase at startup, so dispatch is held rather than parking tickets | an operator moves the pin; one alert per tenant, no per-ticket action | an operator |
|
|
21
|
+
| `routing_unavailable` | claimed, then refused at kickoff: no route, no eligible coding-account slot, or the provider is unavailable | provider recovery or a slot freeing | usually no one acts; if it persists, a tenant owner or admin, in settings, checks coding accounts |
|
|
22
|
+
| `cooling_down` (with a named callback) | the phase is parked on a condition the cloud watches | the callback fires; the `detail` names it | no one acts; an operator can release it directly if the callback stalls |
|
|
23
|
+
| `no_branch_to_remediate` / `branch_missing` | a branch-dependent phase has no branch yet | the cloud releases these parks on its own budget | no one acts |
|
|
24
|
+
| `stale_failure_episode` | the ladder advanced after the recorded failure, so the round would repair a phase already passed | the round is dropped automatically; the reason is informational | no one acts |
|
|
25
|
+
| `wip_limit` | the project (one Linear team, all its repositories) is at its WIP limit, default 12, so this ticket gets no first phase; tickets already in progress are unaffected | a ticket in progress finishes. If the fleet is idle at the limit, the in-progress tickets are the ones waiting (merge queue, holds, paused repositories, asks): unstick those, never the queue. Raising the limit is the project owner's call | no one acts on this ticket; the in-progress tickets it waits on each have their own row |
|
|
21
26
|
|
|
22
27
|
## Reasons that need a human
|
|
23
28
|
|
|
24
29
|
| reason | what it means | who acts and how |
|
|
25
30
|
| -- | -- | -- |
|
|
26
|
-
| `ask_ticket` | it carries an ask label; a question is never work | the
|
|
27
|
-
| `ask_shape_suspected` | its own text reads as a decision request though it carries no ask label | the
|
|
28
|
-
| `blocked` | a live blocking relation holds it | whoever owns the blocker closes it;
|
|
29
|
-
| `not_at_dispatch_stage` | the card is not in the team's dispatch column |
|
|
30
|
-
| `not_at_pr_stage` | merge is next but the card is not in the PR column | the
|
|
31
|
-
| `no_change_hold` | a repair round changed nothing |
|
|
32
|
-
| `waiting_on` | a merge-gate failure with no cause the cloud can repair |
|
|
33
|
-
| `externally_claimed` | a worker outside the cloud holds it | that worker, or the
|
|
34
|
-
| `environment_check_required`
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
31
|
+
| `ask_ticket` | it carries an ask label; a question is never work | the person, with their own login, answers the ask (`what-needs-me`) |
|
|
32
|
+
| `ask_shape_suspected` | its own text reads as a decision request though it carries no ask label | the person, with their own login, either answers it as an ask or applies the release label the contract names |
|
|
33
|
+
| `blocked` | a live blocking relation holds it | whoever owns the blocker closes it; when the blocker is an ask, that is the person, with their own login |
|
|
34
|
+
| `not_at_dispatch_stage` | the card is not in the team's dispatch column | the person, with their own login, moves the card to the dispatch slot (`references/reprioritising.md`) |
|
|
35
|
+
| `not_at_pr_stage` | merge is next but the card is not in the PR column | the person, with their own login, returns the card to the pr slot; an automation that bounced it should be found |
|
|
36
|
+
| `no_change_hold` | a repair round changed nothing | the person, with their own login, comments on the ticket or pushes to the branch |
|
|
37
|
+
| `waiting_on` | a merge-gate failure with no cause the cloud can repair | the person, with their own login, reads the merge-wait comment on the ticket; usually a hold label or a review they must give (`catalyst-github`) |
|
|
38
|
+
| `externally_claimed` | a worker outside the cloud holds it | that worker releases it, or the person, with their own login, removes the local-lane label |
|
|
39
|
+
| `environment_check_required` | the repository's environment check has not run | a tenant owner or admin, in settings, runs the environment check |
|
|
40
|
+
| `environment_check_failed` | the repository's environment check failed | a tenant owner or admin, in settings, fixes and reruns the environment check |
|
|
41
|
+
| `environment_check_expired` | the environment check's verdict aged out | a tenant owner or admin, in settings, reruns the environment check |
|
|
42
|
+
| `environment_check_hash_mismatch` | the environment changed since its recorded verdict | a tenant owner or admin, in settings, reruns the environment check |
|
|
43
|
+
| `repo_paused` | an operator paused the repository | an operator resumes it |
|
|
44
|
+
| `scope_overlap` | its declared file scope intersects a ticket already in flight | the person, with their own login, decides which goes first, or waits for the other ticket |
|
|
45
|
+
| `human_owned_pr` | a person's own pull request holds the ticket | the person, with their own login, closes or merges that pull request, or hands it to Catalyst; never close someone's pull request for them. No release clears this one — the release command refuses it terminally — so do not route it to `unstick` expecting one |
|
|
46
|
+
| `review_not_converging` | review and repair kept finding new problems without converging | the person, with their own login, reads the findings (`catalyst-github`) and comments on the ticket to resume; raise an ask for that read. No release clears this one — the release command refuses it terminally — so do not route it to `unstick` expecting one |
|
|
47
|
+
| `round_threshold` | the ticket spent its lifetime repair budget | the person, with their own login, answers the ask the cloud already raised, or pushes a fix; point at that ask rather than raising a second one. No release clears this one — the release command refuses it terminally — so do not route it to `unstick` expecting one |
|
|
37
48
|
|
|
38
49
|
## Reasons a release clears once the cause is fixed
|
|
39
50
|
|
|
40
|
-
These do not release themselves
|
|
51
|
+
These do not release themselves: once the recorded cause is fixed, the person's own login releases them with `catalyst-skills release <ticket>`. The desk does not release; route the ticket to the `unstick` skill, which reads the history, previews the release and runs it, or names what a person must do first.
|
|
41
52
|
|
|
42
|
-
| reason | what it means | note |
|
|
43
|
-
| -- | -- | -- |
|
|
44
|
-
| `phase_parked` / `cooling_down` (parked after repeated failures, or the round cap was spent) | the phase is parked and does not release itself | `unstick`; `explain --history` names the park and the failure class it recorded |
|
|
45
|
-
| `remediate_parked` | the repair phase itself is parked, so the failing phase has nowhere to be repaired | as above |
|
|
46
|
-
| `validate_class_spent` | this validate failure already spent its one repair round in this episode | a push or a comment saying what to change releases it on its own; otherwise `unstick` |
|
|
47
|
-
| `
|
|
48
|
-
| `review_not_converging` | review and repair kept finding new problems without converging | a person reads the findings and comments on the ticket to resume; raise an ask for that read |
|
|
49
|
-
| `round_threshold` | the ticket spent its lifetime repair budget | a person answers the ask the cloud raised, or pushes a fix |
|
|
50
|
-
| `no_branch_to_remediate` / `branch_missing` / `branch_gone` | a branch-dependent phase has no branch to clone | the cloud releases missing-branch parks on its own budget; a deleted branch needs a human to decide |
|
|
51
|
-
| `stale_failure_episode` | the ladder advanced after the recorded failure | informational; the round is dropped |
|
|
53
|
+
| reason | what it means | note | who acts |
|
|
54
|
+
| -- | -- | -- | -- |
|
|
55
|
+
| `phase_parked` / `cooling_down` (parked after repeated failures, or the round cap was spent) | the phase is parked and does not release itself | `unstick`; `explain --history` names the park and the failure class it recorded | the person, with their own login |
|
|
56
|
+
| `remediate_parked` | the repair phase itself is parked, so the failing phase has nowhere to be repaired | as above | the person, with their own login |
|
|
57
|
+
| `validate_class_spent` | this validate failure already spent its one repair round in this episode | a push or a comment saying what to change releases it on its own; otherwise `unstick` | the person, with their own login |
|
|
58
|
+
| `branch_gone` | a branch that existed was deleted | the person, with their own login, decides whether to recreate it or drop the ticket | the person, with their own login |
|
|
52
59
|
|
|
53
60
|
## Reasons that are not problems
|
|
54
61
|
|
|
55
|
-
`ticket_terminal` (done or canceled), `pipeline_complete` (every phase ran), and `pr_merged` (the PR merged, nothing left to clone) describe finished work. Report them as closed, not blocked.
|
|
62
|
+
`ticket_terminal` (done or canceled), `pipeline_complete` (every phase ran), and `pr_merged` (the PR merged, nothing left to clone) describe finished work; no one acts. Report them as closed, not blocked.
|
|
56
63
|
|
|
57
64
|
## When the cloud could not judge
|
|
58
65
|
|