@catalyst-cloud/catalyst-skills 0.5.0 → 0.8.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 (127) hide show
  1. package/README.md +11 -178
  2. package/bin/catalyst-skills.js +3 -49
  3. package/bin/catalyst.js +3 -0
  4. package/package.json +8 -29
  5. package/CHANGELOG.md +0 -41
  6. package/LICENSE +0 -21
  7. package/dist/args.js +0 -261
  8. package/dist/ask.js +0 -158
  9. package/dist/browser.js +0 -20
  10. package/dist/cli.js +0 -356
  11. package/dist/config.js +0 -148
  12. package/dist/contract-types.js +0 -4
  13. package/dist/contract.js +0 -178
  14. package/dist/detach.js +0 -10
  15. package/dist/errors.js +0 -27
  16. package/dist/events.js +0 -106
  17. package/dist/execution.js +0 -369
  18. package/dist/oauth.js +0 -300
  19. package/dist/prompt.js +0 -35
  20. package/dist/query.js +0 -225
  21. package/dist/ready.js +0 -163
  22. package/dist/release.js +0 -142
  23. package/dist/replica.js +0 -325
  24. package/dist/sdk.js +0 -26
  25. package/dist/skill-shape.js +0 -179
  26. package/dist/skills.js +0 -78
  27. package/dist/transport.js +0 -205
  28. package/dist/ts-deps-loader.js +0 -68
  29. package/dist/watch/consumer.js +0 -141
  30. package/dist/watch/cursor-file.js +0 -62
  31. package/dist/watch.js +0 -175
  32. package/dist/write.js +0 -224
  33. package/skills/catalyst-github/SKILL.md +0 -35
  34. package/skills/catalyst-github/agents/openai.yaml +0 -6
  35. package/skills/catalyst-github/agents/portability.yaml +0 -4
  36. package/skills/catalyst-github/references/is-it-mergeable.md +0 -57
  37. package/skills/catalyst-github/references/what-a-pr-accumulates.md +0 -61
  38. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +0 -124
  39. package/skills/catalyst-github/scripts/lib/cli.mjs +0 -103
  40. package/skills/catalyst-github/scripts/lib/credential.mjs +0 -29
  41. package/skills/catalyst-github/scripts/lib/pull.mjs +0 -82
  42. package/skills/catalyst-github/scripts/read-pr.mjs +0 -97
  43. package/skills/catalyst-linear/SKILL.md +0 -43
  44. package/skills/catalyst-linear/agents/openai.yaml +0 -6
  45. package/skills/catalyst-linear/agents/portability.yaml +0 -5
  46. package/skills/catalyst-linear/references/reading-a-ticket.md +0 -50
  47. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +0 -53
  48. package/skills/catalyst-linear/references/writing-to-linear.md +0 -43
  49. package/skills/catalyst-linear/scripts/comment.mjs +0 -59
  50. package/skills/catalyst-linear/scripts/create-ticket.mjs +0 -44
  51. package/skills/catalyst-linear/scripts/label.mjs +0 -48
  52. package/skills/catalyst-linear/scripts/lib/cli.mjs +0 -164
  53. package/skills/catalyst-linear/scripts/lib/credential.mjs +0 -29
  54. package/skills/catalyst-linear/scripts/move.mjs +0 -41
  55. package/skills/catalyst-linear/scripts/read-ticket.mjs +0 -93
  56. package/skills/catalyst-linear/scripts/search.mjs +0 -49
  57. package/skills/catalyst-setup/SKILL.md +0 -32
  58. package/skills/catalyst-setup/agents/openai.yaml +0 -6
  59. package/skills/catalyst-setup/agents/portability.yaml +0 -4
  60. package/skills/catalyst-setup/references/what-each-check-means.md +0 -66
  61. package/skills/catalyst-setup/scripts/check.mjs +0 -75
  62. package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
  63. package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
  64. package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
  65. package/skills/connect-me/SKILL.md +0 -63
  66. package/skills/connect-me/agents/openai.yaml +0 -6
  67. package/skills/connect-me/agents/portability.yaml +0 -5
  68. package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
  69. package/skills/connect-me/scripts/lib/cli.mjs +0 -185
  70. package/skills/connect-me/scripts/lib/credential.mjs +0 -29
  71. package/skills/connect-me/scripts/verify-connection.mjs +0 -68
  72. package/skills/how-catalyst-works/SKILL.md +0 -42
  73. package/skills/how-catalyst-works/agents/openai.yaml +0 -6
  74. package/skills/how-catalyst-works/agents/portability.yaml +0 -4
  75. package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
  76. package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -54
  77. package/skills/how-catalyst-works/references/the-ladder.md +0 -41
  78. package/skills/how-catalyst-works/references/what-runs-next.md +0 -77
  79. package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
  80. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
  81. package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
  82. package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
  83. package/skills/how-catalyst-works/scripts/show-my-map.mjs +0 -94
  84. package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
  85. package/skills/run-this-project/SKILL.md +0 -45
  86. package/skills/run-this-project/agents/openai.yaml +0 -6
  87. package/skills/run-this-project/agents/portability.yaml +0 -5
  88. package/skills/run-this-project/assets/stall-policy.json +0 -15
  89. package/skills/run-this-project/references/making-work-ready.md +0 -60
  90. package/skills/run-this-project/references/reacting-to-events.md +0 -76
  91. package/skills/run-this-project/references/stalls-and-escalation.md +0 -63
  92. package/skills/run-this-project/scripts/lib/cli.mjs +0 -185
  93. package/skills/run-this-project/scripts/lib/credential.mjs +0 -29
  94. package/skills/run-this-project/scripts/make-ready.mjs +0 -64
  95. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  96. package/skills/run-this-project/scripts/watch-scope.mjs +0 -61
  97. package/skills/unstick/SKILL.md +0 -41
  98. package/skills/unstick/agents/openai.yaml +0 -6
  99. package/skills/unstick/agents/portability.yaml +0 -5
  100. package/skills/unstick/references/playbook.md +0 -51
  101. package/skills/unstick/scripts/lib/cli.mjs +0 -135
  102. package/skills/unstick/scripts/lib/credential.mjs +0 -29
  103. package/skills/unstick/scripts/unstick.mjs +0 -57
  104. package/skills/what-needs-me/SKILL.md +0 -41
  105. package/skills/what-needs-me/agents/openai.yaml +0 -6
  106. package/skills/what-needs-me/agents/portability.yaml +0 -5
  107. package/skills/what-needs-me/references/raising-a-decision.md +0 -41
  108. package/skills/what-needs-me/references/reading-the-inbox.md +0 -38
  109. package/skills/what-needs-me/references/settling-an-answer.md +0 -37
  110. package/skills/what-needs-me/scripts/inbox.mjs +0 -56
  111. package/skills/what-needs-me/scripts/lib/cli.mjs +0 -135
  112. package/skills/what-needs-me/scripts/lib/credential.mjs +0 -29
  113. package/skills/what-needs-me/scripts/raise.mjs +0 -53
  114. package/skills/what-needs-me/scripts/settle.mjs +0 -73
  115. package/skills/whats-happening/SKILL.md +0 -43
  116. package/skills/whats-happening/agents/openai.yaml +0 -6
  117. package/skills/whats-happening/agents/portability.yaml +0 -4
  118. package/skills/whats-happening/assets/status-reply.json +0 -77
  119. package/skills/whats-happening/references/reading-the-board.md +0 -43
  120. package/skills/whats-happening/references/reprioritising.md +0 -37
  121. package/skills/whats-happening/references/routing-work.md +0 -36
  122. package/skills/whats-happening/references/status-reply.md +0 -34
  123. package/skills/whats-happening/references/why-is-it-stuck.md +0 -62
  124. package/skills/whats-happening/scripts/explain.mjs +0 -28
  125. package/skills/whats-happening/scripts/lib/cli.mjs +0 -135
  126. package/skills/whats-happening/scripts/lib/credential.mjs +0 -29
  127. package/skills/whats-happening/scripts/snapshot.mjs +0 -145
