@catalyst-cloud/cli 0.14.6 → 0.14.8
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 +14 -0
- package/README.md +7 -5
- package/dist/args.js +68 -18
- package/dist/browser.js +37 -3
- package/dist/cli.js +58 -12
- package/dist/config.js +17 -5
- package/dist/consent-browser.js +58 -0
- 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 +250 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +205 -0
- package/dist/events-cloud-tail.js +146 -0
- package/dist/events-cloud.js +619 -0
- package/dist/events.js +186 -31
- package/dist/oauth.js +129 -34
- 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-github.js +14 -3
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-next.js +139 -0
- package/dist/onboard-personal.js +14 -3
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +146 -37
- package/dist/onboard-runtime.js +108 -13
- 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 +85 -59
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard-workspace.js +14 -3
- package/dist/onboard.js +422 -39
- package/dist/owned-fetch.js +182 -0
- package/dist/pagination.js +13 -1
- package/dist/query.js +104 -31
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/repository-id.js +5 -0
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +4 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/local-replica.md +78 -0
- package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
- 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 +5 -3
- package/skills/catalyst-linear/references/local-replica.md +91 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +6 -4
- package/skills/catalyst-onboard/references/local-replica.md +40 -0
- package/skills/catalyst-onboard/references/local-sync.md +8 -4
- package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +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 +4 -2
- package/skills/whats-happening/references/local-replica.md +51 -0
- package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
- 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.8 — 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
|
|
|
@@ -30,6 +30,8 @@ Exit codes: 0 done, 1 not found or a usage error, 2 this machine is not connecte
|
|
|
30
30
|
| "what did Catalyst write on this ticket?", a comment shape, a label, the marker, the 👀 | `references/what-a-ticket-accumulates.md` |
|
|
31
31
|
| before any read you will report on; "is this current?"; citing; searching | `references/reading-a-ticket.md` |
|
|
32
32
|
| before any comment, move, label or new ticket; identity, budget, slots, ids | `references/writing-to-linear.md` |
|
|
33
|
+
| which command answers a ticket question from the cloud: stage, last phase, PR, what changed | `references/reading-from-the-cloud.md` |
|
|
34
|
+
| local SQL, only when `catalyst replica status --json` reports `configured: true` | `references/local-replica.md` |
|
|
33
35
|
|
|
34
36
|
## Rules
|
|
35
37
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Reading a ticket from the local replica (opt-in)
|
|
2
|
+
|
|
3
|
+
Load this only when `catalyst replica status --json` reports `configured: true`. On any other machine, read from the cloud (`references/reading-from-the-cloud.md`) and do not mention the replica.
|
|
4
|
+
|
|
5
|
+
The replica is an on-machine SQLite copy of the account. It exists only when the person opted in to local sync. It is for ad hoc SQL and many repeated reads. It is never required, and the cloud read stays the default.
|
|
6
|
+
|
|
7
|
+
## Check it is caught up first
|
|
8
|
+
|
|
9
|
+
Run `catalyst replica status --probe --json` before every local read.
|
|
10
|
+
|
|
11
|
+
- Read locally only when `verdict` is `fresh` and `cursor` equals `head`. That proves the copy is caught up with the cloud at that moment.
|
|
12
|
+
- `fresh` without the probe proves a live writer, not a caught-up copy.
|
|
13
|
+
- `stale` (exit 1) or `absent` (exit 3) with `configured: true` means the person opted in and the writer is down. Answer from the cloud and say the local copy is behind. `catalyst replica start --detach` restarts it, but only start it when the person asks.
|
|
14
|
+
|
|
15
|
+
Quote the cursor with any local answer, for example "from the local replica at cursor 4810".
|
|
16
|
+
|
|
17
|
+
## Two ways to read it
|
|
18
|
+
|
|
19
|
+
- `catalyst query issue <KEY-123> --source replica` reads the ticket from the local copy. A replica built by an older SDK can lack fields the cloud read has, such as `linked_pulls`. `--source replica` on an absent replica is refused, never silently answered from the API.
|
|
20
|
+
- `catalyst replica sql "<one SELECT>" --json` runs one read-only `SELECT` and prints rows. Anything other than a single `SELECT` is refused.
|
|
21
|
+
|
|
22
|
+
`catalyst replica schema` prints every table with its columns, and `catalyst replica schema <table>` prints one. Read the columns before writing SQL against a table this page does not cover.
|
|
23
|
+
|
|
24
|
+
## What the tables hold
|
|
25
|
+
|
|
26
|
+
Timestamps are milliseconds since the epoch. A row with `removed_at` set was deleted upstream, so filter `removed_at is null` for current rows.
|
|
27
|
+
|
|
28
|
+
| table | one row is | join on |
|
|
29
|
+
| -- | -- | -- |
|
|
30
|
+
| `issues` | a ticket: `identifier`, `title`, `state`, `state_type`, `assignee`, `delegate_name`, `priority`, `team_key`, `project_id`, `cycle_id`, `parent_identifier`, `updated_at` | `id` |
|
|
31
|
+
| `comments` | a comment: `body`, `author_name`, `is_bot`, `parent_id`, `created_at` | `issue_id` = `issues.id` |
|
|
32
|
+
| `issue_history` | one change to a ticket: `from_state` / `to_state` and the other `from_*` / `to_*` pairs, `created_at` | `issue_id` = `issues.id` |
|
|
33
|
+
| `relations` | a link between tickets: `type`, `issue_identifier`, `related_identifier` | identifiers |
|
|
34
|
+
| `labels`, `issue_labels` | a label, and which ticket carries it | `issue_labels.label_id` = `labels.id` |
|
|
35
|
+
| `agent_sessions` | an agent session on a ticket: `status`, `created_at` | `issue_id` = `issues.id` |
|
|
36
|
+
| `pull_requests` | a pull request that names a ticket in `linear_issue_identifier` | `linear_issue_identifier` = `issues.identifier` |
|
|
37
|
+
|
|
38
|
+
## Recipes
|
|
39
|
+
|
|
40
|
+
Replace `ENG-123` and `ENG` with the real identifier and team key.
|
|
41
|
+
|
|
42
|
+
One ticket's current fields:
|
|
43
|
+
|
|
44
|
+
```sql
|
|
45
|
+
select identifier, title, state, assignee, delegate_name, priority, updated_at
|
|
46
|
+
from issues where identifier = 'ENG-123' and removed_at is null
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The newest comments on a ticket, Catalyst's own first:
|
|
50
|
+
|
|
51
|
+
```sql
|
|
52
|
+
select c.created_at, c.author_name, c.is_bot, substr(c.body, 1, 200) as body
|
|
53
|
+
from comments c join issues i on i.id = c.issue_id
|
|
54
|
+
where i.identifier = 'ENG-123' and c.removed_at is null
|
|
55
|
+
order by c.is_bot desc, c.created_at desc limit 20
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Every state change on a ticket, oldest first:
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
select h.created_at, h.from_state, h.to_state
|
|
62
|
+
from issue_history h join issues i on i.id = h.issue_id
|
|
63
|
+
where i.identifier = 'ENG-123' and h.to_state is not null
|
|
64
|
+
order by h.created_at
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
How many tickets sit in each state on one team:
|
|
68
|
+
|
|
69
|
+
```sql
|
|
70
|
+
select state, count(*) as n from issues
|
|
71
|
+
where team_key = 'ENG' and removed_at is null
|
|
72
|
+
group by state order by n desc
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
What a ticket blocks and what blocks it:
|
|
76
|
+
|
|
77
|
+
```sql
|
|
78
|
+
select type, issue_identifier, related_identifier from relations
|
|
79
|
+
where issue_identifier = 'ENG-123' or related_identifier = 'ENG-123'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pull requests that name a ticket:
|
|
83
|
+
|
|
84
|
+
```sql
|
|
85
|
+
select repo_id, number, state, draft, merged, head_sha
|
|
86
|
+
from pull_requests where linear_issue_identifier = 'ENG-123'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What the replica does not answer
|
|
90
|
+
|
|
91
|
+
Phase attempts, remediate rounds, park state and the live lease are not in the replica. `catalyst history <KEY-123>` reads them from the cloud. So do eligibility (`catalyst explain <KEY-123>`) and search (`node scripts/search.mjs`).
|
|
@@ -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
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Reading ticket facts from the cloud
|
|
2
|
+
|
|
3
|
+
Cloud reads are the default on every machine. They need no local writer and no local database. Every command below reads the cloud through the `catalyst` CLI.
|
|
4
|
+
|
|
5
|
+
| question | command |
|
|
6
|
+
| -- | -- |
|
|
7
|
+
| What stage is the ticket in, who has it, which PRs name it? | `catalyst query issue <KEY-123>`, or `node scripts/read-ticket.mjs <KEY-123>` |
|
|
8
|
+
| What did Catalyst and people write on it? | `node scripts/read-ticket.mjs <KEY-123> --comments`; comments and activity come inline with the ticket |
|
|
9
|
+
| What was the last phase, and how did it end? | `catalyst history <KEY-123>` |
|
|
10
|
+
| How many remediate rounds against the cap? Is it parked, and what releases it? | `catalyst history <KEY-123>` |
|
|
11
|
+
| Why is it not running, or what runs next? | `catalyst explain <KEY-123>` |
|
|
12
|
+
| What is the PR's state, checks and reviews? | `catalyst query pulls --ticket <KEY-123>`, then `catalyst query pull <node-id>` |
|
|
13
|
+
| What changed across the account since a point? | `catalyst query changes --since head`, then the cursor it prints |
|
|
14
|
+
| Find a ticket by words | `node scripts/search.mjs <terms>` |
|
|
15
|
+
| What happened to it, event by event? | `catalyst events query --ticket <KEY-123>` |
|
|
16
|
+
| Wait until its phase finishes | `catalyst events wait-for --ticket <KEY-123> --type relay.phase.completed --after <head> --timeout 300` |
|
|
17
|
+
|
|
18
|
+
## Reading the answer
|
|
19
|
+
|
|
20
|
+
The first stderr line names the source: `source: api (...)`. Quote it when freshness matters ("from the API just now").
|
|
21
|
+
|
|
22
|
+
The ticket read returns the whole record, so a question about one field still downloads comments and activity. That is fine for one ticket. For many tickets, use a list (`catalyst query issues --team <key>`) and read one record only where the list is not enough.
|
|
23
|
+
|
|
24
|
+
A smaller read is coming: the cloud will answer a ticket or its execution history with only the status fields. No CLI flag selects it yet, so read the full record and pick the fields you need.
|
|
25
|
+
|
|
26
|
+
## Events
|
|
27
|
+
|
|
28
|
+
`catalyst events` reads the cloud by default and needs no local file.
|
|
29
|
+
|
|
30
|
+
- `catalyst events query --ticket <KEY-123>` prints the ticket's events, newest first. Add `--type`, `--limit` (up to 200), `--order asc` and `--before` or `--after` to page. An empty page says which sequences the cloud has indexed, so "no match" is not "never happened".
|
|
31
|
+
- To wait for something to happen, first run `catalyst events status --json` and keep its `head`. Then run `catalyst events wait-for --ticket <KEY-123> --type relay.phase.completed --after <head> --timeout 300`. Exit 0 prints the event, exit 1 is a timeout, and exit 4 means the cloud was unreachable.
|
|
32
|
+
- `catalyst events tail --ticket <KEY-123>` stays in the foreground and prints one JSON line per new event.
|
|
33
|
+
|
|
34
|
+
The ticket filter reads the event's ticket entity, `payload.ticket`, and the lease that caused it. `--from-cache` reads this machine's local event cache instead. It exists only where local sync is on, and it pays off when several agents on one machine share it.
|
|
35
|
+
|
|
36
|
+
## The local replica
|
|
37
|
+
|
|
38
|
+
Only on a machine where `catalyst replica status --json` reports `configured: true`, `references/local-replica.md` adds local SQL. Everywhere else, these cloud reads are the whole answer.
|