@catalyst-cloud/cli 0.9.2 → 0.9.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 +14 -0
- package/dist/cli.js +20 -4
- package/dist/config.js +16 -0
- package/dist/execution.js +14 -2
- package/dist/skills.js +13 -1
- 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 +50 -34
- package/skills/catalyst-onboard/references/choosing-a-coding-account.md +52 -0
- package/skills/catalyst-onboard/references/declaring-a-repository.md +4 -2
- package/skills/catalyst-onboard/references/replacing-a-credential.md +5 -1
- package/skills/catalyst-onboard/references/the-one-path.md +52 -86
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +1 -1
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +2 -1
- package/skills/catalyst-onboard/references/who-fixes-what.md +4 -3
- package/skills/catalyst-onboard/scripts/lib/cli.mjs +15 -1
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +195 -34
- package/skills/catalyst-setup/SKILL.md +1 -1
- package/skills/connect-me/SKILL.md +1 -1
- package/skills/how-catalyst-works/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/whats-happening/SKILL.md +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// where-am-i.mjs — how far has setup got?
|
|
2
|
+
// where-am-i.mjs — how far has setup got? EIGHT PARTS, EACH READ BY THE INSTRUMENT THAT OWNS IT, and
|
|
3
|
+
// reported in the order a person can act on them (coding account first, then the integrations, the
|
|
4
|
+
// project, its repository, the person's own connected accounts, the repository's declaration, the host);
|
|
3
5
|
// each finding labelled with the part it belongs to. The whole point of this script is that no part
|
|
4
6
|
// answers for another: a project that is not ready is reported as a project finding with a tenant
|
|
5
7
|
// owner's name on it, never as something the person at this keyboard can fix by running anything.
|
|
@@ -18,8 +20,9 @@ const NOTES = [
|
|
|
18
20
|
"`replica status --probe --json` and `events status --probe --json` (optional local freshness),",
|
|
19
21
|
"`me --json` and personal connection statuses (person), `contract --path …` for the account, the projects and the repositories,",
|
|
20
22
|
"`contract --path codingAccounts` and `accounts --json` (coding accounts, and which one needs a new credential), and each",
|
|
21
|
-
"project's hosts_current check with its fixedWhere (host).",
|
|
23
|
+
"project's hosts_current check with its fixedWhere (host), and each project's environment_declared check with its per-repository notes (repository declarations).",
|
|
22
24
|
"Also runs `gh repo view <owner>/thoughts` for each registered repository's owner, as a note: it shows the repository exists, never that the GitHub App can reach it.",
|
|
25
|
+
"When the tenant serves a contract version this CLI refuses, runs `npm view @catalyst-cloud/cli version` once to say whether a newer CLI is published.",
|
|
23
26
|
"Writes nothing and changes nothing. Runs before this machine is connected — that is one of the states it reports.",
|
|
24
27
|
];
|
|
25
28
|
|
|
@@ -45,8 +48,28 @@ let repositoryRegistered = false;
|
|
|
45
48
|
// `blocking` is false for a finding that is real and reportable but does not stop the next step —
|
|
46
49
|
// an unmatched Linear identity is the one that matters: it must be said, and it must not become the
|
|
47
50
|
// thing the person is told to go and do before they can map a project.
|
|
48
|
-
const add = (part, instrument, verdict, lines, owner = null, where = null, blocking = true) =>
|
|
49
|
-
|
|
51
|
+
const add = (part, instrument, verdict, lines, owner = null, where = null, blocking = true) => {
|
|
52
|
+
const p = { part, instrument, verdict, lines, owner, where, blocking };
|
|
53
|
+
parts.push(p);
|
|
54
|
+
return p;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// A tenant contract outside this CLI's range makes every contract read fail the same way. When that
|
|
58
|
+
// happens, no part read from the contract can back a next step, and the only honest next step is the
|
|
59
|
+
// version itself. The CLI's own hint says to update, which is wrong when no newer CLI is published.
|
|
60
|
+
let contractMismatch = null;
|
|
61
|
+
const noteMismatch = (text) => {
|
|
62
|
+
const m =
|
|
63
|
+
(text ?? "").match(/serves contract version (\S+) but this bundle accepts (\S+)/) ??
|
|
64
|
+
(text ?? "").match(/contract: version (\S+) is outside this bundle's range (\S+)/);
|
|
65
|
+
if (m !== null) contractMismatch ??= { served: m[1], accepts: m[2] };
|
|
66
|
+
return m !== null;
|
|
67
|
+
};
|
|
68
|
+
const readContract = (args) => {
|
|
69
|
+
const r = runCli(args);
|
|
70
|
+
noteMismatch(r.stderr);
|
|
71
|
+
return r;
|
|
72
|
+
};
|
|
50
73
|
|
|
51
74
|
// ── machine ───────────────────────────────────────────────────────────────────────────────────────
|
|
52
75
|
const status = runCli(["status"]);
|
|
@@ -87,10 +110,13 @@ if (connected) {
|
|
|
87
110
|
projectChecks = checks.filter((c) => typeof c.id === "string" && c.id.startsWith("team:"));
|
|
88
111
|
const machineChecks = checks.filter((c) => !(typeof c.id === "string" && c.id.startsWith("team:")));
|
|
89
112
|
const failed = machineChecks.filter((c) => !c.ok && !c.note);
|
|
90
|
-
|
|
113
|
+
// A refused contract version is printed without the CLI's "update" fix: whether an update exists
|
|
114
|
+
// is checked below, once, against what npm has published.
|
|
115
|
+
const refused = (c) => !c.ok && !c.note && noteMismatch(c.line);
|
|
116
|
+
for (const c of machineChecks) machineLines.push(`${c.note ? "note" : c.ok ? "ok " : "FAIL"} ${c.line}${!c.ok && !c.note && c.fix && !refused(c) ? ` — fix: ${c.fix}` : ""}`);
|
|
91
117
|
if (failed.length > 0) {
|
|
92
118
|
machineVerdict = "unfinished";
|
|
93
|
-
machineFix = failed.find((c) => typeof c.fix === "string")?.fix ?? null;
|
|
119
|
+
machineFix = failed.find((c) => typeof c.fix === "string" && !refused(c))?.fix ?? null;
|
|
94
120
|
}
|
|
95
121
|
}
|
|
96
122
|
// Supplemental only: local caches are optional and do not change setup completion or --next.
|
|
@@ -111,7 +137,8 @@ add(
|
|
|
111
137
|
machineVerdict,
|
|
112
138
|
machineLines,
|
|
113
139
|
"you, on this machine",
|
|
114
|
-
|
|
140
|
+
// The keyless login alone: a person who holds a personal key already knows the key form.
|
|
141
|
+
connected ? null : CONNECT_LINE.split(" (or")[0],
|
|
115
142
|
);
|
|
116
143
|
|
|
117
144
|
// ── person ────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -122,7 +149,8 @@ if (!connected) {
|
|
|
122
149
|
const doc = tryJson(me.stdout);
|
|
123
150
|
const user = doc && typeof doc.user === "object" && doc.user !== null ? doc.user : null;
|
|
124
151
|
if (doc === null) {
|
|
125
|
-
add("person", "catalyst-skills me", "unreadable", [`me could not be read (${(me.stderr || me.stdout).trim().split("\n")[0] ?? "no output"})`], null, null)
|
|
152
|
+
add("person", "catalyst-skills me", "unreadable", [`me could not be read (${(me.stderr || me.stdout).trim().split("\n")[0] ?? "no output"})`], null, null).next =
|
|
153
|
+
"run this again; catalyst-skills me could not be read, so no step after it can be named yet";
|
|
126
154
|
} else if (user === null) {
|
|
127
155
|
add(
|
|
128
156
|
"person",
|
|
@@ -161,7 +189,7 @@ if (!connected) {
|
|
|
161
189
|
"catalyst-skills me, and catalyst-skills connections personal <provider> status --json",
|
|
162
190
|
matched && !personalGrantIncomplete ? "ok" : "unfinished",
|
|
163
191
|
[`${user.label ?? "(unnamed)"} (${user.role ?? "role unknown"})`, matched ? "Linear identity matched" : "Linear identity NOT matched — asks assigned to you cannot be told apart from everyone else's. It blocks nothing below. Run catalyst-skills identity linear options for self-service recovery; personal Linear consent normally binds its viewer automatically.", ...grantLines],
|
|
164
|
-
personalGrantIncomplete ? "you" : matched ? null : "a
|
|
192
|
+
personalGrantIncomplete ? "you" : matched ? null : "a workspace owner or admin",
|
|
165
193
|
personalGrantIncomplete ? "catalyst-skills connections personal <provider> start or status" : matched ? null : link("/settings/account"),
|
|
166
194
|
false,
|
|
167
195
|
);
|
|
@@ -172,10 +200,11 @@ if (!connected) {
|
|
|
172
200
|
if (!connected) {
|
|
173
201
|
add("account", "catalyst-skills contract --path account", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
174
202
|
} else {
|
|
175
|
-
const acct =
|
|
203
|
+
const acct = readContract(["contract", "--path", "account", "--json"]);
|
|
176
204
|
const doc = tryJson(acct.stdout);
|
|
177
205
|
if (doc === null) {
|
|
178
|
-
add("account", "catalyst-skills contract --path account", "unreadable", ["the account block could not be read — try: catalyst-skills contract --refresh"], null, null)
|
|
206
|
+
add("account", "catalyst-skills contract --path account", "unreadable", ["the account block could not be read — try: catalyst-skills contract --refresh"], null, null).next =
|
|
207
|
+
"refresh the contract (catalyst-skills contract --refresh), then run this again; the account could not be read, so no step after it can be named yet";
|
|
179
208
|
} else {
|
|
180
209
|
const workspace = typeof doc.linearWorkspaceSlug === "string" && doc.linearWorkspaceSlug !== "" ? doc.linearWorkspaceSlug : typeof doc.linearWorkspaceId === "string" && doc.linearWorkspaceId !== "" ? doc.linearWorkspaceId : null;
|
|
181
210
|
workspaceResolved = workspace !== null;
|
|
@@ -204,7 +233,7 @@ if (!connected) {
|
|
|
204
233
|
"the GitHub App install is NOT carried here — the connections page is the only place that shows it",
|
|
205
234
|
...envLines,
|
|
206
235
|
],
|
|
207
|
-
"a
|
|
236
|
+
"a workspace owner or admin",
|
|
208
237
|
link("/settings/connections"),
|
|
209
238
|
);
|
|
210
239
|
}
|
|
@@ -216,12 +245,13 @@ let teamRows = null;
|
|
|
216
245
|
if (!connected) {
|
|
217
246
|
add("projects", "catalyst-skills contract --path teams", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
218
247
|
} else {
|
|
219
|
-
const teams =
|
|
248
|
+
const teams = readContract(["contract", "--path", "teams", "--json"]);
|
|
220
249
|
const doc = tryJson(teams.stdout);
|
|
221
250
|
const rows = Array.isArray(doc) ? doc : null;
|
|
222
251
|
teamRows = rows;
|
|
223
252
|
if (rows === null) {
|
|
224
|
-
add("projects", "catalyst-skills contract --path teams", "unreadable", ["the project list could not be read — try: catalyst-skills contract --refresh"], null, null)
|
|
253
|
+
add("projects", "catalyst-skills contract --path teams", "unreadable", ["the project list could not be read — try: catalyst-skills contract --refresh"], null, null).next =
|
|
254
|
+
"refresh the contract (catalyst-skills contract --refresh), then run this again; the project list could not be read, so no step after it can be named yet";
|
|
225
255
|
} else {
|
|
226
256
|
const lines = [`${rows.length} mapped`];
|
|
227
257
|
for (const t of rows) {
|
|
@@ -243,7 +273,7 @@ if (!connected) {
|
|
|
243
273
|
}
|
|
244
274
|
}
|
|
245
275
|
const ready = rows.length > 0 && rows.every(setUp);
|
|
246
|
-
add("projects", "catalyst-skills contract --path teams, and the team: checks of ready", ready ? "ok" : "unfinished", lines, "a
|
|
276
|
+
add("projects", "catalyst-skills contract --path teams, and the team: checks of ready", ready ? "ok" : "unfinished", lines, "a workspace owner or admin", link("/settings/linear-teams"));
|
|
247
277
|
}
|
|
248
278
|
}
|
|
249
279
|
|
|
@@ -262,17 +292,18 @@ function thoughtsNote(owner) {
|
|
|
262
292
|
if (!connected) {
|
|
263
293
|
add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
264
294
|
} else {
|
|
265
|
-
const repos =
|
|
295
|
+
const repos = readContract(["contract", "--path", "merge.repositories", "--json"]);
|
|
266
296
|
const doc = tryJson(repos.stdout);
|
|
267
297
|
const rows = Array.isArray(doc) ? doc : null;
|
|
268
298
|
if (rows === null) {
|
|
269
|
-
add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["the repository list could not be read — try: catalyst-skills contract --refresh"], null, null)
|
|
299
|
+
add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["the repository list could not be read — try: catalyst-skills contract --refresh"], null, null).next =
|
|
300
|
+
"refresh the contract (catalyst-skills contract --refresh), then run this again; the repository list could not be read, so no step after it can be named yet";
|
|
270
301
|
} else {
|
|
271
302
|
const lines = [`${rows.length} registered`, ...rows.map((r) => `${r.owner ?? "?"}/${r.name ?? "?"}`)];
|
|
272
303
|
repositoryRegistered = rows.length > 0;
|
|
273
304
|
lines.push("⛔ REGISTRATION only. This carries no status and no project attachment, so it never proves a repository can be dispatched into.");
|
|
274
305
|
for (const owner of [...new Set(rows.map((r) => r.owner).filter((o) => typeof o === "string" && o !== ""))]) lines.push(thoughtsNote(owner));
|
|
275
|
-
add("repositories", "catalyst-skills contract --path merge.repositories", rows.length > 0 ? "ok" : "unfinished", lines, "a
|
|
306
|
+
add("repositories", "catalyst-skills contract --path merge.repositories", rows.length > 0 ? "ok" : "unfinished", lines, "a workspace owner or admin", link("/settings/repositories"));
|
|
276
307
|
}
|
|
277
308
|
}
|
|
278
309
|
|
|
@@ -282,13 +313,29 @@ if (!connected) {
|
|
|
282
313
|
// The contract says the state, its sentence, who enrolls one and on which page. An older cloud's
|
|
283
314
|
// contract has no `codingAccounts`, and only then is the owner assumed.
|
|
284
315
|
let accountsNext = "enrol a coding account a phase can run on";
|
|
316
|
+
// A note --next prints beside the next step: an ended or cancelled account is not unfinished, but the
|
|
317
|
+
// person must hear that it is kept for reporting and never re-tokened, before anyone reaches for a setup token.
|
|
318
|
+
let retireNote = null;
|
|
285
319
|
// The contract counts accounts by declared rotation, so one healthy account can hide another whose
|
|
286
320
|
// credential is dead. When it says `enrolled`, each account is read as well. An account needs a new
|
|
287
321
|
// credential when it is quarantined, expired or revoked, or when its last polls failed on the
|
|
288
|
-
// credential itself. The
|
|
289
|
-
|
|
322
|
+
// credential itself. The cloud's own verdict, `needsCredential`, wins when it is sent:
|
|
323
|
+
// it leaves out ended and revoked accounts and usage-endpoint refusals, which are not verdicts on the
|
|
324
|
+
// credential. Older clouds send only the poll fields, read with the same narrowed code list.
|
|
325
|
+
const CREDENTIAL_ERROR_CODES = new Set(["no_access_token", "no_credential"]);
|
|
290
326
|
const CREDENTIAL_FAILURE_STREAK = 3;
|
|
327
|
+
// A cancelled subscription or an ended account is not a credential problem: no token brings it
|
|
328
|
+
// back. It is kept for reporting, never re-credentialed, and it does not make setup unfinished.
|
|
329
|
+
const isRetirable = (a) => a?.renewalStatus === "canceled" || a?.status === "ended" || a?.declaredState === "ended";
|
|
291
330
|
const credentialProblem = (a) => {
|
|
331
|
+
if (isRetirable(a)) return null;
|
|
332
|
+
if (typeof a?.needsCredential === "boolean") {
|
|
333
|
+
if (!a.needsCredential) return null;
|
|
334
|
+
if (a.quarantined === true) return `quarantined${typeof a.quarantineReason === "string" && a.quarantineReason !== "" ? `: ${a.quarantineReason}` : ""}`;
|
|
335
|
+
if (a.status === "expired-or-revoked") return "expired or revoked";
|
|
336
|
+
return typeof a.lastPollErrorCode === "string" ? `its last ${a.pollFailureCount} polls failed with ${a.lastPollErrorCode}` : "the cloud says it needs a new credential";
|
|
337
|
+
}
|
|
338
|
+
if (a?.status === "ended" || (a?.revokedAtMs ?? null) !== null) return null;
|
|
292
339
|
if (a?.quarantined === true) return `quarantined${typeof a.quarantineReason === "string" && a.quarantineReason !== "" ? `: ${a.quarantineReason}` : ""}`;
|
|
293
340
|
if (a?.status === "expired-or-revoked") return "expired or revoked";
|
|
294
341
|
if (CREDENTIAL_ERROR_CODES.has(a?.lastPollErrorCode) && typeof a?.pollFailureCount === "number" && a.pollFailureCount >= CREDENTIAL_FAILURE_STREAK) {
|
|
@@ -305,7 +352,7 @@ const readAccountRows = () => {
|
|
|
305
352
|
if (!connected) {
|
|
306
353
|
add("coding accounts", "codingAccounts in catalyst-skills contract", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
307
354
|
} else {
|
|
308
|
-
const res =
|
|
355
|
+
const res = readContract(["contract", "--path", "codingAccounts", "--json"]);
|
|
309
356
|
const ca = tryJson(res.stdout);
|
|
310
357
|
const older = ca === null && /has nothing at/.test(res.stderr ?? "");
|
|
311
358
|
const INSTRUMENT = "codingAccounts in catalyst-skills contract";
|
|
@@ -318,18 +365,22 @@ if (!connected) {
|
|
|
318
365
|
const head = [line, `${ca.activeCount ?? "?"} active`];
|
|
319
366
|
const { rows, error } = readAccountRows();
|
|
320
367
|
const dead = rows === null ? [] : rows.filter((a) => credentialProblem(a) !== null);
|
|
368
|
+
const retirable = rows === null ? [] : rows.filter(isRetirable);
|
|
369
|
+
const who = (a) => [a.label, a.email].find((v) => typeof v === "string" && v !== "");
|
|
370
|
+
const retireLine = retirable.length === 0 ? [] : [`${retirable.length} account(s) cancelled or ended (${retirable.map((a) => `${who(a) ?? a.displayName ?? a.accountSlot ?? "?"} (${a.provider ?? "unknown provider"})`).join(", ")}): kept for reporting, not used, and not counted here. Never replace their credential; reactivate one on the AI accounts page only if its subscription is live again.`];
|
|
371
|
+
retireNote = retireLine[0] ?? null;
|
|
321
372
|
if (rows === null) {
|
|
322
373
|
accountsNext = "read the coding accounts again; each account's credential could not be checked";
|
|
323
374
|
add("coding accounts", DETAIL, "unreadable", [...head, `coding accounts could not be checked in detail (${error}). Whether each one still has a working credential is unknown. Do not enroll one on this reading.`], null, null);
|
|
324
375
|
} else if (dead.length > 0) {
|
|
325
|
-
const name = (a) => `${a.provider ?? "unknown provider"} account ${a.accountSlot ?? "?"}`;
|
|
376
|
+
const name = (a) => `${a.provider ?? "unknown provider"} account ${a.accountSlot ?? "?"}${who(a) ? ` (${who(a)})` : ""}`;
|
|
326
377
|
const more = dead.length > 1 ? ` (and ${dead.length - 1} more)` : "";
|
|
327
378
|
accountsNext = `${name(dead[0])} needs a new credential${more}. Replace it on the AI accounts page (Settings → AI accounts → the account → Replace credential). Do not enroll another account.`;
|
|
328
379
|
add(
|
|
329
380
|
"coding accounts",
|
|
330
381
|
DETAIL,
|
|
331
382
|
"unfinished",
|
|
332
|
-
[...head, ...dead.map((a) => `${name(a)} needs a new credential: ${credentialProblem(a)}`), "Replace its credential on its own page. Do not enroll another account. The steps are in references/replacing-a-credential.md."],
|
|
383
|
+
[...head, ...dead.map((a) => `${name(a)} needs a new credential: ${credentialProblem(a)}`), "Replace its credential on its own page, with a credential minted from THAT account: check the login you mint from matches the account named here first. Do not enroll another account. The steps are in references/replacing-a-credential.md.", ...retireLine],
|
|
333
384
|
owner,
|
|
334
385
|
where,
|
|
335
386
|
);
|
|
@@ -337,7 +388,7 @@ if (!connected) {
|
|
|
337
388
|
accountsNext = "replace the credential of the coding account the page marks; do not enroll another account";
|
|
338
389
|
add("coding accounts", DETAIL, "unfinished", [...head, "The contract says an account needs a new credential, and the account list does not say which. Open the page and look for it."], owner, where);
|
|
339
390
|
} else {
|
|
340
|
-
add("coding accounts", DETAIL, "ok", [...head, `${rows.length} checked, none needs a new credential
|
|
391
|
+
add("coding accounts", DETAIL, "ok", [...head, `${rows.length} checked, none needs a new credential`, ...retireLine]);
|
|
341
392
|
}
|
|
342
393
|
} else if (ca.state === "inactive") {
|
|
343
394
|
accountsNext = "reactivate a coding account that is out of rotation; do not enroll another one";
|
|
@@ -353,6 +404,7 @@ if (!connected) {
|
|
|
353
404
|
const { rows, error } = readAccountRows();
|
|
354
405
|
const olderLine = "this cloud is older than the bundle: its contract does not say whether a coding account is enrolled, so the account list is read instead";
|
|
355
406
|
if (rows === null) {
|
|
407
|
+
accountsNext = "read the coding accounts again; the account list could not be read this time";
|
|
356
408
|
add("coding accounts", "catalyst-skills accounts", "unreadable", [olderLine, `coding accounts could not be read (${error})`], null, null);
|
|
357
409
|
} else {
|
|
358
410
|
// An expired or revoked slot, or a quarantined one, cannot take work until an admin acts on it.
|
|
@@ -360,7 +412,7 @@ if (!connected) {
|
|
|
360
412
|
const lines = [olderLine, `${rows.length} enrolled, ${usable.length} able to take work`];
|
|
361
413
|
for (const a of rows) lines.push(`${a.accountSlot ?? "?"}: ${a.provider ?? "?"}, ${a.status ?? "status unknown"}${a.quarantined ? ", quarantined" : ""}`);
|
|
362
414
|
if (usable.length === 0) lines.push("no phase can start until one is enrolled and able to take work");
|
|
363
|
-
add("coding accounts", "catalyst-skills accounts", usable.length > 0 ? "ok" : "unfinished", lines, "a
|
|
415
|
+
add("coding accounts", "catalyst-skills accounts", usable.length > 0 ? "ok" : "unfinished", lines, "a workspace owner or admin", link("/settings/coding-accounts"));
|
|
364
416
|
}
|
|
365
417
|
} else {
|
|
366
418
|
accountsNext = "read the coding accounts again; the contract could not be read";
|
|
@@ -376,7 +428,7 @@ if (!connected) {
|
|
|
376
428
|
add("host", "hosts_current in catalyst-skills contract --path teams", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
377
429
|
} else {
|
|
378
430
|
const checks = (teamRows ?? []).flatMap((t) => (Array.isArray(t.readiness?.checks) ? t.readiness.checks : []).filter((c) => c.id === "hosts_current").map((c) => ({ ...c, team: t.key ?? t.id ?? "(unkeyed)" })));
|
|
379
|
-
const meta = tryJson(
|
|
431
|
+
const meta = tryJson(readContract(["contract", "--path", "readinessChecks", "--json"]).stdout);
|
|
380
432
|
const row = Array.isArray(meta) ? meta.find((r) => r.id === "hosts_current") : null;
|
|
381
433
|
const hc = checks.find((c) => c.state === "fail") ?? checks.find((c) => c.state === "unknown") ?? checks[0] ?? null;
|
|
382
434
|
// The contract's printed line for this owner is written for a host that is behind. When no host is
|
|
@@ -392,7 +444,10 @@ if (!connected) {
|
|
|
392
444
|
const how = fw === null ? null : link(fw.page);
|
|
393
445
|
const howLines = fw?.command ? [`the owner runs: ${fw.command}`] : [];
|
|
394
446
|
if (hc === null) {
|
|
395
|
-
add("host", "hosts_current in catalyst-skills contract --path teams", "unreadable", [teamRows === null ? "the project list could not be read, so the host check cannot be either" : "no project has a readiness check yet, so whether a host is connected cannot be read. Press Re-check on the projects page."], "a
|
|
447
|
+
add("host", "hosts_current in catalyst-skills contract --path teams", "unreadable", [teamRows === null ? "the project list could not be read, so the host check cannot be either" : "no project has a readiness check yet, so whether a host is connected cannot be read. Press Re-check on the projects page."], "a workspace owner or admin", link("/settings/linear-teams")).next =
|
|
448
|
+
teamRows === null
|
|
449
|
+
? "refresh the contract (catalyst-skills contract --refresh), then run this again; the project list could not be read, so the host check cannot be either"
|
|
450
|
+
: "press Re-check on the projects page, then run this again";
|
|
396
451
|
} else if (hc.state === "pass") {
|
|
397
452
|
add("host", "hosts_current in catalyst-skills contract --path teams", "ok", [`hosts_current pass (read on ${hc.team})`]);
|
|
398
453
|
} else {
|
|
@@ -401,6 +456,70 @@ if (!connected) {
|
|
|
401
456
|
}
|
|
402
457
|
}
|
|
403
458
|
|
|
459
|
+
// ── repository declarations ───────────────────────────────────────────────────────────────────────
|
|
460
|
+
// A repository's own settings live in its committed `.catalyst/catalyst.toml`. Whether the project's
|
|
461
|
+
// default repository has one in effect is the contract's `environment_declared` check; the project's
|
|
462
|
+
// other repositories appear under that check's `repos`. Names only: nothing here reads a value, and
|
|
463
|
+
// nothing here reads a file on this machine. A cloud that sends no such check has nothing to read.
|
|
464
|
+
const DECL_INSTRUMENT = "environment_declared in catalyst-skills contract --path teams";
|
|
465
|
+
const DECL_REASONS = {
|
|
466
|
+
no_team_repo_default: { text: "no repository is the project's default yet: register one and make it the default", who: "a workspace owner or admin", page: "/settings/repositories" },
|
|
467
|
+
no_environment_declaration: { text: "no .catalyst/catalyst.toml on its default branch yet: write it with the person (names only, never a value), open a pull request, merge it", who: "the person, in the repository, with your help", page: null },
|
|
468
|
+
declaration_invalid: { text: "the committed .catalyst/catalyst.toml did not validate: fix the file (the ingest names the error) and merge the fix", who: "the person, in the repository, with your help", page: null },
|
|
469
|
+
declaration_read_failed: { text: "the committed .catalyst/catalyst.toml could not be read: fix the file (the ingest names the error) and merge the fix", who: "the person, in the repository, with your help", page: null },
|
|
470
|
+
declaration_awaiting_approval: { text: "the declaration is proposed and waits for approval: Settings → Repositories → the repository → Environment → Setup declaration → Approve this revision", who: "a workspace owner or admin", page: "/settings/repositories" },
|
|
471
|
+
};
|
|
472
|
+
const DECL_DO = "write .catalyst/catalyst.toml with references/declaring-a-repository.md, then open a pull request";
|
|
473
|
+
let declNext = "commit .catalyst/catalyst.toml to the repository and have an owner or admin approve it";
|
|
474
|
+
if (!connected) {
|
|
475
|
+
add("repository declarations", DECL_INSTRUMENT, "unreadable", ["not readable until this machine is connected"], null, null);
|
|
476
|
+
} else if (teamRows === null) {
|
|
477
|
+
add("repository declarations", DECL_INSTRUMENT, "unreadable", ["the project list could not be read, so no repository declaration can be either"], null, null).next =
|
|
478
|
+
"refresh the contract (catalyst-skills contract --refresh), then run this again; the project list could not be read, so no repository declaration can be either";
|
|
479
|
+
} else if (teamRows.length === 0) {
|
|
480
|
+
add("repository declarations", DECL_INSTRUMENT, "unreadable", ["no project is mapped yet, so there is no repository whose declaration could be read"], null, null).next =
|
|
481
|
+
"map a project first; a repository declaration is read per project";
|
|
482
|
+
} else if (teamRows.every((t) => !Array.isArray(t.readiness?.checks) || t.readiness.checks.length === 0)) {
|
|
483
|
+
// Mapped, never checked: the contract carries no checks at all until someone presses Re-check, and
|
|
484
|
+
// that silence is not "nothing to declare".
|
|
485
|
+
add("repository declarations", DECL_INSTRUMENT, "unreadable", ["no project has a readiness check yet, so whether its repository is declared cannot be read. Press Re-check on the projects page."], "a workspace owner or admin", link("/settings/linear-teams")).next =
|
|
486
|
+
"press Re-check on the projects page, then run this again";
|
|
487
|
+
} else {
|
|
488
|
+
const found = teamRows.flatMap((t) => (Array.isArray(t.readiness?.checks) ? t.readiness.checks : []).filter((c) => c.id === "environment_declared").map((c) => ({ ...c, team: t.key ?? t.id ?? "(unkeyed)" })));
|
|
489
|
+
const lines = [];
|
|
490
|
+
let verdict = "ok";
|
|
491
|
+
let owner = null;
|
|
492
|
+
let where = null;
|
|
493
|
+
let unknown = false;
|
|
494
|
+
const describe = (reason) => DECL_REASONS[reason] ?? { text: `environment_declared ${reason ?? "failed"} (a reason this bundle does not know; read it on the page)`, who: "a workspace owner or admin", page: "/settings/repositories" };
|
|
495
|
+
const flag = (team, repo, reason) => {
|
|
496
|
+
const r = describe(reason);
|
|
497
|
+
lines.push(`${team}${repo ? `, ${repo}` : ""}: ${r.text}`);
|
|
498
|
+
if (verdict === "ok") declNext = `${repo ? `${repo}: ` : ""}${r.text}`;
|
|
499
|
+
verdict = "unfinished";
|
|
500
|
+
owner ??= r.who;
|
|
501
|
+
where ??= r.page === null ? DECL_DO : link(r.page);
|
|
502
|
+
};
|
|
503
|
+
if (found.length === 0) {
|
|
504
|
+
lines.push("this cloud reports no repository declaration check for the mapped projects, so there is nothing to read here; the repository's Environment page is where one would show");
|
|
505
|
+
}
|
|
506
|
+
for (const c of found) {
|
|
507
|
+
if (c.state === "pass") lines.push(`${c.team}: a declaration is in effect for the project's default repository`);
|
|
508
|
+
else if (c.state === "fail") flag(c.team, null, c.reason);
|
|
509
|
+
else {
|
|
510
|
+
unknown = true;
|
|
511
|
+
lines.push(`${c.team}: environment_declared could not be read${c.reason ? ` (${c.reason})` : ""}; press Re-check on the projects page`);
|
|
512
|
+
}
|
|
513
|
+
for (const note of Array.isArray(c.repos) ? c.repos : []) if (typeof note?.repo === "string" && note.reason) flag(c.team, note.repo, note.reason);
|
|
514
|
+
}
|
|
515
|
+
if (unknown && verdict === "ok") {
|
|
516
|
+
add("repository declarations", DECL_INSTRUMENT, "unreadable", lines, "a workspace owner or admin", link("/settings/linear-teams")).next = "press Re-check on the projects page, then run this again";
|
|
517
|
+
} else {
|
|
518
|
+
if (verdict !== "ok") lines.push("Names only: a value never passes through this script or the file. The person enters values on the repository's Environment page.");
|
|
519
|
+
add("repository declarations", DECL_INSTRUMENT, verdict, lines, verdict === "ok" ? null : owner, verdict === "ok" ? null : where);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
404
523
|
// ── the single next step ──────────────────────────────────────────────────────────────────────────
|
|
405
524
|
// The machine's next action is not one sentence: an unconnected machine needs the login, and a
|
|
406
525
|
// connected one needs whatever check failed — and `ready` already printed that check's own fix, so
|
|
@@ -417,12 +536,20 @@ const NEXT = {
|
|
|
417
536
|
: personalGithubIncomplete
|
|
418
537
|
? "install the tenant GitHub App and register its repository before connecting your personal GitHub account"
|
|
419
538
|
: "get this person's seat and Linear identity sorted",
|
|
420
|
-
account: "connect Linear
|
|
539
|
+
account: "connect the Linear integration on the Connections page (the GitHub App comes later, with the repository)",
|
|
421
540
|
projects: "pick ONE project and map its stages (or adopt the Catalyst workflow)",
|
|
422
|
-
repositories: "
|
|
541
|
+
repositories: "install the GitHub App on the Connections page, granting it the repository you want worked; registering that repository on the Repositories page, attached to the project, is the step right after",
|
|
423
542
|
"coding accounts": accountsNext,
|
|
543
|
+
"repository declarations": declNext,
|
|
424
544
|
host: "connect a Catalyst host",
|
|
425
545
|
};
|
|
546
|
+
// The parts are read in the order their data allows and reported in the order a person can act on
|
|
547
|
+
// them: the coding account first (nothing runs without one, and it needs no other step), then the
|
|
548
|
+
// workspace's Linear integration, the project, the GitHub App with its repository, the person's own
|
|
549
|
+
// connected accounts (personal GitHub after the repository proves the App), the repository's
|
|
550
|
+
// declaration, and last the host. `--next` is the first unfinished part in this order.
|
|
551
|
+
const ORDER = ["machine", "coding accounts", "account", "projects", "repositories", "person", "repository declarations", "host"];
|
|
552
|
+
parts.sort((a, b) => ORDER.indexOf(a.part) - ORDER.indexOf(b.part));
|
|
426
553
|
// A personal Linear grant cannot start until the tenant's Linear workspace exists. Once that account
|
|
427
554
|
// connection is present, a missing personal grant becomes the next member step before project setup.
|
|
428
555
|
const personPart = parts.find((p) => p.part === "person");
|
|
@@ -434,16 +561,50 @@ if (personPart && personalGrantIncomplete && workspaceResolved) {
|
|
|
434
561
|
}
|
|
435
562
|
const blocked = parts.filter((p) => p.verdict !== "ok" && p.blocking);
|
|
436
563
|
const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
|
|
437
|
-
|
|
564
|
+
// ⛔ AN UNREADABLE PART NEVER BORROWS ITS "UNFINISHED" ACTION. NEXT holds what to do when a part was
|
|
565
|
+
// read and is not done. A part that could not be read has no such basis: its step is reading it
|
|
566
|
+
// again, and the parts after it depend on it, so none of theirs is named either. Coding accounts set
|
|
567
|
+
// their own unreadable step in `accountsNext`.
|
|
568
|
+
const actionFor = (p) =>
|
|
569
|
+
p.verdict === "unreadable" && p.part !== "coding accounts"
|
|
570
|
+
? (p.next ?? `run this again; the ${p.part} part could not be read, so no step after it can be named yet`)
|
|
571
|
+
: NEXT[p.part];
|
|
572
|
+
let next =
|
|
438
573
|
stuck === null
|
|
439
574
|
? null
|
|
440
|
-
: { part: stuck.part, action:
|
|
575
|
+
: { part: stuck.part, action: actionFor(stuck), owner: stuck.owner, where: stuck.where, blocking: stuck.blocking };
|
|
576
|
+
|
|
577
|
+
// A refused contract version outranks every step above: each of them was read from, or waits on, a
|
|
578
|
+
// contract this CLI would not accept. Whether an update exists is asked of npm, once, and only here.
|
|
579
|
+
function versionAdvice() {
|
|
580
|
+
const semver = (t) => (t ?? "").match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
|
581
|
+
const installed = semver(field("Bundle"));
|
|
582
|
+
const npm = spawnSync("npm", ["view", "@catalyst-cloud/cli", "version"], { encoding: "utf8", timeout: 10_000, shell: process.platform === "win32" });
|
|
583
|
+
const latest = npm.error || npm.status !== 0 ? null : semver(npm.stdout);
|
|
584
|
+
// The installer is the update path, not `catalyst-skills install`. Its command is on the app's
|
|
585
|
+
// setup page; this names the script it fetches rather than composing the request here.
|
|
586
|
+
const reinstall = `re-run the install command from the app's setup page (it installs from ${link("/install.sh")})`;
|
|
587
|
+
if (installed === null || latest === null) {
|
|
588
|
+
return { text: `Whether a newer Catalyst CLI is published could not be checked. You can ${reinstall}; if this still appears afterwards, tell the Catalyst team.`, owner: null };
|
|
589
|
+
}
|
|
590
|
+
const order = [1, 2, 3].map((i) => Math.sign(Number(installed[i]) - Number(latest[i]))).find((d) => d !== 0) ?? 0;
|
|
591
|
+
return order >= 0
|
|
592
|
+
? { text: "A newer Catalyst CLI isn't published yet. Tell the Catalyst team; nothing on this machine needs to change.", owner: "the Catalyst team" }
|
|
593
|
+
: { text: `Update the CLI: ${reinstall}.`, owner: "you, on this machine" };
|
|
594
|
+
}
|
|
595
|
+
if (contractMismatch !== null && stuck !== null) {
|
|
596
|
+
const advice = versionAdvice();
|
|
597
|
+
const line = `the tenant serves contract ${contractMismatch.served} and this CLI accepts ${contractMismatch.accepts}. ${advice.text}`;
|
|
598
|
+
stuck.lines.push(line);
|
|
599
|
+
next = { part: stuck.part, action: line, owner: advice.owner, where: null, blocking: true };
|
|
600
|
+
}
|
|
441
601
|
const finished = parts.every((p) => p.verdict === "ok");
|
|
442
602
|
|
|
443
603
|
if (flags.json) {
|
|
444
|
-
console.log(JSON.stringify({ cli: via, connected, cloud, personalConnections, parts, localSync, next, finished }));
|
|
604
|
+
console.log(JSON.stringify({ cli: via, connected, cloud, personalConnections, parts, localSync, next, finished, notes: retireNote === null ? [] : [retireNote] }));
|
|
445
605
|
} else if (flags.next) {
|
|
446
|
-
if (
|
|
606
|
+
process.on("exit", () => { if (retireNote !== null) console.log(`note: ${retireNote}`); });
|
|
607
|
+
if (next === null) console.log("nothing left: every part is finished, a coding account is enrolled and the host check passes. Move one card into the project's start stage (usually Todo).");
|
|
447
608
|
else console.log(`${next.part}: ${next.action}${next.blocking ? "" : " (does not block the steps below)"}${next.owner ? ` — who: ${next.owner}` : ""}${next.where ? ` — ${next.where.startsWith("http") ? "where" : "do"}: ${next.where}` : ""}`);
|
|
448
609
|
} else {
|
|
449
610
|
for (const p of parts) {
|
|
@@ -456,6 +617,6 @@ if (flags.json) {
|
|
|
456
617
|
if (p.verdict !== "ok" && p.where) console.log(` ${p.where.startsWith("http") ? "where" : "do"}: ${p.where}`);
|
|
457
618
|
console.log("");
|
|
458
619
|
}
|
|
459
|
-
console.log(next === null ? "next: nothing left — move one card into the project's
|
|
620
|
+
console.log(next === null ? "next: nothing left — move one card into the project's start stage (usually Todo) and watch." : `next: ${next.part} — ${next.action}${next.owner ? ` (${next.owner})` : ""}${next.where ? ` — ${next.where}` : ""}`);
|
|
460
621
|
}
|
|
461
622
|
process.exit(finished ? 0 : 1);
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Am I set up? Machine readiness (Node, the tenant connection, the cached contract, the CLI path, the skills, the SDK, the optional replica) plus tenant readiness from the contract's per-team checks, in one verdict: what passes, what is blocked, what is merely waiting, and who can click what. Use when someone asks "am I set up", "what is missing", "why does nothing happen", "is the replica running", or right after connecting a new machine. Reports; never repairs.
|
|
5
5
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# Am I set up?
|
|
10
10
|
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Connect me
|
|
11
11
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
How Catalyst Cloud runs a ticket on the customer's own tenant, as facts an agent loads on demand: the eight-phase ladder and what each phase produces, the eleven board slots and this team's live stage map, what happens when a phase fails (retry, backoff, Remediate, park), how the queue is ordered and routed and every reason a ticket is excluded, and the coding-account model. Use when a person asks "how does this work?", "why did it do that?", "why is this stuck?", "what runs next?" or "how does it prioritise?". Read-only; its scripts explain one ticket's eligibility in plain English, show what is running and queued, and print the tenant's stage map and thresholds straight from the contract.
|
|
5
5
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# How Catalyst works
|
|
10
10
|
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Run this project
|
|
11
11
|
|
package/skills/unstick/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Unstick
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# What needs me
|
|
11
11
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
The desk for a Catalyst Cloud tenant. Use when the person asks "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", or asks for something to be done rather than known. Reads the tenant contract, what is running and queued, the eligibility explainer and the open asks through the catalyst-skills CLI, and answers in one reply with ticket ids. Routes work to a project owner and decisions to what-needs-me. Never composes a URL, never polls, never answers as the human.
|
|
5
5
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|