@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,43 @@
1
+ ---
2
+ name: catalyst-linear
3
+ description: >-
4
+ Catalyst's view of Linear on the customer's own tenant. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the local replica when it is fresh and the origin-fresh API otherwise, always naming the source; searches tickets, PRs, projects and initiatives; writes comments, card moves, labels and new tickets through the tenant's agent proxy as the app actor, with every route, stage id, label id and marker read from the tenant contract. Knows what a ticket accumulates as Catalyst works it (phase-outcome comments, the document per phase, the agent session, the labels, the single blocks relation, the bookkeeping marker, the eyes acknowledgement). Use when a person says "show me the ticket", "what did Catalyst write on it", "comment on it", "move it", "label it" or "file a ticket". Not for raising a decision for a human (what-needs-me) and not for pull requests (catalyst-github).
5
+ allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
+ disable-model-invocation: true
7
+ ---
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.8.0 — written in this repository for customer tenants -->
9
+
10
+ # Catalyst Linear
11
+
12
+ You read tickets and write to them on the customer's tenant, as Catalyst. Reads come from the replica when it is fresh, else from the API, and every read names its source. Writes go through the tenant's agent proxy as the app actor, with routes, stage ids, label ids and the bookkeeping marker resolved from the tenant contract by the `catalyst-skills` CLI. You never compose a URL, never name a stage by its display name, and never quote a label id or marker from memory.
13
+
14
+ ## Run first
15
+
16
+ Run each with `--help` first; scripts are executed, never read.
17
+
18
+ - `node scripts/read-ticket.mjs <ticket> [--comments]` — one ticket, everything inline, source line on stderr.
19
+ - `node scripts/search.mjs <terms>` — tickets, PRs, projects, initiatives matching the terms.
20
+ - `node scripts/comment.mjs <ticket> --body <text> [--parent <id>] [--bookkeeping]` — a comment as the app actor; a machine record takes `--bookkeeping`.
21
+ - `node scripts/move.mjs <ticket> --slot <slot>` — a card move by slot (`--state-type backlog` parks).
22
+ - `node scripts/label.mjs <ticket> --add <name> --remove <name>` — labels, resolved through the contract.
23
+ - `node scripts/create-ticket.mjs --team <key> --title <text>` — a new ticket; cite its identifier only after it prints.
24
+
25
+ Exit codes: 0 done, 1 not found or a usage error, 2 this machine is not connected or the write was refused (budget spent, slot unmapped, label absent; the one line printed says which).
26
+
27
+ ## Load on demand
28
+
29
+ | when | read |
30
+ | -- | -- |
31
+ | "what did Catalyst write on this ticket?", a comment shape, a label, the marker, the 👀 | `references/what-a-ticket-accumulates.md` |
32
+ | before any read you will report on; "is this current?"; citing; searching | `references/reading-a-ticket.md` |
33
+ | before any comment, move, label or new ticket; identity, budget, slots, ids | `references/writing-to-linear.md` |
34
+
35
+ ## Rules
36
+
37
+ - **Read before acting.** Never summarise a ticket from its title; read the description and the thread. Cite identifiers and comment ids only after a script printed them.
38
+ - **Name the source.** Quote the source line when freshness matters; a stale replica is never read silently, because the CLI falls back to the API and says so.
39
+ - **Reply where it arrived, as Catalyst.** Thread under the comment you answer (`--parent`); never post as the human.
40
+ - **Records carry the marker.** A merge note, a state-move log, a chain summary: `--bookkeeping`, so it never reads as a human turn. A real question is not bookkeeping.
41
+ - **Move by slot, label by contract.** A stage name is display; the id is the authority, and the CLI resolves both. Moving to `dispatch` dispatches; the backlog-type state parks.
42
+ - **A decision is an ask, not a ticket.** Raise it through `what-needs-me` with options, a default and what it blocks; a question filed as a ticket is held out of dispatch by shape.
43
+ - **One write per need, no loops.** Writes spend a daily budget the contract names; batch, and report a refusal rather than retrying.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "Catalyst Linear"
3
+ short_description: "Read a ticket and what Catalyst wrote on it; comment, move, label and file tickets as the app actor"
4
+ default_prompt: "Use $catalyst-linear to show me the ticket, its history, and what Catalyst wrote on it."
5
+ policy:
6
+ allow_implicit_invocation: false
@@ -0,0 +1,5 @@
1
+ identity: { pack: catalyst-cloud-skills, skill: catalyst-linear }
2
+ effects: [external-write]
3
+ mutating: true
4
+ invocation: explicit
5
+ exposure: [catalog]
@@ -0,0 +1,52 @@
1
+ # Reading a ticket: freshness first, then the row
2
+
3
+ This reference restates invariants about how a read is made and reported. Nothing in it is a tenant value.
4
+
5
+ ## Freshness first
6
+
7
+ Every `query` verb, and therefore `node scripts/read-ticket.mjs` and `node scripts/search.mjs`, prints one stderr line before the answer:
8
+
9
+ - `source: replica (cursor N)` — the local replica was fresh (its writer heartbeat is young and its cursor non-empty), so the answer is local and cheap. N is the position it had applied.
10
+ - `source: api (replica stale)`, `source: api (replica absent)` or `source: api (replica not configured)` — the answer came from the tenant API, which is origin-fresh: it reflects the mirror's latest ingest, not a cached copy.
11
+ - `source: api (<verb> is api-only)` — search, cycles, pull detail and change feeds are never served from the replica.
12
+
13
+ Read that line every time. A fresh replica is a preference; the API is the fallback and it is never wrong to read it. What is wrong is silently reading a stale replica, and the CLI does not let that happen: a stale replica falls back to the API and says so. Quote the source in your answer when the freshness of a fact matters ("as of the replica at cursor N" or "from the API just now").
14
+
15
+ `--source replica` or `--source api` forces one. Forcing the replica when it is absent or stale is refused.
16
+
17
+ ## Then the row
18
+
19
+ One ticket read returns the whole record: the fields, the description, `labels[]`, `relations[]`, `linked_pulls[]`, `comments[]`, `activity[]` and `agent_sessions[]` inline, plus the project, cycle, team, delegate and parent fields. One call answers "what is this ticket, what happened on it, what is it waiting on, which PR is it". There is no second call to make for the comments.
20
+
21
+ The summary the script prints puts the counts first and the description last; `--comments` prints every comment with its id, author, time and whether the author is a bot. A comment with a `reply-to` marker is threaded under another; reply under the same parent when you answer it.
22
+
23
+ ## Reading Catalyst's own writes
24
+
25
+ The comment shapes in `references/what-a-ticket-accumulates.md` tell you which comments are the cloud's. To answer "what happened to this ticket":
26
+
27
+ 1. Phase-outcome comments, newest first, give the attempts and their results.
28
+ 2. Remediate-attempt comments give the failure class each round repaired.
29
+ 3. The projection-link comments name the documents; open the document (it is attached to the ticket) to read what a phase actually concluded. A fallback comment carries the body inline instead.
30
+ 4. `linked_pulls[]` names the PR; `catalyst-github` reads it.
31
+ 5. For "what will it do next", leave this skill: `how-catalyst-works` explains the eligibility row.
32
+
33
+ ## When to read a transcript
34
+
35
+ A phase's transcript (the full session log) exists in the cloud per ticket, but this bundle has no verb for it yet. Read the artifact document first; it is the phase's own account of what it did. Reach for the transcript only when the document leaves the question open, and say that the bundle cannot fetch it so the human opens it from the ticket's attachments.
36
+
37
+ ## What the ledger adds to the comments
38
+
39
+ Per-ticket execution history beyond the comments (the attempt ledger, the remediation round count against the cap, park state and what releases it) is `catalyst-skills explain --history <ticket>`, read from the cloud's own relay ledger. Do not reconstruct a round count from comments and present it as the cap's count; read it.
40
+
41
+ ## How to cite
42
+
43
+ - Cite a ticket by its identifier (`KEY-123`), never by title alone, and only after you read it back from a script's output.
44
+ - Cite a comment by its id when you refer to it or reply under it.
45
+ - Cite a document by the title the projection comment gave it.
46
+ - Never summarise a ticket from its title. Read the description and the thread.
47
+
48
+ ## Searching and listing
49
+
50
+ `node scripts/search.mjs <terms>` matches ticket identifiers and titles, PR titles, project and initiative names and returns a few of each. It is the only search; a list read with a filter is not a search and will hand back the ordinary first page, which reads as a false "not found". Lists (`catalyst-skills query issues --team <key>`, `query projects`, `query cycles`) are for a board view, not for finding one ticket.
51
+
52
+ A list verb now says when the cloud cut it short: without `--all`, `query issues`/`query pulls` print `truncated at N of M` on stderr the moment a scope holds more than one page. `query issues --all` and `query pulls --all` follow the cloud's page cursor to the end of the scope instead of stopping at the first page — use `--all` when the count, not just a sample, has to be right.
@@ -0,0 +1,53 @@
1
+ # What a ticket accumulates as Catalyst works it
2
+
3
+ This reference restates invariants: the shapes of what the cloud writes onto a ticket. The one live value it needs — the bookkeeping marker and the label names — is on the contract under `vocabulary` and `teams[].labels`; print them with `catalyst-skills contract --path vocabulary` and `catalyst-skills contract --path teams`. Never quote a marker or a label id from memory.
4
+
5
+ ## Comments the cloud posts
6
+
7
+ | Kind | When | How it opens |
8
+ | -- | -- | -- |
9
+ | Phase outcome | every phase attempt ends | `✅ **Phase complete**` or `🛑 **Phase FAILED**`, then `**Phase**`, an optional `**Attempt**`, `**Artifact**`, a quoted summary, and a park, hold or failure block when there is one; a footer names the event that produced it |
10
+ | Remediate attempt | every remediation round ends | `🔧 **Remediate attempt N — SUCCEEDED**` or `FAILED`, then `**Class**` (the failure class being repaired) and optionally `**Repairing**` and `**Artifact**` |
11
+ | Projection link | an artifact document was created in Linear | `**<phase>** · attempt N — [<title>](<document url>)`, then the storage key and the thoughts-repository path |
12
+ | Projection fallback | Linear refused a document | `> ⚠️ **Fallback comment.** Linear refused a document for this artifact: <reason>` followed by the artifact body fenced inline |
13
+ | Board health | a ticket sat in an active stage with no sign of life | `**Catalyst Cloud · board health**`, what stalled, and "Action taken" |
14
+ | Merge wait | a hold at PR or merge named a cause | says what the merge is waiting on |
15
+ | Ask reply | a human answered an ask | a threaded reply under the answer |
16
+
17
+ Every one of these is posted by the app actor and is skipped by the comment-wake trigger; none of them wakes an agent.
18
+
19
+ ## One document per phase, attached
20
+
21
+ Each artifact-bearing phase (research, plan, implement, validate, pr, remediate) projects its artifact into Linear as a document at a deterministic id, titled `<TICKET> · <phase> · attempt <n> · <YYYY-MM-DD>`, attached to the ticket, and announced by the projection-link comment above. Research and plan documents on a ticket that belongs to a project are also linked from the project. The same body is committed into the tenant's thoughts repository when one is configured. Reading the document is how you read what a phase concluded; the phase-outcome comment only summarises it.
22
+
23
+ ## The agent session
24
+
25
+ A ticket being worked grows a Linear agent session whose **plan** is the ladder itself: phases before the current one completed, the current one in progress, later ones pending. A remediation round is an interrupt with no ladder position and anchors on the phase it interrupted. Activities record a phase starting, the gate running, an artifact being written, a PR being opened, and a terminal report. A session in a pending, active or awaiting-input status is reused; a stale, complete or errored one is replaced by a fresh session. Emission is best-effort: a failure to narrate never blocks a phase.
26
+
27
+ ## Labels
28
+
29
+ | Role (contract name) | Who applies it | Meaning |
30
+ | -- | -- | -- |
31
+ | ask marker (`vocabulary.askMarkerLabel`, listed under `teams[].labels.ask`) | the cloud, atomically when it files an ask; or a human filing one by hand | this ticket is a question for a human, never work |
32
+ | ask kind family (`vocabulary.askLabelPrefix`, and per-phase `vocabulary.askPhaseLabelPrefix`) | the cloud, or a human | what kind of ask, and which phase raised it |
33
+ | release (`vocabulary.releaseLabel`, listed under `teams[].labels.release`) | **a human only** | releases a ticket the shape detector flagged as an ask; a human-applied ask-kind label outranks it |
34
+ | hold (listed under `teams[].labels.hold`) | the cloud | the marker a team gets instead of a Remediate state move when it has no remediate stage mapped |
35
+ | a local-lane marker | the cloud | a worker outside the cloud holds this ticket; removed at that lane's terminal step |
36
+
37
+ `(absent)` beside a label in `teams[].labels` means the workspace has no such label yet; the CLI refuses to apply a name it cannot resolve.
38
+
39
+ ## Relations
40
+
41
+ `blocks` is the only relation Catalyst creates. It is written when an ask is raised, so the ask blocks every ticket waiting on the answer, and it is the signal that ranks the human's inbox: an ask with nothing to block is refused unless the caller declares nothing is blocked, because such an ask would never surface. No other relation type is written by the cloud.
42
+
43
+ ## The bookkeeping marker
44
+
45
+ A comment that is a machine RECORD (a merge note, a state-move log, a chain summary) rather than a turn in a conversation must begin with the bookkeeping marker from `vocabulary.bookkeeping.marker`. Rules the contract states alongside it: it is a prefix only, never a substring; ASCII case-insensitive; leading whitespace is trimmed first. A marked comment never wakes an agent and never counts as "something happened" for the no-change and validate holds.
46
+
47
+ It matters on the human-attributed path: a record posted with a personal identity is wire-identical to the human typing it and would wake an agent to reply to its own writeup. A comment posted as the app actor already carries a bot actor and is already skipped; `--bookkeeping` on the comment script is still correct there, and harmless.
48
+
49
+ A real question that merely mentions the word is not bookkeeping and still wakes, deliberately.
50
+
51
+ ## The eyes acknowledgement
52
+
53
+ When a genuine human comment lands (not a bot, not agent-authored, not an automation signature, not bookkeeping), the cloud reacts with 👀 and records a durable wake; the record is written even if the reaction fails. When a bot or agent reply observed at ingest is threaded under that comment, the 👀 is removed and the wake resolves. No live session is required for either half. So: reply in-thread, under the comment you are answering, or the acknowledgement never clears.
@@ -0,0 +1,43 @@
1
+ # Writing to Linear: identity, budget, slots, labels
2
+
3
+ This reference restates the write mechanism. The live values it depends on — the route table, the daily write budget, the stage map, the label ids — are read by the CLI from the contract (`routes`, `thresholds`, `teams[].stages`, `teams[].labels`) on every write. Nothing here is a literal to copy.
4
+
5
+ ## Every write is one script
6
+
7
+ | Want | Run |
8
+ | -- | -- |
9
+ | a comment, threaded or top-level | `node scripts/comment.mjs <ticket> --body <text> [--parent <commentId>] [--bookkeeping]` |
10
+ | a card move | `node scripts/move.mjs <ticket> --slot <slot>` or `--state-type backlog` |
11
+ | a label on or off | `node scripts/label.mjs <ticket> --add <name> --remove <name>` |
12
+ | a new ticket | `node scripts/create-ticket.mjs --team <key> --title <text>` |
13
+ | a decision for a human | not here: the `what-needs-me` skill raises an ask with options, a default and what it blocks |
14
+
15
+ Each script wraps one `catalyst-skills write` verb, which posts to the route the contract names, as the tenant's app actor, with the personal key this machine connected with — so the write carries the person's identity for attribution, and an ask names them. No script composes a URL, and none needs a Linear credential of its own.
16
+
17
+ ## App actor versus personal identity
18
+
19
+ Every write goes out as the tenant's Catalyst app actor by default. That identity is what the cloud's own comment-wake trigger recognises as "not a human", so an app-actor comment never wakes an agent to reply to it, never needs the bookkeeping marker to be safe, and reads to the human as Catalyst speaking. `--as-user` switches a comment or a new ticket to the personal identity behind the key; use it only when the human asked for that attribution, and then mark any machine record with `--bookkeeping`, because on that path the record is indistinguishable from the human typing it.
20
+
21
+ Reply where the message arrived: a comment inside a thread is answered with `--parent <commentId>`, in that thread, and never as a new ticket. Never post as the human; you speak as Catalyst, or as yourself by role.
22
+
23
+ ## The daily write budget
24
+
25
+ Every write route spends one unit of a per-host daily budget the contract publishes as `thresholds.hostDailyWriteBudget`; a label call spends one unit whatever the label count; reads spend none. When the budget is spent, the CLI refuses with exit 2 and a line that names the budget and the retry time. Do not retry in a loop; report it, and batch what you can into fewer writes (one comment, not five).
26
+
27
+ ## State moves are by slot, never by name
28
+
29
+ `--slot <slot>` names one of the eleven slots; the CLI resolves it to this team's live state id from the contract. It refuses when the slot is unmapped for the team, and when the mapped state no longer exists in Linear (the map needs fixing in tenant settings first). Moving to `dispatch` is how work is dispatched; nothing is offered from any other column. Backlog is not a slot: `--state-type backlog` resolves the team's first backlog-type state from its live workflow states, and moving a card there parks it and stops remediation rounds. A move never fabricates a state id, and a move by stage name is not offered because names are display fields that survive re-imports while ids do not.
30
+
31
+ ## Labels are by contract id
32
+
33
+ A name the contract lists for the team (the ask marker, the hold label, the release label) resolves to that team's preferred id; when the workspace has no such label the CLI refuses rather than inventing one. Any other value is sent as a label id as given, so a label outside Catalyst's own set needs its id, which the ticket record's `labels[]` shows.
34
+
35
+ Two labels are a human's, not yours: the release label (which frees a ticket the shape detector held) is applied by a human; the hold labels on a pull request are `catalyst-github`'s subject.
36
+
37
+ ## A new ticket takes the team key
38
+
39
+ `create-ticket.mjs --team <key>` names the team by the prefix its identifiers carry; the CLI resolves the team id from the contract and the cloud fences the write to your tenant on the team, not only on the credential. Cite the identifier only after the script prints it; a guessed number is usually a real, unrelated ticket. Do not file a question for a human this way: a ticket whose text reads as a decision request is held out of dispatch by shape until someone releases it, and it would never reach the human's inbox with what it blocks.
40
+
41
+ ## What is never offered
42
+
43
+ Delegation (assigning a ticket to an agent) is an operator action, not a tenant write. Answering an ask on the human's behalf is not a write you make; the human answers, and `what-needs-me` records the acceptance. Editing another actor's comment is not available.
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // comment.mjs — post a comment on a ticket as the tenant's app actor through the agent proxy.
3
+ // A bookkeeping comment (a machine record, not a turn in a conversation) takes --bookkeeping, which
4
+ // prefixes the marker the contract's vocabulary names.
5
+ import { readFileSync } from "node:fs";
6
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
7
+
8
+ const HELP = `Usage: node scripts/comment.mjs <ticket> (--body <text> | --stdin) [--parent <commentId>] [--bookkeeping] [--as-user] [--json]
9
+
10
+ Posts one comment on a ticket. Wraps: catalyst-skills write comment. Spends one unit of the tenant's
11
+ daily write budget.
12
+
13
+ <ticket> the Linear identifier, e.g. KEY-123
14
+ --body <text> the comment body
15
+ --stdin read the body from stdin instead
16
+ --parent <commentId> reply under this comment (reply where the message arrived)
17
+ --bookkeeping this is a machine record: prefix the contract's bookkeeping marker so it
18
+ never reads as a human turn
19
+ --as-user post with the personal identity instead of the app actor (rare; then a
20
+ record needs --bookkeeping to avoid waking an agent)
21
+ --json print the CLI's JSON result
22
+
23
+ Exit 0 posted, 1 on a usage error, 2 when this machine is not connected to a tenant or the write
24
+ was refused (the daily budget is spent, the ticket is unknown, the route is missing; the line
25
+ says which).`;
26
+
27
+ const argv = process.argv.slice(2);
28
+ if (wantsHelp(argv)) {
29
+ console.log(HELP);
30
+ process.exit(0);
31
+ }
32
+ const { flags, positionals } = parseFlags(argv, {
33
+ bool: ["stdin", "bookkeeping", "as-user", "json"],
34
+ value: ["body", "parent"],
35
+ });
36
+ const ticket = positionals[0];
37
+ if (!ticket) usage("comment needs a ticket identifier (see --help)");
38
+ if (!flags.body && !flags.stdin) usage("comment needs --body <text> or --stdin (see --help)");
39
+ if (flags.body && flags.stdin) usage("give --body or --stdin, not both");
40
+
41
+ let stdin;
42
+ const args = ["write", "comment", ticket, "--json"];
43
+ if (flags.stdin) {
44
+ stdin = readFileSync(0, "utf8");
45
+ if (!stdin.trim()) usage("stdin was empty");
46
+ args.push("--stdin");
47
+ } else {
48
+ args.push("--body", flags.body);
49
+ }
50
+ if (flags.parent) args.push("--parent", flags.parent);
51
+ if (flags.bookkeeping) args.push("--bookkeeping");
52
+ if (flags["as-user"]) args.push("--as-user");
53
+
54
+ const r = runCli(args, { stdin });
55
+ exitOnFailure(r);
56
+ relayStderr(r);
57
+ const result = parseJson(r.stdout);
58
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
59
+ else console.log(`comment posted on ${ticket}${result && result.id ? ` (${result.id})` : ""}`);
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // create-ticket.mjs — file a new ticket on a team as the app actor. The team is named by its key;
3
+ // the CLI resolves the team id from the contract. A decision for a human is NOT a ticket filed here:
4
+ // that is an ask, raised through the what-needs-me skill so it carries options, a default and blocks.
5
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
6
+
7
+ const HELP = `Usage: node scripts/create-ticket.mjs --team <key> --title <text> [--label <name|id>]... [--priority <0-4>] [--as-user] [--json]
8
+
9
+ Creates one ticket. Wraps: catalyst-skills write create. Spends one unit of the daily write budget.
10
+
11
+ --team <key> the team key (the prefix of its ticket identifiers)
12
+ --title <text> the ticket title
13
+ --label <name|id> a label to apply (repeatable); Catalyst label names resolve via the contract
14
+ --priority <0-4> Linear priority: 0 none, 1 urgent, 2 high, 3 medium, 4 low
15
+ --as-user create with the personal identity instead of the app actor (rare)
16
+ --json print the CLI's JSON result (carries the new identifier)
17
+
18
+ Never cite the new ticket's identifier until this script has printed it. Do not use this to ask a
19
+ human a question: raise an ask instead (what-needs-me), or the question will look like work.
20
+
21
+ Exit 0 created, 1 on a usage error, 2 when this machine is not connected to a tenant or the write
22
+ was refused (an unknown team key, the daily budget spent; the line says which).`;
23
+
24
+ const argv = process.argv.slice(2);
25
+ if (wantsHelp(argv)) {
26
+ console.log(HELP);
27
+ process.exit(0);
28
+ }
29
+ const { flags } = parseFlags(argv, { bool: ["as-user", "json"], value: ["team", "title", "priority"], repeat: ["label"] });
30
+ if (!flags.team || !flags.title) usage("create-ticket needs --team <key> and --title <text> (see --help)");
31
+
32
+ const args = ["write", "create", "--team", flags.team, "--title", flags.title, "--json"];
33
+ for (const l of flags.label ?? []) args.push("--label", l);
34
+ if (flags.priority !== undefined) args.push("--priority", flags.priority);
35
+ if (flags["as-user"]) args.push("--as-user");
36
+ const r = runCli(args);
37
+ exitOnFailure(r);
38
+ relayStderr(r);
39
+ const result = parseJson(r.stdout);
40
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
41
+ else {
42
+ const ident = result && (result.identifier ?? result.id);
43
+ console.log(`created${ident ? ` ${ident}` : ""} on team ${flags.team}: ${flags.title}`);
44
+ }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // label.mjs — add or remove labels on a ticket. A Catalyst label name (ask, hold, release) resolves
3
+ // to the id the contract lists for this team; anything else is passed through as a label id.
4
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/label.mjs <ticket> [--add <name|id>]... [--remove <name|id>]... [--json]
7
+
8
+ Adds and/or removes labels. Wraps: catalyst-skills write label. One write-budget unit per call
9
+ direction (add, remove), whatever the label count.
10
+
11
+ <ticket> the Linear identifier, e.g. KEY-123
12
+ --add <name|id> a label to add (repeatable)
13
+ --remove <name|id> a label to remove (repeatable)
14
+ --json print the CLI's JSON result
15
+
16
+ Names the contract knows (the ask marker, the hold label, the release label) resolve to this team's
17
+ label id; a name the workspace lacks is refused rather than guessed. Any other value is sent as a
18
+ label id unchanged.
19
+
20
+ Exit 0 written, 1 on a usage error, 2 when this machine is not connected to a tenant or the write
21
+ was refused (a Catalyst label the workspace lacks, the daily budget spent; the line says which).`;
22
+
23
+ const argv = process.argv.slice(2);
24
+ if (wantsHelp(argv)) {
25
+ console.log(HELP);
26
+ process.exit(0);
27
+ }
28
+ const { flags, positionals } = parseFlags(argv, { bool: ["json"], repeat: ["add", "remove"] });
29
+ const ticket = positionals[0];
30
+ if (!ticket) usage("label needs a ticket identifier (see --help)");
31
+ const add = flags.add ?? [];
32
+ const remove = flags.remove ?? [];
33
+ if (add.length === 0 && remove.length === 0) usage("label needs --add and/or --remove (see --help)");
34
+
35
+ const args = ["write", "label", ticket, "--json"];
36
+ for (const a of add) args.push("--add", a);
37
+ for (const x of remove) args.push("--remove", x);
38
+ const r = runCli(args);
39
+ exitOnFailure(r);
40
+ relayStderr(r);
41
+ const result = parseJson(r.stdout);
42
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
43
+ else {
44
+ const bits = [];
45
+ if (add.length) bits.push(`added ${add.join(", ")}`);
46
+ if (remove.length) bits.push(`removed ${remove.join(", ")}`);
47
+ console.log(`${ticket}: ${bits.join("; ")}`);
48
+ }
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ // lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: it spawns the catalyst-skills CLI
3
+ // this machine connected with (the path recorded in customer.json, else npx) and hands back its
4
+ // output. Scripts import it; a person runs it with --help to see what it does. No dependencies.
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
10
+
11
+ export const PACKAGE = "@catalyst-cloud/catalyst-skills";
12
+ export const CONNECT_HINT = CONNECT_COMMAND;
13
+
14
+ /** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME (used by tests) over HOME. */
15
+ export function configPath() {
16
+ const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
17
+ return join(home, ".config", "catalyst-cloud", "customer.json");
18
+ }
19
+
20
+ /** The config, or a one-line reason it could not be read. Never throws. */
21
+ export function loadConfig() {
22
+ const path = configPath();
23
+ if (!existsSync(path)) return { ok: false, reason: `no config at ${path}` };
24
+ try {
25
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
26
+ if (!hasCredential(cfg) || typeof cfg.account !== "string") {
27
+ return { ok: false, reason: `${path} is missing a key or login, or the account` };
28
+ }
29
+ return { ok: true, cfg, path };
30
+ } catch (err) {
31
+ return { ok: false, reason: `${path} is unreadable: ${err instanceof Error ? err.message : String(err)}` };
32
+ }
33
+ }
34
+
35
+ /** Exit 2 with the one line every script prints when this machine is not connected to a tenant. */
36
+ export function notConfigured(reason) {
37
+ console.error(`not connected to a Catalyst Cloud tenant (${reason}) — run: ${CONNECT_HINT}`);
38
+ process.exit(2);
39
+ }
40
+
41
+ /**
42
+ * Run one catalyst-skills verb. Returns { code, stdout, stderr }. The CLI is `node <cliPath>` when
43
+ * the config recorded one that still exists, else `npx @catalyst-cloud/catalyst-skills`.
44
+ * Exits 2 (not configured) before spawning anything when the config is absent or unreadable.
45
+ */
46
+ export function runCli(args, { stdin } = {}) {
47
+ const loaded = loadConfig();
48
+ if (!loaded.ok) notConfigured(loaded.reason);
49
+ const { cfg } = loaded;
50
+ let cmd;
51
+ let argv;
52
+ let shell = false;
53
+ if (typeof cfg.cliPath === "string" && existsSync(cfg.cliPath)) {
54
+ cmd = process.execPath;
55
+ argv = [cfg.cliPath, ...args];
56
+ } else {
57
+ cmd = process.platform === "win32" ? "npx.cmd" : "npx";
58
+ argv = [PACKAGE, ...args];
59
+ shell = process.platform === "win32";
60
+ }
61
+ const r = spawnSync(cmd, argv, {
62
+ encoding: "utf8",
63
+ input: stdin,
64
+ env: process.env,
65
+ shell,
66
+ maxBuffer: 64 * 1024 * 1024,
67
+ });
68
+ if (r.error) {
69
+ console.error(`could not run ${cmd}: ${r.error.message} — re-run ${CONNECT_HINT} to record the CLI path`);
70
+ process.exit(2);
71
+ }
72
+ return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
73
+ }
74
+
75
+ /** Print the CLI's stderr (the source line, the contract line, any refusal) on our stderr. */
76
+ export function relayStderr(r) {
77
+ const text = r.stderr.trimEnd();
78
+ if (text) console.error(text);
79
+ }
80
+
81
+ /** A non-zero CLI result ends the script with the same code, its stdout shown so nothing is lost. */
82
+ export function exitOnFailure(r) {
83
+ if (r.code === 0) return;
84
+ relayStderr(r);
85
+ const out = r.stdout.trimEnd();
86
+ if (out) console.log(out);
87
+ process.exit(r.code);
88
+ }
89
+
90
+ export function parseJson(text) {
91
+ try {
92
+ return JSON.parse(text.trim());
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * A small flag parser: `spec.value` names flags that take a value, `spec.bool` boolean flags,
100
+ * `spec.repeat` value flags that may repeat (collected into arrays). Unknown flags are a usage error.
101
+ */
102
+ export function parseFlags(argv, spec = {}) {
103
+ const value = new Set(spec.value ?? []);
104
+ const bool = new Set(spec.bool ?? []);
105
+ const repeat = new Set(spec.repeat ?? []);
106
+ const flags = {};
107
+ const positionals = [];
108
+ for (let i = 0; i < argv.length; i++) {
109
+ const a = argv[i];
110
+ if (a === "--") {
111
+ positionals.push(...argv.slice(i + 1));
112
+ break;
113
+ }
114
+ if (!a.startsWith("--")) {
115
+ positionals.push(a);
116
+ continue;
117
+ }
118
+ let name = a.slice(2);
119
+ let inline;
120
+ const eq = name.indexOf("=");
121
+ if (eq !== -1) {
122
+ inline = name.slice(eq + 1);
123
+ name = name.slice(0, eq);
124
+ }
125
+ if (bool.has(name)) {
126
+ flags[name] = true;
127
+ } else if (value.has(name) || repeat.has(name)) {
128
+ const v = inline ?? argv[++i];
129
+ if (v === undefined || v === "") usage(`--${name} needs a value`);
130
+ if (repeat.has(name)) (flags[name] ??= []).push(v);
131
+ else flags[name] = v;
132
+ } else {
133
+ usage(`unknown option --${name}`);
134
+ }
135
+ }
136
+ return { flags, positionals };
137
+ }
138
+
139
+ export function usage(message) {
140
+ console.error(message);
141
+ process.exit(1);
142
+ }
143
+
144
+ export function wantsHelp(argv) {
145
+ return argv.includes("--help") || argv.includes("-h");
146
+ }
147
+
148
+ const runDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
149
+ if (runDirectly) {
150
+ if (wantsHelp(process.argv.slice(2)) || process.argv.length <= 2) {
151
+ console.log(
152
+ [
153
+ "lib/cli.mjs — shared helper for this skill's scripts (not a command of its own)",
154
+ "",
155
+ "Reads ~/.config/catalyst-cloud/customer.json and runs the catalyst-skills CLI recorded there",
156
+ `(or npx ${PACKAGE} when no path is recorded). Exit 2 when the machine is not connected.`,
157
+ "",
158
+ `Connect first with: ${CONNECT_HINT}`,
159
+ `Config in use: ${configPath()}`,
160
+ ].join("\n"),
161
+ );
162
+ process.exit(0);
163
+ }
164
+ }
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // move.mjs — move a card by SLOT, never by stage name: the CLI resolves the slot to this team's
3
+ // live state id from the contract and refuses a slot that is unmapped or whose state is gone.
4
+ import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
+
6
+ const HELP = `Usage: node scripts/move.mjs <ticket> (--slot <slot> | --state-type <type>) [--json]
7
+
8
+ Moves a ticket's card. Wraps: catalyst-skills write state. Spends one unit of the daily write budget.
9
+
10
+ <ticket> the Linear identifier, e.g. KEY-123
11
+ --slot <slot> one of the eleven slots: dispatch, intake, research, plan, implement,
12
+ remediate, verify, review, pr, done, canceled. Moving to dispatch is how
13
+ work is dispatched.
14
+ --state-type <type> the team's first state of this Linear type, resolved from its live
15
+ workflow states; use "backlog" to park a card (Backlog is not a slot)
16
+ --json print the CLI's JSON result
17
+
18
+ Exit 0 moved, 1 on a usage error, 2 when this machine is not connected to a tenant or the move
19
+ was refused (the slot is unmapped for the team, its state no longer exists in Linear, the daily
20
+ budget is spent; the line says which).`;
21
+
22
+ const argv = process.argv.slice(2);
23
+ if (wantsHelp(argv)) {
24
+ console.log(HELP);
25
+ process.exit(0);
26
+ }
27
+ const { flags, positionals } = parseFlags(argv, { bool: ["json"], value: ["slot", "state-type"] });
28
+ const ticket = positionals[0];
29
+ if (!ticket) usage("move needs a ticket identifier (see --help)");
30
+ if (!flags.slot && !flags["state-type"]) usage("move needs --slot <slot> or --state-type <type> (see --help)");
31
+ if (flags.slot && flags["state-type"]) usage("give --slot or --state-type, not both");
32
+
33
+ const args = ["write", "state", ticket, "--json"];
34
+ if (flags.slot) args.push("--slot", flags.slot);
35
+ else args.push("--state-type", flags["state-type"]);
36
+ const r = runCli(args);
37
+ exitOnFailure(r);
38
+ relayStderr(r);
39
+ const result = parseJson(r.stdout);
40
+ if (flags.json) console.log(JSON.stringify(result ?? {}));
41
+ else console.log(`${ticket} moved to ${flags.slot ? `slot ${flags.slot}` : `the team's ${flags["state-type"]} state`}`);