@catalyst-cloud/catalyst-skills 0.4.0 → 0.5.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.
- package/CHANGELOG.md +12 -0
- package/README.md +13 -10
- package/dist/args.js +18 -0
- package/dist/cli.js +10 -1
- package/dist/events.js +106 -0
- package/dist/execution.js +54 -8
- package/dist/release.js +142 -0
- package/dist/replica.js +7 -1
- package/dist/transport.js +5 -2
- package/package.json +5 -4
- package/skills/catalyst-github/scripts/lib/cli.mjs +3 -2
- package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +4 -3
- package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/SKILL.md +1 -1
- package/skills/catalyst-setup/references/what-each-check-means.md +5 -1
- package/skills/catalyst-setup/scripts/lib/cli.mjs +3 -2
- package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +1 -1
- package/skills/connect-me/scripts/lib/cli.mjs +3 -2
- package/skills/connect-me/scripts/lib/credential.mjs +29 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +1 -1
- package/skills/how-catalyst-works/references/what-runs-next.md +1 -1
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +4 -4
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +4 -3
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/run-this-project/references/making-work-ready.md +1 -1
- package/skills/run-this-project/references/stalls-and-escalation.md +2 -2
- package/skills/run-this-project/scripts/lib/cli.mjs +3 -2
- package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
- package/skills/unstick/SKILL.md +41 -0
- package/skills/unstick/agents/openai.yaml +6 -0
- package/skills/unstick/agents/portability.yaml +5 -0
- package/skills/unstick/references/playbook.md +51 -0
- package/skills/unstick/scripts/lib/cli.mjs +135 -0
- package/skills/unstick/scripts/lib/credential.mjs +29 -0
- package/skills/unstick/scripts/unstick.mjs +57 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +3 -2
- package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/SKILL.md +1 -1
- package/skills/whats-happening/references/why-is-it-stuck.md +9 -4
- package/skills/whats-happening/scripts/lib/cli.mjs +3 -2
- package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: spawn the catalyst-skills CLI.
|
|
3
|
+
// The CLI holds the SDK and the key; this file holds neither. It reads customer.json only
|
|
4
|
+
// to learn where the CLI lives, and it exits 2 with one line when the machine is not connected.
|
|
5
|
+
// Run any script beside this one with --help; this file is a library and is never run directly.
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
10
|
+
|
|
11
|
+
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
12
|
+
export const NOT_CONFIGURED_EXIT = 2;
|
|
13
|
+
|
|
14
|
+
export function configPath() {
|
|
15
|
+
const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/";
|
|
16
|
+
return join(home, ".config", "catalyst-cloud", "customer.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The connected-machine config, or exit 2 with the one line that says how to connect. */
|
|
20
|
+
export function loadConfig() {
|
|
21
|
+
const path = configPath();
|
|
22
|
+
if (!existsSync(path)) notConfigured(`no config at ${path}`);
|
|
23
|
+
try {
|
|
24
|
+
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
+
if (!hasCredential(cfg)) notConfigured(`config at ${path} has no key or login`);
|
|
26
|
+
return cfg;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err && err.exitCode === NOT_CONFIGURED_EXIT) throw err;
|
|
29
|
+
notConfigured(`config at ${path} is unreadable`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function notConfigured(why) {
|
|
34
|
+
process.stderr.write(`not connected (${why}) — run: ${CONNECT_COMMAND}\n`);
|
|
35
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run one catalyst-skills verb and return {code, stdout, stderr}. Uses the CLI path the login
|
|
40
|
+
* recorded when it still exists, else `npx <package>`. A CLI exit of 2 with a "not joined" or
|
|
41
|
+
* "not connected" line is turned into this script's own exit 2, so every caller sees one contract.
|
|
42
|
+
*/
|
|
43
|
+
export function runCli(args, opts = {}) {
|
|
44
|
+
const cfg = loadConfig();
|
|
45
|
+
const useRecorded = typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
|
|
46
|
+
const cmd = useRecorded ? process.execPath : "npx";
|
|
47
|
+
const argv = useRecorded ? [cfg.cliPath, ...args] : [PACKAGE, ...args];
|
|
48
|
+
const res = spawnSync(cmd, argv, {
|
|
49
|
+
input: opts.stdin,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
52
|
+
shell: !useRecorded && process.platform === "win32",
|
|
53
|
+
env: process.env,
|
|
54
|
+
});
|
|
55
|
+
if (res.error) {
|
|
56
|
+
process.stderr.write(`could not run ${cmd}: ${res.error.message}\n`);
|
|
57
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
58
|
+
}
|
|
59
|
+
const stdout = res.stdout ?? "";
|
|
60
|
+
const stderr = res.stderr ?? "";
|
|
61
|
+
if (res.status === NOT_CONFIGURED_EXIT && /not (joined|connected|configured)/i.test(stderr)) {
|
|
62
|
+
process.stderr.write(stderr);
|
|
63
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
64
|
+
}
|
|
65
|
+
return { code: res.status ?? 1, stdout, stderr };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Run a verb and require success; on failure print the CLI's own stderr and exit with its code (or 1). */
|
|
69
|
+
export function mustRun(args, opts = {}) {
|
|
70
|
+
const r = runCli(args, opts);
|
|
71
|
+
if (r.code !== 0) {
|
|
72
|
+
if (r.stderr) process.stderr.write(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`);
|
|
73
|
+
if (r.stdout && !opts.quiet) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`);
|
|
74
|
+
process.exit(r.code === 0 ? 1 : r.code);
|
|
75
|
+
}
|
|
76
|
+
return r;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Parse the CLI's --json stdout; a non-JSON answer is a failed check, exit 1. */
|
|
80
|
+
export function parseJson(stdout, what = "output") {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(stdout.trim());
|
|
83
|
+
} catch {
|
|
84
|
+
process.stderr.write(`could not parse ${what} as JSON: ${stdout.trim().slice(0, 200)}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Minimal flag parsing for the scripts: --name value, --name=value, --flag, repeated flags collect. */
|
|
90
|
+
export function parseFlags(argv, spec) {
|
|
91
|
+
const flags = {};
|
|
92
|
+
const positionals = [];
|
|
93
|
+
for (let i = 0; i < argv.length; i++) {
|
|
94
|
+
const a = argv[i];
|
|
95
|
+
if (a === "--help" || a === "-h") return { help: true, flags, positionals };
|
|
96
|
+
if (a.startsWith("--")) {
|
|
97
|
+
let name = a.slice(2);
|
|
98
|
+
let value;
|
|
99
|
+
const eq = name.indexOf("=");
|
|
100
|
+
if (eq !== -1) {
|
|
101
|
+
value = name.slice(eq + 1);
|
|
102
|
+
name = name.slice(0, eq);
|
|
103
|
+
}
|
|
104
|
+
const s = spec[name];
|
|
105
|
+
if (!s) {
|
|
106
|
+
process.stderr.write(`unknown option --${name} (try --help)\n`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
if (s.value) {
|
|
110
|
+
if (value === undefined) value = argv[++i];
|
|
111
|
+
if (value === undefined || value === "") {
|
|
112
|
+
process.stderr.write(`--${name} needs a value\n`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
if (s.repeat) (flags[name] ??= []).push(value);
|
|
116
|
+
else flags[name] = value;
|
|
117
|
+
} else flags[name] = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
positionals.push(a);
|
|
121
|
+
}
|
|
122
|
+
return { help: false, flags, positionals };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function printHelp(usage, spec, notes = []) {
|
|
126
|
+
const lines = [`Usage: ${usage}`, ""];
|
|
127
|
+
const names = Object.keys(spec);
|
|
128
|
+
if (names.length) {
|
|
129
|
+
lines.push("Options:");
|
|
130
|
+
for (const n of names) lines.push(` --${n}${spec[n].value ? " <value>" : ""} ${spec[n].help}`);
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|
|
133
|
+
lines.push(...notes, "", "Exit codes: 0 ok · 1 the check failed or the arguments were wrong · 2 this machine is not connected to a tenant");
|
|
134
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
135
|
+
}
|
|
@@ -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,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// unstick.mjs — "why is this stuck, and can it be released?" in one JSON document. Runs
|
|
3
|
+
// `catalyst-skills explain`, `explain --history` and a dry-run `release` for one ticket (or a dry-run
|
|
4
|
+
// class release for one team), and only with --because runs the real release. Reaches the cloud only
|
|
5
|
+
// by spawning the CLI.
|
|
6
|
+
import { mustRun, parseFlags, parseJson, printHelp, runCli } from "./lib/cli.mjs";
|
|
7
|
+
|
|
8
|
+
const SPEC = {
|
|
9
|
+
because: { value: true, help: "what changed since the ticket was held; runs the real release after the preview" },
|
|
10
|
+
"retry-unchanged": { value: false, help: "release even though nothing the cloud can see changed (say what did in --because)" },
|
|
11
|
+
class: { value: true, help: "release every ticket on --team parked under this failure class" },
|
|
12
|
+
team: { value: true, help: "with --class: the team key" },
|
|
13
|
+
limit: { value: true, help: "with --class: at most this many tickets (the cloud caps it at 25)" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
|
|
17
|
+
const ticket = positionals[0];
|
|
18
|
+
const byClass = typeof flags.class === "string";
|
|
19
|
+
if (help || (!ticket && !byClass) || (byClass && (ticket || typeof flags.team !== "string"))) {
|
|
20
|
+
printHelp("node scripts/unstick.mjs <ticket> [--because <text>] [--retry-unchanged] [--help] | --class <c> --team <K> [--because <text>] [--retry-unchanged] [--limit N] [--help]", SPEC, [
|
|
21
|
+
"Without --because it changes nothing: it prints the explanation, the history (every governor holding the ticket",
|
|
22
|
+
"and past releases) and a dry-run release. With --because it then runs the real release. Read references/playbook.md",
|
|
23
|
+
"before passing --because. A refused release exits 1 with the refusals in the document.",
|
|
24
|
+
]);
|
|
25
|
+
process.exit(help ? 0 : 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const releaseArgs = byClass ? ["release", "--class", flags.class, "--team", flags.team] : ["release", ticket];
|
|
29
|
+
if (flags["retry-unchanged"]) releaseArgs.push("--retry-unchanged");
|
|
30
|
+
if (byClass && typeof flags.limit === "string") releaseArgs.push("--limit", flags.limit);
|
|
31
|
+
|
|
32
|
+
const out = {};
|
|
33
|
+
if (!byClass) {
|
|
34
|
+
out.explain = parseJson(mustRun(["explain", ticket, "--json"]).stdout, "explain");
|
|
35
|
+
out.history = parseJson(mustRun(["explain", ticket, "--history", "--json"]).stdout, "history");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const preview = runCli([...releaseArgs, "--dry-run", "--json"]);
|
|
39
|
+
if (preview.code !== 0 && preview.code !== 1) {
|
|
40
|
+
process.stderr.write(preview.stderr);
|
|
41
|
+
process.exit(preview.code);
|
|
42
|
+
}
|
|
43
|
+
out.preview = parseJson(preview.stdout, "the dry-run release");
|
|
44
|
+
|
|
45
|
+
let exit = 0;
|
|
46
|
+
if (typeof flags.because === "string") {
|
|
47
|
+
const real = runCli([...releaseArgs, "--because", flags.because, "--json"]);
|
|
48
|
+
if (real.code !== 0 && real.code !== 1) {
|
|
49
|
+
process.stderr.write(real.stderr);
|
|
50
|
+
process.exit(real.code);
|
|
51
|
+
}
|
|
52
|
+
out.release = parseJson(real.stdout, "the release");
|
|
53
|
+
exit = real.code;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
57
|
+
process.exit(exit);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
9
10
|
|
|
10
11
|
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
11
12
|
export const NOT_CONFIGURED_EXIT = 2;
|
|
@@ -21,7 +22,7 @@ export function loadConfig() {
|
|
|
21
22
|
if (!existsSync(path)) notConfigured(`no config at ${path}`);
|
|
22
23
|
try {
|
|
23
24
|
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
-
if (
|
|
25
|
+
if (!hasCredential(cfg)) notConfigured(`config at ${path} has no key or login`);
|
|
25
26
|
return cfg;
|
|
26
27
|
} catch (err) {
|
|
27
28
|
if (err && err.exitCode === NOT_CONFIGURED_EXIT) throw err;
|
|
@@ -30,7 +31,7 @@ export function loadConfig() {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function notConfigured(why) {
|
|
33
|
-
process.stderr.write(`not connected (${why}) — run:
|
|
34
|
+
process.stderr.write(`not connected (${why}) — run: ${CONNECT_COMMAND}\n`);
|
|
34
35
|
process.exit(NOT_CONFIGURED_EXIT);
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -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
|
+
}
|
|
@@ -36,7 +36,7 @@ For depth beyond these, load the fact skills: `how-catalyst-works` (the ladder,
|
|
|
36
36
|
- **Their tenant, as them.** Every read goes through the CLI, which holds the person's own key, the tenant and who they are. You never name, guess at, or try another tenant, and you never paste the key anywhere. "You" in your reply means the connected person: their assigned tickets, their asks.
|
|
37
37
|
- **Tenant facts come from the contract, live.** Stage names, label names, team keys, thresholds and the ladder are in the snapshot's `tenant` block; read them there each time and never restate them from memory.
|
|
38
38
|
- **One reply, ticket ids on every line, source named.** The reply opens with when the snapshot was taken and whether the replica or the API answered. A stale replica is stated, never hidden.
|
|
39
|
-
- **Say what a key cannot see.** PR labels and reactions are not mirrored, and
|
|
39
|
+
- **Say what a key cannot see.** PR labels and reactions are not mirrored; the CLI prints the settings URL, and you repeat it instead of guessing. A park or hold is released from the person's own login once its cause is fixed: route that to the `unstick` skill (`catalyst-skills release`), never tell them it needs an operator. Coding-account status (`accounts`) and per-ticket execution history (`explain --history`) are readable — read them.
|
|
40
40
|
- **No polling.** One snapshot per question. Waiting on a change is the project owner's job (`run-this-project` subscribes to the tenant stream); the desk never loops a read.
|
|
41
41
|
- **You are not the owner.** You route work and make it visible; you do not dispatch, overrule a project owner, or run long work in this session.
|
|
42
42
|
- **Never answer as the human.** A decision is an ask through `what-needs-me`, filed before anyone proceeds on its default; the answer is recorded there as the app actor.
|
|
@@ -34,13 +34,18 @@ This reference restates an invariant: the vocabulary the cloud's eligibility exp
|
|
|
34
34
|
| `repo_paused` | an operator paused the repository | the operator resumes it |
|
|
35
35
|
| `scope_overlap` | its declared file scope intersects a ticket already in flight | wait for the other ticket, or the human decides which goes first |
|
|
36
36
|
|
|
37
|
-
## Reasons
|
|
37
|
+
## Reasons a release clears once the cause is fixed
|
|
38
|
+
|
|
39
|
+
These do not release themselves, and nobody but the person needs to act: once the recorded cause is fixed, the person's own login releases them with `catalyst-skills release <ticket>`. The desk does not release; route the ticket to the `unstick` skill, which reads the history, previews the release and runs it, or names what a person must do first.
|
|
38
40
|
|
|
39
41
|
| reason | what it means | note |
|
|
40
42
|
| -- | -- | -- |
|
|
41
|
-
| `cooling_down` (parked after repeated failures, or the round cap was spent) | the phase is parked and does not release itself |
|
|
43
|
+
| `phase_parked` / `cooling_down` (parked after repeated failures, or the round cap was spent) | the phase is parked and does not release itself | `unstick`; `explain --history` names the park and the failure class it recorded |
|
|
42
44
|
| `remediate_parked` | the repair phase itself is parked, so the failing phase has nowhere to be repaired | as above |
|
|
43
|
-
| `validate_class_spent` | this validate failure already spent its one repair round in this episode |
|
|
45
|
+
| `validate_class_spent` | this validate failure already spent its one repair round in this episode | a push or a comment saying what to change releases it on its own; otherwise `unstick` |
|
|
46
|
+
| `human_owned_pr` | a person's own pull request holds the ticket | that person closes or merges it; no release clears it |
|
|
47
|
+
| `review_not_converging` | review and repair kept finding new problems without converging | a person reads the findings and comments on the ticket to resume; raise an ask for that read |
|
|
48
|
+
| `round_threshold` | the ticket spent its lifetime repair budget | a person answers the ask the cloud raised, or pushes a fix |
|
|
44
49
|
| `no_branch_to_remediate` / `branch_missing` / `branch_gone` | a branch-dependent phase has no branch to clone | the cloud releases missing-branch parks on its own budget; a deleted branch needs a human to decide |
|
|
45
50
|
| `stale_failure_episode` | the ladder advanced after the recorded failure | informational; the round is dropped |
|
|
46
51
|
|
|
@@ -50,7 +55,7 @@ This reference restates an invariant: the vocabulary the cloud's eligibility exp
|
|
|
50
55
|
|
|
51
56
|
## When the cloud could not judge
|
|
52
57
|
|
|
53
|
-
A status of `unknown` with a reason such as `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` or `branch_snapshot_unknown` means the evaluator failed closed rather than guessing. Most clear on the cloud's next pass. If one persists for a team, the readiness checks (`catalyst-setup`) are the next read.
|
|
58
|
+
A status of `unknown` with a reason such as `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` or `branch_snapshot_unknown` means the evaluator failed closed rather than guessing. Most clear on the cloud's next pass. `workflow_mapping_unknown`, and `ordering_never_published` beside it, does not clear by itself when the team has no saved stage mapping: `explain` then names the missing stages, and a tenant owner or admin maps the team in Settings → Linear teams. If any other one persists for a team, the readiness checks (`catalyst-setup`) are the next read.
|
|
54
59
|
|
|
55
60
|
## The one advisory
|
|
56
61
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
9
10
|
|
|
10
11
|
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
11
12
|
export const NOT_CONFIGURED_EXIT = 2;
|
|
@@ -21,7 +22,7 @@ export function loadConfig() {
|
|
|
21
22
|
if (!existsSync(path)) notConfigured(`no config at ${path}`);
|
|
22
23
|
try {
|
|
23
24
|
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
-
if (
|
|
25
|
+
if (!hasCredential(cfg)) notConfigured(`config at ${path} has no key or login`);
|
|
25
26
|
return cfg;
|
|
26
27
|
} catch (err) {
|
|
27
28
|
if (err && err.exitCode === NOT_CONFIGURED_EXIT) throw err;
|
|
@@ -30,7 +31,7 @@ export function loadConfig() {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function notConfigured(why) {
|
|
33
|
-
process.stderr.write(`not connected (${why}) — run:
|
|
34
|
+
process.stderr.write(`not connected (${why}) — run: ${CONNECT_COMMAND}\n`);
|
|
34
35
|
process.exit(NOT_CONFIGURED_EXIT);
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -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
|
+
}
|