@catalyst-cloud/cli 0.14.6 → 0.14.8

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 (79) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -5
  3. package/dist/args.js +68 -18
  4. package/dist/browser.js +37 -3
  5. package/dist/cli.js +58 -12
  6. package/dist/config.js +17 -5
  7. package/dist/consent-browser.js +58 -0
  8. package/dist/daemon-authority-lifetime.js +171 -0
  9. package/dist/daemon-authority.js +216 -0
  10. package/dist/daemon-cache.js +348 -0
  11. package/dist/daemon-store.js +370 -0
  12. package/dist/daemon-supervisor.js +460 -0
  13. package/dist/env/onboard-draft-store.js +250 -0
  14. package/dist/env/onboard-draft.js +33 -0
  15. package/dist/env/selected-import-writer.js +163 -0
  16. package/dist/env/settings-authority.js +205 -0
  17. package/dist/events-cloud-tail.js +146 -0
  18. package/dist/events-cloud.js +619 -0
  19. package/dist/events.js +186 -31
  20. package/dist/oauth.js +129 -34
  21. package/dist/onboard-accounts.js +259 -0
  22. package/dist/onboard-automation-management.js +15 -0
  23. package/dist/onboard-bootstrap.js +162 -0
  24. package/dist/onboard-capabilities.js +15 -7
  25. package/dist/onboard-capacity.js +226 -0
  26. package/dist/onboard-existing.js +106 -32
  27. package/dist/onboard-file-snapshot.js +45 -0
  28. package/dist/onboard-github.js +14 -3
  29. package/dist/onboard-login-candidate.js +261 -0
  30. package/dist/onboard-next.js +139 -0
  31. package/dist/onboard-personal.js +14 -3
  32. package/dist/onboard-projects.js +350 -0
  33. package/dist/onboard-repositories.js +146 -37
  34. package/dist/onboard-runtime.js +108 -13
  35. package/dist/onboard-settings-context.js +141 -0
  36. package/dist/onboard-settings-import.js +127 -0
  37. package/dist/onboard-settings.js +234 -0
  38. package/dist/onboard-ui.js +85 -59
  39. package/dist/onboard-workflow.js +316 -0
  40. package/dist/onboard-workspace.js +14 -3
  41. package/dist/onboard.js +422 -39
  42. package/dist/owned-fetch.js +182 -0
  43. package/dist/pagination.js +13 -1
  44. package/dist/query.js +104 -31
  45. package/dist/ready.js +143 -35
  46. package/dist/replica.js +11 -4
  47. package/dist/repository-id.js +5 -0
  48. package/dist/sdk.js +33 -0
  49. package/dist/watch.js +2 -2
  50. package/package.json +3 -2
  51. package/skills/catalyst-github/SKILL.md +4 -2
  52. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  53. package/skills/catalyst-github/references/local-replica.md +78 -0
  54. package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
  55. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  56. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  57. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  58. package/skills/catalyst-linear/SKILL.md +5 -3
  59. package/skills/catalyst-linear/references/local-replica.md +91 -0
  60. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  61. package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
  62. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  63. package/skills/catalyst-onboard/SKILL.md +6 -4
  64. package/skills/catalyst-onboard/references/local-replica.md +40 -0
  65. package/skills/catalyst-onboard/references/local-sync.md +8 -4
  66. package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
  67. package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
  68. package/skills/run-this-project/SKILL.md +1 -1
  69. package/skills/unstick/SKILL.md +1 -1
  70. package/skills/what-needs-me/SKILL.md +1 -1
  71. package/skills/what-this-repo-needs/SKILL.md +1 -1
  72. package/skills/whats-happening/SKILL.md +4 -2
  73. package/skills/whats-happening/references/local-replica.md +51 -0
  74. package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
  75. package/skills/whats-happening/references/status-reply.md +1 -1
  76. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  77. package/vendor/settings/index.d.ts +4 -0
  78. package/vendor/settings/index.js +2937 -0
  79. package/vendor/settings/provenance.json +148 -0
@@ -1,8 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // read-ticket.mjs — one ticket with its comments, relations, labels, linked pull requests and agent
3
- // sessions inline, from the replica when it is fresh, else the origin-fresh API. The source line the
3
+ // sessions inline, from the cloud API by default, or an explicitly chosen local replica. The source line the
4
4
  // CLI prints on stderr is the freshness verdict; it is always shown.
