@catalyst-cloud/cli 0.14.7 → 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 (43) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -2
  3. package/dist/args.js +17 -5
  4. package/dist/browser.js +37 -3
  5. package/dist/cli.js +4 -6
  6. package/dist/config.js +1 -2
  7. package/dist/consent-browser.js +58 -0
  8. package/dist/env/onboard-draft-store.js +10 -3
  9. package/dist/env/settings-authority.js +3 -1
  10. package/dist/events-cloud-tail.js +25 -13
  11. package/dist/events-cloud.js +124 -5
  12. package/dist/events.js +175 -29
  13. package/dist/oauth.js +4 -1
  14. package/dist/onboard-github.js +14 -3
  15. package/dist/onboard-next.js +139 -0
  16. package/dist/onboard-personal.js +14 -3
  17. package/dist/onboard-repositories.js +3 -4
  18. package/dist/onboard-runtime.js +4 -4
  19. package/dist/onboard-ui.js +14 -76
  20. package/dist/onboard-workspace.js +14 -3
  21. package/dist/onboard.js +63 -12
  22. package/dist/pagination.js +13 -1
  23. package/dist/query.js +39 -16
  24. package/dist/repository-id.js +5 -0
  25. package/package.json +1 -1
  26. package/skills/catalyst-github/SKILL.md +3 -1
  27. package/skills/catalyst-github/references/local-replica.md +78 -0
  28. package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
  29. package/skills/catalyst-linear/SKILL.md +3 -1
  30. package/skills/catalyst-linear/references/local-replica.md +91 -0
  31. package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
  32. package/skills/catalyst-onboard/SKILL.md +3 -2
  33. package/skills/catalyst-onboard/references/local-replica.md +40 -0
  34. package/skills/catalyst-onboard/references/local-sync.md +5 -3
  35. package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
  36. package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
  37. package/skills/run-this-project/SKILL.md +1 -1
  38. package/skills/unstick/SKILL.md +1 -1
  39. package/skills/what-needs-me/SKILL.md +1 -1
  40. package/skills/what-this-repo-needs/SKILL.md +1 -1
  41. package/skills/whats-happening/SKILL.md +3 -1
  42. package/skills/whats-happening/references/local-replica.md +51 -0
  43. package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
@@ -1,5 +1,5 @@
1
1
  import { createOnboardProgress, } from "./onboard-progress.js";
2
- import { ONBOARD_STEPS, ONBOARD_TITLES, onboardIdentityLines, } from "./onboard.js";
2
+ import { ONBOARD_STEPS, ONBOARD_TITLES, onboardIdentityLines, onboardNextActions, onboardReadyForWork, onboardStepAction, } from "./onboard.js";
3
3
  export function shouldUseOnboardUi(args, stdoutTty) {
4
4
  return (stdoutTty &&
5
5
  !args.json &&
@@ -194,81 +194,9 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
194
194
  }
195
195
  start(ONBOARD_TITLES[id]);
196
196
  },
