@catalyst-cloud/cli 0.13.0 → 0.14.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 (149) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +27 -30
  3. package/dist/args.js +299 -61
  4. package/dist/ask.js +30 -18
  5. package/dist/capabilities.js +7 -0
  6. package/dist/cli.js +125 -20
  7. package/dist/config.js +3 -1
  8. package/dist/env/draft.js +134 -0
  9. package/dist/env/legacy.js +54 -0
  10. package/dist/env/parse-env.js +50 -0
  11. package/dist/env/settings-toml.js +66 -0
  12. package/dist/env.js +126 -26
  13. package/dist/environment.js +1 -1
  14. package/dist/legacy.js +224 -0
  15. package/dist/onboard-consent.js +65 -0
  16. package/dist/onboard.js +836 -0
  17. package/dist/project.js +191 -0
  18. package/dist/ready.js +11 -1
  19. package/dist/release.js +1 -1
  20. package/dist/repo.js +297 -0
  21. package/dist/runtime-verb.js +1 -1
  22. package/dist/secret.js +25 -4
  23. package/dist/skill-shape.js +21 -13
  24. package/dist/skills.js +44 -4
  25. package/dist/transport.js +26 -1
  26. package/dist/var.js +118 -0
  27. package/package.json +6 -4
  28. package/skills/catalyst-github/SKILL.md +7 -7
  29. package/skills/catalyst-github/references/is-it-mergeable.md +8 -2
  30. package/skills/catalyst-github/references/what-a-pr-accumulates.md +3 -3
  31. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +3 -3
  32. package/skills/catalyst-github/scripts/lib/cli.mjs +9 -7
  33. package/skills/catalyst-github/scripts/lib/credential.mjs +2 -2
  34. package/skills/catalyst-github/scripts/read-pr.mjs +2 -2
  35. package/skills/catalyst-linear/SKILL.md +17 -19
  36. package/skills/catalyst-linear/agents/openai.yaml +1 -1
  37. package/skills/catalyst-linear/agents/portability.yaml +1 -1
  38. package/skills/catalyst-linear/references/reading-a-ticket.md +9 -26
  39. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +2 -2
  40. package/skills/catalyst-linear/references/writing-to-linear.md +7 -9
  41. package/skills/catalyst-linear/scripts/comment.mjs +1 -1
  42. package/skills/catalyst-linear/scripts/create-ticket.mjs +1 -1
  43. package/skills/catalyst-linear/scripts/label.mjs +1 -1
  44. package/skills/catalyst-linear/scripts/lib/cli.mjs +9 -7
  45. package/skills/catalyst-linear/scripts/lib/credential.mjs +2 -2
  46. package/skills/catalyst-linear/scripts/move.mjs +1 -1
  47. package/skills/catalyst-linear/scripts/read-ticket.mjs +1 -1
  48. package/skills/catalyst-linear/scripts/search.mjs +1 -1
  49. package/skills/catalyst-onboard/SKILL.md +37 -57
  50. package/skills/catalyst-onboard/agents/openai.yaml +2 -2
  51. package/skills/catalyst-onboard/agents/portability.yaml +1 -1
  52. package/skills/catalyst-onboard/references/connecting-this-machine.md +35 -0
  53. package/skills/catalyst-onboard/references/declaring-a-repository.md +18 -21
  54. package/skills/catalyst-onboard/references/local-sync.md +19 -7
  55. package/skills/catalyst-onboard/references/reading-ready.md +79 -0
  56. package/skills/catalyst-onboard/references/replacing-a-credential.md +8 -33
  57. package/skills/catalyst-onboard/references/repository-agent-setup.md +23 -0
  58. package/skills/catalyst-onboard/references/skill-sources.md +13 -19
  59. package/skills/catalyst-onboard/references/the-one-path.md +18 -74
  60. package/skills/catalyst-onboard/references/what-a-phase-needs.md +21 -28
  61. package/skills/catalyst-onboard/references/what-the-browser-owns.md +20 -37
  62. package/skills/{catalyst-setup → catalyst-onboard}/scripts/check.mjs +10 -9
  63. package/skills/catalyst-onboard/scripts/lib/cli.mjs +6 -4
  64. package/skills/catalyst-onboard/scripts/lib/credential.mjs +2 -2
  65. package/skills/catalyst-onboard/scripts/lib/project-coverage.mjs +53 -0
  66. package/skills/catalyst-onboard/scripts/where-am-i.mjs +929 -184
  67. package/skills/run-this-project/SKILL.md +23 -25
  68. package/skills/run-this-project/agents/openai.yaml +1 -1
  69. package/skills/run-this-project/agents/portability.yaml +1 -1
  70. package/skills/run-this-project/references/making-work-ready.md +20 -37
  71. package/skills/run-this-project/references/reacting-to-events.md +25 -48
  72. package/skills/run-this-project/references/stalls-and-escalation.md +20 -42
  73. package/skills/run-this-project/scripts/lib/cli.mjs +7 -5
  74. package/skills/run-this-project/scripts/lib/credential.mjs +2 -2
  75. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  76. package/skills/run-this-project/scripts/watch-scope.mjs +1 -1
  77. package/skills/unstick/SKILL.md +12 -12
  78. package/skills/unstick/agents/openai.yaml +1 -1
  79. package/skills/unstick/agents/portability.yaml +1 -1
  80. package/skills/unstick/references/playbook.md +13 -13
  81. package/skills/unstick/scripts/lib/cli.mjs +6 -4
  82. package/skills/unstick/scripts/lib/credential.mjs +2 -2
  83. package/skills/unstick/scripts/unstick.mjs +1 -1
  84. package/skills/what-needs-me/SKILL.md +12 -14
  85. package/skills/what-needs-me/agents/openai.yaml +2 -2
  86. package/skills/what-needs-me/agents/portability.yaml +1 -1
  87. package/skills/what-needs-me/references/raising-a-decision.md +8 -16
  88. package/skills/what-needs-me/references/reading-the-inbox.md +6 -13
  89. package/skills/what-needs-me/references/settling-an-answer.md +6 -12
  90. package/skills/what-needs-me/scripts/inbox.mjs +14 -13
  91. package/skills/what-needs-me/scripts/lib/cli.mjs +6 -4
  92. package/skills/what-needs-me/scripts/lib/credential.mjs +2 -2
  93. package/skills/what-needs-me/scripts/raise.mjs +1 -1
  94. package/skills/what-needs-me/scripts/settle.mjs +1 -1
  95. package/skills/what-this-repo-needs/SKILL.md +10 -19
  96. package/skills/what-this-repo-needs/references/what-each-group-means.md +14 -44
  97. package/skills/what-this-repo-needs/scripts/check.mjs +13 -14
  98. package/skills/what-this-repo-needs/scripts/inventory.mjs +2 -2
  99. package/skills/what-this-repo-needs/scripts/lib/cli.mjs +11 -9
  100. package/skills/what-this-repo-needs/scripts/lib/credential.mjs +2 -2
  101. package/skills/whats-happening/SKILL.md +20 -30
  102. package/skills/whats-happening/agents/openai.yaml +1 -1
  103. package/skills/whats-happening/references/coding-accounts.md +11 -0
  104. package/skills/whats-happening/references/routing-work.md +4 -33
  105. package/skills/whats-happening/references/settings-and-where-they-live.md +17 -0
  106. package/skills/whats-happening/references/stages-and-mapping.md +19 -0
  107. package/skills/whats-happening/references/status-reply.md +11 -31
  108. package/skills/whats-happening/references/the-ladder.md +20 -0
  109. package/skills/whats-happening/references/what-runs-next.md +13 -0
  110. package/skills/whats-happening/references/when-a-phase-fails.md +21 -0
  111. package/skills/whats-happening/references/why-is-it-stuck.md +38 -62
  112. package/skills/whats-happening/scripts/explain.mjs +1 -1
  113. package/skills/whats-happening/scripts/lib/cli.mjs +7 -5
  114. package/skills/whats-happening/scripts/lib/credential.mjs +2 -2
  115. package/skills/{how-catalyst-works → whats-happening}/scripts/show-my-map.mjs +18 -32
  116. package/skills/whats-happening/scripts/snapshot.mjs +10 -6
  117. package/dist/env/declaration-rules.js +0 -163
  118. package/skills/catalyst-onboard/references/choosing-a-coding-account.md +0 -52
  119. package/skills/catalyst-onboard/references/who-fixes-what.md +0 -64
  120. package/skills/catalyst-setup/SKILL.md +0 -36
  121. package/skills/catalyst-setup/agents/openai.yaml +0 -6
  122. package/skills/catalyst-setup/agents/portability.yaml +0 -4
  123. package/skills/catalyst-setup/references/what-each-check-means.md +0 -88
  124. package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
  125. package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
  126. package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
  127. package/skills/connect-me/SKILL.md +0 -63
  128. package/skills/connect-me/agents/openai.yaml +0 -6
  129. package/skills/connect-me/agents/portability.yaml +0 -5
  130. package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
  131. package/skills/connect-me/scripts/lib/cli.mjs +0 -185
  132. package/skills/connect-me/scripts/lib/credential.mjs +0 -29
  133. package/skills/connect-me/scripts/verify-connection.mjs +0 -68
  134. package/skills/how-catalyst-works/SKILL.md +0 -45
  135. package/skills/how-catalyst-works/agents/openai.yaml +0 -6
  136. package/skills/how-catalyst-works/agents/portability.yaml +0 -4
  137. package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
  138. package/skills/how-catalyst-works/references/settings-and-where-they-live.md +0 -20
  139. package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -56
  140. package/skills/how-catalyst-works/references/the-ladder.md +0 -41
  141. package/skills/how-catalyst-works/references/what-catalyst-is.md +0 -30
  142. package/skills/how-catalyst-works/references/what-runs-next.md +0 -95
  143. package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
  144. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
  145. package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
  146. package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
  147. package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
  148. package/skills/whats-happening/references/reading-the-board.md +0 -43
  149. package/skills/whats-happening/references/reprioritising.md +0 -37
