@catalyst-cloud/cli 0.15.1 → 0.15.3

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 (40) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +13 -4
  3. package/dist/args.js +1 -1
  4. package/dist/capabilities.js +3 -1
  5. package/dist/cli.js +25 -30
  6. package/dist/consent-browser.js +3 -1
  7. package/dist/oauth.js +1 -1
  8. package/dist/onboard-accounts.js +67 -171
  9. package/dist/onboard-checklist.js +309 -0
  10. package/dist/onboard-existing.js +1 -1
  11. package/dist/onboard-first-ticket.js +199 -0
  12. package/dist/onboard-github.js +13 -1
  13. package/dist/onboard-headless.js +6 -4
  14. package/dist/onboard-housekeeping.js +93 -0
  15. package/dist/onboard-next.js +124 -13
  16. package/dist/onboard-ready.js +61 -0
  17. package/dist/onboard-runtime.js +117 -26
  18. package/dist/onboard-standalone-copy.js +73 -66
  19. package/dist/onboard-ui.js +189 -81
  20. package/dist/onboard-values.js +47 -0
  21. package/dist/onboard.js +59 -18
  22. package/dist/ready-copy.js +325 -0
  23. package/dist/ready.js +35 -98
  24. package/dist/setup-onboard-copy.js +262 -41
  25. package/dist/setup-render.js +73 -14
  26. package/dist/setup.js +6 -3
  27. package/package.json +2 -2
  28. package/skills/catalyst-github/SKILL.md +1 -1
  29. package/skills/catalyst-linear/SKILL.md +1 -1
  30. package/skills/catalyst-onboard/SKILL.md +3 -3
  31. package/skills/catalyst-onboard/references/replacing-a-credential.md +9 -15
  32. package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -11
  33. package/skills/catalyst-onboard/scripts/where-am-i.mjs +3 -2
  34. package/skills/run-this-project/SKILL.md +2 -2
  35. package/skills/unstick/SKILL.md +1 -1
  36. package/skills/what-needs-me/SKILL.md +1 -1
  37. package/skills/what-this-repo-needs/SKILL.md +1 -1
  38. package/skills/whats-happening/SKILL.md +2 -2
  39. package/skills/whats-happening/references/coding-accounts.md +1 -1
  40. package/skills/whats-happening/scripts/snapshot.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.3
4
+
5
+ - Setup can finish once the steps needed to run work are ready. It lists optional next steps and keeps your first-ticket choice when you resume.
6
+ - Setup shows one progress display with numbered steps and the reason a step needs attention.
7
+ - When the Connect accounts page is available, setup uses it to finish account connections. Any usable AI account counts without a test request.
8
+ - Setup offers API keys by default. Subscription account options appear only when your workspace enables them.
9
+
10
+ ## 0.15.2
11
+
12
+ - `catalyst setup` is the command to set up Catalyst. It runs the same steps as `catalyst onboard`, which still works.
13
+ - When a browser link times out, setup says so and asks whether you are ready to try again. Yes gives a new link; stopping pauses setup without moving on to steps that depend on it.
14
+ - The GitHub link opens GitHub's install page directly. An install request already waiting on an organization owner is not requested again, and setup still shows the link to install it yourself.
15
+ - Setup asks whether to run work on this machine only after Catalyst is installed on GitHub.
16
+ - Sign-in says which Catalyst workspace you joined, without internal account numbers. `catalyst ready` describes each team problem in plain words, with the page to fix it and who can fix it.
17
+
3
18
  ## 0.15.1
4
19
 
5
20
  - Runner setup accepts verified personal team admission before a repository has been mapped to that team. Older cloud servers keep their existing admission check.
package/README.md CHANGED
@@ -4,13 +4,22 @@
4
4
 
5
5
  ## Install
6
6
 
