@catalyst-cloud/catalyst-skills 0.1.1 → 0.2.1
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.
- package/CHANGELOG.md +8 -0
- package/README.md +109 -53
- package/bin/catalyst-skills.js +42 -2
- package/dist/args.js +242 -0
- package/dist/ask.js +137 -0
- package/dist/cli.js +167 -279
- package/dist/config.js +106 -0
- package/dist/contract-types.js +4 -0
- package/dist/contract.js +178 -0
- package/dist/detach.js +10 -0
- package/dist/errors.js +27 -0
- package/dist/execution.js +308 -0
- package/dist/http.js +175 -0
- package/dist/prompt.js +35 -0
- package/dist/query.js +225 -0
- package/dist/ready.js +123 -0
- package/dist/replica.js +318 -0
- package/dist/sdk.js +26 -0
- package/dist/skill-shape.js +179 -0
- package/dist/skills.js +74 -0
- package/dist/ts-deps-loader.js +68 -0
- package/dist/watch/consumer.js +141 -0
- package/dist/watch/cursor-file.js +62 -0
- package/dist/watch.js +173 -0
- package/dist/write.js +224 -0
- package/package.json +12 -7
- package/skills/catalyst-github/SKILL.md +35 -0
- package/skills/catalyst-github/agents/openai.yaml +6 -0
- package/skills/catalyst-github/agents/portability.yaml +4 -0
- package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
- package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
- package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
- package/skills/catalyst-linear/SKILL.md +43 -0
- package/skills/catalyst-linear/agents/openai.yaml +6 -0
- package/skills/catalyst-linear/agents/portability.yaml +5 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
- package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
- package/skills/catalyst-linear/scripts/comment.mjs +59 -0
- package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
- package/skills/catalyst-linear/scripts/label.mjs +48 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
- package/skills/catalyst-linear/scripts/move.mjs +41 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
- package/skills/catalyst-linear/scripts/search.mjs +49 -0
- package/skills/catalyst-setup/SKILL.md +32 -0
- package/skills/catalyst-setup/agents/openai.yaml +6 -0
- package/skills/catalyst-setup/agents/portability.yaml +4 -0
- package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
- package/skills/catalyst-setup/scripts/check.mjs +75 -0
- package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
- package/skills/connect-me/SKILL.md +55 -0
- package/skills/connect-me/agents/openai.yaml +6 -0
- package/skills/connect-me/agents/portability.yaml +5 -0
- package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
- package/skills/connect-me/scripts/lib/cli.mjs +184 -0
- package/skills/connect-me/scripts/verify-connection.mjs +68 -0
- package/skills/how-catalyst-works/SKILL.md +42 -0
- package/skills/how-catalyst-works/agents/openai.yaml +6 -0
- package/skills/how-catalyst-works/agents/portability.yaml +4 -0
- package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
- package/skills/how-catalyst-works/references/the-ladder.md +41 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
- package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
- package/skills/run-this-project/SKILL.md +45 -0
- package/skills/run-this-project/agents/openai.yaml +6 -0
- package/skills/run-this-project/agents/portability.yaml +5 -0
- package/skills/run-this-project/assets/stall-policy.json +15 -0
- package/skills/run-this-project/references/making-work-ready.md +60 -0
- package/skills/run-this-project/references/reacting-to-events.md +76 -0
- package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
- package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
- package/skills/run-this-project/scripts/make-ready.mjs +64 -0
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
- package/skills/what-needs-me/SKILL.md +41 -0
- package/skills/what-needs-me/agents/openai.yaml +6 -0
- package/skills/what-needs-me/agents/portability.yaml +5 -0
- package/skills/what-needs-me/references/raising-a-decision.md +41 -0
- package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
- package/skills/what-needs-me/references/settling-an-answer.md +37 -0
- package/skills/what-needs-me/scripts/inbox.mjs +47 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
- package/skills/what-needs-me/scripts/raise.mjs +53 -0
- package/skills/what-needs-me/scripts/settle.mjs +73 -0
- package/skills/whats-happening/SKILL.md +43 -0
- package/skills/whats-happening/agents/openai.yaml +6 -0
- package/skills/whats-happening/agents/portability.yaml +4 -0
- package/skills/whats-happening/assets/status-reply.json +77 -0
- package/skills/whats-happening/references/reading-the-board.md +43 -0
- package/skills/whats-happening/references/reprioritising.md +37 -0
- package/skills/whats-happening/references/routing-work.md +36 -0
- package/skills/whats-happening/references/status-reply.md +34 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
- package/skills/whats-happening/scripts/explain.mjs +28 -0
- package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
- package/skills/whats-happening/scripts/snapshot.mjs +145 -0
- package/skills/ask/SKILL.md +0 -37
- package/skills/concierge/SKILL.md +0 -41
- package/skills/join/SKILL.md +0 -30
- package/skills/linearis/SKILL.md +0 -40
- package/skills/setup/SKILL.md +0 -27
- package/skills/steward/SKILL.md +0 -30
|
@@ -0,0 +1,184 @@
|
|
|
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 account 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
|
+
|
|
14
|
+
export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
|
|
15
|
+
export const NOT_CONFIGURED_EXIT = 2;
|
|
16
|
+
export const CHECK_FAILED_EXIT = 1;
|
|
17
|
+
|
|
18
|
+
export function homeDir() {
|
|
19
|
+
return process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "/";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configDir() {
|
|
23
|
+
return join(homeDir(), ".config", "catalyst-cloud");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function configPath() {
|
|
27
|
+
return join(configDir(), "customer.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The stored config, or null when the file is absent or unreadable. Never throws. */
|
|
31
|
+
export function loadCustomerConfig() {
|
|
32
|
+
const path = configPath();
|
|
33
|
+
if (!existsSync(path)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.key !== "string" || typeof parsed.account !== "string") return null;
|
|
37
|
+
return parsed;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Print the one not-connected line and exit 2. */
|
|
44
|
+
export function requireConfigured() {
|
|
45
|
+
const cfg = loadCustomerConfig();
|
|
46
|
+
if (cfg) return cfg;
|
|
47
|
+
console.error(`not connected: ${configPath()} is missing or unreadable — run: CATALYST_CLOUD_TOKEN=<account key> npx ${PACKAGE_NAME} login`);
|
|
48
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The command and argument prefix that reaches the CLI on this machine. */
|
|
52
|
+
export function cliCommand(cfg = loadCustomerConfig()) {
|
|
53
|
+
if (cfg && typeof cfg.cliPath === "string" && existsSync(cfg.cliPath)) {
|
|
54
|
+
return { command: process.execPath, prefix: [cfg.cliPath], via: `node ${cfg.cliPath}` };
|
|
55
|
+
}
|
|
56
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
57
|
+
return { command: npx, prefix: [PACKAGE_NAME], via: `npx ${PACKAGE_NAME}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run one CLI verb and capture its output. Resolves `{code, stdout, stderr, notConfigured}`;
|
|
62
|
+
* `notConfigured` is true when the CLI itself said the machine is not connected.
|
|
63
|
+
*/
|
|
64
|
+
export function runCli(args, { stdin } = {}) {
|
|
65
|
+
const { command, prefix } = cliCommand();
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const child = spawn(command, [...prefix, ...args], {
|
|
68
|
+
stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
69
|
+
env: process.env,
|
|
70
|
+
shell: process.platform === "win32",
|
|
71
|
+
});
|
|
72
|
+
let stdout = "";
|
|
73
|
+
let stderr = "";
|
|
74
|
+
child.stdout.on("data", (d) => (stdout += String(d)));
|
|
75
|
+
child.stderr.on("data", (d) => (stderr += String(d)));
|
|
76
|
+
child.on("error", reject);
|
|
77
|
+
child.on("close", (code) => {
|
|
78
|
+
resolve({ code: code ?? 1, stdout, stderr, notConfigured: /not (joined|connected)/i.test(stderr) || /not (joined|connected)/i.test(stdout) });
|
|
79
|
+
});
|
|
80
|
+
if (stdin !== undefined) child.stdin.end(stdin);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run one CLI verb with the terminal attached, so a long-running verb such as `watch` streams
|
|
86
|
+
* straight through. Resolves with the exit code; SIGINT and SIGTERM are forwarded to the child.
|
|
87
|
+
*/
|
|
88
|
+
export function execCli(args) {
|
|
89
|
+
const { command, prefix } = cliCommand();
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const child = spawn(command, [...prefix, ...args], { stdio: "inherit", env: process.env, shell: process.platform === "win32" });
|
|
92
|
+
const forward = (sig) => () => {
|
|
93
|
+
try {
|
|
94
|
+
child.kill(sig);
|
|
95
|
+
} catch {
|
|
96
|
+
// already gone
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const onInt = forward("SIGINT");
|
|
100
|
+
const onTerm = forward("SIGTERM");
|
|
101
|
+
process.on("SIGINT", onInt);
|
|
102
|
+
process.on("SIGTERM", onTerm);
|
|
103
|
+
child.on("error", reject);
|
|
104
|
+
child.on("close", (code) => {
|
|
105
|
+
process.off("SIGINT", onInt);
|
|
106
|
+
process.off("SIGTERM", onTerm);
|
|
107
|
+
resolve(code ?? 1);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Parse the CLI's --json output; a parse failure names the first line of what came back. */
|
|
113
|
+
export function parseJson(stdout) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(stdout);
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error(`the CLI did not answer with JSON: ${stdout.split("\n")[0] ?? "(empty)"}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Exit 2 with the CLI's own not-connected line when a call reports it; otherwise return the result. */
|
|
122
|
+
export function guard(result) {
|
|
123
|
+
if (result.notConfigured) {
|
|
124
|
+
process.stderr.write(result.stderr || result.stdout);
|
|
125
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A tiny flag parser: `--name value`, `--name=value`, `--flag`, repeatable names collected as arrays. */
|
|
131
|
+
export function parseFlags(argv, { values = [], repeat = [], booleans = [] } = {}) {
|
|
132
|
+
const flags = {};
|
|
133
|
+
const positionals = [];
|
|
134
|
+
for (let i = 0; i < argv.length; i++) {
|
|
135
|
+
const a = argv[i];
|
|
136
|
+
if (a === "--") {
|
|
137
|
+
positionals.push(...argv.slice(i + 1));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (!a.startsWith("--")) {
|
|
141
|
+
positionals.push(a);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let name = a.slice(2);
|
|
145
|
+
let value;
|
|
146
|
+
const eq = name.indexOf("=");
|
|
147
|
+
if (eq !== -1) {
|
|
148
|
+
value = name.slice(eq + 1);
|
|
149
|
+
name = name.slice(0, eq);
|
|
150
|
+
}
|
|
151
|
+
if (booleans.includes(name)) {
|
|
152
|
+
flags[name] = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!values.includes(name) && !repeat.includes(name)) {
|
|
156
|
+
console.error(`unknown option: --${name} (try --help)`);
|
|
157
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
158
|
+
}
|
|
159
|
+
if (value === undefined) {
|
|
160
|
+
value = argv[++i];
|
|
161
|
+
if (value === undefined) {
|
|
162
|
+
console.error(`--${name} needs a value`);
|
|
163
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (repeat.includes(name)) flags[name] = [...(flags[name] ?? []), value];
|
|
167
|
+
else flags[name] = value;
|
|
168
|
+
}
|
|
169
|
+
return { flags, positionals };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function wantsHelp(argv) {
|
|
173
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const HELP = `lib/cli.mjs — shared helper; not a command.
|
|
177
|
+
|
|
178
|
+
Resolves the catalyst-skills CLI (the path login recorded in ${configPath()}, else npx ${PACKAGE_NAME})
|
|
179
|
+
and runs one verb for the script that imports it. Run any sibling script with --help instead.`;
|
|
180
|
+
|
|
181
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
182
|
+
console.log(HELP);
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
@@ -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,42 @@
|
|
|
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 — 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 are the phases, what does each produce, when is a ticket Done?" | `references/the-ladder.md` |
|
|
30
|
+
| "which column is which, why does nothing dispatch, what is a slot?" | `references/stages-and-mapping.md` |
|
|
31
|
+
| a phase FAILED, a card went to Remediate, a ticket is parked or on hold | `references/when-a-phase-fails.md` |
|
|
32
|
+
| "what runs next, why not this one, what does this exclusion reason mean?" | `references/what-runs-next.md` |
|
|
33
|
+
| "why is nothing running", walls, quarantine, which provider ran a phase | `references/coding-accounts.md` |
|
|
34
|
+
|
|
35
|
+
## Rules
|
|
36
|
+
|
|
37
|
+
- **Print, never recall.** A stage name, label, threshold or route in your answer must have come from a script's output in this session.
|
|
38
|
+
- **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).
|
|
39
|
+
- **Unknown is not absent.** An `unknown` verdict from the explainer means the cloud could not look; report it as inconclusive.
|
|
40
|
+
- **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.
|
|
41
|
+
- **Say what a key cannot see.** Coding-account status and per-ticket execution history are not readable with an account key yet; the scripts say so and name the settings page. Do not fill the gap with a guess.
|
|
42
|
+
- **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,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, their usage and their state are NOT readable with an account key today: `node scripts/whats-running.mjs --accounts` prints that by name, with the settings page where the facts live. When a tenant-facing read ships, the same script will print the live rows and this reference stays true.
|
|
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. Say plainly that the account key cannot read account state, and give the settings link the script prints. Do not guess a wall or a quarantine from silence.
|
|
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,54 @@
|
|
|
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 separate switch, off by default, because enabling it can delete a team's own review automation in Linear; it is never bundled into "adopt recommended".
|
|
32
|
+
|
|
33
|
+
## The state id is the authority; names are display
|
|
34
|
+
|
|
35
|
+
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:
|
|
36
|
+
|
|
37
|
+
- 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.
|
|
38
|
+
- `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.
|
|
39
|
+
- 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.
|
|
40
|
+
|
|
41
|
+
## How to read the printed map
|
|
42
|
+
|
|
43
|
+
`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.
|
|
44
|
+
|
|
45
|
+
Reading it for a question:
|
|
46
|
+
|
|
47
|
+
- "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.
|
|
48
|
+
- "Why is merge not running?" — is the card in the `pr` slot's stage?
|
|
49
|
+
- "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.
|
|
50
|
+
- "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.
|
|
51
|
+
|
|
52
|
+
## Readiness over the mapping
|
|
53
|
+
|
|
54
|
+
The contract carries a ten-check 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,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.
|
|
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.
|
|
@@ -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 | an operator, not a clock |
|
|
37
|
+
| remediate round cap reached | an operator, not a clock |
|
|
38
|
+
| missing branch | its own budgeted release loop |
|
|
39
|
+
| rebase conflict | its own budgeted release loop |
|
|
40
|
+
|
|
41
|
+
An account key cannot release a park today; the repair verbs are operator-only. When a ticket is parked, say so, name the failure class the explainer shows, and hand the release to whoever administers the tenant.
|
|
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 round count of the cap and the park history are not readable with an account key yet; `catalyst-skills explain --history <ticket>` says so and names where to read them.
|
|
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 (an operator, 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);
|