@catalyst-cloud/cli 0.14.5 → 0.14.7

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 (63) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -3
  3. package/dist/args.js +51 -13
  4. package/dist/cli.js +58 -10
  5. package/dist/config.js +16 -3
  6. package/dist/daemon-authority-lifetime.js +171 -0
  7. package/dist/daemon-authority.js +216 -0
  8. package/dist/daemon-cache.js +348 -0
  9. package/dist/daemon-store.js +370 -0
  10. package/dist/daemon-supervisor.js +460 -0
  11. package/dist/env/onboard-draft-store.js +243 -0
  12. package/dist/env/onboard-draft.js +33 -0
  13. package/dist/env/selected-import-writer.js +163 -0
  14. package/dist/env/settings-authority.js +203 -0
  15. package/dist/events-cloud-tail.js +134 -0
  16. package/dist/events-cloud.js +500 -0
  17. package/dist/events.js +13 -4
  18. package/dist/legacy.js +145 -32
  19. package/dist/oauth.js +125 -33
  20. package/dist/onboard-accounts.js +259 -0
  21. package/dist/onboard-automation-management.js +15 -0
  22. package/dist/onboard-bootstrap.js +162 -0
  23. package/dist/onboard-capabilities.js +15 -7
  24. package/dist/onboard-capacity.js +226 -0
  25. package/dist/onboard-existing.js +106 -32
  26. package/dist/onboard-file-snapshot.js +45 -0
  27. package/dist/onboard-login-candidate.js +261 -0
  28. package/dist/onboard-projects.js +350 -0
  29. package/dist/onboard-repositories.js +147 -37
  30. package/dist/onboard-runtime.js +104 -9
  31. package/dist/onboard-settings-context.js +141 -0
  32. package/dist/onboard-settings-import.js +127 -0
  33. package/dist/onboard-settings.js +234 -0
  34. package/dist/onboard-ui.js +99 -11
  35. package/dist/onboard-workflow.js +316 -0
  36. package/dist/onboard.js +359 -27
  37. package/dist/owned-fetch.js +182 -0
  38. package/dist/query.js +77 -27
  39. package/dist/ready.js +143 -35
  40. package/dist/replica.js +11 -4
  41. package/dist/sdk.js +33 -0
  42. package/dist/watch.js +2 -2
  43. package/package.json +3 -2
  44. package/skills/catalyst-github/SKILL.md +2 -2
  45. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  46. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  47. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  48. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  49. package/skills/catalyst-linear/SKILL.md +3 -3
  50. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  51. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  52. package/skills/catalyst-onboard/SKILL.md +5 -4
  53. package/skills/catalyst-onboard/references/local-sync.md +3 -1
  54. package/skills/run-this-project/SKILL.md +1 -1
  55. package/skills/unstick/SKILL.md +1 -1
  56. package/skills/what-needs-me/SKILL.md +1 -1
  57. package/skills/what-this-repo-needs/SKILL.md +1 -1
  58. package/skills/whats-happening/SKILL.md +2 -2
  59. package/skills/whats-happening/references/status-reply.md +1 -1
  60. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  61. package/vendor/settings/index.d.ts +4 -0
  62. package/vendor/settings/index.js +2937 -0
  63. package/vendor/settings/provenance.json +148 -0
