@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,185 @@
1
+ #!/usr/bin/env node
2
+ // lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: by spawning the catalyst-skills
3
+ // CLI. The CLI holds the SDK and the key; this file holds neither. It reads
4
+ // ~/.config/catalyst-cloud/customer.json (under CATALYST_SKILLS_HOME when set, else HOME) for the
5
+ // CLI path that login recorded and falls back to `npx @catalyst-cloud/catalyst-skills`.
6
+ //
7
+ // Exit codes every script built on this file shares: 2 = this machine is not connected, 1 = the
8
+ // script's own check failed, 0 = fine.
9
+ import { spawn } from "node:child_process";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
14
+
15
+ export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
16
+ export const NOT_CONFIGURED_EXIT = 2;
17
+ export const CHECK_FAILED_EXIT = 1;
18
+
19
+ export function homeDir() {
20
+ return process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "/";
21
+ }
22
+
23
+ export function configDir() {
24
+ return join(homeDir(), ".config", "catalyst-cloud");
25
+ }
26
+
27
+ export function configPath() {
28
+ return join(configDir(), "customer.json");
29
+ }
30
+
31
+ /** The stored config, or null when the file is absent or unreadable. Never throws. */
32
+ export function loadCustomerConfig() {
33
+ const path = configPath();
34
+ if (!existsSync(path)) return null;
35
+ try {
36
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
37
+ if (!hasCredential(parsed) || typeof parsed.account !== "string") return null;
38
+ return parsed;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /** Print the one not-connected line and exit 2. */
45
+ export function requireConfigured() {
46
+ const cfg = loadCustomerConfig();
47
+ if (cfg) return cfg;
48
+ console.error(`not connected: ${configPath()} is missing or unreadable — run: ${CONNECT_COMMAND}`);
49
+ process.exit(NOT_CONFIGURED_EXIT);
50
+ }
51
+
52
+ /** The command and argument prefix that reaches the CLI on this machine. */
53
+ export function cliCommand(cfg = loadCustomerConfig()) {
54
+ if (cfg && typeof cfg.cliPath === "string" && existsSync(cfg.cliPath)) {
55
+ return { command: process.execPath, prefix: [cfg.cliPath], via: `node ${cfg.cliPath}` };
56
+ }
57
+ const npx = process.platform === "win32" ? "npx.cmd" : "npx";
58
+ return { command: npx, prefix: [PACKAGE_NAME], via: `npx ${PACKAGE_NAME}` };
59
+ }
60
+
61
+ /**
62
+ * Run one CLI verb and capture its output. Resolves `{code, stdout, stderr, notConfigured}`;
63
+ * `notConfigured` is true when the CLI itself said the machine is not connected.
64
+ */
65
+ export function runCli(args, { stdin } = {}) {
66
+ const { command, prefix } = cliCommand();
67
+ return new Promise((resolve, reject) => {
68
+ const child = spawn(command, [...prefix, ...args], {
69
+ stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
70
+ env: process.env,
71
+ shell: process.platform === "win32",
72
+ });
73
+ let stdout = "";
74
+ let stderr = "";
75
+ child.stdout.on("data", (d) => (stdout += String(d)));
76
+ child.stderr.on("data", (d) => (stderr += String(d)));
77
+ child.on("error", reject);
78
+ child.on("close", (code) => {
79
+ resolve({ code: code ?? 1, stdout, stderr, notConfigured: /not (joined|connected)/i.test(stderr) || /not (joined|connected)/i.test(stdout) });
80
+ });
81
+ if (stdin !== undefined) child.stdin.end(stdin);
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Run one CLI verb with the terminal attached, so a long-running verb such as `watch` streams
87
+ * straight through. Resolves with the exit code; SIGINT and SIGTERM are forwarded to the child.
88
+ */
89
+ export function execCli(args) {
90
+ const { command, prefix } = cliCommand();
91
+ return new Promise((resolve, reject) => {
92
+ const child = spawn(command, [...prefix, ...args], { stdio: "inherit", env: process.env, shell: process.platform === "win32" });
93
+ const forward = (sig) => () => {
94
+ try {
95
+ child.kill(sig);
96
+ } catch {
97
+ // already gone
98
+ }
99
+ };
100
+ const onInt = forward("SIGINT");
101
+ const onTerm = forward("SIGTERM");
102
+ process.on("SIGINT", onInt);
103
+ process.on("SIGTERM", onTerm);
104
+ child.on("error", reject);
105
+ child.on("close", (code) => {
106
+ process.off("SIGINT", onInt);
107
+ process.off("SIGTERM", onTerm);
108
+ resolve(code ?? 1);
109
+ });
110
+ });
111
+ }
112
+
113
+ /** Parse the CLI's --json output; a parse failure names the first line of what came back. */
114
+ export function parseJson(stdout) {
115
+ try {
116
+ return JSON.parse(stdout);
117
+ } catch {
118
+ throw new Error(`the CLI did not answer with JSON: ${stdout.split("\n")[0] ?? "(empty)"}`);
119
+ }
120
+ }
121
+
122
+ /** Exit 2 with the CLI's own not-connected line when a call reports it; otherwise return the result. */
123
+ export function guard(result) {
124
+ if (result.notConfigured) {
125
+ process.stderr.write(result.stderr || result.stdout);
126
+ process.exit(NOT_CONFIGURED_EXIT);
127
+ }
128
+ return result;
129
+ }
130
+
131
+ /** A tiny flag parser: `--name value`, `--name=value`, `--flag`, repeatable names collected as arrays. */
132
+ export function parseFlags(argv, { values = [], repeat = [], booleans = [] } = {}) {
133
+ const flags = {};
134
+ const positionals = [];
135
+ for (let i = 0; i < argv.length; i++) {
136
+ const a = argv[i];
137
+ if (a === "--") {
138
+ positionals.push(...argv.slice(i + 1));
139
+ break;
140
+ }
141
+ if (!a.startsWith("--")) {
142
+ positionals.push(a);
143
+ continue;
144
+ }
145
+ let name = a.slice(2);
146
+ let value;
147
+ const eq = name.indexOf("=");
148
+ if (eq !== -1) {
149
+ value = name.slice(eq + 1);
150
+ name = name.slice(0, eq);
151
+ }
152
+ if (booleans.includes(name)) {
153
+ flags[name] = true;
154
+ continue;
155
+ }
156
+ if (!values.includes(name) && !repeat.includes(name)) {
157
+ console.error(`unknown option: --${name} (try --help)`);
158
+ process.exit(CHECK_FAILED_EXIT);
159
+ }
160
+ if (value === undefined) {
161
+ value = argv[++i];
162
+ if (value === undefined) {
163
+ console.error(`--${name} needs a value`);
164
+ process.exit(CHECK_FAILED_EXIT);
165
+ }
166
+ }
167
+ if (repeat.includes(name)) flags[name] = [...(flags[name] ?? []), value];
168
+ else flags[name] = value;
169
+ }
170
+ return { flags, positionals };
171
+ }
172
+
173
+ export function wantsHelp(argv) {
174
+ return argv.includes("--help") || argv.includes("-h");
175
+ }
176
+
177
+ const HELP = `lib/cli.mjs — shared helper; not a command.
178
+
179
+ Resolves the catalyst-skills CLI (the path login recorded in ${configPath()}, else npx ${PACKAGE_NAME})
180
+ and runs one verb for the script that imports it. Run any sibling script with --help instead.`;
181
+
182
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
183
+ console.log(HELP);
184
+ process.exit(0);
185
+ }
@@ -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,68 @@
1
+ #!/usr/bin/env node
2
+ // verify-connection.mjs — did the connect step land? One line each for the tenant, the contract version and the
3
+ // replica, from the CLI's own verbs. The machine is connected when `status` names a tenant.
4
+ import { CHECK_FAILED_EXIT, cliCommand, loadCustomerConfig, parseFlags, runCli, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/verify-connection.mjs [--json]
7
+
8
+ Runs catalyst-skills status, contract --path contractVersion, and replica status, and prints one
9
+ line for each: the tenant this machine is connected to, the cached contract version, and the replica
10
+ verdict. Nothing is written.
11
+
12
+ Options:
13
+ --json one JSON document instead of three lines
14
+ --help this text
15
+
16
+ Exit codes: 1 when status reports the machine is not connected (or the CLI could not be run);
17
+ 0 otherwise, whatever the replica verdict is, because the replica is optional.`;
18
+
19
+ const argv = process.argv.slice(2);
20
+ if (wantsHelp(argv)) {
21
+ console.log(HELP);
22
+ process.exit(0);
23
+ }
24
+ const { flags, positionals } = parseFlags(argv, { booleans: ["json"] });
25
+ if (positionals.length > 0) {
26
+ console.error(`unexpected argument: ${positionals[0]} (see --help)`);
27
+ process.exit(CHECK_FAILED_EXIT);
28
+ }
29
+
30
+ const cfg = loadCustomerConfig();
31
+ const { via } = cliCommand(cfg);
32
+ const out = { cli: via, connected: false, tenant: null, contractVersion: null, replica: null };
33
+
34
+ let status;
35
+ try {
36
+ status = await runCli(["status"]);
37
+ } catch (err) {
38
+ console.error(`could not run the CLI (${via}): ${err instanceof Error ? err.message : String(err)}`);
39
+ process.exit(CHECK_FAILED_EXIT);
40
+ }
41
+ const tenantLine = status.stdout.split("\n").find((l) => l.startsWith("Tenant:"));
42
+ out.connected = status.code === 0 && Boolean(tenantLine) && !status.notConfigured;
43
+ out.tenant = tenantLine ? tenantLine.slice("Tenant:".length).trim() : null;
44
+
45
+ if (out.connected) {
46
+ const contract = await runCli(["contract", "--path", "contractVersion"]);
47
+ out.contractVersion = contract.code === 0 ? contract.stdout.trim() : null;
48
+ out.contractError = contract.code === 0 ? undefined : (contract.stderr || contract.stdout).trim().split("\n").at(-1);
49
+ const replica = await runCli(["replica", "status", "--json"]);
50
+ try {
51
+ const parsed = JSON.parse(replica.stdout);
52
+ out.replica = { verdict: parsed.verdict, exitCode: replica.code, cursor: parsed.cursor ?? null, reasons: parsed.reasons ?? [] };
53
+ } catch {
54
+ out.replica = { verdict: "unknown", exitCode: replica.code, line: (replica.stdout || replica.stderr).trim() };
55
+ }
56
+ }
57
+
58
+ if (flags.json) {
59
+ console.log(JSON.stringify(out));
60
+ } else if (!out.connected) {
61
+ console.log(`not connected (${via}): ${(status.stdout || status.stderr).trim().split("\n")[0] ?? "status printed nothing"}`);
62
+ } else {
63
+ console.log(`tenant: ${out.tenant}`);
64
+ console.log(out.contractVersion ? `contract: version ${out.contractVersion} cached` : `contract: not cached (${out.contractError ?? "unknown reason"}) — run: catalyst-skills contract --refresh`);
65
+ const r = out.replica;
66
+ console.log(`replica: ${r.verdict}${r.cursor !== null && r.cursor !== undefined ? ` (cursor ${r.cursor})` : ""}${r.reasons && r.reasons.length ? ` — ${r.reasons.join("; ")}` : ""}${r.verdict === "absent" ? " — optional; start it with: catalyst-skills replica start --detach" : ""}`);
67
+ }
68
+ process.exit(out.connected ? 0 : CHECK_FAILED_EXIT);
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: how-catalyst-works
3
+ description: >-
4
+ How Catalyst Cloud runs a ticket on the customer's own tenant, as facts an agent loads on demand: the eight-phase ladder and what each phase produces, the eleven board slots and this team's live stage map, what happens when a phase fails (retry, backoff, Remediate, park), how the queue is ordered and routed and every reason a ticket is excluded, and the coding-account model. Use when a person asks "how does this work?", "why did it do that?", "why is this stuck?", "what runs next?" or "how does it prioritise?". Read-only; its scripts explain one ticket's eligibility in plain English, show what is running and queued, and print the tenant's stage map and thresholds straight from the contract.
5
+ allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
+ ---
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.8.0 — written in this repository for customer tenants -->
8
+
9
+ # How Catalyst works
10
+
11
+ You explain the machine. A person asks why Catalyst did something, what it will do next, or how it decides; you answer from the tenant's live contract and eligibility explainer, and from the invariants in the references below. You never guess a stage name, a label, a threshold or a route: the scripts print the live values.
12
+
13
+ Every tenant-specific fact (stage names and ids, label ids, the ladder keying, the thresholds, the merge policy) comes from `GET /api/v1/agent/contract`, cached per session by the `catalyst-skills` CLI. The references restate only what does not vary per tenant.
14
+
15
+ ## Run first
16
+
17
+ Run each with `--help` before reading anything else; the scripts are black boxes, not reading material.
18
+
19
+ - `node scripts/explain-ticket.mjs <ticket>` — why one ticket is or is not about to run, as one paragraph plus the raw eligibility row.
20
+ - `node scripts/whats-running.mjs [--queue] [--accounts] [--team <key>]` — fleet activity, the agent roster and lease attributions; with `--queue` the dispatch order; with `--accounts` the coding-account line.
21
+ - `node scripts/show-my-map.mjs [--team <key>]` — this tenant's slot-to-stage map, ladder and live thresholds.
22
+
23
+ Exit codes: 0 answered, 1 not found or a usage error, 2 this machine is not connected or the cloud refused (the one line printed says which; connect first if it names the login).
24
+
25
+ ## Load on demand
26
+
27
+ | when | read |
28
+ | -- | -- |
29
+ | "what is Catalyst Cloud?", "what changes for me?" | `references/what-catalyst-is.md` |
30
+ | "what are the phases, what does each produce, when is a ticket Done?" | `references/the-ladder.md` |
31
+ | "which column is which, why does nothing dispatch, what is a slot?" | `references/stages-and-mapping.md` |
32
+ | a phase FAILED, a card went to Remediate, a ticket is parked or on hold | `references/when-a-phase-fails.md` |
33
+ | "what runs next, why not this one, what does this exclusion reason mean?" | `references/what-runs-next.md` |
34
+ | "why is nothing running", walls, quarantine, which provider ran a phase | `references/coding-accounts.md` |
35
+
36
+ ## Rules
37
+
38
+ - **Print, never recall.** A stage name, label, threshold or route in your answer must have come from a script's output in this session.
39
+ - **A reason is a layer.** Translate an exclusion reason through `references/what-runs-next.md`; name what releases it and who can do that (a clock, a comment, a push, an operator).
40
+ - **Unknown is not absent.** An `unknown` verdict from the explainer means the cloud could not look; report it as inconclusive.
41
+ - **System causes are one alert.** A provider outage, the runner image breaker or a paused repository holds many tickets for one reason; never escalate it ticket by ticket.
42
+ - **Say what a key cannot see.** A few PR facts are not mirrored (labels, the reviewer's reaction) and a park is released only by an operator; the scripts say so and name the settings page. Do not fill the gap with a guess.
43
+ - **Depth lives elsewhere.** What a ticket accumulates in Linear is `catalyst-linear`; what a PR accumulates and whether it is mergeable is `catalyst-github`; raising a decision is `what-needs-me`.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "How Catalyst Works"
3
+ short_description: "Explain how Catalyst Cloud runs a ticket: the ladder, your stage map, failures, what runs next, coding accounts"
4
+ default_prompt: "Use $how-catalyst-works to explain why Catalyst did that, or what it will run next."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,4 @@
1
+ identity: { pack: catalyst-cloud-skills, skill: how-catalyst-works }
2
+ effects: []
3
+ invocation: implicit
4
+ exposure: [catalog]
@@ -0,0 +1,51 @@
1
+ # Coding accounts: slots, windows, walls, and what a key cannot read yet
2
+
3
+ This reference restates the model, which does not vary per tenant. Your tenant's actual accounts — provider, harness, declared and observed state, usage windows, walls, quarantine — are readable with your key: `node scripts/whats-running.mjs --accounts` prints the live rows (`catalyst-skills accounts`). No credential or email ever rides in that read; the settings page (`/settings/coding-accounts`) is where an admin enrols, pauses or removes one.
4
+
5
+ ## What a slot is
6
+
7
+ A slot is one enrolled coding-account credential: a subscription or token that runs phases. A tenant may enrol several accounts of the same provider under the same email, because identity is the credential, not the email; the email is a display field. A revoked slot leaves the roster.
8
+
9
+ ## Providers and harnesses
10
+
11
+ | Provider | Harness | Credential |
12
+ | -- | -- | -- |
13
+ | Claude | the Claude CLI | its own subscription |
14
+ | Codex | Codex | its own subscription |
15
+ | GLM | the Claude CLI | its own token |
16
+ | Qwen | the Claude CLI | its own token |
17
+ | GLM via OpenCode | OpenCode | the same GLM enrolment |
18
+ | Qwen via OpenCode | OpenCode | the same Qwen enrolment |
19
+
20
+ The harness is a property of the catalog entry, never a routing input. Which provider a phase runs on is the routing decision in `references/what-runs-next.md`; the routing rows, not the slot, name the model.
21
+
22
+ ## Two state axes
23
+
24
+ - **Declared**: `active` or `disabled`. The operator sets it.
25
+ - **Observed**: `healthy`, `degraded` or `unknown`. The poller sets it from what the provider reports.
26
+
27
+ Two more lifecycle facts sit beside them: **quarantined** (system-set, on a credential conflict or an authentication mismatch; sticky, only an operator clears it) and **revoked** (operator-set).
28
+
29
+ ## Windows, walls, headroom
30
+
31
+ - Subscriptions meter usage over a **5-hour** window and a **7-day** window, each with a used percentage and a reset time.
32
+ - A **wall** is the window limit a session can die at mid-run. Before a slot is granted, a fit gate projects the phase's expected burn against the remaining headroom with a safety margin; a slot that would hit the wall is not offered.
33
+ - **Headroom** per provider is advisory: it counts eligible and degraded slots and the best remaining percentage, and it can never promise what a reservation would refuse because both read the same eligibility predicate.
34
+ - Holds: a Claude slot can serve several phases at once up to a per-account cap; a Codex slot serves one at a time because its refresh tokens are single-use.
35
+ - Slot choice orders by usage band, then live-hold count, then used percent, then the latest reset, so bursts spread across accounts rather than stacking on one.
36
+
37
+ A poller refreshes an active slot's usage every 5 minutes and a disabled one daily. Separately, vendor status pages are polled for provider health; that signal says nothing about your own accounts' windows, walls or quarantine.
38
+
39
+ ## What the settings page shows
40
+
41
+ Your tenant's coding-accounts page lists each slot with a five-value status, first match wins: expired-or-revoked, walled, active, attested (healthy and active, but the provider is unobserved), unobserved. A drilldown shows the windows, the holds and the history.
42
+
43
+ ## What a key cannot read yet, and what to say
44
+
45
+ Coding-account status (provider, declared and observed state, window percentages and resets, walls, quarantine, live holds) is served only behind the tenant's admin gate. So when a human asks "why is nothing running?":
46
+
47
+ 1. Run `node scripts/explain-ticket.mjs <ticket>` for a stuck ticket. `routing_unavailable` with a detail naming a slot or provider, or `no_eligible_account_slot` in the routing block, points at accounts.
48
+ 2. Read the live rows with `catalyst-skills accounts` and say what they show; an empty list means no account is enrolled, and the script prints the settings link. Do not guess a wall or a quarantine from silence — a wall is the `walled` field, a quarantine is `quarantined` with its reason.
49
+ 3. Treat it as ONE fleet-level cause for every ticket it holds, never as a per-ticket escalation.
50
+
51
+ The customer session never holds or mints a coding-account credential; phases run in the cloud on the enrolled accounts, and repository access is a per-phase installation token.
@@ -0,0 +1,56 @@
1
+ # Stages and mapping: eleven slots, five that matter, and how to read this team's map
2
+
3
+ This reference restates invariants (the slot vocabulary, which slots are load-bearing, the allowed state types, the three mapping modes). Everything about YOUR team — which Linear stage each slot maps to, its name, its type, whether it still exists — is live on the contract under `teams[].stages`. Read it with `node scripts/show-my-map.mjs [--team <key>]`; never quote a stage name or id from memory.
4
+
5
+ ## The eleven slots, in pipeline order
6
+
7
+ `dispatch`, `intake`, `research`, `plan`, `implement`, `remediate`, `verify`, `review`, `pr`, `done`, `canceled`
8
+
9
+ A slot is Catalyst's name for a position on the board. A stage is the Linear workflow state your team actually has. Mapping is the per-team table that binds each slot to one stage.
10
+
11
+ ## Five slots are load-bearing
12
+
13
+ | Slot | Why it matters | Allowed Linear state types |
14
+ | -- | -- | -- |
15
+ | `dispatch` | Moving a card here is how work is dispatched. Nothing is offered from any other column. | `unstarted`, `backlog` |
16
+ | `intake` | Where a never-seen ticket lands for the optional intake pass. | `unstarted`, `backlog` (never Linear's reserved triage type) |
17
+ | `pr` | Where a card must sit for `merge` to be offered. | `started` |
18
+ | `done` | Written by the merge webhook. | `completed` |
19
+ | `canceled` | Terminal; a card here is never work. | `canceled` |
20
+
21
+ An absent or wrongly-typed mapping on one of these five is a distinct silent failure: the ladder simply never moves. The other six slots are informational — a wrong value costs a warning in readiness, not a stall. That asymmetry is why "map my stages" is a five-field decision.
22
+
23
+ ## Three mapping modes
24
+
25
+ The contract's `teams[].workflowMode` reports which one a team is in:
26
+
27
+ - **adopted**: Catalyst created its recommended stage set for the team, one stage per slot, with verify and review sharing one validation stage.
28
+ - **mapped**: the team kept its existing stages and a human chose which stage fills each slot.
29
+ - **mixed**: some slots adopted, some hand-chosen.
30
+
31
+ `teams[].gitAutomation` is a stored consent for a feature that is not built yet (Catalyst managing a team's Linear git automations); nothing reads it, so `off` never stops work and turning it on would start none. What decides whether a team's tickets start is the mapping above: until dispatch, pr, done and canceled each point at a live stage, Catalyst starts nothing in that team, and `explain` says so by name.
32
+
33
+ Stage mappings live in the app, at Settings → Linear teams. The cloud does not read `.catalyst/catalyst.toml` yet, so a mapping written there changes nothing today. An older `.catalyst/config.json` is not imported.
34
+
35
+ ## The state id is the authority; names are display
36
+
37
+ Each mapped stage carries a `stateId`, a display `name`, a `type`, `stateStillExists` and a `source` (how the mapping was chosen). Only the id is a lookup key. A Linear-to-Linear import can preserve every human-readable name while re-minting every state id, and then a name-based lookup points at nothing. So:
38
+
39
+ - Move cards by slot (`catalyst-linear`'s move script does this) and let the CLI resolve the id from the contract. Never move a card "to Todo" by name.
40
+ - `stateStillExists: false` means the mapped state is provably gone; the map needs fixing in your tenant settings before that slot can be written to. The CLI refuses such a move rather than guessing.
41
+ - The stage names the contract shows come from the mirror's live view of Linear, not from a stored snapshot, so they are current at read time.
42
+
43
+ ## How to read the printed map
44
+
45
+ `node scripts/show-my-map.mjs` prints, per team, one row per slot: the slot, the stage name (or `(unmapped)`, or `(state gone)`), the state type, whether the state still exists, and the source. Load-bearing slots are starred. Then the team's ask, hold and release labels with `(absent)` where the workspace has no such label, then the ladder (phases, keying, intake on or off) and the live thresholds.
46
+
47
+ Reading it for a question:
48
+
49
+ - "Why does nothing dispatch?" — is `dispatch` mapped, does its state still exist, and is the card actually in that stage? `node scripts/explain-ticket.mjs <ticket>` names `not_at_dispatch_stage` when the card is elsewhere.
50
+ - "Why is merge not running?" — is the card in the `pr` slot's stage?
51
+ - "Why did the card go to Remediate?" — `remediate` is mapped, so a failed phase moved it there (see `references/when-a-phase-fails.md`); if `remediate` is unmapped the same episode shows up as the hold label from `teams[].labels.hold` instead.
52
+ - "The team's Backlog is not in the list" — correct. Backlog is not a slot. Parking a card is a move to the team's backlog-type state, which the CLI resolves from the team's live workflow states rather than from the map.
53
+
54
+ ## Readiness over the mapping
55
+
56
+ The contract carries a readiness vector per team (`teams[].readiness`), including whether every mapped state exists, whether the mapping is total, whether the types are compatible, whether the labels are present, whether writes land and whether the webhook covers the team. A check the cloud could not run is `unknown`, never `pass`. `catalyst-setup` reads this vector; this skill only points at it.
@@ -0,0 +1,41 @@
1
+ # The ladder: eight phases, what each one produces
2
+
3
+ This reference restates invariants. The phase order and what each phase leaves behind do not vary per tenant. The only live values are whether intake is switched on for your tenant and which keying the advance table uses; both are on the contract under `ladder` (print them with `node scripts/show-my-map.mjs`).
4
+
5
+ ## The phases, in order
6
+
7
+ | Phase | What it produces | Notes |
8
+ | -- | -- | -- |
9
+ | `intake` | nothing projected (a classification pass) | Optional head. Offered only to a ticket that has never entered the ladder, and only when the contract says `ladder.intakeEnabled` is true. |
10
+ | `research` | `research.md` | The first ladder phase. Offered when the card sits in the team's dispatch slot. |
11
+ | `plan` | `plan.md` | |
12
+ | `implement` | `implement.md` | Creates the ticket branch (named exactly the ticket identifier) and opens a draft pull request. |
13
+ | `validate` | `validation.md` | Its success moves the card to the team's review stage; verify and review are two slots that both land on that stage. |
14
+ | `pr` | `pr.md` | The first line is the pull-request title, then a blank line, then the body. The phase rebases onto the default branch, force-pushes, writes the title and body, and marks the PR ready for review. |
15
+ | `remediate` | `remediation.json` | An interrupt, not a step forward. It repairs a phase that failed and never advances the card past the PR slot. |
16
+ | `merge` | a receipt, no artifact | Applies the queue-ready label once its evidence gate passes. It writes no board state. |
17
+
18
+ Every artifact-bearing phase (all but `intake` and `merge`) projects its artifact into Linear as a document attached to the ticket, with a short link comment; `catalyst-linear` describes those shapes.
19
+
20
+ ## What moves the card
21
+
22
+ An advance table maps a phase outcome to a card move: research done moves the card from the dispatch slot to the research slot, plan done from research to plan, implement done from plan to implement, validate done from implement to review, pr done from review to pr. The table is served on the contract as `ladder.advance` so you can read the exact rows; do not restate them from memory, because the tenant's keying decides whether a stage names the phase that just finished (trailing, the default) or the phase still ahead (leading).
23
+
24
+ Three rows never move the card:
25
+
26
+ - A **failed** phase writes no board state. The ticket stays where it was, retryable, and the failure handling in `references/when-a-phase-fails.md` takes over.
27
+ - **`remediate` done** writes nothing by itself; the card restores to its exact pre-failure stage once a remediation round succeeds.
28
+ - **`merge` done** writes nothing. The receipt only ever meant "the queue-ready label was applied".
29
+
30
+ ## Done is written by the merge, never by a phase
31
+
32
+ The terminal move to the done slot is keyed to the real pull-request-merged event from GitHub, not to any phase receipt. When the PR merges, the mirror's own ingest path moves the ticket to Done within a few seconds, with no session and no human sweep, and a bounded periodic sweep backstops a dropped webhook. So:
33
+
34
+ - A ticket still not Done a minute after its PR merged is a finding, not a chore to hand-close.
35
+ - Done is not the same as live. A change to something that is deployed separately is inert until that deployment happens; the ticket state says the code merged, nothing more.
36
+
37
+ ## How to read a ticket's position on the ladder
38
+
39
+ 1. The card's stage tells you which phase last finished (under trailing keying) — read the team's map with `node scripts/show-my-map.mjs --team <key>` to translate a stage name into a slot.
40
+ 2. The phase-outcome comments on the ticket tell you which attempts ran and how they ended; the document attachments are the artifacts themselves.
41
+ 3. `node scripts/explain-ticket.mjs <ticket>` tells you what the cloud will run next, or why it will not.
@@ -0,0 +1,30 @@
1
+ # What Catalyst Cloud is
2
+
3
+ Explain it in this person's terms: their repositories, their tickets, how they work today. Use their examples. Keep the facts exact; the wording is yours.
4
+
5
+ ## The short version
6
+
7
+ They keep filing tickets in Linear. Moving a card into the team's dispatch column starts the work. Catalyst Cloud then takes the ticket through research, a plan, the implementation, validation, a pull request and the merge.
8
+
9
+ Each phase runs in a cloud container, on a coding account the tenant enrolled. Each phase leaves a document and an outcome comment on the ticket, so the ticket is the record.
10
+
11
+ Their laptop stops being where coding sessions run. There are no local sessions, no home server, and no test runners or headless browsers competing for the machine. They install two skill packs and a small CLI that holds their sign-in.
12
+
13
+ Each container is set up for the repository: the same skills, MCP servers, environment variable names and secrets. They declare the names once; the values stay in the app.
14
+
15
+ Agents react to events instead of polling: a ticket edited, a pull request opened, a review landing, a check failing. The `run-this-project` skill explains the watch.
16
+
17
+ A decision only they can make arrives as a ticket in their own Linear. It has options, a default if they stay silent, and a list of what it holds. Everything else keeps moving.
18
+
19
+ ## What changes in their day
20
+
21
+ - They start work by moving a card into the dispatch column. Nothing else starts a ticket. See `references/stages-and-mapping.md`.
22
+ - They write the ticket for a reader who cannot see their chat: an outcome in the title, what done looks like, a priority, no open blocker.
23
+ - They never move a card into a working stage by hand, and they never close a ticket. The merge writes Done. See `references/the-ladder.md`.
24
+ - A draft pull request appears that they did not open. A later phase marks it ready. A green pull request with no open review threads merges through the queue. A hold label keeps it out.
25
+ - They answer asks instead of being paged. One stuck ticket or a provider outage is never an ask.
26
+ - A comment they post gets an eyes reaction and a reply in its thread.
27
+
28
+ ## Why it is different
29
+
30
+ Every item above has a mechanism in another reference. Retries and repair rounds are in `references/when-a-phase-fails.md`. An unknown verdict means "could not look": see `references/what-runs-next.md`. One outage is one alert, not one per ticket.
@@ -0,0 +1,77 @@
1
+ # What runs next: queue order, concurrency, routing, and every exclusion reason
2
+
3
+ This reference restates invariants. The live queue is `node scripts/whats-running.mjs --queue [--team <key>]`; one ticket's verdict is `node scripts/explain-ticket.mjs <ticket>`. The reason phrases below match the CLI's own table, so a reason you see printed can be looked up here.
4
+
5
+ ## Queue order
6
+
7
+ Each team's dispatch queue is derived by the tenant's own store, continuously, from the board:
8
+
9
+ 1. Queued tickets (cards in the dispatch slot) rank by **priority ascending, then created time ascending, then identifier ascending**.
10
+ 2. Mid-ladder tickets (any ticket with a phase already completed) join the candidate set and order **by completed-phase count descending**, so a ticket close to the end goes before one that just started. That count is an ordering surrogate only; it never decides membership or eligibility.
11
+
12
+ The queue re-derives within seconds of any board change and at the top of every periodic pass as a backstop. `dispatch_queue.source` reads `self-derived` when the tenant store built it itself, which is the normal case.
13
+
14
+ ## Concurrency
15
+
16
+ - Each repository has a concurrency cap (default 20 running phases). An operator can raise or lower it; a paused repository resolves to a cap of zero without touching the stored value, so resuming restores it.
17
+ - Dispatch buckets by repository and splits slots across teams so no team starves another. A slot that is running, might be running, or is restarting is never free capacity.
18
+ - Comment-wake work (an agent answering a human comment) shares the same cap as ladder work.
19
+
20
+ ## The routing decision, in five checks
21
+
22
+ When a phase is about to start, a route is chosen per candidate, in order, and the first survivor wins:
23
+
24
+ 1. capability match (can this route run this phase);
25
+ 2. a model is configured;
26
+ 3. the provider is available;
27
+ 4. if the candidate needs a coding-account slot: an eligible slot exists and the headroom floor is met;
28
+ 5. degraded, equivalent or stage-default parameters resolve.
29
+
30
+ No survivor is `no_eligible_account_slot` when any candidate was skipped on capacity, else `routing_unavailable` (a misconfiguration). See `references/coding-accounts.md` for what a slot is.
31
+
32
+ ## Every exclusion reason, one line each
33
+
34
+ | Reason | Meaning |
35
+ | -- | -- |
36
+ | `ticket_terminal` | the ticket is in a done or canceled state |
37
+ | `pipeline_complete` | every phase has already completed |
38
+ | `not_at_dispatch_stage` | the card is not in the team's dispatch column; move it there to dispatch |
39
+ | `not_at_pr_stage` | merge is next but the card is not in the PR column (waived when a tenant automation bounced it off PR and the bounce was recorded) |
40
+ | `blocked` | a live blocking relation; the blocker closes first |
41
+ | `cooling_down` | the offered phase is parked; an operator or a callback releases it, not a clock |
42
+ | `lease_held` | a live container already holds this phase |
43
+ | `intake_lease_held` | a later phase is offered while an intake container still holds the ticket |
44
+ | `ask_ticket` | it carries an ask label; a question is never work |
45
+ | `ask_shape_suspected` | its own text reads as a decision request; a human releases it with the release label from the contract's `vocabulary` |
46
+ | `externally_claimed` | a worker outside the cloud claimed it |
47
+ | `environment_check_required` | the repository's environment check has not run |
48
+ | `environment_check_running` | the environment check is in flight |
49
+ | `environment_check_failed` | the environment check failed |
50
+ | `environment_check_expired` | the environment verdict aged out |
51
+ | `environment_check_hash_mismatch` | the environment changed since the verdict |
52
+ | `scope_overlap` | its declared file scope intersects a ticket in flight (implement only; enforced once a team activates a scope policy) |
53
+ | `waiting_on` | a merge-gate failure with no remediable cause holds the card at PR |
54
+ | `branch_missing` | the ticket branch has never been seen |
55
+ | `branch_gone` | the ticket branch was deleted |
56
+ | `pr_merged` | its PR merged and no other PR is open |
57
+ | `no_change_hold` | a remediate round changed nothing; a human comment or a new push releases it |
58
+ | `validate_class_spent` | this validate failure already spent its one repair round |
59
+ | `stale_failure_episode` | the ladder advanced after the failure, so the round would repair a phase already passed |
60
+ | `runner_image_breaker` | fleet-wide: the live runner image fails every phase at startup; clears when the pin moves |
61
+ | `no_branch_to_remediate` | a remediate round is queued on a ticket with no branch |
62
+ | `retry_backoff` | retrying in place, waiting out its 2/5/15-minute rung |
63
+ | `routing_unavailable` | claimed then refused at kickoff: no route, no eligible slot, or the provider is unavailable; the detail names which |
64
+ | `repo_paused` | an operator paused the repository |
65
+ | `remediate_parked` | the remediate phase is parked, so the failing phase has nowhere to be repaired |
66
+
67
+ ## The unknowns (the evaluator fails closed)
68
+
69
+ When the cloud cannot answer, it says so rather than guessing: `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown`, `branch_snapshot_unknown`. An unknown is "I could not look", never "it is not there"; report it as inconclusive. The exception is a team with no saved stage mapping: `workflow_mapping_unknown` never clears on its own there, `explain` names the missing stages, and the fix is a tenant owner or admin mapping the team.
70
+
71
+ One advisory gates nothing: `human_addressed_unlabeled_ask_suspect` (assigned to a human with no delegate; possibly an unlabelled ask).
72
+
73
+ ## The three levers a human has
74
+
75
+ 1. **Priority** on the ticket reorders the queue.
76
+ 2. **The dispatch column**: moving a card into the dispatch slot dispatches it; moving it to the team's backlog-type state parks it and stops rounds.
77
+ 3. **Holds**: the hold labels on the pull request (from the contract's `merge.prLabels`) keep a green PR out of the merge queue; a human comment on the ticket clears the validate hold and the no-change hold.