197
- stepEnd(step) {
197
+ stepEnd(step, journal) {
198
198
  stop();
199
- const reasons = {
200
- account_enrollment_required: "No coding account is enrolled. An administrator can add one in the workspace's coding-account settings, then run catalyst onboard again.",
201
- account_identity_unverified: "Your login changed while coding accounts were checked. Run catalyst onboard again to verify your person and workspace.",
202
- account_inventory_unavailable: "Coding accounts could not be read. Run catalyst onboard again to retry.",
203
- account_inventory_unverified: "The server did not return a fresh supported coding-account list. Run catalyst onboard after the server update.",
204
- account_validation_admin_required: "An administrator must check the workspace's coding-account access.",
205
- account_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
206
- account_validation_unavailable: "The provider check could not finish. Run catalyst onboard again to retry.",
207
- account_validation_unverified: "The server did not return a fresh provider check. Run catalyst onboard after the server update.",
208
- account_provider_access_unverified: "Coding-account provider access is not freshly verified. Check the account in the workspace's coding-account settings, then resume setup.",
209
- codex_provider_access_unverified: "Codex credentials are stored, but provider access is not freshly verified. Setup did not refresh or rotate the Codex login.",
210
- account_provider_walled: "Claude's usage limit is spent for now. Check the account's reset time, then run catalyst onboard again.",
211
- account_provider_rejected: "Claude refused the stored login. Replace it in the workspace's coding-account settings, then resume setup.",
212
- personal_consent_handoff: "The browser approval link could not be verified. Run catalyst onboard to try again.",
213
- personal_identity_refused: "Your current login no longer matches this setup. Resume with the original workspace and person.",
214
- personal_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
215
- onboard_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
216
- personal_status_unavailable: "Your personal connection could not be checked. Run catalyst onboard to try again.",
217
- personal_browser_unavailable: "The browser could not open. Run catalyst onboard to try again.",
218
- cloud_capability_unavailable: "This setup step is not available on this server yet. Use the web app link shown above, then resume after the server update.",
219
- onboarding_capability_unavailable: "The server's setup capabilities could not be checked. Run catalyst onboard to try again.",
220
- onboarding_capability_identity_unverified: "The server's setup capabilities belong to another login or workspace. Sign in to the original workspace to resume.",
221
- onboarding_capability_login_refresh_required: "Your login needs a refresh before setup capabilities can be checked. Run catalyst login, then catalyst onboard.",
222
- workspace_consent_refused: "The server did not accept this login for Linear workspace setup. Sign in again or ask your workspace administrator.",
223
- github_installation_status_unavailable: "Your GitHub App installation could not be checked. Run catalyst onboard to try again.",
224
- github_installation_status_shape: "The server returned a GitHub installation result that could not be verified.",
225
- github_installation_consent_handoff: "The GitHub installation approval link could not be verified. Run catalyst onboard to try again.",
226
- github_installation_consent_refused: "The server did not accept this login for GitHub App setup. Sign in again or ask your workspace administrator.",
227
- github_installation_identity_refused: "GitHub setup belongs to another login or workspace. Sign in to the original workspace to resume.",
228
- github_installation_admin_required: "A workspace owner or administrator approves the GitHub App installation.",
229
- github_installation_login_refresh_required: "Your login needs a refresh. Run catalyst onboard to resume.",
230
- github_installation_browser_unavailable: "The GitHub App approval page could not be opened. Use your workspace's Connections page, then resume.",
231
- step_not_available_in_this_release: "This setup step is not available yet.",
232
- prerequisite_not_ready: "Waiting for an earlier setup step.",
233
- local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
234
- local_sync_capability_unavailable: "Local sync was selected but could not be verified.",
235
- member_scope: "Your workspace administrator handles this step.",
236
- onboarding_checks_pending: "Some required checks are still unverified.",
237
- interrupted: "Setup paused. Run the same command to resume.",
238
- signin_timeout: "Sign-in timed out. Run catalyst onboard to try again.",
239
- linear_workspace_unverified: "Your Linear workspace connection could not be verified.",
240
- team_inventory_empty: "No existing Linear teams could be verified.",
241
- team_inventory_shape: "The cloud returned a team list that could not be verified.",
242
- team_read_unavailable: "Your existing Linear teams could not be read. Run catalyst onboard to try again.",
243
- team_read_identity_unverified: "Your login changed while setup was reading this workspace. Resume with the original workspace and person.",
244
- project_identity_unverified: "Your login changed while setup was checking this project. Resume with the original workspace and person.",
245
- project_options_unavailable: "Available teams and repositories could not be checked. Run catalyst onboard to try again.",
246
- project_provider_inventory_unavailable: "Linear or GitHub access could not be verified. Check this workspace's connections, then resume.",
247
- project_options_unverified: "The server returned a team or repository list that could not be verified.",
248
- project_registration_visibility_pending: "Your project exists, but its repository binding is not visible yet. Run catalyst onboard to check again.",
249
- project_binding_conflict: "This team or repository is already bound to a project. Ask your workspace administrator to check its binding, then resume.",
250
- project_login_refresh_required: "Renew your login with catalyst onboard, then review the plan before creating this project.",
251
- project_create_unverified: "Project creation could not be confirmed. Run catalyst onboard to check for the project before trying again.",
252
- team_read_login_refresh_required: "Your login needs a refresh. Run catalyst onboard to resume.",
253
- team_choice_required: "Choose an existing team with catalyst onboard --team <team ID or key>.",
254
- team_selection_unverified: "Your selected Linear team could not be verified. Run catalyst onboard --team <team ID or key> to choose again.",
255
- repository_choice_required: "Choose repositories with catalyst onboard --repo <owner/name>. Repeat --repo to choose more than one.",
256
- repository_inventory_empty: "No accessible repositories are registered to this Linear team yet.",
257
- repository_read_unavailable: "Your repository list could not be read. Run catalyst onboard to try again.",
258
- repository_inventory_shape: "The cloud returned a repository list that could not be verified.",
259
- repository_contract_unavailable: "Current repository IDs could not be checked. Run catalyst onboard to try again.",
260
- repository_contract_unverified: "The cloud returned repository IDs that could not be verified for this workspace.",
261
- repository_id_unverified: "A repository's current ID could not be verified. Keep your selection and try again.",
262
- repository_binding_ambiguous: "A repository's Linear team binding is ambiguous. Your administrator needs to check it.",
263
- repository_identity_unverified: "Sign in to the original workspace to choose repositories.",
264
- settings_review_timeout: "The settings review timed out. Run catalyst onboard to resume.",
265
- settings_checkout_unverified: "A selected repository's local checkout or full settings file could not be verified. Resume from its checkout.",
266
- settings_draft_storage_unavailable: "A private settings draft could not be kept. Run catalyst onboard to retry.",
267
- settings_draft_storage_timeout: "Keeping your settings draft took too long. Run catalyst onboard to retry.",
268
- settings_approval_unverified: "Settings still need review and approval. Kept drafts and local validation do not approve them.",
269
- repository_selection_unverified: "Your selected repositories could not be verified. Run catalyst onboard --repo <owner/name> to choose again.",
270
- };
271
- const text = `${ONBOARD_TITLES[step.id]}${step.reason ? `: ${reasons[step.reason] ?? step.reason.replaceAll("_", " ")}` : ""}`;
199
+ const text = `${ONBOARD_TITLES[step.id]}${step.reason ? `: ${onboardStepAction(journal, step, deps.baseUrl?.())}` : ""}`;
272
200
  const kind = step.state === "done"
273
201
  ? "info"
274
202
  : step.state === "failed"
@@ -283,12 +211,22 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
283
211
  message,
284
212
  finish(journal, only) {
285
213
  stop();
286
- let text = "Setup still needs checks. Run catalyst onboard to resume.";
214
+ let text = [
215
+ "Setup still needs these steps:",
216
+ ...onboardNextActions(journal, deps.baseUrl?.(), only),
217
+ "resume: catalyst onboard",
218
+ ].join("\n");
287
219
  if (abort.signal.aborted)
288
220
  text =
289
221
  "Setup paused. Your progress is saved.\nresume: catalyst onboard";
290
222
  else if (journal.complete && journal.exit === 0 && !only)
291
223
  text = "Onboarding complete.";
224
+ else if (onboardReadyForWork(journal, only))
225
+ text = [
226
+ "Ready for work.",
227
+ "Next, when you want:",
228
+ ...onboardNextActions(journal, deps.baseUrl?.()),
229
+ ].join("\n");
292
230
  else if (only && journal.exit === 0)
293
231
  text = `${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.\nresume: catalyst onboard`;
294
232
  prompts.outro(text, { output: streams.output });
@@ -1,3 +1,4 @@
1
+ import { finishOnTheWeb } from "./consent-browser.js";
1
2
  import { loadConfig, normalizeBaseUrl } from "./config.js";
2
3
  import { pollConsent } from "./onboard-consent.js";
3
4
  const STATUS = "/api/v1/me/connections/linear/workspace";
@@ -252,11 +253,15 @@ export function linearWorkspaceAdapter(options) {
252
253
  return { state: "refused", reason: "workspace_admin_required" };
253
254
  ctx.stderr("Approve the organization's Linear connection in your browser. Your personal approval follows.");
254
255
  // The signed URL is transient credential material: only the browser receives it.
256
+ let browserUnavailable = false;
255
257
  try {
256
- options.openBrowser(url);
258
+ await options.openBrowser(url, signal);
257
259
  }
258
260
  catch {
259
- return waiting("workspace_browser_unavailable");
261
+ if (signal?.aborted)
262
+ return waiting("interrupted");
263
+ browserUnavailable = true;
264
+ ctx.stderr(finishOnTheWeb(current.baseUrl, "connections", "connect Linear"));
260
265
  }
261
266
  let latest = waiting("workspace_status_unavailable");
262
267
  const run = () => pollConsent({
@@ -271,6 +276,8 @@ export function linearWorkspaceAdapter(options) {
271
276
  outcome: "waiting",
272
277
  reason: "cloud_capability_unavailable",
273
278
  };
279
+ if (latest.reason === "workspace_login_refresh_required")
280
+ return { outcome: "waiting", reason: latest.reason };
274
281
  if (latest.state === "done")
275
282
  return { outcome: "connected" };
276
283
  if (latest.state === "refused" || latest.state === "failed")
@@ -283,7 +290,11 @@ export function linearWorkspaceAdapter(options) {
283
290
  const result = options.wait
284
291
  ? await options.wait("Waiting for workspace approval", run)
285
292
  : await run();
286
- return result.state === "done" ? latest : result;
293
+ if (result.state === "done")
294
+ return latest;
295
+ return browserUnavailable && result.reason === "consent_timeout"
296
+ ? waiting("workspace_browser_unavailable")
297
+ : result;
287
298
  },
288
299
  };
289
300
  }
package/dist/onboard.js CHANGED
@@ -8,6 +8,7 @@ import { machinePathsFile } from "../vendor/paths/node.js";
8
8
  import { configPathFor } from "./config.js";
9
9
  import { CliError, UsageError } from "./errors.js";
10
10
  import { onboardFileSnapshot } from "./onboard-file-snapshot.js";
11
+ import { onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
11
12
  import { parseBootstrapPlan, bootstrapPlanHash, bootstrapPlanLines, } from "./onboard-bootstrap.js";
12
13
  export const ONBOARD_STEPS = [
13
14
  "machine",
@@ -102,15 +103,14 @@ export const ONBOARD_DEPENDENCIES = {
102
103
  values: ["settings"],
103
104
  capacity: ["projects"],
104
105
  daemon: ["signin"],
106
+ // What dispatch needs. Environment approval gates only implement and pr, and the first phase
107
+ // needs no imported values, so repository settings and local sync do not hold a first ticket.
105
108
  "first-ticket": [
106
109
  "projects",
107
110
  "linear.adopt",
108
111
  "linear.automations",
109
112
  "accounts",
110
- "settings",
111
- "values",
112
113
  "capacity",
113
- "daemon",
114
114
  ],
115
115
  };
116
116
  const ADMIN_STEPS = new Set([
@@ -604,6 +604,41 @@ function stepSatisfied(step) {
604
604
  (step.id === "linear.automations" &&
605
605
  step.reason === "automation_management_unavailable"))));
606
606
  }
607
+ /** Steps whose wait does not hold a full run's exit code: work runs without them. A failure still
608
+ * fails the run, and the receipt's `complete` still needs them satisfied. Never used for gating. */
609
+ export const ONBOARD_DEFERRED_STEPS = new Set([
610
+ "settings",
611
+ "values",
612
+ "housekeeping",
613
+ "first-ticket",
614
+ ]);
615
+ /** A full run that exited 0 with deferred steps still open: ready for work, not complete. */
616
+ export function onboardReadyForWork(journal, only) {
617
+ return !only && journal.exit === 0 && !journal.complete;
618
+ }
619
+ /** A recorded step's next action; a prerequisite wait names the first unfinished step it needs. */
620
+ export function onboardStepAction(journal, step, baseUrl) {
621
+ if (!step.reason)
622
+ return "Not checked yet. Run catalyst onboard.";
623
+ const waitsFor = journal &&
624
+ (ONBOARD_DEPENDENCIES[step.id] ?? []).find((parent) => !stepSatisfied(journalStep(journal, parent)));
625
+ return onboardReasonText(step, {
626
+ baseUrl,
627
+ journal,
628
+ ...(waitsFor ? { waitsFor: ONBOARD_TITLES[waitsFor] } : {}),
629
+ });
630
+ }
631
+ /** "<title>: <action>" for each recorded step still blocking onboarding, then the first-ticket hint
632
+ * once the steps a ticket needs are done. Display only: completeness and exits stay with the engine. */
633
+ export function onboardNextActions(journal, baseUrl, only) {
634
+ const lines = journal.steps
635
+ .filter((step) => (!only || step.id === only) && !stepSatisfied(step))
636
+ .sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id))
637
+ .map((step) => `${ONBOARD_TITLES[step.id]}: ${onboardStepAction(journal, step, baseUrl)}`);
638
+ if (["projects", "accounts", "linear.adopt", "capacity"].every((id) => journalStep(journal, id)?.state === "done"))
639
+ lines.push(`Move a ticket in ${onboardTeamKey(journal) ?? "<TEAM KEY>"} to Todo; \`catalyst explain <ticket>\` says why it is or is not starting.`);
640
+ return lines;
641
+ }
607
642
  function requireMatchingIdentity(journal, identity) {
608
643
  const account = journal.account ?? journal.tenant;
609
644
  if ((account && (!identity || account !== identity.account)) ||
@@ -920,7 +955,10 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
920
955
  process.on(name, interrupted);
921
956
  const finish = (code) => {
922
957
  journal.exit = code;
923
- journal.complete = !only && code === 0;
958
+ journal.complete =
959
+ !only &&
960
+ code === 0 &&
961
+ ONBOARD_STEPS.every((id) => stepSatisfied(journalStep(journal, id)));
924
962
  writeOnboardJournal(statePath, journal);
925
963
  if (args.json)
926
964
  ctx.stdout(JSON.stringify(journal));
@@ -929,10 +967,19 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
929
967
  else {
930
968
  if (journal.complete)
931
969
  ctx.stdout("Onboarding complete.");
970
+ else if (onboardReadyForWork(journal, only)) {
971
+ ctx.stdout("Ready for work.");
972
+ ctx.stdout("Next, when you want:");
973
+ for (const line of onboardNextActions(journal, savedOnboardBaseUrl(ctx.home)))
974
+ ctx.stdout(line);
975
+ }
932
976
  else if (only && code === 0)
933
977
  ctx.stdout(`${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.`);
934
- else
978
+ else {
935
979
  ctx.stdout(`Setup still needs ${journal.steps.filter((step) => !stepSatisfied(step)).length} checks.`);
980
+ for (const line of onboardNextActions(journal, savedOnboardBaseUrl(ctx.home), only))
981
+ ctx.stdout(line);
982
+ }
936
983
  ctx.stdout(bootstrap && journalStep(journal, "machine")?.state !== "done"
937
984
  ? "resume: run the original setup command"
938
985
  : "resume: catalyst onboard");
@@ -1086,7 +1133,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1086
1133
  reason: "member_scope",
1087
1134
  at: isoNow(ctx, deps),
1088
1135
  });
1089
- deps.ui?.stepEnd(journalStep(journal, id));
1136
+ deps.ui?.stepEnd(journalStep(journal, id), journal);
1090
1137
  continue;
1091
1138
  }
1092
1139
  const missing = (ONBOARD_DEPENDENCIES[id] ?? []).find((parent) => !stepSatisfied(journalStep(journal, parent)));
@@ -1098,7 +1145,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1098
1145
  at: isoNow(ctx, deps),
1099
1146
  });
1100
1147
  writeOnboardJournal(statePath, journal);
1101
- deps.ui?.stepEnd(journalStep(journal, id));
1148
+ deps.ui?.stepEnd(journalStep(journal, id), journal);
1102
1149
  continue;
1103
1150
  }
1104
1151
  let adapter = deps.adapters?.[id];
@@ -1122,7 +1169,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1122
1169
  at: isoNow(ctx, deps),
1123
1170
  });
