@catalyst-cloud/catalyst-skills 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +19 -12
  3. package/dist/args.js +34 -4
  4. package/dist/ask.js +6 -3
  5. package/dist/cli.js +19 -5
  6. package/dist/config.js +4 -0
  7. package/dist/contract.js +6 -0
  8. package/dist/environment.js +207 -0
  9. package/dist/events.js +106 -0
  10. package/dist/execution.js +146 -18
  11. package/dist/pagination.js +76 -0
  12. package/dist/published.js +75 -0
  13. package/dist/query.js +41 -18
  14. package/dist/ready.js +110 -19
  15. package/dist/release.js +142 -0
  16. package/dist/replica.js +296 -30
  17. package/dist/semver.js +18 -0
  18. package/dist/skill-shape.js +10 -0
  19. package/dist/skills.js +52 -1
  20. package/dist/transport.js +5 -2
  21. package/dist/ts-deps-loader.js +46 -1
  22. package/package.json +7 -4
  23. package/skills/catalyst-github/SKILL.md +1 -1
  24. package/skills/catalyst-github/scripts/lib/cli.mjs +3 -2
  25. package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
  26. package/skills/catalyst-linear/SKILL.md +1 -1
  27. package/skills/catalyst-linear/references/reading-a-ticket.md +2 -0
  28. package/skills/catalyst-linear/scripts/lib/cli.mjs +4 -3
  29. package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
  30. package/skills/catalyst-onboard/SKILL.md +49 -0
  31. package/skills/catalyst-onboard/agents/openai.yaml +6 -0
  32. package/skills/catalyst-onboard/agents/portability.yaml +5 -0
  33. package/skills/catalyst-onboard/references/the-one-path.md +143 -0
  34. package/skills/catalyst-onboard/references/what-the-browser-owns.md +50 -0
  35. package/skills/catalyst-onboard/references/who-fixes-what.md +41 -0
  36. package/skills/catalyst-onboard/scripts/lib/cli.mjs +117 -0
  37. package/skills/catalyst-onboard/scripts/lib/credential.mjs +29 -0
  38. package/skills/catalyst-onboard/scripts/where-am-i.mjs +245 -0
  39. package/skills/catalyst-setup/SKILL.md +3 -3
  40. package/skills/catalyst-setup/references/what-each-check-means.md +22 -6
  41. package/skills/catalyst-setup/scripts/lib/cli.mjs +3 -2
  42. package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
  43. package/skills/catalyst-setup/scripts/replica-status.mjs +1 -1
  44. package/skills/connect-me/SKILL.md +1 -1
  45. package/skills/connect-me/scripts/lib/cli.mjs +3 -2
  46. package/skills/connect-me/scripts/lib/credential.mjs +29 -0
  47. package/skills/how-catalyst-works/SKILL.md +1 -1
  48. package/skills/how-catalyst-works/references/stages-and-mapping.md +2 -2
  49. package/skills/how-catalyst-works/references/what-runs-next.md +1 -1
  50. package/skills/how-catalyst-works/references/when-a-phase-fails.md +4 -4
  51. package/skills/how-catalyst-works/scripts/lib/cli.mjs +4 -3
  52. package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
  53. package/skills/run-this-project/SKILL.md +2 -2
  54. package/skills/run-this-project/references/making-work-ready.md +1 -1
  55. package/skills/run-this-project/references/stalls-and-escalation.md +2 -2
  56. package/skills/run-this-project/scripts/lib/cli.mjs +3 -2
  57. package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
  58. package/skills/unstick/SKILL.md +41 -0
  59. package/skills/unstick/agents/openai.yaml +6 -0
  60. package/skills/unstick/agents/portability.yaml +5 -0
  61. package/skills/unstick/references/playbook.md +51 -0
  62. package/skills/unstick/scripts/lib/cli.mjs +135 -0
  63. package/skills/unstick/scripts/lib/credential.mjs +29 -0
  64. package/skills/unstick/scripts/unstick.mjs +57 -0
  65. package/skills/what-needs-me/SKILL.md +1 -1
  66. package/skills/what-needs-me/references/reading-the-inbox.md +1 -1
  67. package/skills/what-needs-me/scripts/lib/cli.mjs +3 -2
  68. package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
  69. package/skills/whats-happening/SKILL.md +2 -2
  70. package/skills/whats-happening/references/why-is-it-stuck.md +9 -4
  71. package/skills/whats-happening/scripts/lib/cli.mjs +3 -2
  72. package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
  73. package/skills/whats-happening/scripts/snapshot.mjs +4 -0
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ // where-am-i.mjs — how far has setup got? FIVE PARTS, EACH READ BY THE INSTRUMENT THAT OWNS IT, and
3
+ // each finding labelled with the part it belongs to. The whole point of this script is that no part
4
+ // answers for another: a project that is not ready is reported as a project finding with a tenant
5
+ // owner's name on it, never as something the person at this keyboard can fix by running anything.
6
+ //
7
+ // Every count and every name below is read off what a verb printed. Nothing here is written down.
8
+ import { cliTarget, CONNECT_LINE, parseFlags, printHelp, runCli, tryJson, tryLoadConfig } from "./lib/cli.mjs";
9
+
10
+ const SPEC = {
11
+ next: { help: "print only the single next step" },
12
+ json: { help: "one JSON document instead of the report" },
13
+ };
14
+ const NOTES = [
15
+ "Reads, in this order: `status` (machine), `ready --json` (machine checks and project checks, kept apart),",
16
+ "`me --json` (person), and `contract --path …` for the account, the projects and the repositories.",
17
+ "Writes nothing and changes nothing. Runs before this machine is connected — that is one of the states it reports.",
18
+ ];
19
+
20
+ const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
21
+ if (help) {
22
+ printHelp("node scripts/where-am-i.mjs [--next] [--json]", SPEC, NOTES);
23
+ process.exit(0);
24
+ }
25
+ if (positionals.length > 0) {
26
+ process.stderr.write(`unexpected argument: ${positionals[0]} (try --help)\n`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const via = cliTarget().via;
31
+ const parts = [];
32
+ // `blocking` is false for a finding that is real and reportable but does not stop the next step —
33
+ // an unmatched Linear identity is the one that matters: it must be said, and it must not become the
34
+ // thing the person is told to go and do before they can map a project.
35
+ const add = (part, instrument, verdict, lines, owner = null, where = null, blocking = true) =>
36
+ parts.push({ part, instrument, verdict, lines, owner, where, blocking });
37
+
38
+ // ── machine ───────────────────────────────────────────────────────────────────────────────────────
39
+ const status = runCli(["status"]);
40
+ if (!status.ran) {
41
+ process.stderr.write(`${status.stderr}\n`);
42
+ process.stderr.write(`the catalyst-skills CLI could not be started. Install it, then run this again.\n`);
43
+ process.exit(2);
44
+ }
45
+ const statusLines = status.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
46
+ const field = (name) => {
47
+ const line = statusLines.find((l) => l.startsWith(`${name}:`));
48
+ return line ? line.slice(name.length + 1).trim() : null;
49
+ };
50
+ const tenant = field("Tenant");
51
+ const api = field("API");
52
+ const connected = tenant !== null && tryLoadConfig() !== null;
53
+ // The app and the API share an origin; the base URL is whatever this machine was connected to, so
54
+ // no page link in this report is a host anyone typed from memory.
55
+ const cloud = api === null ? null : api.split(" ")[0].replace(/\/+$/, "");
56
+ const link = (path) => (cloud === null ? `<your cloud>${path}` : `${cloud}${path}`);
57
+
58
+ const machineLines = connected ? statusLines : [statusLines[0] ?? "status printed nothing"];
59
+ let machineVerdict = connected ? "ok" : "unfinished";
60
+
61
+ // `ready` is ONE verdict over two parts; split it by check id before anything is reported. A check
62
+ // whose id begins with "team:" belongs to a project and cannot be moved from this machine.
63
+ let projectChecks = [];
64
+ let machineFix = null;
65
+ if (connected) {
66
+ const ready = runCli(["ready", "--json"]);
67
+ const report = tryJson(ready.stdout);
68
+ const checks = Array.isArray(report?.checks) ? report.checks : null;
69
+ if (checks === null) {
70
+ machineLines.push(`ready: could not be read (${(ready.stderr || ready.stdout).trim().split("\n")[0] ?? "no output"})`);
71
+ machineVerdict = "unfinished";
72
+ } else {
73
+ projectChecks = checks.filter((c) => typeof c.id === "string" && c.id.startsWith("team:"));
74
+ const machineChecks = checks.filter((c) => !(typeof c.id === "string" && c.id.startsWith("team:")));
75
+ const failed = machineChecks.filter((c) => !c.ok && !c.note);
76
+ for (const c of machineChecks) machineLines.push(`${c.note ? "note" : c.ok ? "ok " : "FAIL"} ${c.line}${!c.ok && !c.note && c.fix ? ` — fix: ${c.fix}` : ""}`);
77
+ if (failed.length > 0) {
78
+ machineVerdict = "unfinished";
79
+ machineFix = failed.find((c) => typeof c.fix === "string")?.fix ?? null;
80
+ }
81
+ }
82
+ }
83
+ add(
84
+ "machine",
85
+ "catalyst-skills status, and the non-team checks of catalyst-skills ready",
86
+ machineVerdict,
87
+ machineLines,
88
+ "you, on this machine",
89
+ connected ? null : CONNECT_LINE,
90
+ );
91
+
92
+ // ── person ────────────────────────────────────────────────────────────────────────────────────────
93
+ if (!connected) {
94
+ add("person", "catalyst-skills me", "unreadable", ["not readable until this machine is connected"], null, null);
95
+ } else {
96
+ const me = runCli(["me", "--json"]);
97
+ const doc = tryJson(me.stdout);
98
+ const user = doc && typeof doc.user === "object" && doc.user !== null ? doc.user : null;
99
+ if (doc === null) {
100
+ add("person", "catalyst-skills me", "unreadable", [`me could not be read (${(me.stderr || me.stdout).trim().split("\n")[0] ?? "no output"})`], null, null);
101
+ } else if (user === null) {
102
+ add(
103
+ "person",
104
+ "catalyst-skills me",
105
+ "unfinished",
106
+ ["this credential names no person — it is a host credential, so nothing an agent writes will carry a name"],
107
+ "you: connect again with your own login",
108
+ CONNECT_LINE,
109
+ );
110
+ } else {
111
+ const matched = typeof user.linearUserId === "string" && user.linearUserId !== "";
112
+ add(
113
+ "person",
114
+ "catalyst-skills me",
115
+ matched ? "ok" : "unfinished",
116
+ [`${user.label ?? "(unnamed)"} (${user.role ?? "role unknown"})`, matched ? "Linear identity matched" : "Linear identity NOT matched — asks assigned to you cannot be told apart from everyone else's. It blocks nothing below; get it fixed when convenient."],
117
+ matched ? null : "a tenant owner or admin",
118
+ matched ? null : link("/settings/account"),
119
+ false,
120
+ );
121
+ }
122
+ }
123
+
124
+ // ── account ───────────────────────────────────────────────────────────────────────────────────────
125
+ if (!connected) {
126
+ add("account", "catalyst-skills contract --path account", "unreadable", ["not readable until this machine is connected"], null, null);
127
+ } else {
128
+ const acct = runCli(["contract", "--path", "account", "--json"]);
129
+ const doc = tryJson(acct.stdout);
130
+ if (doc === null) {
131
+ add("account", "catalyst-skills contract --path account", "unreadable", ["the account block could not be read — try: catalyst-skills contract --refresh"], null, null);
132
+ } else {
133
+ const workspace = typeof doc.linearWorkspaceSlug === "string" && doc.linearWorkspaceSlug !== "" ? doc.linearWorkspaceSlug : typeof doc.linearWorkspaceId === "string" && doc.linearWorkspaceId !== "" ? doc.linearWorkspaceId : null;
134
+ // The declaration is the one part of the account a key can also READ — and it is the one part a
135
+ // key can WRITE, so it is reported here rather than left to the settings page like the rest.
136
+ const envLines = [];
137
+ const env = runCli(["environment", "read", "--json"]);
138
+ const envDoc = tryJson(env.stdout);
139
+ if (envDoc === null) {
140
+ envLines.push(`environment: not readable (${(env.stderr || env.stdout).trim().split("\n")[0] ?? "no output"})`);
141
+ } else if (envDoc.current === null || envDoc.current === undefined) {
142
+ envLines.push("environment: nothing declared yet — declare it with `catalyst-skills environment propose --file <path> --approve`");
143
+ } else {
144
+ envLines.push(`environment: revision ${envDoc.current.revision} (${envDoc.current.canonicalHash}), ${envDoc.isApproved ? "approved" : "NOT approved — a proposal nobody approved changes nothing"}`);
145
+ envLines.push(envDoc.delivered ? `environment delivered to phases: revision ${envDoc.delivered.revision}` : "environment delivered to phases: nothing yet");
146
+ const unresolved = Array.isArray(envDoc.unresolvedReferences) ? envDoc.unresolvedReferences : [];
147
+ if (unresolved.length > 0) envLines.push(`environment names values this tenant does not carry yet: ${unresolved.join(", ")}`);
148
+ }
149
+ add(
150
+ "account",
151
+ "catalyst-skills contract --path account, and catalyst-skills environment read",
152
+ workspace === null ? "unfinished" : "ok",
153
+ [
154
+ `${doc.name ?? "(unnamed tenant)"} (${doc.slug ?? "?"})`,
155
+ workspace === null ? "no Linear workspace resolved on this contract" : `Linear workspace resolved: ${workspace}`,
156
+ "the GitHub App install is NOT carried here — the connections page is the only place that shows it",
157
+ ...envLines,
158
+ ],
159
+ "a tenant owner or admin",
160
+ link("/settings/connections"),
161
+ );
162
+ }
163
+ }
164
+
165
+ // ── projects (a project is one Linear team) ───────────────────────────────────────────────────────
166
+ if (!connected) {
167
+ add("projects", "catalyst-skills contract --path teams", "unreadable", ["not readable until this machine is connected"], null, null);
168
+ } else {
169
+ const teams = runCli(["contract", "--path", "teams", "--json"]);
170
+ const doc = tryJson(teams.stdout);
171
+ const rows = Array.isArray(doc) ? doc : null;
172
+ if (rows === null) {
173
+ add("projects", "catalyst-skills contract --path teams", "unreadable", ["the project list could not be read — try: catalyst-skills contract --refresh"], null, null);
174
+ } else {
175
+ const lines = [`${rows.length} mapped`];
176
+ for (const t of rows) {
177
+ const key = t.key ?? t.id ?? "(unkeyed)";
178
+ const readiness = t.readiness ?? {};
179
+ const bad = Array.isArray(readiness.checks) ? readiness.checks.filter((c) => c.state !== "pass") : [];
180
+ lines.push(`${key}: ${readiness.status ?? "unknown"}${bad.length ? ` — ${bad.map((c) => `${c.id} ${c.state}`).join(", ")}` : ""}`);
181
+ }
182
+ for (const c of projectChecks) lines.push(`${c.ok ? "note" : "FAIL"} ${c.line}${c.who ? ` — who: ${c.who}` : ""}`);
183
+ lines.push("⛔ MAPPED projects only. An empty list means nothing is mapped yet, NOT that there are no projects — the full list is on the page below.");
184
+ const ready = rows.length > 0 && rows.every((t) => (t.readiness?.status ?? "unchecked") === "ready");
185
+ add("projects", "catalyst-skills contract --path teams, and the team: checks of ready", ready ? "ok" : "unfinished", lines, "a tenant owner or admin", link("/settings/linear-teams"));
186
+ }
187
+ }
188
+
189
+ // ── repositories ──────────────────────────────────────────────────────────────────────────────────
190
+ if (!connected) {
191
+ add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["not readable until this machine is connected"], null, null);
192
+ } else {
193
+ const repos = runCli(["contract", "--path", "merge.repositories", "--json"]);
194
+ const doc = tryJson(repos.stdout);
195
+ const rows = Array.isArray(doc) ? doc : null;
196
+ if (rows === null) {
197
+ add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["the repository list could not be read — try: catalyst-skills contract --refresh"], null, null);
198
+ } else {
199
+ const lines = [`${rows.length} registered`, ...rows.map((r) => `${r.owner ?? "?"}/${r.name ?? "?"}`)];
200
+ lines.push("⛔ REGISTRATION only. This carries no status and no project attachment, so it never proves a repository can be dispatched into.");
201
+ add("repositories", "catalyst-skills contract --path merge.repositories", rows.length > 0 ? "ok" : "unfinished", lines, "a tenant owner or admin", link("/settings/repositories"));
202
+ }
203
+ }
204
+
205
+ // ── the single next step ──────────────────────────────────────────────────────────────────────────
206
+ // The machine's next action is not one sentence: an unconnected machine needs the login, and a
207
+ // connected one needs whatever check failed — and `ready` already printed that check's own fix, so
208
+ // this carries it through rather than inventing a second answer to the same question.
209
+ const machineNext = connected
210
+ ? `clear the machine check that failed — ${machineFix ?? "see the machine lines above"}`
211
+ : "connect this machine";
212
+ const NEXT = {
213
+ machine: machineNext,
214
+ person: "get this person's seat and Linear identity sorted",
215
+ account: "connect Linear, and install the GitHub App",
216
+ projects: "pick ONE project and map its stages (or adopt the Catalyst workflow)",
217
+ repositories: "register the repository, attaching it to the project you mapped",
218
+ };
219
+ const blocked = parts.filter((p) => p.verdict !== "ok" && p.blocking);
220
+ const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
221
+ const next =
222
+ stuck === null
223
+ ? null
224
+ : { part: stuck.part, action: NEXT[stuck.part], owner: stuck.owner, where: stuck.where, blocking: stuck.blocking };
225
+ const finished = parts.every((p) => p.verdict === "ok");
226
+
227
+ if (flags.json) {
228
+ console.log(JSON.stringify({ cli: via, connected, cloud, parts, next, finished }));
229
+ } else if (flags.next) {
230
+ if (next === null) console.log("nothing left: every part this machine can read is finished. Move one card into the project's dispatch stage.");
231
+ else console.log(`${next.part}: ${next.action}${next.blocking ? "" : " (does not block the steps below)"}${next.owner ? ` — who: ${next.owner}` : ""}${next.where ? ` — ${next.where.startsWith("http") ? "where" : "do"}: ${next.where}` : ""}`);
232
+ } else {
233
+ for (const p of parts) {
234
+ console.log(`${p.part} [${p.verdict}]`);
235
+ console.log(` instrument: ${p.instrument}`);
236
+ for (const l of p.lines) console.log(` ${l}`);
237
+ if (p.verdict !== "ok" && p.owner) console.log(` who: ${p.owner}`);
238
+ // A page gets "where"; a command gets "do". Labelling a command "where" is how a person ends up
239
+ // looking for a settings page that is actually a line to run.
240
+ if (p.verdict !== "ok" && p.where) console.log(` ${p.where.startsWith("http") ? "where" : "do"}: ${p.where}`);
241
+ console.log("");
242
+ }
243
+ console.log(next === null ? "next: nothing left — move one card into the project's dispatch stage and watch." : `next: ${next.part} — ${next.action}${next.owner ? ` (${next.owner})` : ""}${next.where ? ` — ${next.where}` : ""}`);
244
+ }
245
+ process.exit(finished ? 0 : 1);
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: catalyst-setup
3
3
  description: >-