5
- import { parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
+ import {
6
+ parseFlags,
7
+ parseJson,
8
+ relayStderr,
9
+ runCli,
10
+ usage,
11
+ wantsHelp,
12
+ } from "./lib/cli.mjs";
6
13
 
7
14
  const HELP = `Usage: node scripts/read-ticket.mjs <ticket> [--comments] [--source replica|api] [--json]
8
15
 
@@ -10,11 +17,10 @@ Reads one ticket record. Wraps: catalyst query issue.
10
17
 
11
18
  <ticket> the Linear identifier, e.g. KEY-123
12
19
  --comments also print every comment (id, author, time, body)
13
- --source replica|api force one source; default is the replica when fresh, else the API
20
+ --source replica|api choose one source; default is the cloud API
14
21
  --json print the full record as JSON
15
22
 
16
- The first stderr line names the source ("source: replica (cursor N)" or "source: api (replica
17
- stale|absent|not configured)"); quote it when the freshness of the answer matters.
23
+ The first stderr line names the source ("source: api (cloud reads by default)" or "source: replica (--source replica)"); quote it when the freshness of the answer matters.
18
24
 
19
25
  Exit 0 found, 1 not found or a usage error, 2 when this machine is not connected to a tenant or the
20
26
  cloud refused the read (the line says which).`;
@@ -24,7 +30,10 @@ if (wantsHelp(argv)) {
24
30
  console.log(HELP);
25
31
  process.exit(0);
26
32
  }
27
- const { flags, positionals } = parseFlags(argv, { bool: ["comments", "json"], value: ["source"] });
33
+ const { flags, positionals } = parseFlags(argv, {
34
+ bool: ["comments", "json"],
35
+ value: ["source"],
36
+ });
28
37
  const ticket = positionals[0];
29
38
  if (!ticket) usage("read-ticket needs a ticket identifier (see --help)");
30
39
 
@@ -49,9 +58,16 @@ if (flags.json) {
49
58
 
50
59
  const line = (label, value) => {
51
60
  if (value === undefined || value === null || value === "") return;
52
- console.log(`${label}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
61
+ console.log(
62
+ `${label}: ${typeof value === "string" ? value : JSON.stringify(value)}`,
63
+ );
53
64
  };
54
- const names = (list, key) => (Array.isArray(list) ? list.map((x) => (x && typeof x === "object" ? (x[key] ?? JSON.stringify(x)) : String(x))) : []);
65
+ const names = (list, key) =>
66
+ Array.isArray(list)
67
+ ? list.map((x) =>
68
+ x && typeof x === "object" ? (x[key] ?? JSON.stringify(x)) : String(x),
69
+ )
70
+ : [];
55
71
 
56
72
  line("ticket", row.identifier ?? row.id);
57
73
  line("title", row.title);
@@ -68,13 +84,20 @@ line("url", row.url);
68
84
  line("updated", row.updated_at);
69
85
  if (Array.isArray(row.relations) && row.relations.length) {
70
86
  console.log(`relations (${row.relations.length}):`);
71
- for (const rel of row.relations) console.log(` ${rel.type ?? "?"}: ${rel.issue_identifier ?? ""} -> ${rel.related_identifier ?? JSON.stringify(rel)}`);
87
+ for (const rel of row.relations)
88
+ console.log(
89
+ ` ${rel.type ?? "?"}: ${rel.issue_identifier ?? ""} -> ${rel.related_identifier ?? JSON.stringify(rel)}`,
90
+ );
72
91
  }
73
92
  if (Array.isArray(row.linked_pulls) && row.linked_pulls.length) {
74
93
  console.log(`linked pull requests (${row.linked_pulls.length}):`);
75
- for (const pr of row.linked_pulls) console.log(` #${pr.number ?? "?"} ${pr.repo_id ?? ""} [${pr.node_id ?? ""}]`);
94
+ for (const pr of row.linked_pulls)
95
+ console.log(
96
+ ` #${pr.number ?? "?"} ${pr.repo_id ?? ""} [${pr.node_id ?? ""}]`,
97
+ );
76
98
  }
77
- if (Array.isArray(row.agent_sessions)) line("agent sessions", row.agent_sessions.length);
99
+ if (Array.isArray(row.agent_sessions))
100
+ line("agent sessions", row.agent_sessions.length);
78
101
  if (Array.isArray(row.activity)) line("activity entries", row.activity.length);
79
102
  if (Array.isArray(row.comments)) line("comments", row.comments.length);
80
103
  if (row.description) {
@@ -87,7 +110,9 @@ if (flags.comments && Array.isArray(row.comments)) {
87
110
  const who = c.author_name ?? c.author_id ?? "?";
88
111
  const bot = c.is_bot ? " [bot]" : "";
89
112
  const parent = c.parent_id ? ` reply-to ${c.parent_id}` : "";
90
- console.log(`-- ${c.id} · ${who}${bot} · ${c.updated_at ?? c.created_at ?? ""}${parent}`);
113
+ console.log(
114
+ `-- ${c.id} · ${who}${bot} · ${c.updated_at ?? c.created_at ?? ""}${parent}`,
115
+ );
91
116
  console.log(String(c.body ?? "").trimEnd());
92
117
  }
93
118
  }
@@ -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.14.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.8 — written in this repository for customer accounts -->
8
8
 
9
9
  # Onboard me
10
10
 
@@ -14,10 +14,11 @@ Guide setup through the same `catalyst onboard` flow that the installer runs. Th
14
14
 
15
15
  1. Run `catalyst capabilities --json`. If its `onboard` entry is available and advertises `bootstrapHandoff: true`, use this path. If absent, use the older step-by-step references below and name the CLI update needed for the continuous flow.
16
16
  2. For a requested setup, run `catalyst onboard`. To inspect first, run `catalyst onboard --dry-run --json`. Resume with `catalyst onboard`; the command rechecks saved results. Do not ask the person to confirm completed browser steps or add a separate approval before the command's plan question.
17
- 3. For the current onboarding checks, run `catalyst ready --onboarding --json`. A failed check needs attention. An unknown check needs evidence. Report observed project work separately from incomplete setup; an unknown setup check does not prove work stopped.
18
- 4. Exit 0 completes the requested scope. Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. A successful `--only` step does not mean onboarding is complete. Keep the command's safe fix and supported resume command in your reply.
17
+ 3. In a terminal, the command shows the verified person, workspace and plan before approval. Without terminal input, or with `--json`, use `--yes` only when the person has accepted the displayed defaults; otherwise setup waits with exit 11 and keeps the saved connection and receipt. JSON keeps progress, identity, the plan and quota disclosure on stderr and returns one result on stdout. A supplied key needs `catalyst login` before onboarding; browser sign-in does not replace it.
18
+ 4. For the current onboarding checks, run `catalyst ready --onboarding --json`. A failed check needs attention. An unknown check needs evidence. Report observed project work separately from incomplete setup; an unknown setup check does not prove work stopped.
19
+ 5. Exit 0 completes the requested scope. For a full run, exit 0 means work can start. If the receipt's `complete` is false, repository settings, local values, the daily update or the first ticket are still open, and the command lists them under "Next, when you want". Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. A successful `--only` step does not mean onboarding is complete. Keep the command's safe fix and supported resume command in your reply.
19
20
 
20
- Use cloud reads for ordinary workstation setup checks. Offer local sync for local SQL, offline access or sustained reads inside the plan review. It is optional; do not make an absent replica a setup failure. Browser approval belongs to the person. Missing bearer capabilities stay unfinished; never call a session-only route from the CLI or invent a command.
21
+ Use cloud reads for ordinary workstation setup checks. Offer local sync for local SQL or sustained local reads inside the plan review. Local sync does not promise offline browsing. It is optional; do not make an absent replica a setup failure. Browser approval belongs to the person. Missing bearer capabilities stay unfinished; never call a session-only route from the CLI or invent a command.
21
22
 
22
23
  Choose an existing Linear team explicitly in the command's team picker or with `catalyst onboard --team <team ID or unique key>`. Unattended setup does not pick the first team, even when only one is visible. Resume rechecks the saved selection; final readiness reads only that team. A team used to verify the workspace grant is not a project selection. Setup does not create a Linear team. Native sign-in waits for up to ten minutes across all device-code rounds; a timeout saves progress for the next run.
23
24
 
@@ -57,6 +58,7 @@ Scripts are run, never read; each prints `--help`.
57
58
  | a browser step, or confirming a page that said it worked | `references/what-the-browser-owns.md` |
58
59
  | the repository's `.catalyst/catalyst.toml` and its approval; its AGENTS.md block and agent layout | `references/declaring-a-repository.md`; `references/repository-agent-setup.md` |
59
60
  | the replica or event cache | `references/local-sync.md` |
61
+ | local SQL, only when `catalyst replica status --json` reports `configured: true` | `references/local-replica.md` |
60
62
  | installing, updating, or migrating Catalyst skills | `references/skill-sources.md` |
61
63
  | what Catalyst is, or why the first ticket did not start | the `whats-happening` skill, then `unstick` if something holds it |
62
64
 
@@ -0,0 +1,40 @@
1
+ # Local SQL on an opted-in machine
2
+
3
+ Load this only when `catalyst replica status --json` reports `configured: true`. On any other machine, setup is complete without a replica: every skill reads from the cloud, and `references/local-sync.md` explains how a person opts in if they ask for local SQL.
4
+
5
+ `configured: true` means this machine opted in: `CATALYST_REPLICA_DB` is set, or the machine paths file declares `replicaDb`. It does not mean the writer is running. `references/local-sync.md` reads liveness and starts the writer.
6
+
7
+ ## Before any query
8
+
9
+ Run `catalyst replica status --probe --json`. Trust local rows only when `verdict` is `fresh` and `cursor` equals `head`. A first start seeds the whole snapshot, so a new replica reads stale until its cursor appears.
10
+
11
+ ## Running SQL
12
+
13
+ - `catalyst replica sql "<one SELECT>" --json` runs one read-only `SELECT` and prints rows. Anything else is refused.
14
+ - `catalyst replica schema` prints every table with its columns; `catalyst replica schema <table>` prints one.
15
+ - `catalyst query issues --source replica`, `issue`, `pulls` and `projects` read the local copy through the same verbs as the cloud read. Without `--source replica`, they read the cloud.
16
+
17
+ Timestamps are milliseconds since the epoch. Rows with `removed_at` set were deleted upstream.
18
+
19
+ ## First checks after a start
20
+
21
+ Is the copy populated?
22
+
23
+ ```sql
24
+ select (select count(*) from issues where removed_at is null) as tickets,
25
+ (select count(*) from pull_requests) as pull_requests,
26
+ (select count(*) from projects) as projects
27
+ ```
28
+
29
+ Which teams does it hold?
30
+
31
+ ```sql
32
+ select team_key, count(*) as tickets from issues
33
+ where removed_at is null group by team_key order by team_key
34
+ ```
35
+
36
+ Compare these counts with what the person sees in Linear. A team missing here is a scope question for the account, not a replica fault.
37
+
38
+ ## Where the reading recipes live
39
+
40
+ Ticket recipes are in the `catalyst-linear` skill's local replica reference, and pull request recipes in the `catalyst-github` skill's. Both carry the same gate as this page.
@@ -4,19 +4,23 @@ The replica is an on-machine SQLite copy of the account for cheap repeated reads
4
4
 
5
5
  ## The replica's exit codes
6
6
 
7
- `catalyst replica status` needs no network. Exit 0 is fresh (a live writer, a heartbeat under 15 seconds, a cursor), and skills read the replica. Exit 1 is stale and 3 is absent; skills read the API, and `catalyst replica start --detach` restarts a stale one. Exit 2 is not connected (`references/connecting-this-machine.md`). `--probe` adds one network call comparing the local cursor with the cloud head. Every read verb names its source on standard error.
7
+ `catalyst replica status` needs no network. Exit 0 is fresh (a live writer, a heartbeat under 15 seconds, a cursor), exit 1 is stale and 3 is absent. Skills use cloud reads by default; a fresh replica is used only with explicit `--source replica`. `catalyst replica start --detach` restarts a stale writer after local sync is selected. Exit 2 is not connected (`references/connecting-this-machine.md`). `--probe` adds one network call comparing the local cursor with the cloud head. Every read verb names its source on standard error.
8
+
9
+ Local sync is opt-in. `catalyst replica status --json` carries `configured`, which is true only when this machine opted in: `CATALYST_REPLICA_DB` is set, or the machine paths file declares `replicaDb`. A machine that never opted in prints `replica: not configured (cloud reads; local sync is opt-in)` and reads the cloud. A machine with `configured: true` and exit 1 or 3 opted in and its writer is down.
8
10
 
9
11
  ## Checking and starting both
10
12
 
11
13
  `node scripts/local-sync.mjs` shows the local state. Only after the person opts in, `--start` starts the replica and event cache writers and waits up to 90 seconds (`--wait <seconds>`, 0 to 300).
12
14
 
13
- Say `Local sync current` only when both writers have live heartbeats and each cursor equals its cloud head. A detached process starting, one fresh heartbeat, or replica freshness alone does not prove event freshness. `catalyst replica status --probe --json` checks the replica cursor; `catalyst events status --probe --json` checks the separate event cursor. Unknown means the comparison could not be proved, which is not stale. The script exits 0 current, 1 stale, 3 absent, and 2 for "not connected" or "unknown", with a printed reason. After a restart, check again before relying on local data.
15
+ Say `Local sync current` only when both writers have live heartbeats and each cursor equals its cloud head. A detached process starting, one fresh heartbeat, or replica freshness alone does not prove event freshness. `catalyst replica status --probe --json` checks the replica cursor; `catalyst events status --from-cache --probe --json` checks the separate event cursor. Unknown means the comparison could not be proved, which is not stale. The script exits 0 current, 1 stale, 3 absent, and 2 for "not connected" or "unknown", with a printed reason. After a restart, check again before relying on local data.
14
16
 
15
17
  ## First ticket event
16
18
 
17
- Before the person moves a card, run `node scripts/local-sync.mjs --start` (after they opt in) and wait until it reports current. Then run `catalyst events status --probe --json` and record its `cursor` as `<cursor-before-move>`. Move the card only after this baseline is captured. Then run `catalyst events wait-for --ticket <ticket-identifier> --after <cursor-before-move> --timeout 300` with the identifier `explain` printed, and no guessed event type. The explicit cursor finds an event that reached the cache before the command began.
19
+ The default wait reads the cloud and needs no local sync. Before the person moves a card, run `catalyst events status --json` and record its `head` as `<cursor-before-move>`. Move the card only after this baseline is captured. Then run `catalyst events wait-for --ticket <ticket-identifier> --after <cursor-before-move> --timeout 300` with the identifier `explain` printed, and no guessed event type. The explicit cursor finds an event that happened before the command began.
20
+
21
+ Exit 0 prints a matching event after the baseline. Exit 1 means no matching event arrived within five minutes; check `catalyst explain <ticket-identifier>` before inferring a cloud or webhook failure. Exit 4 means the cloud event service was unreachable: an outage, not a timeout. Other errors are unknown. `explain` stays the API-backed answer to why a ticket can or cannot run.
18
22
 
19
- Exit 0 prints a matching event after the baseline, proving it reached this machine's cache. Exit 1 means no matching cached event arrived within five minutes; recheck `node scripts/local-sync.mjs` and report stale or unknown evidence without inferring a cloud or webhook failure. Exit 3 means the event cache is absent; ask before starting it. Other errors are unknown. `explain` stays the API-backed answer to why a ticket can or cannot run.
23
+ On a machine that opted in to local sync, the same wait can also prove the event reached this machine's cache. Run `node scripts/local-sync.mjs --start` and wait until it reports current. Then run `catalyst events status --from-cache --probe --json` and record its `cursor` as `<cursor-before-move>`. Move the card only after this baseline is captured. Then run `catalyst events wait-for --from-cache --ticket <ticket-identifier> --after <cursor-before-move> --timeout 300`. Exit 1 means no matching cached event arrived within five minutes; recheck `node scripts/local-sync.mjs` and report stale or unknown evidence without inferring a cloud or webhook failure. Exit 3 means the event cache is absent; ask before starting it.
20
24
 
21
25
  ## The writer
22
26
 
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { cliTarget } from "./lib/cli.mjs";
7
7
 
8
8
  const STATUS_COMMAND = ["replica", "status", "--probe", "--json"];
9
- const EVENTS_COMMAND = ["events", "status", "--probe", "--json"];
9
+ const EVENTS_COMMAND = ["events", "status", "--from-cache", "--probe", "--json"];
10
10
  const START_COMMAND = ["replica", "start", "--detach"];
11
11
  const RECOVERY_COMMAND = "catalyst replica start --detach";
12
12
 
@@ -120,7 +120,7 @@ function readStatus(run) {
120
120
  current: false,
121
121
  reason: `event freshness is unknown: ${eventsResult.error || eventsResult.stderr.trim() || "invalid JSON"}`,
122
122
  },
123
- recovery: "catalyst events status --probe --json",
123
+ recovery: "catalyst events status --from-cache --probe --json",
124
124
  };
125
125
  }
126
126
  return { status, events, assessment: assessLocalSync(status, events) };
@@ -30,7 +30,7 @@ const SPEC = {
30
30
  };
31
31
  const NOTES = [
32
32
  "Reads, in this order: `status` (machine), `ready --json` (machine checks and project checks, kept apart),",
33
- "`replica status --probe --json` and `events status --probe --json` (optional local freshness),",
33
+ "`replica status --probe --json` and `events status --from-cache --probe --json` (optional local freshness),",
34
34
  "`me --json` and personal connection statuses (person), `contract --path …` for the account, the projects and the repositories,",
35
35
  "`contract --path codingAccounts` and `accounts --json` (coding accounts, and which one needs a new credential), and each",
36
36
  "project's hosts_current check with its fixedWhere (host), and each project's environment_declared check with its per-repository notes (repository declarations). The teams read is refreshed, so a mapping just saved reads back mapped.",
@@ -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.14.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.8 — 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.14.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.8 — 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.14.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.8 — 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.14.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.8 — written in this repository for customer accounts -->
8
8
 
9
9
  # What this repository needs
10
10
 
@@ -4,13 +4,13 @@ description: >-
4
4
  Use for "what's happening?", "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.14.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.8 — written in this repository for customer accounts -->
8
8
 
9
9
  # What's happening
10
10
 
11
11
  Use one snapshot and the person's login. Run scripts, never read them. --help explains usage; exit 2 needs onboard, exit 1 means failure.
12
12
 
13
- - `node scripts/snapshot.mjs` prints contract, running work, queue, ranked asks, replica verdict. --board adds stages, --accounts coding accounts, --team K narrows.
13
+ - `node scripts/snapshot.mjs` reads contract, running work, queue and ranked asks from the cloud. --board adds stages, --accounts coding accounts, --team K narrows. --replica adds optional local diagnostics.
14
14
  - `node scripts/explain.mjs <ticket>` explains eligibility; --history adds attempts, rounds, holds and releases.
15
15
  - `node scripts/show-my-map.mjs [--team K]` prints stages, labels, ladder and thresholds.
16
16
 
@@ -29,5 +29,7 @@ Comments/relations: catalyst-linear. PRs: catalyst-github. Readiness: catalyst-o
29
29
  | columns or slots? | references/stages-and-mapping.md |
30
30
  | accounts, windows, quarantine or provider? | references/coding-accounts.md |
31
31
  | setting, screen, route or role? | references/settings-and-where-they-live.md |
32
+ | which command answers it from the cloud? | references/reading-from-the-cloud.md |
33
+ | local SQL? only if `catalyst replica status --json` says `configured: true` | references/local-replica.md |
32
34
 
33
35
  Read-only: give ids, source, release and actor. Route releases to `unstick`, decisions to what-needs-me before defaults, ownership to run-this-project. Subscribe, never poll. Report each system cause once.
@@ -0,0 +1,51 @@
1
+ # Local replica (opt-in)
2
+
3
+ Load this only when `catalyst replica status --json` reports `configured: true`. Otherwise answer from the cloud (`references/reading-from-the-cloud.md`) and never mention the replica.
4
+
5
+ The replica is an on-machine SQLite copy of the account, present only after the person opted in to local sync. It adds ad hoc SQL over the board. It never holds running work, the queue, asks, eligibility or phase history; those stay cloud reads.
6
+
7
+ ## Freshness
8
+
9
+ `catalyst replica status --probe --json` first. Use local rows only when `verdict` is `fresh` and `cursor` equals `head`, and quote the cursor. `stale` or `absent` with `configured: true` means the opted-in writer is down: answer from the cloud and say so in the source clause. Start it only when asked (`catalyst replica start --detach`).
10
+
11
+ The snapshot's replica verdict line belongs in the status reply's source clause only on an opted-in machine.
12
+
13
+ ## SQL
14
+
15
+ `catalyst replica sql "<one SELECT>" --json` runs one read-only `SELECT`. `catalyst replica schema <table>` prints columns. Timestamps are milliseconds since the epoch; filter `removed_at is null` for current rows. Replace `<team key>` and `KEY-123` with real values from the contract.
16
+
17
+ Cards per stage on one team:
18
+
19
+ ```sql
20
+ select state, count(*) as n from issues
21
+ where team_key = '<team key>' and removed_at is null
22
+ group by state order by n desc
23
+ ```
24
+
25
+ What closed in the last day:
26
+
27
+ ```sql
28
+ select identifier, title, completed_at from issues
29
+ where completed_at > (strftime('%s', 'now') - 86400) * 1000 and removed_at is null
30
+ order by completed_at desc
31
+ ```
32
+
33
+ Stage moves in the last day, newest first:
34
+
35
+ ```sql
36
+ select i.identifier, h.from_state, h.to_state, h.created_at
37
+ from issue_history h join issues i on i.id = h.issue_id
38
+ where h.to_state is not null and h.created_at > (strftime('%s', 'now') - 86400) * 1000
39
+ order by h.created_at desc limit 50
40
+ ```
41
+
42
+ Cards unmoved for three days in a stage, oldest first:
43
+
44
+ ```sql
45
+ select identifier, state, updated_at from issues
46
+ where team_key = '<team key>' and state = '<stage name>' and removed_at is null
47
+ and updated_at < (strftime('%s', 'now') - 3 * 86400) * 1000
48
+ order by updated_at
49
+ ```
50
+
51
+ An unmoved card may still be running, retrying or remediating. Check `catalyst history KEY-123` before calling it stuck.
@@ -0,0 +1,21 @@
1
+ # Reading from the cloud
2
+
3
+ Cloud reads are the default everywhere and need no local writer or database. Each command reads the cloud through the CLI.
4
+
5
+ | question | command |
6
+ | -- | -- |
7
+ | ticket stage, owner, linked PRs | `catalyst query issue KEY-123` |
8
+ | last phase, attempts, rounds, park, lease | `catalyst history KEY-123` |
9
+ | why not running, what runs next | `node scripts/explain.mjs KEY-123` |
10
+ | running now | `catalyst running` |
11
+ | queue order | `catalyst queue` |
12
+ | PR state and checks | `catalyst query pulls --ticket KEY-123`, then `catalyst query pull <node-id>` |
13
+ | what changed since a point | `catalyst query changes --since head` |
14
+ | a ticket's events, newest first | `catalyst events query --ticket KEY-123` |
15
+ | wait for a phase to finish | `catalyst events wait-for --ticket KEY-123 --type relay.phase.completed --after <head> --timeout 300` |
16
+
17
+ A status-only read of a ticket and of its execution history is coming. No CLI flag selects it yet; read the full record.
18
+
19
+ Events: `catalyst events` reads the cloud by default. `events status --json` gives the `head` to wait after. Exit 4 from `wait-for` is an outage, not a timeout. `--from-cache` reads the local event cache, which exists only where local sync is on.
20
+
21
+ Local SQL: `references/local-replica.md`, only when `configured: true`.
@@ -1,6 +1,6 @@
1
1
  # Status
2
2
 
3
- Open with takenAt, source and replica cursor/API reason. Then:
3
+ Open with takenAt and the cloud source. Include a replica cursor only when optional local diagnostics were requested. Then:
4
4
 
5
5
  1. In flight: ticket, phase and elapsed time from running.
6
6
  2. Blocked: reason and release actor.
@@ -2,25 +2,48 @@
2
2
  // snapshot.mjs — one call that answers "where are we?": the account's contract (trimmed to what a
3
3
  // status reply needs), what is running, what is queued, what is waiting on a human, and whether the
4
4
  // local replica is fresh. Prints ONE JSON document on stdout and the source line on stderr.
5
- import { mustRun, parseFlags, parseJson, printHelp, runCli } from "./lib/cli.mjs";
5
+ import {
6
+ mustRun,
7
+ parseFlags,
8
+ parseJson,
9
+ printHelp,
10
+ runCli,
11
+ } from "./lib/cli.mjs";
6
12
 
7
13
  const SPEC = {
8
14
  team: { value: true, help: "limit the queue and the board to this team key" },
9
- board: { value: false, help: "also include open tickets grouped by stage, in the contract's slot order" },
15
+ board: {
16
+ value: false,
17
+ help: "also include open tickets grouped by stage, in the contract's slot order",
18
+ },
10
19
  limit: { value: true, help: "board: max tickets to read (default 200)" },
11
- "full-contract": { value: false, help: "include the whole contract instead of the trimmed `tenant` block" },
12
- accounts: { value: false, help: "also include the coding accounts: provider, state, usage windows, walls, quarantine (never a credential)" },
20
+ "full-contract": {
21
+ value: false,
22
+ help: "include the whole contract instead of the trimmed `tenant` block",
23
+ },
24
+ accounts: {
25
+ value: false,
26
+ help: "also include the coding accounts: provider, state, usage windows, walls, quarantine (never a credential)",
27
+ },
28
+ replica: {
29
+ value: false,
30
+ help: "also read optional local replica diagnostics",
31
+ },
13
32
  };
14
33
 
15
34
  const { help, flags } = parseFlags(process.argv.slice(2), SPEC);
16
35
  if (help) {
17
- printHelp("node scripts/snapshot.mjs [--team K] [--board] [--accounts] [--limit N] [--full-contract] [--help]", SPEC, [
18
- "Runs, in order: catalyst contract, running, queue, ask list, replica status (query issues with --board,",
19
- "accounts with --accounts).",
20
- "Output: one JSON document {takenAt, source, tenant, running, queue, waitingOnHuman, board?, accounts?, errors?}.",
21
- "A section the cloud refused is reported under errors and the rest still prints; exit 1 in that case.",
22
- "Every account fact (stage names, thresholds, teams) comes from the contract in this output, never from prose.",
23
- ]);
36
+ printHelp(
37
+ "node scripts/snapshot.mjs [--team K] [--board] [--accounts] [--limit N] [--full-contract] [--help]",
38
+ SPEC,
39
+ [
40
+ "Reads catalyst contract, running, queue and ask list from the cloud (query issues with --board,",
41
+ "accounts with --accounts, optional local diagnostics with --replica).",
42
+ "Output: one JSON document {takenAt, source, tenant, running, queue, waitingOnHuman, board?, accounts?, errors?}.",
43
+ "A section the cloud refused is reported under errors and the rest still prints; exit 1 in that case.",
44
+ "Every account fact (stage names, thresholds, teams) comes from the contract in this output, never from prose.",
45
+ ],
46
+ );
24
47
  process.exit(0);
25
48
  }
26
49
 
@@ -28,26 +51,40 @@ const errors = {};
28
51
  const section = (name, args) => {
29
52
  const r = runCli(args);
30
53
  if (r.code !== 0) {
31
- errors[name] = (r.stderr || r.stdout || `exit ${r.code}`).trim().split("\n").at(-1);
54
+ errors[name] = (r.stderr || r.stdout || `exit ${r.code}`)
55
+ .trim()
56
+ .split("\n")
57
+ .at(-1);
32
58
  return null;
33
59
  }
34
60
  return parseJson(r.stdout, name);
35
61
  };
36
62
 
37
- const contract = parseJson(mustRun(["contract", "--json"], { quiet: true }).stdout, "contract");
63
+ const contract = parseJson(
64
+ mustRun(["contract", "--json"], { quiet: true }).stdout,
65
+ "contract",
66
+ );
38
67
  // `replica status` exits 0 fresh, 1 stale, 3 absent — all three are verdicts with JSON on stdout,
39
68
  // not failures; only a non-JSON answer is an error.
40
69
  const replica = (() => {
70
+ if (!flags.replica)
71
+ return { verdict: "not-selected", cursor: null, heartbeatAgeMs: null };
41
72
  const r = runCli(["replica", "status", "--json"]);
42
73
  try {
43
74
  return JSON.parse(r.stdout.trim());
44
75
  } catch {
45
- errors.replica = (r.stderr || r.stdout || `exit ${r.code}`).trim().split("\n").at(-1);
76
+ errors.replica = (r.stderr || r.stdout || `exit ${r.code}`)
77
+ .trim()
78
+ .split("\n")
79
+ .at(-1);
46
80
  return { verdict: "unknown", cursor: null, heartbeatAgeMs: null };
47
81
  }
48
82
  })();
49
83
  const running = section("running", ["running", "--json"]);
50
- const queue = section("queue", flags.team ? ["queue", "--team", flags.team, "--json"] : ["queue", "--json"]);
84
+ const queue = section(
85
+ "queue",
86
+ flags.team ? ["queue", "--team", flags.team, "--json"] : ["queue", "--json"],
87
+ );
51
88
  const asks = section("waitingOnHuman", ["ask", "list", "--json"]);
52
89
 
53
90
  const out = {
@@ -58,7 +95,9 @@ const out = {
58
95
  replicaCursor: replica.cursor ?? null,
59
96
  replicaHeartbeatAgeMs: replica.heartbeatAgeMs ?? null,
60
97
  },
61
- tenant: flags["full-contract"] ? contract : trimContract(contract, flags.team),
98
+ tenant: flags["full-contract"]
99
+ ? contract
100
+ : trimContract(contract, flags.team),
62
101
  running,
63
102
  queue,
64
103
  waitingOnHuman: asks,
@@ -67,14 +106,23 @@ const out = {
67
106
  if (flags.accounts) out.accounts = section("accounts", ["accounts", "--json"]);
68
107
 
69
108
  if (flags.board) {
70
- const args = ["query", "issues", "--limit", String(flags.limit ?? 200), "--json"];
109
+ const args = [
110
+ "query",
111
+ "issues",
112
+ "--source",
113
+ "api",
114
+ "--limit",
115
+ String(flags.limit ?? 200),
116
+ "--json",
117
+ ];
71
118
  if (flags.team) args.push("--team", flags.team);
72
119
  const r = runCli(args);
73
120
  const sourceLine = (r.stderr.match(/^source: .*$/m) ?? [null])[0];
74
121
  // A board read is a window (`--limit` stays the default), and a window the cloud cut short must
75
122
  // say so rather than reading as the whole team.
76
123
  const truncatedLine = (r.stderr.match(/^truncated at .*$/m) ?? [null])[0];
77
- if (r.code !== 0) errors.board = (r.stderr || `exit ${r.code}`).trim().split("\n").at(-1);
124
+ if (r.code !== 0)
125
+ errors.board = (r.stderr || `exit ${r.code}`).trim().split("\n").at(-1);
78
126
  else {
79
127
  const rows = parseJson(r.stdout, "issues");
80
128
  out.board = groupByStage(rows, contract, flags.team);
@@ -84,18 +132,27 @@ if (flags.board) {
84
132
  }
85
133
 
86
134
  if (Object.keys(errors).length) out.errors = errors;
87
- process.stderr.write(`source: api for running/queue/asks; replica ${replica.verdict}${replica.cursor != null ? ` (cursor ${replica.cursor})` : ""}${out.source.board ? `; board from ${out.source.board}` : ""}\n`);
135
+ process.stderr.write(
136
+ `source: api for running/queue/asks; replica ${replica.verdict}${replica.cursor != null ? ` (cursor ${replica.cursor})` : ""}${out.source.board ? `; board from ${out.source.board}` : ""}\n`,
137
+ );
88
138
  process.stdout.write(JSON.stringify(out, null, 2) + "\n");
89
139
  process.exit(Object.keys(errors).length ? 1 : 0);
90
140
 
91
141
  /** The contract facts a status reply needs, and nothing a reply should not paste. */
92
142
  function trimContract(doc, teamKey) {
93
- const teams = (doc.teams ?? []).filter((t) => !teamKey || String(t.key ?? "").toUpperCase() === teamKey.toUpperCase());
143
+ const teams = (doc.teams ?? []).filter(
144
+ (t) =>
145
+ !teamKey || String(t.key ?? "").toUpperCase() === teamKey.toUpperCase(),
146
+ );
94
147
  return {
95
148
  contractVersion: doc.contractVersion,
96
149
  account: { slug: doc.account?.slug, name: doc.account?.name },
97
150
  slots: doc.slots,
98
- ladder: { phases: doc.ladder?.phases, intakeEnabled: doc.ladder?.intakeEnabled, keying: doc.ladder?.keying },
151
+ ladder: {
152
+ phases: doc.ladder?.phases,
153
+ intakeEnabled: doc.ladder?.intakeEnabled,
154
+ keying: doc.ladder?.keying,
155
+ },
99
156
  thresholds: doc.thresholds,
100
157
  mergeDefaultPolicy: doc.merge?.defaultPolicy,
101
158
  teams: teams.map((t) => ({
@@ -106,7 +163,14 @@ function trimContract(doc, teamKey) {
106
163
  stages: Object.fromEntries(
107
164
  (doc.slots ?? Object.keys(t.stages ?? {}))
108
165
  .filter((slot) => t.stages?.[slot])
109
- .map((slot) => [slot, { name: t.stages[slot].name, type: t.stages[slot].type, stateStillExists: t.stages[slot].stateStillExists }]),
166
+ .map((slot) => [
167
+ slot,
168
+ {
169
+ name: t.stages[slot].name,
170
+ type: t.stages[slot].type,
171
+ stateStillExists: t.stages[slot].stateStillExists,
172
+ },
173
+ ]),
110
174
  ),
111
175
  labels: {
112
176
  ask: (t.labels?.ask ?? []).map((l) => l.name),
@@ -124,11 +188,19 @@ function groupByStage(rows, doc, teamKey) {
124
188
  const seen = new Set();
125
189
  const terminal = new Set();
126
190
  for (const t of doc.teams ?? []) {
127
- if (teamKey && String(t.key ?? "").toUpperCase() !== teamKey.toUpperCase()) continue;
191
+ if (teamKey && String(t.key ?? "").toUpperCase() !== teamKey.toUpperCase())
192
+ continue;
128
193
  for (const slot of doc.slots ?? []) {
129
194
  const s = t.stages?.[slot];
130
195
  if (!s?.name) continue;
131
- if (slot === "done" || slot === "canceled" || ["completed", "canceled", "cancelled"].includes(String(s.type ?? "").toLowerCase())) terminal.add(s.name);
196
+ if (
197
+ slot === "done" ||
198
+ slot === "canceled" ||
199
+ ["completed", "canceled", "cancelled"].includes(
200
+ String(s.type ?? "").toLowerCase(),
201
+ )
202
+ )
203
+ terminal.add(s.name);
132
204
  else if (!seen.has(s.name)) {
133
205
  seen.add(s.name);
134
206
  order.push(s.name);
@@ -148,6 +220,12 @@ function groupByStage(rows, doc, teamKey) {
148
220
  projectId: r.project_id ?? null,
149
221
  });
150
222
  }
151
- const extra = Object.keys(byStage).filter((s) => !seen.has(s)).sort();
152
- return { stageOrder: [...order, ...extra], byStage, terminalStagesDropped: [...terminal] };
223
+ const extra = Object.keys(byStage)
224
+ .filter((s) => !seen.has(s))
225
+ .sort();
226
+ return {
227
+ stageOrder: [...order, ...extra],
228
+ byStage,
229
+ terminalStagesDropped: [...terminal],
230
+ };
153
231
  }
@@ -0,0 +1,4 @@
1
+ export type FullSettingsValidation =
2
+ | { ok: false; errors: string[] }
3
+ | { ok: true; errors: []; linearTeam: string; variableNames: string[]; secretNames: string[] };
4
+ export declare function validateFullSettings(text: string): FullSettingsValidation;