@catalyst-cloud/cli 0.15.2 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -2
- package/dist/args.js +5 -1
- package/dist/cli.js +44 -20
- package/dist/oauth.js +1 -1
- package/dist/onboard-accounts.js +67 -171
- package/dist/onboard-checklist.js +310 -0
- package/dist/onboard-existing.js +1 -1
- package/dist/onboard-first-ticket.js +199 -0
- package/dist/onboard-headless.js +2 -2
- package/dist/onboard-housekeeping.js +93 -0
- package/dist/onboard-next.js +108 -9
- package/dist/onboard-ready.js +61 -0
- package/dist/onboard-runtime.js +60 -27
- package/dist/onboard-standalone-copy.js +73 -66
- package/dist/onboard-ui.js +278 -105
- package/dist/onboard-values.js +47 -0
- package/dist/onboard.js +63 -18
- package/dist/ready-copy.js +87 -0
- package/dist/ready.js +1 -57
- package/dist/setup-brand.js +202 -0
- package/dist/setup-onboard-copy.js +314 -42
- package/dist/setup-render.js +99 -17
- package/dist/setup.js +6 -3
- package/dist/terminal-background.js +220 -0
- package/package.json +1 -1
- 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 +1 -1
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ONBOARD_DEPENDENCIES, ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
|
|
2
|
-
import { onboardReasonText } from "./onboard-next.js";
|
|
2
|
+
import { dispatchStageName, onboardReasonText } from "./onboard-next.js";
|
|
3
|
+
/** The step numbers `--json` reports. People read SETUP_PART_NUMBERS instead (CTC-4680). */
|
|
3
4
|
export const SETUP_NUMBERS = {
|
|
4
5
|
machine: 1,
|
|
5
6
|
cli: 1,
|
|
@@ -23,13 +24,94 @@ export const SETUP_NUMBERS = {
|
|
|
23
24
|
values: 16,
|
|
24
25
|
"first-ticket": 17,
|
|
25
26
|
};
|
|
27
|
+
/** CTC-4680: setup runs in three parts. The times are estimates until setup logs give medians. */
|
|
28
|
+
export const SETUP_PARTS = {
|
|
29
|
+
1: {
|
|
30
|
+
title: "This computer",
|
|
31
|
+
estimate: "about 2 min",
|
|
32
|
+
summary: "skills and sign-in",
|
|
33
|
+
done: "This computer is set up.",
|
|
34
|
+
},
|
|
35
|
+
2: {
|
|
36
|
+
title: "Linear and GitHub",
|
|
37
|
+
estimate: "about 10 min",
|
|
38
|
+
summary: "approvals in your browser",
|
|
39
|
+
done: "Linear and GitHub are connected.",
|
|
40
|
+
},
|
|
41
|
+
3: {
|
|
42
|
+
title: "Ready for work",
|
|
43
|
+
estimate: "about 5 min",
|
|
44
|
+
summary: "a first ticket",
|
|
45
|
+
done: "Catalyst is ready for work.",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
export const SETUP_PART_OF = {
|
|
49
|
+
machine: 1,
|
|
50
|
+
cli: 1,
|
|
51
|
+
skills: 1,
|
|
52
|
+
legacy: 1,
|
|
53
|
+
signin: 1,
|
|
54
|
+
daemon: 1,
|
|
55
|
+
housekeeping: 1,
|
|
56
|
+
"linear.workspace": 2,
|
|
57
|
+
"linear.personal": 2,
|
|
58
|
+
"linear.team": 2,
|
|
59
|
+
"linear.adopt": 2,
|
|
60
|
+
"linear.automations": 2,
|
|
61
|
+
"github.install": 2,
|
|
62
|
+
"github.personal": 2,
|
|
63
|
+
"github.repos": 3,
|
|
64
|
+
projects: 3,
|
|
65
|
+
accounts: 3,
|
|
66
|
+
capacity: 3,
|
|
67
|
+
runner: 3,
|
|
68
|
+
settings: 3,
|
|
69
|
+
values: 3,
|
|
70
|
+
"first-ticket": 3,
|
|
71
|
+
ready: 3,
|
|
72
|
+
};
|
|
73
|
+
/** What a person reads: each part restarts at 1, in the order setup runs the steps. Steps that
|
|
74
|
+
* share a row (the computer checks, repositories and projects) share a number. */
|
|
75
|
+
export const SETUP_PART_NUMBERS = {
|
|
76
|
+
machine: 1,
|
|
77
|
+
cli: 1,
|
|
78
|
+
skills: 1,
|
|
79
|
+
legacy: 1,
|
|
80
|
+
signin: 2,
|
|
81
|
+
housekeeping: 3,
|
|
82
|
+
"linear.workspace": 1,
|
|
83
|
+
"linear.personal": 2,
|
|
84
|
+
"linear.team": 3,
|
|
85
|
+
"linear.adopt": 4,
|
|
86
|
+
"linear.automations": 5,
|
|
87
|
+
"github.install": 6,
|
|
88
|
+
"github.personal": 7,
|
|
89
|
+
"github.repos": 1,
|
|
90
|
+
projects: 1,
|
|
91
|
+
accounts: 2,
|
|
92
|
+
capacity: 3,
|
|
93
|
+
runner: 4,
|
|
94
|
+
settings: 5,
|
|
95
|
+
values: 6,
|
|
96
|
+
"first-ticket": 7,
|
|
97
|
+
};
|
|
98
|
+
/** CTC-4680: people type `catalyst setup`; `catalyst onboard` stays its alias. What setup draws
|
|
99
|
+
* names setup. `--json` and headless output keep their own words. */
|
|
100
|
+
export function setupCommandWords(text) {
|
|
101
|
+
return text.replace(/\bcatalyst onboard\b/g, "catalyst setup");
|
|
102
|
+
}
|
|
103
|
+
/** "Part 1 of 3 · This computer", with a colon where the output is plain. */
|
|
104
|
+
export function setupPartHeading(part, unicode) {
|
|
105
|
+
return `Part ${part} of 3${unicode ? " ·" : ":"} ${SETUP_PARTS[part].title}`;
|
|
106
|
+
}
|
|
26
107
|
const TITLES = {
|
|
27
108
|
"linear.adopt": "Set up the team's workflow",
|
|
28
109
|
"linear.automations": "Check Linear's PR automations",
|
|
29
110
|
projects: "Choose repositories",
|
|
30
|
-
accounts: "Add
|
|
111
|
+
accounts: "Add an AI account",
|
|
112
|
+
housekeeping: "Daily update",
|
|
31
113
|
capacity: "Check runners",
|
|
32
|
-
values: "
|
|
114
|
+
values: "Check repository values",
|
|
33
115
|
};
|
|
34
116
|
const OPTIONAL = new Set([
|
|
35
117
|
"local_sync_not_selected",
|
|
@@ -83,18 +165,23 @@ function succeeded(step) {
|
|
|
83
165
|
case "projects":
|
|
84
166
|
return "repositories are now Catalyst projects";
|
|
85
167
|
case "accounts":
|
|
86
|
-
return `${value(step, "provider") === "claude" ? "Claude" : value(step, "provider") === "codex" ? "Codex" : "
|
|
168
|
+
return `${value(step, "provider") === "claude" ? "Claude" : value(step, "provider") === "codex" ? "Codex" : "an AI account"} can take work`;
|
|
169
|
+
case "housekeeping":
|
|
170
|
+
return "scheduled, runs daily";
|
|
87
171
|
case "capacity":
|
|
88
172
|
return typeof step.evidence?.remainingUnits === "number"
|
|
89
173
|
? `${step.evidence.remainingUnits} runner slots available`
|
|
90
174
|
: "runners can take work";
|
|
91
175
|
case "settings":
|
|
92
|
-
case "values":
|
|
93
176
|
return "settings reviewed";
|
|
94
|
-
case "
|
|
95
|
-
return
|
|
96
|
-
|
|
177
|
+
case "values":
|
|
178
|
+
return "values set";
|
|
179
|
+
case "first-ticket": {
|
|
180
|
+
const key = value(step, "ticket") ?? value(step, "ticketKey");
|
|
181
|
+
return key
|
|
182
|
+
? `${key} moved to ${dispatchStageName(step.evidence?.stage)}. Catalyst picks it up next.`
|
|
97
183
|
: "first ticket started";
|
|
184
|
+
}
|
|
98
185
|
case "daemon":
|
|
99
186
|
return "local sync is running";
|
|
100
187
|
case "runner":
|
|
@@ -114,15 +201,23 @@ function unfinished(step) {
|
|
|
114
201
|
return "could not verify the connection's permissions";
|
|
115
202
|
if (step.reason === "housekeeping_service_unverified")
|
|
116
203
|
return "no scheduler here. catalyst says when an update is out";
|
|
204
|
+
if (step.reason === "housekeeping_off_chosen")
|
|
205
|
+
return "skipped, as you chose";
|
|
206
|
+
if (step.reason === "housekeeping_no_scheduler")
|
|
207
|
+
return "can't run on this computer";
|
|
208
|
+
if (step.id === "housekeeping")
|
|
209
|
+
return "not scheduled; the install command turns it on";
|
|
117
210
|
if (step.reason === "local_sync_not_selected")
|
|
118
211
|
return "off, reads come from the cloud";
|
|
119
212
|
if (step.reason === "runner_docker_missing")
|
|
120
213
|
return "Docker is not running";
|
|
121
214
|
if (step.reason === "member_scope")
|
|
122
|
-
return "
|
|
215
|
+
return step.id === "linear.team" || step.id === "values"
|
|
216
|
+
? "run catalyst setup --team <KEY> to choose your team"
|
|
217
|
+
: "an admin handles this step";
|
|
123
218
|
if (step.reason === "automation_management_unavailable")
|
|
124
219
|
return "could not read them; checked again before work starts";
|
|
125
|
-
if (step.reason === "interrupted")
|
|
220
|
+
if (step.reason === "interrupted" || step.reason === "first_ticket_skipped")
|
|
126
221
|
return "skipped for now";
|
|
127
222
|
if (TIMED_OUT_REASONS.has(step.reason ?? ""))
|
|
128
223
|
return "the link timed out";
|
|
@@ -134,7 +229,7 @@ function unfinished(step) {
|
|
|
134
229
|
case "github.install":
|
|
135
230
|
return "not installed yet";
|
|
136
231
|
case "accounts":
|
|
137
|
-
return "no
|
|
232
|
+
return "no AI account ready yet";
|
|
138
233
|
case "linear.workspace":
|
|
139
234
|
case "linear.personal":
|
|
140
235
|
case "github.personal":
|
|
@@ -144,8 +239,13 @@ function unfinished(step) {
|
|
|
144
239
|
case "capacity":
|
|
145
240
|
return "no runner can take work yet";
|
|
146
241
|
case "settings":
|
|
147
|
-
case "values":
|
|
148
242
|
return "optional; not reviewed yet";
|
|
243
|
+
case "values":
|
|
244
|
+
return step.reason === "required_values_missing"
|
|
245
|
+
? "a value is missing"
|
|
246
|
+
: "not checked yet";
|
|
247
|
+
case "first-ticket":
|
|
248
|
+
return "no ticket chosen yet";
|
|
149
249
|
default:
|
|
150
250
|
return step.state === "skipped" ? "not chosen" : "not done yet";
|
|
151
251
|
}
|
|
@@ -166,31 +266,192 @@ export function setupStepView(step, journal, required = false) {
|
|
|
166
266
|
: "act";
|
|
167
267
|
let outcome = step.state === "done" ? succeeded(step) : unfinished(step);
|
|
168
268
|
if (mark === "later") {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
269
|
+
const waitsOn = step.reason === "github_install_pending"
|
|
270
|
+
? ["github.install"]
|
|
271
|
+
: (ONBOARD_DEPENDENCIES[step.id] ?? []).filter((id) => {
|
|
172
272
|
const s = journal?.steps.find((s) => s.id === id);
|
|
173
273
|
return s?.state !== "done" && s?.state !== "skipped";
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
274
|
+
});
|
|
275
|
+
// Numbers restart in each part: this part's steps come bare, then each other part once.
|
|
276
|
+
const byPart = new Map();
|
|
277
|
+
for (const id of waitsOn) {
|
|
278
|
+
const n = SETUP_PART_NUMBERS[id];
|
|
279
|
+
if (n === undefined)
|
|
280
|
+
continue;
|
|
281
|
+
const p = SETUP_PART_OF[id];
|
|
282
|
+
const list = byPart.get(p) ?? [];
|
|
283
|
+
if (!list.includes(n))
|
|
284
|
+
byPart.set(p, [...list, n]);
|
|
285
|
+
}
|
|
286
|
+
const own = SETUP_PART_OF[step.id];
|
|
287
|
+
const steps = (ns) => {
|
|
288
|
+
const sorted = [...ns].sort((x, y) => x - y);
|
|
289
|
+
const last = sorted.pop();
|
|
290
|
+
return sorted.length
|
|
291
|
+
? `steps ${sorted.join(", ")} and ${last}`
|
|
292
|
+
: `step ${last}`;
|
|
293
|
+
};
|
|
294
|
+
const groups = [...byPart]
|
|
295
|
+
.sort(([x], [y]) => (x === own ? -1 : y === own ? 1 : x - y))
|
|
296
|
+
.map(([p, ns]) => (p === own ? steps(ns) : `${steps(ns)} of part ${p}`));
|
|
180
297
|
outcome =
|
|
181
298
|
step.state === "pending" && !step.reason
|
|
182
299
|
? "not checked yet"
|
|
183
|
-
:
|
|
184
|
-
? `after ${
|
|
300
|
+
: groups.length
|
|
301
|
+
? `after ${groups.length > 1 ? `${groups.slice(0, -1).join(", ")}, and ${groups.at(-1)}` : groups[0]}`
|
|
185
302
|
: "after an earlier step";
|
|
186
303
|
}
|
|
187
304
|
return {
|
|
188
|
-
number:
|
|
305
|
+
number: SETUP_PART_NUMBERS[step.id] ?? 0,
|
|
189
306
|
title: TITLES[step.id] ?? ONBOARD_TITLES[step.id],
|
|
190
307
|
mark,
|
|
191
308
|
outcome,
|
|
192
309
|
};
|
|
193
310
|
}
|
|
311
|
+
/** The signed-in person's Catalyst role, when the sign-in step recorded one. */
|
|
312
|
+
function signedInRole(journal) {
|
|
313
|
+
const role = journal?.steps.find((s) => s.id === "signin" && s.state === "done")?.evidence?.role;
|
|
314
|
+
return typeof role === "string" ? role : undefined;
|
|
315
|
+
}
|
|
316
|
+
const notOwnerOrAdmin = (journal) => {
|
|
317
|
+
const role = signedInRole(journal);
|
|
318
|
+
return role !== undefined && role !== "owner" && role !== "admin";
|
|
319
|
+
};
|
|
320
|
+
/** The steps readiness says an owner or admin has left (`admin_setup_pending`), if it said so. */
|
|
321
|
+
function readinessAdminSteps(journal) {
|
|
322
|
+
const ready = journal?.steps.find((s) => s.id === "ready");
|
|
323
|
+
const ids = ready?.evidence?.admin;
|
|
324
|
+
return ready?.reason === "admin_setup_pending" && typeof ids === "string"
|
|
325
|
+
? new Set(ids.split(","))
|
|
326
|
+
: new Set();
|
|
327
|
+
}
|
|
328
|
+
/** A member's skip of their own team (or the values it gates): rerun with --team, nobody else. */
|
|
329
|
+
const memberOwnSkip = (step) => step.reason === "member_scope" &&
|
|
330
|
+
(step.id === "linear.team" || step.id === "values");
|
|
331
|
+
/**
|
|
332
|
+
* CTC-4680: a step only an owner or admin of the Catalyst workspace can finish, for the person
|
|
333
|
+
* running setup. A member's run skips every admin step whether or not the admin has done it, so a
|
|
334
|
+
* skip waits on an admin only when readiness names that step. Refusals for the person's role, a
|
|
335
|
+
* member's unverified runner or workflow, and a member's AI account or values do too.
|
|
336
|
+
*/
|
|
337
|
+
export function setupWaitsOnAdmin(step, journal) {
|
|
338
|
+
if (step.state === "done")
|
|
339
|
+
return false;
|
|
340
|
+
const reason = step.reason ?? "";
|
|
341
|
+
if (reason === "member_scope")
|
|
342
|
+
return !memberOwnSkip(step) && readinessAdminSteps(journal).has(step.id);
|
|
343
|
+
if (reason === "github_installation_admin_required")
|
|
344
|
+
return true;
|
|
345
|
+
if (reason === "runner_identity_unverified" ||
|
|
346
|
+
reason === "workflow_identity_unverified")
|
|
347
|
+
return notOwnerOrAdmin(journal);
|
|
348
|
+
// Only reasons an owner or admin clears. A probe that could not finish (a stale read, a login
|
|
349
|
+
// to refresh) is the person's own to check again.
|
|
350
|
+
if ((step.id === "accounts" &&
|
|
351
|
+
(reason === "account_enrollment_required" ||
|
|
352
|
+
reason === "ai_account_not_usable")) ||
|
|
353
|
+
(step.id === "values" && reason === "required_values_missing"))
|
|
354
|
+
return notOwnerOrAdmin(journal);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
/** How far each part has got, by its numbered rows. A row is done when every step in it is done
|
|
358
|
+
* or skipped by choice, needs someone when any of its steps does, and waits on an owner or admin
|
|
359
|
+
* when only they can finish it. */
|
|
360
|
+
export function setupPartProgress(journal, scope,
|
|
361
|
+
/** "run" before readiness is known: a member's admin skips count aside, neither done nor owed.
|
|
362
|
+
* At the end they count done only when readiness passed. */
|
|
363
|
+
moment = "end") {
|
|
364
|
+
const empty = () => ({ total: 0, done: 0, needs: 0, failed: 0, admin: 0, aside: 0 });
|
|
365
|
+
const progress = { 1: empty(), 2: empty(), 3: empty() };
|
|
366
|
+
const readinessPassed = journal.steps.find((s) => s.id === "ready")?.state === "done";
|
|
367
|
+
const rows = new Map();
|
|
368
|
+
for (const id of ONBOARD_STEPS) {
|
|
369
|
+
const number = SETUP_PART_NUMBERS[id];
|
|
370
|
+
if (number === undefined || (scope && !scope.includes(id)))
|
|
371
|
+
continue;
|
|
372
|
+
const step = journal.steps.find((s) => s.id === id) ?? {
|
|
373
|
+
id,
|
|
374
|
+
state: "pending",
|
|
375
|
+
};
|
|
376
|
+
const key = `${SETUP_PART_OF[id]}:${number}`;
|
|
377
|
+
rows.set(key, [
|
|
378
|
+
...(rows.get(key) ?? []),
|
|
379
|
+
{
|
|
380
|
+
// A member's own skip (no --team) is theirs to finish, so it is never counted done.
|
|
381
|
+
mark: memberOwnSkip(step) ? "act" : setupStepView(step, journal).mark,
|
|
382
|
+
kind: setupWaitsOnAdmin(step, journal)
|
|
383
|
+
? "admin"
|
|
384
|
+
: step.reason === "member_scope" &&
|
|
385
|
+
!memberOwnSkip(step) &&
|
|
386
|
+
(moment === "run" || !readinessPassed)
|
|
387
|
+
? "aside"
|
|
388
|
+
: "own",
|
|
389
|
+
},
|
|
390
|
+
]);
|
|
391
|
+
}
|
|
392
|
+
for (const [key, steps] of rows) {
|
|
393
|
+
const part = progress[Number(key.split(":")[0])];
|
|
394
|
+
part.total++;
|
|
395
|
+
const own = steps.filter((s) => s.kind === "own");
|
|
396
|
+
if (own.length === steps.length && own.every((s) => s.mark === "done" || s.mark === "skip"))
|
|
397
|
+
part.done++;
|
|
398
|
+
else if (own.some((s) => s.mark === "act" || s.mark === "fail")) {
|
|
399
|
+
part.needs++;
|
|
400
|
+
if (own.some((s) => s.mark === "fail"))
|
|
401
|
+
part.failed++;
|
|
402
|
+
}
|
|
403
|
+
else if (steps.some((s) => s.kind === "admin"))
|
|
404
|
+
part.admin++;
|
|
405
|
+
else if (steps.some((s) => s.kind === "aside"))
|
|
406
|
+
part.aside++;
|
|
407
|
+
}
|
|
408
|
+
return progress;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The tracker's rows: on the first screen (`plan`, the part about to start), at a boundary
|
|
412
|
+
* (`next`, the part about to start), or at the end. A part with no rows in this run is left out.
|
|
413
|
+
*/
|
|
414
|
+
export function setupTrackerRows(progress, moment) {
|
|
415
|
+
const steps = (n) => `${n} ${n === 1 ? "step" : "steps"}`;
|
|
416
|
+
const waiting = (n) => `${steps(n)} ${n === 1 ? "waits" : "wait"} on an owner or admin`;
|
|
417
|
+
return [1, 2, 3]
|
|
418
|
+
.filter((p) => progress[p].total > 0)
|
|
419
|
+
.map((p) => {
|
|
420
|
+
const { title, estimate, summary } = SETUP_PARTS[p];
|
|
421
|
+
const got = progress[p];
|
|
422
|
+
const row = (mark, note) => ({
|
|
423
|
+
mark,
|
|
424
|
+
number: p,
|
|
425
|
+
title,
|
|
426
|
+
note,
|
|
427
|
+
});
|
|
428
|
+
if (typeof moment === "object" && "plan" in moment)
|
|
429
|
+
return row(p === moment.plan ? "now" : "later", `${estimate} · ${summary}`);
|
|
430
|
+
const finished = got.done === got.total;
|
|
431
|
+
const needsMark = got.failed ? "fail" : "act";
|
|
432
|
+
if (typeof moment === "object") {
|
|
433
|
+
if (p === moment.next)
|
|
434
|
+
return row("now", `next · ${estimate}`);
|
|
435
|
+
if (p > moment.next)
|
|
436
|
+
return row("later", estimate);
|
|
437
|
+
if (finished)
|
|
438
|
+
return row("done", "done");
|
|
439
|
+
const theirs = got.admin + got.aside;
|
|
440
|
+
if (theirs && got.done + theirs === got.total)
|
|
441
|
+
return row("act", `${steps(theirs)} ${theirs === 1 ? "is" : "are"} for an owner or admin`);
|
|
442
|
+
return row(needsMark, `${steps(got.total - got.done)} not finished`);
|
|
443
|
+
}
|
|
444
|
+
if (finished)
|
|
445
|
+
return row("done", "");
|
|
446
|
+
if (got.needs)
|
|
447
|
+
return row(needsMark, `${steps(got.needs)} ${got.needs === 1 ? "needs" : "need"} someone`);
|
|
448
|
+
if (got.admin)
|
|
449
|
+
return row("act", waiting(got.admin));
|
|
450
|
+
if (got.aside)
|
|
451
|
+
return row("act", `${steps(got.aside)} ${got.aside === 1 ? "is" : "are"} for an owner or admin`);
|
|
452
|
+
return row("later", `${got.done} of ${steps(got.total)} done`);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
194
455
|
function page(base, path) {
|
|
195
456
|
try {
|
|
196
457
|
const url = new URL(path, base);
|
|
@@ -206,7 +467,8 @@ function page(base, path) {
|
|
|
206
467
|
}
|
|
207
468
|
function action(step, base, journal) {
|
|
208
469
|
if (CONNECTION_REPAIR_REASONS.has(step.reason ?? "") ||
|
|
209
|
-
step.reason === "github_installation_approval_pending"
|
|
470
|
+
step.reason === "github_installation_approval_pending" ||
|
|
471
|
+
step.reason === "member_scope")
|
|
210
472
|
return onboardReasonText(step, { baseUrl: base, journal });
|
|
211
473
|
const link = (path, verb) => {
|
|
212
474
|
const url = page(base, path);
|
|
@@ -229,7 +491,9 @@ function action(step, base, journal) {
|
|
|
229
491
|
case "github.personal":
|
|
230
492
|
return link("/settings/connected-accounts?connect=github", "connect your GitHub account");
|
|
231
493
|
case "accounts":
|
|
232
|
-
return
|
|
494
|
+
return step.reason === "ai_account_not_usable"
|
|
495
|
+
? onboardReasonText(step, { baseUrl: base, journal })
|
|
496
|
+
: link("/settings/coding-accounts", "add an AI account");
|
|
233
497
|
case "linear.adopt": {
|
|
234
498
|
const team = journal?.steps.find((row) => row.id === "linear.team" && row.state === "done");
|
|
235
499
|
const key = team && value(team, "teamKey");
|
|
@@ -245,24 +509,30 @@ function action(step, base, journal) {
|
|
|
245
509
|
? onboardReasonText(step, { baseUrl: base, journal })
|
|
246
510
|
: "Run catalyst onboard --runner to check whether this computer can take work.";
|
|
247
511
|
case "settings":
|
|
248
|
-
case "values":
|
|
249
512
|
return "Open your repository checkout and review its settings.";
|
|
513
|
+
case "values":
|
|
514
|
+
return onboardReasonText({ ...step, reason: step.reason ?? "required_values_unverified" }, { baseUrl: base, journal });
|
|
250
515
|
case "linear.team":
|
|
251
516
|
return "Choose a Linear team when setup checks again.";
|
|
252
517
|
case "github.repos":
|
|
253
518
|
case "projects":
|
|
254
519
|
return "Choose repositories when setup checks again.";
|
|
255
520
|
case "first-ticket":
|
|
256
|
-
return "
|
|
521
|
+
return onboardReasonText({ ...step, reason: step.reason ?? "first_ticket_choice_required" }, { baseUrl: base, journal });
|
|
257
522
|
default:
|
|
258
523
|
return "Try this step again when setup checks again.";
|
|
259
524
|
}
|
|
260
525
|
}
|
|
261
526
|
export function setupFinalScreen(journal, base, paused = false, required = new Set()) {
|
|
527
|
+
// A member's run skips admin steps; readiness names the ones an admin has not finished yet.
|
|
528
|
+
const readyStep = journal.steps.find((step) => step.id === "ready");
|
|
529
|
+
const adminLeft = new Set(readyStep?.reason === "admin_setup_pending" && typeof readyStep.evidence?.admin === "string"
|
|
530
|
+
? readyStep.evidence.admin.split(",")
|
|
531
|
+
: []);
|
|
262
532
|
const roots = journal.steps
|
|
263
533
|
.filter((step) => {
|
|
264
534
|
const m = setupStepView(step, journal, required.has(step.id)).mark;
|
|
265
|
-
return ((m === "act" || m === "fail") &&
|
|
535
|
+
return ((m === "act" || m === "fail" || adminLeft.has(step.id)) &&
|
|
266
536
|
(ONBOARD_DEPENDENCIES[step.id] ?? []).every((id) => {
|
|
267
537
|
const parent = journal.steps.find((s) => s.id === id);
|
|
268
538
|
return parent?.state === "done" || parent?.state === "skipped";
|
|
@@ -270,7 +540,7 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
270
540
|
})
|
|
271
541
|
.filter((step) => step.id !== "ready")
|
|
272
542
|
.sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id));
|
|
273
|
-
const folded = (id) => id === "projects" ? "github.repos" : id
|
|
543
|
+
const folded = (id) => id === "projects" ? "github.repos" : id;
|
|
274
544
|
const unique = roots.filter((step, i) => roots.findIndex((s) => folded(s.id) === folded(step.id)) === i);
|
|
275
545
|
const actions = unique
|
|
276
546
|
.slice(0, 5)
|
|
@@ -297,16 +567,17 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
297
567
|
: journal.exit === 0
|
|
298
568
|
? "Ready for work"
|
|
299
569
|
: "Not ready for work yet";
|
|
300
|
-
const
|
|
301
|
-
const
|
|
570
|
+
const firstTicket = journal.steps.find((s) => s.id === "first-ticket");
|
|
571
|
+
const ticket = firstTicket?.state === "done" ? firstTicket : undefined;
|
|
572
|
+
const key = ticket && (value(ticket, "ticket") ?? value(ticket, "ticketKey"));
|
|
302
573
|
const savedTicketUrl = ticket && (value(ticket, "url") ?? value(ticket, "ticketUrl"));
|
|
303
574
|
const ticketUrl = savedTicketUrl && page(base, savedTicketUrl);
|
|
304
575
|
const next = paused
|
|
305
576
|
? "Next: run catalyst onboard to continue."
|
|
306
|
-
: key
|
|
307
|
-
? `
|
|
577
|
+
: key
|
|
578
|
+
? `Follow ${ticketUrl ?? key} in Linear; Catalyst comments there as each phase finishes.`
|
|
308
579
|
: heading === "Setup complete"
|
|
309
|
-
?
|
|
580
|
+
? `Next: move a ticket to ${dispatchStageName(firstTicket?.evidence?.stage)} in Linear.`
|
|
310
581
|
: journal.exit === 0 && actions[0]
|
|
311
582
|
? `Next: ${actions[0].text[0].toLowerCase() + actions[0].text.slice(1)}`
|
|
312
583
|
: actions.length === 1
|
|
@@ -317,7 +588,9 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
317
588
|
? "Next: run catalyst onboard after you finish 1 to " +
|
|
318
589
|
actions.length +
|
|
319
590
|
"."
|
|
320
|
-
:
|
|
591
|
+
: readyStep?.reason === "member_team_required"
|
|
592
|
+
? "Next: run catalyst onboard --team <KEY> to check again."
|
|
593
|
+
: "Next: run catalyst onboard to check again.";
|
|
321
594
|
return {
|
|
322
595
|
heading,
|
|
323
596
|
readinessCause,
|
|
@@ -351,8 +624,8 @@ export function setupBrowserInstruction(id, base, opened = false) {
|
|
|
351
624
|
],
|
|
352
625
|
accounts: [
|
|
353
626
|
"/settings/coding-accounts",
|
|
354
|
-
"add
|
|
355
|
-
"Add
|
|
627
|
+
"add an API key",
|
|
628
|
+
"Add an API key",
|
|
356
629
|
],
|
|
357
630
|
};
|
|
358
631
|
const words = copy[id];
|
|
@@ -363,9 +636,6 @@ export function setupBrowserInstruction(id, base, opened = false) {
|
|
|
363
636
|
? `Opened your browser. ${words[2]} there.`
|
|
364
637
|
: `Open this link and ${words[1]}:`,
|
|
365
638
|
url: page(base, words[0]),
|
|
366
|
-
preparation: id === "accounts"
|
|
367
|
-
? "In another terminal, run claude setup-token."
|
|
368
|
-
: undefined,
|
|
369
639
|
};
|
|
370
640
|
}
|
|
371
641
|
/** JSON facts use the same reachable actions as the terminal, while retaining the existing journal. */
|
|
@@ -412,7 +682,9 @@ export function onboardJsonView(journal, base, paused = false, options = {}) {
|
|
|
412
682
|
// CTC-4680: refused by the person's Catalyst role, not by GitHub, so a workspace admin acts.
|
|
413
683
|
step.reason === "github_installation_admin_required" ||
|
|
414
684
|
step.reason?.endsWith("_identity_unverified") ||
|
|
415
|
-
(action.id === "accounts"
|
|
685
|
+
((action.id === "accounts" || action.id === "values") &&
|
|
686
|
+
role !== "owner" &&
|
|
687
|
+
role !== "admin");
|
|
416
688
|
const url = action.text
|
|
417
689
|
.match(/https?:\/\/[^\s<>]+/)?.[0]
|
|
418
690
|
?.replace(/[.,;:!?]+$/, "");
|