@catalyst-cloud/catalyst-skills 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +109 -53
- package/bin/catalyst-skills.js +42 -2
- package/dist/args.js +242 -0
- package/dist/ask.js +137 -0
- package/dist/cli.js +167 -279
- package/dist/config.js +106 -0
- package/dist/contract-types.js +4 -0
- package/dist/contract.js +178 -0
- package/dist/detach.js +10 -0
- package/dist/errors.js +27 -0
- package/dist/execution.js +308 -0
- package/dist/http.js +175 -0
- package/dist/prompt.js +35 -0
- package/dist/query.js +225 -0
- package/dist/ready.js +123 -0
- package/dist/replica.js +318 -0
- package/dist/sdk.js +26 -0
- package/dist/skill-shape.js +179 -0
- package/dist/skills.js +74 -0
- package/dist/ts-deps-loader.js +68 -0
- package/dist/watch/consumer.js +141 -0
- package/dist/watch/cursor-file.js +62 -0
- package/dist/watch.js +173 -0
- package/dist/write.js +224 -0
- package/package.json +12 -7
- package/skills/catalyst-github/SKILL.md +35 -0
- package/skills/catalyst-github/agents/openai.yaml +6 -0
- package/skills/catalyst-github/agents/portability.yaml +4 -0
- package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
- package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
- package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
- package/skills/catalyst-linear/SKILL.md +43 -0
- package/skills/catalyst-linear/agents/openai.yaml +6 -0
- package/skills/catalyst-linear/agents/portability.yaml +5 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
- package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
- package/skills/catalyst-linear/scripts/comment.mjs +59 -0
- package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
- package/skills/catalyst-linear/scripts/label.mjs +48 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
- package/skills/catalyst-linear/scripts/move.mjs +41 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
- package/skills/catalyst-linear/scripts/search.mjs +49 -0
- package/skills/catalyst-setup/SKILL.md +32 -0
- package/skills/catalyst-setup/agents/openai.yaml +6 -0
- package/skills/catalyst-setup/agents/portability.yaml +4 -0
- package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
- package/skills/catalyst-setup/scripts/check.mjs +75 -0
- package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
- package/skills/connect-me/SKILL.md +55 -0
- package/skills/connect-me/agents/openai.yaml +6 -0
- package/skills/connect-me/agents/portability.yaml +5 -0
- package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
- package/skills/connect-me/scripts/lib/cli.mjs +184 -0
- package/skills/connect-me/scripts/verify-connection.mjs +68 -0
- package/skills/how-catalyst-works/SKILL.md +42 -0
- package/skills/how-catalyst-works/agents/openai.yaml +6 -0
- package/skills/how-catalyst-works/agents/portability.yaml +4 -0
- package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
- package/skills/how-catalyst-works/references/the-ladder.md +41 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
- package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
- package/skills/run-this-project/SKILL.md +45 -0
- package/skills/run-this-project/agents/openai.yaml +6 -0
- package/skills/run-this-project/agents/portability.yaml +5 -0
- package/skills/run-this-project/assets/stall-policy.json +15 -0
- package/skills/run-this-project/references/making-work-ready.md +60 -0
- package/skills/run-this-project/references/reacting-to-events.md +76 -0
- package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
- package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
- package/skills/run-this-project/scripts/make-ready.mjs +64 -0
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
- package/skills/what-needs-me/SKILL.md +41 -0
- package/skills/what-needs-me/agents/openai.yaml +6 -0
- package/skills/what-needs-me/agents/portability.yaml +5 -0
- package/skills/what-needs-me/references/raising-a-decision.md +41 -0
- package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
- package/skills/what-needs-me/references/settling-an-answer.md +37 -0
- package/skills/what-needs-me/scripts/inbox.mjs +47 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
- package/skills/what-needs-me/scripts/raise.mjs +53 -0
- package/skills/what-needs-me/scripts/settle.mjs +73 -0
- package/skills/whats-happening/SKILL.md +43 -0
- package/skills/whats-happening/agents/openai.yaml +6 -0
- package/skills/whats-happening/agents/portability.yaml +4 -0
- package/skills/whats-happening/assets/status-reply.json +77 -0
- package/skills/whats-happening/references/reading-the-board.md +43 -0
- package/skills/whats-happening/references/reprioritising.md +37 -0
- package/skills/whats-happening/references/routing-work.md +36 -0
- package/skills/whats-happening/references/status-reply.md +34 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
- package/skills/whats-happening/scripts/explain.mjs +28 -0
- package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
- package/skills/whats-happening/scripts/snapshot.mjs +145 -0
- package/skills/ask/SKILL.md +0 -37
- package/skills/concierge/SKILL.md +0 -41
- package/skills/join/SKILL.md +0 -30
- package/skills/linearis/SKILL.md +0 -40
- package/skills/setup/SKILL.md +0 -27
- package/skills/steward/SKILL.md +0 -30
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: by spawning the catalyst-skills
|
|
3
|
+
// CLI. The CLI holds the SDK and the account key; this file holds neither. It reads
|
|
4
|
+
// ~/.config/catalyst-cloud/customer.json (under CATALYST_SKILLS_HOME when set, else HOME) for the
|
|
5
|
+
// CLI path that login recorded and falls back to `npx @catalyst-cloud/catalyst-skills`.
|
|
6
|
+
//
|
|
7
|
+
// Exit codes every script built on this file shares: 2 = this machine is not connected, 1 = the
|
|
8
|
+
// script's own check failed, 0 = fine.
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
|
|
15
|
+
export const NOT_CONFIGURED_EXIT = 2;
|
|
16
|
+
export const CHECK_FAILED_EXIT = 1;
|
|
17
|
+
|
|
18
|
+
export function homeDir() {
|
|
19
|
+
return process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "/";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configDir() {
|
|
23
|
+
return join(homeDir(), ".config", "catalyst-cloud");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function configPath() {
|
|
27
|
+
return join(configDir(), "customer.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The stored config, or null when the file is absent or unreadable. Never throws. */
|
|
31
|
+
export function loadCustomerConfig() {
|
|
32
|
+
const path = configPath();
|
|
33
|
+
if (!existsSync(path)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.key !== "string" || typeof parsed.account !== "string") return null;
|
|
37
|
+
return parsed;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Print the one not-connected line and exit 2. */
|
|
44
|
+
export function requireConfigured() {
|
|
45
|
+
const cfg = loadCustomerConfig();
|
|
46
|
+
if (cfg) return cfg;
|
|
47
|
+
console.error(`not connected: ${configPath()} is missing or unreadable — run: CATALYST_CLOUD_TOKEN=<account key> npx ${PACKAGE_NAME} login`);
|
|
48
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The command and argument prefix that reaches the CLI on this machine. */
|
|
52
|
+
export function cliCommand(cfg = loadCustomerConfig()) {
|
|
53
|
+
if (cfg && typeof cfg.cliPath === "string" && existsSync(cfg.cliPath)) {
|
|
54
|
+
return { command: process.execPath, prefix: [cfg.cliPath], via: `node ${cfg.cliPath}` };
|
|
55
|
+
}
|
|
56
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
57
|
+
return { command: npx, prefix: [PACKAGE_NAME], via: `npx ${PACKAGE_NAME}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run one CLI verb and capture its output. Resolves `{code, stdout, stderr, notConfigured}`;
|
|
62
|
+
* `notConfigured` is true when the CLI itself said the machine is not connected.
|
|
63
|
+
*/
|
|
64
|
+
export function runCli(args, { stdin } = {}) {
|
|
65
|
+
const { command, prefix } = cliCommand();
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const child = spawn(command, [...prefix, ...args], {
|
|
68
|
+
stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
69
|
+
env: process.env,
|
|
70
|
+
shell: process.platform === "win32",
|
|
71
|
+
});
|
|
72
|
+
let stdout = "";
|
|
73
|
+
let stderr = "";
|
|
74
|
+
child.stdout.on("data", (d) => (stdout += String(d)));
|
|
75
|
+
child.stderr.on("data", (d) => (stderr += String(d)));
|
|
76
|
+
child.on("error", reject);
|
|
77
|
+
child.on("close", (code) => {
|
|
78
|
+
resolve({ code: code ?? 1, stdout, stderr, notConfigured: /not (joined|connected)/i.test(stderr) || /not (joined|connected)/i.test(stdout) });
|
|
79
|
+
});
|
|
80
|
+
if (stdin !== undefined) child.stdin.end(stdin);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run one CLI verb with the terminal attached, so a long-running verb such as `watch` streams
|
|
86
|
+
* straight through. Resolves with the exit code; SIGINT and SIGTERM are forwarded to the child.
|
|
87
|
+
*/
|
|
88
|
+
export function execCli(args) {
|
|
89
|
+
const { command, prefix } = cliCommand();
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const child = spawn(command, [...prefix, ...args], { stdio: "inherit", env: process.env, shell: process.platform === "win32" });
|
|
92
|
+
const forward = (sig) => () => {
|
|
93
|
+
try {
|
|
94
|
+
child.kill(sig);
|
|
95
|
+
} catch {
|
|
96
|
+
// already gone
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const onInt = forward("SIGINT");
|
|
100
|
+
const onTerm = forward("SIGTERM");
|
|
101
|
+
process.on("SIGINT", onInt);
|
|
102
|
+
process.on("SIGTERM", onTerm);
|
|
103
|
+
child.on("error", reject);
|
|
104
|
+
child.on("close", (code) => {
|
|
105
|
+
process.off("SIGINT", onInt);
|
|
106
|
+
process.off("SIGTERM", onTerm);
|
|
107
|
+
resolve(code ?? 1);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Parse the CLI's --json output; a parse failure names the first line of what came back. */
|
|
113
|
+
export function parseJson(stdout) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(stdout);
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error(`the CLI did not answer with JSON: ${stdout.split("\n")[0] ?? "(empty)"}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Exit 2 with the CLI's own not-connected line when a call reports it; otherwise return the result. */
|
|
122
|
+
export function guard(result) {
|
|
123
|
+
if (result.notConfigured) {
|
|
124
|
+
process.stderr.write(result.stderr || result.stdout);
|
|
125
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A tiny flag parser: `--name value`, `--name=value`, `--flag`, repeatable names collected as arrays. */
|
|
131
|
+
export function parseFlags(argv, { values = [], repeat = [], booleans = [] } = {}) {
|
|
132
|
+
const flags = {};
|
|
133
|
+
const positionals = [];
|
|
134
|
+
for (let i = 0; i < argv.length; i++) {
|
|
135
|
+
const a = argv[i];
|
|
136
|
+
if (a === "--") {
|
|
137
|
+
positionals.push(...argv.slice(i + 1));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (!a.startsWith("--")) {
|
|
141
|
+
positionals.push(a);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let name = a.slice(2);
|
|
145
|
+
let value;
|
|
146
|
+
const eq = name.indexOf("=");
|
|
147
|
+
if (eq !== -1) {
|
|
148
|
+
value = name.slice(eq + 1);
|
|
149
|
+
name = name.slice(0, eq);
|
|
150
|
+
}
|
|
151
|
+
if (booleans.includes(name)) {
|
|
152
|
+
flags[name] = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!values.includes(name) && !repeat.includes(name)) {
|
|
156
|
+
console.error(`unknown option: --${name} (try --help)`);
|
|
157
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
158
|
+
}
|
|
159
|
+
if (value === undefined) {
|
|
160
|
+
value = argv[++i];
|
|
161
|
+
if (value === undefined) {
|
|
162
|
+
console.error(`--${name} needs a value`);
|
|
163
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (repeat.includes(name)) flags[name] = [...(flags[name] ?? []), value];
|
|
167
|
+
else flags[name] = value;
|
|
168
|
+
}
|
|
169
|
+
return { flags, positionals };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function wantsHelp(argv) {
|
|
173
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const HELP = `lib/cli.mjs — shared helper; not a command.
|
|
177
|
+
|
|
178
|
+
Resolves the catalyst-skills CLI (the path login recorded in ${configPath()}, else npx ${PACKAGE_NAME})
|
|
179
|
+
and runs one verb for the script that imports it. Run any sibling script with --help instead.`;
|
|
180
|
+
|
|
181
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
182
|
+
console.log(HELP);
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// make-ready.mjs — the two card moves a steward makes. Dispatch is a move into the team's dispatch
|
|
3
|
+
// slot; parking is a move into the team's backlog-type state, which is not a slot, so the CLI
|
|
4
|
+
// resolves it from the team's live workflow states. Every id comes from the contract or the live
|
|
5
|
+
// state list; nothing here names a stage.
|
|
6
|
+
import { CHECK_FAILED_EXIT, guard, parseFlags, parseJson, requireConfigured, runCli, wantsHelp } from "./lib/cli.mjs";
|
|
7
|
+
|
|
8
|
+
const HELP = `Usage: node scripts/make-ready.mjs <ticket> [--park] [--note <text>] [--json]
|
|
9
|
+
|
|
10
|
+
Without --park: moves the card into the team's dispatch column, so the cloud offers its next phase.
|
|
11
|
+
With --park: moves the card into the team's backlog-type state, so nothing further is offered.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--note <text> also post a bookkeeping comment saying why (prefixed with the contract's marker,
|
|
15
|
+
so it wakes nothing)
|
|
16
|
+
--json print the CLI's JSON answers instead of the summary lines
|
|
17
|
+
--help this text
|
|
18
|
+
|
|
19
|
+
After a dispatch move the script asks the eligibility explainer about the ticket and prints its
|
|
20
|
+
verdict. A verdict about a stale or unpublished ordering right after a move is normal: the cloud
|
|
21
|
+
re-derives the queue within a pass; ask again in a minute.
|
|
22
|
+
|
|
23
|
+
Exit codes: 2 not connected; 1 the move was refused (the CLI's reason is printed); 0 moved.`;
|
|
24
|
+
|
|
25
|
+
const argv = process.argv.slice(2);
|
|
26
|
+
if (wantsHelp(argv)) {
|
|
27
|
+
console.log(HELP);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
const { flags, positionals } = parseFlags(argv, { values: ["note"], booleans: ["park", "json"] });
|
|
31
|
+
const ticket = positionals[0];
|
|
32
|
+
if (!ticket || positionals.length !== 1) {
|
|
33
|
+
console.error("make-ready needs exactly one ticket identifier (see --help)");
|
|
34
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
35
|
+
}
|
|
36
|
+
requireConfigured();
|
|
37
|
+
|
|
38
|
+
const moveArgs = flags.park ? ["write", "state", ticket, "--state-type", "backlog"] : ["write", "state", ticket, "--slot", "dispatch"];
|
|
39
|
+
const moved = guard(await runCli([...moveArgs, "--json"]));
|
|
40
|
+
if (moved.code !== 0) {
|
|
41
|
+
process.stderr.write(moved.stderr || moved.stdout);
|
|
42
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
43
|
+
}
|
|
44
|
+
const out = { ticket, action: flags.park ? "parked" : "dispatched", move: parseJson(moved.stdout) };
|
|
45
|
+
|
|
46
|
+
if (flags.note) {
|
|
47
|
+
const body = `${flags.park ? "parked" : "dispatched"} by run-this-project: ${flags.note}`;
|
|
48
|
+
const noted = guard(await runCli(["write", "comment", ticket, "--bookkeeping", "--body", body, "--json"]));
|
|
49
|
+
out.note = noted.code === 0 ? parseJson(noted.stdout) : { failed: (noted.stderr || noted.stdout).trim() };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!flags.park) {
|
|
53
|
+
const explained = guard(await runCli(["explain", ticket, "--json"]));
|
|
54
|
+
out.eligibility = explained.code === 0 ? parseJson(explained.stdout) : { failed: (explained.stderr || explained.stdout).trim() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (flags.json) {
|
|
58
|
+
console.log(JSON.stringify(out));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`${ticket}: ${out.action}`);
|
|
61
|
+
if (out.note) console.log(out.note.failed ? `note: not posted (${out.note.failed})` : "note: posted as a bookkeeping comment");
|
|
62
|
+
if (out.eligibility) console.log(out.eligibility.failed ? `eligibility: unknown (${out.eligibility.failed})` : `eligibility: ${out.eligibility.explanation}`);
|
|
63
|
+
}
|
|
64
|
+
process.exit(0);
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// watch-scope.mjs — subscribe to the tenant stream filtered to one scope and pass every in-scope
|
|
3
|
+
// frame through, one JSON line each. This is `catalyst-skills watch` with the scope flags checked
|
|
4
|
+
// up front; the CLI owns the socket, the cursor file, the replay and the reconnects.
|
|
5
|
+
import { CHECK_FAILED_EXIT, execCli, parseFlags, requireConfigured, wantsHelp } from "./lib/cli.mjs";
|
|
6
|
+
|
|
7
|
+
const HELP = `Usage: node scripts/watch-scope.mjs (--project <id> | --team <key> | --ticket <id>...) [options]
|
|
8
|
+
|
|
9
|
+
Streams one JSON line per change inside the scope: a ticket, a comment, a pull request, a check, a
|
|
10
|
+
review thread, an agent session or a fleet anomaly. Runs until the session ends (Ctrl-C).
|
|
11
|
+
|
|
12
|
+
Scope (at least one; they combine):
|
|
13
|
+
--project <id> every ticket in this Linear project, resolved through the ticket's own record
|
|
14
|
+
--team <key> every ticket whose identifier carries this team key
|
|
15
|
+
--ticket <id> one ticket (repeatable)
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--exec <command> run this shell command once per frame with the frame on stdin; a non-zero exit
|
|
19
|
+
is a failed reaction, so the cursor holds and the frame is offered again
|
|
20
|
+
--from cursor|head start from the saved cursor (default) or skip straight to the tenant head
|
|
21
|
+
--cursor-file <p> where the cursor lives (default ~/.config/catalyst-cloud/watch-cursor.json)
|
|
22
|
+
--help this text
|
|
23
|
+
|
|
24
|
+
Exit codes: 2 not connected; 1 no scope given; otherwise the CLI's own exit code.
|
|
25
|
+
|
|
26
|
+
In Claude Code, arm a monitor on this command and react to each line in the same turn. In a harness
|
|
27
|
+
with no monitor, pass --exec so the reaction still happens per frame and nothing polls.`;
|
|
28
|
+
|
|
29
|
+
const argv = process.argv.slice(2);
|
|
30
|
+
if (wantsHelp(argv)) {
|
|
31
|
+
console.log(HELP);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
const { flags, positionals } = parseFlags(argv, {
|
|
35
|
+
values: ["project", "team", "exec", "from", "cursor-file"],
|
|
36
|
+
repeat: ["ticket"],
|
|
37
|
+
});
|
|
38
|
+
if (positionals.length > 0) {
|
|
39
|
+
console.error(`unexpected argument: ${positionals[0]} (scope is given with --project, --team or --ticket)`);
|
|
40
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
41
|
+
}
|
|
42
|
+
if (!flags.project && !flags.team && !(flags.ticket && flags.ticket.length)) {
|
|
43
|
+
console.error("watch-scope needs a scope: --project <id>, --team <key> or --ticket <id> (see --help)");
|
|
44
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
45
|
+
}
|
|
46
|
+
if (flags.from && flags.from !== "cursor" && flags.from !== "head") {
|
|
47
|
+
console.error("--from must be cursor or head");
|
|
48
|
+
process.exit(CHECK_FAILED_EXIT);
|
|
49
|
+
}
|
|
50
|
+
requireConfigured();
|
|
51
|
+
|
|
52
|
+
const args = ["watch"];
|
|
53
|
+
if (flags.project) args.push("--project", flags.project);
|
|
54
|
+
if (flags.team) args.push("--team", flags.team);
|
|
55
|
+
for (const t of flags.ticket ?? []) args.push("--ticket", t);
|
|
56
|
+
if (flags.exec) args.push("--exec", flags.exec);
|
|
57
|
+
if (flags.from) args.push("--from", flags.from);
|
|
58
|
+
if (flags["cursor-file"]) args.push("--cursor-file", flags["cursor-file"]);
|
|
59
|
+
|
|
60
|
+
const code = await execCli(args);
|
|
61
|
+
process.exit(code);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: what-needs-me
|
|
3
|
+
description: >-
|
|
4
|
+
The human's decision inbox on a Catalyst Cloud tenant, and the one way an agent raises a decision on their behalf. Use when the person asks "what needs me?", "what am I blocking?", or when active work is gated on a choice only they can make. Lists open asks ranked by what each answer releases, files an ask through the cloud's ask route with the tenant's own template, and records the answer so the held work releases. Never answers as the human, never duplicates an open ask.
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
|
+
---
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
|
|
9
|
+
|
|
10
|
+
# What needs me
|
|
11
|
+
|
|
12
|
+
An ask is one decision only the human can make, filed as a ticket in their own Linear so that question, options, default, answer and who answered are one record. This skill reads that inbox ranked by blast radius, raises a new ask when work is gated, and settles the answer so the held work releases. The cloud renders the ask body from the tenant's template, applies the ask labels and creates the blocking relations; you pass fields, never headings.
|
|
13
|
+
|
|
14
|
+
## Run first
|
|
15
|
+
|
|
16
|
+
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 or an argument was missing.
|
|
17
|
+
|
|
18
|
+
- `node scripts/inbox.mjs --help` — the open asks, ranked by the priority-weighted work each holds; `--json` for the rows.
|
|
19
|
+
- `node scripts/raise.mjs --help` — file one decision: `--team`, `--title`, `--option` (repeated), `--default`, and `--blocks` (repeated) or `--nothing-to-block`.
|
|
20
|
+
- `node scripts/settle.mjs --help` — record the answering comment on an ask and post a release note on every ticket it held; `--close` moves the ask to the done slot.
|
|
21
|
+
|
|
22
|
+
## Load on demand
|
|
23
|
+
|
|
24
|
+
| when | read |
|
|
25
|
+
| -- | -- |
|
|
26
|
+
| about to file an ask, or unsure whether something is one | `references/raising-a-decision.md` |
|
|
27
|
+
| presenting the inbox, or the person asks what "waiting on me" means | `references/reading-the-inbox.md` |
|
|
28
|
+
| an answer arrived, anywhere | `references/settling-an-answer.md` |
|
|
29
|
+
|
|
30
|
+
The `catalyst-linear` skill posts the comment that carries an answer given in chat and reads an ask's thread; `whats-happening` explains a held ticket that stays excluded after the answer.
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- **File before proceeding on the default.** Work may run on a default only after the ask exists; a default with no ticket behind it is a guess, not a decision.
|
|
35
|
+
- **One ask per decision.** Read the inbox first; attach new held tickets to an open ask rather than filing a twin. Duplicates split one decision's urgency across rows.
|
|
36
|
+
- **Every ask names what it blocks.** The ranking the human sees is by held work weighted by priority, never by age; an ask that holds nothing does not reach Waiting on me unless you chose `--nothing-to-block` on purpose.
|
|
37
|
+
- **Never answer as the human.** You do not pick an option, close an ask on their behalf, or post in their voice. Their answer is quoted into the ask as the app actor and recorded with its comment id.
|
|
38
|
+
- **A free-text reply is recorded, never interpreted into an option.** If it does not answer the question, ask the one clarification in the thread and leave the ask open.
|
|
39
|
+
- **Escalate inward.** A project owner raises asks for its scope; the desk raises what has no owner; a single stuck ticket or a provider outage is never an ask.
|
|
40
|
+
- **Tenant facts come from the contract.** Team keys, label names, the option cap and the template are read live by the CLI; never restate them.
|
|
41
|
+
- **Cite an identifier only after the create call returned it.**
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "What needs me"
|
|
3
|
+
short_description: "Your decision inbox on Catalyst Cloud, ranked by what each answer releases; raises and settles asks"
|
|
4
|
+
default_prompt: "Use $what-needs-me to show me what is waiting on my decision."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: false
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Raising a decision
|
|
2
|
+
|
|
3
|
+
This reference restates invariants: what an ask is, when it is filed, and what it must carry. The body text, the headings, the option format and the cap on options are tenant facts served by the contract's `askTemplate` block and rendered by the cloud itself; `node scripts/raise.mjs` passes fields and never composes a heading.
|
|
4
|
+
|
|
5
|
+
## What an ask is
|
|
6
|
+
|
|
7
|
+
An ask is one decision only the human can make, filed as a ticket in the tenant's own Linear so that the question, the options, the default, the answer and who answered are one record the next agent can read. The cloud labels it as an ask, excludes it from dispatch (a question is never work), and holds every ticket it blocks until it is answered. It appears in the human's Waiting-on-me view when it is assigned to them and blocks open work.
|
|
8
|
+
|
|
9
|
+
## When to file one
|
|
10
|
+
|
|
11
|
+
File an ask when active work is gated on a product call, a priority call between two things that cannot both go first, an approval, or an action only the human can physically take (a click in settings, a credential, a payment). File it **before** proceeding on the default, never after, and never only as a TODO line, a board row, a handoff note or a chat question. Those may point at the ask's identifier; they never replace it.
|
|
12
|
+
|
|
13
|
+
Do not file one for brainstorming, a design back-and-forth, a question the human asked first, a retry-or-abandon call a project owner can make, or a system-level failure (a provider down, out of capacity, rate-limited: that is one status line, and the affected tickets retry on their own).
|
|
14
|
+
|
|
15
|
+
## What it carries
|
|
16
|
+
|
|
17
|
+
1. **The question**, one sentence, as the title (`--title`).
|
|
18
|
+
2. **Context** the human needs to answer without opening anything else (`--context`), short.
|
|
19
|
+
3. **Options**, each one line, realistic, at most the number the contract allows (`--option`, repeated). The cloud letters and formats them.
|
|
20
|
+
4. **The default if silent** (`--default`): what proceeds and after how long. It must be sane enough to actually run. Note that nothing in the cloud applies the default on a timer; the raising agent applies it, after the ask exists, and records that it did.
|
|
21
|
+
5. **What it blocks** (`--blocks`, repeated): every ticket held until the answer lands. The cloud creates the blocking relations atomically with the ticket. An ask that holds nothing is refused unless you say `--nothing-to-block` on purpose, because an ask with no blocking relation never surfaces in Waiting on me and is indistinguishable from ordinary work.
|
|
22
|
+
|
|
23
|
+
`--ask-key` is an idempotency key: a re-run with the same key does not file a second ask.
|
|
24
|
+
|
|
25
|
+
## One ask per decision
|
|
26
|
+
|
|
27
|
+
Run `node scripts/inbox.mjs` first and read the titles. When the same decision is already open, attach the new held tickets to it rather than filing again (the `catalyst-linear` skill adds the relation or a comment naming them). Duplicates split one decision's urgency across several rows and sink it below trivia in the ranking.
|
|
28
|
+
|
|
29
|
+
## Who raises, and where
|
|
30
|
+
|
|
31
|
+
- A decision inside a project scope is raised by that project's owner; the desk raises what has no owner.
|
|
32
|
+
- The ask is filed on the team the held work belongs to (`--team`), from the contract's team list. An approvals team, when the contract names one, is where an approval with no natural team goes.
|
|
33
|
+
- Never raise an ask on someone else's behalf about their own scope, and never answer one for the human.
|
|
34
|
+
|
|
35
|
+
## After filing
|
|
36
|
+
|
|
37
|
+
Cite the identifier the script printed, and only that. Proceed on the default if the work allows it, and say so in the ask's thread (a bookkeeping comment) so the record shows the default was taken. When the answer arrives, `references/settling-an-answer.md`.
|
|
38
|
+
|
|
39
|
+
## Ranking
|
|
40
|
+
|
|
41
|
+
The human sees asks ranked by how much open work each holds, weighted by that work's priority (urgent counts most), never by age. That is why `--blocks` must be complete: an ask that names one held ticket when it really holds a project sinks below a chore.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Reading the inbox
|
|
2
|
+
|
|
3
|
+
This reference restates invariants: what "waiting on me" means, how declared and inferred asks differ, and how to present the queue. The label that marks an ask, its prefix, and the release label are the contract's `vocabulary`; `node scripts/inbox.mjs` reads them from the CLI and never from prose.
|
|
4
|
+
|
|
5
|
+
## What waiting-on-me means
|
|
6
|
+
|
|
7
|
+
The tenant's own Waiting-on-me view lists a ticket for a person when all four hold:
|
|
8
|
+
|
|
9
|
+
1. it is **assigned** to that person;
|
|
10
|
+
2. it has **no delegate** (a ticket delegated to an agent is the agent's, not the human's);
|
|
11
|
+
3. it is still **open**;
|
|
12
|
+
4. what it **blocks** reaches open work: at least one ticket it holds is itself not done or canceled.
|
|
13
|
+
|
|
14
|
+
A ticket that carries the ask marker label and is assigned to the person shows even with zero blocks. That is the **declared** ask. The four-part rule above is the **inferred** one, and the view offers both modes: asks only (the default) and everything the person is holding up, minus items an open unblock ask already covers.
|
|
15
|
+
|
|
16
|
+
So an ask that holds nothing is real, but the person may never see it in their own view. `inbox.mjs` lists such asks last and marked, so you can attach the work they should block.
|
|
17
|
+
|
|
18
|
+
## What the script reads
|
|
19
|
+
|
|
20
|
+
`catalyst-skills ask list` reads the open tickets carrying the team's ask label (by the label ids and prefix the contract serves), follows each one's blocking relations to the tickets it holds, drops held tickets that are already terminal, and scores the rest by priority weight: urgent 4, high 3, medium 2, low 1, none 1. It does not filter by assignee, because the account key reads the tenant, not one person. When the human asks "what needs me", the answer is the ranked list; when they ask "what needs anyone", it is the same list.
|
|
21
|
+
|
|
22
|
+
## Presenting the queue
|
|
23
|
+
|
|
24
|
+
- Ranked by score, highest first. Never by age, never by identifier. An ask that holds a project outranks one that holds a chore, whatever their dates.
|
|
25
|
+
- One line per ask: rank, identifier, what it holds (identifiers), the question. The human decides from the question and the held work, so both must be on the line.
|
|
26
|
+
- Asks that hold nothing come after a break, marked as not visible in Waiting on me until they block something.
|
|
27
|
+
- When the list is empty, say "nothing needs you" and stop. Do not pad it with suspected asks.
|
|
28
|
+
- A ticket `explain` flagged as `ask_shape_suspected` (its text reads as a decision but it carries no ask label) is mentioned separately with a question mark: it is either an ask the human should label, or a false positive they release with the release label the contract names.
|
|
29
|
+
|
|
30
|
+
## What the inbox is not
|
|
31
|
+
|
|
32
|
+
- It is not the dispatch queue. Held tickets are excluded from dispatch by their blocking relation; answering the ask releases them into the ordinary order.
|
|
33
|
+
- It is not a place to answer. Nothing in this skill picks an option, closes an ask on the human's behalf, or posts in their voice. When they answer in chat, `references/settling-an-answer.md`.
|
|
34
|
+
- It is not a stall detector. A ticket parked by repeated failures, a merge hold, or a coding-account wall is a status question for `whats-happening`, not a decision until someone makes it one by raising an ask.
|
|
35
|
+
|
|
36
|
+
## Free-text and interpreted replies
|
|
37
|
+
|
|
38
|
+
A human reply that names no option is recorded by the cloud and interpreted for display, but never auto-applied and never written back to Linear as a decision. Treat it the same way: read it, ask the human to confirm which option it means if that is unclear, and settle only once a comment on the ask states the answer.
|
|
@@ -0,0 +1,37 @@
|
|
|
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.
|
|
@@ -0,0 +1,47 @@
|
|
|
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. Prints the ranked list; --json prints the CLI's rows unchanged.
|
|
6
|
+
import { mustRun, parseFlags, parseJson, printHelp } from "./lib/cli.mjs";
|
|
7
|
+
|
|
8
|
+
const SPEC = {
|
|
9
|
+
json: { value: false, help: "print the ranked rows as JSON: {identifier, title, state, blocks[], score}" },
|
|
10
|
+
team: { value: true, help: "only asks whose identifier carries this team key" },
|
|
11
|
+
all: { value: false, help: "include asks that hold nothing (by default they are listed last, marked)" },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { help, flags } = parseFlags(process.argv.slice(2), SPEC);
|
|
15
|
+
if (help) {
|
|
16
|
+
printHelp("node scripts/inbox.mjs [--json] [--team K] [--all] [--help]", SPEC, [
|
|
17
|
+
"Rank: the sum over held open tickets of a priority weight (urgent 4 ... low 1, none 1), highest first.",
|
|
18
|
+
"An ask that holds nothing is real but will not appear in the tenant's Waiting-on-me view; it is",
|
|
19
|
+
"listed last with a marker so you can attach the work it should block (references/reading-the-inbox.md).",
|
|
20
|
+
]);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rows = parseJson(mustRun(["ask", "list", "--json"], { quiet: true }).stdout, "ask list");
|
|
25
|
+
const wanted = flags.team ? rows.filter((r) => String(r.identifier).toUpperCase().startsWith(`${flags.team.toUpperCase()}-`)) : rows;
|
|
26
|
+
if (flags.json) {
|
|
27
|
+
process.stdout.write(JSON.stringify(wanted) + "\n");
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
if (wanted.length === 0) {
|
|
31
|
+
process.stdout.write("nothing needs you: no open asks\n");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
const holding = wanted.filter((r) => r.blocks.length > 0);
|
|
35
|
+
const idle = wanted.filter((r) => r.blocks.length === 0);
|
|
36
|
+
let n = 0;
|
|
37
|
+
for (const r of holding) {
|
|
38
|
+
n += 1;
|
|
39
|
+
process.stdout.write(`${n}. ${r.identifier} holds ${r.blocks.length} (weight ${r.score}): ${r.blocks.join(", ")}\n ${r.title}\n`);
|
|
40
|
+
}
|
|
41
|
+
if (idle.length && (flags.all || holding.length === 0)) {
|
|
42
|
+
process.stdout.write(`${holding.length ? "\n" : ""}asks that hold nothing (not shown in Waiting on me until they block something):\n`);
|
|
43
|
+
for (const r of idle) process.stdout.write(` ${r.identifier} ${r.title}\n`);
|
|
44
|
+
} else if (idle.length) {
|
|
45
|
+
process.stdout.write(`\n(${idle.length} more ask${idle.length === 1 ? "" : "s"} hold nothing; --all lists them)\n`);
|
|
46
|
+
}
|
|
47
|
+
process.exit(0);
|