@catalyst-cloud/cli 0.16.23 → 0.16.24

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.
@@ -20,19 +20,21 @@ import { onboardStateRoot, } from "./onboard.js";
20
20
  /** The Compose project name the vendored file declares. */
21
21
  export const RUNNER_PROJECT = "catalyst-host";
22
22
  export const RUNNER_SESSION_NETWORK = "catalyst-session-v1";
23
- /** Catalyst's approved host release, generation 2 (hr-a19f7b0859a98147-9a96426c93e32173-63a9b20c3be3dc3e):
24
- * runner, supervisor and watchdog all built from catalyst-cloud 2ec4a551. A machine set up from these
23
+ /** Catalyst's approved host release, generation 48 (hr-598fd0ef2675bfd8-69310962e37ad7f2-2d264b793edb843e):
24
+ * the runner built from catalyst-cloud caf921a4, the supervisor and watchdog from a4165555, the
25
+ * first supervisor that serves several teams (CTC-4455). A machine set up from these
25
26
  * pins then follows newer approved releases itself (`catalyst runner update`, CTC-4646).
26
27
  * Explicit environment values override these pins; a saved runner image is preserved. */
27
28
  export const RUNNER_HOST_IMAGES = {
28
- supervisor: "ghcr.io/coalesce-labs/catalyst-supervisor@sha256:9a96426c93e32173a67e849aa5a462e9f5466b872e4117f9a8a7df1972b2a136",
29
- watchdog: "ghcr.io/coalesce-labs/catalyst-deadline-watchdog@sha256:63a9b20c3be3dc3ea328d77aa68dece942a40be46cb3126d9a7868e082828feb",
30
- runner: "ghcr.io/coalesce-labs/catalyst-runner@sha256:a19f7b0859a9814740935f7aadd8fe5c5c9c5974f9a5956a057b983ada16ea01",
29
+ supervisor: "ghcr.io/coalesce-labs/catalyst-supervisor@sha256:69310962e37ad7f21ccab5c131b0734f106a6b0cc9fb163cb5d8917c9d7a2fc1",
30
+ watchdog: "ghcr.io/coalesce-labs/catalyst-deadline-watchdog@sha256:2d264b793edb843e47b4c5a86fa6133dd2bd0b5ecdc0b7e997368893de074b28",
31
+ runner: "ghcr.io/coalesce-labs/catalyst-runner@sha256:598fd0ef2675bfd8e0665f70ef4ed8bf30706fba25fa2c39fc0df0801512a423",
31
32
  };
32
33
  export const IMAGE_REF = /^[a-z0-9][a-z0-9._:/-]{0,255}@sha256:[0-9a-f]{64}$/;
33
34
  const HOST_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
34
35
  const JOIN_TOKEN = /^[A-Za-z0-9._~+/=-]{16,1024}$/;
35
- const ENROLLMENTS = "/api/v1/hosts/enrollments";
36
+ const HOSTS = "/api/v1/hosts";
37
+ const ENROLLMENTS = `${HOSTS}/enrollments`;
36
38
  const JOIN_TOKENS = "/api/v1/hosts/join-tokens";
37
39
  const CAPACITY = "/api/v1/me/runner-capacity";
38
40
  const RUNNER_UID = "10001:10001";
@@ -549,6 +551,7 @@ export function dockerRunnerEngine(deps = {}) {
549
551
  },
550
552
  };
551
553
  }
554
+ const serves = (host, teamKey) => host.teams.includes(teamKey);
552
555
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
553
556
  ? value
554
557
  : null;
