@catalyst-cloud/cli 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 (157) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/bin/catalyst-skills.js +8 -0
  5. package/bin/catalyst.js +5 -0
  6. package/bin/launch.js +154 -0
  7. package/dist/args.js +280 -0
  8. package/dist/ask.js +161 -0
  9. package/dist/browser.js +20 -0
  10. package/dist/cli.js +397 -0
  11. package/dist/config.js +241 -0
  12. package/dist/contract-types.js +4 -0
  13. package/dist/contract.js +184 -0
  14. package/dist/detach.js +10 -0
  15. package/dist/environment.js +207 -0
  16. package/dist/errors.js +27 -0
  17. package/dist/events.js +106 -0
  18. package/dist/execution.js +451 -0
  19. package/dist/oauth.js +300 -0
  20. package/dist/pagination.js +76 -0
  21. package/dist/prompt.js +35 -0
  22. package/dist/published.js +79 -0
  23. package/dist/query.js +248 -0
  24. package/dist/ready.js +380 -0
  25. package/dist/release.js +142 -0
  26. package/dist/replica.js +614 -0
  27. package/dist/runtime-store.js +135 -0
  28. package/dist/runtime-verb.js +66 -0
  29. package/dist/runtime.js +87 -0
  30. package/dist/sdk.js +29 -0
  31. package/dist/secret.js +190 -0
  32. package/dist/semver.js +18 -0
  33. package/dist/skill-shape.js +189 -0
  34. package/dist/skills.js +129 -0
  35. package/dist/transport.js +205 -0
  36. package/dist/ts-deps-loader.js +113 -0
  37. package/dist/watch/consumer.js +141 -0
  38. package/dist/watch/cursor-file.js +62 -0
  39. package/dist/watch.js +175 -0
  40. package/dist/write.js +224 -0
  41. package/package.json +60 -0
  42. package/skills/catalyst-github/SKILL.md +35 -0
  43. package/skills/catalyst-github/agents/openai.yaml +6 -0
  44. package/skills/catalyst-github/agents/portability.yaml +4 -0
  45. package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
  46. package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
  47. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
  48. package/skills/catalyst-github/scripts/lib/cli.mjs +103 -0
  49. package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
  50. package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
  51. package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
  52. package/skills/catalyst-linear/SKILL.md +43 -0
  53. package/skills/catalyst-linear/agents/openai.yaml +6 -0
  54. package/skills/catalyst-linear/agents/portability.yaml +5 -0
  55. package/skills/catalyst-linear/references/reading-a-ticket.md +52 -0
  56. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
  57. package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
  58. package/skills/catalyst-linear/scripts/comment.mjs +59 -0
  59. package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
  60. package/skills/catalyst-linear/scripts/label.mjs +48 -0
  61. package/skills/catalyst-linear/scripts/lib/cli.mjs +164 -0
  62. package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
  63. package/skills/catalyst-linear/scripts/move.mjs +41 -0
  64. package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
  65. package/skills/catalyst-linear/scripts/search.mjs +49 -0
  66. package/skills/catalyst-onboard/SKILL.md +57 -0
  67. package/skills/catalyst-onboard/agents/openai.yaml +6 -0
  68. package/skills/catalyst-onboard/agents/portability.yaml +5 -0
  69. package/skills/catalyst-onboard/references/declaring-a-repository.md +23 -0
  70. package/skills/catalyst-onboard/references/skill-sources.md +35 -0
  71. package/skills/catalyst-onboard/references/the-one-path.md +149 -0
  72. package/skills/catalyst-onboard/references/what-a-phase-needs.md +46 -0
  73. package/skills/catalyst-onboard/references/what-the-browser-owns.md +50 -0
  74. package/skills/catalyst-onboard/references/who-fixes-what.md +44 -0
  75. package/skills/catalyst-onboard/scripts/lib/cli.mjs +117 -0
  76. package/skills/catalyst-onboard/scripts/lib/credential.mjs +29 -0
  77. package/skills/catalyst-onboard/scripts/where-am-i.mjs +345 -0
  78. package/skills/catalyst-setup/SKILL.md +36 -0
  79. package/skills/catalyst-setup/agents/openai.yaml +6 -0
  80. package/skills/catalyst-setup/agents/portability.yaml +4 -0
  81. package/skills/catalyst-setup/references/what-each-check-means.md +88 -0
  82. package/skills/catalyst-setup/scripts/check.mjs +75 -0
  83. package/skills/catalyst-setup/scripts/lib/cli.mjs +103 -0
  84. package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
  85. package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
  86. package/skills/connect-me/SKILL.md +63 -0
  87. package/skills/connect-me/agents/openai.yaml +6 -0
  88. package/skills/connect-me/agents/portability.yaml +5 -0
  89. package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
  90. package/skills/connect-me/scripts/lib/cli.mjs +185 -0
  91. package/skills/connect-me/scripts/lib/credential.mjs +29 -0
  92. package/skills/connect-me/scripts/verify-connection.mjs +68 -0
  93. package/skills/how-catalyst-works/SKILL.md +43 -0
  94. package/skills/how-catalyst-works/agents/openai.yaml +6 -0
  95. package/skills/how-catalyst-works/agents/portability.yaml +4 -0
  96. package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
  97. package/skills/how-catalyst-works/references/stages-and-mapping.md +56 -0
  98. package/skills/how-catalyst-works/references/the-ladder.md +41 -0
  99. package/skills/how-catalyst-works/references/what-catalyst-is.md +30 -0
  100. package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
  101. package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
  102. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
  103. package/skills/how-catalyst-works/scripts/lib/cli.mjs +164 -0
  104. package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
  105. package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
  106. package/skills/how-catalyst-works/scripts/whats-running.mjs +65 -0
  107. package/skills/run-this-project/SKILL.md +45 -0
  108. package/skills/run-this-project/agents/openai.yaml +6 -0
  109. package/skills/run-this-project/agents/portability.yaml +5 -0
  110. package/skills/run-this-project/assets/stall-policy.json +15 -0
  111. package/skills/run-this-project/references/making-work-ready.md +60 -0
  112. package/skills/run-this-project/references/reacting-to-events.md +76 -0
  113. package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
  114. package/skills/run-this-project/scripts/lib/cli.mjs +185 -0
  115. package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
  116. package/skills/run-this-project/scripts/make-ready.mjs +64 -0
  117. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  118. package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
  119. package/skills/unstick/SKILL.md +41 -0
  120. package/skills/unstick/agents/openai.yaml +6 -0
  121. package/skills/unstick/agents/portability.yaml +5 -0
  122. package/skills/unstick/references/playbook.md +51 -0
  123. package/skills/unstick/scripts/lib/cli.mjs +135 -0
  124. package/skills/unstick/scripts/lib/credential.mjs +29 -0
  125. package/skills/unstick/scripts/unstick.mjs +57 -0
  126. package/skills/what-needs-me/SKILL.md +41 -0
  127. package/skills/what-needs-me/agents/openai.yaml +6 -0
  128. package/skills/what-needs-me/agents/portability.yaml +5 -0
  129. package/skills/what-needs-me/references/raising-a-decision.md +41 -0
  130. package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
  131. package/skills/what-needs-me/references/settling-an-answer.md +37 -0
  132. package/skills/what-needs-me/scripts/inbox.mjs +56 -0
  133. package/skills/what-needs-me/scripts/lib/cli.mjs +135 -0
  134. package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
  135. package/skills/what-needs-me/scripts/raise.mjs +53 -0
  136. package/skills/what-needs-me/scripts/settle.mjs +73 -0
  137. package/skills/whats-happening/SKILL.md +43 -0
  138. package/skills/whats-happening/agents/openai.yaml +6 -0
  139. package/skills/whats-happening/agents/portability.yaml +4 -0
  140. package/skills/whats-happening/assets/status-reply.json +77 -0
  141. package/skills/whats-happening/references/reading-the-board.md +43 -0
  142. package/skills/whats-happening/references/reprioritising.md +37 -0
  143. package/skills/whats-happening/references/routing-work.md +36 -0
  144. package/skills/whats-happening/references/status-reply.md +34 -0
  145. package/skills/whats-happening/references/why-is-it-stuck.md +62 -0
  146. package/skills/whats-happening/scripts/explain.mjs +28 -0
  147. package/skills/whats-happening/scripts/lib/cli.mjs +135 -0
  148. package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
  149. package/skills/whats-happening/scripts/snapshot.mjs +149 -0
  150. package/vendor/README.md +9 -0
  151. package/vendor/paths/index.d.ts +85 -0
  152. package/vendor/paths/index.js +148 -0
  153. package/vendor/paths/legacy-installer.d.ts +36 -0
  154. package/vendor/paths/legacy-installer.js +154 -0
  155. package/vendor/paths/node.d.ts +18 -0
  156. package/vendor/paths/node.js +102 -0
  157. package/vendor/paths/provenance.json +17 -0
