@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.
- package/CHANGELOG.md +15 -0
- package/README.md +13 -4
- package/dist/args.js +1 -1
- package/dist/capabilities.js +3 -1
- package/dist/cli.js +25 -30
- package/dist/consent-browser.js +3 -1
- package/dist/oauth.js +1 -1
- package/dist/onboard-accounts.js +67 -171
- package/dist/onboard-checklist.js +309 -0
- package/dist/onboard-existing.js +1 -1
- package/dist/onboard-first-ticket.js +199 -0
- package/dist/onboard-github.js +13 -1
- package/dist/onboard-headless.js +6 -4
- package/dist/onboard-housekeeping.js +93 -0
- package/dist/onboard-next.js +124 -13
- package/dist/onboard-ready.js +61 -0
- package/dist/onboard-runtime.js +117 -26
- package/dist/onboard-standalone-copy.js +73 -66
- package/dist/onboard-ui.js +189 -81
- package/dist/onboard-values.js +47 -0
- package/dist/onboard.js +59 -18
- package/dist/ready-copy.js +325 -0
- package/dist/ready.js +35 -98
- package/dist/setup-onboard-copy.js +262 -41
- package/dist/setup-render.js +73 -14
- package/dist/setup.js +6 -3
- package/package.json +2 -2
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +3 -3
- package/skills/catalyst-onboard/references/replacing-a-credential.md +9 -15
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -11
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +3 -2
- package/skills/run-this-project/SKILL.md +2 -2
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +2 -2
- package/skills/whats-happening/references/coding-accounts.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +1 -1
package/dist/onboard.js
CHANGED
|
@@ -6,11 +6,11 @@ import { dirname, isAbsolute, join } from "node:path";
|
|
|
6
6
|
import { parseMachinePaths, resolveCatalystPath, } from "../vendor/paths/index.js";
|
|
7
7
|
import { machinePathsFile } from "../vendor/paths/node.js";
|
|
8
8
|
import { configPathFor } from "./config.js";
|
|
9
|
-
import { onboardJsonView } from "./setup-onboard-copy.js";
|
|
9
|
+
import { onboardJsonView, TIMED_OUT_REASONS } from "./setup-onboard-copy.js";
|
|
10
10
|
import { standalonePlan, standalonePlanNotes, } from "./onboard-standalone-copy.js";
|
|
11
11
|
import { CliError, UsageError } from "./errors.js";
|
|
12
12
|
import { onboardFileSnapshot } from "./onboard-file-snapshot.js";
|
|
13
|
-
import { onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
|
|
13
|
+
import { dispatchStageName, onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
|
|
14
14
|
import { parseBootstrapPlan, bootstrapPlanHash, bootstrapPlanLines, } from "./onboard-bootstrap.js";
|
|
15
15
|
export const ONBOARD_STEPS = [
|
|
16
16
|
"machine",
|
|
@@ -37,6 +37,14 @@ export const ONBOARD_STEPS = [
|
|
|
37
37
|
"first-ticket",
|
|
38
38
|
"ready",
|
|
39
39
|
];
|
|
40
|
+
function dailyUpdateChoice(value) {
|
|
41
|
+
const row = object(value);
|
|
42
|
+
if (row?.state === "on" || row?.state === "off")
|
|
43
|
+
return { state: row.state };
|
|
44
|
+
if (row?.state === "skipped" && row.reason === "no_scheduler")
|
|
45
|
+
return { state: "skipped", reason: "no_scheduler" };
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
40
48
|
export function onboardIdentityLines(identity) {
|
|
41
49
|
if (!identity)
|
|
42
50
|
return [
|
|
@@ -51,10 +59,11 @@ export function onboardIdentityLines(identity) {
|
|
|
51
59
|
const person = label(display?.personLabel || identity.membershipId);
|
|
52
60
|
const email = display?.email ? label(display.email) : "";
|
|
53
61
|
const workspace = label(display?.workspaceName || identity.account);
|
|
54
|
-
|
|
62
|
+
// CTC-4680 (Ryan, round 2): "Signed in to Catalyst as …" and "Catalyst workspace: …", with no
|
|
63
|
+
// repeated email and no internal slug.
|
|
55
64
|
return [
|
|
56
|
-
`Signed in as ${person}${email ? ` (${email})` : ""}`,
|
|
57
|
-
`
|
|
65
|
+
`Signed in to Catalyst as ${person}${email && email !== person ? ` (${email})` : ""}`,
|
|
66
|
+
`Catalyst workspace: ${workspace} · ${identity.role}`,
|
|
58
67
|
];
|
|
59
68
|
}
|
|
60
69
|
const EXIT_FAILED = 10;
|
|
@@ -103,7 +112,8 @@ export const ONBOARD_DEPENDENCIES = {
|
|
|
103
112
|
projects: ["linear.team", "github.repos"],
|
|
104
113
|
accounts: ["signin"],
|
|
105
114
|
settings: ["projects"],
|
|
106
|
-
values
|
|
115
|
+
// The cloud's required-values check reads the registered repositories, not a local checkout.
|
|
116
|
+
values: ["projects"],
|
|
107
117
|
capacity: ["projects"],
|
|
108
118
|
// The join token names the selected team's pool.
|
|
109
119
|
runner: ["linear.team"],
|
|
@@ -126,7 +136,7 @@ const ADMIN_STEPS = new Set([
|
|
|
126
136
|
"github.repos",
|
|
127
137
|
"projects",
|
|
128
138
|
"settings",
|
|
129
|
-
|
|
139
|
+
// `values` is not here: its check only reads, so a member's run finds missing values too.
|
|
130
140
|
"capacity",
|
|
131
141
|
"runner",
|
|
132
142
|
"first-ticket",
|
|
@@ -146,9 +156,9 @@ export const ONBOARD_TITLES = {
|
|
|
146
156
|
"github.personal": "Connect your GitHub account",
|
|
147
157
|
"github.repos": "Choose repositories",
|
|
148
158
|
projects: "Register your projects",
|
|
149
|
-
accounts: "Check
|
|
159
|
+
accounts: "Check AI accounts",
|
|
150
160
|
settings: "Review repository settings",
|
|
151
|
-
values: "
|
|
161
|
+
values: "Check the repository's values",
|
|
152
162
|
capacity: "Check runner capacity",
|
|
153
163
|
runner: "Run Catalyst's work on this machine",
|
|
154
164
|
daemon: "Check optional local sync",
|
|
@@ -250,6 +260,10 @@ function normalizeStep(value, fallbackAt) {
|
|
|
250
260
|
"repository",
|
|
251
261
|
"installation",
|
|
252
262
|
"ticket",
|
|
263
|
+
// The team's dispatch stage name, as the contract gave it.
|
|
264
|
+
"stage",
|
|
265
|
+
// Admin-owned step ids a member's run found unfinished.
|
|
266
|
+
"admin",
|
|
253
267
|
"cursor",
|
|
254
268
|
"lag",
|
|
255
269
|
"heartbeatAgeMs",
|
|
@@ -287,6 +301,8 @@ function normalizeStep(value, fallbackAt) {
|
|
|
287
301
|
"org",
|
|
288
302
|
"url",
|
|
289
303
|
"actor",
|
|
304
|
+
// CTC-4716: which AI accounts the workspace may add, so the step's text offers only those.
|
|
305
|
+
"aiAccountKinds",
|
|
290
306
|
]) {
|
|
291
307
|
const item = evidence?.[key];
|
|
292
308
|
if (item === null ||
|
|
@@ -322,6 +338,7 @@ export function readOnboardJournal(path, cliVersion = "0.13.1", now = new Date()
|
|
|
322
338
|
: typeof value.exitCode === "number"
|
|
323
339
|
? value.exitCode
|
|
324
340
|
: null;
|
|
341
|
+
const dailyUpdate = dailyUpdateChoice(value.dailyUpdate);
|
|
325
342
|
return {
|
|
326
343
|
schema: 1,
|
|
327
344
|
runId: typeof value.runId === "string" ? value.runId : randomUUID(),
|
|
@@ -353,6 +370,7 @@ export function readOnboardJournal(path, cliVersion = "0.13.1", now = new Date()
|
|
|
353
370
|
...(typeof value.localSync === "boolean"
|
|
354
371
|
? { localSync: value.localSync }
|
|
355
372
|
: {}),
|
|
373
|
+
...(dailyUpdate ? { dailyUpdate } : {}),
|
|
356
374
|
operations: Object.fromEntries(Object.entries(object(value.operations) ?? {}).filter(([key, v]) => ONBOARD_STEPS.includes(key) &&
|
|
357
375
|
typeof v === "string" &&
|
|
358
376
|
/^[a-zA-Z0-9_.:-]{1,160}$/.test(v))),
|
|
@@ -572,7 +590,8 @@ function printPlan(ctx, journal, identity, options) {
|
|
|
572
590
|
for (const line of onboardIdentityLines(identity))
|
|
573
591
|
ctx.stdout(line);
|
|
574
592
|
let group;
|
|
575
|
-
|
|
593
|
+
// Beside --json and headless output: the numbers JSON actions carry (CTC-4680).
|
|
594
|
+
for (const row of standalonePlan(journal, options?.scope, options?.runner, "json")) {
|
|
576
595
|
if (row.group !== group) {
|
|
577
596
|
ctx.stdout("");
|
|
578
597
|
ctx.stdout(row.group);
|
|
@@ -628,14 +647,28 @@ export function stepSatisfied(step) {
|
|
|
628
647
|
return (step?.state === "done" ||
|
|
629
648
|
(step?.state === "skipped" &&
|
|
630
649
|
(step.id === "legacy" ||
|
|
650
|
+
// Optional: a computer without the daily update still runs work.
|
|
651
|
+
step.id === "housekeeping" ||
|
|
631
652
|
step.reason === "member_scope" ||
|
|
632
653
|
(step.id === "daemon" && step.reason === "local_sync_not_selected") ||
|
|
633
654
|
(step.id === "runner" && step.reason === "runner_not_selected") ||
|
|
655
|
+
(step.id === "first-ticket" && step.reason === "first_ticket_skipped") ||
|
|
634
656
|
(step.id === "linear.automations" &&
|
|
635
657
|
step.reason === "automation_management_unavailable"))));
|
|
636
658
|
}
|
|
659
|
+
/** Optional work this computer or checkout cannot do. Work runs without it, so its wait never holds
|
|
660
|
+
* a complete setup. */
|
|
661
|
+
const OPTIONAL_WAITS = new Set([
|
|
662
|
+
"settings_checkout_unverified",
|
|
663
|
+
"settings_approval_unverified",
|
|
664
|
+
]);
|
|
665
|
+
/** Satisfied, or an optional wait. What a complete setup needs of every step. */
|
|
666
|
+
export function stepSettled(step) {
|
|
667
|
+
return (stepSatisfied(step) ||
|
|
668
|
+
(step?.state === "waiting" && OPTIONAL_WAITS.has(step.reason ?? "")));
|
|
669
|
+
}
|
|
637
670
|
/** Steps whose wait does not hold a full run's exit code: work runs without them. A failure still
|
|
638
|
-
* fails the run, and the receipt's `complete` still needs them
|
|
671
|
+
* fails the run, and the receipt's `complete` still needs them settled. Never used for gating. */
|
|
639
672
|
export const ONBOARD_DEFERRED_STEPS = new Set([
|
|
640
673
|
"settings",
|
|
641
674
|
"values",
|
|
@@ -676,8 +709,11 @@ export function onboardNextActions(journal, baseUrl, only) {
|
|
|
676
709
|
.filter((step) => (!only || step.id === only) && !stepSatisfied(step))
|
|
677
710
|
.sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id))
|
|
678
711
|
.map((step) => `${ONBOARD_TITLES[step.id]}: ${onboardStepAction(journal, step, baseUrl)}`);
|
|
679
|
-
|
|
680
|
-
|
|
712
|
+
// A waiting first ticket already says to move a ticket, in its own line.
|
|
713
|
+
const firstTicket = journalStep(journal, "first-ticket");
|
|
714
|
+
if (["projects", "accounts", "linear.adopt", "capacity"].every((id) => journalStep(journal, id)?.state === "done") &&
|
|
715
|
+
firstTicket?.state !== "waiting")
|
|
716
|
+
lines.push(`Move a ticket in ${onboardTeamKey(journal) ?? "<TEAM KEY>"} to ${dispatchStageName(firstTicket?.evidence?.stage)}; \`catalyst explain <ticket>\` says why it is or is not starting.`);
|
|
681
717
|
return lines;
|
|
682
718
|
}
|
|
683
719
|
function requireMatchingIdentity(journal, identity) {
|
|
@@ -726,9 +762,10 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
726
762
|
const only = typeof args.flags.only === "string"
|
|
727
763
|
? args.flags.only
|
|
728
764
|
: undefined;
|
|
765
|
+
// A member's values check reads the selected team, so without --team it waits on the same skip.
|
|
729
766
|
const memberScopeSkips = (id) => ADMIN_STEPS.has(id) ||
|
|
730
|
-
(id === "linear.team" &&
|
|
731
|
-
only !==
|
|
767
|
+
((id === "linear.team" || id === "values") &&
|
|
768
|
+
only !== id &&
|
|
732
769
|
typeof args.flags.team !== "string");
|
|
733
770
|
const resume = typeof args.flags["resume-from"] === "string"
|
|
734
771
|
? args.flags["resume-from"]
|
|
@@ -1068,7 +1105,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1068
1105
|
journal.complete =
|
|
1069
1106
|
!only &&
|
|
1070
1107
|
code === 0 &&
|
|
1071
|
-
ONBOARD_STEPS.every((id) =>
|
|
1108
|
+
ONBOARD_STEPS.every((id) => stepSettled(journalStep(journal, id)));
|
|
1072
1109
|
writeOnboardJournal(statePath, journal);
|
|
1073
1110
|
if (args.json)
|
|
1074
1111
|
ctx.stdout(JSON.stringify(journal));
|
|
@@ -1398,7 +1435,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1398
1435
|
}
|
|
1399
1436
|
if (signal.aborted) {
|
|
1400
1437
|
const settled = journalStep(journal, id);
|
|
1401
|
-
|
|
1438
|
+
// CTC-4680 round 5: a person who stopped at "Ready to try again?" stopped a step whose link
|
|
1439
|
+
// timed out; keep that reason so the summary says so, not "skipped for now".
|
|
1440
|
+
if (settled?.state !== "done" &&
|
|
1441
|
+
settled?.state !== "failed" &&
|
|
1442
|
+
!TIMED_OUT_REASONS.has(settled?.reason ?? ""))
|
|
1402
1443
|
recordStep({
|
|
1403
1444
|
id,
|
|
1404
1445
|
state: "waiting",
|
|
@@ -1437,7 +1478,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1437
1478
|
journal.complete =
|
|
1438
1479
|
!only &&
|
|
1439
1480
|
code === 0 &&
|
|
1440
|
-
ONBOARD_STEPS.every((id) =>
|
|
1481
|
+
ONBOARD_STEPS.every((id) => stepSettled(journalStep(journal, id)));
|
|
1441
1482
|
const unfinished = journal.steps.filter((step) => !stepSatisfied(step));
|
|
1442
1483
|
if (!only &&
|
|
1443
1484
|
!refused &&
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// ready-copy.ts — CTC-4680: what `catalyst ready` says about a team, in words a person can act on.
|
|
2
|
+
//
|
|
3
|
+
// The check ids, states and reason codes stay in `--json` (`id`) for scripts. The `line`, `fix` and
|
|
4
|
+
// `who` a person reads never carry a check id, a reason code, an API route or the word "tenant", and
|
|
5
|
+
// every fix is either a full URL or a step in Linear's own settings. `line` keeps its `team KEY: `
|
|
6
|
+
// prefix because install.sh strips exactly that before printing a team row.
|
|
7
|
+
const FIXER = {
|
|
8
|
+
oauth_scope: "owner_or_admin",
|
|
9
|
+
token_live: "owner_or_admin",
|
|
10
|
+
team_visible: "owner_or_admin_in_linear",
|
|
11
|
+
mapped_states_exist: "owner_or_admin",
|
|
12
|
+
mapping_total: "owner_or_admin",
|
|
13
|
+
types_compatible: "owner_or_admin",
|
|
14
|
+
labels_present: "nobody",
|
|
15
|
+
writes_land: "owner_or_admin",
|
|
16
|
+
webhook_covers_team: "owner_or_admin",
|
|
17
|
+
hosts_current: "host_operator",
|
|
18
|
+
environment_declared: "owner_or_admin",
|
|
19
|
+
tools_resolvable: "owner_or_admin",
|
|
20
|
+
required_values: "owner_or_admin",
|
|
21
|
+
reviewer_required: "owner_or_admin",
|
|
22
|
+
reviewer_configured: "nobody",
|
|
23
|
+
reviewer_answering: "repository_admin",
|
|
24
|
+
linear_automation_pr_open: "owner_or_admin_in_linear",
|
|
25
|
+
linear_automation_pr_review: "owner_or_admin_in_linear",
|
|
26
|
+
linear_automation_pr_ready: "owner_or_admin_in_linear",
|
|
27
|
+
linear_automation_pr_merge: "owner_or_admin_in_linear",
|
|
28
|
+
merge_queue_configured: "repository_admin",
|
|
29
|
+
coding_account_enrolled: "owner_or_admin",
|
|
30
|
+
github_app_installed: "repository_admin",
|
|
31
|
+
thoughts_reachable: "repository_admin",
|
|
32
|
+
};
|
|
33
|
+
const WHO = {
|
|
34
|
+
nobody: "nobody; it clears on its own",
|
|
35
|
+
owner_or_admin: "an owner or admin of your Catalyst workspace",
|
|
36
|
+
owner_or_admin_in_linear: "an admin of your Linear workspace",
|
|
37
|
+
host_operator: "whoever runs your Catalyst host",
|
|
38
|
+
repository_admin: "an admin of the team's GitHub repository",
|
|
39
|
+
};
|
|
40
|
+
/** Who fixes a check, as a phrase. An unknown check goes to the workspace's owners and admins. */
|
|
41
|
+
export function whoFixes(checkId) {
|
|
42
|
+
return WHO[FIXER[checkId] ?? "owner_or_admin"];
|
|
43
|
+
}
|
|
44
|
+
const SLOT_WORDS = {
|
|
45
|
+
dispatch: "starting work",
|
|
46
|
+
intake: "intake",
|
|
47
|
+
research: "research",
|
|
48
|
+
plan: "planning",
|
|
49
|
+
implement: "writing the code",
|
|
50
|
+
remediate: "fixing review findings",
|
|
51
|
+
verify: "checking the work",
|
|
52
|
+
review: "review",
|
|
53
|
+
pr: "pull requests",
|
|
54
|
+
done: "Done",
|
|
55
|
+
canceled: "Canceled",
|
|
56
|
+
};
|
|
57
|
+
function wordList(items) {
|
|
58
|
+
if (items.length <= 1)
|
|
59
|
+
return items.join("");
|
|
60
|
+
return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
61
|
+
}
|
|
62
|
+
function slotList(slots) {
|
|
63
|
+
return wordList(slots.map((s) => SLOT_WORDS[s] ?? s.replaceAll("_", " ")));
|
|
64
|
+
}
|
|
65
|
+
function origin(baseUrl) {
|
|
66
|
+
return baseUrl.replace(/\/+$/, "");
|
|
67
|
+
}
|
|
68
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
69
|
+
const REPOSITORY = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/;
|
|
70
|
+
const identifiers = (value) => Array.isArray(value)
|
|
71
|
+
? value.filter((name) => typeof name === "string" && NAME.test(name))
|
|
72
|
+
: [];
|
|
73
|
+
const unresolvedFacts = (value) => Array.isArray(value)
|
|
74
|
+
? value.flatMap((row) => typeof row?.name === "string" && NAME.test(row.name) && identifiers(row.references).length
|
|
75
|
+
? [{ name: row.name, references: identifiers(row.references) }]
|
|
76
|
+
: [])
|
|
77
|
+
: [];
|
|
78
|
+
/** The declared names a failing check carries, kept in the receipt for the fix line: which variables
|
|
79
|
+
* need a value, which refer to a secret with no value, and the same for the team's other
|
|
80
|
+
* repositories. Identifiers and repository names only. */
|
|
81
|
+
export function valuesFacts(value) {
|
|
82
|
+
const row = (value ?? {});
|
|
83
|
+
return {
|
|
84
|
+
names: identifiers(row.names),
|
|
85
|
+
unresolved: unresolvedFacts(row.unresolved),
|
|
86
|
+
repos: Array.isArray(row.repos)
|
|
87
|
+
? row.repos.flatMap((note) => typeof note?.repo === "string" && REPOSITORY.test(note.repo)
|
|
88
|
+
? [{ repo: note.repo, names: identifiers(note.names), unresolved: unresolvedFacts(note.unresolved) }]
|
|
89
|
+
: [])
|
|
90
|
+
: [],
|
|
91
|
+
};
|
|
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`;
|
|
98
|
+
}
|
|
99
|
+
function nameList(v) {
|
|
100
|
+
return Array.isArray(v)
|
|
101
|
+
? v.filter((n) => typeof n === "string" && n.length > 0)
|
|
102
|
+
: [];
|
|
103
|
+
}
|
|
104
|
+
/** Entries whose name and references are both present; anything malformed is dropped, not guessed. */
|
|
105
|
+
function unresolvedList(v) {
|
|
106
|
+
if (!Array.isArray(v))
|
|
107
|
+
return [];
|
|
108
|
+
return v.flatMap((u) => {
|
|
109
|
+
if (typeof u !== "object" || u === null)
|
|
110
|
+
return [];
|
|
111
|
+
const { name, references } = u;
|
|
112
|
+
const refs = nameList(references);
|
|
113
|
+
return typeof name === "string" && name.length > 0 && refs.length > 0
|
|
114
|
+
? [{ name, references: refs }]
|
|
115
|
+
: [];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function unresolvedLine(u) {
|
|
119
|
+
const which = u.references.length === 1 ? "which has" : "which have";
|
|
120
|
+
return `${u.name} refers to ${u.references.join(", ")}, ${which} no value, so Catalyst can't start work there`;
|
|
121
|
+
}
|
|
122
|
+
/** A team check's fix line. A check that carries `names` (CTC-3561: `required_values`, contract
|
|
123
|
+
* 1.24.0) names them and where to set them. CTC-3606: it also names each `unresolved` variable and
|
|
124
|
+
* the reference with no value, and each other repository's missing names from `repos[]`. `unresolved`
|
|
125
|
+
* is a subset of `names` (the variable exists; its reference does not), so those are not told to be
|
|
126
|
+
* "set". Every field is optional so an older cloud still works. Everything printed is a declared
|
|
127
|
+
* identifier or a repository name, never a value, and nothing here reads a value. Any other check
|
|
128
|
+
* keeps the generic line. Shared with setup's values step, which has no web address when the
|
|
129
|
+
* saved login has none. */
|
|
130
|
+
export function valuesFix(base, c) {
|
|
131
|
+
const unresolved = unresolvedList(c.unresolved);
|
|
132
|
+
const unresolvedNames = new Set(unresolved.map((u) => u.name));
|
|
133
|
+
const names = nameList(c.names).filter((n) => !unresolvedNames.has(n));
|
|
134
|
+
const parts = [];
|
|
135
|
+
if (names.length > 0)
|
|
136
|
+
parts.push(setValuesFix(base, names));
|
|
137
|
+
for (const u of unresolved)
|
|
138
|
+
parts.push(unresolvedLine(u));
|
|
139
|
+
for (const note of Array.isArray(c.repos) ? c.repos : []) {
|
|
140
|
+
if (typeof note !== "object" ||
|
|
141
|
+
note === null ||
|
|
142
|
+
typeof note.repo !== "string" ||
|
|
143
|
+
note.repo.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
const repoUnresolved = unresolvedList(note.unresolved);
|
|
146
|
+
const skip = new Set(repoUnresolved.map((u) => u.name));
|
|
147
|
+
const missing = nameList(note.names).filter((n) => !skip.has(n));
|
|
148
|
+
if (missing.length > 0)
|
|
149
|
+
parts.push(`${note.repo} is missing ${missing.join(", ")}`);
|
|
150
|
+
for (const u of repoUnresolved)
|
|
151
|
+
parts.push(`in ${note.repo}, ${unresolvedLine(u)}`);
|
|
152
|
+
}
|
|
153
|
+
return parts.length === 0 ? null : parts.join(". ");
|
|
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
|
+
/** The dispatch gate: whether Catalyst may start any ticket in the team. */
|
|
160
|
+
export function dispatchGateCopy(baseUrl, key, status, missingSlots) {
|
|
161
|
+
if (status === "open")
|
|
162
|
+
return { line: `team ${key}: Catalyst can start ${key} tickets` };
|
|
163
|
+
// CTC-4708: the cloud says the mapping is saved and its copy of Linear hasn't caught up. That is a
|
|
164
|
+
// wait, not something to fix.
|
|
165
|
+
if (status === "stages_syncing")
|
|
166
|
+
return {
|
|
167
|
+
line: `team ${key}: Catalyst is still copying ${key}'s stages from Linear. It starts ${key} tickets once that finishes.`,
|
|
168
|
+
};
|
|
169
|
+
const slots = missingSlots.length > 0 ? slotList(missingSlots) : "its moves";
|
|
170
|
+
if (status === "mapping_missing")
|
|
171
|
+
return {
|
|
172
|
+
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")}`,
|
|
174
|
+
};
|
|
175
|
+
if (status === "mapping_state_unresolved")
|
|
176
|
+
// The gate reads Catalyst's own copy of the team, which can lag Linear by a minute after a team
|
|
177
|
+
// is created or its workflow is set up (CTC-4680 Rerun A). Say that, not "map your stages".
|
|
178
|
+
return {
|
|
179
|
+
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)}`,
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
line: `team ${key}: Catalyst can't start ${key} tickets yet.`,
|
|
184
|
+
fix: `open ${teamPage(baseUrl, key)} to see why`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const AUTOMATION_RULES = {
|
|
188
|
+
linear_automation_pr_open: { rule: "On PR open", when: "when a pull request opens" },
|
|
189
|
+
linear_automation_pr_review: {
|
|
190
|
+
rule: "On PR review request or activity",
|
|
191
|
+
when: "when a pull request gets a review",
|
|
192
|
+
},
|
|
193
|
+
linear_automation_pr_ready: {
|
|
194
|
+
rule: "On PR ready for merge",
|
|
195
|
+
when: "when a pull request is ready to merge",
|
|
196
|
+
},
|
|
197
|
+
linear_automation_pr_merge: { rule: "On PR merge", when: "when a pull request merges" },
|
|
198
|
+
};
|
|
199
|
+
/** Plain names for checks with no sentence of their own below. */
|
|
200
|
+
const CHECK_NAMES = {
|
|
201
|
+
labels_present: "Catalyst's labels in Linear",
|
|
202
|
+
writes_land: "Catalyst's changes to Linear",
|
|
203
|
+
webhook_covers_team: "events from Linear and GitHub for this team",
|
|
204
|
+
hosts_current: "your connected Catalyst hosts",
|
|
205
|
+
environment_declared: "the repository's settings file",
|
|
206
|
+
tools_resolvable: "the tools the repository's settings name",
|
|
207
|
+
reviewer_required: "the required code reviewer",
|
|
208
|
+
reviewer_configured: "a code reviewer",
|
|
209
|
+
reviewer_answering: "the code reviewer's answers",
|
|
210
|
+
merge_queue_configured: "the repository's merge queue",
|
|
211
|
+
github_app_installed: "Catalyst on GitHub",
|
|
212
|
+
thoughts_reachable: "the thoughts repository",
|
|
213
|
+
};
|
|
214
|
+
/** The names above that read as plural ("labels need attention"). */
|
|
215
|
+
const PLURAL = new Set([
|
|
216
|
+
"labels_present",
|
|
217
|
+
"writes_land",
|
|
218
|
+
"webhook_covers_team",
|
|
219
|
+
"hosts_current",
|
|
220
|
+
"tools_resolvable",
|
|
221
|
+
"reviewer_answering",
|
|
222
|
+
]);
|
|
223
|
+
/** The settings page that owns each check, from the cloud's CHECK_COPY settingsPath. */
|
|
224
|
+
const CHECK_PAGES = {
|
|
225
|
+
oauth_scope: "/settings/connections",
|
|
226
|
+
token_live: "/settings/connections",
|
|
227
|
+
webhook_covers_team: "/settings/projects",
|
|
228
|
+
hosts_current: "/settings/projects",
|
|
229
|
+
environment_declared: "/settings/projects",
|
|
230
|
+
tools_resolvable: "/settings/environment",
|
|
231
|
+
required_values: "/settings/projects",
|
|
232
|
+
reviewer_required: "/settings/projects",
|
|
233
|
+
reviewer_configured: "/settings/projects",
|
|
234
|
+
reviewer_answering: "/settings/projects",
|
|
235
|
+
merge_queue_configured: "/settings/projects",
|
|
236
|
+
coding_account_enrolled: "/settings/coding-accounts",
|
|
237
|
+
github_app_installed: "/settings/projects",
|
|
238
|
+
thoughts_reachable: "/settings/projects",
|
|
239
|
+
};
|
|
240
|
+
const AGAIN = "run catalyst ready again in a few minutes";
|
|
241
|
+
/** One failing or unknown team check, in plain words. `valuesFix` is the variable-naming fix the
|
|
242
|
+
* caller already built for `required_values`, or null when the cloud named no variable. */
|
|
243
|
+
export function teamCheckCopy(baseUrl, key, id, state, valuesFix) {
|
|
244
|
+
const team = (text) => `team ${key}: ${text}`;
|
|
245
|
+
const base = origin(baseUrl);
|
|
246
|
+
const unknown = state === "unknown";
|
|
247
|
+
const automation = AUTOMATION_RULES[id];
|
|
248
|
+
if (automation)
|
|
249
|
+
return unknown
|
|
250
|
+
? {
|
|
251
|
+
line: team(`Catalyst couldn't read ${key}'s pull request automations in Linear.`),
|
|
252
|
+
fix: AGAIN,
|
|
253
|
+
}
|
|
254
|
+
: {
|
|
255
|
+
line: team(`Linear moves ${key}'s tickets by itself ${automation.when}. That fights Catalyst, which moves them itself.`),
|
|
256
|
+
fix: `in Linear, open Settings → Teams → ${key} → Workflow → Pull request and commit automations and set "${automation.rule}" to No action, including any branch overrides. Then run catalyst ready again`,
|
|
257
|
+
};
|
|
258
|
+
switch (id) {
|
|
259
|
+
case "oauth_scope":
|
|
260
|
+
return unknown
|
|
261
|
+
? { line: team("Catalyst couldn't check its Linear connection."), fix: `check that Linear is connected at ${base}/settings/connections` }
|
|
262
|
+
: {
|
|
263
|
+
line: team("Catalyst's Linear connection is missing a permission it needs."),
|
|
264
|
+
fix: `reconnect Linear at ${base}/settings/connections?reauthorize=linear`,
|
|
265
|
+
};
|
|
266
|
+
case "token_live":
|
|
267
|
+
return unknown
|
|
268
|
+
? { line: team("Catalyst couldn't reach Linear to check its connection."), fix: AGAIN }
|
|
269
|
+
: {
|
|
270
|
+
line: team("Linear stopped accepting Catalyst's connection. It was revoked or expired."),
|
|
271
|
+
fix: `reconnect Linear at ${base}/settings/connections`,
|
|
272
|
+
};
|
|
273
|
+
case "team_visible":
|
|
274
|
+
return unknown
|
|
275
|
+
? { line: team(`Catalyst hasn't confirmed it can see ${key} in Linear yet.`), fix: AGAIN }
|
|
276
|
+
: {
|
|
277
|
+
line: team(`Catalyst can't see ${key} in Linear, usually because the team is private.`),
|
|
278
|
+
fix: `in Linear, open Settings → API → Catalyst Cloud → Team access and add ${key}`,
|
|
279
|
+
};
|
|
280
|
+
case "mapped_states_exist":
|
|
281
|
+
case "mapping_total":
|
|
282
|
+
case "types_compatible":
|
|
283
|
+
if (unknown)
|
|
284
|
+
return { line: team(`Catalyst is still reading ${key}'s stages from Linear.`), fix: "run catalyst ready again in a minute" };
|
|
285
|
+
if (id === "mapped_states_exist")
|
|
286
|
+
return {
|
|
287
|
+
line: team(`A stage Catalyst uses in ${key} was deleted or renamed in Linear.`),
|
|
288
|
+
fix: `pick its replacement at ${teamPage(baseUrl, key, "map")}`,
|
|
289
|
+
};
|
|
290
|
+
if (id === "mapping_total")
|
|
291
|
+
return {
|
|
292
|
+
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")}`,
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
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")}`,
|
|
298
|
+
};
|
|
299
|
+
case "required_values":
|
|
300
|
+
return unknown
|
|
301
|
+
? {
|
|
302
|
+
line: team(`Catalyst hasn't read the settings of ${key}'s repository yet.`),
|
|
303
|
+
fix: "catalyst setup reads them when you choose repositories. Run catalyst setup",
|
|
304
|
+
}
|
|
305
|
+
: {
|
|
306
|
+
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`,
|
|
308
|
+
};
|
|
309
|
+
case "coding_account_enrolled":
|
|
310
|
+
return unknown
|
|
311
|
+
? { line: team("Catalyst couldn't check your AI accounts."), fix: AGAIN }
|
|
312
|
+
: {
|
|
313
|
+
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`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const name = CHECK_NAMES[id] ?? id.replaceAll("_", " ");
|
|
318
|
+
const page = CHECK_PAGES[id] ?? `/settings/linear-teams/${encodeURIComponent(key)}`;
|
|
319
|
+
return unknown
|
|
320
|
+
? { line: team(`Catalyst hasn't checked ${name} yet.`), fix: AGAIN }
|
|
321
|
+
: {
|
|
322
|
+
line: team(`${name.charAt(0).toUpperCase()}${name.slice(1)} ${PLURAL.has(id) ? "need" : "needs"} attention.`),
|
|
323
|
+
fix: `open ${base}${page}`,
|
|
324
|
+
};
|
|
325
|
+
}
|