@catalyst-cloud/catalyst-skills 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +11 -178
  2. package/bin/catalyst-skills.js +3 -49
  3. package/bin/catalyst.js +3 -0
  4. package/package.json +8 -29
  5. package/CHANGELOG.md +0 -41
  6. package/LICENSE +0 -21
  7. package/dist/args.js +0 -261
  8. package/dist/ask.js +0 -158
  9. package/dist/browser.js +0 -20
  10. package/dist/cli.js +0 -356
  11. package/dist/config.js +0 -148
  12. package/dist/contract-types.js +0 -4
  13. package/dist/contract.js +0 -178
  14. package/dist/detach.js +0 -10
  15. package/dist/errors.js +0 -27
  16. package/dist/events.js +0 -106
  17. package/dist/execution.js +0 -369
  18. package/dist/oauth.js +0 -300
  19. package/dist/prompt.js +0 -35
  20. package/dist/query.js +0 -225
  21. package/dist/ready.js +0 -163
  22. package/dist/release.js +0 -142
  23. package/dist/replica.js +0 -325
  24. package/dist/sdk.js +0 -26
  25. package/dist/skill-shape.js +0 -179
  26. package/dist/skills.js +0 -78
  27. package/dist/transport.js +0 -205
  28. package/dist/ts-deps-loader.js +0 -68
  29. package/dist/watch/consumer.js +0 -141
  30. package/dist/watch/cursor-file.js +0 -62
  31. package/dist/watch.js +0 -175
  32. package/dist/write.js +0 -224
  33. package/skills/catalyst-github/SKILL.md +0 -35
  34. package/skills/catalyst-github/agents/openai.yaml +0 -6
  35. package/skills/catalyst-github/agents/portability.yaml +0 -4
  36. package/skills/catalyst-github/references/is-it-mergeable.md +0 -57
  37. package/skills/catalyst-github/references/what-a-pr-accumulates.md +0 -61
  38. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +0 -124
  39. package/skills/catalyst-github/scripts/lib/cli.mjs +0 -103
  40. package/skills/catalyst-github/scripts/lib/credential.mjs +0 -29
  41. package/skills/catalyst-github/scripts/lib/pull.mjs +0 -82
  42. package/skills/catalyst-github/scripts/read-pr.mjs +0 -97
  43. package/skills/catalyst-linear/SKILL.md +0 -43
  44. package/skills/catalyst-linear/agents/openai.yaml +0 -6
  45. package/skills/catalyst-linear/agents/portability.yaml +0 -5
  46. package/skills/catalyst-linear/references/reading-a-ticket.md +0 -50
  47. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +0 -53
  48. package/skills/catalyst-linear/references/writing-to-linear.md +0 -43
  49. package/skills/catalyst-linear/scripts/comment.mjs +0 -59
  50. package/skills/catalyst-linear/scripts/create-ticket.mjs +0 -44
  51. package/skills/catalyst-linear/scripts/label.mjs +0 -48
  52. package/skills/catalyst-linear/scripts/lib/cli.mjs +0 -164
  53. package/skills/catalyst-linear/scripts/lib/credential.mjs +0 -29
  54. package/skills/catalyst-linear/scripts/move.mjs +0 -41
  55. package/skills/catalyst-linear/scripts/read-ticket.mjs +0 -93
  56. package/skills/catalyst-linear/scripts/search.mjs +0 -49
  57. package/skills/catalyst-setup/SKILL.md +0 -32
  58. package/skills/catalyst-setup/agents/openai.yaml +0 -6
  59. package/skills/catalyst-setup/agents/portability.yaml +0 -4
  60. package/skills/catalyst-setup/references/what-each-check-means.md +0 -66
  61. package/skills/catalyst-setup/scripts/check.mjs +0 -75
  62. package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
  63. package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
  64. package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
  65. package/skills/connect-me/SKILL.md +0 -63
  66. package/skills/connect-me/agents/openai.yaml +0 -6
  67. package/skills/connect-me/agents/portability.yaml +0 -5
  68. package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
  69. package/skills/connect-me/scripts/lib/cli.mjs +0 -185
  70. package/skills/connect-me/scripts/lib/credential.mjs +0 -29
  71. package/skills/connect-me/scripts/verify-connection.mjs +0 -68
  72. package/skills/how-catalyst-works/SKILL.md +0 -42
  73. package/skills/how-catalyst-works/agents/openai.yaml +0 -6
  74. package/skills/how-catalyst-works/agents/portability.yaml +0 -4
  75. package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
  76. package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -54
  77. package/skills/how-catalyst-works/references/the-ladder.md +0 -41
  78. package/skills/how-catalyst-works/references/what-runs-next.md +0 -77
  79. package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
  80. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
  81. package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
  82. package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
  83. package/skills/how-catalyst-works/scripts/show-my-map.mjs +0 -94
  84. package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
  85. package/skills/run-this-project/SKILL.md +0 -45
  86. package/skills/run-this-project/agents/openai.yaml +0 -6
  87. package/skills/run-this-project/agents/portability.yaml +0 -5
  88. package/skills/run-this-project/assets/stall-policy.json +0 -15
  89. package/skills/run-this-project/references/making-work-ready.md +0 -60
  90. package/skills/run-this-project/references/reacting-to-events.md +0 -76
  91. package/skills/run-this-project/references/stalls-and-escalation.md +0 -63
  92. package/skills/run-this-project/scripts/lib/cli.mjs +0 -185
  93. package/skills/run-this-project/scripts/lib/credential.mjs +0 -29
  94. package/skills/run-this-project/scripts/make-ready.mjs +0 -64
  95. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  96. package/skills/run-this-project/scripts/watch-scope.mjs +0 -61
  97. package/skills/unstick/SKILL.md +0 -41
  98. package/skills/unstick/agents/openai.yaml +0 -6
  99. package/skills/unstick/agents/portability.yaml +0 -5
  100. package/skills/unstick/references/playbook.md +0 -51
  101. package/skills/unstick/scripts/lib/cli.mjs +0 -135
  102. package/skills/unstick/scripts/lib/credential.mjs +0 -29
  103. package/skills/unstick/scripts/unstick.mjs +0 -57
  104. package/skills/what-needs-me/SKILL.md +0 -41
  105. package/skills/what-needs-me/agents/openai.yaml +0 -6
  106. package/skills/what-needs-me/agents/portability.yaml +0 -5
  107. package/skills/what-needs-me/references/raising-a-decision.md +0 -41
  108. package/skills/what-needs-me/references/reading-the-inbox.md +0 -38
  109. package/skills/what-needs-me/references/settling-an-answer.md +0 -37
  110. package/skills/what-needs-me/scripts/inbox.mjs +0 -56
  111. package/skills/what-needs-me/scripts/lib/cli.mjs +0 -135
  112. package/skills/what-needs-me/scripts/lib/credential.mjs +0 -29
  113. package/skills/what-needs-me/scripts/raise.mjs +0 -53
  114. package/skills/what-needs-me/scripts/settle.mjs +0 -73
  115. package/skills/whats-happening/SKILL.md +0 -43
  116. package/skills/whats-happening/agents/openai.yaml +0 -6
  117. package/skills/whats-happening/agents/portability.yaml +0 -4
  118. package/skills/whats-happening/assets/status-reply.json +0 -77
  119. package/skills/whats-happening/references/reading-the-board.md +0 -43
  120. package/skills/whats-happening/references/reprioritising.md +0 -37
  121. package/skills/whats-happening/references/routing-work.md +0 -36
  122. package/skills/whats-happening/references/status-reply.md +0 -34
  123. package/skills/whats-happening/references/why-is-it-stuck.md +0 -62
  124. package/skills/whats-happening/scripts/explain.mjs +0 -28
  125. package/skills/whats-happening/scripts/lib/cli.mjs +0 -135
  126. package/skills/whats-happening/scripts/lib/credential.mjs +0 -29
  127. package/skills/whats-happening/scripts/snapshot.mjs +0 -145
