@catalyst-cloud/catalyst-skills 0.1.1 → 0.2.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 (111) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +109 -53
  3. package/dist/args.js +237 -0
  4. package/dist/ask.js +137 -0
  5. package/dist/cli.js +165 -279
  6. package/dist/config.js +106 -0
  7. package/dist/contract-types.js +4 -0
  8. package/dist/contract.js +178 -0
  9. package/dist/detach.js +10 -0
  10. package/dist/errors.js +27 -0
  11. package/dist/execution.js +156 -0
  12. package/dist/http.js +142 -0
  13. package/dist/prompt.js +35 -0
  14. package/dist/query.js +187 -0
  15. package/dist/ready.js +123 -0
  16. package/dist/replica.js +318 -0
  17. package/dist/sdk.js +26 -0
  18. package/dist/skill-shape.js +179 -0
  19. package/dist/skills.js +74 -0
  20. package/dist/ts-deps-loader.js +68 -0
  21. package/dist/watch/consumer.js +141 -0
  22. package/dist/watch/cursor-file.js +62 -0
  23. package/dist/watch.js +173 -0
  24. package/dist/write.js +224 -0
  25. package/package.json +9 -5
  26. package/skills/catalyst-github/SKILL.md +35 -0
  27. package/skills/catalyst-github/agents/openai.yaml +6 -0
  28. package/skills/catalyst-github/agents/portability.yaml +4 -0
  29. package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
  30. package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
  31. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
  32. package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
  33. package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
  34. package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
  35. package/skills/catalyst-linear/SKILL.md +43 -0
  36. package/skills/catalyst-linear/agents/openai.yaml +6 -0
  37. package/skills/catalyst-linear/agents/portability.yaml +5 -0
  38. package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
  39. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
  40. package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
  41. package/skills/catalyst-linear/scripts/comment.mjs +59 -0
  42. package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
  43. package/skills/catalyst-linear/scripts/label.mjs +48 -0
  44. package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
  45. package/skills/catalyst-linear/scripts/move.mjs +41 -0
  46. package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
  47. package/skills/catalyst-linear/scripts/search.mjs +49 -0
  48. package/skills/catalyst-setup/SKILL.md +32 -0
  49. package/skills/catalyst-setup/agents/openai.yaml +6 -0
  50. package/skills/catalyst-setup/agents/portability.yaml +4 -0
  51. package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
  52. package/skills/catalyst-setup/scripts/check.mjs +75 -0
  53. package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
  54. package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
  55. package/skills/connect-me/SKILL.md +55 -0
  56. package/skills/connect-me/agents/openai.yaml +6 -0
  57. package/skills/connect-me/agents/portability.yaml +5 -0
  58. package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
  59. package/skills/connect-me/scripts/lib/cli.mjs +184 -0
  60. package/skills/connect-me/scripts/verify-connection.mjs +68 -0
  61. package/skills/how-catalyst-works/SKILL.md +42 -0
  62. package/skills/how-catalyst-works/agents/openai.yaml +6 -0
  63. package/skills/how-catalyst-works/agents/portability.yaml +4 -0
  64. package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
  65. package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
  66. package/skills/how-catalyst-works/references/the-ladder.md +41 -0
  67. package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
  68. package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
  69. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
  70. package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
  71. package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
  72. package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
  73. package/skills/run-this-project/SKILL.md +45 -0
  74. package/skills/run-this-project/agents/openai.yaml +6 -0
  75. package/skills/run-this-project/agents/portability.yaml +5 -0
  76. package/skills/run-this-project/assets/stall-policy.json +15 -0
  77. package/skills/run-this-project/references/making-work-ready.md +60 -0
  78. package/skills/run-this-project/references/reacting-to-events.md +76 -0
  79. package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
  80. package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
  81. package/skills/run-this-project/scripts/make-ready.mjs +64 -0
  82. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  83. package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
  84. package/skills/what-needs-me/SKILL.md +41 -0
  85. package/skills/what-needs-me/agents/openai.yaml +6 -0
  86. package/skills/what-needs-me/agents/portability.yaml +5 -0
  87. package/skills/what-needs-me/references/raising-a-decision.md +41 -0
  88. package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
  89. package/skills/what-needs-me/references/settling-an-answer.md +37 -0
  90. package/skills/what-needs-me/scripts/inbox.mjs +47 -0
  91. package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
  92. package/skills/what-needs-me/scripts/raise.mjs +53 -0
  93. package/skills/what-needs-me/scripts/settle.mjs +73 -0
  94. package/skills/whats-happening/SKILL.md +43 -0
  95. package/skills/whats-happening/agents/openai.yaml +6 -0
  96. package/skills/whats-happening/agents/portability.yaml +4 -0
  97. package/skills/whats-happening/assets/status-reply.json +77 -0
  98. package/skills/whats-happening/references/reading-the-board.md +43 -0
  99. package/skills/whats-happening/references/reprioritising.md +37 -0
  100. package/skills/whats-happening/references/routing-work.md +36 -0
  101. package/skills/whats-happening/references/status-reply.md +34 -0
  102. package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
  103. package/skills/whats-happening/scripts/explain.mjs +28 -0
  104. package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
  105. package/skills/whats-happening/scripts/snapshot.mjs +145 -0
  106. package/skills/ask/SKILL.md +0 -37
  107. package/skills/concierge/SKILL.md +0 -41
  108. package/skills/join/SKILL.md +0 -30
  109. package/skills/linearis/SKILL.md +0 -40
  110. package/skills/setup/SKILL.md +0 -27
  111. package/skills/steward/SKILL.md +0 -30
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // comment.mjs — post a comment on a ticket as the tenant's app actor through the agent proxy.
3
+ // A bookkeeping comment (a machine record, not a turn in a conversation) takes --bookkeeping, which
4
+ // prefixes the marker the contract's vocabulary names.
5
+ import { readFileSync } from "node:fs";
6
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
7
+
8
+ const HELP = `Usage: node scripts/comment.mjs <ticket> (--body <text> | --stdin) [--parent <commentId>] [--bookkeeping] [--as-user] [--json]
9
+
10
+ Posts one comment on a ticket. Wraps: catalyst-skills write comment. Spends one unit of the tenant's
11
+ daily write budget.
12
+
13
+ <ticket> the Linear identifier, e.g. KEY-123
14
+ --body <text> the comment body
15
+ --stdin read the body from stdin instead
16
+ --parent <commentId> reply under this comment (reply where the message arrived)
17
+ --bookkeeping this is a machine record: prefix the contract's bookkeeping marker so it
18
+ never reads as a human turn
19
+ --as-user post with the personal identity instead of the app actor (rare; then a
20
+ record needs --bookkeeping to avoid waking an agent)
21
+ --json print the CLI's JSON result
22
+
23
+ Exit 0 posted, 1 on a usage error, 2 when this machine is not connected to a tenant or the write
24
+ was refused (the daily budget is spent, the ticket is unknown, the route is missing; the line
25
+ says which).`;
26
+
27
+ const argv = process.argv.slice(2);
28
+ if (wantsHelp(argv)) {
29
+ console.log(HELP);
30
+ process.exit(0);
31
+ }
32
+ const { flags, positionals } = parseFlags(argv, {
33
+ bool: ["stdin", "bookkeeping", "as-user", "json"],
34
+ value: ["body", "parent"],
35
+ });
36
+ const ticket = positionals[0];
37
+ if (!ticket) usage("comment needs a ticket identifier (see --help)");
38
+ if (!flags.body && !flags.stdin) usage("comment needs --body <text> or --stdin (see --help)");
39
+ if (flags.body && flags.stdin) usage("give --body or --stdin, not both");
40
+
41
+ let stdin;
42
+ const args = ["write", "comment", ticket, "--json"];
43
+ if (flags.stdin) {
44
+ stdin = readFileSync(0, "utf8");
45
+ if (!stdin.trim()) usage("stdin was empty");
46
+ args.push("--stdin");
47
+ } else {
48
+ args.push("--body", flags.body);
49
+ }
50
+ if (flags.parent) args.push("--parent", flags.parent);
51
+ if (flags.bookkeeping) args.push("--bookkeeping");
52
+ if (flags["as-user"]) args.push("--as-user");
53
+
54
+ const r = runCli(args, { stdin });
55
+ exitOnFailure(r);
56
+ relayStderr(r);
57
+ const result = parseJson(r.stdout);
58
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
59
+ else console.log(`comment posted on ${ticket}${result && result.id ? ` (${result.id})` : ""}`);
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // create-ticket.mjs — file a new ticket on a team as the app actor. The team is named by its key;
3
+ // the CLI resolves the team id from the contract. A decision for a human is NOT a ticket filed here:
4
+ // that is an ask, raised through the what-needs-me skill so it carries options, a default and blocks.
5
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
6
+
7
+ const HELP = `Usage: node scripts/create-ticket.mjs --team <key> --title <text> [--label <name|id>]... [--priority <0-4>] [--as-user] [--json]
8
+
9
+ Creates one ticket. Wraps: catalyst-skills write create. Spends one unit of the daily write budget.
10
+
11
+ --team <key> the team key (the prefix of its ticket identifiers)
12
+ --title <text> the ticket title
13
+ --label <name|id> a label to apply (repeatable); Catalyst label names resolve via the contract
14
+ --priority <0-4> Linear priority: 0 none, 1 urgent, 2 high, 3 medium, 4 low
15
+ --as-user create with the personal identity instead of the app actor (rare)
16
+ --json print the CLI's JSON result (carries the new identifier)
17
+
18
+ Never cite the new ticket's identifier until this script has printed it. Do not use this to ask a
19
+ human a question: raise an ask instead (what-needs-me), or the question will look like work.
20
+
21
+ Exit 0 created, 1 on a usage error, 2 when this machine is not connected to a tenant or the write
22
+ was refused (an unknown team key, the daily budget spent; the line says which).`;
23
+
24
+ const argv = process.argv.slice(2);
25
+ if (wantsHelp(argv)) {
26
+ console.log(HELP);
27
+ process.exit(0);
28
+ }
29
+ const { flags } = parseFlags(argv, { bool: ["as-user", "json"], value: ["team", "title", "priority"], repeat: ["label"] });
30
+ if (!flags.team || !flags.title) usage("create-ticket needs --team <key> and --title <text> (see --help)");
31
+
32
+ const args = ["write", "create", "--team", flags.team, "--title", flags.title, "--json"];
33
+ for (const l of flags.label ?? []) args.push("--label", l);
34
+ if (flags.priority !== undefined) args.push("--priority", flags.priority);
35
+ if (flags["as-user"]) args.push("--as-user");
36
+ const r = runCli(args);
37
+ exitOnFailure(r);
38
+ relayStderr(r);
39
+ const result = parseJson(r.stdout);
40
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
41
+ else {
42
+ const ident = result && (result.identifier ?? result.id);
43
+ console.log(`created${ident ? ` ${ident}` : ""} on team ${flags.team}: ${flags.title}`);
44
+ }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // label.mjs — add or remove labels on a ticket. A Catalyst label name (ask, hold, release) resolves
3
+ // to the id the contract lists for this team; anything else is passed through as a label id.
4
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/label.mjs <ticket> [--add <name|id>]... [--remove <name|id>]... [--json]
7
+
8
+ Adds and/or removes labels. Wraps: catalyst-skills write label. One write-budget unit per call
9
+ direction (add, remove), whatever the label count.
10
+
11
+ <ticket> the Linear identifier, e.g. KEY-123
12
+ --add <name|id> a label to add (repeatable)
13
+ --remove <name|id> a label to remove (repeatable)
14
+ --json print the CLI's JSON result
15
+
16
+ Names the contract knows (the ask marker, the hold label, the release label) resolve to this team's
17
+ label id; a name the workspace lacks is refused rather than guessed. Any other value is sent as a
18
+ label id unchanged.
19
+
20
+ Exit 0 written, 1 on a usage error, 2 when this machine is not connected to a tenant or the write
21
+ was refused (a Catalyst label the workspace lacks, the daily budget spent; the line says which).`;
22
+
23
+ const argv = process.argv.slice(2);
24
+ if (wantsHelp(argv)) {
25
+ console.log(HELP);
26
+ process.exit(0);
27
+ }
28
+ const { flags, positionals } = parseFlags(argv, { bool: ["json"], repeat: ["add", "remove"] });
29
+ const ticket = positionals[0];
30
+ if (!ticket) usage("label needs a ticket identifier (see --help)");
31
+ const add = flags.add ?? [];
32
+ const remove = flags.remove ?? [];
33
+ if (add.length === 0 && remove.length === 0) usage("label needs --add and/or --remove (see --help)");
34
+
35
+ const args = ["write", "label", ticket, "--json"];
36
+ for (const a of add) args.push("--add", a);
37
+ for (const x of remove) args.push("--remove", x);
38
+ const r = runCli(args);
39
+ exitOnFailure(r);
40
+ relayStderr(r);
41
+ const result = parseJson(r.stdout);
42
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
43
+ else {
44
+ const bits = [];
45
+ if (add.length) bits.push(`added ${add.join(", ")}`);
46
+ if (remove.length) bits.push(`removed ${remove.join(", ")}`);
47
+ console.log(`${ticket}: ${bits.join("; ")}`);
48
+ }
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ // lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: it spawns the catalyst-skills CLI
3
+ // this machine connected with (the path recorded in customer.json, else npx) and hands back its
4
+ // output. Scripts import it; a person runs it with --help to see what it does. No dependencies.
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+
10
+ export const PACKAGE = "@catalyst-cloud/catalyst-skills";
11
+ export const CONNECT_HINT = `CATALYST_CLOUD_TOKEN=<account key> npx ${PACKAGE} login`;
12
+
13
+ /** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME (used by tests) over HOME. */
14
+ export function configPath() {
15
+ const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
16
+ return join(home, ".config", "catalyst-cloud", "customer.json");
17
+ }
18
+
19
+ /** The config, or a one-line reason it could not be read. Never throws. */
20
+ export function loadConfig() {
21
+ const path = configPath();
22
+ if (!existsSync(path)) return { ok: false, reason: `no config at ${path}` };
23
+ try {
24
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
25
+ if (!cfg || typeof cfg.key !== "string" || typeof cfg.account !== "string") {
26
+ return { ok: false, reason: `${path} is missing the key or account` };
27
+ }
28
+ return { ok: true, cfg, path };
29
+ } catch (err) {
30
+ return { ok: false, reason: `${path} is unreadable: ${err instanceof Error ? err.message : String(err)}` };
31
+ }
32
+ }
33
+
34
+ /** Exit 2 with the one line every script prints when this machine is not connected to a tenant. */
35
+ export function notConfigured(reason) {
36
+ console.error(`not connected to a Catalyst Cloud tenant (${reason}) — run: ${CONNECT_HINT}`);
37
+ process.exit(2);
38
+ }
39
+
40
+ /**
41
+ * Run one catalyst-skills verb. Returns { code, stdout, stderr }. The CLI is `node <cliPath>` when
42
+ * the config recorded one that still exists, else `npx @catalyst-cloud/catalyst-skills`.
43
+ * Exits 2 (not configured) before spawning anything when the config is absent or unreadable.
44
+ */
45
+ export function runCli(args, { stdin } = {}) {
46
+ const loaded = loadConfig();
47
+ if (!loaded.ok) notConfigured(loaded.reason);
48
+ const { cfg } = loaded;
49
+ let cmd;
50
+ let argv;
51
+ let shell = false;
52
+ if (typeof cfg.cliPath === "string" && existsSync(cfg.cliPath)) {
53
+ cmd = process.execPath;
54
+ argv = [cfg.cliPath, ...args];
55
+ } else {
56
+ cmd = process.platform === "win32" ? "npx.cmd" : "npx";
57
+ argv = [PACKAGE, ...args];
58
+ shell = process.platform === "win32";
59
+ }
60
+ const r = spawnSync(cmd, argv, {
61
+ encoding: "utf8",
62
+ input: stdin,
63
+ env: process.env,
64
+ shell,
65
+ maxBuffer: 64 * 1024 * 1024,
66
+ });
67
+ if (r.error) {
68
+ console.error(`could not run ${cmd}: ${r.error.message} — re-run ${CONNECT_HINT} to record the CLI path`);
69
+ process.exit(2);
70
+ }
71
+ return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
72
+ }
73
+
74
+ /** Print the CLI's stderr (the source line, the contract line, any refusal) on our stderr. */
75
+ export function relayStderr(r) {
76
+ const text = r.stderr.trimEnd();
77
+ if (text) console.error(text);
78
+ }
79
+
80
+ /** A non-zero CLI result ends the script with the same code, its stdout shown so nothing is lost. */
81
+ export function exitOnFailure(r) {
82
+ if (r.code === 0) return;
83
+ relayStderr(r);
84
+ const out = r.stdout.trimEnd();
85
+ if (out) console.log(out);
86
+ process.exit(r.code);
87
+ }
88
+
89
+ export function parseJson(text) {
90
+ try {
91
+ return JSON.parse(text.trim());
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * A small flag parser: `spec.value` names flags that take a value, `spec.bool` boolean flags,
99
+ * `spec.repeat` value flags that may repeat (collected into arrays). Unknown flags are a usage error.
100
+ */
101
+ export function parseFlags(argv, spec = {}) {
102
+ const value = new Set(spec.value ?? []);
103
+ const bool = new Set(spec.bool ?? []);
104
+ const repeat = new Set(spec.repeat ?? []);
105
+ const flags = {};
106
+ const positionals = [];
107
+ for (let i = 0; i < argv.length; i++) {
108
+ const a = argv[i];
109
+ if (a === "--") {
110
+ positionals.push(...argv.slice(i + 1));
111
+ break;
112
+ }
113
+ if (!a.startsWith("--")) {
114
+ positionals.push(a);
115
+ continue;
116
+ }
117
+ let name = a.slice(2);
118
+ let inline;
119
+ const eq = name.indexOf("=");
120
+ if (eq !== -1) {
121
+ inline = name.slice(eq + 1);
122
+ name = name.slice(0, eq);
123
+ }
124
+ if (bool.has(name)) {
125
+ flags[name] = true;
126
+ } else if (value.has(name) || repeat.has(name)) {
127
+ const v = inline ?? argv[++i];
128
+ if (v === undefined || v === "") usage(`--${name} needs a value`);
129
+ if (repeat.has(name)) (flags[name] ??= []).push(v);
130
+ else flags[name] = v;
131
+ } else {
132
+ usage(`unknown option --${name}`);
133
+ }
134
+ }
135
+ return { flags, positionals };
136
+ }
137
+
138
+ export function usage(message) {
139
+ console.error(message);
140
+ process.exit(1);
141
+ }
142
+
143
+ export function wantsHelp(argv) {
144
+ return argv.includes("--help") || argv.includes("-h");
145
+ }
146
+
147
+ const runDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
148
+ if (runDirectly) {
149
+ if (wantsHelp(process.argv.slice(2)) || process.argv.length <= 2) {
150
+ console.log(
151
+ [
152
+ "lib/cli.mjs — shared helper for this skill's scripts (not a command of its own)",
153
+ "",
154
+ "Reads ~/.config/catalyst-cloud/customer.json and runs the catalyst-skills CLI recorded there",
155
+ `(or npx ${PACKAGE} when no path is recorded). Exit 2 when the machine is not connected.`,
156
+ "",
157
+ `Connect first with: ${CONNECT_HINT}`,
158
+ `Config in use: ${configPath()}`,
159
+ ].join("\n"),
160
+ );
161
+ process.exit(0);
162
+ }
163
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // move.mjs — move a card by SLOT, never by stage name: the CLI resolves the slot to this team's
3
+ // live state id from the contract and refuses a slot that is unmapped or whose state is gone.
4
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/move.mjs <ticket> (--slot <slot> | --state-type <type>) [--json]
7
+
8
+ Moves a ticket's card. Wraps: catalyst-skills write state. Spends one unit of the daily write budget.
9
+
10
+ <ticket> the Linear identifier, e.g. KEY-123
11
+ --slot <slot> one of the eleven slots: dispatch, intake, research, plan, implement,
12
+ remediate, verify, review, pr, done, canceled. Moving to dispatch is how
13
+ work is dispatched.
14
+ --state-type <type> the team's first state of this Linear type, resolved from its live
15
+ workflow states; use "backlog" to park a card (Backlog is not a slot)
16
+ --json print the CLI's JSON result
17
+
18
+ Exit 0 moved, 1 on a usage error, 2 when this machine is not connected to a tenant or the move
19
+ was refused (the slot is unmapped for the team, its state no longer exists in Linear, the daily
20
+ budget is spent; the line says which).`;
21
+
22
+ const argv = process.argv.slice(2);
23
+ if (wantsHelp(argv)) {
24
+ console.log(HELP);
25
+ process.exit(0);
26
+ }
27
+ const { flags, positionals } = parseFlags(argv, { bool: ["json"], value: ["slot", "state-type"] });
28
+ const ticket = positionals[0];
29
+ if (!ticket) usage("move needs a ticket identifier (see --help)");
30
+ if (!flags.slot && !flags["state-type"]) usage("move needs --slot <slot> or --state-type <type> (see --help)");
31
+ if (flags.slot && flags["state-type"]) usage("give --slot or --state-type, not both");
32
+
33
+ const args = ["write", "state", ticket, "--json"];
34
+ if (flags.slot) args.push("--slot", flags.slot);
35
+ else args.push("--state-type", flags["state-type"]);
36
+ const r = runCli(args);
37
+ exitOnFailure(r);
38
+ relayStderr(r);
39
+ const result = parseJson(r.stdout);
40
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
41
+ else console.log(`${ticket} moved to ${flags.slot ? `slot ${flags.slot}` : `the team's ${flags["state-type"]} state`}`);
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // read-ticket.mjs — one ticket with its comments, relations, labels, linked pull requests and agent
3
+ // sessions inline, from the replica when it is fresh, else the origin-fresh API. The source line the
4
+ // CLI prints on stderr is the freshness verdict; it is always shown.
5
+ import { parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
6
+
7
+ const HELP = `Usage: node scripts/read-ticket.mjs <ticket> [--comments] [--source replica|api] [--json]
8
+
9
+ Reads one ticket record. Wraps: catalyst-skills query issue.
10
+
11
+ <ticket> the Linear identifier, e.g. KEY-123
12
+ --comments also print every comment (id, author, time, body)
13
+ --source replica|api force one source; default is the replica when fresh, else the API
14
+ --json print the full record as JSON
15
+
16
+ The first stderr line names the source ("source: replica (cursor N)" or "source: api (replica
17
+ stale|absent|not configured)"); quote it when the freshness of the answer matters.
18
+
19
+ Exit 0 found, 1 not found or a usage error, 2 when this machine is not connected to a tenant or the
20
+ cloud refused the read (the line says which).`;
21
+
22
+ const argv = process.argv.slice(2);
23
+ if (wantsHelp(argv)) {
24
+ console.log(HELP);
25
+ process.exit(0);
26
+ }
27
+ const { flags, positionals } = parseFlags(argv, { bool: ["comments", "json"], value: ["source"] });
28
+ const ticket = positionals[0];
29
+ if (!ticket) usage("read-ticket needs a ticket identifier (see --help)");
30
+
31
+ const args = ["query", "issue", ticket, "--json"];
32
+ if (flags.source) args.push("--source", flags.source);
33
+ const r = runCli(args);
34
+ relayStderr(r);
35
+ if (r.code !== 0) {
36
+ const out = r.stdout.trimEnd();
37
+ if (out) console.log(out);
38
+ process.exit(r.code);
39
+ }
40
+ const row = parseJson(r.stdout);
41
+ if (!row || typeof row !== "object") {
42
+ console.log(r.stdout.trimEnd());
43
+ process.exit(1);
44
+ }
45
+ if (flags.json) {
46
+ console.log(JSON.stringify(row));
47
+ process.exit(0);
48
+ }
49
+
50
+ const line = (label, value) => {
51
+ if (value === undefined || value === null || value === "") return;
52
+ console.log(`${label}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
53
+ };
54
+ const names = (list, key) => (Array.isArray(list) ? list.map((x) => (x && typeof x === "object" ? (x[key] ?? JSON.stringify(x)) : String(x))) : []);
55
+
56
+ line("ticket", row.identifier ?? row.id);
57
+ line("title", row.title);
58
+ line("state", row.state);
59
+ line("priority", row.priority_label ?? row.priority);
60
+ line("assignee", row.assignee_name ?? row.assignee);
61
+ line("delegate", row.delegate_name ?? row.delegate);
62
+ line("team", row.team_key ?? row.team_name ?? row.team_id);
63
+ line("project", row.project_name ?? row.project_id);
64
+ line("cycle", row.cycle_name ?? row.cycle_id);
65
+ line("parent", row.parent_identifier);
66
+ line("labels", names(row.labels, "name").join(", "));
67
+ line("url", row.url);
68
+ line("updated", row.updated_at);
69
+ if (Array.isArray(row.relations) && row.relations.length) {
70
+ console.log(`relations (${row.relations.length}):`);
71
+ for (const rel of row.relations) console.log(` ${rel.type ?? "?"}: ${rel.issue_identifier ?? ""} -> ${rel.related_identifier ?? JSON.stringify(rel)}`);
72
+ }
73
+ if (Array.isArray(row.linked_pulls) && row.linked_pulls.length) {
74
+ console.log(`linked pull requests (${row.linked_pulls.length}):`);
75
+ for (const pr of row.linked_pulls) console.log(` #${pr.number ?? "?"} ${pr.repo_id ?? ""} [${pr.node_id ?? ""}]`);
76
+ }
77
+ if (Array.isArray(row.agent_sessions)) line("agent sessions", row.agent_sessions.length);
78
+ if (Array.isArray(row.activity)) line("activity entries", row.activity.length);
79
+ if (Array.isArray(row.comments)) line("comments", row.comments.length);
80
+ if (row.description) {
81
+ console.log("description:");
82
+ console.log(String(row.description).trimEnd());
83
+ }
84
+ if (flags.comments && Array.isArray(row.comments)) {
85
+ console.log(`== comments (${row.comments.length})`);
86
+ for (const c of row.comments) {
87
+ const who = c.author_name ?? c.author_id ?? "?";
88
+ const bot = c.is_bot ? " [bot]" : "";
89
+ const parent = c.parent_id ? ` reply-to ${c.parent_id}` : "";
90
+ console.log(`-- ${c.id} · ${who}${bot} · ${c.updated_at ?? c.created_at ?? ""}${parent}`);
91
+ console.log(String(c.body ?? "").trimEnd());
92
+ }
93
+ }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // search.mjs — tenant search across ticket identifiers and titles, pull-request titles, project and
3
+ // initiative names. Always the origin-fresh API (the replica holds no search view).
4
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/search.mjs <terms...> [--limit <n>] [--json]
7
+
8
+ Searches your tenant for tickets, pull requests, projects and initiatives matching the terms.
9
+ Wraps: catalyst-skills query search.
10
+
11
+ <terms> one or more words; matched against identifiers, titles and names
12
+ --limit <n> max rows (default 50)
13
+ --json print the rows as JSON
14
+
15
+ Exit 0 (an empty result is still 0 and prints "no matches"), 1 on a usage error, 2 when this
16
+ machine is not connected to a tenant or the cloud refused the search (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"], value: ["limit"] });
24
+ if (positionals.length === 0) usage("search needs at least one term (see --help)");
25
+
26
+ const args = ["query", "search", ...positionals, "--json"];
27
+ if (flags.limit) args.push("--limit", flags.limit);
28
+ const r = runCli(args);
29
+ exitOnFailure(r);
30
+ relayStderr(r);
31
+ const rows = parseJson(r.stdout);
32
+ if (flags.json) {
33
+ console.log(JSON.stringify(rows ?? r.stdout.trimEnd()));
34
+ process.exit(0);
35
+ }
36
+ const list = Array.isArray(rows) ? rows : rows && typeof rows === "object" ? Object.values(rows).flat() : [];
37
+ if (list.length === 0) {
38
+ console.log("no matches");
39
+ process.exit(0);
40
+ }
41
+ for (const row of list) {
42
+ if (!row || typeof row !== "object") {
43
+ console.log(String(row));
44
+ continue;
45
+ }
46
+ const kind = row.kind ?? (row.identifier ? "issue" : row.number ? "pull" : "row");
47
+ const id = row.identifier ?? (row.number !== undefined ? `#${row.number}` : row.id ?? "");
48
+ console.log(`${kind} ${id} ${row.title ?? row.name ?? ""}${row.state ? ` (${row.state})` : ""}`);
49
+ }
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: catalyst-setup
3
+ description:
4
+ Am I set up? Machine readiness (Node, the tenant connection, the cached contract, the CLI path, the skills, the SDK, the optional replica) plus tenant readiness from the contract's ten per-team checks, in one verdict: what passes, what is blocked, what is merely waiting, and who can click what. Use when someone asks "am I set up", "what is missing", "why does nothing happen", "is the replica running", or right after connecting a new machine. Reports; never repairs.
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
+ # Am I set up?
10
+
11
+ You answer one question with one verdict and one list. The verdict is READY or NOT READY. The list is who can click what: the fixes only the person at the keyboard can make, the fixes only a tenant owner or admin can make in settings, and the checks that are simply waiting for the first event. The live check ids, severities, states and the people who can answer come from the contract; the scripts print them, you never restate them.
12
+
13
+ ## Run first
14
+
15
+ - `node scripts/check.mjs --help` — every machine and tenant check, one line each, the fix and who for each failure, the verdict, then the who-can-click-what list. Exit 1 when NOT READY.
16
+ - `node scripts/replica-status.mjs --help` — the optional replica's verdict with no network (add `--probe` for how far behind it is). Exit 0 fresh, 1 stale, 2 not connected, 3 absent.
17
+
18
+ ## Load on demand
19
+
20
+ | when | read |
21
+ | -- | -- |
22
+ | any check is red, unknown or waiting and the person asks what it proves, how to fix it, or who can | `references/what-each-check-means.md` |
23
+
24
+ ## Rules
25
+
26
+ - Report, never repair. No tenant-reachable repair verb exists for an account key yet; the settings page is where a tenant owner or admin fixes a mapping, a connection or a label, and you say which one.
27
+ - End every answer with the verdict and the who-can-click-what list, in that order.
28
+ - A stale or absent replica is a note, never a failure: every skill reads the API meanwhile and says so. Do not tell the person to start it unless they want local SQL or cheaper repeated reads.
29
+ - Not connected (exit 2) means the connect step, not a retry: `CATALYST_CLOUD_TOKEN=<account key> npx @catalyst-cloud/catalyst-skills login`, with the key from their tenant admin. Never guess a tenant; the key is the only selector.
30
+ - Waiting is not failing. "No write observed", "no delivery observed" and "no host connected" clear themselves the first time the thing happens; say that instead of raising them.
31
+ - Unknown is not a pass. A check the engine could not run is reported as such, never rounded up.
32
+ - Never run a check in a loop. If the person wants to know when a waiting check clears, that is the project-running skill's watch.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "Am I set up?"
3
+ short_description: "Machine and tenant readiness in one verdict: what passes, what is blocked, and who can click what"
4
+ default_prompt: "Use $catalyst-setup to tell me whether this machine and my tenant are ready, and what is missing."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,4 @@
1
+ identity: { pack: catalyst-cloud-skills, skill: catalyst-setup }
2
+ effects: []
3
+ invocation: implicit
4
+ exposure: [catalog]
@@ -0,0 +1,61 @@
1
+ # What each check means
2
+
3
+ This page restates invariants: the ten per-team readiness checks the cloud runs, what each proves, its fix and who can click it; the machine checks the CLI adds; and the four replica verdicts. The live values are never restated: each check's severity and whether it needs a human's answer come from the contract's `readinessChecks[]`, each team's current states from `teams[].readiness`, and the people who can answer from `humans[]`. `node scripts/check.mjs` prints all of it; this page is how to read what it printed.
4
+
5
+ ## How a team's readiness is scored
6
+
7
+ A team's status is one of `ready`, `degraded`, `blocked` or `unchecked`. The engine always reports all ten checks; a check it could not run is `unknown`, never `pass`. Any failing check the contract marks as blocking makes the team blocked; any unknown, and any failing check marked degrading, makes it degraded. Unchecked means no readiness pass has run for that team yet, which is a note, not a failure. Readiness is stamped with the account-wide mapping revision it was computed against, so a stale verdict is visible as such.
8
+
9
+ ## The ten checks
10
+
11
+ | check id | proves | when it fails, the fix | who clicks |
12
+ | -- | -- | -- | -- |
13
+ | `oauth_scope` | Catalyst holds the Linear permissions it needs | re-authorise the Linear connection to grant the missing scope | a tenant owner or admin, in settings |
14
+ | `token_live` | the Linear connection is accepted right now | reconnect Linear (expired or revoked), or wait and re-check (Linear unreachable). A distinct reason says Linear was never connected at all | owner or admin |
15
+ | `team_visible` | Catalyst can see this team | most often the team was made private: grant Catalyst access in Linear's team settings, then re-check | owner or admin, in Linear |
16
+ | `mapped_states_exist` | every stage Catalyst mapped still exists in Linear | re-map the team; nothing in the workspace was changed. A pending-write reason means a mapping was just saved and the read has not caught up: wait, do not re-map | owner or admin, in settings |
17
+ | `mapping_total` | every stage Catalyst moves tickets into is mapped | map the missing stages. "Absent" means the team was never mapped; "incomplete" means a few of the load-bearing stages are missing | owner or admin, in settings |
18
+ | `types_compatible` | each load-bearing mapped stage is the right kind (dispatch and intake unstarted or backlog, PR started, done completed, canceled canceled) | change the mapping to a stage of the right kind | owner or admin, in settings |
19
+ | `labels_present` | the labels Catalyst uses exist in the workspace | none needed by a person: Catalyst creates them the first time it uses them | nobody |
20
+ | `writes_land` | Catalyst has written to this team successfully | "no write observed" is waiting, not failing: it clears the first time Catalyst moves a ticket. "Write refused" means Linear rejected the last write: check the connection and the team's permissions | owner or admin when refused; otherwise nobody |
21
+ | `webhook_covers_team` | events for this team are arriving | confirmed once a repository is registered and events flow; "no delivery observed" is waiting | owner or admin, by registering the repository |
22
+ | `hosts_current` | no connected host runs an older mapping revision | a host that is behind re-loads the mapping on its next connect; a host that did not report its revision is flagged rather than assumed current; "no host connected" is waiting | whoever runs that host |
23
+
24
+ Three checks are informational by design (labels, event delivery, host currency): they degrade a team but never block it. Which are which is served on `readinessChecks[].severity`; do not memorise the split.
25
+
26
+ ## Reasons that look like failures and are not
27
+
28
+ - `no_write_observed`, `no_delivery_observed`, `no_host_connected`: nothing has happened yet. Expected on a fresh tenant; they clear on their own.
29
+ - `stages_pending_write`: a mapping was saved and the read predates it. Re-check shortly; re-mapping would be a second write for no reason.
30
+ - `unknown` on any check: the engine could not look. It is not a pass and not a fail; say so.
31
+
32
+ ## The machine checks the CLI adds
33
+
34
+ `catalyst-skills ready` prepends checks about this machine before the tenant's:
35
+
36
+ | id | proves | fix |
37
+ | -- | -- | -- |
38
+ | `node` | Node 22 or newer, which the SDK's built-in SQLite engine needs | install Node 22+ |
39
+ | `config` | this machine is connected: `customer.json` exists and loads | `CATALYST_CLOUD_TOKEN=<account key> npx @catalyst-cloud/catalyst-skills login`; the key comes from the tenant admin |
40
+ | `contract` | the tenant contract is cached and its major version is one this bundle accepts | `catalyst-skills contract --refresh`; a version outside the range means update the bundle. A refresh needs an account key, not a workstation key |
41
+ | `cliPath` | the CLI path recorded at login still exists, so skill scripts can spawn it | re-run login |
42
+ | `skills` | every skill of this bundle is present where the CLI installed them | `catalyst-skills install` |
43
+ | `sdk` | the SDK loads, so the replica and the watch are available | run under Node 22.15 or newer; every read still works through the API meanwhile |
44
+ | `replica` | the optional replica is fresh | a note, never a failure; see below |
45
+
46
+ ## The replica's four verdicts
47
+
48
+ `node scripts/replica-status.mjs` needs no network and exits with the verdict:
49
+
50
+ | exit | verdict | what a skill does with it |
51
+ | -- | -- | -- |
52
+ | 0 | fresh: a live writer, a heartbeat younger than the staleness threshold, a non-empty cursor | reads the replica and says so |
53
+ | 1 | stale: the file exists but the writer is gone, the heartbeat is old, or there is no cursor | reads the API and says so; `catalyst-skills replica start --detach` brings it back |
54
+ | 2 | not connected to a tenant | the connect step first |
55
+ | 3 | absent: no replica file at all | reads the API; the replica is optional and one command away |
56
+
57
+ `--probe` adds the one network call: it compares the local cursor with the cloud's head and prints how far behind the replica is, the only honest "how stale" number. A skill must never refuse to work because the replica is down, and must never silently read a stale one; both halves are one exit code away.
58
+
59
+ ## Who can click what
60
+
61
+ The last block `check.mjs` prints groups every failure by the person it needs. Machine fixes name "you", the person at the keyboard. Tenant fixes name the owner or admin roles the contract lists (as Linear user ids, since that is how the cloud knows them), because the settings page that repairs a mapping, a connection or a label is theirs. A check the contract marks as not needing an answer names nobody: it is informational or self-clearing. This skill reports; it never repairs, because no tenant-reachable repair verb exists for an account key yet.