@catalyst-cloud/cli 0.14.5 → 0.14.7

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 (63) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -3
  3. package/dist/args.js +51 -13
  4. package/dist/cli.js +58 -10
  5. package/dist/config.js +16 -3
  6. package/dist/daemon-authority-lifetime.js +171 -0
  7. package/dist/daemon-authority.js +216 -0
  8. package/dist/daemon-cache.js +348 -0
  9. package/dist/daemon-store.js +370 -0
  10. package/dist/daemon-supervisor.js +460 -0
  11. package/dist/env/onboard-draft-store.js +243 -0
  12. package/dist/env/onboard-draft.js +33 -0
  13. package/dist/env/selected-import-writer.js +163 -0
  14. package/dist/env/settings-authority.js +203 -0
  15. package/dist/events-cloud-tail.js +134 -0
  16. package/dist/events-cloud.js +500 -0
  17. package/dist/events.js +13 -4
  18. package/dist/legacy.js +145 -32
  19. package/dist/oauth.js +125 -33
  20. package/dist/onboard-accounts.js +259 -0
  21. package/dist/onboard-automation-management.js +15 -0
  22. package/dist/onboard-bootstrap.js +162 -0
  23. package/dist/onboard-capabilities.js +15 -7
  24. package/dist/onboard-capacity.js +226 -0
  25. package/dist/onboard-existing.js +106 -32
  26. package/dist/onboard-file-snapshot.js +45 -0
  27. package/dist/onboard-login-candidate.js +261 -0
  28. package/dist/onboard-projects.js +350 -0
  29. package/dist/onboard-repositories.js +147 -37
  30. package/dist/onboard-runtime.js +104 -9
  31. package/dist/onboard-settings-context.js +141 -0
  32. package/dist/onboard-settings-import.js +127 -0
  33. package/dist/onboard-settings.js +234 -0
  34. package/dist/onboard-ui.js +99 -11
  35. package/dist/onboard-workflow.js +316 -0
  36. package/dist/onboard.js +359 -27
  37. package/dist/owned-fetch.js +182 -0
  38. package/dist/query.js +77 -27
  39. package/dist/ready.js +143 -35
  40. package/dist/replica.js +11 -4
  41. package/dist/sdk.js +33 -0
  42. package/dist/watch.js +2 -2
  43. package/package.json +3 -2
  44. package/skills/catalyst-github/SKILL.md +2 -2
  45. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  46. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  47. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  48. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  49. package/skills/catalyst-linear/SKILL.md +3 -3
  50. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  51. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  52. package/skills/catalyst-onboard/SKILL.md +5 -4
  53. package/skills/catalyst-onboard/references/local-sync.md +3 -1
  54. package/skills/run-this-project/SKILL.md +1 -1
  55. package/skills/unstick/SKILL.md +1 -1
  56. package/skills/what-needs-me/SKILL.md +1 -1
  57. package/skills/what-this-repo-needs/SKILL.md +1 -1
  58. package/skills/whats-happening/SKILL.md +2 -2
  59. package/skills/whats-happening/references/status-reply.md +1 -1
  60. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  61. package/vendor/settings/index.d.ts +4 -0
  62. package/vendor/settings/index.js +2937 -0
  63. package/vendor/settings/provenance.json +148 -0
@@ -2,18 +2,19 @@
2
2
  // is-it-mergeable.mjs — the three legs of merge evidence for one PR (checks, reviewer signal,
3
3
  // unresolved threads) judged under the policy the tenant contract resolves for its repository, plus
4
4
  // the prerequisites a queue applies before any leg counts (open, not a draft, no conflict). Every
5
- // input comes from `catalyst query`, `contract` and, when fresh, `replica sql`.
5
+ // input comes from cloud-backed `catalyst query` and `contract`; --local-threads opts into `replica sql`.
6
6
  // Exit 0 no leg is red, 1 a leg or prerequisite is red, 2 this machine is not connected.
7
7
  import { parseJson, runCli, runCliOrExit } from "./lib/cli.mjs";