@@ -1,37 +0,0 @@
1
- # Settling an answer
2
-
3
- This reference restates invariants: where an answer is posted, how it is recorded, and how held work is released. The routes and the bookkeeping marker are the contract's; `node scripts/settle.mjs` reads them through the CLI.
4
-
5
- ## The rule
6
-
7
- The answer lives on the ask. Wherever it arrived (a comment on the ask, a comment on a held ticket, a chat message, a call), it ends as a comment on the ask ticket, and that comment is recorded as the accepted answer. Question, options, default, answer and who answered are then one record.
8
-
9
- ## The three steps
10
-
11
- **1. Post the answer where it should live.** If the human answered in the ask's thread, nothing to do. If they answered anywhere else, post a comment on the ask that states the answer, as the app actor, never as the human, and attribute it in the text ("the owner answered in chat: option B"). Use the `catalyst-linear` skill's comment script; it returns the comment id. Do not paraphrase into a different option; quote what they said.
12
-
13
- **2. Record it.** `node scripts/settle.mjs <ask> --answer <commentId> --role <role>` calls the cloud's ask-accept with the ask, the answering comment and the role doing the recording. The script first checks the comment is really on the ask (an id from a different ticket is refused before anything is written), then records, then reads the ask's blocking relations.
14
-
15
- **3. Release the held work.** For every open ticket the ask blocks, the script posts one bookkeeping comment naming the ask, the comment id and the first line of the answer, so the next phase or agent on that ticket reads the decision without opening the ask. The comment carries the contract's bookkeeping prefix, which means the cloud's comment-wake trigger ignores it: a record, not a turn in a conversation. Pass `--no-release-note` to skip this when the held tickets are about to be canceled anyway.
16
-
17
- With `--close`, the script also moves the ask to its team's done slot. The blocking relations stay on the record; a done ticket does not block anything, so the held tickets become dispatchable on the cloud's next pass. Close only when the answer is complete; an answer that raises a follow-up question keeps the ask open and the follow-up goes in the same thread.
18
-
19
- ## Free-text replies
20
-
21
- A reply that names no option ("do whichever is cheaper", "ask me again Thursday") is recorded exactly as written. Nothing in the cloud or in this skill turns it into an option. Read it, decide whether it answers the question, and if it does not, reply in the thread with the one clarification needed and leave the ask open. If it does, settle with that comment as the answer and let the release note carry the quoted line.
22
-
23
- ## Who settles
24
-
25
- The role that raised the ask, or the owner of the scope it belongs to. The desk settles asks that have no owner. The human never has to run anything; their part ends when they answer. Never settle another role's ask without saying so in the thread.
26
-
27
- ## After settling
28
-
29
- - Tell the human, in one line, what was recorded and what it released.
30
- - The held tickets need no further action from you: their exclusion reason was the blocking relation, and the queue recomputes on the cloud's next pass. If one stays excluded, `whats-happening` explains why.
31
- - If the answer changes priorities or scope, that is a routing change for the project owner, not a second ask.
32
-
33
- ## When it cannot be settled
34
-
35
- - The comment id is not on the ask: post the answer on the ask first, then settle with the new id.
36
- - The write budget for the day is spent: the CLI names the budget from the contract and exits 2. Say so; the record waits, the human's answer is not lost.
37
- - The ask is on a team the contract does not list: the tenant admin maps the team in settings; the `catalyst-setup` skill reads readiness.
@@ -1,56 +0,0 @@
1
- #!/usr/bin/env node
2
- // inbox.mjs — "what needs me?": the open asks on this tenant, ranked by what each answer releases.
3
- // A thin wrapper over `catalyst-skills ask list`, which reads the open tickets carrying the team's
4
- // ask label (from the contract), follows each one's blocking relations, and weights held work by
5
- // priority. By default the CLI keeps only the asks assigned to the connected person; --anyone widens
6
- // to the whole tenant. Prints the ranked list; --json prints the CLI's {scope, asks} unchanged.
7
- import { mustRun, parseFlags, parseJson, printHelp } from "./lib/cli.mjs";
8
-
9
- const SPEC = {
10
- json: { value: false, help: "print the CLI's answer as JSON: {scope, asks: [{identifier, title, state, blocks[], score, assigneeId}]}" },
11
- team: { value: true, help: "only asks whose identifier carries this team key" },
12
- all: { value: false, help: "include asks that hold nothing (by default they are listed last, marked)" },
13
- anyone: { value: false, help: "every open ask in the tenant, not only the ones assigned to you" },
14
- };
15
-
16
- const { help, flags } = parseFlags(process.argv.slice(2), SPEC);
17
- if (help) {
18
- printHelp("node scripts/inbox.mjs [--json] [--team K] [--all] [--anyone] [--help]", SPEC, [
19
- "Rank: the sum over held open tickets of a priority weight (urgent 4 ... low 1, none 1), highest first.",
20
- "An ask that holds nothing is real but will not appear in the tenant's Waiting-on-me view; it is",
21
- "listed last with a marker so you can attach the work it should block (references/reading-the-inbox.md).",
22
- ]);
23
- process.exit(0);
24
- }
25
-
26
- const answer = parseJson(mustRun(["ask", "list", "--json", ...(flags.anyone ? ["--anyone"] : [])], { quiet: true }).stdout, "ask list");
27
- const rows = answer.asks;
28
- const scope = answer.scope;
29
- const wanted = flags.team ? rows.filter((r) => String(r.identifier).toUpperCase().startsWith(`${flags.team.toUpperCase()}-`)) : rows;
30
- if (flags.json) {
31
- process.stdout.write(JSON.stringify({ scope, asks: wanted }) + "\n");
32
- process.exit(0);
33
- }
34
- // Say whose list this is before the list, so an empty "mine" never reads as "nothing needs anyone".
35
- if (scope.kind === "mine") process.stdout.write(`asks assigned to ${scope.label}:\n`);
36
- else if (scope.kind === "anyone") process.stdout.write("every open ask in the tenant:\n");
37
- else if (scope.kind === "unmatched") process.stdout.write(`every open ask (your Linear identity is not matched yet — an admin matches it in Settings → Members):\n`);
38
- else process.stdout.write("every open ask (connected with the tenant's account key, which names no person — log in with your personal key to see only yours):\n");
39
- if (wanted.length === 0) {
40
- process.stdout.write(scope.kind === "mine" ? "nothing needs you: no open asks assigned to you (--anyone lists the tenant's)\n" : "nothing needs anyone: no open asks\n");
41
- process.exit(0);
42
- }
43
- const holding = wanted.filter((r) => r.blocks.length > 0);
44
- const idle = wanted.filter((r) => r.blocks.length === 0);
45
- let n = 0;
46
- for (const r of holding) {
47
- n += 1;
48
- process.stdout.write(`${n}. ${r.identifier} holds ${r.blocks.length} (weight ${r.score}): ${r.blocks.join(", ")}\n ${r.title}\n`);
49
- }
50
- if (idle.length && (flags.all || holding.length === 0)) {
51
- process.stdout.write(`${holding.length ? "\n" : ""}asks that hold nothing (not shown in Waiting on me until they block something):\n`);
52
- for (const r of idle) process.stdout.write(` ${r.identifier} ${r.title}\n`);
53
- } else if (idle.length) {
54
- process.stdout.write(`\n(${idle.length} more ask${idle.length === 1 ? "" : "s"} hold nothing; --all lists them)\n`);
55
- }
56
- process.exit(0);
@@ -1,135 +0,0 @@
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
- }
@@ -1,29 +0,0 @@
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
- }
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
- // raise.mjs — file one decision for the human as an ask ticket, through the cloud's own ask route.
3
- // Wraps `catalyst-skills ask raise`. You pass fields (question, options, default, what it blocks);
4
- // the cloud renders the body from the tenant's ask template, applies the ask labels, and creates the
5
- // blocking relations in one atomic write. Headings are never composed here.
6
- import { mustRun, parseFlags, parseJson, printHelp } from "./lib/cli.mjs";
7
-
8
- const SPEC = {
9
- team: { value: true, help: "the team key the ask is filed on (required)" },
10
- title: { value: true, help: "the question, one sentence (required)" },
11
- context: { value: true, help: "a short paragraph of context the human needs to answer" },
12
- option: { value: true, repeat: true, help: "one realistic option (repeat per option; the contract caps how many)" },
13
- default: { value: true, help: "what proceeds if the human stays silent, and after how long" },
14
- blocks: { value: true, repeat: true, help: "a ticket this decision holds (repeat per ticket)" },
15
- "nothing-to-block": { value: false, help: "declare that no ticket is held (the ask then never shows in Waiting on me)" },
16
- "ask-key": { value: true, help: "idempotency key so a re-run does not file a second ask" },
17
- json: { value: false, help: "print the cloud's response as JSON" },
18
- };
19
-
20
- const { help, flags } = parseFlags(process.argv.slice(2), SPEC);
21
- if (help) {
22
- printHelp("node scripts/raise.mjs --team <key> --title <question> [--context <text>] [--option <text>]... [--default <text>] --blocks <ticket>... | --nothing-to-block [--ask-key <key>] [--json] [--help]", SPEC, [
23
- "Search for an existing ask first (node scripts/inbox.mjs); one decision, one ask.",
24
- "File BEFORE proceeding on the default. Cite the identifier only from this script's output.",
25
- ]);
26
- process.exit(0);
27
- }
28
- const missing = [];
29
- if (!flags.team) missing.push("--team");
30
- if (!flags.title) missing.push("--title");
31
- if (!flags.blocks && !flags["nothing-to-block"]) missing.push("--blocks <ticket> (or --nothing-to-block)");
32
- if (missing.length) {
33
- process.stderr.write(`raise needs ${missing.join(", ")} (try --help)\n`);
34
- process.exit(1);
35
- }
36
-
37
- const args = ["ask", "raise", "--team", flags.team, "--title", flags.title];
38
- if (flags.context) args.push("--context", flags.context);
39
- for (const o of flags.option ?? []) args.push("--option", o);
40
- if (flags.default) args.push("--default", flags.default);
41
- for (const b of flags.blocks ?? []) args.push("--blocks", b);
42
- if (flags["nothing-to-block"]) args.push("--nothing-to-block");
43
- if (flags["ask-key"]) args.push("--ask-key", flags["ask-key"]);
44
- args.push("--json");
45
-
46
- const result = parseJson(mustRun(args, { quiet: true }).stdout, "ask raise");
47
- const id = result.identifier ?? result.id ?? null;
48
- if (flags.json) process.stdout.write(JSON.stringify(result) + "\n");
49
- else {
50
- process.stdout.write(`ask raised: ${id ?? "(no identifier returned)"}${flags.blocks ? ` — holds ${flags.blocks.join(", ")}` : " — holds nothing"}\n`);
51
- if (flags.default) process.stdout.write(`default if silent: ${flags.default}\n`);
52
- }
53
- process.exit(0);
@@ -1,73 +0,0 @@
1
- #!/usr/bin/env node
2
- // settle.mjs — record the human's answer on an ask and release the work it held.
3
- // 1. `catalyst-skills ask accept <ask> --answer <commentId> --role <role>` records which comment is
4
- // the accepted answer, as the app actor.
5
- // 2. Every open ticket the ask blocks gets one bookkeeping comment naming the ask and the answer,
6
- // so the next agent on that ticket reads the decision without opening the ask.
7
- // 3. With --close, the ask moves to its team's done slot (the contract's mapping), since a settled
8
- // ask is a closed record.
9
- import { mustRun, parseFlags, parseJson, printHelp, runCli } from "./lib/cli.mjs";
10
-
11
- const SPEC = {
12
- answer: { value: true, help: "the id of the comment that holds the human's answer (required)" },
13
- role: { value: true, help: "the role recording it, e.g. steward or concierge (required)" },
14
- close: { value: false, help: "also move the ask to the done slot" },
15
- "no-release-note": { value: false, help: "skip the bookkeeping comment on the held tickets" },
16
- json: { value: false, help: "print what happened as JSON" },
17
- };
18
-
19
- const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
20
- const askTicket = positionals[0];
21
- if (help || !askTicket || !flags.answer || !flags.role) {
22
- printHelp("node scripts/settle.mjs <askTicket> --answer <commentId> --role <role> [--close] [--no-release-note] [--json] [--help]", SPEC, [
23
- "The answer must already be a comment on the ask. When it arrived in chat, post it there first with",
24
- "the catalyst-linear skill's comment script (as the app actor, never as the human), then settle with that",
25
- "comment's id. A free-text reply that names no option is recorded exactly as written, never interpreted.",
26
- ]);
27
- process.exit(help ? 0 : 1);
28
- }
29
-
30
- const detail = parseJson(mustRun(["query", "issue", askTicket, "--json"], { quiet: true }).stdout, "issue");
31
- const comments = Array.isArray(detail.comments) ? detail.comments : [];
32
- const answer = comments.find((c) => String(c.id) === String(flags.answer));
33
- if (!answer) {
34
- process.stderr.write(`comment ${flags.answer} is not on ${detail.identifier ?? askTicket} (${comments.length} comment${comments.length === 1 ? "" : "s"} read); post the answer there first\n`);
35
- process.exit(1);
36
- }
37
- const answerLine = String(answer.body ?? "").trim().split("\n")[0].slice(0, 200) || "(empty comment)";
38
-
39
- const accepted = parseJson(mustRun(["ask", "accept", askTicket, "--answer", flags.answer, "--role", flags.role, "--json"], { quiet: true }).stdout, "ask accept");
40
-
41
- const relations = Array.isArray(detail.relations) ? detail.relations : [];
42
- const held = [...new Set(relations
43
- .filter((r) => r.type === "blocks" && (r.issue_identifier === detail.identifier || r.issue_identifier === undefined))
44
- .map((r) => r.related_identifier ?? r.related_issue_identifier)
45
- .filter((id) => typeof id === "string"))];
46
-
47
- const released = [];
48
- const failed = [];
49
- if (!flags["no-release-note"]) {
50
- for (const t of held) {
51
- const body = `${detail.identifier ?? askTicket} was answered (comment ${flags.answer}, recorded by ${flags.role}): ${answerLine}`;
52
- const r = runCli(["write", "comment", t, "--bookkeeping", "--body", body, "--json"]);
53
- if (r.code === 0) released.push(t);
54
- else failed.push({ ticket: t, error: (r.stderr || `exit ${r.code}`).trim().split("\n").at(-1) });
55
- }
56
- }
57
-
58
- let closed = false;
59
- if (flags.close) {
60
- const r = runCli(["write", "state", askTicket, "--slot", "done", "--json"]);
61
- if (r.code === 0) closed = true;
62
- else failed.push({ ticket: askTicket, error: (r.stderr || `exit ${r.code}`).trim().split("\n").at(-1) });
63
- }
64
-
65
- const out = { ask: detail.identifier ?? askTicket, answerComment: flags.answer, role: flags.role, answer: answerLine, accepted, held, released, closed, failed };
66
- if (flags.json) process.stdout.write(JSON.stringify(out) + "\n");
67
- else {
68
- process.stdout.write(`${out.ask}: answer ${flags.answer} recorded by ${flags.role} — "${answerLine}"\n`);
69
- process.stdout.write(held.length ? `held: ${held.join(", ")}; release note posted on: ${released.join(", ") || "none"}\n` : "held nothing\n");
70
- if (closed) process.stdout.write(`${out.ask} moved to the done slot\n`);
71
- for (const f of failed) process.stdout.write(`failed on ${f.ticket}: ${f.error}\n`);
72
- }
73
- process.exit(failed.length ? 1 : 0);
@@ -1,43 +0,0 @@
1
- ---
2
- name: whats-happening
3
- description: >-
4
- The desk for a Catalyst Cloud tenant. Use when the person asks "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", or asks for something to be done rather than known. Reads the tenant contract, what is running and queued, the eligibility explainer and the open asks through the catalyst-skills CLI, and answers in one reply with ticket ids. Routes work to a project owner and decisions to what-needs-me. Never composes a URL, never polls, never answers as the human.
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
- # What's happening
10
-
11
- You are the one desk the person talks to about their Catalyst Cloud tenant. They ask four questions: where are we, why is that stuck, what closed, what is next. You answer each in one reply, from their tenant's data only, with a ticket identifier on every line. When they ask for something to be done, you route it to an owner; when only they can decide something, you raise it through `what-needs-me`.
12
-
13
- ## Run first
14
-
15
- Scripts are run, never read. Each prints `--help`; exit 2 means this machine is not connected (run the `connect-me` skill), exit 1 means the check itself failed.
16
-
17
- - `node scripts/snapshot.mjs --help` — one JSON document: the trimmed contract (teams, stage names by slot, thresholds, ladder), what is running, the queue, the open asks ranked by what they hold, and the replica verdict. Add `--board` for open tickets grouped by stage in the contract's slot order, `--team K` to narrow.
18
- - `node scripts/explain.mjs --help` — why nothing is offered for one ticket, in one paragraph.
19
-
20
- For a single ticket's comments, relations and linked PRs use the `catalyst-linear` skill (`catalyst-skills query issue <id>`); for a pull request's checks and threads, `catalyst-github`.
21
-
22
- ## Load on demand
23
-
24
- | when | read |
25
- | -- | -- |
26
- | writing any status answer | `references/status-reply.md` (schema: `assets/status-reply.json`) |
27
- | the board looks wrong, a column is long, or you must decide whether something is stuck | `references/reading-the-board.md` |
28
- | `explain` returned a reason and the person wants the next action | `references/why-is-it-stuck.md` |
29
- | "do this one first", "why is that not next", "stop that" | `references/reprioritising.md` |
30
- | the person asks for work to be done, or a decision surfaces | `references/routing-work.md` |
31
-
32
- For depth beyond these, load the fact skills: `how-catalyst-works` (the ladder, slots and mapping, failure layers, queue order, coding accounts), `catalyst-linear` (what a ticket accumulates, reading and writing), `catalyst-github` (what a PR accumulates, mergeability), `catalyst-setup` (readiness).
33
-
34
- ## Rules
35
-
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
- - **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
- - **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; 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
- - **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
- - **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
- - **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.
43
- - **Cite an identifier only after a create call returned it.**
@@ -1,6 +0,0 @@
1
- interface:
2
- display_name: "What's happening"
3
- short_description: "Where your Catalyst work stands: in flight, blocked and on whom, closed, and what runs next"
4
- default_prompt: "Use $whats-happening to tell me where my work stands and why anything is stuck."
5
- policy:
6
- allow_implicit_invocation: true
@@ -1,4 +0,0 @@
1
- identity: { pack: catalyst-cloud-skills, skill: whats-happening }
2
- effects: []
3
- invocation: implicit
4
- exposure: [catalog]
@@ -1,77 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "title": "Catalyst status reply",
4
- "description": "The one-reply shape a status answer follows. Every line names a ticket identifier; nothing in it is a guess. Fill it from scripts/snapshot.mjs and scripts/explain.mjs, then render it as prose in this order.",
5
- "type": "object",
6
- "required": ["asOf", "source", "inFlight", "blocked", "closed", "next"],
7
- "properties": {
8
- "asOf": { "type": "string", "format": "date-time", "description": "When the snapshot was taken (snapshot.takenAt)." },
9
- "source": {
10
- "type": "string",
11
- "description": "Where the facts came from, in one clause: the API, or the replica with its cursor and heartbeat age. Copy the snapshot's source line; never omit it when the replica was stale."
12
- },
13
- "scope": { "type": "string", "description": "The team key or project the reply covers, when the human asked about one." },
14
- "inFlight": {
15
- "type": "array",
16
- "description": "Tickets a phase is running on right now, or that sit mid-ladder with a live lease. One entry per ticket.",
17
- "items": { "$ref": "#/$defs/line" }
18
- },
19
- "blocked": {
20
- "type": "array",
21
- "description": "Tickets nothing is offered for, each with the reason in the human's words and who or what releases it.",
22
- "items": {
23
- "allOf": [
24
- { "$ref": "#/$defs/line" },
25
- {
26
- "properties": {
27
- "reason": { "type": "string", "description": "The explainer's reason, translated (references/why-is-it-stuck.md)." },
28
- "onWhom": { "type": "string", "description": "The human, a named role, the cloud itself, or a clock — never blank." }
29
- },
30
- "required": ["reason", "onWhom"]
31
- }
32
- ]
33
- }
34
- },
35
- "waitingOnHuman": {
36
- "type": "array",
37
- "description": "Open asks, ranked by what each holds. Taken from snapshot.waitingOnHuman; the what-needs-me skill owns the detail.",
38
- "items": {
39
- "type": "object",
40
- "required": ["ticket", "question", "holds"],
41
- "properties": {
42
- "ticket": { "type": "string" },
43
- "question": { "type": "string" },
44
- "holds": { "type": "array", "items": { "type": "string" }, "description": "Ticket identifiers the answer releases." }
45
- }
46
- }
47
- },
48
- "closed": {
49
- "type": "array",
50
- "description": "Tickets that reached the done slot since the window the human asked about (or since the last reply).",
51
- "items": { "$ref": "#/$defs/line" }
52
- },
53
- "next": {
54
- "type": "array",
55
- "description": "What the queue will pick up next, in queue order, with the phase each will run.",
56
- "items": { "$ref": "#/$defs/line" }
57
- },
58
- "cannotSee": {
59
- "type": "array",
60
- "description": "Facts your key cannot read (PR labels and reactions; an operator-only park release), named, with where the human can read them (the CLI prints the URL).",
61
- "items": { "type": "string" }
62
- }
63
- },
64
- "$defs": {
65
- "line": {
66
- "type": "object",
67
- "required": ["ticket", "summary"],
68
- "properties": {
69
- "ticket": { "type": "string", "description": "The identifier, KEY-123 shape." },
70
- "summary": { "type": "string", "description": "One clause: what it is and where it stands." },
71
- "stage": { "type": "string", "description": "The stage name as the contract spells it for this team." },
72
- "phase": { "type": "string", "description": "The ladder phase running or offered, when known." },
73
- "age": { "type": "string", "description": "How long it has sat where it is, in human units." }
74
- }
75
- }
76
- }
77
- }
@@ -1,43 +0,0 @@
1
- # Reading the board
2
-
3
- This reference restates invariants: what the stages are, what order they come in, and what "stuck" means. The names your team uses for each stage, and which state ids they map to, are tenant facts read live from the contract; `node scripts/snapshot.mjs --board` prints the board already grouped and ordered by them.
4
-
5
- ## Stages are slots, names are display
6
-
7
- Catalyst thinks in eleven **slots** in pipeline order: dispatch, intake, research, plan, implement, remediate, verify, review, pr, done, canceled. Each of your teams maps some or all of those slots onto its own Linear workflow states. The contract carries, per team and per slot, the state id (the authority), the live state name and type (display), and whether the state still exists in Linear. A team may have adopted Catalyst's recommended states, mapped its existing ones, or mixed the two; the contract's `workflowMode` says which.
8
-
9
- Five slots are load-bearing and a missing one is a distinct silent failure: **dispatch** (where a card goes to be picked up), **intake** (an optional first pass for tickets that have never entered the ladder), **pr** (where a card sits while its pull request is reviewed and merged), **done** and **canceled**. The other six are informational: a wrong mapping there costs a warning, not a stall.
10
-
11
- Read a board by slot, never by name. Two teams can call the same slot different things, and a state renamed in Linear keeps its id and its slot.
12
-
13
- ## Work in progress by stage
14
-
15
- The snapshot's `board` block groups open tickets by state name in slot order, drops terminal states, and lists any state name that maps to no slot at the end. Read it top to bottom:
16
-
17
- - **dispatch** holds what is ready and waiting for a container. A long dispatch column with nothing in flight is a capacity or eligibility question, not a work question: run `explain` on the first row.
18
- - **intake** through **pr** are the ladder. A card advances one slot when the matching phase completes; a failed phase writes no board state, so a card that has not moved is either still running, retrying in place, or moved sideways to remediate.
19
- - **remediate** is an interrupt, not a step. A card there was moved by a failure and returns to its exact previous stage when a repair round succeeds. Count remediate cards separately; they are not progress.
20
- - Backlog-type states are not slots. A ticket there is parked or not yet chosen; nothing is offered for it and nothing is wrong with it.
21
-
22
- ## Aging
23
-
24
- Age is time since `updated_at`, or since the lease started for a ticket in flight. Report it in human units. Compare it to the phase, not to a fixed number: a research phase and a merge wait have different natural durations, and the cloud's own retry backoff (the contract's `thresholds.retryBackoffMs`) explains many short waits.
25
-
26
- ## What counts as stuck
27
-
28
- A ticket is **stuck** only when all three hold:
29
-
30
- 1. Nothing is offered for it: the queue lists it excluded, or `explain` gives a reason rather than a position.
31
- 2. The reason does not release itself. Backoffs, cooldowns that name a callback, and fleet-wide holds that clear when the cloud repins are waits, not stalls. The table in `references/why-is-it-stuck.md` marks which is which.
32
- 3. No one is acting on the release: no open ask names it, no human comment landed after the failure, no push arrived.
33
-
34
- Everything else is **waiting**, and the reply says what it is waiting on. Calling a wait "stuck" sends the human to fix something the cloud is already handling.
35
-
36
- ## The signals the cloud posts on the ticket itself
37
-
38
- Before declaring anything, read the ticket's comments (`catalyst-skills query issue <id>`; the `catalyst-linear` skill explains each shape). The cloud posts a phase-outcome card on every completion and failure, a remediate-attempt card per repair round, a board-health note when it detects a stall itself, and a merge-wait note when a merge is held. Those comments are the narrative; the per-ticket ledger behind them — attempts, rounds against the cap, park state — is `catalyst-skills explain --history <ticket>`, and the two must agree.
39
-
40
- ## Two things that look like work and are not
41
-
42
- - A ticket carrying the ask marker label, or whose text reads as a decision request, is a question. It is excluded from dispatch by design and belongs in the waiting-on-human block.
43
- - A ticket claimed by a worker outside the cloud (the local-lane label) is being worked, but not by Catalyst; report it as in flight elsewhere and do not chase the cloud for it.
@@ -1,37 +0,0 @@
1
- # Reprioritising
2
-
3
- This reference restates invariants: how Catalyst orders work and which levers a person actually has. The live queue is `catalyst-skills queue [--team K]` (inside `node scripts/snapshot.mjs`); the thresholds behind backoffs and parks are the contract's `thresholds` block, printed in the snapshot's `tenant` section.
4
-
5
- ## How the cloud orders work
6
-
7
- Each team's queue is derived by the cloud itself from the team's own tickets, so there is nothing a session publishes and nothing that goes stale between sessions. The order is:
8
-
9
- 1. **Priority** ascending, as set on the Linear ticket (urgent first).
10
- 2. **Created time** ascending: older tickets first among equals.
11
- 3. **Identifier** ascending, as the final tiebreak.
12
-
13
- Tickets that are already **mid-ladder** join the same candidate set and sort ahead by how many phases they have completed, so a ticket that is nearly done finishes before a fresh one starts. That is an ordering rule only; it never makes an ineligible ticket eligible.
14
-
15
- Within the order, dispatch is bounded per repository (a concurrency cap the tenant admin can set in settings, and a paused repository resolves to zero) and split across teams so one team cannot starve another. Comment-wake work, where the cloud answers a human comment on a ticket, shares the same cap as relay work.
16
-
17
- The queue is recomputed on every ingest that touches the team and on each alarm pass, so a change you make shows up within seconds, not on a schedule.
18
-
19
- ## The three levers a person has
20
-
21
- **1. Priority on the ticket.** Change the Linear priority and the queue reorders itself. This is the lever for "do this one first". It does not jump a running phase; it changes what is picked up next.
22
-
23
- **2. The dispatch column.** A card is offered only when it sits in the team's dispatch slot (and, for a ticket that has never entered the ladder, the intake slot when intake is enabled). Moving a card into that slot is how work is started; moving it out to a backlog-type state is how work is stopped without cancelling it. The `run-this-project` skill has the script; through this skill you describe the move and let the person or the project owner make it.
24
-
25
- **3. Holds.** A PR label from the contract's `merge.prLabels` set keeps an otherwise-mergeable pull request out of the merge queue until a person removes it. A blocking relation from an ask holds every ticket the ask names. Applying the release label the contract names frees a ticket the ask-shape detector flagged by mistake.
26
-
27
- Everything else that looks like a lever is not one from a key: unparking a phase, clearing a repair hold, resetting a validate budget and re-pinning a base are operator actions today, and the reply says so and names the settings page rather than promising them.
28
-
29
- ## What not to promise
30
-
31
- - **A running phase is not interrupted** by any of the three levers. It finishes or fails on its own; then the new order applies.
32
- - **A failed phase does not need re-queueing.** A pre-branch failure or an infrastructure failure retries in place after a backoff; a later failure moves the card to the remediate slot and queues a repair round, capped by the contract's `thresholds.remediateRoundCap`; repeated failures park it after `thresholds.parkAfterConsecutiveFailures`. Moving the card by hand during that sequence usually restarts the count rather than shortening it.
33
- - **Priority is not urgency to the human.** An urgent ticket that is blocked on an ask still waits for the ask. The what-needs-me ranking (by what an answer releases) is the right list for the human; the queue is the right list for the fleet.
34
-
35
- ## Answering "why is that one not next?"
36
-
37
- Run `explain` on it. If the answer is a position, it is next in order and the cloud is bounded by capacity: say how many are ahead and whether any run at all (if nothing runs anywhere, the likely cause is coding-account capacity, which a key cannot read yet, so name the settings page). If the answer is a reason, use `references/why-is-it-stuck.md`. If the answer is that the ticket is not in the explainer, it is on another team, terminal, or unknown to the mirror: check the identifier before anything else.