@catalyst-cloud/catalyst-skills 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +11 -182
  2. package/bin/catalyst-skills.js +3 -49
  3. package/bin/catalyst.js +3 -0
  4. package/package.json +8 -31
  5. package/CHANGELOG.md +0 -65
  6. package/LICENSE +0 -21
  7. package/dist/args.js +0 -273
  8. package/dist/ask.js +0 -161
  9. package/dist/browser.js +0 -20
  10. package/dist/cli.js +0 -361
  11. package/dist/config.js +0 -152
  12. package/dist/contract-types.js +0 -4
  13. package/dist/contract.js +0 -184
  14. package/dist/detach.js +0 -10
  15. package/dist/environment.js +0 -207
  16. package/dist/errors.js +0 -27
  17. package/dist/events.js +0 -106
  18. package/dist/execution.js +0 -451
  19. package/dist/oauth.js +0 -300
  20. package/dist/pagination.js +0 -76
  21. package/dist/prompt.js +0 -35
  22. package/dist/published.js +0 -75
  23. package/dist/query.js +0 -248
  24. package/dist/ready.js +0 -254
  25. package/dist/release.js +0 -142
  26. package/dist/replica.js +0 -585
  27. package/dist/sdk.js +0 -26
  28. package/dist/semver.js +0 -18
  29. package/dist/skill-shape.js +0 -189
  30. package/dist/skills.js +0 -129
  31. package/dist/transport.js +0 -205
  32. package/dist/ts-deps-loader.js +0 -113
  33. package/dist/watch/consumer.js +0 -141
  34. package/dist/watch/cursor-file.js +0 -62
  35. package/dist/watch.js +0 -175
  36. package/dist/write.js +0 -224
  37. package/skills/catalyst-github/SKILL.md +0 -35
  38. package/skills/catalyst-github/agents/openai.yaml +0 -6
  39. package/skills/catalyst-github/agents/portability.yaml +0 -4
  40. package/skills/catalyst-github/references/is-it-mergeable.md +0 -57
  41. package/skills/catalyst-github/references/what-a-pr-accumulates.md +0 -61
  42. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +0 -124
  43. package/skills/catalyst-github/scripts/lib/cli.mjs +0 -103
  44. package/skills/catalyst-github/scripts/lib/credential.mjs +0 -29
  45. package/skills/catalyst-github/scripts/lib/pull.mjs +0 -82
  46. package/skills/catalyst-github/scripts/read-pr.mjs +0 -97
  47. package/skills/catalyst-linear/SKILL.md +0 -43
  48. package/skills/catalyst-linear/agents/openai.yaml +0 -6
  49. package/skills/catalyst-linear/agents/portability.yaml +0 -5
  50. package/skills/catalyst-linear/references/reading-a-ticket.md +0 -52
  51. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +0 -53
  52. package/skills/catalyst-linear/references/writing-to-linear.md +0 -43
  53. package/skills/catalyst-linear/scripts/comment.mjs +0 -59
  54. package/skills/catalyst-linear/scripts/create-ticket.mjs +0 -44
  55. package/skills/catalyst-linear/scripts/label.mjs +0 -48
  56. package/skills/catalyst-linear/scripts/lib/cli.mjs +0 -164
  57. package/skills/catalyst-linear/scripts/lib/credential.mjs +0 -29
  58. package/skills/catalyst-linear/scripts/move.mjs +0 -41
  59. package/skills/catalyst-linear/scripts/read-ticket.mjs +0 -93
  60. package/skills/catalyst-linear/scripts/search.mjs +0 -49
  61. package/skills/catalyst-onboard/SKILL.md +0 -49
  62. package/skills/catalyst-onboard/agents/openai.yaml +0 -6
  63. package/skills/catalyst-onboard/agents/portability.yaml +0 -5
  64. package/skills/catalyst-onboard/references/the-one-path.md +0 -143
  65. package/skills/catalyst-onboard/references/what-the-browser-owns.md +0 -50
  66. package/skills/catalyst-onboard/references/who-fixes-what.md +0 -41
  67. package/skills/catalyst-onboard/scripts/lib/cli.mjs +0 -117
  68. package/skills/catalyst-onboard/scripts/lib/credential.mjs +0 -29
  69. package/skills/catalyst-onboard/scripts/where-am-i.mjs +0 -245
  70. package/skills/catalyst-setup/SKILL.md +0 -32
  71. package/skills/catalyst-setup/agents/openai.yaml +0 -6
  72. package/skills/catalyst-setup/agents/portability.yaml +0 -4
  73. package/skills/catalyst-setup/references/what-each-check-means.md +0 -78
  74. package/skills/catalyst-setup/scripts/check.mjs +0 -75
  75. package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
  76. package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
  77. package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
  78. package/skills/connect-me/SKILL.md +0 -63
  79. package/skills/connect-me/agents/openai.yaml +0 -6
  80. package/skills/connect-me/agents/portability.yaml +0 -5
  81. package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
  82. package/skills/connect-me/scripts/lib/cli.mjs +0 -185
  83. package/skills/connect-me/scripts/lib/credential.mjs +0 -29
  84. package/skills/connect-me/scripts/verify-connection.mjs +0 -68
  85. package/skills/how-catalyst-works/SKILL.md +0 -42
  86. package/skills/how-catalyst-works/agents/openai.yaml +0 -6
  87. package/skills/how-catalyst-works/agents/portability.yaml +0 -4
  88. package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
  89. package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -54
  90. package/skills/how-catalyst-works/references/the-ladder.md +0 -41
  91. package/skills/how-catalyst-works/references/what-runs-next.md +0 -77
  92. package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
  93. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
  94. package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
  95. package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
  96. package/skills/how-catalyst-works/scripts/show-my-map.mjs +0 -94
  97. package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
  98. package/skills/run-this-project/SKILL.md +0 -45
  99. package/skills/run-this-project/agents/openai.yaml +0 -6
  100. package/skills/run-this-project/agents/portability.yaml +0 -5
  101. package/skills/run-this-project/assets/stall-policy.json +0 -15
  102. package/skills/run-this-project/references/making-work-ready.md +0 -60
  103. package/skills/run-this-project/references/reacting-to-events.md +0 -76
  104. package/skills/run-this-project/references/stalls-and-escalation.md +0 -63
  105. package/skills/run-this-project/scripts/lib/cli.mjs +0 -185
  106. package/skills/run-this-project/scripts/lib/credential.mjs +0 -29
  107. package/skills/run-this-project/scripts/make-ready.mjs +0 -64
  108. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  109. package/skills/run-this-project/scripts/watch-scope.mjs +0 -61
  110. package/skills/unstick/SKILL.md +0 -41
  111. package/skills/unstick/agents/openai.yaml +0 -6
  112. package/skills/unstick/agents/portability.yaml +0 -5
  113. package/skills/unstick/references/playbook.md +0 -51
  114. package/skills/unstick/scripts/lib/cli.mjs +0 -135
  115. package/skills/unstick/scripts/lib/credential.mjs +0 -29
  116. package/skills/unstick/scripts/unstick.mjs +0 -57
  117. package/skills/what-needs-me/SKILL.md +0 -41
  118. package/skills/what-needs-me/agents/openai.yaml +0 -6
  119. package/skills/what-needs-me/agents/portability.yaml +0 -5
  120. package/skills/what-needs-me/references/raising-a-decision.md +0 -41
  121. package/skills/what-needs-me/references/reading-the-inbox.md +0 -38
  122. package/skills/what-needs-me/references/settling-an-answer.md +0 -37
  123. package/skills/what-needs-me/scripts/inbox.mjs +0 -56
  124. package/skills/what-needs-me/scripts/lib/cli.mjs +0 -135
  125. package/skills/what-needs-me/scripts/lib/credential.mjs +0 -29
  126. package/skills/what-needs-me/scripts/raise.mjs +0 -53
  127. package/skills/what-needs-me/scripts/settle.mjs +0 -73
  128. package/skills/whats-happening/SKILL.md +0 -43
  129. package/skills/whats-happening/agents/openai.yaml +0 -6
  130. package/skills/whats-happening/agents/portability.yaml +0 -4
  131. package/skills/whats-happening/assets/status-reply.json +0 -77
  132. package/skills/whats-happening/references/reading-the-board.md +0 -43
  133. package/skills/whats-happening/references/reprioritising.md +0 -37
  134. package/skills/whats-happening/references/routing-work.md +0 -36
  135. package/skills/whats-happening/references/status-reply.md +0 -34
  136. package/skills/whats-happening/references/why-is-it-stuck.md +0 -62
  137. package/skills/whats-happening/scripts/explain.mjs +0 -28
  138. package/skills/whats-happening/scripts/lib/cli.mjs +0 -135
  139. package/skills/whats-happening/scripts/lib/credential.mjs +0 -29
  140. package/skills/whats-happening/scripts/snapshot.mjs +0 -149