8
8
  import { resolvePull, truthy } from "./lib/pull.mjs";
9
9
 
10
- const HELP = `Usage: node scripts/is-it-mergeable.mjs <ticket | pr-node-id> [--json]
10
+ const HELP = `Usage: node scripts/is-it-mergeable.mjs <ticket | pr-node-id> [--json] [--local-threads]
11
11
 
12
12
  Judges one pull request against the merge policy the tenant contract resolves for its repository:
13
13
  checks every gating check at the head completed without failing (pending or absent is not green)
14
14
  reviewer the reviewer signal the policy requires; the clean-pass reaction itself is not mirrored,
15
15
  so this leg is reported as inconclusive rather than guessed, unless the policy waives it
16
- threads zero unresolved review threads, read from the local replica when it is fresh
16
+ threads inconclusive when the cloud PR detail carries no thread count;
17
+ --local-threads opts into reading a local replica caught up to a probed cloud head
17
18
 
18
19
  A red prerequisite (closed, draft, merge conflict) is also exit 1. Inconclusive legs never fail the
19
20
  exit code; the cloud's own evaluator, which holds the reaction and the thread ancestry, is authoritative.
@@ -32,14 +33,20 @@ if (!target) {
32
33
  }
33
34
 
34
35
  const { ticket, detail } = resolvePull(target);
35
- const merge = parseJson(runCliOrExit(["contract", "--path", "merge", "--json"]).stdout);
36
+ const merge = parseJson(
37
+ runCliOrExit(["contract", "--path", "merge", "--json"]).stdout,
38
+ );
36
39
  if (!merge || !Array.isArray(merge.policies)) {
37
- console.error("the contract's merge block did not parse; run: catalyst contract --refresh");
40
+ console.error(
41
+ "the contract's merge block did not parse; run: catalyst contract --refresh",
42
+ );
38
43
  process.exit(1);
39
44
  }
40
45
 
41
- const repoId = String(detail.repo_id ?? "");
42
- const repoRow = (merge.repositories ?? []).find((r) => `${r.owner}/${r.name}`.toLowerCase() === repoId.toLowerCase());
46
+ const repoId = typeof detail.repo_id === "string" ? detail.repo_id : "";
47
+ const repoRow = (merge.repositories ?? []).find(
48
+ (r) => `${r.owner}/${r.name}`.toLowerCase() === repoId.toLowerCase(),
49
+ );
43
50
  const policy = repoRow?.policy ?? merge.defaultPolicy;
44
51
  const policySource = repoRow ? repoRow.policySource : "default";
45
52
  const requiresReviewer = policy !== "checks-and-threads";
@@ -50,75 +57,233 @@ const add = (leg, state, line) => legs.push({ leg, state, line });
50
57
 
51
58
  // ── prerequisites the queue applies before any leg counts ──────────────────────────────────────
52
59
  if (truthy(detail.merged)) {
53
- add("state", "pass", `already merged${detail.merged_at ? ` at ${new Date(Number(detail.merged_at)).toISOString()}` : ""}; nothing left to judge`);
60
+ add(
61
+ "state",
62
+ "pass",
63
+ `already merged${detail.merged_at ? ` at ${new Date(Number(detail.merged_at)).toISOString()}` : ""}; nothing left to judge`,
64
+ );
54
65
  } else {
55
66
  const state = String(detail.state ?? "").toLowerCase();
56
- if (state && state !== "open") add("state", "fail", `the PR is ${state}, not open`);
57
- else if (truthy(detail.draft)) add("state", "fail", "the PR is still a draft; the pr phase marks it ready for review, a queue never takes a draft");
67
+ if (state && state !== "open")
68
+ add("state", "fail", `the PR is ${state}, not open`);
69
+ else if (truthy(detail.draft))
70
+ add(
71
+ "state",
72
+ "fail",
73
+ "the PR is still a draft; the pr phase marks it ready for review, a queue never takes a draft",
74
+ );
58
75
  else add("state", "pass", "open and ready for review");
59
76
  const ms = String(detail.mergeable_state ?? "").toLowerCase();
60
- if (ms === "dirty" || detail.mergeable === false || detail.mergeable === 0) add("conflict", "fail", `GitHub reports a merge conflict with the base branch (mergeable_state=${ms || "unknown"}); a remediate round rebases it`);
77
+ if (ms === "dirty" || detail.mergeable === false || detail.mergeable === 0)
78
+ add(
79
+ "conflict",
80
+ "fail",
81
+ `GitHub reports a merge conflict with the base branch (mergeable_state=${ms || "unknown"}); a remediate round rebases it`,
82
+ );
61
83
  else if (ms) add("conflict", "pass", `GitHub reports mergeable_state=${ms}`);
62
- else add("conflict", "inconclusive", "GitHub has not reported a mergeable state for this head yet");
84
+ else
85
+ add(
86
+ "conflict",
87
+ "inconclusive",
88
+ "GitHub has not reported a mergeable state for this head yet",
89
+ );
63
90
  }