package/dist/prompt.js DELETED
@@ -1,35 +0,0 @@
1
- // prompt.ts — the one interactive read in the CLI: asking for the personal key on a terminal without
2
- // echoing it. The streams are parameters so the tests can drive it without a real TTY.
3
- import { createInterface } from "node:readline";
4
- /** True when there is a terminal to prompt on. Both ends matter: the question needs somewhere to go. */
5
- export function stdinIsTty() {
6
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
7
- }
8
- /**
9
- * Read one line without echoing it. Everything readline would write after the question itself goes
10
- * to a muted stream, so the key never reaches the screen or a scrollback buffer; the caller still
11
- * gets it back, trimmed.
12
- */
13
- export async function promptSecret(question, io) {
14
- const input = io?.input ?? process.stdin;
15
- const target = io?.output ?? process.stdout;
16
- let muted = false;
17
- const output = Object.create(target);
18
- output.write = (chunk) => {
19
- if (!muted)
20
- target.write(chunk);
21
- return true;
22
- };
23
- const rl = createInterface({ input, output, terminal: true });
24
- try {
25
- const answer = await new Promise((resolve) => {
26
- rl.question(question, (value) => resolve(value));
27
- muted = true;
28
- });
29
- target.write("\n");
30
- return answer.trim();
31
- }
32
- finally {
33
- rl.close();
34
- }
35
- }
package/dist/query.js DELETED
@@ -1,225 +0,0 @@
1
- // query.ts — `query issues|issue|pulls|pull|projects|cycles|search|changes`: the replica when it is
2
- // fresh, else the API, with the source named on stderr every time. Output shape is the read-model
3
- // view either way. `pull`, `cycles`, `search` and `changes` are API-only (the SDK wraps no view for
4
- // them and the read-model package is not a declared dependency of this bundle).
5
- import { flagInt, flagString, positionals } from "./args.js";
6
- import { requireConfig, replicaDbPath } from "./config.js";
7
- import { CliError, UsageError } from "./errors.js";
8
- import { apiClient } from "./transport.js";
9
- import { engineFor, replicaStatus } from "./replica.js";
10
- import { loadSdk } from "./sdk.js";
11
- const REPLICA_CAPABLE = new Set(["issues", "issue", "pulls", "projects"]);
12
- export async function cmdQuery(args, ctx, deps = {}) {
13
- const [sub, ...rest] = positionals(args);
14
- if (!sub)
15
- throw new UsageError("query needs a subcommand: issues | issue <id> | pulls | pull <id> | projects | cycles | search <terms> | changes --since <n>");
16
- const cfg = requireConfig(ctx);
17
- const filters = {
18
- team: flagString(args, "team"),
19
- project: flagString(args, "project"),
20
- state: flagString(args, "state"),
21
- limit: flagInt(args, "limit", 50),
22
- };
23
- const forced = flagString(args, "source");
24
- if (forced && forced !== "replica" && forced !== "api")
25
- throw new UsageError("--source must be replica or api");
26
- const status = replicaStatus(ctx, cfg);
27
- let source;
28
- let why;
29
- if (!REPLICA_CAPABLE.has(sub)) {
30
- source = "api";
31
- why = `${sub} is api-only`;
32
- }
33
- else if (forced) {
34
- source = forced;
35
- why = `--source ${forced}`;
36
- }
37
- else if (status.verdict === "fresh") {
38
- source = "replica";
39
- why = `cursor ${status.cursor}`;
40
- }
41
- else {
42
- source = "api";
43
- why = `replica ${status.verdict}`;
44
- }
45
- ctx.stderr(source === "replica" ? `source: replica (${why})` : `source: api (${why})`);
46
- let result;
47
- if (source === "replica") {
48
- if (status.verdict === "absent" || status.verdict === "not-configured") {
49
- throw new UsageError(`--source replica but the replica is ${status.verdict}`);
50
- }
51
- result = await fromReplica(ctx, cfg, sub, rest, filters, deps);
52
- }
53
- else {
54
- result = await fromApi(ctx, cfg, sub, rest, filters, args);
55
- }
56
- if (result === null) {
57
- ctx.stderr(`${sub} ${rest[0] ?? ""}: not found`);
58
- return 1;
59
- }
60
- printResult(ctx, args, sub, result);
61
- return 0;
62
- }
63
- async function fromReplica(ctx, cfg, sub, rest, f, deps) {
64
- const sdk = await loadSdk();
65
- const dbPath = replicaDbPath(cfg, ctx.home);
66
- const engine = await engineFor(sdk, dbPath, ctx, { ...deps.engineDeps, readonly: true });
67
- const replica = await sdk.CatalystReplica.openReadOnly({ dbPath, engine, log: () => { } });
68
- try {
69
- switch (sub) {
70
- case "issues": {
71
- const rows = replica.issues({ limit: Math.max(f.limit, 200) });
72
- return applyFilters(rows, f).slice(0, f.limit);
73
- }
74
- case "issue":
75
- return replica.issue(needArg(rest, "issue <identifier>"));
76
- case "pulls":
77
- return replica.pulls({ limit: f.limit });
78
- case "projects":
79
- return applyFilters(replica.projects({ limit: Math.max(f.limit, 200) }), { limit: f.limit, state: f.state }).slice(0, f.limit);
80
- default:
81
- throw new UsageError(`${sub} is not readable from the replica`);
82
- }
83
- }
84
- finally {
85
- await replica.close();
86
- }
87
- }
88
- async function fromApi(ctx, cfg, sub, rest, f, args) {
89
- const api = apiClient(cfg, ctx);
90
- const common = { team: f.team, project: f.project, state: f.state, limit: f.limit };
91
- switch (sub) {
92
- case "issues": {
93
- const res = await api.getJson("/api/v1/issues", { query: common });
94
- return applyFilters(rowsOf(res.body), f).slice(0, f.limit);
95
- }
96
- case "issue": {
97
- const id = needArg(rest, "issue <identifier>");
98
- const res = await api.getJson(`/api/v1/issues/${encodeURIComponent(id)}`, { accept: [404] });
99
- return res.status === 404 ? null : res.body;
100
- }
101
- case "pulls": {
102
- const res = await api.getJson("/api/v1/pulls", { query: { limit: f.limit, ticket: flagString(args, "ticket") } });
103
- return rowsOf(res.body).slice(0, f.limit);
104
- }
105
- case "pull": {
106
- const id = needArg(rest, "pull <node id>");
107
- const res = await api.getJson(`/api/v1/pulls/${encodeURIComponent(id)}`, { accept: [404] });
108
- return res.status === 404 ? null : res.body;
109
- }
110
- case "projects": {
111
- const res = await api.getJson("/api/v1/projects", { query: { limit: f.limit } });
112
- return rowsOf(res.body).slice(0, f.limit);
113
- }
114
- case "cycles": {
115
- const res = await api.getJson("/api/v1/cycles", { query: { team: f.team, limit: f.limit } });
116
- return rowsOf(res.body);
117
- }
118
- case "search": {
119
- const q = rest.join(" ").trim();
120
- if (!q)
121
- throw new UsageError("search needs terms: query search <terms>");
122
- const res = await api.getJson("/api/v1/search", { query: { q, limit: f.limit } });
123
- return rowsOf(res.body);
124
- }
125
- case "changes": {
126
- const since = flagString(args, "since");
127
- if (since === undefined)
128
- throw new UsageError("changes needs --since <cursor>, or --since head to start from now");
129
- return await readChanges(api, since, f.limit);
130
- }
131
- default:
132
- throw new UsageError(`unknown query subcommand: ${sub}`);
133
- }
134
- }
135
- /** CTC-137 stamps the live head seq on EVERY `/changes` response — the 200 stream and both 409s. */
136
- const HEAD_SEQ_HEADER = "x-catalyst-head-seq";
137
- /**
138
- * `query changes --since <cursor|head>`.
139
- *
140
- * ⛔ THE CHANGEFEED EVICTS, so a refusal must name a cursor that works. `buildChanges` answers a
141
- * `since` below the oldest retained seq with 409 `cursor_underflow`, and one past the head with 409
142
- * `cursor_ahead_of_head` — both `{resync: true}`. `--since 0`, the form the docs show, is therefore
143
- * a 409 on any tenant whose log has rotated, and 0.2.0 surfaced it as a bare "GET /api/v1/changes
144
- * failed (409)" that left the customer with no way to learn a usable cursor. The head is stamped on
145
- * the refusal itself, so there is always something actionable to say.
146
- */
147
- async function readChanges(api, since, limit) {
148
- const resolved = since === "head" ? String(await headCursor(api)) : since;
149
- // ⛔ NDJSON ON 200, JSON ON A REFUSAL — see `getNdjson`. Reading this with `getJson` turned every
150
- // real success into "returned a non-JSON body" while every refusal parsed, which is why the verb
151
- // looked fine: `--since 0` 409s on a rotated feed, so no 200 was ever reached to fail on.
152
- const res = await api.getNdjson("/api/v1/changes", {
153
- query: { since: resolved, limit },
154
- accept: [409],
155
- });
156
- const head = res.headers.get(HEAD_SEQ_HEADER);
157
- if (res.status !== 409) {
158
- return { since: Number(resolved), head: head === null ? null : Number(head), changes: res.body };
159
- }
160
- const where = head === null ? "" : ` The feed's live cursor is ${head}.`;
161
- throw new CliError(`cursor ${resolved} is not usable: it is either past the feed's head or no longer in the change log, which keeps only recent changes.${where} Re-run with \`--since head\` to start from now, or with a cursor the feed still holds.`, "changefeed-resync", 1, 409);
162
- }
163
- /** The live head, read off any `/changes` response's own header — a 409 carries it too, which is
164
- * what makes this work on a tenant whose log has rotated past every cursor the caller could guess. */
165
- async function headCursor(api) {
166
- const probe = await api.getNdjson("/api/v1/changes", { query: { since: "0", limit: 1 }, accept: [409] });
167
- const raw = probe.headers.get(HEAD_SEQ_HEADER);
168
- const head = raw === null ? Number.NaN : Number(raw);
169
- if (!Number.isFinite(head)) {
170
- throw new CliError(`--since head needs the feed's live cursor, and this cloud did not send one (no ${HEAD_SEQ_HEADER} header). Pass an explicit --since <cursor>.`, "changefeed-head-unknown", 1);
171
- }
172
- return head;
173
- }
174
- function needArg(rest, usage) {
175
- const v = rest[0];
176
- if (!v)
177
- throw new UsageError(`query ${usage}`);
178
- return v;
179
- }
180
- /** The API answers a list route with either a bare array or `{rows|items|issues|...: []}`. */
181
- export function rowsOf(body) {
182
- if (Array.isArray(body))
183
- return body;
184
- if (body && typeof body === "object") {
185
- for (const key of ["rows", "items", "issues", "pulls", "projects", "cycles", "results", "changes", "data"]) {
186
- const v = body[key];
187
- if (Array.isArray(v))
188
- return v;
189
- }
190
- }
191
- return [];
192
- }
193
- export function applyFilters(rows, f) {
194
- return rows.filter((r) => {
195
- if (f.team && !(String(r.identifier ?? "").toUpperCase().startsWith(`${f.team.toUpperCase()}-`) || r.team_id === f.team || r.team_key === f.team))
196
- return false;
197
- if (f.project && r.project_id !== f.project)
198
- return false;
199
- if (f.state && String(r.state ?? "").toLowerCase() !== f.state.toLowerCase())
200
- return false;
201
- return true;
202
- });
203
- }
204
- function printResult(ctx, args, sub, result) {
205
- if (args.json || !Array.isArray(result)) {
206
- ctx.stdout(JSON.stringify(result, null, args.json ? 0 : 2));
207
- return;
208
- }
209
- for (const row of result)
210
- ctx.stdout(summaryLine(sub, row));
211
- }
212
- export function summaryLine(sub, r) {
213
- switch (sub) {
214
- case "issues":
215
- return `${r.identifier ?? r.id} ${r.state ?? "?"} ${r.title ?? ""}${r.assignee_name || r.assignee ? ` (${r.assignee_name ?? r.assignee})` : ""}`;
216
- case "pulls":
217
- return `#${r.number ?? "?"} ${r.state ?? "?"}${r.merged ? " merged" : ""} ${r.title ?? ""} [${r.node_id ?? ""}]`;
218
- case "projects":
219
- return `${r.id} ${r.state ?? "?"} ${r.name ?? ""}`;
220
- case "cycles":
221
- return `cycle ${r.number ?? r.id} ${r.name ?? ""} ${r.starts_at ?? ""} → ${r.ends_at ?? ""}`;
222
- default:
223
- return JSON.stringify(r);
224
- }
225
- }
package/dist/ready.js DELETED
@@ -1,163 +0,0 @@
1
- // ready.ts — `ready`: the machine checks plus the tenant's own readiness vector from the contract,
2
- // one verdict, and per failure the fix and who can apply it. The replica is optional, so its absence
3
- // is a note, never a failure; the SDK loading IS a check, because the replica and watch need it.
4
- import { existsSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { defaultSkillsDirFor, loadConfig, readManifest } from "./config.js";
7
- import { contractVersionInRange, readContractCache } from "./contract.js";
8
- import { CliError } from "./errors.js";
9
- import { replicaStatus, statusLine } from "./replica.js";
10
- import { loadSdk } from "./sdk.js";
11
- /** True when `a` is a semver-older release than `b`. Prerelease/build metadata is ignored; a segment
12
- * that does not parse counts as 0. A tiny compare on purpose — no dependency for three fields. */
13
- export function semverOlder(a, b) {
14
- const parse = (v) => {
15
- const core = v.split("-")[0].split("+")[0];
16
- const p = core.split(".");
17
- return [Number(p[0]) || 0, Number(p[1]) || 0, Number(p[2]) || 0];
18
- };
19
- const [a0, a1, a2] = parse(a);
20
- const [b0, b1, b2] = parse(b);
21
- if (a0 !== b0)
22
- return a0 < b0;
23
- if (a1 !== b1)
24
- return a1 < b1;
25
- return a2 < b2;
26
- }
27
- function whoCanAnswer(doc) {
28
- const roles = doc.humans.map((h) => `${h.role} ${h.linearUserId}`);
29
- return roles.length ? roles.join(", ") : "a tenant owner or admin (none resolved on the contract)";
30
- }
31
- export async function readyReport(ctx, deps) {
32
- const checks = [];
33
- const nodeMajor = deps.nodeMajor ?? Number(process.versions.node.split(".")[0]);
34
- checks.push(nodeMajor >= 22
35
- ? { id: "node", ok: true, line: `node: ${nodeMajor} (22 or newer required)` }
36
- : { id: "node", ok: false, line: `node: ${nodeMajor} is too old`, fix: "install Node 22 or newer (the SDK's built-in SQLite engine needs it)", who: "you" });
37
- let cfg = null;
38
- try {
39
- cfg = loadConfig(ctx.home);
40
- checks.push(cfg
41
- ? {
42
- id: "config",
43
- ok: true,
44
- // `principal` is "service" for every api key (it means "a key, not a browser session"); it
45
- // never names a person. Since CTC-2076 a personal key's /me carries a `user` block, so name
46
- // the connected person when there is one, and fall back to the account for a host key.
47
- line: cfg.user
48
- ? `config: joined ${cfg.name} as ${cfg.user.label} (${cfg.user.role})`
49
- : `config: joined ${cfg.name} (${cfg.slug}) as ${cfg.principal}`,
50
- }
51
- : { id: "config", ok: false, line: "config: not connected", fix: "npx @catalyst-cloud/catalyst-skills login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN)", who: "you (approve the login in your browser)" });
52
- }
53
- catch (err) {
54
- checks.push({ id: "config", ok: false, line: `config: ${err instanceof CliError ? err.message : String(err)}`, fix: "re-run login to rewrite it", who: "you" });
55
- }
56
- const cache = readContractCache(ctx.home);
57
- const range = readManifest().tenantContractRange;
58
- if (!cache) {
59
- checks.push({ id: "contract", ok: false, line: "contract: not cached", fix: "catalyst-skills contract --refresh", who: "you" });
60
- }
61
- else if (contractVersionInRange(cache.contractVersion, range) !== true) {
62
- checks.push({ id: "contract", ok: false, line: `contract: version ${cache.contractVersion} is outside this bundle's range ${range}`, fix: "npm install -g @catalyst-cloud/catalyst-skills@latest && catalyst-skills login", who: "you" });
63
- }
64
- else {
65
- checks.push({ id: "contract", ok: true, line: `contract: ${cache.contractVersion} cached ${cache.fetchedAt} (range ${range})` });
66
- }
67
- // The cloud MAY publish the bundle it expects (catalyst-cloud#3746). Read it defensively: warn (never
68
- // refuse) when this installed bundle is older than the minimum, and emit nothing when the field is
69
- // absent (an older cloud) — a stale bundle is a degrading note, so `ready` still returns READY.
70
- const minVersion = cache?.doc.skillsBundle?.minVersion;
71
- if (minVersion) {
72
- const installed = readManifest().version;
73
- if (semverOlder(installed, minVersion)) {
74
- checks.push({
75
- id: "bundle",
76
- ok: false,
77
- note: true,
78
- line: `bundle: ${installed} installed is older than the tenant's minimum ${minVersion} — upgrade: npm install -g @catalyst-cloud/catalyst-skills@latest && catalyst-skills login`,
79
- });
80
- }
81
- }
82
- if (cfg) {
83
- checks.push(cfg.cliPath && existsSync(cfg.cliPath)
84
- ? { id: "cliPath", ok: true, line: `cliPath: ${cfg.cliPath}` }
85
- : { id: "cliPath", ok: false, line: `cliPath: ${cfg.cliPath ? `${cfg.cliPath} does not exist` : "not recorded"}`, fix: "re-run login so the skill scripts can find this CLI", who: "you" });
86
- }
87
- // The skills are installed by the customer's own agent (a plugin, or `npx skills add`), so an
88
- // empty copy directory is the normal case and must not read as NOT READY. A PARTIAL copy is the
89
- // one broken state this check can see: half a set this package put there and never finished.
90
- const skillsDir = cfg?.skillsDir ?? defaultSkillsDirFor(ctx.home);
91
- const present = deps.skillNames.filter((n) => existsSync(join(skillsDir, n, "SKILL.md")));
92
- const missing = deps.skillNames.filter((n) => !present.includes(n));
93
- if (present.length === 0) {
94
- checks.push({ id: "skills", ok: true, note: true, line: `skills: none copied to ${skillsDir} — you are reading one, so your agent installed them its own way` });
95
- }
96
- else if (missing.length === 0) {
97
- checks.push({ id: "skills", ok: true, line: `skills: all ${deps.skillNames.length} present in ${skillsDir}` });
98
- }
99
- else {
100
- checks.push({ id: "skills", ok: false, line: `skills: ${present.length} of ${deps.skillNames.length} in ${skillsDir}, missing ${missing.join(", ")}`, fix: "catalyst-skills install", who: "you" });
101
- }
102
- try {
103
- await (deps.loadSdk ?? loadSdk)();
104
- checks.push({ id: "sdk", ok: true, line: "sdk: loads (replica and watch are available)" });
105
- }
106
- catch (err) {
107
- checks.push({ id: "sdk", ok: false, line: `sdk: ${err instanceof Error ? err.message : String(err)}`, fix: "run under Node 22.15 or newer (or bun); every read still works through the API meanwhile", who: "you" });
108
- }
109
- const replica = replicaStatus(ctx, cfg);
110
- checks.push({ id: "replica", ok: replica.verdict === "fresh", note: true, line: statusLine(replica) });
111
- if (cache) {
112
- const doc = cache.doc;
113
- const who = whoCanAnswer(doc);
114
- for (const team of doc.teams) {
115
- const label = team.key ?? team.id;
116
- if (team.readiness.status === "unchecked") {
117
- checks.push({ id: `team:${label}`, ok: true, note: true, line: `team ${label}: readiness not checked yet` });
118
- continue;
119
- }
120
- const bad = team.readiness.checks.filter((c) => c.state !== "pass");
121
- if (bad.length === 0 && team.readiness.status === "ready") {
122
- checks.push({ id: `team:${label}`, ok: true, line: `team ${label}: ready` });
123
- continue;
124
- }
125
- for (const c of bad) {
126
- const meta = doc.readinessChecks.find((r) => r.id === c.id);
127
- const needsAnswer = meta?.needsAnswer ?? true;
128
- checks.push({
129
- id: `team:${label}:${c.id}`,
130
- ok: !needsAnswer && c.state !== "fail",
131
- note: !needsAnswer,
132
- line: `team ${label}: ${c.id} is ${c.state}${c.reason ? ` (${c.reason}${c.count !== undefined ? ` ×${c.count}` : ""})` : ""}${meta ? `, ${meta.severity}` : ""}`,
133
- fix: `open settings for team ${label} and resolve ${c.id}`,
134
- who: needsAnswer ? who : "nobody yet; it is informational",
135
- });
136
- }
137
- if (bad.length === 0 && team.readiness.status !== "ready") {
138
- checks.push({ id: `team:${label}`, ok: team.readiness.status !== "blocked", note: team.readiness.status === "degraded", line: `team ${label}: ${team.readiness.status}`, fix: `open settings for team ${label}`, who });
139
- }
140
- }
141
- }
142
- const ready = checks.every((c) => c.ok || c.note);
143
- return { ready, checks };
144
- }
145
- export async function cmdReady(args, ctx, deps) {
146
- const report = await readyReport(ctx, deps);
147
- if (args.json) {
148
- ctx.stdout(JSON.stringify(report));
149
- }
150
- else {
151
- for (const c of report.checks) {
152
- ctx.stdout(`${c.note ? "note" : c.ok ? "ok " : "FAIL"} ${c.line}`);
153
- if (!c.ok && !c.note) {
154
- if (c.fix)
155
- ctx.stdout(` fix: ${c.fix}`);
156
- if (c.who)
157
- ctx.stdout(` who: ${c.who}`);
158
- }
159
- }
160
- ctx.stdout(report.ready ? "READY" : "NOT READY");
161
- }
162
- return report.ready ? 0 : 1;
163
- }
package/dist/release.js DELETED
@@ -1,142 +0,0 @@
1
- // release.ts — `catalyst-skills release`: a person's release of a parked or held ticket, or of every
2
- // ticket on one team parked under one failure class, through the contract's `ticket-release` and
3
- // `ticket-release-class` routes. The cloud reads every governor holding the ticket and either releases
4
- // all of them or releases nothing and names, per governor, the human action that does fix it; this verb
5
- // prints that answer and exits 1 on a refusal so a script can branch on it.
6
- //
7
- // ⛔ NOTHING IS RELEASED WITHOUT A REASON. `--because` (what changed) is required on a real release and
8
- // is recorded in the tenant's release audit; only `--dry-run` may omit it. `--retry-unchanged` is the
9
- // explicit statement that something the mirror cannot see changed — the cloud refuses an unchanged
10
- // cause without it.
11
- import { flagBool, flagString, positionals } from "./args.js";
12
- import { requireConfig } from "./config.js";
13
- import { loadContract } from "./contract.js";
14
- import { CliError, UsageError } from "./errors.js";
15
- import { needsNewerCloud } from "./execution.js";
16
- import { apiClient } from "./transport.js";
17
- /** One release written as a person reads it: "unparked implement", "cleared the validate hold". */
18
- export function describeAction(a) {
19
- switch (a.op) {
20
- case "unpark":
21
- return `unparked ${a.phase ?? "a phase"}`;
22
- case "clear_validate_hold":
23
- return "cleared the validate hold";
24
- case "clear_no_change_hold":
25
- return "cleared the no-change hold";
26
- case "grant_round_threshold_cycle":
27
- return "bought one more repair cycle";
28
- default:
29
- return `${a.op}${a.phase ? ` at ${a.phase}` : ""}`;
30
- }
31
- }
32
- const refusalLine = (r) => ` ${r.code}: ${r.humanAction}`;
33
- export function renderTicketRelease(ticket, r) {
34
- if (r.error === "team_changed") {
35
- return [`${ticket} moved from team ${r.expectedTeamKey ?? "(none)"} to ${r.teamKey ?? "(none)"} while the release was being checked — run it again`];
36
- }
37
- const lines = [];
38
- const dry = r.dryRun === true;
39
- if (r.outcome === "nothing-held") {
40
- lines.push(`${ticket}: nothing holds this ticket — run \`catalyst-skills explain ${ticket}\` for why it is not running`);
41
- }
42
- else if (r.outcome === "refused") {
43
- lines.push(dry ? `${ticket} (dry run): would be refused — nothing would be released:` : `${ticket}: refused — nothing was released:`);
44
- for (const x of r.refused ?? [])
45
- lines.push(refusalLine(x));
46
- const blocked = r.releasable ?? [];
47
- if (blocked.length > 0)
48
- lines.push(` (it would otherwise have ${blocked.map(describeAction).join(", ")})`);
49
- }
50
- else if (dry) {
51
- lines.push(`${ticket} (dry run): would release — ${(r.releasable ?? []).map(describeAction).join(", ")}`);
52
- }
53
- else {
54
- lines.push(`${ticket}: released — ${(r.released ?? []).map(describeAction).join(", ")}`);
55
- }
56
- for (const w of r.warnings ?? [])
57
- lines.push(`warning: ${w}`);
58
- return lines;
59
- }
60
- export function renderClassRelease(r) {
61
- const released = r.released ?? [];
62
- const refused = r.refused ?? [];
63
- const nothing = r.nothingHeld ?? [];
64
- const head = `${r.team ?? "?"} ${r.class ?? "?"}${r.dryRun ? " (dry run)" : ""}: released ${released.length}, refused ${refused.length}, nothing held ${nothing.length}${r.truncated ? " (more remain — run it again)" : ""}`;
65
- const lines = [head];
66
- for (const t of released) {
67
- const actions = r.dryRun ? (t.releasable ?? []) : (t.released ?? []);
68
- lines.push(` ${r.dryRun ? "would release" : "released"} ${t.ticket ?? "?"}: ${actions.map(describeAction).join(", ")}`);
69
- }
70
- for (const t of refused) {
71
- for (const x of t.refused ?? [])
72
- lines.push(` refused ${t.ticket ?? "?"} — ${x.code}: ${x.humanAction}`);
73
- }
74
- if (nothing.length > 0)
75
- lines.push(` nothing held: ${nothing.join(", ")}`);
76
- return lines;
77
- }
78
- /** The contract's route by name, or the "your cloud is older" error when this tenant does not serve it. */
79
- function releaseRoute(doc, name, cfg) {
80
- const route = doc.routes.find((r) => r.method === "POST" && r.path.split("/").filter(Boolean).at(-1) === name);
81
- if (!route)
82
- throw needsNewerCloud(`release (the tenant contract serves no "${name}" route)`, cfg);
83
- return route.path;
84
- }
85
- export async function cmdRelease(args, ctx) {
86
- const [ticket, ...extra] = positionals(args);
87
- const failureClass = flagString(args, "class");
88
- const team = flagString(args, "team");
89
- const because = flagString(args, "because");
90
- const dryRun = flagBool(args, "dry-run");
91
- const retryUnchanged = flagBool(args, "retry-unchanged");
92
- if (extra.length > 0)
93
- throw new UsageError("release takes one ticket");
94
- if (failureClass !== undefined && ticket !== undefined) {
95
- throw new UsageError("release takes a ticket or --class with --team, not both");
96
- }
97
- if (failureClass === undefined && ticket === undefined) {
98
- throw new UsageError("release needs a ticket, or --class <failure-class> --team <K>");
99
- }
100
- if (failureClass !== undefined && team === undefined)
101
- throw new UsageError("release --class needs --team <K>");
102
- if (because === undefined && !dryRun) {
103
- throw new UsageError("release needs --because <what changed> (recorded in the release audit), or --dry-run to preview");
104
- }
105
- const limitRaw = flagString(args, "limit");
106
- const limit = limitRaw === undefined ? undefined : Number(limitRaw);
107
- if (limit !== undefined && !Number.isInteger(limit))
108
- throw new UsageError("--limit must be an integer");
109
- const cfg = requireConfig(ctx);
110
- const { doc } = await loadContract(ctx, cfg);
111
- const api = apiClient(cfg, ctx);
112
- const common = { ...(because !== undefined ? { because } : {}), retryUnchanged, dryRun };
113
- if (failureClass !== undefined) {
114
- const path = releaseRoute(doc, "ticket-release-class", cfg);
115
- const res = await api.postJson(path, {
116
- team,
117
- class: failureClass,
118
- ...common,
119
- ...(limit !== undefined ? { limit } : {}),
120
- });
121
- if (args.json)
122
- ctx.stdout(JSON.stringify(res.body));
123
- else
124
- for (const line of renderClassRelease(res.body))
125
- ctx.stdout(line);
126
- // A real class release that refused any ticket exits 1, the same contract as a single refusal.
127
- return res.body.dryRun !== true && (res.body.refused ?? []).length > 0 ? 1 : 0;
128
- }
129
- const id = ticket;
130
- const path = releaseRoute(doc, "ticket-release", cfg);
131
- const res = await api.postJson(path, { ticket: id, ...common }, { accept: [404, 409] });
132
- if (res.status === 404) {
133
- throw new CliError(`ticket ${id} is not in this tenant's mirror — check the identifier`, "ticket-unknown");
134
- }
135
- if (args.json)
136
- ctx.stdout(JSON.stringify(res.body));
137
- else
138
- for (const line of renderTicketRelease(id, res.body))
139
- ctx.stdout(line);
140
- const refused = res.status === 409 || (res.body.outcome === "refused" && res.body.dryRun !== true);
141
- return refused ? 1 : 0;
142
- }