@@ -1,62 +0,0 @@
1
- # Why is it stuck
2
-
3
- This reference restates an invariant: the vocabulary the cloud's eligibility explainer uses when nothing is offered for a ticket, translated for a person, with what releases each. `node scripts/explain.mjs <ticket>` prints the reason for one ticket; this table turns it into the next action. The `how-catalyst-works` skill carries the mechanism behind each row.
4
-
5
- ## How to read a reason
6
-
7
- `explain` prints one paragraph: the queue position (or none), the status, the reason, a detail or marker when the cloud gave one, the last failure, and any advisories. Match the reason to a row below. "Releases itself" means the cloud clears it with no human action; "needs" names who acts. When a reason is not in this table, `explain` prints it as the cloud spelled it; report it verbatim and say the bundle does not know it.
8
-
9
- ## Reasons that release themselves
10
-
11
- | reason | what it means | how it clears |
12
- | -- | -- | -- |
13
- | `retry_backoff` | the failed phase is retrying in place and waiting out its rung | a clock; the rungs are the contract's `thresholds.retryBackoffMs` |
14
- | `lease_held` | a live container already holds this phase | the phase finishes |
15
- | `intake_lease_held` | a later phase is offered while an intake container still holds the ticket | intake finishes |
16
- | `environment_check_running` | the repository's environment check is in flight | the check finishes |
17
- | `runner_image_breaker` | fleet-wide: the live runner image fails every phase at startup, so dispatch is held rather than parking tickets | the cloud moves the pin; one alert per tenant, no per-ticket action |
18
- | `routing_unavailable` | claimed, then refused at kickoff: no route, no eligible coding-account slot, or the provider is unavailable | provider recovery or a slot freeing; if it persists, the tenant admin checks coding accounts in settings |
19
- | `cooling_down` (with a named callback) | the phase is parked on a condition the cloud watches | the callback fires; the `detail` names it |
20
-
21
- ## Reasons that need a human
22
-
23
- | reason | what it means | who acts and how |
24
- | -- | -- | -- |
25
- | `ask_ticket` | it carries an ask label; a question is never work | the human answers the ask (`what-needs-me`) |
26
- | `ask_shape_suspected` | its own text reads as a decision request though it carries no ask label | the human either answers it as an ask or applies the release label the contract names |
27
- | `blocked` | a live blocking relation holds it | whoever owns the blocker closes it; if the blocker is an ask, that is the human |
28
- | `not_at_dispatch_stage` | the card is not in the team's dispatch column | someone moves the card to the dispatch slot (`references/reprioritising.md`) |
29
- | `not_at_pr_stage` | merge is next but the card is not in the PR column | the card returns to the pr slot; an automation that bounced it should be found |
30
- | `no_change_hold` | a repair round changed nothing | a human comment on the ticket, or a new push to the branch |
31
- | `waiting_on` | a merge-gate failure with no cause the cloud can repair | read the merge-wait comment on the ticket; usually a hold label or a review a person must give (`catalyst-github`) |
32
- | `externally_claimed` | a worker outside the cloud holds it | that worker, or the human removes the local-lane label |
33
- | `environment_check_required` / `_failed` / `_expired` / `_hash_mismatch` | the repository's environment gate has not passed | the tenant admin runs or fixes the environment check in settings |
34
- | `repo_paused` | an operator paused the repository | the operator resumes it |
35
- | `scope_overlap` | its declared file scope intersects a ticket already in flight | wait for the other ticket, or the human decides which goes first |
36
-
37
- ## Reasons a release clears once the cause is fixed
38
-
39
- These do not release themselves, and nobody but the person needs to act: once the recorded cause is fixed, the person's own login releases them with `catalyst-skills release <ticket>`. The desk does not release; route the ticket to the `unstick` skill, which reads the history, previews the release and runs it, or names what a person must do first.
40
-
41
- | reason | what it means | note |
42
- | -- | -- | -- |
43
- | `phase_parked` / `cooling_down` (parked after repeated failures, or the round cap was spent) | the phase is parked and does not release itself | `unstick`; `explain --history` names the park and the failure class it recorded |
44
- | `remediate_parked` | the repair phase itself is parked, so the failing phase has nowhere to be repaired | as above |
45
- | `validate_class_spent` | this validate failure already spent its one repair round in this episode | a push or a comment saying what to change releases it on its own; otherwise `unstick` |
46
- | `human_owned_pr` | a person's own pull request holds the ticket | that person closes or merges it; no release clears it |
47
- | `review_not_converging` | review and repair kept finding new problems without converging | a person reads the findings and comments on the ticket to resume; raise an ask for that read |
48
- | `round_threshold` | the ticket spent its lifetime repair budget | a person answers the ask the cloud raised, or pushes a fix |
49
- | `no_branch_to_remediate` / `branch_missing` / `branch_gone` | a branch-dependent phase has no branch to clone | the cloud releases missing-branch parks on its own budget; a deleted branch needs a human to decide |
50
- | `stale_failure_episode` | the ladder advanced after the recorded failure | informational; the round is dropped |
51
-
52
- ## Reasons that are not problems
53
-
54
- `ticket_terminal` (done or canceled), `pipeline_complete` (every phase ran), and `pr_merged` (the PR merged, nothing left to clone) describe finished work. Report them as closed, not blocked.
55
-
56
- ## When the cloud could not judge
57
-
58
- A status of `unknown` with a reason such as `ordering_never_published`, `ordering_stale`, `workflow_mapping_unknown`, `ticket_unknown`, `dependency_snapshot_unknown`, `blocker_unknown`, `label_snapshot_unknown`, `prior_artifact_unknown`, `scope_unknown`, `scope_occupancy_unknown` or `branch_snapshot_unknown` means the evaluator failed closed rather than guessing. Most clear on the cloud's next pass. `workflow_mapping_unknown`, and `ordering_never_published` beside it, does not clear by itself when the team has no saved stage mapping: `explain` then names the missing stages, and a tenant owner or admin maps the team in Settings → Linear teams. If any other one persists for a team, the readiness checks (`catalyst-setup`) are the next read.
59
-
60
- ## The one advisory
61
-
62
- `human_addressed_unlabeled_ask_suspect` gates nothing: the ticket is assigned to a human with no delegate and may be an unlabelled ask. Mention it in the waiting-on-human block with a question mark.
@@ -1,28 +0,0 @@
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);
@@ -1,135 +0,0 @@
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 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
- import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
10
-
11
- export const PACKAGE = "@catalyst-cloud/catalyst-skills";
12
- export const NOT_CONFIGURED_EXIT = 2;
13
-
14
- export function configPath() {
15
- const home = process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/";
16
- return join(home, ".config", "catalyst-cloud", "customer.json");
17
- }
18
-
19
- /** The connected-machine config, or exit 2 with the one line that says how to connect. */
20
- export function loadConfig() {
21
- const path = configPath();
22
- if (!existsSync(path)) notConfigured(`no config at ${path}`);
23
- try {
24
- const cfg = JSON.parse(readFileSync(path, "utf8"));
25
- if (!hasCredential(cfg)) notConfigured(`config at ${path} has no key or login`);
26
- return cfg;
27
- } catch (err) {
28
- if (err && err.exitCode === NOT_CONFIGURED_EXIT) throw err;
29
- notConfigured(`config at ${path} is unreadable`);
30
- }
31
- }
32
-
33
- function notConfigured(why) {
34
- process.stderr.write(`not connected (${why}) — run: ${CONNECT_COMMAND}\n`);
35
- process.exit(NOT_CONFIGURED_EXIT);
36
- }
37
-
38
- /**
39
- * Run one catalyst-skills verb and return {code, stdout, stderr}. Uses the CLI path the login
40
- * recorded when it still exists, else `npx <package>`. A CLI exit of 2 with a "not joined" or
41
- * "not connected" line is turned into this script's own exit 2, so every caller sees one contract.
42
- */
43
- export function runCli(args, opts = {}) {
44
- const cfg = loadConfig();
45
- const useRecorded = typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
46
- const cmd = useRecorded ? process.execPath : "npx";
47
- const argv = useRecorded ? [cfg.cliPath, ...args] : [PACKAGE, ...args];
48
- const res = spawnSync(cmd, argv, {
49
- input: opts.stdin,
50
- encoding: "utf8",
51
- maxBuffer: 64 * 1024 * 1024,
52
- shell: !useRecorded && process.platform === "win32",
53
- env: process.env,
54
- });
55
- if (res.error) {
56
- process.stderr.write(`could not run ${cmd}: ${res.error.message}\n`);
57
- process.exit(NOT_CONFIGURED_EXIT);
58
- }
59
- const stdout = res.stdout ?? "";
60
- const stderr = res.stderr ?? "";
61
- if (res.status === NOT_CONFIGURED_EXIT && /not (joined|connected|configured)/i.test(stderr)) {
62
- process.stderr.write(stderr);
63
- process.exit(NOT_CONFIGURED_EXIT);
64
- }
65
- return { code: res.status ?? 1, stdout, stderr };
66
- }
67
-
68
- /** Run a verb and require success; on failure print the CLI's own stderr and exit with its code (or 1). */
69
- export function mustRun(args, opts = {}) {
70
- const r = runCli(args, opts);
71
- if (r.code !== 0) {
72
- if (r.stderr) process.stderr.write(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`);
73
- if (r.stdout && !opts.quiet) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`);
74
- process.exit(r.code === 0 ? 1 : r.code);
75
- }
76
- return r;
77
- }
78
-
79
- /** Parse the CLI's --json stdout; a non-JSON answer is a failed check, exit 1. */
80
- export function parseJson(stdout, what = "output") {
81
- try {
82
- return JSON.parse(stdout.trim());
83
- } catch {
84
- process.stderr.write(`could not parse ${what} as JSON: ${stdout.trim().slice(0, 200)}\n`);
85
- process.exit(1);
86
- }
87
- }
88
-
89
- /** Minimal flag parsing for the scripts: --name value, --name=value, --flag, repeated flags collect. */
90
- export function parseFlags(argv, spec) {
91
- const flags = {};
92
- const positionals = [];
93
- for (let i = 0; i < argv.length; i++) {
94
- const a = argv[i];
95
- if (a === "--help" || a === "-h") return { help: true, flags, positionals };
96
- if (a.startsWith("--")) {
97
- let name = a.slice(2);
98
- let value;
99
- const eq = name.indexOf("=");
100
- if (eq !== -1) {
101
- value = name.slice(eq + 1);
102
- name = name.slice(0, eq);
103
- }
104
- const s = spec[name];
105
- if (!s) {
106
- process.stderr.write(`unknown option --${name} (try --help)\n`);
107
- process.exit(1);
108
- }
109
- if (s.value) {
110
- if (value === undefined) value = argv[++i];
111
- if (value === undefined || value === "") {
112
- process.stderr.write(`--${name} needs a value\n`);
113
- process.exit(1);
114
- }
115
- if (s.repeat) (flags[name] ??= []).push(value);
116
- else flags[name] = value;
117
- } else flags[name] = true;
118
- continue;
119
- }
120
- positionals.push(a);
121
- }
122
- return { help: false, flags, positionals };
123
- }
124
-
125
- export function printHelp(usage, spec, notes = []) {
126
- const lines = [`Usage: ${usage}`, ""];
127
- const names = Object.keys(spec);
128
- if (names.length) {
129
- lines.push("Options:");
130
- for (const n of names) lines.push(` --${n}${spec[n].value ? " <value>" : ""} ${spec[n].help}`);
131
- lines.push("");
132
- }
133
- 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");
134
- process.stdout.write(lines.join("\n") + "\n");
135
- }
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env node
2
- // lib/credential.mjs — is this machine connected? The ONE place a skill script decides it, vendored
3
- // byte-identical into every skill's scripts/lib/ from skill-lib/credential.mjs at the package root
4
- // (`npm run skill-lib:sync`; a test fails on any drift). Skills install one directory at a time, so
5
- // each carries its own copy. This file is a library — run a sibling script with --help for usage.
6
- //
7
- // customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
8
- // session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
9
- // catalyst-skills CLI, which authenticates with whichever is present and refreshes a login's token
10
- // itself. A new credential kind lands here, once.
11
-
12
- /** The command that connects this machine, as every not-connected line names it. */
13
- export const CONNECT_COMMAND =
14
- "npx @catalyst-cloud/catalyst-skills login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx @catalyst-cloud/catalyst-skills login)";
15
-
16
- /** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
17
- export function hasCredential(cfg) {
18
- if (cfg === null || typeof cfg !== "object") return false;
19
- const key = cfg["key"];
20
- if (typeof key === "string" && key !== "") return true;
21
- const login = cfg["auth"];
22
- return (
23
- login !== null &&
24
- typeof login === "object" &&
25
- login["kind"] === "oauth" &&
26
- typeof login["refreshToken"] === "string" &&
27
- login["refreshToken"] !== ""
28
- );
29
- }
@@ -1,149 +0,0 @@
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
- // A board read is a window (`--limit` stays the default), and a window the cloud cut short must
71
- // say so rather than reading as the whole team.
72
- const truncatedLine = (r.stderr.match(/^truncated at .*$/m) ?? [null])[0];
73
- if (r.code !== 0) errors.board = (r.stderr || `exit ${r.code}`).trim().split("\n").at(-1);
74
- else {
75
- const rows = parseJson(r.stdout, "issues");
76
- out.board = groupByStage(rows, contract, flags.team);
77
- if (sourceLine) out.source.board = sourceLine.replace(/^source: /, "");
78
- if (truncatedLine) out.source.boardTruncated = truncatedLine;
79
- }
80
- }
81
-
82
- if (Object.keys(errors).length) out.errors = errors;
83
- 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`);
84
- process.stdout.write(JSON.stringify(out, null, 2) + "\n");
85
- process.exit(Object.keys(errors).length ? 1 : 0);
86
-
87
- /** The contract facts a status reply needs, and nothing a reply should not paste. */
88
- function trimContract(doc, teamKey) {
89
- const teams = (doc.teams ?? []).filter((t) => !teamKey || String(t.key ?? "").toUpperCase() === teamKey.toUpperCase());
90
- return {
91
- contractVersion: doc.contractVersion,
92
- account: { slug: doc.account?.slug, name: doc.account?.name },
93
- slots: doc.slots,
94
- ladder: { phases: doc.ladder?.phases, intakeEnabled: doc.ladder?.intakeEnabled, keying: doc.ladder?.keying },
95
- thresholds: doc.thresholds,
96
- mergeDefaultPolicy: doc.merge?.defaultPolicy,
97
- teams: teams.map((t) => ({
98
- key: t.key,
99
- name: t.name,
100
- workflowMode: t.workflowMode,
101
- readiness: t.readiness?.status,
102
- stages: Object.fromEntries(
103
- (doc.slots ?? Object.keys(t.stages ?? {}))
104
- .filter((slot) => t.stages?.[slot])
105
- .map((slot) => [slot, { name: t.stages[slot].name, type: t.stages[slot].type, stateStillExists: t.stages[slot].stateStillExists }]),
106
- ),
107
- labels: {
108
- ask: (t.labels?.ask ?? []).map((l) => l.name),
109
- hold: (t.labels?.hold ?? []).map((l) => l.name),
110
- release: (t.labels?.release ?? []).map((l) => l.name),
111
- },
112
- })),
113
- };
114
- }
115
-
116
- /** Open tickets by stage name, ordered by the contract's slot order for the ticket's team; unmapped
117
- * state names follow, alphabetically. Terminal-typed states are dropped so the board is work only. */
118
- function groupByStage(rows, doc, teamKey) {
119
- const order = [];
120
- const seen = new Set();
121
- const terminal = new Set();
122
- for (const t of doc.teams ?? []) {
123
- if (teamKey && String(t.key ?? "").toUpperCase() !== teamKey.toUpperCase()) continue;
124
- for (const slot of doc.slots ?? []) {
125
- const s = t.stages?.[slot];
126
- if (!s?.name) continue;
127
- if (slot === "done" || slot === "canceled" || ["completed", "canceled", "cancelled"].includes(String(s.type ?? "").toLowerCase())) terminal.add(s.name);
128
- else if (!seen.has(s.name)) {
129
- seen.add(s.name);
130
- order.push(s.name);
131
- }
132
- }
133
- }
134
- const byStage = {};
135
- for (const r of rows) {
136
- const state = String(r.state ?? "unknown");
137
- if (terminal.has(state)) continue;
138
- (byStage[state] ??= []).push({
139
- identifier: r.identifier,
140
- title: r.title,
141
- priority: r.priority ?? null,
142
- assignee: r.assignee_name ?? r.assignee ?? null,
143
- updatedAt: r.updated_at ?? null,
144
- projectId: r.project_id ?? null,
145
- });
146
- }
147
- const extra = Object.keys(byStage).filter((s) => !seen.has(s)).sort();
148
- return { stageOrder: [...order, ...extra], byStage, terminalStagesDropped: [...terminal] };
149
- }