64
91
 
65
92
  // ── leg 1: checks ───────────────────────────────────────────────────────────────────────────────
66
93
  const checks = Array.isArray(detail.checks) ? detail.checks : [];
67
- const required = Array.isArray(merge.cloudRemediateRequiredChecks) ? merge.cloudRemediateRequiredChecks : [];
68
- const notQueue = checks.filter((c) => !/mergify|^queue:|^summary$/i.test(String(c.name ?? "")));
94
+ const required = Array.isArray(merge.cloudRemediateRequiredChecks)
95
+ ? merge.cloudRemediateRequiredChecks
96
+ : [];
97
+ const notQueue = checks.filter(
98
+ (c) => !/mergify|^queue:|^summary$/i.test(String(c.name ?? "")),
99
+ );
69
100
  const gating = notQueue.filter((c) => required.includes(String(c.name)));
70
101
  const judged = gating.length > 0 ? gating : notQueue;
71
102
  const ok = new Set(["success", "neutral", "skipped"]);
72
- const red = judged.filter((c) => String(c.status).toLowerCase() === "completed" && !ok.has(String(c.conclusion).toLowerCase()));
73
- const pending = judged.filter((c) => String(c.status).toLowerCase() !== "completed");
74
- const statuses = Array.isArray(detail.commit_statuses) ? detail.commit_statuses : [];
75
- const redStatuses = statuses.filter((s) => ["failure", "error"].includes(String(s.state).toLowerCase()));
76
- const scope = gating.length > 0 ? `the ${gating.length} required check(s) the contract names` : `every mirrored check (none of the contract's required names is present at this head)`;
77
- if (judged.length === 0 && redStatuses.length === 0) add("checks", "inconclusive", "no check has reported at this head; an unrun set is unknown, never green");
78
- else if (red.length > 0 || redStatuses.length > 0) add("checks", "fail", `red: ${[...red.map((c) => `${c.name} (${c.conclusion})`), ...redStatuses.map((s) => `${s.context} (${s.state})`)].join(", ")}`);
79
- else if (pending.length > 0) add("checks", "inconclusive", `still running: ${pending.map((c) => c.name).join(", ")} — judged over ${scope}`);
103
+ const red = judged.filter(
104
+ (c) =>
105
+ String(c.status).toLowerCase() === "completed" &&
106
+ !ok.has(String(c.conclusion).toLowerCase()),
107
+ );
108
+ const pending = judged.filter(
109
+ (c) => String(c.status).toLowerCase() !== "completed",
110
+ );
111
+ const statuses = Array.isArray(detail.commit_statuses)
112
+ ? detail.commit_statuses
113
+ : [];
114
+ const redStatuses = statuses.filter((s) =>
115
+ ["failure", "error"].includes(String(s.state).toLowerCase()),
116
+ );
117
+ const scope =
118
+ gating.length > 0
119
+ ? `the ${gating.length} required check(s) the contract names`
120
+ : `every mirrored check (none of the contract's required names is present at this head)`;
121
+ if (judged.length === 0 && redStatuses.length === 0)
122
+ add(
123
+ "checks",
124
+ "inconclusive",
125
+ "no check has reported at this head; an unrun set is unknown, never green",
126
+ );
127
+ else if (red.length > 0 || redStatuses.length > 0)
128
+ add(
129
+ "checks",
130
+ "fail",
131
+ `red: ${[...red.map((c) => `${c.name} (${c.conclusion})`), ...redStatuses.map((s) => `${s.context} (${s.state})`)].join(", ")}`,
132
+ );
133
+ else if (pending.length > 0)
134
+ add(
135
+ "checks",
136
+ "inconclusive",
137
+ `still running: ${pending.map((c) => c.name).join(", ")} — judged over ${scope}`,
138
+ );
80
139
  else add("checks", "pass", `green over ${scope}`);