1124
1171
  writeOnboardJournal(statePath, journal);
1125
- deps.ui?.stepEnd(journalStep(journal, id));
1172
+ deps.ui?.stepEnd(journalStep(journal, id), journal);
1126
1173
  continue;
1127
1174
  }
1128
1175
  try {
@@ -1142,7 +1189,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1142
1189
  at: isoNow(ctx, deps),
1143
1190
  });
1144
1191
  writeOnboardJournal(statePath, journal);
1145
- deps.ui?.stepEnd(journalStep(journal, id));
1192
+ deps.ui?.stepEnd(journalStep(journal, id), journal);
1146
1193
  continue;
1147
1194
  }
1148
1195
  }
@@ -1169,7 +1216,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1169
1216
  at: isoNow(ctx, deps),
1170
1217
  });
1171
1218
  writeOnboardJournal(statePath, journal);
1172
- deps.ui?.stepEnd(journalStep(journal, id));
1219
+ deps.ui?.stepEnd(journalStep(journal, id), journal);
1173
1220
  continue;
1174
1221
  }
1175
1222
  }
@@ -1223,8 +1270,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1223
1270
  });
1224
1271
  return finish(EXIT_WAITING);
1225
1272
  }
1226
- deps.ui?.stepEnd(journalStep(journal, id));
1273
+ deps.ui?.stepEnd(journalStep(journal, id), journal);
1227
1274
  writeOnboardJournal(statePath, journal);
