agent-kitchen 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +105 -0
- package/dist/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.js +22 -0
- package/dist/commands/sync.d.ts +23 -0
- package/dist/commands/sync.js +111 -0
- package/dist/menu.d.ts +21 -0
- package/dist/menu.js +31 -0
- package/dist/output.d.ts +29 -0
- package/dist/output.js +31 -0
- package/dist/piece.d.ts +49 -0
- package/dist/piece.js +14 -0
- package/dist/pieces/cloud.d.ts +17 -0
- package/dist/pieces/cloud.js +93 -0
- package/dist/pieces/guard.d.ts +40 -0
- package/dist/pieces/guard.js +173 -0
- package/dist/pieces/issue-flow.d.ts +32 -0
- package/dist/pieces/issue-flow.js +147 -0
- package/dist/pieces/skills.d.ts +17 -0
- package/dist/pieces/skills.js +155 -0
- package/dist/repo.d.ts +48 -0
- package/dist/repo.js +117 -0
- package/dist/skills-files.d.ts +62 -0
- package/dist/skills-files.js +93 -0
- package/dist/tools.d.ts +48 -0
- package/dist/tools.js +85 -0
- package/package.json +20 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Claude cloud sessions (`@ramon-sg/claude-cloud`). The repo's version is the one
|
|
3
|
+
* `.claude/cloud.json` pins: the cloud machine runs exactly that one. Installing runs its
|
|
4
|
+
* `init`; updating only moves that version (and a `$schema` pinned to a version), which is
|
|
5
|
+
* how `claude-cloud` updates. Outside
|
|
6
|
+
* the cloud its hook does nothing, so a repo nobody opens in the cloud loses nothing.
|
|
7
|
+
*/
|
|
8
|
+
import { warn } from "../output.js";
|
|
9
|
+
import { readJson, writeJson } from "../repo.js";
|
|
10
|
+
import { compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
|
|
11
|
+
/** The piece's name, as `--skip` takes it. */
|
|
12
|
+
export const NAME = "claude-cloud";
|
|
13
|
+
/** The package on npm. */
|
|
14
|
+
const PACKAGE = "@ramon-sg/claude-cloud";
|
|
15
|
+
/** Its config in the repo, which pins its version. */
|
|
16
|
+
const CONFIG = ".claude/cloud.json";
|
|
17
|
+
/**
|
|
18
|
+
* The version `.claude/cloud.json` pins.
|
|
19
|
+
* @param config - The config, or null.
|
|
20
|
+
* @returns The version, or null if it has none.
|
|
21
|
+
*/
|
|
22
|
+
function pinned(config) {
|
|
23
|
+
return typeof config?.version === "string" && isRelease(config.version) ? config.version : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The step the repo needs, if any.
|
|
27
|
+
* @param config - The config, or null.
|
|
28
|
+
* @param latest - The latest on npm, or null.
|
|
29
|
+
* @returns The step, or null.
|
|
30
|
+
*/
|
|
31
|
+
function stepFor(config, latest) {
|
|
32
|
+
const version = pinned(config);
|
|
33
|
+
if (config === null) {
|
|
34
|
+
const spec = `${PACKAGE}@${latest ?? "1"}`;
|
|
35
|
+
return {
|
|
36
|
+
piece: NAME,
|
|
37
|
+
checked: true,
|
|
38
|
+
label: `claude-cloud ${latest ?? "1.x"}: prepares Claude cloud sessions (${CONFIG})`,
|
|
39
|
+
run: (context) => {
|
|
40
|
+
if (runNpx(context.root, [spec, "init"]))
|
|
41
|
+
return true;
|
|
42
|
+
warn(context.out, `claude-cloud init failed; run 'npx ${spec} init'.`);
|
|
43
|
+
return false;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (latest === null || (version !== null && compareVersions(latest, version) <= 0))
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
piece: NAME,
|
|
51
|
+
checked: true,
|
|
52
|
+
label: `claude-cloud ${version ?? "?"} → ${latest}: the version ${CONFIG} pins`,
|
|
53
|
+
run: (context) => {
|
|
54
|
+
const current = readJson(context.root, CONFIG) ?? {};
|
|
55
|
+
const schema = typeof current.$schema === "string" ? current.$schema.replace(/@ramon-sg\/claude-cloud@[^/]+\//, `${PACKAGE}@${latest}/`) : undefined;
|
|
56
|
+
writeJson(context.root, CONFIG, { ...current, ...(schema === undefined ? {} : { $schema: schema }), version: latest });
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What the repo has of claude-cloud, and the step it needs.
|
|
63
|
+
* @param root - The repo.
|
|
64
|
+
* @returns The piece's status.
|
|
65
|
+
* @example cloudStatus(root).state // "1.0.0, the latest"
|
|
66
|
+
*/
|
|
67
|
+
export function cloudStatus(root) {
|
|
68
|
+
const config = readJson(root, CONFIG);
|
|
69
|
+
const latest = npmLatest(PACKAGE);
|
|
70
|
+
const version = pinned(config);
|
|
71
|
+
const step = stepFor(config, latest);
|
|
72
|
+
const known = latest === null ? " (npm cannot tell the latest)" : "";
|
|
73
|
+
const state = config === null ? `not installed${known}` : version === null ? `${CONFIG} pins no version` : step === null ? `${version}${known}` : `${version}, ${latest ?? "?"} available`;
|
|
74
|
+
return { name: NAME, summary: "Claude cloud sessions: tools, Docker and the full clone", installed: config !== null, state, steps: step === null ? [] : [step] };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether `.claude/cloud.json` pins a version, and whether a newer one exists.
|
|
78
|
+
* @param root - The repo.
|
|
79
|
+
* @returns The checks.
|
|
80
|
+
* @example cloudChecks(root)[0].status // "ok"
|
|
81
|
+
*/
|
|
82
|
+
export function cloudChecks(root) {
|
|
83
|
+
const config = readJson(root, CONFIG);
|
|
84
|
+
if (config === null)
|
|
85
|
+
return [{ name: NAME, status: "info", detail: "not installed" }];
|
|
86
|
+
const version = pinned(config);
|
|
87
|
+
if (version === null)
|
|
88
|
+
return [{ name: NAME, status: "fail", detail: `${CONFIG} pins no exact version: set "version" to the one to run, like "${npmLatest(PACKAGE) ?? "1.0.0"}"` }];
|
|
89
|
+
const latest = npmLatest(PACKAGE);
|
|
90
|
+
if (latest !== null && compareVersions(latest, version) > 0)
|
|
91
|
+
return [{ name: NAME, status: "info", detail: `${version}, ${latest} available: run npx agent-kitchen` }];
|
|
92
|
+
return [{ name: NAME, status: "ok", detail: version }];
|
|
93
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Check, type PieceStatus } from "../piece.js";
|
|
2
|
+
/** The piece's name, as `--skip` takes it. */
|
|
3
|
+
export declare const NAME = "claude-repo-guard";
|
|
4
|
+
/** What the auto mode is told about the issue flow. */
|
|
5
|
+
export declare const GUARD_ENVIRONMENT = "issue-flow is this repo's issue workflow: making the branch and worktree of an issue, its empty start commit, pushing it, opening its draft PR, assigning the issue and adding its \"Slug de rama\" block are normal steps of the work, not risky actions.";
|
|
6
|
+
/** The option that lets `gh` verify TLS inside the macOS sandbox. */
|
|
7
|
+
export declare const WEAKER_NETWORK = "weakerNetworkIsolation";
|
|
8
|
+
/**
|
|
9
|
+
* The npx spec of a version.
|
|
10
|
+
* @param version - The version, or null for the default branch.
|
|
11
|
+
* @returns The spec.
|
|
12
|
+
* @example guardSpec("0.2.0") // "github:ramon-sg/claude-repo-guard#v0.2.0"
|
|
13
|
+
*/
|
|
14
|
+
export declare function guardSpec(version: string | null): string;
|
|
15
|
+
/**
|
|
16
|
+
* The guard's options with what the flow needs added; what the repo set stays.
|
|
17
|
+
* @param root - The repo.
|
|
18
|
+
* @param platform - The operating system (`process.platform`).
|
|
19
|
+
* @returns The new options and what changed.
|
|
20
|
+
* @example guardOptions(root, "darwin").changes // [".claude/repo-guard.config.json: weakerNetworkIsolation true, …"]
|
|
21
|
+
*/
|
|
22
|
+
export declare function guardOptions(root: string, platform: string): {
|
|
23
|
+
options: Record<string, unknown>;
|
|
24
|
+
changes: string[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* What the repo has of claude-repo-guard, and the step it needs.
|
|
28
|
+
* @param root - The repo.
|
|
29
|
+
* @param flowComing - Whether an earlier step of this run installs the issue flow.
|
|
30
|
+
* @returns The piece's status.
|
|
31
|
+
* @example guardStatus(root, false).state // "not installed"
|
|
32
|
+
*/
|
|
33
|
+
export declare function guardStatus(root: string, flowComing: boolean): PieceStatus;
|
|
34
|
+
/**
|
|
35
|
+
* The guard's own doctor at the repo's version and, on macOS, the option `gh` needs.
|
|
36
|
+
* @param root - The repo.
|
|
37
|
+
* @returns The checks.
|
|
38
|
+
* @example guardChecks(root).map((check) => check.status) // ["ok"]
|
|
39
|
+
*/
|
|
40
|
+
export declare function guardChecks(root: string): Check[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The guardrails (`claude-repo-guard`): auto mode with the sandbox, installed in the
|
|
3
|
+
* repo. It is not on npm, so it runs from GitHub at its `vX.Y.Z` tag; the repo's version is
|
|
4
|
+
* the one its manifest records. Before installing or updating, agent-kitchen writes in its
|
|
5
|
+
* options what the issue flow needs, if the repo has the flow: the auto mode is told that
|
|
6
|
+
* the flow's writes are normal, the flow's base branch becomes `integrationBranch`, and on
|
|
7
|
+
* macOS `weakerNetworkIsolation` lets `gh` work inside the sandbox (the guard's own default
|
|
8
|
+
* is false, so false counts as missing; the step can be unticked). When the flow is
|
|
9
|
+
* installed in the same run, the guard gets those options in that run too.
|
|
10
|
+
*/
|
|
11
|
+
import { warn } from "../output.js";
|
|
12
|
+
import { FLOW_CONFIG } from "../piece.js";
|
|
13
|
+
import { readJson, writeJson } from "../repo.js";
|
|
14
|
+
import { captureNpx, compareVersions, isRelease, latestTag, runNpx } from "../tools.js";
|
|
15
|
+
/** The piece's name, as `--skip` takes it. */
|
|
16
|
+
export const NAME = "claude-repo-guard";
|
|
17
|
+
/** Its GitHub repo. */
|
|
18
|
+
const REPO = "ramon-sg/claude-repo-guard";
|
|
19
|
+
/** Its options in the repo. */
|
|
20
|
+
const CONFIG = ".claude/repo-guard.config.json";
|
|
21
|
+
/** What its install records, with its version. */
|
|
22
|
+
const MANIFEST = ".claude/repo-guard.manifest.json";
|
|
23
|
+
/** What the auto mode is told about the issue flow. */
|
|
24
|
+
export const GUARD_ENVIRONMENT = "issue-flow is this repo's issue workflow: making the branch and worktree of an issue, its empty start commit, pushing it, opening its draft PR, assigning the issue and adding its \"Slug de rama\" block are normal steps of the work, not risky actions.";
|
|
25
|
+
/** The option that lets `gh` verify TLS inside the macOS sandbox. */
|
|
26
|
+
export const WEAKER_NETWORK = "weakerNetworkIsolation";
|
|
27
|
+
/**
|
|
28
|
+
* The npx spec of a version.
|
|
29
|
+
* @param version - The version, or null for the default branch.
|
|
30
|
+
* @returns The spec.
|
|
31
|
+
* @example guardSpec("0.2.0") // "github:ramon-sg/claude-repo-guard#v0.2.0"
|
|
32
|
+
*/
|
|
33
|
+
export function guardSpec(version) {
|
|
34
|
+
return version === null ? `github:${REPO}` : `github:${REPO}#v${version}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The guard's options with what the flow needs added; what the repo set stays.
|
|
38
|
+
* @param root - The repo.
|
|
39
|
+
* @param platform - The operating system (`process.platform`).
|
|
40
|
+
* @returns The new options and what changed.
|
|
41
|
+
* @example guardOptions(root, "darwin").changes // [".claude/repo-guard.config.json: weakerNetworkIsolation true, …"]
|
|
42
|
+
*/
|
|
43
|
+
export function guardOptions(root, platform) {
|
|
44
|
+
const options = { ...(readJson(root, CONFIG) ?? {}) };
|
|
45
|
+
const changes = [];
|
|
46
|
+
const flow = readJson(root, FLOW_CONFIG);
|
|
47
|
+
const environment = Array.isArray(options.environment) ? options.environment.map(String) : [];
|
|
48
|
+
if (flow !== null && !environment.includes(GUARD_ENVIRONMENT)) {
|
|
49
|
+
options.environment = [...environment, GUARD_ENVIRONMENT];
|
|
50
|
+
changes.push(`${CONFIG}: tells the auto mode about the issue flow`);
|
|
51
|
+
}
|
|
52
|
+
const baseBranch = typeof flow?.baseBranch === "string" ? flow.baseBranch : null;
|
|
53
|
+
if (baseBranch !== null && (options.integrationBranch === undefined || options.integrationBranch === "")) {
|
|
54
|
+
options.integrationBranch = baseBranch;
|
|
55
|
+
changes.push(`${CONFIG}: integrationBranch ${baseBranch}`);
|
|
56
|
+
}
|
|
57
|
+
if (platform === "darwin" && options[WEAKER_NETWORK] !== true) {
|
|
58
|
+
options[WEAKER_NETWORK] = true;
|
|
59
|
+
changes.push(`${CONFIG}: ${WEAKER_NETWORK} true, so gh works inside the macOS sandbox`);
|
|
60
|
+
}
|
|
61
|
+
return { options, changes };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Writes the flow's options and installs the guard at a version.
|
|
65
|
+
* @param context - Where it runs.
|
|
66
|
+
* @param version - The version, or null for the default branch.
|
|
67
|
+
* @returns Whether it installed.
|
|
68
|
+
*/
|
|
69
|
+
function install(context, version) {
|
|
70
|
+
const { options, changes } = guardOptions(context.root, process.platform);
|
|
71
|
+
if (changes.length > 0)
|
|
72
|
+
writeJson(context.root, CONFIG, options);
|
|
73
|
+
for (const change of changes)
|
|
74
|
+
context.out.stdout(` ${change}\n`);
|
|
75
|
+
if (runNpx(context.root, [guardSpec(version), "install", "--yes"]))
|
|
76
|
+
return true;
|
|
77
|
+
failed(context.out, version);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Writes the flow's options and reinstalls the guard at its version, if they are missing
|
|
82
|
+
* by now (the flow may have been installed earlier in this run).
|
|
83
|
+
* @param context - Where it runs.
|
|
84
|
+
* @param version - The guard's version, or null.
|
|
85
|
+
* @returns Whether it worked, or true if nothing was missing.
|
|
86
|
+
*/
|
|
87
|
+
function addOptions(context, version) {
|
|
88
|
+
if (guardOptions(context.root, process.platform).changes.length > 0)
|
|
89
|
+
return install(context, version);
|
|
90
|
+
context.out.stdout(" its options already have what the issue flow needs\n");
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Says how to retry an install that failed.
|
|
95
|
+
* @param out - Where to write.
|
|
96
|
+
* @param version - The version it tried.
|
|
97
|
+
*/
|
|
98
|
+
function failed(out, version) {
|
|
99
|
+
warn(out, `claude-repo-guard did not install; run 'npx ${guardSpec(version)} install' from your terminal (inside a Claude session its sandbox blocks .claude/settings.json).`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The version its manifest records.
|
|
103
|
+
* @param root - The repo.
|
|
104
|
+
* @returns The version, null if it records none, or undefined if it is not installed.
|
|
105
|
+
*/
|
|
106
|
+
function installedVersion(root) {
|
|
107
|
+
const manifest = readJson(root, MANIFEST);
|
|
108
|
+
if (manifest === null)
|
|
109
|
+
return undefined;
|
|
110
|
+
return typeof manifest.version === "string" && isRelease(manifest.version) ? manifest.version : null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The step the repo needs, if any.
|
|
114
|
+
* @param root - The repo.
|
|
115
|
+
* @param installed - The repo's version, null if unknown, undefined if not installed.
|
|
116
|
+
* @param latest - The latest tag, or null.
|
|
117
|
+
* @returns The step, or null.
|
|
118
|
+
*/
|
|
119
|
+
function stepFor(root, installed, latest, flowComing) {
|
|
120
|
+
if (installed === undefined)
|
|
121
|
+
return { piece: NAME, checked: true, label: `claude-repo-guard ${latest ?? "(main)"}: auto mode with the sandbox, and a doc of what it protects`, run: (context) => install(context, latest) };
|
|
122
|
+
if (latest !== null && (installed === null || compareVersions(latest, installed) > 0))
|
|
123
|
+
return { piece: NAME, checked: true, label: `claude-repo-guard ${installed ?? "?"} → ${latest}`, run: (context) => install(context, latest) };
|
|
124
|
+
if (flowComing || guardOptions(root, process.platform).changes.length > 0)
|
|
125
|
+
return { piece: NAME, checked: true, label: "claude-repo-guard: what the issue flow needs in its options", run: (context) => addOptions(context, installed) };
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* What the repo has of claude-repo-guard, and the step it needs.
|
|
130
|
+
* @param root - The repo.
|
|
131
|
+
* @param flowComing - Whether an earlier step of this run installs the issue flow.
|
|
132
|
+
* @returns The piece's status.
|
|
133
|
+
* @example guardStatus(root, false).state // "not installed"
|
|
134
|
+
*/
|
|
135
|
+
export function guardStatus(root, flowComing) {
|
|
136
|
+
const installed = installedVersion(root);
|
|
137
|
+
const latest = latestTag(REPO);
|
|
138
|
+
const known = latest === null ? " (GitHub has no release tag)" : "";
|
|
139
|
+
const state = installed === undefined ? `not installed${known}` : latest !== null && (installed === null || compareVersions(latest, installed) > 0) ? `${installed ?? "unknown version"}, ${latest} available` : `${installed ?? "unknown version"}${known}`;
|
|
140
|
+
const step = stepFor(root, installed, latest, flowComing);
|
|
141
|
+
return { name: NAME, summary: "guardrails: auto mode with the sandbox, writes only inside the worktree", installed: installed !== undefined, state, steps: step === null ? [] : [step] };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The guard's own doctor at the repo's version and, on macOS, the option `gh` needs.
|
|
145
|
+
* @param root - The repo.
|
|
146
|
+
* @returns The checks.
|
|
147
|
+
* @example guardChecks(root).map((check) => check.status) // ["ok"]
|
|
148
|
+
*/
|
|
149
|
+
export function guardChecks(root) {
|
|
150
|
+
const installed = installedVersion(root);
|
|
151
|
+
if (installed === undefined)
|
|
152
|
+
return [{ name: NAME, status: "info", detail: "not installed (Claude opens with your own permissions)" }];
|
|
153
|
+
const checks = [];
|
|
154
|
+
if (process.platform === "darwin" && readJson(root, CONFIG)?.[WEAKER_NETWORK] !== true)
|
|
155
|
+
checks.push({ name: `${NAME}: ${WEAKER_NETWORK}`, status: "fail", detail: `not true in ${CONFIG}, so gh fails inside the macOS sandbox: run npx agent-kitchen` });
|
|
156
|
+
const result = captureNpx(root, [guardSpec(installed), "doctor", "--json"]);
|
|
157
|
+
let found;
|
|
158
|
+
try {
|
|
159
|
+
found = JSON.parse(result?.out ?? "");
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
found = null;
|
|
163
|
+
}
|
|
164
|
+
if (!Array.isArray(found))
|
|
165
|
+
return [...checks, { name: NAME, status: "fail", detail: `could not run its doctor; run 'npx ${guardSpec(installed)} doctor'` }];
|
|
166
|
+
const bad = found.filter((check) => typeof check !== "object" || check === null || check.ok !== true);
|
|
167
|
+
if (bad.length === 0)
|
|
168
|
+
return [...checks, { name: NAME, status: "ok", detail: `${installed ?? "unknown version"}, its doctor passes` }];
|
|
169
|
+
return [...checks, ...bad.map((check) => {
|
|
170
|
+
const entry = typeof check === "object" && check !== null ? check : { name: "?", detail: String(check) };
|
|
171
|
+
return { name: `${NAME}: ${String(entry.name)}`, status: "fail", detail: String(entry.detail) };
|
|
172
|
+
})];
|
|
173
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Check, type PieceStatus } from "../piece.js";
|
|
2
|
+
/** The piece's name, as `--skip` takes it. */
|
|
3
|
+
export declare const NAME = "issue-flow";
|
|
4
|
+
/**
|
|
5
|
+
* The issue flow's version in the repo, as package.json has it: an exact release when the
|
|
6
|
+
* flow set it up, or anything else (a range, a `file:` link) someone wrote by hand.
|
|
7
|
+
* @param root - The repo.
|
|
8
|
+
* @returns The version in package.json, or null.
|
|
9
|
+
* @example flowVersion(root) // "0.2.0"
|
|
10
|
+
*/
|
|
11
|
+
export declare function flowSpec(root: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* The issue flow's release in the repo: what package.json pins, if it is an exact release.
|
|
14
|
+
* @param root - The repo.
|
|
15
|
+
* @returns The version, or null if there is none or it is not a release.
|
|
16
|
+
* @example flowVersion(root) // "0.2.0"
|
|
17
|
+
*/
|
|
18
|
+
export declare function flowVersion(root: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* What the repo has of the issue flow, and the step it needs.
|
|
21
|
+
* @param root - The repo.
|
|
22
|
+
* @returns The piece's status.
|
|
23
|
+
* @example issueFlowStatus(root).state // "0.1.0, 0.2.0 available"
|
|
24
|
+
*/
|
|
25
|
+
export declare function issueFlowStatus(root: string): PieceStatus;
|
|
26
|
+
/**
|
|
27
|
+
* The flow's own doctor at the repo's version, and whether its skills are at that tag.
|
|
28
|
+
* @param root - The repo.
|
|
29
|
+
* @returns The checks.
|
|
30
|
+
* @example issueFlowChecks(root)[0].name // "issue-flow: .agents/flow.json"
|
|
31
|
+
*/
|
|
32
|
+
export declare function issueFlowChecks(root: string): Check[];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The issue flow (`@ramon-sg/issue-flow`): its CLI, hooks and config, and its skills.
|
|
3
|
+
* The repo's version is the one in package.json. Installing runs its `init` at the latest
|
|
4
|
+
* version; updating runs its `upgrade` at the new one and then moves its skills to the same
|
|
5
|
+
* tag, because the skills call the CLI. If only the skills are off, it moves them alone.
|
|
6
|
+
*/
|
|
7
|
+
import { warn } from "../output.js";
|
|
8
|
+
import { FLOW_CATALOG, FLOW_PACKAGE } from "../piece.js";
|
|
9
|
+
import { dependencyVersion, readJson } from "../repo.js";
|
|
10
|
+
import { catalogSkills, manifestCatalogs, pinCatalog, updateSkills } from "../skills-files.js";
|
|
11
|
+
import { captureNpx, compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
|
|
12
|
+
/** The piece's name, as `--skip` takes it. */
|
|
13
|
+
export const NAME = "issue-flow";
|
|
14
|
+
/** How the catalog's ref is named when it is off. */
|
|
15
|
+
const SKILLS_CATALOG_LABEL = "the catalog";
|
|
16
|
+
/** The first version whose `doctor` prints `--json`. */
|
|
17
|
+
const JSON_DOCTOR = "0.2.0";
|
|
18
|
+
/**
|
|
19
|
+
* The issue flow's version in the repo, as package.json has it: an exact release when the
|
|
20
|
+
* flow set it up, or anything else (a range, a `file:` link) someone wrote by hand.
|
|
21
|
+
* @param root - The repo.
|
|
22
|
+
* @returns The version in package.json, or null.
|
|
23
|
+
* @example flowVersion(root) // "0.2.0"
|
|
24
|
+
*/
|
|
25
|
+
export function flowSpec(root) {
|
|
26
|
+
return dependencyVersion(readJson(root, "package.json"), FLOW_PACKAGE);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The issue flow's release in the repo: what package.json pins, if it is an exact release.
|
|
30
|
+
* @param root - The repo.
|
|
31
|
+
* @returns The version, or null if there is none or it is not a release.
|
|
32
|
+
* @example flowVersion(root) // "0.2.0"
|
|
33
|
+
*/
|
|
34
|
+
export function flowVersion(root) {
|
|
35
|
+
const spec = flowSpec(root);
|
|
36
|
+
return spec !== null && isRelease(spec) ? spec : null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The flow's skills that are not at a version's tag, or the catalog pinned elsewhere.
|
|
40
|
+
* @param root - The repo.
|
|
41
|
+
* @param version - The flow's version.
|
|
42
|
+
* @returns Their names; the catalog's ref as well if it is off.
|
|
43
|
+
*/
|
|
44
|
+
function skillsOff(root, version) {
|
|
45
|
+
const tag = `v${version}`;
|
|
46
|
+
const off = catalogSkills(root, FLOW_CATALOG).filter((skill) => skill.tag !== tag).map((skill) => `${skill.name} at ${skill.tag ?? "no tag"}`);
|
|
47
|
+
const source = manifestCatalogs(root).find((item) => item.catalog === FLOW_CATALOG);
|
|
48
|
+
return source !== undefined && source.ref !== tag ? [...off, `${SKILLS_CATALOG_LABEL} at ${source.ref ?? "the latest"}`] : off;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Moves the flow's skills to a version's tag: the catalog's ref in the manifest, and then
|
|
52
|
+
* `skills-manager update` on them.
|
|
53
|
+
* @param context - Where it runs.
|
|
54
|
+
* @param version - The flow's version.
|
|
55
|
+
* @returns Whether it worked, or true if the repo has none of the flow's skills.
|
|
56
|
+
*/
|
|
57
|
+
function moveSkills(context, version) {
|
|
58
|
+
if (!pinCatalog(context.root, FLOW_CATALOG, `v${version}`))
|
|
59
|
+
return true;
|
|
60
|
+
const failed = updateSkills(context.root, catalogSkills(context.root, FLOW_CATALOG).map((skill) => skill.name));
|
|
61
|
+
if (failed === null)
|
|
62
|
+
return true;
|
|
63
|
+
warn(context.out, `skills-manager did not update the flow's skills; run '${failed}' and then agent-kitchen again.`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Runs the flow's own command at a version.
|
|
68
|
+
* @param context - Where it runs.
|
|
69
|
+
* @param version - The version.
|
|
70
|
+
* @param args - The command and its flags.
|
|
71
|
+
* @returns Whether it exited 0.
|
|
72
|
+
*/
|
|
73
|
+
function runFlow(context, version, args) {
|
|
74
|
+
if (runNpx(context.root, [`${FLOW_PACKAGE}@${version}`, ...args]))
|
|
75
|
+
return true;
|
|
76
|
+
warn(context.out, `issue-flow ${args[0] ?? ""} failed; run 'npx ${FLOW_PACKAGE}@${version} ${args.join(" ")}'.`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The step the repo needs, if any.
|
|
81
|
+
* @param installed - The repo's version, or null.
|
|
82
|
+
* @param latest - The latest on npm, or null.
|
|
83
|
+
* @param root - The repo.
|
|
84
|
+
* @returns The step, or null.
|
|
85
|
+
*/
|
|
86
|
+
function stepFor(installed, latest, root) {
|
|
87
|
+
if (installed === null && latest !== null)
|
|
88
|
+
return { piece: NAME, checked: true, label: `issue-flow ${latest}: .agents/flow.json with its recipes, the hooks and scripts`, run: (context) => runFlow(context, latest, ["init", ...(context.yes ? ["--yes"] : [])]) };
|
|
89
|
+
if (installed !== null && latest !== null && compareVersions(latest, installed) > 0)
|
|
90
|
+
return { piece: NAME, checked: true, label: `issue-flow ${installed} → ${latest}: the CLI, its hooks and its skills`, run: (context) => runFlow(context, latest, ["upgrade"]) && moveSkills(context, latest) };
|
|
91
|
+
if (installed !== null && skillsOff(root, installed).length > 0)
|
|
92
|
+
return { piece: NAME, checked: true, label: `issue-flow's skills → v${installed}, the CLI's version`, run: (context) => moveSkills(context, installed) };
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What the repo has of the issue flow, and the step it needs.
|
|
97
|
+
* @param root - The repo.
|
|
98
|
+
* @returns The piece's status.
|
|
99
|
+
* @example issueFlowStatus(root).state // "0.1.0, 0.2.0 available"
|
|
100
|
+
*/
|
|
101
|
+
export function issueFlowStatus(root) {
|
|
102
|
+
const summary = "the issue flow: branch, worktree and draft PR per issue, and its skills";
|
|
103
|
+
const spec = flowSpec(root);
|
|
104
|
+
if (spec !== null && !isRelease(spec))
|
|
105
|
+
return { name: NAME, summary, installed: true, state: `package.json has ${spec}, not an exact release; agent-kitchen leaves it`, steps: [] };
|
|
106
|
+
const installed = flowVersion(root);
|
|
107
|
+
const latest = npmLatest(FLOW_PACKAGE);
|
|
108
|
+
const step = stepFor(installed, latest, root);
|
|
109
|
+
const known = latest === null ? " (npm cannot tell the latest)" : "";
|
|
110
|
+
const state = installed === null ? `not installed${known}` : latest !== null && compareVersions(latest, installed) > 0 ? `${installed}, ${latest} available` : `${installed}${known}`;
|
|
111
|
+
return { name: NAME, summary, installed: installed !== null, state, steps: step === null ? [] : [step] };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The flow's own doctor at the repo's version, and whether its skills are at that tag.
|
|
115
|
+
* @param root - The repo.
|
|
116
|
+
* @returns The checks.
|
|
117
|
+
* @example issueFlowChecks(root)[0].name // "issue-flow: .agents/flow.json"
|
|
118
|
+
*/
|
|
119
|
+
export function issueFlowChecks(root) {
|
|
120
|
+
const spec = flowSpec(root);
|
|
121
|
+
if (spec === null)
|
|
122
|
+
return [{ name: NAME, status: "info", detail: "not installed" }];
|
|
123
|
+
const installed = flowVersion(root);
|
|
124
|
+
if (installed === null)
|
|
125
|
+
return [{ name: NAME, status: "info", detail: `package.json has ${spec}, not an exact release` }];
|
|
126
|
+
if (compareVersions(installed, JSON_DOCTOR) < 0)
|
|
127
|
+
return [{ name: NAME, status: "info", detail: `${installed} predates its doctor --json: run npx agent-kitchen to update it` }];
|
|
128
|
+
const result = captureNpx(root, [`${FLOW_PACKAGE}@${installed}`, "doctor", "--json"]);
|
|
129
|
+
let found;
|
|
130
|
+
try {
|
|
131
|
+
found = JSON.parse(result?.out ?? "");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
found = null;
|
|
135
|
+
}
|
|
136
|
+
const own = Array.isArray(found)
|
|
137
|
+
? found.map((check) => ({ name: `${NAME}: ${String(check.name)}`, status: check.ok === true ? "ok" : "fail", detail: String(check.detail) }))
|
|
138
|
+
: [{ name: NAME, status: "fail", detail: `could not run its doctor; run 'npx ${FLOW_PACKAGE}@${installed} doctor'` }];
|
|
139
|
+
const off = skillsOff(root, installed);
|
|
140
|
+
const count = catalogSkills(root, FLOW_CATALOG).length;
|
|
141
|
+
const skills = count === 0 && off.length === 0
|
|
142
|
+
? { name: `${NAME}: skills`, status: "info", detail: "none installed" }
|
|
143
|
+
: off.length === 0
|
|
144
|
+
? { name: `${NAME}: skills`, status: "ok", detail: `${count} at v${installed}` }
|
|
145
|
+
: { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}, the CLI is ${installed}: run npx agent-kitchen` };
|
|
146
|
+
return [...own, skills];
|
|
147
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Check, type PieceStatus } from "../piece.js";
|
|
2
|
+
/** The piece's name, as `--skip` takes it. */
|
|
3
|
+
export declare const NAME = "skills";
|
|
4
|
+
/**
|
|
5
|
+
* What the repo has of the skills, and the steps it can take.
|
|
6
|
+
* @param root - The repo.
|
|
7
|
+
* @returns The piece's status.
|
|
8
|
+
* @example skillsStatus(root).state // "6 installed"
|
|
9
|
+
*/
|
|
10
|
+
export declare function skillsStatus(root: string): PieceStatus;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `skills-manager check` passes: disk, lock and manifest agree.
|
|
13
|
+
* @param root - The repo.
|
|
14
|
+
* @returns The checks.
|
|
15
|
+
* @example skillsChecks(root)[0].status // "ok"
|
|
16
|
+
*/
|
|
17
|
+
export declare function skillsChecks(root: string): Check[];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The skills, through `skills-manager`: the flow's catalog (pinned to the flow's
|
|
3
|
+
* version, because its skills call the CLI) and the general catalogs (each skill at its
|
|
4
|
+
* own tag). The first time it opens the `skills-manager` menu with every catalog. Later it
|
|
5
|
+
* offers to move the general skills to their catalog's latest tag, and, unticked, to pick
|
|
6
|
+
* the skills a catalog has that the repo does not take yet. The flow's skills move with the
|
|
7
|
+
* flow (pieces/issue-flow.ts), never here.
|
|
8
|
+
*/
|
|
9
|
+
import { warn } from "../output.js";
|
|
10
|
+
import { FLOW_CATALOG, GENERAL_CATALOGS, SKILLS_MANIFEST } from "../piece.js";
|
|
11
|
+
import { readJson } from "../repo.js";
|
|
12
|
+
import { catalogSkills, lockedSkills, manifestCatalogs, pinCatalog, skillsManagerSpec, updateSkills } from "../skills-files.js";
|
|
13
|
+
import { captureNpx, compareVersions, latestTag, runNpx } from "../tools.js";
|
|
14
|
+
import { flowVersion } from "./issue-flow.js";
|
|
15
|
+
/** The piece's name, as `--skip` takes it. */
|
|
16
|
+
export const NAME = "skills";
|
|
17
|
+
/** The skills picked without a terminal: the flow's, and the audit `close-pr` uses. */
|
|
18
|
+
const DEFAULT_SKILLS = [
|
|
19
|
+
...["github-issues", "start-issue", "close-pr", "pr-review-comments", "pr-worktree"].map((name) => `catalog:${FLOW_CATALOG}/${name}`),
|
|
20
|
+
"catalog:ramon-sg/agent-skills/code-audit",
|
|
21
|
+
];
|
|
22
|
+
/** A release tag. */
|
|
23
|
+
const RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
|
|
24
|
+
/**
|
|
25
|
+
* The `--catalog` flags: the flow's at the repo's flow version, if the repo has the flow,
|
|
26
|
+
* and the general ones.
|
|
27
|
+
* @param root - The repo.
|
|
28
|
+
* @returns The flags.
|
|
29
|
+
*/
|
|
30
|
+
function catalogArgs(root) {
|
|
31
|
+
const flow = flowVersion(root);
|
|
32
|
+
const catalogs = [...(flow === null ? [] : [`${FLOW_CATALOG}#v${flow}`]), ...GENERAL_CATALOGS];
|
|
33
|
+
return catalogs.flatMap((catalog) => ["--catalog", catalog]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The default skills the manifest does not take yet, of the catalogs offered.
|
|
37
|
+
* @param root - The repo.
|
|
38
|
+
* @returns Their ids.
|
|
39
|
+
*/
|
|
40
|
+
function missingDefaults(root) {
|
|
41
|
+
const manifest = readJson(root, SKILLS_MANIFEST);
|
|
42
|
+
const sources = Array.isArray(manifest?.sources) ? manifest.sources : [];
|
|
43
|
+
const taken = new Set(sources.flatMap((source) => (typeof source.catalog === "string" && Array.isArray(source.skills) ? source.skills.map((name) => `catalog:${String(source.catalog)}/${String(name)}`) : [])));
|
|
44
|
+
const flow = flowVersion(root) !== null;
|
|
45
|
+
return DEFAULT_SKILLS.filter((id) => !taken.has(id) && (flow || !id.startsWith(`catalog:${FLOW_CATALOG}/`)));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Opens the `skills-manager` menu with the catalogs; without a terminal or with `--yes`,
|
|
49
|
+
* picks the default skills the repo does not have yet.
|
|
50
|
+
* @param context - Where it runs.
|
|
51
|
+
* @returns Whether `skills-manager` worked, or true if there was nothing to pick.
|
|
52
|
+
*/
|
|
53
|
+
function pick(context) {
|
|
54
|
+
const spec = skillsManagerSpec(context.root);
|
|
55
|
+
const args = [spec, readJson(context.root, SKILLS_MANIFEST) === null ? "init" : "add", ...catalogArgs(context.root)];
|
|
56
|
+
const choose = context.yes || !context.out.interactive;
|
|
57
|
+
const missing = choose ? missingDefaults(context.root) : [];
|
|
58
|
+
if (choose && missing.length === 0) {
|
|
59
|
+
context.out.stdout(` the default skills are already in ${SKILLS_MANIFEST}\n`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (runNpx(context.root, choose ? [...args, "--select", missing.join(",")] : args))
|
|
63
|
+
return true;
|
|
64
|
+
warn(context.out, `skills-manager did not install the skills; run 'npx ${args.join(" ")}'.`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The step that moves a general catalog's skills to its latest tag, if they are behind. A
|
|
69
|
+
* catalog that follows a branch has no tag to move to, so it gets no step.
|
|
70
|
+
* @param root - The repo.
|
|
71
|
+
* @param catalog - The catalog.
|
|
72
|
+
* @param ref - The ref the manifest pins it to, or null.
|
|
73
|
+
* @returns The step, or null.
|
|
74
|
+
*/
|
|
75
|
+
function updateStep(root, catalog, ref) {
|
|
76
|
+
if (ref !== null && !RELEASE_TAG.test(ref))
|
|
77
|
+
return null;
|
|
78
|
+
const latest = latestTag(catalog);
|
|
79
|
+
if (latest === null)
|
|
80
|
+
return null;
|
|
81
|
+
const behind = catalogSkills(root, catalog).filter((skill) => skill.tag !== null && RELEASE_TAG.test(skill.tag) && compareVersions(latest, skill.tag.replace(/^v/, "")) > 0);
|
|
82
|
+
if (behind.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const from = [...new Set(behind.map((skill) => skill.tag ?? "?"))].join(", ");
|
|
85
|
+
return {
|
|
86
|
+
piece: NAME,
|
|
87
|
+
checked: true,
|
|
88
|
+
label: `${catalog}: ${behind.map((skill) => skill.name).join(", ")} ${from} → v${latest}`,
|
|
89
|
+
run: (context) => {
|
|
90
|
+
if (ref !== null)
|
|
91
|
+
pinCatalog(context.root, catalog, `v${latest}`);
|
|
92
|
+
const failed = updateSkills(context.root, behind.map((skill) => skill.name));
|
|
93
|
+
if (failed === null)
|
|
94
|
+
return true;
|
|
95
|
+
warn(context.out, `skills-manager did not update ${catalog}; run '${failed}' and then agent-kitchen again.`);
|
|
96
|
+
return false;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The skills the catalogs have and the repo does not take, as `skills-manager` lists them,
|
|
102
|
+
* without those already in the lock (it lists the ones that came through `requires`).
|
|
103
|
+
* @param root - The repo.
|
|
104
|
+
* @returns Their names, or null if `skills-manager` could not tell.
|
|
105
|
+
*/
|
|
106
|
+
function newSkills(root) {
|
|
107
|
+
const result = captureNpx(root, [skillsManagerSpec(root), "add", "--candidates", ...catalogArgs(root)]);
|
|
108
|
+
let found;
|
|
109
|
+
try {
|
|
110
|
+
found = JSON.parse(result?.out ?? "");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(found))
|
|
116
|
+
return null;
|
|
117
|
+
const locked = lockedSkills(root);
|
|
118
|
+
return found.map((candidate) => String(candidate.name)).filter((name) => locked[name] === undefined);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What the repo has of the skills, and the steps it can take.
|
|
122
|
+
* @param root - The repo.
|
|
123
|
+
* @returns The piece's status.
|
|
124
|
+
* @example skillsStatus(root).state // "6 installed"
|
|
125
|
+
*/
|
|
126
|
+
export function skillsStatus(root) {
|
|
127
|
+
const summary = "skills: the flow's, planning, code and test audits";
|
|
128
|
+
if (readJson(root, SKILLS_MANIFEST) === null) {
|
|
129
|
+
const label = `skills: pick them from ${[FLOW_CATALOG, ...GENERAL_CATALOGS].join(" and ")} (without a terminal, the flow's and code-audit)`;
|
|
130
|
+
return { name: NAME, summary, installed: false, state: "not installed", steps: [{ piece: NAME, checked: true, label, run: pick }] };
|
|
131
|
+
}
|
|
132
|
+
const general = manifestCatalogs(root).filter((source) => source.catalog !== FLOW_CATALOG);
|
|
133
|
+
const steps = general.flatMap((source) => updateStep(root, source.catalog, source.ref) ?? []);
|
|
134
|
+
const fresh = newSkills(root);
|
|
135
|
+
if (fresh !== null && fresh.length > 0)
|
|
136
|
+
steps.push({ piece: NAME, checked: false, label: `skills: ${fresh.length} more to pick (${fresh.join(", ")})`, run: pick });
|
|
137
|
+
const count = manifestCatalogs(root).reduce((total, source) => total + catalogSkills(root, source.catalog).length, 0);
|
|
138
|
+
const behind = steps.filter((step) => step.checked).length;
|
|
139
|
+
return { name: NAME, summary, installed: true, state: `${count} installed${behind > 0 ? ", some behind" : ""}`, steps };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether `skills-manager check` passes: disk, lock and manifest agree.
|
|
143
|
+
* @param root - The repo.
|
|
144
|
+
* @returns The checks.
|
|
145
|
+
* @example skillsChecks(root)[0].status // "ok"
|
|
146
|
+
*/
|
|
147
|
+
export function skillsChecks(root) {
|
|
148
|
+
if (readJson(root, SKILLS_MANIFEST) === null)
|
|
149
|
+
return [{ name: NAME, status: "info", detail: "not installed" }];
|
|
150
|
+
const spec = skillsManagerSpec(root);
|
|
151
|
+
const result = captureNpx(root, [spec, "check"]);
|
|
152
|
+
if (result?.status === 0)
|
|
153
|
+
return [{ name: NAME, status: "ok", detail: "skills-manager check passes" }];
|
|
154
|
+
return [{ name: NAME, status: "fail", detail: `disk, lock and manifest disagree: run 'npx ${spec} check'` }];
|
|
155
|
+
}
|