@catalyst-cloud/catalyst-skills 0.1.1 → 0.2.1
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 +8 -0
- package/README.md +109 -53
- package/bin/catalyst-skills.js +42 -2
- package/dist/args.js +242 -0
- package/dist/ask.js +137 -0
- package/dist/cli.js +167 -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 +308 -0
- package/dist/http.js +175 -0
- package/dist/prompt.js +35 -0
- package/dist/query.js +225 -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 +12 -7
- 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,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// search.mjs — tenant search across ticket identifiers and titles, pull-request titles, project and
|
|
3
|
+
// initiative names. Always the origin-fresh API (the replica holds no search view).
|
|
4
|
+
import { exitOnFailure, parseFlags, parseJson, relayStderr, runCli, usage, wantsHelp } from "./lib/cli.mjs";
|
|
5
|
+
|
|
6
|
+
const HELP = `Usage: node scripts/search.mjs <terms...> [--limit <n>] [--json]
|
|
7
|
+
|
|
8
|
+
Searches your tenant for tickets, pull requests, projects and initiatives matching the terms.
|
|
9
|
+
Wraps: catalyst-skills query search.
|
|
10
|
+
|
|
11
|
+
<terms> one or more words; matched against identifiers, titles and names
|
|
12
|
+
--limit <n> max rows (default 50)
|
|
13
|
+
--json print the rows as JSON
|
|
14
|
+
|
|
15
|
+
Exit 0 (an empty result is still 0 and prints "no matches"), 1 on a usage error, 2 when this
|
|
16
|
+
machine is not connected to a tenant or the cloud refused the search (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"], value: ["limit"] });
|
|
24
|
+
if (positionals.length === 0) usage("search needs at least one term (see --help)");
|
|
25
|
+
|
|
26
|
+
const args = ["query", "search", ...positionals, "--json"];
|
|
27
|
+
if (flags.limit) args.push("--limit", flags.limit);
|
|
28
|
+
const r = runCli(args);
|
|
29
|
+
exitOnFailure(r);
|
|
30
|
+
relayStderr(r);
|
|
31
|
+
const rows = parseJson(r.stdout);
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
console.log(JSON.stringify(rows ?? r.stdout.trimEnd()));
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const list = Array.isArray(rows) ? rows : rows && typeof rows === "object" ? Object.values(rows).flat() : [];
|
|
37
|
+
if (list.length === 0) {
|
|
38
|
+
console.log("no matches");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
for (const row of list) {
|
|
42
|
+
if (!row || typeof row !== "object") {
|
|
43
|
+
console.log(String(row));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const kind = row.kind ?? (row.identifier ? "issue" : row.number ? "pull" : "row");
|
|
47
|
+
const id = row.identifier ?? (row.number !== undefined ? `#${row.number}` : row.id ?? "");
|
|
48
|
+
console.log(`${kind} ${id} ${row.title ?? row.name ?? ""}${row.state ? ` (${row.state})` : ""}`);
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: catalyst-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Am I set up? Machine readiness (Node, the tenant connection, the cached contract, the CLI path, the skills, the SDK, the optional replica) plus tenant readiness from the contract's ten per-team checks, in one verdict: what passes, what is blocked, what is merely waiting, and who can click what. Use when someone asks "am I set up", "what is missing", "why does nothing happen", "is the replica running", or right after connecting a new machine. Reports; never repairs.
|
|
5
|
+
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
|
+
---
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
|
|
8
|
+
|
|
9
|
+
# Am I set up?
|
|
10
|
+
|
|
11
|
+
You answer one question with one verdict and one list. The verdict is READY or NOT READY. The list is who can click what: the fixes only the person at the keyboard can make, the fixes only a tenant owner or admin can make in settings, and the checks that are simply waiting for the first event. The live check ids, severities, states and the people who can answer come from the contract; the scripts print them, you never restate them.
|
|
12
|
+
|
|
13
|
+
## Run first
|
|
14
|
+
|
|
15
|
+
- `node scripts/check.mjs --help` — every machine and tenant check, one line each, the fix and who for each failure, the verdict, then the who-can-click-what list. Exit 1 when NOT READY.
|
|
16
|
+
- `node scripts/replica-status.mjs --help` — the optional replica's verdict with no network (add `--probe` for how far behind it is). Exit 0 fresh, 1 stale, 2 not connected, 3 absent.
|
|
17
|
+
|
|
18
|
+
## Load on demand
|
|
19
|
+
|
|
20
|
+
| when | read |
|
|
21
|
+
| -- | -- |
|
|
22
|
+
| any check is red, unknown or waiting and the person asks what it proves, how to fix it, or who can | `references/what-each-check-means.md` |
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- Report, never repair. No tenant-reachable repair verb exists for an account key yet; the settings page is where a tenant owner or admin fixes a mapping, a connection or a label, and you say which one.
|
|
27
|
+
- End every answer with the verdict and the who-can-click-what list, in that order.
|
|
28
|
+
- A stale or absent replica is a note, never a failure: every skill reads the API meanwhile and says so. Do not tell the person to start it unless they want local SQL or cheaper repeated reads.
|
|
29
|
+
- Not connected (exit 2) means the connect step, not a retry: `CATALYST_CLOUD_TOKEN=<account key> npx @catalyst-cloud/catalyst-skills login`, with the key from their tenant admin. Never guess a tenant; the key is the only selector.
|
|
30
|
+
- Waiting is not failing. "No write observed", "no delivery observed" and "no host connected" clear themselves the first time the thing happens; say that instead of raising them.
|
|
31
|
+
- Unknown is not a pass. A check the engine could not run is reported as such, never rounded up.
|
|
32
|
+
- Never run a check in a loop. If the person wants to know when a waiting check clears, that is the project-running skill's watch.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Am I set up?"
|
|
3
|
+
short_description: "Machine and tenant readiness in one verdict: what passes, what is blocked, and who can click what"
|
|
4
|
+
default_prompt: "Use $catalyst-setup to tell me whether this machine and my tenant are ready, and what is missing."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# What each check means
|
|
2
|
+
|
|
3
|
+
This page restates invariants: the ten per-team readiness checks the cloud runs, what each proves, its fix and who can click it; the machine checks the CLI adds; and the four replica verdicts. The live values are never restated: each check's severity and whether it needs a human's answer come from the contract's `readinessChecks[]`, each team's current states from `teams[].readiness`, and the people who can answer from `humans[]`. `node scripts/check.mjs` prints all of it; this page is how to read what it printed.
|
|
4
|
+
|
|
5
|
+
## How a team's readiness is scored
|
|
6
|
+
|
|
7
|
+
A team's status is one of `ready`, `degraded`, `blocked` or `unchecked`. The engine always reports all ten checks; a check it could not run is `unknown`, never `pass`. Any failing check the contract marks as blocking makes the team blocked; any unknown, and any failing check marked degrading, makes it degraded. Unchecked means no readiness pass has run for that team yet, which is a note, not a failure. Readiness is stamped with the account-wide mapping revision it was computed against, so a stale verdict is visible as such.
|
|
8
|
+
|
|
9
|
+
## The ten checks
|
|
10
|
+
|
|
11
|
+
| check id | proves | when it fails, the fix | who clicks |
|
|
12
|
+
| -- | -- | -- | -- |
|
|
13
|
+
| `oauth_scope` | Catalyst holds the Linear permissions it needs | re-authorise the Linear connection to grant the missing scope | a tenant owner or admin, in settings |
|
|
14
|
+
| `token_live` | the Linear connection is accepted right now | reconnect Linear (expired or revoked), or wait and re-check (Linear unreachable). A distinct reason says Linear was never connected at all | owner or admin |
|
|
15
|
+
| `team_visible` | Catalyst can see this team | most often the team was made private: grant Catalyst access in Linear's team settings, then re-check | owner or admin, in Linear |
|
|
16
|
+
| `mapped_states_exist` | every stage Catalyst mapped still exists in Linear | re-map the team; nothing in the workspace was changed. A pending-write reason means a mapping was just saved and the read has not caught up: wait, do not re-map | owner or admin, in settings |
|
|
17
|
+
| `mapping_total` | every stage Catalyst moves tickets into is mapped | map the missing stages. "Absent" means the team was never mapped; "incomplete" means a few of the load-bearing stages are missing | owner or admin, in settings |
|
|
18
|
+
| `types_compatible` | each load-bearing mapped stage is the right kind (dispatch and intake unstarted or backlog, PR started, done completed, canceled canceled) | change the mapping to a stage of the right kind | owner or admin, in settings |
|
|
19
|
+
| `labels_present` | the labels Catalyst uses exist in the workspace | none needed by a person: Catalyst creates them the first time it uses them | nobody |
|
|
20
|
+
| `writes_land` | Catalyst has written to this team successfully | "no write observed" is waiting, not failing: it clears the first time Catalyst moves a ticket. "Write refused" means Linear rejected the last write: check the connection and the team's permissions | owner or admin when refused; otherwise nobody |
|
|
21
|
+
| `webhook_covers_team` | events for this team are arriving | confirmed once a repository is registered and events flow; "no delivery observed" is waiting | owner or admin, by registering the repository |
|
|
22
|
+
| `hosts_current` | no connected host runs an older mapping revision | a host that is behind re-loads the mapping on its next connect; a host that did not report its revision is flagged rather than assumed current; "no host connected" is waiting | whoever runs that host |
|
|
23
|
+
|
|
24
|
+
Three checks are informational by design (labels, event delivery, host currency): they degrade a team but never block it. Which are which is served on `readinessChecks[].severity`; do not memorise the split.
|
|
25
|
+
|
|
26
|
+
## Reasons that look like failures and are not
|
|
27
|
+
|
|
28
|
+
- `no_write_observed`, `no_delivery_observed`, `no_host_connected`: nothing has happened yet. Expected on a fresh tenant; they clear on their own.
|
|
29
|
+
- `stages_pending_write`: a mapping was saved and the read predates it. Re-check shortly; re-mapping would be a second write for no reason.
|
|
30
|
+
- `unknown` on any check: the engine could not look. It is not a pass and not a fail; say so.
|
|
31
|
+
|
|
32
|
+
## The machine checks the CLI adds
|
|
33
|
+
|
|
34
|
+
`catalyst-skills ready` prepends checks about this machine before the tenant's:
|
|
35
|
+
|
|
36
|
+
| id | proves | fix |
|
|
37
|
+
| -- | -- | -- |
|
|
38
|
+
| `node` | Node 22 or newer, which the SDK's built-in SQLite engine needs | install Node 22+ |
|
|
39
|
+
| `config` | this machine is connected: `customer.json` exists and loads | `CATALYST_CLOUD_TOKEN=<account key> npx @catalyst-cloud/catalyst-skills login`; the key comes from the tenant admin |
|
|
40
|
+
| `contract` | the tenant contract is cached and its major version is one this bundle accepts | `catalyst-skills contract --refresh`; a version outside the range means update the bundle. A refresh needs an account key, not a workstation key |
|
|
41
|
+
| `cliPath` | the CLI path recorded at login still exists, so skill scripts can spawn it | re-run login |
|
|
42
|
+
| `skills` | every skill of this bundle is present where the CLI installed them | `catalyst-skills install` |
|
|
43
|
+
| `sdk` | the SDK loads, so the replica and the watch are available | run under Node 22.15 or newer; every read still works through the API meanwhile |
|
|
44
|
+
| `replica` | the optional replica is fresh | a note, never a failure; see below |
|
|
45
|
+
|
|
46
|
+
## The replica's four verdicts
|
|
47
|
+
|
|
48
|
+
`node scripts/replica-status.mjs` needs no network and exits with the verdict:
|
|
49
|
+
|
|
50
|
+
| exit | verdict | what a skill does with it |
|
|
51
|
+
| -- | -- | -- |
|
|
52
|
+
| 0 | fresh: a live writer, a heartbeat younger than the staleness threshold, a non-empty cursor | reads the replica and says so |
|
|
53
|
+
| 1 | stale: the file exists but the writer is gone, the heartbeat is old, or there is no cursor | reads the API and says so; `catalyst-skills replica start --detach` brings it back |
|
|
54
|
+
| 2 | not connected to a tenant | the connect step first |
|
|
55
|
+
| 3 | absent: no replica file at all | reads the API; the replica is optional and one command away |
|
|
56
|
+
|
|
57
|
+
`--probe` adds the one network call: it compares the local cursor with the cloud's head and prints how far behind the replica is, the only honest "how stale" number. A skill must never refuse to work because the replica is down, and must never silently read a stale one; both halves are one exit code away.
|
|
58
|
+
|
|
59
|
+
## Who can click what
|
|
60
|
+
|
|
61
|
+
The last block `check.mjs` prints groups every failure by the person it needs. Machine fixes name "you", the person at the keyboard. Tenant fixes name the owner or admin roles the contract lists (as Linear user ids, since that is how the cloud knows them), because the settings page that repairs a mapping, a connection or a label is theirs. A check the contract marks as not needing an answer names nobody: it is informational or self-clearing. This skill reports; it never repairs, because no tenant-reachable repair verb exists for an account key yet.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check.mjs — "am I set up?" in one verdict: the machine checks the CLI runs (Node, the connection,
|
|
3
|
+
// the cached contract, the CLI path, the skills, the SDK, the optional replica) plus every team's
|
|
4
|
+
// readiness vector from the tenant contract, then the list of who can click what. Wraps
|
|
5
|
+
// `catalyst-skills ready --json`; reports, never repairs. Exit 0 READY, 1 NOT READY, 2 not connected.
|
|
6
|
+
import { parseJson, runCli } from "./lib/cli.mjs";
|
|
7
|
+
|
|
8
|
+
const HELP = `Usage: node scripts/check.mjs [--json]
|
|
9
|
+
|
|
10
|
+
Prints one line per check (ok / note / FAIL), the fix and who can apply it for every failure, the
|
|
11
|
+
verdict, and a "who can click what" list grouped by the person or role each fix needs.
|
|
12
|
+
A note never flips the verdict (the replica is optional; an informational readiness check is a note).
|
|
13
|
+
|
|
14
|
+
--json prints the CLI's report document {ready, checks[]} unchanged.
|
|
15
|
+
Exit 0 READY, 1 NOT READY, 2 this machine is not connected to a tenant yet.`;
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
19
|
+
console.log(HELP);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
const unknown = args.filter((a) => a !== "--json");
|
|
23
|
+
if (unknown.length > 0) {
|
|
24
|
+
console.error(`unknown argument: ${unknown.join(" ")}`);
|
|
25
|
+
console.log(HELP);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const res = runCli(["ready", "--json"]);
|
|
30
|
+
if (res.code === 2) {
|
|
31
|
+
console.error(res.stderr.trim() || "catalyst-skills ready refused");
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
const report = parseJson(res.stdout);
|
|
35
|
+
if (!report || !Array.isArray(report.checks)) {
|
|
36
|
+
console.error(res.stderr.trim() || res.stdout.trim() || "catalyst-skills ready printed no report");
|
|
37
|
+
process.exit(res.code === 0 ? 1 : res.code);
|
|
38
|
+
}
|
|
39
|
+
if (args.includes("--json")) {
|
|
40
|
+
console.log(JSON.stringify(report));
|
|
41
|
+
process.exit(report.ready ? 0 : 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const failures = [];
|
|
45
|
+
for (const c of report.checks) {
|
|
46
|
+
const tag = c.note ? "note" : c.ok ? "ok " : "FAIL";
|
|
47
|
+
console.log(`${tag} ${c.line}`);
|
|
48
|
+
if (!c.ok && !c.note) {
|
|
49
|
+
if (c.fix) console.log(` fix: ${c.fix}`);
|
|
50
|
+
if (c.who) console.log(` who: ${c.who}`);
|
|
51
|
+
failures.push(c);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
console.log(report.ready ? "READY" : "NOT READY");
|
|
55
|
+
|
|
56
|
+
if (failures.length > 0) {
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log("Who can click what:");
|
|
59
|
+
const byWho = new Map();
|
|
60
|
+
for (const c of failures) {
|
|
61
|
+
const who = c.who ?? "unknown (the check named nobody)";
|
|
62
|
+
if (!byWho.has(who)) byWho.set(who, []);
|
|
63
|
+
byWho.get(who).push(c);
|
|
64
|
+
}
|
|
65
|
+
for (const [who, list] of byWho) {
|
|
66
|
+
console.log(` ${who}:`);
|
|
67
|
+
for (const c of list) console.log(` - ${c.id}: ${c.fix ?? c.line}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const notes = report.checks.filter((c) => c.note && !c.ok);
|
|
71
|
+
if (notes.length > 0) {
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(`Notes that do not block: ${notes.map((c) => c.id).join(", ")} — see references/what-each-check-means.md`);
|
|
74
|
+
}
|
|
75
|
+
process.exit(report.ready ? 0 : 1);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/cli.mjs — the one way a skill script reaches the Catalyst Cloud SDK and API: by spawning the
|
|
3
|
+
// catalyst-skills CLI this bundle installed. It reads customer.json to find that CLI and nothing
|
|
4
|
+
// else; it never holds the account key itself. This file is a library — run a sibling script with
|
|
5
|
+
// --help for usage. Identical in every skill of this bundle on purpose (skills install one directory
|
|
6
|
+
// at a time, so nothing shared outside the skill would ever be installed).
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
export const PACKAGE = "@catalyst-cloud/catalyst-skills";
|
|
13
|
+
export const CONNECT_HINT = `this machine is not connected to a tenant yet — run: CATALYST_CLOUD_TOKEN=<account key> npx ${PACKAGE} login`;
|
|
14
|
+
|
|
15
|
+
/** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME before HOME. */
|
|
16
|
+
export function configPath() {
|
|
17
|
+
const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
18
|
+
return join(home, ".config", "catalyst-cloud", "customer.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The customer config, or exit 2 with one line naming the connect command. Never throws. */
|
|
22
|
+
export function requireCustomerConfig() {
|
|
23
|
+
const path = configPath();
|
|
24
|
+
if (!existsSync(path)) {
|
|
25
|
+
console.error(CONNECT_HINT);
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
let cfg;
|
|
29
|
+
try {
|
|
30
|
+
cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error(`${path} could not be read (${err instanceof Error ? err.message : String(err)}) — ${CONNECT_HINT}`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
if (!cfg || typeof cfg !== "object" || typeof cfg.key !== "string" || typeof cfg.baseUrl !== "string") {
|
|
36
|
+
console.error(`${path} is missing required fields — ${CONNECT_HINT}`);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
return cfg;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run one catalyst-skills verb and return {code, stdout, stderr}. Spawns the CLI whose path the
|
|
44
|
+
* connect step recorded in customer.json; falls back to `npx @catalyst-cloud/catalyst-skills` when
|
|
45
|
+
* no path is recorded or it no longer exists. Exits 2 when the machine is not connected.
|
|
46
|
+
*/
|
|
47
|
+
export function runCli(args, opts = {}) {
|
|
48
|
+
const cfg = requireCustomerConfig();
|
|
49
|
+
const recorded = typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
|
|
50
|
+
const command = recorded ? process.execPath : process.platform === "win32" ? "npx.cmd" : "npx";
|
|
51
|
+
const argv = recorded ? [cfg.cliPath, ...args] : [PACKAGE, ...args];
|
|
52
|
+
const res = spawnSync(command, argv, {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
input: opts.stdin,
|
|
55
|
+
env: process.env,
|
|
56
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
57
|
+
shell: !recorded && process.platform === "win32",
|
|
58
|
+
});
|
|
59
|
+
if (res.error) {
|
|
60
|
+
console.error(`could not run ${recorded ? cfg.cliPath : `npx ${PACKAGE}`}: ${res.error.message}`);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run a verb that must succeed. On exit 2 (the CLI's "not configured / refused" class) the script
|
|
68
|
+
* exits 2; on any other non-zero exit it exits 1. The CLI's own stderr is forwarded either way so the
|
|
69
|
+
* reason is never lost.
|
|
70
|
+
*/
|
|
71
|
+
export function runCliOrExit(args, opts = {}) {
|
|
72
|
+
const res = runCli(args, opts);
|
|
73
|
+
if (res.code === 0) return res;
|
|
74
|
+
const why = res.stderr.trim() || res.stdout.trim() || `catalyst-skills ${args.join(" ")} exited ${res.code}`;
|
|
75
|
+
console.error(why);
|
|
76
|
+
process.exit(res.code === 2 ? 2 : 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Parse the CLI's --json stdout; null when it is not JSON (the caller decides what that means). */
|
|
80
|
+
export function parseJson(stdout) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(stdout);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Forward the `source: replica|api (...)` line a query verb prints, so the answer names its source. */
|
|
89
|
+
export function forwardSourceLine(res) {
|
|
90
|
+
for (const line of res.stderr.split("\n")) {
|
|
91
|
+
if (line.startsWith("source:")) console.error(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True for a ticket identifier such as ABC-123; false for a GitHub node id or anything else. */
|
|
96
|
+
export function looksLikeTicket(s) {
|
|
97
|
+
return /^[A-Za-z][A-Za-z0-9]*-\d+$/.test(s);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
101
|
+
console.log("lib/cli.mjs is a library used by the scripts beside it; run any of those with --help for usage.");
|
|
102
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// replica-status.mjs — is the optional local replica alive, and how far behind is it? Wraps
|
|
3
|
+
// `catalyst-skills replica status`, which needs no network (a pidfile, a writer-lock heartbeat and
|
|
4
|
+
// the cursor row); `--probe` adds the one network call that compares the cursor with the cloud's
|
|
5
|
+
// head. The CLI's exit code passes straight through: 0 fresh, 1 present but stale, 2 not connected,
|
|
6
|
+
// 3 absent. A skill reads that code to choose its source; it never refuses to work over it.
|
|
7
|
+
import { runCli } from "./lib/cli.mjs";
|
|
8
|
+
|
|
9
|
+
const HELP = `Usage: node scripts/replica-status.mjs [--probe] [--json]
|
|
10
|
+
|
|
11
|
+
--probe also fetch the cloud's head cursor and print how far behind the replica is
|
|
12
|
+
--json print the CLI's status document {verdict, exitCode, cursor, heartbeatAgeMs, ...}
|
|
13
|
+
|
|
14
|
+
Exit codes, passed through from catalyst-skills replica status:
|
|
15
|
+
0 fresh: a live writer, a young heartbeat, a cursor — skills read the replica
|
|
16
|
+
1 stale: the file exists but the writer is gone or behind — skills read the API and say so
|
|
17
|
+
2 not connected to a tenant — run: CATALYST_CLOUD_TOKEN=<account key> npx @catalyst-cloud/catalyst-skills login
|
|
18
|
+
3 absent: no replica file — optional; start one with: catalyst-skills replica start --detach`;
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
22
|
+
console.log(HELP);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
const unknown = args.filter((a) => a !== "--probe" && a !== "--json");
|
|
26
|
+
if (unknown.length > 0) {
|
|
27
|
+
console.error(`unknown argument: ${unknown.join(" ")}`);
|
|
28
|
+
console.log(HELP);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cliArgs = ["replica", "status"];
|
|
33
|
+
if (args.includes("--probe")) cliArgs.push("--probe");
|
|
34
|
+
if (args.includes("--json")) cliArgs.push("--json");
|
|
35
|
+
const res = runCli(cliArgs);
|
|
36
|
+
if (res.stdout.trim()) console.log(res.stdout.trimEnd());
|
|
37
|
+
if (res.code !== 0 && res.stderr.trim()) console.error(res.stderr.trimEnd());
|
|
38
|
+
|
|
39
|
+
const meaning = {
|
|
40
|
+
0: "fresh — skills read the replica",
|
|
41
|
+
1: "stale — skills read the API and name that source",
|
|
42
|
+
2: "not connected to a tenant",
|
|
43
|
+
3: "absent — the replica is optional; nothing is wrong",
|
|
44
|
+
};
|
|
45
|
+
if (!args.includes("--json")) console.log(`verdict ${res.code}: ${meaning[res.code] ?? "unexpected exit code"}`);
|
|
46
|
+
process.exit(res.code);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: connect-me
|
|
3
|
+
description: >-
|
|
4
|
+
Connect this machine to the customer's Catalyst Cloud tenant with their account key, cache the tenant contract, and verify. Use when a person is getting started, when their config is missing or broken, when they rotated their key, when a skill script exits 2 saying the machine is not connected, or when they ask which tenant this machine belongs to. The key names the tenant, so nobody types an account id. Installing the skills is not this skill's job; the agent's own install command did that.
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
|
+
---
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills — written in this repository for customer tenants -->
|
|
9
|
+
|
|
10
|
+
# Connect me
|
|
11
|
+
|
|
12
|
+
Connecting is one command. It asks the cloud who the key belongs to, writes the config every skill and the CLI read, and caches the tenant contract beside it.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
CATALYST_CLOUD_TOKEN=<account-key> catalyst-skills login
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
With no environment variable and a terminal attached, `catalyst-skills login` prompts for the key without echoing it. `--key <account-key>` is the third form, for a script. `npx @catalyst-cloud/catalyst-skills login` works when the package is not installed globally. A non-default cloud is pinned with `CATALYST_CLOUD_BASE_URL` or `--base-url`.
|
|
19
|
+
|
|
20
|
+
**This skill never installs skills.** You are already reading one, so the person's agent installed the set with its own command. Connecting is only about the credential and the contract.
|
|
21
|
+
|
|
22
|
+
**What it writes.** `~/.config/catalyst-cloud/customer.json` (mode 0600) holding the key, the tenant it resolved, and the absolute path of this CLI so skill scripts can spawn it; `~/.config/catalyst-cloud/contract.json`, the tenant contract with its ETag.
|
|
23
|
+
|
|
24
|
+
**The replica is optional and one command away.** Nothing has to be running for reads, writes, asks or explanations; the API is origin-fresh. `catalyst-skills replica start --detach` starts the local replica for cheap repeated reads and ad hoc SQL; `login --start-replica` does the same at the end. Every skill checks `catalyst-skills replica status` first and falls back to the API when the replica is absent or stale, saying so.
|
|
25
|
+
|
|
26
|
+
## What you do
|
|
27
|
+
|
|
28
|
+
1. Ask the person for their account key. Their tenant admin mints it in settings; you cannot. A workstation key connects but is refused by the contract and by machine-only reads (a one-line notice, not a failure); tell them an account key is needed for the full set.
|
|
29
|
+
2. Run the login command. Its output names the tenant (`Connected to <name> (<slug>)`), the config path, and the cached contract version.
|
|
30
|
+
3. Verify with `node scripts/verify-connection.mjs`: one line each for the tenant, the contract version, and the replica; exit 1 when the machine is not connected.
|
|
31
|
+
4. Run `catalyst-skills ready` and read the verdict to them.
|
|
32
|
+
5. Offer the replica: `catalyst-skills replica start --detach`, then `catalyst-skills replica status`. For keeping it alive across a reboot, load `references/keeping-the-replica-running.md`.
|
|
33
|
+
6. If login fails: a `401` means a stale or mistyped key, back to the tenant admin, never a retry loop; a network error names the URL, check `--base-url`. If login succeeds but prints a line naming two contract versions, the tenant serves a contract outside this bundle's range: update the bundle before using the other skills.
|
|
34
|
+
|
|
35
|
+
## The verbs a session runs first
|
|
36
|
+
|
|
37
|
+
Every skill session opens with these, in this order, before doing anything else:
|
|
38
|
+
|
|
39
|
+
1. `catalyst-skills status` to confirm the tenant this machine belongs to and where the config and contract live.
|
|
40
|
+
2. `catalyst-skills contract` to load the tenant contract (cached per its own policy; `--refresh` forces a revalidation; `--path teams.0.stages` prints one sub-document).
|
|
41
|
+
3. `catalyst-skills replica status` to learn the read source: exit 0 fresh, 1 stale, 2 not configured, 3 absent.
|
|
42
|
+
4. `catalyst-skills ready` for the full verdict: Node, config, contract, CLI path, skills, SDK, replica, and the tenant's own readiness checks with who can fix each.
|
|
43
|
+
|
|
44
|
+
## Load on demand
|
|
45
|
+
|
|
46
|
+
| when | read |
|
|
47
|
+
| -- | -- |
|
|
48
|
+
| the person wants the replica writer to survive a reboot, or asks what it stores and whether anything needs cleaning | `references/keeping-the-replica-running.md` |
|
|
49
|
+
| the full readiness vector and what each check means | the `catalyst-setup` skill |
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
- **The key is a credential.** It goes into the login command or `CATALYST_CLOUD_TOKEN` and nowhere else: not into tickets, transcripts, or any file you write besides the one login writes.
|
|
54
|
+
- **Never guess a tenant.** There is no `--account`; the key is the only tenant selector, on purpose. If the cloud names no tenant for the key, stop and say so.
|
|
55
|
+
- **Read the update notice.** After a bundle update, the next command prints one line with the changelog entry and what to run; read it to the person if they are watching.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Connect me"
|
|
3
|
+
short_description: "Connect this machine to your Catalyst Cloud tenant with your account key, cache the tenant contract, and verify"
|
|
4
|
+
default_prompt: "Use $connect-me to connect this machine to my Catalyst Cloud tenant."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: false
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Keeping the replica running
|
|
2
|
+
|
|
3
|
+
This reference restates invariants of the local replica the Catalyst Cloud SDK manages. Paths are the CLI's defaults; the replica file can be moved with `--db` and the config records it.
|
|
4
|
+
|
|
5
|
+
## The supported path is the plain command
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
catalyst-skills replica start --detach # start in the background, write a pidfile, return
|
|
9
|
+
catalyst-skills replica status # 0 fresh, 1 stale, 2 not configured, 3 absent; one line either way
|
|
10
|
+
catalyst-skills replica status --probe # also compare the local cursor with the cloud head
|
|
11
|
+
catalyst-skills replica stop # signal the pidfile's process
|
|
12
|
+
catalyst-skills replica start # foreground, Ctrl-C to stop
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It is a Node process, not a service. Node 22 or newer with its built-in SQLite module runs it the same on macOS, Linux and Windows, and the bundle never requires a daemon, because a required service is the first thing that breaks on a laptop. The writer is not a prerequisite for any skill; a fresh one is a preference. A skill must never refuse to work because the replica is down, and must never silently read a stale one; `replica status` is the one exit code that settles both.
|
|
16
|
+
|
|
17
|
+
## What it holds on disk, and why nothing rotates
|
|
18
|
+
|
|
19
|
+
Under `~/.config/catalyst-cloud/` by default:
|
|
20
|
+
|
|
21
|
+
| file | what it is |
|
|
22
|
+
| -- | -- |
|
|
23
|
+
| `replica.db` | one SQLite file: the tenant's mirrored tables, kept current by upserts and deletes from the stream |
|
|
24
|
+
| `replica.db.writer.lock` | the writer's lock, with a heartbeat the status check reads |
|
|
25
|
+
| `replica.db.pid` | the background writer's process id, written by `--detach` |
|
|
26
|
+
| a `sync_meta` row inside the database | the stream cursor, so a restart resumes where it stopped |
|
|
27
|
+
| `watch-cursor.json` | the events-only cursor for `catalyst-skills watch`, stamped with the tenant; a few bytes |
|
|
28
|
+
|
|
29
|
+
The replica is upserts and deletes into one file, so it neither grows without bound nor needs pruning; the cursor is a row. There is no directory of old files to clean and nothing to rotate. If the writer logs, it logs to standard error and the shell decides where that goes; nothing under the config directory is a log.
|
|
30
|
+
|
|
31
|
+
The first start seeds the file from the cloud's snapshot (one full copy of the tables) and then follows the stream; a restart resumes from the saved cursor. When the cloud can no longer replay from that cursor it tells the writer to re-seed, which the writer does on its own.
|
|
32
|
+
|
|
33
|
+
## Freshness, as the status check judges it
|
|
34
|
+
|
|
35
|
+
`replica status` needs no network. It answers fresh when the pidfile's process is alive, the writer lock's heartbeat is younger than the staleness threshold (15 seconds by default; `--stale-ms` overrides), and the cursor row is non-empty. Anything else is stale (exit 1) with the reasons listed, absent (exit 3) when there is no file, or not configured (exit 2) when the machine is not connected. `--probe` adds the one network call, fetching the cloud head to print how far behind the local cursor is. Every read verb prints `source: replica (cursor N)` or `source: api (replica stale|absent|not configured)` on standard error, so the answer always names where it came from.
|
|
36
|
+
|
|
37
|
+
## Surviving a reboot
|
|
38
|
+
|
|
39
|
+
Optional. The plain command is the supported path; these are for people who want the writer back after a restart. Each example assumes a global install (`npm install -g @catalyst-cloud/catalyst-skills`); substitute the absolute path `catalyst-skills status` prints as the CLI path if you prefer. Run the writer in the foreground under the supervisor (no `--detach`), so the supervisor owns the process.
|
|
40
|
+
|
|
41
|
+
### macOS, launchd
|
|
42
|
+
|
|
43
|
+
Save as `~/Library/LaunchAgents/dev.catalystcloud.replica.plist`, then `launchctl load ~/Library/LaunchAgents/dev.catalystcloud.replica.plist`:
|
|
44
|
+
|
|
45
|
+
```xml
|
|
46
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
47
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
48
|
+
<plist version="1.0"><dict>
|
|
49
|
+
<key>Label</key><string>dev.catalystcloud.replica</string>
|
|
50
|
+
<key>ProgramArguments</key><array>
|
|
51
|
+
<string>/usr/local/bin/catalyst-skills</string><string>replica</string><string>start</string>
|
|
52
|
+
</array>
|
|
53
|
+
<key>RunAtLoad</key><true/>
|
|
54
|
+
<key>KeepAlive</key><true/>
|
|
55
|
+
<key>StandardErrorPath</key><string>/tmp/catalyst-cloud-replica.err</string>
|
|
56
|
+
</dict></plist>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Check the path to the binary with `which catalyst-skills`; Homebrew Node installs under `/opt/homebrew/bin`. `launchctl unload` the same file to stop it.
|
|
60
|
+
|
|
61
|
+
### Linux, systemd user unit
|
|
62
|
+
|
|
63
|
+
Save as `~/.config/systemd/user/catalyst-cloud-replica.service`, then `systemctl --user daemon-reload && systemctl --user enable --now catalyst-cloud-replica`:
|
|
64
|
+
|
|
65
|
+
```ini
|
|
66
|
+
[Unit]
|
|
67
|
+
Description=Catalyst Cloud local replica
|
|
68
|
+
After=network-online.target
|
|
69
|
+
|
|
70
|
+
[Service]
|
|
71
|
+
ExecStart=/usr/bin/env catalyst-skills replica start
|
|
72
|
+
Restart=on-failure
|
|
73
|
+
RestartSec=5
|
|
74
|
+
|
|
75
|
+
[Install]
|
|
76
|
+
WantedBy=default.target
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For a writer that should run while nobody is logged in, `loginctl enable-linger $USER` once. `journalctl --user -u catalyst-cloud-replica` shows its standard error.
|
|
80
|
+
|
|
81
|
+
### Windows
|
|
82
|
+
|
|
83
|
+
There is no service wrapper in the bundle. Task Scheduler runs `catalyst-skills replica start` at logon with "Run whether user is logged on or not" and "If the task fails, restart every 1 minute"; or run `catalyst-skills replica start --detach` from a shell after logging in, which is the plain path and works the same as elsewhere.
|
|
84
|
+
|
|
85
|
+
## Two things that look like problems and are not
|
|
86
|
+
|
|
87
|
+
- **Two writers.** The writer lock guards the file, so a second writer on the same file is refused rather than allowed to corrupt it. Stop the first (`replica stop`, or the supervisor) before starting another or moving the file.
|
|
88
|
+
- **A stale verdict right after start.** The first start seeds the whole snapshot before it goes live; `status` reads stale until the cursor row appears. Ask again once `replica start` has printed its live line, or watch `status --probe` count the lag down.
|