@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,20 +1,28 @@
1
1
  import { nativeEgressInstallCommands } from "./onboard-runner-egress.js";
2
- import { loadConfig, normalizeBaseUrl } from "./config.js";
2
+ import { INSTALL_COMMAND, loadConfig, normalizeBaseUrl } from "./config.js";
3
3
  import { githubInstallationPage } from "./onboard-permissions.js";
4
+ import { valuesFacts, valuesFix } from "./ready-copy.js";
4
5
  /** What a person reads for a step that is not done. Each line ends in an action they can take. */
5
6
  const REASONS = {
6
- account_identity_unverified: "Your login changed while coding accounts were checked. Run catalyst onboard again to verify your person and workspace.",
7
- account_inventory_unavailable: "Coding accounts could not be read. Run catalyst onboard again to retry.",
8
- account_inventory_unverified: "The server did not return a fresh supported coding-account list. Run catalyst onboard after the server update.",
9
- coding_account_not_found: "The coding account named by --coding-account is not enrolled in this workspace. Run catalyst accounts to list the slots, then run catalyst onboard with one of them.",
10
- account_validation_admin_required: "An administrator must check the workspace's coding-account access.",
7
+ account_identity_unverified: "Your login changed while AI accounts were checked. Run catalyst onboard again to verify your person and workspace.",
8
+ account_inventory_unavailable: "AI accounts could not be read. Run catalyst onboard again to retry.",
9
+ account_inventory_unverified: "The server did not return a fresh supported AI account list. Run catalyst onboard after the server update.",
10
+ connect_checklist_refused: "Catalyst refused to show this login the Connect accounts page's status. Sign in again with catalyst login, then run catalyst onboard.",
11
+ connect_checklist_identity_unverified: "Your login changed while setup was checking the Connect accounts page. Run catalyst onboard again to verify your person and workspace.",
12
+ coding_account_inactive: "The AI account named by --coding-account is turned off. Turn it on in the workspace's AI account settings, then run catalyst onboard.",
13
+ coding_account_ended: "The AI account named by --coding-account was removed or its access has ended. Name another one with --coding-account, or add one in the workspace's AI account settings.",
14
+ coding_account_needs_login: "The AI account named by --coding-account needs its key added again. Replace it in the workspace's AI account settings, then run catalyst onboard.",
15
+ coding_account_quarantined: "The AI account named by --coding-account is held after repeated failures. Check it in the workspace's AI account settings, then run catalyst onboard.",
16
+ coding_account_walled: "The AI account named by --coding-account has used its usage limit for now. Check when it resets, then run catalyst onboard again.",
17
+ coding_account_not_found: "The AI account named by --coding-account is not in this workspace. Run catalyst accounts to list the slots, then run catalyst onboard with one of them.",
18
+ account_validation_admin_required: "An administrator must check the workspace's AI account access.",
11
19
  account_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
12
20
  account_validation_unavailable: "The provider check could not finish. Run catalyst onboard again to retry.",
13
21
  account_validation_unverified: "The server did not return a fresh provider check. Run catalyst onboard after the server update.",
14
- 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.",
22
+ account_provider_access_unverified: "AI account access is not freshly verified. Check the account in the workspace's AI account settings, then resume setup.",
15
23
  codex_provider_access_unverified: "Codex credentials are stored, but provider access is not freshly verified. Setup did not refresh or rotate the Codex login.",
16
24
  account_provider_walled: "Claude's usage limit is spent for now. Check the account's reset time, then run catalyst onboard again.",
17
- account_provider_rejected: "Claude refused the stored login. Replace it in the workspace's coding-account settings, then resume setup.",
25
+ account_provider_rejected: "Claude refused the stored login. Replace it in the workspace's AI account settings, then resume setup.",
18
26
  personal_consent_handoff: "The browser approval link could not be verified. Run catalyst onboard to try again.",
19
27
  personal_identity_refused: "Your current login no longer matches this setup. Resume with the original workspace and person.",
20
28
  personal_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
@@ -44,6 +52,7 @@ const REASONS = {
44
52
  local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
45
53
  local_sync_capability_unavailable: "Local sync was selected but could not be verified.",
46
54
  member_scope: "Your workspace administrator handles this step.",
55
+ member_team_required: "Setup needs your Linear team to check what Catalyst can work on. Run catalyst setup again with --team <KEY>, using your team's key from Linear.",
47
56
  onboarding_checks_pending: "Some required checks are still unverified.",
48
57
  interrupted: "Setup paused. Run the same command to resume.",
49
58
  signin_timeout: "Sign-in timed out. Run catalyst onboard to try again.",
@@ -138,7 +147,15 @@ const REASONS = {
138
147
  linear_workspace_permissions_unverified: "The workspace's Linear connection works, but its granted scopes could not be checked. Run catalyst onboard to try again.",
139
148
  linear_personal_scope_outdated: "Your personal Linear connection is missing scopes this version needs. Re-authorize it from Connected accounts, then run catalyst onboard.",
140
149
  personal_permissions_unverified: "Your personal connection's granted scopes could not be checked. Run catalyst onboard to try again.",
150
+ required_values_unverified: "Catalyst has not read the repository's settings yet. Run catalyst setup again in a few minutes.",
151
+ first_ticket_team_unverified: "Setup could not confirm the selected Linear team. Run catalyst setup again.",
152
+ first_ticket_dispatch_unmapped: "Catalyst does not know which of the team's stages starts its work yet. Set up the team's Catalyst workflow, then run catalyst setup again.",
141
153
  housekeeping_service_unverified: "The daily update needs a user service manager, launchd on macOS or systemd --user on Linux. Setup cannot schedule it on this computer. It is optional, and work does not depend on it.",
154
+ housekeeping_off_chosen: "The daily update is off, as you chose when you installed. It is optional. catalyst says when an update is out.",
155
+ housekeeping_no_scheduler: "The daily update can't run on this computer: it needs launchd on macOS or systemd --user on Linux. It is optional. catalyst says when an update is out.",
156
+ housekeeping_not_loaded: "The daily update is installed but not turned on. Run launchctl load ~/Library/LaunchAgents/dev.catalystcloud.housekeeping.plist to turn it on.",
157
+ housekeeping_not_enabled: "The daily update is installed but not turned on. Run systemctl --user enable --now catalyst-housekeeping.timer to turn it on.",
158
+ housekeeping_not_scheduled: `The daily update is not scheduled on this computer. Run the installer again to schedule it: ${INSTALL_COMMAND} -s -- --housekeeping yes`,
142
159
  };
143
160
  /** The saved login's web address, or nothing when the config is missing, unreadable or not http(s). */
144
161
  export function savedOnboardBaseUrl(home) {
@@ -261,6 +278,26 @@ export function onboardStepDetail(step) {
261
278
  : undefined;
262
279
  return granted ? `granted ${granted}` : undefined;
263
280
  }
281
+ /** The dispatch stage's name as the contract gave it, or what it does when unknown. */
282
+ export function dispatchStageName(value) {
283
+ return typeof value === "string" && /^[\p{L}\p{N}][\p{L}\p{N} ._&'()-]{0,59}$/u.test(value)
284
+ ? value
285
+ : "the stage that starts Catalyst's work";
286
+ }
287
+ function firstTicketText(reason, key, stage) {
288
+ const move = `move one of ${key ? `${key}'s` : "your team's"} tickets to ${dispatchStageName(stage)} in Linear`;
289
+ switch (reason) {
290
+ case "first_ticket_choice_required":
291
+ return `${move.charAt(0).toUpperCase()}${move.slice(1)}. Catalyst picks it up next.`;
292
+ case "first_ticket_move_refused":
293
+ return `Catalyst could not move the ticket for you. Ask a Catalyst owner or admin to check its Linear connection, or ${move} yourself.`;
294
+ case "first_ticket_unavailable":
295
+ return `Setup could not read or move the team's tickets. Run catalyst setup to try again, or ${move} yourself.`;
296
+ case "first_ticket_unverified":
297
+ return `Setup could not confirm which ticket moved. Check ${dispatchStageName(stage)} in Linear before choosing again.`;
298
+ }
299
+ return undefined;
300
+ }
264
301
  /** One step's next action. `waitsFor` names the unfinished step a prerequisite wait is blocked on. */
265
302
  export function onboardReasonText(step, context = {}) {
266
303
  const reason = step.reason ?? "";
@@ -284,6 +321,36 @@ export function onboardReasonText(step, context = {}) {
284
321
  if (step.id === "github.install" &&
285
322
  (reason === "github_installation_admin_required" || reason === "member_scope"))
286
323
  return `Only an owner or admin of your Catalyst workspace can install Catalyst on GitHub. Ask one to open ${context.baseUrl ? `${normalizeBaseUrl(context.baseUrl)}/settings/connections` : "Catalyst settings, Integrations,"} and install it there.`;
324
+ if (reason === "admin_setup_pending") {
325
+ const words = {
326
+ "linear.workspace": "connect Linear",
327
+ "github.install": "install Catalyst on GitHub",
328
+ "linear.adopt": "set up the team's Catalyst workflow",
329
+ };
330
+ const left = String(step.evidence?.admin ?? "")
331
+ .split(",")
332
+ .flatMap((id) => (words[id] ? [words[id]] : []));
333
+ const list = left.length > 1 ? `${left.slice(0, -1).join(", ")} and ${left.at(-1)}` : (left[0] ?? "finish setup");
334
+ return `A Catalyst owner or admin still has to ${list}. Ask one to open ${context.baseUrl ? `${normalizeBaseUrl(context.baseUrl)}/settings/connections` : "Catalyst settings"}, then run catalyst setup.`;
335
+ }
336
+ // A member is told which admin action is left and where an admin takes it.
337
+ if (reason === "member_scope") {
338
+ const key = onboardTeamKey(context.journal);
339
+ const adminOnly = {
340
+ "linear.workspace": ["connect Linear", "/settings/connections", " and connect it there"],
341
+ "linear.adopt": [
342
+ "set up the team's Catalyst workflow",
343
+ key ? `/settings/linear-teams/${encodeURIComponent(key)}/adopt` : "/settings/linear-teams",
344
+ "",
345
+ ],
346
+ };
347
+ const admin = adminOnly[step.id];
348
+ if (admin) {
349
+ const [what, path, rest] = admin;
350
+ const url = context.baseUrl ? `${normalizeBaseUrl(context.baseUrl)}${path}` : "Catalyst settings";
351
+ return `Only a Catalyst owner or admin can ${what}. Ask one to open ${url}${rest}.`;
352
+ }
353
+ }
287
354
  // An install request can sit on GitHub for good (a wrong organization, or an owner who said no),
288
355
  // so the step always keeps a way to install it directly.
289
356
  if (reason === "github_installation_approval_pending" && context.baseUrl)
@@ -302,11 +369,22 @@ export function onboardReasonText(step, context = {}) {
302
369
  if (team)
303
370
  return `Setup could not confirm whether Linear created the team ${team}. Check Linear for it. If it is there: ${adopt(team)} If it is not, run catalyst onboard to create it again.`;
304
371
  }
372
+ if (reason === "ai_account_not_usable") {
373
+ const where = context.baseUrl
374
+ ? `at ${normalizeBaseUrl(context.baseUrl)}/settings/coding-accounts`
375
+ : "in the web app on the Settings → AI accounts page";
376
+ return `No AI account in this workspace can take work yet. A workspace owner or administrator checks them ${where}: one may need its key replaced, or may have reached its usage limit. Then run catalyst onboard.`;
377
+ }
305
378
  if (reason === "account_enrollment_required") {
306
379
  const where = context.baseUrl
307
380
  ? `at ${normalizeBaseUrl(context.baseUrl)}/settings/coding-accounts, the Settings → AI accounts page`
308
381
  : "in the web app on the Settings → AI accounts page";
309
- return `No coding account is enrolled. A workspace owner or administrator adds one ${where}. A Claude account needs its email and the token printed by \`claude setup-token\`. Then run catalyst onboard.`;
382
+ // CTC-4716: subscriptions are offered only to a workspace the cloud enables for them.
383
+ const kinds = step.evidence?.aiAccountKinds;
384
+ const what = typeof kinds === "string" && kinds.split(",").includes("subscription")
385
+ ? "an API key, billed per token, or a subscription, a fixed monthly plan"
386
+ : "an API key, billed per token";
387
+ return `No AI account is added yet. A workspace owner or administrator adds one ${where}: ${what}. Then run catalyst onboard.`;
310
388
  }
311
389
  if (reason === "automation_management_unavailable") {
312
390
  const conflicts = typeof step.evidence?.automations === "string" &&
@@ -357,6 +435,27 @@ export function onboardReasonText(step, context = {}) {
357
435
  }
358
436
  if (reason === "runner_admission_operator")
359
437
  return `The admission policy for team ${onboardTeamKey(context.journal) ?? "<TEAM KEY>"} needs a Catalyst operator to check it. Setup changes only restricted admission and preserves existing policy fields. Ask Catalyst support to confirm a compatible policy, then run the same setup command.`;
438
+ if (reason === "required_values_missing") {
439
+ let facts;
440
+ try {
441
+ facts = JSON.parse(String(step.evidence?.requiredValues ?? "null"));
442
+ }
443
+ catch {
444
+ facts = null;
445
+ }
446
+ const fix = valuesFix(context.baseUrl, valuesFacts(facts)) ??
447
+ valuesFix(context.baseUrl, { names: ["the missing values"] });
448
+ // A member cannot set a value; the line says who does, and still never prints one.
449
+ const member = context.journal?.steps.find((row) => row.id === "signin")?.evidence?.role === "member";
450
+ if (member)
451
+ return fix.startsWith("set ")
452
+ ? `Ask a Catalyst owner or admin to ${fix}. Then run catalyst setup.`
453
+ : `${fix.charAt(0).toUpperCase()}${fix.slice(1)}. Ask a Catalyst owner or admin to fix it, then run catalyst setup.`;
454
+ return `${fix.charAt(0).toUpperCase()}${fix.slice(1)}. Then run catalyst setup.`;
455
+ }
456
+ const firstTicket = firstTicketText(reason, onboardTeamKey(context.journal), step.evidence?.stage);
457
+ if (step.id === "first-ticket" && firstTicket)
458
+ return firstTicket;
360
459
  if (reason === "prerequisite_not_ready" && context.waitsFor)
361
460
  return `Runs after "${context.waitsFor}".`;
362
461
  const permission = permissionText(step, context.baseUrl);
@@ -1,5 +1,6 @@
1
1
  import { loadConfig } from "./config.js";
2
2
  import { loadContract } from "./contract.js";
3
+ import { ONBOARD_DEFERRED_STEPS, onboardRequiredStepsWaiting, } from "./onboard.js";
3
4
  const evidenceNames = new Set(["account", "membershipId", "project", "team", "repository", "count", "checkedAt", "expiresAt", "revision", "lag", "cursor", "heartbeatAgeMs", "supervised", "available", "validated", "phaseStarted", "linearComment", "fleetActivity"]);
4
5
  const slug = (value) => value && /^[a-z][a-z0-9_]{1,63}$/.test(value) ? value : undefined;
5
6
  function cleanCheck(check) {
@@ -32,6 +33,66 @@ export async function onboardingReadyReport(ctx, deps) {
32
33
  : required.length === 0 || required.some(check => check.state === "unknown") || work.state === "unknown" ? "unknown" : "complete";
33
34
  return { schema: 1, readMode: deps.localSync ? "local" : "cloud", state, checks, work };
34
35
  }
36
+ /** Team check outcomes that do not hold setup's readiness, as `check:reason`: the cloud's four
37
+ * WAITING_PAIRS (nothing has happened yet before a first ticket: a webhook delivery, a write, a host
38
+ * attaching, a reviewer's first answer), plus two of setup's own. A missing value is the values
39
+ * step's action; a reference with no value is not here, because the checkout refuses it before any
40
+ * work starts. A team with no reviewer still runs. */
41
+ const READY_EXEMPT = new Set([
42
+ "webhook_covers_team:delivery_window_empty",
43
+ "writes_land:no_write_observed",
44
+ "hosts_current:no_host_connected",
45
+ "reviewer_answering:reviewer_not_yet_answered",
46
+ "required_values:required_value_missing",
47
+ "reviewer_configured:no_reviewer_configured",
48
+ ]);
49
+ /** Admin-owned steps a member's run skips, and the team checks that show an admin finished them. A
50
+ * member's skip is not a finished setup: readiness holds until these checks pass. */
51
+ const ADMIN_OWNED = [
52
+ ["linear.workspace", ["oauth_scope", "token_live"]],
53
+ ["github.install", ["github_app_installed"]],
54
+ ["linear.adopt", ["mapping_total", "mapped_states_exist"]],
55
+ ];
56
+ /** The setup step's verdict. Every required step must be satisfied, and every required check must
57
+ * pass or be one of the exempt outcomes above. A failing check fails; anything else unread (a stale
58
+ * or unverified team, a check the cloud could not read, the cloud itself) waits. Work nobody can
59
+ * observe yet does not hold it. */
60
+ export function onboardReadyStep(report, journal) {
61
+ const evidence = {
62
+ checks: report.checks.length,
63
+ passed: report.checks.filter((check) => check.state === "pass").length,
64
+ };
65
+ const failedStep = (journal?.steps ?? []).some((step) => step.id !== "ready" &&
66
+ !ONBOARD_DEFERRED_STEPS.has(step.id) &&
67
+ step.state === "failed");
68
+ if (onboardRequiredStepsWaiting(journal) || failedStep)
69
+ return { state: "waiting", reason: "onboarding_checks_pending", evidence };
70
+ const skipped = (id) => journal?.steps.some((step) => step.id === id && step.state === "skipped" && step.reason === "member_scope");
71
+ // A member's plain run has no team, so nothing can show what an admin finished.
72
+ if (skipped("linear.team"))
73
+ return { state: "waiting", reason: "member_team_required", evidence };
74
+ // Only a check read from a fresh team (`team.<id>.<check>`) that fails counts. An absent, stale or
75
+ // unknown one (an outage, a workflow just adopted) says nothing about the admin step, so it falls
76
+ // through to the waits below.
77
+ const read = new Map(report.checks
78
+ .filter((check) => /^team\.[^.]+\.[a-z0-9_]+$/.test(check.id))
79
+ .map((check) => [check.id.split(".")[2], check.state]));
80
+ const adminLeft = ADMIN_OWNED.filter(([id, checks]) => skipped(id) && checks.some((check) => read.get(check) === "fail")).map(([id]) => id);
81
+ if (adminLeft.length)
82
+ return { state: "waiting", reason: "admin_setup_pending", evidence: { ...evidence, admin: adminLeft.join(",") } };
83
+ const open = report.checks.filter((check) => {
84
+ if (!check.required || check.state === "pass")
85
+ return false;
86
+ // `team.<id>.<check>` is one team check; `team.<id>` alone is the team's whole check list.
87
+ const parts = check.id.split(".");
88
+ return !(parts[0] === "team" && parts.length === 3 && READY_EXEMPT.has(`${parts[2]}:${check.reason}`));
89
+ });
90
+ if (open.some((check) => check.state === "fail"))
91
+ return { state: "failed", reason: "onboarding_checks_pending", evidence };
92
+ if (open.length)
93
+ return { state: "waiting", reason: "onboarding_checks_pending", evidence };
94
+ return { state: "done", evidence };
95
+ }
35
96
  /** The current contract exposes checked project setup, but not fresh starter-ticket proof. */
36
97
  export async function observeCloudOnboarding(ctx, scope = {}) {
37
98
  const cfg = loadConfig(ctx.home);
@@ -15,7 +15,11 @@ import { onboardRunnerAdapter } from "./onboard-runner.js";
15
15
  import { onboardAutomationManagementAdapter } from "./onboard-automation-management.js";
16
16
  import { onboardWorkflowVerificationAdapter } from "./onboard-workflow.js";
17
17
  import { onboardAccountsAdapter } from "./onboard-accounts.js";
18
+ import { onboardFirstTicketAdapter } from "./onboard-first-ticket.js";
19
+ import { onboardValuesAdapter } from "./onboard-values.js";
18
20
  import { githubInstallationAdapter } from "./onboard-github.js";
21
+ import { connectChecklistAdapter, } from "./onboard-checklist.js";
22
+ import { onboardHousekeepingAdapter, } from "./onboard-housekeeping.js";
19
23
  import { guardOnboardCapabilities, } from "./onboard-capabilities.js";
20
24
  import { onboardSettingsAdapter, } from "./onboard-settings.js";
21
25
  import { TIMED_OUT_REASONS } from "./setup-onboard-copy.js";
@@ -147,9 +151,21 @@ export function createOnboardRuntime(args, ctx, hooks) {
147
151
  // CTC-4680 round 5: whether the step now acting ran a browser wait, so a timed-out wait can be
148
152
  // re-offered with a fresh link instead of moving on to steps that need it.
149
153
  let waited = false;
150
- const waitFor = (message, run) => {
154
+ const waitFor = (message, run, page) => {
151
155
  waited = true;
152
- return hooks.ui.wait(message, run);
156
+ return hooks.ui.wait(message, run, page);
157
+ };
158
+ // CTC-4680: the Connect accounts page opens once per run, whichever step needs it first.
159
+ const connectPage = { openedBy: null };
160
+ const checklistOptions = {
161
+ openBrowser: hooks.openBrowser ?? openConsentBrowser,
162
+ wait: interactiveWait
163
+ ? (message, work, page) => waitFor(message, work, page)
164
+ : undefined,
165
+ note: (text) => hooks.ui?.note ? hooks.ui.note(text) : hooks.ui ? hooks.ui.message(text) : ctx.stderr(text),
166
+ sleep: hooks.sleep,
167
+ page: connectPage,
168
+ cache: { last: null },
153
169
  };
154
170
  const personalAdapter = (provider) => personalConsentAdapter({
155
171
  provider,
@@ -237,12 +253,17 @@ export function createOnboardRuntime(args, ctx, hooks) {
237
253
  : undefined,
238
254
  sleep: hooks.sleep,
239
255
  }),
240
- "github.install": githubInstallationAdapter({
241
- openBrowser: hooks.openBrowser ?? openConsentBrowser,
242
- wait: interactiveWait
243
- ? (message, work) => waitFor(message, work)
244
- : undefined,
245
- sleep: hooks.sleep,
256
+ "github.install": connectChecklistAdapter({
257
+ ...checklistOptions,
258
+ step: "github.install",
259
+ item: "github-app",
260
+ fallback: githubInstallationAdapter({
261
+ openBrowser: hooks.openBrowser ?? openConsentBrowser,
262
+ wait: interactiveWait
263
+ ? (message, work) => waitFor(message, work)
264
+ : undefined,
265
+ sleep: hooks.sleep,
266
+ }),
246
267
  }),
247
268
  settings: onboardSettingsAdapter({
248
269
  ...hooks.settings,
@@ -262,21 +283,23 @@ export function createOnboardRuntime(args, ctx, hooks) {
262
283
  : undefined,
263
284
  message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
264
285
  }),
265
- accounts: onboardAccountsAdapter({
266
- ...(hooks.ui &&
267
- hooks.ui.interactive !== false &&
268
- !args.json &&
269
- args.flags.yes !== true &&
270
- args.flags.headless !== true
271
- ? {
272
- waitForAccount: (work) => waitFor("Waiting for a coding account", work),
273
- sleep: hooks.sleep,
274
- }
275
- : {}),
276
- message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
277
- ...(typeof args.flags["coding-account"] === "string"
278
- ? { slot: args.flags["coding-account"] }
279
- : {}),
286
+ accounts: connectChecklistAdapter({
287
+ ...checklistOptions,
288
+ step: "accounts",
289
+ item: "ai-account",
290
+ // A named account is checked on its own: the checklist answers for the whole workspace.
291
+ enabled: typeof args.flags["coding-account"] !== "string",
292
+ fallback: onboardAccountsAdapter({
293
+ ...(interactiveWait
294
+ ? {
295
+ waitForAccount: (work) => waitFor("Waiting for an AI account", work),
296
+ sleep: hooks.sleep,
297
+ }
298
+ : {}),
299
+ ...(typeof args.flags["coding-account"] === "string"
300
+ ? { slot: args.flags["coding-account"] }
301
+ : {}),
302
+ }),
280
303
  }),
281
304
  capacity: onboardCapacityAdapter({
282
305
  message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
@@ -315,9 +338,14 @@ export function createOnboardRuntime(args, ctx, hooks) {
315
338
  evidence: { provider: "cloud" },
316
339
  },
317
340
  },
318
- housekeeping: {
319
- check: async () => waiting("housekeeping_service_unverified"),
320
- },
341
+ housekeeping: onboardHousekeepingAdapter(hooks.scheduler),
342
+ values: onboardValuesAdapter(),
343
+ // Only a person at a terminal picks a ticket; --yes, JSON and headless runs leave it waiting.
344
+ "first-ticket": onboardFirstTicketAdapter(interactiveWait && hooks.ui?.chooseFirstTicket
345
+ ? {
346
+ choose: (tickets, where) => hooks.ui.chooseFirstTicket(tickets.map((ticket) => ({ ...ticket })), { ...where }),
347
+ }
348
+ : {}),
321
349
  ready: { check: hooks.ready },
322
350
  };
323
351
  const get = (path) => ({ method: "GET", path });
@@ -377,6 +405,10 @@ export function createOnboardRuntime(args, ctx, hooks) {
377
405
  check: [get("/api/v1/repos"), get("/api/v1/agent/contract")],
378
406
  fallback: "connections",
379
407
  },
408
+ values: {
409
+ check: [get("/api/v1/agent/contract")],
410
+ fallback: "connections",
411
+ },
380
412
  ready: {
381
413
  check: [get("/api/v1/agent/contract")],
382
414
  fallback: "connections",
@@ -409,7 +441,8 @@ export function createOnboardRuntime(args, ctx, hooks) {
409
441
  const timedOut = result.state === "waiting" &&
410
442
  (TIMED_OUT_REASONS.has(result.reason ?? "") ||
411
443
  (step === "accounts" &&
412
- result.reason === "account_enrollment_required"));
444
+ (result.reason === "account_enrollment_required" ||
445
+ result.reason === "ai_account_not_usable")));
413
446
  if (!timedOut || !waited || signal?.aborted || hooks.ui?.signal.aborted)
414
447
  return result;
415
448
  if (!(await retry(step)))
@@ -1,60 +1,36 @@
1
- import { setupStepView, SETUP_NUMBERS } from "./setup-onboard-copy.js";
1
+ import { setupPartHeading, setupStepView, SETUP_NUMBERS, SETUP_PART_NUMBERS, SETUP_PART_OF, } from "./setup-onboard-copy.js";
2
2
  export const COMPUTER_CHECKS = ["machine", "cli", "skills", "legacy"];
3
3
  const PLAN = [
4
4
  ["On this computer", "signin", "you approve once in your browser"],
5
- [
6
- "On this computer",
7
- "housekeeping",
8
- "schedule the daily update, where supported",
9
- ],
10
- [
11
- "In Linear and GitHub",
12
- "linear.workspace",
13
- "an admin connects the workspace",
14
- ],
15
- [
16
- "In Linear and GitHub",
17
- "linear.personal",
18
- "you connect your own Linear account",
19
- ],
5
+ ["On this computer", "housekeeping", "schedule the daily update"],
6
+ ["In Linear and GitHub", "linear.workspace", "an admin connects the workspace"],
7
+ ["In Linear and GitHub", "linear.personal", "you connect your own Linear account"],
20
8
  ["In Linear and GitHub", "linear.team", "you choose the team"],
21
- [
22
- "In Linear and GitHub",
23
- "linear.adopt",
24
- "review any states and labels to add",
25
- ],
26
- [
27
- "In Linear and GitHub",
28
- "linear.automations",
29
- "check how pull requests change issue states",
30
- ],
31
- [
32
- "In Linear and GitHub",
33
- "github.install",
34
- "an admin chooses access on GitHub",
35
- ],
36
- [
37
- "In Linear and GitHub",
38
- "github.personal",
39
- "you connect your own GitHub account",
40
- ],
41
- [
42
- "In Linear and GitHub",
43
- "github.repos",
44
- "you choose where Catalyst can work",
45
- ],
46
- ["Work", "accounts", "check a coding account"],
9
+ ["In Linear and GitHub", "linear.adopt", "review any states and labels to add"],
10
+ ["In Linear and GitHub", "linear.automations", "check how pull requests change issue states"],
11
+ ["In Linear and GitHub", "github.install", "an admin chooses access on GitHub"],
12
+ ["In Linear and GitHub", "github.personal", "you connect your own GitHub account"],
13
+ ["In Linear and GitHub", "github.repos", "you choose where Catalyst can work"],
14
+ ["Work", "accounts", "check an AI account"],
47
15
  ["Work", "capacity", "check which runners can take work"],
48
16
  ["Work", "settings", "you review settings before saving them"],
17
+ ["Work", "values", "check every value the repositories need has one"],
49
18
  ["Work", "first-ticket", "you choose a ticket to start"],
50
19
  ];
51
- export function standalonePlan(journal, scope, runnerSelected = false) {
20
+ /**
21
+ * The plan's rows. `parts` is what the terminal draws: part headings, numbers restarting in each
22
+ * part. `json` is the plain-text plan an agent reads beside `--json` (dry run, --json on stderr,
23
+ * headless): the older groups and the numbers JSON `actions[].number` reports (CTC-4680).
24
+ */
25
+ export function standalonePlan(journal, scope, runnerSelected = false, numbering = "parts") {
26
+ const json = numbering === "json";
52
27
  const includes = (id) => !scope || scope.includes(id);
53
28
  const steps = new Map(journal.steps.map((step) => [step.id, step]));
54
29
  const rows = [];
55
30
  if (COMPUTER_CHECKS.some((id) => includes(id)))
56
31
  rows.push({
57
- group: "On this computer",
32
+ group: json ? "On this computer" : setupPartHeading(1, false),
33
+ part: 1,
58
34
  number: 1,
59
35
  title: "Check this computer",
60
36
  detail: includes("legacy")
@@ -62,9 +38,31 @@ export function standalonePlan(journal, scope, runnerSelected = false) {
62
38
  : "check the selected machine requirements",
63
39
  });
64
40
  for (const [group, id, detail] of PLAN) {
41
+ // The terminal gives this computer's runner its own row after "Check runners". The plain-text
42
+ // plan keeps it folded into that row, under the number JSON reports for both.
43
+ if (!json && id === "capacity") {
44
+ const row = (step, text) => rows.push({
45
+ group: setupPartHeading(3, false),
46
+ part: 3,
47
+ number: SETUP_PART_NUMBERS[step],
48
+ title: setupStepView({ id: step, state: "pending" }).title,
49
+ detail: steps.get(step)?.state === "done" ? `check again; ${text}` : text,
50
+ });
51
+ if (includes("capacity"))
52
+ row("capacity", detail);
53
+ if (includes("runner"))
54
+ row("runner", runnerSelected
55
+ ? "start a Catalyst runner here with Docker"
56
+ : "optional; this computer takes work only if you choose it");
57
+ continue;
58
+ }
59
+ // The terminal gives repository values their own row; the plain-text plan keeps them folded
60
+ // into the settings row, under the number JSON reports for both.
61
+ if (json && id === "values")
62
+ continue;
65
63
  if (!includes(id) &&
66
64
  !(id === "capacity" && includes("runner")) &&
67
- !(id === "settings" && includes("values")))
65
+ !(json && id === "settings" && includes("values")))
68
66
  continue;
69
67
  const reviewedDetail = id === "capacity" && includes("runner") && runnerSelected
70
68
  ? "start a Catalyst runner here with Docker"
@@ -72,8 +70,9 @@ export function standalonePlan(journal, scope, runnerSelected = false) {
72
70
  ? "review the optional runner on this machine; start only if selected"
73
71
  : detail;
74
72
  rows.push({
75
- group,
76
- number: SETUP_NUMBERS[id],
73
+ group: json ? group : setupPartHeading(SETUP_PART_OF[id], false),
74
+ part: SETUP_PART_OF[id],
75
+ number: (json ? SETUP_NUMBERS : SETUP_PART_NUMBERS)[id],
77
76
  title: setupStepView({ id, state: "pending" }).title,
78
77
  detail: steps.get(id)?.state === "done"
79
78
  ? `check again; ${reviewedDetail}`
@@ -82,29 +81,37 @@ export function standalonePlan(journal, scope, runnerSelected = false) {
82
81
  }
83
82
  return rows;
84
83
  }
85
- export function pendingContinuation(journal) {
86
- const unfinished = (...ids) => ids.some((id) => {
87
- const step = journal.steps.find((step) => step.id === id);
88
- return (step &&
89
- step.state !== "done" &&
90
- setupStepView(step, journal).mark !== "skip");
91
- });
92
- const next = [];
93
- if (unfinished("github.repos", "projects"))
94
- next.push("chooses repositories");
95
- if (unfinished("accounts"))
96
- next.push("checks a coding account");
97
- if (unfinished("capacity", "runner"))
98
- next.push("checks runners");
99
- if (unfinished("settings", "values"))
100
- next.push("reviews repository settings");
101
- if (unfinished("first-ticket"))
102
- next.push("starts a first ticket");
84
+ /** What setup does next on its own, in the order it runs: steps not finished and not already listed
85
+ * as something a person must do (`listed`). A step setup never reached because it paused is here. */
86
+ const CONTINUES = [
87
+ [["linear.workspace"], "connects your Linear workspace"],
88
+ [["linear.personal"], "connects your Linear account"],
89
+ [["linear.team"], "chooses a Linear team"],
90
+ [["linear.adopt"], "sets up the team's workflow"],
91
+ [["linear.automations"], "checks Linear's pull request automations"],
92
+ [["github.install"], "installs Catalyst on GitHub"],
93
+ [["github.personal"], "connects your GitHub account"],
94
+ [["github.repos", "projects"], "chooses repositories"],
95
+ [["accounts"], "checks an AI account"],
96
+ [["capacity", "runner"], "checks runners"],
97
+ [["settings"], "reviews repository settings"],
98
+ [["values"], "checks repository values"],
99
+ [["first-ticket"], "starts a first ticket"],
100
+ ];
101
+ export function pendingContinuation(journal, listed = new Set()) {
102
+ const unfinished = (ids) => !ids.some((id) => listed.has(id)) &&
103
+ ids.some((id) => {
104
+ const step = journal.steps.find((step) => step.id === id);
105
+ return (step &&
106
+ step.state !== "done" &&
107
+ setupStepView(step, journal).mark !== "skip");
108
+ });
109
+ const next = CONTINUES.filter(([ids]) => unfinished(ids)).map(([, words]) => words);
103
110
  if (!next.length)
104
111
  return null;
105
112
  const last = next.pop();
106
113
  return `Then setup ${next.length ? `${next.join(", ")} and ` : ""}${last}.`;
107
114
  }
108
115
  export function standalonePlanNotes(localSync) {
109
- return `${localSync ? "Local sync is selected." : "Local sync stays off, so Catalyst reads from the cloud."} Checking a coding account sends Claude one short request, which counts toward its usage. Completed steps are checked again before work starts. Codex credentials are not refreshed. Your code, git settings and other skills stay as they are.`;
116
+ return `${localSync ? "Local sync is selected." : "Local sync stays off, so Catalyst reads from the cloud."} Setup reads your AI accounts without sending them a request. Completed steps are checked again before work starts. Your code, git settings and other skills stay as they are.`;
110
117
  }