@catalyst-cloud/cli 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 (157) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/bin/catalyst-skills.js +8 -0
  5. package/bin/catalyst.js +5 -0
  6. package/bin/launch.js +154 -0
  7. package/dist/args.js +280 -0
  8. package/dist/ask.js +161 -0
  9. package/dist/browser.js +20 -0
  10. package/dist/cli.js +397 -0
  11. package/dist/config.js +241 -0
  12. package/dist/contract-types.js +4 -0
  13. package/dist/contract.js +184 -0
  14. package/dist/detach.js +10 -0
  15. package/dist/environment.js +207 -0
  16. package/dist/errors.js +27 -0
  17. package/dist/events.js +106 -0
  18. package/dist/execution.js +451 -0
  19. package/dist/oauth.js +300 -0
  20. package/dist/pagination.js +76 -0
  21. package/dist/prompt.js +35 -0
  22. package/dist/published.js +79 -0
  23. package/dist/query.js +248 -0
  24. package/dist/ready.js +380 -0
  25. package/dist/release.js +142 -0
  26. package/dist/replica.js +614 -0
  27. package/dist/runtime-store.js +135 -0
  28. package/dist/runtime-verb.js +66 -0
  29. package/dist/runtime.js +87 -0
  30. package/dist/sdk.js +29 -0
  31. package/dist/secret.js +190 -0
  32. package/dist/semver.js +18 -0
  33. package/dist/skill-shape.js +189 -0
  34. package/dist/skills.js +129 -0
  35. package/dist/transport.js +205 -0
  36. package/dist/ts-deps-loader.js +113 -0
  37. package/dist/watch/consumer.js +141 -0
  38. package/dist/watch/cursor-file.js +62 -0
  39. package/dist/watch.js +175 -0
  40. package/dist/write.js +224 -0
  41. package/package.json +60 -0
  42. package/skills/catalyst-github/SKILL.md +35 -0
  43. package/skills/catalyst-github/agents/openai.yaml +6 -0
  44. package/skills/catalyst-github/agents/portability.yaml +4 -0
  45. package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
  46. package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
  47. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
  48. package/skills/catalyst-github/scripts/lib/cli.mjs +103 -0
  49. package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
  50. package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
  51. package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
  52. package/skills/catalyst-linear/SKILL.md +43 -0
  53. package/skills/catalyst-linear/agents/openai.yaml +6 -0
  54. package/skills/catalyst-linear/agents/portability.yaml +5 -0
  55. package/skills/catalyst-linear/references/reading-a-ticket.md +52 -0
  56. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
  57. package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
  58. package/skills/catalyst-linear/scripts/comment.mjs +59 -0
  59. package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
  60. package/skills/catalyst-linear/scripts/label.mjs +48 -0
  61. package/skills/catalyst-linear/scripts/lib/cli.mjs +164 -0
  62. package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
  63. package/skills/catalyst-linear/scripts/move.mjs +41 -0
  64. package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
  65. package/skills/catalyst-linear/scripts/search.mjs +49 -0
  66. package/skills/catalyst-onboard/SKILL.md +57 -0
  67. package/skills/catalyst-onboard/agents/openai.yaml +6 -0
  68. package/skills/catalyst-onboard/agents/portability.yaml +5 -0
  69. package/skills/catalyst-onboard/references/declaring-a-repository.md +23 -0
  70. package/skills/catalyst-onboard/references/skill-sources.md +35 -0
  71. package/skills/catalyst-onboard/references/the-one-path.md +149 -0
  72. package/skills/catalyst-onboard/references/what-a-phase-needs.md +46 -0
  73. package/skills/catalyst-onboard/references/what-the-browser-owns.md +50 -0
  74. package/skills/catalyst-onboard/references/who-fixes-what.md +44 -0
  75. package/skills/catalyst-onboard/scripts/lib/cli.mjs +117 -0
  76. package/skills/catalyst-onboard/scripts/lib/credential.mjs +29 -0
  77. package/skills/catalyst-onboard/scripts/where-am-i.mjs +345 -0
  78. package/skills/catalyst-setup/SKILL.md +36 -0
  79. package/skills/catalyst-setup/agents/openai.yaml +6 -0
  80. package/skills/catalyst-setup/agents/portability.yaml +4 -0
  81. package/skills/catalyst-setup/references/what-each-check-means.md +88 -0
  82. package/skills/catalyst-setup/scripts/check.mjs +75 -0
  83. package/skills/catalyst-setup/scripts/lib/cli.mjs +103 -0
  84. package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
  85. package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
  86. package/skills/connect-me/SKILL.md +63 -0
  87. package/skills/connect-me/agents/openai.yaml +6 -0
  88. package/skills/connect-me/agents/portability.yaml +5 -0
  89. package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
  90. package/skills/connect-me/scripts/lib/cli.mjs +185 -0
  91. package/skills/connect-me/scripts/lib/credential.mjs +29 -0
  92. package/skills/connect-me/scripts/verify-connection.mjs +68 -0
  93. package/skills/how-catalyst-works/SKILL.md +43 -0
  94. package/skills/how-catalyst-works/agents/openai.yaml +6 -0
  95. package/skills/how-catalyst-works/agents/portability.yaml +4 -0
  96. package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
  97. package/skills/how-catalyst-works/references/stages-and-mapping.md +56 -0
  98. package/skills/how-catalyst-works/references/the-ladder.md +41 -0
  99. package/skills/how-catalyst-works/references/what-catalyst-is.md +30 -0
  100. package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
  101. package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
  102. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
  103. package/skills/how-catalyst-works/scripts/lib/cli.mjs +164 -0
  104. package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
  105. package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
  106. package/skills/how-catalyst-works/scripts/whats-running.mjs +65 -0
  107. package/skills/run-this-project/SKILL.md +45 -0
  108. package/skills/run-this-project/agents/openai.yaml +6 -0
  109. package/skills/run-this-project/agents/portability.yaml +5 -0
  110. package/skills/run-this-project/assets/stall-policy.json +15 -0
  111. package/skills/run-this-project/references/making-work-ready.md +60 -0
  112. package/skills/run-this-project/references/reacting-to-events.md +76 -0
  113. package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
  114. package/skills/run-this-project/scripts/lib/cli.mjs +185 -0
  115. package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
  116. package/skills/run-this-project/scripts/make-ready.mjs +64 -0
  117. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  118. package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
  119. package/skills/unstick/SKILL.md +41 -0
  120. package/skills/unstick/agents/openai.yaml +6 -0
  121. package/skills/unstick/agents/portability.yaml +5 -0
  122. package/skills/unstick/references/playbook.md +51 -0
  123. package/skills/unstick/scripts/lib/cli.mjs +135 -0
  124. package/skills/unstick/scripts/lib/credential.mjs +29 -0
  125. package/skills/unstick/scripts/unstick.mjs +57 -0
  126. package/skills/what-needs-me/SKILL.md +41 -0
  127. package/skills/what-needs-me/agents/openai.yaml +6 -0
  128. package/skills/what-needs-me/agents/portability.yaml +5 -0
  129. package/skills/what-needs-me/references/raising-a-decision.md +41 -0
  130. package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
  131. package/skills/what-needs-me/references/settling-an-answer.md +37 -0
  132. package/skills/what-needs-me/scripts/inbox.mjs +56 -0
  133. package/skills/what-needs-me/scripts/lib/cli.mjs +135 -0
  134. package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
  135. package/skills/what-needs-me/scripts/raise.mjs +53 -0
  136. package/skills/what-needs-me/scripts/settle.mjs +73 -0
  137. package/skills/whats-happening/SKILL.md +43 -0
  138. package/skills/whats-happening/agents/openai.yaml +6 -0
  139. package/skills/whats-happening/agents/portability.yaml +4 -0
  140. package/skills/whats-happening/assets/status-reply.json +77 -0
  141. package/skills/whats-happening/references/reading-the-board.md +43 -0
  142. package/skills/whats-happening/references/reprioritising.md +37 -0
  143. package/skills/whats-happening/references/routing-work.md +36 -0
  144. package/skills/whats-happening/references/status-reply.md +34 -0
  145. package/skills/whats-happening/references/why-is-it-stuck.md +62 -0
  146. package/skills/whats-happening/scripts/explain.mjs +28 -0
  147. package/skills/whats-happening/scripts/lib/cli.mjs +135 -0
  148. package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
  149. package/skills/whats-happening/scripts/snapshot.mjs +149 -0
  150. package/vendor/README.md +9 -0
  151. package/vendor/paths/index.d.ts +85 -0
  152. package/vendor/paths/index.js +148 -0
  153. package/vendor/paths/legacy-installer.d.ts +36 -0
  154. package/vendor/paths/legacy-installer.js +154 -0
  155. package/vendor/paths/node.d.ts +18 -0
  156. package/vendor/paths/node.js +102 -0
  157. package/vendor/paths/provenance.json +17 -0