4
- Am I set up? Machine readiness (Node, the tenant connection, the cached contract, the CLI path, the skills, the SDK, the optional replica) plus tenant readiness from the contract's ten per-team checks, in one verdict: what passes, what is blocked, what is merely waiting, and who can click what. Use when someone asks "am I set up", "what is missing", "why does nothing happen", "is the replica running", or right after connecting a new machine. Reports; never repairs.
4
+ Am I set up? Machine readiness (Node, the tenant connection, the cached contract, the CLI path, the skills, the SDK, the optional replica) 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. Use when someone asks "am I set up", "what is missing", "why does nothing happen", "is the replica running", or right after connecting a new machine. Reports; never repairs.
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.7.0 — written in this repository for customer tenants -->
8
8
 
9
9
  # Am I set up?
10
10
 
@@ -26,7 +26,7 @@ You answer one question with one verdict and one list. The verdict is READY or N
26
26
  - Report, never repair. No tenant-reachable repair verb exists for a key yet; the settings page is where a tenant owner or admin fixes a mapping, a connection or a label, and you say which one.
27
27
  - End every answer with the verdict and the who-can-click-what list, in that order.
28
28
  - A stale or absent replica is a note, never a failure: every skill reads the API meanwhile and says so. Do not tell the person to start it unless they want local SQL or cheaper repeated reads.
