@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +38 -32
  3. package/dist/args.js +16 -2
  4. package/dist/ask.js +2 -2
  5. package/dist/capabilities.js +70 -0
  6. package/dist/cli.js +42 -29
  7. package/dist/config.js +1 -1
  8. package/dist/connections.js +5 -5
  9. package/dist/env/declaration-rules.js +163 -0
  10. package/dist/env/group.js +51 -0
  11. package/dist/env/inventory.js +102 -0
  12. package/dist/env/render.js +39 -0
  13. package/dist/env/scan-env-example.js +62 -0
  14. package/dist/env/scan-source.js +43 -0
  15. package/dist/env/scan-workflow.js +123 -0
  16. package/dist/env/scan-wrangler.js +120 -0
  17. package/dist/env/types.js +1 -0
  18. package/dist/env/walk.js +87 -0
  19. package/dist/env.js +76 -0
  20. package/dist/environment.js +4 -4
  21. package/dist/events.js +1 -1
  22. package/dist/execution.js +3 -2
  23. package/dist/identity.js +3 -3
  24. package/dist/oauth.js +3 -3
  25. package/dist/query.js +27 -1
  26. package/dist/ready.js +3 -3
  27. package/dist/release.js +1 -1
  28. package/dist/replica.js +9 -7
  29. package/dist/runtime-store.js +2 -2
  30. package/dist/runtime.js +1 -1
  31. package/dist/secret.js +1 -1
  32. package/dist/skills.js +1 -1
  33. package/dist/team.js +329 -0
  34. package/package.json +4 -4
  35. package/skills/catalyst-github/SKILL.md +1 -1
  36. package/skills/catalyst-linear/SKILL.md +1 -1
  37. package/skills/catalyst-onboard/SKILL.md +7 -7
  38. package/skills/catalyst-onboard/references/choosing-a-coding-account.md +2 -2
  39. package/skills/catalyst-onboard/references/declaring-a-repository.md +1 -1
  40. package/skills/catalyst-onboard/references/local-sync.md +3 -3
  41. package/skills/catalyst-onboard/references/skill-sources.md +1 -1
  42. package/skills/catalyst-onboard/references/the-one-path.md +12 -10
  43. package/skills/catalyst-onboard/references/what-a-phase-needs.md +5 -5
  44. package/skills/catalyst-onboard/references/what-the-browser-owns.md +6 -9
  45. package/skills/catalyst-onboard/references/who-fixes-what.md +35 -16
  46. package/skills/catalyst-onboard/scripts/local-sync.mjs +6 -6
  47. package/skills/catalyst-onboard/scripts/where-am-i.mjs +133 -54
  48. package/skills/catalyst-setup/SKILL.md +1 -1
  49. package/skills/connect-me/SKILL.md +3 -3
  50. package/skills/how-catalyst-works/SKILL.md +5 -3
  51. package/skills/how-catalyst-works/references/settings-and-where-they-live.md +20 -0
  52. package/skills/how-catalyst-works/references/what-runs-next.md +19 -1
  53. package/skills/run-this-project/SKILL.md +1 -1
  54. package/skills/unstick/SKILL.md +1 -1
  55. package/skills/what-needs-me/SKILL.md +1 -1
  56. package/skills/what-needs-me/references/reading-the-inbox.md +4 -0
  57. package/skills/what-this-repo-needs/SKILL.md +40 -0
  58. package/skills/what-this-repo-needs/agents/openai.yaml +6 -0
  59. package/skills/what-this-repo-needs/agents/portability.yaml +4 -0
  60. package/skills/what-this-repo-needs/references/what-each-group-means.md +68 -0
  61. package/skills/what-this-repo-needs/scripts/check.mjs +64 -0
  62. package/skills/what-this-repo-needs/scripts/inventory.mjs +61 -0
  63. package/skills/what-this-repo-needs/scripts/lib/cli.mjs +114 -0
  64. package/skills/what-this-repo-needs/scripts/lib/credential.mjs +29 -0
  65. package/skills/whats-happening/SKILL.md +1 -1
  66. package/skills/whats-happening/references/reading-the-board.md +1 -1
  67. package/skills/whats-happening/references/reprioritising.md +1 -1
  68. package/skills/whats-happening/references/status-reply.md +2 -0
  69. package/skills/whats-happening/references/why-is-it-stuck.md +41 -33