1275
+ // Later steps share the same login; preserve the actionable cause instead of cascading failures.
1276
+ if (journalStep(journal, id)?.reason?.endsWith("_login_refresh_required"))
1277
+ return finish(EXIT_WAITING);
1228
1278
  if (refused)
1229
1279
  break;
1230
1280
  }
@@ -1234,7 +1284,8 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1234
1284
  ? EXIT_REFUSED
1235
1285
  : scope.some((id) => journalStep(journal, id)?.state === "failed")
1236
1286
  ? EXIT_FAILED
1237
- : scope.every((id) => stepSatisfied(journalStep(journal, id)))
1287
+ : scope.every((id) => stepSatisfied(journalStep(journal, id)) ||
1288
+ (!only && ONBOARD_DEFERRED_STEPS.has(id)))
1238
1289
  ? 0
1239
1290
  : EXIT_WAITING;
1240
1291
  return finish(code);
@@ -12,6 +12,8 @@
12
12
  import { CliError } from "./errors.js";
13
13
  export const NEXT_CURSOR_HEADER = "x-mirror-next-cursor";
14
14
  export const TOTAL_HEADER = "x-mirror-total";
15
+ /** CTC-4556 — the `/issues` params that narrowed the page, comma-separated; absent on older mirrors. */
16
+ export const SCOPE_HEADER = "x-mirror-scope";
15
17
  /** The server clamps every page to this regardless of the requested `limit` (MAX_VIEW_LIMIT). */