29
- - Not connected (exit 2) means the connect step, not a retry: `CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login`, with the person's own key from Settings → API keys. Never guess a tenant; the key is the only selector.
29
+ - Not connected (exit 2) means the connect step, not a retry: `npx @catalyst-cloud/catalyst-skills login`, which logs the person in keyless in their browser; with a personal key from Settings → API keys instead, prefix it with `CATALYST_CLOUD_TOKEN=<your personal key>`. Never guess a tenant; the login (or the key) is the only selector.
30
30
  - Waiting is not failing. "No write observed", "no delivery observed" and "no host connected" clear themselves the first time the thing happens; say that instead of raising them.
31
31
  - Unknown is not a pass. A check the engine could not run is reported as such, never rounded up.
32
32
  - Never run a check in a loop. If the person wants to know when a waiting check clears, that is the project-running skill's watch.
@@ -1,12 +1,14 @@
1
1
  # What each check means
2
2
 
3
- This page restates invariants: the ten per-team readiness checks the cloud runs, what each proves, its fix and who can click it; the machine checks the CLI adds; and the four replica verdicts. The live values are never restated: each check's severity and whether it needs a human's answer come from the contract's `readinessChecks[]`, each team's current states from `teams[].readiness`, and the people who can answer from `humans[]`. `node scripts/check.mjs` prints all of it; this page is how to read what it printed.
3
+ This page restates invariants: the per-team readiness checks the cloud runs, what each proves, its fix and who can click it; the machine checks the CLI adds; and the four replica verdicts. The live values are never restated: each check's severity and whether it needs a human's answer come from the contract's `readinessChecks[]`, each team's current states from `teams[].readiness`, and the people who can answer from `humans[]`. `node scripts/check.mjs` prints all of it; this page is how to read what it printed.
4
4
 