package/dist/team.js ADDED
@@ -0,0 +1,329 @@
1
+ // Team setup uses the person's saved login and the SDK's typed personal-admin methods.
2
+ // A fresh SDK client per call refreshes a device-login token across a multi-chunk migration.
3
+ import { createHash } from "node:crypto";
4
+ import { flagBool, flagList, flagString, positionals } from "./args.js";
5
+ import { requireConfig } from "./config.js";
6
+ import { loadContract } from "./contract.js";
7
+ import { UsageError } from "./errors.js";
8
+ import { bearerFor } from "./oauth.js";
9
+ import { loadHttpSdk } from "./sdk.js";
10
+ const SLOTS = ["dispatch", "intake", "research", "plan", "implement", "remediate", "verify", "review", "pr", "done", "canceled"];
11
+ function isSlot(value) { return SLOTS.includes(value); }
12
+ const NAMES = {
13
+ dispatch: ["todo", "to do", "ready"], intake: ["intake", "triage"], research: ["research"],
14
+ plan: ["plan", "planning"], implement: ["implement", "in progress", "doing"],
15
+ remediate: ["remediate", "fix"], verify: ["verify", "validate", "testing"], review: ["review"],
16
+ pr: ["pr", "in review", "pull request"], done: ["done", "completed"], canceled: ["canceled", "cancelled"],
17
+ };
18
+ const MAX_CHUNKS = 100;
19
+ const MAX_RUN_MS = 5 * 60_000;
20
+ function routes(doc) {
21
+ const read = doc.routes.find((r) => r.method === "GET" && r.path.endsWith("/team-workflow"))?.path;
22
+ if (!read)
23
+ throw new UsageError("team setup needs a newer Catalyst Cloud (team-workflow route absent)");
24
+ const desired = { teams: read.replace(/\/team-workflow$/, "/teams"), read, check: `${read}/check`, save: `${read}/save`, adopt: `${read}/adopt`, undo: `${read}/adopt-undo`, migrate: `${read}/migrate` };
25
+ for (const [name, path] of Object.entries(desired)) {
26
+ const method = name === "teams" || name === "read" ? "GET" : "POST";
27
+ if (!doc.routes.some((r) => r.method === method && r.path === path))
28
+ throw new UsageError(`team setup needs a newer Catalyst Cloud (${method} ${path} absent)`);
29
+ }
30
+ return desired;
31
+ }
32
+ function key(args) {
33
+ const [sub, team, ...extra] = positionals(args);
34
+ if (extra.length > 0)
35
+ throw new UsageError(`team ${sub ?? ""} takes one team key`);
36
+ if (!team && !(sub === "list" || (sub === "check" && flagBool(args, "all"))))
37
+ throw new UsageError(`team ${sub ?? ""} needs a team key`);
38
+ if (team && flagBool(args, "all"))
39
+ throw new UsageError("team check takes a key or --all, not both");
40
+ return team ?? "";
41
+ }
42
+ function emit(ctx, args, value, lines) {
43
+ if (args.json)
44
+ ctx.stdout(JSON.stringify(value));
45
+ else
46
+ for (const line of lines)
47
+ ctx.stdout(line);
48
+ }
49
+ function refusal(ctx, args, reply) {
50
+ const { body, status } = reply;
51
+ emit(ctx, args, { status, ...body }, [`refused (${status}): ${String(body.error ?? "http")}${body.reason || body.message ? ` — ${String(body.reason ?? body.message)}` : ""}`]);
52
+ return 1;
53
+ }
54
+ function readinessLines(value) {
55
+ if (value === null || typeof value !== "object")
56
+ return ["the change landed, but readiness could not be checked; run team check"];
57
+ const r = value;
58
+ return [`readiness: ${String(r.status ?? "unknown")}; checked at ${String(r.checkedAt ?? "unknown")}`,
59
+ ...(Array.isArray(r.checks) ? r.checks : []).filter((c) => c.state === "fail").map((c) => ` ${String(c.id)}: ${String(c.reason ?? "failed")} (${String(c.owner ?? c.whoCanFix ?? "team admin")})`)];
60
+ }
61
+ function fromSdk(result) {
62
+ if (result.outcome === "ok") {
63
+ const { outcome: _outcome, status, ...body } = result;
64
+ return { status, body: body };
65
+ }
66
+ return {
67
+ status: result.outcome === "shape" ? 502 : "status" in result ? result.status : 503,
68
+ body: { error: "error" in result ? result.error ?? result.outcome : result.outcome, reason: result.reason, ...(result.outcome === "shape" ? { upstreamStatus: result.status } : {}) },
69
+ };
70
+ }
71
+ function isRefusal(reply) { return typeof reply.body.error === "string" || !((reply.status >= 200 && reply.status < 300) || reply.status === 304); }
72
+ function assignments(values, label) {
73
+ return values.map((value) => {
74
+ const split = value.indexOf("=");
75
+ if (split <= 0 || split === value.length - 1)
76
+ throw new UsageError(`--${label} needs left=right (got ${value})`);
77
+ return [value.slice(0, split), value.slice(split + 1)];
78
+ });
79
+ }
80
+ function mapping(view, explicit) {
81
+ if (view.stageSource === "none" || !Array.isArray(view.stages))
82
+ throw new UsageError("live Linear stages are unreadable; retry team map after the connection is healthy");
83
+ const stages = view.stages;
84
+ const selected = new Map();
85
+ const unresolved = [];
86
+ if (explicit.length > 0) {
87
+ for (const [slot, stateName] of explicit) {
88
+ if (!isSlot(slot))
89
+ throw new UsageError(`unknown workflow role: ${slot}`);
90
+ if (selected.has(slot))
91
+ throw new UsageError(`duplicate workflow role: ${slot}`);
92
+ const found = stages.filter((s) => s.name.toLocaleLowerCase() === stateName.toLocaleLowerCase() || s.id === stateName);
93
+ if (found.length !== 1)
94
+ throw new UsageError(`${stateName} matches ${found.length} live Linear stages; choose an unambiguous name or id`);
95
+ selected.set(slot, found[0].id);
96
+ }
97
+ }
98
+ else {
99
+ for (const slot of SLOTS) {
100
+ const names = NAMES[slot] ?? [];
101
+ const match = names.flatMap((name) => stages.filter((stage) => stage.name.toLocaleLowerCase() === name));
102
+ const unique = [...new Map(match.map((stage) => [stage.id, stage])).values()];
103
+ if (unique.length === 1)
104
+ selected.set(slot, unique[0].id);
105
+ else
106
+ unresolved.push(slot);
107
+ }
108
+ }
109
+ const previous = new Map((view.rows ?? []).map((r) => [r.slot, r.linearStateId]));
110
+ const diff = SLOTS.map((slot) => {
111
+ const before = previous.get(slot) ?? null;
112
+ const after = selected.get(slot) ?? null;
113
+ return { slot, before, after, change: before === after ? "unchanged" : before === null ? "added" : after === null ? "removed" : "changed" };
114
+ });
115
+ return { rows: SLOTS.filter((slot) => selected.has(slot)).map((slot) => ({ slot, linearStateId: selected.get(slot), source: "chosen" })), diff, unresolved };
116
+ }
117
+ function hashPlan(value) { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
118
+ function planLines(title, rows, hash) {
119
+ return [title, ...rows.map((row) => ` ${JSON.stringify(row)}`), `Plan hash: ${hash}`, `Review this plan with the person, then rerun with --yes --plan-hash ${hash} to apply exactly it.`];
120
+ }
121
+ export async function cmdTeam(args, ctx, deps = {}) {
122
+ const [sub] = positionals(args);
123
+ if (!sub || !["list", "check", "map", "adopt", "migrate", "checklist"].includes(sub))
124
+ throw new UsageError("team needs list | check | map | adopt | migrate | checklist");
125
+ const team = key(args);
126
+ if (sub !== "check" && flagBool(args, "all"))
127
+ throw new UsageError("--all belongs to team check");
128
+ if (sub !== "adopt" && flagBool(args, "undo"))
129
+ throw new UsageError("--undo belongs to team adopt");
130
+ if (sub !== "migrate" && (flagBool(args, "retire") || flagList(args, "choice").length))
131
+ throw new UsageError("--retire and --choice belong to team migrate");
132
+ if (sub !== "map" && flagList(args, "stage").length)
133
+ throw new UsageError("--stage belongs to team map");
134
+ if (!["map", "adopt", "migrate"].includes(sub) && flagString(args, "plan-hash") !== undefined)
135
+ throw new UsageError("--plan-hash belongs to team map, adopt, or migrate");
136
+ if ((sub === "check" || sub === "checklist") && flagBool(args, "yes"))
137
+ throw new UsageError(`team ${sub} takes no --yes`);
138
+ const cfg = requireConfig(ctx);
139
+ let { doc } = await loadContract(ctx, cfg);
140
+ try {
141
+ routes(doc);
142
+ }
143
+ catch (error) {
144
+ if (!(error instanceof UsageError) || !error.message.includes("team-workflow"))
145
+ throw error;
146
+ // A fresh cached contract can predate the team routes. Revalidate once before telling the
147
+ // person the cloud is too old; the conditional GET is cheap when the server has not changed.
148
+ doc = (await loadContract(ctx, cfg, { refresh: true })).doc;
149
+ routes(doc);
150
+ }
151
+ // API hashes detect stale server state. CLI approval additionally binds the reviewed scope,
152
+ // operation and signed-in tenant so consent cannot transfer to a different command or account.
153
+ const approvalHash = (operation, plan) => hashPlan({ baseUrl: cfg.baseUrl, account: cfg.account, actor: cfg.user?.id ?? null, team, operation, plan });
154
+ const createClient = deps.createClient ?? (async (options) => (await loadHttpSdk()).createTenantClient(options));
155
+ const client = async () => (await createClient({ key: await bearerFor(ctx, cfg), baseUrl: cfg.baseUrl, fetch: ctx.fetch })).teamWorkflow;
156
+ if (sub === "list") {
157
+ const reply = fromSdk(await (await client()).teams());
158
+ if (isRefusal(reply))
159
+ return refusal(ctx, args, reply);
160
+ if (!Array.isArray(reply.body.teams))
161
+ return refusal(ctx, args, { status: 502, body: { error: "bad-response", reason: "team list was missing" } });
162
+ const teams = reply.body.teams;
163
+ emit(ctx, args, { teams, liveTeamRead: reply.body.liveTeamRead }, teams.map((t) => `${String(t.teamKey ?? t.teamId ?? t.key ?? "unknown")}: ${String(t.name ?? t.teamName ?? "")}`));
164
+ return 0;
165
+ }
166
+ if (sub === "check") {
167
+ const teamList = flagBool(args, "all") ? fromSdk(await (await client()).teams()) : null;
168
+ if (teamList && isRefusal(teamList))
169
+ return refusal(ctx, args, teamList);
170
+ const teamKeys = teamList ? (Array.isArray(teamList.body.teams) ? teamList.body.teams : []).map((t) => String(t.teamKey || t.teamId || t.key || "")).filter(Boolean) : [team];
171
+ if (teamList && !Array.isArray(teamList.body.teams))
172
+ return refusal(ctx, args, { status: 502, body: { error: "bad-response", reason: "team list was missing" } });
173
+ if (teamList && teamKeys.length === 0)
174
+ return refusal(ctx, args, { status: 404, body: { error: "no-teams", reason: "No Linear teams are visible to this account; connect the tenant's Linear workspace and retry." } });
175
+ const results = [];
176
+ let failed = false;
177
+ for (const teamKey of teamKeys) {
178
+ const reply = fromSdk(await (await client()).check(teamKey));
179
+ if (isRefusal(reply)) {
180
+ results.push({ team: teamKey, status: reply.status, ...reply.body });
181
+ failed = true;
182
+ break;
183
+ }
184
+ results.push({ team: teamKey, ...reply.body });
185
+ if (reply.body.readiness?.status !== "ready")
186
+ failed = true;
187
+ }
188
+ emit(ctx, args, { results }, results.flatMap((result) => [`${String(result.team)}:`, ...(result.error ? [` refused: ${String(result.error)} — ${String(result.reason ?? result.message ?? "")}`] : readinessLines(result.readiness))]));
189
+ return failed ? 1 : 0;
190
+ }
191
+ if (sub === "checklist") {
192
+ const reply = fromSdk(await (await client()).get(team));
193
+ if (isRefusal(reply))
194
+ return refusal(ctx, args, reply);
195
+ const checklist = reply.body.checklist;
196
+ if (!Array.isArray(checklist) || !checklist.every((line) => typeof line === "string"))
197
+ return refusal(ctx, args, { status: 503, body: { error: "stages-unreadable", reason: "Could not read live Linear stages; retry team checklist." } });
198
+ emit(ctx, args, { team, checklist }, checklist);
199
+ return 0;
200
+ }
201
+ if (sub === "map") {
202
+ const reply = fromSdk(await (await client()).get(team));
203
+ if (isRefusal(reply))
204
+ return refusal(ctx, args, reply);
205
+ if (typeof reply.body.mappingHash !== "string")
206
+ return refusal(ctx, args, { status: 502, body: { error: "bad-response", reason: "team workflow read omitted the mapping hash; update Catalyst Cloud before saving" } });
207
+ let proposal;
208
+ try {
209
+ proposal = mapping(reply.body, assignments(flagList(args, "stage"), "stage"));
210
+ }
211
+ catch (error) {
212
+ return refusal(ctx, args, { status: 400, body: { error: "mapping-unresolved", reason: error instanceof Error ? error.message : String(error) } });
213
+ }
214
+ const config = reply.body.config && typeof reply.body.config === "object" ? reply.body.config : undefined;
215
+ const mode = config?.mode ?? "mapped-existing";
216
+ const gitAutomation = config?.gitAutomation ?? "off";
217
+ const planHash = approvalHash("map", { team, rows: proposal.rows, diff: proposal.diff, unresolved: proposal.unresolved, mappingHash: reply.body.mappingHash, mode, gitAutomation });
218
+ const preview = { team, rows: proposal.rows, diff: proposal.diff, unresolved: proposal.unresolved, mappingHash: reply.body.mappingHash, mode, gitAutomation, planHash, decision: "not-confirmed" };
219
+ if (!flagBool(args, "yes")) {
220
+ emit(ctx, args, preview, planLines(`Mapping plan for ${team}:`, proposal.diff, planHash));
221
+ return 3;
222
+ }
223
+ if (flagString(args, "plan-hash") !== planHash)
224
+ return refusal(ctx, args, { status: 409, body: { error: "plan-hash-mismatch", reason: `Preview again and review the updated plan hash ${planHash}.` } });
225
+ if (!args.json)
226
+ for (const line of planLines(`Mapping plan for ${team}:`, proposal.diff, planHash).slice(0, -1))
227
+ ctx.stdout(line);
228
+ const saved = fromSdk(await (await client()).save({ team, mode, gitAutomation, rows: proposal.rows, expectedMappingHash: reply.body.mappingHash }));
229
+ if (isRefusal(saved))
230
+ return refusal(ctx, args, saved);
231
+ emit(ctx, args, { ...preview, decision: "applied", result: saved.body }, readinessLines(saved.body.readiness));
232
+ return 0;
233
+ }
234
+ if (sub === "adopt") {
235
+ const undo = flagBool(args, "undo");
236
+ const previewReply = fromSdk(undo ? await (await client()).undoPreview(team) : await (await client()).adoptPreview(team));
237
+ if (isRefusal(previewReply))
238
+ return refusal(ctx, args, previewReply);
239
+ const hashName = undo ? "undoHash" : "planHash";
240
+ const hash = previewReply.body[hashName];
241
+ if (typeof hash !== "string" || !Array.isArray(previewReply.body[undo ? "candidates" : "stages"]))
242
+ return refusal(ctx, args, { status: 502, body: { error: "bad-response", reason: `adopt preview omitted ${hashName} or scope` } });
243
+ const planHash = approvalHash(undo ? "adopt-undo" : "adopt", {
244
+ serverHash: hash, teamId: previewReply.body.teamId,
245
+ stages: previewReply.body.stages, labels: previewReply.body.labels,
246
+ candidates: previewReply.body.candidates, unfilledLoadBearing: previewReply.body.unfilledLoadBearing,
247
+ });
248
+ const plan = { team, decision: "not-confirmed", preview: previewReply.body, planHash };
249
+ const scope = [
250
+ ...previewReply.body[undo ? "candidates" : "stages"],
251
+ ...(undo ? [] : Array.isArray(previewReply.body.labels) ? previewReply.body.labels : []),
252
+ ];
253
+ if (!flagBool(args, "yes")) {
254
+ emit(ctx, args, plan, planLines(`${undo ? "Undo" : "Adopt"} plan for ${team}:`, scope, planHash));
255
+ return 3;
256
+ }
257
+ if (flagString(args, "plan-hash") !== planHash)
258
+ return refusal(ctx, args, { status: 409, body: { error: "plan-hash-mismatch", reason: `Preview again and review the updated plan hash ${planHash}.` } });
259
+ if (!args.json)
260
+ for (const line of planLines(`${undo ? "Undo" : "Adopt"} plan for ${team}:`, scope, planHash).slice(0, -1))
261
+ ctx.stdout(line);
262
+ const applied = fromSdk(undo ? await (await client()).undoApply(team, hash) : await (await client()).adoptApply(team, hash));
263
+ if (isRefusal(applied))
264
+ return refusal(ctx, args, applied);
265
+ const outcomes = (undo ? [applied.body.archived, applied.body.kept, applied.body.failed] : [applied.body.stages, applied.body.labels, applied.body.labelsNotCreated, applied.body.provenanceGaps, applied.body.labelProvenanceGaps]).flatMap((v) => Array.isArray(v) ? v : []);
266
+ emit(ctx, args, { team, decision: "applied", preview: previewReply.body, result: applied.body }, [...outcomes.map((o) => ` ${JSON.stringify(o)}`), ...readinessLines(applied.body.readiness)]);
267
+ return 0;
268
+ }
269
+ const choices = assignments(flagList(args, "choice"), "choice").map(([sourceStateId, destinationStateId]) => ({ sourceStateId, destinationStateId }));
270
+ const previewReply = fromSdk(await (await client()).migratePreview(team, choices));
271
+ if (isRefusal(previewReply))
272
+ return refusal(ctx, args, previewReply);
273
+ const preview = previewReply.body.preview;
274
+ if (!preview || typeof preview.migrationHash !== "string" || !Array.isArray(preview.sources))
275
+ return refusal(ctx, args, { status: 502, body: { error: "bad-response", reason: "migration preview omitted hash or source rows" } });
276
+ if (preview.actionsAvailable === false)
277
+ return refusal(ctx, args, { status: 409, body: { error: "migration-approval-unavailable", reason: "Catalyst cannot verify the exact tickets in this migration preview, so moves and retirement are paused. Use Linear's bulk editor to move tickets, or retry when approved migration snapshots are available." } });
278
+ const step = flagBool(args, "retire") ? "retire" : "migrate";
279
+ const planHash = approvalHash(step, { preview, choices });
280
+ const plan = { team, step, decision: "not-confirmed", preview, planHash };
281
+ const blocked = preview.overLimit || preview.sources.some((source) => source.outcome === "needs-a-choice");
282
+ if (!flagBool(args, "yes")) {
283
+ emit(ctx, args, plan, planLines(`Migration ${plan.step} plan for ${team}:`, preview.sources, planHash));
284
+ return 3;
285
+ }
286
+ if (flagString(args, "plan-hash") !== planHash)
287
+ return refusal(ctx, args, { status: 409, body: { error: "plan-hash-mismatch", reason: `Preview again and review the updated plan hash ${planHash}.` } });
288
+ if (blocked)
289
+ return refusal(ctx, args, { status: 400, body: { error: "migration-plan-blocked", reason: "Resolve the preview's blocked sources or issue limit before applying." } });
290
+ if (!args.json)
291
+ for (const line of planLines(`Migration ${plan.step} plan for ${team}:`, preview.sources, planHash).slice(0, -1))
292
+ ctx.stdout(line);
293
+ const results = [];
294
+ const deadline = Date.now() + MAX_RUN_MS;
295
+ let hash = preview.migrationHash;
296
+ for (let n = 0; n < (step === "retire" ? 1 : MAX_CHUNKS); n++) {
297
+ if (Date.now() >= deadline)
298
+ return refusal(ctx, args, { status: 503, body: { error: "migration-time-budget", reason: "The bounded run ended; preview again before continuing." } });
299
+ const result = fromSdk(step === "retire" ? await (await client()).migrateRetire(team, hash, choices) : await (await client()).migrateChunk(team, hash, choices));
300
+ if (isRefusal(result))
301
+ return refusal(ctx, args, result);
302
+ results.push(result.body);
303
+ const partial = Array.isArray(result.body.sources) && result.body.sources.some((source) => source.outcome === "partially-moved");
304
+ if (!args.json) {
305
+ ctx.stdout(`${step} chunk ${n + 1}: moved ${String(result.body.moved ?? 0)}, remaining ${String(result.body.remaining ?? 0)}`);
306
+ for (const source of Array.isArray(result.body.sources) ? result.body.sources : [])
307
+ ctx.stdout(` ${JSON.stringify(source)}`);
308
+ for (const [name, value] of Object.entries(result.body))
309
+ if (name === "failed" || name === "blockers" || name === "logGaps")
310
+ ctx.stdout(` ${name}: ${JSON.stringify(value)}`);
311
+ }
312
+ if (partial) {
313
+ emit(ctx, args, { ...plan, decision: "partial", results }, [`Migration partially completed; at least one source reported a refused or incomplete move.`, ...readinessLines(result.body.readiness)]);
314
+ return 1;
315
+ }
316
+ if (step === "retire" && Array.isArray(result.body.failed) && result.body.failed.length > 0) {
317
+ emit(ctx, args, { ...plan, decision: "partial", results }, [`Source retirement partially completed: ${JSON.stringify(result.body.failed)}`]);
318
+ return 1;
319
+ }
320
+ if (step === "retire" || result.body.remaining === 0) {
321
+ emit(ctx, args, { ...plan, decision: "applied", results }, [...readinessLines(result.body.readiness), ...(step === "migrate" ? ["Tickets moved. Retiring source stages requires a separate team migrate --retire confirmation."] : [])]);
322
+ return 0;
323
+ }
324
+ if (typeof result.body.migrationHash !== "string")
325
+ return refusal(ctx, args, { status: 502, body: { error: "bad-response", reason: "migration chunk omitted the next hash" } });
326
+ hash = result.body.migrationHash;
327
+ }
328
+ return refusal(ctx, args, { status: 503, body: { error: "migration-chunk-limit", reason: "The bounded run ended; preview again before continuing." } });
329
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/cli",
3
- "version": "0.9.4",
3
+ "version": "0.13.0",
4
4
  "description": "Customer skill bundle for Catalyst Cloud, named from the executive's seat — get set up, see what is happening, see what needs you, run a project, unstick what is stuck — plus the catalyst CLI that reads through the Catalyst Cloud SDK, writes through the agent proxy, and connects your machine to your tenant with one command. The skills directory is the roster; this line never counts it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,13 +48,13 @@
48
48
  "paths:check": "node scripts/vendor-paths.mjs --check"
49
49
  },
50
50
  "dependencies": {
51
- "@catalyst-cloud/sdk": "^0.12.0"
51
+ "@catalyst-cloud/sdk": "^0.13.0",
52
+ "yaml": "^2.9.0"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/node": "^26.3.0",
55
56
  "@vitest/coverage-v8": "^4.1.11",
56
57
  "typescript": "^7.0.2",
57
- "vitest": "^4.1.11",
58
- "yaml": "^2.9.0"
58
+ "vitest": "^4.1.11"
59
59
  }
60
60
  }
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
8
8
 