16
18
  export const SERVER_PAGE_CAP = 500;
17
19
  /** 100 pages × 500 rows = 50,000. Reaching it is a refusal, not an ending. */
@@ -29,6 +31,12 @@ export function rowsOf(body) {
29
31
  }
30
32
  return [];
31
33
  }
34
+ function readScope(headers) {
35
+ const raw = headers.get(SCOPE_HEADER);
36
+ if (raw === null)
37
+ return null;
38
+ return raw.split(",").map((p) => p.trim()).filter(Boolean);
39
+ }
32
40
  function readTotal(headers) {
33
41
  const raw = headers.get(TOTAL_HEADER);
34
42
  if (raw === null)
@@ -43,6 +51,7 @@ export async function fetchPage(api, path, query) {
43
51
  rows: rowsOf(res.body),
44
52
  nextCursor: next === null || next === "" ? null : next,
45
53
  total: readTotal(res.headers),
54
+ scope: readScope(res.headers),
46
55
  };
47
56
  }
48
57
  export async function fetchAllPages(api, path, query) {
@@ -50,13 +59,16 @@ export async function fetchAllPages(api, path, query) {
50
59
  const seen = new Set();
51
60
  let after;
52
61
  let total = null;
62
+ let scope = null;
53
63
  for (let pages = 1; pages <= MAX_PAGES; pages++) {
54
64
  const page = await fetchPage(api, path, { ...query, limit: SERVER_PAGE_CAP, after });
55
65
  rows.push(...page.rows);
56
66
  if (page.total !== null)
57
67
  total = page.total;
68
+ // A param counts as applied only if every page says so.
69
+ scope = pages === 1 ? page.scope : scope && page.scope && scope.filter((p) => page.scope?.includes(p));
58
70
  if (page.nextCursor === null)
59
- return { rows, total, pages };
71
+ return { rows, total, pages, scope };
60
72
  // ⛔ A cursor that does not advance is an infinite loop, not a long read.
61
73
  if (seen.has(page.nextCursor)) {
62
74
  throw new CliError(`GET ${path} repeated a page cursor after ${rows.length} rows — the cloud is not advancing the keyset, so --all cannot finish`, "pagination-stalled", 1);
package/dist/query.js CHANGED
@@ -20,9 +20,10 @@ export async function cmdQuery(args, ctx, deps = {}) {
20
20
  throw new UsageError("query needs a subcommand: issues | issue <id> | pulls | pull <id> | projects | cycles | search <terms> | changes --since <n>");
21
21
  const cfg = requireConfig(ctx);
22
22
  const filters = {
23
- team: flagString(args, "team"),
24
- project: flagString(args, "project"),
25
- state: flagString(args, "state"),
23
+ // CTC-4556 — trimmed here, so the value sent and the value compared are the one the mirror reads.
24
+ team: trimmedFlag(args, "team"),
25
+ project: trimmedFlag(args, "project"),
26
+ state: trimmedFlag(args, "state"),
26
27
  limit: flagInt(args, "limit", 50),
27
28
  all: flagBool(args, "all"),
28
29
  };
@@ -115,23 +116,25 @@ async function fromReplica(ctx, cfg, sub, rest, f, deps) {
115
116
  }
116
117
  async function fromApi(ctx, cfg, sub, rest, f, args) {
117
118
  const api = apiClient(cfg, ctx);
118
- const common = {
119
- team: f.team,
119
+ // CTC-4556 — the mirror narrows `/issues` by `team_key` and `state_name`; `team` and a state name
120
+ // in `state` narrow nothing there.
121
+ const issueScope = {
122
+ team_key: f.team,
120
123
  project: f.project,
121
- state: f.state,
122
- limit: f.limit,
124
+ state_name: f.state,
123
125
  };
124
126
  switch (sub) {
125
127
  case "issues": {
126
128
  if (f.all) {
127
- const { rows } = await fetchAllPages(api, "/api/v1/issues", {
128
- team: f.team,
129
- project: f.project,
130
- state: f.state,
131
- });
129
+ const { rows, scope } = await fetchAllPages(api, "/api/v1/issues", issueScope);
130
+ requireIssueScope(scope, f);
132
131
  return applyFilters(rows, f); // no .slice — --all means the whole scope
133
132
  }
134
- const page = await fetchPage(api, "/api/v1/issues", common);
133
+ const page = await fetchPage(api, "/api/v1/issues", {
134
+ ...issueScope,
135
+ limit: f.limit,
136
+ });
137
+ requireIssueScope(page.scope, f);
135
138
  const rows = applyFilters(page.rows, f).slice(0, f.limit);
136
139
  const notice = truncationNotice(page);
137
140
  if (notice)
@@ -247,6 +250,27 @@ function needArg(rest, usage) {
247
250
  throw new UsageError(`query ${usage}`);
248
251
  return v;
249
252
  }
253
+ /** CTC-4556 — refuse a page the cloud did not narrow by a filter the user asked for. Filtering it
254
+ * here would print the matches among the newest rows and call that the answer. */
255
+ function requireIssueScope(scope, f) {
256
+ const asked = [
257
+ ["--team", "team_key", f.team],
258
+ ["--state", "state_name", f.state],
259
+ ["--project", "project", f.project],
260
+ ];
261
+ for (const [flag, param, value] of asked) {
262
+ if (!value || scope?.includes(param))
263
+ continue;
264
+ throw new CliError(`the cloud did not apply ${flag} (${scope === null ? "it sent no x-mirror-scope header" : `it applied: ${scope.join(", ") || "nothing"}`}), so the page is not that scope; nothing printed. Read it with --source replica, or drop ${flag}`, "filter-not-applied", 1);
265
+ }
266
+ }
267
+ function trimmedFlag(args, name) {
268
+ const value = flagString(args, name)?.trim();
269
+ return value === "" ? undefined : value;
270
+ }
271
+ /** The mirror's comparison for team keys and state names: trimmed and lowercased, nothing more.
272
+ * "In Progress" and "In_Progress" stay different names. */
273
+ const sameName = (a, b) => String(a ?? "").trim().toLowerCase() === b.trim().toLowerCase();
250
274
  export function applyFilters(rows, f) {
251
275
  return rows.filter((r) => {
252
276
  if (f.team &&
@@ -254,12 +278,11 @@ export function applyFilters(rows, f) {
254
278
  .toUpperCase()
255
279
  .startsWith(`${f.team.toUpperCase()}-`) ||
256
280
  r.team_id === f.team ||
257
- r.team_key === f.team))
281
+ sameName(r.team_key, f.team)))
258
282
  return false;
259
283
  if (f.project && r.project_id !== f.project)
260
284
  return false;
261
- if (f.state &&
262
- String(r.state ?? "").toLowerCase() !== f.state.toLowerCase())
285
+ if (f.state && !sameName(r.state, f.state))
263
286
  return false;
264
287
  return true;
265
288
  });
@@ -0,0 +1,5 @@
1
+ /** Repository row IDs are opaque cloud identifiers, including tenant-prefixed legacy names.
2
+ * Syntax validation never establishes tenant ownership; callers must join fresh ACL and contract reads. */
3
+ export function isRepositoryId(value) {
4
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/cli",
3
- "version": "0.14.7",
3
+ "version": "0.14.8",
4
4
  "description": "Customer skill bundle for Catalyst Cloud, named from the executive's seat \u2014 get set up, see what is happening, see what needs you, run a project, unstick what is stuck \u2014 plus the catalyst CLI that reads through the Catalyst Cloud SDK, writes through the agent proxy, and connects your machine to your cloud account with one command. The skills directory is the roster; this line never counts it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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.14.7 — 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
  # Catalyst's GitHub
10
10
 
@@ -23,6 +23,8 @@ Both read through `catalyst query` and `contract` from the cloud by default; the
23
23
  | -- | -- |
24
24
  | the person asks what Catalyst did to the branch or PR, why the title changed, why history was rewritten, what a label means, or what happens on merge | `references/what-a-pr-accumulates.md` |
25
25
  | the person asks why a PR has not merged, what the queue needs, what a clean review pass looks like, or what a policy requires | `references/is-it-mergeable.md` |
26
+ | which command answers a PR question from the cloud, and what the cloud detail cannot see | `references/reading-from-the-cloud.md` |
27
+ | local SQL or `--local-threads`, only when `catalyst replica status --json` reports `configured: true` | `references/local-replica.md` |
26
28
 
27
29
  ## Rules
28
30
 
@@ -0,0 +1,78 @@
1
+ # Reading pull requests from the local replica (opt-in)
2
+
3
+ Load this only when `catalyst replica status --json` reports `configured: true`. On any other machine, read from the cloud (`references/reading-from-the-cloud.md`) and do not mention the replica.
4
+
5
+ The replica is an on-machine SQLite copy of the account. It exists only when the person opted in to local sync. It adds ad hoc SQL over mirrored pull requests and one leg the cloud PR detail lacks: review-thread rows. It is never required.
6
+
7
+ ## Check it is caught up first
8
+
9
+ Run `catalyst replica status --probe --json` before every local read. Read locally only when `verdict` is `fresh` and `cursor` equals `head`. `stale` or `absent` with `configured: true` means the person opted in and the writer is down. Answer from the cloud and say the local copy is behind.
10
+
11
+ Quote the cursor with any local answer, for example "from the local replica at cursor 4810".
12
+
13
+ ## Threads: the one leg only the replica adds
14
+
15
+ **Zero thread rows does not mean zero open threads.** The thread table is filled only by resolve and unresolve events, and nothing seeds it from GitHub. A thread opened and never touched leaves no row. So zero rows can mean every thread is still open. A local "all resolved" answers only "of the threads seen resolving, which are resolved now". It never replaces the cloud's merge evaluator, which also judges a resolution against force-pushes.
16
+
17
+ `node scripts/is-it-mergeable.mjs <ticket> --local-threads` reads the thread leg from the replica. It does so only after a probe proves the replica is caught up with the cloud head. Without the flag, the thread leg is inconclusive and the cloud's evaluator decides.
18
+
19
+ The query the script runs, for one pull request:
20
+
21
+ ```sql
22
+ select resolved, count(*) as n from pr_review_threads
23
+ where repo_id = 'acme/widgets' and pr_number = 42
24
+ group by resolved
25
+ ```
26
+
27
+ `resolved` is 1 for resolved, 0 for unresolved, and null when no flag was mirrored.
28
+
29
+ ## Running SQL yourself
30
+
31
+ `catalyst replica sql "<one SELECT>" --json` runs one read-only `SELECT` and prints rows. `catalyst replica schema <table>` prints a table's columns; read them before querying a table this page does not cover. Timestamps are milliseconds since the epoch.
32
+
33
+ | table | one row is | key |
34
+ | -- | -- | -- |
35
+ | `pull_requests` | a pull request: `state`, `draft`, `merged`, `merged_at`, `head_sha`, `head_ref`, `base_ref`, `mergeable`, `mergeable_state`, `auto_merge`, `linear_issue_identifier` | `repo_id`, `number`; also `node_id` |
36
+ | `check_runs` | one check at one commit: `name`, `status`, `conclusion` | `repo_id`, `head_sha` |
37
+ | `commit_statuses` | a legacy commit status: `context`, `state` | `repo_id`, `sha` |
38
+ | `reviews` | a submitted review: `user_id`, `state`, `submitted_at` | `repo_id`, `pr_number` |
39
+ | `pr_review_threads` | a review thread seen resolving: `resolved`, `resolved_at` | `repo_id`, `pr_number` |
40
+ | `pr_review_comments`, `pr_conversation_comments` | an inline comment, and a PR conversation comment | `repo_id`, `pr_number` |
41
+
42
+ Replace `acme/widgets`, `42` and `ENG-123` with real values.
43
+
44
+ Every pull request that names a ticket:
45
+
46
+ ```sql
47
+ select repo_id, number, state, draft, merged, head_sha, mergeable_state
48
+ from pull_requests where linear_issue_identifier = 'ENG-123'
49
+ ```
50
+
51
+ The checks at a pull request's current head:
52
+
53
+ ```sql
54
+ select c.name, c.status, c.conclusion
55
+ from check_runs c join pull_requests p
56
+ on p.repo_id = c.repo_id and p.head_sha = c.head_sha
57
+ where p.repo_id = 'acme/widgets' and p.number = 42
58
+ order by c.name
59
+ ```
60
+
61
+ The reviews on a pull request, newest first:
62
+
63
+ ```sql
64
+ select user_id, state, submitted_at from reviews
65
+ where repo_id = 'acme/widgets' and pr_number = 42
66
+ order by submitted_at desc
67
+ ```
68
+
69
+ Open pull requests that GitHub reports in conflict:
70
+
71
+ ```sql
72
+ select repo_id, number, linear_issue_identifier from pull_requests
73
+ where state = 'open' and mergeable_state = 'dirty'
74
+ ```
75
+
76
+ ## What the replica does not answer
77
+
78
+ Reactions, PR labels and the commit each review was submitted against are not mirrored anywhere a read can reach. The cloud's evaluator holds them. `references/is-it-mergeable.md` explains what that leaves inconclusive.
@@ -0,0 +1,24 @@
1
+ # Reading pull request facts from the cloud
2
+
3
+ Cloud reads are the default on every machine. They need no local writer and no local database. Every command below reads the cloud through the `catalyst` CLI.
4
+
5
+ | question | command |
6
+ | -- | -- |
7
+ | Which pull requests name this ticket? | `catalyst query pulls --ticket <KEY-123>` |
8
+ | What state is the PR in: draft, merged, conflict, auto-merge? | `node scripts/read-pr.mjs <KEY-123>` |
9
+ | Which checks are red or still running at the head? | `node scripts/read-pr.mjs <KEY-123>` |
10
+ | Is it mergeable under this repository's policy? | `node scripts/is-it-mergeable.mjs <KEY-123>`, which also reads `catalyst contract --path merge` |
11
+ | Which stage is the linked ticket in? | `catalyst query issue <KEY-123>` |
12
+ | What did the last pr or merge phase do? | `catalyst history <KEY-123>` |
13
+
14
+ The first stderr line names the source: `source: api (...)`. Quote it when freshness matters.
15
+
16
+ ## What the cloud PR detail does not carry
17
+
18
+ The detail has no review-thread count. So the thread leg of `is-it-mergeable.mjs` is inconclusive from a cloud read, and the cloud's own merge evaluator decides. A queue-ready label on the PR means it already said yes. Reactions, PR labels and the commit each review was submitted against are not in the detail either.
19
+
20
+ A smaller ticket read is coming: the cloud will answer with only the status fields and the linked pull requests. No CLI flag selects it yet, so read the full record.
21
+
22
+ ## The local replica
23
+
24
+ Only on a machine where `catalyst replica status --json` reports `configured: true`, `references/local-replica.md` adds local SQL and the opt-in `--local-threads` read. Everywhere else, these cloud reads are the whole answer.
@@ -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.14.7 — 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
  # Catalyst Linear
10
10
 
@@ -30,6 +30,8 @@ Exit codes: 0 done, 1 not found or a usage error, 2 this machine is not connecte
30
30
  | "what did Catalyst write on this ticket?", a comment shape, a label, the marker, the 👀 | `references/what-a-ticket-accumulates.md` |
31
31
  | before any read you will report on; "is this current?"; citing; searching | `references/reading-a-ticket.md` |
32
32
  | before any comment, move, label or new ticket; identity, budget, slots, ids | `references/writing-to-linear.md` |
33
+ | which command answers a ticket question from the cloud: stage, last phase, PR, what changed | `references/reading-from-the-cloud.md` |
34
+ | local SQL, only when `catalyst replica status --json` reports `configured: true` | `references/local-replica.md` |
33
35
 
34
36
  ## Rules
35
37