5
5
  ## How a team's readiness is scored
6
6
 
7
- A team's status is one of `ready`, `degraded`, `blocked` or `unchecked`. The engine always reports all eleven checks; a check it could not run is `unknown`, never `pass`. Any failing check the contract marks as blocking makes the team blocked; any unknown, and any failing check marked degrading, makes it degraded. Unchecked means no readiness pass has run for that team yet, which is a note, not a failure. Readiness is stamped with the account-wide mapping revision it was computed against, so a stale verdict is visible as such.
7
+ A team's status is one of `ready`, `degraded`, `blocked` or `unchecked`. The engine always reports every check the contract lists; a check it could not run is `unknown`, never `pass`. Any failing check the contract marks as blocking makes the team blocked; any unknown, and any failing check marked degrading, makes it degraded. Unchecked means no readiness pass has run for that team yet, which is a note, not a failure. Readiness is stamped with the account-wide mapping revision it was computed against, so a stale verdict is visible as such.
8
8
 
9
- ## The eleven checks
9
+ ## The checks
10
+
11
+ The contract's `readinessChecks[]` is the list of record. A check that appears there and not here is newer than this page; `node scripts/check.mjs` still prints it with its own severity, fix and owner, so read the printed line and say you did.
10
12
 
11
13
  | check id | proves | when it fails, the fix | who clicks |