9
9
  # Catalyst's GitHub
10
10
 
@@ -5,7 +5,7 @@ description: >-
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  disable-model-invocation: true
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
9
9
 
10
10
  # Catalyst Linear
11
11
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: catalyst-onboard
3
3
  description: >-
4
- Walk a person from nothing to their first Catalyst Cloud ticket running, one step at a time, hand-held. Use when someone says "set me up", "onboard me", "I just signed up", "get me started", "what do I do first", or when they have the Catalyst Cloud skills installed and nothing else. Starts with the coding account, then the project, the integrations and the person's connected accounts, each repository's settings file, and ends with one real ticket moving. Reads each part of the setup with the instrument that owns it, does every step a key can do through the catalyst-skills CLI, and for the steps only a browser can do hands over the exact page and says what to come back with. Never claims a step it did not watch succeed.
4
+ Walk a person from nothing to their first Catalyst Cloud ticket running, one step at a time, hand-held. Use when someone says "set me up", "onboard me", "I just signed up", "get me started", "what do I do first", or when they have the Catalyst Cloud skills installed and nothing else. Starts with the coding account, then the project, the integrations and the person's connected accounts, each repository's settings file, and ends with one real ticket moving. Reads each part of the setup with the instrument that owns it, does every step a key can do through the catalyst CLI, and for the steps only a browser can do hands over the exact page and says what to come back with. Never claims a step it did not watch succeed.
5
5
  disable-model-invocation: true