81
140
 
82
141
  // ── leg 2: the reviewer signal ─────────────────────────────────────────────────────────────────
83
142
  const reviews = Array.isArray(detail.reviews) ? detail.reviews : [];
84
143
  const login = String(merge.reviewerLogin ?? "");
85
- const byReviewer = reviews.filter((r) => String(r.reviewer_name ?? "") === login || String(r.reviewer_id ?? "") === `github:${login}`);
86
- if (!requiresReviewer) add("reviewer", "pass", `policy ${policy} needs no reviewer signal`);
144
+ const byReviewer = reviews.filter(
145
+ (r) =>
146
+ String(r.reviewer_name ?? "") === login ||
147
+ String(r.reviewer_id ?? "") === `github:${login}`,
148
+ );
149
+ if (!requiresReviewer)
150
+ add("reviewer", "pass", `policy ${policy} needs no reviewer signal`);
87
151
  else if (byReviewer.length > 0) {
88
- const states = [...new Set(byReviewer.map((r) => String(r.state ?? "?")))].join(", ");
89
- add("reviewer", "inconclusive", `${login} left ${byReviewer.length} review object(s) (${states}); whether they stand at the current head or an earlier one, and whether a clean-pass reaction followed, is not mirrored${acceptsEarlierHead ? " — under this policy, findings on an earlier head plus green checks and zero unresolved threads are accepted" : " — this policy requires a clean pass at the current head"}`);
90
- } else add("reviewer", "inconclusive", `no review object from ${login} is mirrored; a clean pass is a reaction, which this read cannot see`);
152
+ const states = [
153
+ ...new Set(byReviewer.map((r) => String(r.state ?? "?"))),
154
+ ].join(", ");
155
+ add(
156
+ "reviewer",
157
+ "inconclusive",
158
+ `${login} left ${byReviewer.length} review object(s) (${states}); whether they stand at the current head or an earlier one, and whether a clean-pass reaction followed, is not mirrored${acceptsEarlierHead ? " — under this policy, findings on an earlier head plus green checks and zero unresolved threads are accepted" : " — this policy requires a clean pass at the current head"}`,
159
+ );
160
+ } else
161
+ add(
162
+ "reviewer",
163
+ "inconclusive",
164
+ `no review object from ${login} is mirrored; a clean pass is a reaction, which this read cannot see`,
165
+ );
91
166
 
