@catalyst-cloud/cli 0.8.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 (157) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/bin/catalyst-skills.js +8 -0
  5. package/bin/catalyst.js +5 -0
  6. package/bin/launch.js +154 -0
  7. package/dist/args.js +280 -0
  8. package/dist/ask.js +161 -0
  9. package/dist/browser.js +20 -0
  10. package/dist/cli.js +397 -0
  11. package/dist/config.js +241 -0
  12. package/dist/contract-types.js +4 -0
  13. package/dist/contract.js +184 -0
  14. package/dist/detach.js +10 -0
  15. package/dist/environment.js +207 -0
  16. package/dist/errors.js +27 -0
  17. package/dist/events.js +106 -0
  18. package/dist/execution.js +451 -0
  19. package/dist/oauth.js +300 -0
  20. package/dist/pagination.js +76 -0
  21. package/dist/prompt.js +35 -0
  22. package/dist/published.js +79 -0
  23. package/dist/query.js +248 -0
  24. package/dist/ready.js +380 -0
  25. package/dist/release.js +142 -0
  26. package/dist/replica.js +614 -0
  27. package/dist/runtime-store.js +135 -0
  28. package/dist/runtime-verb.js +66 -0
  29. package/dist/runtime.js +87 -0
  30. package/dist/sdk.js +29 -0
  31. package/dist/secret.js +190 -0
  32. package/dist/semver.js +18 -0
  33. package/dist/skill-shape.js +189 -0
  34. package/dist/skills.js +129 -0
  35. package/dist/transport.js +205 -0
  36. package/dist/ts-deps-loader.js +113 -0
  37. package/dist/watch/consumer.js +141 -0
  38. package/dist/watch/cursor-file.js +62 -0
  39. package/dist/watch.js +175 -0
  40. package/dist/write.js +224 -0
  41. package/package.json +60 -0
  42. package/skills/catalyst-github/SKILL.md +35 -0
  43. package/skills/catalyst-github/agents/openai.yaml +6 -0
  44. package/skills/catalyst-github/agents/portability.yaml +4 -0
  45. package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
  46. package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
  47. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
  48. package/skills/catalyst-github/scripts/lib/cli.mjs +103 -0
  49. package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
  50. package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
  51. package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
  52. package/skills/catalyst-linear/SKILL.md +43 -0
  53. package/skills/catalyst-linear/agents/openai.yaml +6 -0
  54. package/skills/catalyst-linear/agents/portability.yaml +5 -0
  55. package/skills/catalyst-linear/references/reading-a-ticket.md +52 -0
  56. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
  57. package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
  58. package/skills/catalyst-linear/scripts/comment.mjs +59 -0
  59. package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
  60. package/skills/catalyst-linear/scripts/label.mjs +48 -0
  61. package/skills/catalyst-linear/scripts/lib/cli.mjs +164 -0
  62. package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
  63. package/skills/catalyst-linear/scripts/move.mjs +41 -0
  64. package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
  65. package/skills/catalyst-linear/scripts/search.mjs +49 -0
  66. package/skills/catalyst-onboard/SKILL.md +57 -0
  67. package/skills/catalyst-onboard/agents/openai.yaml +6 -0
  68. package/skills/catalyst-onboard/agents/portability.yaml +5 -0
  69. package/skills/catalyst-onboard/references/declaring-a-repository.md +23 -0
  70. package/skills/catalyst-onboard/references/skill-sources.md +35 -0
  71. package/skills/catalyst-onboard/references/the-one-path.md +149 -0
  72. package/skills/catalyst-onboard/references/what-a-phase-needs.md +46 -0
  73. package/skills/catalyst-onboard/references/what-the-browser-owns.md +50 -0
  74. package/skills/catalyst-onboard/references/who-fixes-what.md +44 -0
  75. package/skills/catalyst-onboard/scripts/lib/cli.mjs +117 -0
  76. package/skills/catalyst-onboard/scripts/lib/credential.mjs +29 -0
  77. package/skills/catalyst-onboard/scripts/where-am-i.mjs +345 -0
  78. package/skills/catalyst-setup/SKILL.md +36 -0
  79. package/skills/catalyst-setup/agents/openai.yaml +6 -0
  80. package/skills/catalyst-setup/agents/portability.yaml +4 -0
  81. package/skills/catalyst-setup/references/what-each-check-means.md +88 -0
  82. package/skills/catalyst-setup/scripts/check.mjs +75 -0
  83. package/skills/catalyst-setup/scripts/lib/cli.mjs +103 -0
  84. package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
  85. package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
  86. package/skills/connect-me/SKILL.md +63 -0
  87. package/skills/connect-me/agents/openai.yaml +6 -0
  88. package/skills/connect-me/agents/portability.yaml +5 -0
  89. package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
  90. package/skills/connect-me/scripts/lib/cli.mjs +185 -0
  91. package/skills/connect-me/scripts/lib/credential.mjs +29 -0
  92. package/skills/connect-me/scripts/verify-connection.mjs +68 -0
  93. package/skills/how-catalyst-works/SKILL.md +43 -0
  94. package/skills/how-catalyst-works/agents/openai.yaml +6 -0
  95. package/skills/how-catalyst-works/agents/portability.yaml +4 -0
  96. package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
  97. package/skills/how-catalyst-works/references/stages-and-mapping.md +56 -0
  98. package/skills/how-catalyst-works/references/the-ladder.md +41 -0
  99. package/skills/how-catalyst-works/references/what-catalyst-is.md +30 -0
  100. package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
  101. package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
  102. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
  103. package/skills/how-catalyst-works/scripts/lib/cli.mjs +164 -0
  104. package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
  105. package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
  106. package/skills/how-catalyst-works/scripts/whats-running.mjs +65 -0
  107. package/skills/run-this-project/SKILL.md +45 -0
  108. package/skills/run-this-project/agents/openai.yaml +6 -0
  109. package/skills/run-this-project/agents/portability.yaml +5 -0
  110. package/skills/run-this-project/assets/stall-policy.json +15 -0
  111. package/skills/run-this-project/references/making-work-ready.md +60 -0
  112. package/skills/run-this-project/references/reacting-to-events.md +76 -0
  113. package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
  114. package/skills/run-this-project/scripts/lib/cli.mjs +185 -0
  115. package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
  116. package/skills/run-this-project/scripts/make-ready.mjs +64 -0
  117. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  118. package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
  119. package/skills/unstick/SKILL.md +41 -0
  120. package/skills/unstick/agents/openai.yaml +6 -0
  121. package/skills/unstick/agents/portability.yaml +5 -0
  122. package/skills/unstick/references/playbook.md +51 -0
  123. package/skills/unstick/scripts/lib/cli.mjs +135 -0
  124. package/skills/unstick/scripts/lib/credential.mjs +29 -0
  125. package/skills/unstick/scripts/unstick.mjs +57 -0
  126. package/skills/what-needs-me/SKILL.md +41 -0
  127. package/skills/what-needs-me/agents/openai.yaml +6 -0
  128. package/skills/what-needs-me/agents/portability.yaml +5 -0
  129. package/skills/what-needs-me/references/raising-a-decision.md +41 -0
  130. package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
  131. package/skills/what-needs-me/references/settling-an-answer.md +37 -0
  132. package/skills/what-needs-me/scripts/inbox.mjs +56 -0
  133. package/skills/what-needs-me/scripts/lib/cli.mjs +135 -0
  134. package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
  135. package/skills/what-needs-me/scripts/raise.mjs +53 -0
  136. package/skills/what-needs-me/scripts/settle.mjs +73 -0
  137. package/skills/whats-happening/SKILL.md +43 -0
  138. package/skills/whats-happening/agents/openai.yaml +6 -0
  139. package/skills/whats-happening/agents/portability.yaml +4 -0
  140. package/skills/whats-happening/assets/status-reply.json +77 -0
  141. package/skills/whats-happening/references/reading-the-board.md +43 -0
  142. package/skills/whats-happening/references/reprioritising.md +37 -0
  143. package/skills/whats-happening/references/routing-work.md +36 -0
  144. package/skills/whats-happening/references/status-reply.md +34 -0
  145. package/skills/whats-happening/references/why-is-it-stuck.md +62 -0
  146. package/skills/whats-happening/scripts/explain.mjs +28 -0
  147. package/skills/whats-happening/scripts/lib/cli.mjs +135 -0
  148. package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
  149. package/skills/whats-happening/scripts/snapshot.mjs +149 -0
  150. package/vendor/README.md +9 -0
  151. package/vendor/paths/index.d.ts +85 -0
  152. package/vendor/paths/index.js +148 -0
  153. package/vendor/paths/legacy-installer.d.ts +36 -0
  154. package/vendor/paths/legacy-installer.js +154 -0
  155. package/vendor/paths/node.d.ts +18 -0
  156. package/vendor/paths/node.js +102 -0
  157. package/vendor/paths/provenance.json +17 -0
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: spawn the catalyst-skills CLI.
3
+ // The CLI holds the SDK and the key; this file holds neither. It reads customer.json only to learn
4
+ // where the CLI lives. Run any script beside this one with --help; this file is a library.
5
+ //
6
+ // ⛔ THIS LAUNCHER DOES NOT EXIT ON AN UNCONNECTED MACHINE, and that is deliberate. Every other
7
+ // skill runs against a tenant, so "not connected" is a precondition it refuses on. This skill's job
8
+ // is to REPORT where setup has got to, and "this machine holds no credential" is one of the states
9
+ // it has to be able to report — refusing to run would make the first step of onboarding unreadable.
10
+ // So `tryLoadConfig()` returns null instead of exiting, and the caller decides what that means.
11
+ import { spawnSync } from "node:child_process";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
15
+
16
+ export const PACKAGE = "@catalyst-cloud/catalyst-skills";
17
+ export const NOT_CONFIGURED_EXIT = 2;
18
+ export const CONNECT_LINE = CONNECT_COMMAND;
19
+
20
+ export function configPath() {
21
+ const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/";
22
+ return join(home, ".config", "catalyst-cloud", "customer.json");
23
+ }
24
+
25
+ /** The stored config, or null when it is absent, unreadable, or holds no credential. Never exits. */
26
+ export function tryLoadConfig() {
27
+ const path = configPath();
28
+ if (!existsSync(path)) return null;
29
+ try {
30
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
31
+ return hasCredential(cfg) ? cfg : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** Where the CLI is on this machine: the path login recorded when it still exists, else npx. */
38
+ export function cliTarget(cfg = tryLoadConfig()) {
39
+ const recorded = cfg !== null && typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
40
+ return recorded
41
+ ? { command: process.execPath, prefix: [cfg.cliPath], via: `node ${cfg.cliPath}`, recorded: true }
42
+ : { command: "npx", prefix: [PACKAGE], via: `npx ${PACKAGE}`, recorded: false };
43
+ }
44
+
45
+ /**
46
+ * Run one catalyst-skills verb. Returns {code, stdout, stderr, ran} and never exits: a verb that
47
+ * refuses because the machine is not connected is a READING, not a failure of this script.
48
+ * `ran` is false when the CLI could not be started at all.
49
+ */
50
+ export function runCli(args) {
51
+ const target = cliTarget();
52
+ const res = spawnSync(target.command, [...target.prefix, ...args], {
53
+ encoding: "utf8",
54
+ maxBuffer: 64 * 1024 * 1024,
55
+ shell: !target.recorded && process.platform === "win32",
56
+ env: process.env,
57
+ });
58
+ if (res.error) return { code: 1, stdout: "", stderr: `could not run ${target.via}: ${res.error.message}`, ran: false };
59
+ return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "", ran: true };
60
+ }
61
+
62
+ /** Parse a verb's --json stdout, or null when it did not answer with JSON. Never exits. */
63
+ export function tryJson(stdout) {
64
+ try {
65
+ const value = JSON.parse(stdout.trim());
66
+ return value === null || typeof value !== "object" ? null : value;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** Minimal flag parsing: --name value, --name=value, --flag. */
73
+ export function parseFlags(argv, spec) {
74
+ const flags = {};
75
+ const positionals = [];
76
+ for (let i = 0; i < argv.length; i++) {
77
+ const a = argv[i];
78
+ if (a === "--help" || a === "-h") return { help: true, flags, positionals };
79
+ if (!a.startsWith("--")) {
80
+ positionals.push(a);
81
+ continue;
82
+ }
83
+ let name = a.slice(2);
84
+ let value;
85
+ const eq = name.indexOf("=");
86
+ if (eq !== -1) {
87
+ value = name.slice(eq + 1);
88
+ name = name.slice(0, eq);
89
+ }
90
+ const s = spec[name];
91
+ if (!s) {
92
+ process.stderr.write(`unknown option --${name} (try --help)\n`);
93
+ process.exit(1);
94
+ }
95
+ if (s.value) {
96
+ if (value === undefined) value = argv[++i];
97
+ if (value === undefined || value === "") {
98
+ process.stderr.write(`--${name} needs a value\n`);
99
+ process.exit(1);
100
+ }
101
+ flags[name] = value;
102
+ } else flags[name] = true;
103
+ }
104
+ return { help: false, flags, positionals };
105
+ }
106
+
107
+ export function printHelp(usage, spec, notes = []) {
108
+ const lines = [`Usage: ${usage}`, ""];
109
+ const names = Object.keys(spec);
110
+ if (names.length) {
111
+ lines.push("Options:");
112
+ for (const n of names) lines.push(` --${n}${spec[n].value ? " <value>" : ""} ${spec[n].help}`);
113
+ lines.push("");
114
+ }
115
+ lines.push(...notes, "", "Exit codes: 0 every readable part is finished · 1 something is unfinished (the report says what and whose) · 2 the CLI could not be run at all");
116
+ process.stdout.write(lines.join("\n") + "\n");
117
+ }
@@ -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
+ }
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+ // where-am-i.mjs — how far has setup got? SEVEN 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), `contract --path …` for the account, the projects and the repositories,",
17
+ "`contract --path codingAccounts` (coding accounts; `accounts --json` on an older cloud), and each",
18
+ "project's hosts_current check with its fixedWhere (host).",
19
+ "Writes nothing and changes nothing. Runs before this machine is connected — that is one of the states it reports.",
20
+ ];
21
+
22
+ const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
23
+ if (help) {
24
+ printHelp("node scripts/where-am-i.mjs [--next] [--json]", SPEC, NOTES);
25
+ process.exit(0);
26
+ }
27
+ if (positionals.length > 0) {
28
+ process.stderr.write(`unexpected argument: ${positionals[0]} (try --help)\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const via = cliTarget().via;
33
+ const parts = [];
34
+ // `blocking` is false for a finding that is real and reportable but does not stop the next step —
35
+ // an unmatched Linear identity is the one that matters: it must be said, and it must not become the
36
+ // thing the person is told to go and do before they can map a project.
37
+ const add = (part, instrument, verdict, lines, owner = null, where = null, blocking = true) =>
38
+ parts.push({ part, instrument, verdict, lines, owner, where, blocking });
39
+
40
+ // ── machine ───────────────────────────────────────────────────────────────────────────────────────
41
+ const status = runCli(["status"]);
42
+ if (!status.ran) {
43
+ process.stderr.write(`${status.stderr}\n`);
44
+ process.stderr.write(`the catalyst-skills CLI could not be started. Install it, then run this again.\n`);
45
+ process.exit(2);
46
+ }
47
+ const statusLines = status.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
48
+ const field = (name) => {
49
+ const line = statusLines.find((l) => l.startsWith(`${name}:`));
50
+ return line ? line.slice(name.length + 1).trim() : null;
51
+ };
52
+ const tenant = field("Tenant");
53
+ const api = field("API");
54
+ const connected = tenant !== null && tryLoadConfig() !== null;
55
+ // The app and the API share an origin; the base URL is whatever this machine was connected to, so
56
+ // no page link in this report is a host anyone typed from memory.
57
+ const cloud = api === null ? null : api.split(" ")[0].replace(/\/+$/, "");
58
+ const link = (path) => (cloud === null ? `<your cloud>${path}` : `${cloud}${path}`);
59
+
60
+ const machineLines = connected ? statusLines : [statusLines[0] ?? "status printed nothing"];
61
+ let machineVerdict = connected ? "ok" : "unfinished";
62
+
63
+ // `ready` is ONE verdict over two parts; split it by check id before anything is reported. A check
64
+ // whose id begins with "team:" belongs to a project and cannot be moved from this machine.
65
+ let projectChecks = [];
66
+ let machineFix = null;
67
+ if (connected) {
68
+ const ready = runCli(["ready", "--json"]);
69
+ const report = tryJson(ready.stdout);
70
+ const checks = Array.isArray(report?.checks) ? report.checks : null;
71
+ if (checks === null) {
72
+ machineLines.push(`ready: could not be read (${(ready.stderr || ready.stdout).trim().split("\n")[0] ?? "no output"})`);
73
+ machineVerdict = "unfinished";
74
+ } else {
75
+ projectChecks = checks.filter((c) => typeof c.id === "string" && c.id.startsWith("team:"));
76
+ const machineChecks = checks.filter((c) => !(typeof c.id === "string" && c.id.startsWith("team:")));
77
+ const failed = machineChecks.filter((c) => !c.ok && !c.note);
78
+ 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}` : ""}`);
79
+ if (failed.length > 0) {
80
+ machineVerdict = "unfinished";
81
+ machineFix = failed.find((c) => typeof c.fix === "string")?.fix ?? null;
82
+ }
83
+ }
84
+ }
85
+ add(
86
+ "machine",
87
+ "catalyst-skills status, and the non-team checks of catalyst-skills ready",
88
+ machineVerdict,
89
+ machineLines,
90
+ "you, on this machine",
91
+ connected ? null : CONNECT_LINE,
92
+ );
93
+
94
+ // ── person ────────────────────────────────────────────────────────────────────────────────────────
95
+ if (!connected) {
96
+ add("person", "catalyst-skills me", "unreadable", ["not readable until this machine is connected"], null, null);
97
+ } else {
98
+ const me = runCli(["me", "--json"]);
99
+ const doc = tryJson(me.stdout);
100
+ const user = doc && typeof doc.user === "object" && doc.user !== null ? doc.user : null;
101
+ if (doc === null) {
102
+ add("person", "catalyst-skills me", "unreadable", [`me could not be read (${(me.stderr || me.stdout).trim().split("\n")[0] ?? "no output"})`], null, null);
103
+ } else if (user === null) {
104
+ add(
105
+ "person",
106
+ "catalyst-skills me",
107
+ "unfinished",
108
+ ["this credential names no person — it is a host credential, so nothing an agent writes will carry a name"],
109
+ "you: connect again with your own login",
110
+ CONNECT_LINE,
111
+ );
112
+ } else {
113
+ const matched = typeof user.linearUserId === "string" && user.linearUserId !== "";
114
+ add(
115
+ "person",
116
+ "catalyst-skills me",
117
+ matched ? "ok" : "unfinished",
118
+ [`${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."],
119
+ matched ? null : "a tenant owner or admin",
120
+ matched ? null : link("/settings/account"),
121
+ false,
122
+ );
123
+ }
124
+ }
125
+
126
+ // ── account ───────────────────────────────────────────────────────────────────────────────────────
127
+ if (!connected) {
128
+ add("account", "catalyst-skills contract --path account", "unreadable", ["not readable until this machine is connected"], null, null);
129
+ } else {
130
+ const acct = runCli(["contract", "--path", "account", "--json"]);
131
+ const doc = tryJson(acct.stdout);
132
+ if (doc === null) {
133
+ add("account", "catalyst-skills contract --path account", "unreadable", ["the account block could not be read — try: catalyst-skills contract --refresh"], null, null);
134
+ } else {
135
+ const workspace = typeof doc.linearWorkspaceSlug === "string" && doc.linearWorkspaceSlug !== "" ? doc.linearWorkspaceSlug : typeof doc.linearWorkspaceId === "string" && doc.linearWorkspaceId !== "" ? doc.linearWorkspaceId : null;
136
+ // The declaration is the one part of the account a key can also READ — and it is the one part a
137
+ // key can WRITE, so it is reported here rather than left to the settings page like the rest.
138
+ const envLines = [];
139
+ const env = runCli(["environment", "read", "--json"]);
140
+ const envDoc = tryJson(env.stdout);
141
+ if (envDoc === null) {
142
+ envLines.push(`environment: not readable (${(env.stderr || env.stdout).trim().split("\n")[0] ?? "no output"})`);
143
+ } else if (envDoc.current === null || envDoc.current === undefined) {
144
+ envLines.push("environment: nothing declared yet — declare it with `catalyst-skills environment propose --file <path> --approve`");
145
+ } else {
146
+ envLines.push(`environment: revision ${envDoc.current.revision} (${envDoc.current.canonicalHash}), ${envDoc.isApproved ? "approved" : "NOT approved — a proposal nobody approved changes nothing"}`);
147
+ envLines.push(envDoc.delivered ? `environment delivered to phases: revision ${envDoc.delivered.revision}` : "environment delivered to phases: nothing yet");
148
+ const unresolved = Array.isArray(envDoc.unresolvedReferences) ? envDoc.unresolvedReferences : [];
149
+ if (unresolved.length > 0) envLines.push(`environment names values this tenant does not carry yet: ${unresolved.join(", ")}`);
150
+ }
151
+ add(
152
+ "account",
153
+ "catalyst-skills contract --path account, and catalyst-skills environment read",
154
+ workspace === null ? "unfinished" : "ok",
155
+ [
156
+ `${doc.name ?? "(unnamed tenant)"} (${doc.slug ?? "?"})`,
157
+ workspace === null ? "no Linear workspace resolved on this contract" : `Linear workspace resolved: ${workspace}`,
158
+ "the GitHub App install is NOT carried here — the connections page is the only place that shows it",
159
+ ...envLines,
160
+ ],
161
+ "a tenant owner or admin",
162
+ link("/settings/connections"),
163
+ );
164
+ }
165
+ }
166
+
167
+ // The host part reads the same rows, so they are kept rather than read twice.
168
+ let teamRows = null;
169
+ // ── projects (a project is one Linear team) ───────────────────────────────────────────────────────
170
+ if (!connected) {
171
+ add("projects", "catalyst-skills contract --path teams", "unreadable", ["not readable until this machine is connected"], null, null);
172
+ } else {
173
+ const teams = runCli(["contract", "--path", "teams", "--json"]);
174
+ const doc = tryJson(teams.stdout);
175
+ const rows = Array.isArray(doc) ? doc : null;
176
+ teamRows = rows;
177
+ if (rows === null) {
178
+ add("projects", "catalyst-skills contract --path teams", "unreadable", ["the project list could not be read — try: catalyst-skills contract --refresh"], null, null);
179
+ } else {
180
+ const lines = [`${rows.length} mapped`];
181
+ for (const t of rows) {
182
+ const key = t.key ?? t.id ?? "(unkeyed)";
183
+ const readiness = t.readiness ?? {};
184
+ const bad = Array.isArray(readiness.checks) ? readiness.checks.filter((c) => c.state !== "pass") : [];
185
+ lines.push(`${key}: ${readiness.status ?? "unknown"}${bad.length ? ` — ${bad.map((c) => `${c.id} ${c.state}`).join(", ")}` : ""}`);
186
+ }
187
+ for (const c of projectChecks) lines.push(`${c.ok ? "note" : "FAIL"} ${c.line}${c.who ? ` — who: ${c.who}` : ""}`);
188
+ 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.");
189
+ // A project is set up when its dispatch gate is open (its stages are mapped), or when its
190
+ // readiness reads ready. Readiness alone kept `--next` on "map its stages" for a tenant whose
191
+ // gates were open: readiness stays "unchecked" until someone presses Re-check, and a new team
192
+ // stays "degraded" until a repository is attached, which is the step AFTER this one.
193
+ const setUp = (t) => t.dispatchGate?.status === "open" || (t.readiness?.status ?? "unchecked") === "ready";
194
+ for (const t of rows) {
195
+ if (t.dispatchGate?.status === "open" && (t.readiness?.status ?? "unchecked") === "unchecked") {
196
+ lines.push(`${t.key ?? t.id ?? "(unkeyed)"}: stages mapped; readiness not checked yet: press Re-check on the page below to see the rest`);
197
+ }
198
+ }
199
+ const ready = rows.length > 0 && rows.every(setUp);
200
+ 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"));
201
+ }
202
+ }
203
+
204
+ // ── repositories ──────────────────────────────────────────────────────────────────────────────────
205
+ if (!connected) {
206
+ add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["not readable until this machine is connected"], null, null);
207
+ } else {
208
+ const repos = runCli(["contract", "--path", "merge.repositories", "--json"]);
209
+ const doc = tryJson(repos.stdout);
210
+ const rows = Array.isArray(doc) ? doc : null;
211
+ if (rows === null) {
212
+ add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["the repository list could not be read — try: catalyst-skills contract --refresh"], null, null);
213
+ } else {
214
+ const lines = [`${rows.length} registered`, ...rows.map((r) => `${r.owner ?? "?"}/${r.name ?? "?"}`)];
215
+ lines.push("⛔ REGISTRATION only. This carries no status and no project attachment, so it never proves a repository can be dispatched into.");
216
+ add("repositories", "catalyst-skills contract --path merge.repositories", rows.length > 0 ? "ok" : "unfinished", lines, "a tenant owner or admin", link("/settings/repositories"));
217
+ }
218
+ }
219
+
220
+ // ── coding accounts ───────────────────────────────────────────────────────────────────────────────
221
+ // A phase runs on one of the tenant's enrolled coding accounts. With none, every step above can be
222
+ // finished and nothing will ever start, so this part blocks the "ready" line like any other.
223
+ // The contract says the state, its sentence, who enrolls one and on which page. An older cloud's
224
+ // contract has no `codingAccounts`, and only then is the account list read and the owner assumed.
225
+ let accountsNext = "enrol a coding account a phase can run on";
226
+ if (!connected) {
227
+ add("coding accounts", "codingAccounts in catalyst-skills contract", "unreadable", ["not readable until this machine is connected"], null, null);
228
+ } else {
229
+ const res = runCli(["contract", "--path", "codingAccounts", "--json"]);
230
+ const ca = tryJson(res.stdout);
231
+ const older = ca === null && /has nothing at/.test(res.stderr ?? "");
232
+ const INSTRUMENT = "codingAccounts in catalyst-skills contract";
233
+ if (ca !== null && typeof ca === "object" && typeof ca.state === "string") {
234
+ const line = typeof ca.line === "string" ? ca.line : `state ${ca.state}`;
235
+ const owner = typeof ca.enrolledByLine === "string" ? ca.enrolledByLine : null;
236
+ const where = typeof ca.page === "string" ? link(ca.page) : null;
237
+ if (ca.state === "enrolled") {
238
+ add("coding accounts", INSTRUMENT, "ok", [line, `${ca.activeCount ?? "?"} active`]);
239
+ } else if (ca.state === "inactive") {
240
+ accountsNext = "reactivate a coding account that is out of rotation; do not enroll another one";
241
+ add("coding accounts", INSTRUMENT, "unfinished", [line, "Every account is out of rotation. Reactivate one. Do not enroll another account."], owner, where);
242
+ } else if (ca.state === "none_enrolled") {
243
+ add("coding accounts", INSTRUMENT, "unfinished", [line, "no phase can start until one is enrolled"], owner, where);
244
+ } else {
245
+ // `unread`, or a state this bundle does not know: could not look is not the same as none there.
246
+ accountsNext = "read the coding accounts again; the cloud could not read them this time";
247
+ add("coding accounts", INSTRUMENT, "unreadable", [line, "This is not a missing account. Do not enroll one on this reading. Run this again later."], null, null);
248
+ }
249
+ } else if (older) {
250
+ const acc = runCli(["accounts", "--json"]);
251
+ const doc = tryJson(acc.stdout);
252
+ const rows = Array.isArray(doc) ? doc : Array.isArray(doc?.accounts) ? doc.accounts : null;
253
+ const olderLine = "this cloud is older than the bundle: its contract does not say whether a coding account is enrolled, so the account list is read instead";
254
+ if (rows === null) {
255
+ add("coding accounts", "catalyst-skills accounts", "unreadable", [olderLine, `coding accounts could not be read (${(acc.stderr || acc.stdout).trim().split("\n")[0] || "no output"})`], null, null);
256
+ } else {
257
+ // An expired or revoked slot, or a quarantined one, cannot take work until an admin acts on it.
258
+ const usable = rows.filter((a) => a?.status !== "expired-or-revoked" && a?.quarantined !== true);
259
+ const lines = [olderLine, `${rows.length} enrolled, ${usable.length} able to take work`];
260
+ for (const a of rows) lines.push(`${a.accountSlot ?? "?"}: ${a.provider ?? "?"}, ${a.status ?? "status unknown"}${a.quarantined ? ", quarantined" : ""}`);
261
+ if (usable.length === 0) lines.push("no phase can start until one is enrolled and able to take work");
262
+ add("coding accounts", "catalyst-skills accounts", usable.length > 0 ? "ok" : "unfinished", lines, "a tenant owner or admin", link("/settings/coding-accounts"));
263
+ }
264
+ } else {
265
+ accountsNext = "read the coding accounts again; the contract could not be read";
266
+ add("coding accounts", INSTRUMENT, "unreadable", [`the contract could not be read (${(res.stderr || res.stdout).trim().split("\n").pop() || "no output"}); try: catalyst-skills contract --refresh`], null, null);
267
+ }
268
+ }
269
+
270
+ // ── host ──────────────────────────────────────────────────────────────────────────────────────────
271
+ // Read off the contract, never assumed: each checked project carries a hosts_current check, and the
272
+ // contract says who owns it. The check is account-wide, so any one project's reading is the answer.
273
+ // A tenant that runs no host of its own reads `pass` here, and then nothing is asked of anyone.
274
+ if (!connected) {
275
+ add("host", "hosts_current in catalyst-skills contract --path teams", "unreadable", ["not readable until this machine is connected"], null, null);
276
+ } else {
277
+ const checks = (teamRows ?? []).flatMap((t) => (Array.isArray(t.readiness?.checks) ? t.readiness.checks : []).filter((c) => c.id === "hosts_current").map((c) => ({ ...c, team: t.key ?? t.id ?? "(unkeyed)" })));
278
+ const meta = tryJson(runCli(["contract", "--path", "readinessChecks", "--json"]).stdout);
279
+ const row = Array.isArray(meta) ? meta.find((r) => r.id === "hosts_current") : null;
280
+ const hc = checks.find((c) => c.state === "fail") ?? checks.find((c) => c.state === "unknown") ?? checks[0] ?? null;
281
+ // The contract's printed line for this owner is written for a host that is behind. When no host is
282
+ // connected there is nothing behind, so the owner is named by its id instead of by that sentence.
283
+ const owner = row?.fixedBy === undefined
284
+ ? "the contract names no owner for hosts_current"
285
+ : hc?.state === "fail" && typeof row.fixedByLine === "string"
286
+ ? row.fixedByLine
287
+ : `the host operator (the contract's owner for hosts_current: ${row.fixedBy})`;
288
+ // Where the owner acts comes from the contract's `fixedWhere`. Null (every fixer today, and absent on
289
+ // an older cloud) means no page exists, so the owner sentence is printed alone and no page is made up.
290
+ const fw = row?.fixedWhere && typeof row.fixedWhere.page === "string" ? row.fixedWhere : null;
291
+ const how = fw === null ? null : link(fw.page);
292
+ const howLines = fw?.command ? [`the owner runs: ${fw.command}`] : [];
293
+ if (hc === null) {
294
+ add("host", "hosts_current in catalyst-skills contract --path teams", "unreadable", [teamRows === null ? "the project list could not be read, so the host check cannot be either" : "no project has a readiness check yet, so whether a host is connected cannot be read. Press Re-check on the projects page."], "a tenant owner or admin", link("/settings/linear-teams"));
295
+ } else if (hc.state === "pass") {
296
+ add("host", "hosts_current in catalyst-skills contract --path teams", "ok", [`hosts_current pass (read on ${hc.team})`]);
297
+ } else {
298
+ const detail = hc.reason === "no_host_connected" ? "no Catalyst host is connected" : hc.reason === "hosts_behind" ? "a connected host runs an older mapping" : hc.reason === "hosts_unreported" ? "a host is connected but has not reported what it loaded" : `state ${hc.state}`;
299
+ add("host", "hosts_current in catalyst-skills contract --path teams", "unfinished", [`hosts_current ${hc.state}${hc.reason ? ` (${hc.reason})` : ""} on ${hc.team}: ${detail}`, ...howLines], owner, how);
300
+ }
301
+ }
302
+
303
+ // ── the single next step ──────────────────────────────────────────────────────────────────────────
304
+ // The machine's next action is not one sentence: an unconnected machine needs the login, and a
305
+ // connected one needs whatever check failed — and `ready` already printed that check's own fix, so
306
+ // this carries it through rather than inventing a second answer to the same question.
307
+ const machineNext = connected
308
+ ? `clear the machine check that failed — ${machineFix ?? "see the machine lines above"}`
309
+ : "connect this machine";
310
+ const NEXT = {
311
+ machine: machineNext,
312
+ person: "get this person's seat and Linear identity sorted",
313
+ account: "connect Linear, and install the GitHub App",
314
+ projects: "pick ONE project and map its stages (or adopt the Catalyst workflow)",
315
+ repositories: "register the repository, attaching it to the project you mapped",
316
+ "coding accounts": accountsNext,
317
+ host: "connect a Catalyst host",
318
+ };
319
+ const blocked = parts.filter((p) => p.verdict !== "ok" && p.blocking);
320
+ const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
321
+ const next =
322
+ stuck === null
323
+ ? null
324
+ : { part: stuck.part, action: NEXT[stuck.part], owner: stuck.owner, where: stuck.where, blocking: stuck.blocking };
325
+ const finished = parts.every((p) => p.verdict === "ok");
326
+
327
+ if (flags.json) {
328
+ console.log(JSON.stringify({ cli: via, connected, cloud, parts, next, finished }));
329
+ } else if (flags.next) {
330
+ if (next === null) console.log("nothing left: every part is finished, a coding account is enrolled and the host check passes. Move one card into the project's dispatch stage.");
331
+ 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}` : ""}`);
332
+ } else {
333
+ for (const p of parts) {
334
+ console.log(`${p.part} [${p.verdict}]`);
335
+ console.log(` instrument: ${p.instrument}`);
336
+ for (const l of p.lines) console.log(` ${l}`);
337
+ if (p.verdict !== "ok" && p.owner) console.log(` who: ${p.owner}`);
338
+ // A page gets "where"; a command gets "do". Labelling a command "where" is how a person ends up
339
+ // looking for a settings page that is actually a line to run.
340
+ if (p.verdict !== "ok" && p.where) console.log(` ${p.where.startsWith("http") ? "where" : "do"}: ${p.where}`);
341
+ console.log("");
342
+ }
343
+ 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}` : ""}`);
344
+ }
345
+ process.exit(finished ? 0 : 1);
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: catalyst-setup
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 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
+ allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
+ ---
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.8.0 — written in this repository for customer tenants -->
8
+
9
+ # Am I set up?
10
+
11
+ You answer one question with one verdict and one list. The verdict is READY or NOT READY. The list is who can click what: the fixes only the person at the keyboard can make, the fixes only a tenant owner or admin can make in settings, and the checks that are simply waiting for the first event. The live check ids, severities, states and the people who can answer come from the contract; the scripts print them, you never restate them.
12
+
13
+ This readiness skill is part of `catalyst-cloud-skills` and checks tenant setup and operation. Coding workflow skills are a separate pack, `catalyst-dev-skills`. Do not direct a person to the deprecated local runtime or its `catalyst-dev@catalyst` plugin.
14
+
15
+ ## Run first
16
+
17
+ - `node scripts/check.mjs --help` — every machine and tenant check, one line each, the fix and who for each failure, the verdict, then the who-can-click-what list. Exit 1 when NOT READY.
18
+ - `node scripts/replica-status.mjs --help` — the optional replica's verdict with no network (add `--probe` for how far behind it is). Exit 0 fresh, 1 stale, 2 not connected, 3 absent.
19
+
20
+ ## Load on demand
21
+
22
+ | when | read |
23
+ | -- | -- |
24
+ | any check is red, unknown or waiting and the person asks what it proves, how to fix it, or who can | `references/what-each-check-means.md` |
25
+ | the person asks how to install, update, or migrate Catalyst skills | `../catalyst-onboard/references/skill-sources.md` |
26
+
27
+ ## Rules
28
+
29
+ - 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.
30
+ - End every answer with the verdict and the who-can-click-what list, in that order.
31
+ - 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.
32
+ - 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.
33
+ - 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.
34
+ - Unknown is not a pass. A check the engine could not run is reported as such, never rounded up.
35
+ - **Give numbers and a time.** Say how many are `pass`, `fail` and `unknown`, and when the team verdicts were computed: `readiness.checkedAt` per team on the contract, null when no pass has run.
36
+ - 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.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "Am I set up?"
3
+ short_description: "Machine and tenant readiness in one verdict: what passes, what is blocked, and who can click what"
4
+ default_prompt: "Use $catalyst-setup to tell me whether this machine and my tenant are ready, and what is missing."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,4 @@
1
+ identity: { pack: catalyst-cloud-skills, skill: catalyst-setup }
2
+ effects: []
3
+ invocation: implicit
4
+ exposure: [catalog]