@catalyst-cloud/cli 0.15.6 → 0.16.1

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 (54) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +9 -6
  3. package/dist/args.js +26 -1
  4. package/dist/ask.js +56 -0
  5. package/dist/capabilities.js +3 -1
  6. package/dist/cli.js +10 -5
  7. package/dist/environment.js +1 -1
  8. package/dist/execution.js +2 -2
  9. package/dist/hosts.js +89 -0
  10. package/dist/legacy.js +12 -2
  11. package/dist/machine-inventory.js +37 -0
  12. package/dist/onboard-capacity.js +5 -6
  13. package/dist/onboard-github-copy.js +2 -0
  14. package/dist/onboard-github.js +2 -0
  15. package/dist/onboard-headless.js +2 -2
  16. package/dist/onboard-next.js +21 -12
  17. package/dist/onboard-project-cleanup.js +100 -0
  18. package/dist/onboard-returning.js +172 -0
  19. package/dist/onboard-runner.js +149 -34
  20. package/dist/onboard-runtime.js +26 -1
  21. package/dist/onboard-ui.js +67 -1
  22. package/dist/onboard-workflow.js +32 -7
  23. package/dist/onboard.js +9 -2
  24. package/dist/ready-copy.js +30 -24
  25. package/dist/ready.js +2 -2
  26. package/dist/secret.js +2 -2
  27. package/dist/setup-onboard-copy.js +37 -32
  28. package/dist/setup.js +4 -0
  29. package/dist/transport.js +18 -2
  30. package/package.json +8 -5
  31. package/skills/catalyst-github/SKILL.md +1 -1
  32. package/skills/catalyst-linear/SKILL.md +1 -1
  33. package/skills/catalyst-onboard/SKILL.md +3 -3
  34. package/skills/catalyst-onboard/references/connecting-this-machine.md +4 -4
  35. package/skills/catalyst-onboard/references/declaring-a-repository.md +1 -1
  36. package/skills/catalyst-onboard/references/reading-ready.md +14 -14
  37. package/skills/catalyst-onboard/references/replacing-a-credential.md +1 -1
  38. package/skills/catalyst-onboard/references/the-one-path.md +5 -5
  39. package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -4
  40. package/skills/catalyst-onboard/references/what-the-browser-owns.md +20 -10
  41. package/skills/catalyst-onboard/scripts/where-am-i.mjs +50 -27
  42. package/skills/run-this-project/SKILL.md +1 -1
  43. package/skills/run-this-project/references/stalls-and-escalation.md +1 -1
  44. package/skills/unstick/SKILL.md +1 -1
  45. package/skills/what-needs-me/SKILL.md +2 -2
  46. package/skills/what-needs-me/references/raising-a-decision.md +12 -1
  47. package/skills/what-needs-me/references/reading-the-inbox.md +1 -1
  48. package/skills/what-needs-me/scripts/raise.mjs +10 -1
  49. package/skills/what-this-repo-needs/SKILL.md +1 -1
  50. package/skills/whats-happening/SKILL.md +3 -2
  51. package/skills/whats-happening/references/coding-accounts.md +1 -1
  52. package/skills/whats-happening/references/machines.md +9 -0
  53. package/skills/whats-happening/references/settings-and-where-they-live.md +8 -8
  54. package/skills/whats-happening/references/stages-and-mapping.md +1 -1
@@ -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.15.6",
3
+ "version": "0.16.1",
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",
@@ -48,18 +48,21 @@
48
48
  "paths:check": "node scripts/vendor-paths.mjs --check",
49
49
  "prose:check": "node scripts/check-public-prose.mjs",
50
50
  "settings:check": "node scripts/vendor-settings.mjs --check",
51
- "self-host:check": "node scripts/vendor-self-host.mjs --check"
51
+ "self-host:check": "node scripts/vendor-self-host.mjs --check",
52
+ "release:check": "node scripts/release-train-check.mjs && node --test scripts/release-train-check.test.mjs",
53
+ "prepublishOnly": "node scripts/release-train-check.mjs --publish cli"
52
54
  },
53
55
  "dependencies": {
54
- "@catalyst-cloud/sdk": "0.14.1",
56
+ "@catalyst-cloud/sdk": "0.16.1",
55
57
  "@clack/prompts": "1.8.1",
56
58
  "fast-string-width": "3.0.2",
57
59
  "fast-wrap-ansi": "0.2.2",
58
60
  "smol-toml": "^1.7.1",
59
- "yaml": "^2.9.0"
61
+ "yaml": "^2.9.0",
62
+ "@catalyst-cloud/sdk-replica-node": "0.16.1"
60
63
  },