7
- Run the installer, then type `/catalyst-onboard` in your coding agent:
7
+ Set up Catalyst Cloud and manage your account from the command line.
8
+
9
+ Check ticket progress and see which decisions need your attention.
10
+
11
+ Install: `npm install -g @catalyst-cloud/cli`
12
+
13
+ [CLI documentation](https://github.com/coalesce-labs/catalyst-cloud-skills#readme)
14
+
15
+ Run the installer, then run `catalyst onboard`:
8
16
 
9
17
  ```sh
10
18
  curl -fsSL https://staging.catalystcloud.dev/install.sh | sh
19
+ catalyst onboard
11
20
  ```
12
21
 
13
- The installer puts the CLI and both skill packs on this machine and connects it with one browser approval. It ends by printing the next step. Then open a new session of your coding agent in any directory and type `/catalyst-onboard` (`$catalyst-onboard` in Codex). The onboarding skill reads where this machine stands and walks you through the rest one step at a time. It asks before it writes anything to your machine. Everything else on this page is the reference it follows.
22
+ The installer puts the CLI and both skill packs on this machine and connects it with one browser approval. Run `catalyst onboard` to finish setup from the command line. You can also use the onboarding skill in your coding agent after that. Everything else on this page is the reference.
14
23
 
15
24
  This repository supplies skills for setting up and operating your Catalyst Cloud account. A coding workstation also uses [`coalesce-labs/catalyst-dev-skills`](https://github.com/coalesce-labs/catalyst-dev-skills) for research, planning, implementation, review, and shipping.
16
25
 
@@ -102,7 +111,7 @@ The command is `catalyst`. `catalyst-skills` still works as a deprecated alias,
102
111
 
103
112
  ## What this is
104
113
 
105
- A set of skills that let your coding agent run your own Catalyst Cloud account (https://staging.catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your account through the Catalyst Cloud SDK, write to it through the account's agent proxy, and never compose a URL or run a tool of their own; every read, write and subscription is a `catalyst` verb with `--help`. Every skill is plain Markdown under `skills/<name>/SKILL.md` in this repository, and your own login — a keyless device-code session, or a personal key — is the only credential. Your agent acts as you: the asks it raises and the comments it posts carry your name, and "what needs me" means you.
114
+ A set of skills that let your coding agent run your own Catalyst Cloud account (https://staging.catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your account through the Catalyst Cloud SDK, write to it through the account's agent proxy, and never compose a URL or run a tool of their own; every read, write and live watch is a `catalyst` verb with `--help`. Every skill is plain Markdown under `skills/<name>/SKILL.md` in this repository, and your own login — a keyless device-code session, or a personal key — is the only credential. Your agent acts as you: the asks it raises and the comments it posts carry your name, and "what needs me" means you.
106
115
 
107
116
  ## What connecting does
108
117
 
@@ -222,7 +231,7 @@ Some of these only read, and some write: a comment, a card move, an ask, a relea
222
231
 
223
232
  ## What a key cannot see yet
224
233
 
225
- Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst accounts`: provider, declared and observed state, window usage, walls, quarantine — never a credential; enrolling or pausing one is `<your cloud>/settings/coding-accounts`). It also releases a parked or held ticket once its cause is fixed: `catalyst release <ticket> --because <what changed>` (the `unstick` skill runs it), recorded against your name and shown in the ticket's history. And it declares what your containers need: `catalyst environment` reads the account-wide declaration, `environment propose --file <path> --approve` proposes and approves it in one compare-and-set, and the values behind the names stay in the cloud — reading needs any active seat, proposing and approving need an admin or owner one. An admin or owner can put a repository's values in from the terminal. `catalyst secret import .env --repo owner/name` stores every name in the file and lists the declared names that still have no value. `catalyst secret set NAME --repo owner/name --command 'op read op://Vault/item/field'` runs the command on your machine and stores its output; with no `--command` it reads the value from stdin, or asks for it without echoing. No value is ever printed, and the cloud's audit records the command, not its output. That is the account's own declaration — a repository's own `.catalyst/catalyst.toml`, which `catalyst-onboard` already reports on, is a separate thing. `catalyst env inventory` lists the environment variable names a repository needs — never a value; `env check` checks the TOML syntax and environment variable table in `.catalyst/catalyst.toml` offline; `env migrate [catalyst.env.json]` prints a TOML environment table from a legacy declaration, with all names optional and values omitted. These commands need no login (the `what-this-repo-needs` skill walks through them). Two things it cannot do, and the skills say so by name rather than guess:
234
+ Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst accounts`: provider, declared and observed state, usage limits, walls, quarantine — never a credential; enrolling or pausing one is `<your cloud>/settings/coding-accounts`). It also releases a parked or held ticket once its cause is fixed: `catalyst release <ticket> --because <what changed>` (the `unstick` skill runs it), recorded against your name and shown in the ticket's history. And it declares what your containers need: `catalyst environment` reads the account-wide declaration, `environment propose --file <path> --approve` proposes and approves it in one compare-and-set, and the values behind the names stay in the cloud — reading needs any active seat, proposing and approving need an admin or owner one. An admin or owner can put a repository's values in from the terminal. `catalyst secret import .env --repo owner/name` stores every name in the file and lists the declared names that still have no value. `catalyst secret set NAME --repo owner/name --command 'op read op://Vault/item/field'` runs the command on your machine and stores its output; with no `--command` it reads the value from stdin, or asks for it without echoing. No value is ever printed, and the cloud's audit records the command, not its output. That is the account's own declaration — a repository's own `.catalyst/catalyst.toml`, which `catalyst-onboard` already reports on, is a separate thing. `catalyst env inventory` lists the environment variable names a repository needs — never a value; `env check` checks the TOML syntax and environment variable table in `.catalyst/catalyst.toml` offline; `env migrate [catalyst.env.json]` prints a TOML environment table from a legacy declaration, with all names optional and values omitted. These commands need no login (the `what-this-repo-needs` skill walks through them). Two things it cannot do, and the skills say so by name rather than guess:
226
235
 
227
236
  - Read pull-request labels or the reviewer's reaction. The mirror does not carry them; GitHub's own page does.
228
237
  - Compute a flow number — cycle time, throughput, or how long pull requests have been open. Nothing serves those yet, so say they are not computed rather than counting something else and calling it that.
package/dist/args.js CHANGED
@@ -457,7 +457,7 @@ export const FLAG_TABLES = {
457
457
  },
458
458
  };
459
459
  export const VERB_USAGE = {
460
- setup: "setup --engine <file> --engine-sha256 <hex> [-- <install options>] (run by install.sh; install.sh --help lists the options)",
460
+ setup: "setup [onboard options] (sets up Catalyst step by step; same as catalyst onboard) | setup --engine <file> --engine-sha256 <hex> [-- <install options>] (run by install.sh; install.sh --help lists the options)",
461
461
  login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
462
462
  onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--runner|--no-runner] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--json]",
463
463
  install: "install [--skills-dir <dir>] [--force]",
@@ -5,7 +5,9 @@ const agent = (method, name) => ({ method, path: `/api/v1/agent/${name}` });
5
5
  * the person, the workspace, the project, the repository, then work. A new verb is a new row here
6
6
  * in the same change, or `catalyst capabilities` lies to every guide that reads it. */
7
7
  export const CAPABILITIES = [
8
- { verb: "setup", does: "show install.sh's plan, progress and sign-in in this terminal, and ask its questions", needs: "member", routes: [], since: "0.15.0", engineProtocol: 1 },
8
+ // CTC-4680: plain `catalyst setup` runs the onboard flow, so install.sh may hand off to it. install.sh
9
+ // reads this flag rather than `setup --help`, which already exits 0 on 0.15.x.
10
+ { verb: "setup", does: "set up Catalyst step by step, or show install.sh's plan, progress and sign-in in this terminal", needs: "member", routes: [], since: "0.15.0", engineProtocol: 1, bootstrapHandoff: true },
9
11
  { verb: "onboard", does: "run and resume Catalyst setup with a saved step record", needs: "member", routes: [], since: "0.14.0", bootstrapHandoff: true, headless: true },
10
12
  { verb: "status", does: "say whether this machine is connected, as whom, and to which cloud", needs: "member", routes: [], since: "0.1.0" },
11
13
  { verb: "me", does: "read the person's seat, role and Linear identity", needs: "member", routes: [], since: "0.1.0" },
package/dist/cli.js CHANGED
@@ -44,12 +44,12 @@ import { cmdRepo } from "./repo.js";
44
44
  import { cmdLegacy } from "./legacy.js";
45
45
  import { cmdIdentity } from "./identity.js";
46
46
  import { cmdConnections } from "./connections.js";
47
- import { cmdOnboard, onboardErrorJournal, onboardRequiredStepsWaiting, } from "./onboard.js";
47
+ import { cmdOnboard, onboardErrorJournal, } from "./onboard.js";
48
48
  import { createClackOnboardUi, shouldUseOnboardUi, } from "./onboard-ui.js";
49
49
  import { createOnboardRuntime } from "./onboard-runtime.js";
50
50
  import { argvRequestsHeadless, headlessOnboardDeps, headlessUsageRefusal, onboardHeadlessRequested, runOnboardHeadless, } from "./onboard-headless.js";
51
51
  import { savedOnboardBaseUrl } from "./onboard-next.js";
52
- import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
52
+ import { onboardingReadyReport, onboardReadyStep, observeCloudOnboarding, } from "./onboard-ready.js";
53
53
  import { selectedOnboardTeam } from "./onboard-existing.js";
54
54
  export { CONFIG_MODE, DEFAULT_BASE_URL, FIRST_STAMPED_VERSION, LEGACY_PACKAGE_NAME, PACKAGE_NAME, PROVENANCE_MARKER, CliError, MeError, UsageError, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, fetchMe, formatMode, installedBundleVersion, installSkills, loadConfig, modernCliPath, normalizeBaseUrl, parseArgs, parseChangelogEntry, parseProvenanceVersion, readChangelog, readManifest, resolveSkillsDir, saveConfig, skillsSourceDir, updateNoticeLine, writeConfig, };
55
55
  export const CUSTOMER_SKILLS = [
@@ -69,7 +69,7 @@ const VERB_ALIASES = { join: "login" };
69
69
  const HUMAN_VERBS = new Set(["login", "install", "status", "notice"]);
70
70
  export function usageText() {
71
71
  return [
72
- `${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your tenant, read through the SDK, write through the agent proxy`,
72
+ `${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your Catalyst workspace, read through the SDK, write through the agent proxy`,
73
73
  "",
74
74
  "Usage:",
75
75
  " catalyst login [--base-url <url>] [--start-replica] (keyless: logs you in as yourself)",
@@ -96,7 +96,7 @@ export function usageText() {
96
96
  " catalyst var set <NAME> | var import <file> [--repo <owner/name>] [--names NAME[,NAME...]] (plain environment variables)",
97
97
  " catalyst team <list|check|map|adopt|migrate|checklist> ...",
98
98
  " catalyst capabilities [--json] (what this CLI can do, the role each verb needs, and whether this cloud serves it)",
99
- " catalyst project list [--json] (all tenant projects, including those with no stage mapping)",
99
+ " catalyst project list [--json] (all workspace projects, including those with no stage mapping)",
100
100
  " catalyst project wip-limit <get|set <n>|set default> [--team K] (a project's new-start WIP limit; members read, owners and admins set)",
101
101
  " catalyst repo <agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> (a checkout's AGENTS.md block and portable agent layout; working tree only)",
102
102
  " catalyst legacy [--remove [--data] [--yes]] (leftovers of the old local Catalyst runtime: list them; remove them only on a yes)",
@@ -110,8 +110,8 @@ export function usageText() {
110
110
  "--key (or set CATALYST_CLOUD_TOKEN) to use a personal or account key instead. The base URL defaults",
111
111
  `to CATALYST_CLOUD_BASE_URL or ${DEFAULT_BASE_URL}.`,
112
112
  "",
113
- "login discovers your tenant from GET /api/v1/me, writes ~/.config/catalyst-cloud/customer.json",
114
- "(0600) holding your login session (or key) and this CLI's path, and caches the tenant contract",
113
+ "login finds your Catalyst workspace, writes ~/.config/catalyst-cloud/customer.json",
114
+ "(0600) holding your login session (or key) and this CLI's path, and caches the workspace contract",
115
115
  "beside it. A keyless session's short-lived token refreshes silently on every request. login does",
116
116
  "not install skills: your agent's own install command does that, and `install` is only here to",
117
117
  "repair a copy this package made.",
@@ -208,9 +208,15 @@ function maybePrintUpdateNotice(args, ctx) {
208
208
  }
209
209
  export async function main(argv, ctx = defaultCtx(), deps = {}) {
210
210
  // CTC-4625: setup passes the install options through to its engine untouched, so they never meet
211
- // this CLI's own flag tables.
212
- if (argv[0] === "setup")
213
- return runSetup(argv.slice(1), ctx, deps);
211
+ // this CLI's own flag tables. CTC-4680: without --engine, `setup` is the name a person types for
212
+ // the onboard flow, so it runs exactly that.
213
+ if (argv[0] === "setup") {
214
+ const rest = argv.slice(1);
215
+ const own = rest.indexOf("--") === -1 ? rest : rest.slice(0, rest.indexOf("--"));
216
+ if (own.includes("--engine") || own.includes("--help") || own.includes("-h"))
217
+ return runSetup(rest, ctx, deps);
218
+ argv = ["onboard", ...rest];
219
+ }
214
220
  let args;
215
221
  try {
216
222
  args = parseArgs(argv.map((a, i) => (i === 0 && VERB_ALIASES[a] ? VERB_ALIASES[a] : a)));
@@ -231,7 +237,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
231
237
  onboardHeadlessRequested(args, ctx.env))
232
238
  return headlessUsageRefusal(ctx, "--help and --version print text, not the headless document; run them without --json", true, manifest.version);
233
239
  if (args.version) {
234
- ctx.stdout(`${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
240
+ ctx.stdout(
241
+ // Machine-read: install.sh and catalyst-cloud's onboarding harness match this exact line.
242
+ `${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
235
243
  return 0;
236
244
  }
237
245
  if (args.command === null) {
@@ -298,6 +306,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
298
306
  renderer: createSetupRenderer(process.stdout, ctx.env),
299
307
  disposeRenderer: true,
300
308
  signinTimeoutMs: deps.onboardSigninTimeoutMs,
309
+ version: manifest.version,
301
310
  });
302
311
  }
303
312
  catch (error) {
@@ -320,6 +329,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
320
329
  renderer: createSetupRenderer(output, ctx.env),
321
330
  disposeRenderer: true,
322
331
  signinTimeoutMs: deps.onboardSigninTimeoutMs,
332
+ version: manifest.version,
323
333
  interactive: false,
324
334
  verbose: args.flags.verbose === true,
325
335
  });
@@ -387,21 +397,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
387
397
  localSync: journal?.localSync ?? args.flags["local-sync"] === true,
388
398
  observe: observeCloudOnboarding,
389
399
  });
390
- const failed = report.checks.some((check) => check.required && check.state === "fail") && !onboardRequiredStepsWaiting(journal);
391
- return {
392
- state: report.state === "complete"
393
- ? "done"
394
- : failed
395
- ? "failed"
396
- : "waiting",
397
- reason: report.state === "complete"
398
- ? undefined
399
- : "onboarding_checks_pending",
400
- evidence: {
401
- checks: report.checks.length,
402
- passed: report.checks.filter((check) => check.state === "pass").length,
403
- },
404
- };
400
+ return onboardReadyStep(report, journal);
405
401
  },
406
402
  legacy: deps.legacy,
407
403
  openBrowser: deps.openBrowser,
@@ -687,26 +683,25 @@ async function cmdLogin(args, ctx, deps, personalOnly = false) {
687
683
  };
688
684
  checkCancelled();
689
685
  const written = writeConfig(ctx.home, config);
690
- ctx.stdout(`Connected to ${me.name} (${me.slug}) — account ${me.account}`);
686
+ ctx.stdout(`Catalyst workspace: ${me.name} (${me.slug})`);
691
687
  if (me.user) {
692
- ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId
688
+ ctx.stdout(`Signed in to Catalyst as ${me.user.label} (${me.user.role})${me.user.linearUserId
693
689
  ? ""
694
690
  : ' — your Linear identity is not matched yet, so "what needs me" will show everyone\'s asks until an admin matches it in Settings'}`);
695
691
  }
696
692
  else {
697
- ctx.stderr(`[catalyst] this is the tenant's account key (a host credential), not your own — the skills work, but nothing your agent writes will carry your name and "what needs me" cannot mean you. Mint a personal key at Settings → API keys and log in with that.`);
693
+ ctx.stderr(`[catalyst] this is your Catalyst workspace's shared key (a host credential), not your own. The skills work, but nothing your agent writes will carry your name, and "what needs me" cannot mean you. Make a personal key at Settings → API keys and sign in with that.`);
698
694
  }
699
695
  ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your ${auth ? "login session" : "key"} and the CLI path)${written.mode === CONFIG_MODE
700
696
  ? ""
701
697
  : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
702
- ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
703
698
  try {
704
699
  const loaded = await loadContract(ctx, config, {
705
700
  refresh: true,
706
701
  signal: deps.signal,
707
702
  });
708
703
  checkCancelled();
709
- ctx.stdout(`Tenant contract ${loaded.doc.contractVersion} cached at ${loaded.path}`);
704
+ ctx.stdout(`Saved your workspace settings (version ${loaded.doc.contractVersion}) to ${loaded.path}`);
710
705
  }
711
706
  catch (err) {
712
707
  checkCancelled();
@@ -54,5 +54,7 @@ export const openConsentBrowser = createConsentBrowserOpener();
54
54
  /** The web page that finishes a consent step when no browser opens here. The signed link stays private. */
55
55
  export function finishOnTheWeb(baseUrl, page, action) {
56
56
  const origin = baseUrl.replace(/\/+$/, "");
57
- return `No browser opened on this computer. In a browser signed in to Catalyst, open ${origin}/settings/${page} and ${action}. Setup keeps waiting here.`;
57
+ // CTC-4680: the GitHub install has a route that goes straight to GitHub; Integrations starts nothing.
58
+ const path = page === "github-install" ? "/connect/github/start" : `/settings/${page}`;
59
+ return `No browser opened on this computer. In a browser signed in to Catalyst, open ${origin}${path} and ${action}. Setup keeps waiting here.`;
58
60
  }
package/dist/oauth.js CHANGED
@@ -156,7 +156,7 @@ export async function deviceFlowLogin(ctx, baseUrl, deps = {}) {
156
156
  throw new Error("browser-unavailable");
157
157
  await deps.openBrowser(auth.verification_uri_complete ?? auth.verification_uri);
158
158
  browserOpened = true;
159
- ctx.stdout("Opened your browser to that page — approve there, or use the code above.");
159
+ ctx.stdout("Opened your browser to that page. Approve there, or use the code above.");
160
160
  }
161
161
  catch {
162
162
  // a browser that will not open is not a failure; the code and URL still work
@@ -3,7 +3,6 @@ import { readExistingOnboardJson } from "./onboard-existing.js";
3
3
  import { pollConsent } from "./onboard-consent.js";
4
4
  import { verifyOnboardRoutes } from "./onboard-capabilities.js";
5
5
  const listPath = "/api/v1/coding-accounts";
6
- const validatePath = "/api/v1/coding-accounts/:slot/validate";
7
6
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
8
7
  ? value
9
8
  : null;
@@ -94,195 +93,92 @@ async function inventory(ctx, journal, signal) {
94
93
  }
95
94
  return { slots, identity };
96
95
  }
97
- /** Verify one existing Claude slot per run. A stored login, cached poll or Codex shape is not
98
- * fresh provider access. Enrollment and a phase's actual lease eligibility remain separate. */
96
+ /** Why one account cannot take work, or null when it can. The Connect page's AI-account rule
97
+ * (the server's isUsableCodingAccount): any provider, any member's account, and a canceled
98
+ * subscription keeps working until its paid access ends. */
99
+ function unusableCause(row, now) {
100
+ if (row.declaredState !== "active")
101
+ return "coding_account_inactive";
102
+ if (row.revokedAtMs !== null ||
103
+ (row.accessEndsAtMs !== null && row.accessEndsAtMs <= now))
104
+ return "coding_account_ended";
105
+ if (row.needsCredential)
106
+ return "coding_account_needs_login";
107
+ if (row.quarantined)
108
+ return "coding_account_quarantined";
109
+ if (row.walled)
110
+ return "coding_account_walled";
111
+ return null;
112
+ }
113
+ export function usableAiAccount(row, now) {
114
+ return unusableCause(row, now) === null;
115
+ }
116
+ /** CTC-4680: the step is done when the workspace has at least one usable AI account, from any
117
+ * provider. With --coding-account, that named account is the one that must be usable. */
99
118
  export function onboardAccountsAdapter(input = {}) {
100
- let proof = null;
101
- let attempted = false;
102
- const candidate = (slots, now) => slots
119
+ const usable = (slots, now) => slots
103
120
  .filter((row) => (input.slot === undefined || row.accountSlot === input.slot) &&
104
- row.provider === "claude" &&
105
- row.declaredState === "active" &&
106
- row.ownedByMe &&
107
- !row.needsCredential &&
108
- !row.walled &&
109
- !row.quarantined &&
110
- row.renewalStatus !== "canceled" &&
111
- row.revokedAtMs === null &&
112
- (row.accessEndsAtMs === null || row.accessEndsAtMs > now))
121
+ usableAiAccount(row, now))
113
122
  .sort((a, b) => a.accountSlot.localeCompare(b.accountSlot))[0];
114
123
  const check = async (ctx, journal, signal) => {
115
124
  const live = await inventory(ctx, journal, signal);
116
125
  if ("reason" in live)
117
126
  return waiting(live.reason);
118
- const selected = candidate(live.slots, ctx.now().getTime());
119
- if (proof &&
120
- equal(live.identity, proof) &&
121
- live.slots.some((row) => row.accountSlot === proof.slot &&
122
- row.provider === proof.provider &&
123
- row.declaredState === "active" &&
124
- row.ownedByMe &&
125
- !row.needsCredential &&
126
- !row.walled &&
127
- !row.quarantined &&
128
- row.renewalStatus !== "canceled" &&
129
- row.revokedAtMs === null &&
130
- (row.accessEndsAtMs === null ||
131
- row.accessEndsAtMs > ctx.now().getTime())) &&
132
- proof.checkedAtMs <= ctx.now().getTime() + 5000 &&
133
- proof.checkedAtMs >= ctx.now().getTime() - 30000)
127
+ const selected = usable(live.slots, ctx.now().getTime());
128
+ if (selected)
134
129
  return {
135
130
  state: "done",
136
131
  evidence: {
137
- accountSlot: proof.slot,
138
- provider: proof.provider,
139
- checkedAt: new Date(proof.checkedAtMs).toISOString(),
132
+ provider: selected.provider,
133
+ checkedAt: ctx.now().toISOString(),
140
134
  },
141
135
  };
142
- proof = null;
143
- if (!live.slots.length)
144
- return input.waitForAccount &&
145
- ["owner", "admin"].includes(live.identity.role)
146
- ? { state: "pending" }
147
- : waiting("account_enrollment_required");
148
- if (input.slot !== undefined &&
149
- !live.slots.some((row) => row.accountSlot === input.slot))
150
- return waiting("coding_account_not_found");
151
- if (!selected)
152
- return waiting(live.slots.some((row) => row.provider === "codex")
153
- ? "codex_provider_access_unverified"
154
- : "account_provider_access_unverified");
155
- if (attempted)
156
- return waiting("account_provider_access_unverified");
157
- const cfg = loadConfig(ctx.home);
158
- if (!cfg?.user || !["owner", "admin"].includes(cfg.user.role))
159
- return waiting("account_validation_admin_required");
160
- return { state: "pending" };
136
+ if (input.slot !== undefined) {
137
+ const named = live.slots.find((row) => row.accountSlot === input.slot);
138
+ // A named account speaks for itself: other accounts in the workspace may be fine.
139
+ return waiting(named
140
+ ? (unusableCause(named, ctx.now().getTime()) ?? "coding_account_not_found")
141
+ : "coding_account_not_found");
142
+ }
143
+ if (input.waitForAccount &&
144
+ ["owner", "admin"].includes(live.identity.role))
145
+ return { state: "pending" };
146
+ return waiting(live.slots.length ? "ai_account_not_usable" : "account_enrollment_required");
161
147
  };
162
148
  return {
163
149
  check,
164
150
  act: async (ctx, journal, external) => {
165
- if (attempted)
166
- return waiting("account_provider_access_unverified");
167
151
  const initial = await inventory(ctx, journal, external);
168
152
  if ("reason" in initial)
169
153
  return waiting(initial.reason);
170
- let live = initial;
171
- if (!live.slots.length &&
172
- input.waitForAccount &&
173
- ["owner", "admin"].includes(live.identity.role)) {
174
- const result = await input.waitForAccount(() => pollConsent({
175
- timeoutMs: input.accountWaitMs,
176
- signal: external,
177
- sleep: input.sleep,
178
- readStatus: async (signal) => {
179
- const current = await inventory(ctx, journal, signal);
180
- if ("reason" in current)
181
- return { outcome: "waiting", reason: current.reason };
182
- live = current;
183
- return { outcome: current.slots.length ? "connected" : "absent" };
184
- },
185
- }));
186
- if (result.state !== "done")
187
- return waiting(result.reason === "consent_timeout"
188
- ? "account_enrollment_required"
189
- : (result.reason ?? "account_enrollment_required"));
190
- }
191
- const selected = candidate(live.slots, ctx.now().getTime());
192
- if (!selected)
193
- return waiting("account_provider_access_unverified");
194
- const support = await verifyOnboardRoutes(ctx, journal, [{ method: "POST", path: validatePath }], external);
195
- if ("reason" in support)
196
- return waiting(support.reason);
197
- const cfg = loadConfig(ctx.home);
198
- if (!equal(binding(ctx, journal), live.identity) ||
199
- !cfg?.user ||
200
- !["owner", "admin"].includes(cfg.user.role))
201
- return waiting("account_identity_unverified");
202
- const expiry = cfg.auth
203
- ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
204
- : 0;
205
- const bearer = cfg.key ||
206
- (cfg.auth && Number.isFinite(expiry) && expiry > 30000
207
- ? cfg.auth.accessToken
208
- : undefined);
209
- if (!bearer)
210
- return waiting("account_login_refresh_required");
211
- input.message?.("Checking one stored Claude account with a one-token provider request. This may use Claude quota. Codex credentials are not refreshed. Phase eligibility is checked separately.");
212
- // A display callback must not be able to switch the identity used for this request.
213
- if (!equal(binding(ctx, journal), live.identity))
214
- return waiting("account_identity_unverified");
215
- const deadline = new AbortController();
216
- const signal = external
217
- ? AbortSignal.any([external, deadline.signal])
218
- : deadline.signal;
219
- const timer = setTimeout(() => deadline.abort(), 15000);
220
- let remove = () => { };
221
- try {
222
- return await new Promise((resolve) => {
223
- const stopped = () => resolve(waiting(external?.aborted
224
- ? "interrupted"
225
- : "account_validation_unavailable"));
226
- if (signal.aborted) {
227
- stopped();
228
- return;
229
- }
230
- signal.addEventListener("abort", stopped, { once: true });
231
- remove = () => signal.removeEventListener("abort", stopped);
232
- Promise.resolve()
233
- .then(async () => {
234
- if (signal.aborted)
235
- return waiting("interrupted");
236
- // UI callbacks can queue a config change before this deferred send executes.
237
- if (!equal(binding(ctx, journal), live.identity))
238
- return waiting("account_identity_unverified");
239
- attempted = true;
240
- const response = await ctx.fetch(`${support.origin}/api/v1/coding-accounts/${encodeURIComponent(selected.accountSlot)}/validate`, {
241
- method: "POST",
242
- redirect: "error",
243
- signal,
244
- headers: {
245
- authorization: `Bearer ${bearer}`,
246
- accept: "application/json",
247
- },
248
- });
249
- if (response.status !== 200)
250
- return waiting("account_validation_unavailable");
251
- const body = object(await response.json());
252
- if (signal.aborted ||
253
- !equal(binding(ctx, journal), live.identity))
254
- return waiting("account_identity_unverified");
255
- const now = ctx.now().getTime();
256
- if (!body ||
257
- body.provider !== selected.provider ||
258
- typeof body.checkedAtMs !== "number" ||
259
- !Number.isSafeInteger(body.checkedAtMs) ||
260
- body.checkedAtMs > now + 5000 ||
261
- body.checkedAtMs < now - 30000 ||
262
- !["working", "walled", "rejected", "inconclusive"].includes(String(body.result)))
263
- return waiting("account_validation_unverified");
264
- if (body.result !== "working")
265
- return waiting(body.result === "walled"
266
- ? "account_provider_walled"
267
- : body.result === "rejected"
268
- ? "account_provider_rejected"
269
- : "account_provider_access_unverified");
270
- proof = {
271
- ...live.identity,
272
- slot: selected.accountSlot,
273
- provider: selected.provider,
274
- checkedAtMs: body.checkedAtMs,
275
- };
276
- input.message?.("Claude provider access was verified. This does not reserve a runner or prove that a phase can start.");
277
- return check(ctx, journal, signal);
278
- })
279
- .then((result) => (signal.aborted ? stopped() : resolve(result)), () => stopped());
280
- });
281
- }
282
- finally {
283
- clearTimeout(timer);
284
- remove();
285
- }
154
+ if (!input.waitForAccount ||
155
+ !["owner", "admin"].includes(initial.identity.role))
156
+ return check(ctx, journal, external);
157
+ let enrolled = initial.slots.length > 0;
158
+ const result = await input.waitForAccount(() => pollConsent({
159
+ timeoutMs: input.accountWaitMs,
160
+ signal: external,
161
+ sleep: input.sleep,
162
+ readStatus: async (signal) => {
163
+ const current = await inventory(ctx, journal, signal);
164
+ if ("reason" in current)
165
+ return { outcome: "waiting", reason: current.reason };
166
+ enrolled = current.slots.length > 0;
167
+ return {
168
+ outcome: usable(current.slots, ctx.now().getTime())
169
+ ? "connected"
170
+ : "absent",
171
+ };
172
+ },
173
+ }));
174
+ if (result.state === "done")
175
+ return check(ctx, journal, external);
176
+ // A wait that ran out holds the step and offers another try (round 5).
177
+ return waiting(result.reason !== "consent_timeout"
178
+ ? (result.reason ?? "account_enrollment_required")
179
+ : enrolled
180
+ ? "ai_account_not_usable"
181
+ : "account_enrollment_required");
286
182
  },
287
183
  };
288
184
  }