@catalyst-cloud/cli 0.13.0 → 0.14.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 +10 -0
- package/README.md +27 -30
- package/dist/args.js +299 -61
- package/dist/ask.js +30 -18
- package/dist/capabilities.js +7 -0
- package/dist/cli.js +125 -20
- package/dist/config.js +3 -1
- package/dist/env/draft.js +134 -0
- package/dist/env/legacy.js +54 -0
- package/dist/env/parse-env.js +50 -0
- package/dist/env/settings-toml.js +66 -0
- package/dist/env.js +126 -26
- package/dist/environment.js +1 -1
- package/dist/legacy.js +224 -0
- package/dist/onboard-consent.js +65 -0
- package/dist/onboard.js +836 -0
- package/dist/project.js +191 -0
- package/dist/ready.js +11 -1
- package/dist/release.js +1 -1
- package/dist/repo.js +297 -0
- package/dist/runtime-verb.js +1 -1
- package/dist/secret.js +25 -4
- package/dist/skill-shape.js +21 -13
- package/dist/skills.js +44 -4
- package/dist/transport.js +26 -1
- package/dist/var.js +118 -0
- package/package.json +6 -4
- package/skills/catalyst-github/SKILL.md +7 -7
- package/skills/catalyst-github/references/is-it-mergeable.md +8 -2
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +3 -3
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +3 -3
- package/skills/catalyst-github/scripts/lib/cli.mjs +9 -7
- package/skills/catalyst-github/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-github/scripts/read-pr.mjs +2 -2
- package/skills/catalyst-linear/SKILL.md +17 -19
- package/skills/catalyst-linear/agents/openai.yaml +1 -1
- package/skills/catalyst-linear/agents/portability.yaml +1 -1
- package/skills/catalyst-linear/references/reading-a-ticket.md +9 -26
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +2 -2
- package/skills/catalyst-linear/references/writing-to-linear.md +7 -9
- package/skills/catalyst-linear/scripts/comment.mjs +1 -1
- package/skills/catalyst-linear/scripts/create-ticket.mjs +1 -1
- package/skills/catalyst-linear/scripts/label.mjs +1 -1
- package/skills/catalyst-linear/scripts/lib/cli.mjs +9 -7
- package/skills/catalyst-linear/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-linear/scripts/move.mjs +1 -1
- package/skills/catalyst-linear/scripts/read-ticket.mjs +1 -1
- package/skills/catalyst-linear/scripts/search.mjs +1 -1
- package/skills/catalyst-onboard/SKILL.md +37 -57
- package/skills/catalyst-onboard/agents/openai.yaml +2 -2
- package/skills/catalyst-onboard/agents/portability.yaml +1 -1
- package/skills/catalyst-onboard/references/connecting-this-machine.md +35 -0
- package/skills/catalyst-onboard/references/declaring-a-repository.md +18 -21
- package/skills/catalyst-onboard/references/local-sync.md +19 -7
- package/skills/catalyst-onboard/references/reading-ready.md +79 -0
- package/skills/catalyst-onboard/references/replacing-a-credential.md +8 -33
- package/skills/catalyst-onboard/references/repository-agent-setup.md +23 -0
- package/skills/catalyst-onboard/references/skill-sources.md +13 -19
- package/skills/catalyst-onboard/references/the-one-path.md +18 -74
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +21 -28
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +20 -37
- package/skills/{catalyst-setup → catalyst-onboard}/scripts/check.mjs +10 -9
- package/skills/catalyst-onboard/scripts/lib/cli.mjs +6 -4
- package/skills/catalyst-onboard/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-onboard/scripts/lib/project-coverage.mjs +53 -0
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +929 -184
- package/skills/run-this-project/SKILL.md +23 -25
- package/skills/run-this-project/agents/openai.yaml +1 -1
- package/skills/run-this-project/agents/portability.yaml +1 -1
- package/skills/run-this-project/references/making-work-ready.md +20 -37
- package/skills/run-this-project/references/reacting-to-events.md +25 -48
- package/skills/run-this-project/references/stalls-and-escalation.md +20 -42
- package/skills/run-this-project/scripts/lib/cli.mjs +7 -5
- package/skills/run-this-project/scripts/lib/credential.mjs +2 -2
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +1 -1
- package/skills/unstick/SKILL.md +12 -12
- package/skills/unstick/agents/openai.yaml +1 -1
- package/skills/unstick/agents/portability.yaml +1 -1
- package/skills/unstick/references/playbook.md +13 -13
- package/skills/unstick/scripts/lib/cli.mjs +6 -4
- package/skills/unstick/scripts/lib/credential.mjs +2 -2
- package/skills/unstick/scripts/unstick.mjs +1 -1
- package/skills/what-needs-me/SKILL.md +12 -14
- package/skills/what-needs-me/agents/openai.yaml +2 -2
- package/skills/what-needs-me/agents/portability.yaml +1 -1
- package/skills/what-needs-me/references/raising-a-decision.md +8 -16
- package/skills/what-needs-me/references/reading-the-inbox.md +6 -13
- package/skills/what-needs-me/references/settling-an-answer.md +6 -12
- package/skills/what-needs-me/scripts/inbox.mjs +14 -13
- package/skills/what-needs-me/scripts/lib/cli.mjs +6 -4
- package/skills/what-needs-me/scripts/lib/credential.mjs +2 -2
- package/skills/what-needs-me/scripts/raise.mjs +1 -1
- package/skills/what-needs-me/scripts/settle.mjs +1 -1
- package/skills/what-this-repo-needs/SKILL.md +10 -19
- package/skills/what-this-repo-needs/references/what-each-group-means.md +14 -44
- package/skills/what-this-repo-needs/scripts/check.mjs +13 -14
- package/skills/what-this-repo-needs/scripts/inventory.mjs +2 -2
- package/skills/what-this-repo-needs/scripts/lib/cli.mjs +11 -9
- package/skills/what-this-repo-needs/scripts/lib/credential.mjs +2 -2
- package/skills/whats-happening/SKILL.md +20 -30
- package/skills/whats-happening/agents/openai.yaml +1 -1
- package/skills/whats-happening/references/coding-accounts.md +11 -0
- package/skills/whats-happening/references/routing-work.md +4 -33
- package/skills/whats-happening/references/settings-and-where-they-live.md +17 -0
- package/skills/whats-happening/references/stages-and-mapping.md +19 -0
- package/skills/whats-happening/references/status-reply.md +11 -31
- package/skills/whats-happening/references/the-ladder.md +20 -0
- package/skills/whats-happening/references/what-runs-next.md +13 -0
- package/skills/whats-happening/references/when-a-phase-fails.md +21 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +38 -62
- package/skills/whats-happening/scripts/explain.mjs +1 -1
- package/skills/whats-happening/scripts/lib/cli.mjs +7 -5
- package/skills/whats-happening/scripts/lib/credential.mjs +2 -2
- package/skills/{how-catalyst-works → whats-happening}/scripts/show-my-map.mjs +18 -32
- package/skills/whats-happening/scripts/snapshot.mjs +10 -6
- package/dist/env/declaration-rules.js +0 -163
- package/skills/catalyst-onboard/references/choosing-a-coding-account.md +0 -52
- package/skills/catalyst-onboard/references/who-fixes-what.md +0 -64
- package/skills/catalyst-setup/SKILL.md +0 -36
- package/skills/catalyst-setup/agents/openai.yaml +0 -6
- package/skills/catalyst-setup/agents/portability.yaml +0 -4
- package/skills/catalyst-setup/references/what-each-check-means.md +0 -88
- package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
- package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
- package/skills/connect-me/SKILL.md +0 -63
- package/skills/connect-me/agents/openai.yaml +0 -6
- package/skills/connect-me/agents/portability.yaml +0 -5
- package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
- package/skills/connect-me/scripts/lib/cli.mjs +0 -185
- package/skills/connect-me/scripts/lib/credential.mjs +0 -29
- package/skills/connect-me/scripts/verify-connection.mjs +0 -68
- package/skills/how-catalyst-works/SKILL.md +0 -45
- package/skills/how-catalyst-works/agents/openai.yaml +0 -6
- package/skills/how-catalyst-works/agents/portability.yaml +0 -4
- package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
- package/skills/how-catalyst-works/references/settings-and-where-they-live.md +0 -20
- package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -56
- package/skills/how-catalyst-works/references/the-ladder.md +0 -41
- package/skills/how-catalyst-works/references/what-catalyst-is.md +0 -30
- package/skills/how-catalyst-works/references/what-runs-next.md +0 -95
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
- package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
- package/skills/whats-happening/references/reading-the-board.md +0 -43
- package/skills/whats-happening/references/reprioritising.md +0 -37
package/dist/skill-shape.js
CHANGED
|
@@ -85,7 +85,7 @@ export function validateSkillDir(dir) {
|
|
|
85
85
|
if (lines.length > MAX_SKILL_LINES)
|
|
86
86
|
problems.push(`${name}: SKILL.md is ${lines.length} lines (max ${MAX_SKILL_LINES})`);
|
|
87
87
|
const fm = parseFrontmatter(lines);
|
|
88
|
-
let
|
|
88
|
+
let manualOnlyFrontmatter = false;
|
|
89
89
|
if (!fm)
|
|
90
90
|
problems.push(`${name}: SKILL.md must start with YAML frontmatter (---)`);
|
|
91
91
|
else {
|
|
@@ -99,7 +99,7 @@ export function validateSkillDir(dir) {
|
|
|
99
99
|
problems.push(`${name}: frontmatter is missing "description"`);
|
|
100
100
|
else if (fm.fields.description.length > MAX_DESCRIPTION_CHARS)
|
|
101
101
|
problems.push(`${name}: description is ${fm.fields.description.length} characters (max ${MAX_DESCRIPTION_CHARS})`);
|
|
102
|
-
|
|
102
|
+
manualOnlyFrontmatter = fm.fields["disable-model-invocation"] === "true";
|
|
103
103
|
if (fm.closeIndex !== -1) {
|
|
104
104
|
const after = lines[fm.closeIndex + 1] ?? "";
|
|
105
105
|
if (!(after.startsWith("<!--") && after.includes(PROVENANCE_MARKER))) {
|
|
@@ -142,10 +142,14 @@ export function validateSkillDir(dir) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
// Invocation is one choice written in three places: SKILL.md's `disable-model-invocation`, the
|
|
146
|
+
// OpenAI sidecar's `allow_implicit_invocation`, and portability's `invocation`. They must agree.
|
|
147
|
+
// Whether a skill writes (`mutating`) is a separate fact: a skill that writes can still be picked
|
|
148
|
+
// by the model, and its own scoped controls (a write budget, a preview) govern the write.
|
|
145
149
|
const portability = join(dir, "agents", "portability.yaml");
|
|
146
150
|
const openai = join(dir, "agents", "openai.yaml");
|
|
147
|
-
let
|
|
148
|
-
let implicitFalse =
|
|
151
|
+
let explicitPortability = null;
|
|
152
|
+
let implicitFalse = null;
|
|
149
153
|
if (!existsSync(portability))
|
|
150
154
|
problems.push(`${name}: agents/portability.yaml is missing`);
|
|
151
155
|
else {
|
|
@@ -154,7 +158,14 @@ export function validateSkillDir(dir) {
|
|
|
154
158
|
problems.push(`${name}: agents/portability.yaml does not declare "effects"`);
|
|
155
159
|
if (!/^exposure:\s*\[\s*"?catalog"?\s*\]\s*$/m.test(y))
|
|
156
160
|
problems.push(`${name}: agents/portability.yaml must declare exposure: [catalog]`);
|
|
157
|
-
mutating = /^mutating:\s*true\s*$/m.test(y);
|
|
161
|
+
const mutating = /^mutating:\s*true\s*$/m.test(y);
|
|
162
|
+
const effectsEmpty = /^effects:\s*\[\s*\]\s*$/m.test(y);
|
|
163
|
+
if (mutating && effectsEmpty)
|
|
164
|
+
problems.push(`${name}: agents/portability.yaml says mutating: true but lists no effects`);
|
|
165
|
+
if (!mutating && /^effects:\s*\[\s*[^\]\s]/m.test(y))
|
|
166
|
+
problems.push(`${name}: agents/portability.yaml lists effects but lacks mutating: true`);
|
|
167
|
+
const inv = /^invocation:\s*(\w+)\s*$/m.exec(y)?.[1];
|
|
168
|
+
explicitPortability = inv === undefined ? null : inv === "explicit";
|
|
158
169
|
}
|
|
159
170
|
if (!existsSync(openai))
|
|
160
171
|
problems.push(`${name}: agents/openai.yaml is missing`);
|
|
@@ -164,15 +175,12 @@ export function validateSkillDir(dir) {
|
|
|
164
175
|
problems.push(`${name}: agents/openai.yaml has no "policy:" block`);
|
|
165
176
|
if (!/allow_implicit_invocation:\s*(true|false)/.test(y))
|
|
166
177
|
problems.push(`${name}: agents/openai.yaml does not set policy.allow_implicit_invocation`);
|
|
167
|
-
|
|
178
|
+
else
|
|
179
|
+
implicitFalse = /allow_implicit_invocation:\s*false/.test(y);
|
|
168
180
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (!mutatingFrontmatter)
|
|
173
|
-
problems.push(`${name}: is mutating but SKILL.md does not set disable-model-invocation: true`);
|
|
174
|
-
if (!implicitFalse)
|
|
175
|
-
problems.push(`${name}: is mutating but agents/openai.yaml does not set policy.allow_implicit_invocation: false`);
|
|
181
|
+
const manualFlags = [manualOnlyFrontmatter, implicitFalse, explicitPortability].filter((v) => v !== null);
|
|
182
|
+
if (new Set(manualFlags).size > 1) {
|
|
183
|
+
problems.push(`${name}: the invocation fields disagree about whether the model may pick this skill (SKILL.md disable-model-invocation: ${manualOnlyFrontmatter}, agents/openai.yaml allow_implicit_invocation: ${implicitFalse === null ? "unset" : !implicitFalse}, agents/portability.yaml invocation: ${explicitPortability === null ? "unset" : explicitPortability ? "explicit" : "implicit"})`);
|
|
176
184
|
}
|
|
177
185
|
for (const p of walk(dir)) {
|
|
178
186
|
const rel = relative(dir, p);
|
package/dist/skills.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// skills.ts — copying the bundled skills into the user's skills directory, and the one-line update
|
|
2
2
|
// notice a new version prints on its next session.
|
|
3
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { defaultSkillsDirFor, upgradeCommand } from "./config.js";
|
|
6
|
+
import { defaultSkillsDirFor, readManifest, upgradeCommand } from "./config.js";
|
|
7
7
|
import { semverOlder } from "./semver.js";
|
|
8
8
|
import { PROVENANCE_MARKER, parseFrontmatter, parseProvenanceVersion } from "./skill-shape.js";
|
|
9
9
|
export function skillsSourceDir() {
|
|
@@ -16,9 +16,14 @@ export function skillsSourceDir() {
|
|
|
16
16
|
* `onlyExisting` is the update path: it refreshes copies this package already made and creates
|
|
17
17
|
* none. That is what keeps the update notice from planting a second copy of every skill beside a
|
|
18
18
|
* set the customer's agent installed as a plugin.
|
|
19
|
+
*
|
|
20
|
+
* Both paths then remove a retired skill: a folder in the target that carries this bundle's
|
|
21
|
+
* provenance stamp but whose name this bundle no longer ships. Left in place, it keeps routing
|
|
22
|
+
* requests to a skill that was merged away. A folder without the stamp (a person's own skill), a
|
|
23
|
+
* symlink, and a folder stamped by a NEWER bundle than this one are never removed.
|
|
19
24
|
*/
|
|
20
|
-
export function installSkills(targetDir, opts, sourceDir = skillsSourceDir()) {
|
|
21
|
-
const result = { installed: [], skipped: [] };
|
|
25
|
+
export function installSkills(targetDir, opts, sourceDir = skillsSourceDir(), bundleVersion = readManifest().version) {
|
|
26
|
+
const result = { installed: [], skipped: [], removed: [] };
|
|
22
27
|
if (existsSync(targetDir) && !statSync(targetDir).isDirectory()) {
|
|
23
28
|
throw new Error(`${targetDir} is not a directory`);
|
|
24
29
|
}
|
|
@@ -49,8 +54,43 @@ export function installSkills(targetDir, opts, sourceDir = skillsSourceDir()) {
|
|
|
49
54
|
cpSync(src, dst, { recursive: true });
|
|
50
55
|
result.installed.push(name);
|
|
51
56
|
}
|
|
57
|
+
result.removed = removeRetiredSkills(targetDir, new Set(names.filter((n) => existsSync(join(sourceDir, n, "SKILL.md")))), bundleVersion);
|
|
52
58
|
return result;
|
|
53
59
|
}
|
|
60
|
+
/** Remove every stamped folder in `targetDir` whose name `shipped` no longer carries. */
|
|
61
|
+
function removeRetiredSkills(targetDir, shipped, bundleVersion) {
|
|
62
|
+
if (!existsSync(targetDir))
|
|
63
|
+
return [];
|
|
64
|
+
const removed = [];
|
|
65
|
+
for (const e of readdirSync(targetDir, { withFileTypes: true })) {
|
|
66
|
+
if (shipped.has(e.name))
|
|
67
|
+
continue;
|
|
68
|
+
const dir = join(targetDir, e.name);
|
|
69
|
+
// lstat, not the dirent alone: a symlink belongs to whoever linked it and is never followed.
|
|
70
|
+
if (isSymlink(dir) || !e.isDirectory())
|
|
71
|
+
continue;
|
|
72
|
+
const skillMd = join(dir, "SKILL.md");
|
|
73
|
+
if (!existsSync(skillMd))
|
|
74
|
+
continue;
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = readFileSync(skillMd, "utf8");
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const line = provenanceLine(text);
|
|
83
|
+
if (line === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
const stamp = parseProvenanceVersion(line);
|
|
86
|
+
// A newer bundle may ship a skill this one does not know yet; an older CLI must not delete it.
|
|
87
|
+
if (stamp !== null && semverOlder(bundleVersion, stamp))
|
|
88
|
+
continue;
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
removed.push(e.name);
|
|
91
|
+
}
|
|
92
|
+
return removed.sort();
|
|
93
|
+
}
|
|
54
94
|
function isSymlink(path) {
|
|
55
95
|
try {
|
|
56
96
|
return lstatSync(path).isSymbolicLink();
|
package/dist/transport.js
CHANGED
|
@@ -32,7 +32,16 @@ export class ApiClient {
|
|
|
32
32
|
const url = this.url(path, opts.query);
|
|
33
33
|
const res = await this.send(url, { method: "GET", headers });
|
|
34
34
|
if (opts.accept?.includes(res.status)) {
|
|
35
|
-
|
|
35
|
+
let body = undefined;
|
|
36
|
+
if (opts.parseAccepted) {
|
|
37
|
+
try {
|
|
38
|
+
body = (await res.json());
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
body = undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { status: res.status, body, headers: res.headers };
|
|
36
45
|
}
|
|
37
46
|
await this.refuseIfNotOk(res, `GET ${path}`);
|
|
38
47
|
return { status: res.status, body: await this.parseBody(res, `GET ${path}`), headers: res.headers };
|
|
@@ -87,6 +96,22 @@ export class ApiClient {
|
|
|
87
96
|
await this.refuseIfNotOk(res, `POST ${path}`);
|
|
88
97
|
return { status: res.status, body: await this.parseBody(res, `POST ${path}`), headers: res.headers };
|
|
89
98
|
}
|
|
99
|
+
/** PUT a JSON body. Same contract as {@link postJson}: `accept` lists statuses returned as replies. */
|
|
100
|
+
async putJson(path, body, opts = {}) {
|
|
101
|
+
const url = this.url(path);
|
|
102
|
+
const res = await this.send(url, {
|
|
103
|
+
method: "PUT",
|
|
104
|
+
headers: {
|
|
105
|
+
authorization: `Bearer ${await this.credential()}`,
|
|
106
|
+
accept: "application/json",
|
|
107
|
+
"content-type": "application/json",
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
});
|
|
111
|
+
if (!opts.accept?.includes(res.status))
|
|
112
|
+
await this.refuseIfNotOk(res, `PUT ${path}`);
|
|
113
|
+
return { status: res.status, body: await this.parseBody(res, `PUT ${path}`), headers: res.headers };
|
|
114
|
+
}
|
|
90
115
|
async send(url, init) {
|
|
91
116
|
try {
|
|
92
117
|
return await this.fetchImpl(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
package/dist/var.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// var.ts — plain repository/workspace environment values. Values are sent only to the private
|
|
2
|
+
// /me/env-vars routes and are never included in CLI output.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { flagList, flagString, positionals } from "./args.js";
|
|
6
|
+
import { requireConfig } from "./config.js";
|
|
7
|
+
import { readContractCache } from "./contract.js";
|
|
8
|
+
import { UsageError } from "./errors.js";
|
|
9
|
+
import { parseEnvAssignments } from "./env/parse-env.js";
|
|
10
|
+
import { promptSecret, stdinIsTty } from "./prompt.js";
|
|
11
|
+
import { apiClient } from "./transport.js";
|
|
12
|
+
export const ENV_VARS_PATH = "/me/env-vars";
|
|
13
|
+
const NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
14
|
+
const REPO_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
15
|
+
const ACCEPT = [400, 401, 403, 404, 409, 422];
|
|
16
|
+
function scope(args) {
|
|
17
|
+
const repo = flagString(args, "repo");
|
|
18
|
+
if (repo !== undefined && !REPO_RE.test(repo))
|
|
19
|
+
throw new UsageError(`--repo must be owner/name (got "${repo}")`);
|
|
20
|
+
return repo === undefined ? { scope: "tenant" } : { scope: "repo", repo };
|
|
21
|
+
}
|
|
22
|
+
function selectedNames(args) {
|
|
23
|
+
const raw = flagList(args, "names");
|
|
24
|
+
if (raw.length === 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
const names = [...new Set(raw.flatMap((part) => part.split(",").map((name) => name.trim())).filter(Boolean))].sort();
|
|
27
|
+
if (names.length === 0)
|
|
28
|
+
throw new UsageError("--names needs one or more variable names");
|
|
29
|
+
const bad = names.find((name) => !NAME_RE.test(name));
|
|
30
|
+
if (bad !== undefined)
|
|
31
|
+
throw new UsageError(`--names includes an invalid env-style name: ${bad}`);
|
|
32
|
+
return names;
|
|
33
|
+
}
|
|
34
|
+
function scopeBody(target, ctx) {
|
|
35
|
+
if (target.scope === "tenant")
|
|
36
|
+
return { scope: "tenant" };
|
|
37
|
+
const [owner, name] = target.repo.split("/");
|
|
38
|
+
const repositories = readContractCache(ctx.home)?.doc.merge.repositories ?? [];
|
|
39
|
+
const repo = repositories.find((entry) => entry.owner.toLocaleLowerCase() === owner.toLocaleLowerCase() && entry.name.toLocaleLowerCase() === name.toLocaleLowerCase());
|
|
40
|
+
if (!repo)
|
|
41
|
+
throw new UsageError(`--repo ${target.repo} is not in the cached workspace contract; refresh catalyst contract and try again`);
|
|
42
|
+
return { scope: "repo", repoId: repo.repoId };
|
|
43
|
+
}
|
|
44
|
+
async function stdinText() {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin)
|
|
47
|
+
chunks.push(Buffer.from(chunk));
|
|
48
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
49
|
+
}
|
|
50
|
+
export async function cmdVar(args, ctx, deps = {}) {
|
|
51
|
+
const [sub, ...rest] = positionals(args);
|
|
52
|
+
if (sub !== "set" && sub !== "import")
|
|
53
|
+
throw new UsageError(`unknown var subcommand "${sub ?? ""}": set | import`);
|
|
54
|
+
if (rest.length !== 1)
|
|
55
|
+
throw new UsageError(sub === "set" ? "var set takes one NAME" : "var import takes one file (a .env file)");
|
|
56
|
+
const target = scope(args);
|
|
57
|
+
const writeScope = scopeBody(target, ctx);
|
|
58
|
+
const cfg = requireConfig(ctx);
|
|
59
|
+
const client = deps.client ?? apiClient(cfg, ctx);
|
|
60
|
+
if (sub === "set") {
|
|
61
|
+
const name = rest[0];
|
|
62
|
+
if (!NAME_RE.test(name))
|
|
63
|
+
throw new UsageError(`"${name}" is not an env-style name (A-Z, 0-9 and _, starting with a letter)`);
|
|
64
|
+
const raw = (deps.isTty ?? stdinIsTty)()
|
|
65
|
+
? await (deps.prompt ?? promptSecret)(`Value for ${name} (input hidden): `)
|
|
66
|
+
: await (deps.readStdin ?? stdinText)();
|
|
67
|
+
const value = raw.endsWith("\r\n") ? raw.slice(0, -2) : raw.endsWith("\n") ? raw.slice(0, -1) : raw;
|
|
68
|
+
if (value.length === 0)
|
|
69
|
+
throw new UsageError("var set needs a non-empty value");
|
|
70
|
+
const reply = await client.postJson(ENV_VARS_PATH, { ...writeScope, name, value }, { accept: ACCEPT });
|
|
71
|
+
if (reply.status !== 200 && reply.status !== 201) {
|
|
72
|
+
ctx.stderr(`var set refused (${reply.status}): ${reply.body.message ?? reply.body.error ?? "no reason given"}`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
if (args.json)
|
|
76
|
+
ctx.stdout(JSON.stringify({ name, scope: target.scope, repo: target.repo ?? null, created: reply.body.created ?? null }));
|
|
77
|
+
else
|
|
78
|
+
ctx.stdout(`stored ${name} (${target.scope}${target.repo ? `: ${target.repo}` : ""}${reply.body.created ? "; new" : "; updated"})`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
let text;
|
|
82
|
+
try {
|
|
83
|
+
text = readFileSync(rest[0], "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw new UsageError(`could not read ${rest[0]}: ${err instanceof Error ? err.message : String(err)}`);
|
|
87
|
+
}
|
|
88
|
+
const names = selectedNames(args);
|
|
89
|
+
const entries = parseEnvAssignments(text, names === undefined ? undefined : new Set(names));
|
|
90
|
+
const found = new Set(entries.map(({ name }) => name));
|
|
91
|
+
const created = [];
|
|
92
|
+
const updated = [];
|
|
93
|
+
const errors = [];
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const reply = await client.postJson(ENV_VARS_PATH, { ...writeScope, ...entry }, { accept: ACCEPT });
|
|
96
|
+
if (reply.status !== 200 && reply.status !== 201) {
|
|
97
|
+
errors.push({ name: entry.name, reason: reply.body.message ?? reply.body.error ?? `HTTP ${reply.status}` });
|
|
98
|
+
}
|
|
99
|
+
else if (reply.body.created)
|
|
100
|
+
created.push(entry.name);
|
|
101
|
+
else
|
|
102
|
+
updated.push(entry.name);
|
|
103
|
+
}
|
|
104
|
+
const absent = names?.filter((name) => !found.has(name)) ?? [];
|
|
105
|
+
if (args.json)
|
|
106
|
+
ctx.stdout(JSON.stringify({ scope: target.scope, repo: target.repo ?? null, created, updated, errors, ...(names === undefined ? {} : { requested: names, notFoundInFile: absent }) }));
|
|
107
|
+
else {
|
|
108
|
+
const count = created.length + updated.length;
|
|
109
|
+
ctx.stdout(count === 0 ? `stored nothing (${target.scope}${target.repo ? `: ${target.repo}` : ""})` : `stored ${count} variables: ${[...created, ...updated].join(", ")}`);
|
|
110
|
+
if (updated.length > 0)
|
|
111
|
+
ctx.stdout(` updated: ${updated.join(", ")}`);
|
|
112
|
+
if (absent.length > 0)
|
|
113
|
+
ctx.stdout(`not found in ${basename(rest[0])}: ${absent.join(", ")}`);
|
|
114
|
+
for (const error of errors)
|
|
115
|
+
ctx.stdout(`not stored: ${error.name} (${error.reason})`);
|
|
116
|
+
}
|
|
117
|
+
return errors.length > 0 ? 1 : 0;
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Customer skill bundle for Catalyst Cloud, named from the executive's seat — get set up, see what is happening, see what needs you, run a project, unstick what is stuck — plus the catalyst CLI that reads through the Catalyst Cloud SDK, writes through the agent proxy, and connects your machine to your
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "Customer skill bundle for Catalyst Cloud, named from the executive's seat — get set up, see what is happening, see what needs you, run a project, unstick what is stuck — plus the catalyst CLI that reads through the Catalyst Cloud SDK, writes through the agent proxy, and connects your machine to your cloud account with one command. The skills directory is the roster; this line never counts it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
@@ -45,10 +45,12 @@
|
|
|
45
45
|
"skill-lib:sync": "node scripts/sync-skill-lib.mjs",
|
|
46
46
|
"skills:scan": "node scripts/scan-skills.mjs",
|
|
47
47
|
"evals": "node scripts/run-evals.mjs",
|
|
48
|
-
"paths:check": "node scripts/vendor-paths.mjs --check"
|
|
48
|
+
"paths:check": "node scripts/vendor-paths.mjs --check",
|
|
49
|
+
"prose:check": "node scripts/check-public-prose.mjs"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"@catalyst-cloud/sdk": "^0.13.
|
|
52
|
+
"@catalyst-cloud/sdk": "^0.13.1",
|
|
53
|
+
"smol-toml": "^1.7.1",
|
|
52
54
|
"yaml": "^2.9.0"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
name: catalyst-github
|
|
3
3
|
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
|
-
allowed-tools: Bash(catalyst
|
|
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.14.0 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst's GitHub
|
|
10
10
|
|
|
11
|
-
You answer for the pull requests Catalyst opens and moves. The person asking wants to see one PR, know what is red, and know whether it will merge. Everything
|
|
11
|
+
You answer for the pull requests Catalyst opens and moves. The person asking wants to see one PR, know what is red, and know whether it will merge. Everything account-specific (the reviewer's login, the merge policy per repository, the four PR label names, the required checks) comes from the contract at run time and is printed by the scripts; you never restate it from memory.
|
|
12
12
|
|
|
13
13
|
## Run first
|
|
14
14
|
|
|
15
|
-
- `node scripts/read-pr.mjs --help
|
|
16
|
-
- `node scripts/is-it-mergeable.mjs --help
|
|
15
|
+
- `node scripts/read-pr.mjs --help`: a ticket's PR (or a PR by node id): state, branch, head, linked ticket, GitHub's mergeable state, every check, status and review. `--all` lists every PR for a ticket.
|
|
16
|
+
- `node scripts/is-it-mergeable.mjs --help`: the three legs (checks, reviewer signal, unresolved threads) judged under the repository's policy, plus the prerequisites; exit 1 when a leg is red.
|
|
17
17
|
|
|
18
|
-
Both read through `catalyst
|
|
18
|
+
Both read through `catalyst query`, `contract` and, when fresh, `replica`; the first stderr line names the source, and your answer repeats it.
|
|
19
19
|
|
|
20
20
|
## Load on demand
|
|
21
21
|
|
|
@@ -26,7 +26,7 @@ Both read through `catalyst-skills query`, `contract` and, when fresh, `replica`
|
|
|
26
26
|
|
|
27
27
|
## Rules
|
|
28
28
|
|
|
29
|
-
- Never compose a URL or call the API yourself. Every read is a `catalyst
|
|
29
|
+
- Never compose a URL or call the API yourself. Every read is a `catalyst` verb behind a script; run the script, do not read it.
|
|
30
30
|
- A queue-ready label is the cloud's attestation, never a lever you apply; "merged by the queue" is the terminal signal.
|
|
31
31
|
- Hold labels are the human's lever. Describe them; do not apply or remove them.
|
|
32
32
|
- Say what a key cannot see by name (PR labels, the reviewer's reaction, the queue's own state); never guess it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Is it mergeable?
|
|
2
2
|
|
|
3
|
-
The rule is invariant; the policy that applies to a repository is not. The policy names, the default, each repository's resolved policy and its source (repository setting,
|
|
3
|
+
The rule is invariant; the policy that applies to a repository is not. The policy names, the default, each repository's resolved policy and its source (repository setting, account setting, or the default), the reviewer login and the required check names all come from the contract's `merge` block at run time. `node scripts/is-it-mergeable.mjs <ticket>` reads them and prints one line per leg; the text below explains what those lines mean.
|
|
4
4
|
|
|
5
5
|
## Prerequisites before any leg counts
|
|
6
6
|
|
|
@@ -28,7 +28,13 @@ Three policy names are served on `merge.policies`; the default is `merge.default
|
|
|
28
28
|
| the strict variant | a clean pass at the current head, always. Findings on an earlier head keep blocking until the reviewer looks again. |
|
|
29
29
|
| checks and threads only | the reviewer requirement is dropped entirely; the two unconditional legs decide. |
|
|
30
30
|
|
|
31
|
-
A clean pass is
|
|
31
|
+
A clean pass is one of three signals from the reviewer login, most reliable first:
|
|
32
|
+
|
|
33
|
+
1. **A GitHub review on the exact head commit.** A review names the commit it was submitted against, so it cannot be mistaken for an earlier head.
|
|
34
|
+
2. **A 👍 reaction**, which counts only if it was given after the current head commit was made. The reaction carries no commit, so the cloud compares its time with the head commit's date, and that date is set by whoever made the commit. A push of a commit made before the 👍 (a force-push back to an older commit, a commit prepared earlier) can read as covered. A normal rebase or amend stamps a fresh date, so the everyday path works.
|
|
35
|
+
3. **A clean-pass comment** ("no major issues" in place of threads), honoured only if `cleanPassShapes[]` says so for that kind, with the same timing rule as the 👍.
|
|
36
|
+
|
|
37
|
+
A new push needs a fresh signal at the new head. The script judges none of these three itself.
|
|
32
38
|
|
|
33
39
|
## What the script can and cannot see
|
|
34
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# What a pull request accumulates as Catalyst moves a ticket
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
How Catalyst names branches, opens and rewrites pull requests, and what it pushes when, is the same for every account. The parts that vary are read live from the contract's `merge` block and never restated here: the reviewer's login (`merge.reviewerLogin`), which clean-pass shapes are honoured (`merge.cleanPassShapes[]`), the four PR label names (`merge.prLabels`), the checks the cloud waits on (`merge.cloudRemediateRequiredChecks`, on a 1.x contract only), and the policy per repository (`merge.repositories[]`, else `merge.defaultPolicy`). `catalyst contract --path merge` prints them.
|
|
4
4
|
|
|
5
5
|
## The branch
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ Three phases rewrite history on the ticket branch: implement pushes fresh with f
|
|
|
29
29
|
|
|
30
30
|
## The reviewer signal
|
|
31
31
|
|
|
32
|
-
An automated reviewer (login from the contract) reviews every PR. Its signal is one of four: clean, findings at the current head, findings at an earlier head, no review. A clean pass is a reaction,
|
|
32
|
+
An automated reviewer (login from the contract) reviews every PR. Its signal is one of four: clean, findings at the current head, findings at an earlier head, no review. A clean pass is usually a 👍 reaction from the reviewer, which counts only if it was given after the current head commit was made; a GitHub review on that exact commit is the most reliable signal. A terse "no major issues" comment in place of review threads is a documented convention; whether it is honoured is served on `cleanPassShapes[].honoured` and must be read there, never assumed. What counts under each policy is on `references/is-it-mergeable.md`.
|
|
33
33
|
|
|
34
34
|
## Labels on the PR
|
|
35
35
|
|
|
@@ -42,7 +42,7 @@ Four labels matter, all named on `merge.prLabels`, all matched exactly (a queue'
|
|
|
42
42
|
| `handStepsHold` | the cloud, automatically | the PR touches a path the queue configuration excludes (schema or migration paths, typically), so a person has to merge it by hand. |
|
|
43
43
|
| `preview` | a human | "deploy me a preview and hold the merge until I have looked". Removing the label is the approval. |
|
|
44
44
|
|
|
45
|
-
The mirror does not carry PR labels or reactions, so your key cannot read which holds a PR carries or whether the reviewer reacted; the scripts say so by name. GitHub's own page, or the
|
|
45
|
+
The mirror does not carry PR labels or reactions, so your key cannot read which holds a PR carries or whether the reviewer reacted; the scripts say so by name. GitHub's own page, or the account's settings, is where those live.
|
|
46
46
|
|
|
47
47
|
## The queue is optional and opt-out
|
|
48
48
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// is-it-mergeable.mjs — the three legs of merge evidence for one PR (checks, reviewer signal,
|
|
3
3
|
// unresolved threads) judged under the policy the tenant contract resolves for its repository, plus
|
|
4
4
|
// the prerequisites a queue applies before any leg counts (open, not a draft, no conflict). Every
|
|
5
|
-
// input comes from `catalyst
|
|
5
|
+
// input comes from `catalyst query`, `contract` and, when fresh, `replica sql`.
|
|
6
6
|
// Exit 0 no leg is red, 1 a leg or prerequisite is red, 2 this machine is not connected.
|
|
7
7
|
import { parseJson, runCli, runCliOrExit } from "./lib/cli.mjs";
|
|
8
8
|
import { resolvePull, truthy } from "./lib/pull.mjs";
|
|
@@ -34,7 +34,7 @@ if (!target) {
|
|
|
34
34
|
const { ticket, detail } = resolvePull(target);
|
|
35
35
|
const merge = parseJson(runCliOrExit(["contract", "--path", "merge", "--json"]).stdout);
|
|
36
36
|
if (!merge || !Array.isArray(merge.policies)) {
|
|
37
|
-
console.error("the contract's merge block did not parse; run: catalyst
|
|
37
|
+
console.error("the contract's merge block did not parse; run: catalyst contract --refresh");
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -103,7 +103,7 @@ if (replica && replica.verdict === "fresh") {
|
|
|
103
103
|
else if (unknown > 0) add("threads", "inconclusive", `${unknown} thread(s) with no resolved flag mirrored`);
|
|
104
104
|
else add("threads", "pass", `${count(1)} thread(s), all resolved (ancestry against force-pushes is judged by the cloud, not here)`);
|
|
105
105
|
} else {
|
|
106
|
-
add("threads", "inconclusive", `the PR detail carries no thread count and the replica is ${replica?.verdict ?? "unavailable"}; start it (catalyst
|
|
106
|
+
add("threads", "inconclusive", `the PR detail carries no thread count and the replica is ${replica?.verdict ?? "unavailable"}; start it (catalyst replica start --detach) for a local read`);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
const failed = legs.filter((l) => l.state === "fail");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// lib/cli.mjs — the one way a skill script reaches the Catalyst Cloud SDK and API: by spawning the
|
|
3
|
-
// catalyst
|
|
3
|
+
// catalyst CLI this bundle installed. It reads customer.json to find that CLI and nothing
|
|
4
4
|
// else; it never holds the key itself. This file is a library — run a sibling script with
|
|
5
5
|
// --help for usage. Identical in every skill of this bundle on purpose (skills install one directory
|
|
6
6
|
// at a time, so nothing shared outside the skill would ever be installed).
|
|
@@ -10,7 +10,9 @@ import { join } from "node:path";
|
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { CONNECT_COMMAND, hasCredential } from "./credential.mjs";
|
|
12
12
|
|
|
13
|
-
export const PACKAGE = "@catalyst-cloud/
|
|
13
|
+
export const PACKAGE = "@catalyst-cloud/cli";
|
|
14
|
+
/** The package ships two commands, so npx needs the one to run named: `npx -p <package> catalyst`. */
|
|
15
|
+
export const NPX_ARGS = ["-p", PACKAGE, "catalyst"];
|
|
14
16
|
export const CONNECT_HINT = `this machine is not connected to a tenant yet — run: ${CONNECT_COMMAND}`;
|
|
15
17
|
|
|
16
18
|
/** ~/.config/catalyst-cloud/customer.json, honouring CATALYST_SKILLS_HOME before HOME. */
|
|
@@ -41,15 +43,15 @@ export function requireCustomerConfig() {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
|
-
* Run one catalyst
|
|
45
|
-
* connect step recorded in customer.json; falls back to `npx @catalyst-cloud/catalyst
|
|
46
|
+
* Run one catalyst verb and return {code, stdout, stderr}. Spawns the CLI whose path the
|
|
47
|
+
* connect step recorded in customer.json; falls back to `npx -p @catalyst-cloud/cli catalyst` when
|
|
46
48
|
* no path is recorded or it no longer exists. Exits 2 when the machine is not connected.
|
|
47
49
|
*/
|
|
48
50
|
export function runCli(args, opts = {}) {
|
|
49
51
|
const cfg = requireCustomerConfig();
|
|
50
52
|
const recorded = typeof cfg.cliPath === "string" && existsSync(cfg.cliPath);
|
|
51
53
|
const command = recorded ? process.execPath : process.platform === "win32" ? "npx.cmd" : "npx";
|
|
52
|
-
const argv = recorded ? [cfg.cliPath, ...args] : [
|
|
54
|
+
const argv = recorded ? [cfg.cliPath, ...args] : [...NPX_ARGS, ...args];
|
|
53
55
|
const res = spawnSync(command, argv, {
|
|
54
56
|
encoding: "utf8",
|
|
55
57
|
input: opts.stdin,
|
|
@@ -58,7 +60,7 @@ export function runCli(args, opts = {}) {
|
|
|
58
60
|
shell: !recorded && process.platform === "win32",
|
|
59
61
|
});
|
|
60
62
|
if (res.error) {
|
|
61
|
-
console.error(`could not run ${recorded ? cfg.cliPath : `npx ${PACKAGE}`}: ${res.error.message}`);
|
|
63
|
+
console.error(`could not run ${recorded ? cfg.cliPath : `npx -p ${PACKAGE} catalyst`}: ${res.error.message}`);
|
|
62
64
|
process.exit(2);
|
|
63
65
|
}
|
|
64
66
|
return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
@@ -72,7 +74,7 @@ export function runCli(args, opts = {}) {
|
|
|
72
74
|
export function runCliOrExit(args, opts = {}) {
|
|
73
75
|
const res = runCli(args, opts);
|
|
74
76
|
if (res.code === 0) return res;
|
|
75
|
-
const why = res.stderr.trim() || res.stdout.trim() || `catalyst
|
|
77
|
+
const why = res.stderr.trim() || res.stdout.trim() || `catalyst ${args.join(" ")} exited ${res.code}`;
|
|
76
78
|
console.error(why);
|
|
77
79
|
process.exit(res.code === 2 ? 2 : 1);
|
|
78
80
|
}
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
//
|
|
7
7
|
// customer.json carries exactly one credential: a personal key (`key`), or the keyless login's
|
|
8
8
|
// session (`auth`, the recommended rail). A script never reads either for its value: it spawns the
|
|
9
|
-
// catalyst
|
|
9
|
+
// catalyst CLI, which authenticates with whichever is present and refreshes a login's token
|
|
10
10
|
// itself. A new credential kind lands here, once.
|
|
11
11
|
|
|
12
12
|
/** The command that connects this machine, as every not-connected line names it. */
|
|
13
13
|
export const CONNECT_COMMAND =
|
|
14
|
-
"npx @catalyst-cloud/catalyst
|
|
14
|
+
"npx -p @catalyst-cloud/cli catalyst login (or, with a personal key: CATALYST_CLOUD_TOKEN=<your personal key> npx -p @catalyst-cloud/cli catalyst login)";
|
|
15
15
|
|
|
16
16
|
/** True when `cfg` (parsed customer.json) holds a usable credential of either kind. Never throws. */
|
|
17
17
|
export function hasCredential(cfg) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// read-pr.mjs — one ticket's pull request (or one PR by node id) as the mirror holds it: state,
|
|
3
3
|
// branch, head, base, linked ticket and its stage, GitHub's own mergeable verdict, every check,
|
|
4
|
-
// review and commit status. Reads through `catalyst
|
|
4
|
+
// review and commit status. Reads through `catalyst query`; composes no URL.
|
|
5
5
|
import { linkedPulls, resolvePull, truthy } from "./lib/pull.mjs";
|
|
6
6
|
|
|
7
7
|
const HELP = `Usage: node scripts/read-pr.mjs <ticket | pr-node-id> [--all] [--json]
|
|
8
8
|
|
|
9
9
|
<ticket> a ticket identifier such as ABC-123: its open PR is shown (else the merged one, else the newest)
|
|
10
|
-
<pr-node-id> a GitHub pull-request node id, as printed by "catalyst
|
|
10
|
+
<pr-node-id> a GitHub pull-request node id, as printed by "catalyst query pulls"
|
|
11
11
|
--all for a ticket: list every PR that names it instead of one detail
|
|
12
12
|
--json print the raw detail document instead of the summary
|
|
13
13
|
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: catalyst-linear
|
|
3
3
|
description: >-
|
|
4
|
-
Catalyst's view of Linear on the customer's own
|
|
5
|
-
allowed-tools: Bash(catalyst
|
|
6
|
-
disable-model-invocation: true
|
|
4
|
+
Catalyst's view of Linear on the customer's own cloud account. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the local replica when it is fresh and the origin-fresh API otherwise, 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
|
+
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
7
6
|
---
|
|
8
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.0 — written in this repository for customer accounts -->
|
|
9
8
|
|
|
10
9
|
# Catalyst Linear
|
|
11
10
|
|
|
12
|
-
You read tickets and write to them
|
|
11
|
+
You read tickets and write to them in the person's cloud account, as Catalyst. Reads come from the replica when it is fresh, else the API, and name their source. Writes go out as the app actor, with routes, stage ids, label ids and the bookkeeping marker resolved from the contract by the `catalyst` CLI.
|
|
13
12
|
|
|
14
13
|
## Run first
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
Scripts are run, never read; each prints `--help`.
|
|
17
16
|
|
|
18
|
-
- `node scripts/read-ticket.mjs <ticket> [--comments]
|
|
19
|
-
- `node scripts/search.mjs <terms
|
|
20
|
-
- `node scripts/comment.mjs <ticket> --body <text> [--parent <id>] [--bookkeeping]
|
|
21
|
-
- `node scripts/move.mjs <ticket> --slot <slot
|
|
22
|
-
- `node scripts/label.mjs <ticket> --add <name> --remove <name
|
|
23
|
-
- `node scripts/create-ticket.mjs --team <key> --title <text> [--description <text> | --stdin]
|
|
17
|
+
- `node scripts/read-ticket.mjs <ticket> [--comments]`: one ticket, everything inline, source line on stderr.
|
|
18
|
+
- `node scripts/search.mjs <terms>`: tickets, PRs, projects, initiatives matching the terms.
|
|
19
|
+
- `node scripts/comment.mjs <ticket> --body <text> [--parent <id>] [--bookkeeping]`: a comment as the app actor.
|
|
20
|
+
- `node scripts/move.mjs <ticket> --slot <slot>`: a card move by slot (`--state-type backlog` parks).
|
|
21
|
+
- `node scripts/label.mjs <ticket> --add <name> --remove <name>`: labels, resolved through the contract.
|
|
22
|
+
- `node scripts/create-ticket.mjs --team <key> --title <text> [--description <text> | --stdin]`: a new ticket; cite its identifier only after it prints.
|
|
24
23
|
|
|
25
24
|
Exit codes: 0 done, 1 not found or a usage error, 2 this machine is not connected or the write was refused (budget spent, slot unmapped, label absent; the one line printed says which).
|
|
26
25
|
|
|
@@ -34,10 +33,9 @@ Exit codes: 0 done, 1 not found or a usage error, 2 this machine is not connecte
|
|
|
34
33
|
|
|
35
34
|
## Rules
|
|
36
35
|
|
|
37
|
-
- **Read before acting.**
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
43
|
-
- **One write per need, no loops.** Writes spend a daily budget the contract names; batch, and report a refusal rather than retrying.
|
|
36
|
+
- **Read before acting.** Read the description and the thread, never just the title. Cite identifiers and comment ids only after a script printed them, and quote the source line when freshness matters.
|
|
37
|
+
- **Reply where it arrived, as Catalyst.** Thread under the comment you answer (`--parent`); the human speaks for themselves.
|
|
38
|
+
- **Records carry the marker.** A merge note, a state-move log, a chain summary: `--bookkeeping`. A real question is not bookkeeping.
|
|
39
|
+
- **Move by slot, label by contract.** Moving to `dispatch` dispatches; the backlog-type state parks.
|
|
40
|
+
- **A decision is an ask, not a ticket.** Raise it through `what-needs-me`; a question filed as a ticket is held out of dispatch by shape.
|
|
41
|
+
- **One write per need.** Every write is visible in the person's Linear and spends the contract's daily budget; batch, and report a refusal once. A move into dispatch starts paid work on a coding account.
|
|
@@ -3,4 +3,4 @@ interface:
|
|
|
3
3
|
short_description: "Read a ticket and what Catalyst wrote on it; comment, move, label and file tickets as the app actor"
|
|
4
4
|
default_prompt: "Use $catalyst-linear to show me the ticket, its history, and what Catalyst wrote on it."
|
|
5
5
|
policy:
|
|
6
|
-
allow_implicit_invocation:
|
|
6
|
+
allow_implicit_invocation: true
|