@catalyst-cloud/cli 0.9.5 → 0.13.1
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 +28 -0
- package/README.md +28 -29
- package/dist/args.js +19 -1
- package/dist/ask.js +30 -18
- package/dist/capabilities.js +5 -0
- package/dist/cli.js +24 -6
- package/dist/env/declaration-rules.js +163 -0
- package/dist/env/group.js +51 -0
- package/dist/env/inventory.js +102 -0
- package/dist/env/render.js +39 -0
- package/dist/env/scan-env-example.js +62 -0
- package/dist/env/scan-source.js +43 -0
- package/dist/env/scan-workflow.js +123 -0
- package/dist/env/scan-wrangler.js +120 -0
- package/dist/env/types.js +1 -0
- package/dist/env/walk.js +87 -0
- package/dist/env.js +76 -0
- package/dist/environment.js +1 -1
- package/dist/legacy.js +224 -0
- package/dist/project.js +129 -0
- package/dist/release.js +1 -1
- package/dist/repo.js +297 -0
- package/dist/runtime-verb.js +1 -1
- package/dist/secret.js +1 -1
- package/dist/skill-shape.js +21 -13
- package/dist/skills.js +44 -4
- package/dist/transport.js +26 -1
- package/package.json +7 -6
- 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 +7 -17
- 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/where-am-i.mjs +60 -4
- 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 +9 -12
- 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 +31 -0
- package/skills/what-this-repo-needs/agents/openai.yaml +6 -0
- package/skills/what-this-repo-needs/agents/portability.yaml +4 -0
- package/skills/what-this-repo-needs/references/what-each-group-means.md +38 -0
- package/skills/what-this-repo-needs/scripts/check.mjs +64 -0
- package/skills/what-this-repo-needs/scripts/inventory.mjs +61 -0
- package/skills/{catalyst-setup → what-this-repo-needs}/scripts/lib/cli.mjs +35 -22
- package/skills/{catalyst-setup → what-this-repo-needs}/scripts/lib/credential.mjs +2 -2
- package/skills/whats-happening/SKILL.md +27 -21
- package/skills/whats-happening/agents/openai.yaml +1 -1
- package/skills/whats-happening/references/coding-accounts.md +24 -0
- package/skills/whats-happening/references/routing-work.md +11 -29
- package/skills/whats-happening/references/settings-and-where-they-live.md +20 -0
- package/skills/whats-happening/references/stages-and-mapping.md +25 -0
- package/skills/whats-happening/references/status-reply.md +21 -21
- package/skills/whats-happening/references/the-ladder.md +30 -0
- package/skills/whats-happening/references/what-runs-next.md +32 -0
- package/skills/whats-happening/references/when-a-phase-fails.md +27 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +33 -40
- 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/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/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 -43
- 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/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 -88
- 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/repo.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// repo.ts — `catalyst repo agents-block` and `catalyst repo agent-setup`: a repository's agent setup,
|
|
2
|
+
// read and changed in a checkout the person names, never through git and never through the cloud.
|
|
3
|
+
//
|
|
4
|
+
// repo agents-block <path> [--write] the one Catalyst block in AGENTS.md, marker-delimited,
|
|
5
|
+
// replaced in place on a rerun (never duplicated)
|
|
6
|
+
// repo agent-setup <path> [--apply] [--with-check]
|
|
7
|
+
// what the repository holds for agents, whether it is
|
|
8
|
+
// portable, the plan that would make it so, and --apply
|
|
9
|
+
//
|
|
10
|
+
// Portable means: AGENTS.md is canonical; CLAUDE.md is a thin file that imports it with a line that
|
|
11
|
+
// is exactly `@AGENTS.md` plus Claude-only notes; `.agents/skills` and `.agents/rules` are canonical
|
|
12
|
+
// and `.claude/skills` and `.claude/rules` are relative symlinks to them. Every write lands in the
|
|
13
|
+
// working tree only: committing, branching and the pull request are the person's, in the open.
|
|
14
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
16
|
+
import { flagBool, positionals } from "./args.js";
|
|
17
|
+
import { UsageError } from "./errors.js";
|
|
18
|
+
export const BLOCK_START = "<!-- catalyst:start -->";
|
|
19
|
+
export const BLOCK_END = "<!-- catalyst:end -->";
|
|
20
|
+
/** The two skills the block names. ONE place each: the packs are being consolidated, and a rename
|
|
21
|
+
* lands here, then `repo agents-block --write` brings every repository's block up to date. */
|
|
22
|
+
export const BLOCK_PROCESS_SKILL = "whats-happening";
|
|
23
|
+
export const BLOCK_DECISION_SKILL = "catalyst-sop";
|
|
24
|
+
/** ⭐ THE block, word for word. Harness-agnostic; names the skills, never the process. */
|
|
25
|
+
export const CATALYST_AGENTS_BLOCK = [
|
|
26
|
+
BLOCK_START,
|
|
27
|
+
"## Catalyst",
|
|
28
|
+
"",
|
|
29
|
+
`This repository is worked through Catalyst: tickets move through a pipeline of agent-run phases, and pull requests merge through its queue. Before you plan, branch, review or merge here, load the skill that explains the process instead of inferring it from this file: \`${BLOCK_PROCESS_SKILL}\` (how a ticket runs, what each phase produces, why work waits), and \`${BLOCK_DECISION_SKILL}\` when it is installed (what to do when work stalls on a decision). Keep this block as it is; onboarding maintains it.`,
|
|
30
|
+
BLOCK_END,
|
|
31
|
+
].join("\n");
|
|
32
|
+
/** Where the block sits in `text`, or null. Only the FIRST start marker and the first end after it count. */
|
|
33
|
+
function locateBlock(text) {
|
|
34
|
+
const start = text.indexOf(BLOCK_START);
|
|
35
|
+
if (start === -1)
|
|
36
|
+
return null;
|
|
37
|
+
const endAt = text.indexOf(BLOCK_END, start);
|
|
38
|
+
if (endAt === -1)
|
|
39
|
+
return null;
|
|
40
|
+
return { start, end: endAt + BLOCK_END.length };
|
|
41
|
+
}
|
|
42
|
+
/** The block's state in an AGENTS.md text (`null` text = no file). */
|
|
43
|
+
export function blockState(text) {
|
|
44
|
+
if (text === null)
|
|
45
|
+
return "absent";
|
|
46
|
+
const at = locateBlock(text);
|
|
47
|
+
if (at === null)
|
|
48
|
+
return "missing";
|
|
49
|
+
return text.slice(at.start, at.end) === CATALYST_AGENTS_BLOCK ? "current" : "stale";
|
|
50
|
+
}
|
|
51
|
+
/** The text with the block current: replaced in place, appended after one blank line, or the whole
|
|
52
|
+
* file. Pure and idempotent: applying it to its own output changes nothing. */
|
|
53
|
+
export function withBlock(text) {
|
|
54
|
+
if (text === null || text.trim() === "")
|
|
55
|
+
return `${CATALYST_AGENTS_BLOCK}\n`;
|
|
56
|
+
const at = locateBlock(text);
|
|
57
|
+
if (at !== null)
|
|
58
|
+
return `${text.slice(0, at.start)}${CATALYST_AGENTS_BLOCK}${text.slice(at.end)}`;
|
|
59
|
+
const body = text.replace(/\s+$/, "");
|
|
60
|
+
return `${body}\n\n${CATALYST_AGENTS_BLOCK}\n`;
|
|
61
|
+
}
|
|
62
|
+
function linkState(root, rel, canonicalRel) {
|
|
63
|
+
const p = join(root, rel);
|
|
64
|
+
let st;
|
|
65
|
+
try {
|
|
66
|
+
st = lstatSync(p);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { kind: "absent" };
|
|
70
|
+
}
|
|
71
|
+
if (st.isSymbolicLink()) {
|
|
72
|
+
const target = readlinkSync(p);
|
|
73
|
+
const resolved = resolve(dirname(p), target);
|
|
74
|
+
return { kind: "symlink", target, resolvesToCanonical: resolved === resolve(root, canonicalRel) && existsSync(resolved) };
|
|
75
|
+
}
|
|
76
|
+
return st.isDirectory() ? { kind: "dir" } : { kind: "file" };
|
|
77
|
+
}
|
|
78
|
+
const OTHER_MARKERS = [".codex", ".cursor", ".cursorrules", ".windsurfrules", "GEMINI.md", ".github/copilot-instructions.md"];
|
|
79
|
+
/** Read a checkout's agent setup. Never writes. */
|
|
80
|
+
export function inspectRepo(root) {
|
|
81
|
+
const agentsPath = join(root, "AGENTS.md");
|
|
82
|
+
const claudePath = join(root, "CLAUDE.md");
|
|
83
|
+
const agentsText = existsSync(agentsPath) ? readFileSync(agentsPath, "utf8") : null;
|
|
84
|
+
const claudeText = existsSync(claudePath) ? readFileSync(claudePath, "utf8") : null;
|
|
85
|
+
const claudeLines = claudeText === null ? [] : claudeText.split("\n");
|
|
86
|
+
const importsAgentsMd = claudeLines.some((l) => l.trim() === "@AGENTS.md");
|
|
87
|
+
const otherLines = claudeLines.filter((l) => l.trim() !== "" && l.trim() !== "@AGENTS.md").length;
|
|
88
|
+
const report = {
|
|
89
|
+
path: root,
|
|
90
|
+
agentsMd: { present: agentsText !== null, lines: agentsText === null ? 0 : agentsText.replace(/\n$/, "").split("\n").length, bytes: agentsText === null ? 0 : Buffer.byteLength(agentsText), block: blockState(agentsText) },
|
|
91
|
+
claudeMd: { present: claudeText !== null, importsAgentsMd, otherLines },
|
|
92
|
+
agentsDir: { skills: isDir(join(root, ".agents", "skills")), rules: isDir(join(root, ".agents", "rules")) },
|
|
93
|
+
claudeDir: { skills: linkState(root, ".claude/skills", ".agents/skills"), rules: linkState(root, ".claude/rules", ".agents/rules") },
|
|
94
|
+
others: OTHER_MARKERS.filter((m) => existsSync(join(root, m))),
|
|
95
|
+
verdict: "portable",
|
|
96
|
+
plan: [],
|
|
97
|
+
blockers: [],
|
|
98
|
+
};
|
|
99
|
+
// CLAUDE.md with guidance of its own and no import: its content belongs in AGENTS.md.
|
|
100
|
+
if (report.claudeMd.present && !importsAgentsMd) {
|
|
101
|
+
report.plan.push(report.agentsMd.present
|
|
102
|
+
? "append CLAUDE.md's content to AGENTS.md and leave CLAUDE.md as the thin importer (`@AGENTS.md` plus Claude-only notes)"
|
|
103
|
+
: "create AGENTS.md from CLAUDE.md's content and leave CLAUDE.md as the thin importer (`@AGENTS.md`)");
|
|
104
|
+
}
|
|
105
|
+
else if (!report.agentsMd.present && !report.claudeMd.present) {
|
|
106
|
+
report.plan.push("create AGENTS.md (the Catalyst block starts it; add the repository's own guidance there)");
|
|
107
|
+
}
|
|
108
|
+
for (const name of ["skills", "rules"]) {
|
|
109
|
+
const c = report.claudeDir[name];
|
|
110
|
+
const canonical = report.agentsDir[name];
|
|
111
|
+
if (c.kind === "dir" && !canonical)
|
|
112
|
+
report.plan.push(`move .claude/${name} to .agents/${name} and leave .claude/${name} as a relative symlink to it`);
|
|
113
|
+
else if (c.kind === "dir" && canonical)
|
|
114
|
+
report.blockers.push(`.claude/${name} and .agents/${name} are both real directories: merge them by hand, then leave .claude/${name} as a relative symlink`);
|
|
115
|
+
else if (c.kind === "symlink" && !c.resolvesToCanonical)
|
|
116
|
+
report.blockers.push(`.claude/${name} is a symlink to ${c.target}, not to .agents/${name}: point it there by hand`);
|
|
117
|
+
else if (c.kind === "absent" && canonical)
|
|
118
|
+
report.plan.push(`add .claude/${name} as a relative symlink to .agents/${name}`);
|
|
119
|
+
else if (c.kind === "file")
|
|
120
|
+
report.blockers.push(`.claude/${name} is a file, not a directory: look at it by hand`);
|
|
121
|
+
}
|
|
122
|
+
if (report.blockers.length > 0)
|
|
123
|
+
report.verdict = "needs_hand_merge";
|
|
124
|
+
else if (report.plan.length > 0)
|
|
125
|
+
report.verdict = "convertible";
|
|
126
|
+
return report;
|
|
127
|
+
}
|
|
128
|
+
function isDir(p) {
|
|
129
|
+
try {
|
|
130
|
+
return statSync(p).isDirectory();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const THIN_CLAUDE_MD = "# CLAUDE.md\n\nAll project context for any coding agent is in AGENTS.md, imported below. Only Claude-specific notes belong in this file.\n\n@AGENTS.md\n";
|
|
137
|
+
/** Perform a convertible plan in the working tree. Refuses when anything needs a hand merge. */
|
|
138
|
+
export function applyPlan(root, before) {
|
|
139
|
+
if (before.verdict === "needs_hand_merge")
|
|
140
|
+
throw new UsageError(`nothing applied: ${before.blockers.join("; ")}`);
|
|
141
|
+
const done = [];
|
|
142
|
+
const agentsPath = join(root, "AGENTS.md");
|
|
143
|
+
const claudePath = join(root, "CLAUDE.md");
|
|
144
|
+
if (before.claudeMd.present && !before.claudeMd.importsAgentsMd) {
|
|
145
|
+
const claudeText = readFileSync(claudePath, "utf8");
|
|
146
|
+
const existing = existsSync(agentsPath) ? readFileSync(agentsPath, "utf8").replace(/\s+$/, "") : "";
|
|
147
|
+
const merged = existing === "" ? claudeText.replace(/\s+$/, "") + "\n" : `${existing}\n\n${claudeText.replace(/\s+$/, "")}\n`;
|
|
148
|
+
writeFileSync(agentsPath, merged);
|
|
149
|
+
writeFileSync(claudePath, THIN_CLAUDE_MD);
|
|
150
|
+
done.push(before.agentsMd.present ? "appended CLAUDE.md's content to AGENTS.md; CLAUDE.md now imports it" : "created AGENTS.md from CLAUDE.md; CLAUDE.md now imports it");
|
|
151
|
+
}
|
|
152
|
+
else if (!before.agentsMd.present && !before.claudeMd.present) {
|
|
153
|
+
writeFileSync(agentsPath, withBlock(null));
|
|
154
|
+
done.push("created AGENTS.md holding the Catalyst block");
|
|
155
|
+
}
|
|
156
|
+
for (const name of ["skills", "rules"]) {
|
|
157
|
+
const c = before.claudeDir[name];
|
|
158
|
+
const canonicalPath = join(root, ".agents", name);
|
|
159
|
+
const claudeLink = join(root, ".claude", name);
|
|
160
|
+
if (c.kind === "dir" && !before.agentsDir[name]) {
|
|
161
|
+
mkdirSync(join(root, ".agents"), { recursive: true });
|
|
162
|
+
renameSync(claudeLink, canonicalPath);
|
|
163
|
+
symlinkSync(relative(dirname(claudeLink), canonicalPath), claudeLink, "dir");
|
|
164
|
+
done.push(`moved .claude/${name} to .agents/${name} and left a relative symlink`);
|
|
165
|
+
}
|
|
166
|
+
else if (c.kind === "absent" && before.agentsDir[name]) {
|
|
167
|
+
mkdirSync(join(root, ".claude"), { recursive: true });
|
|
168
|
+
symlinkSync(relative(dirname(claudeLink), canonicalPath), claudeLink, "dir");
|
|
169
|
+
done.push(`added .claude/${name} as a relative symlink to .agents/${name}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return done;
|
|
173
|
+
}
|
|
174
|
+
/** The lightweight check a customer can run in CI: node only, no dependencies, exit 1 on a finding. */
|
|
175
|
+
export const AGENTS_MD_CHECK_SCRIPT = `#!/usr/bin/env node
|
|
176
|
+
// agents-md-check.mjs — keeps this repository's agent setup portable. Run it in CI:
|
|
177
|
+
// node scripts/agents-md-check.mjs
|
|
178
|
+
// It fails when CLAUDE.md stops importing AGENTS.md, when .claude/skills or .claude/rules stops being
|
|
179
|
+
// a relative symlink into .agents/, when AGENTS.md outgrows its budget, or when a repository path
|
|
180
|
+
// AGENTS.md cites no longer exists. Budgets: AGENTS_MD_MAX_LINES and AGENTS_MD_MAX_BYTES (env).
|
|
181
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync } from "node:fs";
|
|
182
|
+
import { dirname, resolve } from "node:path";
|
|
183
|
+
|
|
184
|
+
const root = process.cwd();
|
|
185
|
+
const maxLines = Number(process.env.AGENTS_MD_MAX_LINES ?? 120);
|
|
186
|
+
const maxBytes = Number(process.env.AGENTS_MD_MAX_BYTES ?? 16 * 1024);
|
|
187
|
+
const findings = [];
|
|
188
|
+
const agents = existsSync("AGENTS.md") ? readFileSync("AGENTS.md", "utf8") : null;
|
|
189
|
+
if (agents === null) findings.push("AGENTS.md is missing");
|
|
190
|
+
else {
|
|
191
|
+
const lines = agents.replace(/\\n$/, "").split("\\n");
|
|
192
|
+
if (lines.length > maxLines) findings.push(\`AGENTS.md is \${lines.length} lines (budget \${maxLines})\`);
|
|
193
|
+
if (Buffer.byteLength(agents) > maxBytes) findings.push(\`AGENTS.md is \${Buffer.byteLength(agents)} bytes (budget \${maxBytes})\`);
|
|
194
|
+
for (const m of agents.matchAll(/\`([\\w.-]+(?:\\/[\\w.-]+)+)\`/g)) {
|
|
195
|
+
const p = m[1];
|
|
196
|
+
if (!existsSync(resolve(root, p))) findings.push(\`AGENTS.md cites \${p}, which does not exist\`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (existsSync("CLAUDE.md")) {
|
|
200
|
+
const n = readFileSync("CLAUDE.md", "utf8").split("\\n").filter((l) => l.trim() === "@AGENTS.md").length;
|
|
201
|
+
if (n !== 1) findings.push(\`CLAUDE.md must import AGENTS.md on exactly one line reading @AGENTS.md (found \${n})\`);
|
|
202
|
+
}
|
|
203
|
+
for (const name of ["skills", "rules"]) {
|
|
204
|
+
const link = \`.claude/\${name}\`;
|
|
205
|
+
const canonical = \`.agents/\${name}\`;
|
|
206
|
+
if (!existsSync(canonical) && !existsSync(link)) continue;
|
|
207
|
+
let st;
|
|
208
|
+
try { st = lstatSync(link); } catch { findings.push(\`\${link} is missing; make it a relative symlink to \${canonical}\`); continue; }
|
|
209
|
+
if (!st.isSymbolicLink()) { findings.push(\`\${link} is a real \${st.isDirectory() ? "directory" : "file"}; make \${canonical} canonical and \${link} a relative symlink\`); continue; }
|
|
210
|
+
const target = readlinkSync(link);
|
|
211
|
+
if (target.startsWith("/")) findings.push(\`\${link} is an absolute symlink (\${target}); make it relative\`);
|
|
212
|
+
if (resolve(dirname(link), target) !== resolve(root, canonical)) findings.push(\`\${link} points at \${target}, not \${canonical}\`);
|
|
213
|
+
}
|
|
214
|
+
for (const f of findings) console.error(\`agents-md-check: \${f}\`);
|
|
215
|
+
if (findings.length > 0) process.exit(1);
|
|
216
|
+
console.log("agents-md-check: portable");
|
|
217
|
+
`;
|
|
218
|
+
const describeLink = (s) => (s.kind === "symlink" ? `symlink → ${s.target}${s.resolvesToCanonical ? "" : " (not the canonical directory)"}` : s.kind === "dir" ? "real directory" : s.kind === "file" ? "a file" : "absent");
|
|
219
|
+
export function reportLines(r) {
|
|
220
|
+
const lines = [
|
|
221
|
+
`repository: ${r.path}`,
|
|
222
|
+
`AGENTS.md: ${r.agentsMd.present ? `${r.agentsMd.lines} lines, ${r.agentsMd.bytes} bytes; Catalyst block ${r.agentsMd.block}` : "absent"}`,
|
|
223
|
+
`CLAUDE.md: ${r.claudeMd.present ? (r.claudeMd.importsAgentsMd ? `imports AGENTS.md${r.claudeMd.otherLines ? ` plus ${r.claudeMd.otherLines} lines of Claude-only notes` : ""}` : `${r.claudeMd.otherLines} lines of its own guidance, no @AGENTS.md import`) : "absent"}`,
|
|
224
|
+
`.agents/skills: ${r.agentsDir.skills ? "present" : "absent"}; .claude/skills: ${describeLink(r.claudeDir.skills)}`,
|
|
225
|
+
`.agents/rules: ${r.agentsDir.rules ? "present" : "absent"}; .claude/rules: ${describeLink(r.claudeDir.rules)}`,
|
|
226
|
+
];
|
|
227
|
+
if (r.others.length > 0)
|
|
228
|
+
lines.push(`also present: ${r.others.join(", ")}`);
|
|
229
|
+
lines.push(`verdict: ${r.verdict === "portable" ? "portable (AGENTS.md canonical, Claude files import or link to it)" : r.verdict === "convertible" ? "convertible; --apply performs this plan in the working tree:" : "needs a hand merge before --apply:"}`);
|
|
230
|
+
for (const p of r.plan)
|
|
231
|
+
lines.push(` - ${p}`);
|
|
232
|
+
for (const b of r.blockers)
|
|
233
|
+
lines.push(` ⛔ ${b}`);
|
|
234
|
+
if (r.agentsMd.block !== "current")
|
|
235
|
+
lines.push(` - the Catalyst block is ${r.agentsMd.block}: catalyst repo agents-block ${r.path} --write`);
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
238
|
+
function checkoutRoot(args, sub) {
|
|
239
|
+
const [, path, ...extra] = positionals(args);
|
|
240
|
+
if (!path)
|
|
241
|
+
throw new UsageError(`repo ${sub} needs the checkout's path`);
|
|
242
|
+
if (extra.length > 0)
|
|
243
|
+
throw new UsageError(`repo ${sub} takes one path`);
|
|
244
|
+
const root = resolve(path);
|
|
245
|
+
if (!isDir(root))
|
|
246
|
+
throw new UsageError(`${path} is not a directory on this machine`);
|
|
247
|
+
return root;
|
|
248
|
+
}
|
|
249
|
+
export async function cmdRepo(args, ctx) {
|
|
250
|
+
const [sub] = positionals(args);
|
|
251
|
+
if (sub !== "agents-block" && sub !== "agent-setup")
|
|
252
|
+
throw new UsageError("repo needs agents-block <path> [--write] or agent-setup <path> [--apply] [--with-check]");
|
|
253
|
+
const root = checkoutRoot(args, sub);
|
|
254
|
+
const emit = (body, lines) => { if (args.json)
|
|
255
|
+
ctx.stdout(JSON.stringify(body));
|
|
256
|
+
else
|
|
257
|
+
for (const l of lines)
|
|
258
|
+
ctx.stdout(l); };
|
|
259
|
+
if (sub === "agents-block") {
|
|
260
|
+
if (flagBool(args, "apply") || flagBool(args, "with-check"))
|
|
261
|
+
throw new UsageError("--apply and --with-check belong to repo agent-setup");
|
|
262
|
+
const file = join(root, "AGENTS.md");
|
|
263
|
+
const before = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
264
|
+
const state = blockState(before);
|
|
265
|
+
if (!flagBool(args, "write")) {
|
|
266
|
+
const hint = state === "current" ? "nothing to do" : `run with --write to ${state === "absent" ? "create AGENTS.md with it" : state === "missing" ? "append it" : "bring it up to date"} (in the working tree; commit it on a branch and open a pull request)`;
|
|
267
|
+
emit({ path: root, agentsMd: before !== null, block: state, changed: false }, [`AGENTS.md: Catalyst block ${state} — ${hint}`]);
|
|
268
|
+
return state === "current" ? 0 : 1;
|
|
269
|
+
}
|
|
270
|
+
const after = withBlock(before);
|
|
271
|
+
const changed = after !== before;
|
|
272
|
+
if (changed)
|
|
273
|
+
writeFileSync(file, after);
|
|
274
|
+
emit({ path: root, agentsMd: true, block: "current", changed, was: state }, [
|
|
275
|
+
changed ? `AGENTS.md: Catalyst block ${state === "absent" ? "written to a new AGENTS.md" : state === "missing" ? "appended" : "updated in place"}; commit it on a branch and open a pull request` : "AGENTS.md: Catalyst block already current; nothing changed",
|
|
276
|
+
]);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
if (flagBool(args, "write"))
|
|
280
|
+
throw new UsageError("--write belongs to repo agents-block");
|
|
281
|
+
const before = inspectRepo(root);
|
|
282
|
+
const applied = [];
|
|
283
|
+
if (flagBool(args, "apply"))
|
|
284
|
+
applied.push(...applyPlan(root, before));
|
|
285
|
+
if (flagBool(args, "with-check")) {
|
|
286
|
+
const dir = join(root, "scripts");
|
|
287
|
+
mkdirSync(dir, { recursive: true });
|
|
288
|
+
const target = join(dir, "agents-md-check.mjs");
|
|
289
|
+
const same = existsSync(target) && readFileSync(target, "utf8") === AGENTS_MD_CHECK_SCRIPT;
|
|
290
|
+
if (!same)
|
|
291
|
+
writeFileSync(target, AGENTS_MD_CHECK_SCRIPT, { mode: 0o755 });
|
|
292
|
+
applied.push(same ? "scripts/agents-md-check.mjs already current" : "wrote scripts/agents-md-check.mjs (run it in CI: node scripts/agents-md-check.mjs)");
|
|
293
|
+
}
|
|
294
|
+
const after = applied.length > 0 ? inspectRepo(root) : before;
|
|
295
|
+
emit({ ...after, applied }, [...(applied.length ? ["applied:", ...applied.map((a) => ` - ${a}`)] : []), ...reportLines(after)]);
|
|
296
|
+
return after.verdict === "portable" && after.agentsMd.block === "current" ? 0 : 1;
|
|
297
|
+
}
|
package/dist/runtime-verb.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// runtime-verb.ts — CTC-2158, Tier 2. `catalyst
|
|
1
|
+
// runtime-verb.ts — CTC-2158, Tier 2. `catalyst runtime status|install|path|uninstall`: the
|
|
2
2
|
// verb every unsupported-runtime message points at. It downloads the pinned Node named by
|
|
3
3
|
// package.json's `catalystCloud.pinnedNode`, verifies it, and unpacks it under this CLI's own
|
|
4
4
|
// cache — it never touches the machine's default Node and needs no admin rights.
|
package/dist/secret.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// secret.ts — `catalyst
|
|
1
|
+
// secret.ts — `catalyst secret set|import`: put a repository's secrets into the cloud from the
|
|
2
2
|
// terminal, as the person who is logged in (CTC-3549). The cloud gates both on an admin or owner seat,
|
|
3
3
|
// encrypts the value, and records who wrote it and where it came from, never the value itself.
|
|
4
4
|
//
|
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/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.13.1",
|
|
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,16 +45,17 @@
|
|
|
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.
|
|
52
|
+
"@catalyst-cloud/sdk": "^0.13.1",
|
|
53
|
+
"yaml": "^2.9.0"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@types/node": "^26.3.0",
|
|
55
57
|
"@vitest/coverage-v8": "^4.1.11",
|
|
56
58
|
"typescript": "^7.0.2",
|
|
57
|
-
"vitest": "^4.1.11"
|
|
58
|
-
"yaml": "^2.9.0"
|
|
59
|
+
"vitest": "^4.1.11"
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -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.13.1 — 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
|
|