@catalyst-cloud/cli 0.15.4 → 0.15.5
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 +4 -0
- package/dist/onboard-headless.js +3 -0
- package/dist/onboard-repositories.js +197 -6
- package/dist/onboard-standalone-copy.js +21 -5
- package/dist/onboard-ui.js +39 -2
- package/dist/onboard.js +10 -2
- 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 +1 -1
- package/skills/run-this-project/SKILL.md +1 -1
- 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 +1 -1
package/CHANGELOG.md
CHANGED
package/dist/onboard-headless.js
CHANGED
|
@@ -242,6 +242,9 @@ export function planOnboardHeadless(args, ctx) {
|
|
|
242
242
|
delete flags["key-file"];
|
|
243
243
|
delete flags.runner;
|
|
244
244
|
delete flags["no-runner"];
|
|
245
|
+
// CATALYST_ONBOARD_HEADLESS=1 alone leaves the flag unset; the plan names the headless input
|
|
246
|
+
// rather than a --no-runner nobody typed (CTC-4739).
|
|
247
|
+
flags.headless = true;
|
|
245
248
|
if (runner === "yes")
|
|
246
249
|
flags.runner = true;
|
|
247
250
|
if (runner === "no")
|
|
@@ -97,6 +97,17 @@ export async function readOnboardRepositoryInventory(ctx, journal, signal) {
|
|
|
97
97
|
const teamId = selectedOnboardTeam(journal);
|
|
98
98
|
if (!teamId)
|
|
99
99
|
return { reason: "team_selection_unverified" };
|
|
100
|
+
// CTC-4742: the per-person repository list names every project a repository belongs to, so a
|
|
101
|
+
// team that uses several repositories shows all of them. A member's list is narrowed to their
|
|
102
|
+
// own access by the server. An older server without the route falls back to the one-per-team
|
|
103
|
+
// registry below.
|
|
104
|
+
const agent = await readExistingOnboardJson(ctx, "/api/v1/agent/repos", signal);
|
|
105
|
+
if (!sameIdentity())
|
|
106
|
+
return { reason: "repository_identity_unverified" };
|
|
107
|
+
if (!("reason" in agent))
|
|
108
|
+
return teamRepositories(ctx, journal, teamId, agent.body, sameIdentity, signal);
|
|
109
|
+
if (agent.status !== 404)
|
|
110
|
+
return { reason: "repository_read_unavailable" };
|
|
100
111
|
const read = await readExistingOnboardJson(ctx, "/api/v1/repos", signal);
|
|
101
112
|
if (!sameIdentity())
|
|
102
113
|
return { reason: "repository_identity_unverified" };
|
|
@@ -171,6 +182,162 @@ export async function readOnboardRepositoryInventory(ctx, journal, signal) {
|
|
|
171
182
|
}
|
|
172
183
|
return { repositories };
|
|
173
184
|
}
|
|
185
|
+
/** The saved login is still the person, account and origin this run started with (and, when
|
|
186
|
+
* `manager`, an owner or admin). Returns that config, or null. */
|
|
187
|
+
function journalIdentity(ctx, journal, manager = false) {
|
|
188
|
+
const cfg = loadConfig(ctx.home);
|
|
189
|
+
return cfg?.user &&
|
|
190
|
+
cfg.account === journal.account &&
|
|
191
|
+
cfg.user.id === journal.membershipId &&
|
|
192
|
+
!!journal.baseUrl &&
|
|
193
|
+
normalizeBaseUrl(cfg.baseUrl) === normalizeBaseUrl(journal.baseUrl) &&
|
|
194
|
+
(!manager || ["owner", "admin"].includes(cfg.user.role))
|
|
195
|
+
? cfg
|
|
196
|
+
: null;
|
|
197
|
+
}
|
|
198
|
+
const REGISTER_PATH = "/api/v1/agent/project-repositories";
|
|
199
|
+
/** The fresh contract's repository IDs and the repositories registered to the chosen team (by its
|
|
200
|
+
* id), or why they can't be trusted. */
|
|
201
|
+
async function contractIds(ctx, journal, teamId, sameIdentity, signal) {
|
|
202
|
+
const read = await readExistingOnboardJson(ctx, "/api/v1/agent/contract", signal);
|
|
203
|
+
if (!sameIdentity())
|
|
204
|
+
return { reason: "repository_identity_unverified" };
|
|
205
|
+
if ("reason" in read)
|
|
206
|
+
return { reason: "repository_contract_unavailable" };
|
|
207
|
+
const doc = object(read.body);
|
|
208
|
+
const account = object(doc?.account);
|
|
209
|
+
const merge = object(doc?.merge);
|
|
210
|
+
if (!doc ||
|
|
211
|
+
account?.id !== journal.account ||
|
|
212
|
+
typeof doc.contractVersion !== "string" ||
|
|
213
|
+
contractVersionInRange(doc.contractVersion, readManifest().tenantContractRange) !== true ||
|
|
214
|
+
!merge ||
|
|
215
|
+
!Array.isArray(merge.repositories) ||
|
|
216
|
+
merge.repositories.length > 10_000 ||
|
|
217
|
+
!Array.isArray(doc.teams) ||
|
|
218
|
+
doc.teams.length > 1_000)
|
|
219
|
+
return { reason: "repository_contract_unverified" };
|
|
220
|
+
// The team's repositories, keyed by the team's id (a team key can be renamed or missing).
|
|
221
|
+
const teams = doc.teams.filter((value) => object(value)?.id === teamId);
|
|
222
|
+
const names = object(object(teams[0])?.repositories)?.registered;
|
|
223
|
+
if (teams.length !== 1 ||
|
|
224
|
+
!Array.isArray(names) ||
|
|
225
|
+
names.length > 1_000 ||
|
|
226
|
+
!names.every((name) => typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(name)))
|
|
227
|
+
return { reason: "team_selection_unverified" };
|
|
228
|
+
const registered = new Set(names.map((name) => name.toLowerCase()));
|
|
229
|
+
const ids = new Map();
|
|
230
|
+
const idNames = new Map();
|
|
231
|
+
for (const value of merge.repositories) {
|
|
232
|
+
const row = object(value);
|
|
233
|
+
if (!row ||
|
|
234
|
+
typeof row.owner !== "string" ||
|
|
235
|
+
!part.test(row.owner) ||
|
|
236
|
+
typeof row.name !== "string" ||
|
|
237
|
+
!part.test(row.name) ||
|
|
238
|
+
!isRepositoryId(row.repoId))
|
|
239
|
+
return { reason: "repository_contract_unverified" };
|
|
240
|
+
const name = key({ owner: row.owner, name: row.name });
|
|
241
|
+
if (idNames.has(row.repoId) && idNames.get(row.repoId) !== name)
|
|
242
|
+
return { reason: "repository_contract_unverified" };
|
|
243
|
+
idNames.set(row.repoId, name);
|
|
244
|
+
ids.set(name, [...(ids.get(name) ?? []), row.repoId]);
|
|
245
|
+
}
|
|
246
|
+
// Registering is offered only when this account's contract lists the route (CTC-4742).
|
|
247
|
+
const canRegister = Array.isArray(doc.routes) &&
|
|
248
|
+
doc.routes.some((value) => {
|
|
249
|
+
const route = object(value);
|
|
250
|
+
return route?.method === "POST" && route.path === REGISTER_PATH;
|
|
251
|
+
});
|
|
252
|
+
return { registered, ids, canRegister };
|
|
253
|
+
}
|
|
254
|
+
/** CTC-4742: every repository an active project of the chosen team uses, with its contract ID. */
|
|
255
|
+
async function teamRepositories(ctx, journal, teamId, body, sameIdentity, signal) {
|
|
256
|
+
const list = object(body);
|
|
257
|
+
if (!list || !Array.isArray(list.repositories) || list.repositories.length > 10_000)
|
|
258
|
+
return { reason: "repository_inventory_shape" };
|
|
259
|
+
const contract = await contractIds(ctx, journal, teamId, sameIdentity, signal);
|
|
260
|
+
if ("reason" in contract)
|
|
261
|
+
return contract;
|
|
262
|
+
const repositories = [];
|
|
263
|
+
const seen = new Set();
|
|
264
|
+
for (const value of list.repositories) {
|
|
265
|
+
const row = object(value);
|
|
266
|
+
if (!row ||
|
|
267
|
+
typeof row.id !== "string" ||
|
|
268
|
+
typeof row.owner !== "string" ||
|
|
269
|
+
!part.test(row.owner) ||
|
|
270
|
+
typeof row.name !== "string" ||
|
|
271
|
+
!part.test(row.name))
|
|
272
|
+
return { reason: "repository_inventory_shape" };
|
|
273
|
+
// The contract says which repositories the team uses; this person's list says which of them
|
|
274
|
+
// they can see (the server narrows it for a member).
|
|
275
|
+
const name = key({ owner: row.owner, name: row.name });
|
|
276
|
+
if (!contract.registered.has(name))
|
|
277
|
+
continue;
|
|
278
|
+
if (seen.has(name))
|
|
279
|
+
return { reason: "repository_binding_ambiguous" };
|
|
280
|
+
seen.add(name);
|
|
281
|
+
const ids = contract.ids.get(name);
|
|
282
|
+
if (!ids || ids.length !== 1 || ids[0] !== row.id)
|
|
283
|
+
return { reason: "repository_id_unverified" };
|
|
284
|
+
repositories.push({ teamId, owner: row.owner, name: row.name, repoId: ids[0], registered: true });
|
|
285
|
+
}
|
|
286
|
+
return { repositories, canRegister: contract.canRegister };
|
|
287
|
+
}
|
|
288
|
+
/** CTC-4742: repositories the GitHub App can reach that the team does not use yet, for an owner or
|
|
289
|
+
* admin only (only they can register one). Any read problem means no offers, never a stopped step. */
|
|
290
|
+
async function offeredRepositories(ctx, journal, teamId, registered, signal) {
|
|
291
|
+
if (!journalIdentity(ctx, journal, true))
|
|
292
|
+
return [];
|
|
293
|
+
const read = await readExistingOnboardJson(ctx, "/api/v1/me/repositories/options", signal);
|
|
294
|
+
if (!journalIdentity(ctx, journal, true) || "reason" in read)
|
|
295
|
+
return [];
|
|
296
|
+
const github = object(object(read.body)?.github);
|
|
297
|
+
if (!github || github.connected !== true || !Array.isArray(github.repositories) || github.repositories.length > 10_000)
|
|
298
|
+
return [];
|
|
299
|
+
const used = new Set(registered.map(key));
|
|
300
|
+
const offers = [];
|
|
301
|
+
for (const value of github.repositories) {
|
|
302
|
+
const row = object(value);
|
|
303
|
+
if (!row || typeof row.owner !== "string" || !part.test(row.owner) || typeof row.name !== "string" || !part.test(row.name))
|
|
304
|
+
return [];
|
|
305
|
+
const name = key({ owner: row.owner, name: row.name });
|
|
306
|
+
if (used.has(name))
|
|
307
|
+
continue;
|
|
308
|
+
used.add(name);
|
|
309
|
+
offers.push({ teamId, owner: row.owner, name: row.name, repoId: null, registered: false });
|
|
310
|
+
}
|
|
311
|
+
return offers;
|
|
312
|
+
}
|
|
313
|
+
/** CTC-4742: register one repository to the team's project. Only adds; nothing here removes one. */
|
|
314
|
+
async function registerToTeam(ctx, journal, teamId, repository, signal) {
|
|
315
|
+
// The person who answered the question is the one who registers: checked right before the write.
|
|
316
|
+
const cfg = journalIdentity(ctx, journal, true);
|
|
317
|
+
if (!cfg)
|
|
318
|
+
return "repository_identity_unverified";
|
|
319
|
+
const expiry = cfg.auth ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() : 0;
|
|
320
|
+
const bearer = cfg.key || (cfg.auth && Number.isFinite(expiry) && expiry > 30_000 ? cfg.auth.accessToken : undefined);
|
|
321
|
+
if (!bearer)
|
|
322
|
+
return "project_login_refresh_required";
|
|
323
|
+
// Bounded like every read here, so a server that never answers can't hold setup.
|
|
324
|
+
const deadline = AbortSignal.timeout(30_000);
|
|
325
|
+
const bounded = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
326
|
+
try {
|
|
327
|
+
const response = await ctx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}${REGISTER_PATH}`, {
|
|
328
|
+
method: "POST",
|
|
329
|
+
redirect: "error",
|
|
330
|
+
signal: bounded,
|
|
331
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json", accept: "application/json" },
|
|
332
|
+
body: JSON.stringify({ teamId, repository }),
|
|
333
|
+
});
|
|
334
|
+
// A response is not evidence: the next read must show the repository on the team.
|
|
335
|
+
return response.status === 200 ? null : "repository_register_unverified";
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return signal?.aborted ? "interrupted" : "repository_register_unverified";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
174
341
|
export function existingRepositoryAdapter(args, choose) {
|
|
175
342
|
let chosen;
|
|
176
343
|
const requested = (repositories, journal) => {
|
|
@@ -218,7 +385,7 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
218
385
|
return {
|
|
219
386
|
state: "done",
|
|
220
387
|
evidence: {
|
|
221
|
-
repository: JSON.stringify(rows),
|
|
388
|
+
repository: JSON.stringify(rows.map(({ teamId, owner, name, repoId }) => ({ teamId, owner, name, repoId }))),
|
|
222
389
|
count: rows.length,
|
|
223
390
|
checkedAt: ctx.now().getTime(),
|
|
224
391
|
},
|
|
@@ -234,6 +401,14 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
234
401
|
return waiting("repository_inventory_empty");
|
|
235
402
|
if (!choose)
|
|
236
403
|
return waiting("repository_choice_required");
|
|
404
|
+
const teamId = list.repositories[0].teamId;
|
|
405
|
+
const offers = list.canRegister
|
|
406
|
+
? await offeredRepositories(ctx, journal, teamId, list.repositories, signal)
|
|
407
|
+
: [];
|
|
408
|
+
const choices = [
|
|
409
|
+
...list.repositories.map((row) => ({ ...row })),
|
|
410
|
+
...offers.map((row) => ({ ...row })),
|
|
411
|
+
];
|
|
237
412
|
let remove = () => { };
|
|
238
413
|
const answer = await new Promise((resolve, reject) => {
|
|
239
414
|
const stopped = () => resolve(null);
|
|
@@ -244,9 +419,8 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
244
419
|
signal?.addEventListener("abort", stopped, { once: true });
|
|
245
420
|
remove = () => signal?.removeEventListener("abort", stopped);
|
|
246
421
|
Promise.resolve()
|
|
247
|
-
|
|
248
|
-
? null
|
|
249
|
-
: choose(list.repositories.map((row) => ({ ...row }))))
|
|
422
|
+
// The hook gets copies: changing one can never change what is matched below.
|
|
423
|
+
.then(() => signal?.aborted ? null : choose(choices.map((row) => ({ ...row }))))
|
|
250
424
|
.then(resolve, reject);
|
|
251
425
|
}).finally(() => remove());
|
|
252
426
|
if (!answer || signal?.aborted)
|
|
@@ -259,9 +433,26 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
259
433
|
const names = answer.map((name) => name.toLowerCase());
|
|
260
434
|
if (new Set(names).size !== names.length)
|
|
261
435
|
return waiting("repository_selection_unverified");
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
436
|
+
const picked = names.map((name) => choices.find((row) => key(row) === name));
|
|
437
|
+
if (!picked.every((row) => !!row))
|
|
264
438
|
return waiting("repository_selection_unverified");
|
|
439
|
+
// CTC-4742: a selected offer is registered to the team. Leaving a registered repository
|
|
440
|
+
// unselected never unregisters it; nothing here removes a repository.
|
|
441
|
+
for (const row of picked) {
|
|
442
|
+
if (row.registered !== false)
|
|
443
|
+
continue;
|
|
444
|
+
const failed = await registerToTeam(ctx, journal, teamId, `${row.owner}/${row.name}`, signal);
|
|
445
|
+
if (failed)
|
|
446
|
+
return waiting(failed);
|
|
447
|
+
}
|
|
448
|
+
const fresh = picked.some((row) => row.registered === false)
|
|
449
|
+
? await readOnboardRepositoryInventory(ctx, journal, signal)
|
|
450
|
+
: list;
|
|
451
|
+
if ("reason" in fresh)
|
|
452
|
+
return waiting(fresh.reason);
|
|
453
|
+
const rows = names.map((name) => fresh.repositories.find((row) => key(row) === name));
|
|
454
|
+
if (!rows.every((row) => !!row))
|
|
455
|
+
return waiting("repository_registration_visibility_pending");
|
|
265
456
|
chosen = rows;
|
|
266
457
|
return check(ctx, journal, signal);
|
|
267
458
|
},
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { setupPartHeading, setupStepView, SETUP_NUMBERS, SETUP_PART_NUMBERS, SETUP_PART_OF, } from "./setup-onboard-copy.js";
|
|
2
2
|
export const COMPUTER_CHECKS = ["machine", "cli", "skills", "legacy"];
|
|
3
|
+
/** The runner row when no runner was chosen up front, naming where that choice came from, so the
|
|
4
|
+
* plan says why no question comes (CTC-4739). */
|
|
5
|
+
const runnerDeclined = (by) => `this computer will not take work, because ${by}`;
|
|
3
6
|
const PLAN = [
|
|
4
7
|
["On this computer", "signin", "you approve once in your browser"],
|
|
5
8
|
["On this computer", "housekeeping", "schedule the daily update"],
|
|
@@ -22,7 +25,14 @@ const PLAN = [
|
|
|
22
25
|
* part. `json` is the plain-text plan an agent reads beside `--json` (dry run, --json on stderr,
|
|
23
26
|
* headless): the older groups and the numbers JSON `actions[].number` reports (CTC-4680).
|
|
24
27
|
*/
|
|
25
|
-
export function standalonePlan(journal, scope,
|
|
28
|
+
export function standalonePlan(journal, scope,
|
|
29
|
+
/** `true` for --runner, `false` for --no-runner, absent when setup will ask (CTC-4739). */
|
|
30
|
+
runner, numbering = "parts",
|
|
31
|
+
/** Why `runner` is false: the flag on an interactive run, the headless input otherwise. */
|
|
32
|
+
declinedBy = "--no-runner was passed") {
|
|
33
|
+
const runnerSelected = runner === true;
|
|
34
|
+
const RUNNER_DECLINED = runnerDeclined(declinedBy);
|
|
35
|
+
const declined = runner === false;
|
|
26
36
|
const json = numbering === "json";
|
|
27
37
|
const includes = (id) => !scope || scope.includes(id);
|
|
28
38
|
const steps = new Map(journal.steps.map((step) => [step.id, step]));
|
|
@@ -53,7 +63,9 @@ export function standalonePlan(journal, scope, runnerSelected = false, numbering
|
|
|
53
63
|
if (includes("runner"))
|
|
54
64
|
row("runner", runnerSelected
|
|
55
65
|
? "start a Catalyst runner here with Docker"
|
|
56
|
-
:
|
|
66
|
+
: declined
|
|
67
|
+
? RUNNER_DECLINED
|
|
68
|
+
: "optional; this computer takes work only if you choose it");
|
|
57
69
|
continue;
|
|
58
70
|
}
|
|
59
71
|
// The terminal gives repository values their own row; the plain-text plan keeps them folded
|
|
@@ -66,9 +78,13 @@ export function standalonePlan(journal, scope, runnerSelected = false, numbering
|
|
|
66
78
|
continue;
|
|
67
79
|
const reviewedDetail = id === "capacity" && includes("runner") && runnerSelected
|
|
68
80
|
? "start a Catalyst runner here with Docker"
|
|
69
|
-
: id === "capacity" &&
|
|
70
|
-
? "
|
|
71
|
-
|
|
81
|
+
: id === "capacity" && includes("runner") && declined
|
|
82
|
+
? includes("capacity")
|
|
83
|
+
? `${detail}; ${RUNNER_DECLINED}`
|
|
84
|
+
: RUNNER_DECLINED
|
|
85
|
+
: id === "capacity" && !includes("capacity")
|
|
86
|
+
? "review the optional runner on this machine; start only if selected"
|
|
87
|
+
: detail;
|
|
72
88
|
rows.push({
|
|
73
89
|
group: json ? group : setupPartHeading(SETUP_PART_OF[id], false),
|
|
74
90
|
part: SETUP_PART_OF[id],
|
package/dist/onboard-ui.js
CHANGED
|
@@ -8,6 +8,12 @@ import { FIRST_TICKET_SAMPLE, FIRST_TICKET_SKIP, } from "./onboard-first-ticket.
|
|
|
8
8
|
import { ONBOARD_STEPS, ONBOARD_TITLES, stepSatisfied, onboardHeaderIdentity, onboardIdentityLines, onboardNextActions, onboardReadyForWork, onboardStepAction, } from "./onboard.js";
|
|
9
9
|
import { onboardStepDetail, onboardReasonText } from "./onboard-next.js";
|
|
10
10
|
import { CONNECT_PAGE_PATH } from "./onboard-checklist.js";
|
|
11
|
+
/** What the runner question's answers mean, said before it is asked (CTC-4739). */
|
|
12
|
+
const RUNNER_EXPLAINED = [
|
|
13
|
+
"Yes makes this computer pick up tickets and run agents in Docker with your workspace's AI accounts. It uses this computer's CPU and memory while it is on, and turning it off or letting it sleep pauses that work.",
|
|
14
|
+
"No is right for a personal workstation: your workspace's runner hosts do the work.",
|
|
15
|
+
"Pass --no-runner or --runner to skip this question next time.",
|
|
16
|
+
];
|
|
11
17
|
export function shouldUseOnboardUi(args, stdoutTty) {
|
|
12
18
|
return (stdoutTty &&
|
|
13
19
|
!args.json &&
|
|
@@ -543,16 +549,35 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
543
549
|
...(prompts.multiselect
|
|
544
550
|
? {
|
|
545
551
|
async chooseRepositories(repositories) {
|
|
552
|
+
const projectsPage = () => {
|
|
553
|
+
try {
|
|
554
|
+
const base = deps.baseUrl?.();
|
|
555
|
+
return base ? ` (${new URL("/settings/projects", base).href})` : "";
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return "";
|
|
559
|
+
}
|
|
560
|
+
};
|
|
546
561
|
stop();
|
|
547
562
|
if (!repositories.length || abort.signal.aborted)
|
|
548
563
|
return null;
|
|
564
|
+
// CTC-4742: what the team already uses starts selected. Unselecting never removes one.
|
|
565
|
+
const selected = repositories
|
|
566
|
+
.filter((row) => row.registered === true)
|
|
567
|
+
.map((row) => `${row.owner}/${row.name}`);
|
|
549
568
|
const answer = await multiselect({
|
|
550
569
|
...options,
|
|
551
|
-
message:
|
|
570
|
+
message: selected.length
|
|
571
|
+
? `Which repositories should use this Linear team? Leaving a repository unselected does not remove it from this team; an admin removes one in Settings → Projects${projectsPage()}.`
|
|
572
|
+
: "Which repositories should use this Linear team?",
|
|
552
573
|
required: true,
|
|
574
|
+
...(selected.length ? { initialValues: selected } : {}),
|
|
553
575
|
options: repositories.map((row) => ({
|
|
554
576
|
value: `${row.owner}/${row.name}`,
|
|
555
577
|
label: `${row.owner}/${row.name}`,
|
|
578
|
+
...(row.registered === false
|
|
579
|
+
? { hint: "not used by this team yet; selecting it adds it" }
|
|
580
|
+
: {}),
|
|
556
581
|
})),
|
|
557
582
|
});
|
|
558
583
|
if (prompts.isCancel(answer)) {
|
|
@@ -636,9 +661,21 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
636
661
|
return typeof answer === "string" ? answer : null;
|
|
637
662
|
},
|
|
638
663
|
async chooseRunner() {
|
|
639
|
-
stop();
|
|
640
664
|
if (abort.signal.aborted)
|
|
641
665
|
return null;
|
|
666
|
+
// CTC-4739: say what each answer does to this computer before asking, and how to skip it.
|
|
667
|
+
stop();
|
|
668
|
+
if (renderer) {
|
|
669
|
+
renderer.begin(number("runner"), title("runner"), "", "ask");
|
|
670
|
+
for (const line of RUNNER_EXPLAINED)
|
|
671
|
+
renderer.detail(line);
|
|
672
|
+
active = true;
|
|
673
|
+
questionOpen = true;
|
|
674
|
+
}
|
|
675
|
+
else
|
|
676
|
+
prompts.log.message(RUNNER_EXPLAINED.join("\n"), {
|
|
677
|
+
output: streams.output,
|
|
678
|
+
});
|
|
642
679
|
const answer = await select({
|
|
643
680
|
...options,
|
|
644
681
|
message: "Run Catalyst's work on this machine?",
|
package/dist/onboard.js
CHANGED
|
@@ -600,7 +600,7 @@ function printPlan(ctx, journal, identity, options) {
|
|
|
600
600
|
ctx.stdout(line);
|
|
601
601
|
let group;
|
|
602
602
|
// Beside --json and headless output: the numbers JSON actions carry (CTC-4680).
|
|
603
|
-
for (const row of standalonePlan(journal, options?.scope, options?.runner, "json")) {
|
|
603
|
+
for (const row of standalonePlan(journal, options?.scope, options?.runner, "json", options?.runnerDeclinedBy)) {
|
|
604
604
|
if (row.group !== group) {
|
|
605
605
|
ctx.stdout("");
|
|
606
606
|
ctx.stdout(row.group);
|
|
@@ -797,7 +797,15 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
797
797
|
for (const id of ONBOARD_STEPS)
|
|
798
798
|
reviewedScope.add(id);
|
|
799
799
|
const planOptions = (journal) => ({
|
|
800
|
-
runner: args.flags.runner === true
|
|
800
|
+
runner: args.flags.runner === true
|
|
801
|
+
? true
|
|
802
|
+
: args.flags["no-runner"] === true
|
|
803
|
+
? false
|
|
804
|
+
: undefined,
|
|
805
|
+
// Headless turns --runner no and CATALYST_ONBOARD_RUNNER=no into --no-runner; say which.
|
|
806
|
+
runnerDeclinedBy: args.flags.headless === true
|
|
807
|
+
? "the headless runner input is no"
|
|
808
|
+
: "--no-runner was passed",
|
|
801
809
|
localSync: args.flags["local-sync"] === true || journal.localSync === true,
|
|
802
810
|
scope: ONBOARD_STEPS.filter((id) => reviewedScope.has(id)),
|
|
803
811
|
});
|
package/package.json
CHANGED
|
@@ -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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — 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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — 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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Onboard me
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Own one Catalyst Cloud project end to end until it closes. Use when the person says "run this project for me", "own this until it ships", "keep this moving", or hands you a project id or a set of tickets to drive. Subscribes to the account's event stream for the scope through the catalyst CLI, reacts to each change in the same turn, makes tickets ready and moves them to dispatch, parks what should stop, chases stalls, escalates inward, and keeps one status summary current. Writes to Linear as the app actor; never polls.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Run this project
|
|
10
10
|
|
package/skills/unstick/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Get a stuck Catalyst Cloud ticket moving again. Use when the person asks "why is this parked and can you release it?", "unpark this", "get things flowing again", "the outage is over, retry what failed", or hands you a ticket that is not moving. Reads why nothing runs and what holds the ticket through the catalyst CLI, decides whether the recorded cause is fixed, previews the release, releases every governor holding the ticket the right way (or one failure class across a team), and raises an ask only for what a person has to do. Never releases a cause it cannot show changed without saying so.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Unstick
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
The human's decision inbox on Catalyst Cloud, and the one way an agent raises a decision on their behalf. Use when the person asks "what needs me?", "what am I blocking?", or when active work is gated on a choice only they can make. Lists open asks ranked by how much open work each one holds, files an ask through the cloud's ask route with the account's own template, and records the answer so the held work releases. Never answers as the human, never duplicates an open ask.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What needs me
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
What environment variable names does this repository need, and where does each one come from? Scans the repository offline (no login, no network) and lists the names in three groups (build/test, deploy-only, bindings), each with where it was found, what uses it, and where a local value would come from. Never reads a value or prints one. Also checks the environment variable table in `.catalyst/catalyst.toml` offline. Use when someone asks "what does catalyst.env.json mean", "what env vars does this repo need", "why does the container need this", or before reviewing or writing the declaration.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What this repository needs
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Use for "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", "how does this work?", "why did it do that?", "how does it prioritise?" and "what does this setting do, where is it set?" in Catalyst Cloud, or asks for something to be done rather than known. Reads the contract via catalyst CLI. Read-only: route work/decisions, never answer as the human.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.5 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|