12
14
  | -- | -- | -- | -- |
@@ -20,9 +22,12 @@ A team's status is one of `ready`, `degraded`, `blocked` or `unchecked`. The eng
20
22
  | `writes_land` | Catalyst has written to this team successfully | "no write observed" is waiting, not failing: it clears the first time Catalyst moves a ticket. "Write refused" means Linear rejected the last write: check the connection and the team's permissions | owner or admin when refused; otherwise nobody |
21
23
  | `webhook_covers_team` | events for this team are arriving | confirmed once a repository is registered and events flow; "no delivery observed" is waiting | owner or admin, by registering the repository |
22
24
  | `hosts_current` | no connected host runs an older mapping revision | a host that is behind re-loads the mapping on its next connect; a host that did not report its revision is flagged rather than assumed current; "no host connected" is waiting | whoever runs that host |
23
- | `environment_declared` | a committed environment declaration for the team's default repository is in effect (`.catalyst/environment.json` was ingested, is valid, and its latest proposal is approved) | `no_team_repo_default`: register a repository and make it the team's default; `no_environment_declaration`: commit the declaration file to that repository; `declaration_invalid` / `declaration_read_failed`: fix the file (the ingest names the error); `declaration_awaiting_approval`: an owner or admin approves the proposal in Settings → Environment |
25
+ | `environment_declared` | a committed environment declaration for the team's default repository is in effect (`catalyst.env.json` at the repository root was ingested, is valid, and its latest proposal is approved) | `no_team_repo_default`: register a repository and make it the team's default; `no_environment_declaration`: commit the declaration file to that repository; `declaration_invalid` / `declaration_read_failed`: fix the file (the ingest names the error); `declaration_awaiting_approval`: an owner or admin approves the proposal in Settings → Environment | owner or admin, except committing the file, which is whoever can push to the repository |
26
+ | `tools_resolvable` | every MCP server and CLI the team's approved environment declaration names can be resolved for that team's repository | `tool_reference_unresolved`: the declaration names a vault secret that does not exist — add it at the right scope or correct the declaration; `toolchain_cli_missing`: a declared CLI is not in the runner image, which is not self-service; `tool_declarations_unread` reads as `unknown` and clears itself | owner or admin, in the declaration or Settings → Environment |
27
+ | `reviewer_required` | this team's repository can merge under its own merge policy with the reviewers configured | it fails only under a strict merge policy with no reviewer — configure one at Settings → Repositories → Code reviews, or change the repository's merge policy; work still runs, only the merge waits | owner or admin, in settings |
28
+ | `reviewer_configured` | a code reviewer is configured for this team's repository at all, whatever the current policy requires | a *failing* `reviewer_configured` never blocks or degrades a team — with no reviewer, pull requests merge on green checks and resolved threads alone; configure one at Settings → Repositories → Code reviews. An `unknown` one (`merge_reviewer_unread`: the reviewer registry could not be read) still degrades the team, like any unknown | owner or admin, in settings; nobody is required when it fails |
24
29
 