6
- allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
+ allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.4 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.13.0 — written in this repository for customer tenants -->
9
9
 
10
10
  # Onboard me
11
11
 
@@ -17,9 +17,9 @@ The path has five stops, in this order: a **coding account** the work will run o
17
17
 
18
18
  1. Run `node scripts/where-am-i.mjs --next` and read the one line it prints. If a `note:` line follows it, say the note in one clause in the same turn (a cancelled account is kept for reporting, not used, never re-tokened); it is information, not a task.
19
19
  2. Say, in one or two sentences, what that step is for. The person should always know why they are doing it.
20
- 3. Ask one question, or do one thing. If it is a command a key can run, run it and show the lines it printed. If it needs a browser, hand over one link, one action named the way the page names it, and say what you will check when they are back.
20
+ 3. Ask one question, or do one thing. If it is a command a key can run (the script's `do:` line), run it now without asking whether to, and show the lines it printed. If it needs a browser, hand over one link, one action named the way the page names it, and say what you will check when they are back.
21
21
  4. Stop. Wait for their answer.
22
- 5. When they answer, run the script again and read them the part that should have changed. If it changed, say so and move on. If it did not, refresh the contract once (`catalyst-skills contract --refresh`) and read it again; if it still did not, say what the page told them and what the instrument says, and ask what they saw.
22
+ 5. When they answer, run the script again and read them the part that should have changed. If it changed, say so and move on. If it did not, refresh the contract once (`catalyst contract --refresh`) and read it again; if it still did not, say what the page told them and what the instrument says, and ask what they saw.
23
23
 
24
24
  Two or three short sentences per turn is the right size. A turn that lists every part's verdict is a wall of status, not help. The full report exists for you to read, not to paste.
25
25
 
@@ -67,8 +67,8 @@ Use the product's words, and name the page label Settings shows today when it di
67
67
  - **Not ready is a question about who.** Name which check, who can fix it, and where. If the owner is not the person in front of you, say so and stop.
68
68
  - **Never invent a count or a list.** Every number and every name comes from what a command printed.
69
69
  - **Provider consent belongs to the person in a browser.** Login approval, the workspace's Linear connection and GitHub App install, and the person's own connected accounts. Give the printed URL, then check `status`. Never claim a grant succeeded because a browser opened.
70
- - **Some steps a key cannot do yet.** Listing every project, saving a stage mapping, adopting the workflow, registering a repository and approving one repository's settings file are settings-page work today. Route those through the browser and say plainly that it is a gap, not the design. Never guess at a route.
71
- - **The workspace-wide environment declaration is the one setup write you can perform.** `catalyst-skills environment` reads it, proposes it and approves it.
70
+ - **Team setup is CLI work now; two steps still are not.** `team list`, `team check`, `team map`, `team adopt`, `team migrate` and `team checklist` run with the person's own login: show each preview, and pass `--yes --plan-hash` only after they approve that exact plan. `catalyst capabilities` says what this CLI can do on this cloud, and the script consults it and the person's role before it names a command or a page. Registering a repository and approving one repository's settings file are settings-page work today; route those through the browser and say plainly that it is a gap, not the design. Never guess at a route.
71
+ - **The workspace-wide environment declaration is the one setup write you can perform.** `catalyst environment` reads it, proposes it and approves it.
72
72
  - **Local sync is opt-in.** API-backed skills work without it. Ask before running `local-sync.mjs --start`; a detached process starting is not evidence of freshness. See `references/local-sync.md`.
73
73
  - **A cancelled or ended coding account is kept, not retired.** It stays for reporting, is not used, and is never given a new token; a token minted from a live login must never land on it. Reactivating it is the person's call if the subscription comes back. Never tell anyone to retire or delete it.
74
74
  - **Their workspace, as them.** Everything goes through the CLI and the person's own login. You never name another workspace, never ask for a key you could avoid, and never see a credential or a secret's value.
@@ -29,13 +29,13 @@ Enrolling is a browser step, by construction: the person pastes a credential int
29
29
 
30
30
  1. Hand over the page the script printed for the part (Settings → AI accounts). Say: "Choose the provider, give the account a label you will recognise, and paste the credential the page asks for. Nothing on this side sees it."
31
31
  2. Wait. Do not run anything while they are on the page.
32
- 3. When they say it is saved, run `node scripts/where-am-i.mjs` and read them the `coding accounts` part. An enrolled and active account, named by its label, is proof. If the part still says none is enrolled, refresh the contract once (`catalyst-skills contract --refresh`) and read it again.
32
+ 3. When they say it is saved, run `node scripts/where-am-i.mjs` and read them the `coding accounts` part. An enrolled and active account, named by its label, is proof. If the part still says none is enrolled, refresh the contract once (`catalyst contract --refresh`) and read it again.
33
33
 
34
34
  Only a workspace owner or admin can enrol one (the page still says "tenant" in places). If the person is neither, say who can, and stop this step there.
35
35
 
36
36
  ## Naming accounts
37
37
 
38
- Call an account what the page calls it: its label, else its email, else its slot. `catalyst-skills accounts --json` carries this as `displayName`. Never call one "the first account" or "account 2", and never read out a credential.
38
+ Call an account what the page calls it: its label, else its email, else its slot. `catalyst accounts --json` carries this as `displayName`. Never call one "the first account" or "account 2", and never read out a credential.
39
39
 
40
40
  ## An account that already exists
41
41
 
@@ -43,4 +43,4 @@ run = ["npm", "ci"]
43
43
 
44
44
  ## After the merge
45
45
 
46
- `catalyst-skills ready` reports `environment_declared` for the team's default repository and names the next step: commit the file, fix it, or approve it. The `catalyst-setup` check table has each reason. The workspace-wide declaration is separate; `catalyst-skills environment` handles it (also step 8 of `references/the-one-path.md`).
46
+ `catalyst ready` reports `environment_declared` for the team's default repository and names the next step: commit the file, fix it, or approve it. The `catalyst-setup` check table has each reason. The workspace-wide declaration is separate; `catalyst environment` handles it (also step 8 of `references/the-one-path.md`).
@@ -2,14 +2,14 @@
2
2
 
3
3
  **For:** a person who wants an on-machine event file and searchable replica. The normal API-backed skills work without this, so ask before starting a background writer.
4
4
 
5
- **You run:** `node scripts/local-sync.mjs` to show the current local state. Only if the person opts in, run `node scripts/local-sync.mjs --start`. The script uses `catalyst-skills replica start --detach`, whose writer also starts the local event cache. It waits up to 90 seconds; `--wait <seconds>` sets a bounded wait from 0 to 300.
5
+ **You run:** `node scripts/local-sync.mjs` to show the current local state. Only if the person opts in, run `node scripts/local-sync.mjs --start`. The script uses `catalyst replica start --detach`, whose writer also starts the local event cache. It waits up to 90 seconds; `--wait <seconds>` sets a bounded wait from 0 to 300.
6
6
 
7
- **Read back:** `Local sync current` only when both the replica and event cache have live writer heartbeats and each cursor equals its own cloud head. A detached process starting, one fresh heartbeat alone, or replica freshness alone does not prove event freshness. `catalyst-skills replica status --probe --json` checks the replica cursor; `catalyst-skills events status --probe --json` checks the separate event cursor. Either command can report stale or unknown; unknown means the cloud comparison could not be proved, not that the cache is stale. Exit 0 means current, 1 stale, 2 unknown or not connected, and 3 absent. Do not tell the person local data is current unless the combined check says so.
7
+ **Read back:** `Local sync current` only when both the replica and event cache have live writer heartbeats and each cursor equals its own cloud head. A detached process starting, one fresh heartbeat alone, or replica freshness alone does not prove event freshness. `catalyst replica status --probe --json` checks the replica cursor; `catalyst events status --probe --json` checks the separate event cursor. Either command can report stale or unknown; unknown means the cloud comparison could not be proved, not that the cache is stale. Exit 0 means current, 1 stale, 2 unknown or not connected, and 3 absent. Do not tell the person local data is current unless the combined check says so.
8
8
 
9
9
  **Owner:** you check and, with the person's opt-in, start. The local writer is optional. A reboot recovery service is separate work; if this machine restarts, recheck status before relying on local data.
10
10
 
11
11
  ## First ticket event
12
12
 
13
- Ask before starting the local writer. Before the person moves a card, run `node scripts/local-sync.mjs --start` after they opt in and wait until it reports current. Then run `catalyst-skills events status --probe --json` and record its `cursor` as `<cursor-before-move>`. Move the card only after this baseline is captured. Once the card is moved, run `catalyst-skills events wait-for --ticket <ticket-identifier> --after <cursor-before-move> --timeout 300` with the identifier `explain` printed. Do not guess an event type. The explicit cursor lets `wait-for` find the event if it reached the local cache before the command began.
13
+ Ask before starting the local writer. Before the person moves a card, run `node scripts/local-sync.mjs --start` after they opt in and wait until it reports current. Then run `catalyst events status --probe --json` and record its `cursor` as `<cursor-before-move>`. Move the card only after this baseline is captured. Once the card is moved, run `catalyst events wait-for --ticket <ticket-identifier> --after <cursor-before-move> --timeout 300` with the identifier `explain` printed. Do not guess an event type. The explicit cursor lets `wait-for` find the event if it reached the local cache before the command began.
14
14
 
15
15
  Exit 0 prints a matching event after the recorded baseline and proves it reached this machine's cache. Exit 1 means no matching cached event arrived within five minutes; recheck `node scripts/local-sync.mjs` and report stale or unknown evidence without inferring a cloud or webhook failure. Exit 3 means the event cache is absent; ask before starting it. Other errors are unknown. `explain` remains the API-backed source for why a ticket can or cannot run.
@@ -23,7 +23,7 @@ The `npx skills add --all` command replaces existing same-named directories and
23
23
 
24
24
  ## Is each pack there
25
25
 
26
- `catalyst-skills ready` checks the Cloud pack only. Check `catalyst-onboard/SKILL.md` (Cloud pack) and `research-codebase/SKILL.md` (development pack) in the intended directory. If both are there, have the person type `/catalyst-onboard` (`$catalyst-onboard` in Codex).
26
+ `catalyst ready` checks the Cloud pack only. Check `catalyst-onboard/SKILL.md` (Cloud pack) and `research-codebase/SKILL.md` (development pack) in the intended directory. If both are there, have the person type `/catalyst-onboard` (`$catalyst-onboard` in Codex).
27
27
 
28
28
  ## Migrate an existing installation
29
29
 
@@ -18,7 +18,7 @@ Each step states: what it is for, what you ask or run or hand over, **what you r
18
18
 
19
19
  **For:** giving this machine a credential, so every later read is the person's own. The installer normally did this; you are here only when the script says the machine is not connected.
20
20
 
21
- **You ask:** "Shall I start the login now?" On a yes, run `catalyst-skills login`. It prints a short code and a URL. The person approves it in a browser, from a phone if this machine has none. Do not offer the key form; it exists for a person who already holds a personal key and says so.
21
+ **You ask:** "Shall I start the login now?" On a yes, run `catalyst login`. It prints a short code and a URL. The person approves it in a browser, from a phone if this machine has none. Do not offer the key form; it exists for a person who already holds a personal key and says so.
22
22
 
23
23
  **Read back:** the `Connected to …` and `Connected as …` lines, then the script again so the machine part flips.
24
24
 
@@ -40,7 +40,7 @@ Each step states: what it is for, what you ask or run or hand over, **what you r
40
40
 
41
41
  **You hand over:** `<their cloud>/settings/connections` (the Integrations page; Settings labels it Connections today), and say: connect Linear. It sends them to Linear to authorise and brings them back.
42
42
 
43
- **Read back:** after they say it is done, re-run the script and read them the `account` line. A resolved workspace is proof. If it still reads unresolved, run `catalyst-skills contract --refresh` and read it again.
43
+ **Read back:** after they say it is done, re-run the script and read them the `account` line. A resolved workspace is proof. If it still reads unresolved, run `catalyst contract --refresh` and read it again.
44
44
 
45
45
  **Owner:** a workspace owner or admin, in a browser. **Browser by construction**: it is an authorization grant, and no key can perform one.
46
46
 
@@ -48,11 +48,13 @@ Each step states: what it is for, what you ask or run or hand over, **what you r
48
48
 
49
49
  **For:** the project. A project is one Linear team plus the repositories registered to it. Until a project's stages are mapped, a card moved into it does nothing at all; this is the single most common reason a new workspace sees no activity.
50
50
 
51
- **You hand over:** `<their cloud>/settings/linear-teams`. They pick one team and press **Map my stages**, or **Adopt the Catalyst workflow** if they want Catalyst's stages created for them.
51
+ **You run:** `catalyst team list` to see the teams without checking readiness. Have the person choose one team key. Then `catalyst team map <KEY>`, or `catalyst team adopt <KEY>` if they want Catalyst's stages created for them: each prints a preview; show it, and pass `--yes --plan-hash <hash>` only after they approve that exact plan. Run `catalyst team check <KEY>` only for the selected team when you need its verdict. The same steps exist on `<their cloud>/settings/linear-teams` (**Map my stages**, **Adopt the Catalyst workflow**) for a person who prefers the page.
52
52
 
53
- **Read back:** re-run the script and read the `projects` line. A project that now appears with a readiness verdict is proof it was saved. Read them the verdict and any failing checks by name.
53
+ **Read back:** re-run the script and read the `projects` line: the verdict and any failing checks by name. A project that reads `blocked` is not set up whatever its gate says; the script names the blocking checks and their owner. Three of them are Linear's own Git automation rules (in Linear: Settings → Teams → the team → Workflow → Git automation, set the named rule to No action). No Catalyst key can change those, so say so, hand it over, and afterwards run `catalyst team check <KEY>`.
54
54
 
55
- **Owner:** a workspace owner or admin. ⛔ Listing the projects and saving a mapping are settings-page work today; a key cannot do either yet. Say that plainly; it is a gap in the product, not something they did wrong.
55
+ For manual setup, `catalyst team checklist <KEY>` prints the same lines as the browser. If it cannot read the live stages, fix the Linear connection and retry. Moving tickets out of old stages is a separate decision: preview with `team migrate <KEY>`, review source and destination ids and counts, then confirm with `--yes --plan-hash <hash>` from that preview. Retiring emptied source stages requires a later `team migrate <KEY> --retire` preview and a separate approval using its preview hash. `team adopt <KEY> --undo` likewise previews the exact stages previously created by Adopt before asking for confirmation.
56
+
57
+ **Owner:** a workspace owner or admin, with their own login. You run the commands; the person picks the team and approves each write after seeing its preview. A run that exits 3 stopped before applying anything.
56
58
 
57
59
  ⭐ **One project at a time is safe, and lead with this.** Mapping one project changes no other project's stages and moves no other project's tickets. Encourage a pilot: the project they care least about breaking.
58
60
 
@@ -80,9 +82,9 @@ Each step states: what it is for, what you ask or run or hand over, **what you r
80
82
 
81
83
  **For:** the person. The integrations serve the whole workspace; a connected account is this person's own Linear or GitHub login, so that what Catalyst does for them is attributed to them, and asks assigned to them can be told apart from everyone else's.
82
84
 
83
- **You run:** `catalyst-skills connections personal linear start`. It prints a short-lived URL and tries to open it; if the browser does not open, hand them the URL. Then `catalyst-skills connections personal linear status`. Do GitHub the same way, with `github`, only after the GitHub App is installed and the repository is registered: `catalyst-skills connections personal github start`, then `status`.
85
+ **You run:** `catalyst connections personal linear start`. It prints a short-lived URL and tries to open it; if the browser does not open, hand them the URL. Then `catalyst connections personal linear status`. Do GitHub the same way, with `github`, only after the GitHub App is installed and the repository is registered: `catalyst connections personal github start`, then `status`.
84
86
 
85
- **Read back:** each `status`, and the script's `person` part: their label and role, whether their Linear identity is matched, and each grant. A URL opening is not proof a grant landed. If Linear identity stays unmatched after consent, `catalyst-skills identity linear status`, then `options`; the person picks their own listed identity and you run `catalyst-skills identity linear set <linearUserId>`. An already-resolved or claimed identity, and an inactive seat, need an owner or admin.
87
+ **Read back:** each `status`, and the script's `person` part: their label and role, whether their Linear identity is matched, and each grant. A URL opening is not proof a grant landed. If Linear identity stays unmatched after consent, `catalyst identity linear status`, then `options`; the person picks their own listed identity and you run `catalyst identity linear set <linearUserId>`. An already-resolved or claimed identity, and an inactive seat, need an owner or admin.
86
88
 
87
89
  **Owner:** you start and check; the person approves in a browser.
88
90
 
@@ -90,7 +92,7 @@ Each step states: what it is for, what you ask or run or hand over, **what you r
90
92
 
91
93
  **For:** the repository's own settings, in `.catalyst/catalyst.toml` on its default branch: the Linear team it belongs to, the names of the variables and secrets its build needs, and its setup commands. Names leave the machine; values are entered once, by them, in the app.
92
94
 
93
- **You ask:** one thing at a time. First: "Shall I draft it from your repository's own build files?" On a yes, ask where the repository is checked out on this machine; if it is not, write the file here for them to add. Then `references/declaring-a-repository.md`: the inventory, the file's shape, the pull request. Say what happens after the merge as context, not as a second task for now. After the merge, an owner or admin approves the revision on the repository's Environment page (Settings → Repositories → the repository → Environment → Setup declaration → Approve this revision) and enters the values on the same page. A workspace-wide declaration, if they want one, goes through `catalyst-skills environment` (read, propose, `--approve`), the one setup write you can perform.
95
+ **You ask:** one thing at a time. First: "Shall I draft it from your repository's own build files?" On a yes, ask where the repository is checked out on this machine; if it is not, write the file here for them to add. Then `references/declaring-a-repository.md`: the inventory, the file's shape, the pull request. Say what happens after the merge as context, not as a second task for now. After the merge, an owner or admin approves the revision on the repository's Environment page (Settings → Repositories → the repository → Environment → Setup declaration → Approve this revision) and enters the values on the same page. A workspace-wide declaration, if they want one, goes through `catalyst environment` (read, propose, `--approve`), the one setup write you can perform.
94
96
 
95
97
  **Read back:** the script's `repository declarations` part, read from each project's `environment_declared` check: no file yet, invalid, awaiting approval, or in effect, per repository. Names only, never a value.
96
98
 
@@ -100,9 +102,9 @@ Each step states: what it is for, what you ask or run or hand over, **what you r
100
102
 
101
103
  **For:** the only thing that proves setup worked.
102
104
 
103
- **You run:** `catalyst-skills ready`. Its READY does not cover the coding account or the host; the script does. Read them the verdict and every failing line with its own fix and owner. If it says NOT READY, go to `references/who-fixes-what.md` before you touch anything.
105
+ **You run:** `catalyst team check <KEY>` first when the script's next step names it (an owner or admin whose CLI has the verb), then `catalyst ready`. Its READY does not cover the coding account or the host; the script does. Read them the verdict and every failing line with its own fix and owner. If it says NOT READY, go to `references/who-fixes-what.md` before you touch anything.
104
106
 
105
- **Then:** ask them to move one card into the project's start stage (usually Todo) and tell you which ticket it was. That is the whole question; do not offer to explain the pipeline or to search the backlog instead. Then watch. `catalyst-skills explain <ticket>` says why it is or is not about to run. If they opted into local sync, use the optional first-event check in `references/local-sync.md`.
107
+ **Then:** one question, worded like this: "Move one ticket into ENG's start stage (usually Todo) and tell me its id." Nothing else in that turn: no offer to explain the pipeline, to look at the board, or to pick one for them. Then watch. `catalyst explain <ticket>` says why it is or is not about to run. If they opted into local sync, use the optional first-event check in `references/local-sync.md`.
106
108
 
107
109
  **Read back:** what `explain` actually said, and the first comment the agent leaves on the ticket. If `explain` says the ticket cannot start, the reason it names is the answer; use the `how-catalyst-works` skill for what it means, then `unstick` if something is holding it.
108
110
 
@@ -6,7 +6,7 @@ A person can finish every other step and still see nothing run. A phase needs tw
6
6
 
7
7
  **For:** a phase runs on one of the tenant's own enrolled coding accounts. With none active, no phase can start.
8
8
 
9
- **Instrument:** `codingAccounts` in `catalyst-skills contract`. It carries a `state`, a printable `line`, who enrolls an account (`enrolledByLine`) and the `page`. The script prints all of them. It never shows a credential or an email. When the state is `enrolled`, the script also reads `catalyst-skills accounts` to check each account's credential.
9
+ **Instrument:** `codingAccounts` in `catalyst contract`. It carries a `state`, a printable `line`, who enrolls an account (`enrolledByLine`) and the `page`. The script prints all of them. It never shows a credential or an email. When the state is `enrolled`, the script also reads `catalyst accounts` to check each account's credential.
10
10
 
11
11
  **What each state means:**
12
12
 
@@ -19,13 +19,13 @@ A person can finish every other step and still see nothing run. A phase needs tw
19
19
 
20
20
  **Owner:** the one the contract names. The person does it in the browser. Never ask for the credential, and never handle it. A key cannot enrol one. Which kinds of account exist, what each asks for, and what to do with an ended or cancelled one are in `references/choosing-a-coding-account.md`; it is step 2 of the path, the first thing asked.
21
21
 
22
- **Older cloud:** if the contract has no `codingAccounts`, the script says the cloud is older and reads `catalyst-skills accounts` instead. That list counts the accounts enrolled and the ones able to take work. An expired, revoked or quarantined account does not count. The owner is then a tenant owner or admin, at `<their cloud>/settings/coding-accounts`.
22
+ **Older cloud:** if the contract has no `codingAccounts`, the script says the cloud is older and reads `catalyst accounts` instead. That list counts the accounts enrolled and the ones able to take work. An expired, revoked or quarantined account does not count. The owner is then a tenant owner or admin, at `<their cloud>/settings/coding-accounts`.
23
23
 
24
24
  ## Host
25
25
 
26
26
  **For:** the contract carries this as the `hosts_current` readiness check on each project. It reads the same on every project, because it is about the whole account.
27
27
 
28
- **Instrument:** the `hosts_current` check in `catalyst-skills contract --path teams`, and its owner in `catalyst-skills contract --path readinessChecks`.
28
+ **Instrument:** the `hosts_current` check in `catalyst contract --path teams`, and its owner in `catalyst contract --path readinessChecks`.
29
29
 
30
30
  **What each reading means:**
31
31
 
@@ -35,11 +35,11 @@ A person can finish every other step and still see nothing run. A phase needs tw
35
35
  | `unknown`, `no_host_connected` | no Catalyst host is connected | name the owner and stop |
36
36
  | `unknown`, `hosts_unreported` | a host is connected but has not said which mapping it loaded | name the owner; it clears when the host reconnects |
37
37
  | `fail`, `hosts_behind` | a connected host runs an older mapping | name the owner |
38
- | the part reads `unreadable` | no project has been checked yet | press Re-check on the projects page, then read it again |
38
+ | the part reads `unreadable` | no project has been checked yet | run `catalyst team check <KEY>` when the script names it (it does when the CLI has the verb and the person is an owner or admin); otherwise press Re-check on the projects page; then read it again |
39
39
 
40
40
  **Owner:** the contract names the owner of `hosts_current`, and where they act in its `fixedWhere`. When `fixedWhere` has a page, the script prints it, and the command too when there is one. When it is null, which it is today, the script prints the owner sentence alone. Then there is no page, so do not invent one. Say who owns it and that they connect it.
41
41
 
42
- ⛔ `catalyst-skills ready` treats this check as a note, so it can print READY while no host is connected. READY there is not proof a phase can run. The `host` part is.
42
+ ⛔ `catalyst ready` treats this check as a note, so it can print READY while no host is connected. READY there is not proof a phase can run. The `host` part is.
43
43
 
44
44
  ## Saying it is ready
45
45