@catalyst-cloud/catalyst-skills 0.7.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 (140) hide show
  1. package/README.md +11 -182
  2. package/bin/catalyst-skills.js +3 -49
  3. package/bin/catalyst.js +3 -0
  4. package/package.json +8 -31
  5. package/CHANGELOG.md +0 -65
  6. package/LICENSE +0 -21
  7. package/dist/args.js +0 -273
  8. package/dist/ask.js +0 -161
  9. package/dist/browser.js +0 -20
  10. package/dist/cli.js +0 -361
  11. package/dist/config.js +0 -152
  12. package/dist/contract-types.js +0 -4
  13. package/dist/contract.js +0 -184
  14. package/dist/detach.js +0 -10
  15. package/dist/environment.js +0 -207
  16. package/dist/errors.js +0 -27
  17. package/dist/events.js +0 -106
  18. package/dist/execution.js +0 -451
  19. package/dist/oauth.js +0 -300
  20. package/dist/pagination.js +0 -76
  21. package/dist/prompt.js +0 -35
  22. package/dist/published.js +0 -75
  23. package/dist/query.js +0 -248
  24. package/dist/ready.js +0 -254
  25. package/dist/release.js +0 -142
  26. package/dist/replica.js +0 -585
  27. package/dist/sdk.js +0 -26
  28. package/dist/semver.js +0 -18
  29. package/dist/skill-shape.js +0 -189
  30. package/dist/skills.js +0 -129
  31. package/dist/transport.js +0 -205
  32. package/dist/ts-deps-loader.js +0 -113
  33. package/dist/watch/consumer.js +0 -141
  34. package/dist/watch/cursor-file.js +0 -62
  35. package/dist/watch.js +0 -175
  36. package/dist/write.js +0 -224
  37. package/skills/catalyst-github/SKILL.md +0 -35
  38. package/skills/catalyst-github/agents/openai.yaml +0 -6
  39. package/skills/catalyst-github/agents/portability.yaml +0 -4
  40. package/skills/catalyst-github/references/is-it-mergeable.md +0 -57
  41. package/skills/catalyst-github/references/what-a-pr-accumulates.md +0 -61
  42. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +0 -124
  43. package/skills/catalyst-github/scripts/lib/cli.mjs +0 -103
  44. package/skills/catalyst-github/scripts/lib/credential.mjs +0 -29
  45. package/skills/catalyst-github/scripts/lib/pull.mjs +0 -82
  46. package/skills/catalyst-github/scripts/read-pr.mjs +0 -97
  47. package/skills/catalyst-linear/SKILL.md +0 -43
  48. package/skills/catalyst-linear/agents/openai.yaml +0 -6
  49. package/skills/catalyst-linear/agents/portability.yaml +0 -5
  50. package/skills/catalyst-linear/references/reading-a-ticket.md +0 -52
  51. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +0 -53
  52. package/skills/catalyst-linear/references/writing-to-linear.md +0 -43
  53. package/skills/catalyst-linear/scripts/comment.mjs +0 -59
  54. package/skills/catalyst-linear/scripts/create-ticket.mjs +0 -44
  55. package/skills/catalyst-linear/scripts/label.mjs +0 -48
  56. package/skills/catalyst-linear/scripts/lib/cli.mjs +0 -164
  57. package/skills/catalyst-linear/scripts/lib/credential.mjs +0 -29
  58. package/skills/catalyst-linear/scripts/move.mjs +0 -41
  59. package/skills/catalyst-linear/scripts/read-ticket.mjs +0 -93
  60. package/skills/catalyst-linear/scripts/search.mjs +0 -49
  61. package/skills/catalyst-onboard/SKILL.md +0 -49
  62. package/skills/catalyst-onboard/agents/openai.yaml +0 -6
  63. package/skills/catalyst-onboard/agents/portability.yaml +0 -5
  64. package/skills/catalyst-onboard/references/the-one-path.md +0 -143
  65. package/skills/catalyst-onboard/references/what-the-browser-owns.md +0 -50
  66. package/skills/catalyst-onboard/references/who-fixes-what.md +0 -41
  67. package/skills/catalyst-onboard/scripts/lib/cli.mjs +0 -117
  68. package/skills/catalyst-onboard/scripts/lib/credential.mjs +0 -29
  69. package/skills/catalyst-onboard/scripts/where-am-i.mjs +0 -245
  70. package/skills/catalyst-setup/SKILL.md +0 -32
  71. package/skills/catalyst-setup/agents/openai.yaml +0 -6
  72. package/skills/catalyst-setup/agents/portability.yaml +0 -4
  73. package/skills/catalyst-setup/references/what-each-check-means.md +0 -78
  74. package/skills/catalyst-setup/scripts/check.mjs +0 -75
  75. package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
  76. package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
  77. package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
  78. package/skills/connect-me/SKILL.md +0 -63
  79. package/skills/connect-me/agents/openai.yaml +0 -6
  80. package/skills/connect-me/agents/portability.yaml +0 -5
  81. package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
  82. package/skills/connect-me/scripts/lib/cli.mjs +0 -185
  83. package/skills/connect-me/scripts/lib/credential.mjs +0 -29
  84. package/skills/connect-me/scripts/verify-connection.mjs +0 -68
  85. package/skills/how-catalyst-works/SKILL.md +0 -42
  86. package/skills/how-catalyst-works/agents/openai.yaml +0 -6
  87. package/skills/how-catalyst-works/agents/portability.yaml +0 -4
  88. package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
  89. package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -54
  90. package/skills/how-catalyst-works/references/the-ladder.md +0 -41
  91. package/skills/how-catalyst-works/references/what-runs-next.md +0 -77
  92. package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
  93. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
  94. package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
  95. package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
  96. package/skills/how-catalyst-works/scripts/show-my-map.mjs +0 -94
  97. package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
  98. package/skills/run-this-project/SKILL.md +0 -45
  99. package/skills/run-this-project/agents/openai.yaml +0 -6
  100. package/skills/run-this-project/agents/portability.yaml +0 -5
  101. package/skills/run-this-project/assets/stall-policy.json +0 -15
  102. package/skills/run-this-project/references/making-work-ready.md +0 -60
  103. package/skills/run-this-project/references/reacting-to-events.md +0 -76
  104. package/skills/run-this-project/references/stalls-and-escalation.md +0 -63
  105. package/skills/run-this-project/scripts/lib/cli.mjs +0 -185
  106. package/skills/run-this-project/scripts/lib/credential.mjs +0 -29
  107. package/skills/run-this-project/scripts/make-ready.mjs +0 -64
  108. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  109. package/skills/run-this-project/scripts/watch-scope.mjs +0 -61
  110. package/skills/unstick/SKILL.md +0 -41
  111. package/skills/unstick/agents/openai.yaml +0 -6
  112. package/skills/unstick/agents/portability.yaml +0 -5
  113. package/skills/unstick/references/playbook.md +0 -51
  114. package/skills/unstick/scripts/lib/cli.mjs +0 -135
  115. package/skills/unstick/scripts/lib/credential.mjs +0 -29
  116. package/skills/unstick/scripts/unstick.mjs +0 -57
  117. package/skills/what-needs-me/SKILL.md +0 -41
  118. package/skills/what-needs-me/agents/openai.yaml +0 -6
  119. package/skills/what-needs-me/agents/portability.yaml +0 -5
  120. package/skills/what-needs-me/references/raising-a-decision.md +0 -41
  121. package/skills/what-needs-me/references/reading-the-inbox.md +0 -38
  122. package/skills/what-needs-me/references/settling-an-answer.md +0 -37
  123. package/skills/what-needs-me/scripts/inbox.mjs +0 -56
  124. package/skills/what-needs-me/scripts/lib/cli.mjs +0 -135
  125. package/skills/what-needs-me/scripts/lib/credential.mjs +0 -29
  126. package/skills/what-needs-me/scripts/raise.mjs +0 -53
  127. package/skills/what-needs-me/scripts/settle.mjs +0 -73
  128. package/skills/whats-happening/SKILL.md +0 -43
  129. package/skills/whats-happening/agents/openai.yaml +0 -6
  130. package/skills/whats-happening/agents/portability.yaml +0 -4
  131. package/skills/whats-happening/assets/status-reply.json +0 -77
  132. package/skills/whats-happening/references/reading-the-board.md +0 -43
  133. package/skills/whats-happening/references/reprioritising.md +0 -37
  134. package/skills/whats-happening/references/routing-work.md +0 -36
  135. package/skills/whats-happening/references/status-reply.md +0 -34
  136. package/skills/whats-happening/references/why-is-it-stuck.md +0 -62
  137. package/skills/whats-happening/scripts/explain.mjs +0 -28
  138. package/skills/whats-happening/scripts/lib/cli.mjs +0 -135
  139. package/skills/whats-happening/scripts/lib/credential.mjs +0 -29
  140. package/skills/whats-happening/scripts/snapshot.mjs +0 -149
