@catalyst-cloud/cli 0.16.0 → 0.16.2

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 (49) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +9 -6
  3. package/dist/args.js +6 -0
  4. package/dist/capabilities.js +3 -1
  5. package/dist/cli.js +10 -5
  6. package/dist/environment.js +1 -1
  7. package/dist/execution.js +2 -2
  8. package/dist/hosts.js +89 -0
  9. package/dist/legacy.js +12 -2
  10. package/dist/machine-inventory.js +37 -0
  11. package/dist/onboard-capacity.js +5 -6
  12. package/dist/onboard-headless.js +2 -2
  13. package/dist/onboard-next.js +18 -10
  14. package/dist/onboard-project-cleanup.js +100 -0
  15. package/dist/onboard-returning.js +172 -0
  16. package/dist/onboard-runner.js +149 -34
  17. package/dist/onboard-runtime.js +26 -1
  18. package/dist/onboard-ui.js +61 -0
  19. package/dist/onboard.js +9 -2
  20. package/dist/ready-copy.js +30 -24
  21. package/dist/ready.js +2 -2
  22. package/dist/secret.js +2 -2
  23. package/dist/setup-onboard-copy.js +37 -32
  24. package/dist/setup.js +4 -0
  25. package/dist/transport.js +18 -2
  26. package/package.json +3 -3
  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/connecting-this-machine.md +4 -4
  31. package/skills/catalyst-onboard/references/declaring-a-repository.md +1 -1
  32. package/skills/catalyst-onboard/references/reading-ready.md +13 -13
  33. package/skills/catalyst-onboard/references/replacing-a-credential.md +1 -1
  34. package/skills/catalyst-onboard/references/the-one-path.md +5 -5
  35. package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -4
  36. package/skills/catalyst-onboard/references/what-the-browser-owns.md +20 -10
  37. package/skills/catalyst-onboard/scripts/where-am-i.mjs +49 -26
  38. package/skills/run-this-project/SKILL.md +1 -1
  39. package/skills/run-this-project/references/stalls-and-escalation.md +1 -1
  40. package/skills/unstick/SKILL.md +1 -1
  41. package/skills/what-needs-me/SKILL.md +1 -1
  42. package/skills/what-needs-me/references/raising-a-decision.md +1 -1
  43. package/skills/what-needs-me/references/reading-the-inbox.md +1 -1
  44. package/skills/what-this-repo-needs/SKILL.md +1 -1
  45. package/skills/whats-happening/SKILL.md +3 -2
  46. package/skills/whats-happening/references/coding-accounts.md +1 -1
  47. package/skills/whats-happening/references/machines.md +9 -0
  48. package/skills/whats-happening/references/settings-and-where-they-live.md +8 -8
  49. package/skills/whats-happening/references/stages-and-mapping.md +1 -1
@@ -444,6 +444,49 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
444
444
  ...(answer === "signin" ? { signin: true } : {}),
445
445
  };
446
446
  },
447
+ async reviewProjects(projects) {
448
+ stop();
449
+ if (abort.signal.aborted)
450
+ return null;
451
+ const answer = await select({
452
+ ...options,
453
+ message: "What should setup do with this workspace's projects?",
454
+ initialValue: "move-on",
455
+ options: [
456
+ { value: "move-on", label: "Move on and finish this computer's setup", hint: "default" },
457
+ ...projects.map(p => ({ value: `repair:${p.id}`, label: `Review or repair ${p.name} (${p.key})` })),
458
+ ...projects.map(p => ({ value: `cleanup:${p.id}`, label: `Preview archiving ${p.name} (${p.key})` })),
459
+ { value: "new", label: "Create a new Catalyst project" },
460
+ ],
461
+ });
462
+ if (prompts.isCancel(answer)) {
463
+ abort.abort();
464
+ return null;
465
+ }
466
+ if (answer === "move-on" || answer === "new")
467
+ return answer;
468
+ if (typeof answer === "string" && answer.startsWith("cleanup:"))
469
+ return { cleanup: answer.slice(8) };
470
+ return typeof answer === "string" && answer.startsWith("repair:") ? { repair: answer.slice(7) } : null;
471
+ },
472
+ async confirmProjectCleanup(project, lines) {
473
+ stop();
474
+ for (const line of lines)
475
+ message(line);
476
+ if (abort.signal.aborted)
477
+ return false;
478
+ const answer = await select({ ...options,
479
+ message: `Archive ${project.name} after reviewing these changes?`,
480
+ initialValue: "keep",
481
+ options: [{ value: "keep", label: "Keep the project and finish this computer's setup" },
482
+ { value: "archive", label: "Archive this project" }],
483
+ });
484
+ if (prompts.isCancel(answer)) {
485
+ abort.abort();
486
+ return false;
487
+ }
488
+ return answer === "archive";
489
+ },
447
490
  async chooseTeam(teams, create) {
448
491
  stop();
449
492
  // Creating needs a name prompt; a port without one offers only the existing teams.
@@ -671,6 +714,23 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
671
714
  }
