@catalyst-cloud/cli 0.15.5 → 0.16.0
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/dist/args.js +20 -1
- package/dist/ask.js +56 -0
- package/dist/onboard-github-copy.js +2 -0
- package/dist/onboard-github.js +2 -0
- package/dist/onboard-next.js +10 -3
- package/dist/onboard-readiness-fresh.js +32 -0
- package/dist/onboard-ready.js +3 -2
- package/dist/onboard-runtime.js +5 -1
- package/dist/onboard-ui.js +35 -11
- package/dist/onboard-values.js +131 -26
- package/dist/onboard-workflow.js +32 -7
- package/dist/onboard.js +15 -1
- package/dist/ready-copy.js +2 -2
- package/dist/setup-onboard-copy.js +61 -40
- package/package.json +8 -5
- 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/catalyst-onboard/references/declaring-a-repository.md +1 -1
- package/skills/catalyst-onboard/references/reading-ready.md +1 -1
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +2 -2
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +2 -2
- package/skills/what-needs-me/references/raising-a-decision.md +11 -0
- package/skills/what-needs-me/scripts/raise.mjs +10 -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.0
|
|
4
|
+
|
|
5
|
+
The CLI, skills and plugin now share the 0.16 release line with the SDK and installer. The CLI installs the matching Node replica module for local cache commands. Update the CLI and update the Catalyst plugin through your agent's plugin manager.
|
|
6
|
+
|
|
7
|
+
## 0.15.6
|
|
8
|
+
|
|
9
|
+
Setup checks current readiness and waits up to one minute while Catalyst reads repository settings.
|
|
10
|
+
|
|
3
11
|
## 0.15.5
|
|
4
12
|
|
|
5
13
|
Setup explains what running work on this computer means and lists every repository registered to your Linear team.
|
package/dist/args.js
CHANGED
|
@@ -280,6 +280,25 @@ export const FLAG_TABLES = {
|
|
|
280
280
|
value: false,
|
|
281
281
|
help: "raise: declare that nothing is blocked",
|
|
282
282
|
},
|
|
283
|
+
"gates-pr": {
|
|
284
|
+
value: true,
|
|
285
|
+
repeat: true,
|
|
286
|
+
help: "raise: a PR whose hold the answer releases (repeatable; needs --released-by)",
|
|
287
|
+
},
|
|
288
|
+
"gates-label": {
|
|
289
|
+
value: true,
|
|
290
|
+
repeat: true,
|
|
291
|
+
help: "raise: the hold label the answer removes: hold or hold:preview (repeatable; default hold)",
|
|
292
|
+
},
|
|
293
|
+
"released-by": {
|
|
294
|
+
value: true,
|
|
295
|
+
repeat: true,
|
|
296
|
+
help: "raise: an option letter whose answer releases the hold (repeatable)",
|
|
297
|
+
},
|
|
298
|
+
"gates-repo": {
|
|
299
|
+
value: true,
|
|
300
|
+
help: "raise: the --gates-pr repository as owner/name (default: the account's only repository)",
|
|
301
|
+
},
|
|
283
302
|
anyone: {
|
|
284
303
|
value: false,
|
|
285
304
|
help: "list: every open ask in the tenant, not only the ones assigned to you",
|
|
@@ -479,7 +498,7 @@ export const VERB_USAGE = {
|
|
|
479
498
|
queue: "queue [--team K] [--json]",
|
|
480
499
|
watch: "watch [--team K] [--ticket T]... [--project P] [--exec CMD] [--cursor-file <path>] [--from cursor|head]",
|
|
481
500
|
write: "write <comment <ticket> --body|--stdin [--parent] [--bookkeeping] [--as-user] | state <ticket> --slot|--state-id|--state-type | label <ticket> --add... --remove... | create --team --title [--description|--stdin] [--label] [--priority] | reaction <ticket>|--comment <id> --emoji <e> | attachment <ticket> --title --url | session <ticket> [--title] [--plan-file] [--activity]>",
|
|
482
|
-
ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] | accept <askTicket> --answer <commentId> --role <role> | list [--anyone] [--json]>",
|
|
501
|
+
ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] [--gates-pr <n>... --released-by <letter>... [--gates-label hold|hold:preview]... [--gates-repo <owner/name>]] | accept <askTicket> --answer <commentId> --role <role> | list [--anyone] [--json]>",
|
|
483
502
|
ready: "ready [--onboarding] [--local-sync] [--json] [--offline]",
|
|
484
503
|
accounts: "accounts [--json]",
|
|
485
504
|
mcp: "mcp <add <name> --url URL <--auth none|--bearer SECRET_NAME|--header NAME=SECRET_NAME...>|list|remove <name>> [--json]",
|
package/dist/ask.js
CHANGED
|
@@ -39,6 +39,7 @@ async function raise(args, ctx, doc, api) {
|
|
|
39
39
|
if (options.length > doc.askTemplate.maxLetteredOptions) {
|
|
40
40
|
throw new UsageError(`ask raise takes at most ${doc.askTemplate.maxLetteredOptions} options`);
|
|
41
41
|
}
|
|
42
|
+
const gates = askGates(args, options, doc);
|
|
42
43
|
const blocks = [];
|
|
43
44
|
for (const t of flagList(args, "blocks"))
|
|
44
45
|
blocks.push((await resolveIssue(api, t)).id);
|
|
@@ -55,10 +56,65 @@ async function raise(args, ctx, doc, api) {
|
|
|
55
56
|
body.nothingToBlock = true;
|
|
56
57
|
if (flagString(args, "ask-key"))
|
|
57
58
|
body.askKey = flagString(args, "ask-key");
|
|
59
|
+
if (gates)
|
|
60
|
+
body.gates = gates;
|
|
58
61
|
const result = await postAgent(api, doc, "ask", body);
|
|
59
62
|
ctx.stdout(args.json ? JSON.stringify(result) : `ask raised: ${String(result.identifier ?? result.id ?? "ok")}${blocks.length ? ` (blocks ${blocks.length})` : ""}`);
|
|
60
63
|
return 0;
|
|
61
64
|
}
|
|
65
|
+
/** The hold labels an answered ask may lift; the cloud's ask route refuses any other with a 400. */
|
|
66
|
+
const GATE_LABELS = ["hold", "hold:preview"];
|
|
67
|
+
const OWNER_NAME = /^[^/\s]+\/[^/\s]+$/;
|
|
68
|
+
/** CTC-5044 — the PR hold an answer releases. Absent unless --gates-pr is given, so an ask without it
|
|
69
|
+
* posts exactly the body it always did. Everything the route would refuse (a label outside
|
|
70
|
+
* {@link GATE_LABELS}, a letter that is not one of this ask's options) is refused here first, before
|
|
71
|
+
* any ticket is resolved or anything is posted. Options are lettered A, B, C in the order given. */
|
|
72
|
+
function askGates(args, options, doc) {
|
|
73
|
+
const prs = flagList(args, "gates-pr");
|
|
74
|
+
if (prs.length === 0) {
|
|
75
|
+
for (const f of ["released-by", "gates-label", "gates-repo"]) {
|
|
76
|
+
if (flagList(args, f).length || flagString(args, f))
|
|
77
|
+
throw new UsageError(`ask raise takes --${f} only with --gates-pr <n>`);
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const pr = [...new Set(prs.map((n) => {
|
|
82
|
+
if (!/^[1-9]\d*$/.test(n))
|
|
83
|
+
throw new UsageError(`--gates-pr must be a PR number (got "${n}")`);
|
|
84
|
+
return Number(n);
|
|
85
|
+
}))];
|
|
86
|
+
const labels = [...new Set(flagList(args, "gates-label"))];
|
|
87
|
+
for (const l of labels) {
|
|
88
|
+
if (!GATE_LABELS.includes(l))
|
|
89
|
+
throw new UsageError(`--gates-label ${l} is not a hold an answer can release; use one of: ${GATE_LABELS.join(", ")}`);
|
|
90
|
+
}
|
|
91
|
+
const released = [...new Set(flagList(args, "released-by").map((l) => l.trim().toUpperCase()))];
|
|
92
|
+
if (released.length === 0)
|
|
93
|
+
throw new UsageError("--gates-pr needs --released-by <letter>: the option whose answer releases the hold");
|
|
94
|
+
const letters = options.map((_, i) => String.fromCharCode(65 + i));
|
|
95
|
+
for (const l of released) {
|
|
96
|
+
if (!letters.includes(l))
|
|
97
|
+
throw new UsageError(`--released-by ${l} is not one of this ask's options (${letters.length ? letters.join(", ") : "it has none; add --option"})`);
|
|
98
|
+
}
|
|
99
|
+
const registered = doc.merge.repositories.map((r) => `${r.owner}/${r.name}`);
|
|
100
|
+
const named = flagString(args, "gates-repo");
|
|
101
|
+
let repo;
|
|
102
|
+
if (named !== undefined) {
|
|
103
|
+
if (!OWNER_NAME.test(named))
|
|
104
|
+
throw new UsageError(`--gates-repo must be owner/name (got "${named}")`);
|
|
105
|
+
const match = registered.find((r) => r.toLowerCase() === named.toLowerCase());
|
|
106
|
+
if (registered.length && !match)
|
|
107
|
+
throw new UsageError(`--gates-repo ${named} is not a repository this account registers (${registered.join(", ")})`);
|
|
108
|
+
repo = match ?? named;
|
|
109
|
+
}
|
|
110
|
+
else if (registered.length === 1) {
|
|
111
|
+
repo = registered[0];
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw new UsageError(`--gates-pr needs --gates-repo <owner/name>: this account registers ${registered.length ? registered.join(", ") : "no repository"}`);
|
|
115
|
+
}
|
|
116
|
+
return { repo, pr: pr.length === 1 ? pr[0] : pr, labels: labels.length ? labels : ["hold"], releasedBy: released };
|
|
117
|
+
}
|
|
62
118
|
async function accept(args, ctx, doc, api, rest) {
|
|
63
119
|
const askTicket = rest[0];
|
|
64
120
|
const answer = flagString(args, "answer");
|
package/dist/onboard-github.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { finishOnTheWeb } from "./consent-browser.js";
|
|
2
|
+
import { GITHUB_CONFIRM_ACCESS } from "./onboard-github-copy.js";
|
|
2
3
|
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
3
4
|
import { pollConsent } from "./onboard-consent.js";
|
|
4
5
|
import { GITHUB_PERMISSION, githubInstallationPage, parsePermissions, } from "./onboard-permissions.js";
|
|
@@ -468,6 +469,7 @@ export function githubInstallationAdapter(options) {
|
|
|
468
469
|
reason: "github_installation_admin_required",
|
|
469
470
|
};
|
|
470
471
|
// The signed URL is transient credential material: only the browser receives it.
|
|
472
|
+
ctx.stderr(GITHUB_CONFIRM_ACCESS);
|
|
471
473
|
let browserUnavailable = false;
|
|
472
474
|
try {
|
|
473
475
|
await options.openBrowser(url, signal);
|
package/dist/onboard-next.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nativeEgressInstallCommands } from "./onboard-runner-egress.js";
|
|
2
|
+
import { GITHUB_CONFIRM_ACCESS } from "./onboard-github-copy.js";
|
|
2
3
|
import { INSTALL_COMMAND, loadConfig, normalizeBaseUrl } from "./config.js";
|
|
3
4
|
import { githubInstallationPage } from "./onboard-permissions.js";
|
|
4
5
|
import { valuesFacts, valuesFix } from "./ready-copy.js";
|
|
@@ -138,7 +139,7 @@ const REASONS = {
|
|
|
138
139
|
runner_needs_runner_flag: "This machine was set up to run Catalyst's work, but its runner is not running or not enrolled. To bring it up, run catalyst onboard --runner.",
|
|
139
140
|
runner_capability_pending: "The runner enrolled but has not reported its capacity yet. Run catalyst onboard again in a minute.",
|
|
140
141
|
runner_admission_unverified: "Setup could not read whether the team's pool admits runner hosts. Run catalyst onboard again to retry.",
|
|
141
|
-
github_app_permissions_outdated:
|
|
142
|
+
github_app_permissions_outdated: `The existing GitHub App installation needs updated permissions accepted. ${GITHUB_CONFIRM_ACCESS} An organization owner on GitHub opens the App's installation page, chooses Review permissions, then Accept new permissions. Run catalyst onboard afterward.`,
|
|
142
143
|
github_app_repository_missing: "The GitHub App installation cannot reach a repository a project registers. An organization owner on GitHub adds it under the installation's Repository access, then run catalyst onboard.",
|
|
143
144
|
github_app_repository_not_installed: "No GitHub App installation can reach a repository a project registers. A workspace owner or administrator installs the GitHub App on its owner: run catalyst onboard.",
|
|
144
145
|
github_app_permissions_unverified: "The GitHub App installation works, but its granted permissions could not be checked. Run catalyst onboard to try again.",
|
|
@@ -147,7 +148,7 @@ const REASONS = {
|
|
|
147
148
|
linear_workspace_permissions_unverified: "The workspace's Linear connection works, but its granted scopes could not be checked. Run catalyst onboard to try again.",
|
|
148
149
|
linear_personal_scope_outdated: "Your personal Linear connection is missing scopes this version needs. Re-authorize it from Connected accounts, then run catalyst onboard.",
|
|
149
150
|
personal_permissions_unverified: "Your personal connection's granted scopes could not be checked. Run catalyst onboard to try again.",
|
|
150
|
-
required_values_unverified: "Catalyst
|
|
151
|
+
required_values_unverified: "Setup couldn't get a current readiness check for this team from Catalyst, so it can't tell whether the repositories' values are set. Run catalyst ready to check.",
|
|
151
152
|
first_ticket_team_unverified: "Setup could not confirm the selected Linear team. Run catalyst setup again.",
|
|
152
153
|
first_ticket_dispatch_unmapped: "Catalyst does not know which of the team's stages starts its work yet. Set up the team's Catalyst workflow, then run catalyst setup again.",
|
|
153
154
|
housekeeping_service_unverified: "The daily update needs a user service manager, launchd on macOS or systemd --user on Linux. Setup cannot schedule it on this computer. It is optional, and work does not depend on it.",
|
|
@@ -246,7 +247,7 @@ function permissionText(step, baseUrl) {
|
|
|
246
247
|
case "github_app_permissions_outdated":
|
|
247
248
|
if (!url)
|
|
248
249
|
return undefined;
|
|
249
|
-
return `GitHub App
|
|
250
|
+
return `The existing GitHub App installation${org ? ` on ${org}` : ""} needs updated permissions accepted${missing ? `: ${missing}` : ""}. ${GITHUB_CONFIRM_ACCESS} ${github} chooses Review permissions, then Accept new permissions at ${url}. Run catalyst onboard afterward.`;
|
|
250
251
|
case "github_app_repository_missing":
|
|
251
252
|
if (!url || !repository)
|
|
252
253
|
return undefined;
|
|
@@ -435,6 +436,12 @@ export function onboardReasonText(step, context = {}) {
|
|
|
435
436
|
}
|
|
436
437
|
if (reason === "runner_admission_operator")
|
|
437
438
|
return `The admission policy for team ${onboardTeamKey(context.journal) ?? "<TEAM KEY>"} needs a Catalyst operator to check it. Setup changes only restricted admission and preserves existing policy fields. Ask Catalyst support to confirm a compatible policy, then run the same setup command.`;
|
|
439
|
+
// CTC-4744: the cloud has not read the settings yet. Setup already waited a minute; this is
|
|
440
|
+
// the cloud's work, not the person's.
|
|
441
|
+
if (reason === "required_values_unread") {
|
|
442
|
+
const which = typeof step.evidence?.repositories === "string" ? step.evidence.repositories : "this team's repositories";
|
|
443
|
+
return `Catalyst hasn't finished reading the settings of ${which}, so it can't check the values yet. You don't need to do anything: Catalyst does this on its own, and catalyst ready shows when it's done.`;
|
|
444
|
+
}
|
|
438
445
|
if (reason === "required_values_missing") {
|
|
439
446
|
let facts;
|
|
440
447
|
try {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// CTC-4744: the team's readiness verdict in the contract lasts five minutes, and the contract never
|
|
2
|
+
// recomputes it. Only a readiness read does. Setup asks for that read before it judges a team, so
|
|
3
|
+
// the end of setup and `catalyst ready` read the same current verdict.
|
|
4
|
+
import { normalizeBaseUrl } from "./config.js";
|
|
5
|
+
import { loadContract } from "./contract.js";
|
|
6
|
+
const READ_THROUGH_TIMEOUT_MS = 30_000;
|
|
7
|
+
/** Ask the cloud to bring each team's verdict up to date, then read the contract past its cache.
|
|
8
|
+
* The read-through's own answer is not trusted: the contract it refreshes is the evidence. A cloud
|
|
9
|
+
* without the route, or any failure here, leaves the verdict as it was. */
|
|
10
|
+
export async function loadFreshTeamContract(ctx, cfg, teamIds, signal, timeoutMs = READ_THROUGH_TIMEOUT_MS) {
|
|
11
|
+
const expiry = cfg.auth ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() : 0;
|
|
12
|
+
const bearer = cfg.key ||
|
|
13
|
+
(cfg.auth && Number.isFinite(expiry) && expiry > 30_000 ? cfg.auth.accessToken : undefined);
|
|
14
|
+
if (bearer)
|
|
15
|
+
for (const id of new Set(teamIds)) {
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
break;
|
|
18
|
+
const deadline = AbortSignal.timeout(timeoutMs);
|
|
19
|
+
try {
|
|
20
|
+
const response = await ctx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}/api/v1/agent/tenant/readiness?team=${encodeURIComponent(id)}`, {
|
|
21
|
+
redirect: "error",
|
|
22
|
+
signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
|
|
23
|
+
headers: { authorization: `Bearer ${bearer}`, accept: "application/json" },
|
|
24
|
+
});
|
|
25
|
+
await response.body?.cancel().catch(() => { });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Unreachable or refused: the contract read below decides.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return loadContract(ctx, cfg, { refresh: true, signal });
|
|
32
|
+
}
|
package/dist/onboard-ready.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "./config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { loadFreshTeamContract } from "./onboard-readiness-fresh.js";
|
|
3
3
|
import { ONBOARD_DEFERRED_STEPS, onboardRequiredStepsWaiting, } from "./onboard.js";
|
|
4
4
|
const evidenceNames = new Set(["account", "membershipId", "project", "team", "repository", "count", "checkedAt", "expiresAt", "revision", "lag", "cursor", "heartbeatAgeMs", "supervised", "available", "validated", "phaseStarted", "linearComment", "fleetActivity"]);
|
|
5
5
|
const slug = (value) => value && /^[a-z][a-z0-9_]{1,63}$/.test(value) ? value : undefined;
|
|
@@ -98,7 +98,8 @@ export async function observeCloudOnboarding(ctx, scope = {}) {
|
|
|
98
98
|
const cfg = loadConfig(ctx.home);
|
|
99
99
|
if (!cfg?.user)
|
|
100
100
|
return { checks: [{ id: "signin", state: "fail", required: true, reason: "personal_login_required" }], work: { state: "unknown" } };
|
|
101
|
-
|
|
101
|
+
// CTC-4744: ask for each team's verdict fresh, the same read `catalyst setup`'s values step makes.
|
|
102
|
+
const { doc, source } = await loadFreshTeamContract(ctx, cfg, scope.teamIds ?? []);
|
|
102
103
|
if (source === "cache")
|
|
103
104
|
return { checks: [{ id: "cloud.setup", state: "unknown", required: true, reason: "cloud_observation_unavailable" }], work: { state: "unknown" } };
|
|
104
105
|
const checks = [];
|
package/dist/onboard-runtime.js
CHANGED
|
@@ -339,7 +339,11 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
339
339
|
},
|
|
340
340
|
},
|
|
341
341
|
housekeeping: onboardHousekeepingAdapter(hooks.scheduler),
|
|
342
|
-
|
|
342
|
+
// CTC-4744: a settings read the cloud has not finished is waited on here, on one line.
|
|
343
|
+
values: onboardValuesAdapter({
|
|
344
|
+
message: (text) => hooks.ui?.note ? hooks.ui.note(text) : hooks.ui ? hooks.ui.message(text) : ctx.stderr(text),
|
|
345
|
+
sleep: hooks.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
346
|
+
}),
|
|
343
347
|
// Only a person at a terminal picks a ticket; --yes, JSON and headless runs leave it waiting.
|
|
344
348
|
"first-ticket": onboardFirstTicketAdapter(interactiveWait && hooks.ui?.chooseFirstTicket
|
|
345
349
|
? {
|
package/dist/onboard-ui.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { GITHUB_CONFIRM_ACCESS } from "./onboard-github-copy.js";
|
|
1
2
|
import { COMPUTER_CHECKS, standalonePlan, standalonePlanNotes, pendingContinuation, } from "./onboard-standalone-copy.js";
|
|
2
3
|
import { trackedPrompt } from "./setup-prompt-frame.js";
|
|
3
4
|
import { createOnboardInterrupts } from "./onboard-interrupts.js";
|
|
4
|
-
import { setupStepView, CONNECTION_REPAIR_REASONS, setupFinalScreen, setupBrowserInstruction, setupCommandWords as say, setupWaitsOnAdmin, TIMED_OUT_REASONS, setupPartHeading, setupPartProgress, setupTrackerRows, SETUP_PART_NUMBERS, SETUP_PART_OF, SETUP_PARTS, } from "./setup-onboard-copy.js";
|
|
5
|
+
import { CLOUD_WORK, setupStepView, CONNECTION_REPAIR_REASONS, setupFinalScreen, setupBrowserInstruction, setupCommandWords as say, setupWaitsOnAdmin, TIMED_OUT_REASONS, setupPartHeading, setupPartProgress, setupTrackerRows, SETUP_PART_NUMBERS, SETUP_PART_OF, SETUP_PARTS, } from "./setup-onboard-copy.js";
|
|
5
6
|
import { createOnboardProgress, } from "./onboard-progress.js";
|
|
6
7
|
import { CREATE_TEAM_CHOICE, suggestTeamKey, teamKeyProblem, teamNameProblem, } from "./onboard-team-create.js";
|
|
7
8
|
import { FIRST_TICKET_SAMPLE, FIRST_TICKET_SKIP, } from "./onboard-first-ticket.js";
|
|
@@ -157,11 +158,13 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
157
158
|
return;
|
|
158
159
|
if (renderer &&
|
|
159
160
|
currentStep === "linear.adopt" &&
|
|
160
|
-
text.endsWith("
|
|
161
|
+
text.endsWith("has every required state and label"))
|
|
161
162
|
return;
|
|
162
163
|
if (renderer) {
|
|
163
164
|
if (currentStep &&
|
|
164
165
|
setupBrowserInstruction(currentStep, deps.baseUrl?.())) {
|
|
166
|
+
if (currentStep === "github.install" && text === GITHUB_CONFIRM_ACCESS)
|
|
167
|
+
renderer.detail(text);
|
|
165
168
|
if (/no browser opened|could not open a browser/i.test(text))
|
|
166
169
|
browserOpened = false;
|
|
167
170
|
return;
|
|
@@ -260,8 +263,11 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
260
263
|
return `Open ${connectWaits.get(action.id)} and ${what}.`;
|
|
261
264
|
};
|
|
262
265
|
const live = screen.actions.filter((action) => step(action.id).reason !== "interrupted");
|
|
263
|
-
|
|
264
|
-
const
|
|
266
|
+
// CTC-4744: what only the cloud's own work holds up is neither the person's nor an admin's.
|
|
267
|
+
const working = live.filter((action) => CLOUD_WORK.has(step(action.id).reason ?? ""));
|
|
268
|
+
const theirs = live.filter((action) => !working.includes(action) &&
|
|
269
|
+
setupWaitsOnAdmin(step(action.id), journal));
|
|
270
|
+
const mine = live.filter((action) => !theirs.includes(action) && !working.includes(action));
|
|
265
271
|
// Steps only an owner or admin can do, less any already listed as their task above.
|
|
266
272
|
const skipped = journal.steps.filter((s) => s.reason === "member_scope" &&
|
|
267
273
|
setupWaitsOnAdmin(s, journal) &&
|
|
@@ -278,7 +284,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
278
284
|
: waiting
|
|
279
285
|
? "Next: run catalyst setup once they have."
|
|
280
286
|
: "Next: run catalyst setup to check again.";
|
|
281
|
-
return { screen, mine, theirs, skipped, waiting, next, text, live };
|
|
287
|
+
return { screen, mine, theirs, working, skipped, waiting, next, text, live };
|
|
282
288
|
};
|
|
283
289
|
const summary = (journal) => {
|
|
284
290
|
const sorted = sortActions(journal);
|
|
@@ -299,6 +305,11 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
299
305
|
if (screen.more)
|
|
300
306
|
renderer.line(`and ${screen.more} more after these`);
|
|
301
307
|
}
|
|
308
|
+
if (sorted.working.length) {
|
|
309
|
+
renderer.line("Still in progress, nothing for you to do:");
|
|
310
|
+
for (const action of sorted.working)
|
|
311
|
+
renderer.item("· ", sorted.text(action));
|
|
312
|
+
}
|
|
302
313
|
if (sorted.waiting) {
|
|
303
314
|
renderer.line("Waiting on an owner or admin of your Catalyst workspace:");
|
|
304
315
|
for (const action of theirs)
|
|
@@ -894,22 +905,33 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
894
905
|
}
|
|
895
906
|
},
|
|
896
907
|
async checkAgain(journal) {
|
|
908
|
+
const sorted = sortActions(journal);
|
|
897
909
|
if (!renderer ||
|
|
898
910
|
!interactive ||
|
|
899
911
|
abort.signal.aborted ||
|
|
900
|
-
!
|
|
912
|
+
(!sorted.mine.length && !sorted.working.length))
|
|
901
913
|
return false;
|
|
902
914
|
active = false;
|
|
903
915
|
summary(journal);
|
|
904
916
|
summaryShown = true;
|
|
917
|
+
// CTC-4744: one question, about one thing. When the person has something to do, checking
|
|
918
|
+
// again is for after they do it; when only the cloud is working, it is a wait.
|
|
919
|
+
const yours = sorted.mine.length > 0;
|
|
905
920
|
const answer = await select({
|
|
906
921
|
...options,
|
|
907
|
-
message:
|
|
922
|
+
message: yours
|
|
923
|
+
? `Check again once ${sorted.mine.length === 1 ? "that's" : "those are"} done?`
|
|
924
|
+
: "Catalyst is still reading the repositories' settings. Wait for it?",
|
|
908
925
|
initialValue: "again",
|
|
909
|
-
options:
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
926
|
+
options: yours
|
|
927
|
+
? [
|
|
928
|
+
{ value: "again", label: "Check again now" },
|
|
929
|
+
{ value: "stop", label: "Finish now; run catalyst setup later to check again" },
|
|
930
|
+
]
|
|
931
|
+
: [
|
|
932
|
+
{ value: "again", label: "Wait and check again (about 1 minute)" },
|
|
933
|
+
{ value: "stop", label: "Finish now; check later with catalyst ready" },
|
|
934
|
+
],
|
|
913
935
|
}, true);
|
|
914
936
|
if (prompts.isCancel(answer)) {
|
|
915
937
|
abort.abort();
|
|
@@ -1009,6 +1031,8 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
1009
1031
|
? status()
|
|
1010
1032
|
: "waiting for you, up to 10 minutes");
|
|
1011
1033
|
renderer.action(browser.instruction);
|
|
1034
|
+
if (currentStep === "github.install")
|
|
1035
|
+
renderer.detail(GITHUB_CONFIRM_ACCESS);
|
|
1012
1036
|
if (browser.url)
|
|
1013
1037
|
renderer.detail(renderer.link(browser.url));
|
|
1014
1038
|
renderer.detail(renderer.dim(interactive ? "Ctrl-C skips this step." : "Ctrl-C stops setup."));
|
package/dist/onboard-values.js
CHANGED
|
@@ -1,47 +1,152 @@
|
|
|
1
1
|
import { loadConfig } from "./config.js";
|
|
2
|
-
import { loadContract } from "./contract.js";
|
|
3
2
|
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
3
|
+
import { loadFreshTeamContract } from "./onboard-readiness-fresh.js";
|
|
4
4
|
import { valuesFacts } from "./ready-copy.js";
|
|
5
5
|
const waiting = (reason, evidence) => ({
|
|
6
6
|
state: "waiting",
|
|
7
7
|
reason,
|
|
8
8
|
...(evidence ? { evidence } : {}),
|
|
9
9
|
});
|
|
10
|
+
/** How long setup waits for the cloud to read a repository's settings, and how often it looks. */
|
|
11
|
+
export const VALUES_WAIT_MS = 60_000;
|
|
12
|
+
const VALUES_POLL_MS = 10_000;
|
|
13
|
+
/** While waiting, one slow read-through can't stretch "up to 1 minute". */
|
|
14
|
+
const WAIT_READ_TIMEOUT_MS = 10_000;
|
|
15
|
+
/** The repositories a team's verdict covers, by name, for the one waiting line. */
|
|
16
|
+
function teamRepositories(team, check) {
|
|
17
|
+
const fromCheck = check ? (valuesFacts(check).repos ?? []).map((row) => row.repo) : [];
|
|
18
|
+
if (fromCheck.length)
|
|
19
|
+
return fromCheck;
|
|
20
|
+
const registered = team?.repositories
|
|
21
|
+
?.registered;
|
|
22
|
+
return Array.isArray(registered)
|
|
23
|
+
? registered.filter((name) => typeof name === "string")
|
|
24
|
+
: [];
|
|
25
|
+
}
|
|
26
|
+
function named(repositories) {
|
|
27
|
+
if (!repositories.length)
|
|
28
|
+
return "this team's repositories";
|
|
29
|
+
if (repositories.length === 1)
|
|
30
|
+
return repositories[0];
|
|
31
|
+
return `${repositories[0]} and ${repositories.length - 1} more`;
|
|
32
|
+
}
|
|
10
33
|
/** The selected team's `required_values` readiness check decides this step. It passes when the
|
|
11
34
|
* selected repositories declare no required value, or every one has a value. Nothing here reads,
|
|
12
|
-
* imports or prints a value: a missing one is named, with the page where it is set.
|
|
13
|
-
*
|
|
14
|
-
|
|
35
|
+
* imports or prints a value: a missing one is named, with the page where it is set.
|
|
36
|
+
*
|
|
37
|
+
* CTC-4744: the verdict is asked for fresh first (it lasts five minutes and only a readiness read
|
|
38
|
+
* recomputes it). When the cloud has not read the settings yet, setup waits up to a minute on one
|
|
39
|
+
* line, instead of telling the person to run setup again. */
|
|
40
|
+
export function onboardValuesAdapter(deps = {}) {
|
|
15
41
|
return {
|
|
16
42
|
check: async (ctx, journal, signal) => {
|
|
17
43
|
const cfg = loadConfig(ctx.home);
|
|
18
44
|
const teamId = selectedOnboardTeam(journal);
|
|
19
45
|
if (!cfg || !teamId)
|
|
20
46
|
return waiting("required_values_unverified");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
const read = async (timeoutMs, readSignal = signal) => {
|
|
48
|
+
let loaded;
|
|
49
|
+
try {
|
|
50
|
+
// The contract transport owns another timeout. Carry our caller's cancellation through
|
|
51
|
+
// its fetch too, including response-body reads and any credential refresh.
|
|
52
|
+
const readCtx = readSignal
|
|
53
|
+
? {
|
|
54
|
+
...ctx,
|
|
55
|
+
fetch: (input, init) => ctx.fetch(input, {
|
|
56
|
+
...init,
|
|
57
|
+
signal: init?.signal
|
|
58
|
+
? AbortSignal.any([readSignal, init.signal])
|
|
59
|
+
: readSignal,
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
62
|
+
: ctx;
|
|
63
|
+
loaded = await loadFreshTeamContract(readCtx, cfg, [teamId], readSignal, timeoutMs);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { kind: "unverified" };
|
|
67
|
+
}
|
|
68
|
+
if (loaded.source === "cache")
|
|
69
|
+
return { kind: "unverified" };
|
|
70
|
+
const team = loaded.doc.teams.find((row) => row.id === teamId);
|
|
71
|
+
const readiness = team?.readiness;
|
|
72
|
+
const fresh = readiness?.checkedAt != null &&
|
|
73
|
+
readiness.expiresAt != null &&
|
|
74
|
+
readiness.expiresAt > ctx.now().getTime();
|
|
75
|
+
// A verdict the cloud could not bring up to date says nothing either way.
|
|
76
|
+
if (!readiness || !fresh)
|
|
77
|
+
return { kind: "unverified" };
|
|
78
|
+
const check = readiness.checks.find((row) => row.id === "required_values");
|
|
79
|
+
// An older cloud may not send this check at all: that says nothing, it is not an unread.
|
|
80
|
+
if (!check)
|
|
81
|
+
return { kind: "unverified" };
|
|
82
|
+
if (check.state === "pass")
|
|
83
|
+
return { kind: "pass" };
|
|
84
|
+
if (check.state === "fail")
|
|
85
|
+
return { kind: "fail", check };
|
|
86
|
+
return { kind: "unread", repositories: teamRepositories(team, check) };
|
|
87
|
+
};
|
|
88
|
+
let reading = await read();
|
|
89
|
+
if (reading.kind === "unread" && deps.sleep) {
|
|
90
|
+
deps.message?.(`Waiting for Catalyst to read the settings of ${named(reading.repositories)} (up to 1 minute)…`);
|
|
91
|
+
// One deadline covers sleep, readiness refresh and contract/body reads. Attempts also
|
|
92
|
+
// bound an injected sleep whose clock stands still.
|
|
93
|
+
const deadline = Date.now() + VALUES_WAIT_MS;
|
|
94
|
+
const owned = new AbortController();
|
|
95
|
+
const waitSignal = signal ? AbortSignal.any([signal, owned.signal]) : owned.signal;
|
|
96
|
+
let stop = () => { };
|
|
97
|
+
const stopped = new Promise((resolve) => {
|
|
98
|
+
stop = () => {
|
|
99
|
+
owned.abort();
|
|
100
|
+
resolve(null);
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const timer = setTimeout(stop, VALUES_WAIT_MS);
|
|
104
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
105
|
+
try {
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
stop();
|
|
108
|
+
for (let attempt = 0; reading.kind === "unread" &&
|
|
109
|
+
attempt < VALUES_WAIT_MS / VALUES_POLL_MS &&
|
|
110
|
+
Date.now() < deadline &&
|
|
111
|
+
!waitSignal.aborted; attempt++) {
|
|
112
|
+
// Ctrl-C or expiry ends either stage at once, even if an injected promise stalls.
|
|
113
|
+
await Promise.race([
|
|
114
|
+
deps.sleep(Math.min(VALUES_POLL_MS, Math.max(0, deadline - Date.now()))),
|
|
115
|
+
stopped,
|
|
116
|
+
]);
|
|
117
|
+
if (waitSignal.aborted || Date.now() >= deadline)
|
|
118
|
+
break;
|
|
119
|
+
const next = await Promise.race([
|
|
120
|
+
read(Math.min(WAIT_READ_TIMEOUT_MS, deadline - Date.now()), waitSignal),
|
|
121
|
+
stopped,
|
|
122
|
+
]);
|
|
123
|
+
// A result after expiry says nothing about the last observed unread verdict.
|
|
124
|
+
if (waitSignal.aborted || Date.now() >= deadline || !next)
|
|
125
|
+
break;
|
|
126
|
+
reading = next;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
signal?.removeEventListener("abort", stop);
|
|
132
|
+
owned.abort();
|
|
133
|
+
}
|
|
27
134
|
}
|
|
28
|
-
if (
|
|
29
|
-
return waiting("
|
|
30
|
-
|
|
31
|
-
const fresh = readiness?.checkedAt != null &&
|
|
32
|
-
readiness.expiresAt != null &&
|
|
33
|
-
readiness.expiresAt > ctx.now().getTime();
|
|
34
|
-
const check = fresh
|
|
35
|
-
? readiness.checks.find((row) => row.id === "required_values")
|
|
36
|
-
: undefined;
|
|
37
|
-
if (check?.state === "pass")
|
|
135
|
+
if (signal?.aborted)
|
|
136
|
+
return waiting("interrupted");
|
|
137
|
+
if (reading.kind === "pass")
|
|
38
138
|
return { state: "done", evidence: { team: teamId } };
|
|
39
|
-
if (
|
|
40
|
-
return waiting("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
139
|
+
if (reading.kind === "fail")
|
|
140
|
+
return waiting("required_values_missing", {
|
|
141
|
+
team: teamId,
|
|
142
|
+
requiredValues: JSON.stringify(valuesFacts(reading.check)),
|
|
143
|
+
});
|
|
144
|
+
if (reading.kind === "unread")
|
|
145
|
+
return waiting("required_values_unread", {
|
|
146
|
+
team: teamId,
|
|
147
|
+
repositories: named(reading.repositories),
|
|
148
|
+
});
|
|
149
|
+
return waiting("required_values_unverified");
|
|
45
150
|
},
|
|
46
151
|
};
|
|
47
152
|
}
|
package/dist/onboard-workflow.js
CHANGED
|
@@ -145,13 +145,36 @@ export function observeOnboardWorkflow(value, team, now) {
|
|
|
145
145
|
: {}),
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
-
const
|
|
148
|
+
const plainText = (text) => text
|
|
149
149
|
.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2066-\u2069\ufeff]/g, "")
|
|
150
|
-
.trim()
|
|
151
|
-
|
|
150
|
+
.trim();
|
|
151
|
+
const label = (text) => plainText(text).slice(0, 80);
|
|
152
152
|
const listed = (names) => names.length > 10
|
|
153
153
|
? `${names.slice(0, 10).join(", ")}, and ${names.length - 10} more`
|
|
154
154
|
: names.join(", ");
|
|
155
|
+
function unfinishedLabelLines(plan) {
|
|
156
|
+
return [
|
|
157
|
+
...plan.labels.filter((row) => row.outcome === "refused").map((row) => {
|
|
158
|
+
const name = plainText(row.name);
|
|
159
|
+
const reason = plainText(row.reason ?? "Linear did not supply a reason.");
|
|
160
|
+
const guidance = name === "review-followup"
|
|
161
|
+
? "Optional for the cloud workflow; no action is needed to finish setup."
|
|
162
|
+
: [
|
|
163
|
+
name === "catalyst-not-an-ask"
|
|
164
|
+
? "Required for the cloud workflow. Without this label, work held by the ask-shape check cannot be released."
|
|
165
|
+
: "Only missing required labels block workflow readiness.",
|
|
166
|
+
"Ask a Linear workspace administrator to resolve the reported refusal and make this label available to the team, then run catalyst onboard.",
|
|
167
|
+
].join(" ");
|
|
168
|
+
return `Linear refused label: ${name}. ${reason} ${guidance}`;
|
|
169
|
+
}),
|
|
170
|
+
...(plan.labelsNotCreated ?? []).map((row) => {
|
|
171
|
+
const entry = object(row);
|
|
172
|
+
const name = typeof entry?.name === "string" ? plainText(entry.name) : "";
|
|
173
|
+
const reason = typeof entry?.reason === "string" ? plainText(entry.reason) : "";
|
|
174
|
+
return `Label not added: ${name || "Unnamed label"}. ${reason || "The server did not supply a reason; run catalyst onboard again to retry."}`;
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
155
178
|
/** The adopt preview in plain words: stages and labels it creates or keeps. Linear's names are data. */
|
|
156
179
|
export function adoptPlanLines(plan) {
|
|
157
180
|
const group = (rows, outcome) => rows.filter((row) => row.outcome === outcome).map((row) => label(row.name));
|
|
@@ -161,11 +184,10 @@ export function adoptPlanLines(plan) {
|
|
|
161
184
|
["Linear refused stages", group(plan.stages, "refused")],
|
|
162
185
|
["Create labels", group(plan.labels, "would-create")],
|
|
163
186
|
["Labels already present", group(plan.labels, "already-present")],
|
|
164
|
-
["Linear refused labels", group(plan.labels, "refused")],
|
|
165
187
|
]
|
|
166
188
|
.filter(([, names]) => names.length > 0)
|
|
167
189
|
.map(([title, names]) => `${title}: ${listed(names)}`)
|
|
168
|
-
.concat(plan.unfilledLoadBearing?.length ? [`Required work stages still missing: ${plan.unfilledLoadBearing.length}`] : [], (plan.provenanceGaps?.length ?? 0) + (plan.labelProvenanceGaps?.length ?? 0) ? ["Some existing states or labels have no recorded creator; setup keeps them"] : [], plan
|
|
190
|
+
.concat(plan.unfilledLoadBearing?.length ? [`Required work stages still missing: ${plan.unfilledLoadBearing.length}`] : [], (plan.provenanceGaps?.length ?? 0) + (plan.labelProvenanceGaps?.length ?? 0) ? ["Some existing states or labels have no recorded creator; setup keeps them"] : [], unfinishedLabelLines(plan));
|
|
169
191
|
}
|
|
170
192
|
/** The only adopt bodies setup sends: this team, and the approved mode and hash. */
|
|
171
193
|
export function adoptRequestAllowed(body, team, allow) {
|
|
@@ -391,7 +413,7 @@ export function onboardWorkflowVerificationAdapter(input = {}) {
|
|
|
391
413
|
: {}),
|
|
392
414
|
},
|
|
393
415
|
};
|
|
394
|
-
}, () => input.message?.(`${onboardTeamKey(journal) ?? "This team"}
|
|
416
|
+
}, () => input.message?.(`${onboardTeamKey(journal) ?? "This team"} has every required state and label`));
|
|
395
417
|
if (!adopts)
|
|
396
418
|
return { check };
|
|
397
419
|
// A local non-admin never gets here; a 403 means the seat changed after login.
|
|
@@ -440,7 +462,10 @@ export function onboardWorkflowVerificationAdapter(input = {}) {
|
|
|
440
462
|
}
|
|
441
463
|
const created = (rows) => rows.filter((row) => row.outcome === "created").length;
|
|
442
464
|
const stages = created(reply.stages), labels = created(reply.labels);
|
|
443
|
-
summary =
|
|
465
|
+
summary = [
|
|
466
|
+
`Applied the Catalyst workflow to ${key}: created ${stages} ${stages === 1 ? "stage" : "stages"} and ${labels} ${labels === 1 ? "label" : "labels"}.`,
|
|
467
|
+
...unfinishedLabelLines(reply),
|
|
468
|
+
].join("\n");
|
|
444
469
|
return { state: "done" };
|
|
445
470
|
}, () => input.message?.(summary));
|
|
446
471
|
// Only the server's own plan-stale answer plans again; a later identity refusal does not.
|
package/dist/onboard.js
CHANGED
|
@@ -135,6 +135,8 @@ export const ONBOARD_DEPENDENCIES = {
|
|
|
135
135
|
"linear.automations",
|
|
136
136
|
"accounts",
|
|
137
137
|
"capacity",
|
|
138
|
+
// CTC-4744: no work is offered while the cloud is still reading the settings (dependencySettled).
|
|
139
|
+
"values",
|
|
138
140
|
],
|
|
139
141
|
};
|
|
140
142
|
const ADMIN_STEPS = new Set([
|
|
@@ -652,6 +654,18 @@ function sanitizedResult(id, result, at) {
|
|
|
652
654
|
row.at = at;
|
|
653
655
|
return row;
|
|
654
656
|
}
|
|
657
|
+
/** CTC-4744: whether a step a later one depends on has settled. Only the cloud actively reading
|
|
658
|
+
* the settings (`required_values_unread`, after setup's own one-minute wait) holds a first
|
|
659
|
+
* ticket. A finished check that found missing values has settled: the first phase needs no
|
|
660
|
+
* values, and the missing ones are named. So has one that could not be checked at all (an older
|
|
661
|
+
* cloud, a cached contract): holding work on it would hold it for good. */
|
|
662
|
+
export function dependencySettled(step) {
|
|
663
|
+
return (stepSatisfied(step) ||
|
|
664
|
+
(step?.id === "values" &&
|
|
665
|
+
step.state === "waiting" &&
|
|
666
|
+
step.reason !== "required_values_unread" &&
|
|
667
|
+
step.reason !== "interrupted"));
|
|
668
|
+
}
|
|
655
669
|
export function stepSatisfied(step) {
|
|
656
670
|
return (step?.state === "done" ||
|
|
657
671
|
(step?.state === "skipped" &&
|
|
@@ -1315,7 +1329,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1315
1329
|
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1316
1330
|
continue;
|
|
1317
1331
|
}
|
|
1318
|
-
const missing = (ONBOARD_DEPENDENCIES[id] ?? []).find((parent) => !
|
|
1332
|
+
const missing = (ONBOARD_DEPENDENCIES[id] ?? []).find((parent) => !dependencySettled(journalStep(journal, parent)));
|
|
1319
1333
|
if (missing) {
|
|
1320
1334
|
recordStep({
|
|
1321
1335
|
id,
|
package/dist/ready-copy.js
CHANGED
|
@@ -299,8 +299,8 @@ export function teamCheckCopy(baseUrl, key, id, state, valuesFix) {
|
|
|
299
299
|
case "required_values":
|
|
300
300
|
return unknown
|
|
301
301
|
? {
|
|
302
|
-
line: team(`Catalyst hasn't
|
|
303
|
-
fix: "
|
|
302
|
+
line: team(`Catalyst hasn't finished reading the settings of ${key}'s repositories yet.`),
|
|
303
|
+
fix: "nothing to do; Catalyst reads them on its own. Run catalyst ready again in a minute to see the result",
|
|
304
304
|
}
|
|
305
305
|
: {
|
|
306
306
|
line: team(`A variable ${key}'s repository needs has no value, so Catalyst can't run its work.`),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ONBOARD_DEPENDENCIES, ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
|
|
1
|
+
import { dependencySettled, ONBOARD_DEPENDENCIES, ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
|
|
2
2
|
import { dispatchStageName, onboardReasonText } from "./onboard-next.js";
|
|
3
3
|
/** The step numbers `--json` reports. People read SETUP_PART_NUMBERS instead (CTC-4680). */
|
|
4
4
|
export const SETUP_NUMBERS = {
|
|
@@ -250,6 +250,8 @@ function unfinished(step) {
|
|
|
250
250
|
return step.state === "skipped" ? "not chosen" : "not done yet";
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
+
/** CTC-4744: reasons a step waits on the cloud's own work, never on the person or an admin. */
|
|
254
|
+
export const CLOUD_WORK = new Set(["required_values_unread"]);
|
|
253
255
|
export function setupStepView(step, journal, required = false) {
|
|
254
256
|
required ||= step.id === "runner" && step.evidence?.selected === true;
|
|
255
257
|
let mark = step.state === "done"
|
|
@@ -265,12 +267,16 @@ export function setupStepView(step, journal, required = false) {
|
|
|
265
267
|
? "skip"
|
|
266
268
|
: "act";
|
|
267
269
|
let outcome = step.state === "done" ? succeeded(step) : unfinished(step);
|
|
268
|
-
if (
|
|
270
|
+
if (step.state !== "done" && CLOUD_WORK.has(step.reason ?? "")) {
|
|
271
|
+
mark = "later";
|
|
272
|
+
outcome = "still in progress; Catalyst is reading the settings";
|
|
273
|
+
}
|
|
274
|
+
else if (mark === "later") {
|
|
269
275
|
const waitsOn = step.reason === "github_install_pending"
|
|
270
276
|
? ["github.install"]
|
|
271
277
|
: (ONBOARD_DEPENDENCIES[step.id] ?? []).filter((id) => {
|
|
272
278
|
const s = journal?.steps.find((s) => s.id === id);
|
|
273
|
-
return s?.state !== "
|
|
279
|
+
return s?.state !== "skipped" && !dependencySettled(s);
|
|
274
280
|
});
|
|
275
281
|
// Numbers restart in each part: this part's steps come bare, then each other part once.
|
|
276
282
|
const byPart = new Map();
|
|
@@ -361,7 +367,7 @@ export function setupPartProgress(journal, scope,
|
|
|
361
367
|
/** "run" before readiness is known: a member's admin skips count aside, neither done nor owed.
|
|
362
368
|
* At the end they count done only when readiness passed. */
|
|
363
369
|
moment = "end") {
|
|
364
|
-
const empty = () => ({ total: 0, done: 0, needs: 0, failed: 0, admin: 0, aside: 0 });
|
|
370
|
+
const empty = () => ({ total: 0, done: 0, needs: 0, working: 0, failed: 0, admin: 0, aside: 0 });
|
|
365
371
|
const progress = { 1: empty(), 2: empty(), 3: empty() };
|
|
366
372
|
const readinessPassed = journal.steps.find((s) => s.id === "ready")?.state === "done";
|
|
367
373
|
const rows = new Map();
|
|
@@ -379,6 +385,7 @@ moment = "end") {
|
|
|
379
385
|
{
|
|
380
386
|
// A member's own skip (no --team) is theirs to finish, so it is never counted done.
|
|
381
387
|
mark: memberOwnSkip(step) ? "act" : setupStepView(step, journal).mark,
|
|
388
|
+
working: step.state !== "done" && CLOUD_WORK.has(step.reason ?? ""),
|
|
382
389
|
kind: setupWaitsOnAdmin(step, journal)
|
|
383
390
|
? "admin"
|
|
384
391
|
: step.reason === "member_scope" &&
|
|
@@ -400,6 +407,8 @@ moment = "end") {
|
|
|
400
407
|
if (own.some((s) => s.mark === "fail"))
|
|
401
408
|
part.failed++;
|
|
402
409
|
}
|
|
410
|
+
else if (own.some((s) => s.working))
|
|
411
|
+
part.working++;
|
|
403
412
|
else if (steps.some((s) => s.kind === "admin"))
|
|
404
413
|
part.admin++;
|
|
405
414
|
else if (steps.some((s) => s.kind === "aside"))
|
|
@@ -445,6 +454,8 @@ export function setupTrackerRows(progress, moment) {
|
|
|
445
454
|
return row("done", "");
|
|
446
455
|
if (got.needs)
|
|
447
456
|
return row(needsMark, `${steps(got.needs)} ${got.needs === 1 ? "needs" : "need"} someone`);
|
|
457
|
+
if (got.working)
|
|
458
|
+
return row("later", `${steps(got.working)} still in progress`);
|
|
448
459
|
if (got.admin)
|
|
449
460
|
return row("act", waiting(got.admin));
|
|
450
461
|
if (got.aside)
|
|
@@ -532,10 +543,13 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
532
543
|
const roots = journal.steps
|
|
533
544
|
.filter((step) => {
|
|
534
545
|
const m = setupStepView(step, journal, required.has(step.id)).mark;
|
|
535
|
-
return ((m === "act" ||
|
|
546
|
+
return ((m === "act" ||
|
|
547
|
+
m === "fail" ||
|
|
548
|
+
adminLeft.has(step.id) ||
|
|
549
|
+
(step.state !== "done" && CLOUD_WORK.has(step.reason ?? ""))) &&
|
|
536
550
|
(ONBOARD_DEPENDENCIES[step.id] ?? []).every((id) => {
|
|
537
551
|
const parent = journal.steps.find((s) => s.id === id);
|
|
538
|
-
return parent?.state === "
|
|
552
|
+
return parent?.state === "skipped" || dependencySettled(parent);
|
|
539
553
|
}));
|
|
540
554
|
})
|
|
541
555
|
.filter((step) => step.id !== "ready")
|
|
@@ -545,6 +559,8 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
545
559
|
const actions = unique
|
|
546
560
|
.slice(0, 5)
|
|
547
561
|
.map((step) => ({ id: step.id, text: action(step, base, journal) }));
|
|
562
|
+
// CTC-4744: the Next line is about what the person does; the cloud's own work is listed apart.
|
|
563
|
+
const personal = actions.filter((row) => !CLOUD_WORK.has(journal.steps.find((s) => s.id === row.id)?.reason ?? ""));
|
|
548
564
|
const readiness = journal.steps.find((step) => step.id === "ready");
|
|
549
565
|
const readinessCause = !paused &&
|
|
550
566
|
!actions.length &&
|
|
@@ -578,19 +594,21 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
578
594
|
? `Follow ${ticketUrl ?? key} in Linear; Catalyst comments there as each phase finishes.`
|
|
579
595
|
: heading === "Setup complete"
|
|
580
596
|
? `Next: move a ticket to ${dispatchStageName(firstTicket?.evidence?.stage)} in Linear.`
|
|
581
|
-
: journal.exit === 0 &&
|
|
582
|
-
? `Next: ${
|
|
583
|
-
:
|
|
597
|
+
: journal.exit === 0 && personal[0]
|
|
598
|
+
? `Next: ${personal[0].text[0].toLowerCase() + personal[0].text.slice(1)}`
|
|
599
|
+
: personal.length === 1
|
|
584
600
|
? "Next: run catalyst onboard after you finish 1."
|
|
585
|
-
:
|
|
601
|
+
: personal.length === 2
|
|
586
602
|
? "Next: run catalyst onboard after you finish 1 and 2."
|
|
587
|
-
:
|
|
603
|
+
: personal.length
|
|
588
604
|
? "Next: run catalyst onboard after you finish 1 to " +
|
|
589
|
-
|
|
605
|
+
personal.length +
|
|
590
606
|
"."
|
|
591
|
-
:
|
|
592
|
-
? "Next:
|
|
593
|
-
:
|
|
607
|
+
: actions.length
|
|
608
|
+
? "Next: nothing for you to do. catalyst ready shows when Catalyst has read the settings."
|
|
609
|
+
: readyStep?.reason === "member_team_required"
|
|
610
|
+
? "Next: run catalyst onboard --team <KEY> to check again."
|
|
611
|
+
: "Next: run catalyst onboard to check again.";
|
|
594
612
|
return {
|
|
595
613
|
heading,
|
|
596
614
|
readinessCause,
|
|
@@ -696,31 +714,34 @@ export function onboardJsonView(journal, base, paused = false, options = {}) {
|
|
|
696
714
|
number: SETUP_NUMBERS[action.id] ?? 0,
|
|
697
715
|
text: text.charAt(0).toUpperCase() + text.slice(1),
|
|
698
716
|
...(url ? { url } : {}),
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
"github
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
717
|
+
// CTC-4744: the cloud's own work is nobody's to do.
|
|
718
|
+
who: CLOUD_WORK.has(step.reason ?? "")
|
|
719
|
+
? "catalyst"
|
|
720
|
+
: admin
|
|
721
|
+
? "admin"
|
|
722
|
+
: action.id === "github.install"
|
|
723
|
+
? "github-org-admin"
|
|
724
|
+
: [
|
|
725
|
+
"linear.personal",
|
|
726
|
+
"github.personal",
|
|
727
|
+
"signin",
|
|
728
|
+
"linear.team",
|
|
729
|
+
"github.repos",
|
|
730
|
+
"projects",
|
|
731
|
+
"first-ticket",
|
|
732
|
+
"settings",
|
|
733
|
+
"values",
|
|
734
|
+
"accounts",
|
|
735
|
+
"runner",
|
|
736
|
+
"machine",
|
|
737
|
+
"cli",
|
|
738
|
+
"skills",
|
|
739
|
+
"legacy",
|
|
740
|
+
"daemon",
|
|
741
|
+
"housekeeping",
|
|
742
|
+
].includes(action.id)
|
|
743
|
+
? "you"
|
|
744
|
+
: "admin",
|
|
724
745
|
};
|
|
725
746
|
});
|
|
726
747
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Set up Catalyst Cloud, check your tickets and manage your account from the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,18 +48,21 @@
|
|
|
48
48
|
"paths:check": "node scripts/vendor-paths.mjs --check",
|
|
49
49
|
"prose:check": "node scripts/check-public-prose.mjs",
|
|
50
50
|
"settings:check": "node scripts/vendor-settings.mjs --check",
|
|
51
|
-
"self-host:check": "node scripts/vendor-self-host.mjs --check"
|
|
51
|
+
"self-host:check": "node scripts/vendor-self-host.mjs --check",
|
|
52
|
+
"release:check": "node scripts/release-train-check.mjs && node --test scripts/release-train-check.test.mjs",
|
|
53
|
+
"prepublishOnly": "node scripts/release-train-check.mjs --publish cli"
|
|
52
54
|
},
|
|
53
55
|
"dependencies": {
|
|
54
|
-
"@catalyst-cloud/sdk": "0.
|
|
56
|
+
"@catalyst-cloud/sdk": "0.16.0",
|
|
55
57
|
"@clack/prompts": "1.8.1",
|
|
56
58
|
"fast-string-width": "3.0.2",
|
|
57
59
|
"fast-wrap-ansi": "0.2.2",
|
|
58
60
|
"smol-toml": "^1.7.1",
|
|
59
|
-
"yaml": "^2.9.0"
|
|
61
|
+
"yaml": "^2.9.0",
|
|
62
|
+
"@catalyst-cloud/sdk-replica-node": "0.16.0"
|
|
60
63
|
},
|
|
61
64
|
"devDependencies": {
|
|
62
|
-
"@catalyst-cloud/schema": "0.
|
|
65
|
+
"@catalyst-cloud/schema": "0.16.0",
|
|
63
66
|
"@types/node": "^26.3.0",
|
|
64
67
|
"@vitest/coverage-v8": "^4.1.11",
|
|
65
68
|
"typescript": "^7.0.2",
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst's GitHub
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Catalyst's view of Linear on the customer's own cloud account. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the 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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst Linear
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Set up Catalyst Cloud and say whether setup is working. Walks a person from nothing to their first ticket running, one step at a time: the coding account, the project, the integrations and connected accounts, each repository's settings file, then one real ticket moving. Also logs this machine in, reads the readiness verdict with who can fix each failure, and checks the optional local replica. Use when someone says "set me up", "onboard me", "I just signed up", "get me started", "what do I do first", "log me in", "which account is this machine on", "am I set up", "what is missing", "why does nothing happen" or "is the replica running", when a login expired, or when a skill script exits 2 saying this machine is not connected. Does every step a key can do through the catalyst CLI, hands over the exact page for the steps only a browser can do, and never claims a step it did not watch succeed.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Onboard me
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ The assistant prepares `.catalyst/catalyst.toml` from the repository. Never ask
|
|
|
14
14
|
|
|
15
15
|
Show a short preview with the project/team, setup and verify commands, variable names, which are marked secret, and confirmation that every variable is optional. Do not include values. Ask the person to approve this exact draft. Before approval, do not write or replace `.catalyst/catalyst.toml`, remove `catalyst.env.json`, or open a settings PR.
|
|
16
16
|
|
|
17
|
-
After approval, write the validated file. When converting a legacy-only repository, remove `catalyst.env.json` in the same change. Open the settings PR, then offer to watch it merge.
|
|
17
|
+
After approval, write the validated file. When converting a legacy-only repository, remove `catalyst.env.json` in the same change. Open the settings PR, then offer to watch it merge. A reviewed merge is the approval. Catalyst approves the exact merged revision, with no second step, when someone other than the PR's author approved the merged head commit, or when a workspace owner or admin merged the PR. Tell the person this before they merge: the approving review is their consent to the setup commands and secret names in the file, so a reviewer should read it like code. A direct push, an unreviewed merge, or an approval left before the last commit leaves the revision proposed. Only then give an owner or admin a direct link to approve it at Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision. The direct route is `/settings/projects/$projectId/repositories/$repoId/environment/declaration`; use the project and repository ids from Catalyst instead of guessing them.
|
|
18
18
|
|
|
19
19
|
## File shape
|
|
20
20
|
|
|
@@ -44,7 +44,7 @@ A team is `ready`, `degraded`, `blocked` or `unchecked` (no pass yet, a note). S
|
|
|
44
44
|
| `writes_land` | Catalyst has written to the team | `no_write_observed` clears on the first move; a refused write: check the connection and permissions | owner or admin when refused; otherwise nobody |
|
|
45
45
|
| `webhook_covers_team` | the team's events arrive | `no_delivery_observed` clears once a registered repository's events flow | owner or admin, by registering the repository |
|
|
46
46
|
| `hosts_current` | no connected host runs an older mapping | `references/what-a-phase-needs.md`; `no_host_connected` is waiting | whoever runs that host |
|
|
47
|
-
| `environment_declared` | the default repository's `.catalyst/catalyst.toml` is ingested, valid and approved | the reason names the step: register a default repository, commit or fix the file, or approve
|
|
47
|
+
| `environment_declared` | the default repository's `.catalyst/catalyst.toml` is ingested, valid and approved | the reason names the step: register a default repository, commit or fix the file through a reviewed pull request, or approve a revision that merged without one (`references/declaring-a-repository.md`) | owner or admin, except committing the file, which is whoever can push |
|
|
48
48
|
| `tools_resolvable` | every MCP server and CLI the declaration names resolves | `tool_reference_unresolved`: add the secret or fix the declaration; `toolchain_cli_missing` is not self-service | owner or admin, in the declaration or Settings → Environment |
|
|
49
49
|
| `reviewer_required` | the repository can merge under its policy | a strict policy with no reviewer: configure one at Settings → Your projects → the project → Repositories → the repository → Code reviews, or relax the policy; only the merge waits | owner or admin, in settings |
|
|
50
50
|
| `reviewer_configured` | a code reviewer is configured at all | configure one there; a fail never blocks or degrades | owner or admin, in settings; nobody is required when it fails |
|
|
@@ -1050,7 +1050,7 @@ const DECL_REASONS = {
|
|
|
1050
1050
|
page: null,
|
|
1051
1051
|
},
|
|
1052
1052
|
declaration_awaiting_approval: {
|
|
1053
|
-
text: "the declaration is proposed and waits for approval: Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision",
|
|
1053
|
+
text: "the declaration is proposed and waits for approval, because no review approved it (a non-author approval of the merged head, or a merge by an owner or admin): approve it at Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision",
|
|
1054
1054
|
who: "a workspace owner or admin",
|
|
1055
1055
|
page: "/settings/projects",
|
|
1056
1056
|
},
|
|
@@ -1058,7 +1058,7 @@ const DECL_REASONS = {
|
|
|
1058
1058
|
const DECL_DO =
|
|
1059
1059
|
"write .catalyst/catalyst.toml with references/declaring-a-repository.md, then open a pull request";
|
|
1060
1060
|
let declNext =
|
|
1061
|
-
"
|
|
1061
|
+
"merge .catalyst/catalyst.toml through a pull request that someone other than the author approves; that review approves it";
|
|
1062
1062
|
if (!connected) {
|
|
1063
1063
|
add(
|
|
1064
1064
|
"repository declarations",
|
|
@@ -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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — 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.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Unstick
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
The human's decision inbox on Catalyst Cloud, and the one way an agent raises a decision on their behalf. Use when the person asks "what needs me?", "what am I blocking?", or when active work is gated on a choice only they can make. Lists open asks ranked by how much open work each one holds, files an ask through the cloud's ask route with the account's own template, and records the answer so the held work releases. Never answers as the human, never duplicates an open ask.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What needs me
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ An ask is one decision only the human can make, filed as a ticket in their own L
|
|
|
15
15
|
Scripts are run, never read. Each prints `--help`; exit 2 means this machine is not connected (the `catalyst-onboard` skill connects it), exit 1 means the check itself failed or an argument was missing.
|
|
16
16
|
|
|
17
17
|
- `node scripts/inbox.mjs --help`: the open asks assigned to the connected person, most open tickets held first, then the oldest; `--anyone` for everyone's; `--json` for `{scope, asks}`.
|
|
18
|
-
- `node scripts/raise.mjs --help`: file one decision: `--team`, `--title`, `--option` (repeated), `--default`, and `--blocks` (repeated) or `--nothing-to-block`.
|
|
18
|
+
- `node scripts/raise.mjs --help`: file one decision: `--team`, `--title`, `--option` (repeated), `--default`, and `--blocks` (repeated) or `--nothing-to-block`. When the ask decides whether a held PR may merge, `--gates-pr` (repeated) with `--released-by <letter>` makes the answer lift that PR's hold label.
|
|
19
19
|
- `node scripts/settle.mjs --help`: record the answering comment on an ask and post a release note on every ticket it held; `--close` moves the ask to the done slot.
|
|
20
20
|
|
|
21
21
|
## Load on demand
|
|
@@ -18,6 +18,17 @@ Brainstorming, design back-and-forth, a question the human asked first, a retry-
|
|
|
18
18
|
|
|
19
19
|
`--ask-key` is an idempotency key: a re-run with the same key does not file a second ask.
|
|
20
20
|
|
|
21
|
+
## When the answer releases a PR hold
|
|
22
|
+
|
|
23
|
+
When the decision is whether a held PR may merge, name the PR so the answer lifts the hold itself:
|
|
24
|
+
|
|
25
|
+
- `--gates-pr <n>` (repeated): each PR the answer releases.
|
|
26
|
+
- `--released-by <letter>` (repeated, required with `--gates-pr`): the option letters that release it. Options are lettered A, B, C in the order you gave them, and a letter outside that range is refused.
|
|
27
|
+
- `--gates-label hold|hold:preview` (repeated, default `hold`): the labels the answer removes. No other label is accepted.
|
|
28
|
+
- `--gates-repo <owner/name>`: the PR's repository. It defaults to the account's repository when the account registers exactly one, and must be one the account registers.
|
|
29
|
+
|
|
30
|
+
An answer with any other letter leaves the labels in place.
|
|
31
|
+
|
|
21
32
|
## One ask per decision
|
|
22
33
|
|
|
23
34
|
Run `node scripts/inbox.mjs` first. When the same decision is already open, attach the new held tickets to it (the `catalyst-linear` skill adds the relation or a comment) rather than filing a twin that splits its urgency.
|
|
@@ -14,12 +14,16 @@ const SPEC = {
|
|
|
14
14
|
blocks: { value: true, repeat: true, help: "a ticket this decision holds (repeat per ticket)" },
|
|
15
15
|
"nothing-to-block": { value: false, help: "declare that no ticket is held (the ask then never shows in Waiting on me)" },
|
|
16
16
|
"ask-key": { value: true, help: "idempotency key so a re-run does not file a second ask" },
|
|
17
|
+
"gates-pr": { value: true, repeat: true, help: "a PR whose hold label the answer removes (repeat per PR; needs --released-by)" },
|
|
18
|
+
"gates-label": { value: true, repeat: true, help: "the hold the answer removes: hold or hold:preview (repeatable; default hold)" },
|
|
19
|
+
"released-by": { value: true, repeat: true, help: "an option letter whose answer releases the hold (repeat per letter)" },
|
|
20
|
+
"gates-repo": { value: true, help: "the --gates-pr repository as owner/name (default: the account's only repository)" },
|
|
17
21
|
json: { value: false, help: "print the cloud's response as JSON" },
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
const { help, flags } = parseFlags(process.argv.slice(2), SPEC);
|
|
21
25
|
if (help) {
|
|
22
|
-
printHelp("node scripts/raise.mjs --team <key> --title <question> [--context <text>] [--option <text>]... [--default <text>] --blocks <ticket>... | --nothing-to-block [--ask-key <key>] [--json] [--help]", SPEC, [
|
|
26
|
+
printHelp("node scripts/raise.mjs --team <key> --title <question> [--context <text>] [--option <text>]... [--default <text>] --blocks <ticket>... | --nothing-to-block [--ask-key <key>] [--gates-pr <n>... --released-by <letter>... [--gates-label hold|hold:preview]... [--gates-repo <owner/name>]] [--json] [--help]", SPEC, [
|
|
23
27
|
"Search for an existing ask first (node scripts/inbox.mjs); one decision, one ask.",
|
|
24
28
|
"File BEFORE proceeding on the default. Cite the identifier only from this script's output.",
|
|
25
29
|
]);
|
|
@@ -41,6 +45,10 @@ if (flags.default) args.push("--default", flags.default);
|
|
|
41
45
|
for (const b of flags.blocks ?? []) args.push("--blocks", b);
|
|
42
46
|
if (flags["nothing-to-block"]) args.push("--nothing-to-block");
|
|
43
47
|
if (flags["ask-key"]) args.push("--ask-key", flags["ask-key"]);
|
|
48
|
+
for (const n of flags["gates-pr"] ?? []) args.push("--gates-pr", n);
|
|
49
|
+
for (const l of flags["gates-label"] ?? []) args.push("--gates-label", l);
|
|
50
|
+
for (const l of flags["released-by"] ?? []) args.push("--released-by", l);
|
|
51
|
+
if (flags["gates-repo"]) args.push("--gates-repo", flags["gates-repo"]);
|
|
44
52
|
args.push("--json");
|
|
45
53
|
|
|
46
54
|
const result = parseJson(mustRun(args, { quiet: true }).stdout, "ask raise");
|
|
@@ -49,5 +57,6 @@ if (flags.json) process.stdout.write(JSON.stringify(result) + "\n");
|
|
|
49
57
|
else {
|
|
50
58
|
process.stdout.write(`ask raised: ${id ?? "(no identifier returned)"}${flags.blocks ? ` — holds ${flags.blocks.join(", ")}` : " — holds nothing"}\n`);
|
|
51
59
|
if (flags.default) process.stdout.write(`default if silent: ${flags.default}\n`);
|
|
60
|
+
if (flags["gates-pr"]) process.stdout.write(`answer ${flags["released-by"].join(" or ")} releases the hold on PR ${flags["gates-pr"].map((n) => `#${n}`).join(", ")}\n`);
|
|
52
61
|
}
|
|
53
62
|
process.exit(0);
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
What environment variable names does this repository need, and where does each one come from? Scans the repository offline (no login, no network) and lists the names in three groups (build/test, deploy-only, bindings), each with where it was found, what uses it, and where a local value would come from. Never reads a value or prints one. Also checks the environment variable table in `.catalyst/catalyst.toml` offline. Use when someone asks "what does catalyst.env.json mean", "what env vars does this repo need", "why does the container need this", or before reviewing or writing the declaration.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What this repository needs
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Use for "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", "how does this work?", "why did it do that?", "how does it prioritise?" and "what does this setting do, where is it set?" in Catalyst Cloud, or asks for something to be done rather than known. Reads the contract via catalyst CLI. Read-only: route work/decisions, never answer as the human.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|