25
- Three checks are informational by design (labels, event delivery, host currency): they degrade a team but never block it. Which are which is served on `readinessChecks[].severity`; do not memorise the split.
30
+ Some checks degrade a team without ever blocking it, and one is informational only — informational means a *fail* never moves the verdict, not that the check cannot: an unknown still degrades. Which check is which is served on `readinessChecks[].severity`; do not memorise the split.
26
31
 
27
32
  ## Reasons that look like failures and are not
28
33
 
@@ -37,10 +42,13 @@ Three checks are informational by design (labels, event delivery, host currency)
37
42
  | id | proves | fix |
38
43
  | -- | -- | -- |
39
44
  | `node` | Node 22 or newer, which the SDK's built-in SQLite engine needs | install Node 22+ |
40
- | `config` | this machine is connected: `customer.json` exists and loads | `CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login`; the person mints the key at Settings → API keys |
45
+ | `config` | this machine is connected: `customer.json` exists and loads | `npx @catalyst-cloud/catalyst-skills login` (keyless: the person approves in their browser); or, with a personal key minted at Settings → API keys, the same command prefixed with `CATALYST_CLOUD_TOKEN=<your personal key>` |
41
46
  | `contract` | the tenant contract is cached and its major version is one this bundle accepts | `catalyst-skills contract --refresh`; a version outside the range means update the bundle. A 403 naming an older cloud means the cloud has not yet deployed personal-key access |
47
+ | `bundle` | the installed CLI is at least the version this tenant requires | `npm install -g @catalyst-cloud/catalyst-skills@latest && catalyst-skills login`; a note, never a failure |
42
48
  | `cliPath` | the CLI path recorded at login still exists, so skill scripts can spawn it | re-run login |
43
49
  | `skills` | every skill of this bundle is present where the CLI installed them | `catalyst-skills install` |
50
+ | `cliRelease` | the installed CLI is not behind the newest published release | the same upgrade command as `bundle`; a note, never a failure |
51
+ | `skillsRelease` | the installed skill files are not behind the newest published bundle | `npx skills update -y`; a note, never a failure. A line saying the check could not run means the registry was unreachable, not that anything is wrong |
44
52
  | `sdk` | the SDK loads, so the replica and the watch are available | run under Node 22.15 or newer; every read still works through the API meanwhile |
45
53
  | `replica` | the optional replica is fresh | a note, never a failure; see below |
46
54
 
@@ -57,6 +65,14 @@ Three checks are informational by design (labels, event delivery, host currency)
57
65
 
58
66
  `--probe` adds the one network call: it compares the local cursor with the cloud's head and prints how far behind the replica is, the only honest "how stale" number. A skill must never refuse to work because the replica is down, and must never silently read a stale one; both halves are one exit code away.
59
67
 
68
+ ## The writer stops itself after repeated snapshot failures
69
+
70
+ `replica start` backs off with jitter after a failed or incomplete snapshot pull, and gives up after five consecutive snapshot failures rather than retrying forever — a writer that keeps failing never hammers the tenant. A snapshot that completes resets the count to zero. Both `catalyst-skills replica status` and `catalyst-skills ready` name the stopped state, the count, the last error, and the command that restarts it. The four exit codes above are unchanged: a stopped writer with a database already on disk still reads stale, since there truly is no live writer.
71
+
60
72
  ## Who can click what
61
73
 
62
74
  The last block `check.mjs` prints groups every failure by the person it needs. Machine fixes name "you", the person at the keyboard. Tenant fixes name the owner or admin roles the contract lists (as Linear user ids, since that is how the cloud knows them), because the settings page that repairs a mapping, a connection or a label is theirs. A check the contract marks as not needing an answer names nobody: it is informational or self-clearing. This skill reports; it never repairs, because no key-reachable repair verb exists yet.
75
+
76
+ ## Setting up one team at a time
77
+
78
+ A team starts receiving work only once its stages are saved, and that is done one team at a time: a tenant owner or admin opens Settings → Linear teams, picks the team and presses Map my stages (or Adopt the Catalyst workflow). No other team's stages or tickets change; only the labels Adopt creates are shared across the workspace. Before saving, the screen lists which of that team's tickets would start and which stay where they are. Once saved, the tickets in the team's dispatch stage start; tickets in other stages that Catalyst never worked on stay where they are. To pilot safely, pick a low-stakes team, move anything in its dispatch stage that should not start back to Backlog, and leave the other teams unmapped. `gitAutomation` in the contract plays no part: nothing reads it.
@@ -8,9 +8,10 @@ import { spawnSync } from "node:child_process";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
11
12
 
12
13
  export const PACKAGE = "@catalyst-cloud/catalyst-skills";
13
- export const CONNECT_HINT = `this machine is not connected to a tenant yet — run: CATALYST_CLOUD_TOKEN=<your personal key> npx ${PACKAGE} login`;
14
+ export const CONNECT_HINT = `this machine is not connected to a tenant yet — run: ${CONNECT_COMMAND}`;
14
15
 
15
16
  /** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME before HOME. */