92
- // ── leg 3: unresolved threads, from the replica when it is fresh ───────────────────────────────
93
- const replica = parseJson(runCli(["replica", "status", "--json"]).stdout);
94
- if (replica && replica.verdict === "fresh") {
95
- const num = Number(detail.number);
96
- const esc = repoId.replace(/'/g, "''");
97
- const sql = `select resolved, count(*) as n from pr_review_threads where repo_id = '${esc}' and pr_number = ${Number.isInteger(num) ? num : -1} group by resolved`;
98
- const rows = parseJson(runCli(["replica", "sql", sql, "--json"]).stdout) ?? [];
99
- const count = (want) => rows.filter((r) => (want === null ? r.resolved === null || r.resolved === undefined : Number(r.resolved) === want)).reduce((a, r) => a + Number(r.n ?? 0), 0);
100
- const unresolved = count(0);
101
- const unknown = count(null);
102
- if (unresolved > 0) add("threads", "fail", `${unresolved} unresolved review thread(s)`);
103
- else if (unknown > 0) add("threads", "inconclusive", `${unknown} thread(s) with no resolved flag mirrored`);
104
- else add("threads", "pass", `${count(1)} thread(s), all resolved (ancestry against force-pushes is judged by the cloud, not here)`);
167
+ // The default cloud read requires no local writer or database. Thread ancestry remains the
168
+ // cloud evaluator's responsibility; absence of thread evidence never becomes a green leg.
169
+ const localThreads = args.includes("--local-threads");
170
+ const replicaResult = localThreads
171
+ ? runCli(["replica", "status", "--probe", "--json"])
172
+ : null;
173
+ const replica =
174
+ replicaResult?.code === 0 ? parseJson(replicaResult.stdout) : null;
175
+ if (
176
+ localThreads &&
177
+ replica?.verdict === "fresh" &&
178
+ Number.isSafeInteger(replica.cursor) &&
179
+ replica.cursor >= 0 &&
180
+ replica.cursor === replica.head
181
+ ) {
182
+ const num = detail.number;
183
+ if (
184
+ typeof num !== "number" ||
185
+ !Number.isSafeInteger(num) ||
186
+ num <= 0 ||
187
+ !repoId ||
188
+ repoId !== repoId.trim() ||
189
+ repoId.length > 512 ||
190
+ /[\u0000-\u001f\u007f-\u009f]/.test(repoId)
191
+ ) {
192
+ add(
193
+ "threads",
194
+ "inconclusive",
195
+ "the PR identity is incomplete; local thread evidence could not be read",
196
+ );
197
+ } else {
198
+ const esc = repoId.replace(/'/g, "''");
199
+ const sql = `select resolved, count(*) as n from pr_review_threads where repo_id = '${esc}' and pr_number = ${num} group by resolved`;
200
+ const result = runCli(["replica", "sql", sql, "--json"]);
201
+ const rows = parseJson(result.stdout);
202
+ if (
203
+ result.code !== 0 ||
204
+ !Array.isArray(rows) ||
205
+ rows.some(
206
+ (row) =>
207
+ !row ||
208
+ ![0, 1, null].includes(row.resolved) ||
209
+ typeof row.n !== "number" ||
210
+ !Number.isSafeInteger(row.n) ||
211
+ row.n < 0,
212
+ )
213
+ ) {
214
+ add(
215
+ "threads",
216
+ "inconclusive",
217
+ "the optional local thread read could not be verified; the cloud evaluator decides",
218
+ );
219
+ } else {
220
+ const count = (want) =>
221
+ rows
222
+ .filter((r) =>
223
+ want === null
224
+ ? r.resolved === null || r.resolved === undefined
225
+ : r.resolved === want,
226
+ )
227
+ .reduce((a, r) => a + Number(r.n ?? 0), 0);
228
+ const unresolved = count(0);
229
+ const unknown = count(null);
230
+ if (unresolved > 0)
231
+ add("threads", "fail", `${unresolved} unresolved review thread(s)`);
232
+ else if (unknown > 0)
233
+ add(
234
+ "threads",
235
+ "inconclusive",
236
+ `${unknown} thread(s) with no resolved flag mirrored`,
237
+ );
238
+ else
239
+ add(
240
+ "threads",
241
+ "pass",
242
+ `${count(1)} thread(s), all resolved (ancestry against force-pushes is judged by the cloud, not here)`,
243
+ );
244
+ }
245
+ }
105
246
  } else {
106
- add("threads", "inconclusive", `the PR detail carries no thread count and the replica is ${replica?.verdict ?? "unavailable"}; start it (catalyst replica start --detach) for a local read`);
247
+ add(
248
+ "threads",
249
+ "inconclusive",
250
+ localThreads
251
+ ? "the optional local replica has not proven that it is caught up to the cloud head; the cloud evaluator decides"
252
+ : "the cloud PR detail carries no thread count; the cloud evaluator decides",
253
+ );
107
254
  }
108
255
 
109
256
  const failed = legs.filter((l) => l.state === "fail");
110
257
  const open = legs.filter((l) => l.state === "inconclusive");
111
258
  let verdict;
112
- if (failed.length > 0) verdict = `NOT MERGEABLE: ${failed.map((l) => l.leg).join(", ")}`;
113
- else if (open.length > 0) verdict = `NOT PROVEN MERGEABLE from this read (${open.map((l) => l.leg).join(", ")} inconclusive); the cloud's evaluator decides`;
259
+ if (failed.length > 0)
260
+ verdict = `NOT MERGEABLE: ${failed.map((l) => l.leg).join(", ")}`;
261
+ else if (open.length > 0)
262
+ verdict = `NOT PROVEN MERGEABLE from this read (${open.map((l) => l.leg).join(", ")} inconclusive); the cloud's evaluator decides`;
114
263
  else verdict = "MERGEABLE as far as this read can see";
115
264
 
116
265
  const header = `${repoId}#${detail.number ?? "?"}${ticket ? ` (${ticket})` : ""} head ${String(detail.head_sha ?? "").slice(0, 12)} — policy ${policy} (${policySource})`;
117
266
  if (json) {
118
- console.log(JSON.stringify({ pr: { repo_id: repoId, number: detail.number, node_id: detail.node_id, ticket, head_sha: detail.head_sha }, policy: { name: policy, source: policySource }, legs, verdict }));
267
+ console.log(
268
+ JSON.stringify({
269
+ pr: {
270
+ repo_id: repoId,
271
+ number: detail.number,
272
+ node_id: detail.node_id,
273
+ ticket,
274
+ head_sha: detail.head_sha,
275
+ },
276
+ policy: { name: policy, source: policySource },
277
+ legs,
278
+ verdict,
279
+ }),
280
+ );
119
281
  } else {
120
282
  console.log(header);
121
- for (const l of legs) console.log(`${l.state === "pass" ? "ok " : l.state === "fail" ? "FAIL" : "? "} ${l.leg}: ${l.line}`);
283
+ for (const l of legs)
284
+ console.log(
285
+ `${l.state === "pass" ? "ok " : l.state === "fail" ? "FAIL" : "? "} ${l.leg}: ${l.line}`,
286
+ );
122
287
  console.log(verdict);
123
288
  }
124
289
  process.exit(failed.length > 0 ? 1 : 0);
@@ -12,7 +12,7 @@ const HELP = `Usage: node scripts/read-pr.mjs <ticket | pr-node-id> [--all] [--j
12
12
  --json print the raw detail document instead of the summary
13
13
 
14
14
  Exit 0 shown, 1 not found or no PR yet, 2 this machine is not connected to a tenant.
15
- The first stderr line names the source the CLI read from (a fresh replica, or the API).`;
15
+ The first stderr line names the source the CLI read from (the API by default, or an explicitly selected replica).`;
16
16
 
17
17
  const args = process.argv.slice(2);
18
18
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -52,8 +52,15 @@ if (all) {
52
52
  process.exit(1);
53
53
  }
54
54
  for (const p of pulls) {
55
- const flags = [truthy(p.draft) ? "draft" : null, truthy(p.merged) ? "merged" : null].filter(Boolean).join(" ");
56
- console.log(`${fmt(p.repo_id)}#${fmt(p.number)} ${fmt(p.state)}${flags ? ` ${flags}` : ""} ${fmt(p.title, "")} [${fmt(p.node_id, "")}]`);
55
+ const flags = [
56
+ truthy(p.draft) ? "draft" : null,
57
+ truthy(p.merged) ? "merged" : null,
58
+ ]
59
+ .filter(Boolean)
60
+ .join(" ");
61
+ console.log(
62
+ `${fmt(p.repo_id)}#${fmt(p.number)} ${fmt(p.state)}${flags ? ` ${flags}` : ""} ${fmt(p.title, "")} [${fmt(p.node_id, "")}]`,
63
+ );
57
64
  }
58
65
  process.exit(0);
59
66
  }
@@ -66,23 +73,44 @@ if (json) {
66
73
 
67
74
  const checks = Array.isArray(detail.checks) ? detail.checks : [];
68
75
  const reviews = Array.isArray(detail.reviews) ? detail.reviews : [];
69
- const statuses = Array.isArray(detail.commit_statuses) ? detail.commit_statuses : [];
70
- const state = [fmt(detail.state), truthy(detail.draft) ? "draft" : null, truthy(detail.merged) ? `merged ${when(detail.merged_at)}` : null].filter(Boolean).join(", ");
76
+ const statuses = Array.isArray(detail.commit_statuses)
77
+ ? detail.commit_statuses
78
+ : [];
79
+ const state = [
80
+ fmt(detail.state),
81
+ truthy(detail.draft) ? "draft" : null,
82
+ truthy(detail.merged) ? `merged ${when(detail.merged_at)}` : null,
83
+ ]
84
+ .filter(Boolean)
85
+ .join(", ");
71
86
 
72
- console.log(`${fmt(detail.repo_id)}#${fmt(detail.number)} ${fmt(detail.title, "(no title yet)")}`);
73
- console.log(`state: ${state} by ${fmt(detail.author_login)} opened ${when(detail.created_at)} updated ${when(detail.updated_at)}`);
74
- console.log(`branch: ${fmt(detail.head_ref)} → ${fmt(detail.base_ref)} head ${fmt(detail.head_sha).slice(0, 12)}`);
75
- console.log(`ticket: ${fmt(ticket ?? detail.linear_issue_identifier, "none named")}${detail.linked_issue_state ? ` (stage: ${detail.linked_issue_state})` : ""}`);
76
- console.log(`github says: mergeable=${fmt(detail.mergeable, "unknown")} state=${fmt(detail.mergeable_state, "unknown")} auto-merge=${truthy(detail.auto_merge) ? "on" : "off"}`);
87
+ console.log(
88
+ `${fmt(detail.repo_id)}#${fmt(detail.number)} ${fmt(detail.title, "(no title yet)")}`,
89
+ );
90
+ console.log(
91
+ `state: ${state} by ${fmt(detail.author_login)} opened ${when(detail.created_at)} updated ${when(detail.updated_at)}`,
92
+ );
93
+ console.log(
94
+ `branch: ${fmt(detail.head_ref)} → ${fmt(detail.base_ref)} head ${fmt(detail.head_sha).slice(0, 12)}`,
95
+ );
96
+ console.log(
97
+ `ticket: ${fmt(ticket ?? detail.linear_issue_identifier, "none named")}${detail.linked_issue_state ? ` (stage: ${detail.linked_issue_state})` : ""}`,
98
+ );
99
+ console.log(
100
+ `github says: mergeable=${fmt(detail.mergeable, "unknown")} state=${fmt(detail.mergeable_state, "unknown")} auto-merge=${truthy(detail.auto_merge) ? "on" : "off"}`,
101
+ );
77
102
  if (detail.blocked_on_ask && typeof detail.blocked_on_ask === "object") {
78
103
  const b = detail.blocked_on_ask;
79
- console.log(`blocked on an ask: ${fmt(b.identifier ?? b.id, "")} ${fmt(b.title, "")}`.trim());
104
+ console.log(
105
+ `blocked on an ask: ${fmt(b.identifier ?? b.id, "")} ${fmt(b.title, "")}`.trim(),
106
+ );
80
107
  }
81
108
  console.log(`node id: ${fmt(detail.node_id)}`);
82
109
 
83
110
  console.log(`checks (${checks.length}):`);
84
111
  if (checks.length === 0) console.log(" none reported at this head yet");
85
- for (const c of checks) console.log(` ${fmt(c.status)}/${fmt(c.conclusion, "-")} ${fmt(c.name)}`);
112
+ for (const c of checks)
113
+ console.log(` ${fmt(c.status)}/${fmt(c.conclusion, "-")} ${fmt(c.name)}`);
86
114
 
87
115
  if (statuses.length > 0) {
88
116
  console.log(`commit statuses (${statuses.length}):`);
@@ -90,8 +118,16 @@ if (statuses.length > 0) {
90
118
  }
91
119
 
92
120
  console.log(`reviews (${reviews.length}):`);
93
- if (reviews.length === 0) console.log(" none mirrored; a reviewer's clean pass is a reaction, which this read does not carry");
94
- for (const r of reviews) console.log(` ${fmt(r.state)} ${fmt(r.reviewer_name, fmt(r.reviewer_id))} ${when(r.submitted_at)}`);
121
+ if (reviews.length === 0)
122
+ console.log(
123
+ " none mirrored; a reviewer's clean pass is a reaction, which this read does not carry",
124
+ );
125
+ for (const r of reviews)
126
+ console.log(
127
+ ` ${fmt(r.state)} ${fmt(r.reviewer_name, fmt(r.reviewer_id))} ${when(r.submitted_at)}`,
128
+ );
95
129
 
96
- console.log("not mirrored here: PR labels (holds, queue attestation) and the reviewer's reaction — see references/what-a-pr-accumulates.md");
130
+ console.log(
131
+ "not mirrored here: PR labels (holds, queue attestation) and the reviewer's reaction — see references/what-a-pr-accumulates.md",
132
+ );
97
133
  process.exit(0);
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: catalyst-linear
3
3
  description: >-
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 local replica when it is fresh and the origin-fresh API otherwise, 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).
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.14.5 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.7 — written in this repository for customer accounts -->
8
8
 
9
9
  # Catalyst Linear
10
10
 
11
- You read tickets and write to them in the person's cloud account, as Catalyst. Reads come from the replica when it is fresh, else the API, and name their source. Writes go out as the app actor, with routes, stage ids, label ids and the bookkeeping marker resolved from the contract by the `catalyst` CLI.
11
+ You read tickets and write to them in the person's cloud account, as Catalyst. Reads come from the cloud API by default and name their source. A person may explicitly choose a local replica read; setup does not require a replica. Writes go out as the app actor, with routes, stage ids, label ids and the bookkeeping marker resolved from the contract by the `catalyst` CLI.
12
12
 
13
13
  ## Run first
14
14
 
@@ -4,13 +4,13 @@
4
4
 
5
5
  Every `query` verb, and therefore `node scripts/read-ticket.mjs` and `node scripts/search.mjs`, prints one stderr line before the answer:
6
6
 
7
- - `source: replica (cursor N)`: the local replica was fresh, so the answer is local and cheap.
8
- - `source: api (replica stale|absent|not configured)`: the answer came from the origin-fresh cloud API; a stale replica always falls back.
7
+ - `source: replica (--source replica)`: the person explicitly selected the optional local replica.
8
+ - `source: api (cloud reads by default)`: the answer came from the cloud API; no local replica is needed.
9
9
  - `source: api (<verb> is api-only)`: search, cycles, pull detail and change feeds never come from the replica.
10
10
 
11
11
  Quote the source when a fact's freshness matters ("as of the replica at cursor N", "from the API just now").
12
12
 
13
- `--source replica` or `--source api` forces one. Forcing the replica when it is absent or stale is refused.
13
+ `--source replica` or `--source api` forces one. Forcing the replica when it is absent is refused. No offline-browsing promise is made.
14
14
 
15
15
  ## Then the row
16
16
 
@@ -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.5 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.7 — 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. 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
 
@@ -4,7 +4,9 @@ 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
 
@@ -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.5 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.7 — 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.5 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.7 — written in this repository for customer accounts -->
8
8
 
9
9
  # Unstick
10
10