@catalyst-cloud/cli 0.9.4 → 0.13.0
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 +34 -0
- package/README.md +38 -32
- package/dist/args.js +16 -2
- package/dist/ask.js +2 -2
- package/dist/capabilities.js +70 -0
- package/dist/cli.js +42 -29
- package/dist/config.js +1 -1
- package/dist/connections.js +5 -5
- package/dist/env/declaration-rules.js +163 -0
- package/dist/env/group.js +51 -0
- package/dist/env/inventory.js +102 -0
- package/dist/env/render.js +39 -0
- package/dist/env/scan-env-example.js +62 -0
- package/dist/env/scan-source.js +43 -0
- package/dist/env/scan-workflow.js +123 -0
- package/dist/env/scan-wrangler.js +120 -0
- package/dist/env/types.js +1 -0
- package/dist/env/walk.js +87 -0
- package/dist/env.js +76 -0
- package/dist/environment.js +4 -4
- package/dist/events.js +1 -1
- package/dist/execution.js +3 -2
- package/dist/identity.js +3 -3
- package/dist/oauth.js +3 -3
- package/dist/query.js +27 -1
- package/dist/ready.js +3 -3
- package/dist/release.js +1 -1
- package/dist/replica.js +9 -7
- package/dist/runtime-store.js +2 -2
- package/dist/runtime.js +1 -1
- package/dist/secret.js +1 -1
- package/dist/skills.js +1 -1
- package/dist/team.js +329 -0
- package/package.json +4 -4
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +7 -7
- package/skills/catalyst-onboard/references/choosing-a-coding-account.md +2 -2
- package/skills/catalyst-onboard/references/declaring-a-repository.md +1 -1
- package/skills/catalyst-onboard/references/local-sync.md +3 -3
- package/skills/catalyst-onboard/references/skill-sources.md +1 -1
- package/skills/catalyst-onboard/references/the-one-path.md +12 -10
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +5 -5
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +6 -9
- package/skills/catalyst-onboard/references/who-fixes-what.md +35 -16
- package/skills/catalyst-onboard/scripts/local-sync.mjs +6 -6
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +133 -54
- package/skills/catalyst-setup/SKILL.md +1 -1
- package/skills/connect-me/SKILL.md +3 -3
- package/skills/how-catalyst-works/SKILL.md +5 -3
- package/skills/how-catalyst-works/references/settings-and-where-they-live.md +20 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +19 -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-needs-me/references/reading-the-inbox.md +4 -0
- package/skills/what-this-repo-needs/SKILL.md +40 -0
- package/skills/what-this-repo-needs/agents/openai.yaml +6 -0
- package/skills/what-this-repo-needs/agents/portability.yaml +4 -0
- package/skills/what-this-repo-needs/references/what-each-group-means.md +68 -0
- package/skills/what-this-repo-needs/scripts/check.mjs +64 -0
- package/skills/what-this-repo-needs/scripts/inventory.mjs +61 -0
- package/skills/what-this-repo-needs/scripts/lib/cli.mjs +114 -0
- package/skills/what-this-repo-needs/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/SKILL.md +1 -1
- package/skills/whats-happening/references/reading-the-board.md +1 -1
- package/skills/whats-happening/references/reprioritising.md +1 -1
- package/skills/whats-happening/references/status-reply.md +2 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +41 -33
|
@@ -20,7 +20,8 @@ const NOTES = [
|
|
|
20
20
|
"`replica status --probe --json` and `events status --probe --json` (optional local freshness),",
|
|
21
21
|
"`me --json` and personal connection statuses (person), `contract --path …` for the account, the projects and the repositories,",
|
|
22
22
|
"`contract --path codingAccounts` and `accounts --json` (coding accounts, and which one needs a new credential), and each",
|
|
23
|
-
"project's hosts_current check with its fixedWhere (host), and each project's environment_declared check with its per-repository notes (repository declarations).",
|
|
23
|
+
"project's hosts_current check with its fixedWhere (host), and each project's environment_declared check with its per-repository notes (repository declarations). The teams read is refreshed, so a mapping just saved reads back mapped.",
|
|
24
|
+
"Reads `capabilities --json` once: when the installed CLI can run a step for this person's role (`team check`, `team map`), the next step is that command; otherwise the page, with who can.",
|
|
24
25
|
"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
26
|
"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.",
|
|
26
27
|
"Writes nothing and changes nothing. Runs before this machine is connected — that is one of the states it reports.",
|
|
@@ -45,6 +46,15 @@ let personalGithubIncomplete = false;
|
|
|
45
46
|
let personalNext = null;
|
|
46
47
|
let workspaceResolved = false;
|
|
47
48
|
let repositoryRegistered = false;
|
|
49
|
+
// what the installed CLI can do, read once from `capabilities --json` (an older CLI has no
|
|
50
|
+
// such verb and reads as "nothing known"), and the person's role from `me`. Together they decide
|
|
51
|
+
// whether a next step is a command this person can run or a page for the person who can.
|
|
52
|
+
let capabilities = null;
|
|
53
|
+
let personRole = null;
|
|
54
|
+
const canManage = () => personRole === "admin" || personRole === "owner";
|
|
55
|
+
const capability = (verb) => (Array.isArray(capabilities) ? capabilities.find((c) => c && c.verb === verb) : undefined);
|
|
56
|
+
const verbAvailable = (verb) => { const c = capability(verb); return c?.availability === "available" && (c.needs === "member" || canManage()); };
|
|
57
|
+
const verbAdminOnly = (verb) => { const c = capability(verb); return c?.availability === "available" && c.needs === "admin" && !canManage(); };
|
|
48
58
|
// `blocking` is false for a finding that is real and reportable but does not stop the next step —
|
|
49
59
|
// an unmatched Linear identity is the one that matters: it must be said, and it must not become the
|
|
50
60
|
// thing the person is told to go and do before they can map a project.
|
|
@@ -75,7 +85,7 @@ const readContract = (args) => {
|
|
|
75
85
|
const status = runCli(["status"]);
|
|
76
86
|
if (!status.ran) {
|
|
77
87
|
process.stderr.write(`${status.stderr}\n`);
|
|
78
|
-
process.stderr.write(`the catalyst
|
|
88
|
+
process.stderr.write(`the catalyst CLI could not be started. Install it, then run this again.\n`);
|
|
79
89
|
process.exit(2);
|
|
80
90
|
}
|
|
81
91
|
const statusLines = status.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
@@ -95,7 +105,7 @@ const machineLines = connected ? statusLines : [statusLines[0] ?? "status printe
|
|
|
95
105
|
let machineVerdict = connected ? "ok" : "unfinished";
|
|
96
106
|
|
|
97
107
|
// `ready` is ONE verdict over two parts; split it by check id before anything is reported. A check
|
|
98
|
-
// whose id begins with "team:" belongs to a project and
|
|
108
|
+
// whose id begins with "team:" belongs to a project and needs the member's admin or owner seat.
|
|
99
109
|
let projectChecks = [];
|
|
100
110
|
let machineFix = null;
|
|
101
111
|
let localSync;
|
|
@@ -128,33 +138,38 @@ if (connected) {
|
|
|
128
138
|
localSync = {
|
|
129
139
|
assessment: { verdict: "unknown", current: false, reason: "connect this machine before local freshness can be checked" },
|
|
130
140
|
started: false,
|
|
131
|
-
recovery: "catalyst
|
|
141
|
+
recovery: "catalyst login",
|
|
132
142
|
};
|
|
133
143
|
}
|
|
134
144
|
add(
|
|
135
145
|
"machine",
|
|
136
|
-
"catalyst
|
|
146
|
+
"catalyst status, and the non-team checks of catalyst ready",
|
|
137
147
|
machineVerdict,
|
|
138
148
|
machineLines,
|
|
139
149
|
"you, on this machine",
|
|
140
|
-
// The keyless login alone: a person who holds a personal key already knows the key form.
|
|
141
|
-
|
|
150
|
+
// The keyless login alone: a person who holds a personal key already knows the key form. A machine
|
|
151
|
+
// that holds the CLI is told its own command; only one with no CLI at all gets the npx line.
|
|
152
|
+
connected ? null : cliTarget().recorded ? "catalyst login" : CONNECT_LINE.split(" (or")[0],
|
|
142
153
|
);
|
|
154
|
+
if (connected) {
|
|
155
|
+
const capDoc = tryJson(runCli(["capabilities", "--json"]).stdout);
|
|
156
|
+
capabilities = Array.isArray(capDoc?.capabilities) ? capDoc.capabilities : null;
|
|
157
|
+
}
|
|
143
158
|
|
|
144
159
|
// ── person ────────────────────────────────────────────────────────────────────────────────────────
|
|
145
160
|
if (!connected) {
|
|
146
|
-
add("person", "catalyst
|
|
161
|
+
add("person", "catalyst me", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
147
162
|
} else {
|
|
148
163
|
const me = runCli(["me", "--json"]);
|
|
149
164
|
const doc = tryJson(me.stdout);
|
|
150
165
|
const user = doc && typeof doc.user === "object" && doc.user !== null ? doc.user : null;
|
|
151
166
|
if (doc === null) {
|
|
152
|
-
add("person", "catalyst
|
|
153
|
-
"run this again; catalyst
|
|
167
|
+
add("person", "catalyst me", "unreadable", [`me could not be read (${(me.stderr || me.stdout).trim().split("\n")[0] ?? "no output"})`], null, null).next =
|
|
168
|
+
"run this again; catalyst me could not be read, so no step after it can be named yet";
|
|
154
169
|
} else if (user === null) {
|
|
155
170
|
add(
|
|
156
171
|
"person",
|
|
157
|
-
"catalyst
|
|
172
|
+
"catalyst me",
|
|
158
173
|
"unfinished",
|
|
159
174
|
["this credential names no person — it is a host credential, so nothing an agent writes will carry a name"],
|
|
160
175
|
"you: connect again with your own login",
|
|
@@ -162,6 +177,7 @@ if (!connected) {
|
|
|
162
177
|
);
|
|
163
178
|
} else {
|
|
164
179
|
const matched = typeof user.linearUserId === "string" && user.linearUserId !== "";
|
|
180
|
+
personRole = typeof user.role === "string" ? user.role : null;
|
|
165
181
|
const grantLines = [];
|
|
166
182
|
for (const provider of ["linear", "github"]) {
|
|
167
183
|
const read = runCli(["connections", "personal", provider, "status", "--json"]);
|
|
@@ -180,17 +196,17 @@ if (!connected) {
|
|
|
180
196
|
personalGrantIncomplete = true;
|
|
181
197
|
if (provider === "linear") personalLinearIncomplete = true;
|
|
182
198
|
else personalGithubIncomplete = true;
|
|
183
|
-
grantLines.push(`personal ${provider}: ${outcome === "unavailable" ? "temporarily unavailable; grant state unknown" : "could not be checked; update the catalyst
|
|
199
|
+
grantLines.push(`personal ${provider}: ${outcome === "unavailable" ? "temporarily unavailable; grant state unknown" : "could not be checked; update the catalyst CLI or inspect its status output"}`);
|
|
184
200
|
if (provider === "linear") personalNext ??= "re-check your personal linear connection";
|
|
185
201
|
}
|
|
186
202
|
}
|
|
187
203
|
add(
|
|
188
204
|
"person",
|
|
189
|
-
"catalyst
|
|
205
|
+
"catalyst me, and catalyst connections personal <provider> status --json",
|
|
190
206
|
matched && !personalGrantIncomplete ? "ok" : "unfinished",
|
|
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
|
|
207
|
+
[`${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 identity linear options for self-service recovery; personal Linear consent normally binds its viewer automatically.", ...grantLines],
|
|
192
208
|
personalGrantIncomplete ? "you" : matched ? null : "a workspace owner or admin",
|
|
193
|
-
personalGrantIncomplete ? "catalyst
|
|
209
|
+
personalGrantIncomplete ? "catalyst connections personal <provider> start or status" : matched ? null : link("/settings/account"),
|
|
194
210
|
false,
|
|
195
211
|
);
|
|
196
212
|
}
|
|
@@ -198,13 +214,13 @@ if (!connected) {
|
|
|
198
214
|
|
|
199
215
|
// ── account ───────────────────────────────────────────────────────────────────────────────────────
|
|
200
216
|
if (!connected) {
|
|
201
|
-
add("account", "catalyst
|
|
217
|
+
add("account", "catalyst contract --path account", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
202
218
|
} else {
|
|
203
219
|
const acct = readContract(["contract", "--path", "account", "--json"]);
|
|
204
220
|
const doc = tryJson(acct.stdout);
|
|
205
221
|
if (doc === null) {
|
|
206
|
-
add("account", "catalyst
|
|
207
|
-
"refresh the contract (catalyst
|
|
222
|
+
add("account", "catalyst contract --path account", "unreadable", ["the account block could not be read — try: catalyst contract --refresh"], null, null).next =
|
|
223
|
+
"refresh the contract (catalyst contract --refresh), then run this again; the account could not be read, so no step after it can be named yet";
|
|
208
224
|
} else {
|
|
209
225
|
const workspace = typeof doc.linearWorkspaceSlug === "string" && doc.linearWorkspaceSlug !== "" ? doc.linearWorkspaceSlug : typeof doc.linearWorkspaceId === "string" && doc.linearWorkspaceId !== "" ? doc.linearWorkspaceId : null;
|
|
210
226
|
workspaceResolved = workspace !== null;
|
|
@@ -216,7 +232,7 @@ if (!connected) {
|
|
|
216
232
|
if (envDoc === null) {
|
|
217
233
|
envLines.push(`environment: not readable (${(env.stderr || env.stdout).trim().split("\n")[0] ?? "no output"})`);
|
|
218
234
|
} else if (envDoc.current === null || envDoc.current === undefined) {
|
|
219
|
-
envLines.push("environment: nothing declared yet — declare it with `catalyst
|
|
235
|
+
envLines.push("environment: nothing declared yet — declare it with `catalyst environment propose --file <path> --approve`");
|
|
220
236
|
} else {
|
|
221
237
|
envLines.push(`environment: revision ${envDoc.current.revision} (${envDoc.current.canonicalHash}), ${envDoc.isApproved ? "approved" : "NOT approved — a proposal nobody approved changes nothing"}`);
|
|
222
238
|
envLines.push(envDoc.delivered ? `environment delivered to phases: revision ${envDoc.delivered.revision}` : "environment delivered to phases: nothing yet");
|
|
@@ -225,7 +241,7 @@ if (!connected) {
|
|
|
225
241
|
}
|
|
226
242
|
add(
|
|
227
243
|
"account",
|
|
228
|
-
"catalyst
|
|
244
|
+
"catalyst contract --path account, and catalyst environment read",
|
|
229
245
|
workspace === null ? "unfinished" : "ok",
|
|
230
246
|
[
|
|
231
247
|
`${doc.name ?? "(unnamed tenant)"} (${doc.slug ?? "?"})`,
|
|
@@ -241,17 +257,40 @@ if (!connected) {
|
|
|
241
257
|
|
|
242
258
|
// The host part reads the same rows, so they are kept rather than read twice.
|
|
243
259
|
let teamRows = null;
|
|
260
|
+
// The contract's readinessChecks metadata (severity and owner per check id), read once and shared by
|
|
261
|
+
// the projects, host and declarations parts. An older cloud that has none answers [].
|
|
262
|
+
let readinessMetaCache;
|
|
263
|
+
const readinessMeta = () => {
|
|
264
|
+
if (readinessMetaCache === undefined) {
|
|
265
|
+
const m = tryJson(readContract(["contract", "--path", "readinessChecks", "--json"]).stdout);
|
|
266
|
+
readinessMetaCache = Array.isArray(m) ? m : [];
|
|
267
|
+
}
|
|
268
|
+
return readinessMetaCache;
|
|
269
|
+
};
|
|
270
|
+
// Linear's own Git automation rules, by check id, as the Settings → Workflow → Git automation screen
|
|
271
|
+
// names them. A rule that moves a card is a conflict with the stage Catalyst manages, and no Catalyst
|
|
272
|
+
// key can change a Linear automation rule: the fix is in Linear, by a Linear admin.
|
|
273
|
+
const LINEAR_AUTOMATION_RULES = {
|
|
274
|
+
linear_automation_pr_open: "On PR open",
|
|
275
|
+
linear_automation_pr_review: "On PR review request or activity",
|
|
276
|
+
linear_automation_pr_ready: "On PR ready for merge",
|
|
277
|
+
linear_automation_pr_merge: "On PR merge",
|
|
278
|
+
};
|
|
279
|
+
let blockedProjects = [];
|
|
280
|
+
|
|
244
281
|
// ── projects (a project is one Linear team) ───────────────────────────────────────────────────────
|
|
245
282
|
if (!connected) {
|
|
246
|
-
add("projects", "catalyst
|
|
283
|
+
add("projects", "catalyst contract --path teams", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
247
284
|
} else {
|
|
248
|
-
|
|
285
|
+
// The mapping write updates the cloud before the cached contract's dispatchGate projection.
|
|
286
|
+
// Revalidate this one read so a successful save does not appear unmapped on the next step.
|
|
287
|
+
const teams = readContract(["contract", "--refresh", "--path", "teams", "--json"]);
|
|
249
288
|
const doc = tryJson(teams.stdout);
|
|
250
289
|
const rows = Array.isArray(doc) ? doc : null;
|
|
251
290
|
teamRows = rows;
|
|
252
291
|
if (rows === null) {
|
|
253
|
-
add("projects", "catalyst
|
|
254
|
-
"refresh the contract (catalyst
|
|
292
|
+
add("projects", "catalyst contract --path teams", "unreadable", ["the project list could not be read — try: catalyst contract --refresh"], null, null).next =
|
|
293
|
+
"refresh the contract (catalyst contract --refresh), then run this again; the project list could not be read, so no step after it can be named yet";
|
|
255
294
|
} else {
|
|
256
295
|
const lines = [`${rows.length} mapped`];
|
|
257
296
|
for (const t of rows) {
|
|
@@ -261,19 +300,42 @@ if (!connected) {
|
|
|
261
300
|
lines.push(`${key}: ${readiness.status ?? "unknown"}${bad.length ? ` — ${bad.map((c) => `${c.id} ${c.state}`).join(", ")}` : ""}`);
|
|
262
301
|
}
|
|
263
302
|
for (const c of projectChecks) lines.push(`${c.ok ? "note" : "FAIL"} ${c.line}${c.who ? ` — who: ${c.who}` : ""}`);
|
|
264
|
-
lines.push("
|
|
303
|
+
lines.push("MAPPED projects only. An empty list means nothing is mapped yet, NOT that there are no projects. Run catalyst team list to inspect the live list without checking readiness.");
|
|
265
304
|
// A project is set up when its dispatch gate is open (its stages are mapped), or when its
|
|
266
305
|
// readiness reads ready. Readiness alone kept `--next` on "map its stages" for a tenant whose
|
|
267
306
|
// gates were open: readiness stays "unchecked" until someone presses Re-check, and a new team
|
|
268
307
|
// stays "degraded" until a repository is attached, which is the step AFTER this one.
|
|
269
|
-
|
|
308
|
+
// ⛔ A team whose readiness reads `blocked` is NOT set up, whatever its gate says: a blocking
|
|
309
|
+
// check (a Linear automation that moves cards off Catalyst's stages, a missing OAuth scope) breaks
|
|
310
|
+
// every run in it. The step is that check's own fix, by its own owner, then a re-check.
|
|
311
|
+
const isBlocking = (c) => readinessMeta().length === 0 || readinessMeta().find((r) => r.id === c.id)?.severity === "blocking";
|
|
312
|
+
for (const t of rows.filter((r) => r.readiness?.status === "blocked")) {
|
|
313
|
+
const key = t.key ?? t.id ?? "(unkeyed)";
|
|
314
|
+
const fails = (Array.isArray(t.readiness?.checks) ? t.readiness.checks : []).filter((c) => c.state === "fail" && isBlocking(c));
|
|
315
|
+
const ids = fails.map((c) => c.id);
|
|
316
|
+
const rules = ids.filter((id) => id in LINEAR_AUTOMATION_RULES).map((id) => LINEAR_AUTOMATION_RULES[id]);
|
|
317
|
+
const others = ids.filter((id) => !(id in LINEAR_AUTOMATION_RULES));
|
|
318
|
+
const ownerRow = fails.map((c) => readinessMeta().find((r) => r.id === c.id)).find((r) => r && typeof r.fixedByLine === "string");
|
|
319
|
+
const owner = ownerRow ? ownerRow.fixedByLine : "a workspace owner or admin";
|
|
320
|
+
const how = [
|
|
321
|
+
rules.length > 0 ? `in Linear, open Settings → Teams → ${key} → Workflow → Git automation and set ${rules.join(", ")} to No action (no Catalyst key can change a Linear automation rule)` : null,
|
|
322
|
+
others.length > 0 ? `${others.join(", ")}: ${owner}` : null,
|
|
323
|
+
].filter(Boolean).join("; ");
|
|
324
|
+
lines.push(`${key}: BLOCKED — ${ids.length > 0 ? ids.join(", ") : "a blocking check failed"}; ${how || `owner: ${owner}`}`);
|
|
325
|
+
blockedProjects.push({ key, action: `fix ${key}'s blocking check${ids.length === 1 ? "" : "s"} (${ids.join(", ") || "see the projects lines"}): ${how || owner}; then run catalyst team check ${key} (or press Re-check) and run this again`, owner });
|
|
326
|
+
}
|
|
327
|
+
const setUp = (t) => t.readiness?.status !== "blocked" && (t.dispatchGate?.status === "open" || (t.readiness?.status ?? "unchecked") === "ready");
|
|
270
328
|
for (const t of rows) {
|
|
271
329
|
if (t.dispatchGate?.status === "open" && (t.readiness?.status ?? "unchecked") === "unchecked") {
|
|
272
|
-
lines.push(`${t.key ?? t.id ?? "(unkeyed)"}: stages mapped; readiness not checked yet:
|
|
330
|
+
lines.push(`${t.key ?? t.id ?? "(unkeyed)"}: stages mapped; readiness not checked yet: run catalyst team check ${t.key ?? t.id} to see the rest`);
|
|
273
331
|
}
|
|
274
332
|
}
|
|
275
333
|
const ready = rows.length > 0 && rows.every(setUp);
|
|
276
|
-
|
|
334
|
+
const mapOwner = verbAvailable("team map") ? "you, with your own login" : "a workspace owner or admin";
|
|
335
|
+
const mapWhere = verbAvailable("team map") ? "catalyst team list" : link("/settings/linear-teams");
|
|
336
|
+
const projectOwner = blockedProjects.length > 0 ? blockedProjects[0].owner : mapOwner;
|
|
337
|
+
const projectWhere = blockedProjects.length > 0 ? null : mapWhere;
|
|
338
|
+
add("projects", "catalyst contract --path teams, and the team: checks of ready", ready ? "ok" : "unfinished", lines, projectOwner, projectWhere);
|
|
277
339
|
}
|
|
278
340
|
}
|
|
279
341
|
|
|
@@ -290,20 +352,20 @@ function thoughtsNote(owner) {
|
|
|
290
352
|
return `note ${repo}: gh could not see it (${why}). It does not exist, or this GitHub login cannot see it. Create or confirm it (step 5a).`;
|
|
291
353
|
}
|
|
292
354
|
if (!connected) {
|
|
293
|
-
add("repositories", "catalyst
|
|
355
|
+
add("repositories", "catalyst contract --path merge.repositories", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
294
356
|
} else {
|
|
295
357
|
const repos = readContract(["contract", "--path", "merge.repositories", "--json"]);
|
|
296
358
|
const doc = tryJson(repos.stdout);
|
|
297
359
|
const rows = Array.isArray(doc) ? doc : null;
|
|
298
360
|
if (rows === null) {
|
|
299
|
-
add("repositories", "catalyst
|
|
300
|
-
"refresh the contract (catalyst
|
|
361
|
+
add("repositories", "catalyst contract --path merge.repositories", "unreadable", ["the repository list could not be read — try: catalyst contract --refresh"], null, null).next =
|
|
362
|
+
"refresh the contract (catalyst contract --refresh), then run this again; the repository list could not be read, so no step after it can be named yet";
|
|
301
363
|
} else {
|
|
302
364
|
const lines = [`${rows.length} registered`, ...rows.map((r) => `${r.owner ?? "?"}/${r.name ?? "?"}`)];
|
|
303
365
|
repositoryRegistered = rows.length > 0;
|
|
304
366
|
lines.push("⛔ REGISTRATION only. This carries no status and no project attachment, so it never proves a repository can be dispatched into.");
|
|
305
367
|
for (const owner of [...new Set(rows.map((r) => r.owner).filter((o) => typeof o === "string" && o !== ""))]) lines.push(thoughtsNote(owner));
|
|
306
|
-
add("repositories", "catalyst
|
|
368
|
+
add("repositories", "catalyst contract --path merge.repositories", rows.length > 0 ? "ok" : "unfinished", lines, "a workspace owner or admin", link("/settings/repositories"));
|
|
307
369
|
}
|
|
308
370
|
}
|
|
309
371
|
|
|
@@ -350,18 +412,18 @@ const readAccountRows = () => {
|
|
|
350
412
|
return { rows, error: rows === null ? (acc.stderr || acc.stdout).trim().split("\n")[0] || "no output" : null };
|
|
351
413
|
};
|
|
352
414
|
if (!connected) {
|
|
353
|
-
add("coding accounts", "codingAccounts in catalyst
|
|
415
|
+
add("coding accounts", "codingAccounts in catalyst contract", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
354
416
|
} else {
|
|
355
417
|
const res = readContract(["contract", "--path", "codingAccounts", "--json"]);
|
|
356
418
|
const ca = tryJson(res.stdout);
|
|
357
419
|
const older = ca === null && /has nothing at/.test(res.stderr ?? "");
|
|
358
|
-
const INSTRUMENT = "codingAccounts in catalyst
|
|
420
|
+
const INSTRUMENT = "codingAccounts in catalyst contract";
|
|
359
421
|
if (ca !== null && typeof ca === "object" && typeof ca.state === "string") {
|
|
360
422
|
const line = typeof ca.line === "string" ? ca.line : `state ${ca.state}`;
|
|
361
423
|
const owner = typeof ca.enrolledByLine === "string" ? ca.enrolledByLine : null;
|
|
362
424
|
const where = typeof ca.page === "string" ? link(ca.page) : null;
|
|
363
425
|
if (ca.state === "enrolled" || ca.state === "needs_credential") {
|
|
364
|
-
const DETAIL = `${INSTRUMENT}, and catalyst
|
|
426
|
+
const DETAIL = `${INSTRUMENT}, and catalyst accounts`;
|
|
365
427
|
const head = [line, `${ca.activeCount ?? "?"} active`];
|
|
366
428
|
const { rows, error } = readAccountRows();
|
|
367
429
|
const dead = rows === null ? [] : rows.filter((a) => credentialProblem(a) !== null);
|
|
@@ -405,31 +467,40 @@ if (!connected) {
|
|
|
405
467
|
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";
|
|
406
468
|
if (rows === null) {
|
|
407
469
|
accountsNext = "read the coding accounts again; the account list could not be read this time";
|
|
408
|
-
add("coding accounts", "catalyst
|
|
470
|
+
add("coding accounts", "catalyst accounts", "unreadable", [olderLine, `coding accounts could not be read (${error})`], null, null);
|
|
409
471
|
} else {
|
|
410
472
|
// An expired or revoked slot, or a quarantined one, cannot take work until an admin acts on it.
|
|
411
473
|
const usable = rows.filter((a) => a?.status !== "expired-or-revoked" && a?.quarantined !== true);
|
|
412
474
|
const lines = [olderLine, `${rows.length} enrolled, ${usable.length} able to take work`];
|
|
413
475
|
for (const a of rows) lines.push(`${a.accountSlot ?? "?"}: ${a.provider ?? "?"}, ${a.status ?? "status unknown"}${a.quarantined ? ", quarantined" : ""}`);
|
|
414
476
|
if (usable.length === 0) lines.push("no phase can start until one is enrolled and able to take work");
|
|
415
|
-
add("coding accounts", "catalyst
|
|
477
|
+
add("coding accounts", "catalyst accounts", usable.length > 0 ? "ok" : "unfinished", lines, "a workspace owner or admin", link("/settings/coding-accounts"));
|
|
416
478
|
}
|
|
417
479
|
} else {
|
|
418
480
|
accountsNext = "read the coding accounts again; the contract could not be read";
|
|
419
|
-
add("coding accounts", INSTRUMENT, "unreadable", [`the contract could not be read (${(res.stderr || res.stdout).trim().split("\n").pop() || "no output"}); try: catalyst
|
|
481
|
+
add("coding accounts", INSTRUMENT, "unreadable", [`the contract could not be read (${(res.stderr || res.stdout).trim().split("\n").pop() || "no output"}); try: catalyst contract --refresh`], null, null);
|
|
420
482
|
}
|
|
421
483
|
}
|
|
422
484
|
|
|
485
|
+
// the Re-check step, as a command when this person can run it, else as the page with the
|
|
486
|
+
// person who can. `team check` is the CLI twin of the page's Re-check button (admin or owner).
|
|
487
|
+
const mappedKeys = () => (teamRows ?? []).map((t) => t.key ?? t.id).filter((k) => typeof k === "string" && k !== "");
|
|
488
|
+
const recheckStep = () => {
|
|
489
|
+
const cmds = mappedKeys().map((k) => `catalyst team check ${k}`);
|
|
490
|
+
if (cmds.length > 0 && verbAvailable("team check")) return { action: `run ${cmds.join(" and ")}, then run this again`, owner: "you, the assistant: run it now with the person's login, without asking", where: cmds[0] };
|
|
491
|
+
if (cmds.length > 0 && verbAdminOnly("team check")) return { action: `a workspace owner or admin runs ${cmds.join(" and ")} (or presses Re-check on the projects page); then run this again`, owner: "a workspace owner or admin", where: link("/settings/linear-teams") };
|
|
492
|
+
return { action: "press Re-check on the projects page, then run this again", owner: "a workspace owner or admin", where: link("/settings/linear-teams") };
|
|
493
|
+
};
|
|
494
|
+
|
|
423
495
|
// ── host ──────────────────────────────────────────────────────────────────────────────────────────
|
|
424
496
|
// Read off the contract, never assumed: each checked project carries a hosts_current check, and the
|
|
425
497
|
// contract says who owns it. The check is account-wide, so any one project's reading is the answer.
|
|
426
498
|
// A tenant that runs no host of its own reads `pass` here, and then nothing is asked of anyone.
|
|
427
499
|
if (!connected) {
|
|
428
|
-
add("host", "hosts_current in catalyst
|
|
500
|
+
add("host", "hosts_current in catalyst contract --path teams", "unreadable", ["not readable until this machine is connected"], null, null);
|
|
429
501
|
} else {
|
|
430
502
|
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)" })));
|
|
431
|
-
const
|
|
432
|
-
const row = Array.isArray(meta) ? meta.find((r) => r.id === "hosts_current") : null;
|
|
503
|
+
const row = readinessMeta().find((r) => r.id === "hosts_current") ?? null;
|
|
433
504
|
const hc = checks.find((c) => c.state === "fail") ?? checks.find((c) => c.state === "unknown") ?? checks[0] ?? null;
|
|
434
505
|
// The contract's printed line for this owner is written for a host that is behind. When no host is
|
|
435
506
|
// connected there is nothing behind, so the owner is named by its id instead of by that sentence.
|
|
@@ -444,15 +515,16 @@ if (!connected) {
|
|
|
444
515
|
const how = fw === null ? null : link(fw.page);
|
|
445
516
|
const howLines = fw?.command ? [`the owner runs: ${fw.command}`] : [];
|
|
446
517
|
if (hc === null) {
|
|
447
|
-
|
|
518
|
+
const step = recheckStep();
|
|
519
|
+
add("host", "hosts_current in catalyst 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."], teamRows === null ? "a workspace owner or admin" : step.owner, teamRows === null ? link("/settings/linear-teams") : step.where).next =
|
|
448
520
|
teamRows === null
|
|
449
|
-
? "refresh the contract (catalyst
|
|
450
|
-
:
|
|
521
|
+
? "refresh the contract (catalyst contract --refresh), then run this again; the project list could not be read, so the host check cannot be either"
|
|
522
|
+
: step.action;
|
|
451
523
|
} else if (hc.state === "pass") {
|
|
452
|
-
add("host", "hosts_current in catalyst
|
|
524
|
+
add("host", "hosts_current in catalyst contract --path teams", "ok", [`hosts_current pass (read on ${hc.team})`]);
|
|
453
525
|
} else {
|
|
454
526
|
const detail = hc.reason === "no_host_connected" ? "no Catalyst host is connected" : hc.reason === "hosts_behind" ? "a connected host runs an older mapping" : hc.reason === "hosts_unreported" ? "a host is connected but has not reported what it loaded" : `state ${hc.state}`;
|
|
455
|
-
add("host", "hosts_current in catalyst
|
|
527
|
+
add("host", "hosts_current in catalyst contract --path teams", "unfinished", [`hosts_current ${hc.state}${hc.reason ? ` (${hc.reason})` : ""} on ${hc.team}: ${detail}`, ...howLines], owner, how);
|
|
456
528
|
}
|
|
457
529
|
}
|
|
458
530
|
|
|
@@ -461,7 +533,7 @@ if (!connected) {
|
|
|
461
533
|
// default repository has one in effect is the contract's `environment_declared` check; the project's
|
|
462
534
|
// other repositories appear under that check's `repos`. Names only: nothing here reads a value, and
|
|
463
535
|
// 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
|
|
536
|
+
const DECL_INSTRUMENT = "environment_declared in catalyst contract --path teams";
|
|
465
537
|
const DECL_REASONS = {
|
|
466
538
|
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
539
|
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 },
|
|
@@ -475,15 +547,15 @@ if (!connected) {
|
|
|
475
547
|
add("repository declarations", DECL_INSTRUMENT, "unreadable", ["not readable until this machine is connected"], null, null);
|
|
476
548
|
} else if (teamRows === null) {
|
|
477
549
|
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
|
|
550
|
+
"refresh the contract (catalyst contract --refresh), then run this again; the project list could not be read, so no repository declaration can be either";
|
|
479
551
|
} else if (teamRows.length === 0) {
|
|
480
552
|
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
553
|
"map a project first; a repository declaration is read per project";
|
|
482
554
|
} else if (teamRows.every((t) => !Array.isArray(t.readiness?.checks) || t.readiness.checks.length === 0)) {
|
|
483
555
|
// Mapped, never checked: the contract carries no checks at all until someone presses Re-check, and
|
|
484
556
|
// that silence is not "nothing to declare".
|
|
485
|
-
|
|
486
|
-
|
|
557
|
+
const step = recheckStep();
|
|
558
|
+
add("repository declarations", DECL_INSTRUMENT, "unreadable", ["no project has a readiness check yet, so whether its repository is declared cannot be read."], step.owner, step.where).next = step.action;
|
|
487
559
|
} else {
|
|
488
560
|
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
561
|
const lines = [];
|
|
@@ -508,12 +580,13 @@ if (!connected) {
|
|
|
508
580
|
else if (c.state === "fail") flag(c.team, null, c.reason);
|
|
509
581
|
else {
|
|
510
582
|
unknown = true;
|
|
511
|
-
lines.push(`${c.team}: environment_declared could not be read${c.reason ? ` (${c.reason})` : ""};
|
|
583
|
+
lines.push(`${c.team}: environment_declared could not be read${c.reason ? ` (${c.reason})` : ""}; a re-check is needed`);
|
|
512
584
|
}
|
|
513
585
|
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
586
|
}
|
|
515
587
|
if (unknown && verdict === "ok") {
|
|
516
|
-
|
|
588
|
+
const step = recheckStep();
|
|
589
|
+
add("repository declarations", DECL_INSTRUMENT, "unreadable", lines, step.owner, step.where).next = step.action;
|
|
517
590
|
} else {
|
|
518
591
|
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
592
|
add("repository declarations", DECL_INSTRUMENT, verdict, lines, verdict === "ok" ? null : owner, verdict === "ok" ? null : where);
|
|
@@ -537,7 +610,13 @@ const NEXT = {
|
|
|
537
610
|
? "install the tenant GitHub App and register its repository before connecting your personal GitHub account"
|
|
538
611
|
: "get this person's seat and Linear identity sorted",
|
|
539
612
|
account: "connect the Linear integration on the Connections page (the GitHub App comes later, with the repository)",
|
|
540
|
-
projects:
|
|
613
|
+
projects: blockedProjects.length > 0
|
|
614
|
+
? blockedProjects.map((b) => b.action).join("; and ")
|
|
615
|
+
: verbAvailable("team map")
|
|
616
|
+
? "pick ONE project: run catalyst team list, then catalyst team map <KEY> (or team adopt <KEY>) and approve its preview"
|
|
617
|
+
: verbAdminOnly("team map")
|
|
618
|
+
? "a workspace owner or admin maps ONE project: catalyst team map <KEY>, or Map my stages on the projects page"
|
|
619
|
+
: "pick ONE project and map its stages (or adopt the Catalyst workflow)",
|
|
541
620
|
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",
|
|
542
621
|
"coding accounts": accountsNext,
|
|
543
622
|
"repository declarations": declNext,
|
|
@@ -581,7 +660,7 @@ function versionAdvice() {
|
|
|
581
660
|
const installed = semver(field("Bundle"));
|
|
582
661
|
const npm = spawnSync("npm", ["view", "@catalyst-cloud/cli", "version"], { encoding: "utf8", timeout: 10_000, shell: process.platform === "win32" });
|
|
583
662
|
const latest = npm.error || npm.status !== 0 ? null : semver(npm.stdout);
|
|
584
|
-
// The installer is the update path, not `catalyst
|
|
663
|
+
// The installer is the update path, not `catalyst install`. Its command is on the app's
|
|
585
664
|
// setup page; this names the script it fetches rather than composing the request here.
|
|
586
665
|
const reinstall = `re-run the install command from the app's setup page (it installs from ${link("/install.sh")})`;
|
|
587
666
|
if (installed === null || latest === null) {
|
|
@@ -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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — 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.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Connect me
|
|
11
11
|
|
|
@@ -34,7 +34,7 @@ CATALYST_CLOUD_TOKEN=<your-personal-key> catalyst-skills login
|
|
|
34
34
|
## What you do
|
|
35
35
|
|
|
36
36
|
1. Prefer keyless: run `catalyst-skills login` with no key and have the person approve the code in their browser (or from a phone). Nothing to mint, nothing to paste, nothing to keep out of the transcript. A **personal key** is the fallback for a script or unattended shell — they mint it at Settings → API keys (every active member can; no admin is needed) and hand it to the login command via `CATALYST_CLOUD_TOKEN`; it is shown once, so never put it in a file, a ticket, or this conversation beyond that one command. Either way, do not ask an admin for the tenant's **account key** (`ctc_acct_`, Settings → Account keys): that is a host credential — runners, daemons, the host replica — and a person connected with it has no name on anything their agent writes, and "what needs me" cannot mean them. If they connect with one anyway, login says so on stderr and still works.
|
|
37
|
-
2. Run the login command. Its output names the tenant (`Connected to <name> (<slug>)`), the person (`Connected as <label> (<role>)`), the config path, and the cached contract version. If the person line says their Linear identity is not matched yet, tell them: an admin matches it in Settings → Members, and until then "what needs me" shows everyone's asks.
|
|
37
|
+
2. Run the login command. Its output names the tenant (`Connected to <name> (<slug>)`), the person (`Connected as <label> (<role>)`), the config path, and the cached contract version. If the person line says their Linear identity is not matched yet, tell them: an admin matches it in Settings → Members, and until then "what needs me" shows everyone's asks — connecting Linear personally does not match the identity.
|
|
38
38
|
3. Verify with `node scripts/verify-connection.mjs`: one line each for the tenant, the contract version, and the replica; exit 1 when the machine is not connected.
|
|
39
39
|
4. Run `catalyst-skills ready` and read the verdict to them.
|
|
40
40
|
5. Do not offer the replica. It is optional and off by default for large projects while the snapshot path is made safe, and every read works through the API without it. Start it only if the person asks for local SQL, and then load `references/keeping-the-replica-running.md`.
|
|
@@ -54,7 +54,7 @@ Every skill session opens with these, in this order, before doing anything else:
|
|
|
54
54
|
| when | read |
|
|
55
55
|
| -- | -- |
|
|
56
56
|
| the person wants the replica writer to survive a reboot, or asks what it stores and whether anything needs cleaning | `references/keeping-the-replica-running.md` |
|
|
57
|
-
| the full readiness vector and
|
|
57
|
+
| the full readiness vector — the check, its severity, and who can click what | the `catalyst-setup` skill |
|
|
58
58
|
|
|
59
59
|
## Rules
|
|
60
60
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: how-catalyst-works
|
|
3
3
|
description: >-
|
|
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,
|
|
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, the coding-account model, and which settings screen governs which rule. Use when a person asks "how does this work?", "why did it do that?", "why is this stuck?", "what runs next?", "how does it prioritise?" or "what does this setting do, where is it set?". 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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# How Catalyst works
|
|
10
10
|
|
|
@@ -30,12 +30,14 @@ Exit codes: 0 answered, 1 not found or a usage error, 2 this machine is not conn
|
|
|
30
30
|
| "what are the phases, what does each produce, when is a ticket Done?" | `references/the-ladder.md` |
|
|
31
31
|
| "which column is which, why does nothing dispatch, what is a slot?" | `references/stages-and-mapping.md` |
|
|
32
32
|
| a phase FAILED, a card went to Remediate, a ticket is parked or on hold | `references/when-a-phase-fails.md` |
|
|
33
|
-
| "what runs next, why not this one, what does this exclusion reason mean?" | `references/what-runs-next.md` |
|
|
33
|
+
| "what runs next, why not this one, what does this exclusion reason mean?", "why does nothing start while nothing runs?" (the WIP limit) | `references/what-runs-next.md` |
|
|
34
34
|
| "why is nothing running", walls, quarantine, which provider ran a phase | `references/coding-accounts.md` |
|
|
35
|
+
| "what does this setting do, where is it set, who can change it?" | `references/settings-and-where-they-live.md` |
|
|
35
36
|
|
|
36
37
|
## Rules
|
|
37
38
|
|
|
38
39
|
- **Print, never recall.** A stage name, label, threshold or route in your answer must have come from a script's output in this session.
|
|
40
|
+
- **A setting has a page.** Name the screen, its route and the rule from `references/settings-and-where-they-live.md`; never answer a settings question from memory, and never invent a route.
|
|
39
41
|
- **A reason is a layer.** Translate an exclusion reason through `references/what-runs-next.md`; name what releases it and who can do that (a clock, a comment, a push, an operator).
|
|
40
42
|
- **Unknown is not absent.** An `unknown` verdict from the explainer means the cloud could not look; report it as inconclusive.
|
|
41
43
|
- **System causes are one alert.** A provider outage, the runner image breaker or a paused repository holds many tickets for one reason; never escalate it ticket by ticket.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Settings and where they live
|
|
2
|
+
|
|
3
|
+
This reference restates rules that do not vary per tenant: what a setting does, the screen it lives on, and who can change it. Every live value — whether a connection is up, which repositories are registered, the current merge policy — comes from the contract or a script; every route below is prefixed with `<their cloud>` because the host is the tenant's own, never a literal you invent.
|
|
4
|
+
|
|
5
|
+
| what | where it is set | the rule | who can set it |
|
|
6
|
+
| -- | -- | -- | -- |
|
|
7
|
+
| **Connecting Linear** | `<their cloud>/settings/connections` | Needs an admin or owner browser session; the tenant comes from that session, never from a URL parameter. | owner or admin |
|
|
8
|
+
| **One workspace, one account** | `<their cloud>/settings/connections` | A Linear workspace binds to exactly one account and an account to exactly one workspace; a second tenant for the same workspace is refused and stores nothing. That is a decision to raise, not a retry. | owner or admin |
|
|
9
|
+
| **Installing the GitHub App** | `<their cloud>/settings/connections`, but the admin's own personal GitHub grant comes first at `<their cloud>/settings/profile` | A refusal naming a missing GitHub identity, or an installation that is not yours, means "grant your personal GitHub first", never "the App is broken". | owner or admin |
|
|
10
|
+
| **Uninstalling is safe either way** | `<their cloud>/settings/connections` | Removing the App on GitHub self-heals the registry; unlinking in Catalyst uninstalls upstream best-effort. Neither needs a support ticket. | owner or admin |
|
|
11
|
+
| **One team, one repository** | `<their cloud>/settings/repositories` | One Linear team maps to at most one repository per account, and one GitHub repository to at most one, case-insensitively. | owner or admin |
|
|
12
|
+
| **Neither half can be re-pointed** | `<their cloud>/settings/repositories` | A repository is exactly one team plus one GitHub repo; the only editable field is its name. A monorepo across two teams, or one team across two repos, cannot be modelled today — do not promise it. | owner or admin |
|
|
13
|
+
| **An archived repository still holds both keys** | `<their cloud>/settings/repositories` | Re-registering the same team-plus-repo pair is refused until the archived entry is restored. | owner or admin |
|
|
14
|
+
| **Merge policy** | `<their cloud>/settings/repositories/$repoId/merging` | The effective policy and where it came from are served on the contract's `merge` block; this is the page that changes it (`catalyst-github`'s mergeability read never sets it). | owner or admin |
|
|
15
|
+
| **Reviewers** | `<their cloud>/settings/repositories/$repoId/code-reviews` | "Post review requests as" names one connected member; a member cannot volunteer or refuse. A strict merge policy with no reviewer configured never earns the merge label — configure a reviewer, or change the policy on the merging page. | owner or admin |
|
|
16
|
+
| **The merge queue is assumed, and this bundle cannot see it** | (no settings page; the repository's own merge automation — a Mergify configuration, GitHub's native merge queue, or whatever else that repository runs) | The merge phase never merges: its one write is the ready label. Without a queue consuming that label the label is still the cloud's attestation, and the merge itself is whatever the repository's own setup does with it. Nothing here detects or repairs that, and nothing here knows which queue a tenant runs; say that by name rather than guessing, and never name a vendor the tenant has not named first. | whoever administers the GitHub repository |
|
|
17
|
+
| **The runner cap** | `<their cloud>/settings/repositories` | Default 20 running phases per repository; a paused repository resolves to zero without losing the stored value. The tenant page is display-only — lowering it is an operator action. See `references/what-runs-next.md`'s concurrency section. | operator (the page only displays it) |
|
|
18
|
+
| **Secrets are write-only; environment variables are readable** | `<their cloud>/settings/secrets` and `<their cloud>/settings/environment`, plus the per-repository sections under `<their cloud>/settings/repositories` | A name is a secret **or** an environment variable, never both; a repository-scoped value wins over a tenant-scoped one; an environment variable may reference a vault secret by name, expanded only when the container is built. | owner or admin |
|
|
19
|
+
| **"My secrets are not in the container"** | `<their cloud>/settings/repositories` (the team the repository is registered under) | When a repository was registered without naming its team, nothing reaches a runner, and the failure is silent — the symptom is a phase whose build cannot find its environment. The fix is an admin or operator action, not a retry; raise it as an ask. Name the symptom and the page, never the registry table behind it. | owner or admin |
|
|
20
|
+
| **Routing has no write route and no UI** | none — there is no settings page for this | Stage defaults are global and the per-tenant precedence table is empty, so an empty precedence always resolves to the default provider. A Codex-first pipeline is not something to promise today. See `references/coding-accounts.md` and `references/what-runs-next.md`'s routing decision. | nobody — not configurable yet |
|
|
@@ -17,6 +17,16 @@ The queue re-derives within seconds of any board change and at the top of every
|
|
|
17
17
|
- Dispatch buckets by repository and splits slots across teams so no team starves another. A slot that is running, might be running, or is restarting is never free capacity.
|
|
18
18
|
- Comment-wake work (an agent answering a human comment) shares the same cap as ladder work.
|
|
19
19
|
|
|
20
|
+
## The WIP limit
|
|
21
|
+
|
|
22
|
+
A Catalyst project (one Linear team, across every repository registered to it) starts no new ticket while its work in progress is at its limit. The limit is per project, never per repository and never workspace-wide: the project's own stored setting when there is one, else a tenant-wide value the operator set, else 12. `0` is a real limit and holds every new start. A workspace owner or admin sets it per project through the project-settings route; there is no page for it yet.
|
|
23
|
+
|
|
24
|
+
It holds only new starts. A ticket in the dispatch column that has never run gets no first phase while the team is at its limit, and the explainer prints `wip_limit` with a detail such as "14 tickets in progress in <team key>, at or above its WIP limit of 12; this ticket starts once one finishes". A ticket that has already started keeps getting every later phase, whatever the count.
|
|
25
|
+
|
|
26
|
+
What counts as in progress: every live ticket of the team that is past the dispatch column, or was granted a first phase even if its card still sits there, and is not in a terminal state (done, canceled, duplicate). Blocked, parked, waiting on the merge queue or waiting on a human all count: the count is tickets, not running containers. What does not count: triage and backlog states, a dispatch-column ticket that never started, a ticket fenced for a worker outside the cloud (the local-lane label), and a ticket whose PR merged and that only the runner-pin gate holds.
|
|
27
|
+
|
|
28
|
+
How to read a fleet idle at the limit: nothing runs because the tickets in progress are all waiting, on the merge queue, on a hold, on a paused repository or on a person. Unstick those (`unstick`, `catalyst-github`, `what-needs-me`); the limit is doing its job. Raising the limit is the project owner's call, not a repair.
|
|
29
|
+
|
|
20
30
|
## The routing decision, in five checks
|
|
21
31
|
|
|
22
32
|
When a phase is about to start, a route is chosen per candidate, in order, and the first survivor wins:
|
|
@@ -62,7 +72,15 @@ No survivor is `no_eligible_account_slot` when any candidate was skipped on capa
|
|
|
62
72
|
| `retry_backoff` | retrying in place, waiting out its 2/5/15-minute rung |
|
|
63
73
|
| `routing_unavailable` | claimed then refused at kickoff: no route, no eligible slot, or the provider is unavailable; the detail names which |
|
|
64
74
|
| `repo_paused` | an operator paused the repository |
|
|
65
|
-
| `
|
|
75
|
+
| `wip_limit` | the project is at its WIP limit (see above): its tickets in progress are waiting, and this new start waits until one of them finishes |
|
|
76
|
+
| `remediate_parked` | the remediate phase is parked, so the failing phase has nowhere to be repaired; `catalyst-skills release <ticket>` releases the park once its cause is fixed |
|
|
77
|
+
| `phase_parked` | the offered phase is parked after repeated failures or a spent repair-round cap; the person releases it once its cause is fixed |
|
|
78
|
+
| `later_phase_lease_held` | an earlier phase is offered while a live container still holds a later phase of this ticket |
|
|
79
|
+
| `human_owned_pr` | a person's own pull request holds this ticket; that person closes or merges the PR themselves, and no release command clears it |
|
|
80
|
+
| `review_not_converging` | review and repair kept finding new problems without converging |
|
|
81
|
+
| `round_threshold` | the ticket spent its lifetime repair budget |
|
|
82
|
+
| `claim_storm` | claimed too many times in the last hour; it waits the hour out, and there is nothing to release |
|
|
83
|
+
| `repo_at_capacity` | the repository's runner seats are all in use; it starts when one frees |
|
|
66
84
|
|
|
67
85
|
## The unknowns (the evaluator fails closed)
|
|
68
86
|
|
|
@@ -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.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — 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.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Unstick
|
|
11
11
|
|