16
17
  export function configPath() {
@@ -32,7 +33,7 @@ export function requireCustomerConfig() {
32
33
  console.error(`${path} could not be read (${err instanceof Error ? err.message : String(err)}) — ${CONNECT_HINT}`);
33
34
  process.exit(2);
34
35
  }
35
- if (!cfg || typeof cfg !== "object" || typeof cfg.key !== "string" || typeof cfg.baseUrl !== "string") {
36
+ if (!hasCredential(cfg) || typeof cfg.baseUrl !== "string") {
36
37
  console.error(`${path} is missing required fields — ${CONNECT_HINT}`);
37
38
  process.exit(2);
38
39
  }
@@ -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
+ }
@@ -14,7 +14,7 @@ const HELP = `Usage: node scripts/replica-status.mjs [--probe] [--json]
14
14
  Exit codes, passed through from catalyst-skills replica status:
15
15
  0 fresh: a live writer, a young heartbeat, a cursor — skills read the replica
16
16
  1 stale: the file exists but the writer is gone or behind — skills read the API and say so
17
- 2 not connected to a tenant — run: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login
17
+ 2 not connected to a tenant — run: npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)
18
18
  3 absent: no replica file — optional; start one with: catalyst-skills replica start --detach`;
19
19
 
20
20
  const args = process.argv.slice(2);
@@ -5,7 +5,7 @@ description: >-
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.7.0 — written in this repository for customer tenants -->
9
9
 
10
10
  # Connect me
11
11
 
@@ -10,6 +10,7 @@ import { spawn } from "node:child_process";
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
13
14
 
14
15
  export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
15
16
  export const NOT_CONFIGURED_EXIT = 2;
@@ -33,7 +34,7 @@ export function loadCustomerConfig() {
33
34
  if (!existsSync(path)) return null;
34
35
  try {
35
36
  const parsed = JSON.parse(readFileSync(path, "utf8"));
36
- if (!parsed || typeof parsed !== "object" || typeof parsed.key !== "string" || typeof parsed.account !== "string") return null;
37
+ if (!hasCredential(parsed) || typeof parsed.account !== "string") return null;
37
38
  return parsed;
38
39
  } catch {
39
40
  return null;
@@ -44,7 +45,7 @@ export function loadCustomerConfig() {
44
45
  export function requireConfigured() {
45
46
  const cfg = loadCustomerConfig();
46
47
  if (cfg) return cfg;
47
- console.error(`not connected: ${configPath()} is missing or unreadable — run: CATALYST_CLOUD_TOKEN=<your personal key> npx ${PACKAGE_NAME} login`);
48
+ console.error(`not connected: ${configPath()} is missing or unreadable — run: ${CONNECT_COMMAND}`);
48
49
  process.exit(NOT_CONFIGURED_EXIT);
49
50
  }
50
51
 
@@ -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
  How Catalyst Cloud runs a ticket on the customer's own tenant, as facts an agent loads on demand: the eight-phase ladder and what each phase produces, the eleven board slots and this team's live stage map, what happens when a phase fails (retry, backoff, Remediate, park), how the queue is ordered and routed and every reason a ticket is excluded, and the coding-account model. Use when a person asks "how does this work?", "why did it do that?", "why is this stuck?", "what runs next?" or "how does it prioritise?". Read-only; its scripts explain one ticket's eligibility in plain English, show what is running and queued, and print the tenant's stage map and thresholds straight from the contract.
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.7.0 — written in this repository for customer tenants -->
8
8
 
9
9
  # How Catalyst works
10
10
 
@@ -28,7 +28,7 @@ The contract's `teams[].workflowMode` reports which one a team is in:
28
28
  - **mapped**: the team kept its existing stages and a human chose which stage fills each slot.
29
29
  - **mixed**: some slots adopted, some hand-chosen.
30
30
 
31
- `teams[].gitAutomation` is a separate switch, off by default, because enabling it can delete a team's own review automation in Linear; it is never bundled into "adopt recommended".
31
+ `teams[].gitAutomation` is a stored consent for a feature that is not built yet (Catalyst managing a team's Linear git automations); nothing reads it, so `off` never stops work and turning it on would start none. What decides whether a team's tickets start is the mapping above: until dispatch, pr, done and canceled each point at a live stage, Catalyst starts nothing in that team, and `explain` says so by name.
32
32
 
33
33
  ## The state id is the authority; names are display
34
34
 
@@ -51,4 +51,4 @@ Reading it for a question:
51
51
 
52
52
  ## Readiness over the mapping
53
53
 
54
- The contract carries a ten-check readiness vector per team (`teams[].readiness`), including whether every mapped state exists, whether the mapping is total, whether the types are compatible, whether the labels are present, whether writes land and whether the webhook covers the team. A check the cloud could not run is `unknown`, never `pass`. `catalyst-setup` reads this vector; this skill only points at it.
54
+ The contract carries a readiness vector per team (`teams[].readiness`), including whether every mapped state exists, whether the mapping is total, whether the types are compatible, whether the labels are present, whether writes land and whether the webhook covers the team. A check the cloud could not run is `unknown`, never `pass`. `catalyst-setup` reads this vector; this skill only points at it.
@@ -66,7 +66,7 @@ No survivor is `no_eligible_account_slot` when any candidate was skipped on capa
66
66
 
67
67
  ## The unknowns (the evaluator fails closed)
68
68
 
69
- When the cloud cannot answer, it says so rather than guessing: `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown`, `branch_snapshot_unknown`. An unknown is "I could not look", never "it is not there"; report it as inconclusive.
69
+ When the cloud cannot answer, it says so rather than guessing: `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown`, `branch_snapshot_unknown`. An unknown is "I could not look", never "it is not there"; report it as inconclusive. The exception is a team with no saved stage mapping: `workflow_mapping_unknown` never clears on its own there, `explain` names the missing stages, and the fix is a tenant owner or admin mapping the team.
70
70
 
71
71
  One advisory gates nothing: `human_addressed_unlabeled_ask_suspect` (assigned to a human with no delegate; possibly an unlabelled ask).
72
72
 
@@ -33,12 +33,12 @@ Which parks release themselves:
33
33
 
34
34
  | Park | Releases by |
35
35
  | -- | -- |
36
- | repeated failure | an operator, not a clock |
37
- | remediate round cap reached | an operator, not a clock |
36
+ | repeated failure | a release from the person's own login once its cause is fixed (`catalyst-skills release`), not a clock |
37
+ | remediate round cap reached | the same release, which buys one more round, not a clock |
38
38
  | missing branch | its own budgeted release loop |
39
39
  | rebase conflict | its own budgeted release loop |
40
40
 
41
- Your key cannot release a park; the repair verbs are operator-only. When a ticket is parked, say so, name the failure class the explainer shows, and hand the release to whoever administers the tenant.
41
+ A person's own key releases a park, and every other hold that a retry can fix, in one step: `catalyst-skills release <ticket> --because <what changed>`. The cloud releases every governor holding the ticket or releases nothing and names what a person must do instead (a person's own pull request, a review that will not converge, the lifetime repair budget). It refuses a cause it cannot see change unless the caller says what changed. The `unstick` skill runs that loop. When a ticket is parked, name the failure class the explainer shows and whether its cause is fixed.
42
42
 
43
43
  ## Two holds that are not failures
44
44
 
@@ -53,5 +53,5 @@ Each attempt posts a phase-outcome comment (complete or FAILED, with phase, atte
53
53
 
54
54
  1. Run `node scripts/explain-ticket.mjs <ticket>`; the reason names the layer.
55
55
  2. `retry_backoff` or `routing_unavailable`: wait; it retries itself. Say when.
56
- 3. `cooling_down`, `remediate_parked`, `no_change_hold`, `validate_class_spent`: name what releases it (an operator, a comment, a push) and who can do that.
56
+ 3. `cooling_down`, `remediate_parked`, `no_change_hold`, `validate_class_spent`: name what releases it (a release once the cause is fixed, a comment, a push) and who can do that.
57
57
  4. A system-level cause (provider down, runner image breaker, repo paused) is ONE alert, never a per-ticket escalation.
@@ -6,9 +6,10 @@ import { spawnSync } from "node:child_process";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
+ import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
9
10
 
10
11
  export const PACKAGE = "@catalyst-cloud/catalyst-skills";
11
- export const CONNECT_HINT = `CATALYST_CLOUD_TOKEN=<your personal key> npx ${PACKAGE} login`;
12
+ export const CONNECT_HINT = CONNECT_COMMAND;
12
13
 
13
14
  /** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME (used by tests) over HOME. */
14
15
  export function configPath() {
@@ -22,8 +23,8 @@ export function loadConfig() {
22
23
  if (!existsSync(path)) return { ok: false, reason: `no config at ${path}` };
23
24
  try {
24
25
  const cfg = JSON.parse(readFileSync(path, "utf8"));
25
- if (!cfg || typeof cfg.key !== "string" || typeof cfg.account !== "string") {
26
- return { ok: false, reason: `${path} is missing the key or account` };
26
+ if (!hasCredential(cfg) || typeof cfg.account !== "string") {
27
+ return { ok: false, reason: `${path} is missing a key or login, or the account` };
27
28
  }
28
29
  return { ok: true, cfg, path };
29
30
  } catch (err) {
@@ -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
+ }
@@ -5,7 +5,7 @@ description: >-
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.7.0 — written in this repository for customer tenants -->
9
9
 
10
10
  # Run this project
11
11
 
@@ -42,4 +42,4 @@ Every script exits 2 when this machine is not connected (run `catalyst-skills lo
42
42
  - **Escalate inward.** Instrument, then you, then the desk, then the human as an ask filed through `what-needs-me` with what it blocks. Decide the technical calls yourself; take the sane default and record it; a fleet or provider condition is one note, never one ask per ticket.
43
43
  - **Reply where the message arrived,** in-thread, tagged, as the app actor. Never post as the human, never answer someone else's ask. Bookkeeping records take the contract's marker (`catalyst-skills write comment --bookkeeping`).
44
44
  - **One status summary,** kept current after every reaction: in flight, blocked and on whom, closed, next, each line with a ticket id. Where a human decision is pending, the line carries the ask's id.
45
- - **Say what a key cannot see.** A park is released only by an operator, and PR labels and reactions are not mirrored; the CLI says so by name and points at settings. Execution history (`explain --history`) and coding-account status (`accounts`) ARE readable — read them rather than reconstructing them from comments.
45
+ - **Say what a key cannot see.** PR labels and reactions are not mirrored; the CLI says so by name and points at settings. A park or hold is released from the person's own login once its cause is fixed, through the `unstick` skill (`catalyst-skills release`). Execution history (`explain --history`) and coding-account status (`accounts`) ARE readable — read them rather than reconstructing them from comments.