@catalyst-cloud/cli 0.15.2 → 0.15.4

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 (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -2
  3. package/dist/args.js +5 -1
  4. package/dist/cli.js +44 -20
  5. package/dist/oauth.js +1 -1
  6. package/dist/onboard-accounts.js +67 -171
  7. package/dist/onboard-checklist.js +310 -0
  8. package/dist/onboard-existing.js +1 -1
  9. package/dist/onboard-first-ticket.js +199 -0
  10. package/dist/onboard-headless.js +2 -2
  11. package/dist/onboard-housekeeping.js +93 -0
  12. package/dist/onboard-next.js +108 -9
  13. package/dist/onboard-ready.js +61 -0
  14. package/dist/onboard-runtime.js +60 -27
  15. package/dist/onboard-standalone-copy.js +73 -66
  16. package/dist/onboard-ui.js +278 -105
  17. package/dist/onboard-values.js +47 -0
  18. package/dist/onboard.js +63 -18
  19. package/dist/ready-copy.js +87 -0
  20. package/dist/ready.js +1 -57
  21. package/dist/setup-brand.js +202 -0
  22. package/dist/setup-onboard-copy.js +314 -42
  23. package/dist/setup-render.js +99 -17
  24. package/dist/setup.js +6 -3
  25. package/dist/terminal-background.js +220 -0
  26. package/package.json +1 -1
  27. package/skills/catalyst-github/SKILL.md +1 -1
  28. package/skills/catalyst-linear/SKILL.md +1 -1
  29. package/skills/catalyst-onboard/SKILL.md +3 -3
  30. package/skills/catalyst-onboard/references/replacing-a-credential.md +9 -15
  31. package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -11
  32. package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
  33. package/skills/run-this-project/SKILL.md +2 -2
  34. package/skills/unstick/SKILL.md +1 -1
  35. package/skills/what-needs-me/SKILL.md +1 -1
  36. package/skills/what-this-repo-needs/SKILL.md +1 -1
  37. package/skills/whats-happening/SKILL.md +2 -2
  38. package/skills/whats-happening/references/coding-accounts.md +1 -1
  39. package/skills/whats-happening/scripts/snapshot.mjs +1 -1
@@ -1,5 +1,6 @@
1
1
  import { ONBOARD_DEPENDENCIES, ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
2
- import { onboardReasonText } from "./onboard-next.js";
2
+ import { dispatchStageName, onboardReasonText } from "./onboard-next.js";
3
+ /** The step numbers `--json` reports. People read SETUP_PART_NUMBERS instead (CTC-4680). */
3
4
  export const SETUP_NUMBERS = {
4
5
  machine: 1,
5
6
  cli: 1,
@@ -23,13 +24,94 @@ export const SETUP_NUMBERS = {
23
24
  values: 16,
24
25
  "first-ticket": 17,
25
26
  };
27
+ /** CTC-4680: setup runs in three parts. The times are estimates until setup logs give medians. */
28
+ export const SETUP_PARTS = {
29
+ 1: {
30
+ title: "This computer",
31
+ estimate: "about 2 min",
32
+ summary: "skills and sign-in",
33
+ done: "This computer is set up.",
34
+ },
35
+ 2: {
36
+ title: "Linear and GitHub",
37
+ estimate: "about 10 min",
38
+ summary: "approvals in your browser",
39
+ done: "Linear and GitHub are connected.",
40
+ },
41
+ 3: {
42
+ title: "Ready for work",
43
+ estimate: "about 5 min",
44
+ summary: "a first ticket",
45
+ done: "Catalyst is ready for work.",
46
+ },
47
+ };
48
+ export const SETUP_PART_OF = {
49
+ machine: 1,
50
+ cli: 1,
51
+ skills: 1,
52
+ legacy: 1,
53
+ signin: 1,
54
+ daemon: 1,
55
+ housekeeping: 1,
56
+ "linear.workspace": 2,
57
+ "linear.personal": 2,
58
+ "linear.team": 2,
59
+ "linear.adopt": 2,
60
+ "linear.automations": 2,
61
+ "github.install": 2,
62
+ "github.personal": 2,
63
+ "github.repos": 3,
64
+ projects: 3,
65
+ accounts: 3,
66
+ capacity: 3,
67
+ runner: 3,
68
+ settings: 3,
69
+ values: 3,
70
+ "first-ticket": 3,
71
+ ready: 3,
72
+ };
73
+ /** What a person reads: each part restarts at 1, in the order setup runs the steps. Steps that
74
+ * share a row (the computer checks, repositories and projects) share a number. */
75
+ export const SETUP_PART_NUMBERS = {
76
+ machine: 1,
77
+ cli: 1,
78
+ skills: 1,
79
+ legacy: 1,
80
+ signin: 2,
81
+ housekeeping: 3,
82
+ "linear.workspace": 1,
83
+ "linear.personal": 2,
84
+ "linear.team": 3,
85
+ "linear.adopt": 4,
86
+ "linear.automations": 5,
87
+ "github.install": 6,
88
+ "github.personal": 7,
89
+ "github.repos": 1,
90
+ projects: 1,
91
+ accounts: 2,
92
+ capacity: 3,
93
+ runner: 4,
94
+ settings: 5,
95
+ values: 6,
96
+ "first-ticket": 7,
97
+ };
98
+ /** CTC-4680: people type `catalyst setup`; `catalyst onboard` stays its alias. What setup draws
99
+ * names setup. `--json` and headless output keep their own words. */
100
+ export function setupCommandWords(text) {
101
+ return text.replace(/\bcatalyst onboard\b/g, "catalyst setup");
102
+ }
103
+ /** "Part 1 of 3 · This computer", with a colon where the output is plain. */
104
+ export function setupPartHeading(part, unicode) {
105
+ return `Part ${part} of 3${unicode ? " ·" : ":"} ${SETUP_PARTS[part].title}`;
106
+ }
26
107
  const TITLES = {
27
108
  "linear.adopt": "Set up the team's workflow",
28
109
  "linear.automations": "Check Linear's PR automations",
29
110
  projects: "Choose repositories",
30
- accounts: "Add a coding account",
111
+ accounts: "Add an AI account",
112
+ housekeeping: "Daily update",
31
113
  capacity: "Check runners",
32
- values: "Review repository settings",
114
+ values: "Check repository values",
33
115
  };
34
116
  const OPTIONAL = new Set([
35
117
  "local_sync_not_selected",
@@ -83,18 +165,23 @@ function succeeded(step) {
83
165
  case "projects":
84
166
  return "repositories are now Catalyst projects";
85
167
  case "accounts":
86
- return `${value(step, "provider") === "claude" ? "Claude" : value(step, "provider") === "codex" ? "Codex" : "a coding account"} can take work`;
168
+ return `${value(step, "provider") === "claude" ? "Claude" : value(step, "provider") === "codex" ? "Codex" : "an AI account"} can take work`;
169
+ case "housekeeping":
170
+ return "scheduled, runs daily";
87
171
  case "capacity":
88
172
  return typeof step.evidence?.remainingUnits === "number"
89
173
  ? `${step.evidence.remainingUnits} runner slots available`
90
174
  : "runners can take work";
91
175
  case "settings":
92
- case "values":
93
176
  return "settings reviewed";
94
- case "first-ticket":
95
- return value(step, "ticketKey")
96
- ? `${value(step, "ticketKey")} started`
177
+ case "values":
178
+ return "values set";
179
+ case "first-ticket": {
180
+ const key = value(step, "ticket") ?? value(step, "ticketKey");
181
+ return key
182
+ ? `${key} moved to ${dispatchStageName(step.evidence?.stage)}. Catalyst picks it up next.`
97
183
  : "first ticket started";
184
+ }
98
185
  case "daemon":
99
186
  return "local sync is running";
100
187
  case "runner":
@@ -114,15 +201,23 @@ function unfinished(step) {
114
201
  return "could not verify the connection's permissions";
115
202
  if (step.reason === "housekeeping_service_unverified")
116
203
  return "no scheduler here. catalyst says when an update is out";
204
+ if (step.reason === "housekeeping_off_chosen")
205
+ return "skipped, as you chose";
206
+ if (step.reason === "housekeeping_no_scheduler")
207
+ return "can't run on this computer";
208
+ if (step.id === "housekeeping")
209
+ return "not scheduled; the install command turns it on";
117
210
  if (step.reason === "local_sync_not_selected")
118
211
  return "off, reads come from the cloud";
119
212
  if (step.reason === "runner_docker_missing")
120
213
  return "Docker is not running";
121
214
  if (step.reason === "member_scope")
122
- return "an admin handles this step";
215
+ return step.id === "linear.team" || step.id === "values"
216
+ ? "run catalyst setup --team <KEY> to choose your team"
217
+ : "an admin handles this step";
123
218
  if (step.reason === "automation_management_unavailable")
124
219
  return "could not read them; checked again before work starts";
125
- if (step.reason === "interrupted")
220
+ if (step.reason === "interrupted" || step.reason === "first_ticket_skipped")
126
221
  return "skipped for now";
127
222
  if (TIMED_OUT_REASONS.has(step.reason ?? ""))
128
223
  return "the link timed out";
@@ -134,7 +229,7 @@ function unfinished(step) {
134
229
  case "github.install":
135
230
  return "not installed yet";
136
231
  case "accounts":
137
- return "no coding account ready yet";
232
+ return "no AI account ready yet";
138
233
  case "linear.workspace":
139
234
  case "linear.personal":
140
235
  case "github.personal":
@@ -144,8 +239,13 @@ function unfinished(step) {
144
239
  case "capacity":
145
240
  return "no runner can take work yet";
146
241
  case "settings":
147
- case "values":
148
242
  return "optional; not reviewed yet";
243
+ case "values":
244
+ return step.reason === "required_values_missing"
245
+ ? "a value is missing"
246
+ : "not checked yet";
247
+ case "first-ticket":
248
+ return "no ticket chosen yet";
149
249
  default:
150
250
  return step.state === "skipped" ? "not chosen" : "not done yet";
151
251
  }
@@ -166,31 +266,192 @@ export function setupStepView(step, journal, required = false) {
166
266
  : "act";
167
267
  let outcome = step.state === "done" ? succeeded(step) : unfinished(step);
168
268
  if (mark === "later") {
169
- const numbers = [
170
- ...new Set((ONBOARD_DEPENDENCIES[step.id] ?? [])
171
- .filter((id) => {
269
+ const waitsOn = step.reason === "github_install_pending"
270
+ ? ["github.install"]
271
+ : (ONBOARD_DEPENDENCIES[step.id] ?? []).filter((id) => {
172
272
  const s = journal?.steps.find((s) => s.id === id);
173
273
  return s?.state !== "done" && s?.state !== "skipped";
174
- })
175
- .map((id) => SETUP_NUMBERS[id])
176
- .filter((n) => n !== undefined)),
177
- ];
178
- if (step.reason === "github_install_pending")
179
- numbers.splice(0, numbers.length, SETUP_NUMBERS["github.install"]);
274
+ });
275
+ // Numbers restart in each part: this part's steps come bare, then each other part once.
276
+ const byPart = new Map();
277
+ for (const id of waitsOn) {
278
+ const n = SETUP_PART_NUMBERS[id];
279
+ if (n === undefined)
280
+ continue;
281
+ const p = SETUP_PART_OF[id];
282
+ const list = byPart.get(p) ?? [];
283
+ if (!list.includes(n))
284
+ byPart.set(p, [...list, n]);
285
+ }
286
+ const own = SETUP_PART_OF[step.id];
287
+ const steps = (ns) => {
288
+ const sorted = [...ns].sort((x, y) => x - y);
289
+ const last = sorted.pop();
290
+ return sorted.length
291
+ ? `steps ${sorted.join(", ")} and ${last}`
292
+ : `step ${last}`;
293
+ };
294
+ const groups = [...byPart]
295
+ .sort(([x], [y]) => (x === own ? -1 : y === own ? 1 : x - y))
296
+ .map(([p, ns]) => (p === own ? steps(ns) : `${steps(ns)} of part ${p}`));
180
297
  outcome =
181
298
  step.state === "pending" && !step.reason
182
299
  ? "not checked yet"
183
- : numbers.length
184
- ? `after ${numbers.length === 1 ? "step" : "steps"} ${numbers.join(" and ")}`
300
+ : groups.length
301
+ ? `after ${groups.length > 1 ? `${groups.slice(0, -1).join(", ")}, and ${groups.at(-1)}` : groups[0]}`
185
302
  : "after an earlier step";
186
303
  }
187
304
  return {
188
- number: SETUP_NUMBERS[step.id] ?? 0,
305
+ number: SETUP_PART_NUMBERS[step.id] ?? 0,
189
306
  title: TITLES[step.id] ?? ONBOARD_TITLES[step.id],
190
307
  mark,
191
308
  outcome,
192
309
  };
193
310
  }
311
+ /** The signed-in person's Catalyst role, when the sign-in step recorded one. */
312
+ function signedInRole(journal) {
313
+ const role = journal?.steps.find((s) => s.id === "signin" && s.state === "done")?.evidence?.role;
314
+ return typeof role === "string" ? role : undefined;
315
+ }
316
+ const notOwnerOrAdmin = (journal) => {
317
+ const role = signedInRole(journal);
318
+ return role !== undefined && role !== "owner" && role !== "admin";
319
+ };
320
+ /** The steps readiness says an owner or admin has left (`admin_setup_pending`), if it said so. */
321
+ function readinessAdminSteps(journal) {
322
+ const ready = journal?.steps.find((s) => s.id === "ready");
323
+ const ids = ready?.evidence?.admin;
324
+ return ready?.reason === "admin_setup_pending" && typeof ids === "string"
325
+ ? new Set(ids.split(","))
326
+ : new Set();
327
+ }
328
+ /** A member's skip of their own team (or the values it gates): rerun with --team, nobody else. */
329
+ const memberOwnSkip = (step) => step.reason === "member_scope" &&
330
+ (step.id === "linear.team" || step.id === "values");
331
+ /**
332
+ * CTC-4680: a step only an owner or admin of the Catalyst workspace can finish, for the person
333
+ * running setup. A member's run skips every admin step whether or not the admin has done it, so a
334
+ * skip waits on an admin only when readiness names that step. Refusals for the person's role, a
335
+ * member's unverified runner or workflow, and a member's AI account or values do too.
336
+ */
337
+ export function setupWaitsOnAdmin(step, journal) {
338
+ if (step.state === "done")
339
+ return false;
340
+ const reason = step.reason ?? "";
341
+ if (reason === "member_scope")
342
+ return !memberOwnSkip(step) && readinessAdminSteps(journal).has(step.id);
343
+ if (reason === "github_installation_admin_required")
344
+ return true;
345
+ if (reason === "runner_identity_unverified" ||
346
+ reason === "workflow_identity_unverified")
347
+ return notOwnerOrAdmin(journal);
348
+ // Only reasons an owner or admin clears. A probe that could not finish (a stale read, a login
349
+ // to refresh) is the person's own to check again.
350
+ if ((step.id === "accounts" &&
351
+ (reason === "account_enrollment_required" ||
352
+ reason === "ai_account_not_usable")) ||
353
+ (step.id === "values" && reason === "required_values_missing"))
354
+ return notOwnerOrAdmin(journal);
355
+ return false;
356
+ }
357
+ /** How far each part has got, by its numbered rows. A row is done when every step in it is done
358
+ * or skipped by choice, needs someone when any of its steps does, and waits on an owner or admin
359
+ * when only they can finish it. */
360
+ export function setupPartProgress(journal, scope,
361
+ /** "run" before readiness is known: a member's admin skips count aside, neither done nor owed.
362
+ * At the end they count done only when readiness passed. */
363
+ moment = "end") {
364
+ const empty = () => ({ total: 0, done: 0, needs: 0, failed: 0, admin: 0, aside: 0 });
365
+ const progress = { 1: empty(), 2: empty(), 3: empty() };
366
+ const readinessPassed = journal.steps.find((s) => s.id === "ready")?.state === "done";
367
+ const rows = new Map();
368
+ for (const id of ONBOARD_STEPS) {
369
+ const number = SETUP_PART_NUMBERS[id];
370
+ if (number === undefined || (scope && !scope.includes(id)))
371
+ continue;
372
+ const step = journal.steps.find((s) => s.id === id) ?? {
373
+ id,
374
+ state: "pending",
375
+ };
376
+ const key = `${SETUP_PART_OF[id]}:${number}`;
377
+ rows.set(key, [
378
+ ...(rows.get(key) ?? []),
379
+ {
380
+ // A member's own skip (no --team) is theirs to finish, so it is never counted done.
381
+ mark: memberOwnSkip(step) ? "act" : setupStepView(step, journal).mark,
382
+ kind: setupWaitsOnAdmin(step, journal)
383
+ ? "admin"
384
+ : step.reason === "member_scope" &&
385
+ !memberOwnSkip(step) &&
386
+ (moment === "run" || !readinessPassed)
387
+ ? "aside"
388
+ : "own",
389
+ },
390
+ ]);
391
+ }
392
+ for (const [key, steps] of rows) {
393
+ const part = progress[Number(key.split(":")[0])];
394
+ part.total++;
395
+ const own = steps.filter((s) => s.kind === "own");
396
+ if (own.length === steps.length && own.every((s) => s.mark === "done" || s.mark === "skip"))
397
+ part.done++;
398
+ else if (own.some((s) => s.mark === "act" || s.mark === "fail")) {
399
+ part.needs++;
400
+ if (own.some((s) => s.mark === "fail"))
401
+ part.failed++;
402
+ }
403
+ else if (steps.some((s) => s.kind === "admin"))
404
+ part.admin++;
405
+ else if (steps.some((s) => s.kind === "aside"))
406
+ part.aside++;
407
+ }
408
+ return progress;
409
+ }
410
+ /**
411
+ * The tracker's rows: on the first screen (`plan`, the part about to start), at a boundary
412
+ * (`next`, the part about to start), or at the end. A part with no rows in this run is left out.
413
+ */
414
+ export function setupTrackerRows(progress, moment) {
415
+ const steps = (n) => `${n} ${n === 1 ? "step" : "steps"}`;
416
+ const waiting = (n) => `${steps(n)} ${n === 1 ? "waits" : "wait"} on an owner or admin`;
417
+ return [1, 2, 3]
418
+ .filter((p) => progress[p].total > 0)
419
+ .map((p) => {
420
+ const { title, estimate, summary } = SETUP_PARTS[p];
421
+ const got = progress[p];
422
+ const row = (mark, note) => ({
423
+ mark,
424
+ number: p,
425
+ title,
426
+ note,
427
+ });
428
+ if (typeof moment === "object" && "plan" in moment)
429
+ return row(p === moment.plan ? "now" : "later", `${estimate} · ${summary}`);
430
+ const finished = got.done === got.total;
431
+ const needsMark = got.failed ? "fail" : "act";
432
+ if (typeof moment === "object") {
433
+ if (p === moment.next)
434
+ return row("now", `next · ${estimate}`);
435
+ if (p > moment.next)
436
+ return row("later", estimate);
437
+ if (finished)
438
+ return row("done", "done");
439
+ const theirs = got.admin + got.aside;
440
+ if (theirs && got.done + theirs === got.total)
441
+ return row("act", `${steps(theirs)} ${theirs === 1 ? "is" : "are"} for an owner or admin`);
442
+ return row(needsMark, `${steps(got.total - got.done)} not finished`);
443
+ }
444
+ if (finished)
445
+ return row("done", "");
446
+ if (got.needs)
447
+ return row(needsMark, `${steps(got.needs)} ${got.needs === 1 ? "needs" : "need"} someone`);
448
+ if (got.admin)
449
+ return row("act", waiting(got.admin));
450
+ if (got.aside)
451
+ return row("act", `${steps(got.aside)} ${got.aside === 1 ? "is" : "are"} for an owner or admin`);
452
+ return row("later", `${got.done} of ${steps(got.total)} done`);
453
+ });
454
+ }
194
455
  function page(base, path) {
195
456
  try {
196
457
  const url = new URL(path, base);
@@ -206,7 +467,8 @@ function page(base, path) {
206
467
  }
207
468
  function action(step, base, journal) {
208
469
  if (CONNECTION_REPAIR_REASONS.has(step.reason ?? "") ||
209
- step.reason === "github_installation_approval_pending")
470
+ step.reason === "github_installation_approval_pending" ||
471
+ step.reason === "member_scope")
210
472
  return onboardReasonText(step, { baseUrl: base, journal });
211
473
  const link = (path, verb) => {
212
474
  const url = page(base, path);
@@ -229,7 +491,9 @@ function action(step, base, journal) {
229
491
  case "github.personal":
230
492
  return link("/settings/connected-accounts?connect=github", "connect your GitHub account");
231
493
  case "accounts":
232
- return link("/settings/coding-accounts", "add a coding account");
494
+ return step.reason === "ai_account_not_usable"
495
+ ? onboardReasonText(step, { baseUrl: base, journal })
496
+ : link("/settings/coding-accounts", "add an AI account");
233
497
  case "linear.adopt": {
234
498
  const team = journal?.steps.find((row) => row.id === "linear.team" && row.state === "done");
235
499
  const key = team && value(team, "teamKey");
@@ -245,24 +509,30 @@ function action(step, base, journal) {
245
509
  ? onboardReasonText(step, { baseUrl: base, journal })
246
510
  : "Run catalyst onboard --runner to check whether this computer can take work.";
247
511
  case "settings":
248
- case "values":
249
512
  return "Open your repository checkout and review its settings.";
513
+ case "values":
514
+ return onboardReasonText({ ...step, reason: step.reason ?? "required_values_unverified" }, { baseUrl: base, journal });
250
515
  case "linear.team":
251
516
  return "Choose a Linear team when setup checks again.";
252
517
  case "github.repos":
253
518
  case "projects":
254
519
  return "Choose repositories when setup checks again.";
255
520
  case "first-ticket":
256
- return "Choose a first ticket when setup checks again.";
521
+ return onboardReasonText({ ...step, reason: step.reason ?? "first_ticket_choice_required" }, { baseUrl: base, journal });
257
522
  default:
258
523
  return "Try this step again when setup checks again.";
259
524
  }
260
525
  }
261
526
  export function setupFinalScreen(journal, base, paused = false, required = new Set()) {
527
+ // A member's run skips admin steps; readiness names the ones an admin has not finished yet.
528
+ const readyStep = journal.steps.find((step) => step.id === "ready");
529
+ const adminLeft = new Set(readyStep?.reason === "admin_setup_pending" && typeof readyStep.evidence?.admin === "string"
530
+ ? readyStep.evidence.admin.split(",")
531
+ : []);
262
532
  const roots = journal.steps
263
533
  .filter((step) => {
264
534
  const m = setupStepView(step, journal, required.has(step.id)).mark;
265
- return ((m === "act" || m === "fail") &&
535
+ return ((m === "act" || m === "fail" || adminLeft.has(step.id)) &&
266
536
  (ONBOARD_DEPENDENCIES[step.id] ?? []).every((id) => {
267
537
  const parent = journal.steps.find((s) => s.id === id);
268
538
  return parent?.state === "done" || parent?.state === "skipped";
@@ -270,7 +540,7 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
270
540
  })
271
541
  .filter((step) => step.id !== "ready")
272
542
  .sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id));
273
- const folded = (id) => id === "projects" ? "github.repos" : id === "values" ? "settings" : id;
543
+ const folded = (id) => id === "projects" ? "github.repos" : id;
274
544
  const unique = roots.filter((step, i) => roots.findIndex((s) => folded(s.id) === folded(step.id)) === i);
275
545
  const actions = unique
276
546
  .slice(0, 5)
@@ -297,16 +567,17 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
297
567
  : journal.exit === 0
298
568
  ? "Ready for work"
299
569
  : "Not ready for work yet";
300
- const ticket = journal.steps.find((s) => s.id === "first-ticket" && s.state === "done");
301
- const key = ticket && value(ticket, "ticketKey");
570
+ const firstTicket = journal.steps.find((s) => s.id === "first-ticket");
571
+ const ticket = firstTicket?.state === "done" ? firstTicket : undefined;
572
+ const key = ticket && (value(ticket, "ticket") ?? value(ticket, "ticketKey"));
302
573
  const savedTicketUrl = ticket && (value(ticket, "url") ?? value(ticket, "ticketUrl"));
303
574
  const ticketUrl = savedTicketUrl && page(base, savedTicketUrl);
304
575
  const next = paused
305
576
  ? "Next: run catalyst onboard to continue."
306
- : key && ticketUrl
307
- ? `Next: open ${ticketUrl} and follow ${key}.`
577
+ : key
578
+ ? `Follow ${ticketUrl ?? key} in Linear; Catalyst comments there as each phase finishes.`
308
579
  : heading === "Setup complete"
309
- ? "Next: move a ticket to Todo in Linear."
580
+ ? `Next: move a ticket to ${dispatchStageName(firstTicket?.evidence?.stage)} in Linear.`
310
581
  : journal.exit === 0 && actions[0]
311
582
  ? `Next: ${actions[0].text[0].toLowerCase() + actions[0].text.slice(1)}`
312
583
  : actions.length === 1
@@ -317,7 +588,9 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
317
588
  ? "Next: run catalyst onboard after you finish 1 to " +
318
589
  actions.length +
319
590
  "."
320
- : "Next: run catalyst onboard to check again.";
591
+ : readyStep?.reason === "member_team_required"
592
+ ? "Next: run catalyst onboard --team <KEY> to check again."
593
+ : "Next: run catalyst onboard to check again.";
321
594
  return {
322
595
  heading,
323
596
  readinessCause,
@@ -351,8 +624,8 @@ export function setupBrowserInstruction(id, base, opened = false) {
351
624
  ],
352
625
  accounts: [
353
626
  "/settings/coding-accounts",
354
- "add the token with your Claude email",
355
- "Add a coding account",
627
+ "add an API key",
628
+ "Add an API key",
356
629
  ],
357
630
  };
358
631
  const words = copy[id];
@@ -363,9 +636,6 @@ export function setupBrowserInstruction(id, base, opened = false) {
363
636
  ? `Opened your browser. ${words[2]} there.`
364
637
  : `Open this link and ${words[1]}:`,
365
638
  url: page(base, words[0]),
366
- preparation: id === "accounts"
367
- ? "In another terminal, run claude setup-token."
368
- : undefined,
369
639
  };
370
640
  }
371
641
  /** JSON facts use the same reachable actions as the terminal, while retaining the existing journal. */
@@ -412,7 +682,9 @@ export function onboardJsonView(journal, base, paused = false, options = {}) {
412
682
  // CTC-4680: refused by the person's Catalyst role, not by GitHub, so a workspace admin acts.
413
683
  step.reason === "github_installation_admin_required" ||
414
684
  step.reason?.endsWith("_identity_unverified") ||
415
- (action.id === "accounts" && role !== "owner" && role !== "admin");
685
+ ((action.id === "accounts" || action.id === "values") &&
686
+ role !== "owner" &&
687
+ role !== "admin");
416
688
  const url = action.text
417
689
  .match(/https?:\/\/[^\s<>]+/)?.[0]
418
690
  ?.replace(/[.,;:!?]+$/, "");