@@ -0,0 +1,57 @@
1
+ # When a phase fails: four layers, in order
2
+
3
+ This reference restates the mechanism, which does not vary per tenant. The numbers quoted are the fleet defaults; the live values are served on the contract under `thresholds` and `node scripts/show-my-map.mjs` prints them. Quote the printed values, not these.
4
+
5
+ A failed phase writes no board state on its own: the ticket stays at its current stage, retryable. What happens next is decided by four stacked layers.
6
+
7
+ ## Layer 1: retry in place
8
+
9
+ Some failures are not the ticket's fault, so the SAME phase is simply re-offered and no remediation round is minted:
10
+
11
+ - the phase failed before any branch existed (research, plan or implement on a ticket whose own record shows no branch);
12
+ - an infrastructure failure at any phase: a vendor 5xx, a dropped stream, a refused setup, an environment gap, a phase timeout, an unwritable workspace.
13
+
14
+ Nothing is written for the retry; the ordinary dispatch path re-offers the phase. A remediation round on a branchless ticket could only fail to find the branch while spending a counted attempt, which is why this layer exists.
15
+
16
+ ## Layer 2: backoff
17
+
18
+ Every retry-in-place, and a routing refusal at kickoff, waits out a rung of a backoff ladder before the phase is re-offered: 2 minutes, then 5, then 15, indexed by the consecutive-failure count and clamped at the last rung. While waiting, the eligibility explainer shows `retry_backoff` (or `routing_unavailable` with a kickoff detail). `node scripts/explain-ticket.mjs <ticket>` prints it. The live ladder is `thresholds.retryBackoffMs`.
19
+
20
+ ## Layer 3: move to Remediate
21
+
22
+ Any failure that is not retry-in-place moves the card to the team's remediate stage and queues a remediation round. The round is an interrupt phase (`remediate`) that repairs what failed; when a round succeeds, the card restores to its exact pre-failure stage. A team with no remediate stage mapped gets the hold label from `teams[].labels.hold` instead of a state move.
23
+
24
+ Rounds are capped: after `thresholds.remediateRoundCap` rounds (default 3) in one failure episode, no further round is queued. A remediation round that itself fails never queues another round. A round that changes nothing puts the ticket on a no-change hold, released by a human comment on the ticket or a new push, never by a clock. A validate failure with the same fingerprint spends only one repair round per episode.
25
+
26
+ Two structural cases the round machinery refuses: a `remediate` round is never offered on a ticket with no recorded branch (`no_branch_to_remediate`), and one dispatched anyway is cancelled at the clone.
27
+
28
+ ## Layer 4: park
29
+
30
+ After `thresholds.parkAfterConsecutiveFailures` consecutive failures (default 3) the phase is parked. A parked phase shows as `cooling_down` in the explainer; the ticket's `remediate` phase being parked shows as `remediate_parked`.
31
+
32
+ Which parks release themselves:
33
+
34
+ | Park | Releases by |
35
+ | -- | -- |
36
+ | repeated failure | a release from the person's own login once its cause is fixed (`catalyst-skills release`), not a clock |
37
+ | remediate round cap reached | the same release, which buys one more round, not a clock |
38
+ | missing branch | its own budgeted release loop |
39
+ | rebase conflict | its own budgeted release loop |
40
+
41
+ A person's own key releases a park, and every other hold that a retry can fix, in one step: `catalyst-skills release <ticket> --because <what changed>`. The cloud releases every governor holding the ticket or releases nothing and names what a person must do instead (a person's own pull request, a review that will not converge, the lifetime repair budget). It refuses a cause it cannot see change unless the caller says what changed. The `unstick` skill runs that loop. When a ticket is parked, name the failure class the explainer shows and whether its cause is fixed.
42
+
43
+ ## Two holds that are not failures
44
+
45
+ - **Validate budget hold**: a validate failure that has spent its repair round holds the card; a human comment on the ticket clears it.
46
+ - **Runner image breaker**: fleet-wide, not per ticket. When three distinct tickets fail the same startup class on the live runner image, dispatch pauses for the affected tickets, one anomaly alert is raised per tenant, and dispatch resumes automatically when the image pin moves. The explainer shows `runner_image_breaker`. Never escalate this one ticket at a time.
47
+
48
+ ## What a human sees on the ticket
49
+
50
+ Each attempt posts a phase-outcome comment (complete or FAILED, with phase, attempt, artifact, a summary and any park or hold block) and each remediation round posts a remediate-attempt comment naming the failure class it is repairing. `catalyst-linear` describes the shapes. The attempt ledger, the round count against the cap and the park history are readable: `catalyst-skills explain --history <ticket>` prints them from the cloud's own relay ledger.
51
+
52
+ ## Rule of thumb for answering "why is it stuck?"
53
+
54
+ 1. Run `node scripts/explain-ticket.mjs <ticket>`; the reason names the layer.
55
+ 2. `retry_backoff` or `routing_unavailable`: wait; it retries itself. Say when.
56
+ 3. `cooling_down`, `remediate_parked`, `no_change_hold`, `validate_class_spent`: name what releases it (a release once the cause is fixed, a comment, a push) and who can do that.
57
+ 4. A system-level cause (provider down, runner image breaker, repo paused) is ONE alert, never a per-ticket escalation.
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // explain-ticket.mjs — why one ticket is, or is not, about to run: the cloud's own eligibility row
3
+ // for it, rendered as one paragraph, plus the raw row so the failure block is not lost.
4
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/explain-ticket.mjs <ticket> [--json]
7
+
8
+ Asks Catalyst Cloud for the ticket's eligibility row (position, status, the exclusion reason in plain
9
+ English, the last failure, advisories) and prints it as one paragraph. Wraps: catalyst-skills explain.
10
+
11
+ <ticket> the Linear identifier, e.g. KEY-123
12
+ --json print the CLI's JSON document instead of the paragraph
13
+
14
+ Exit 0 explained (even when the ticket is excluded), 1 when the ticket is unknown to the mirror or
15
+ a usage error, 2 when this machine is not connected to a tenant or the cloud refused the read
16
+ (the line says which).`;
17
+
18
+ const argv = process.argv.slice(2);
19
+ if (wantsHelp(argv)) {
20
+ console.log(HELP);
21
+ process.exit(0);
22
+ }
23
+ const { flags, positionals } = parseFlags(argv, { bool: ["json"] });
24
+ const ticket = positionals[0];
25
+ if (!ticket) usage("explain-ticket needs a ticket identifier (see --help)");
26
+
27
+ const r = runCli(["explain", ticket, "--json"]);
28
+ exitOnFailure(r);
29
+ relayStderr(r);
30
+ const doc = parseJson(r.stdout);
31
+ if (!doc || typeof doc.explanation !== "string") {
32
+ console.log(r.stdout.trimEnd());
33
+ process.exit(1);
34
+ }
35
+ if (flags.json) {
36
+ console.log(JSON.stringify(doc));
37
+ } else {
38
+ console.log(doc.explanation);
39
+ if (doc.row) console.log(`row: ${JSON.stringify(doc.row)}`);
40
+ }
41
+ process.exit(doc.row ? 0 : 1);
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ // lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: it spawns the catalyst-skills CLI
3
+ // this machine connected with (the path recorded in customer.json, else npx) and hands back its
4
+ // output. Scripts import it; a person runs it with --help to see what it does. No dependencies.
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
10
+
11
+ export const PACKAGE = "@catalyst-cloud/catalyst-skills";
12
+ export const CONNECT_HINT = CONNECT_COMMAND;
13
+
14
+ /** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME (used by tests) over HOME. */
15
+ export function configPath() {
16
+ const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
17
+ return join(home, ".config", "catalyst-cloud", "customer.json");
18
+ }
19
+
20
+ /** The config, or a one-line reason it could not be read. Never throws. */
21
+ export function loadConfig() {
22
+ const path = configPath();
23
+ if (!existsSync(path)) return { ok: false, reason: `no config at ${path}` };
24
+ try {
25
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
26
+ if (!hasCredential(cfg) || typeof cfg.account !== "string") {
27
+ return { ok: false, reason: `${path} is missing a key or login, or the account` };
28
+ }
29
+ return { ok: true, cfg, path };
30
+ } catch (err) {
31
+ return { ok: false, reason: `${path} is unreadable: ${err instanceof Error ? err.message : String(err)}` };
32
+ }
33
+ }
34
+
35
+ /** Exit 2 with the one line every script prints when this machine is not connected to a tenant. */
36
+ export function notConfigured(reason) {
37
+ console.error(`not connected to a Catalyst Cloud tenant (${reason}) — run: ${CONNECT_HINT}`);
38
+ process.exit(2);
39
+ }
40
+
41
+ /**
42
+ * Run one catalyst-skills verb. Returns { code, stdout, stderr }. The CLI is `node <cliPath>` when
43
+ * the config recorded one that still exists, else `npx @catalyst-cloud/catalyst-skills`.
44
+ * Exits 2 (not configured) before spawning anything when the config is absent or unreadable.
45
+ */
46
+ export function runCli(args, { stdin } = {}) {
47
+ const loaded = loadConfig();
48
+ if (!loaded.ok) notConfigured(loaded.reason);
49
+ const { cfg } = loaded;
50
+ let cmd;
51
+ let argv;
52
+ let shell = false;
53
+ if (typeof cfg.cliPath === "string" && existsSync(cfg.cliPath)) {
54
+ cmd = process.execPath;
55
+ argv = [cfg.cliPath, ...args];
56
+ } else {
57
+ cmd = process.platform === "win32" ? "npx.cmd" : "npx";
58
+ argv = [PACKAGE, ...args];
59
+ shell = process.platform === "win32";
60
+ }
61
+ const r = spawnSync(cmd, argv, {
62
+ encoding: "utf8",
63
+ input: stdin,
64
+ env: process.env,
65
+ shell,
66
+ maxBuffer: 64 * 1024 * 1024,
67
+ });
68
+ if (r.error) {
69
+ console.error(`could not run ${cmd}: ${r.error.message} — re-run ${CONNECT_HINT} to record the CLI path`);
70
+ process.exit(2);
71
+ }
72
+ return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
73
+ }
74
+
75
+ /** Print the CLI's stderr (the source line, the contract line, any refusal) on our stderr. */
76
+ export function relayStderr(r) {
77
+ const text = r.stderr.trimEnd();
78
+ if (text) console.error(text);
79
+ }
80
+
81
+ /** A non-zero CLI result ends the script with the same code, its stdout shown so nothing is lost. */
82
+ export function exitOnFailure(r) {
83
+ if (r.code === 0) return;
84
+ relayStderr(r);
85
+ const out = r.stdout.trimEnd();
86
+ if (out) console.log(out);
87
+ process.exit(r.code);
88
+ }
89
+
90
+ export function parseJson(text) {
91
+ try {
92
+ return JSON.parse(text.trim());
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * A small flag parser: `spec.value` names flags that take a value, `spec.bool` boolean flags,
100
+ * `spec.repeat` value flags that may repeat (collected into arrays). Unknown flags are a usage error.
101
+ */
102
+ export function parseFlags(argv, spec = {}) {
103
+ const value = new Set(spec.value ?? []);
104
+ const bool = new Set(spec.bool ?? []);
105
+ const repeat = new Set(spec.repeat ?? []);
106
+ const flags = {};
107
+ const positionals = [];
108
+ for (let i = 0; i < argv.length; i++) {
109
+ const a = argv[i];
110
+ if (a === "--") {
111
+ positionals.push(...argv.slice(i + 1));
112
+ break;
113
+ }
114
+ if (!a.startsWith("--")) {
115
+ positionals.push(a);
116
+ continue;
117
+ }
118
+ let name = a.slice(2);
119
+ let inline;
120
+ const eq = name.indexOf("=");
121
+ if (eq !== -1) {
122
+ inline = name.slice(eq + 1);
123
+ name = name.slice(0, eq);
124
+ }
125
+ if (bool.has(name)) {
126
+ flags[name] = true;
127
+ } else if (value.has(name) || repeat.has(name)) {
128
+ const v = inline ?? argv[++i];
129
+ if (v === undefined || v === "") usage(`--${name} needs a value`);
130
+ if (repeat.has(name)) (flags[name] ??= []).push(v);
131
+ else flags[name] = v;
132
+ } else {
133
+ usage(`unknown option --${name}`);
134
+ }
135
+ }
136
+ return { flags, positionals };
137
+ }
138
+
139
+ export function usage(message) {
140
+ console.error(message);
141
+ process.exit(1);
142
+ }
143
+
144
+ export function wantsHelp(argv) {
145
+ return argv.includes("--help") || argv.includes("-h");
146
+ }
147
+
148
+ const runDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
149
+ if (runDirectly) {
150
+ if (wantsHelp(process.argv.slice(2)) || process.argv.length <= 2) {
151
+ console.log(
152
+ [
153
+ "lib/cli.mjs — shared helper for this skill's scripts (not a command of its own)",
154
+ "",
155
+ "Reads ~/.config/catalyst-cloud/customer.json and runs the catalyst-skills CLI recorded there",
156
+ `(or npx ${PACKAGE} when no path is recorded). Exit 2 when the machine is not connected.`,
157
+ "",
158
+ `Connect first with: ${CONNECT_HINT}`,
159
+ `Config in use: ${configPath()}`,
160
+ ].join("\n"),
161
+ );
162
+ process.exit(0);
163
+ }
164
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
3
+ // byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
4
+ // (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
5
+ // each carries its own copy. This file is a library — run a sibling script with --help for usage.
6
+ //
7
+ // customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
8
+ // session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
9
+ // catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
10
+ // itself. A new credential kind lands here, once.
11
+
12
+ /** The command that connects this machine, as every not-connected line names it. */
13
+ export const CONNECT_COMMAND =
14
+ "npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
15
+
16
+ /** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
17
+ export function hasCredential(cfg) {
18
+ if (cfg === null || typeof cfg !== "object") return false;
19
+ const key = cfg["key"];
20
+ if (typeof key === "string" && key !== "") return true;
21
+ const login = cfg["auth"];
22
+ return (
23
+ login !== null &&
24
+ typeof login === "object" &&
25
+ login["kind"] === "oauth" &&
26
+ typeof login["refreshToken"] === "string" &&
27
+ login["refreshToken"] !== ""
28
+ );
29
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // show-my-map.mjs — this tenant's stage map, ladder and thresholds, straight from the contract:
3
+ // per team, every slot with the Linear stage it maps to, its type, whether that state still exists
4
+ // and how the mapping was chosen; then the ladder and the live failure thresholds.
5
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, wantsHelp } from "./lib/cli.mjs";
6
+
7
+ const HELP = `Usage: node scripts/show-my-map.mjs [--team <key>] [--json]
8
+
9
+ Prints the tenant contract's stage map, ladder and thresholds. Nothing here is guessed: every value is
10
+ read from the contract Catalyst Cloud serves for your tenant. Wraps: catalyst-skills contract --path.
11
+
12
+ --team <key> only this team
13
+ --json one JSON document: { slots, teams, ladder, thresholds }
14
+ --help this text
15
+
16
+ A slot marked * is load-bearing: dispatch, intake, pr, done and canceled must be mapped for the
17
+ ladder to move at all; the others are informational. "(unmapped)" means the team has no stage for
18
+ that slot; "(state gone)" means the mapped Linear state no longer exists and needs re-mapping in
19
+ settings.
20
+
21
+ Exit 0, 1 on a usage error or an unknown team, 2 when this machine is not connected to a tenant or
22
+ the contract could not be read (the line says which).`;
23
+
24
+ const LOAD_BEARING = new Set(["dispatch", "intake", "pr", "done", "canceled"]);
25
+
26
+ const argv = process.argv.slice(2);
27
+ if (wantsHelp(argv)) {
28
+ console.log(HELP);
29
+ process.exit(0);
30
+ }
31
+ const { flags } = parseFlags(argv, { bool: ["json"], value: ["team"] });
32
+
33
+ function contractPath(path) {
34
+ const r = runCli(["contract", "--path", path, "--json"]);
35
+ exitOnFailure(r);
36
+ const v = parseJson(r.stdout);
37
+ if (v === null || v === undefined) {
38
+ relayStderr(r);
39
+ console.error(`contract --path ${path} returned nothing readable`);
40
+ process.exit(1);
41
+ }
42
+ return { value: v, stderr: r.stderr };
43
+ }
44
+
45
+ const slotsRead = contractPath("slots");
46
+ if (slotsRead.stderr.trim()) console.error(slotsRead.stderr.trimEnd());
47
+ const slots = slotsRead.value;
48
+ let teams = contractPath("teams").value;
49
+ const ladder = contractPath("ladder").value;
50
+ const thresholds = contractPath("thresholds").value;
51
+
52
+ if (flags.team) {
53
+ const want = flags.team.toUpperCase();
54
+ teams = teams.filter((t) => String(t.key ?? "").toUpperCase() === want);
55
+ if (teams.length === 0) {
56
+ console.error(`no team with key ${flags.team} on this tenant's contract`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ if (flags.json) {
62
+ console.log(JSON.stringify({ slots, teams, ladder, thresholds }));
63
+ process.exit(0);
64
+ }
65
+
66
+ const pad = (s, n) => String(s).padEnd(n);
67
+ for (const team of teams) {
68
+ console.log(`== team ${team.key ?? "(no key)"} — ${team.name ?? team.id} — workflow ${team.workflowMode}, git automation ${team.gitAutomation}, readiness ${team.readiness?.status ?? "unknown"}`);
69
+ console.log(` ${pad("slot", 12)}${pad("stage", 22)}${pad("type", 12)}${pad("exists", 8)}source`);
70
+ for (const slot of slots) {
71
+ const s = team.stages?.[slot];
72
+ const mark = LOAD_BEARING.has(slot) ? "*" : " ";
73
+ if (!s) {
74
+ console.log(` ${mark}${pad(slot, 12)}(unmapped)`);
75
+ continue;
76
+ }
77
+ const name = s.stateStillExists ? (s.name ?? "(unnamed)") : "(state gone)";
78
+ console.log(` ${mark}${pad(slot, 12)}${pad(name, 22)}${pad(s.type ?? "-", 12)}${pad(s.stateStillExists ? "yes" : "NO", 8)}${s.source}`);
79
+ }
80
+ const labels = team.labels ?? {};
81
+ const describe = (list) => (list ?? []).map((l) => `${l.name}${l.preferredId ? "" : " (absent)"}`).join(", ") || "none";
82
+ console.log(` labels: ask ${describe(labels.ask)}; hold ${describe(labels.hold)}; release ${describe(labels.release)}`);
83
+ }
84
+
85
+ console.log("== ladder");
86
+ console.log(` phases: ${(ladder.phases ?? []).join(" → ")}`);
87
+ console.log(` keying: ${ladder.keying} (${ladder.keyingScope}); intake ${ladder.intakeEnabled ? "enabled" : "off"}`);
88
+
89
+ console.log("== thresholds (live, fleet-wide)");
90
+ const minutes = (ms) => `${Math.round(ms / 60000)} min`;
91
+ console.log(` park after ${thresholds.parkAfterConsecutiveFailures} consecutive failures`);
92
+ console.log(` remediate round cap ${thresholds.remediateRoundCap} (escalated budget ${thresholds.remediateEscalatedRoundBudget}, rewind budget ${thresholds.remediateRewindBudget})`);
93
+ console.log(` retry backoff ${(thresholds.retryBackoffMs ?? []).map(minutes).join(", ")}`);
94
+ console.log(` write budget ${thresholds.hostDailyWriteBudget} per host per day`);
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // whats-running.mjs — what Catalyst is executing right now (fleet activity, the agent roster, lease
3
+ // attributions), optionally the dispatch queue and the coding-account line.
4
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/whats-running.mjs [--queue] [--accounts] [--team <key>] [--json]
7
+
8
+ Prints what is executing on your tenant. Wraps: catalyst-skills running, queue, accounts.
9
+
10
+ --queue also print the dispatch queue (what runs next, in order)
11
+ --team <key> with --queue: one team's queue
12
+ --accounts also print the coding-account line (provider, state, windows — never a credential)
13
+ --json one JSON document: { running, queue?, accounts? }
14
+ --help this text
15
+
16
+ Exit 0, 1 on a usage error, 2 when this machine is not connected to a tenant or the cloud refused
17
+ a read (the line says which).`;
18
+
19
+ const argv = process.argv.slice(2);
20
+ if (wantsHelp(argv)) {
21
+ console.log(HELP);
22
+ process.exit(0);
23
+ }
24
+ const { flags } = parseFlags(argv, { bool: ["queue", "accounts", "json"], value: ["team"] });
25
+
26
+ const out = {};
27
+ const running = runCli(["running", "--json"]);
28
+ exitOnFailure(running);
29
+ relayStderr(running);
30
+ out.running = parseJson(running.stdout) ?? running.stdout.trimEnd();
31
+
32
+ if (flags.queue) {
33
+ const args = ["queue", "--json"];
34
+ if (flags.team) args.push("--team", flags.team);
35
+ const queue = runCli(args);
36
+ exitOnFailure(queue);
37
+ relayStderr(queue);
38
+ out.queue = parseJson(queue.stdout) ?? queue.stdout.trimEnd();
39
+ }
40
+
41
+ if (flags.accounts) {
42
+ const accounts = runCli(["accounts"]);
43
+ exitOnFailure(accounts);
44
+ relayStderr(accounts);
45
+ out.accounts = accounts.stdout.trimEnd();
46
+ }
47
+
48
+ if (flags.json) {
49
+ console.log(JSON.stringify(out));
50
+ process.exit(0);
51
+ }
52
+
53
+ const section = (title, value) => {
54
+ console.log(`== ${title}`);
55
+ console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
56
+ };
57
+ if (out.running && typeof out.running === "object") {
58
+ section("fleet activity", out.running.fleetActivity ?? out.running);
59
+ if (out.running.agentRoster !== undefined) section("agent roster", out.running.agentRoster);
60
+ if (out.running.leaseAttributions !== undefined) section("lease attributions", out.running.leaseAttributions);
61
+ } else {
62
+ section("running", out.running);
63
+ }
64
+ if (out.queue !== undefined) section("dispatch queue", out.queue);
65
+ if (out.accounts !== undefined) section("coding accounts", out.accounts);
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: run-this-project
3
+ description: >-
4
+ Own one Catalyst Cloud project end to end until it closes. Use when the person says "run this project for me", "own this until it ships", "keep this moving", or hands you a project id or a set of tickets to drive. Subscribes to the tenant stream for the scope through the catalyst-skills CLI, reacts to each change in the same turn, makes tickets ready and moves them to dispatch, parks what should stop, chases stalls, escalates inward, and keeps one status summary current. Writes to Linear as the app actor; never polls.
5
+ disable-model-invocation: true
6
+ allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
+ ---
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.8.0 — written in this repository for customer tenants -->
9
+
10
+ # Run this project
11
+
12
+ You are the steward: the single-threaded owner of ONE project or ticket set, from now until it closes. The person says "run this project for me" and hands you a scope. Catalyst does the phases; you make work ready and visible, react to what happens, unblock, and keep one status summary the person can read. You hold no authority over other stewards and you are not the desk; `whats-happening` is the desk.
13
+
14
+ ## Run first
15
+
16
+ Scripts are run, never read. Each prints its own `--help`.
17
+
18
+ 1. `node scripts/scope-status.mjs --project <id>` (or `--team <key>`): tickets by stage in the contract's order, what is running and queued in scope, and stalls against the local policy. Run it once to take the scope, and again after any resync.
19
+ 2. `node scripts/watch-scope.mjs --project <id>`: the subscription. In Claude Code, arm a monitor on it and react to each printed line in the same turn. In a harness with no monitor, add `--exec <command>` so a reaction still runs per frame.
20
+ 3. `node scripts/make-ready.mjs <ticket>` to dispatch; `--park` to stop; `--note <why>` to record it.
21
+ 4. `catalyst-skills explain <ticket>` whenever a ticket is not moving: one paragraph naming the reason and what releases it.
22
+
23
+ Every script exits 2 when this machine is not connected (run `catalyst-skills login`), 1 when its own check fails.
24
+
25
+ ## Load on demand
26
+
27
+ | when | read |
28
+ | -- | -- |
29
+ | arming the watch, deciding what a frame means, a reaction failed, a resync line printed | `references/reacting-to-events.md` |
30
+ | dispatching or parking a ticket, judging whether a phase actually ran, writing a ticket for Catalyst | `references/making-work-ready.md` |
31
+ | something is not moving, tuning the stall policy, deciding whether a human needs to hear about it | `references/stalls-and-escalation.md` |
32
+ | the exclusion vocabulary in depth, the ladder, this team's stage map | the `how-catalyst-works` skill |
33
+ | raising or settling a decision | the `what-needs-me` skill |
34
+ | a PR's checks, review and merge legs | the `catalyst-github` skill |
35
+
36
+ ## Rules
37
+
38
+ - **React, never poll.** A loop that re-reads the API or the replica is a defect. The stream and the cursor file are the mechanism; a reaction that throws leaves the cursor so the frame is offered again.
39
+ - **Dispatch is a card move.** The cloud takes work from the dispatch column and writes every later stage itself. Your two moves are into dispatch and into the backlog; never hand-move a card into a ladder stage.
40
+ - **Evidence a phase ran is the outcome comment, the attached document and the agent session,** not the clock and not the card's column.
41
+ - **Tenant facts come from the contract, live.** Never restate a stage name, label id, team id, threshold or template in prose; run `catalyst-skills contract --path <a.b.c>` when you need one.
42
+ - **Escalate inward.** Instrument, then you, then the desk, then the human as an ask filed through `what-needs-me` with what it blocks. Decide the technical calls yourself; take the sane default and record it; a fleet or provider condition is one note, never one ask per ticket.
43
+ - **Reply where the message arrived,** in-thread, tagged, as the app actor. Never post as the human, never answer someone else's ask. Bookkeeping records take the contract's marker (`catalyst-skills write comment --bookkeeping`).
44
+ - **One status summary,** kept current after every reaction: in flight, blocked and on whom, closed, next, each line with a ticket id. Where a human decision is pending, the line carries the ask's id.
45
+ - **Say what a key cannot see.** PR labels and reactions are not mirrored; the CLI says so by name and points at settings. A park or hold is released from the person's own login once its cause is fixed, through the `unstick` skill (`catalyst-skills release`). Execution history (`explain --history`) and coding-account status (`accounts`) ARE readable — read them rather than reconstructing them from comments.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "Run this project"
3
+ short_description: "Own one Catalyst project end to end: react to its events, make tickets ready, chase stalls, escalate inward"
4
+ default_prompt: "Use $run-this-project to run project <id> for me until it closes."
5
+ policy:
6
+ allow_implicit_invocation: false
@@ -0,0 +1,5 @@
1
+ identity: { pack: catalyst-cloud-skills, skill: run-this-project }
2
+ effects: [external-write]
3
+ mutating: true
4
+ invocation: explicit
5
+ exposure: [catalog]
@@ -0,0 +1,15 @@
1
+ {
2
+ "$comment": "Stall thresholds in minutes, per workflow slot, for scope-status.mjs. This is local policy a human sets once, not a cloud value: copy this file to ~/.config/catalyst-cloud/stall-policy.json and edit it there. A ticket counts as stalled when it has sat in a slot longer than the threshold with no update and nothing running or leased on it. 'dispatch' is how long a card may wait in the dispatch column before you ask why nothing picked it up; 'pr' is how long a card may wait for merge evidence; 'default' covers any slot not named.",
3
+ "minutes": {
4
+ "dispatch": 60,
5
+ "intake": 60,
6
+ "research": 90,
7
+ "plan": 90,
8
+ "implement": 120,
9
+ "remediate": 120,
10
+ "verify": 90,
11
+ "review": 90,
12
+ "pr": 240,
13
+ "default": 120
14
+ }
15
+ }
@@ -0,0 +1,60 @@
1
+ # Making work ready
2
+
3
+ This reference restates invariants of how Catalyst takes work. The tenant's own values (which Linear state is the dispatch column, which state is the backlog, the ask and release label ids, the round cap and the park threshold) are read live from `catalyst-skills contract`; the scripts here never name a stage.
4
+
5
+ ## The steward's two moves
6
+
7
+ Catalyst does not take work by being asked. It takes work by finding a card in the team's dispatch column and offering that card's next phase to a runner. So the steward's dispatch verb is a card move, and its stop verb is a card move:
8
+
9
+ ```sh
10
+ node scripts/make-ready.mjs ENG-41 # move into the dispatch column; the cloud offers the next phase
11
+ node scripts/make-ready.mjs ENG-41 --park # move into the team's backlog-type state; nothing further is offered
12
+ node scripts/make-ready.mjs ENG-41 --park --note "waiting on the vendor's API change"
13
+ ```
14
+
15
+ Dispatch resolves the dispatch slot on the ticket's team from the contract. Parking resolves the team's first backlog-type state from its live workflow states, because the backlog is deliberately not one of the eleven slots. Those are the only two state moves you make. Every other stage move on a ticket in the ladder is the cloud's, written when a phase completes; moving a card forward by hand does not run a phase, it only confuses the advance table, and moving it backwards by hand does not undo one.
16
+
17
+ ## What a dispatchable ticket carries
18
+
19
+ Before the move, the ticket needs, in the record itself:
20
+
21
+ - A title that states the outcome. A phase agent reads the ticket, not your chat.
22
+ - A description that says what done looks like: the behaviour, the constraints, the files or surfaces if you know them, acceptance criteria a validate phase can check.
23
+ - The right team. The team key is the ticket prefix, and the team's stage map and labels are what the cloud will use.
24
+ - A priority. Queue order inside a team is priority first, then creation time, then identifier, so an unset priority sorts last.
25
+ - No live blocking relation. A ticket with an open blocker is excluded as `blocked` until the blocker closes.
26
+ - No ask on it. A ticket that carries the ask label, or whose own text reads as a decision request (an ask-shaped title, lettered options, a "default if silent" line), is excluded as a question rather than work. If a real ticket trips the shape detector, a human applies the release label named in the contract's vocabulary; you can also rewrite the text so it reads as work.
27
+ - Declared scope when the team enforces it: a ticket whose declared files overlap a ticket already in flight is held as `scope_overlap` for the implement phase.
28
+
29
+ A ticket does not need a branch, a PR, or any artifact to be dispatched: the ladder creates those. A ticket that has never entered the ladder starts at intake when the tenant enables it, otherwise at research.
30
+
31
+ ## After the move
32
+
33
+ `make-ready.mjs` asks the eligibility explainer as soon as the move lands and prints the verdict. Read it as follows:
34
+
35
+ - `offered` or `eligible` with a queue position: done; a runner will pick it up in the next dispatch pass, ordered by priority, then age, then identifier, with tickets already mid-ladder ahead of fresh ones.
36
+ - An ordering that is stale or never published: the cloud re-derives the team's queue within a pass of the move; ask again in a minute with `catalyst-skills explain <ticket>`.
37
+ - Any other exclusion reason: the paragraph names it and what releases it. The reasons and what unblocks each are in `how-catalyst-works` and in `whats-happening`'s "why is it stuck" reference.
38
+
39
+ ## Evidence a phase ran
40
+
41
+ Do not infer progress from time passing. A phase leaves three kinds of evidence on the ticket, and a card move alone is not one of them:
42
+
43
+ 1. **The outcome comment.** The cloud posts a card per phase: a completed phase names the phase, the attempt, and its artifact; a failed phase names the phase, the attempt, and the failure class, and may carry a park or hold block. A remediate round posts its own attempt card with the round number and the class it is repairing. These arrive as comment frames on the watch.
44
+ 2. **The attachment and document.** Each artifact-bearing phase (research, plan, implement, validate, pr, remediate) is projected to a Linear document titled with the ticket, the phase, the attempt and the date, attached to the ticket, with a short link comment. Research and plan on a ticket with a project also appear as a project link.
45
+ 3. **The agent session.** The ticket's agent session carries the ladder as its plan, with the phases before the current one completed, the current one in progress, and the rest pending; its activities are the phase start, the gate, artifacts written, the PR opened, and the report.
46
+
47
+ The card's stage is the fourth, weakest signal: a completed phase moves the card to the stage the advance table names, a failed phase writes no stage at all, and Done is written only when the pull request actually merges, by the merge webhook, never by a phase. A card sitting in a stage tells you which phase last finished, not whether the next one is running; `scripts/scope-status.mjs` shows running and leased phases beside the stage for exactly that reason.
48
+
49
+ ## Parking, and what it does not do
50
+
51
+ Parking is the lever that stops the cloud offering more rounds on a ticket: moved out of the dispatch column and the ladder's stages, the ticket is excluded at the next offer. It does not kill a phase that is already running under a lease; that container finishes its phase, posts its outcome, and the next offer finds the card parked. If you park a ticket the cloud has itself parked (three consecutive failures, or the remediate round cap), record why in a bookkeeping note. Releasing a cloud park is not a card move: once its cause is fixed, the `unstick` skill releases it with `catalyst-skills release <ticket>`, and only a refusal that names a person's action becomes an ask.
52
+
53
+ Un-parking is the same dispatch move again. The counted attempts and rounds do not reset when a card comes back; the contract's thresholds say how many remain.
54
+
55
+ ## What you never do to make work ready
56
+
57
+ - Never move a card into a research, plan, implement, validate, PR, done or canceled stage by hand to "skip ahead". The cloud reads those stages as the record of what ran.
58
+ - Never remove an ask label or apply the release label yourself; a human decides whether a ticket is a question.
59
+ - Never dispatch two tickets that touch the same files at once; serialise them or let the second one wait as `scope_overlap`.
60
+ - Never dispatch a ticket you have not read in full.