package/dist/query.js DELETED
@@ -1,248 +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 { flagBool, 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 { fetchAllPages, fetchPage, rowsOf, truncationNotice } from "./pagination.js";
10
- import { engineFor, replicaStatus } from "./replica.js";
11
- import { loadSdk } from "./sdk.js";
12
- export { rowsOf };
13
- const REPLICA_CAPABLE = new Set(["issues", "issue", "pulls", "projects"]);
14
- /** The two keyset routes `--all` can follow. `projects`/`cycles`/`search` are offset-paged or
15
- * unpaginated and send no cursor header. */
16
- const PAGINATED = new Set(["issues", "pulls"]);
17
- export async function cmdQuery(args, ctx, deps = {}) {
18
- const [sub, ...rest] = positionals(args);
19
- if (!sub)
20
- throw new UsageError("query needs a subcommand: issues | issue <id> | pulls | pull <id> | projects | cycles | search <terms> | changes --since <n>");
21
- const cfg = requireConfig(ctx);
22
- const filters = {
23
- team: flagString(args, "team"),
24
- project: flagString(args, "project"),
25
- state: flagString(args, "state"),
26
- limit: flagInt(args, "limit", 50),
27
- all: flagBool(args, "all"),
28
- };
29
- const forced = flagString(args, "source");
30
- if (forced && forced !== "replica" && forced !== "api")
31
- throw new UsageError("--source must be replica or api");
32
- if (filters.all && !PAGINATED.has(sub)) {
33
- throw new UsageError(`--all follows the cloud's keyset pages, which only ${[...PAGINATED].join(" and ")} have`);
34
- }
35
- if (filters.all && forced === "replica") {
36
- throw new UsageError("--all follows the cloud's page cursor; --source replica reads a local mirror that has none. Pick one.");
37
- }
38
- if (filters.all && flagString(args, "limit") !== undefined) {
39
- ctx.stderr("--all reads the whole scope, so --limit is ignored");
40
- }
41
- const status = replicaStatus(ctx, cfg);
42
- let source;
43
- let why;
44
- if (!REPLICA_CAPABLE.has(sub)) {
45
- source = "api";
46
- why = `${sub} is api-only`;
47
- }
48
- else if (forced) {
49
- source = forced;
50
- why = `--source ${forced}`;
51
- }
52
- else if (filters.all) {
53
- source = "api";
54
- why = "--all follows the cloud's pages";
55
- }
56
- else if (status.verdict === "fresh") {
57
- source = "replica";
58
- why = `cursor ${status.cursor}`;
59
- }
60
- else {
61
- source = "api";
62
- why = `replica ${status.verdict}`;
63
- }
64
- ctx.stderr(source === "replica" ? `source: replica (${why})` : `source: api (${why})`);
65
- let result;
66
- if (source === "replica") {
67
- if (status.verdict === "absent" || status.verdict === "not-configured") {
68
- throw new UsageError(`--source replica but the replica is ${status.verdict}`);
69
- }
70
- result = await fromReplica(ctx, cfg, sub, rest, filters, deps);
71
- }
72
- else {
73
- result = await fromApi(ctx, cfg, sub, rest, filters, args);
74
- }
75
- if (result === null) {
76
- ctx.stderr(`${sub} ${rest[0] ?? ""}: not found`);
77
- return 1;
78
- }
79
- printResult(ctx, args, sub, result);
80
- return 0;
81
- }
82
- async function fromReplica(ctx, cfg, sub, rest, f, deps) {
83
- const sdk = await loadSdk();
84
- const dbPath = replicaDbPath(cfg, ctx.home);
85
- const engine = await engineFor(sdk, dbPath, ctx, { ...deps.engineDeps, readonly: true });
86
- const replica = await sdk.CatalystReplica.openReadOnly({ dbPath, engine, log: () => { } });
87
- try {
88
- switch (sub) {
89
- case "issues": {
90
- const rows = replica.issues({ limit: Math.max(f.limit, 200) });
91
- return applyFilters(rows, f).slice(0, f.limit);
92
- }
93
- case "issue":
94
- return replica.issue(needArg(rest, "issue <identifier>"));
95
- case "pulls":
96
- return replica.pulls({ limit: f.limit });
97
- case "projects":
98
- return applyFilters(replica.projects({ limit: Math.max(f.limit, 200) }), { limit: f.limit, state: f.state }).slice(0, f.limit);
99
- default:
100
- throw new UsageError(`${sub} is not readable from the replica`);
101
- }
102
- }
103
- finally {
104
- await replica.close();
105
- }
106
- }
107
- async function fromApi(ctx, cfg, sub, rest, f, args) {
108
- const api = apiClient(cfg, ctx);
109
- const common = { team: f.team, project: f.project, state: f.state, limit: f.limit };
110
- switch (sub) {
111
- case "issues": {
112
- if (f.all) {
113
- const { rows } = await fetchAllPages(api, "/api/v1/issues", { team: f.team, project: f.project, state: f.state });
114
- return applyFilters(rows, f); // no .slice — --all means the whole scope
115
- }
116
- const page = await fetchPage(api, "/api/v1/issues", common);
117
- const rows = applyFilters(page.rows, f).slice(0, f.limit);
118
- const notice = truncationNotice(page);
119
- if (notice)
120
- ctx.stderr(notice);
121
- return rows;
122
- }
123
- case "issue": {
124
- const id = needArg(rest, "issue <identifier>");
125
- const res = await api.getJson(`/api/v1/issues/${encodeURIComponent(id)}`, { accept: [404] });
126
- return res.status === 404 ? null : res.body;
127
- }
128
- case "pulls": {
129
- const ticket = flagString(args, "ticket");
130
- if (f.all) {
131
- const { rows } = await fetchAllPages(api, "/api/v1/pulls", { ticket });
132
- return rows;
133
- }
134
- const page = await fetchPage(api, "/api/v1/pulls", { limit: f.limit, ticket });
135
- const rows = page.rows.slice(0, f.limit);
136
- const notice = truncationNotice(page);
137
- if (notice)
138
- ctx.stderr(notice);
139
- return rows;
140
- }
141
- case "pull": {
142
- const id = needArg(rest, "pull <node id>");
143
- const res = await api.getJson(`/api/v1/pulls/${encodeURIComponent(id)}`, { accept: [404] });
144
- return res.status === 404 ? null : res.body;
145
- }
146
- case "projects": {
147
- const res = await api.getJson("/api/v1/projects", { query: { limit: f.limit } });
148
- return rowsOf(res.body).slice(0, f.limit);
149
- }
150
- case "cycles": {
151
- const res = await api.getJson("/api/v1/cycles", { query: { team: f.team, limit: f.limit } });
152
- return rowsOf(res.body);
153
- }
154
- case "search": {
155
- const q = rest.join(" ").trim();
156
- if (!q)
157
- throw new UsageError("search needs terms: query search <terms>");
158
- const res = await api.getJson("/api/v1/search", { query: { q, limit: f.limit } });
159
- return rowsOf(res.body);
160
- }
161
- case "changes": {
162
- const since = flagString(args, "since");
163
- if (since === undefined)
164
- throw new UsageError("changes needs --since <cursor>, or --since head to start from now");
165
- return await readChanges(api, since, f.limit);
166
- }
167
- default:
168
- throw new UsageError(`unknown query subcommand: ${sub}`);
169
- }
170
- }
171
- /** CTC-137 stamps the live head seq on EVERY `/changes` response — the 200 stream and both 409s. */
172
- const HEAD_SEQ_HEADER = "x-catalyst-head-seq";
173
- /**
174
- * `query changes --since <cursor|head>`.
175
- *
176
- * ⛔ THE CHANGEFEED EVICTS, so a refusal must name a cursor that works. `buildChanges` answers a
177
- * `since` below the oldest retained seq with 409 `cursor_underflow`, and one past the head with 409
178
- * `cursor_ahead_of_head` — both `{resync: true}`. `--since 0`, the form the docs show, is therefore
179
- * a 409 on any tenant whose log has rotated, and 0.2.0 surfaced it as a bare "GET /api/v1/changes
180
- * failed (409)" that left the customer with no way to learn a usable cursor. The head is stamped on
181
- * the refusal itself, so there is always something actionable to say.
182
- */
183
- async function readChanges(api, since, limit) {
184
- const resolved = since === "head" ? String(await headCursor(api)) : since;
185
- // ⛔ NDJSON ON 200, JSON ON A REFUSAL — see `getNdjson`. Reading this with `getJson` turned every
186
- // real success into "returned a non-JSON body" while every refusal parsed, which is why the verb
187
- // looked fine: `--since 0` 409s on a rotated feed, so no 200 was ever reached to fail on.
188
- const res = await api.getNdjson("/api/v1/changes", {
189
- query: { since: resolved, limit },
190
- accept: [409],
191
- });
192
- const head = res.headers.get(HEAD_SEQ_HEADER);
193
- if (res.status !== 409) {
194
- return { since: Number(resolved), head: head === null ? null : Number(head), changes: res.body };
195
- }
196
- const where = head === null ? "" : ` The feed's live cursor is ${head}.`;
197
- 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);
198
- }
199
- /** The live head, read off any `/changes` response's own header — a 409 carries it too, which is
200
- * what makes this work on a tenant whose log has rotated past every cursor the caller could guess. */
201
- async function headCursor(api) {
202
- const probe = await api.getNdjson("/api/v1/changes", { query: { since: "0", limit: 1 }, accept: [409] });
203
- const raw = probe.headers.get(HEAD_SEQ_HEADER);
204
- const head = raw === null ? Number.NaN : Number(raw);
205
- if (!Number.isFinite(head)) {
206
- 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);
207
- }
208
- return head;
209
- }
210
- function needArg(rest, usage) {
211
- const v = rest[0];
212
- if (!v)
213
- throw new UsageError(`query ${usage}`);
214
- return v;
215
- }
216
- export function applyFilters(rows, f) {
217
- return rows.filter((r) => {
218
- if (f.team && !(String(r.identifier ?? "").toUpperCase().startsWith(`${f.team.toUpperCase()}-`) || r.team_id === f.team || r.team_key === f.team))
219
- return false;
220
- if (f.project && r.project_id !== f.project)
221
- return false;
222
- if (f.state && String(r.state ?? "").toLowerCase() !== f.state.toLowerCase())
223
- return false;
224
- return true;
225
- });
226
- }
227
- function printResult(ctx, args, sub, result) {
228
- if (args.json || !Array.isArray(result)) {
229
- ctx.stdout(JSON.stringify(result, null, args.json ? 0 : 2));
230
- return;
231
- }
232
- for (const row of result)
233
- ctx.stdout(summaryLine(sub, row));
234
- }
235
- export function summaryLine(sub, r) {
236
- switch (sub) {
237
- case "issues":
238
- return `${r.identifier ?? r.id} ${r.state ?? "?"} ${r.title ?? ""}${r.assignee_name || r.assignee ? ` (${r.assignee_name ?? r.assignee})` : ""}`;
239
- case "pulls":
240
- return `#${r.number ?? "?"} ${r.state ?? "?"}${r.merged ? " merged" : ""} ${r.title ?? ""} [${r.node_id ?? ""}]`;
241
- case "projects":
242
- return `${r.id} ${r.state ?? "?"} ${r.name ?? ""}`;
243
- case "cycles":
244
- return `cycle ${r.number ?? r.id} ${r.name ?? ""} ${r.starts_at ?? ""} → ${r.ends_at ?? ""}`;
245
- default:
246
- return JSON.stringify(r);
247
- }
248
- }
package/dist/ready.js DELETED
@@ -1,254 +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 { latestPublishedVersion } from "./published.js";
10
- import { replicaStatus, writerIsRunning } from "./replica.js";
11
- import { loadSdk } from "./sdk.js";
12
- import { semverOlder } from "./semver.js";
13
- import { FIRST_STAMPED_VERSION } from "./skill-shape.js";
14
- import { installedBundleVersion } from "./skills.js";
15
- export { semverOlder };
16
- /** `ready`'s own wording for the replica. It never recommends adopting the replica — until the
17
- * mirror's snapshot path is safe for a large tenant, a customer following that advice is the
18
- * failure mode. It DOES name the restart command when the writer gave up, because that is an
19
- * instruction about a thing already running, not a nudge to adopt one (CTC-2499). */
20
- function readyReplicaLine(s) {
21
- const w = s.writer;
22
- if (w?.stopped) {
23
- return (`replica: the writer stopped ${new Date(w.stopped.at).toISOString()}: ${w.stopped.reason} ` +
24
- `(last error: ${w.lastError}) — the replica is optional and every read ` +
25
- `still works through the API; restart it with: ${w.stopped.restartWith}`);
26
- }
27
- if (w && w.consecutiveFailures > 0) {
28
- // Present tense only for a writer that is actually running: the record survives a SIGKILL, and
29
- // "is backing off" about a dead process promises a retry that will never come (CTC-2499).
30
- if (!writerIsRunning(s)) {
31
- return `replica: the writer recorded ${w.consecutiveFailures} failed snapshot pulls and is no longer running (last error: ${w.lastError}) — the replica is optional and every read still works through the API`;
32
- }
33
- return `replica: the writer has failed ${w.consecutiveFailures} snapshot pulls in a row and is backing off (last error: ${w.lastError}) — reads fall back to the API`;
34
- }
35
- switch (s.verdict) {
36
- case "not-configured":
37
- return "replica: not configured — run login first";
38
- case "absent":
39
- return `replica: absent at ${s.dbPath} — optional, and off by default for large tenants while the snapshot path is being made safe; every read works through the API`;
40
- case "fresh":
41
- return `replica: fresh at ${s.dbPath} (cursor ${s.cursor}, heartbeat ${s.heartbeatAgeMs}ms ago${s.lag !== undefined ? `, ${s.lag} behind head ${s.head}` : ""})`;
42
- case "stale":
43
- return `replica: stale at ${s.dbPath} (${s.reasons.join("; ")}${s.cursor !== null ? `; cursor ${s.cursor}` : ""}) — reads fall back to the API`;
44
- }
45
- }
46
- function whoCanAnswer(doc) {
47
- const roles = doc.humans.map((h) => `${h.role} ${h.linearUserId}`);
48
- return roles.length ? roles.join(", ") : "a tenant owner or admin (none resolved on the contract)";
49
- }
50
- export async function readyReport(ctx, deps) {
51
- const checks = [];
52
- const nodeMajor = deps.nodeMajor ?? Number(process.versions.node.split(".")[0]);
53
- checks.push(nodeMajor >= 22
54
- ? { id: "node", ok: true, line: `node: ${nodeMajor} (22 or newer required)` }
55
- : { 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" });
56
- let cfg = null;
57
- try {
58
- cfg = loadConfig(ctx.home);
59
- checks.push(cfg
60
- ? {
61
- id: "config",
62
- ok: true,
63
- // `principal` is "service" for every api key (it means "a key, not a browser session"); it
64
- // never names a person. Since CTC-2076 a personal key's /me carries a `user` block, so name
65
- // the connected person when there is one, and fall back to the account for a host key.
66
- line: cfg.user
67
- ? `config: joined ${cfg.name} as ${cfg.user.label} (${cfg.user.role})`
68
- : `config: joined ${cfg.name} (${cfg.slug}) as ${cfg.principal}`,
69
- }
70
- : { 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)" });
71
- }
72
- catch (err) {
73
- checks.push({ id: "config", ok: false, line: `config: ${err instanceof CliError ? err.message : String(err)}`, fix: "re-run login to rewrite it", who: "you" });
74
- }
75
- const cache = readContractCache(ctx.home);
76
- const range = readManifest().tenantContractRange;
77
- if (!cache) {
78
- checks.push({ id: "contract", ok: false, line: "contract: not cached", fix: "catalyst-skills contract --refresh", who: "you" });
79
- }
80
- else if (contractVersionInRange(cache.contractVersion, range) !== true) {
81
- 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" });
82
- }
83
- else {
84
- checks.push({ id: "contract", ok: true, line: `contract: ${cache.contractVersion} cached ${cache.fetchedAt} (range ${range})` });
85
- }
86
- // The cloud MAY publish the bundle it expects (catalyst-cloud#3746). Read it defensively: warn (never
87
- // refuse) when this installed bundle is older than the minimum, and emit nothing when the field is
88
- // absent (an older cloud) — a stale bundle is a degrading note, so `ready` still returns READY.
89
- const minVersion = cache?.doc.skillsBundle?.minVersion;
90
- if (minVersion) {
91
- const installed = readManifest().version;
92
- if (semverOlder(installed, minVersion)) {
93
- checks.push({
94
- id: "bundle",
95
- ok: false,
96
- note: true,
97
- line: `bundle: ${installed} installed is older than the tenant's minimum ${minVersion} — upgrade: npm install -g @catalyst-cloud/catalyst-skills@latest && catalyst-skills login`,
98
- });
99
- }
100
- }
101
- // CTC-2160 — the two artefacts a customer installs drift apart: the CLI comes from npm, the skill
102
- // files come from `npx skills add` (GitHub) or the plugin, and only the CLI's own version was ever
103
- // knowable here. Both are NOTES: being behind a publish is a degrading fact about an install that
104
- // still works, so `ready` stays READY. This is the one place `ready` touches the network, and it is
105
- // capped, cached and skippable — see src/published.ts.
106
- const skillsDir = cfg?.skillsDir ?? defaultSkillsDirFor(ctx.home);
107
- const offline = deps.offline === true || ctx.env.CATALYST_SKILLS_OFFLINE === "1";
108
- if (!offline) {
109
- const pub = await (deps.fetchLatestRelease ?? (() => latestPublishedVersion(ctx)))();
110
- if (pub.latest === null) {
111
- checks.push({
112
- id: "cliRelease",
113
- ok: false,
114
- note: true,
115
- line: `cliRelease: could not check for a newer release (${pub.reason}) — nothing about this install is known to be wrong; re-run when the network is back`,
116
- });
117
- }
118
- else {
119
- // clause 2 — the CLI. Suppressed when the tenant-minimum note already named this binary (D9).
120
- const installedCli = readManifest().version;
121
- const bundleNoteFired = checks.some((c) => c.id === "bundle");
122
- if (!bundleNoteFired && semverOlder(installedCli, pub.latest)) {
123
- checks.push({
124
- id: "cliRelease",
125
- ok: false,
126
- note: true,
127
- line: `cliRelease: ${installedCli} installed is older than the latest published ${pub.latest} — upgrade: npm install -g @catalyst-cloud/catalyst-skills@latest && catalyst-skills login`,
128
- });
129
- }
130
- // clause 1 — the skill files on disk, read independently of whichever CLI is answering. Both
131
- // facts can hold at once: a machine that pulled at different hours carries some skills stamped
132
- // behind AND some old enough to carry no stamp at all — the field report's own mixed install.
133
- // So they are ADDITIVE, not exclusive: an unstamped file's real version is unknown and may be
134
- // older than any stamp, which is why the stamped half says "oldest STAMPED" and the unstamped
135
- // names are always printed rather than hidden behind whichever branch happened to win.
136
- const found = installedBundleVersion(skillsDir, deps.skillNames);
137
- const stampBehind = found.version !== null && semverOlder(found.version, pub.latest);
138
- const unstampedBehind = found.unstamped.length > 0 && !semverOlder(pub.latest, FIRST_STAMPED_VERSION);
139
- if (stampBehind || unstampedBehind) {
140
- const facts = [];
141
- if (stampBehind)
142
- facts.push(`the oldest stamped skill is ${found.version} (${found.skill})`);
143
- if (unstampedBehind) {
144
- const one = found.unstamped.length === 1;
145
- facts.push(`${found.unstamped.join(", ")} ${one ? "carries" : "carry"} no version and so ${one ? "predates" : "predate"} ${FIRST_STAMPED_VERSION}`);
146
- }
147
- checks.push({
148
- id: "skillsRelease",
149
- ok: false,
150
- note: true,
151
- line: `skillsRelease: in ${skillsDir} ${facts.join(", and ")}, while the published bundle is ${pub.latest} — update: npx skills update -y`,
152
- });
153
- }
154
- }
155
- }
156
- if (cfg) {
157
- checks.push(cfg.cliPath && existsSync(cfg.cliPath)
158
- ? { id: "cliPath", ok: true, line: `cliPath: ${cfg.cliPath}` }
159
- : { 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" });
160
- }
161
- // The skills are installed by the customer's own agent (a plugin, or `npx skills add`), so an
162
- // empty copy directory is the normal case and must not read as NOT READY. A PARTIAL copy is the
163
- // one broken state this check can see: half a set this package put there and never finished.
164
- const present = deps.skillNames.filter((n) => existsSync(join(skillsDir, n, "SKILL.md")));
165
- const missing = deps.skillNames.filter((n) => !present.includes(n));
166
- if (present.length === 0) {
167
- 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` });
168
- }
169
- else if (missing.length === 0) {
170
- checks.push({ id: "skills", ok: true, line: `skills: all ${deps.skillNames.length} present in ${skillsDir}` });
171
- }
172
- else {
173
- 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" });
174
- }
175
- try {
176
- await (deps.loadSdk ?? loadSdk)();
177
- checks.push({ id: "sdk", ok: true, line: "sdk: loads (replica and watch are available)" });
178
- }
179
- catch (err) {
180
- 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" });
181
- }
182
- const replica = replicaStatus(ctx, cfg);
183
- checks.push({ id: "replica", ok: replica.verdict === "fresh", note: true, line: readyReplicaLine(replica) });
184
- if (cache) {
185
- const doc = cache.doc;
186
- const who = whoCanAnswer(doc);
187
- for (const team of doc.teams) {
188
- const label = team.key ?? team.id;
189
- // The team's dispatch gate, straight off the cached contract: an older cloud omits it and this
190
- // emits nothing (the `skillsBundle` precedent above). A shut gate means NOTHING in the team can
191
- // start, so it is a FAIL carrying the cloud's own remedy as the fix — never an informational
192
- // note. It is emitted before the `unchecked` branch below so a team whose readiness was never
193
- // checked still reports its gate, which is exactly the team most likely to be unmapped.
194
- const dg = team.dispatchGate;
195
- if (dg && typeof dg.status === "string") {
196
- const slots = (dg.missingSlots ?? []).join(", ");
197
- checks.push(dg.status === "open"
198
- ? { id: `team:${label}:dispatchGate`, ok: true, line: `team ${label}: dispatch gate open` }
199
- : {
200
- id: `team:${label}:dispatchGate`,
201
- ok: false,
202
- line: `team ${label}: dispatch gate ${dg.status}${slots ? ` (${slots})` : ""}, blocking`,
203
- fix: dg.remedy ?? `open settings for team ${label} and map its stages`,
204
- who,
205
- });
206
- }
207
- if (team.readiness.status === "unchecked") {
208
- checks.push({ id: `team:${label}`, ok: true, note: true, line: `team ${label}: readiness not checked yet` });
209
- continue;
210
- }
211
- const bad = team.readiness.checks.filter((c) => c.state !== "pass");
212
- if (bad.length === 0 && team.readiness.status === "ready") {
213
- checks.push({ id: `team:${label}`, ok: true, line: `team ${label}: ready` });
214
- continue;
215
- }
216
- for (const c of bad) {
217
- const meta = doc.readinessChecks.find((r) => r.id === c.id);
218
- const needsAnswer = meta?.needsAnswer ?? true;
219
- checks.push({
220
- id: `team:${label}:${c.id}`,
221
- ok: !needsAnswer && c.state !== "fail",
222
- note: !needsAnswer,
223
- line: `team ${label}: ${c.id} is ${c.state}${c.reason ? ` (${c.reason}${c.count !== undefined ? ` ×${c.count}` : ""})` : ""}${meta ? `, ${meta.severity}` : ""}`,
224
- fix: `open settings for team ${label} and resolve ${c.id}`,
225
- who: needsAnswer ? who : "nobody yet; it is informational",
226
- });
227
- }
228
- if (bad.length === 0 && team.readiness.status !== "ready") {
229
- 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 });
230
- }
231
- }
232
- }
233
- const ready = checks.every((c) => c.ok || c.note);
234
- return { ready, checks, replica };
235
- }
236
- export async function cmdReady(args, ctx, deps) {
237
- const report = await readyReport(ctx, deps);
238
- if (args.json) {
239
- ctx.stdout(JSON.stringify(report));
240
- }
241
- else {
242
- for (const c of report.checks) {
243
- ctx.stdout(`${c.note ? "note" : c.ok ? "ok " : "FAIL"} ${c.line}`);
244
- if (!c.ok && !c.note) {
245
- if (c.fix)
246
- ctx.stdout(` fix: ${c.fix}`);
247
- if (c.who)
248
- ctx.stdout(` who: ${c.who}`);
249
- }
250
- }
251
- ctx.stdout(report.ready ? "READY" : "NOT READY");
252
- }
253
- return report.ready ? 0 : 1;
254
- }
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
- }