@catalyst-cloud/cli 0.8.0 → 0.9.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 +18 -0
- package/README.md +28 -4
- package/dist/args.js +18 -3
- package/dist/cli.js +14 -2
- package/dist/connections.js +85 -0
- package/dist/event-status.js +66 -0
- package/dist/events.js +8 -2
- package/dist/identity.js +53 -0
- package/dist/mcp.js +101 -0
- package/dist/sdk.js +23 -0
- package/dist/skill-shape.js +4 -0
- package/dist/write.js +9 -0
- package/package.json +2 -2
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +2 -2
- package/skills/catalyst-linear/references/writing-to-linear.md +2 -2
- package/skills/catalyst-linear/scripts/create-ticket.mjs +14 -3
- package/skills/catalyst-onboard/SKILL.md +5 -2
- package/skills/catalyst-onboard/references/declaring-a-repository.md +26 -5
- package/skills/catalyst-onboard/references/local-sync.md +15 -0
- package/skills/catalyst-onboard/references/skill-sources.md +5 -2
- package/skills/catalyst-onboard/references/the-one-path.md +18 -18
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +3 -1
- package/skills/catalyst-onboard/references/who-fixes-what.md +2 -2
- package/skills/catalyst-onboard/scripts/local-sync.mjs +216 -0
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +68 -8
- package/skills/catalyst-setup/SKILL.md +1 -1
- package/skills/catalyst-setup/references/what-each-check-means.md +1 -1
- package/skills/connect-me/SKILL.md +1 -1
- package/skills/how-catalyst-works/SKILL.md +1 -1
- package/skills/how-catalyst-works/references/stages-and-mapping.md +1 -1
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Optional onboarding step. The replica command starts the event cache with the writer;
|
|
3
|
+
// only a live heartbeat at the cloud head proves that both were started successfully.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { cliTarget } from "./lib/cli.mjs";
|
|
7
|
+
|
|
8
|
+
const STATUS_COMMAND = ["replica", "status", "--probe", "--json"];
|
|
9
|
+
const EVENTS_COMMAND = ["events", "status", "--probe", "--json"];
|
|
10
|
+
const START_COMMAND = ["replica", "start", "--detach"];
|
|
11
|
+
const RECOVERY_COMMAND = "catalyst-skills replica start --detach";
|
|
12
|
+
|
|
13
|
+
function defaultRun(args) {
|
|
14
|
+
const target = cliTarget();
|
|
15
|
+
const result = spawnSync(target.command, [...target.prefix, ...args], {
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
timeout: 30_000,
|
|
18
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
20
|
+
shell: !target.recorded && process.platform === "win32",
|
|
21
|
+
env: process.env,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
code: result.status ?? 1,
|
|
25
|
+
stdout: result.stdout ?? "",
|
|
26
|
+
stderr: result.stderr ?? "",
|
|
27
|
+
error: result.error?.message,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function assessLocalSync(status, events) {
|
|
32
|
+
if (!status || typeof status !== "object")
|
|
33
|
+
return { verdict: "unknown", current: false, reason: "replica status did not return a document" };
|
|
34
|
+
if (status.verdict === "not-configured")
|
|
35
|
+
return { verdict: "unknown", current: false, reason: "this machine is not connected" };
|
|
36
|
+
if (status.verdict === "absent")
|
|
37
|
+
return { verdict: "absent", current: false, reason: "local replica is absent" };
|
|
38
|
+
if (status.verdict === "unknown" || status.verdict === "unverified" || !["fresh", "stale"].includes(status.verdict))
|
|
39
|
+
return { verdict: "unknown", current: false, reason: `replica freshness is unknown: ${(status.reasons ?? []).join("; ")}` };
|
|
40
|
+
if (status.verdict !== "fresh")
|
|
41
|
+
return {
|
|
42
|
+
verdict: "stale",
|
|
43
|
+
current: false,
|
|
44
|
+
reason: `replica ${status.verdict ?? "unknown"}: ${(status.reasons ?? []).join("; ")}`,
|
|
45
|
+
};
|
|
46
|
+
if (status.writerAlive !== true)
|
|
47
|
+
return { verdict: "stale", current: false, reason: "replica writer heartbeat is absent or stale" };
|
|
48
|
+
if (!Number.isFinite(status.heartbeatAgeMs))
|
|
49
|
+
return { verdict: "unknown", current: false, reason: "replica heartbeat age was not reported" };
|
|
50
|
+
if (status.heartbeatAgeMs >= 15_000)
|
|
51
|
+
return { verdict: "stale", current: false, reason: `replica writer heartbeat is ${status.heartbeatAgeMs}ms old` };
|
|
52
|
+
if (!Number.isSafeInteger(status.cursor) || !Number.isSafeInteger(status.head) || !Number.isSafeInteger(status.lag))
|
|
53
|
+
return { verdict: "unknown", current: false, reason: "replica cursor or cloud head was not reported" };
|
|
54
|
+
if (status.lag !== 0 || status.cursor !== status.head) {
|
|
55
|
+
return {
|
|
56
|
+
verdict: "stale",
|
|
57
|
+
current: false,
|
|
58
|
+
reason: "cloud head and local cursor have not been proved equal",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!events || !["current", "stale", "absent"].includes(events.verdict)) {
|
|
62
|
+
return {
|
|
63
|
+
verdict: "unknown",
|
|
64
|
+
current: false,
|
|
65
|
+
reason: `event freshness is unknown: ${(events?.reasons ?? []).join("; ")}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (events.verdict === "absent") {
|
|
69
|
+
return { verdict: "absent", current: false, reason: "local event cache is absent" };
|
|
70
|
+
}
|
|
71
|
+
if (events.verdict === "stale")
|
|
72
|
+
return {
|
|
73
|
+
verdict: "stale",
|
|
74
|
+
current: false,
|
|
75
|
+
reason: `event cache is ${events?.verdict ?? "unverified"}: ${(events?.reasons ?? []).join("; ")}`,
|
|
76
|
+
};
|
|
77
|
+
if (events.writerAlive !== true)
|
|
78
|
+
return { verdict: "stale", current: false, reason: "event writer heartbeat is absent or stale" };
|
|
79
|
+
if (!Number.isFinite(events.heartbeatAgeMs))
|
|
80
|
+
return { verdict: "unknown", current: false, reason: "event writer heartbeat age was not reported" };
|
|
81
|
+
if (events.heartbeatAgeMs >= 15_000)
|
|
82
|
+
return { verdict: "stale", current: false, reason: `event writer heartbeat is ${events.heartbeatAgeMs}ms old` };
|
|
83
|
+
if (!Number.isSafeInteger(events.cursor) || !Number.isSafeInteger(events.head))
|
|
84
|
+
return { verdict: "unknown", current: false, reason: "event cursor or cloud head was not reported" };
|
|
85
|
+
if (events.cursor !== events.head)
|
|
86
|
+
return { verdict: "stale", current: false, reason: `event cache cursor ${events.cursor} differs from cloud head ${events.head}` };
|
|
87
|
+
return {
|
|
88
|
+
verdict: "current",
|
|
89
|
+
current: true,
|
|
90
|
+
reason: `replica cursor ${status.cursor} and event cursor ${events.cursor} each match their cloud head; both writer heartbeats are live`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readStatus(run) {
|
|
95
|
+
const replicaResult = run(STATUS_COMMAND);
|
|
96
|
+
let status, events;
|
|
97
|
+
try {
|
|
98
|
+
status = JSON.parse(replicaResult.stdout);
|
|
99
|
+
} catch {
|
|
100
|
+
return {
|
|
101
|
+
status: null,
|
|
102
|
+
events: null,
|
|
103
|
+
assessment: {
|
|
104
|
+
verdict: "unknown",
|
|
105
|
+
current: false,
|
|
106
|
+
reason: `replica status is unknown: ${replicaResult.error || replicaResult.stderr.trim() || "invalid JSON"}`,
|
|
107
|
+
},
|
|
108
|
+
recovery: replicaResult.code === 2 ? "catalyst-skills login" : "catalyst-skills replica status --probe --json",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const eventsResult = run(EVENTS_COMMAND);
|
|
112
|
+
try {
|
|
113
|
+
events = JSON.parse(eventsResult.stdout);
|
|
114
|
+
} catch {
|
|
115
|
+
return {
|
|
116
|
+
status,
|
|
117
|
+
events: null,
|
|
118
|
+
assessment: {
|
|
119
|
+
verdict: "unknown",
|
|
120
|
+
current: false,
|
|
121
|
+
reason: `event freshness is unknown: ${eventsResult.error || eventsResult.stderr.trim() || "invalid JSON"}`,
|
|
122
|
+
},
|
|
123
|
+
recovery: "catalyst-skills events status --probe --json",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { status, events, assessment: assessLocalSync(status, events) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A bounded check. Starting the optional writer requires an explicit opt-in. */
|
|
130
|
+
export async function runLocalSync({
|
|
131
|
+
start = false,
|
|
132
|
+
waitSeconds = 90,
|
|
133
|
+
run = defaultRun,
|
|
134
|
+
now = Date.now,
|
|
135
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
136
|
+
} = {}) {
|
|
137
|
+
if (!Number.isInteger(waitSeconds) || waitSeconds < 0 || waitSeconds > 300)
|
|
138
|
+
throw new Error("--wait must be an integer from 0 to 300 seconds");
|
|
139
|
+
let reading = readStatus(run);
|
|
140
|
+
if (reading.assessment.current || !start)
|
|
141
|
+
return { ...reading, started: false, recovery: reading.recovery ?? RECOVERY_COMMAND };
|
|
142
|
+
if (!reading.status)
|
|
143
|
+
return { ...reading, started: false, recovery: reading.recovery ?? "catalyst-skills replica status --probe --json" };
|
|
144
|
+
if (
|
|
145
|
+
reading.status?.verdict === "not-configured" ||
|
|
146
|
+
reading.status?.dbPath == null
|
|
147
|
+
) {
|
|
148
|
+
return { ...reading, started: false, recovery: "catalyst-skills login" };
|
|
149
|
+
}
|
|
150
|
+
let started = false;
|
|
151
|
+
if (reading.status.writerAlive !== true && ["absent", "stale"].includes(reading.assessment.verdict)) {
|
|
152
|
+
const result = run(START_COMMAND);
|
|
153
|
+
if (result.code !== 0) {
|
|
154
|
+
return {
|
|
155
|
+
status: reading.status,
|
|
156
|
+
events: reading.events,
|
|
157
|
+
assessment: {
|
|
158
|
+
verdict: "unknown",
|
|
159
|
+
current: false,
|
|
160
|
+
reason: `writer start failed: ${result.error || result.stderr.trim() || result.stdout.trim()}`,
|
|
161
|
+
},
|
|
162
|
+
started: false,
|
|
163
|
+
recovery: RECOVERY_COMMAND,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
started = true;
|
|
167
|
+
}
|
|
168
|
+
const deadline = now() + waitSeconds * 1000;
|
|
169
|
+
do {
|
|
170
|
+
reading = readStatus(run);
|
|
171
|
+
if (reading.assessment.current || now() >= deadline) break;
|
|
172
|
+
await sleep(Math.min(3000, deadline - now()));
|
|
173
|
+
} while (true);
|
|
174
|
+
return {
|
|
175
|
+
...reading,
|
|
176
|
+
started,
|
|
177
|
+
recovery: reading.assessment.verdict === "unknown"
|
|
178
|
+
? "node scripts/local-sync.mjs --json"
|
|
179
|
+
: reading.status?.writerAlive
|
|
180
|
+
? "catalyst-skills replica stop && catalyst-skills replica start --detach"
|
|
181
|
+
: RECOVERY_COMMAND,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseArgs(argv) {
|
|
186
|
+
const opts = { start: false, waitSeconds: 90, json: false };
|
|
187
|
+
for (let i = 0; i < argv.length; i++) {
|
|
188
|
+
if (argv[i] === "--start") opts.start = true;
|
|
189
|
+
else if (argv[i] === "--json") opts.json = true;
|
|
190
|
+
else if (argv[i] === "--wait") opts.waitSeconds = Number(argv[++i]);
|
|
191
|
+
else if (argv[i] === "--help") {
|
|
192
|
+
console.log(
|
|
193
|
+
"Usage: node scripts/local-sync.mjs [--start] [--wait 0..300] [--json]\nWithout --start, this only checks status. Local sync is optional.",
|
|
194
|
+
);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
} else throw new Error(`Unknown argument: ${argv[i]}`);
|
|
197
|
+
}
|
|
198
|
+
return opts;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
202
|
+
try {
|
|
203
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
204
|
+
const result = await runLocalSync(opts);
|
|
205
|
+
if (opts.json) console.log(JSON.stringify(result));
|
|
206
|
+
else {
|
|
207
|
+
const label = result.assessment.verdict === "current" ? "current" : result.assessment.verdict;
|
|
208
|
+
console.log(`Local sync ${label}: ${result.assessment.reason}`);
|
|
209
|
+
if (!result.assessment.current) console.log(`Next: ${result.recovery}`);
|
|
210
|
+
}
|
|
211
|
+
process.exitCode = result.assessment.verdict === "current" ? 0 : result.assessment.verdict === "stale" ? 1 : result.assessment.verdict === "absent" ? 3 : 2;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
214
|
+
process.exitCode = 2;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Every count and every name below is read off what a verb printed. Nothing here is written down.
|
|
8
8
|
import { cliTarget, CONNECT_LINE, parseFlags, printHelp, runCli, tryJson, tryLoadConfig } from "./lib/cli.mjs";
|
|
9
|
+
import { runLocalSync } from "./local-sync.mjs";
|
|
9
10
|
|
|
10
11
|
const SPEC = {
|
|
11
12
|
next: { help: "print only the single next step" },
|
|
@@ -13,7 +14,8 @@ const SPEC = {
|
|
|
13
14
|
};
|
|
14
15
|
const NOTES = [
|
|
15
16
|
"Reads, in this order: `status` (machine), `ready --json` (machine checks and project checks, kept apart),",
|
|
16
|
-
"`
|
|
17
|
+
"`replica status --probe --json` and `events status --probe --json` (optional local freshness),",
|
|
18
|
+
"`me --json` and personal connection statuses (person), `contract --path …` for the account, the projects and the repositories,",
|
|
17
19
|
"`contract --path codingAccounts` (coding accounts; `accounts --json` on an older cloud), and each",
|
|
18
20
|
"project's hosts_current check with its fixedWhere (host).",
|
|
19
21
|
"Writes nothing and changes nothing. Runs before this machine is connected — that is one of the states it reports.",
|
|
@@ -31,6 +33,13 @@ if (positionals.length > 0) {
|
|
|
31
33
|
|
|
32
34
|
const via = cliTarget().via;
|
|
33
35
|
const parts = [];
|
|
36
|
+
const personalConnections = {};
|
|
37
|
+
let personalGrantIncomplete = false;
|
|
38
|
+
let personalLinearIncomplete = false;
|
|
39
|
+
let personalGithubIncomplete = false;
|
|
40
|
+
let personalNext = null;
|
|
41
|
+
let workspaceResolved = false;
|
|
42
|
+
let repositoryRegistered = false;
|
|
34
43
|
// `blocking` is false for a finding that is real and reportable but does not stop the next step —
|
|
35
44
|
// an unmatched Linear identity is the one that matters: it must be said, and it must not become the
|
|
36
45
|
// thing the person is told to go and do before they can map a project.
|
|
@@ -64,6 +73,7 @@ let machineVerdict = connected ? "ok" : "unfinished";
|
|
|
64
73
|
// whose id begins with "team:" belongs to a project and cannot be moved from this machine.
|
|
65
74
|
let projectChecks = [];
|
|
66
75
|
let machineFix = null;
|
|
76
|
+
let localSync;
|
|
67
77
|
if (connected) {
|
|
68
78
|
const ready = runCli(["ready", "--json"]);
|
|
69
79
|
const report = tryJson(ready.stdout);
|
|
@@ -81,6 +91,17 @@ if (connected) {
|
|
|
81
91
|
machineFix = failed.find((c) => typeof c.fix === "string")?.fix ?? null;
|
|
82
92
|
}
|
|
83
93
|
}
|
|
94
|
+
// Supplemental only: local caches are optional and do not change setup completion or --next.
|
|
95
|
+
localSync = await runLocalSync({ waitSeconds: 0 });
|
|
96
|
+
machineLines.push(
|
|
97
|
+
`note optional local sync ${localSync.assessment.verdict}: ${localSync.assessment.reason}; check with 'node scripts/local-sync.mjs', and start only with the person's opt-in via 'node scripts/local-sync.mjs --start'`,
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
localSync = {
|
|
101
|
+
assessment: { verdict: "unknown", current: false, reason: "connect this machine before local freshness can be checked" },
|
|
102
|
+
started: false,
|
|
103
|
+
recovery: "catalyst-skills login",
|
|
104
|
+
};
|
|
84
105
|
}
|
|
85
106
|
add(
|
|
86
107
|
"machine",
|
|
@@ -111,13 +132,35 @@ if (!connected) {
|
|
|
111
132
|
);
|
|
112
133
|
} else {
|
|
113
134
|
const matched = typeof user.linearUserId === "string" && user.linearUserId !== "";
|
|
135
|
+
const grantLines = [];
|
|
136
|
+
for (const provider of ["linear", "github"]) {
|
|
137
|
+
const read = runCli(["connections", "personal", provider, "status", "--json"]);
|
|
138
|
+
const result = tryJson(read.stdout);
|
|
139
|
+
const outcome = typeof result?.outcome === "string" ? result.outcome : "unreadable";
|
|
140
|
+
personalConnections[provider] = outcome;
|
|
141
|
+
if (outcome === "connected") {
|
|
142
|
+
grantLines.push(`personal ${provider}: connected`);
|
|
143
|
+
} else if (outcome === "absent" || outcome === "lapsed") {
|
|
144
|
+
personalGrantIncomplete = true;
|
|
145
|
+
if (provider === "linear") personalLinearIncomplete = true;
|
|
146
|
+
else personalGithubIncomplete = true;
|
|
147
|
+
grantLines.push(`personal ${provider}: ${outcome === "absent" ? "not connected" : "expired"}`);
|
|
148
|
+
if (provider === "linear") personalNext ??= "connect your personal linear account";
|
|
149
|
+
} else {
|
|
150
|
+
personalGrantIncomplete = true;
|
|
151
|
+
if (provider === "linear") personalLinearIncomplete = true;
|
|
152
|
+
else personalGithubIncomplete = true;
|
|
153
|
+
grantLines.push(`personal ${provider}: ${outcome === "unavailable" ? "temporarily unavailable; grant state unknown" : "could not be checked; update the catalyst-skills CLI or inspect its status output"}`);
|
|
154
|
+
if (provider === "linear") personalNext ??= "re-check your personal linear connection";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
114
157
|
add(
|
|
115
158
|
"person",
|
|
116
|
-
"catalyst-skills me",
|
|
117
|
-
matched ? "ok" : "unfinished",
|
|
118
|
-
[`${user.label ?? "(unnamed)"} (${user.role ?? "role unknown"})`, matched ? "Linear identity matched" : "Linear identity NOT matched — asks assigned to you cannot be told apart from everyone else's. It blocks nothing below;
|
|
119
|
-
matched ? null : "a tenant owner or admin",
|
|
120
|
-
matched ? null : link("/settings/account"),
|
|
159
|
+
"catalyst-skills me, and catalyst-skills connections personal <provider> status --json",
|
|
160
|
+
matched && !personalGrantIncomplete ? "ok" : "unfinished",
|
|
161
|
+
[`${user.label ?? "(unnamed)"} (${user.role ?? "role unknown"})`, matched ? "Linear identity matched" : "Linear identity NOT matched — asks assigned to you cannot be told apart from everyone else's. It blocks nothing below. Run catalyst-skills identity linear options for self-service recovery; personal Linear consent normally binds its viewer automatically.", ...grantLines],
|
|
162
|
+
personalGrantIncomplete ? "you" : matched ? null : "a tenant owner or admin",
|
|
163
|
+
personalGrantIncomplete ? "catalyst-skills connections personal <provider> start or status" : matched ? null : link("/settings/account"),
|
|
121
164
|
false,
|
|
122
165
|
);
|
|
123
166
|
}
|
|
@@ -133,6 +176,7 @@ if (!connected) {
|
|
|
133
176
|
add("account", "catalyst-skills contract --path account", "unreadable", ["the account block could not be read — try: catalyst-skills contract --refresh"], null, null);
|
|
134
177
|
} else {
|
|
135
178
|
const workspace = typeof doc.linearWorkspaceSlug === "string" && doc.linearWorkspaceSlug !== "" ? doc.linearWorkspaceSlug : typeof doc.linearWorkspaceId === "string" && doc.linearWorkspaceId !== "" ? doc.linearWorkspaceId : null;
|
|
179
|
+
workspaceResolved = workspace !== null;
|
|
136
180
|
// The declaration is the one part of the account a key can also READ — and it is the one part a
|
|
137
181
|
// key can WRITE, so it is reported here rather than left to the settings page like the rest.
|
|
138
182
|
const envLines = [];
|
|
@@ -212,6 +256,7 @@ if (!connected) {
|
|
|
212
256
|
add("repositories", "catalyst-skills contract --path merge.repositories", "unreadable", ["the repository list could not be read — try: catalyst-skills contract --refresh"], null, null);
|
|
213
257
|
} else {
|
|
214
258
|
const lines = [`${rows.length} registered`, ...rows.map((r) => `${r.owner ?? "?"}/${r.name ?? "?"}`)];
|
|
259
|
+
repositoryRegistered = rows.length > 0;
|
|
215
260
|
lines.push("⛔ REGISTRATION only. This carries no status and no project attachment, so it never proves a repository can be dispatched into.");
|
|
216
261
|
add("repositories", "catalyst-skills contract --path merge.repositories", rows.length > 0 ? "ok" : "unfinished", lines, "a tenant owner or admin", link("/settings/repositories"));
|
|
217
262
|
}
|
|
@@ -309,13 +354,28 @@ const machineNext = connected
|
|
|
309
354
|
: "connect this machine";
|
|
310
355
|
const NEXT = {
|
|
311
356
|
machine: machineNext,
|
|
312
|
-
person:
|
|
357
|
+
person: personalLinearIncomplete
|
|
358
|
+
? personalNext
|
|
359
|
+
: personalGithubIncomplete && repositoryRegistered
|
|
360
|
+
? "connect your personal github account"
|
|
361
|
+
: personalGithubIncomplete
|
|
362
|
+
? "install the tenant GitHub App and register its repository before connecting your personal GitHub account"
|
|
363
|
+
: "get this person's seat and Linear identity sorted",
|
|
313
364
|
account: "connect Linear, and install the GitHub App",
|
|
314
365
|
projects: "pick ONE project and map its stages (or adopt the Catalyst workflow)",
|
|
315
366
|
repositories: "register the repository, attaching it to the project you mapped",
|
|
316
367
|
"coding accounts": accountsNext,
|
|
317
368
|
host: "connect a Catalyst host",
|
|
318
369
|
};
|
|
370
|
+
// A personal Linear grant cannot start until the tenant's Linear workspace exists. Once that account
|
|
371
|
+
// connection is present, a missing personal grant becomes the next member step before project setup.
|
|
372
|
+
const personPart = parts.find((p) => p.part === "person");
|
|
373
|
+
// A personal Linear grant is the next provider step once the tenant workspace exists. A personal
|
|
374
|
+
// GitHub grant is sequenced after the tenant GitHub App: successful repository registration is the
|
|
375
|
+
// onboarding path's existing proof that the App installation is usable.
|
|
376
|
+
if (personPart && personalGrantIncomplete && workspaceResolved) {
|
|
377
|
+
personPart.blocking = personalLinearIncomplete || (personalGithubIncomplete && repositoryRegistered);
|
|
378
|
+
}
|
|
319
379
|
const blocked = parts.filter((p) => p.verdict !== "ok" && p.blocking);
|
|
320
380
|
const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
|
|
321
381
|
const next =
|
|
@@ -325,7 +385,7 @@ const next =
|
|
|
325
385
|
const finished = parts.every((p) => p.verdict === "ok");
|
|
326
386
|
|
|
327
387
|
if (flags.json) {
|
|
328
|
-
console.log(JSON.stringify({ cli: via, connected, cloud, parts, next, finished }));
|
|
388
|
+
console.log(JSON.stringify({ cli: via, connected, cloud, personalConnections, parts, localSync, next, finished }));
|
|
329
389
|
} else if (flags.next) {
|
|
330
390
|
if (next === null) console.log("nothing left: every part is finished, a coding account is enrolled and the host check passes. Move one card into the project's dispatch stage.");
|
|
331
391
|
else console.log(`${next.part}: ${next.action}${next.blocking ? "" : " (does not block the steps below)"}${next.owner ? ` — who: ${next.owner}` : ""}${next.where ? ` — ${next.where.startsWith("http") ? "where" : "do"}: ${next.where}` : ""}`);
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
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 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
5
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.1 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# Am I set up?
|
|
10
10
|
|
|
@@ -22,7 +22,7 @@ The contract's `readinessChecks[]` is the list of record. A check that appears t
|
|
|
22
22
|
| `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 |
|
|
23
23
|
| `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 |
|
|
24
24
|
| `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 |
|
|
25
|
-
| `environment_declared` | a committed environment declaration for the team's default repository is in effect (
|
|
25
|
+
| `environment_declared` | a committed environment declaration for the team's default repository is in effect (`.catalyst/catalyst.toml` on the default branch was ingested, is valid, and its latest proposal is approved) | `no_team_repo_default`: register a repository and make it the team's default; `no_environment_declaration`: commit `.catalyst/catalyst.toml` with its `[environment]` names to that repository's default branch; `declaration_invalid` / `declaration_read_failed`: fix the file (the ingest names the error); `declaration_awaiting_approval`: an owner or admin opens Settings → Repositories → the repository → Environment → Setup declaration and clicks Approve this revision | owner or admin, except committing the file, which is whoever can push to the repository |
|
|
26
26
|
| `tools_resolvable` | every MCP server and CLI the team's approved environment declaration names can be resolved for that team's repository | `tool_reference_unresolved`: the declaration names a vault secret that does not exist — add it at the right scope or correct the declaration; `toolchain_cli_missing`: a declared CLI is not in the runner image, which is not self-service; `tool_declarations_unread` reads as `unknown` and clears itself | owner or admin, in the declaration or Settings → Environment |
|
|
27
27
|
| `reviewer_required` | this team's repository can merge under its own merge policy with the reviewers configured | it fails only under a strict merge policy with no reviewer — configure one at Settings → Repositories → Code reviews, or change the repository's merge policy; work still runs, only the merge waits | owner or admin, in settings |
|
|
28
28
|
| `reviewer_configured` | a code reviewer is configured for this team's repository at all, whatever the current policy requires | a *failing* `reviewer_configured` never blocks or degrades a team — with no reviewer, pull requests merge on green checks and resolved threads alone; configure one at Settings → Repositories → Code reviews. An `unknown` one (`merge_reviewer_unread`: the reviewer registry could not be read) still degrades the team, like any unknown | owner or admin, in settings; nobody is required when it fails |
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.1 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Connect me
|
|
11
11
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
How Catalyst Cloud runs a ticket on the customer's own tenant, as facts an agent loads on demand: the eight-phase ladder and what each phase produces, the eleven board slots and this team's live stage map, what happens when a phase fails (retry, backoff, Remediate, park), how the queue is ordered and routed and every reason a ticket is excluded, and the coding-account model. Use when a person asks "how does this work?", "why did it do that?", "why is this stuck?", "what runs next?" or "how does it prioritise?". Read-only; its scripts explain one ticket's eligibility in plain English, show what is running and queued, and print the tenant's stage map and thresholds straight from the contract.
|
|
5
5
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.1 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# How Catalyst works
|
|
10
10
|
|
|
@@ -30,7 +30,7 @@ The contract's `teams[].workflowMode` reports which one a team is in:
|
|
|
30
30
|
|
|
31
31
|
`teams[].gitAutomation` is a stored consent for a feature that is not built yet (Catalyst managing a team's Linear git automations); nothing reads it, so `off` never stops work and turning it on would start none. What decides whether a team's tickets start is the mapping above: until dispatch, pr, done and canceled each point at a live stage, Catalyst starts nothing in that team, and `explain` says so by name.
|
|
32
32
|
|
|
33
|
-
Stage mappings live in the app, at Settings → Linear teams. The cloud
|
|
33
|
+
Stage mappings live in the app, at Settings → Linear teams. The cloud reads each repository's `.catalyst/catalyst.toml`, but that file has no stage-mapping section, so a mapping written there changes nothing. An older `.catalyst/config.json` is not imported.
|
|
34
34
|
|
|
35
35
|
## The state id is the authority; names are display
|
|
36
36
|
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.1 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Run this project
|
|
11
11
|
|
package/skills/unstick/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.1 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# Unstick
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
7
7
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
8
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.1 — written in this repository for customer tenants -->
|
|
9
9
|
|
|
10
10
|
# What needs me
|
|
11
11
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
The desk for a Catalyst Cloud tenant. Use when the person asks "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", or asks for something to be done rather than known. Reads the tenant contract, what is running and queued, the eligibility explainer and the open asks through the catalyst-skills CLI, and answers in one reply with ticket ids. Routes work to a project owner and decisions to what-needs-me. Never composes a URL, never polls, never answers as the human.
|
|
5
5
|
allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.1 — written in this repository for customer tenants -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|