@catalyst-cloud/catalyst-skills 0.1.1 → 0.2.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.
- package/CHANGELOG.md +4 -0
- package/README.md +109 -53
- package/dist/args.js +237 -0
- package/dist/ask.js +137 -0
- package/dist/cli.js +165 -279
- package/dist/config.js +106 -0
- package/dist/contract-types.js +4 -0
- package/dist/contract.js +178 -0
- package/dist/detach.js +10 -0
- package/dist/errors.js +27 -0
- package/dist/execution.js +156 -0
- package/dist/http.js +142 -0
- package/dist/prompt.js +35 -0
- package/dist/query.js +187 -0
- package/dist/ready.js +123 -0
- package/dist/replica.js +318 -0
- package/dist/sdk.js +26 -0
- package/dist/skill-shape.js +179 -0
- package/dist/skills.js +74 -0
- package/dist/ts-deps-loader.js +68 -0
- package/dist/watch/consumer.js +141 -0
- package/dist/watch/cursor-file.js +62 -0
- package/dist/watch.js +173 -0
- package/dist/write.js +224 -0
- package/package.json +9 -5
- package/skills/catalyst-github/SKILL.md +35 -0
- package/skills/catalyst-github/agents/openai.yaml +6 -0
- package/skills/catalyst-github/agents/portability.yaml +4 -0
- package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
- package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
- package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
- package/skills/catalyst-linear/SKILL.md +43 -0
- package/skills/catalyst-linear/agents/openai.yaml +6 -0
- package/skills/catalyst-linear/agents/portability.yaml +5 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
- package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
- package/skills/catalyst-linear/scripts/comment.mjs +59 -0
- package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
- package/skills/catalyst-linear/scripts/label.mjs +48 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
- package/skills/catalyst-linear/scripts/move.mjs +41 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
- package/skills/catalyst-linear/scripts/search.mjs +49 -0
- package/skills/catalyst-setup/SKILL.md +32 -0
- package/skills/catalyst-setup/agents/openai.yaml +6 -0
- package/skills/catalyst-setup/agents/portability.yaml +4 -0
- package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
- package/skills/catalyst-setup/scripts/check.mjs +75 -0
- package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
- package/skills/connect-me/SKILL.md +55 -0
- package/skills/connect-me/agents/openai.yaml +6 -0
- package/skills/connect-me/agents/portability.yaml +5 -0
- package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
- package/skills/connect-me/scripts/lib/cli.mjs +184 -0
- package/skills/connect-me/scripts/verify-connection.mjs +68 -0
- package/skills/how-catalyst-works/SKILL.md +42 -0
- package/skills/how-catalyst-works/agents/openai.yaml +6 -0
- package/skills/how-catalyst-works/agents/portability.yaml +4 -0
- package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
- package/skills/how-catalyst-works/references/the-ladder.md +41 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
- package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
- package/skills/run-this-project/SKILL.md +45 -0
- package/skills/run-this-project/agents/openai.yaml +6 -0
- package/skills/run-this-project/agents/portability.yaml +5 -0
- package/skills/run-this-project/assets/stall-policy.json +15 -0
- package/skills/run-this-project/references/making-work-ready.md +60 -0
- package/skills/run-this-project/references/reacting-to-events.md +76 -0
- package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
- package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
- package/skills/run-this-project/scripts/make-ready.mjs +64 -0
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
- package/skills/what-needs-me/SKILL.md +41 -0
- package/skills/what-needs-me/agents/openai.yaml +6 -0
- package/skills/what-needs-me/agents/portability.yaml +5 -0
- package/skills/what-needs-me/references/raising-a-decision.md +41 -0
- package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
- package/skills/what-needs-me/references/settling-an-answer.md +37 -0
- package/skills/what-needs-me/scripts/inbox.mjs +47 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
- package/skills/what-needs-me/scripts/raise.mjs +53 -0
- package/skills/what-needs-me/scripts/settle.mjs +73 -0
- package/skills/whats-happening/SKILL.md +43 -0
- package/skills/whats-happening/agents/openai.yaml +6 -0
- package/skills/whats-happening/agents/portability.yaml +4 -0
- package/skills/whats-happening/assets/status-reply.json +77 -0
- package/skills/whats-happening/references/reading-the-board.md +43 -0
- package/skills/whats-happening/references/reprioritising.md +37 -0
- package/skills/whats-happening/references/routing-work.md +36 -0
- package/skills/whats-happening/references/status-reply.md +34 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
- package/skills/whats-happening/scripts/explain.mjs +28 -0
- package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
- package/skills/whats-happening/scripts/snapshot.mjs +145 -0
- package/skills/ask/SKILL.md +0 -37
- package/skills/concierge/SKILL.md +0 -41
- package/skills/join/SKILL.md +0 -30
- package/skills/linearis/SKILL.md +0 -40
- package/skills/setup/SKILL.md +0 -27
- package/skills/steward/SKILL.md +0 -30
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Catalyst status reply",
|
|
4
|
+
"description": "The one-reply shape a status answer follows. Every line names a ticket identifier; nothing in it is a guess. Fill it from scripts/snapshot.mjs and scripts/explain.mjs, then render it as prose in this order.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["asOf", "source", "inFlight", "blocked", "closed", "next"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"asOf": { "type": "string", "format": "date-time", "description": "When the snapshot was taken (snapshot.takenAt)." },
|
|
9
|
+
"source": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Where the facts came from, in one clause: the API, or the replica with its cursor and heartbeat age. Copy the snapshot's source line; never omit it when the replica was stale."
|
|
12
|
+
},
|
|
13
|
+
"scope": { "type": "string", "description": "The team key or project the reply covers, when the human asked about one." },
|
|
14
|
+
"inFlight": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"description": "Tickets a phase is running on right now, or that sit mid-ladder with a live lease. One entry per ticket.",
|
|
17
|
+
"items": { "$ref": "#/$defs/line" }
|
|
18
|
+
},
|
|
19
|
+
"blocked": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"description": "Tickets nothing is offered for, each with the reason in the human's words and who or what releases it.",
|
|
22
|
+
"items": {
|
|
23
|
+
"allOf": [
|
|
24
|
+
{ "$ref": "#/$defs/line" },
|
|
25
|
+
{
|
|
26
|
+
"properties": {
|
|
27
|
+
"reason": { "type": "string", "description": "The explainer's reason, translated (references/why-is-it-stuck.md)." },
|
|
28
|
+
"onWhom": { "type": "string", "description": "The human, a named role, the cloud itself, or a clock — never blank." }
|
|
29
|
+
},
|
|
30
|
+
"required": ["reason", "onWhom"]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"waitingOnHuman": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"description": "Open asks, ranked by what each holds. Taken from snapshot.waitingOnHuman; the what-needs-me skill owns the detail.",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["ticket", "question", "holds"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"ticket": { "type": "string" },
|
|
43
|
+
"question": { "type": "string" },
|
|
44
|
+
"holds": { "type": "array", "items": { "type": "string" }, "description": "Ticket identifiers the answer releases." }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"closed": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"description": "Tickets that reached the done slot since the window the human asked about (or since the last reply).",
|
|
51
|
+
"items": { "$ref": "#/$defs/line" }
|
|
52
|
+
},
|
|
53
|
+
"next": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"description": "What the queue will pick up next, in queue order, with the phase each will run.",
|
|
56
|
+
"items": { "$ref": "#/$defs/line" }
|
|
57
|
+
},
|
|
58
|
+
"cannotSee": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"description": "Facts the account key cannot read yet, named, with where the human can read them (the CLI prints the URL).",
|
|
61
|
+
"items": { "type": "string" }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"$defs": {
|
|
65
|
+
"line": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"required": ["ticket", "summary"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"ticket": { "type": "string", "description": "The identifier, KEY-123 shape." },
|
|
70
|
+
"summary": { "type": "string", "description": "One clause: what it is and where it stands." },
|
|
71
|
+
"stage": { "type": "string", "description": "The stage name as the contract spells it for this team." },
|
|
72
|
+
"phase": { "type": "string", "description": "The ladder phase running or offered, when known." },
|
|
73
|
+
"age": { "type": "string", "description": "How long it has sat where it is, in human units." }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Reading the board
|
|
2
|
+
|
|
3
|
+
This reference restates invariants: what the stages are, what order they come in, and what "stuck" means. The names your team uses for each stage, and which state ids they map to, are tenant facts read live from the contract; `node scripts/snapshot.mjs --board` prints the board already grouped and ordered by them.
|
|
4
|
+
|
|
5
|
+
## Stages are slots, names are display
|
|
6
|
+
|
|
7
|
+
Catalyst thinks in eleven **slots** in pipeline order: dispatch, intake, research, plan, implement, remediate, verify, review, pr, done, canceled. Each of your teams maps some or all of those slots onto its own Linear workflow states. The contract carries, per team and per slot, the state id (the authority), the live state name and type (display), and whether the state still exists in Linear. A team may have adopted Catalyst's recommended states, mapped its existing ones, or mixed the two; the contract's `workflowMode` says which.
|
|
8
|
+
|
|
9
|
+
Five slots are load-bearing and a missing one is a distinct silent failure: **dispatch** (where a card goes to be picked up), **intake** (an optional first pass for tickets that have never entered the ladder), **pr** (where a card sits while its pull request is reviewed and merged), **done** and **canceled**. The other six are informational: a wrong mapping there costs a warning, not a stall.
|
|
10
|
+
|
|
11
|
+
Read a board by slot, never by name. Two teams can call the same slot different things, and a state renamed in Linear keeps its id and its slot.
|
|
12
|
+
|
|
13
|
+
## Work in progress by stage
|
|
14
|
+
|
|
15
|
+
The snapshot's `board` block groups open tickets by state name in slot order, drops terminal states, and lists any state name that maps to no slot at the end. Read it top to bottom:
|
|
16
|
+
|
|
17
|
+
- **dispatch** holds what is ready and waiting for a container. A long dispatch column with nothing in flight is a capacity or eligibility question, not a work question: run `explain` on the first row.
|
|
18
|
+
- **intake** through **pr** are the ladder. A card advances one slot when the matching phase completes; a failed phase writes no board state, so a card that has not moved is either still running, retrying in place, or moved sideways to remediate.
|
|
19
|
+
- **remediate** is an interrupt, not a step. A card there was moved by a failure and returns to its exact previous stage when a repair round succeeds. Count remediate cards separately; they are not progress.
|
|
20
|
+
- Backlog-type states are not slots. A ticket there is parked or not yet chosen; nothing is offered for it and nothing is wrong with it.
|
|
21
|
+
|
|
22
|
+
## Aging
|
|
23
|
+
|
|
24
|
+
Age is time since `updated_at`, or since the lease started for a ticket in flight. Report it in human units. Compare it to the phase, not to a fixed number: a research phase and a merge wait have different natural durations, and the cloud's own retry backoff (the contract's `thresholds.retryBackoffMs`) explains many short waits.
|
|
25
|
+
|
|
26
|
+
## What counts as stuck
|
|
27
|
+
|
|
28
|
+
A ticket is **stuck** only when all three hold:
|
|
29
|
+
|
|
30
|
+
1. Nothing is offered for it: the queue lists it excluded, or `explain` gives a reason rather than a position.
|
|
31
|
+
2. The reason does not release itself. Backoffs, cooldowns that name a callback, and fleet-wide holds that clear when the cloud repins are waits, not stalls. The table in `references/why-is-it-stuck.md` marks which is which.
|
|
32
|
+
3. No one is acting on the release: no open ask names it, no human comment landed after the failure, no push arrived.
|
|
33
|
+
|
|
34
|
+
Everything else is **waiting**, and the reply says what it is waiting on. Calling a wait "stuck" sends the human to fix something the cloud is already handling.
|
|
35
|
+
|
|
36
|
+
## The signals the cloud posts on the ticket itself
|
|
37
|
+
|
|
38
|
+
Before declaring anything, read the ticket's comments (`catalyst-skills query issue <id>`; the `catalyst-linear` skill explains each shape). The cloud posts a phase-outcome card on every completion and failure, a remediate-attempt card per repair round, a board-health note when it detects a stall itself, and a merge-wait note when a merge is held. Those comments are the execution history a key can read today; the per-ticket ledger behind them is not visible to an account key yet, and `explain --history` says so.
|
|
39
|
+
|
|
40
|
+
## Two things that look like work and are not
|
|
41
|
+
|
|
42
|
+
- A ticket carrying the ask marker label, or whose text reads as a decision request, is a question. It is excluded from dispatch by design and belongs in the waiting-on-human block.
|
|
43
|
+
- A ticket claimed by a worker outside the cloud (the local-lane label) is being worked, but not by Catalyst; report it as in flight elsewhere and do not chase the cloud for it.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Reprioritising
|
|
2
|
+
|
|
3
|
+
This reference restates invariants: how Catalyst orders work and which levers a person actually has. The live queue is `catalyst-skills queue [--team K]` (inside `node scripts/snapshot.mjs`); the thresholds behind backoffs and parks are the contract's `thresholds` block, printed in the snapshot's `tenant` section.
|
|
4
|
+
|
|
5
|
+
## How the cloud orders work
|
|
6
|
+
|
|
7
|
+
Each team's queue is derived by the cloud itself from the team's own tickets, so there is nothing a session publishes and nothing that goes stale between sessions. The order is:
|
|
8
|
+
|
|
9
|
+
1. **Priority** ascending, as set on the Linear ticket (urgent first).
|
|
10
|
+
2. **Created time** ascending: older tickets first among equals.
|
|
11
|
+
3. **Identifier** ascending, as the final tiebreak.
|
|
12
|
+
|
|
13
|
+
Tickets that are already **mid-ladder** join the same candidate set and sort ahead by how many phases they have completed, so a ticket that is nearly done finishes before a fresh one starts. That is an ordering rule only; it never makes an ineligible ticket eligible.
|
|
14
|
+
|
|
15
|
+
Within the order, dispatch is bounded per repository (a concurrency cap the tenant admin can set in settings, and a paused repository resolves to zero) and split across teams so one team cannot starve another. Comment-wake work, where the cloud answers a human comment on a ticket, shares the same cap as relay work.
|
|
16
|
+
|
|
17
|
+
The queue is recomputed on every ingest that touches the team and on each alarm pass, so a change you make shows up within seconds, not on a schedule.
|
|
18
|
+
|
|
19
|
+
## The three levers a person has
|
|
20
|
+
|
|
21
|
+
**1. Priority on the ticket.** Change the Linear priority and the queue reorders itself. This is the lever for "do this one first". It does not jump a running phase; it changes what is picked up next.
|
|
22
|
+
|
|
23
|
+
**2. The dispatch column.** A card is offered only when it sits in the team's dispatch slot (and, for a ticket that has never entered the ladder, the intake slot when intake is enabled). Moving a card into that slot is how work is started; moving it out to a backlog-type state is how work is stopped without cancelling it. The `run-this-project` skill has the script; through this skill you describe the move and let the person or the project owner make it.
|
|
24
|
+
|
|
25
|
+
**3. Holds.** A PR label from the contract's `merge.prLabels` set keeps an otherwise-mergeable pull request out of the merge queue until a person removes it. A blocking relation from an ask holds every ticket the ask names. Applying the release label the contract names frees a ticket the ask-shape detector flagged by mistake.
|
|
26
|
+
|
|
27
|
+
Everything else that looks like a lever is not one from a key: unparking a phase, clearing a repair hold, resetting a validate budget and re-pinning a base are operator actions today, and the reply says so and names the settings page rather than promising them.
|
|
28
|
+
|
|
29
|
+
## What not to promise
|
|
30
|
+
|
|
31
|
+
- **A running phase is not interrupted** by any of the three levers. It finishes or fails on its own; then the new order applies.
|
|
32
|
+
- **A failed phase does not need re-queueing.** A pre-branch failure or an infrastructure failure retries in place after a backoff; a later failure moves the card to the remediate slot and queues a repair round, capped by the contract's `thresholds.remediateRoundCap`; repeated failures park it after `thresholds.parkAfterConsecutiveFailures`. Moving the card by hand during that sequence usually restarts the count rather than shortening it.
|
|
33
|
+
- **Priority is not urgency to the human.** An urgent ticket that is blocked on an ask still waits for the ask. The what-needs-me ranking (by what an answer releases) is the right list for the human; the queue is the right list for the fleet.
|
|
34
|
+
|
|
35
|
+
## Answering "why is that one not next?"
|
|
36
|
+
|
|
37
|
+
Run `explain` on it. If the answer is a position, it is next in order and the cloud is bounded by capacity: say how many are ahead and whether any run at all (if nothing runs anywhere, the likely cause is coding-account capacity, which a key cannot read yet, so name the settings page). If the answer is a reason, use `references/why-is-it-stuck.md`. If the answer is that the ticket is not in the explainer, it is on another team, terminal, or unknown to the mirror: check the identifier before anything else.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Routing work
|
|
2
|
+
|
|
3
|
+
This reference restates an invariant: what this skill does when the person asks for something to be done rather than known, and where a decision goes. Nothing here is tenant-specific.
|
|
4
|
+
|
|
5
|
+
## Three kinds of request
|
|
6
|
+
|
|
7
|
+
A message from the person is one of three things, and you decide which in the first sentence of your reply:
|
|
8
|
+
|
|
9
|
+
1. **A question.** "Where is X? Why is Y stuck? What closed today?" Answer it in the one-reply shape (`references/status-reply.md`). No routing.
|
|
10
|
+
2. **A request for work.** "Get X done. Ship the Y change. Own this until it closes." Turn it into a project with an owner, in one pass, as below.
|
|
11
|
+
3. **A decision only they can make**, surfaced by you or by the cloud. That is never answered here; it goes to `what-needs-me`.
|
|
12
|
+
|
|
13
|
+
A request that contains a decision is both: route the work and raise the decision, and say in the reply that you did both.
|
|
14
|
+
|
|
15
|
+
## A request becomes a project with an owner, in one pass
|
|
16
|
+
|
|
17
|
+
You hold no authority over project owners. Your job is to make the work visible and hand it to a single-threaded owner, then get out of the way.
|
|
18
|
+
|
|
19
|
+
1. **Find the existing home first.** `catalyst-skills query projects` and `query search <terms>`. A request that fits an open project is a ticket in that project, not a new project. Duplicate projects split one goal's status across two places.
|
|
20
|
+
2. **Scaffold when there is no home.** State the outcome in one sentence, the first ticket or two, and who owns it. Tickets are created through the `catalyst-linear` skill (the app actor, the team key from the contract); a project itself is created by the person in Linear, so name what you want it called and ask them to create it if none fits.
|
|
21
|
+
3. **Name the owner.** The owner is a `run-this-project` session for that project, or a person. Say which, in the reply. Tell the person the one command that starts the owner session, and do not start long-running work inside this session: the desk answers questions; the owner reacts to events.
|
|
22
|
+
4. **Say what you did.** The reply ends with the identifiers created, the owner named, and the next thing the person will see.
|
|
23
|
+
|
|
24
|
+
Cite an identifier only after the create call returned it. A guessed number is usually a real, unrelated ticket.
|
|
25
|
+
|
|
26
|
+
## What goes to what-needs-me
|
|
27
|
+
|
|
28
|
+
Anything that gates active work on a choice only the human can make: a product call, a priority call between two things that cannot both go first, an approval, or an action only they can physically take (a click in settings, a credential). It is raised as an ask through the `what-needs-me` skill with the question, the options, the default that fires if they stay silent, and what it blocks, and it is raised **before** anyone proceeds on the default.
|
|
29
|
+
|
|
30
|
+
Not an ask: brainstorming, a design back-and-forth, a question the person asked first, a retry-or-abandon call an owner can make, a provider outage (that is one status line, not a per-ticket question).
|
|
31
|
+
|
|
32
|
+
## Three rules that bind this skill
|
|
33
|
+
|
|
34
|
+
- **Never answer as the human.** You do not pick an option on an open ask, close one, or post in their voice. When their answer arrives in chat, it is recorded on the ask through `what-needs-me` so the record is complete, and it is recorded as the app actor, never as them.
|
|
35
|
+
- **Escalate inward, never outward.** An instrument reports to the project owner; the owner asks the desk; the desk asks the human, as an ask. A single stuck ticket is never a page to the human. A system-level failure (provider down, out of capacity, rate-limited) is one line in the status reply, not a question per ticket.
|
|
36
|
+
- **One door.** If the person needs a second place to look after your reply, add the missing block to the reply next time rather than pointing them at a dashboard.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# The one-reply shape
|
|
2
|
+
|
|
3
|
+
This reference restates an invariant of this skill: how a status answer is shaped. The facts inside it come live from `node scripts/snapshot.mjs` and `node scripts/explain.mjs`; the machine-readable schema is `assets/status-reply.json`.
|
|
4
|
+
|
|
5
|
+
## The rule
|
|
6
|
+
|
|
7
|
+
The person asked one question and gets one reply. If they would need a second surface, a second message, or a follow-up question from you to know where things stand, the reply is wrong. Every line names a ticket identifier. Nothing in the reply is a guess: a fact you could not read is named as unreadable, with where it lives.
|
|
8
|
+
|
|
9
|
+
## The five blocks, in this order
|
|
10
|
+
|
|
11
|
+
1. **In flight.** Tickets a phase is running on right now, or that hold a live lease mid-ladder. Source: the snapshot's `running` block (fleet activity, the agent roster, lease attributions). One line per ticket: identifier, the phase running, how long it has run.
|
|
12
|
+
2. **Blocked, and on whom.** Tickets nothing is offered for. Source: the queue's excluded rows, then `explain` on each one the person cares about. Every line carries the reason in the person's words and who releases it: the human (an ask), a role, the cloud itself (a backoff, a park that self-releases), or a clock. "On whom" is never blank; if you cannot tell, say the reason the cloud gave and that the release is unknown to a key.
|
|
13
|
+
3. **Waiting on the human.** The open asks, ranked by what each one holds. Source: `waitingOnHuman` in the snapshot. Keep this to identifier, the question, and what it releases; the `what-needs-me` skill owns the detail and the settling.
|
|
14
|
+
4. **Closed.** What reached the done slot in the window the person asked about (or since your last reply). Source: `query issues` filtered by the team's done-slot stage name from the contract, or the change feed for a time window. When you did not read a window, say "since my last reply" and mean it.
|
|
15
|
+
5. **Next.** What the queue picks up next, in the cloud's order, with the phase each will run. Source: the snapshot's `queue` block. Do not reorder it to what you think should be next; the levers are in `references/reprioritising.md`.
|
|
16
|
+
|
|
17
|
+
A sixth block, **cannot see**, appears only when it is non-empty: the facts an account key cannot read yet (coding-account status, per-ticket execution history), each with the URL the CLI printed.
|
|
18
|
+
|
|
19
|
+
## The header line
|
|
20
|
+
|
|
21
|
+
The reply opens with one clause that says when and from where: the snapshot's `takenAt` and its source line. When the replica was stale or absent, the reply says the numbers came from the API; when it was fresh, it says the cursor. This is not decoration. A stale source that goes unmentioned is the way a wrong status reply happens.
|
|
22
|
+
|
|
23
|
+
## Writing the lines
|
|
24
|
+
|
|
25
|
+
- Identifier first, then the stage as the contract spells it for that team, then one clause. `KEY-123 · <stage name> · implement running 14 min`.
|
|
26
|
+
- Age in human units (minutes, hours, days), from `updated_at` or the lease's start, never a raw timestamp.
|
|
27
|
+
- A reason is the translation from `references/why-is-it-stuck.md`, not the cloud's snake_case token, unless the token is one the table does not know, in which case quote it as the cloud spelled it.
|
|
28
|
+
- No adjectives about health. "Stuck" has a definition (`references/reading-the-board.md`); use it only when it applies.
|
|
29
|
+
|
|
30
|
+
## What the reply never does
|
|
31
|
+
|
|
32
|
+
- It never restates a stage name, threshold or label from memory. The snapshot's `tenant` block carries the live values; read them there each time.
|
|
33
|
+
- It never answers an ask, proposes a default on the human's behalf inside the status reply, or moves anything. Routing a request is `references/routing-work.md`; a decision is the `what-needs-me` skill.
|
|
34
|
+
- It never pads a short answer. When one ticket is in flight and nothing is blocked, the reply is three lines.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Why is it stuck
|
|
2
|
+
|
|
3
|
+
This reference restates an invariant: the vocabulary the cloud's eligibility explainer uses when nothing is offered for a ticket, translated for a person, with what releases each. `node scripts/explain.mjs <ticket>` prints the reason for one ticket; this table turns it into the next action. The `how-catalyst-works` skill carries the mechanism behind each row.
|
|
4
|
+
|
|
5
|
+
## How to read a reason
|
|
6
|
+
|
|
7
|
+
`explain` prints one paragraph: the queue position (or none), the status, the reason, a detail or marker when the cloud gave one, the last failure, and any advisories. Match the reason to a row below. "Releases itself" means the cloud clears it with no human action; "needs" names who acts. When a reason is not in this table, `explain` prints it as the cloud spelled it; report it verbatim and say the bundle does not know it.
|
|
8
|
+
|
|
9
|
+
## Reasons that release themselves
|
|
10
|
+
|
|
11
|
+
| reason | what it means | how it clears |
|
|
12
|
+
| -- | -- | -- |
|
|
13
|
+
| `retry_backoff` | the failed phase is retrying in place and waiting out its rung | a clock; the rungs are the contract's `thresholds.retryBackoffMs` |
|
|
14
|
+
| `lease_held` | a live container already holds this phase | the phase finishes |
|
|
15
|
+
| `intake_lease_held` | a later phase is offered while an intake container still holds the ticket | intake finishes |
|
|
16
|
+
| `environment_check_running` | the repository's environment check is in flight | the check finishes |
|
|
17
|
+
| `runner_image_breaker` | fleet-wide: the live runner image fails every phase at startup, so dispatch is held rather than parking tickets | the cloud moves the pin; one alert per tenant, no per-ticket action |
|
|
18
|
+
| `routing_unavailable` | claimed, then refused at kickoff: no route, no eligible coding-account slot, or the provider is unavailable | provider recovery or a slot freeing; if it persists, the tenant admin checks coding accounts in settings |
|
|
19
|
+
| `cooling_down` (with a named callback) | the phase is parked on a condition the cloud watches | the callback fires; the `detail` names it |
|
|
20
|
+
|
|
21
|
+
## Reasons that need a human
|
|
22
|
+
|
|
23
|
+
| reason | what it means | who acts and how |
|
|
24
|
+
| -- | -- | -- |
|
|
25
|
+
| `ask_ticket` | it carries an ask label; a question is never work | the human answers the ask (`what-needs-me`) |
|
|
26
|
+
| `ask_shape_suspected` | its own text reads as a decision request though it carries no ask label | the human either answers it as an ask or applies the release label the contract names |
|
|
27
|
+
| `blocked` | a live blocking relation holds it | whoever owns the blocker closes it; if the blocker is an ask, that is the human |
|
|
28
|
+
| `not_at_dispatch_stage` | the card is not in the team's dispatch column | someone moves the card to the dispatch slot (`references/reprioritising.md`) |
|
|
29
|
+
| `not_at_pr_stage` | merge is next but the card is not in the PR column | the card returns to the pr slot; an automation that bounced it should be found |
|
|
30
|
+
| `no_change_hold` | a repair round changed nothing | a human comment on the ticket, or a new push to the branch |
|
|
31
|
+
| `waiting_on` | a merge-gate failure with no cause the cloud can repair | read the merge-wait comment on the ticket; usually a hold label or a review a person must give (`catalyst-github`) |
|
|
32
|
+
| `externally_claimed` | a worker outside the cloud holds it | that worker, or the human removes the local-lane label |
|
|
33
|
+
| `environment_check_required` / `_failed` / `_expired` / `_hash_mismatch` | the repository's environment gate has not passed | the tenant admin runs or fixes the environment check in settings |
|
|
34
|
+
| `repo_paused` | an operator paused the repository | the operator resumes it |
|
|
35
|
+
| `scope_overlap` | its declared file scope intersects a ticket already in flight | wait for the other ticket, or the human decides which goes first |
|
|
36
|
+
|
|
37
|
+
## Reasons that need an operator, and what a key cannot do
|
|
38
|
+
|
|
39
|
+
| reason | what it means | note |
|
|
40
|
+
| -- | -- | -- |
|
|
41
|
+
| `cooling_down` (parked after repeated failures, or the round cap was spent) | the phase is parked and does not release itself | an operator unparks it; an account key has no unpark verb yet, so the reply names the park and the settings page |
|
|
42
|
+
| `remediate_parked` | the repair phase itself is parked, so the failing phase has nowhere to be repaired | as above |
|
|
43
|
+
| `validate_class_spent` | this validate failure already spent its one repair round in this episode | as above; a human comment may release the hold |
|
|
44
|
+
| `no_branch_to_remediate` / `branch_missing` / `branch_gone` | a branch-dependent phase has no branch to clone | the cloud releases missing-branch parks on its own budget; a deleted branch needs a human to decide |
|
|
45
|
+
| `stale_failure_episode` | the ladder advanced after the recorded failure | informational; the round is dropped |
|
|
46
|
+
|
|
47
|
+
## Reasons that are not problems
|
|
48
|
+
|
|
49
|
+
`ticket_terminal` (done or canceled), `pipeline_complete` (every phase ran), and `pr_merged` (the PR merged, nothing left to clone) describe finished work. Report them as closed, not blocked.
|
|
50
|
+
|
|
51
|
+
## When the cloud could not judge
|
|
52
|
+
|
|
53
|
+
A status of `unknown` with a reason such as `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown` or `branch_snapshot_unknown` means the evaluator failed closed rather than guessing. Most clear on the cloud's next pass. If one persists for a team, the readiness checks (`catalyst-setup`) are the next read.
|
|
54
|
+
|
|
55
|
+
## The one advisory
|
|
56
|
+
|
|
57
|
+
`human_addressed_unlabeled_ask_suspect` gates nothing: the ticket is assigned to a human with no delegate and may be an unlabelled ask. Mention it in the waiting-on-human block with a question mark.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// explain.mjs — "why is this ticket stuck?" in one paragraph. A thin wrapper over
|
|
3
|
+
// `catalyst-skills explain <ticket>`, which reads the cloud's eligibility explainer for the ticket's
|
|
4
|
+
// team and translates the exclusion reason, the failure block and any advisories into plain English.
|
|
5
|
+
import { mustRun, parseFlags, printHelp } from "./lib/cli.mjs";
|
|
6
|
+
|
|
7
|
+
const SPEC = {
|
|
8
|
+
json: { value: false, help: "print the raw eligibility row and the explanation as JSON" },
|
|
9
|
+
history: { value: false, help: "ask for the per-ticket execution history (the CLI says when a key cannot see it yet)" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
|
|
13
|
+
const ticket = positionals[0];
|
|
14
|
+
if (help || !ticket) {
|
|
15
|
+
printHelp("node scripts/explain.mjs <ticket> [--json] [--history] [--help]", SPEC, [
|
|
16
|
+
"The ticket is its identifier (the KEY-123 shape). The explanation names the queue position, the status,",
|
|
17
|
+
"the reason nothing is offered, the last failure, and what releases it; an unknown reason is printed as the",
|
|
18
|
+
"cloud spelled it. Read references/why-is-it-stuck.md to turn the reason into the next action.",
|
|
19
|
+
]);
|
|
20
|
+
process.exit(help ? 0 : 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const args = ["explain", ticket];
|
|
24
|
+
if (flags.history) args.push("--history");
|
|
25
|
+
if (flags.json) args.push("--json");
|
|
26
|
+
const r = mustRun(args);
|
|
27
|
+
process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`);
|
|
28
|
+
process.exit(0);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/cli.mjs — the one way a skill script reaches Catalyst Cloud: spawn the catalyst-skills CLI.
|
|
3
|
+
// The CLI holds the SDK and the account key; this file holds neither. It reads customer.json only
|
|
4
|
+
// to learn where the CLI lives, and it exits 2 with one line when the machine is not connected.
|
|
5
|
+
// Run any script beside this one with --help; this file is a library and is never run directly.
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
11
|
+
export const NOT_CONFIGURED_EXIT = 2;
|
|
12
|
+
|
|
13
|
+
export function configPath() {
|
|
14
|
+
const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/";
|
|
15
|
+
return join(home, ".config", "catalyst-cloud", "customer.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The connected-machine config, or exit 2 with the one line that says how to connect. */
|
|
19
|
+
export function loadConfig() {
|
|
20
|
+
const path = configPath();
|
|
21
|
+
if (!existsSync(path)) notConfigured(`no config at ${path}`);
|
|
22
|
+
try {
|
|
23
|
+
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
+
if (typeof cfg !== "object" || cfg === null || typeof cfg.key !== "string") notConfigured(`config at ${path} has no key`);
|
|
25
|
+
return cfg;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err && err.exitCode === NOT_CONFIGURED_EXIT) throw err;
|
|
28
|
+
notConfigured(`config at ${path} is unreadable`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function notConfigured(why) {
|
|
33
|
+
process.stderr.write(`not connected (${why}) — run: CATALYST_CLOUD_TOKEN=<account key> npx ${PACKAGE} login\n`);
|
|
34
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run one catalyst-skills verb and return {code, stdout, stderr}. Uses the CLI path the login
|
|
39
|
+
* recorded when it still exists, else `npx <package>`. A CLI exit of 2 with a "not joined" or
|
|
40
|
+
* "not connected" line is turned into this script's own exit 2, so every caller sees one contract.
|
|
41
|
+
*/
|
|
42
|
+
export function runCli(args, opts = {}) {
|
|
43
|
+
const cfg = loadConfig();
|
|
44
|
+
const useRecorded = typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
|
|
45
|
+
const cmd = useRecorded ? process.execPath : "npx";
|
|
46
|
+
const argv = useRecorded ? [cfg.cliPath, ...args] : [PACKAGE, ...args];
|
|
47
|
+
const res = spawnSync(cmd, argv, {
|
|
48
|
+
input: opts.stdin,
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
51
|
+
shell: !useRecorded && process.platform === "win32",
|
|
52
|
+
env: process.env,
|
|
53
|
+
});
|
|
54
|
+
if (res.error) {
|
|
55
|
+
process.stderr.write(`could not run ${cmd}: ${res.error.message}\n`);
|
|
56
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
57
|
+
}
|
|
58
|
+
const stdout = res.stdout ?? "";
|
|
59
|
+
const stderr = res.stderr ?? "";
|
|
60
|
+
if (res.status === NOT_CONFIGURED_EXIT && /not (joined|connected|configured)/i.test(stderr)) {
|
|
61
|
+
process.stderr.write(stderr);
|
|
62
|
+
process.exit(NOT_CONFIGURED_EXIT);
|
|
63
|
+
}
|
|
64
|
+
return { code: res.status ?? 1, stdout, stderr };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Run a verb and require success; on failure print the CLI's own stderr and exit with its code (or 1). */
|
|
68
|
+
export function mustRun(args, opts = {}) {
|
|
69
|
+
const r = runCli(args, opts);
|
|
70
|
+
if (r.code !== 0) {
|
|
71
|
+
if (r.stderr) process.stderr.write(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`);
|
|
72
|
+
if (r.stdout && !opts.quiet) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`);
|
|
73
|
+
process.exit(r.code === 0 ? 1 : r.code);
|
|
74
|
+
}
|
|
75
|
+
return r;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Parse the CLI's --json stdout; a non-JSON answer is a failed check, exit 1. */
|
|
79
|
+
export function parseJson(stdout, what = "output") {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(stdout.trim());
|
|
82
|
+
} catch {
|
|
83
|
+
process.stderr.write(`could not parse ${what} as JSON: ${stdout.trim().slice(0, 200)}\n`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Minimal flag parsing for the scripts: --name value, --name=value, --flag, repeated flags collect. */
|
|
89
|
+
export function parseFlags(argv, spec) {
|
|
90
|
+
const flags = {};
|
|
91
|
+
const positionals = [];
|
|
92
|
+
for (let i = 0; i < argv.length; i++) {
|
|
93
|
+
const a = argv[i];
|
|
94
|
+
if (a === "--help" || a === "-h") return { help: true, flags, positionals };
|
|
95
|
+
if (a.startsWith("--")) {
|
|
96
|
+
let name = a.slice(2);
|
|
97
|
+
let value;
|
|
98
|
+
const eq = name.indexOf("=");
|
|
99
|
+
if (eq !== -1) {
|
|
100
|
+
value = name.slice(eq + 1);
|
|
101
|
+
name = name.slice(0, eq);
|
|
102
|
+
}
|
|
103
|
+
const s = spec[name];
|
|
104
|
+
if (!s) {
|
|
105
|
+
process.stderr.write(`unknown option --${name} (try --help)\n`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
if (s.value) {
|
|
109
|
+
if (value === undefined) value = argv[++i];
|
|
110
|
+
if (value === undefined || value === "") {
|
|
111
|
+
process.stderr.write(`--${name} needs a value\n`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
if (s.repeat) (flags[name] ??= []).push(value);
|
|
115
|
+
else flags[name] = value;
|
|
116
|
+
} else flags[name] = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
positionals.push(a);
|
|
120
|
+
}
|
|
121
|
+
return { help: false, flags, positionals };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function printHelp(usage, spec, notes = []) {
|
|
125
|
+
const lines = [`Usage: ${usage}`, ""];
|
|
126
|
+
const names = Object.keys(spec);
|
|
127
|
+
if (names.length) {
|
|
128
|
+
lines.push("Options:");
|
|
129
|
+
for (const n of names) lines.push(` --${n}${spec[n].value ? " <value>" : ""} ${spec[n].help}`);
|
|
130
|
+
lines.push("");
|
|
131
|
+
}
|
|
132
|
+
lines.push(...notes, "", "Exit codes: 0 ok · 1 the check failed or the arguments were wrong · 2 this machine is not connected to a tenant");
|
|
133
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
134
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// snapshot.mjs — one call that answers "where are we?": the tenant contract (trimmed to what a
|
|
3
|
+
// status reply needs), what is running, what is queued, what is waiting on a human, and whether the
|
|
4
|
+
// local replica is fresh. Prints ONE JSON document on stdout and the source line on stderr.
|
|
5
|
+
import { mustRun, parseFlags, parseJson, printHelp, runCli } from "./lib/cli.mjs";
|
|
6
|
+
|
|
7
|
+
const SPEC = {
|
|
8
|
+
team: { value: true, help: "limit the queue and the board to this team key" },
|
|
9
|
+
board: { value: false, help: "also include open tickets grouped by stage, in the contract's slot order" },
|
|
10
|
+
limit: { value: true, help: "board: max tickets to read (default 200)" },
|
|
11
|
+
"full-contract": { value: false, help: "include the whole contract instead of the trimmed tenant block" },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { help, flags } = parseFlags(process.argv.slice(2), SPEC);
|
|
15
|
+
if (help) {
|
|
16
|
+
printHelp("node scripts/snapshot.mjs [--team K] [--board] [--limit N] [--full-contract] [--help]", SPEC, [
|
|
17
|
+
"Runs, in order: catalyst-skills contract, running, queue, ask list, replica status (and query issues with --board).",
|
|
18
|
+
"Output: one JSON document {takenAt, source, tenant, running, queue, waitingOnHuman, board?, errors?}.",
|
|
19
|
+
"A section the cloud refused is reported under errors and the rest still prints; exit 1 in that case.",
|
|
20
|
+
"Every tenant fact (stage names, thresholds, teams) comes from the contract in this output, never from prose.",
|
|
21
|
+
]);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const errors = {};
|
|
26
|
+
const section = (name, args) => {
|
|
27
|
+
const r = runCli(args);
|
|
28
|
+
if (r.code !== 0) {
|
|
29
|
+
errors[name] = (r.stderr || r.stdout || `exit ${r.code}`).trim().split("\n").at(-1);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return parseJson(r.stdout, name);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const contract = parseJson(mustRun(["contract", "--json"], { quiet: true }).stdout, "contract");
|
|
36
|
+
// `replica status` exits 0 fresh, 1 stale, 3 absent — all three are verdicts with JSON on stdout,
|
|
37
|
+
// not failures; only a non-JSON answer is an error.
|
|
38
|
+
const replica = (() => {
|
|
39
|
+
const r = runCli(["replica", "status", "--json"]);
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(r.stdout.trim());
|
|
42
|
+
} catch {
|
|
43
|
+
errors.replica = (r.stderr || r.stdout || `exit ${r.code}`).trim().split("\n").at(-1);
|
|
44
|
+
return { verdict: "unknown", cursor: null, heartbeatAgeMs: null };
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
const running = section("running", ["running", "--json"]);
|
|
48
|
+
const queue = section("queue", flags.team ? ["queue", "--team", flags.team, "--json"] : ["queue", "--json"]);
|
|
49
|
+
const asks = section("waitingOnHuman", ["ask", "list", "--json"]);
|
|
50
|
+
|
|
51
|
+
const out = {
|
|
52
|
+
takenAt: new Date().toISOString(),
|
|
53
|
+
source: {
|
|
54
|
+
reads: "api",
|
|
55
|
+
replica: replica.verdict,
|
|
56
|
+
replicaCursor: replica.cursor ?? null,
|
|
57
|
+
replicaHeartbeatAgeMs: replica.heartbeatAgeMs ?? null,
|
|
58
|
+
},
|
|
59
|
+
tenant: flags["full-contract"] ? contract : trimContract(contract, flags.team),
|
|
60
|
+
running,
|
|
61
|
+
queue,
|
|
62
|
+
waitingOnHuman: asks,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (flags.board) {
|
|
66
|
+
const args = ["query", "issues", "--limit", String(flags.limit ?? 200), "--json"];
|
|
67
|
+
if (flags.team) args.push("--team", flags.team);
|
|
68
|
+
const r = runCli(args);
|
|
69
|
+
const sourceLine = (r.stderr.match(/^source: .*$/m) ?? [null])[0];
|
|
70
|
+
if (r.code !== 0) errors.board = (r.stderr || `exit ${r.code}`).trim().split("\n").at(-1);
|
|
71
|
+
else {
|
|
72
|
+
const rows = parseJson(r.stdout, "issues");
|
|
73
|
+
out.board = groupByStage(rows, contract, flags.team);
|
|
74
|
+
if (sourceLine) out.source.board = sourceLine.replace(/^source: /, "");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Object.keys(errors).length) out.errors = errors;
|
|
79
|
+
process.stderr.write(`source: api for running/queue/asks; replica ${replica.verdict}${replica.cursor != null ? ` (cursor ${replica.cursor})` : ""}${out.source.board ? `; board from ${out.source.board}` : ""}\n`);
|
|
80
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
81
|
+
process.exit(Object.keys(errors).length ? 1 : 0);
|
|
82
|
+
|
|
83
|
+
/** The contract facts a status reply needs, and nothing a reply should not paste. */
|
|
84
|
+
function trimContract(doc, teamKey) {
|
|
85
|
+
const teams = (doc.teams ?? []).filter((t) => !teamKey || String(t.key ?? "").toUpperCase() === teamKey.toUpperCase());
|
|
86
|
+
return {
|
|
87
|
+
contractVersion: doc.contractVersion,
|
|
88
|
+
account: { slug: doc.account?.slug, name: doc.account?.name },
|
|
89
|
+
slots: doc.slots,
|
|
90
|
+
ladder: { phases: doc.ladder?.phases, intakeEnabled: doc.ladder?.intakeEnabled, keying: doc.ladder?.keying },
|
|
91
|
+
thresholds: doc.thresholds,
|
|
92
|
+
mergeDefaultPolicy: doc.merge?.defaultPolicy,
|
|
93
|
+
teams: teams.map((t) => ({
|
|
94
|
+
key: t.key,
|
|
95
|
+
name: t.name,
|
|
96
|
+
workflowMode: t.workflowMode,
|
|
97
|
+
readiness: t.readiness?.status,
|
|
98
|
+
stages: Object.fromEntries(
|
|
99
|
+
(doc.slots ?? Object.keys(t.stages ?? {}))
|
|
100
|
+
.filter((slot) => t.stages?.[slot])
|
|
101
|
+
.map((slot) => [slot, { name: t.stages[slot].name, type: t.stages[slot].type, stateStillExists: t.stages[slot].stateStillExists }]),
|
|
102
|
+
),
|
|
103
|
+
labels: {
|
|
104
|
+
ask: (t.labels?.ask ?? []).map((l) => l.name),
|
|
105
|
+
hold: (t.labels?.hold ?? []).map((l) => l.name),
|
|
106
|
+
release: (t.labels?.release ?? []).map((l) => l.name),
|
|
107
|
+
},
|
|
108
|
+
})),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Open tickets by stage name, ordered by the contract's slot order for the ticket's team; unmapped
|
|
113
|
+
* state names follow, alphabetically. Terminal-typed states are dropped so the board is work only. */
|
|
114
|
+
function groupByStage(rows, doc, teamKey) {
|
|
115
|
+
const order = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const terminal = new Set();
|
|
118
|
+
for (const t of doc.teams ?? []) {
|
|
119
|
+
if (teamKey && String(t.key ?? "").toUpperCase() !== teamKey.toUpperCase()) continue;
|
|
120
|
+
for (const slot of doc.slots ?? []) {
|
|
121
|
+
const s = t.stages?.[slot];
|
|
122
|
+
if (!s?.name) continue;
|
|
123
|
+
if (slot === "done" || slot === "canceled" || ["completed", "canceled", "cancelled"].includes(String(s.type ?? "").toLowerCase())) terminal.add(s.name);
|
|
124
|
+
else if (!seen.has(s.name)) {
|
|
125
|
+
seen.add(s.name);
|
|
126
|
+
order.push(s.name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const byStage = {};
|
|
131
|
+
for (const r of rows) {
|
|
132
|
+
const state = String(r.state ?? "unknown");
|
|
133
|
+
if (terminal.has(state)) continue;
|
|
134
|
+
(byStage[state] ??= []).push({
|
|
135
|
+
identifier: r.identifier,
|
|
136
|
+
title: r.title,
|
|
137
|
+
priority: r.priority ?? null,
|
|
138
|
+
assignee: r.assignee_name ?? r.assignee ?? null,
|
|
139
|
+
updatedAt: r.updated_at ?? null,
|
|
140
|
+
projectId: r.project_id ?? null,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const extra = Object.keys(byStage).filter((s) => !seen.has(s)).sort();
|
|
144
|
+
return { stageOrder: [...order, ...extra], byStage, terminalStagesDropped: [...terminal] };
|
|
145
|
+
}
|
package/skills/ask/SKILL.md
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ask
|
|
3
|
-
description:
|
|
4
|
-
Record a decision request to the human on the customer's catalyst-cloud tenant. Use when active work is gated on a decision only the human can make — a product/priority/approval call or an action only they can take. The ask carries the question, concrete options, a default that fires if they are silent, and what it blocks. Never use for brainstorming or questions the human asked first.
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills — adapted from the catalyst repository's catalyst-dev plugin (plugins/dev/skills/ask) for customer tenants -->
|
|
8
|
-
|
|
9
|
-
# Ask — one decision, one record
|
|
10
|
-
|
|
11
|
-
An ask is a decision request with a deadline-shaped default. It exists so that a question the human must answer is **one record they can find**, not a TODO line, a chat message, or a hope.
|
|
12
|
-
|
|
13
|
-
## Shape of an ask
|
|
14
|
-
|
|
15
|
-
An ask always carries, verbatim:
|
|
16
|
-
|
|
17
|
-
1. **The question** — one sentence, as its title.
|
|
18
|
-
2. **Options** — the realistic choices, each one line.
|
|
19
|
-
3. **Default if silent** — what proceeds, and after how long. The default must be sane enough to actually run.
|
|
20
|
-
4. **Blocks** — what work is held until it is answered, named by ticket or scope.
|
|
21
|
-
|
|
22
|
-
## Where an ask lives
|
|
23
|
-
|
|
24
|
-
An ask is a **ticket in the tenant's own Linear**, labelled `catalyst-ask`, written through the same Linear write access the `linearis` skill uses (the customer's own Linear tooling and credential — never a credential you improvised). The tenant's mirror recognises that label and holds the ticket out of the dispatchable pool, so a question is never mistaken for work. Cite the ask's identifier only after the create call returned it.
|
|
25
|
-
|
|
26
|
-
## Rules
|
|
27
|
-
|
|
28
|
-
- **File the ask BEFORE proceeding on the default.** Work may continue on the default only after the ask ticket exists; it may never run gated on an unasked question, and it may never run on a default that has no ticket behind it.
|
|
29
|
-
- **One ask per decision.** Search for an existing ask first (`/api/v1/search?q=<terms>` through the `linearis` skill) — a duplicate splits one decision's urgency across two records.
|
|
30
|
-
- **The human answers in the ask**, and when the answer arrives elsewhere (chat), post it back to the ask so question, options, default, answer, and who answered are one record.
|
|
31
|
-
- **Never answer another role's ask**, and never answer as the human.
|
|
32
|
-
- **Not everything is an ask.** Brainstorming, design back-and-forth, and questions the human asked first need no ticket. Operational retries, provider outages, and anything another agent can decide are never asks.
|
|
33
|
-
- **Rank by blast radius**, not age: an ask that blocks a whole project outranks one that blocks a chore.
|
|
34
|
-
|
|
35
|
-
## When no ticket can be filed
|
|
36
|
-
|
|
37
|
-
If the tenant has no Linear write access wired up (the `linearis` skill reports none), the ask **cannot be filed, so the default does not fire**. Stop the gated work, and say so in your reply to the human with all four fields spelled out and the one action that unblocks everything: wire the Linear write path (or file the ask ticket by hand and tell you its identifier). Do not record the ask in a status summary, a TODO, or a handoff as a substitute — those are pointers to an ask, never the ask — and do not proceed on the default while no ticket exists.
|