@catalyst-cloud/cli 0.14.6 → 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.
- package/CHANGELOG.md +5 -0
- package/README.md +5 -3
- package/dist/args.js +51 -13
- package/dist/cli.js +58 -10
- package/dist/config.js +16 -3
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +243 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +203 -0
- package/dist/events-cloud-tail.js +134 -0
- package/dist/events-cloud.js +500 -0
- package/dist/events.js +13 -4
- package/dist/oauth.js +125 -33
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +147 -37
- package/dist/onboard-runtime.js +104 -9
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +99 -11
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard.js +359 -27
- package/dist/owned-fetch.js +182 -0
- package/dist/query.js +77 -27
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +2 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +3 -3
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +5 -4
- package/skills/catalyst-onboard/references/local-sync.md +3 -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 +2 -2
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
// is-it-mergeable.mjs — the three legs of merge evidence for one PR (checks, reviewer signal,
|
|
3
3
|
// unresolved threads) judged under the policy the tenant contract resolves for its repository, plus
|
|
4
4
|
// the prerequisites a queue applies before any leg counts (open, not a draft, no conflict). Every
|
|
5
|
-
// input comes from `catalyst query
|
|
5
|
+
// input comes from cloud-backed `catalyst query` and `contract`; --local-threads opts into `replica sql`.
|
|
6
6
|
// Exit 0 no leg is red, 1 a leg or prerequisite is red, 2 this machine is not connected.
|
|
7
7
|
import { parseJson, runCli, runCliOrExit } from "./lib/cli.mjs";
|
|
8
8
|
import { resolvePull, truthy } from "./lib/pull.mjs";
|
|
9
9
|
|
|
10
|
-
const HELP = `Usage: node scripts/is-it-mergeable.mjs <ticket | pr-node-id> [--json]
|
|
10
|
+
const HELP = `Usage: node scripts/is-it-mergeable.mjs <ticket | pr-node-id> [--json] [--local-threads]
|
|
11
11
|
|
|
12
12
|
Judges one pull request against the merge policy the tenant contract resolves for its repository:
|
|
13
13
|
checks every gating check at the head completed without failing (pending or absent is not green)
|
|
14
14
|
reviewer the reviewer signal the policy requires; the clean-pass reaction itself is not mirrored,
|
|
15
15
|
so this leg is reported as inconclusive rather than guessed, unless the policy waives it
|
|
16
|
-
threads
|
|
16
|
+
threads inconclusive when the cloud PR detail carries no thread count;
|
|
17
|
+
--local-threads opts into reading a local replica caught up to a probed cloud head
|
|
17
18
|
|
|
18
19
|
A red prerequisite (closed, draft, merge conflict) is also exit 1. Inconclusive legs never fail the
|
|
19
20
|
exit code; the cloud's own evaluator, which holds the reaction and the thread ancestry, is authoritative.
|
|
@@ -32,14 +33,20 @@ if (!target) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const { ticket, detail } = resolvePull(target);
|
|
35
|
-
const merge = parseJson(
|
|
36
|
+
const merge = parseJson(
|
|
37
|
+
runCliOrExit(["contract", "--path", "merge", "--json"]).stdout,
|
|
38
|
+
);
|
|
36
39
|
if (!merge || !Array.isArray(merge.policies)) {
|
|
37
|
-
console.error(
|
|
40
|
+
console.error(
|
|
41
|
+
"the contract's merge block did not parse; run: catalyst contract --refresh",
|
|
42
|
+
);
|
|
38
43
|
process.exit(1);
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
const repoId =
|
|
42
|
-
const repoRow = (merge.repositories ?? []).find(
|
|
46
|
+
const repoId = typeof detail.repo_id === "string" ? detail.repo_id : "";
|
|
47
|
+
const repoRow = (merge.repositories ?? []).find(
|
|
48
|
+
(r) => `${r.owner}/${r.name}`.toLowerCase() === repoId.toLowerCase(),
|
|
49
|
+
);
|
|
43
50
|
const policy = repoRow?.policy ?? merge.defaultPolicy;
|
|
44
51
|
const policySource = repoRow ? repoRow.policySource : "default";
|
|
45
52
|
const requiresReviewer = policy !== "checks-and-threads";
|
|
@@ -50,75 +57,233 @@ const add = (leg, state, line) => legs.push({ leg, state, line });
|
|
|
50
57
|
|
|
51
58
|
// ── prerequisites the queue applies before any leg counts ──────────────────────────────────────
|
|
52
59
|
if (truthy(detail.merged)) {
|
|
53
|
-
add(
|
|
60
|
+
add(
|
|
61
|
+
"state",
|
|
62
|
+
"pass",
|
|
63
|
+
`already merged${detail.merged_at ? ` at ${new Date(Number(detail.merged_at)).toISOString()}` : ""}; nothing left to judge`,
|
|
64
|
+
);
|
|
54
65
|
} else {
|
|
55
66
|
const state = String(detail.state ?? "").toLowerCase();
|
|
56
|
-
if (state && state !== "open")
|
|
57
|
-
|
|
67
|
+
if (state && state !== "open")
|
|
68
|
+
add("state", "fail", `the PR is ${state}, not open`);
|
|
69
|
+
else if (truthy(detail.draft))
|
|
70
|
+
add(
|
|
71
|
+
"state",
|
|
72
|
+
"fail",
|
|
73
|
+
"the PR is still a draft; the pr phase marks it ready for review, a queue never takes a draft",
|
|
74
|
+
);
|
|
58
75
|
else add("state", "pass", "open and ready for review");
|
|
59
76
|
const ms = String(detail.mergeable_state ?? "").toLowerCase();
|
|
60
|
-
if (ms === "dirty" || detail.mergeable === false || detail.mergeable === 0)
|
|
77
|
+
if (ms === "dirty" || detail.mergeable === false || detail.mergeable === 0)
|
|
78
|
+
add(
|
|
79
|
+
"conflict",
|
|
80
|
+
"fail",
|
|
81
|
+
`GitHub reports a merge conflict with the base branch (mergeable_state=${ms || "unknown"}); a remediate round rebases it`,
|
|
82
|
+
);
|
|
61
83
|
else if (ms) add("conflict", "pass", `GitHub reports mergeable_state=${ms}`);
|
|
62
|
-
else
|
|
84
|
+
else
|
|
85
|
+
add(
|
|
86
|
+
"conflict",
|
|
87
|
+
"inconclusive",
|
|
88
|
+
"GitHub has not reported a mergeable state for this head yet",
|
|
89
|
+
);
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
// ── leg 1: checks ───────────────────────────────────────────────────────────────────────────────
|
|
66
93
|
const checks = Array.isArray(detail.checks) ? detail.checks : [];
|
|
67
|
-
const required = Array.isArray(merge.cloudRemediateRequiredChecks)
|
|
68
|
-
|
|
94
|
+
const required = Array.isArray(merge.cloudRemediateRequiredChecks)
|
|
95
|
+
? merge.cloudRemediateRequiredChecks
|
|
96
|
+
: [];
|
|
97
|
+
const notQueue = checks.filter(
|
|
98
|
+
(c) => !/mergify|^queue:|^summary$/i.test(String(c.name ?? "")),
|
|
99
|
+
);
|
|
69
100
|
const gating = notQueue.filter((c) => required.includes(String(c.name)));
|
|
70
101
|
const judged = gating.length > 0 ? gating : notQueue;
|
|
71
102
|
const ok = new Set(["success", "neutral", "skipped"]);
|
|
72
|
-
const red = judged.filter(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
const red = judged.filter(
|
|
104
|
+
(c) =>
|
|
105
|
+
String(c.status).toLowerCase() === "completed" &&
|
|
106
|
+
!ok.has(String(c.conclusion).toLowerCase()),
|
|
107
|
+
);
|
|
108
|
+
const pending = judged.filter(
|
|
109
|
+
(c) => String(c.status).toLowerCase() !== "completed",
|
|
110
|
+
);
|
|
111
|
+
const statuses = Array.isArray(detail.commit_statuses)
|
|
112
|
+
? detail.commit_statuses
|
|
113
|
+
: [];
|
|
114
|
+
const redStatuses = statuses.filter((s) =>
|
|
115
|
+
["failure", "error"].includes(String(s.state).toLowerCase()),
|
|
116
|
+
);
|
|
117
|
+
const scope =
|
|
118
|
+
gating.length > 0
|
|
119
|
+
? `the ${gating.length} required check(s) the contract names`
|
|
120
|
+
: `every mirrored check (none of the contract's required names is present at this head)`;
|
|
121
|
+
if (judged.length === 0 && redStatuses.length === 0)
|
|
122
|
+
add(
|
|
123
|
+
"checks",
|
|
124
|
+
"inconclusive",
|
|
125
|
+
"no check has reported at this head; an unrun set is unknown, never green",
|
|
126
|
+
);
|
|
127
|
+
else if (red.length > 0 || redStatuses.length > 0)
|
|
128
|
+
add(
|
|
129
|
+
"checks",
|
|
130
|
+
"fail",
|
|
131
|
+
`red: ${[...red.map((c) => `${c.name} (${c.conclusion})`), ...redStatuses.map((s) => `${s.context} (${s.state})`)].join(", ")}`,
|
|
132
|
+
);
|
|
133
|
+
else if (pending.length > 0)
|
|
134
|
+
add(
|
|
135
|
+
"checks",
|
|
136
|
+
"inconclusive",
|
|
137
|
+
`still running: ${pending.map((c) => c.name).join(", ")} — judged over ${scope}`,
|
|
138
|
+
);
|
|
80
139
|
else add("checks", "pass", `green over ${scope}`);
|
|
81
140
|
|
|
82
141
|
// ── leg 2: the reviewer signal ─────────────────────────────────────────────────────────────────
|
|
83
142
|
const reviews = Array.isArray(detail.reviews) ? detail.reviews : [];
|
|
84
143
|
const login = String(merge.reviewerLogin ?? "");
|
|
85
|
-
const byReviewer = reviews.filter(
|
|
86
|
-
|
|
144
|
+
const byReviewer = reviews.filter(
|
|
145
|
+
(r) =>
|
|
146
|
+
String(r.reviewer_name ?? "") === login ||
|
|
147
|
+
String(r.reviewer_id ?? "") === `github:${login}`,
|
|
148
|
+
);
|
|
149
|
+
if (!requiresReviewer)
|
|
150
|
+
add("reviewer", "pass", `policy ${policy} needs no reviewer signal`);
|
|
87
151
|
else if (byReviewer.length > 0) {
|
|
88
|
-
const states = [
|
|
89
|
-
|
|
90
|
-
|
|
152
|
+
const states = [
|
|
153
|
+
...new Set(byReviewer.map((r) => String(r.state ?? "?"))),
|
|
154
|
+
].join(", ");
|
|
155
|
+
add(
|
|
156
|
+
"reviewer",
|
|
157
|
+
"inconclusive",
|
|
158
|
+
`${login} left ${byReviewer.length} review object(s) (${states}); whether they stand at the current head or an earlier one, and whether a clean-pass reaction followed, is not mirrored${acceptsEarlierHead ? " — under this policy, findings on an earlier head plus green checks and zero unresolved threads are accepted" : " — this policy requires a clean pass at the current head"}`,
|
|
159
|
+
);
|
|
160
|
+
} else
|
|
161
|
+
add(
|
|
162
|
+
"reviewer",
|
|
163
|
+
"inconclusive",
|
|
164
|
+
`no review object from ${login} is mirrored; a clean pass is a reaction, which this read cannot see`,
|
|
165
|
+
);
|
|
91
166
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
167
|
+
// The default cloud read requires no local writer or database. Thread ancestry remains the
|
|
168
|
+
// cloud evaluator's responsibility; absence of thread evidence never becomes a green leg.
|
|
169
|
+
const localThreads = args.includes("--local-threads");
|
|
170
|
+
const replicaResult = localThreads
|
|
171
|
+
? runCli(["replica", "status", "--probe", "--json"])
|
|
172
|
+
: null;
|
|
173
|
+
const replica =
|
|
174
|
+
replicaResult?.code === 0 ? parseJson(replicaResult.stdout) : null;
|
|
175
|
+
if (
|
|
176
|
+
localThreads &&
|
|
177
|
+
replica?.verdict === "fresh" &&
|
|
178
|
+
Number.isSafeInteger(replica.cursor) &&
|
|
179
|
+
replica.cursor >= 0 &&
|
|
180
|
+
replica.cursor === replica.head
|
|
181
|
+
) {
|
|
182
|
+
const num = detail.number;
|
|
183
|
+
if (
|
|
184
|
+
typeof num !== "number" ||
|
|
185
|
+
!Number.isSafeInteger(num) ||
|
|
186
|
+
num <= 0 ||
|
|
187
|
+
!repoId ||
|
|
188
|
+
repoId !== repoId.trim() ||
|
|
189
|
+
repoId.length > 512 ||
|
|
190
|
+
/[\u0000-\u001f\u007f-\u009f]/.test(repoId)
|
|
191
|
+
) {
|
|
192
|
+
add(
|
|
193
|
+
"threads",
|
|
194
|
+
"inconclusive",
|
|
195
|
+
"the PR identity is incomplete; local thread evidence could not be read",
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
const esc = repoId.replace(/'/g, "''");
|
|
199
|
+
const sql = `select resolved, count(*) as n from pr_review_threads where repo_id = '${esc}' and pr_number = ${num} group by resolved`;
|
|
200
|
+
const result = runCli(["replica", "sql", sql, "--json"]);
|
|
201
|
+
const rows = parseJson(result.stdout);
|
|
202
|
+
if (
|
|
203
|
+
result.code !== 0 ||
|
|
204
|
+
!Array.isArray(rows) ||
|
|
205
|
+
rows.some(
|
|
206
|
+
(row) =>
|
|
207
|
+
!row ||
|
|
208
|
+
![0, 1, null].includes(row.resolved) ||
|
|
209
|
+
typeof row.n !== "number" ||
|
|
210
|
+
!Number.isSafeInteger(row.n) ||
|
|
211
|
+
row.n < 0,
|
|
212
|
+
)
|
|
213
|
+
) {
|
|
214
|
+
add(
|
|
215
|
+
"threads",
|
|
216
|
+
"inconclusive",
|
|
217
|
+
"the optional local thread read could not be verified; the cloud evaluator decides",
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
const count = (want) =>
|
|
221
|
+
rows
|
|
222
|
+
.filter((r) =>
|
|
223
|
+
want === null
|
|
224
|
+
? r.resolved === null || r.resolved === undefined
|
|
225
|
+
: r.resolved === want,
|
|
226
|
+
)
|
|
227
|
+
.reduce((a, r) => a + Number(r.n ?? 0), 0);
|
|
228
|
+
const unresolved = count(0);
|
|
229
|
+
const unknown = count(null);
|
|
230
|
+
if (unresolved > 0)
|
|
231
|
+
add("threads", "fail", `${unresolved} unresolved review thread(s)`);
|
|
232
|
+
else if (unknown > 0)
|
|
233
|
+
add(
|
|
234
|
+
"threads",
|
|
235
|
+
"inconclusive",
|
|
236
|
+
`${unknown} thread(s) with no resolved flag mirrored`,
|
|
237
|
+
);
|
|
238
|
+
else
|
|
239
|
+
add(
|
|
240
|
+
"threads",
|
|
241
|
+
"pass",
|
|
242
|
+
`${count(1)} thread(s), all resolved (ancestry against force-pushes is judged by the cloud, not here)`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
105
246
|
} else {
|
|
106
|
-
add(
|
|
247
|
+
add(
|
|
248
|
+
"threads",
|
|
249
|
+
"inconclusive",
|
|
250
|
+
localThreads
|
|
251
|
+
? "the optional local replica has not proven that it is caught up to the cloud head; the cloud evaluator decides"
|
|
252
|
+
: "the cloud PR detail carries no thread count; the cloud evaluator decides",
|
|
253
|
+
);
|
|
107
254
|
}
|
|
108
255
|
|
|
109
256
|
const failed = legs.filter((l) => l.state === "fail");
|
|
110
257
|
const open = legs.filter((l) => l.state === "inconclusive");
|
|
111
258
|
let verdict;
|
|
112
|
-
if (failed.length > 0)
|
|
113
|
-
|
|
259
|
+
if (failed.length > 0)
|
|
260
|
+
verdict = `NOT MERGEABLE: ${failed.map((l) => l.leg).join(", ")}`;
|
|
261
|
+
else if (open.length > 0)
|
|
262
|
+
verdict = `NOT PROVEN MERGEABLE from this read (${open.map((l) => l.leg).join(", ")} inconclusive); the cloud's evaluator decides`;
|
|
114
263
|
else verdict = "MERGEABLE as far as this read can see";
|
|
115
264
|
|
|
116
265
|
const header = `${repoId}#${detail.number ?? "?"}${ticket ? ` (${ticket})` : ""} head ${String(detail.head_sha ?? "").slice(0, 12)} — policy ${policy} (${policySource})`;
|
|
117
266
|
if (json) {
|
|
118
|
-
console.log(
|
|
267
|
+
console.log(
|
|
268
|
+
JSON.stringify({
|
|
269
|
+
pr: {
|
|
270
|
+
repo_id: repoId,
|
|
271
|
+
number: detail.number,
|
|
272
|
+
node_id: detail.node_id,
|
|
273
|
+
ticket,
|
|
274
|
+
head_sha: detail.head_sha,
|
|
275
|
+
},
|
|
276
|
+
policy: { name: policy, source: policySource },
|
|
277
|
+
legs,
|
|
278
|
+
verdict,
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
119
281
|
} else {
|
|
120
282
|
console.log(header);
|
|
121
|
-
for (const l of legs)
|
|
283
|
+
for (const l of legs)
|
|
284
|
+
console.log(
|
|
285
|
+
`${l.state === "pass" ? "ok " : l.state === "fail" ? "FAIL" : "? "} ${l.leg}: ${l.line}`,
|
|
286
|
+
);
|
|
122
287
|
console.log(verdict);
|
|
123
288
|
}
|
|
124
289
|
process.exit(failed.length > 0 ? 1 : 0);
|
|
@@ -12,7 +12,7 @@ const HELP = `Usage: node scripts/read-pr.mjs <ticket | pr-node-id> [--all] [--j
|
|
|
12
12
|
--json print the raw detail document instead of the summary
|
|
13
13
|
|
|
14
14
|
Exit 0 shown, 1 not found or no PR yet, 2 this machine is not connected to a tenant.
|
|
15
|
-
The first stderr line names the source the CLI read from (
|
|
15
|
+
The first stderr line names the source the CLI read from (the API by default, or an explicitly selected replica).`;
|
|
16
16
|
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
18
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
@@ -52,8 +52,15 @@ if (all) {
|
|
|
52
52
|
process.exit(1);
|
|
53
53
|
}
|
|
54
54
|
for (const p of pulls) {
|
|
55
|
-
const flags = [
|
|
56
|
-
|
|
55
|
+
const flags = [
|
|
56
|
+
truthy(p.draft) ? "draft" : null,
|
|
57
|
+
truthy(p.merged) ? "merged" : null,
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(" ");
|
|
61
|
+
console.log(
|
|
62
|
+
`${fmt(p.repo_id)}#${fmt(p.number)} ${fmt(p.state)}${flags ? ` ${flags}` : ""} ${fmt(p.title, "")} [${fmt(p.node_id, "")}]`,
|
|
63
|
+
);
|
|
57
64
|
}
|
|
58
65
|
process.exit(0);
|
|
59
66
|
}
|
|
@@ -66,23 +73,44 @@ if (json) {
|
|
|
66
73
|
|
|
67
74
|
const checks = Array.isArray(detail.checks) ? detail.checks : [];
|
|
68
75
|
const reviews = Array.isArray(detail.reviews) ? detail.reviews : [];
|
|
69
|
-
const statuses = Array.isArray(detail.commit_statuses)
|
|
70
|
-
|
|
76
|
+
const statuses = Array.isArray(detail.commit_statuses)
|
|
77
|
+
? detail.commit_statuses
|
|
78
|
+
: [];
|
|
79
|
+
const state = [
|
|
80
|
+
fmt(detail.state),
|
|
81
|
+
truthy(detail.draft) ? "draft" : null,
|
|
82
|
+
truthy(detail.merged) ? `merged ${when(detail.merged_at)}` : null,
|
|
83
|
+
]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join(", ");
|
|
71
86
|
|
|
72
|
-
console.log(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.log(
|
|
76
|
-
|
|
87
|
+
console.log(
|
|
88
|
+
`${fmt(detail.repo_id)}#${fmt(detail.number)} ${fmt(detail.title, "(no title yet)")}`,
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
`state: ${state} by ${fmt(detail.author_login)} opened ${when(detail.created_at)} updated ${when(detail.updated_at)}`,
|
|
92
|
+
);
|
|
93
|
+
console.log(
|
|
94
|
+
`branch: ${fmt(detail.head_ref)} → ${fmt(detail.base_ref)} head ${fmt(detail.head_sha).slice(0, 12)}`,
|
|
95
|
+
);
|
|
96
|
+
console.log(
|
|
97
|
+
`ticket: ${fmt(ticket ?? detail.linear_issue_identifier, "none named")}${detail.linked_issue_state ? ` (stage: ${detail.linked_issue_state})` : ""}`,
|
|
98
|
+
);
|
|
99
|
+
console.log(
|
|
100
|
+
`github says: mergeable=${fmt(detail.mergeable, "unknown")} state=${fmt(detail.mergeable_state, "unknown")} auto-merge=${truthy(detail.auto_merge) ? "on" : "off"}`,
|
|
101
|
+
);
|
|
77
102
|
if (detail.blocked_on_ask && typeof detail.blocked_on_ask === "object") {
|
|
78
103
|
const b = detail.blocked_on_ask;
|
|
79
|
-
console.log(
|
|
104
|
+
console.log(
|
|
105
|
+
`blocked on an ask: ${fmt(b.identifier ?? b.id, "")} ${fmt(b.title, "")}`.trim(),
|
|
106
|
+
);
|
|
80
107
|
}
|
|
81
108
|
console.log(`node id: ${fmt(detail.node_id)}`);
|
|
82
109
|
|
|
83
110
|
console.log(`checks (${checks.length}):`);
|
|
84
111
|
if (checks.length === 0) console.log(" none reported at this head yet");
|
|
85
|
-
for (const c of checks)
|
|
112
|
+
for (const c of checks)
|
|
113
|
+
console.log(` ${fmt(c.status)}/${fmt(c.conclusion, "-")} ${fmt(c.name)}`);
|
|
86
114
|
|
|
87
115
|
if (statuses.length > 0) {
|
|
88
116
|
console.log(`commit statuses (${statuses.length}):`);
|
|
@@ -90,8 +118,16 @@ if (statuses.length > 0) {
|
|
|
90
118
|
}
|
|
91
119
|
|
|
92
120
|
console.log(`reviews (${reviews.length}):`);
|
|
93
|
-
if (reviews.length === 0)
|
|
94
|
-
|
|
121
|
+
if (reviews.length === 0)
|
|
122
|
+
console.log(
|
|
123
|
+
" none mirrored; a reviewer's clean pass is a reaction, which this read does not carry",
|
|
124
|
+
);
|
|
125
|
+
for (const r of reviews)
|
|
126
|
+
console.log(
|
|
127
|
+
` ${fmt(r.state)} ${fmt(r.reviewer_name, fmt(r.reviewer_id))} ${when(r.submitted_at)}`,
|
|
128
|
+
);
|
|
95
129
|
|
|
96
|
-
console.log(
|
|
130
|
+
console.log(
|
|
131
|
+
"not mirrored here: PR labels (holds, queue attestation) and the reviewer's reaction — see references/what-a-pr-accumulates.md",
|
|
132
|
+
);
|
|
97
133
|
process.exit(0);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: catalyst-linear
|
|
3
3
|
description: >-
|
|
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
|
|
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 cloud API by default, with local replica reads available as an explicit opt-in, 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.7 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst Linear
|
|
10
10
|
|
|
11
|
-
You read tickets and write to them in the person's cloud account, as Catalyst. Reads come from the
|
|
11
|
+
You read tickets and write to them in the person's cloud account, as Catalyst. Reads come from the cloud API by default and name their source. A person may explicitly choose a local replica read; setup does not require a replica. Writes go out as the app actor, with routes, stage ids, label ids and the bookkeeping marker resolved from the contract by the `catalyst` CLI.
|
|
12
12
|
|
|
13
13
|
## Run first
|
|
14
14
|
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
Every `query` verb, and therefore `node scripts/read-ticket.mjs` and `node scripts/search.mjs`, prints one stderr line before the answer:
|
|
6
6
|
|
|
7
|
-
- `source: replica (
|
|
8
|
-
- `source: api (
|
|
7
|
+
- `source: replica (--source replica)`: the person explicitly selected the optional local replica.
|
|
8
|
+
- `source: api (cloud reads by default)`: the answer came from the cloud API; no local replica is needed.
|
|
9
9
|
- `source: api (<verb> is api-only)`: search, cycles, pull detail and change feeds never come from the replica.
|
|
10
10
|
|
|
11
11
|
Quote the source when a fact's freshness matters ("as of the replica at cursor N", "from the API just now").
|
|
12
12
|
|
|
13
|
-
`--source replica` or `--source api` forces one. Forcing the replica when it is absent
|
|
13
|
+
`--source replica` or `--source api` forces one. Forcing the replica when it is absent is refused. No offline-browsing promise is made.
|
|
14
14
|
|
|
15
15
|
## Then the row
|
|
16
16
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// read-ticket.mjs — one ticket with its comments, relations, labels, linked pull requests and agent
|
|
3
|
-
// sessions inline, from the
|
|
3
|
+
// sessions inline, from the cloud API by default, or an explicitly chosen local replica. The source line the
|
|
4
4
|
// CLI prints on stderr is the freshness verdict; it is always shown.
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
parseFlags,
|
|
7
|
+
parseJson,
|
|
8
|
+
relayStderr,
|
|
9
|
+
runCli,
|
|
10
|
+
usage,
|
|
11
|
+
wantsHelp,
|
|
12
|
+
} from "./lib/cli.mjs";
|
|
6
13
|
|
|
7
14
|
const HELP = `Usage: node scripts/read-ticket.mjs <ticket> [--comments] [--source replica|api] [--json]
|
|
8
15
|
|
|
@@ -10,11 +17,10 @@ Reads one ticket record. Wraps: catalyst query issue.
|
|
|
10
17
|
|
|
11
18
|
<ticket> the Linear identifier, e.g. KEY-123
|
|
12
19
|
--comments also print every comment (id, author, time, body)
|
|
13
|
-
--source replica|api
|
|
20
|
+
--source replica|api choose one source; default is the cloud API
|
|
14
21
|
--json print the full record as JSON
|
|
15
22
|
|
|
16
|
-
The first stderr line names the source ("source:
|
|
17
|
-
stale|absent|not configured)"); quote it when the freshness of the answer matters.
|
|
23
|
+
The first stderr line names the source ("source: api (cloud reads by default)" or "source: replica (--source replica)"); quote it when the freshness of the answer matters.
|
|
18
24
|
|
|
19
25
|
Exit 0 found, 1 not found or a usage error, 2 when this machine is not connected to a tenant or the
|
|
20
26
|
cloud refused the read (the line says which).`;
|
|
@@ -24,7 +30,10 @@ if (wantsHelp(argv)) {
|
|
|
24
30
|
console.log(HELP);
|
|
25
31
|
process.exit(0);
|
|
26
32
|
}
|
|
27
|
-
const { flags, positionals } = parseFlags(argv, {
|
|
33
|
+
const { flags, positionals } = parseFlags(argv, {
|
|
34
|
+
bool: ["comments", "json"],
|
|
35
|
+
value: ["source"],
|
|
36
|
+
});
|
|
28
37
|
const ticket = positionals[0];
|
|
29
38
|
if (!ticket) usage("read-ticket needs a ticket identifier (see --help)");
|
|
30
39
|
|
|
@@ -49,9 +58,16 @@ if (flags.json) {
|
|
|
49
58
|
|
|
50
59
|
const line = (label, value) => {
|
|
51
60
|
if (value === undefined || value === null || value === "") return;
|
|
52
|
-
console.log(
|
|
61
|
+
console.log(
|
|
62
|
+
`${label}: ${typeof value === "string" ? value : JSON.stringify(value)}`,
|
|
63
|
+
);
|
|
53
64
|
};
|
|
54
|
-
const names = (list, key) =>
|
|
65
|
+
const names = (list, key) =>
|
|
66
|
+
Array.isArray(list)
|
|
67
|
+
? list.map((x) =>
|
|
68
|
+
x && typeof x === "object" ? (x[key] ?? JSON.stringify(x)) : String(x),
|
|
69
|
+
)
|
|
70
|
+
: [];
|
|
55
71
|
|
|
56
72
|
line("ticket", row.identifier ?? row.id);
|
|
57
73
|
line("title", row.title);
|
|
@@ -68,13 +84,20 @@ line("url", row.url);
|
|
|
68
84
|
line("updated", row.updated_at);
|
|
69
85
|
if (Array.isArray(row.relations) && row.relations.length) {
|
|
70
86
|
console.log(`relations (${row.relations.length}):`);
|
|
71
|
-
for (const rel of row.relations)
|
|
87
|
+
for (const rel of row.relations)
|
|
88
|
+
console.log(
|
|
89
|
+
` ${rel.type ?? "?"}: ${rel.issue_identifier ?? ""} -> ${rel.related_identifier ?? JSON.stringify(rel)}`,
|
|
90
|
+
);
|
|
72
91
|
}
|
|
73
92
|
if (Array.isArray(row.linked_pulls) && row.linked_pulls.length) {
|
|
74
93
|
console.log(`linked pull requests (${row.linked_pulls.length}):`);
|
|
75
|
-
for (const pr of row.linked_pulls)
|
|
94
|
+
for (const pr of row.linked_pulls)
|
|
95
|
+
console.log(
|
|
96
|
+
` #${pr.number ?? "?"} ${pr.repo_id ?? ""} [${pr.node_id ?? ""}]`,
|
|
97
|
+
);
|
|
76
98
|
}
|
|
77
|
-
if (Array.isArray(row.agent_sessions))
|
|
99
|
+
if (Array.isArray(row.agent_sessions))
|
|
100
|
+
line("agent sessions", row.agent_sessions.length);
|
|
78
101
|
if (Array.isArray(row.activity)) line("activity entries", row.activity.length);
|
|
79
102
|
if (Array.isArray(row.comments)) line("comments", row.comments.length);
|
|
80
103
|
if (row.description) {
|
|
@@ -87,7 +110,9 @@ if (flags.comments && Array.isArray(row.comments)) {
|
|
|
87
110
|
const who = c.author_name ?? c.author_id ?? "?";
|
|
88
111
|
const bot = c.is_bot ? " [bot]" : "";
|
|
89
112
|
const parent = c.parent_id ? ` reply-to ${c.parent_id}` : "";
|
|
90
|
-
console.log(
|
|
113
|
+
console.log(
|
|
114
|
+
`-- ${c.id} · ${who}${bot} · ${c.updated_at ?? c.created_at ?? ""}${parent}`,
|
|
115
|
+
);
|
|
91
116
|
console.log(String(c.body ?? "").trimEnd());
|
|
92
117
|
}
|
|
93
118
|
}
|
|
@@ -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.7 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Onboard me
|
|
10
10
|
|
|
@@ -14,10 +14,11 @@ Guide setup through the same `catalyst onboard` flow that the installer runs. Th
|
|
|
14
14
|
|
|
15
15
|
1. Run `catalyst capabilities --json`. If its `onboard` entry is available and advertises `bootstrapHandoff: true`, use this path. If absent, use the older step-by-step references below and name the CLI update needed for the continuous flow.
|
|
16
16
|
2. For a requested setup, run `catalyst onboard`. To inspect first, run `catalyst onboard --dry-run --json`. Resume with `catalyst onboard`; the command rechecks saved results. Do not ask the person to confirm completed browser steps or add a separate approval before the command's plan question.
|
|
17
|
-
3.
|
|
18
|
-
4.
|
|
17
|
+
3. In a terminal, the command shows the verified person, workspace and plan before approval. Without terminal input, or with `--json`, use `--yes` only when the person has accepted the displayed defaults; otherwise setup waits with exit 11 and keeps the saved connection and receipt. JSON keeps progress, identity, the plan and quota disclosure on stderr and returns one result on stdout. A supplied key needs `catalyst login` before onboarding; browser sign-in does not replace it.
|
|
18
|
+
4. For the current onboarding checks, run `catalyst ready --onboarding --json`. A failed check needs attention. An unknown check needs evidence. Report observed project work separately from incomplete setup; an unknown setup check does not prove work stopped.
|
|
19
|
+
5. Exit 0 completes the requested scope. Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. A successful `--only` step does not mean onboarding is complete. Keep the command's safe fix and supported resume command in your reply.
|
|
19
20
|
|
|
20
|
-
Use cloud reads for ordinary workstation setup checks. Offer local sync for local SQL
|
|
21
|
+
Use cloud reads for ordinary workstation setup checks. Offer local sync for local SQL or sustained local reads inside the plan review. Local sync does not promise offline browsing. It is optional; do not make an absent replica a setup failure. Browser approval belongs to the person. Missing bearer capabilities stay unfinished; never call a session-only route from the CLI or invent a command.
|
|
21
22
|
|
|
22
23
|
Choose an existing Linear team explicitly in the command's team picker or with `catalyst onboard --team <team ID or unique key>`. Unattended setup does not pick the first team, even when only one is visible. Resume rechecks the saved selection; final readiness reads only that team. A team used to verify the workspace grant is not a project selection. Setup does not create a Linear team. Native sign-in waits for up to ten minutes across all device-code rounds; a timeout saves progress for the next run.
|
|
23
24
|
|
|
@@ -4,7 +4,9 @@ The replica is an on-machine SQLite copy of the account for cheap repeated reads
|
|
|
4
4
|
|
|
5
5
|
## The replica's exit codes
|
|
6
6
|
|
|
7
|
-
`catalyst replica status` needs no network. Exit 0 is fresh (a live writer, a heartbeat under 15 seconds, a cursor),
|
|
7
|
+
`catalyst replica status` needs no network. Exit 0 is fresh (a live writer, a heartbeat under 15 seconds, a cursor), exit 1 is stale and 3 is absent. Skills use cloud reads by default; a fresh replica is used only with explicit `--source replica`. `catalyst replica start --detach` restarts a stale writer after local sync is selected. Exit 2 is not connected (`references/connecting-this-machine.md`). `--probe` adds one network call comparing the local cursor with the cloud head. Every read verb names its source on standard error.
|
|
8
|
+
|
|
9
|
+
Local sync is opt-in. `catalyst replica status --json` carries `configured`, which is true only when this machine opted in: `CATALYST_REPLICA_DB` is set, or the machine paths file declares `replicaDb`. A machine that never opted in prints `replica: not configured (cloud reads; local sync is opt-in)` and reads the cloud. A machine with `configured: true` and exit 1 or 3 opted in and its writer is down.
|
|
8
10
|
|
|
9
11
|
## Checking and starting both
|
|
10
12
|
|
|
@@ -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.7 — 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.7 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Unstick
|
|
10
10
|
|