@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,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,64 +0,0 @@
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);
@@ -1,61 +0,0 @@
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);
@@ -1,41 +0,0 @@
1
- ---
2
- name: unstick
3
- description: >-
4
- Get a stuck Catalyst Cloud ticket moving again. Use when the person asks "why is this parked and can you release it?", "unpark this", "get things flowing again", "the outage is over, retry what failed", or hands you a ticket that is not moving. Reads why nothing runs and what holds the ticket through the catalyst-skills CLI, decides whether the recorded cause is fixed, previews the release, releases every governor holding the ticket the right way (or one failure class across a team), and raises an ask only for what a person has to do. Never releases a cause it cannot show changed without saying so.
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
- # Unstick
11
-
12
- You get one stuck ticket, or one set of tickets stuck for the same reason, moving again. The cloud parks or holds a ticket when retrying would only repeat a failure: after repeated failures, when the repair-round cap is spent, when a repair changed nothing, when the same validate failure recurred. Each of those is a governor. Once its cause is fixed, the person's own key releases it; nothing here needs an operator.
13
-
14
- ## Run first
15
-
16
- Scripts are run, never read. Each prints `--help`; exit 2 means this machine is not connected (run `catalyst-skills login`).
17
-
18
- - `node scripts/unstick.mjs <ticket>` — one JSON document: the eligibility explanation, the execution history (every governor holding the ticket with what releases it, and past releases), and a dry-run release showing what a release would clear and refuse. Changes nothing.
19
- - `node scripts/unstick.mjs <ticket> --because "<what changed>"` — the same, then the real release. Add `--retry-unchanged` only when you can say what changed outside what the cloud can see.
20
- - `node scripts/unstick.mjs --class <failure-class> --team <key> [--because "<what changed>"]` — the same for every ticket on one team parked under one failure class (at most 25 per call).
21
-
22
- The verbs underneath are `catalyst-skills explain <ticket>`, `catalyst-skills explain <ticket> --history` and `catalyst-skills release <ticket> --because <text> [--retry-unchanged] [--dry-run]`.
23
-
24
- ## Load on demand
25
-
26
- | when | read |
27
- | -- | -- |
28
- | deciding whether to release, what a refusal means, when to ask a person | `references/playbook.md` |
29
- | what a reason in the explanation means | the `whats-happening` skill's why-is-it-stuck reference, or `how-catalyst-works` |
30
- | a refusal needs a decision or a person's action | the `what-needs-me` skill |
31
- | a refusal names a person's pull request or its review | the `catalyst-github` skill |
32
-
33
- ## Rules
34
-
35
- - **Read before you release.** Explain, then history, then a dry run. A release is never the first call.
36
- - **The cause decides, not the wish to move.** Release when you can name what changed since the ticket was held: a push, a comment that says what to change, an outage that ended, a secret that was rotated, an account that was re-enrolled. Put that sentence in `--because`; it is recorded against your name.
37
- - **Unchanged means say so.** The cloud refuses a release whose cause it cannot see change. Pass `--retry-unchanged` only with a `--because` that names the change it cannot see. If nothing changed, do not release; say so.
38
- - **One release per cause.** If the history shows a release for the same failure and nothing has changed since, a second release buys the same failure. Raise an ask instead.
39
- - **A refusal names its fix.** Do the fix when it is yours to do; everything else is one ask through `what-needs-me`, attached to an existing ask for the same decision. Never close or merge a person's pull request, and never answer an ask as the person.
40
- - **A shared cause is one note.** Several tickets parked by one outage or one bad image are one class release and one line in your reply, never one ask per ticket.
41
- - **Their tenant, as them.** Every read and write goes through the CLI and the person's own login. You never name another tenant.
@@ -1,6 +0,0 @@
1
- interface:
2
- display_name: "Unstick"
3
- short_description: "Find out why a Catalyst ticket is parked or held and release it once its cause is fixed; asks only for what needs a person"
4
- default_prompt: "Use $unstick to find out why <ticket> is stuck and get it moving again."
5
- policy:
6
- allow_implicit_invocation: false
@@ -1,5 +0,0 @@
1
- identity: { pack: catalyst-cloud-skills, skill: unstick }
2
- effects: [external-write]
3
- mutating: true
4
- invocation: explicit
5
- exposure: [catalog]
@@ -1,51 +0,0 @@
1
- # The unstick playbook
2
-
3
- This reference restates how a person's release of a stuck ticket works and the order you follow. The cloud does the deciding: it reads every governor holding the ticket and either releases all of them or releases nothing and names, for each one it refuses, the action that does fix it. Your job is to read, judge whether the cause is fixed, and say so honestly.
4
-
5
- ## The order
6
-
7
- 1. **Why is nothing running?** `catalyst-skills explain <ticket>`. If the reason is not a park or a hold (for example `blocked`, `not_at_dispatch_stage`, an ask, a missing environment check, `scope_overlap`), there is nothing to release: follow the `whats-happening` skill's why-is-it-stuck reference instead and stop here.
8
- 2. **What holds it, and has it been released before?** `catalyst-skills explain <ticket> --history`. Read three things: the "Held by" lines (each governor and what releases it), the last failure (its class and summary), and the "Releases" lines (who released it before, why, and what happened).
9
- 3. **Is the cause fixed?** Decide from evidence, in this order:
10
- - a push to the ticket's branch, a comment on the ticket that says what to change, or main moving on since the park: the cloud can see these, and a release goes through;
11
- - a change the cloud cannot see (a provider outage that ended, a rotated secret, a re-enrolled coding account, a fixed tenant setting): release with `--retry-unchanged`, and the reason you give names that change;
12
- - nothing changed: do not release. Tell the person what failed and what would have to change.
13
- If a previous release in the history named the same failure and nothing has changed since, the next release will fail the same way. Do not release again; raise an ask.
14
- 4. **Preview.** `catalyst-skills release <ticket> --dry-run`. It prints what a release would clear and what it would refuse, and changes nothing.
15
- 5. **Release.** `catalyst-skills release <ticket> --because "<the change>"`. Report what it released, verbatim, with the ticket id. A warning line (for example, that a released repair round runs again at the escalated tier and a second cap parks it again) goes in your reply too.
16
- 6. **Act on each refusal.** A refusal is terminal for that attempt: nothing was released. Each names its fix; the table below says who does it.
17
-
18
- ## What a refusal means and who fixes it
19
-
20
- | refusal | what it means | who acts |
21
- | -- | -- | -- |
22
- | `cause_unchanged` | nothing the cloud can see changed since it was held | you, if you can name a change it cannot see (`--retry-unchanged` with that `--because`); otherwise nobody should release it yet |
23
- | `lease_held` | a phase is running on the ticket right now | nobody; wait for the phase's outcome comment, then look again |
24
- | `branch_still_missing` | the ticket's branch was never pushed | it releases itself when the branch appears; check the earlier phases through `catalyst-linear` |
25
- | `human_owned_pr` | a person opened the pull request this ticket would work on | that person closes or merges it, or hands it to Catalyst; never close a person's pull request yourself |
26
- | `review_not_converging` | review and repair kept finding new problems | a person reads the findings (`catalyst-github`) and comments on the ticket to resume; raise an ask for that read |
27
- | `round_threshold` | the ticket spent its lifetime repair budget | a person answers the ask the cloud already raised, or pushes a fix; point at that ask, do not raise a second one |
28
- | `base_revision_lost` | the ticket's base commit is gone | whoever administers the tenant re-pins it; raise one ask |
29
- | `remediate_cap` | this tenant keeps the repair-round cap for an administrator | whoever administers the tenant; raise one ask |
30
- | `unknown_park` | a park this bundle does not know | raise one ask with the ticket id and the park name, verbatim |
31
- | `held_beyond_class` | a class release reached a ticket held by more than that class | release that ticket on its own, from step 1 |
32
- | `team_changed` (the ticket moved teams during the check) | the release was checked for one team and the ticket is now on another | run the release again |
33
-
34
- Raise an ask only through `what-needs-me`, with the stuck ticket in what it blocks, and attach to an existing ask for the same decision rather than filing a second one.
35
-
36
- ## Several tickets, one cause
37
-
38
- When several tickets on one team are parked for the same failure class (the explanation and the history show the same class, typically an outage or a runner problem that has since ended):
39
-
40
- 1. `catalyst-skills release --class <failure-class> --team <key> --dry-run` to see which tickets it reaches.
41
- 2. `catalyst-skills release --class <failure-class> --team <key> --because "<the change>" --retry-unchanged` when the change is one the cloud cannot see.
42
- 3. Report one line: how many released, which were refused and why, and whether more remain (run it again for the next batch).
43
-
44
- A ticket the class release refuses is listed and left alone; take it through the single-ticket order above.
45
-
46
- ## What a release does not do
47
-
48
- - It does not skip a phase or accept work as done; a released phase runs again.
49
- - It does not reset a lifetime budget. A released repair round still counts toward the ticket's lifetime threshold, and the cloud stops a loop that claims the same unit too often in an hour.
50
- - It does not move the card. The ticket stays where it is and runs when the cloud next offers it.
51
- - It is recorded: who released, with which login, why, what it cleared and what it refused. `explain --history` shows it to everyone who reads the ticket.
@@ -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,57 +0,0 @@
1
- #!/usr/bin/env node
2
- // unstick.mjs — "why is this stuck, and can it be released?" in one JSON document. Runs
3
- // `catalyst-skills explain`, `explain --history` and a dry-run `release` for one ticket (or a dry-run
4
- // class release for one team), and only with --because runs the real release. Reaches the cloud only
5
- // by spawning the CLI.
6
- import { mustRun, parseFlags, parseJson, printHelp, runCli } from "./lib/cli.mjs";
7
-
8
- const SPEC = {
9
- because: { value: true, help: "what changed since the ticket was held; runs the real release after the preview" },
10
- "retry-unchanged": { value: false, help: "release even though nothing the cloud can see changed (say what did in --because)" },
11
- class: { value: true, help: "release every ticket on --team parked under this failure class" },
12
- team: { value: true, help: "with --class: the team key" },
13
- limit: { value: true, help: "with --class: at most this many tickets (the cloud caps it at 25)" },
14
- };
15
-
16
- const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
17
- const ticket = positionals[0];
18
- const byClass = typeof flags.class === "string";
19
- if (help || (!ticket && !byClass) || (byClass && (ticket || typeof flags.team !== "string"))) {
20
- printHelp("node scripts/unstick.mjs <ticket> [--because <text>] [--retry-unchanged] [--help] | --class <c> --team <K> [--because <text>] [--retry-unchanged] [--limit N] [--help]", SPEC, [
21
- "Without --because it changes nothing: it prints the explanation, the history (every governor holding the ticket",
22
- "and past releases) and a dry-run release. With --because it then runs the real release. Read references/playbook.md",
23
- "before passing --because. A refused release exits 1 with the refusals in the document.",
24
- ]);
25
- process.exit(help ? 0 : 1);
26
- }
27
-
28
- const releaseArgs = byClass ? ["release", "--class", flags.class, "--team", flags.team] : ["release", ticket];
29
- if (flags["retry-unchanged"]) releaseArgs.push("--retry-unchanged");
30
- if (byClass && typeof flags.limit === "string") releaseArgs.push("--limit", flags.limit);
31
-
32
- const out = {};
33
- if (!byClass) {
34
- out.explain = parseJson(mustRun(["explain", ticket, "--json"]).stdout, "explain");
35
- out.history = parseJson(mustRun(["explain", ticket, "--history", "--json"]).stdout, "history");
36
- }
37
-
38
- const preview = runCli([...releaseArgs, "--dry-run", "--json"]);
39
- if (preview.code !== 0 && preview.code !== 1) {
40
- process.stderr.write(preview.stderr);
41
- process.exit(preview.code);
42
- }
43
- out.preview = parseJson(preview.stdout, "the dry-run release");
44
-
45
- let exit = 0;
46
- if (typeof flags.because === "string") {
47
- const real = runCli([...releaseArgs, "--because", flags.because, "--json"]);
48
- if (real.code !== 0 && real.code !== 1) {
49
- process.stderr.write(real.stderr);
50
- process.exit(real.code);
51
- }
52
- out.release = parseJson(real.stdout, "the release");
53
- exit = real.code;
54
- }
55
-
56
- process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
57
- process.exit(exit);
@@ -1,41 +0,0 @@
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 assigned to the connected person, ranked by the priority-weighted work each holds; `--anyone` for the whole tenant; `--json` for `{scope, asks}`.
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.**
@@ -1,6 +0,0 @@
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
@@ -1,5 +0,0 @@
1
- identity: { pack: catalyst-cloud-skills, skill: what-needs-me }
2
- effects: [external-write]
3
- mutating: true
4
- invocation: explicit
5
- exposure: [catalog]
@@ -1,41 +0,0 @@
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.
@@ -1,38 +0,0 @@
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 filters to **the connected person** by default: `login` recorded who they are and their Linear user id, and the list keeps only asks assigned to that id. `--anyone` lists the whole tenant. The JSON answer carries a `scope` — `mine` (with the label and Linear id), `anyone`, `unmatched` (the person's Linear identity is not matched yet: an admin does that in Settings → Members, and until then the whole list is shown with a stderr line saying so), or `no-person` (the machine is connected with the tenant's account key, which names nobody: the whole list, with a line saying to log in with a personal key). Read the scope before presenting the list, and say which you got: "what needs me" is the `mine` list, "what needs anyone" is `--anyone`, and an empty `mine` list names the wider count so it never reads as "nothing needs anyone".
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.