61
64
  "devDependencies": {
62
- "@catalyst-cloud/schema": "0.13.1",
65
+ "@catalyst-cloud/schema": "0.16.0",
63
66
  "@types/node": "^26.3.0",
64
67
  "@vitest/coverage-v8": "^4.1.11",
65
68
  "typescript": "^7.0.2",
@@ -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.15.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.1 — 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.15.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.1 — 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.15.6 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.1 — 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. Once merged, give the owner or admin a direct link to approve the new declaration revision 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
 
@@ -7,7 +7,7 @@ Read this before telling a person something is not ready or what to do about it.
7
7
  | part | instrument | a pass does not prove |
8
8
  | -- | -- | -- |
9
9
  | **machine** | `catalyst status`, and the machine checks of `ready` | anything about the cloud account |
10
- | **person** | `catalyst me`, `catalyst connections personal <linear|github> status` | an active seat (an owner or admin fixes it in Settings → Members) |
10
+ | **person** | `catalyst me`, `catalyst connections personal <linear|github> status` | an active seat (an owner or admin fixes it in Settings → Members; no command does this yet, so raise one ask to them) |
11
11
  | **account** | `catalyst contract --path account` (a resolved Linear workspace) | that the GitHub App is installed |
12
12
  | **project** | `catalyst team list`, then `team check <KEY>` and the `team:` checks of `ready` | that an unchecked team is ready |
13
13
  | **repository** | `catalyst contract --path merge.repositories` | that it is active, attached to a project, or declared |
@@ -21,7 +21,7 @@ Read this before telling a person something is not ready or what to do about it.
21
21
  Split the verdict first. In `catalyst ready --json`, an id that begins with `team:` is a **project** finding; the rest are **machine** findings. Report the two groups separately, machine first, each with its owner, its `pass`, `fail` and `unknown` counts, and when each team verdict was computed (`teams[].readiness.checkedAt`, null before any pass).
22
22
 
23
23
  - **A machine check failed:** the person's, here, now. Do its `fix` line.
24
- - **A project check failed:** name the check, the project and the `who`. A mapping is repaired with `catalyst team check`, `team map` or `team adopt`; a Linear connection, permission or team setting in a browser. Re-running `ready` fixes nothing.
24
+ - **A project check failed:** name the check, the project and the `who`. Offer to run the repair: `catalyst team check`, `team map` or `team adopt` for a mapping, and `catalyst onboard` to start a Linear reconnection, whose consent link the owner or admin approves. Only Linear's own team access is a page in Linear. Re-running `ready` fixes nothing.
25
25
  - **A note** never moves the verdict; waiting is not failing, and unknown is not a pass.
26
26
 
27
27
  End every readiness answer with the verdict and the who-can-click-what list, in that order. Run each check once; waiting for one to clear is the project owner's watch (`run-this-project`).
@@ -34,20 +34,20 @@ A team is `ready`, `degraded`, `blocked` or `unchecked` (no pass yet, a note). S
34
34
 
35
35
  | check id | proves | when it fails | who clicks |
36
36
  | -- | -- | -- | -- |
37
- | `oauth_scope` | the Linear connection has the permissions it needs | re-authorise it | owner or admin, in settings |
38
- | `token_live` | the Linear connection is accepted now | reconnect if expired, revoked or never connected; re-check if Linear was unreachable | owner or admin |
39
- | `team_visible` | Catalyst can see the team | usually it went private: grant access in Linear | owner or admin, in Linear |
37
+ | `oauth_scope` | the Linear connection has the permissions it needs | re-authorise it: `catalyst onboard` starts it and prints the consent link | owner or admin, in their browser |
38
+ | `token_live` | the Linear connection is accepted now | reconnect with `catalyst onboard` if expired, revoked or never connected; `catalyst team check <KEY>` if Linear was unreachable | owner or admin |
39
+ | `team_visible` | Catalyst can see the team | usually it went private: grant access in Linear, then `catalyst team check <KEY>` | owner or admin, in Linear |
40
40
  | `mapped_states_exist` | every mapped stage still exists | `catalyst team map <KEY>`; `stages_pending_write` means a mapping was just saved, so wait | owner or admin |
41
41
  | `mapping_total` | every stage Catalyst moves tickets into is mapped | map them ("absent": never mapped) | owner or admin |
42
42
  | `types_compatible` | each load-bearing stage is the right kind (dispatch and intake unstarted or backlog, PR started, done completed, canceled canceled) | map a stage of the right kind | owner or admin |
43
43
  | `labels_present` | Catalyst's labels exist | none: they are created on first use | nobody |
44
44
  | `writes_land` | Catalyst has written to the team | `no_write_observed` clears on the first move; a refused write: check the connection and permissions | owner or admin when refused; otherwise nobody |
45
- | `webhook_covers_team` | the team's events arrive | `no_delivery_observed` clears once a registered repository's events flow | owner or admin, by registering the repository |
45
+ | `webhook_covers_team` | the team's events arrive | `no_delivery_observed` clears once a registered repository's events flow | owner or admin, by registering the repository: `catalyst onboard --team <KEY> --repo <owner/name>` |
46
46
  | `hosts_current` | no connected host runs an older mapping | `references/what-a-phase-needs.md`; `no_host_connected` is waiting | whoever runs that host |
47
- | `environment_declared` | the default repository's `.catalyst/catalyst.toml` is ingested, valid and approved | the reason names the step: register a default repository, commit or fix the file, or approve it (`references/declaring-a-repository.md`) | owner or admin, except committing the file, which is whoever can push |
48
- | `tools_resolvable` | every MCP server and CLI the declaration names resolves | `tool_reference_unresolved`: add the secret or fix the declaration; `toolchain_cli_missing` is not self-service | owner or admin, in the declaration or Settings → Environment |
49
- | `reviewer_required` | the repository can merge under its policy | a strict policy with no reviewer: configure one at Settings → Your projects → the project → Repositories → the repository → Code reviews, or relax the policy; only the merge waits | owner or admin, in settings |
50
- | `reviewer_configured` | a code reviewer is configured at all | configure one there; a fail never blocks or degrades | owner or admin, in settings; nobody is required when it fails |
47
+ | `environment_declared` | the default repository's `.catalyst/catalyst.toml` is ingested, valid and approved | the reason names the step: register a default repository, commit or fix the file through a reviewed pull request, or approve a revision that merged without one (`references/declaring-a-repository.md`) | owner or admin, except committing the file, which is whoever can push |
48
+ | `tools_resolvable` | every MCP server and CLI the declaration names resolves | `tool_reference_unresolved`: set the secret with `catalyst secret set NAME --repo <owner/name>`, or fix `.catalyst/catalyst.toml` in a pull request; `toolchain_cli_missing` is not self-service | owner or admin |
49
+ | `reviewer_required` | the repository can merge under its policy | a strict policy with no reviewer: relax the policy with `catalyst var set CATALYST_MERGE_EVIDENCE_POLICY --repo <owner/name>` (a policy declared in `.catalyst/catalyst.toml` wins over the variable, so change it there in a pull request), or configure a reviewer at Settings → Your projects → the project → Repositories → the repository → Code reviews (no command for that yet); only the merge waits | owner or admin |
50
+ | `reviewer_configured` | a code reviewer is configured at all | configure one there; a fail never blocks or degrades | owner or admin; nobody is required when it fails |
51
51
 
52
52
  ## The machine checks the CLI adds
53
53
 
@@ -70,10 +70,10 @@ Each failing line prints its own fix.
70
70
 
71
71
  A team receives work once its stages are saved. Saving one team changes no other team's stages or tickets; only the labels Adopt creates are shared. Once saved, tickets in its dispatch stage start, so pilot on a low-stakes team and first move anything there that should wait back to Backlog. Nothing reads `gitAutomation` in the contract.
72
72
 
73
- Each action exists in the terminal and on `<their cloud>/settings/linear-teams`:
73
+ Each action is a command; offer to run it for an owner or admin:
74
74
 
75
- - **Check** (`catalyst team check <KEY>`, Re-check) saves the team's verdict, changing no ticket or mapping, and files one setup ticket for an admin if something is missing.
76
- - **Map** (`catalyst team map <KEY>`, Map my stages) saves a mapping from the team's existing stages and creates nothing in Linear.
77
- - **Adopt** (`catalyst team adopt <KEY>`, Adopt the Catalyst workflow) creates the missing stages and Catalyst's labels with the admin's own Linear authorisation, including the hold label a failed phase uses when the team has no remediate stage (`teams[].labels.hold`). The label alone means nothing failed.
75
+ - **Check** (`catalyst team check <KEY>`) saves the team's verdict, changing no ticket or mapping, and files one setup ticket for an admin if something is missing.
76
+ - **Map** (`catalyst team map <KEY>`) saves a mapping from the team's existing stages and creates nothing in Linear.
77
+ - **Adopt** (`catalyst team adopt <KEY>`) creates the missing stages and Catalyst's labels with the admin's own Linear authorisation, including the hold label a failed phase uses when the team has no remediate stage (`teams[].labels.hold`). The label alone means nothing failed.
78
78
 
79
79
  Map when the team's stages already cover the work, Adopt when they do not.
@@ -1,6 +1,6 @@
1
1
  # Replacing an AI account's credential
2
2
 
3
- When the script prints "<provider> account <slot> needs a new credential" (quarantined, expired, revoked, or three or more polls failed on the credential), replace that account's credential on its own page. Do not enroll a second account. It would be a duplicate beside the dead one. A workspace owner or admin does it in the browser, from the link the script prints; the person copies and pastes the credential, and you never read, print or paste it.
3
+ When the script prints "<provider> account <slot> needs a new credential" (quarantined, expired, revoked, or three or more polls failed on the credential), replace that account's credential on its own page. Do not enroll a second account. It would be a duplicate beside the dead one. No command replaces a credential yet, so a workspace owner or admin does it in the browser, from the link the script prints; the person copies and pastes the credential, and you never read, print or paste it.
4
4
 
5
5
  ## The steps
6
6
 
@@ -1,6 +1,6 @@
1
1
  # The one path
2
2
 
3
- The steps run in the order a person can act on them, each the cheapest place to catch what the next would hide. The script prints each step's owner and link; this page adds why it matters, what to ask, and its trap. Browser handovers follow `references/what-the-browser-owns.md`.
3
+ The steps run in the order a person can act on them, each the cheapest place to catch what the next would hide. The script prints each step's owner and link; this page adds why it matters, what to ask, and its trap. Offer to run each step a command can do; browser handovers follow `references/what-the-browser-owns.md`.
4
4
 
5
5
  ## 0. Where are we
6
6
 
@@ -20,7 +20,7 @@ Each stage of a ticket runs on an account the person owns, at their own rate, un
20
20
 
21
21
  ## 3. Connect the Linear integration
22
22
 
23
- The one Linear connection the whole workspace shares, made once by an owner or admin on `<their cloud>/settings/connections`; no project can be read without it.
23
+ The one Linear connection the whole workspace shares, made once by an owner or admin; no project can be read without it. Offer to start it: `catalyst onboard` asks Linear for the consent link and prints it, and the owner or admin approves it in their browser. Without that link, the page is `<their cloud>/settings/connections`.
24
24
 
25
25
  ## 4. Pick one project, and map its stages
26
26
 
@@ -28,15 +28,15 @@ A card moved into an unmapped project does nothing, the most common reason a new
28
28
 
29
29
  `catalyst team list` shows the teams without checking readiness; the person picks one key. `catalyst team map <KEY>` or `catalyst team adopt <KEY>` prints a preview to approve before `--yes --plan-hash <hash>` (exit 3 applied nothing). Run `catalyst team check <KEY>` only for the selected team. `team migrate <KEY>`, its `--retire`, and `team adopt <KEY> --undo` each preview and need their own approval. `catalyst capabilities` says which verbs this cloud serves.
30
30
 
31
- A `blocked` project is not set up. Change Linear's own Git automation rules in Linear for now (Settings → Teams → the team → Workflow → Workflows & automations → Pull request and commit automations → No action); hand that over, then re-check.
31
+ A `blocked` project is not set up. There is no command for Linear's own Git automation rules yet, so hand over the page in Linear (Settings → Teams → the team → Workflow → Workflows & automations → Pull request and commit automations → No action), then offer to run `catalyst team check <KEY>`.
32
32
 
33
33
  ## 5. Install the GitHub App
34
34
 
35
- Without it Catalyst cannot touch code. On the same page, the owner or admin installs the App and grants it the repository plus `<org>/thoughts`, where cloud phases write their notes (`<org>` owns the code repository). If it does not exist, they create a private repository named `thoughts`, initialized with a README, then on the App's installation page for that org choose All repositories or add `thoughts`. The script notes whether `gh` can see that repository, never that the App can reach it. Step 6 succeeding is the proof the App is installed.
35
+ Without it Catalyst cannot touch code. Offer to start the install: `catalyst onboard` prints the App's install link, and the owner or admin grants it the repository plus `<org>/thoughts` on GitHub, where cloud phases write their notes (`<org>` owns the code repository). If `thoughts` does not exist, offer to create it as a private repository named `thoughts`, initialized with a README (`gh repo create <org>/thoughts --private --add-readme`); then on the App's installation page for that org they choose All repositories or add `thoughts`. The script notes whether `gh` can see that repository, never that the App can reach it. Step 6 succeeding is the proof the App is installed.
36
36
 
37
37
  ## 6. Register the repository
38
38
 
39
- On `<their cloud>/settings/projects`, add the repository **and attach it to the project from step 4** in the same form. A repository registered without a project is listed and never receives work.
39
+ Offer to register the repository **and attach it to the project from step 4** in one command: `catalyst onboard --team <KEY> --repo <owner/name>` (an owner or admin). A repository registered without a project is listed and never receives work.
40
40
 
41
41
  ## 7. Connect your own accounts
42
42