@@ -0,0 +1,316 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { configPathFor, loadConfig, normalizeBaseUrl } from "./config.js";
4
+ import { verifyOnboardRoutes } from "./onboard-capabilities.js";
5
+ import { selectedOnboardTeam } from "./onboard-existing.js";
6
+ import { ownedFetch } from "./owned-fetch.js";
7
+ import { loadHttpSdk } from "./sdk.js";
8
+ import { fetchMe } from "./transport.js";
9
+ const path = "/api/v1/agent/team-workflow";
10
+ const slots = [
11
+ "dispatch",
12
+ "intake",
13
+ "research",
14
+ "plan",
15
+ "implement",
16
+ "remediate",
17
+ "verify",
18
+ "review",
19
+ "pr",
20
+ "done",
21
+ "canceled",
22
+ ];
23
+ const requiredTypes = {
24
+ dispatch: ["unstarted", "backlog"],
25
+ intake: ["unstarted", "backlog"],
26
+ pr: ["started"],
27
+ done: ["completed"],
28
+ canceled: ["canceled"],
29
+ };
30
+ const waiting = (reason) => ({
31
+ state: "waiting",
32
+ reason,
33
+ });
34
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
35
+ ? value
36
+ : null;
37
+ const id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value);
38
+ const integer = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
39
+ /** Existing mappings can satisfy this step by observation. This never means a preview was applied. */
40
+ export function observeOnboardWorkflow(value, team, now) {
41
+ const row = object(value), config = object(row?.config), readiness = object(row?.readiness);
42
+ if (!row ||
43
+ !config ||
44
+ !readiness ||
45
+ config.teamId !== team ||
46
+ readiness.teamId !== team ||
47
+ typeof config.mode !== "string" ||
48
+ !["mapped-existing", "adopted-recommended", "mixed"].includes(config.mode) ||
49
+ (config.gitAutomation !== "off" && config.gitAutomation !== "managed") ||
50
+ !integer(config.workflowRev) ||
51
+ !integer(readiness.workflowRev) ||
52
+ typeof readiness.status !== "string" ||
53
+ !["ready", "degraded", "blocked"].includes(readiness.status) ||
54
+ !integer(readiness.checkedAt) ||
55
+ readiness.checkedAt > now ||
56
+ now - readiness.checkedAt >= 300_000 ||
57
+ (row.stageSource !== "linear" && row.stageSource !== "mirror") ||
58
+ typeof row.mappingHash !== "string" ||
59
+ !/^[a-f0-9]{64}$/.test(row.mappingHash) ||
60
+ !Array.isArray(row.rows) ||
61
+ row.rows.length > slots.length ||
62
+ !Array.isArray(row.stages) ||
63
+ row.stages.length > 1_000 ||
64
+ !Array.isArray(readiness.checks) ||
65
+ readiness.checks.length > 100)
66
+ return null;
67
+ const stages = new Map();
68
+ for (const candidate of row.stages) {
69
+ const stage = object(candidate);
70
+ if (!stage ||
71
+ !id(stage.id) ||
72
+ stages.has(stage.id) ||
73
+ typeof stage.type !== "string" ||
74
+ ![
75
+ "triage",
76
+ "backlog",
77
+ "unstarted",
78
+ "started",
79
+ "completed",
80
+ "canceled",
81
+ ].includes(stage.type))
82
+ return null;
83
+ stages.set(stage.id, stage.type);
84
+ }
85
+ const mapping = new Map();
86
+ for (const candidate of row.rows) {
87
+ const item = object(candidate);
88
+ if (!item ||
89
+ typeof item.slot !== "string" ||
90
+ !slots.includes(item.slot) ||
91
+ mapping.has(item.slot) ||
92
+ (item.linearStateId !== null && !id(item.linearStateId)) ||
93
+ (item.source !== undefined &&
94
+ item.source !== "created" &&
95
+ item.source !== "matched" &&
96
+ item.source !== "chosen") ||
97
+ (item.linearStateId !== null &&
98
+ (item.stateStillExists !== true || !stages.has(item.linearStateId))))
99
+ return null;
100
+ mapping.set(item.slot, item.linearStateId);
101
+ }
102
+ for (const [slot, types] of Object.entries(requiredTypes)) {
103
+ const target = mapping.get(slot);
104
+ if (!target ||
105
+ !types.includes(stages.get(target) ?? ""))
106
+ return null;
107
+ }
108
+ const checks = new Map();
109
+ for (const candidate of readiness.checks) {
110
+ const check = object(candidate);
111
+ if (!check ||
112
+ typeof check.id !== "string" ||
113
+ checks.has(check.id) ||
114
+ (check.state !== "pass" &&
115
+ check.state !== "fail" &&
116
+ check.state !== "unknown"))
117
+ return null;
118
+ checks.set(check.id, check.state);
119
+ }
120
+ // Adoption does not establish coding accounts, automation rule safety or first-ticket readiness.
121
+ if ([
122
+ "team_visible",
123
+ "mapped_states_exist",
124
+ "mapping_total",
125
+ "types_compatible",
126
+ "labels_present",
127
+ ].some((check) => checks.get(check) !== "pass"))
128
+ return null;
129
+ return {
130
+ mappingHash: row.mappingHash,
131
+ mappingRevision: config.workflowRev,
132
+ readinessRevision: readiness.workflowRev,
133
+ checkedAt: readiness.checkedAt,
134
+ mappedSlots: [...mapping.values()].filter((value) => value !== null).length,
135
+ };
136
+ }
137
+ /** HTTP only, without rotating credentials or calling adopt/apply. Every entered fetch/body joins. */
138
+ export function onboardWorkflowVerificationAdapter(input = {}) {
139
+ return {
140
+ check: async (ctx, journal, external) => {
141
+ let cfg;
142
+ try {
143
+ cfg = loadConfig(ctx.home);
144
+ }
145
+ catch {
146
+ return waiting("workflow_identity_unverified");
147
+ }
148
+ const team = selectedOnboardTeam(journal);
149
+ if (!team ||
150
+ !cfg?.user ||
151
+ (cfg.user.role !== "owner" && cfg.user.role !== "admin") ||
152
+ cfg.account !== (journal.account ?? journal.tenant) ||
153
+ cfg.user.id !== journal.membershipId ||
154
+ !journal.baseUrl ||
155
+ normalizeBaseUrl(cfg.baseUrl) !== normalizeBaseUrl(journal.baseUrl))
156
+ return waiting("workflow_identity_unverified");
157
+ const origin = normalizeBaseUrl(cfg.baseUrl), person = cfg.user.id, role = cfg.user.role;
158
+ let url;
159
+ try {
160
+ url = new URL(origin);
161
+ }
162
+ catch {
163
+ return waiting("workflow_identity_unverified");
164
+ }
165
+ if (url.protocol !== "https:" ||
166
+ url.origin !== origin ||
167
+ url.username ||
168
+ url.password)
169
+ return waiting("workflow_identity_unverified");
170
+ const credential = cfg.key ||
171
+ (cfg.auth &&
172
+ Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() > 30_000
173
+ ? cfg.auth.accessToken
174
+ : undefined);
175
+ if (!credential)
176
+ return waiting("workflow_login_refresh_required");
177
+ const configWitness = () => createHash("sha256")
178
+ .update(readFileSync(configPathFor(ctx.home)))
179
+ .digest("hex");
180
+ let before;
181
+ try {
182
+ before = configWitness();
183
+ }
184
+ catch {
185
+ return waiting("workflow_identity_unverified");
186
+ }
187
+ const current = () => {
188
+ try {
189
+ const now = loadConfig(ctx.home);
190
+ return (now?.user?.id === person &&
191
+ now.user.role === role &&
192
+ now.account === cfg.account &&
193
+ normalizeBaseUrl(now.baseUrl) === origin &&
194
+ configWitness() === before &&
195
+ selectedOnboardTeam(journal) === team &&
196
+ (!!cfg.key ||
197
+ (!!cfg.auth &&
198
+ Date.parse(cfg.auth.expiresAt) > ctx.now().getTime())));
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ };
204
+ const deadline = new AbortController();
205
+ const timer = setTimeout(() => deadline.abort(), 30_000);
206
+ const signal = external
207
+ ? AbortSignal.any([external, deadline.signal])
208
+ : deadline.signal;
209
+ const owned = ownedFetch(ctx.fetch, signal, { maxBodyBytes: 524_288 });
210
+ const read = (request, init) => {
211
+ if (!current() || signal.aborted)
212
+ return Promise.reject(new Error("workflow_read_stopped"));
213
+ const requested = new URL(request instanceof Request ? request.url : String(request));
214
+ const method = init?.method ?? (request instanceof Request ? request.method : "GET");
215
+ if (method !== "GET" ||
216
+ requested.origin !== origin ||
217
+ requested.username ||
218
+ requested.password ||
219
+ requested.hash ||
220
+ !["/api/v1/me", "/api/v1/agent/contract", path].includes(requested.pathname) ||
221
+ (requested.pathname === path
222
+ ? requested.search !== `?team=${encodeURIComponent(team)}`
223
+ : requested.search !== ""))
224
+ return Promise.reject(new Error("workflow_read_scope"));
225
+ return owned.fetch(request, { ...init, signal, redirect: "error" });
226
+ };
227
+ const live = async () => {
228
+ const me = await fetchMe(origin, credential, read);
229
+ return (current() &&
230
+ !signal.aborted &&
231
+ me.account === cfg.account &&
232
+ me.user?.id === person &&
233
+ me.user.role === role);
234
+ };
235
+ const result = await (async () => {
236
+ try {
237
+ if (!(await live()))
238
+ return waiting("workflow_identity_unverified");
239
+ const support = await verifyOnboardRoutes({ ...ctx, fetch: read }, journal, [{ method: "GET", path }], signal);
240
+ if ("reason" in support)
241
+ return waiting(support.reason);
242
+ if (!current() || signal.aborted)
243
+ return waiting("workflow_identity_unverified");
244
+ const sdk = await loadHttpSdk();
245
+ if (!current() || signal.aborted)
246
+ return waiting("workflow_identity_unverified");
247
+ const client = sdk.createTenantClient({
248
+ baseUrl: origin,
249
+ key: credential,
250
+ fetch: read,
251
+ timeoutMs: 30_000,
252
+ now: () => ctx.now().getTime(),
253
+ });
254
+ const first = await client.teamWorkflow.get(team);
255
+ const observed = first.outcome === "ok"
256
+ ? observeOnboardWorkflow(first, team, ctx.now().getTime())
257
+ : null;
258
+ if (!observed || !current() || signal.aborted) {
259
+ input.message?.("Workflow adoption is waiting for a verified mapping or full-plan server support. Setup made no workflow change.");
260
+ return waiting("workflow_mapping_unverified");
261
+ }
262
+ if (!(await live()))
263
+ return waiting("workflow_identity_unverified");
264
+ const last = await client.teamWorkflow.get(team);
265
+ const verified = last.outcome === "ok"
266
+ ? observeOnboardWorkflow(last, team, ctx.now().getTime())
267
+ : null;
268
+ if (!verified ||
269
+ verified.mappingHash !== observed.mappingHash ||
270
+ verified.mappingRevision !== observed.mappingRevision ||
271
+ verified.readinessRevision !== observed.readinessRevision ||
272
+ !current() ||
273
+ signal.aborted)
274
+ return waiting("workflow_mapping_changed");
275
+ if (!(await live()))
276
+ return waiting("workflow_identity_unverified");
277
+ if (!current() || signal.aborted)
278
+ return waiting("workflow_identity_unverified");
279
+ return {
280
+ state: "done",
281
+ evidence: {
282
+ team,
283
+ revision: verified.readinessRevision,
284
+ mappingRevision: verified.mappingRevision,
285
+ checkedAt: verified.checkedAt,
286
+ mappingHash: verified.mappingHash,
287
+ count: verified.mappedSlots,
288
+ },
289
+ };
290
+ }
291
+ catch {
292
+ return waiting(external?.aborted ? "interrupted" : "workflow_unavailable");
293
+ }
294
+ finally {
295
+ try {
296
+ owned.abort(new Error("workflow_read_complete"));
297
+ await owned.settle();
298
+ }
299
+ finally {
300
+ clearTimeout(timer);
301
+ }
302
+ }
303
+ })();
304
+ if (!current())
305
+ return waiting("workflow_identity_unverified");
306
+ if (signal.aborted)
307
+ return waiting(external?.aborted ? "interrupted" : "workflow_unavailable");
308
+ if (result.state === "done") {
309
+ input.message?.("The selected team's existing workflow mapping and labels are verified. Setup made no workflow change.");
310
+ if (!current() || signal.aborted)
311
+ return waiting("workflow_identity_unverified");
312
+ }
313
+ return result;
314
+ },
315
+ };
316
+ }