@@ -0,0 +1,53 @@
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);
@@ -0,0 +1,73 @@
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);
@@ -0,0 +1,43 @@
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@0.8.0 — 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.**
@@ -0,0 +1,6 @@
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
@@ -0,0 +1,4 @@
1
+ identity: { pack: catalyst-cloud-skills, skill: whats-happening }
2
+ effects: []
3
+ invocation: implicit
4
+ exposure: [catalog]
@@ -0,0 +1,77 @@
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
+ }
@@ -0,0 +1,43 @@
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.
@@ -0,0 +1,37 @@
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.
@@ -0,0 +1,36 @@
1
+ # Routing work
2
+
3
+ This reference restates an invariant: what this skill does when the person asks for something to be done rather than known, and where a decision goes. Nothing here is tenant-specific.
4
+
5
+ ## Three kinds of request
6
+
7
+ A message from the person is one of three things, and you decide which in the first sentence of your reply:
8
+
9
+ 1. **A question.** "Where is X? Why is Y stuck? What closed today?" Answer it in the one-reply shape (`references/status-reply.md`). No routing.
10
+ 2. **A request for work.** "Get X done. Ship the Y change. Own this until it closes." Turn it into a project with an owner, in one pass, as below.
11
+ 3. **A decision only they can make**, surfaced by you or by the cloud. That is never answered here; it goes to `what-needs-me`.
12
+
13
+ A request that contains a decision is both: route the work and raise the decision, and say in the reply that you did both.
14
+
15
+ ## A request becomes a project with an owner, in one pass
16
+
17
+ You hold no authority over project owners. Your job is to make the work visible and hand it to a single-threaded owner, then get out of the way.
18
+
19
+ 1. **Find the existing home first.** `catalyst-skills query projects` and `query search <terms>`. A request that fits an open project is a ticket in that project, not a new project. Duplicate projects split one goal's status across two places.
20
+ 2. **Scaffold when there is no home.** State the outcome in one sentence, the first ticket or two, and who owns it. Tickets are created through the `catalyst-linear` skill (the app actor, the team key from the contract); a project itself is created by the person in Linear, so name what you want it called and ask them to create it if none fits.
21
+ 3. **Name the owner.** The owner is a `run-this-project` session for that project, or a person. Say which, in the reply. Tell the person the one command that starts the owner session, and do not start long-running work inside this session: the desk answers questions; the owner reacts to events.
22
+ 4. **Say what you did.** The reply ends with the identifiers created, the owner named, and the next thing the person will see.
23
+
24
+ Cite an identifier only after the create call returned it. A guessed number is usually a real, unrelated ticket.
25
+
26
+ ## What goes to what-needs-me
27
+
28
+ Anything that gates active work on a choice only the human can make: a product call, a priority call between two things that cannot both go first, an approval, or an action only they can physically take (a click in settings, a credential). It is raised as an ask through the `what-needs-me` skill with the question, the options, the default that fires if they stay silent, and what it blocks, and it is raised **before** anyone proceeds on the default.
29
+
30
+ Not an ask: brainstorming, a design back-and-forth, a question the person asked first, a retry-or-abandon call an owner can make, a provider outage (that is one status line, not a per-ticket question).
31
+
32
+ ## Three rules that bind this skill
33
+
34
+ - **Never answer as the human.** You do not pick an option on an open ask, close one, or post in their voice. When their answer arrives in chat, it is recorded on the ask through `what-needs-me` so the record is complete, and it is recorded as the app actor, never as them.
35
+ - **Escalate inward, never outward.** An instrument reports to the project owner; the owner asks the desk; the desk asks the human, as an ask. A single stuck ticket is never a page to the human. A system-level failure (provider down, out of capacity, rate-limited) is one line in the status reply, not a question per ticket.
36
+ - **One door.** If the person needs a second place to look after your reply, add the missing block to the reply next time rather than pointing them at a dashboard.
@@ -0,0 +1,34 @@
1
+ # The one-reply shape
2
+
3
+ This reference restates an invariant of this skill: how a status answer is shaped. The facts inside it come live from `node scripts/snapshot.mjs` and `node scripts/explain.mjs`; the machine-readable schema is `assets/status-reply.json`.
4
+
5
+ ## The rule
6
+
7
+ The person asked one question and gets one reply. If they would need a second surface, a second message, or a follow-up question from you to know where things stand, the reply is wrong. Every line names a ticket identifier. Nothing in the reply is a guess: a fact you could not read is named as unreadable, with where it lives.
8
+
9
+ ## The five blocks, in this order
10
+
11
+ 1. **In flight.** Tickets a phase is running on right now, or that hold a live lease mid-ladder. Source: the snapshot's `running` block (fleet activity, the agent roster, lease attributions). One line per ticket: identifier, the phase running, how long it has run.
12
+ 2. **Blocked, and on whom.** Tickets nothing is offered for. Source: the queue's excluded rows, then `explain` on each one the person cares about. Every line carries the reason in the person's words and who releases it: the human (an ask), a role, the cloud itself (a backoff, a park that self-releases), or a clock. "On whom" is never blank; if you cannot tell, say the reason the cloud gave and that the release is unknown to a key.
13
+ 3. **Waiting on the human.** The open asks, ranked by what each one holds. Source: `waitingOnHuman` in the snapshot. Keep this to identifier, the question, and what it releases; the `what-needs-me` skill owns the detail and the settling.
14
+ 4. **Closed.** What reached the done slot in the window the person asked about (or since your last reply). Source: `query issues` filtered by the team's done-slot stage name from the contract, or the change feed for a time window. When you did not read a window, say "since my last reply" and mean it.
15
+ 5. **Next.** What the queue picks up next, in the cloud's order, with the phase each will run. Source: the snapshot's `queue` block. Do not reorder it to what you think should be next; the levers are in `references/reprioritising.md`.
16
+
17
+ A sixth block, **cannot see**, appears only when it is non-empty: the facts your key cannot read (PR labels and reactions, which are not mirrored; an operator-only park release), each with the URL the CLI printed.
18
+
19
+ ## The header line
20
+
21
+ The reply opens with one clause that says when and from where: the snapshot's `takenAt` and its source line. When the replica was stale or absent, the reply says the numbers came from the API; when it was fresh, it says the cursor. This is not decoration. A stale source that goes unmentioned is the way a wrong status reply happens.
22
+
23
+ ## Writing the lines
24
+
25
+ - Identifier first, then the stage as the contract spells it for that team, then one clause. `KEY-123 · <stage name> · implement running 14 min`.
26
+ - Age in human units (minutes, hours, days), from `updated_at` or the lease's start, never a raw timestamp.
27
+ - A reason is the translation from `references/why-is-it-stuck.md`, not the cloud's snake_case token, unless the token is one the table does not know, in which case quote it as the cloud spelled it.
28
+ - No adjectives about health. "Stuck" has a definition (`references/reading-the-board.md`); use it only when it applies.
29
+
30
+ ## What the reply never does
31
+
32
+ - It never restates a stage name, threshold or label from memory. The snapshot's `tenant` block carries the live values; read them there each time.
33
+ - It never answers an ask, proposes a default on the human's behalf inside the status reply, or moves anything. Routing a request is `references/routing-work.md`; a decision is the `what-needs-me` skill.
34
+ - It never pads a short answer. When one ticket is in flight and nothing is blocked, the reply is three lines.
@@ -0,0 +1,62 @@
1
+ # Why is it stuck
2
+
3
+ This reference restates an invariant: the vocabulary the cloud's eligibility explainer uses when nothing is offered for a ticket, translated for a person, with what releases each. `node scripts/explain.mjs <ticket>` prints the reason for one ticket; this table turns it into the next action. The `how-catalyst-works` skill carries the mechanism behind each row.
4
+
5
+ ## How to read a reason
6
+
7
+ `explain` prints one paragraph: the queue position (or none), the status, the reason, a detail or marker when the cloud gave one, the last failure, and any advisories. Match the reason to a row below. "Releases itself" means the cloud clears it with no human action; "needs" names who acts. When a reason is not in this table, `explain` prints it as the cloud spelled it; report it verbatim and say the bundle does not know it.
8
+
9
+ ## Reasons that release themselves
10
+
11
+ | reason | what it means | how it clears |
12
+ | -- | -- | -- |
13
+ | `retry_backoff` | the failed phase is retrying in place and waiting out its rung | a clock; the rungs are the contract's `thresholds.retryBackoffMs` |
14
+ | `lease_held` | a live container already holds this phase | the phase finishes |
15
+ | `intake_lease_held` | a later phase is offered while an intake container still holds the ticket | intake finishes |
16
+ | `environment_check_running` | the repository's environment check is in flight | the check finishes |
17
+ | `runner_image_breaker` | fleet-wide: the live runner image fails every phase at startup, so dispatch is held rather than parking tickets | the cloud moves the pin; one alert per tenant, no per-ticket action |
18
+ | `routing_unavailable` | claimed, then refused at kickoff: no route, no eligible coding-account slot, or the provider is unavailable | provider recovery or a slot freeing; if it persists, the tenant admin checks coding accounts in settings |
19
+ | `cooling_down` (with a named callback) | the phase is parked on a condition the cloud watches | the callback fires; the `detail` names it |
20
+
21
+ ## Reasons that need a human
22
+
23
+ | reason | what it means | who acts and how |
24
+ | -- | -- | -- |
25
+ | `ask_ticket` | it carries an ask label; a question is never work | the human answers the ask (`what-needs-me`) |
26
+ | `ask_shape_suspected` | its own text reads as a decision request though it carries no ask label | the human either answers it as an ask or applies the release label the contract names |
27
+ | `blocked` | a live blocking relation holds it | whoever owns the blocker closes it; if the blocker is an ask, that is the human |
28
+ | `not_at_dispatch_stage` | the card is not in the team's dispatch column | someone moves the card to the dispatch slot (`references/reprioritising.md`) |
29
+ | `not_at_pr_stage` | merge is next but the card is not in the PR column | the card returns to the pr slot; an automation that bounced it should be found |
30
+ | `no_change_hold` | a repair round changed nothing | a human comment on the ticket, or a new push to the branch |
31
+ | `waiting_on` | a merge-gate failure with no cause the cloud can repair | read the merge-wait comment on the ticket; usually a hold label or a review a person must give (`catalyst-github`) |
32
+ | `externally_claimed` | a worker outside the cloud holds it | that worker, or the human removes the local-lane label |
33
+ | `environment_check_required` / `_failed` / `_expired` / `_hash_mismatch` | the repository's environment gate has not passed | the tenant admin runs or fixes the environment check in settings |
34
+ | `repo_paused` | an operator paused the repository | the operator resumes it |
35
+ | `scope_overlap` | its declared file scope intersects a ticket already in flight | wait for the other ticket, or the human decides which goes first |
36
+
37
+ ## Reasons a release clears once the cause is fixed
38
+
39
+ These do not release themselves, and nobody but the person needs to act: once the recorded cause is fixed, the person's own login releases them with `catalyst-skills release <ticket>`. The desk does not release; route the ticket to the `unstick` skill, which reads the history, previews the release and runs it, or names what a person must do first.
40
+
41
+ | reason | what it means | note |
42
+ | -- | -- | -- |
43
+ | `phase_parked` / `cooling_down` (parked after repeated failures, or the round cap was spent) | the phase is parked and does not release itself | `unstick`; `explain --history` names the park and the failure class it recorded |
44
+ | `remediate_parked` | the repair phase itself is parked, so the failing phase has nowhere to be repaired | as above |
45
+ | `validate_class_spent` | this validate failure already spent its one repair round in this episode | a push or a comment saying what to change releases it on its own; otherwise `unstick` |
46
+ | `human_owned_pr` | a person's own pull request holds the ticket | that person closes or merges it; no release clears it |
47
+ | `review_not_converging` | review and repair kept finding new problems without converging | a person reads the findings and comments on the ticket to resume; raise an ask for that read |
48
+ | `round_threshold` | the ticket spent its lifetime repair budget | a person answers the ask the cloud raised, or pushes a fix |
49
+ | `no_branch_to_remediate` / `branch_missing` / `branch_gone` | a branch-dependent phase has no branch to clone | the cloud releases missing-branch parks on its own budget; a deleted branch needs a human to decide |
50
+ | `stale_failure_episode` | the ladder advanced after the recorded failure | informational; the round is dropped |
51
+
52
+ ## Reasons that are not problems
53
+
54
+ `ticket_terminal` (done or canceled), `pipeline_complete` (every phase ran), and `pr_merged` (the PR merged, nothing left to clone) describe finished work. Report them as closed, not blocked.
55
+
56
+ ## When the cloud could not judge
57
+
58
+ A status of `unknown` with a reason such as `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown` or `branch_snapshot_unknown` means the evaluator failed closed rather than guessing. Most clear on the cloud's next pass. `workflow_mapping_unknown`, and `ordering_never_published` beside it, does not clear by itself when the team has no saved stage mapping: `explain` then names the missing stages, and a tenant owner or admin maps the team in Settings → Linear teams. If any other one persists for a team, the readiness checks (`catalyst-setup`) are the next read.
59
+
60
+ ## The one advisory
61
+
62
+ `human_addressed_unlabeled_ask_suspect` gates nothing: the ticket is assigned to a human with no delegate and may be an unlabelled ask. Mention it in the waiting-on-human block with a question mark.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // explain.mjs — "why is this ticket stuck?" in one paragraph. A thin wrapper over
3
+ // `catalyst-skills explain <ticket>`, which reads the cloud's eligibility explainer for the ticket's
4
+ // team and translates the exclusion reason, the failure block and any advisories into plain English.
5
+ import { mustRun, parseFlags, printHelp } from "./lib/cli.mjs";
6
+
7
+ const SPEC = {
8
+ json: { value: false, help: "print the raw eligibility row and the explanation as JSON" },
9
+ history: { value: false, help: "ask for the per-ticket execution history (the CLI says when a key cannot see it yet)" },
10
+ };
11
+
12
+ const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
13
+ const ticket = positionals[0];
14
+ if (help || !ticket) {
15
+ printHelp("node scripts/explain.mjs <ticket> [--json] [--history] [--help]", SPEC, [
16
+ "The ticket is its identifier (the KEY-123 shape). The explanation names the queue position, the status,",
17
+ "the reason nothing is offered, the last failure, and what releases it; an unknown reason is printed as the",
18
+ "cloud spelled it. Read references/why-is-it-stuck.md to turn the reason into the next action.",
19
+ ]);
20
+ process.exit(help ? 0 : 1);
21
+ }
22
+
23
+ const args = ["explain", ticket];
24
+ if (flags.history) args.push("--history");
25
+ if (flags.json) args.push("--json");
26
+ const r = mustRun(args);
27
+ process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`);
28
+ process.exit(0);