@catalyst-cloud/cli 0.14.3 → 0.14.4
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 +5 -0
- package/dist/onboard-personal.js +229 -0
- package/dist/onboard-runtime.js +123 -97
- package/dist/onboard-ui.js +112 -46
- package/package.json +1 -1
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.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/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.4
|
|
4
|
+
|
|
5
|
+
- Personal Linear and GitHub approval links stay out of logs and receipts. Setup validates the signed browser destination and its expiry before opening it.
|
|
6
|
+
- Personal connection checks and browser waits stop on interruption or a deadline. Setup rechecks the current person and workspace, and keeps existing approval when its status is available.
|
|
7
|
+
|
|
3
8
|
## 0.14.3
|
|
4
9
|
|
|
5
10
|
- Setup can open the workspace's Linear approval from a personal login and wait for a fresh connection check before continuing. A saved credential alone does not count as connected.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { loadConfig, normalizeBaseUrl, } from "./config.js";
|
|
2
|
+
import { pollConsent } from "./onboard-consent.js";
|
|
3
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: null;
|
|
6
|
+
const waiting = (reason) => ({
|
|
7
|
+
state: "waiting",
|
|
8
|
+
reason,
|
|
9
|
+
});
|
|
10
|
+
const same = (a, b) => !!b?.user &&
|
|
11
|
+
!!a.user &&
|
|
12
|
+
a.account === b.account &&
|
|
13
|
+
a.user.id === b.user.id &&
|
|
14
|
+
normalizeBaseUrl(a.baseUrl) === normalizeBaseUrl(b.baseUrl);
|
|
15
|
+
/** Headers and body share an owned deadline; abandoned reads cannot rotate local credentials. */
|
|
16
|
+
async function read(ctx, journal, path, timeoutMs, external) {
|
|
17
|
+
const cfg = loadConfig(ctx.home);
|
|
18
|
+
if (!cfg?.user)
|
|
19
|
+
return { reason: "personal_login_required" };
|
|
20
|
+
if (((journal.account ?? journal.tenant) &&
|
|
21
|
+
(journal.account ?? journal.tenant) !== cfg.account) ||
|
|
22
|
+
(journal.membershipId && journal.membershipId !== cfg.user.id) ||
|
|
23
|
+
(journal.baseUrl &&
|
|
24
|
+
normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl)))
|
|
25
|
+
return { reason: "personal_identity_refused" };
|
|
26
|
+
const expiry = cfg.auth
|
|
27
|
+
? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
|
|
28
|
+
: 0;
|
|
29
|
+
const bearer = cfg.key ||
|
|
30
|
+
(cfg.auth && Number.isFinite(expiry) && expiry > 30_000
|
|
31
|
+
? cfg.auth.accessToken
|
|
32
|
+
: undefined);
|
|
33
|
+
if (!bearer)
|
|
34
|
+
return { reason: "personal_login_refresh_required" };
|
|
35
|
+
const owned = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => owned.abort(), timeoutMs);
|
|
37
|
+
const signal = external
|
|
38
|
+
? AbortSignal.any([external, owned.signal])
|
|
39
|
+
: owned.signal;
|
|
40
|
+
let remove = () => { };
|
|
41
|
+
try {
|
|
42
|
+
return await new Promise((resolve) => {
|
|
43
|
+
const stopped = () => resolve({
|
|
44
|
+
reason: external?.aborted
|
|
45
|
+
? "interrupted"
|
|
46
|
+
: "personal_status_unavailable",
|
|
47
|
+
});
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
stopped();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
53
|
+
remove = () => signal.removeEventListener("abort", stopped);
|
|
54
|
+
Promise.resolve()
|
|
55
|
+
.then(async () => {
|
|
56
|
+
if (signal.aborted)
|
|
57
|
+
return { reason: "interrupted" };
|
|
58
|
+
const response = await ctx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}${path}`, {
|
|
59
|
+
method: "GET",
|
|
60
|
+
redirect: "error",
|
|
61
|
+
headers: {
|
|
62
|
+
authorization: `Bearer ${bearer}`,
|
|
63
|
+
accept: "application/json",
|
|
64
|
+
},
|
|
65
|
+
signal,
|
|
66
|
+
});
|
|
67
|
+
if (response.status !== 200)
|
|
68
|
+
return { status: response.status };
|
|
69
|
+
const body = await response.json();
|
|
70
|
+
if (!same(cfg, loadConfig(ctx.home)))
|
|
71
|
+
return { reason: "personal_identity_refused" };
|
|
72
|
+
return { status: 200, body };
|
|
73
|
+
})
|
|
74
|
+
.then((value) => (signal.aborted ? stopped() : resolve(value)), () => signal.aborted
|
|
75
|
+
? stopped()
|
|
76
|
+
: resolve({ reason: "personal_status_unavailable" }));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
remove();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function failure(value, start = false) {
|
|
85
|
+
if ("reason" in value)
|
|
86
|
+
return ["personal_login_required", "personal_identity_refused"].includes(value.reason)
|
|
87
|
+
? { state: "refused", reason: value.reason }
|
|
88
|
+
: waiting(value.reason);
|
|
89
|
+
if (value.status === 401 || value.status === 403)
|
|
90
|
+
return { state: "refused", reason: "personal_consent_refused" };
|
|
91
|
+
if (value.status === 503)
|
|
92
|
+
return waiting("personal_status_unavailable");
|
|
93
|
+
if (value.status === 404)
|
|
94
|
+
return waiting("cloud_capability_unavailable");
|
|
95
|
+
return value.status === 200
|
|
96
|
+
? null
|
|
97
|
+
: {
|
|
98
|
+
state: "failed",
|
|
99
|
+
reason: start
|
|
100
|
+
? "personal_consent_start_failed"
|
|
101
|
+
: "personal_status_failed",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function handoff(body, provider, cfg, now) {
|
|
105
|
+
const row = object(body);
|
|
106
|
+
if (!row ||
|
|
107
|
+
typeof row.authorizationUrl !== "string" ||
|
|
108
|
+
row.authorizationUrl.length > 8192 ||
|
|
109
|
+
/[\u0000-\u0020\u007f]/.test(row.authorizationUrl) ||
|
|
110
|
+
typeof row.expiresAt !== "number" ||
|
|
111
|
+
!Number.isFinite(row.expiresAt) ||
|
|
112
|
+
row.expiresAt <= now ||
|
|
113
|
+
row.expiresAt > now + 630_000)
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(row.authorizationUrl);
|
|
117
|
+
const keys = [...url.searchParams.keys()];
|
|
118
|
+
const token = url.searchParams.get("handoff");
|
|
119
|
+
if (url.protocol !== "https:" ||
|
|
120
|
+
url.origin !== new URL(normalizeBaseUrl(cfg.baseUrl)).origin ||
|
|
121
|
+
url.username ||
|
|
122
|
+
url.password ||
|
|
123
|
+
url.pathname !== `/connect/${provider}/personal/start` ||
|
|
124
|
+
url.hash ||
|
|
125
|
+
keys.length !== 1 ||
|
|
126
|
+
keys[0] !== "handoff" ||
|
|
127
|
+
!token ||
|
|
128
|
+
token.length > 4096 ||
|
|
129
|
+
/[\s\u0000-\u001f\u007f]/.test(token))
|
|
130
|
+
return null;
|
|
131
|
+
return url.toString();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** The supported personal status reports server-resolved grant availability. A warm grant is
|
|
138
|
+
* not a new provider viewer probe, repository authorization, or workspace installation verdict. */
|
|
139
|
+
export function personalConsentAdapter(options) {
|
|
140
|
+
const timeout = options.requestTimeoutMs ?? 30_000;
|
|
141
|
+
const consentTimeout = options.consentTimeoutMs ?? 600_000;
|
|
142
|
+
if (!["linear", "github"].includes(options.provider) ||
|
|
143
|
+
!Number.isSafeInteger(timeout) ||
|
|
144
|
+
timeout < 1 ||
|
|
145
|
+
timeout > 30_000 ||
|
|
146
|
+
!Number.isSafeInteger(consentTimeout) ||
|
|
147
|
+
consentTimeout < 1 ||
|
|
148
|
+
consentTimeout > 600_000)
|
|
149
|
+
throw new Error("personal_timeout_invalid");
|
|
150
|
+
const statusPath = `/api/v1/me/connections/${options.provider}/personal`;
|
|
151
|
+
const inspect = async (ctx, journal, signal) => {
|
|
152
|
+
const response = await read(ctx, journal, statusPath, timeout, signal);
|
|
153
|
+
const failed = failure(response);
|
|
154
|
+
if (failed)
|
|
155
|
+
return failed;
|
|
156
|
+
const body = object("body" in response ? response.body : undefined);
|
|
157
|
+
if (!body ||
|
|
158
|
+
typeof body.connected !== "boolean" ||
|
|
159
|
+
(body.reason !== undefined &&
|
|
160
|
+
(body.connected || body.reason !== "lapsed")))
|
|
161
|
+
return { state: "failed", reason: "personal_status_shape" };
|
|
162
|
+
return body.connected
|
|
163
|
+
? {
|
|
164
|
+
state: "done",
|
|
165
|
+
evidence: {
|
|
166
|
+
provider: options.provider,
|
|
167
|
+
checkedAt: ctx.now().getTime(),
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: { state: "pending" };
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
check: inspect,
|
|
174
|
+
act: async (ctx, journal, signal) => {
|
|
175
|
+
if (signal?.aborted)
|
|
176
|
+
return waiting("interrupted");
|
|
177
|
+
const cfg = loadConfig(ctx.home);
|
|
178
|
+
if (!cfg?.user)
|
|
179
|
+
return { state: "refused", reason: "personal_login_required" };
|
|
180
|
+
const before = await inspect(ctx, journal, signal);
|
|
181
|
+
if (before.state !== "pending")
|
|
182
|
+
return before;
|
|
183
|
+
const start = await read(ctx, journal, `/connect/${options.provider}/personal/start`, timeout, signal);
|
|
184
|
+
if ("status" in start &&
|
|
185
|
+
start.status === 409 &&
|
|
186
|
+
options.provider === "linear")
|
|
187
|
+
return waiting("linear_workspace_required");
|
|
188
|
+
const failed = failure(start, true);
|
|
189
|
+
if (failed)
|
|
190
|
+
return failed;
|
|
191
|
+
const url = handoff("body" in start ? start.body : undefined, options.provider, cfg, ctx.now().getTime());
|
|
192
|
+
if (!url)
|
|
193
|
+
return { state: "refused", reason: "personal_consent_handoff" };
|
|
194
|
+
if (signal?.aborted)
|
|
195
|
+
return waiting("interrupted");
|
|
196
|
+
if (!same(cfg, loadConfig(ctx.home)))
|
|
197
|
+
return { state: "refused", reason: "personal_identity_refused" };
|
|
198
|
+
const name = options.provider === "linear" ? "Linear" : "GitHub";
|
|
199
|
+
ctx.stderr(`Approve your personal ${name} connection in the browser.`);
|
|
200
|
+
// Signed continuation credentials belong only to the browser, never transcript or journal.
|
|
201
|
+
try {
|
|
202
|
+
options.openBrowser(url);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return waiting("personal_browser_unavailable");
|
|
206
|
+
}
|
|
207
|
+
let latest = waiting("personal_status_unavailable");
|
|
208
|
+
const run = () => pollConsent({
|
|
209
|
+
timeoutMs: consentTimeout,
|
|
210
|
+
signal,
|
|
211
|
+
sleep: options.sleep,
|
|
212
|
+
readStatus: async (pollSignal) => {
|
|
213
|
+
latest = await inspect(ctx, journal, pollSignal);
|
|
214
|
+
if (latest.state === "done")
|
|
215
|
+
return { outcome: "connected" };
|
|
216
|
+
if (latest.state === "refused" || latest.state === "failed")
|
|
217
|
+
return { outcome: latest.state, reason: latest.reason };
|
|
218
|
+
return {
|
|
219
|
+
outcome: latest.state === "pending" ? "absent" : "unavailable",
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const result = options.wait
|
|
224
|
+
? await options.wait(`Waiting for your ${name} approval`, run)
|
|
225
|
+
: await run();
|
|
226
|
+
return result.state === "done" ? latest : result;
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
package/dist/onboard-runtime.js
CHANGED
|
@@ -2,17 +2,20 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { userInfo } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { openBrowser } from "./browser.js";
|
|
5
|
-
import { defaultSkillsDirFor, loadConfig, normalizeBaseUrl, readManifest } from "./config.js";
|
|
5
|
+
import { defaultSkillsDirFor, loadConfig, normalizeBaseUrl, readManifest, } from "./config.js";
|
|
6
6
|
import { CliError } from "./errors.js";
|
|
7
7
|
import { cmdLegacy, findLegacy } from "./legacy.js";
|
|
8
8
|
import { bearerFor } from "./oauth.js";
|
|
9
|
-
import {
|
|
9
|
+
import { personalConsentAdapter } from "./onboard-personal.js";
|
|
10
10
|
import { boundedOnboardSignin } from "./onboard-signin.js";
|
|
11
11
|
import { existingLinearAdapters } from "./onboard-existing.js";
|
|
12
12
|
import { linearWorkspaceAdapter } from "./onboard-workspace.js";
|
|
13
13
|
import { existingRepositoryAdapter } from "./onboard-repositories.js";
|
|
14
14
|
import { fetchMe } from "./transport.js";
|
|
15
|
-
const waiting = (reason) => ({
|
|
15
|
+
const waiting = (reason) => ({
|
|
16
|
+
state: "waiting",
|
|
17
|
+
reason,
|
|
18
|
+
});
|
|
16
19
|
function sameHome(a, b) {
|
|
17
20
|
try {
|
|
18
21
|
return realpathSync(a) === realpathSync(b);
|
|
@@ -28,129 +31,152 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
28
31
|
if (!cfg)
|
|
29
32
|
return null;
|
|
30
33
|
const requested = args.baseUrl ?? stepCtx.env.CATALYST_CLOUD_BASE_URL;
|
|
31
|
-
if (requested &&
|
|
34
|
+
if (requested &&
|
|
35
|
+
normalizeBaseUrl(requested) !== normalizeBaseUrl(cfg.baseUrl))
|
|
32
36
|
throw new CliError("the saved login uses another cloud; resume against that cloud", "onboard-base-url-mismatch", 12);
|
|
33
37
|
if (!cfg.user)
|
|
34
38
|
throw new CliError("sign in as yourself to set up Catalyst", "onboard-person-required", 12);
|
|
35
|
-
if (journal &&
|
|
36
|
-
(journal.
|
|
37
|
-
|
|
39
|
+
if (journal &&
|
|
40
|
+
(((journal.account ?? journal.tenant) &&
|
|
41
|
+
(journal.account ?? journal.tenant) !== cfg.account) ||
|
|
42
|
+
(journal.membershipId && journal.membershipId !== cfg.user.id) ||
|
|
43
|
+
(journal.baseUrl &&
|
|
44
|
+
normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))))
|
|
38
45
|
throw new CliError("the saved setup belongs to another workspace or member", "onboard-identity-mismatch", 12);
|
|
39
46
|
const me = await fetchMe(cfg.baseUrl, await bearerFor(stepCtx, cfg), stepCtx.fetch);
|
|
40
47
|
if (!me.user || me.account !== cfg.account || me.user.id !== cfg.user.id)
|
|
41
48
|
throw new CliError("the saved login and live member do not match", "onboard-identity-mismatch", 12);
|
|
42
49
|
// /me names a person within the account, not an independently exposed membership-row ID.
|
|
43
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
account: me.account,
|
|
52
|
+
membershipId: me.user.id,
|
|
53
|
+
baseUrl: normalizeBaseUrl(cfg.baseUrl),
|
|
54
|
+
role: me.user.role,
|
|
55
|
+
};
|
|
44
56
|
};
|
|
45
57
|
const legacyCheck = async (stepCtx) => {
|
|
46
|
-
const found = findLegacy(stepCtx.home, hooks.legacy?.platform ?? process.platform).filter(item => !item.data).length;
|
|
58
|
+
const found = findLegacy(stepCtx.home, hooks.legacy?.platform ?? process.platform).filter((item) => !item.data).length;
|
|
47
59
|
if (!sameHome(stepCtx.home, (hooks.realHome ?? (() => userInfo().homedir))()))
|
|
48
|
-
return {
|
|
49
|
-
|
|
60
|
+
return {
|
|
61
|
+
state: "skipped",
|
|
62
|
+
reason: "fake_home_report_only",
|
|
63
|
+
evidence: { found, remaining: found },
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
state: found ? "pending" : "done",
|
|
67
|
+
evidence: { found, remaining: found },
|
|
68
|
+
};
|
|
50
69
|
};
|
|
51
|
-
const unsupported = {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!cfg || !cfg.user)
|
|
55
|
-
return { outcome: "refused", reason: "personal_login_required" };
|
|
56
|
-
const response = await stepCtx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}/api/v1/me/connections/${provider}/personal`, {
|
|
57
|
-
headers: { authorization: `Bearer ${await bearerFor(stepCtx, cfg)}`, accept: "application/json" },
|
|
58
|
-
signal: signal ?? AbortSignal.timeout(30_000),
|
|
59
|
-
});
|
|
60
|
-
if (response.status === 401 || response.status === 403)
|
|
61
|
-
return { outcome: "refused", reason: "personal_consent_refused" };
|
|
62
|
-
if (response.status === 503)
|
|
63
|
-
return { outcome: "unavailable" };
|
|
64
|
-
if (!response.ok)
|
|
65
|
-
return { outcome: "failed", reason: "personal_status_failed" };
|
|
66
|
-
const body = await response.json();
|
|
67
|
-
if (!body || typeof body !== "object" || !("connected" in body))
|
|
68
|
-
return { outcome: "failed", reason: "personal_status_shape" };
|
|
69
|
-
if (body.connected === true)
|
|
70
|
-
return { outcome: "connected" };
|
|
71
|
-
if (body.connected !== false)
|
|
72
|
-
return { outcome: "failed", reason: "personal_status_shape" };
|
|
73
|
-
return { outcome: "reason" in body && body.reason === "lapsed" ? "lapsed" : "absent" };
|
|
70
|
+
const unsupported = {
|
|
71
|
+
check: async () => waiting("cloud_capability_unavailable"),
|
|
72
|
+
act: async () => waiting("cloud_capability_unavailable"),
|
|
74
73
|
};
|
|
75
|
-
const personalAdapter = (provider) => ({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return result.outcome === "unavailable" ? waiting("personal_status_unavailable") : { state: "pending" };
|
|
83
|
-
}, act: async (stepCtx, _journal, signal) => {
|
|
84
|
-
const cfg = loadConfig(stepCtx.home);
|
|
85
|
-
if (!cfg || !cfg.user)
|
|
86
|
-
return { state: "refused", reason: "personal_login_required" };
|
|
87
|
-
const response = await stepCtx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}/connect/${provider}/personal/start`, {
|
|
88
|
-
headers: { authorization: `Bearer ${await bearerFor(stepCtx, cfg)}`, accept: "application/json" }, signal: AbortSignal.timeout(30_000),
|
|
89
|
-
});
|
|
90
|
-
if (response.status === 409)
|
|
91
|
-
return waiting(provider === "linear" ? "linear_workspace_required" : "github_workspace_required");
|
|
92
|
-
if (response.status === 401 || response.status === 403)
|
|
93
|
-
return { state: "refused", reason: "personal_consent_refused" };
|
|
94
|
-
if (!response.ok)
|
|
95
|
-
return { state: "failed", reason: "personal_consent_start_failed" };
|
|
96
|
-
const body = await response.json();
|
|
97
|
-
if (typeof body.authorizationUrl !== "string" || typeof body.expiresAt !== "number")
|
|
98
|
-
return { state: "failed", reason: "personal_consent_shape" };
|
|
99
|
-
let url;
|
|
100
|
-
try {
|
|
101
|
-
url = new URL(body.authorizationUrl);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return { state: "failed", reason: "personal_consent_shape" };
|
|
105
|
-
}
|
|
106
|
-
if (url.protocol !== "https:" || url.origin !== new URL(cfg.baseUrl).origin || url.username || url.password)
|
|
107
|
-
return { state: "refused", reason: "personal_consent_origin" };
|
|
108
|
-
stepCtx.stderr(`Approve your personal ${provider === "linear" ? "Linear" : "GitHub"} connection in the browser.`);
|
|
109
|
-
stepCtx.stderr(url.toString());
|
|
110
|
-
(hooks.openBrowser ?? openBrowser)(url.toString());
|
|
111
|
-
const poll = () => pollConsent({ readStatus: pollSignal => personalStatus(provider, stepCtx, pollSignal), sleep: hooks.sleep, signal });
|
|
112
|
-
return hooks.ui ? hooks.ui.wait(`Waiting for your ${provider === "linear" ? "Linear" : "GitHub"} approval`, poll) : poll();
|
|
113
|
-
}
|
|
74
|
+
const personalAdapter = (provider) => personalConsentAdapter({
|
|
75
|
+
provider,
|
|
76
|
+
openBrowser: hooks.openBrowser ?? openBrowser,
|
|
77
|
+
sleep: hooks.sleep,
|
|
78
|
+
wait: hooks.ui
|
|
79
|
+
? (message, run) => hooks.ui.wait(message, run)
|
|
80
|
+
: undefined,
|
|
114
81
|
});
|
|
115
|
-
const linear = existingLinearAdapters(args, hooks.ui?.chooseTeam
|
|
82
|
+
const linear = existingLinearAdapters(args, hooks.ui?.chooseTeam
|
|
83
|
+
? (teams) => hooks.ui.chooseTeam(teams.map((team) => ({ ...team })))
|
|
84
|
+
: undefined);
|
|
116
85
|
return {
|
|
117
|
-
identity: journal => identity(ctx, journal),
|
|
86
|
+
identity: (journal) => identity(ctx, journal),
|
|
87
|
+
bindSignals: true,
|
|
88
|
+
ui: hooks.ui,
|
|
118
89
|
adapters: {
|
|
119
|
-
machine: {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
machine: {
|
|
91
|
+
check: async () => process.platform === "darwin" || process.platform === "linux"
|
|
92
|
+
? { state: "done", evidence: { provider: process.platform } }
|
|
93
|
+
: waiting("machine_platform_unsupported"),
|
|
94
|
+
},
|
|
95
|
+
cli: {
|
|
96
|
+
check: async () => ({
|
|
97
|
+
state: "done",
|
|
98
|
+
evidence: { version: readManifest().version },
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
skills: {
|
|
102
|
+
check: async (stepCtx) => {
|
|
123
103
|
const cfg = loadConfig(stepCtx.home);
|
|
124
|
-
const dir = stepCtx.env.CATALYST_SKILLS_DIR ??
|
|
104
|
+
const dir = stepCtx.env.CATALYST_SKILLS_DIR ??
|
|
105
|
+
cfg?.skillsDir ??
|
|
106
|
+
defaultSkillsDirFor(stepCtx.home);
|
|
125
107
|
const names = hooks.skillNames ?? [];
|
|
126
|
-
const count = names.filter(name => existsSync(join(dir, name, "SKILL.md"))).length;
|
|
127
|
-
return names.length > 0 && count === names.length
|
|
128
|
-
|
|
129
|
-
|
|
108
|
+
const count = names.filter((name) => existsSync(join(dir, name, "SKILL.md"))).length;
|
|
109
|
+
return names.length > 0 && count === names.length
|
|
110
|
+
? { state: "done", evidence: { count, path: dir } }
|
|
111
|
+
: waiting("skills_install_unverified");
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
legacy: {
|
|
115
|
+
check: legacyCheck,
|
|
116
|
+
act: async (stepCtx) => {
|
|
130
117
|
const before = await legacyCheck(stepCtx);
|
|
131
118
|
if (before.state !== "pending")
|
|
132
119
|
return before;
|
|
133
120
|
// Provider/process output is not trusted to be secret-free; keep it out of the receipt/log.
|
|
134
121
|
const quiet = { ...stepCtx, stdout: () => { }, stderr: () => { } };
|
|
135
|
-
const code = await cmdLegacy({
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
122
|
+
const code = await cmdLegacy({
|
|
123
|
+
...args,
|
|
124
|
+
command: "legacy",
|
|
125
|
+
subcommand: null,
|
|
126
|
+
rest: [],
|
|
127
|
+
flags: { remove: true, yes: true },
|
|
128
|
+
json: false,
|
|
129
|
+
}, quiet, hooks.legacy);
|
|
130
|
+
return code === 0
|
|
131
|
+
? legacyCheck(stepCtx)
|
|
132
|
+
: { state: "failed", reason: "legacy_cleanup_failed" };
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
signin: {
|
|
136
|
+
check: async (stepCtx, journal) => {
|
|
139
137
|
const who = await identity(stepCtx, journal);
|
|
140
|
-
return who
|
|
141
|
-
|
|
138
|
+
return who
|
|
139
|
+
? {
|
|
140
|
+
state: "done",
|
|
141
|
+
evidence: {
|
|
142
|
+
account: who.account,
|
|
143
|
+
membershipId: who.membershipId,
|
|
144
|
+
role: who.role,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
: { state: "pending" };
|
|
148
|
+
},
|
|
149
|
+
act: async (stepCtx, _journal, signal) => {
|
|
142
150
|
return boundedOnboardSignin({ ...stepCtx, stdout: stepCtx.stderr }, hooks.login, signal, hooks.signinTimeoutMs);
|
|
143
|
-
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
144
153
|
...linear,
|
|
145
|
-
"linear.workspace": linearWorkspaceAdapter({
|
|
146
|
-
|
|
154
|
+
"linear.workspace": linearWorkspaceAdapter({
|
|
155
|
+
fallback: linear["linear.workspace"],
|
|
156
|
+
openBrowser: hooks.openBrowser ?? openBrowser,
|
|
157
|
+
wait: hooks.ui
|
|
158
|
+
? (message, work) => hooks.ui.wait(message, work)
|
|
159
|
+
: undefined,
|
|
160
|
+
sleep: hooks.sleep,
|
|
161
|
+
}),
|
|
147
162
|
"github.install": unsupported,
|
|
148
|
-
"github.repos": existingRepositoryAdapter(args, hooks.ui?.chooseRepositories
|
|
163
|
+
"github.repos": existingRepositoryAdapter(args, hooks.ui?.chooseRepositories
|
|
164
|
+
? (repositories) => hooks.ui.chooseRepositories(repositories.map((row) => ({ ...row })))
|
|
165
|
+
: undefined),
|
|
149
166
|
"linear.personal": personalAdapter("linear"),
|
|
150
167
|
"github.personal": personalAdapter("github"),
|
|
151
|
-
daemon: {
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
daemon: {
|
|
169
|
+
check: async (_stepCtx, journal) => (journal.localSync ?? args.flags["local-sync"] === true)
|
|
170
|
+
? waiting("local_sync_capability_unavailable")
|
|
171
|
+
: {
|
|
172
|
+
state: "skipped",
|
|
173
|
+
reason: "local_sync_not_selected",
|
|
174
|
+
evidence: { provider: "cloud" },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
housekeeping: {
|
|
178
|
+
check: async () => waiting("housekeeping_service_unverified"),
|
|
179
|
+
},
|
|
154
180
|
ready: { check: hooks.ready },
|
|
155
181
|
},
|
|
156
182
|
};
|
package/dist/onboard-ui.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { createOnboardProgress } from "./onboard-progress.js";
|
|
2
|
-
import { ONBOARD_STEPS, ONBOARD_TITLES } from "./onboard.js";
|
|
1
|
+
import { createOnboardProgress, } from "./onboard-progress.js";
|
|
2
|
+
import { ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
|
|
3
3
|
export function shouldUseOnboardUi(args, stdoutTty) {
|
|
4
|
-
return stdoutTty &&
|
|
4
|
+
return (stdoutTty &&
|
|
5
|
+
!args.json &&
|
|
6
|
+
args.flags.yes !== true &&
|
|
7
|
+
args.flags["dry-run"] !== true);
|
|
5
8
|
}
|
|
6
9
|
export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
7
10
|
const abort = new AbortController();
|
|
8
11
|
const options = { ...streams, signal: abort.signal };
|
|
9
12
|
// Clack 1.8.1's spinner calls process.exit(0) on raw Ctrl-C, bypassing receipt/lock cleanup.
|
|
10
|
-
const spin = deps.progress ??
|
|
13
|
+
const spin = deps.progress ??
|
|
14
|
+
createOnboardProgress(streams.output, abort.signal, () => abort.abort());
|
|
11
15
|
const signals = deps.signals ?? process;
|
|
12
16
|
const interrupt = () => abort.abort();
|
|
13
17
|
for (const event of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
@@ -15,15 +19,23 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
15
19
|
let active = false;
|
|
16
20
|
let introduced = false;
|
|
17
21
|
let group;
|
|
18
|
-
const stop = () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const stop = () => {
|
|
23
|
+
if (active) {
|
|
24
|
+
spin.stop();
|
|
25
|
+
active = false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const message = (text) => {
|
|
29
|
+
stop();
|
|
30
|
+
prompts.log.message(text, { output: streams.output });
|
|
31
|
+
};
|
|
32
|
+
const start = (text) => {
|
|
33
|
+
stop();
|
|
34
|
+
if (!abort.signal.aborted) {
|
|
35
|
+
spin.start(text);
|
|
36
|
+
active = true;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
27
39
|
return {
|
|
28
40
|
signal: abort.signal,
|
|
29
41
|
plan(journal) {
|
|
@@ -32,52 +44,89 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
32
44
|
prompts.intro("Catalyst setup", { output: streams.output });
|
|
33
45
|
introduced = true;
|
|
34
46
|
}
|
|
35
|
-
const steps = new Map(journal.steps.map(step => [step.id, step]));
|
|
36
|
-
message(ONBOARD_STEPS.map(id => `· ${ONBOARD_TITLES[id]}${steps.get(id)?.state === "done" ? " (recheck)" : ""}`).join("\n"));
|
|
47
|
+
const steps = new Map(journal.steps.map((step) => [step.id, step]));
|
|
48
|
+
message(ONBOARD_STEPS.map((id) => `· ${ONBOARD_TITLES[id]}${steps.get(id)?.state === "done" ? " (recheck)" : ""}`).join("\n"));
|
|
37
49
|
message("Use cloud reads by default. Local sync is optional for SQL, offline work or sustained reads.");
|
|
38
50
|
},
|
|
39
51
|
async confirmPlan(localSync) {
|
|
40
52
|
stop();
|
|
41
|
-
const answer = await prompts.select({
|
|
42
|
-
|
|
53
|
+
const answer = await prompts.select({
|
|
54
|
+
...options,
|
|
55
|
+
message: "Continue with this plan?",
|
|
56
|
+
initialValue: localSync ? "local" : "cloud",
|
|
57
|
+
options: [
|
|
58
|
+
{
|
|
59
|
+
value: "cloud",
|
|
60
|
+
label: "Continue using cloud reads",
|
|
61
|
+
hint: "default",
|
|
62
|
+
},
|
|
43
63
|
{ value: "local", label: "Continue and set up local sync" },
|
|
44
64
|
{ value: "stop", label: "Stop without changes" },
|
|
45
|
-
]
|
|
65
|
+
],
|
|
66
|
+
});
|
|
46
67
|
if (prompts.isCancel(answer)) {
|
|
47
68
|
abort.abort();
|
|
48
69
|
return { proceed: false, localSync };
|
|
49
70
|
}
|
|
50
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
proceed: answer === "cloud" || answer === "local",
|
|
73
|
+
localSync: answer === "local",
|
|
74
|
+
};
|
|
51
75
|
},
|
|
52
76
|
async chooseTeam(teams) {
|
|
53
77
|
stop();
|
|
54
78
|
if (!teams.length || abort.signal.aborted)
|
|
55
79
|
return null;
|
|
56
|
-
const answer = await prompts.select({
|
|
57
|
-
options
|
|
80
|
+
const answer = await prompts.select({
|
|
81
|
+
...options,
|
|
82
|
+
message: "Which Linear team should this project use?",
|
|
83
|
+
initialValue: teams[0].id,
|
|
84
|
+
options: teams.map((team) => ({
|
|
85
|
+
value: team.id,
|
|
86
|
+
label: `${team.name || team.key || team.id}${team.key && team.name ? ` (${team.key})` : ""}`,
|
|
87
|
+
})),
|
|
88
|
+
});
|
|
58
89
|
if (prompts.isCancel(answer)) {
|
|
59
90
|
abort.abort();
|
|
60
91
|
return null;
|
|
61
92
|
}
|
|
62
93
|
return typeof answer === "string" ? answer : null;
|
|
63
94
|
},
|
|
64
|
-
...(prompts.multiselect
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
...(prompts.multiselect
|
|
96
|
+
? {
|
|
97
|
+
async chooseRepositories(repositories) {
|
|
98
|
+
stop();
|
|
99
|
+
if (!repositories.length || abort.signal.aborted)
|
|
100
|
+
return null;
|
|
101
|
+
const answer = await prompts.multiselect({
|
|
102
|
+
...options,
|
|
103
|
+
message: "Which repositories should use this Linear team?",
|
|
104
|
+
required: true,
|
|
105
|
+
options: repositories.map((row) => ({
|
|
106
|
+
value: `${row.owner}/${row.name}`,
|
|
107
|
+
label: `${row.owner}/${row.name}`,
|
|
108
|
+
})),
|
|
109
|
+
});
|
|
110
|
+
if (prompts.isCancel(answer)) {
|
|
111
|
+
abort.abort();
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return Array.isArray(answer) ? answer : null;
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
76
118
|
stepStart(id) {
|
|
77
|
-
const next = ["machine", "cli", "skills", "legacy"].includes(id)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
const next = ["machine", "cli", "skills", "legacy"].includes(id)
|
|
120
|
+
? "This computer"
|
|
121
|
+
: id === "signin"
|
|
122
|
+
? "Catalyst sign-in"
|
|
123
|
+
: id.startsWith("linear.") || id.startsWith("github.")
|
|
124
|
+
? "Connections"
|
|
125
|
+
: ["projects", "accounts", "settings", "values"].includes(id)
|
|
126
|
+
? "Project setup"
|
|
127
|
+
: ["capacity", "daemon", "housekeeping"].includes(id)
|
|
128
|
+
? "Runner and services"
|
|
129
|
+
: "Work and readiness";
|
|
81
130
|
if (next !== group) {
|
|
82
131
|
message(next);
|
|
83
132
|
group = next;
|
|
@@ -87,6 +136,11 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
87
136
|
stepEnd(step) {
|
|
88
137
|
stop();
|
|
89
138
|
const reasons = {
|
|
139
|
+
personal_consent_handoff: "The browser approval link could not be verified. Run catalyst onboard to try again.",
|
|
140
|
+
personal_identity_refused: "Your current login no longer matches this setup. Resume with the original workspace and person.",
|
|
141
|
+
personal_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
|
|
142
|
+
personal_status_unavailable: "Your personal connection could not be checked. Run catalyst onboard to try again.",
|
|
143
|
+
personal_browser_unavailable: "The browser could not open. Run catalyst onboard to try again.",
|
|
90
144
|
cloud_capability_unavailable: "Your cloud does not support this setup step yet.",
|
|
91
145
|
step_not_available_in_this_release: "This setup step is not available yet.",
|
|
92
146
|
prerequisite_not_ready: "Waiting for an earlier setup step.",
|
|
@@ -115,27 +169,39 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
115
169
|
repository_selection_unverified: "Your selected repositories could not be verified. Run catalyst onboard --repo <owner/name> to choose again.",
|
|
116
170
|
};
|
|
117
171
|
const text = `${ONBOARD_TITLES[step.id]}${step.reason ? `: ${reasons[step.reason] ?? step.reason.replaceAll("_", " ")}` : ""}`;
|
|
118
|
-
const kind = step.state === "done"
|
|
119
|
-
|
|
172
|
+
const kind = step.state === "done"
|
|
173
|
+
? "info"
|
|
174
|
+
: step.state === "failed"
|
|
175
|
+
? "error"
|
|
176
|
+
: step.state === "waiting" || step.state === "pending"
|
|
177
|
+
? "warn"
|
|
178
|
+
: "message";
|
|
179
|
+
prompts.log[kind](`${step.state === "done" ? "✓ " : ""}${text}`, {
|
|
180
|
+
output: streams.output,
|
|
181
|
+
});
|
|
120
182
|
},
|
|
121
183
|
message,
|
|
122
184
|
finish(journal, only) {
|
|
123
185
|
stop();
|
|
124
186
|
let text = "Setup still needs checks. Run catalyst onboard to resume.";
|
|
125
187
|
if (abort.signal.aborted)
|
|
126
|
-
text =
|
|
188
|
+
text =
|
|
189
|
+
"Setup paused. Your progress is saved.\nresume: catalyst onboard";
|
|
127
190
|
else if (journal.complete && journal.exit === 0 && !only)
|
|
128
191
|
text = "Onboarding complete.";
|
|
129
192
|
else if (only && journal.exit === 0)
|
|
130
193
|
text = `${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.\nresume: catalyst onboard`;
|
|
131
194
|
prompts.outro(text, { output: streams.output });
|
|
132
195
|
},
|
|
133
|
-
async wait(text, run) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
196
|
+
async wait(text, run) {
|
|
197
|
+
start(text);
|
|
198
|
+
try {
|
|
199
|
+
return await run();
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
stop();
|
|
203
|
+
}
|
|
204
|
+
},
|
|
139
205
|
dispose() {
|
|
140
206
|
try {
|
|
141
207
|
stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
4
4
|
"description": "Customer skill bundle for Catalyst Cloud, named from the executive's seat \u2014 get set up, see what is happening, see what needs you, run a project, unstick what is stuck \u2014 plus the catalyst CLI that reads through the Catalyst Cloud SDK, writes through the agent proxy, and connects your machine to your cloud account with one command. The skills directory is the roster; this line never counts it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst's GitHub
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Catalyst's view of Linear on the customer's own cloud account. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the local replica when it is fresh and the origin-fresh API otherwise, always naming the source; searches tickets, PRs, projects and initiatives; writes comments, card moves, labels and new tickets through the account's agent proxy as the app actor, with every route, stage id, label id and marker read from the account's contract. Knows what a ticket accumulates as Catalyst works it (phase-outcome comments, the document per phase, the agent session, the labels, the single blocks relation, the bookkeeping marker, the eyes acknowledgement). Use when a person says "show me the ticket", "what did Catalyst write on it", "comment on it", "move it", "label it" or "file a ticket". Not for raising a decision for a human (what-needs-me) and not for pull requests (catalyst-github).
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst Linear
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Set up Catalyst Cloud and say whether setup is working. Walks a person from nothing to their first ticket running, one step at a time: the coding account, the project, the integrations and connected accounts, each repository's settings file, then one real ticket moving. Also logs this machine in, reads the readiness verdict with who can fix each failure, and checks the optional local replica. Use when someone says "set me up", "onboard me", "I just signed up", "get me started", "what do I do first", "log me in", "which account is this machine on", "am I set up", "what is missing", "why does nothing happen" or "is the replica running", when a login expired, or when a skill script exits 2 saying this machine is not connected. Does every step a key can do through the catalyst CLI, hands over the exact page for the steps only a browser can do, and never claims a step it did not watch succeed.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Onboard me
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Own one Catalyst Cloud project end to end until it closes. Use when the person says "run this project for me", "own this until it ships", "keep this moving", or hands you a project id or a set of tickets to drive. Subscribes to the account's event stream for the scope through the catalyst CLI, reacts to each change in the same turn, makes tickets ready and moves them to dispatch, parks what should stop, chases stalls, escalates inward, and keeps one status summary current. Writes to Linear as the app actor; never polls.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Run this project
|
|
10
10
|
|
package/skills/unstick/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Get a stuck Catalyst Cloud ticket moving again. Use when the person asks "why is this parked and can you release it?", "unpark this", "get things flowing again", "the outage is over, retry what failed", or hands you a ticket that is not moving. Reads why nothing runs and what holds the ticket through the catalyst CLI, decides whether the recorded cause is fixed, previews the release, releases every governor holding the ticket the right way (or one failure class across a team), and raises an ask only for what a person has to do. Never releases a cause it cannot show changed without saying so.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Unstick
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
The human's decision inbox on Catalyst Cloud, and the one way an agent raises a decision on their behalf. Use when the person asks "what needs me?", "what am I blocking?", or when active work is gated on a choice only they can make. Lists open asks ranked by how much open work each one holds, files an ask through the cloud's ask route with the account's own template, and records the answer so the held work releases. Never answers as the human, never duplicates an open ask.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What needs me
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
What environment variable names does this repository need, and where does each one come from? Scans the repository offline (no login, no network) and lists the names in three groups (build/test, deploy-only, bindings), each with where it was found, what uses it, and where a local value would come from. Never reads a value or prints one. Also checks the environment variable table in `.catalyst/catalyst.toml` offline. Use when someone asks "what does catalyst.env.json mean", "what env vars does this repo need", "why does the container need this", or before reviewing or writing the declaration.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What this repository needs
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Use for "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", "how does this work?", "why did it do that?", "how does it prioritise?" and "what does this setting do, where is it set?" in Catalyst Cloud, or asks for something to be done rather than known. Reads the contract via catalyst CLI. Read-only: route work/decisions, never answer as the human.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.4 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|