@catalyst-cloud/cli 0.16.29 → 0.16.30
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 +4 -0
- package/dist/args.js +15 -0
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +9 -0
- package/dist/lease.js +302 -0
- package/dist/onboard-existing.js +12 -3
- package/dist/onboard-next.js +14 -0
- package/dist/onboard-projects.js +3 -3
- package/dist/onboard-repositories.js +10 -3
- package/dist/onboard-ui.js +5 -1
- package/dist/onboard.js +3 -0
- package/dist/setup-onboard-copy.js +41 -2
- 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/run-this-project/references/stalls-and-escalation.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/skills/whats-happening/references/what-runs-next.md +1 -1
- package/skills/whats-happening/references/why-is-it-stuck.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.30
|
|
4
|
+
|
|
5
|
+
An agent seat can now hold a ticket while it works on it. `catalyst lease take <ticket> --agent <seat>` takes the ticket for that seat for a set time (`--ttl <minutes>`). `catalyst lease renew <ticket>` keeps it, and each renewal names one piece of progress: a commit (`--commit`), an artifact (`--artifact`), a finding (`--finding`) or a pull request (`--pr`). `catalyst lease release <ticket>` hands it back, and `catalyst lease show <ticket>` prints who holds it. A refused renewal, for example one that names no new progress, prints the reason and exits 1. A renewal or release refused because the lease already lapsed, or because another seat took the ticket, forgets the lease this machine stored. The machine stores only the seat name and the lease generation, in a file only you can read. New: `catalyst work available` lists, per team, the tickets nobody holds, with the last holder and their progress, and the open pull requests no live lease covers, drafts included. Both commands use your login. Setup run unattended now says what it skipped and names the exact flags each skipped choice needs. The skill references now say how to see whether a repository is paused, and who can pause or resume it.
|
|
6
|
+
|
|
3
7
|
## 0.16.29
|
|
4
8
|
|
|
5
9
|
The CLI now tells Catalyst Cloud which release you have installed: each request to Catalyst Cloud carries the installed version, and so does `catalyst watch` when it connects. Catalyst Cloud can then tell an out-of-date CLI from a current one. Nothing about your requests changes otherwise. On a Mac self-hosted machine, setup now installs a small agent that measures the Mac's disk, so the machine can take work instead of showing zero free slots. When a machine still shows zero free slots, setup and `catalyst ready` now say why: its disk has not been measured yet, or its free space is below the floor (25 GiB by default), with the numbers and the fix. An updated disk sampler also takes effect straight away: setup reloads the running disk agent whenever the sampler script changes, not only when its schedule file does.
|
package/dist/args.js
CHANGED
|
@@ -508,6 +508,19 @@ export const FLAG_TABLES = {
|
|
|
508
508
|
help: "with --remove: answer the question yes (a run with no terminal only reports otherwise)",
|
|
509
509
|
},
|
|
510
510
|
},
|
|
511
|
+
lease: {
|
|
512
|
+
agent: { value: true, help: "the seat's name, 1 to 40 of a-z, 0-9 and - (take: required; renew, release: when this machine holds the ticket for several seats)" },
|
|
513
|
+
ttl: { value: true, help: "take, renew: minutes until the lease lapses without a renewal (the cloud keeps it between 5 and 60; default 30)" },
|
|
514
|
+
commit: { value: true, help: "renew: the full 40-character sha of a commit you pushed" },
|
|
515
|
+
artifact: { value: true, help: "renew: a thoughts path or check-run id you wrote" },
|
|
516
|
+
finding: { value: true, help: "renew: a comment or review id you posted" },
|
|
517
|
+
pr: { value: true, help: "renew: the number of a pull request you opened" },
|
|
518
|
+
note: { value: true, help: "renew: a few words about the progress (not part of what counts as new)" },
|
|
519
|
+
generation: { value: true, help: "renew, release: the generation the take printed (default: the one this machine stored)" },
|
|
520
|
+
},
|
|
521
|
+
work: {
|
|
522
|
+
team: { value: true, help: "available: one team's key (default: every team in the workspace contract)" },
|
|
523
|
+
},
|
|
511
524
|
repo: {
|
|
512
525
|
reason: { value: true, help: "pause: required; resume: optional; 1 to 500 characters after trimming (owner or admin)" },
|
|
513
526
|
write: {
|
|
@@ -594,6 +607,8 @@ export const VERB_USAGE = {
|
|
|
594
607
|
messages: "messages read --ticket CTC-N [--since SEQUENCE] [--json]",
|
|
595
608
|
capabilities: "capabilities [--json]",
|
|
596
609
|
project: "project <list [--json] | wip-limit <get|set <n>|set default> [--team K] [--json]>",
|
|
610
|
+
lease: "lease <take <ticket> --agent <seat> [--ttl <minutes>] | renew <ticket> (--commit <sha>|--artifact <path or id>|--finding <id>|--pr <number>) [--note <text>] [--agent <seat>] [--generation <n>] | release <ticket> [--agent <seat>] [--generation <n>] | show <ticket>> [--json] (a seat's ticket lease: take, renew with new progress, release, read; needs login)",
|
|
611
|
+
work: "work available [--team K] [--json] (per team: tickets nobody holds, with the last holder and progress, and open pull requests no live lease covers, drafts included)",
|
|
597
612
|
repo: "repo <status|pause <owner/name> --reason <text>|resume <owner/name> [--reason <text>]|agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> [--json] (status: visible workspace repositories; agents-block/agent-setup: local checkout)",
|
|
598
613
|
legacy: "legacy [--remove [--data] [--yes]] [--json]",
|
|
599
614
|
connections: "connections personal <linear|github> <start|status> [--wait <seconds>] [--json]",
|
package/dist/capabilities.js
CHANGED
|
@@ -32,6 +32,8 @@ export const CAPABILITIES = [
|
|
|
32
32
|
{ verb: "project wip-limit set", does: "set a project's new-start WIP limit (0 to 9999, or back to the workspace default)", needs: "admin", routes: [agent("POST", "team-wip-limit")], since: "0.13.1" },
|
|
33
33
|
{ verb: "repo status", does: "list visible repositories with their state, pause details and projects", needs: "member", routes: [agent("GET", "repos")], since: "0.16.27" },
|
|
34
34
|
{ verb: "repo pause|resume", does: "pause or resume a repository as a workspace owner or admin", needs: "admin", routes: [agent("GET", "repos"), agent("POST", "repos/pause"), agent("POST", "repos/resume")], since: "0.16.27" },
|
|
35
|
+
{ verb: "lease take|renew|release|show", does: "hold the ticket a seat works through a ticket lease that lapses without new progress, instead of a label", needs: "member", routes: [agent("POST", "ticket-lease/take"), agent("POST", "ticket-lease/renew"), agent("POST", "ticket-lease/release"), agent("GET", "ticket-lease")], since: "0.16.30" },
|
|
36
|
+
{ verb: "work available", does: "list tickets nobody holds, with the last holder and progress, and open pull requests no lease covers", needs: "member", routes: [], since: "0.16.30" },
|
|
35
37
|
{ verb: "repo agents-block", does: "add or refresh the one Catalyst block in a checkout's AGENTS.md, in the working tree, for the person to commit and open as a pull request", needs: "member", routes: [], since: "0.13.1" },
|
|
36
38
|
{ verb: "repo agent-setup", does: "read a checkout's agent setup (AGENTS.md, CLAUDE.md, .agents and .claude skills and rules) and, on request, make it portable in the working tree", needs: "member", routes: [], since: "0.13.1" },
|
|
37
39
|
{ verb: "env shell", does: "open this repository's environment on this computer: Catalyst's image, the declared toolchain and variables (needs Docker; no login for Catalyst's image)", needs: "member", routes: [], since: "0.16.20" },
|
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,7 @@ import { cmdTeam } from "./team.js";
|
|
|
46
46
|
import { cmdCapabilities } from "./capabilities.js";
|
|
47
47
|
import { cmdProject } from "./project.js";
|
|
48
48
|
import { cmdRepo } from "./repo.js";
|
|
49
|
+
import { cmdLease, cmdWork } from "./lease.js";
|
|
49
50
|
import { cmdLegacy } from "./legacy.js";
|
|
50
51
|
import { cmdIdentity } from "./identity.js";
|
|
51
52
|
import { cmdMessages } from "./messages.js";
|
|
@@ -113,6 +114,8 @@ export function usageText() {
|
|
|
113
114
|
" catalyst repo pause <owner/name> --reason <text> | repo resume <owner/name> [--reason <text>] (owner or admin; reason: 1 to 500 trimmed characters)",
|
|
114
115
|
" catalyst repo status [--json] (visible workspace repositories and their paused state; needs login)",
|
|
115
116
|
" catalyst repo <agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> (a checkout's AGENTS.md block and portable agent layout; working tree only)",
|
|
117
|
+
" catalyst lease <take <ticket> --agent <seat> [--ttl <minutes>]|renew <ticket> --commit|--artifact|--finding|--pr <ref> [--note <text>]|release <ticket>|show <ticket>> [--json] (a seat's ticket lease; needs login)",
|
|
118
|
+
" catalyst work available [--team K] [--json] (tickets nobody holds and open pull requests no lease covers)",
|
|
116
119
|
" catalyst legacy [--remove [--data] [--yes]] (leftovers of the old local Catalyst runtime: list them; remove them only on a yes)",
|
|
117
120
|
" catalyst identity linear <status|options|set> [<linearUserId>] [--json]",
|
|
118
121
|
" catalyst messages read --ticket CTC-N [--since SEQUENCE] [--json] (messages sent to a ticket on your teams)",
|
|
@@ -548,6 +551,10 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
548
551
|
return await cmdProject(args, ctx);
|
|
549
552
|
case "repo":
|
|
550
553
|
return await cmdRepo(args, ctx);
|
|
554
|
+
case "lease":
|
|
555
|
+
return await cmdLease(args, ctx);
|
|
556
|
+
case "work":
|
|
557
|
+
return await cmdWork(args, ctx);
|
|
551
558
|
case "legacy":
|
|
552
559
|
return await cmdLegacy(args, ctx, { localSyncSelected: savedLocalSync(ctx), ...deps.legacy });
|
|
553
560
|
case "identity":
|
|
@@ -624,6 +631,8 @@ const VERB_HELP_KNOWN = Object.fromEntries([
|
|
|
624
631
|
"capabilities",
|
|
625
632
|
"project",
|
|
626
633
|
"repo",
|
|
634
|
+
"lease",
|
|
635
|
+
"work",
|
|
627
636
|
"legacy",
|
|
628
637
|
"onboard",
|
|
629
638
|
].map((v) => [v, true]));
|
package/dist/lease.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// lease.ts — `catalyst lease` and `catalyst work available` (CTC-6400): a seat or one-shot session
|
|
2
|
+
// holds the ticket it works through a ticket lease, never a label, and sees what work is free.
|
|
3
|
+
//
|
|
4
|
+
// lease take <TICKET> --agent <name> [--ttl <minutes>] POST /api/v1/agent/ticket-lease/take
|
|
5
|
+
// lease renew <TICKET> --commit|--artifact|--finding|--pr <ref> [--note <text>]
|
|
6
|
+
// POST /api/v1/agent/ticket-lease/renew
|
|
7
|
+
// lease release <TICKET> POST /api/v1/agent/ticket-lease/release
|
|
8
|
+
// lease show <TICKET> GET /api/v1/agent/ticket-lease?ticket=
|
|
9
|
+
// work available [--team K] GET /api/v1/work-eligibility?team=
|
|
10
|
+
//
|
|
11
|
+
// ⛔ THE KEY IS THE CREDENTIAL. The cloud mints no lease credential (CTC-6398): it derives the holder
|
|
12
|
+
// `seat:<keyId>:<agent>` from the login on every call. What this machine keeps per lease is only what
|
|
13
|
+
// the next call must name: the seat name and the generation the take returned, in a 0600 file under
|
|
14
|
+
// the config directory. Renew and release read it, so a seat names the ticket and its progress.
|
|
15
|
+
//
|
|
16
|
+
// ⭐ A REFUSAL IS A 200. The store answers `outcome: "refused"` with a reason (`no_new_progress`,
|
|
17
|
+
// `ticket_lease_held`, `not_holder`, …); this module prints it and exits 1, so a script can stop on
|
|
18
|
+
// it. A malformed request (400), a seat that does not reach the ticket's team (403) and an exhausted
|
|
19
|
+
// budget (429) exit 1 the same way, with the server's reason.
|
|
20
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, chmodSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { flagString, positionals } from "./args.js";
|
|
23
|
+
import { CONFIG_MODE, configDirFor, requireConfig } from "./config.js";
|
|
24
|
+
import { loadContract } from "./contract.js";
|
|
25
|
+
import { UsageError } from "./errors.js";
|
|
26
|
+
import { apiClient } from "./transport.js";
|
|
27
|
+
const ROUTE = "/api/v1/agent/ticket-lease";
|
|
28
|
+
const TICKET = /^[A-Za-z][A-Za-z0-9]*-[1-9][0-9]*$/;
|
|
29
|
+
const AGENT = /^[a-z0-9-]{1,40}$/;
|
|
30
|
+
const ACCEPT = [400, 403, 404, 409, 429];
|
|
31
|
+
function object(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function text(value) {
|
|
35
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
36
|
+
}
|
|
37
|
+
function leaseRow(value) {
|
|
38
|
+
return object(value) && typeof value.generation === "number" && typeof value.deadlineMs === "number";
|
|
39
|
+
}
|
|
40
|
+
function leaseDir(ctx) {
|
|
41
|
+
return join(configDirFor(ctx.home), "leases");
|
|
42
|
+
}
|
|
43
|
+
function leasePath(ctx, ticket, agent) {
|
|
44
|
+
return join(leaseDir(ctx), `${ticket}--${agent}.json`);
|
|
45
|
+
}
|
|
46
|
+
function saveLease(ctx, lease) {
|
|
47
|
+
mkdirSync(leaseDir(ctx), { recursive: true });
|
|
48
|
+
const path = leasePath(ctx, lease.ticket, lease.agent);
|
|
49
|
+
writeFileSync(path, JSON.stringify(lease) + "\n", { mode: CONFIG_MODE });
|
|
50
|
+
chmodSync(path, CONFIG_MODE);
|
|
51
|
+
}
|
|
52
|
+
function forgetLease(ctx, ticket, agent) {
|
|
53
|
+
rmSync(leasePath(ctx, ticket, agent), { force: true });
|
|
54
|
+
}
|
|
55
|
+
/** Refusals that mean this seat's lease is gone: it lapsed, or a later take replaced it. */
|
|
56
|
+
const GONE = new Set(["expired", "not_holder", "stale_generation"]);
|
|
57
|
+
/** Forget a stored lease the cloud says this seat no longer holds, so a later seat on the same
|
|
58
|
+
* ticket is not asked to name one with --agent. The exit stays 1: the renew or release did not happen. */
|
|
59
|
+
function forgetIfGone(ctx, ticket, agent, body) {
|
|
60
|
+
if (!object(body) || !object(body.refusal) || !GONE.has(String(body.refusal.reason)))
|
|
61
|
+
return;
|
|
62
|
+
forgetLease(ctx, ticket, agent);
|
|
63
|
+
ctx.stderr(`[catalyst] ${agent} no longer holds ${ticket}; forgot its stored lease`);
|
|
64
|
+
}
|
|
65
|
+
/** The leases this machine holds on a ticket, one per seat that took it. */
|
|
66
|
+
function storedLeases(ctx, ticket) {
|
|
67
|
+
let names;
|
|
68
|
+
try {
|
|
69
|
+
names = readdirSync(leaseDir(ctx));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const found = [];
|
|
75
|
+
for (const name of names.filter((n) => n.startsWith(`${ticket}--`) && n.endsWith(".json"))) {
|
|
76
|
+
try {
|
|
77
|
+
const value = JSON.parse(readFileSync(join(leaseDir(ctx), name), "utf8"));
|
|
78
|
+
if (object(value) && value.ticket === ticket && text(value.agent) && typeof value.generation === "number")
|
|
79
|
+
found.push({ ticket, agent: value.agent, generation: value.generation, holder: text(value.holder) ? value.holder : null });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// An unreadable file is not a lease this machine can name; --agent and --generation still can.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
function parseTicket(raw, verb) {
|
|
88
|
+
if (!raw || !TICKET.test(raw))
|
|
89
|
+
throw new UsageError(`lease ${verb} needs one ticket such as ENG-123`);
|
|
90
|
+
return raw.toUpperCase();
|
|
91
|
+
}
|
|
92
|
+
function parseAgent(raw) {
|
|
93
|
+
if (raw === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (!AGENT.test(raw))
|
|
96
|
+
throw new UsageError("--agent is the seat's name: 1 to 40 of a-z, 0-9 and -");
|
|
97
|
+
return raw;
|
|
98
|
+
}
|
|
99
|
+
/** The seat and generation a renew or release names: the flags, else the one lease this machine
|
|
100
|
+
* stored for the ticket. Two stored seats need --agent to choose. */
|
|
101
|
+
function heldLease(args, ctx, ticket) {
|
|
102
|
+
const agent = parseAgent(flagString(args, "agent"));
|
|
103
|
+
const rawGeneration = flagString(args, "generation");
|
|
104
|
+
let generation;
|
|
105
|
+
if (rawGeneration !== undefined) {
|
|
106
|
+
generation = Number(rawGeneration);
|
|
107
|
+
if (!Number.isSafeInteger(generation) || generation < 1)
|
|
108
|
+
throw new UsageError("--generation is the positive integer the take printed");
|
|
109
|
+
}
|
|
110
|
+
const stored = storedLeases(ctx, ticket).filter((s) => agent === undefined || s.agent === agent);
|
|
111
|
+
if (agent !== undefined && generation !== undefined)
|
|
112
|
+
return { agent, generation };
|
|
113
|
+
if (stored.length === 1)
|
|
114
|
+
return { agent: stored[0].agent, generation: generation ?? stored[0].generation };
|
|
115
|
+
if (stored.length > 1)
|
|
116
|
+
throw new UsageError(`this machine holds ${ticket} for ${stored.map((s) => s.agent).join(", ")}; name one with --agent`);
|
|
117
|
+
throw new UsageError(`this machine holds no lease on ${ticket}${agent ? ` for ${agent}` : ""}; take it first, or pass --agent and --generation`);
|
|
118
|
+
}
|
|
119
|
+
/** The one progress fact a renewal asserts, from exactly one of the four flags. */
|
|
120
|
+
function parseProgress(args) {
|
|
121
|
+
const given = ["commit", "artifact", "finding", "pr"]
|
|
122
|
+
.map((kind) => ({ kind, ref: flagString(args, kind) }))
|
|
123
|
+
.filter((p) => p.ref !== undefined);
|
|
124
|
+
if (given.length !== 1)
|
|
125
|
+
throw new UsageError("lease renew needs exactly one of --commit <sha>, --artifact <path or id>, --finding <id> or --pr <number>");
|
|
126
|
+
const { kind } = given[0];
|
|
127
|
+
const ref = given[0].ref.trim();
|
|
128
|
+
if (kind === "commit" && !/^[0-9a-f]{40}$/.test(ref))
|
|
129
|
+
throw new UsageError("--commit is the full 40-character lowercase sha");
|
|
130
|
+
if (kind === "pr" && !/^[1-9][0-9]{0,9}$/.test(ref))
|
|
131
|
+
throw new UsageError("--pr is the pull request's number");
|
|
132
|
+
if ((kind === "artifact" || kind === "finding") && (ref === "" || /\s/.test(ref)))
|
|
133
|
+
throw new UsageError(`--${kind} is one token with no spaces`);
|
|
134
|
+
const note = flagString(args, "note");
|
|
135
|
+
return { kind, ref, ...(note !== undefined && note.trim() !== "" ? { note } : {}) };
|
|
136
|
+
}
|
|
137
|
+
function parseTtlMs(args) {
|
|
138
|
+
const raw = flagString(args, "ttl");
|
|
139
|
+
if (raw === undefined)
|
|
140
|
+
return {};
|
|
141
|
+
const minutes = Number(raw);
|
|
142
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
143
|
+
throw new UsageError("--ttl is a number of minutes (the cloud keeps it between 5 and 60)");
|
|
144
|
+
return { ttlMs: Math.round(minutes * 60_000) };
|
|
145
|
+
}
|
|
146
|
+
function when(ms) {
|
|
147
|
+
return new Date(ms).toISOString();
|
|
148
|
+
}
|
|
149
|
+
function detail(body) {
|
|
150
|
+
if (!object(body))
|
|
151
|
+
return "";
|
|
152
|
+
const parts = [body.reason, body.error, body.message].filter(text);
|
|
153
|
+
return parts.length ? `: ${[...new Set(parts)].join(": ")}` : "";
|
|
154
|
+
}
|
|
155
|
+
/** Print a refusal or an HTTP failure and return the exit code 1. */
|
|
156
|
+
function refuse(args, ctx, status, body, verb) {
|
|
157
|
+
if (args.json) {
|
|
158
|
+
ctx.stdout(JSON.stringify(object(body) ? { status, ...body } : { status }));
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
if (status === 200 && object(body) && object(body.refusal)) {
|
|
162
|
+
const r = body.refusal;
|
|
163
|
+
const current = leaseRow(r.current) ? r.current : null;
|
|
164
|
+
const parts = [`refused: ${text(r.reason) ? r.reason : "unknown"}`];
|
|
165
|
+
if (current?.holder)
|
|
166
|
+
parts.push(`held by ${current.holder} until ${when(current.deadlineMs)}`);
|
|
167
|
+
if (typeof r.retryAtMs === "number")
|
|
168
|
+
parts.push(`retry at ${when(r.retryAtMs)}`);
|
|
169
|
+
ctx.stdout(parts.join("; "));
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const meaning = status === 404 ? `this Catalyst Cloud does not serve lease ${verb} yet (needs a newer cloud)`
|
|
173
|
+
: status === 403 ? "your login does not reach this ticket's team"
|
|
174
|
+
: status === 429 ? "your write budget is spent for now"
|
|
175
|
+
: `lease ${verb} failed`;
|
|
176
|
+
ctx.stdout(`refused (${status}): ${meaning}${detail(body)}`);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
function printLease(ctx, ticket, lease) {
|
|
180
|
+
ctx.stdout(`${ticket}: held by ${lease.holder ?? "nobody"} until ${when(lease.deadlineMs)}, generation ${lease.generation}`);
|
|
181
|
+
if (text(lease.lastProgress))
|
|
182
|
+
ctx.stdout(` last progress: ${lease.lastProgress}`);
|
|
183
|
+
}
|
|
184
|
+
export async function cmdLease(args, ctx) {
|
|
185
|
+
const [action, rawTicket, ...extra] = positionals(args);
|
|
186
|
+
if (action !== "take" && action !== "renew" && action !== "release" && action !== "show")
|
|
187
|
+
throw new UsageError("lease needs take, renew, release or show");
|
|
188
|
+
if (extra.length)
|
|
189
|
+
throw new UsageError(`lease ${action} takes one ticket`);
|
|
190
|
+
const ticket = parseTicket(rawTicket, action);
|
|
191
|
+
const client = apiClient(requireConfig(ctx), ctx);
|
|
192
|
+
if (action === "show") {
|
|
193
|
+
const res = await client.getJson(ROUTE, { query: { ticket }, accept: ACCEPT, parseAccepted: true });
|
|
194
|
+
if (res.status >= 400)
|
|
195
|
+
return refuse(args, ctx, res.status, res.body, action);
|
|
196
|
+
if (args.json) {
|
|
197
|
+
ctx.stdout(JSON.stringify(res.body));
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
const lease = object(res.body) && leaseRow(res.body.lease) ? res.body.lease : null;
|
|
201
|
+
if (lease === null)
|
|
202
|
+
ctx.stdout(`${ticket}: no ticket lease`);
|
|
203
|
+
else if (lease.live === false) {
|
|
204
|
+
ctx.stdout(`${ticket}: free; last held by ${lease.lastHolder ?? lease.holder ?? "nobody"}, generation ${lease.generation}`);
|
|
205
|
+
if (text(lease.lastProgress))
|
|
206
|
+
ctx.stdout(` last progress: ${lease.lastProgress}`);
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
printLease(ctx, ticket, lease);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
if (action === "take") {
|
|
213
|
+
const agent = parseAgent(flagString(args, "agent"));
|
|
214
|
+
if (agent === undefined)
|
|
215
|
+
throw new UsageError("lease take needs --agent <seat name>");
|
|
216
|
+
const res = await client.postJson(`${ROUTE}/take`, { ticket, agent, ...parseTtlMs(args) }, { accept: ACCEPT });
|
|
217
|
+
if (res.status !== 200 || !object(res.body) || res.body.outcome !== "taken" || !leaseRow(res.body.lease))
|
|
218
|
+
return refuse(args, ctx, res.status, res.body, action);
|
|
219
|
+
const lease = res.body.lease;
|
|
220
|
+
saveLease(ctx, { ticket, agent, generation: lease.generation, holder: lease.holder });
|
|
221
|
+
if (args.json)
|
|
222
|
+
ctx.stdout(JSON.stringify(res.body));
|
|
223
|
+
else
|
|
224
|
+
printLease(ctx, ticket, lease);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
if (action === "renew") {
|
|
228
|
+
const progress = parseProgress(args);
|
|
229
|
+
const held = heldLease(args, ctx, ticket);
|
|
230
|
+
const res = await client.postJson(`${ROUTE}/renew`, { ticket, ...held, progress, ...parseTtlMs(args) }, { accept: ACCEPT });
|
|
231
|
+
if (res.status !== 200 || !object(res.body) || res.body.outcome !== "renewed" || !leaseRow(res.body.lease)) {
|
|
232
|
+
forgetIfGone(ctx, ticket, held.agent, res.body);
|
|
233
|
+
return refuse(args, ctx, res.status, res.body, action);
|
|
234
|
+
}
|
|
235
|
+
if (args.json)
|
|
236
|
+
ctx.stdout(JSON.stringify(res.body));
|
|
237
|
+
else
|
|
238
|
+
printLease(ctx, ticket, res.body.lease);
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
const held = heldLease(args, ctx, ticket);
|
|
242
|
+
const res = await client.postJson(`${ROUTE}/release`, { ticket, ...held }, { accept: ACCEPT });
|
|
243
|
+
if (res.status !== 200 || !object(res.body) || res.body.outcome !== "released") {
|
|
244
|
+
forgetIfGone(ctx, ticket, held.agent, res.body);
|
|
245
|
+
return refuse(args, ctx, res.status, res.body, action);
|
|
246
|
+
}
|
|
247
|
+
forgetLease(ctx, ticket, held.agent);
|
|
248
|
+
if (args.json)
|
|
249
|
+
ctx.stdout(JSON.stringify(res.body));
|
|
250
|
+
else
|
|
251
|
+
ctx.stdout(`${ticket}: released by ${held.agent}`);
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
/** The free tickets in one team's rows: offered, or offered with no capable runner (a seat can still
|
|
255
|
+
* take it), with no live holder named on them. */
|
|
256
|
+
function freeTickets(rows) {
|
|
257
|
+
return rows.filter((r) => text(r.ticket) && (r.status === "offered" || r.status === "capability_mismatch") && !text(r.holder));
|
|
258
|
+
}
|
|
259
|
+
/** `work available`: per team, the tickets nobody holds (with the last holder and progress when a
|
|
260
|
+
* lease lapsed) and the open pull requests no live lease covers, drafts included. */
|
|
261
|
+
export async function cmdWork(args, ctx) {
|
|
262
|
+
const [action, ...extra] = positionals(args);
|
|
263
|
+
if (action !== "available" || extra.length)
|
|
264
|
+
throw new UsageError("work needs available");
|
|
265
|
+
const cfg = requireConfig(ctx);
|
|
266
|
+
const client = apiClient(cfg, ctx);
|
|
267
|
+
const named = flagString(args, "team");
|
|
268
|
+
let teams;
|
|
269
|
+
if (named !== undefined)
|
|
270
|
+
teams = [named.toUpperCase()];
|
|
271
|
+
else {
|
|
272
|
+
const { doc } = await loadContract(ctx, cfg);
|
|
273
|
+
teams = doc.teams.map((t) => t.key).filter((k) => typeof k === "string" && k !== "");
|
|
274
|
+
if (teams.length === 0)
|
|
275
|
+
throw new UsageError("work available needs --team: the workspace contract names no team");
|
|
276
|
+
}
|
|
277
|
+
const results = {};
|
|
278
|
+
for (const team of teams) {
|
|
279
|
+
const res = await client.getJson("/api/v1/work-eligibility", { query: { team } });
|
|
280
|
+
const rows = Array.isArray(res.body.eligibility?.rows) ? res.body.eligibility.rows : [];
|
|
281
|
+
const pulls = Array.isArray(res.body.openPulls) ? res.body.openPulls : [];
|
|
282
|
+
results[team] = {
|
|
283
|
+
tickets: freeTickets(rows),
|
|
284
|
+
unownedPulls: pulls.filter((p) => object(p) && typeof p.number === "number" && p.owner === "unowned"),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (args.json) {
|
|
288
|
+
ctx.stdout(JSON.stringify(named !== undefined ? results[teams[0]] : results));
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
for (const team of teams) {
|
|
292
|
+
const { tickets, unownedPulls } = results[team];
|
|
293
|
+
ctx.stdout(`${team}: ${tickets.length} free ${tickets.length === 1 ? "ticket" : "tickets"}, ${unownedPulls.length} unowned open ${unownedPulls.length === 1 ? "pull request" : "pull requests"}`);
|
|
294
|
+
for (const row of tickets) {
|
|
295
|
+
const last = text(row.lastHolder) ? `; last held by ${row.lastHolder}${text(row.lastProgress) ? `, last progress ${row.lastProgress}` : ""}` : "";
|
|
296
|
+
ctx.stdout(` ${row.ticket}${last}`);
|
|
297
|
+
}
|
|
298
|
+
for (const pull of unownedPulls)
|
|
299
|
+
ctx.stdout(` #${pull.number}${pull.draft ? " (draft)" : ""}${text(pull.ticket) ? ` ${pull.ticket}` : ""}: unowned${text(pull.title) ? `, ${pull.title}` : ""}`);
|
|
300
|
+
}
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
package/dist/onboard-existing.js
CHANGED
|
@@ -7,6 +7,14 @@ const waiting = (reason) => ({
|
|
|
7
7
|
state: "waiting",
|
|
8
8
|
reason,
|
|
9
9
|
});
|
|
10
|
+
/**
|
|
11
|
+
* CTC-5932: at most 20 choices, so the advice stays one readable line, and the full count, so the
|
|
12
|
+
* advice never reports the cap as the workspace's total.
|
|
13
|
+
*/
|
|
14
|
+
export const choiceEvidence = (values) => {
|
|
15
|
+
const unique = [...new Set(values)].sort();
|
|
16
|
+
return { choices: unique.slice(0, 20).join(","), choiceTotal: unique.length };
|
|
17
|
+
};
|
|
10
18
|
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
11
19
|
? value
|
|
12
20
|
: null;
|
|
@@ -175,7 +183,8 @@ export function existingLinearAdapters(args, choose, nameNewTeam, message = () =
|
|
|
175
183
|
evidence: { teamKey: uncertain.key },
|
|
176
184
|
}
|
|
177
185
|
: undefined;
|
|
178
|
-
const stopTeam = (reason) => uncertainResult() ??
|
|
186
|
+
const stopTeam = (reason, teams) => uncertainResult() ??
|
|
187
|
+
(teams?.length ? { ...waiting(reason), evidence: choiceEvidence(teams.map((team) => team.key)) } : waiting(reason));
|
|
179
188
|
const requested = (teams, journal) => {
|
|
180
189
|
const explicit = args.flags.team;
|
|
181
190
|
if (typeof explicit === "string") {
|
|
@@ -210,7 +219,7 @@ export function existingLinearAdapters(args, choose, nameNewTeam, message = () =
|
|
|
210
219
|
return stopTeam("team_selection_unverified");
|
|
211
220
|
return choose
|
|
212
221
|
? (uncertainResult("pending") ?? { state: "pending" })
|
|
213
|
-
: stopTeam("team_choice_required");
|
|
222
|
+
: stopTeam("team_choice_required", list.teams);
|
|
214
223
|
}
|
|
215
224
|
const result = await observe(ctx, team.id, signal);
|
|
216
225
|
if (!result || result.checks.get("team_visible") !== "pass")
|
|
@@ -257,7 +266,7 @@ export function existingLinearAdapters(args, choose, nameNewTeam, message = () =
|
|
|
257
266
|
if ("reason" in list)
|
|
258
267
|
return stopTeam(list.reason);
|
|
259
268
|
if (!choose)
|
|
260
|
-
return stopTeam(list.teams.length ? "team_choice_required" : "team_inventory_empty");
|
|
269
|
+
return stopTeam(list.teams.length ? "team_choice_required" : "team_inventory_empty", list.teams);
|
|
261
270
|
let offer = nameNewTeam && typeof args.flags.team !== "string"
|
|
262
271
|
? await teamCreateOffer(ctx, journal, signal)
|
|
263
272
|
: undefined;
|
package/dist/onboard-next.js
CHANGED
|
@@ -325,9 +325,23 @@ function firstTicketText(reason, key, stage) {
|
|
|
325
325
|
}
|
|
326
326
|
return undefined;
|
|
327
327
|
}
|
|
328
|
+
/** CTC-5932: the choices a step recorded, for advice that names real flag values. */
|
|
329
|
+
function recordedChoices(step) {
|
|
330
|
+
const value = step.evidence?.choices;
|
|
331
|
+
return typeof value === "string" ? value.split(",").filter(Boolean) : [];
|
|
332
|
+
}
|
|
328
333
|
/** One step's next action. `waitsFor` names the unfinished step a prerequisite wait is blocked on. */
|
|
329
334
|
export function onboardReasonText(step, context = {}) {
|
|
330
335
|
const reason = step.reason ?? "";
|
|
336
|
+
const choices = recordedChoices(step);
|
|
337
|
+
const recordedTotal = step.evidence?.choiceTotal;
|
|
338
|
+
const total = typeof recordedTotal === "number" && recordedTotal > choices.length ? recordedTotal : choices.length;
|
|
339
|
+
// The list is capped at 20, so a longer inventory says which part it shows.
|
|
340
|
+
const shown = total > choices.length ? `, the first ${choices.length} of them` : "";
|
|
341
|
+
if (reason === "team_choice_required" && choices.length)
|
|
342
|
+
return `This workspace has ${total} Linear ${total === 1 ? "team" : "teams"}${shown}: ${choices.join(", ")}. Add --team with one of them to the same command, for example --team ${choices[0]}.`;
|
|
343
|
+
if (reason === "repository_choice_required" && choices.length)
|
|
344
|
+
return `${total === 1 ? "One repository is" : `${total} repositories are`} available${shown}: ${choices.join(", ")}. Add --repo <owner/name> to the same command, for example --repo ${choices[0]}. Repeat --repo to choose more than one.`;
|
|
331
345
|
if ([
|
|
332
346
|
"workspace_approval_required",
|
|
333
347
|
"personal_approval_required",
|
package/dist/onboard-projects.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
2
2
|
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
3
3
|
import { readExistingOnboardJson, selectedOnboardTeam, } from "./onboard-existing.js";
|
|
4
|
-
import { existingRepositoryAdapter, } from "./onboard-repositories.js";
|
|
4
|
+
import { existingRepositoryAdapter, repositoryChoiceWait, } from "./onboard-repositories.js";
|
|
5
5
|
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
6
6
|
? value
|
|
7
7
|
: null;
|
|
@@ -211,7 +211,7 @@ export function firstProjectAdapters(args, input = {}) {
|
|
|
211
211
|
: waiting("repository_selection_unverified");
|
|
212
212
|
return input.chooseFirst && list.rows.length
|
|
213
213
|
? { state: "pending" }
|
|
214
|
-
:
|
|
214
|
+
: repositoryChoiceWait(list.rows);
|
|
215
215
|
},
|
|
216
216
|
act: async (ctx, journal, signal) => {
|
|
217
217
|
const live = await existing.check(ctx, journal, signal);
|
|
@@ -221,7 +221,7 @@ export function firstProjectAdapters(args, input = {}) {
|
|
|
221
221
|
if ("reason" in list)
|
|
222
222
|
return waiting(list.reason);
|
|
223
223
|
if (!input.chooseFirst || !list.rows.length)
|
|
224
|
-
return
|
|
224
|
+
return repositoryChoiceWait(list.rows);
|
|
225
225
|
if (list.truncated)
|
|
226
226
|
input.message?.("GitHub returned part of its repository list. Choose a displayed repository, or stop and adjust the App access before resuming.");
|
|
227
227
|
let remove = () => { };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isRepositoryId } from "./repository-id.js";
|
|
2
2
|
import { loadConfig, normalizeBaseUrl, readManifest, } from "./config.js";
|
|
3
3
|
import { contractVersionInRange } from "./contract.js";
|
|
4
|
-
import { readExistingOnboardJson, selectedOnboardTeam, } from "./onboard-existing.js";
|
|
4
|
+
import { choiceEvidence, readExistingOnboardJson, selectedOnboardTeam, } from "./onboard-existing.js";
|
|
5
5
|
const part = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/;
|
|
6
6
|
const id = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
7
7
|
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
@@ -12,6 +12,13 @@ const waiting = (reason) => ({
|
|
|
12
12
|
state: "waiting",
|
|
13
13
|
reason,
|
|
14
14
|
});
|
|
15
|
+
/** CTC-5932: a choice no one was there to make, with the repositories it could have been. */
|
|
16
|
+
export const repositoryChoiceWait = (rows) => rows.length
|
|
17
|
+
? {
|
|
18
|
+
...waiting("repository_choice_required"),
|
|
19
|
+
evidence: choiceEvidence(rows.map((row) => `${row.owner}/${row.name}`)),
|
|
20
|
+
}
|
|
21
|
+
: waiting("repository_choice_required");
|
|
15
22
|
const valid = (value) => {
|
|
16
23
|
const row = object(value);
|
|
17
24
|
return (!!row &&
|
|
@@ -381,7 +388,7 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
381
388
|
? waiting("repository_selection_unverified")
|
|
382
389
|
: choose
|
|
383
390
|
? { state: "pending" }
|
|
384
|
-
:
|
|
391
|
+
: repositoryChoiceWait(list.repositories);
|
|
385
392
|
return {
|
|
386
393
|
state: "done",
|
|
387
394
|
evidence: {
|
|
@@ -400,7 +407,7 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
400
407
|
if (!list.repositories.length)
|
|
401
408
|
return waiting("repository_inventory_empty");
|
|
402
409
|
if (!choose)
|
|
403
|
-
return
|
|
410
|
+
return repositoryChoiceWait(list.repositories);
|
|
404
411
|
const teamId = list.repositories[0].teamId;
|
|
405
412
|
const offers = list.canRegister
|
|
406
413
|
? await offeredRepositories(ctx, journal, teamId, list.repositories, signal)
|
package/dist/onboard-ui.js
CHANGED
|
@@ -2,7 +2,7 @@ import { GITHUB_CONFIRM_ACCESS } from "./onboard-github-copy.js";
|
|
|
2
2
|
import { COMPUTER_CHECKS, standalonePlan, standalonePlanNotes, pendingContinuation, } from "./onboard-standalone-copy.js";
|
|
3
3
|
import { trackedPrompt } from "./setup-prompt-frame.js";
|
|
4
4
|
import { createOnboardInterrupts } from "./onboard-interrupts.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
|
+
import { CLOUD_WORK, setupStepView, CONNECTION_REPAIR_REASONS, setupFinalScreen, setupBrowserInstruction, setupCommandWords as say, setupWaitsOnAdmin, TIMED_OUT_REASONS, setupPartHeading, setupPartProgress, setupTrackerRows, unattendedSkipLines, SETUP_PART_NUMBERS, SETUP_PART_OF, SETUP_PARTS, } from "./setup-onboard-copy.js";
|
|
6
6
|
import { createOnboardProgress, } from "./onboard-progress.js";
|
|
7
7
|
import { CREATE_TEAM_CHOICE, suggestTeamKey, teamKeyProblem, teamNameProblem, } from "./onboard-team-create.js";
|
|
8
8
|
import { FIRST_TICKET_SAMPLE, FIRST_TICKET_SKIP, } from "./onboard-first-ticket.js";
|
|
@@ -297,6 +297,10 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
297
297
|
if (screen.paused)
|
|
298
298
|
renderer.line("Your progress is saved.");
|
|
299
299
|
renderer.tracker(setupTrackerRows(setupPartProgress(journal, planned.scope), "end"));
|
|
300
|
+
// CTC-5932: with no one to ask, setup says what it left out instead of letting it pass unsaid.
|
|
301
|
+
if (!interactive && !screen.paused)
|
|
302
|
+
for (const line of unattendedSkipLines(journal, planned))
|
|
303
|
+
renderer.line(line);
|
|
300
304
|
if (screen.readinessCause)
|
|
301
305
|
renderer.line(say(screen.readinessCause));
|
|
302
306
|
if (mine.length) {
|
package/dist/onboard.js
CHANGED
|
@@ -298,6 +298,9 @@ function normalizeStep(value, fallbackAt) {
|
|
|
298
298
|
"passed",
|
|
299
299
|
"team",
|
|
300
300
|
"teamKey",
|
|
301
|
+
// CTC-5932: the comma-joined choices a run with no one to ask names in its flag advice.
|
|
302
|
+
"choices",
|
|
303
|
+
"choiceTotal",
|
|
301
304
|
"selected",
|
|
302
305
|
"hostName",
|
|
303
306
|
"supervisorImage",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dependencySettled, ONBOARD_DEPENDENCIES, ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
|
|
1
|
+
import { dependencySettled, stepSatisfied, 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 = {
|
|
@@ -385,6 +385,9 @@ moment = "end") {
|
|
|
385
385
|
{
|
|
386
386
|
// A member's own skip (no --team) is theirs to finish, so it is never counted done.
|
|
387
387
|
mark: memberOwnSkip(step) ? "act" : setupStepView(step, journal).mark,
|
|
388
|
+
// CTC-5932: a skipped step finishes its row only when it was skipped by choice. A skipped
|
|
389
|
+
// connection is still owed, so a part never reads "done" over it.
|
|
390
|
+
settled: step.state !== "skipped" || stepSatisfied(step),
|
|
388
391
|
working: step.state !== "done" && CLOUD_WORK.has(step.reason ?? ""),
|
|
389
392
|
kind: setupWaitsOnAdmin(step, journal)
|
|
390
393
|
? "admin"
|
|
@@ -400,7 +403,8 @@ moment = "end") {
|
|
|
400
403
|
const part = progress[Number(key.split(":")[0])];
|
|
401
404
|
part.total++;
|
|
402
405
|
const own = steps.filter((s) => s.kind === "own");
|
|
403
|
-
if (own.length === steps.length &&
|
|
406
|
+
if (own.length === steps.length &&
|
|
407
|
+
own.every((s) => s.mark === "done" || (s.mark === "skip" && s.settled)))
|
|
404
408
|
part.done++;
|
|
405
409
|
else if (own.some((s) => s.mark === "act" || s.mark === "fail")) {
|
|
406
410
|
part.needs++;
|
|
@@ -416,6 +420,41 @@ moment = "end") {
|
|
|
416
420
|
}
|
|
417
421
|
return progress;
|
|
418
422
|
}
|
|
423
|
+
/** The connections part 2 makes, each approved by a person in a browser. */
|
|
424
|
+
const BROWSER_CONNECTIONS = [
|
|
425
|
+
"linear.workspace",
|
|
426
|
+
"linear.personal",
|
|
427
|
+
"github.install",
|
|
428
|
+
"github.personal",
|
|
429
|
+
];
|
|
430
|
+
/**
|
|
431
|
+
* CTC-5932: what a run that could not ask anyone left out, said plainly, with the command that
|
|
432
|
+
* resumes it. Empty when nothing was left out. A runner chosen or declined by flag is the person's
|
|
433
|
+
* choice and is not repeated back.
|
|
434
|
+
*/
|
|
435
|
+
export function unattendedSkipLines(journal, options = {}) {
|
|
436
|
+
const inScope = (id) => !options.scope || options.scope.includes(id);
|
|
437
|
+
const step = (id) => journal.steps.find((s) => s.id === id);
|
|
438
|
+
const lines = [];
|
|
439
|
+
const runner = step("runner");
|
|
440
|
+
if (options.runner === undefined &&
|
|
441
|
+
inScope("runner") &&
|
|
442
|
+
runner?.state === "skipped" &&
|
|
443
|
+
runner.reason === "runner_not_selected")
|
|
444
|
+
lines.push("Setup ran without questions, so it chose no runner: this computer will not run tickets.", "To add one, run in a terminal: catalyst setup --runner");
|
|
445
|
+
const open = BROWSER_CONNECTIONS.filter((id) => inScope(id) && step(id)?.state !== "done");
|
|
446
|
+
// Name only the providers still open, so a re-run with Linear connected never calls it skipped.
|
|
447
|
+
const providers = [
|
|
448
|
+
...(open.some((id) => id.startsWith("linear.")) ? ["Linear"] : []),
|
|
449
|
+
...(open.some((id) => id.startsWith("github.")) ? ["GitHub"] : []),
|
|
450
|
+
];
|
|
451
|
+
const both = providers.length > 1;
|
|
452
|
+
if (open.length)
|
|
453
|
+
lines.push(`${providers.join(" and ")} ${both ? "were" : "was"} skipped, because ${both ? "they need" : "it needs"} you in a browser. Not connected: ${open
|
|
454
|
+
.map((id) => TITLES[id] ?? ONBOARD_TITLES[id])
|
|
455
|
+
.join(", ")}.`, `To connect ${both ? "them" : "it"}, run in a terminal: catalyst setup`);
|
|
456
|
+
return lines;
|
|
457
|
+
}
|
|
419
458
|
/**
|
|
420
459
|
* The tracker's rows: on the first screen (`plan`, the part about to start), at a boundary
|
|
421
460
|
* (`next`, the part about to start), or at the end. A part with no rows in this run is left out.
|
package/package.json
CHANGED
|
@@ -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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — 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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — 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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Onboard me
|
|
10
10
|
|
|
@@ -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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Run this project
|
|
10
10
|
|
|
@@ -24,7 +24,7 @@ Quote a reason the table does not know as the explainer spelled it.
|
|
|
24
24
|
|
|
25
25
|
## Shared causes are one note
|
|
26
26
|
|
|
27
|
-
When several tickets are offered and nothing takes them, or the explainer names a routing, slot, provider or image condition, the cause is shared (coding-account headroom, a provider outage, a paused repository, a bad runner image). Write one summary line naming the condition and the tickets it holds. `catalyst accounts` reads coding-account status; enrolling or pausing one is the settings page.
|
|
27
|
+
When several tickets are offered and nothing takes them, or the explainer names a routing, slot, provider or image condition, the cause is shared (coding-account headroom, a provider outage, a paused repository, a bad runner image). Write one summary line naming the condition and the tickets it holds. For a paused repository, `catalyst repo status` says who paused it, when and why; put that in the line, and leave resuming (`catalyst repo resume <owner/name>`) to a workspace owner or admin. `catalyst accounts` reads coding-account status; enrolling or pausing one is the settings page.
|
|
28
28
|
|
|
29
29
|
## Escalate inward
|
|
30
30
|
|
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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — 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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What needs me
|
|
10
10
|
|
|
@@ -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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — 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?", "what machines do we have?", "how much capacity do we have?", "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.16.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.16.30 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# What's happening
|
|
10
10
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Queues update within seconds. Dispatch sorts by priority, created time, identifier; mid-ladder tickets precede them by phases completed. Order never grants eligibility.
|
|
4
4
|
|
|
5
|
-
An operator sets the repository concurrency cap, default 20, displayed in settings. Pause means zero, preserving the cap. Teams share capacity, including comment-wake.
|
|
5
|
+
An operator sets the repository concurrency cap, default 20, displayed in settings. Pause means zero, preserving the cap. Any member reads which repositories are paused, by whom, when and why with `catalyst repo status`; a workspace owner or admin pauses one with `catalyst repo pause <owner/name> --reason <why>` and resumes it with `catalyst repo resume <owner/name>`. Teams share capacity, including comment-wake.
|
|
6
6
|
|
|
7
7
|
WIP limits starts per project, one Linear team across repositories: project setting, operator's account value, then 12; zero prevents starts. Members read `catalyst project wip-limit get --team <KEY>`; owners/admins use `set <n>` or set default. Only first phases are held; later phases continue. Count live tickets past dispatch or granted a first phase, including waits/blocks/parks; exclude never-started, backlog and local-lane. Idle at the limit means started tickets wait; use unstick, catalyst-github or what-needs-me.
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ Automatic waits, where no one acts: `retry_backoff`, `lease_held`, `intake_lease
|
|
|
25
25
|
| `waiting_on` | the person, with their own login: unrepairable PR merge gate; read merge-wait comment for hold/review owed, using catalyst-github |
|
|
26
26
|
| `externally_claimed` | the person, with their own login: outside worker owns it; worker/person removes local-lane label |
|
|
27
27
|
| `environment_check_required` / `environment_check_failed` / `environment_check_expired` / `environment_check_hash_mismatch` | absent/failed/aged/changed environment check; a workspace owner or admin, in settings, runs or fixes it |
|
|
28
|
-
| `repo_paused` |
|
|
28
|
+
| `repo_paused` | the repository is paused; `catalyst repo status` says by whom, when and why; a workspace owner or admin, in settings or with `catalyst repo resume <owner/name>`, resumes it |
|
|
29
29
|
| `scope_overlap` | the person, with their own login: declared files overlap running work; choose order or wait |
|
|
30
30
|
| `human_owned_pr` | the person, with their own login: only its human owner closes, merges or hands it over; release refused |
|
|
31
31
|
| `review_not_converging` | the person, with their own login: read findings through catalyst-github, comment; release refused |
|