@@ -1,95 +0,0 @@
1
- # What runs next: queue order, concurrency, routing, and every exclusion reason
2
-
3
- This reference restates invariants. The live queue is `node scripts/whats-running.mjs --queue [--team <key>]`; one ticket's verdict is `node scripts/explain-ticket.mjs <ticket>`. The reason phrases below match the CLI's own table, so a reason you see printed can be looked up here.
4
-
5
- ## Queue order
6
-
7
- Each team's dispatch queue is derived by the tenant's own store, continuously, from the board:
8
-
9
- 1. Queued tickets (cards in the dispatch slot) rank by **priority ascending, then created time ascending, then identifier ascending**.
10
- 2. Mid-ladder tickets (any ticket with a phase already completed) join the candidate set and order **by completed-phase count descending**, so a ticket close to the end goes before one that just started. That count is an ordering surrogate only; it never decides membership or eligibility.
11
-
12
- The queue re-derives within seconds of any board change and at the top of every periodic pass as a backstop. `dispatch_queue.source` reads `self-derived` when the tenant store built it itself, which is the normal case.
13
-
14
- ## Concurrency
15
-
16
- - Each repository has a concurrency cap (default 20 running phases). An operator can raise or lower it; a paused repository resolves to a cap of zero without touching the stored value, so resuming restores it.
17
- - Dispatch buckets by repository and splits slots across teams so no team starves another. A slot that is running, might be running, or is restarting is never free capacity.
18
- - Comment-wake work (an agent answering a human comment) shares the same cap as ladder work.
19
-
20
- ## The WIP limit
21
-
22
- A Catalyst project (one Linear team, across every repository registered to it) starts no new ticket while its work in progress is at its limit. The limit is per project, never per repository and never workspace-wide: the project's own stored setting when there is one, else a tenant-wide value the operator set, else 12. `0` is a real limit and holds every new start. A workspace owner or admin sets it per project through the project-settings route; there is no page for it yet.
23
-
24
- It holds only new starts. A ticket in the dispatch column that has never run gets no first phase while the team is at its limit, and the explainer prints `wip_limit` with a detail such as "14 tickets in progress in <team key>, at or above its WIP limit of 12; this ticket starts once one finishes". A ticket that has already started keeps getting every later phase, whatever the count.
25
-
26
- What counts as in progress: every live ticket of the team that is past the dispatch column, or was granted a first phase even if its card still sits there, and is not in a terminal state (done, canceled, duplicate). Blocked, parked, waiting on the merge queue or waiting on a human all count: the count is tickets, not running containers. What does not count: triage and backlog states, a dispatch-column ticket that never started, a ticket fenced for a worker outside the cloud (the local-lane label), and a ticket whose PR merged and that only the runner-pin gate holds.
27
-
28
- How to read a fleet idle at the limit: nothing runs because the tickets in progress are all waiting, on the merge queue, on a hold, on a paused repository or on a person. Unstick those (`unstick`, `catalyst-github`, `what-needs-me`); the limit is doing its job. Raising the limit is the project owner's call, not a repair.
29
-
30
- ## The routing decision, in five checks
31
-
32
- When a phase is about to start, a route is chosen per candidate, in order, and the first survivor wins:
33
-
34
- 1. capability match (can this route run this phase);
35
- 2. a model is configured;
36
- 3. the provider is available;
37
- 4. if the candidate needs a coding-account slot: an eligible slot exists and the headroom floor is met;
38
- 5. degraded, equivalent or stage-default parameters resolve.
39
-
40
- No survivor is `no_eligible_account_slot` when any candidate was skipped on capacity, else `routing_unavailable` (a misconfiguration). See `references/coding-accounts.md` for what a slot is.
41
-
42
- ## Every exclusion reason, one line each
43
-
44
- | Reason | Meaning |
45
- | -- | -- |
46
- | `ticket_terminal` | the ticket is in a done or canceled state |
47
- | `pipeline_complete` | every phase has already completed |
48
- | `not_at_dispatch_stage` | the card is not in the team's dispatch column; move it there to dispatch |
49
- | `not_at_pr_stage` | merge is next but the card is not in the PR column (waived when a tenant automation bounced it off PR and the bounce was recorded) |
50
- | `blocked` | a live blocking relation; the blocker closes first |
51
- | `cooling_down` | the offered phase is parked; an operator or a callback releases it, not a clock |
52
- | `lease_held` | a live container already holds this phase |
53
- | `intake_lease_held` | a later phase is offered while an intake container still holds the ticket |
54
- | `ask_ticket` | it carries an ask label; a question is never work |
55
- | `ask_shape_suspected` | its own text reads as a decision request; a human releases it with the release label from the contract's `vocabulary` |
56
- | `externally_claimed` | a worker outside the cloud claimed it |
57
- | `environment_check_required` | the repository's environment check has not run |
58
- | `environment_check_running` | the environment check is in flight |
59
- | `environment_check_failed` | the environment check failed |
60
- | `environment_check_expired` | the environment verdict aged out |
61
- | `environment_check_hash_mismatch` | the environment changed since the verdict |
62
- | `scope_overlap` | its declared file scope intersects a ticket in flight (implement only; enforced once a team activates a scope policy) |
63
- | `waiting_on` | a merge-gate failure with no remediable cause holds the card at PR |
64
- | `branch_missing` | the ticket branch has never been seen |
65
- | `branch_gone` | the ticket branch was deleted |
66
- | `pr_merged` | its PR merged and no other PR is open |
67
- | `no_change_hold` | a remediate round changed nothing; a human comment or a new push releases it |
68
- | `validate_class_spent` | this validate failure already spent its one repair round |
69
- | `stale_failure_episode` | the ladder advanced after the failure, so the round would repair a phase already passed |
70
- | `runner_image_breaker` | fleet-wide: the live runner image fails every phase at startup; clears when the pin moves |
71
- | `no_branch_to_remediate` | a remediate round is queued on a ticket with no branch |
72
- | `retry_backoff` | retrying in place, waiting out its 2/5/15-minute rung |
73
- | `routing_unavailable` | claimed then refused at kickoff: no route, no eligible slot, or the provider is unavailable; the detail names which |
74
- | `repo_paused` | an operator paused the repository |
75
- | `wip_limit` | the project is at its WIP limit (see above): its tickets in progress are waiting, and this new start waits until one of them finishes |
76
- | `remediate_parked` | the remediate phase is parked, so the failing phase has nowhere to be repaired; `catalyst-skills release <ticket>` releases the park once its cause is fixed |
77
- | `phase_parked` | the offered phase is parked after repeated failures or a spent repair-round cap; the person releases it once its cause is fixed |
78
- | `later_phase_lease_held` | an earlier phase is offered while a live container still holds a later phase of this ticket |
79
- | `human_owned_pr` | a person's own pull request holds this ticket; that person closes or merges the PR themselves, and no release command clears it |
80
- | `review_not_converging` | review and repair kept finding new problems without converging |
81
- | `round_threshold` | the ticket spent its lifetime repair budget |
82
- | `claim_storm` | claimed too many times in the last hour; it waits the hour out, and there is nothing to release |
83
- | `repo_at_capacity` | the repository's runner seats are all in use; it starts when one frees |
84
-
85
- ## The unknowns (the evaluator fails closed)
86
-
87
- When the cloud cannot answer, it says so rather than guessing: `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`, `branch_snapshot_unknown`. An unknown is "I could not look", never "it is not there"; report it as inconclusive. The exception is a team with no saved stage mapping: `workflow_mapping_unknown` never clears on its own there, `explain` names the missing stages, and the fix is a tenant owner or admin mapping the team.
88
-
89
- One advisory gates nothing: `human_addressed_unlabeled_ask_suspect` (assigned to a human with no delegate; possibly an unlabelled ask).
90
-
91
- ## The three levers a human has
92
-
93
- 1. **Priority** on the ticket reorders the queue.
94
- 2. **The dispatch column**: moving a card into the dispatch slot dispatches it; moving it to the team's backlog-type state parks it and stops rounds.
95
- 3. **Holds**: the hold labels on the pull request (from the contract's `merge.prLabels`) keep a green PR out of the merge queue; a human comment on the ticket clears the validate hold and the no-change hold.
@@ -1,57 +0,0 @@
1
- # When a phase fails: four layers, in order
2
-
3
- This reference restates the mechanism, which does not vary per tenant. The numbers quoted are the fleet defaults; the live values are served on the contract under `thresholds` and `node scripts/show-my-map.mjs` prints them. Quote the printed values, not these.
4
-
5
- A failed phase writes no board state on its own: the ticket stays at its current stage, retryable. What happens next is decided by four stacked layers.
6
-
7
- ## Layer 1: retry in place
8
-
9
- Some failures are not the ticket's fault, so the SAME phase is simply re-offered and no remediation round is minted:
10
-
11
- - the phase failed before any branch existed (research, plan or implement on a ticket whose own record shows no branch);
12
- - an infrastructure failure at any phase: a vendor 5xx, a dropped stream, a refused setup, an environment gap, a phase timeout, an unwritable workspace.
13
-
14
- Nothing is written for the retry; the ordinary dispatch path re-offers the phase. A remediation round on a branchless ticket could only fail to find the branch while spending a counted attempt, which is why this layer exists.
15
-
16
- ## Layer 2: backoff
17
-
18
- Every retry-in-place, and a routing refusal at kickoff, waits out a rung of a backoff ladder before the phase is re-offered: 2 minutes, then 5, then 15, indexed by the consecutive-failure count and clamped at the last rung. While waiting, the eligibility explainer shows `retry_backoff` (or `routing_unavailable` with a kickoff detail). `node scripts/explain-ticket.mjs <ticket>` prints it. The live ladder is `thresholds.retryBackoffMs`.
19
-
20
- ## Layer 3: move to Remediate
21
-
22
- Any failure that is not retry-in-place moves the card to the team's remediate stage and queues a remediation round. The round is an interrupt phase (`remediate`) that repairs what failed; when a round succeeds, the card restores to its exact pre-failure stage. A team with no remediate stage mapped gets the hold label from `teams[].labels.hold` instead of a state move.
23
-
24
- Rounds are capped: after `thresholds.remediateRoundCap` rounds (default 3) in one failure episode, no further round is queued. A remediation round that itself fails never queues another round. A round that changes nothing puts the ticket on a no-change hold, released by a human comment on the ticket or a new push, never by a clock. A validate failure with the same fingerprint spends only one repair round per episode.
25
-
26
- Two structural cases the round machinery refuses: a `remediate` round is never offered on a ticket with no recorded branch (`no_branch_to_remediate`), and one dispatched anyway is cancelled at the clone.
27
-
28
- ## Layer 4: park
29
-
30
- After `thresholds.parkAfterConsecutiveFailures` consecutive failures (default 3) the phase is parked. A parked phase shows as `cooling_down` in the explainer; the ticket's `remediate` phase being parked shows as `remediate_parked`.
31
-
32
- Which parks release themselves:
33
-
34
- | Park | Releases by |
35
- | -- | -- |
36
- | repeated failure | a release from the person's own login once its cause is fixed (`catalyst-skills release`), not a clock |
37
- | remediate round cap reached | the same release, which buys one more round, not a clock |
38
- | missing branch | its own budgeted release loop |
39
- | rebase conflict | its own budgeted release loop |
40
-
41
- A person's own key releases a park, and every other hold that a retry can fix, in one step: `catalyst-skills release <ticket> --because <what changed>`. The cloud releases every governor holding the ticket or releases nothing and names what a person must do instead (a person's own pull request, a review that will not converge, the lifetime repair budget). It refuses a cause it cannot see change unless the caller says what changed. The `unstick` skill runs that loop. When a ticket is parked, name the failure class the explainer shows and whether its cause is fixed.
42
-
43
- ## Two holds that are not failures
44
-
45
- - **Validate budget hold**: a validate failure that has spent its repair round holds the card; a human comment on the ticket clears it.
46
- - **Runner image breaker**: fleet-wide, not per ticket. When three distinct tickets fail the same startup class on the live runner image, dispatch pauses for the affected tickets, one anomaly alert is raised per tenant, and dispatch resumes automatically when the image pin moves. The explainer shows `runner_image_breaker`. Never escalate this one ticket at a time.
47
-
48
- ## What a human sees on the ticket
49
-
50
- Each attempt posts a phase-outcome comment (complete or FAILED, with phase, attempt, artifact, a summary and any park or hold block) and each remediation round posts a remediate-attempt comment naming the failure class it is repairing. `catalyst-linear` describes the shapes. The attempt ledger, the round count against the cap and the park history are readable: `catalyst-skills explain --history <ticket>` prints them from the cloud's own relay ledger.
51
-
52
- ## Rule of thumb for answering "why is it stuck?"
53
-
54
- 1. Run `node scripts/explain-ticket.mjs <ticket>`; the reason names the layer.
55
- 2. `retry_backoff` or `routing_unavailable`: wait; it retries itself. Say when.
56
- 3. `cooling_down`, `remediate_parked`, `no_change_hold`, `validate_class_spent`: name what releases it (a release once the cause is fixed, a comment, a push) and who can do that.
57
- 4. A system-level cause (provider down, runner image breaker, repo paused) is ONE alert, never a per-ticket escalation.
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env node
2
- // explain-ticket.mjs — why one ticket is, or is not, about to run: the cloud's own eligibility row
3
- // for it, rendered as one paragraph, plus the raw row so the failure block is not lost.
4
- import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
5
-
6
- const HELP = `Usage: node scripts/explain-ticket.mjs <ticket> [--json]
7
-
8
- Asks Catalyst Cloud for the ticket's eligibility row (position, status, the exclusion reason in plain
9
- English, the last failure, advisories) and prints it as one paragraph. Wraps: catalyst-skills explain.
10
-
11
- <ticket> the Linear identifier, e.g. KEY-123
12
- --json print the CLI's JSON document instead of the paragraph
13
-
14
- Exit 0 explained (even when the ticket is excluded), 1 when the ticket is unknown to the mirror or
15
- a usage error, 2 when this machine is not connected to a tenant or the cloud refused the read
16
- (the line says which).`;
17
-
18
- const argv = process.argv.slice(2);
19
- if (wantsHelp(argv)) {
20
- console.log(HELP);
21
- process.exit(0);
22
- }
23
- const { flags, positionals } = parseFlags(argv, { bool: ["json"] });
24
- const ticket = positionals[0];
25
- if (!ticket) usage("explain-ticket needs a ticket identifier (see --help)");
26
-
27
- const r = runCli(["explain", ticket, "--json"]);
28
- exitOnFailure(r);
29
- relayStderr(r);
30
- const doc = parseJson(r.stdout);
31
- if (!doc || typeof doc.explanation !== "string") {
32
- console.log(r.stdout.trimEnd());
33
- process.exit(1);
34
- }
35
- if (flags.json) {
36
- console.log(JSON.stringify(doc));
37
- } else {
38
- console.log(doc.explanation);
39
- if (doc.row) console.log(`row: ${JSON.stringify(doc.row)}`);
40
- }
41
- process.exit(doc.row ? 0 : 1);
@@ -1,164 +0,0 @@
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
- }
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env node
2
- // lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
3
- // byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
4
- // (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
5
- // each carries its own copy. This file is a library — run a sibling script with --help for usage.
6
- //
7
- // customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
8
- // session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
9
- // catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
10
- // itself. A new credential kind lands here, once.
11
-
12
- /** The command that connects this machine, as every not-connected line names it. */
13
- export const CONNECT_COMMAND =
14
- "npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
15
-
16
- /** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
17
- export function hasCredential(cfg) {
18
- if (cfg === null || typeof cfg !== "object") return false;
19
- const key = cfg["key"];
20
- if (typeof key === "string" && key !== "") return true;
21
- const login = cfg["auth"];
22
- return (
23
- login !== null &&
24
- typeof login === "object" &&
25
- login["kind"] === "oauth" &&
26
- typeof login["refreshToken"] === "string" &&
27
- login["refreshToken"] !== ""
28
- );
29
- }
@@ -1,65 +0,0 @@
1
- #!/usr/bin/env node
2
- // whats-running.mjs — what Catalyst is executing right now (fleet activity, the agent roster, lease
3
- // attributions), optionally the dispatch queue and the coding-account line.
4
- import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, wantsHelp } from "./lib/cli.mjs";
5
-
6
- const HELP = `Usage: node scripts/whats-running.mjs [--queue] [--accounts] [--team <key>] [--json]
7
-
8
- Prints what is executing on your tenant. Wraps: catalyst-skills running, queue, accounts.
9
-
10
- --queue also print the dispatch queue (what runs next, in order)
11
- --team <key> with --queue: one team's queue
12
- --accounts also print the coding-account line (provider, state, windows — never a credential)
13
- --json one JSON document: { running, queue?, accounts? }
14
- --help this text
15
-
16
- Exit 0, 1 on a usage error, 2 when this machine is not connected to a tenant or the cloud refused
17
- a read (the line says which).`;
18
-
19
- const argv = process.argv.slice(2);
20
- if (wantsHelp(argv)) {
21
- console.log(HELP);
22
- process.exit(0);
23
- }
24
- const { flags } = parseFlags(argv, { bool: ["queue", "accounts", "json"], value: ["team"] });
25
-
26
- const out = {};
27
- const running = runCli(["running", "--json"]);
28
- exitOnFailure(running);
29
- relayStderr(running);
30
- out.running = parseJson(running.stdout) ?? running.stdout.trimEnd();
31
-
32
- if (flags.queue) {
33
- const args = ["queue", "--json"];
34
- if (flags.team) args.push("--team", flags.team);
35
- const queue = runCli(args);
36
- exitOnFailure(queue);
37
- relayStderr(queue);
38
- out.queue = parseJson(queue.stdout) ?? queue.stdout.trimEnd();
39
- }
40
-
41
- if (flags.accounts) {
42
- const accounts = runCli(["accounts"]);
43
- exitOnFailure(accounts);
44
- relayStderr(accounts);
45
- out.accounts = accounts.stdout.trimEnd();
46
- }
47
-
48
- if (flags.json) {
49
- console.log(JSON.stringify(out));
50
- process.exit(0);
51
- }
52
-
53
- const section = (title, value) => {
54
- console.log(`== ${title}`);
55
- console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
56
- };
57
- if (out.running && typeof out.running === "object") {
58
- section("fleet activity", out.running.fleetActivity ?? out.running);
59
- if (out.running.agentRoster !== undefined) section("agent roster", out.running.agentRoster);
60
- if (out.running.leaseAttributions !== undefined) section("lease attributions", out.running.leaseAttributions);
61
- } else {
62
- section("running", out.running);
63
- }
64
- if (out.queue !== undefined) section("dispatch queue", out.queue);
65
- if (out.accounts !== undefined) section("coding accounts", out.accounts);
@@ -1,43 +0,0 @@
1
- # Reading the board
2
-
3
- This reference restates invariants: what the stages are, what order they come in, and what "stuck" means. The names your team uses for each stage, and which state ids they map to, are tenant facts read live from the contract; `node scripts/snapshot.mjs --board` prints the board already grouped and ordered by them.
4
-
5
- ## Stages are slots, names are display
6
-
7
- Catalyst thinks in eleven **slots** in pipeline order: dispatch, intake, research, plan, implement, remediate, verify, review, pr, done, canceled. Each of your teams maps some or all of those slots onto its own Linear workflow states. The contract carries, per team and per slot, the state id (the authority), the live state name and type (display), and whether the state still exists in Linear. A team may have adopted Catalyst's recommended states, mapped its existing ones, or mixed the two; the contract's `workflowMode` says which.
8
-
9
- Five slots are load-bearing and a missing one is a distinct silent failure: **dispatch** (where a card goes to be picked up), **intake** (an optional first pass for tickets that have never entered the ladder), **pr** (where a card sits while its pull request is reviewed and merged), **done** and **canceled**. The other six are informational: a wrong mapping there costs a warning, not a stall.
10
-
11
- Read a board by slot, never by name. Two teams can call the same slot different things, and a state renamed in Linear keeps its id and its slot.
12
-
13
- ## Work in progress by stage
14
-
15
- The snapshot's `board` block groups open tickets by state name in slot order, drops terminal states, and lists any state name that maps to no slot at the end. Read it top to bottom:
16
-
17
- - **dispatch** holds what is ready and waiting for a container. A long dispatch column with nothing in flight is a capacity or eligibility question, not a work question: run `explain` on the first row. If it says `wip_limit`, the project is at its WIP limit (tickets in progress, counting blocked, parked and waiting ones, not containers): the work in progress is what is waiting, so read that column, not this one.
18
- - **intake** through **pr** are the ladder. A card advances one slot when the matching phase completes; a failed phase writes no board state, so a card that has not moved is either still running, retrying in place, or moved sideways to remediate.
19
- - **remediate** is an interrupt, not a step. A card there was moved by a failure and returns to its exact previous stage when a repair round succeeds. Count remediate cards separately; they are not progress.
20
- - Backlog-type states are not slots. A ticket there is parked or not yet chosen; nothing is offered for it and nothing is wrong with it.
21
-
22
- ## Aging
23
-
24
- Age is time since `updated_at`, or since the lease started for a ticket in flight. Report it in human units. Compare it to the phase, not to a fixed number: a research phase and a merge wait have different natural durations, and the cloud's own retry backoff (the contract's `thresholds.retryBackoffMs`) explains many short waits.
25
-
26
- ## What counts as stuck
27
-
28
- A ticket is **stuck** only when all three hold:
29
-
30
- 1. Nothing is offered for it: the queue lists it excluded, or `explain` gives a reason rather than a position.
31
- 2. The reason does not release itself. Backoffs, cooldowns that name a callback, and fleet-wide holds that clear when the cloud repins are waits, not stalls. The table in `references/why-is-it-stuck.md` marks which is which.
32
- 3. No one is acting on the release: no open ask names it, no human comment landed after the failure, no push arrived.
33
-
34
- Everything else is **waiting**, and the reply says what it is waiting on. Calling a wait "stuck" sends the human to fix something the cloud is already handling.
35
-
36
- ## The signals the cloud posts on the ticket itself
37
-
38
- Before declaring anything, read the ticket's comments (`catalyst-skills query issue <id>`; the `catalyst-linear` skill explains each shape). The cloud posts a phase-outcome card on every completion and failure, a remediate-attempt card per repair round, a board-health note when it detects a stall itself, and a merge-wait note when a merge is held. Those comments are the narrative; the per-ticket ledger behind them — attempts, rounds against the cap, park state — is `catalyst-skills explain --history <ticket>`, and the two must agree.
39
-
40
- ## Two things that look like work and are not
41
-
42
- - A ticket carrying the ask marker label, or whose text reads as a decision request, is a question. It is excluded from dispatch by design and belongs in the waiting-on-human block.
43
- - A ticket claimed by a worker outside the cloud (the local-lane label) is being worked, but not by Catalyst; report it as in flight elsewhere and do not chase the cloud for it.
@@ -1,37 +0,0 @@
1
- # Reprioritising
2
-
3
- This reference restates invariants: how Catalyst orders work and which levers a person actually has. The live queue is `catalyst-skills queue [--team K]` (inside `node scripts/snapshot.mjs`); the thresholds behind backoffs and parks are the contract's `thresholds` block, printed in the snapshot's `tenant` section.
4
-
5
- ## How the cloud orders work
6
-
7
- Each team's queue is derived by the cloud itself from the team's own tickets, so there is nothing a session publishes and nothing that goes stale between sessions. The order is:
8
-
9
- 1. **Priority** ascending, as set on the Linear ticket (urgent first).
10
- 2. **Created time** ascending: older tickets first among equals.
11
- 3. **Identifier** ascending, as the final tiebreak.
12
-
13
- Tickets that are already **mid-ladder** join the same candidate set and sort ahead by how many phases they have completed, so a ticket that is nearly done finishes before a fresh one starts. That is an ordering rule only; it never makes an ineligible ticket eligible.
14
-
15
- Within the order, dispatch is bounded per repository (a concurrency cap of 20 running phases by default that an operator raises or lowers — the tenant's own settings page displays it and does not change it — and a paused repository resolves to zero without losing the stored value) 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.