672
715
  return typeof answer === "string" ? answer : null;
673
716
  },
717
+ async nameMachine(defaultName) {
718
+ if (abort.signal.aborted)
719
+ return null;
720
+ const answer = await text({
721
+ ...options,
722
+ message: "Name for this machine",
723
+ initialValue: defaultName,
724
+ validate: (value) => /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/.test(value ?? "")
725
+ ? undefined
726
+ : "Use 1 to 63 letters, digits, dots, underscores or hyphens, starting with a letter or digit.",
727
+ });
728
+ if (typeof answer !== "string") {
729
+ abort.abort();
730
+ return null;
731
+ }
732
+ return answer;
733
+ },
674
734
  async chooseRunner() {
675
735
  if (abort.signal.aborted)
676
736
  return null;
@@ -1114,6 +1174,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
1114
1174
  delete ui.chooseFirstRepository;
1115
1175
  delete ui.reviewSettings;
1116
1176
  delete ui.chooseRunner;
1177
+ delete ui.nameMachine;
1117
1178
  delete ui.chooseFirstTicket;
1118
1179
  delete ui.retryTimedOut;
1119
1180
  delete ui.confirmWorkflowAdoption;
package/dist/onboard.js CHANGED
@@ -268,6 +268,7 @@ function normalizeStep(value, fallbackAt) {
268
268
  "membershipId",
269
269
  "checkedAt",
270
270
  "project",
271
+ "projectCount",
271
272
  "repository",
272
273
  "installation",
273
274
  "ticket",
@@ -673,6 +674,7 @@ export function stepSatisfied(step) {
673
674
  // Optional: a computer without the daily update still runs work.
674
675
  step.id === "housekeeping" ||
675
676
  step.reason === "member_scope" ||
677
+ step.reason === "returning_workspace_move_on" ||
676
678
  (step.id === "daemon" && step.reason === "local_sync_not_selected") ||
677
679
  (step.id === "runner" && step.reason === "runner_not_selected") ||
678
680
  (step.id === "first-ticket" && step.reason === "first_ticket_skipped") ||
@@ -711,7 +713,8 @@ export function onboardRequiredStepsWaiting(journal) {
711
713
  }
712
714
  /** A full run that exited 0 with deferred steps still open: ready for work, not complete. */
713
715
  export function onboardReadyForWork(journal, only) {
714
- return !only && journal.exit === 0 && !journal.complete;
716
+ return !only && journal.exit === 0 && !journal.complete &&
717
+ !journal.steps.some(step => step.reason === "returning_workspace_move_on");
715
718
  }
716
719
  /** A recorded step's next action; a prerequisite wait names the first unfinished step it needs. */
717
720
  export function onboardStepAction(journal, step, baseUrl) {
@@ -1143,7 +1146,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1143
1146
  else if (deps.ui)
1144
1147
  deps.ui.finish(journal, only);
1145
1148
  else {
1146
- if (journal.complete)
1149
+ if (journal.complete && journal.steps.some(step => step.reason === "returning_workspace_move_on")) {
1150
+ ctx.stdout("This computer is set up. Existing projects were kept in place.");
1151
+ ctx.stdout("Next: run catalyst ready to review their checks.");
1152
+ }
1153
+ else if (journal.complete)
1147
1154
  ctx.stdout("Onboarding complete.");
1148
1155
  else if (onboardReadyForWork(journal, only) &&
1149
1156
  (deps.requiredSteps ?? []).every((id) => journalStep(journal, id)?.state === "done")) {
@@ -90,11 +90,11 @@ export function valuesFacts(value) {
90
90
  : [],
91
91
  };
92
92
  }
93
- /** Where the named variables get their values. Names only: nothing here reads or prints a value. */
94
- function setValuesFix(baseUrl, names) {
95
- return baseUrl
96
- ? `set ${names.join(", ")} on the repository's Environment page: open ${origin(baseUrl)}/settings/projects, then the project, then the repository`
97
- : `set ${names.join(", ")} on the repository's Environment page in Catalyst's project settings`;
93
+ /** How the named variables get their values: the CLI's own verbs, which read each value from a
94
+ * hidden prompt. Names only: nothing here reads or prints a value. */
95
+ function setValuesFix(names) {
96
+ const name = names.length === 1 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(names[0]) ? names[0] : "NAME";
97
+ return `set ${names.join(", ")} with catalyst var set ${name} --repo <owner/name> (catalyst secret set ${name} --repo <owner/name> for a secret)`;
98
98
  }
99
99
  function nameList(v) {
100
100
  return Array.isArray(v)
@@ -127,13 +127,13 @@ function unresolvedLine(u) {
127
127
  * identifier or a repository name, never a value, and nothing here reads a value. Any other check
128
128
  * keeps the generic line. Shared with setup's values step, which has no web address when the
129
129
  * saved login has none. */
130
- export function valuesFix(base, c) {
130
+ export function valuesFix(_base, c) {
131
131
  const unresolved = unresolvedList(c.unresolved);
132
132
  const unresolvedNames = new Set(unresolved.map((u) => u.name));
133
133
  const names = nameList(c.names).filter((n) => !unresolvedNames.has(n));
134
134
  const parts = [];
135
135
  if (names.length > 0)
136
- parts.push(setValuesFix(base, names));
136
+ parts.push(setValuesFix(names));
137
137
  for (const u of unresolved)
138
138
  parts.push(unresolvedLine(u));
139
139
  for (const note of Array.isArray(c.repos) ? c.repos : []) {
@@ -152,10 +152,6 @@ export function valuesFix(base, c) {
152
152
  }
153
153
  return parts.length === 0 ? null : parts.join(". ");
154
154
  }
155
- /** The team's own page in Catalyst settings, optionally one of its sub-pages (`map`, `adopt`). */
156
- export function teamPage(baseUrl, key, sub) {
157
- return `${origin(baseUrl)}/settings/linear-teams/${encodeURIComponent(key)}${sub ? `/${sub}` : ""}`;
158
- }
159
155
  /** The dispatch gate: whether Catalyst may start any ticket in the team. */
160
156
  export function dispatchGateCopy(baseUrl, key, status, missingSlots) {
161
157
  if (status === "open")
@@ -170,18 +166,18 @@ export function dispatchGateCopy(baseUrl, key, status, missingSlots) {
170
166
  if (status === "mapping_missing")
171
167
  return {
172
168
  line: `team ${key}: Catalyst doesn't know which ${key} stages to use for ${slots}. Until it does, it starts no ${key} tickets.`,
173
- fix: `pick them at ${teamPage(baseUrl, key, "map")}`,
169
+ fix: `run catalyst team map ${key}`,
174
170
  };
175
171
  if (status === "mapping_state_unresolved")
176
172
  // The gate reads Catalyst's own copy of the team, which can lag Linear by a minute after a team
177
173
  // is created or its workflow is set up (CTC-4680 Rerun A). Say that, not "map your stages".
178
174
  return {
179
175
  line: `team ${key}: Catalyst can't find the stages it uses for ${slots} in its copy of ${key} yet. Until it can, it starts no ${key} tickets.`,
180
- fix: `if you just set up ${key}, run catalyst ready again in a minute. If this stays, press Re-check at ${teamPage(baseUrl, key)}`,
176
+ fix: `if you just set up ${key}, run catalyst ready again in a minute. If this stays, run catalyst team check ${key}`,
181
177
  };
182
178
  return {
183
179
  line: `team ${key}: Catalyst can't start ${key} tickets yet.`,
184
- fix: `open ${teamPage(baseUrl, key)} to see why`,
180
+ fix: `run catalyst team check ${key} to see why`,
185
181
  };
186
182
  }
187
183
  const AUTOMATION_RULES = {
@@ -237,6 +233,13 @@ const CHECK_PAGES = {
237
233
  github_app_installed: "/settings/projects",
238
234
  thoughts_reachable: "/settings/projects",
239
235
  };
236
+ /** The command that fixes a check, offered before its page. A check with no command keeps the page. */
237
+ const CHECK_COMMANDS = {
238
+ webhook_covers_team: (key) => `an owner or admin registers the repository with catalyst onboard --team ${key} --repo <owner/name>`,
239
+ hosts_current: () => "run catalyst onboard --runner on the machine that runs the work",
240
+ tools_resolvable: () => "set the missing secret with catalyst secret set NAME --repo <owner/name>, or fix the repository's .catalyst/catalyst.toml",
241
+ reviewer_required: () => "relax the merge policy with catalyst var set CATALYST_MERGE_EVIDENCE_POLICY --repo <owner/name> (if .catalyst/catalyst.toml declares the policy, change it there instead, because the declaration wins), or configure a reviewer",
242
+ };
240
243
  const AGAIN = "run catalyst ready again in a few minutes";
241
244
  /** One failing or unknown team check, in plain words. `valuesFix` is the variable-naming fix the
242
245
  * caller already built for `required_values`, or null when the cloud named no variable. */
@@ -258,17 +261,17 @@ export function teamCheckCopy(baseUrl, key, id, state, valuesFix) {
258
261
  switch (id) {
259
262
  case "oauth_scope":
260
263
  return unknown
261
- ? { line: team("Catalyst couldn't check its Linear connection."), fix: `check that Linear is connected at ${base}/settings/connections` }
264
+ ? { line: team("Catalyst couldn't check its Linear connection."), fix: "run catalyst onboard to check the Linear connection" }
262
265
  : {
263
266
  line: team("Catalyst's Linear connection is missing a permission it needs."),
264
- fix: `reconnect Linear at ${base}/settings/connections?reauthorize=linear`,
267
+ fix: "run catalyst onboard and have an owner or admin approve the Linear link it prints",
265
268
  };
266
269
  case "token_live":
267
270
  return unknown
268
271
  ? { line: team("Catalyst couldn't reach Linear to check its connection."), fix: AGAIN }
269
272
  : {
270
273
  line: team("Linear stopped accepting Catalyst's connection. It was revoked or expired."),
271
- fix: `reconnect Linear at ${base}/settings/connections`,
274
+ fix: "run catalyst onboard and have an owner or admin approve the Linear link it prints",
272
275
  };
273
276
  case "team_visible":
274
277
  return unknown
@@ -285,16 +288,16 @@ export function teamCheckCopy(baseUrl, key, id, state, valuesFix) {
285
288
  if (id === "mapped_states_exist")
286
289
  return {
287
290
  line: team(`A stage Catalyst uses in ${key} was deleted or renamed in Linear.`),
288
- fix: `pick its replacement at ${teamPage(baseUrl, key, "map")}`,
291
+ fix: `pick its replacement with catalyst team map ${key}`,
289
292
  };
290
293
  if (id === "mapping_total")
291
294
  return {
292
295
  line: team(`${key}'s stages aren't all mapped yet, so Catalyst doesn't know where to move its tickets.`),
293
- fix: `map them at ${teamPage(baseUrl, key, "map")}, or set up Catalyst's workflow at ${teamPage(baseUrl, key, "adopt")}`,
296
+ fix: `map them with catalyst team map ${key}, or set up Catalyst's workflow with catalyst team adopt ${key}`,
294
297
  };
295
298
  return {
296
299
  line: team(`A stage mapped in ${key} is the wrong kind, for example a Done stage that Linear treats as in progress.`),
297
- fix: `choose the right stages at ${teamPage(baseUrl, key, "map")}`,
300
+ fix: `choose the right stages with catalyst team map ${key}`,
298
301
  };
299
302
  case "required_values":
300
303
  return unknown
@@ -304,22 +307,25 @@ export function teamCheckCopy(baseUrl, key, id, state, valuesFix) {
304
307
  }
305
308
  : {
306
309
  line: team(`A variable ${key}'s repository needs has no value, so Catalyst can't run its work.`),
307
- fix: valuesFix ?? `add the missing values on the repository's Environment page: open ${base}/settings/projects, then the project, then the repository`,
310
+ fix: valuesFix ?? setValuesFix(["the missing values"]),
308
311
  };
309
312
  case "coding_account_enrolled":
310
313
  return unknown
311
314
  ? { line: team("Catalyst couldn't check your AI accounts."), fix: AGAIN }
312
315
  : {
313
316
  line: team("Catalyst has no AI account to do the work with."),
314
- fix: `run catalyst setup to add one, or add it at ${base}/settings/coding-accounts`,
317
+ fix: `an owner or admin adds one at ${base}/settings/coding-accounts; there is no command for that yet`,
315
318
  };
316
319
  }
317
320
  const name = CHECK_NAMES[id] ?? id.replaceAll("_", " ");
318
- const page = CHECK_PAGES[id] ?? `/settings/linear-teams/${encodeURIComponent(key)}`;
321
+ const page = CHECK_PAGES[id];
322
+ const command = CHECK_COMMANDS[id]?.(key);
319
323
  return unknown
320
324
  ? { line: team(`Catalyst hasn't checked ${name} yet.`), fix: AGAIN }
321
325
  : {
322
326
  line: team(`${name.charAt(0).toUpperCase()}${name.slice(1)} ${PLURAL.has(id) ? "need" : "needs"} attention.`),
323
- fix: `open ${base}${page}`,
327
+ fix: command
328
+ ? page ? `${command}, or open ${base}${page}` : command
329
+ : page ? `open ${base}${page}` : `run catalyst team check ${key} to see why`,
324
330
  };
325
331
  }
package/dist/ready.js CHANGED
@@ -15,7 +15,7 @@ import { FIRST_STAMPED_VERSION } from "./skill-shape.js";
15
15
  import { installedBundleVersion } from "./skills.js";
16
16
  import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
17
17
  import { selectedOnboardTeam } from "./onboard-existing.js";
18
- import { dispatchGateCopy, teamCheckCopy, teamPage, valuesFix, whoFixes } from "./ready-copy.js";
18
+ import { dispatchGateCopy, teamCheckCopy, valuesFix, whoFixes } from "./ready-copy.js";
19
19
  import { onboardStatePath, readOnboardJournal } from "./onboard.js";
20
20
  export { semverOlder };
21
21
  /** `ready`'s own wording for the replica. It never recommends adopting the replica — until the
@@ -393,7 +393,7 @@ export async function readyReport(ctx, deps) {
393
393
  line: team.readiness.status === "degraded"
394
394
  ? `team ${label}: works, with some optional settings still missing`
395
395
  : `team ${label}: not ready for work yet`,
396
- fix: `open ${teamPage(base, label)}`,
396
+ fix: `run catalyst team check ${label} to see what is missing`,
397
397
  who: whoFixes("mapping_total"),
398
398
  });
399
399
  }
package/dist/secret.js CHANGED
@@ -36,7 +36,7 @@ const IMPORT_REASONS = {
36
36
  name_taken_by_env_var: "already a plain environment variable; a name is a secret or a variable, never both",
37
37
  invalid_name: "not an env-style name (A-Z, 0-9 and _, starting with a letter)",
38
38
  invalid_value: "the value is empty",
39
- config_rotation_unconfirmed: "the existing entry is a readable config value; change it in the app",
39
+ config_rotation_unconfirmed: "the existing entry is a plain variable; change it with catalyst var set",
40
40
  probe_refused: "the value failed this kind's check",
41
41
  };
42
42
  /** A command's stdout, minus the one trailing newline almost every command prints after a value. */
@@ -74,7 +74,7 @@ function refusalLine(status, body, repo) {
74
74
  return "refused (401): the cloud did not accept your credential here. If `catalyst me` works, this cloud predates secret writes from the CLI and needs a newer release; otherwise log in again.";
75
75
  }
76
76
  if (status === 404)
77
- return `refused (404): ${repo} is not a repository of this tenant (register it in the app first)`;
77
+ return `refused (404): ${repo} is not a repository of this workspace. Register it first with catalyst onboard --team <KEY> --repo ${repo}`;
78
78
  if (status === 409 && body.error === "registry_not_migrated") {
79
79
  return "refused (409): the cloud's secret audit is not set up yet, so nothing was stored. Tell your Catalyst operator.";
80
80
  }
@@ -508,9 +508,7 @@ function action(step, base, journal) {
508
508
  case "linear.adopt": {
509
509
  const team = journal?.steps.find((row) => row.id === "linear.team" && row.state === "done");
510
510
  const key = team && value(team, "teamKey");
511
- return link(key
512
- ? `/settings/linear-teams/${encodeURIComponent(key)}/adopt`
513
- : "/settings/linear-teams", "set up the team's Catalyst workflow");
511
+ return `Run catalyst team adopt ${key || "<TEAM KEY>"} to preview the team's Catalyst workflow, then run it again with the --yes --plan-hash value it prints.`;
514
512
  }
515
513
  case "linear.automations":
516
514
  return "Open your Linear team settings and set each pull request automation to No action.";
@@ -572,17 +570,20 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
572
570
  : undefined
573
571
  : clean(onboardReasonText(readiness, { baseUrl: base, journal }))
574
572
  : undefined;
573
+ const machineOnly = journal.steps.some(step => step.reason === "returning_workspace_move_on");
575
574
  const heading = paused
576
575
  ? "Setup paused"
577
576
  : journal.steps.some((step) => step.id === "runner" &&
578
577
  (step.evidence?.selected === true || required.has(step.id)) &&
579
578
  step.state !== "done")
580
579
  ? "Not ready for work yet"
581
- : journal.exit === 0 && journal.complete
582
- ? "Setup complete"
583
- : journal.exit === 0
584
- ? "Ready for work"
585
- : "Not ready for work yet";
580
+ : journal.exit === 0 && machineOnly
581
+ ? "This computer is set up"
582
+ : journal.exit === 0 && journal.complete
583
+ ? "Setup complete"
584
+ : journal.exit === 0
585
+ ? "Ready for work"
586
+ : "Not ready for work yet";
586
587
  const firstTicket = journal.steps.find((s) => s.id === "first-ticket");
587
588
  const ticket = firstTicket?.state === "done" ? firstTicket : undefined;
588
589
  const key = ticket && (value(ticket, "ticket") ?? value(ticket, "ticketKey"));
@@ -590,25 +591,27 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
590
591
  const ticketUrl = savedTicketUrl && page(base, savedTicketUrl);
591
592
  const next = paused
592
593
  ? "Next: run catalyst onboard to continue."
593
- : key
594
- ? `Follow ${ticketUrl ?? key} in Linear; Catalyst comments there as each phase finishes.`
595
- : heading === "Setup complete"
596
- ? `Next: move a ticket to ${dispatchStageName(firstTicket?.evidence?.stage)} in Linear.`
597
- : journal.exit === 0 && personal[0]
598
- ? `Next: ${personal[0].text[0].toLowerCase() + personal[0].text.slice(1)}`
599
- : personal.length === 1
600
- ? "Next: run catalyst onboard after you finish 1."
601
- : personal.length === 2
602
- ? "Next: run catalyst onboard after you finish 1 and 2."
603
- : personal.length
604
- ? "Next: run catalyst onboard after you finish 1 to " +
605
- personal.length +
606
- "."
607
- : actions.length
608
- ? "Next: nothing for you to do. catalyst ready shows when Catalyst has read the settings."
609
- : readyStep?.reason === "member_team_required"
610
- ? "Next: run catalyst onboard --team <KEY> to check again."
611
- : "Next: run catalyst onboard to check again.";
594
+ : machineOnly && journal.exit === 0
595
+ ? "Next: run catalyst ready to review your existing projects."
596
+ : key
597
+ ? `Follow ${ticketUrl ?? key} in Linear; Catalyst comments there as each phase finishes.`
598
+ : heading === "Setup complete"
599
+ ? `Next: move a ticket to ${dispatchStageName(firstTicket?.evidence?.stage)} in Linear.`
600
+ : journal.exit === 0 && personal[0]
601
+ ? `Next: ${personal[0].text[0].toLowerCase() + personal[0].text.slice(1)}`
602
+ : personal.length === 1
603
+ ? "Next: run catalyst onboard after you finish 1."
604
+ : personal.length === 2
605
+ ? "Next: run catalyst onboard after you finish 1 and 2."
606
+ : personal.length
607
+ ? "Next: run catalyst onboard after you finish 1 to " +
608
+ personal.length +
609
+ "."
610
+ : actions.length
611
+ ? "Next: nothing for you to do. catalyst ready shows when Catalyst has read the settings."
612
+ : readyStep?.reason === "member_team_required"
613
+ ? "Next: run catalyst onboard --team <KEY> to check again."
614
+ : "Next: run catalyst onboard to check again.";
612
615
  return {
613
616
  heading,
614
617
  readinessCause,
@@ -688,11 +691,13 @@ export function onboardJsonView(journal, base, paused = false, options = {}) {
688
691
  ? "paused"
689
692
  : journal.exit === 10
690
693
  ? "failed"
691
- : screen.heading === "Setup complete"
692
- ? "complete"
693
- : screen.heading === "Ready for work"
694
- ? "ready"
695
- : "not-ready";
694
+ : screen.heading === "This computer is set up"
695
+ ? "machine-complete"
696
+ : screen.heading === "Setup complete"
697
+ ? "complete"
698
+ : screen.heading === "Ready for work"
699
+ ? "ready"
700
+ : "not-ready";
696
701
  const actions = screen.actions.map((action) => {
697
702
  const step = view.steps.find((step) => step.id === action.id);
698
703
  const role = view.steps.find((step) => step.id === "signin" && step.state === "done")?.evidence?.role;
package/dist/setup.js CHANGED
@@ -89,6 +89,10 @@ export function engineFlags(args) {
89
89
  seen.verbose = true;
90
90
  if (a === "--with-replica")
91
91
  seen.localSync = true;
92
+ if (a === "--runner")
93
+ seen.runner = true;
94
+ if (a === "--no-runner")
95
+ seen.runner = false;
92
96
  }
93
97
  return seen;
94
98
  }
package/dist/transport.js CHANGED
@@ -112,6 +112,22 @@ export class ApiClient {
112
112
  await this.refuseIfNotOk(res, `PUT ${path}`);
113
113
  return { status: res.status, body: await this.parseBody(res, `PUT ${path}`), headers: res.headers };
114
114
  }
115
+ async deleteJson(path, opts = {}) {
116
+ const res = await this.send(this.url(path), {
117
+ method: "DELETE",
118
+ headers: {
119
+ authorization: `Bearer ${await this.credential()}`,
120
+ accept: "application/json",
121
+ },
122
+ });
123
+ if (!opts.accept?.includes(res.status))
124
+ await this.refuseIfNotOk(res, `DELETE ${path}`);
125
+ return {
126
+ status: res.status,
127
+ body: await this.parseBody(res, `DELETE ${path}`),
128
+ headers: res.headers,
129
+ };
130
+ }
115
131
  async send(url, init) {
116
132
  try {
117
133
  return await this.fetchImpl(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
@@ -142,7 +158,7 @@ export class ApiClient {
142
158
  reason = raw.slice(0, 200);
143
159
  }
144
160
  if (res.status === 401) {
145
- throw new CliError(`${what} failed (401): credential not accepted — mint a personal key at Settings → API keys and log in again`, "unauthorized", 2, 401);
161
+ throw new CliError(`${what} failed (401): credential not accepted. Run catalyst login to sign in again; an unattended machine needs a new personal key from Settings → API keys`, "unauthorized", 2, 401);
146
162
  }
147
163
  if (res.status === 403) {
148
164
  throw new CliError(`${what} failed (403): this key may not reach that route${reason ? ` (${reason})` : ""} — a route that acts as a host (claiming work, publishing artifacts) needs the tenant's account key; every read and write a person makes takes your personal key`, "forbidden", 2, 403);
@@ -183,7 +199,7 @@ export async function fetchMe(baseUrl, key, fetchImpl, timeoutMs = REQUEST_TIMEO
183
199
  reason = "";
184
200
  }
185
201
  const detail = res.status === 401
186
- ? "credential not accepted — mint a personal key at Settings → API keys and log in again"
202
+ ? "credential not accepted. Run catalyst login to sign in again; an unattended machine needs a new personal key from Settings → API keys"
187
203
  : reason || `HTTP ${res.status}`;
188
204
  throw new MeError(`GET /me failed (${res.status}): ${detail}`, "http", res.status);
189
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/cli",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Set up Catalyst Cloud, check your tickets and manage your account from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,13 +53,13 @@
53
53
  "prepublishOnly": "node scripts/release-train-check.mjs --publish cli"
54
54
  },
55
55
  "dependencies": {
56
- "@catalyst-cloud/sdk": "0.16.0",
56
+ "@catalyst-cloud/sdk": "0.16.1",
57
57
  "@clack/prompts": "1.8.1",
58
58
  "fast-string-width": "3.0.2",
59
59
  "fast-wrap-ansi": "0.2.2",
60
60
  "smol-toml": "^1.7.1",
61
61
  "yaml": "^2.9.0",
62
- "@catalyst-cloud/sdk-replica-node": "0.16.0"
62
+ "@catalyst-cloud/sdk-replica-node": "0.16.1"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@catalyst-cloud/schema": "0.16.0",
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.2 — written in this repository for customer accounts -->
8
8
 
9
9
  # Catalyst's GitHub
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's view of Linear on the customer's own cloud account. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the cloud API by default, with local replica reads available as an explicit opt-in, always naming the source; searches tickets, PRs, projects and initiatives; writes comments, card moves, labels and new tickets through the account's agent proxy as the app actor, with every route, stage id, label id and marker read from the account's contract. Knows what a ticket accumulates as Catalyst works it (phase-outcome comments, the document per phase, the agent session, the labels, the single blocks relation, the bookkeeping marker, the eyes acknowledgement). Use when a person says "show me the ticket", "what did Catalyst write on it", "comment on it", "move it", "label it" or "file a ticket". Not for raising a decision for a human (what-needs-me) and not for pull requests (catalyst-github).
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.2 — written in this repository for customer accounts -->
8
8
 
9
9
  # Catalyst Linear
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Set up Catalyst Cloud and say whether setup is working. Walks a person from nothing to their first ticket running, one step at a time: the coding account, the project, the integrations and connected accounts, each repository's settings file, then one real ticket moving. Also logs this machine in, reads the readiness verdict with who can fix each failure, and checks the optional local replica. Use when someone says "set me up", "onboard me", "I just signed up", "get me started", "what do I do first", "log me in", "which account is this machine on", "am I set up", "what is missing", "why does nothing happen" or "is the replica running", when a login expired, or when a skill script exits 2 saying this machine is not connected. Does every step a key can do through the catalyst CLI, hands over the exact page for the steps only a browser can do, and never claims a step it did not watch succeed.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.2 — written in this repository for customer accounts -->
8
8
 
9
9
  # Onboard me
10
10
 
@@ -36,7 +36,7 @@ Default invited-member setup handles personal accounts and leaves workspace admi
36
36
 
37
37
  1. Run `node scripts/where-am-i.mjs --next` and read its one line; trust it over your memory of the last turn. A `note:` after it is worth one clause.
38
38
  2. Say what the step is for.
39
- 3. Ask one question or do one thing. Run a `do:` command a key can run and show its output; hand over a browser step as `references/what-the-browser-owns.md` says.
39
+ 3. Ask one question or do one thing. Offer to run every step a command can do, run it on a yes and show its output; hand over a browser step only as `references/what-the-browser-owns.md` says.
40
40
  4. Wait for the answer. Two or three short sentences a turn; the full report is for you, not to paste.
41
41
  5. Re-run the script and read back the part that should have changed. Still unchanged after one `catalyst contract --refresh`: report both what the page said and what the instrument says.
42
42
 
@@ -55,7 +55,7 @@ Scripts are run, never read; each prints `--help`.
55
55
  | logging in, a refused or expired login, which account this is, the Linear identity | `references/connecting-this-machine.md` |
56
56
  | anything reports not ready, a check id, or who fixes something | `references/reading-ready.md` |
57
57
  | the coding account or the `host` part, or before saying work can run | `references/what-a-phase-needs.md`; a credential to replace, `references/replacing-a-credential.md` |
58
- | a browser step, or confirming a page that said it worked | `references/what-the-browser-owns.md` |
58
+ | which steps a command does and which need a browser, or confirming a page that said it worked | `references/what-the-browser-owns.md` |
59
59
  | the repository's `.catalyst/catalyst.toml` and its approval; its AGENTS.md block and agent layout | `references/declaring-a-repository.md`; `references/repository-agent-setup.md` |
60
60
  | the replica or event cache | `references/local-sync.md` |
61
61
  | local SQL, only when `catalyst replica status --json` reports `configured: true` | `references/local-replica.md` |
@@ -8,7 +8,7 @@
8
8
 
9
9
  Keyless is the preferred rail: `catalyst login` with no key prints a short code and a URL the person approves in a browser, or from a phone. Ask first, and run it once per code. The session refreshes silently, so they stay connected for months.
10
10
 
11
- A **personal key** is the fallback for a script or an unattended shell. Any active member mints one at Settings → API keys. The environment form keeps it out of shell history:
11
+ A **personal key** is the fallback for a script or an unattended shell, where nobody can approve a code. Offer `catalyst login` first; a key is minted only in the browser, so for an unattended machine any active member mints one at Settings → API keys. The environment form keeps it out of shell history:
12
12
 
13
13
  ```sh
14
14
  CATALYST_CLOUD_TOKEN=<your-personal-key> catalyst login
@@ -18,7 +18,7 @@ CATALYST_CLOUD_TOKEN=<your-personal-key> catalyst login
18
18
 
19
19
  ## An unmatched Linear identity
20
20
 
21
- An unmatched Linear identity is the person's to match: `catalyst identity linear status`, then `catalyst identity linear options`; they pick their own listed identity and you run `catalyst identity linear set <linearUserId>`. An identity someone else claimed, and an inactive seat, need an owner or admin at Settings → Members. Connecting Linear personally does not set the identity: the identity and the personal grant are two different stored facts. Until the identity is matched, "what needs me" shows everyone's asks.
21
+ An unmatched Linear identity is the person's to match: `catalyst identity linear status`, then `catalyst identity linear options`; they pick their own listed identity and you run `catalyst identity linear set <linearUserId>`. An identity someone else claimed, and an inactive seat, need an owner or admin at Settings → Members; no command does this yet, so raise one ask to them. Connecting Linear personally does not set the identity: the identity and the personal grant are two different stored facts. Until the identity is matched, "what needs me" shows everyone's asks.
22
22
 
23
23
  ## What login writes
24
24
 
@@ -27,9 +27,9 @@ An unmatched Linear identity is the person's to match: `catalyst identity linear
27
27
  ## When login fails
28
28
 
29
29
  - **"Your login expired or was revoked."** One fresh `catalyst login`.
30
- - **A 401 on the key rail.** A stale, mistyped or revoked key: mint a new one at Settings → API keys.
30
+ - **A 401 on the key rail.** A stale, mistyped or revoked key: offer a keyless `catalyst login` instead; an unattended machine needs a new key from Settings → API keys.
31
31
  - **A 403 on the contract naming an older cloud.** The cloud lacks personal-key access: update the cloud, or use the account key until then.
32
32
  - **A line naming two contract versions.** `npm install -g @catalyst-cloud/cli@latest && catalyst login` before using the other skills.
33
- - **A refusal naming `jwt-no-membership`.** The cloud learns of a person at their first sign-in to the app. Have them sign in once, then log in again. If it repeats, their seat is inactive and an owner or admin activates it.
33
+ - **A refusal naming `jwt-no-membership`.** The cloud learns of a person at their first sign-in to the app. Hand over the app's sign-in link once, then offer to run `catalyst login` again. If it repeats, their seat is inactive and an owner or admin activates it.
34
34
 
35
35
  Retry a failed login once, by hand, after the fix. A key goes into the login command or `CATALYST_CLOUD_TOKEN` and nowhere else: never a ticket, a transcript, or a file you write. After a bundle update, the next command prints a one-line notice; read it to the person if they are watching.
@@ -14,7 +14,7 @@ The assistant prepares `.catalyst/catalyst.toml` from the repository. Never ask
14
14
 
15
15
  Show a short preview with the project/team, setup and verify commands, variable names, which are marked secret, and confirmation that every variable is optional. Do not include values. Ask the person to approve this exact draft. Before approval, do not write or replace `.catalyst/catalyst.toml`, remove `catalyst.env.json`, or open a settings PR.
16
16
 
17
- After approval, write the validated file. When converting a legacy-only repository, remove `catalyst.env.json` in the same change. Open the settings PR, then offer to watch it merge. A reviewed merge is the approval. Catalyst approves the exact merged revision, with no second step, when someone other than the PR's author approved the merged head commit, or when a workspace owner or admin merged the PR. Tell the person this before they merge: the approving review is their consent to the setup commands and secret names in the file, so a reviewer should read it like code. A direct push, an unreviewed merge, or an approval left before the last commit leaves the revision proposed. Only then give an owner or admin a direct link to approve it at Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision. The direct route is `/settings/projects/$projectId/repositories/$repoId/environment/declaration`; use the project and repository ids from Catalyst instead of guessing them.
17
+ After approval, write the validated file. When converting a legacy-only repository, remove `catalyst.env.json` in the same change. Open the settings PR, then offer to watch it merge. A reviewed merge is the approval. Catalyst approves the exact merged revision, with no second step, when someone other than the PR's author approved the merged head commit, or when a workspace owner or admin merged the PR. Tell the person this before they merge: the approving review is their consent to the setup commands and secret names in the file, so a reviewer should read it like code. A direct push, an unreviewed merge, or an approval left before the last commit leaves the revision proposed. No command approves one repository's revision yet, so only then give an owner or admin a direct link to approve it at Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision. The direct route is `/settings/projects/$projectId/repositories/$repoId/environment/declaration`; use the project and repository ids from Catalyst instead of guessing them.
18
18
 
19
19
  ## File shape
20
20