@catalyst-cloud/cli 0.16.41 → 0.16.43
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 +8 -0
- package/README.md +1 -1
- package/dist/ask.js +26 -10
- package/dist/cli.js +1 -1
- package/dist/runner-command.js +50 -9
- package/dist/runner-update-fleet.js +242 -35
- package/dist/runner-update.js +427 -46
- package/dist/update.js +14 -9
- 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,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.43
|
|
4
|
+
|
|
5
|
+
`catalyst ask accept` now waits for an answer that has not reached Catalyst yet, instead of failing. If you accept an ask a few seconds after answering it, Catalyst may not have your answer comment yet. The command used to stop with a refusal, and the same command worked when you ran it again. Now it waits as long as Catalyst asks and tries again, for up to 30 seconds. If the answer still has not arrived, it says so and tells you when to run the same command again. A refusal that will not change on retry is reported at once, with its reason.
|
|
6
|
+
|
|
7
|
+
## 0.16.42
|
|
8
|
+
|
|
9
|
+
A busy machine now takes a waiting release after one day. Before, a busy machine could keep putting off a release that cannot take over from running work in place. Now the next check asks Catalyst to send the machine no new work, waits for the running work to finish, then moves to the release and takes work again. `catalyst runner update --check` says when a machine is waiting like this, and `catalyst runner auto-update off` ends the wait. If the update cannot restore the machine's private thoughts storage, the machine keeps taking no new work, and `catalyst runner update` tells you what to do. This release also makes updates on Catalyst's own machines more reliable.
|
|
10
|
+
|
|
3
11
|
## 0.16.41
|
|
4
12
|
|
|
5
13
|
A runner's automatic update schedule now applies a newly approved runner release at its next check, instead of waiting for one of four set times of day. An owner who wants set times can still choose them with `catalyst update policy windows <HH:MM,...>`, and `catalyst update policy windows default` goes back to applying at the next check. `catalyst runner update` still applies a release at once, and `catalyst update policy` shows which setting is in effect.
|
package/README.md
CHANGED
|
@@ -346,7 +346,7 @@ The skill files themselves are written by whichever install command you ran, in
|
|
|
346
346
|
|
|
347
347
|
The new CLI is installed beside the old one, and your `catalyst` command runs it at the same path. A running local replica restarts on it and keeps its synced data; a stopped one stays stopped. CLI, SDK and image updates keep their existing policy against moving to an older version. Ordinary CLI startup never replaces skills with its bundled copy.
|
|
348
348
|
|
|
349
|
-
On a machine checking for releases every 15 minutes (`catalyst runner auto-update on`), that check applies the same component policies. Otherwise components move when you run `catalyst update`. That check moves the runner images to a newer release as soon as it finds one, with no set times of day. An update does not stop work that is already running: that work finishes on the image it started with. If you want updates only at set times, `catalyst update policy windows 06:00,18:00 --time-zone America/Chicago` sets your own times, `catalyst update policy windows default` goes back to none, and `catalyst runner update` or `catalyst update` applies a waiting release at once. Each component is on by default; turn one off with `catalyst update policy <component> off` (for example `cli`), and `catalyst update policy` lists every setting. If you turned the installer's daily update off, the CLI and both skills packs start held, and `catalyst update policy cli on` turns the CLI back on.
|
|
349
|
+
On a machine checking for releases every 15 minutes (`catalyst runner auto-update on`), that check applies the same component policies. Otherwise components move when you run `catalyst update`. That check moves the runner images to a newer release as soon as it finds one, with no set times of day. An update does not stop work that is already running: that work finishes on the image it started with. When work is running and the new release cannot take over from it in place, the check leaves the release waiting. Once a release has waited a day, the next check asks Catalyst to send the machine no new work. It waits for the running work to finish, however long that takes, then moves to the release and takes work again. `catalyst runner update --check` says when a machine is waiting like this, and `catalyst runner auto-update off` ends the wait. If you want updates only at set times, `catalyst update policy windows 06:00,18:00 --time-zone America/Chicago` sets your own times, `catalyst update policy windows default` goes back to none, and `catalyst runner update` or `catalyst update` applies a waiting release at once. Each component is on by default; turn one off with `catalyst update policy <component> off` (for example `cli`), and `catalyst update policy` lists every setting. If you turned the installer's daily update off, the CLI and both skills packs start held, and `catalyst update policy cli on` turns the CLI back on.
|
|
350
350
|
|
|
351
351
|
The installer's daily job runs the same release check, so it moves the CLI and the Cloud skills only to the approved release and keeps your holds. Until the development skills join the release, it refreshes them from their repository.
|
|
352
352
|
|
package/dist/ask.js
CHANGED
|
@@ -9,7 +9,7 @@ import { CliError, UsageError } from "./errors.js";
|
|
|
9
9
|
import { apiClient } from "./transport.js";
|
|
10
10
|
import { fetchAllPages } from "./pagination.js";
|
|
11
11
|
import { fetchWorkflowStates, postAgent, resolveIssue } from "./write.js";
|
|
12
|
-
export async function cmdAsk(args, ctx) {
|
|
12
|
+
export async function cmdAsk(args, ctx, deps = {}) {
|
|
13
13
|
const [sub, ...rest] = positionals(args);
|
|
14
14
|
if (!sub)
|
|
15
15
|
throw new UsageError("ask needs a subcommand: raise | accept | answer | proxy | disown | list");
|
|
@@ -33,7 +33,7 @@ export async function cmdAsk(args, ctx) {
|
|
|
33
33
|
case "raise":
|
|
34
34
|
return raise(args, ctx, doc, api);
|
|
35
35
|
case "accept":
|
|
36
|
-
return accept(args, ctx, doc, api, rest);
|
|
36
|
+
return accept(args, ctx, doc, api, rest, deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))));
|
|
37
37
|
case "answer":
|
|
38
38
|
return answerByProxy(args, ctx, doc, api, proxyBody);
|
|
39
39
|
case "list":
|
|
@@ -128,20 +128,36 @@ function askGates(args, options, doc) {
|
|
|
128
128
|
}
|
|
129
129
|
return { repo, pr: pr.length === 1 ? pr[0] : pr, labels: labels.length ? labels : ["hold"], releasedBy: released };
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
/** The most `ask accept` waits, in total, for an answer comment the cloud has not copied yet. */
|
|
132
|
+
export const ASK_ACCEPT_RETRY_BUDGET_MS = 30_000;
|
|
133
|
+
async function accept(args, ctx, doc, api, rest, sleep) {
|
|
132
134
|
const askTicket = rest[0];
|
|
133
135
|
const answer = flagString(args, "answer");
|
|
134
136
|
const role = flagString(args, "role");
|
|
135
137
|
if (!askTicket || !answer || !role)
|
|
136
138
|
throw new UsageError("ask accept needs <askTicket> --answer <commentId> --role <role>");
|
|
137
139
|
const ask = await resolveIssue(api, askTicket);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const body = { askIssueId: ask.id, answerCommentId: answer, acceptedByRole: role };
|
|
141
|
+
// An answer posted seconds ago may not have reached the cloud yet. The cloud refuses that one case
|
|
142
|
+
// as retryable and says how long to wait; every other refusal is final and is not retried.
|
|
143
|
+
let waited = 0;
|
|
144
|
+
for (;;) {
|
|
145
|
+
const result = await postAgent(api, doc, "ask-accept", body, { accept: [409] });
|
|
146
|
+
if (result.outcome !== "refused") {
|
|
147
|
+
ctx.stdout(args.json ? JSON.stringify(result) : `ask ${ask.identifier}: answer ${answer} recorded by ${role}`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
const waitMs = Math.max(1, result.retryAfterSeconds ?? 10) * 1000;
|
|
151
|
+
if (result.retryable === true && waited + waitMs <= ASK_ACCEPT_RETRY_BUDGET_MS) {
|
|
152
|
+
await sleep(waitMs);
|
|
153
|
+
waited += waitMs;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const reason = result.reason ?? "refused";
|
|
157
|
+
throw new CliError(result.retryable === true
|
|
158
|
+
? `ask ${ask.identifier}: answer ${answer} has not reached the cloud yet (${reason}) after ${waited / 1000}s of retries; run the same command again in ${result.retryAfterSeconds ?? 10}s`
|
|
159
|
+
: `ask ${ask.identifier}: accept refused (${reason})`, result.retryable === true ? "ask-accept-retry" : "ask-accept-refused", 2, 409);
|
|
160
|
+
}
|
|
145
161
|
}
|
|
146
162
|
/** The cloud's bounds on an answer by proxy, checked here first so a refusal never costs a write. */
|
|
147
163
|
const QUOTE_MAX = 4000;
|
package/dist/cli.js
CHANGED
|
@@ -531,7 +531,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
531
531
|
case "write":
|
|
532
532
|
return await cmdWrite(args, ctx, deps.write);
|
|
533
533
|
case "ask":
|
|
534
|
-
return await cmdAsk(args, ctx);
|
|
534
|
+
return await cmdAsk(args, ctx, { sleep: deps.sleep });
|
|
535
535
|
case "ready":
|
|
536
536
|
return await cmdReady(args, ctx, {
|
|
537
537
|
skillNames: CUSTOMER_SKILLS,
|
package/dist/runner-command.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { flagBool, flagString, positionals } from "./args.js";
|
|
5
5
|
import { CliError, UsageError } from "./errors.js";
|
|
6
6
|
import { autoUpdateStatus, custodyNode, disableAutoUpdate, enableAutoUpdate, scheduledPath, } from "./runner-auto-update.js";
|
|
7
|
-
import { dockerRunnerUpdateEngine, readReleaseState, RELEASE_COMPONENTS, runnerDir, runRunnerUpdate, updateResultLines, lastProblem, lastProblemLines, } from "./runner-update.js";
|
|
8
|
-
import { dockerFleetUpdateEngine, postRollLines, readPostRollResult, runFleetPostRollCheck, runFleetUpdate } from "./runner-update-fleet.js";
|
|
7
|
+
import { dockerRunnerUpdateEngine, readReleaseState, RELEASE_COMPONENTS, runnerDir, runRunnerUpdate, updateResultLines, lastProblem, lastProblemLines, drainLines, custodyUnattested, CUSTODY_UNATTESTED_LINE, releaseUpdateDrain, } from "./runner-update.js";
|
|
8
|
+
import { dockerFleetUpdateEngine, postRollLines, readPostRollResult, runFleetPostRollCheck, runFleetUpdate, } from "./runner-update-fleet.js";
|
|
9
9
|
import { runUpdate, sharedReleaseRead, updateReportJson, updateReportLines } from "./update.js";
|
|
10
10
|
import { effectivePolicy, readUpdatePolicy, setComponentPolicy } from "./update-policy.js";
|
|
11
11
|
import { resolveClaude } from "./update-plugin.js";
|
|
@@ -69,6 +69,18 @@ function statusLine(status) {
|
|
|
69
69
|
return "Automatic runner updates need your systemd user manager, which is not running for you. Run: sudo loginctl enable-linger $USER, then catalyst runner auto-update on";
|
|
70
70
|
return `Automatic runner updates are not available on this machine (${status.detail}). Run catalyst runner update yourself.`;
|
|
71
71
|
}
|
|
72
|
+
function autoLine(result, at) {
|
|
73
|
+
return `${at.toISOString()} ${result.outcome} ${result.detail} generation=${result.generation ?? "none"}`;
|
|
74
|
+
}
|
|
75
|
+
function drainReleaseLine(result) {
|
|
76
|
+
if (result === "released")
|
|
77
|
+
return "Catalyst can send this machine new phases again: it had stopped sending them so the machine could update.";
|
|
78
|
+
if (result === "pending")
|
|
79
|
+
return "The update that was waiting for running phases ends within a few seconds, and Catalyst can then send this machine new phases again.";
|
|
80
|
+
if (result === "failed")
|
|
81
|
+
return "Catalyst may still be holding new phases back from this machine. Run catalyst runner auto-update off again to retry.";
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
72
84
|
/** Why this machine's images must not move now: an owner's hold, or settings that cannot be read
|
|
73
85
|
* (which never count as "everything on"). Null when every image may move. */
|
|
74
86
|
function imageHold(ctx) {
|
|
@@ -145,7 +157,9 @@ export async function cmdRunner(args, ctx, deps = {}) {
|
|
|
145
157
|
if (args.json)
|
|
146
158
|
ctx.stdout(JSON.stringify(extra ? { ...result, components: extra.rows } : result));
|
|
147
159
|
else if (auto) {
|
|
148
|
-
ctx.stdout(
|
|
160
|
+
ctx.stdout(autoLine(result, ctx.now()));
|
|
161
|
+
if (custodyUnattested(result))
|
|
162
|
+
ctx.stdout(CUSTODY_UNATTESTED_LINE);
|
|
149
163
|
if (extra)
|
|
150
164
|
ctx.stdout(extra.line);
|
|
151
165
|
}
|
|
@@ -160,13 +174,27 @@ export async function cmdRunner(args, ctx, deps = {}) {
|
|
|
160
174
|
// windows; then only in a window. An owner's own run applies at once (CTC-7465, update-windows.ts).
|
|
161
175
|
if (hold !== null) {
|
|
162
176
|
const retained = { outcome: "retained", detail: hold, generation: readReleaseState(dir()).generation, exitCode: hold === "owner_setting" ? 0 : 1 };
|
|
177
|
+
if (hold === "owner_setting" && !fleet) {
|
|
178
|
+
const released = await releaseUpdateDrain(engine, dir(), { cloud: false });
|
|
179
|
+
if (released === "failed" && !auto && !args.json)
|
|
180
|
+
ctx.stdout(drainReleaseLine(released));
|
|
181
|
+
}
|
|
163
182
|
// Unreadable settings move nothing; a hold leaves the other components to the release check.
|
|
164
183
|
return print(retained, hold === "owner_setting" ? await components(undefined) : null);
|
|
165
184
|
}
|
|
166
185
|
const ownerWindows = auto && !check && !fleet ? readUpdatePolicy(ctx.home).windows : undefined;
|
|
186
|
+
// A fleet supervisor that misses its qualification deadline names each scope it had not qualified,
|
|
187
|
+
// and why, on stderr before the restore, so stdout keeps its one line or JSON object (CTC-8163).
|
|
167
188
|
const result = fleet
|
|
168
|
-
? await runFleetUpdate(deps.fleetEngine ?? dockerFleetUpdateEngine({ env: ctx.env }), {
|
|
169
|
-
|
|
189
|
+
? await runFleetUpdate(deps.fleetEngine ?? dockerFleetUpdateEngine({ env: ctx.env }), {
|
|
190
|
+
dir: dir(), check, ...deps.fleetClock,
|
|
191
|
+
onQualificationPending: (line) => ctx.stderr(`catalyst: ${line}`),
|
|
192
|
+
})
|
|
193
|
+
: await runRunnerUpdate(engine, {
|
|
194
|
+
dir: dir(), check,
|
|
195
|
+
...(ownerWindows ? { window: windowBounds(ctx.now().getTime(), ownerWindows) } : {}),
|
|
196
|
+
...(auto && !check ? { drain: true, held: () => imageHold(ctx), ...(args.json ? {} : { onPass: (pass) => ctx.stdout(autoLine(pass, ctx.now())) }) } : {}),
|
|
197
|
+
});
|
|
170
198
|
// `runner update --check` also lists every installed component against the approved release, as
|
|
171
199
|
// `catalyst update --check` does. The image fields stay where scripts already read them.
|
|
172
200
|
if (check && !fleet && !auto) {
|
|
@@ -190,10 +218,10 @@ export async function cmdRunner(args, ctx, deps = {}) {
|
|
|
190
218
|
const state = readReleaseState(dir());
|
|
191
219
|
if (args.json) {
|
|
192
220
|
const { schemaVersion, release, components, projects } = updateReportJson(report);
|
|
193
|
-
ctx.stdout(JSON.stringify({ ...result, schemaVersion, release, components, projects, lastProblem: lastProblem(state) }));
|
|
221
|
+
ctx.stdout(JSON.stringify({ ...result, schemaVersion, release, components, projects, lastProblem: lastProblem(state), drain: state.drain === null ? null : { generation: state.drain.generation, acknowledgedAtMs: state.drain.acknowledgedAtMs }, deferral: state.deferral }));
|
|
194
222
|
}
|
|
195
223
|
else
|
|
196
|
-
for (const line of [...updateResultLines(result), ...lastProblemLines(state), ...updateReportLines(report)])
|
|
224
|
+
for (const line of [...updateResultLines(result), ...lastProblemLines(state), ...drainLines(state), ...updateReportLines(report)])
|
|
197
225
|
ctx.stdout(line);
|
|
198
226
|
return result.exitCode;
|
|
199
227
|
}
|
|
@@ -229,12 +257,25 @@ export async function cmdRunner(args, ctx, deps = {}) {
|
|
|
229
257
|
if (mode === "off" || (mode === "on" && status.state === "on"))
|
|
230
258
|
for (const id of RELEASE_COMPONENTS)
|
|
231
259
|
setComponentPolicy(ctx.home, id, mode === "on");
|
|
260
|
+
let updateDrain = "none";
|
|
261
|
+
if (mode === "off" && !fleet) {
|
|
262
|
+
let runner = null;
|
|
263
|
+
try {
|
|
264
|
+
runner = dir();
|
|
265
|
+
}
|
|
266
|
+
catch { /* No runner directory to release. */ }
|
|
267
|
+
if (runner !== null && existsSync(resolve(runner, ".env")) && existsSync(resolve(runner, "compose.yaml")))
|
|
268
|
+
updateDrain = await releaseUpdateDrain(deps.engine ?? dockerRunnerUpdateEngine({ env: ctx.env }), runner, { cloud: true });
|
|
269
|
+
}
|
|
232
270
|
if (args.json) {
|
|
233
271
|
const state = mode === "status" && dirFlag !== undefined ? readReleaseState(dirFlag) : undefined;
|
|
234
|
-
ctx.stdout(JSON.stringify({ ...status, ...(state ? { release: state } : {}) }));
|
|
272
|
+
ctx.stdout(JSON.stringify({ ...status, ...(state ? { release: state } : {}), ...(mode === "off" ? { updateDrain } : {}) }));
|
|
235
273
|
}
|
|
236
274
|
else {
|
|
237
275
|
ctx.stdout(statusLine(status));
|
|
276
|
+
const line = mode === "off" ? drainReleaseLine(updateDrain) : null;
|
|
277
|
+
if (line !== null)
|
|
278
|
+
ctx.stdout(line);
|
|
238
279
|
// A single runner's schedule prints what it always has; a fleet's says so, and for which directory.
|
|
239
280
|
if (status.state === "on" && status.fleet)
|
|
240
281
|
ctx.stdout(`This is a supervisor fleet's schedule: it updates the compose.json in ${status.dir}.`);
|