@@ -565,6 +568,8 @@ function parseHosts(body) {
565
568
  typeof host.hostId !== "string" ||
566
569
  typeof host.hostName !== "string" ||
567
570
  typeof host.team !== "string" ||
571
+ (host.teams !== undefined &&
572
+ (!Array.isArray(host.teams) || !host.teams.every((team) => typeof team === "string"))) ||
568
573
  !(host.revokedAtMs === null || typeof host.revokedAtMs === "number") ||
569
574
  (host.capability !== null &&
570
575
  (!capability ||
@@ -578,6 +583,9 @@ function parseHosts(body) {
578
583
  hostId: host.hostId,
579
584
  hostName: host.hostName,
580
585
  team: host.team,
586
+ teams: Array.isArray(host.teams) && host.teams.includes(host.team)
587
+ ? host.teams
588
+ : [host.team],
581
589
  revoked: host.revokedAtMs !== null,
582
590
  capacity: capability ? capability.runtimeLive
583
591
  ? capability.placeableCapacity
@@ -755,8 +763,8 @@ export function onboardRunnerAdapter(input = {}) {
755
763
  const teamKey = teamId ? await liveTeamKey(ctx, teamId, signal ?? new AbortController().signal) : null;
756
764
  if (!teamId || !teamKey)
757
765
  return waiting("runner_context_unverified", selected);
758
- if (credential && !isUnenrolled(credential) && credential.team !== teamKey)
759
- return waiting("runner_enrolled_for_other_team", selected);
766
+ // An installation enrolled for another team serves this one too once setup adds it (CTC-4455).
767
+ const otherTeamHost = credential && !isUnenrolled(credential) && credential.team !== teamKey ? credential.hostId : undefined;
760
768
  const runnerSupport = await verifyRunnerRoutes(ctx, journal, [{ method: "GET", path: "/api/v1/agent/runner-admission" }], signal);
761
769
  if ("reason" in runnerSupport && runnerSupport.reason !== "cloud_capability_unavailable")
762
770
  return waiting(runnerSupport.reason, selected);
@@ -773,7 +781,8 @@ export function onboardRunnerAdapter(input = {}) {
773
781
  return waiting("runner_native_egress_setup_required", selected);
774
782
  return { account, baseUrl: normalizeBaseUrl(cfg.baseUrl), teamKey, teamId, cloud, dir, info, images,
775
783
  ...(installation && installation !== "missing" ? { installedName: installation.hostName,
776
- installedSupervisorImage: installation.supervisorImage } : {}) };
784
+ installedSupervisorImage: installation.supervisorImage } : {}),
785
+ ...(otherTeamHost ? { otherTeamHost } : {}) };
777
786
  }
778
787
  async function desiredEnv(p, ctx, joinToken) {
779
788
  const saved = readEnvFile(join(p.dir, ".env")) ?? {};
@@ -857,6 +866,58 @@ export function onboardRunnerAdapter(input = {}) {
857
866
  return waiting(signal?.aborted ? "interrupted" : "runner_join_token_unavailable", selected);
858
867
  }
859
868
  }
869
+ /**
870
+ * CTC-4455: adds the selected team to this machine's enrollment. The cloud refuses until the
871
+ * machine's supervisor advertises that it serves a set of teams. Before setup starts the new
872
+ * supervisor that refusal is `deferred`; after, it is retried for as long as setup waits for the
873
+ * supervisor. An older cloud without the route answers 404, and setup refuses as it used to.
874
+ */
875
+ async function addTeam(ctx, p, hostId, retry, signal) {
876
+ const evidence = { ...selected, hostId };
877
+ const cfg = loadConfig(ctx.home);
878
+ const expiry = cfg?.auth ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() : 0;
879
+ const bearer = cfg?.key ||
880
+ (cfg?.auth && Number.isFinite(expiry) && expiry > 30_000 ? cfg.auth.accessToken : undefined);
881
+ if (!bearer)
882
+ return waiting("runner_login_refresh_required", evidence);
883
+ for (let attempt = 1;; attempt++) {
884
+ let status;
885
+ let body;
886
+ try {
887
+ const response = await ctx.fetch(`${p.baseUrl}${HOSTS}/${encodeURIComponent(hostId)}/teams?account=${encodeURIComponent(p.account)}`, {
888
+ method: "POST",
889
+ redirect: "error",
890
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000),
891
+ headers: {
892
+ authorization: `Bearer ${bearer}`,
893
+ "content-type": "application/json",
894
+ accept: "application/json",
895
+ },
896
+ body: JSON.stringify({ version: 1, team: p.teamKey }),
897
+ });
898
+ status = response.status;
899
+ body = object(await response.json().catch(() => null));
900
+ }
901
+ catch {
902
+ return waiting(signal?.aborted ? "interrupted" : "runner_team_add_failed", evidence);
903
+ }
904
+ if (status === 200 && body?.ok === true && Array.isArray(body.teams) && body.teams.includes(p.teamKey))
905
+ return true;
906
+ if (status === 404 && body?.error !== "unknown_host")
907
+ return waiting("runner_enrolled_for_other_team", evidence);
908
+ if (status === 401 || status === 403)
909
+ return waiting("runner_identity_unverified", evidence);
910
+ if (body?.error === "team_limit")
911
+ return waiting("runner_team_limit", evidence);
912
+ if (body?.error !== "supervisor_outdated")
913
+ return waiting("runner_team_add_failed", evidence);
914
+ if (!retry)
915
+ return "deferred";
916
+ if (attempt >= Math.max(1, Math.ceil(waitMs / pollMs)) || signal?.aborted)
917
+ return waiting("runner_supervisor_outdated", evidence);
918
+ await sleep(pollMs);
919
+ }
920
+ }
860
921
  function orgKeyFromFile(ctx) {
861
922
  const path = ctx.env.CATALYST_RUNNER_ORG_KEY_FILE;
862
923
  if (!path)
@@ -887,13 +948,13 @@ export function onboardRunnerAdapter(input = {}) {
887
948
  if (p.info.vm && persistedCustody(p.dir, saved) !== "installed")
888
949
  return mayAct && !afterAct ? { state: "pending" } : waiting(mayAct ? "runner_thoughts_custody_unavailable" : "runner_needs_runner_flag", { ...selected, hostName: name });
889
950
  let enrolled = null;
890
- const live = (hosts) => hosts?.find((host) => host.hostId === enrolled?.hostId && host.team === p.teamKey && !host.revoked);
951
+ const live = (hosts) => hosts?.find((host) => host.hostId === enrolled?.hostId && serves(host, p.teamKey) && !host.revoked);
891
952
  let hosts = await listHosts(ctx, p, signal);
892
953
  if (!hosts)
893
954
  return waiting("runner_enrollment_unavailable", selected);
894
955
  const existingCredential = await engine.enrollment(p.dir, signal);
895
956
  if ((!existingCredential || isUnenrolled(existingCredential)) &&
896
- hosts.some((host) => host.hostName === name && !host.revoked && host.team !== p.teamKey))
957
+ hosts.some((host) => host.hostName === name && !host.revoked && !serves(host, p.teamKey)))
897
958
  return waiting("runner_enrolled_for_other_team", {
898
959
  ...selected,
899
960
  hostName: name,
@@ -902,8 +963,11 @@ export function onboardRunnerAdapter(input = {}) {
902
963
  !isUnenrolled(existingCredential) &&
903
964
  hosts.some((host) => host.hostId === existingCredential.hostId &&
904
965
  !host.revoked &&
905
- host.team !== p.teamKey))
906
- return waiting("runner_enrolled_for_other_team", { ...selected, hostName: name });
966
+ !serves(host, p.teamKey)))
967
+ // CTC-4455: setup's act adds the selected team to a runner enrolled for another one.
968
+ return mayAct && !afterAct
969
+ ? { state: "pending" }
970
+ : waiting(mayAct ? "runner_team_not_added" : "runner_needs_runner_flag", { ...selected, hostName: name });
907
971
  const running = await engine.composeRunning(p.dir, signal);
908
972
  let composeMatches = false;
909
973
  try {
@@ -931,13 +995,11 @@ export function onboardRunnerAdapter(input = {}) {
931
995
  return waiting("runner_enrollment_unavailable", hostEvidence);
932
996
  if (enrolled && (enrolled.tenant !== p.account || enrolled.enrollmentKind !== "self_hosted"))
933
997
  return waiting("runner_identity_unverified", hostEvidence);
934
- if (enrolled && enrolled.team !== p.teamKey)
935
- return waiting("runner_enrolled_for_other_team", hostEvidence);
936
998
  if (enrolled) {
937
999
  const exact = hosts.find((host) => host.hostId === enrolled.hostId);
938
1000
  if (exact?.revoked)
939
1001
  return waiting("runner_enrollment_revoked", { ...hostEvidence, hostId: enrolled.hostId });
940
- if (!exact || exact.team !== p.teamKey)
1002
+ if (!exact || !serves(exact, p.teamKey))
941
1003
  return waiting("runner_enrollment_unverified", hostEvidence);
942
1004
  }
943
1005
  const host = live(hosts);
@@ -986,6 +1048,23 @@ export function onboardRunnerAdapter(input = {}) {
986
1048
  return p;
987
1049
  if (!mayAct)
988
1050
  return waiting("runner_needs_runner_flag", selected);
1051
+ // Settle a team the installed runner does not serve before anything on this machine changes.
1052
+ let addTeamTo = null;
1053
+ if (p.otherTeamHost) {
1054
+ const hosts = await listHosts(ctx, p, signal);
1055
+ if (!hosts)
1056
+ return waiting("runner_enrollment_unavailable", selected);
1057
+ const exact = hosts.find((host) => host.hostId === p.otherTeamHost && !host.revoked);
1058
+ if (!exact)
1059
+ return waiting("runner_enrolled_for_other_team", selected);
1060
+ if (!serves(exact, p.teamKey)) {
1061
+ const added = await addTeam(ctx, p, exact.hostId, false, signal);
1062
+ if (added === "deferred")
1063
+ addTeamTo = exact.hostId;
1064
+ else if (added !== true)
1065
+ return added;
1066
+ }
1067
+ }
989
1068
  let replacingContainers = false;
990
1069
  try {
991
1070
  mkdirSync(p.dir, { recursive: true, mode: 0o700 });
@@ -1104,20 +1183,25 @@ export function onboardRunnerAdapter(input = {}) {
1104
1183
  if (!hosts)
1105
1184
  return waiting("runner_enrollment_unavailable", selected);
1106
1185
  if (!enrolled &&
1107
- hosts.some((host) => host.hostName === name && !host.revoked && host.team !== p.teamKey))
1186
+ hosts.some((host) => host.hostName === name && !host.revoked && !serves(host, p.teamKey)))
1108
1187
  return waiting("runner_enrolled_for_other_team", { ...selected, hostName: name });
1109
1188
  if (enrolled && (enrolled.tenant !== p.account || enrolled.enrollmentKind !== "self_hosted"))
1110
1189
  return waiting("runner_identity_unverified", { ...selected, hostName: name });
1111
- if (enrolled && enrolled.team !== p.teamKey)
1112
- return waiting("runner_enrolled_for_other_team", { ...selected, hostName: name });
1113
1190
  if (hasCredential && !isUnenrolled(credential)) {
1114
1191
  if (!enrolled)
1115
1192
  return waiting("runner_credential_unverified", { ...selected, hostName: name });
1116
1193
  const exact = hosts.find((host) => host.hostId === enrolled.hostId);
1117
1194
  if (exact?.revoked)
1118
1195
  return waiting("runner_enrollment_revoked", { ...selected, hostName: name, hostId: enrolled.hostId });
1119
- if (!exact || exact.team !== p.teamKey)
1196
+ if (!exact)
1120
1197
  return waiting("runner_enrollment_unverified", { ...selected, hostName: name });
1198
+ if (!serves(exact, p.teamKey) && addTeamTo === null) {
1199
+ const added = await addTeam(ctx, p, exact.hostId, false, signal);
1200
+ if (added === "deferred")
1201
+ addTeamTo = exact.hostId;
1202
+ else if (added !== true)
1203
+ return added;
1204
+ }
1121
1205
  }
1122
1206
  // A lost volume also lost the secret that redeemed the old token. Mint a new token and match
1123
1207
  // the new host id from the supervisor's credential, never an old advertisement with this name.
@@ -1184,6 +1268,14 @@ export function onboardRunnerAdapter(input = {}) {
1184
1268
  if (!up.ok)
1185
1269
  return { state: "failed", reason: "runner_compose_failed", evidence: { ...selected,
1186
1270
  project: RUNNER_PROJECT, ...(up.error ? { composeError: up.error } : {}) } };
1271
+ // The supervisor just started can serve a set of teams; the cloud accepts the team once it says so.
1272
+ if (addTeamTo) {
1273
+ const added = await addTeam(ctx, p, addTeamTo, true, signal);
1274
+ if (added === "deferred")
1275
+ return waiting("runner_supervisor_outdated", selected);
1276
+ if (added !== true)
1277
+ return added;
1278
+ }
1187
1279
  acted = true;
1188
1280
  if (input.autoUpdate) {
1189
1281
  try {
package/dist/prompt.js CHANGED
@@ -1,10 +1,23 @@
1
- // prompt.ts — the one interactive read in the CLI: asking for the personal key on a terminal without
2
- // echoing it. The streams are parameters so the tests can drive it without a real TTY.
1
+ // prompt.ts — the CLI's interactive reads: asking for the personal key on a terminal without
2
+ // echoing it, and one [y/N] question. The streams are parameters so the tests can drive it without a real TTY.
3
3
  import { createInterface } from "node:readline";
4
+ import { createInterface as createPromiseInterface } from "node:readline/promises";
4
5
  /** True when there is a terminal to prompt on. Both ends matter: the question needs somewhere to go. */
5
6
  export function stdinIsTty() {
6
7
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
7
8
  }
9
+ /** Ask a `[y/N]` question on stderr and read the answer. False without a terminal, and for anything but y/yes. */
10
+ export async function confirmYesNo(question) {
11
+ if (!stdinIsTty())
12
+ return false;
13
+ const prompt = createPromiseInterface({ input: process.stdin, output: process.stderr });
14
+ try {
15
+ return /^(y|yes)$/i.test((await prompt.question(question)).trim());
16
+ }
17
+ finally {
18
+ prompt.close();
19
+ }
20
+ }
8
21
  /**
9
22
  * Read one line without echoing it. Everything readline would write after the question itself goes
10
23
  * to a muted stream, so the key never reaches the screen or a scrollback buffer; the caller still
package/dist/secret.js CHANGED
@@ -31,7 +31,7 @@ export const SECRETS_IMPORT_PATH = "/me/secrets/import";
31
31
  const NAME_RE = /^[A-Z][A-Z0-9_]*$/;
32
32
  const REPO_RE = /^[^/\s]+\/[^/\s]+$/;
33
33
  /** Why an import line was not stored, in words a person can act on. */
34
- const IMPORT_REASONS = {
34
+ export const IMPORT_REASONS = {
35
35
  name_exists: "already set for this repository; pass --rotate <NAME> to replace it",
36
36
  name_taken_by_env_var: "already a plain environment variable; a name is a secret or a variable, never both",
37
37
  invalid_name: "not an env-style name (A-Z, 0-9 and _, starting with a letter)",
@@ -69,7 +69,7 @@ function requireRepo(args, verb) {
69
69
  return repo;
70
70
  }
71
71
  /** The cloud's refusal, in its own words, plus what to do for the two we can explain. */
72
- function refusalLine(status, body, repo) {
72
+ export function refusalLine(status, body, repo) {
73
73
  if (status === 401) {
74
74
  return "refused (401): the cloud did not accept your credential here. If `catalyst me` works, this cloud predates secret writes from the CLI and needs a newer release; otherwise log in again.";
75
75
  }
@@ -91,7 +91,7 @@ function renderDeclared(declared, repo) {
91
91
  return [`every declared secret has a value (${declared.declared.length})`];
92
92
  return [`declared but still without a value (${declared.missing.length}): ${declared.missing.join(", ")}`];
93
93
  }
94
- const ACCEPT = [400, 401, 403, 404, 409, 422];
94
+ export const ACCEPT = [400, 401, 403, 404, 409, 422];
95
95
  async function readStdinText() {
96
96
  const chunks = [];
97
97
  for await (const chunk of process.stdin)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/cli",
3
- "version": "0.16.23",
3
+ "version": "0.16.24",
4
4
  "description": "Set up Catalyst Cloud, check your tickets and manage your account from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,16 +53,16 @@
53
53
  "prepublishOnly": "node scripts/release-train-check.mjs --publish cli"
54
54
  },
55
55
  "dependencies": {
56
- "@catalyst-cloud/sdk": "0.16.9",
56
+ "@catalyst-cloud/sdk": "0.16.11",
57
57
  "@clack/prompts": "1.8.1",
58
58
  "fast-string-width": "3.0.2",
59
59
  "fast-wrap-ansi": "0.2.2",
60
60
  "smol-toml": "^1.7.1",
61
61
  "yaml": "^2.9.0",
62
- "@catalyst-cloud/sdk-replica-node": "0.16.9"
62
+ "@catalyst-cloud/sdk-replica-node": "0.16.11"
63
63
  },
64
64
  "devDependencies": {
65
- "@catalyst-cloud/schema": "0.16.7",
65
+ "@catalyst-cloud/schema": "0.16.9",
66
66
  "@types/node": "^26.3.0",
67
67
  "@vitest/coverage-v8": "^4.1.11",
68
68
  "typescript": "^7.0.2",
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # Catalyst's GitHub
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's view of Linear on the customer's own cloud account. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the cloud API by default, with local replica reads available as an explicit opt-in, always naming the source; searches tickets, PRs, projects and initiatives; writes comments, card moves, labels and new tickets through the account's agent proxy as the app actor, with every route, stage id, label id and marker read from the account's contract. Knows what a ticket accumulates as Catalyst works it (phase-outcome comments, the document per phase, the agent session, the labels, the single blocks relation, the bookkeeping marker, the eyes acknowledgement). Use when a person says "show me the ticket", "what did Catalyst write on it", "comment on it", "move it", "label it" or "file a ticket". Not for raising a decision for a human (what-needs-me) and not for pull requests (catalyst-github).
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # Catalyst Linear
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Set up Catalyst Cloud and say whether setup is working. Walks a person from nothing to their first ticket running, one step at a time: the coding account, the project, the integrations and connected accounts, each repository's settings file, then one real ticket moving. Also logs this machine in, reads the readiness verdict with who can fix each failure, and checks the optional local replica. Use when someone says "set me up", "onboard me", "I just signed up", "get me started", "what do I do first", "log me in", "which account is this machine on", "am I set up", "what is missing", "why does nothing happen" or "is the replica running", when a login expired, or when a skill script exits 2 saying this machine is not connected. Does every step a key can do through the catalyst CLI, hands over the exact page for the steps only a browser can do, and never claims a step it did not watch succeed.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # Onboard me
10
10
 
@@ -6,7 +6,7 @@ The assistant prepares `.catalyst/catalyst.toml` from the repository. Never ask
6
6
 
7
7
  1. Check `.catalyst/catalyst.toml` and `catalyst.env.json` in the selected repository. Classify it as missing, legacy-only, valid TOML, or invalid TOML. `catalyst env check --json` checks the TOML environment table; when only the legacy file exists, it reports that the JSON declaration is no longer read.
8
8
  2. For a legacy file, run `catalyst env migrate [path-to-catalyst.env.json]`. It prints a TOML environment table containing names only; every name is optional, and names identified as secrets are marked `secret = true`. Do not copy JSON values or provenance into the new file.
9
- 3. For missing or invalid declarations, inspect the repository's package manager files, README setup instructions, `package.json` scripts or equivalent, build and test workflows, `.env.example`, and source references. Use `catalyst env inventory [repo] --json` as supporting evidence. Do not inspect `.env` values or run repository scripts just to infer settings.
9
+ 3. For missing or invalid declarations, run `catalyst env draft --root <repo> --json` first. It drafts names from the repository's standard files and from this computer: the names the repository's `.envrc` exports, and the HTTP MCP servers in Claude Code's `~/.claude.json` with each header turned into a variable reference. It never writes or prints a value, and it lists what it did not draft and why. Then refine the draft from the repository's package manager files, README setup instructions, `package.json` scripts or equivalent, build and test workflows, `.env.example`, and source references. Use `catalyst env inventory [repo] --json` as supporting evidence. Do not inspect `.env` values or run repository scripts just to infer settings.
10
10
  4. Draft `#:schema https://staging.catalystcloud.dev/schemas/catalyst.schema.json`, `[project]` with the mapped project's `linear_team`, the environment variables evidenced by build/test/setup, and setup/verify steps based on the repository's real package manager and build/test commands. Exclude deploy-only variables and platform bindings unless build or tests use them. Mark known secrets with `secret = true`. Set `required = false` on every variable; never make a phase depend on a newly declared value by default.
11
11
  5. Validate the complete draft against the live schema at the URL in its `#:schema` line, then run `catalyst env check <path>`. The environment check is an additional check; it does not replace full schema validation. If a live-schema validator is unavailable or either check fails, fix the draft or report the exact blocker without claiming it is valid.
12
12
 
@@ -14,7 +14,11 @@ The assistant prepares `.catalyst/catalyst.toml` from the repository. Never ask
14
14
 
15
15
  Show a short preview with the project/team, setup and verify commands, variable names, which are marked secret, and confirmation that every variable is optional. Do not include values. Ask the person to approve this exact draft. Before approval, do not write or replace `.catalyst/catalyst.toml`, remove `catalyst.env.json`, or open a settings PR.
16
16
 
17
- After approval, write the validated file. When converting a legacy-only repository, remove `catalyst.env.json` in the same change. Open the settings PR, then offer to watch it merge. A reviewed merge is the approval. Catalyst approves the exact merged revision, with no second step, when someone other than the PR's author approved the merged head commit, or when a workspace owner or admin merged the PR. Tell the person this before they merge: the approving review is their consent to the setup commands and secret names in the file, so a reviewer should read it like code. A direct push, an unreviewed merge, or an approval left before the last commit leaves the revision proposed. No command approves one repository's revision yet, so only then give an owner or admin a direct link to approve it at Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision. The direct route is `/settings/projects/$projectId/repositories/$repoId/environment/declaration`; use the project and repository ids from Catalyst instead of guessing them.
17
+ After approval, write the validated file.
18
+
19
+ When the approved file came from `catalyst env draft`, ask: "Want me to store this computer's values for these names in Catalyst?" On a yes, run `catalyst env import --root <repo> --yes --json` and report its `stored`, `kept`, `noValue` and `notStored` names, with the fix it gives for each. The command reads the values itself and never prints them, so do not read them yourself. It needs an admin or owner seat. A name in `noValue` that `.envrc` computes needs the command run in the person's own shell, where direnv has loaded the repository.
20
+
21
+ When converting a legacy-only repository, remove `catalyst.env.json` in the same change. Open the settings PR, then offer to watch it merge. A reviewed merge is the approval. Catalyst approves the exact merged revision, with no second step, when someone other than the PR's author approved the merged head commit, or when a workspace owner or admin merged the PR. Tell the person this before they merge: the approving review is their consent to the setup commands and secret names in the file, so a reviewer should read it like code. A direct push, an unreviewed merge, or an approval left before the last commit leaves the revision proposed. No command approves one repository's revision yet, so only then give an owner or admin a direct link to approve it at Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision. The direct route is `/settings/projects/$projectId/repositories/$repoId/environment/declaration`; use the project and repository ids from Catalyst instead of guessing them.
18
22
 
19
23
  ## File shape
20
24
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Own one Catalyst Cloud project end to end until it closes. Use when the person says "run this project for me", "own this until it ships", "keep this moving", or hands you a project id or a set of tickets to drive. Subscribes to the account's event stream for the scope through the catalyst CLI, reacts to each change in the same turn, makes tickets ready and moves them to dispatch, parks what should stop, chases stalls, escalates inward, and keeps one status summary current. Writes to Linear as the app actor; never polls.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # Run this project
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Get a stuck Catalyst Cloud ticket moving again. Use when the person asks "why is this parked and can you release it?", "unpark this", "get things flowing again", "the outage is over, retry what failed", or hands you a ticket that is not moving. Reads why nothing runs and what holds the ticket through the catalyst CLI, decides whether the recorded cause is fixed, previews the release, releases every governor holding the ticket the right way (or one failure class across a team), and raises an ask only for what a person has to do. Never releases a cause it cannot show changed without saying so.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # Unstick
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  The human's decision inbox on Catalyst Cloud, and the one way an agent raises a decision on their behalf. Use when the person asks "what needs me?", "what am I blocking?", or when active work is gated on a choice only they can make. Lists open asks ranked by how much open work each one holds, files an ask through the cloud's ask route with the account's own template, and records the answer so the held work releases. Never answers as the human, never duplicates an open ask.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # What needs me
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  What environment variable names does this repository need, and where does each one come from? Scans the repository offline (no login, no network) and lists the names in three groups (build/test, deploy-only, bindings), each with where it was found, what uses it, and where a local value would come from. Never reads a value or prints one. Also checks the environment variable table in `.catalyst/catalyst.toml` offline. Use when someone asks "what does catalyst.env.json mean", "what env vars does this repo need", "why does the container need this", or before reviewing or writing the declaration.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # What this repository needs
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Use for "what's happening?", "what machines do we have?", "how much capacity do we have?", "where are we?", "why is that stuck?", "what closed?", "what's next?", "how does this work?", "why did it do that?", "how does it prioritise?" and "what does this setting do, where is it set?" in Catalyst Cloud, or asks for something to be done rather than known. Reads the contract via catalyst CLI. Read-only: route work/decisions, never answer as the human.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.23 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.24 — written in this repository for customer accounts -->
8
8
 
9
9
  # What's happening
10
10