agent-kitchen 0.3.1 → 0.5.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/README.md CHANGED
@@ -3,10 +3,12 @@
3
3
  Un solo comando para instalar, actualizar y revisar lo que un repo usa para trabajar con agentes: el flujo de issues con sus skills, las skills generales, las sesiones de Claude en la nube y las barandas de seguridad.
4
4
 
5
5
  ```sh
6
- npx agent-kitchen # muestra qué hay, qué falta y qué está atrasado, y pregunta
6
+ npx agent-kitchen apply # muestra qué hay, qué falta y qué está atrasado, y pregunta
7
7
  npx agent-kitchen doctor # revisa que todo funcione, sin cambiar nada
8
8
  ```
9
9
 
10
+ Qué hace cada herramienta y cada skill, y dónde están: [docs/README.md](docs/README.md) para personas y [docs/agent-guide.md](docs/agent-guide.md) para agentes.
11
+
10
12
  Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay, corre esa con los mismos argumentos, así que `npx` nunca usa una copia vieja de su caché.
11
13
 
12
14
  ## En simple
@@ -15,17 +17,19 @@ Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay,
15
17
  - Pregunta una vez: **todo lo sugerido** (Enter), **elegir** los pasos y las skills, o **salir**. Todas las preguntas van antes de instalar; después corre solo.
16
18
  - Cada paso es una línea con ✓ o ✗. La salida de cada herramienta solo aparece si falla, o con `--verbose`.
17
19
  - Es rápido: las versiones se consultan todas a la vez y las herramientas se descargan mientras respondes.
18
- - Cada pieza la instala su propia herramienta. `agent-kitchen` solo sabe cuáles hay, en qué versión están y cómo se conectan entre sí.
20
+ - Cada pieza la instala su propia herramienta. `agent-kitchen` solo sabe cuáles hay, en qué versión están y cómo se conectan entre sí. Las excepciones son una línea de `AGENTS.md` y la versión de `skills-manager` en `package.json`, que escribe él mismo.
19
21
  - Correrlo de nuevo no rompe nada: si todo está al día, lo dice y termina.
20
22
 
21
23
  ## Las piezas
22
24
 
23
25
  | Pieza | Qué es | Versión del repo | Última | Instalar | Actualizar |
24
26
  |---|---|---|---|---|---|
25
- | `issue-flow` | Flujo de issues ([`@ramon-sg/issue-flow`](https://github.com/ramon-sg/issue-flow)): rama, worktree y PR en draft por issue, y sus 5 skills | `package.json` | npm | `issue-flow init` | `issue-flow upgrade` en la versión nueva, y sus skills a la misma tag |
27
+ | `skills-manager` | La versión de [`@ramon-sg/skills-manager`](https://github.com/ramon-sg/skills-manager) que corre `agent-kitchen`: como mínimo 1.2.0. Si falta la agrega y si es menor la sube; nunca la baja. Un rango o `workspace:` no se toca, y avisa si la versión que corre queda bajo el mínimo. Solo en repos con `package.json` que usan skills | `package.json` | — | La agrega `agent-kitchen` | La sube `agent-kitchen` |
28
+ | `issue-flow` | Flujo de issues ([`@ramon-sg/issue-flow`](https://github.com/ramon-sg/issue-flow)): rama, worktree y PR en draft por issue, y sus 5 skills | `package.json` | npm | `issue-flow init` | `issue-flow upgrade` en la versión nueva, y sus skills a la misma tag. Desde 0.2.6, si a GitHub le faltan labels del flujo (`spike`, `P1`–`P4`…), `issue-flow labels sync` los crea |
26
29
  | `claude-cloud` | Sesiones en la nube ([`@ramon-sg/claude-cloud`](https://github.com/ramon-sg/claude-cloud)): herramientas, Docker y el clone completo | `.claude/cloud.json` | npm | `claude-cloud init` | Cambia la versión que fija `.claude/cloud.json` |
27
30
  | `claude-repo-guard` | Barandas ([`claude-repo-guard`](https://github.com/ramon-sg/claude-repo-guard)): modo auto con sandbox, escribe solo en su worktree | `.claude/repo-guard.manifest.json` | Tags `vX.Y.Z` de GitHub | `apply` en esa tag (`install` antes de 0.5.0) | `apply` en la tag nueva |
28
31
  | `skills` | Skills con [`skills-manager`](https://github.com/ramon-sg/skills-manager): las del flujo y las de [`agent-skills`](https://github.com/ramon-sg/agent-skills) (planes, auditoría de código y de tests) | `.agents/skills.lock.json` | Tags del catálogo | `skills-manager init --select` con las elegidas | `skills-manager update` |
32
+ | `agents-md` | Una línea en `AGENTS.md` que le pide al agente mirar la versión instalada de una biblioteca antes de usarla. Si el repo tiene `CLAUDE.md`, le agrega `@AGENTS.md`, porque si no Claude Code no lee `AGENTS.md`. Solo en repos con `package.json` | `AGENTS.md` | — | La escribe `agent-kitchen` | — |
29
33
 
30
34
  Si npm o GitHub no responden, la pieza se muestra pero no se ofrece actualizarla. Si un paso falla, los demás siguen y el comando dice cómo reintentarlo.
31
35
 
@@ -40,19 +44,21 @@ Si npm o GitHub no responden, la pieza se muestra pero no se ofrece actualizarla
40
44
  ## Uso
41
45
 
42
46
  ```sh
43
- npx agent-kitchen # pregunta, en una terminal (Enter: todo lo sugerido)
44
- npx agent-kitchen --dry-run # solo muestra qué haría
45
- npx agent-kitchen --yes # hace lo sugerido sin preguntar
46
- npx agent-kitchen --verbose # muestra la salida de cada herramienta
47
- npx agent-kitchen --skip claude-cloud # deja fuera una pieza (o varias, con comas)
48
- npx agent-kitchen doctor # revisa cada pieza con su propio doctor
47
+ npx agent-kitchen apply # pregunta, en una terminal (Enter: todo lo sugerido)
48
+ npx agent-kitchen apply --dry-run # solo muestra qué haría
49
+ npx agent-kitchen apply --yes # hace lo sugerido sin preguntar
50
+ npx agent-kitchen apply --verbose # muestra la salida de cada herramienta
51
+ npx agent-kitchen apply --skip claude-cloud # deja fuera una pieza (o varias, con comas)
52
+ npx agent-kitchen doctor # revisa cada pieza con su propio doctor
49
53
  ```
50
54
 
55
+ - **Sin subcomando** (`npx agent-kitchen`) hace lo mismo que `apply`, por ahora, y avisa que se use `apply`. En una versión mayor pasará a solo mostrar el estado.
56
+
51
57
  - **Sin terminal y sin `--yes`** (por ejemplo, desde un agente), solo muestra lo que ofrecería.
52
58
  - **Corre en el checkout principal,** no en un worktree, y desde tu terminal. Dentro de una sesión de Claude, el sandbox de `claude-repo-guard` no deja escribir `.claude/settings.json`.
53
59
  - **Al terminar,** corre el `install` de tu gestor de paquetes, revisa el diff y haz commit.
54
60
 
55
- **`doctor`** corre el doctor de cada herramienta en la versión del repo: `issue-flow doctor --json`, `skills-manager check` y `claude-repo-guard doctor --json`. Además revisa que las skills del flujo estén en la tag de su CLI, que `.claude/cloud.json` fije una versión exacta y, en macOS, `weakerNetworkIsolation`. Lo que no está instalado o tiene una versión nueva solo se anota con `·`. Sale con 1 si algo falla (`✗`).
61
+ **`doctor`** corre el doctor de cada herramienta en la versión del repo: `issue-flow doctor --json`, `skills-manager check` y `claude-repo-guard doctor --json`. Además revisa que `skills-manager` esté al menos en 1.2.0, que las skills del flujo estén en la tag de su CLI, qué labels del flujo le faltan a GitHub (con `·`), que `.claude/cloud.json` fije una versión exacta y, en macOS, `weakerNetworkIsolation`. En un repo con `package.json`, también la línea de `AGENTS.md` y, si hay `CLAUDE.md`, su `@AGENTS.md`. Lo que no está instalado o tiene una versión nueva solo se anota con `·`. Sale con 1 si algo falla (`✗`).
56
62
 
57
63
  ## Sumar una pieza
58
64
 
package/dist/cli.js CHANGED
@@ -21,15 +21,15 @@ const PACKAGE = "agent-kitchen";
21
21
  const HANDED_OVER = "AGENT_KITCHEN_HANDED_OVER";
22
22
  /** Usage text. */
23
23
  const USAGE = `Uso:
24
- agent-kitchen [--yes | --dry-run] [--verbose] [--skip <pieza>,...]
24
+ agent-kitchen apply [--yes | --dry-run] [--verbose] [--skip <pieza>,...]
25
25
  agent-kitchen doctor
26
26
 
27
27
  agent-kitchen revisa cada pieza de las herramientas de agentes del repo (${pieceNames().join(", ")}),
28
- muestra qué tiene el repo y la última versión de cada una, y ofrece instalar lo que falta y
29
- actualizar lo que está atrasado. En una terminal pregunta una vez: hacer todo lo sugerido,
28
+ muestra qué tiene el repo y la última versión de cada una, y apply ofrece instalar lo que falta
29
+ y actualizar lo que está atrasado. En una terminal pregunta una vez: hacer todo lo sugerido,
30
30
  o elegir los pasos y las skills. --yes hace lo sugerido sin preguntar; sin terminal solo lo
31
31
  muestra. --verbose muestra la salida de cada herramienta. Córrelo en la copia principal del
32
- repo, desde tu terminal.
32
+ repo, desde tu terminal. Sin subcomando hace lo mismo que apply, por ahora.
33
33
 
34
34
  doctor corre las revisiones de cada pieza en la versión del repo, sin cambiar nada.`;
35
35
  /**
@@ -92,13 +92,15 @@ async function main(argv) {
92
92
  if (handed !== null)
93
93
  return handed;
94
94
  const [command, ...rest] = positionals;
95
- if (rest.length > 0 || (command !== undefined && command !== "doctor"))
95
+ if (rest.length > 0 || (command !== undefined && command !== "doctor" && command !== "apply"))
96
96
  throw new UsageError(`no existe el comando ${positionals.join(" ")}.`);
97
97
  if (command === "doctor") {
98
98
  if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined || values.verbose !== undefined)
99
99
  throw new UsageError("doctor no lleva opciones.");
100
100
  return doctor(process.cwd(), out);
101
101
  }
102
+ if (command === undefined)
103
+ out.stderr("agent-kitchen: usa 'agent-kitchen apply'; sin subcomando dejará de instalar en una versión mayor.\n");
102
104
  const skip = (values.skip ?? "").split(",").map((name) => name.trim()).filter((name) => name !== "");
103
105
  return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, verbose: values.verbose ?? false, skip }, out);
104
106
  }
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * @file `agent-kitchen doctor`: whether each piece the repo has works, with the doctor of each
3
- * tool at the repo's own version: the issue flow (and its skills at its tag),
3
+ * tool at the repo's own version: the version of skills-manager agent-kitchen needs, the
4
+ * issue flow (and its skills at its tag),
4
5
  * `skills-manager check`, the version `.claude/cloud.json` pins, and claude-repo-guard. It
5
- * changes nothing. A piece the repo does not have, or a newer release, is only noted.
6
+ * changes nothing. A piece the repo does not have, or a newer release, is only noted, and so is
7
+ * a missing rule in AGENTS.md.
6
8
  */
7
9
  import type { Output } from "../output.js";
8
10
  /**
@@ -1,7 +1,9 @@
1
+ import { agentsMdChecks } from "../pieces/agents-md.js";
1
2
  import { cloudChecks } from "../pieces/cloud.js";
2
3
  import { guardChecks } from "../pieces/guard.js";
3
4
  import { issueFlowChecks } from "../pieces/issue-flow.js";
4
5
  import { skillsChecks } from "../pieces/skills.js";
6
+ import { skillsManagerChecks } from "../pieces/skills-manager.js";
5
7
  import { mainCheckout } from "../repo.js";
6
8
  /** The mark of each check's status. */
7
9
  const MARKS = { ok: "✓", fail: "✗", info: "·" };
@@ -15,7 +17,7 @@ const MARKS = { ok: "✓", fail: "✗", info: "·" };
15
17
  */
16
18
  export async function doctor(cwd, out) {
17
19
  const root = mainCheckout(cwd, "agent-kitchen doctor");
18
- const checks = (await Promise.all([issueFlowChecks(root), skillsChecks(root), cloudChecks(root), guardChecks(root)])).flat();
20
+ const checks = (await Promise.all([skillsManagerChecks(root), issueFlowChecks(root), skillsChecks(root), cloudChecks(root), guardChecks(root), agentsMdChecks(root)])).flat();
19
21
  for (const check of checks)
20
22
  out.stdout(`${MARKS[check.status]} ${check.name}: ${check.detail}\n`);
21
23
  return checks.some((check) => check.status === "fail") ? 1 : 0;
@@ -10,7 +10,7 @@ export interface SyncOptions {
10
10
  /**
11
11
  * The names `--skip` takes.
12
12
  * @returns The pieces' names.
13
- * @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills"]
13
+ * @example pieceNames() // ["skills-manager", "issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
14
14
  */
15
15
  export declare function pieceNames(): string[];
16
16
  /**
@@ -1,11 +1,14 @@
1
1
  /**
2
- * @file `agent-kitchen [--yes] [--dry-run] [--verbose] [--skip <piece>,...]`: the one command.
3
- * It looks at each piece (the issue flow, claude-cloud, claude-repo-guard, the skills) with
2
+ * @file `agent-kitchen apply [--yes] [--dry-run] [--verbose] [--skip <piece>,...]`: the one command
3
+ * that changes the repo (and, for now, also the bare `agent-kitchen`).
4
+ * It looks at each piece (the version of skills-manager, the issue flow, claude-cloud,
5
+ * claude-repo-guard, the skills, the rule in AGENTS.md) with
4
6
  * every lookup at once, shows what the repo has, and offers the steps that install what is
5
7
  * missing or update what is behind. In a terminal it asks first, all in one go: everything
6
8
  * suggested, or pick the steps and the skills; whether to replace the repo's own skills.
7
- * While the person answers, the tools download. Then the steps run in order (the flow
8
- * first, the skills last, since they follow the flow's version), one line each, with the
9
+ * While the person answers, the tools download. Then the steps run in order (skills-manager
10
+ * first, since the flow moves its skills with it; the skills last, since they follow the
11
+ * flow's version), one line each, with the
9
12
  * tools' output only when one fails; one that fails does not stop the rest. With `--yes`
10
13
  * it does what is suggested without asking (and keeps the repo's own skills); without a
11
14
  * terminal it only shows the steps.
@@ -17,31 +20,37 @@ import { cloudStatus, NAME as CLOUD } from "../pieces/cloud.js";
17
20
  import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
18
21
  import { issueFlowStatus, NAME as FLOW } from "../pieces/issue-flow.js";
19
22
  import { keepOwn, NAME as SKILLS, ownInPick, skillsStatus } from "../pieces/skills.js";
23
+ import { agentsMdStatus, NAME as AGENTS_MD } from "../pieces/agents-md.js";
24
+ import { NAME as SKILLS_MANAGER, skillsManagerStatus } from "../pieces/skills-manager.js";
20
25
  import { mainCheckout, packageManager } from "../repo.js";
21
26
  import { download, failedOutput, setVerbose } from "../tools.js";
22
27
  /** Every piece, in the order its steps run. */
23
- const PIECES = [FLOW, CLOUD, GUARD, SKILLS];
28
+ const PIECES = [SKILLS_MANAGER, FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
24
29
  /**
25
30
  * The names `--skip` takes.
26
31
  * @returns The pieces' names.
27
- * @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills"]
32
+ * @example pieceNames() // ["skills-manager", "issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
28
33
  */
29
34
  export function pieceNames() {
30
35
  return [...PIECES];
31
36
  }
32
37
  /**
33
- * Each piece's status, with every lookup at once. The guard's step depends on whether the
34
- * flow is about to be installed, so it waits for the flow's status.
38
+ * Each piece's status, with every lookup at once. The guard's step and the rule in AGENTS.md
39
+ * depend on whether the flow is about to be installed (it writes package.json), so they wait
40
+ * for the flow's status.
35
41
  * @param root - The repo.
36
42
  * @param skip - The pieces left out.
37
43
  * @returns The statuses, in the pieces' order.
38
44
  */
39
45
  async function statusesOf(root, skip) {
46
+ const skillsManager = skip.includes(SKILLS_MANAGER) ? null : skillsManagerStatus(root, !skip.includes(SKILLS));
40
47
  const flow = skip.includes(FLOW) ? null : issueFlowStatus(root);
41
48
  const cloud = skip.includes(CLOUD) ? null : cloudStatus(root);
42
49
  const skills = skip.includes(SKILLS) ? null : skillsStatus(root, !skip.includes(FLOW));
43
- const guard = skip.includes(GUARD) ? null : (flow ?? Promise.resolve(null)).then((status) => guardStatus(root, status !== null && !status.installed && status.steps.length > 0));
44
- const all = await Promise.all([flow, cloud, guard, skills]);
50
+ const flowComing = (flow ?? Promise.resolve(null)).then((status) => status !== null && !status.installed && status.steps.length > 0);
51
+ const guard = skip.includes(GUARD) ? null : flowComing.then((coming) => guardStatus(root, coming));
52
+ const agentsMd = skip.includes(AGENTS_MD) ? null : flowComing.then((coming) => agentsMdStatus(root, coming));
53
+ const all = await Promise.all([skillsManager, flow, cloud, guard, skills, agentsMd]);
45
54
  return all.flatMap((status) => status ?? []);
46
55
  }
47
56
  /**
@@ -70,7 +79,7 @@ function keepOwnSkills(root, step, out) {
70
79
  if (own.length === 0)
71
80
  return;
72
81
  Object.assign(step.skills, keepOwn(step.skills, own));
73
- out.stdout(`Se dejan fuera las skills que el repo ya tiene como propias: ${own.join(", ")}. Para reemplazarlas, corre agent-kitchen en una terminal.\n`);
82
+ out.stdout(`Se dejan fuera las skills que el repo ya tiene como propias: ${own.join(", ")}. Para reemplazarlas, corre agent-kitchen apply en una terminal.\n`);
74
83
  }
75
84
  /**
76
85
  * Asks, for a step that installs skills, which ones, and whether to replace the repo's own.
@@ -120,7 +129,7 @@ async function chooseSteps(root, steps, options, out) {
120
129
  return suggested;
121
130
  }
122
131
  if (options.dryRun || !out.interactive) {
123
- const hint = options.dryRun ? "" : " (para hacerlos, corre agent-kitchen en una terminal, o con --yes)";
132
+ const hint = options.dryRun ? "" : " (para hacerlos, corre agent-kitchen apply en una terminal, o con --yes)";
124
133
  out.stdout(`Pasos sugeridos${hint}:\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${step.label}\n`).join("")}`);
125
134
  return null;
126
135
  }
@@ -205,7 +214,7 @@ export async function sync(options, out) {
205
214
  throw new UsageError(`no existe la pieza ${unknown}; son ${pieceNames().join(", ")}.`);
206
215
  if (options.yes && options.dryRun)
207
216
  throw new UsageError("usa --yes o --dry-run, no los dos.");
208
- const root = mainCheckout(options.cwd, "agent-kitchen");
217
+ const root = mainCheckout(options.cwd, "agent-kitchen apply");
209
218
  setVerbose(options.verbose);
210
219
  if (out.interactive)
211
220
  out.stdout("Revisando el repo y las últimas versiones…\n");
package/dist/piece.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * @file What agent-kitchen knows of each piece it manages, and the names they share. A piece
3
- * (the issue flow, claude-cloud, claude-repo-guard, the skills) tells what the repo has and
4
- * the steps that would install or update it; agent-kitchen shows them, lets the person pick,
5
- * and runs the picked ones in order. Every step runs the piece's own tool, which owns its
6
- * files: agent-kitchen only writes the few lines that join two pieces.
3
+ * (the version of skills-manager, the issue flow, claude-cloud, claude-repo-guard, the
4
+ * skills, the rule in AGENTS.md) tells what the repo has and the steps that would install or
5
+ * update it; agent-kitchen shows them, lets the person pick, and runs the picked ones in
6
+ * order. Every step runs the piece's own tool, which owns its files: agent-kitchen only
7
+ * writes the few lines that join two pieces, the one line of AGENTS.md, which has no tool,
8
+ * and the version of skills-manager in package.json, which agent-kitchen is the one to run.
7
9
  */
8
10
  /** The issue flow on npm. */
9
11
  export declare const FLOW_PACKAGE = "@ramon-sg/issue-flow";
package/dist/piece.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * @file What agent-kitchen knows of each piece it manages, and the names they share. A piece
3
- * (the issue flow, claude-cloud, claude-repo-guard, the skills) tells what the repo has and
4
- * the steps that would install or update it; agent-kitchen shows them, lets the person pick,
5
- * and runs the picked ones in order. Every step runs the piece's own tool, which owns its
6
- * files: agent-kitchen only writes the few lines that join two pieces.
3
+ * (the version of skills-manager, the issue flow, claude-cloud, claude-repo-guard, the
4
+ * skills, the rule in AGENTS.md) tells what the repo has and the steps that would install or
5
+ * update it; agent-kitchen shows them, lets the person pick, and runs the picked ones in
6
+ * order. Every step runs the piece's own tool, which owns its files: agent-kitchen only
7
+ * writes the few lines that join two pieces, the one line of AGENTS.md, which has no tool,
8
+ * and the version of skills-manager in package.json, which agent-kitchen is the one to run.
7
9
  */
8
10
  /** The issue flow on npm. */
9
11
  export const FLOW_PACKAGE = "@ramon-sg/issue-flow";
@@ -0,0 +1,20 @@
1
+ import type { Check, PieceStatus } from "../piece.js";
2
+ /** The piece's name, as `--skip` takes it. */
3
+ export declare const NAME = "agents-md";
4
+ /** The line, as AGENTS.md has it. */
5
+ export declare const VERSION_RULE = "Antes de usar una biblioteca, mira su versi\u00F3n instalada en `node_modules/<biblioteca>/package.json` y lee sus tipos o su documentaci\u00F3n ah\u00ED. Si no alcanza, busca la documentaci\u00F3n de esa versi\u00F3n, no la \u00FAltima.";
6
+ /**
7
+ * What the repo has of the rule, and the step that adds it.
8
+ * @param root - The repo.
9
+ * @param flowComing - Whether this run installs the flow, which writes package.json.
10
+ * @returns The piece's status, or null if the repo has no package.json and gets none.
11
+ * @example (await agentsMdStatus(root, false))?.state // "falta la regla"
12
+ */
13
+ export declare function agentsMdStatus(root: string, flowComing: boolean): Promise<PieceStatus | null>;
14
+ /**
15
+ * Whether AGENTS.md has the rule and CLAUDE.md, if there is one, imports it. Only noted.
16
+ * @param root - The repo.
17
+ * @returns The checks; none without package.json.
18
+ * @example (await agentsMdChecks(root))[0].status // "ok"
19
+ */
20
+ export declare function agentsMdChecks(root: string): Promise<Check[]>;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @file The rule in AGENTS.md that makes the agent check the installed version of a library
3
+ * before using it, instead of the one it remembers. It is one fixed line, so AGENTS.md stays
4
+ * small: it loads in every session. This piece has no tool of its own: agent-kitchen writes
5
+ * the line, as it writes the guard's options. Claude Code does not read AGENTS.md when the
6
+ * repo has a CLAUDE.md, so that file gets `@AGENTS.md` too. Only for repos with package.json.
7
+ */
8
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ /** The piece's name, as `--skip` takes it. */
11
+ export const NAME = "agents-md";
12
+ /** The line, as AGENTS.md has it. */
13
+ export const VERSION_RULE = "Antes de usar una biblioteca, mira su versión instalada en `node_modules/<biblioteca>/package.json` y lee sus tipos o su documentación ahí. Si no alcanza, busca la documentación de esa versión, no la última.";
14
+ /** The line that makes Claude Code read AGENTS.md from CLAUDE.md. */
15
+ const IMPORT = "@AGENTS.md";
16
+ /**
17
+ * A file's text, or null if it does not exist.
18
+ * @param root - The repo.
19
+ * @param name - The file.
20
+ * @returns Its text, or null.
21
+ */
22
+ function readText(root, name) {
23
+ const path = join(root, name);
24
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
25
+ }
26
+ /**
27
+ * Whether a text has a line, exactly.
28
+ * @param text - The text, or null.
29
+ * @param line - The line.
30
+ * @returns Whether it has it.
31
+ */
32
+ function hasLine(text, line) {
33
+ return text !== null && text.split("\n").some((each) => each.trim() === line);
34
+ }
35
+ /**
36
+ * Whether CLAUDE.md and AGENTS.md are one file through a link (`ln -s AGENTS.md CLAUDE.md`):
37
+ * then Claude Code already reads the rule, and an import would make AGENTS.md import itself.
38
+ * @param root - The repo.
39
+ * @returns Whether they are the same file.
40
+ */
41
+ function sameFile(root) {
42
+ try {
43
+ return realpathSync(join(root, "CLAUDE.md")) === realpathSync(join(root, "AGENTS.md"));
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * What the repo lacks: the rule in AGENTS.md, and the import in a CLAUDE.md that exists and
51
+ * is not AGENTS.md itself.
52
+ * @param root - The repo.
53
+ * @returns Whether each one is missing.
54
+ */
55
+ function missing(root) {
56
+ const claude = readText(root, "CLAUDE.md");
57
+ return { rule: !hasLine(readText(root, "AGENTS.md"), VERSION_RULE), import: claude !== null && !hasLine(claude, IMPORT) && !sameFile(root) };
58
+ }
59
+ /**
60
+ * Adds a line at the end of a file, after a blank line, with the file's own line endings, or
61
+ * creates the file with it. Nothing the file had is lost but its trailing blank lines.
62
+ * @param root - The repo.
63
+ * @param name - The file.
64
+ * @param line - The line.
65
+ */
66
+ function append(root, name, line) {
67
+ const text = readText(root, name) ?? "";
68
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
69
+ const kept = text.replace(/(?:\r?\n)+$/, "");
70
+ writeFileSync(join(root, name), `${kept.trim() === "" ? "" : `${kept}${eol}${eol}`}${line}${eol}`);
71
+ }
72
+ /**
73
+ * Writes what is missing, if the repo has package.json by now.
74
+ * @param context - Where it runs.
75
+ * @returns Always true: a repo without package.json gets nothing, and that is no failure.
76
+ */
77
+ function write(context) {
78
+ if (!existsSync(join(context.root, "package.json"))) {
79
+ context.note("sin package.json: no se agregó nada");
80
+ return true;
81
+ }
82
+ const lack = missing(context.root);
83
+ if (lack.rule)
84
+ append(context.root, "AGENTS.md", VERSION_RULE);
85
+ if (lack.import)
86
+ append(context.root, "CLAUDE.md", IMPORT);
87
+ return true;
88
+ }
89
+ /**
90
+ * What the repo has of the rule, and the step that adds it.
91
+ * @param root - The repo.
92
+ * @param flowComing - Whether this run installs the flow, which writes package.json.
93
+ * @returns The piece's status, or null if the repo has no package.json and gets none.
94
+ * @example (await agentsMdStatus(root, false))?.state // "falta la regla"
95
+ */
96
+ export async function agentsMdStatus(root, flowComing) {
97
+ if (!existsSync(join(root, "package.json")) && !flowComing)
98
+ return null;
99
+ const lack = missing(root);
100
+ const summary = "regla de versiones instaladas en AGENTS.md";
101
+ if (!lack.rule && !lack.import)
102
+ return { name: NAME, summary, installed: true, state: "al día", steps: [] };
103
+ const files = [...(lack.rule ? ["AGENTS.md"] : []), ...(lack.import ? ["CLAUDE.md"] : [])];
104
+ const label = lack.rule ? `Agregar a AGENTS.md la regla de versiones instaladas${lack.import ? " (y @AGENTS.md en CLAUDE.md)" : ""}` : "Agregar @AGENTS.md a CLAUDE.md, para que Claude Code lea AGENTS.md";
105
+ const state = lack.rule ? "falta la regla" : "CLAUDE.md no importa AGENTS.md";
106
+ return { name: NAME, summary, installed: !lack.rule, state, steps: [{ piece: NAME, checked: true, downloads: [], label: `${label}: ${files.join(", ")}`, run: async (context) => write(context) }] };
107
+ }
108
+ /**
109
+ * Whether AGENTS.md has the rule and CLAUDE.md, if there is one, imports it. Only noted.
110
+ * @param root - The repo.
111
+ * @returns The checks; none without package.json.
112
+ * @example (await agentsMdChecks(root))[0].status // "ok"
113
+ */
114
+ export async function agentsMdChecks(root) {
115
+ if (!existsSync(join(root, "package.json")))
116
+ return [];
117
+ const lack = missing(root);
118
+ const checks = [];
119
+ if (lack.rule)
120
+ checks.push({ name: NAME, status: "info", detail: "falta la regla de versiones en AGENTS.md; corre npx agent-kitchen apply" });
121
+ if (lack.import)
122
+ checks.push({ name: NAME, status: "info", detail: "CLAUDE.md no importa AGENTS.md, así que Claude Code no lo lee; corre npx agent-kitchen apply" });
123
+ return checks.length > 0 ? checks : [{ name: NAME, status: "ok", detail: "regla de versiones en AGENTS.md" }];
124
+ }
@@ -83,6 +83,6 @@ export async function cloudChecks(root) {
83
83
  if (version === null)
84
84
  return [{ name: NAME, status: "fail", detail: `${CONFIG} no fija una versión exacta: pon en "version" la que se usa, como "${latest ?? "1.0.0"}"` }];
85
85
  if (latest !== null && compareVersions(latest, version) > 0)
86
- return [{ name: NAME, status: "info", detail: `${version}, hay ${latest}: corre npx agent-kitchen` }];
86
+ return [{ name: NAME, status: "info", detail: `${version}, hay ${latest}: corre npx agent-kitchen apply` }];
87
87
  return [{ name: NAME, status: "ok", detail: version }];
88
88
  }
@@ -175,7 +175,7 @@ export async function guardChecks(root) {
175
175
  return [{ name: NAME, status: "info", detail: "no instalado (Claude se abre con tus propios permisos)" }];
176
176
  const checks = [];
177
177
  if (process.platform === "darwin" && readJson(root, CONFIG)?.[WEAKER_NETWORK] !== true)
178
- checks.push({ name: `${NAME}: ${WEAKER_NETWORK}`, status: "fail", detail: `no está activado en ${CONFIG}, así que gh falla en el sandbox de macOS: corre npx agent-kitchen` });
178
+ checks.push({ name: `${NAME}: ${WEAKER_NETWORK}`, status: "fail", detail: `no está activado en ${CONFIG}, así que gh falla en el sandbox de macOS: corre npx agent-kitchen apply` });
179
179
  const result = await captureNpx(root, [guardSpec(installed), "doctor", "--json"]);
180
180
  let found;
181
181
  try {
@@ -3,6 +3,8 @@
3
3
  * The repo's version is the one in package.json. Installing runs its `init` at the latest
4
4
  * version; updating runs its `upgrade` at the new one and then moves its skills to the same
5
5
  * tag, because the skills call the CLI. If only the skills are off, it moves them alone.
6
+ * From 0.2.6 it also offers `labels sync`, after the install or update, when GitHub lacks
7
+ * some of the labels the flow expects.
6
8
  * `init` runs with `--yes`: the person answered agent-kitchen's questions before the steps.
7
9
  */
8
10
  import { type Check, type PieceStatus } from "../piece.js";
@@ -3,10 +3,12 @@
3
3
  * The repo's version is the one in package.json. Installing runs its `init` at the latest
4
4
  * version; updating runs its `upgrade` at the new one and then moves its skills to the same
5
5
  * tag, because the skills call the CLI. If only the skills are off, it moves them alone.
6
+ * From 0.2.6 it also offers `labels sync`, after the install or update, when GitHub lacks
7
+ * some of the labels the flow expects.
6
8
  * `init` runs with `--yes`: the person answered agent-kitchen's questions before the steps.
7
9
  */
8
10
  import { FLOW_CATALOG, FLOW_PACKAGE } from "../piece.js";
9
- import { dependencyVersion, readJson } from "../repo.js";
11
+ import { dependencyVersion, isObject, readJson } from "../repo.js";
10
12
  import { catalogSkills, manifestCatalogs, pinCatalog, updateSkills } from "../skills-files.js";
11
13
  import { captureNpx, compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
12
14
  /** The piece's name, as `--skip` takes it. */
@@ -15,6 +17,8 @@ export const NAME = "issue-flow";
15
17
  const SKILLS_CATALOG_LABEL = "el catálogo";
16
18
  /** The first version whose `doctor` prints `--json`. */
17
19
  const JSON_DOCTOR = "0.2.0";
20
+ /** The first version with `labels sync`. */
21
+ const LABELS_SINCE = "0.2.6";
18
22
  /**
19
23
  * The issue flow's version in the repo, as package.json has it: an exact release when the
20
24
  * flow set it up, or anything else (a range, a `file:` link) someone wrote by hand.
@@ -63,7 +67,7 @@ async function moveSkills(context, version) {
63
67
  context.note(`skills del flujo en v${version}: ${names.join(", ")}`);
64
68
  return true;
65
69
  }
66
- context.fail(`skills-manager no actualizó las skills del flujo; corre '${failed.command}' y luego agent-kitchen otra vez.`, failed.run.output);
70
+ context.fail(`skills-manager no actualizó las skills del flujo; corre '${failed.command}' y luego agent-kitchen apply otra vez.`, failed.run.output);
67
71
  return false;
68
72
  }
69
73
  /**
@@ -80,6 +84,46 @@ async function runFlow(context, version, args) {
80
84
  context.fail(`issue-flow ${args[0] ?? ""} falló; corre 'npx ${FLOW_PACKAGE}@${version} ${args.join(" ")}'.`, run.output);
81
85
  return false;
82
86
  }
87
+ /**
88
+ * The labels the flow expects that GitHub lacks, as `labels sync --dry-run --json` of a
89
+ * version tells (a label that only differs in color or description does not count).
90
+ * @param root - The repo.
91
+ * @param version - The flow's version.
92
+ * @returns Their names, none for a version without `labels sync`, or null if it could not tell.
93
+ * @example await missingLabels(root, "0.2.6") // ["spike"]
94
+ */
95
+ async function missingLabels(root, version) {
96
+ if (compareVersions(version, LABELS_SINCE) < 0)
97
+ return [];
98
+ const result = await captureNpx(root, [`${FLOW_PACKAGE}@${version}`, "labels", "sync", "--dry-run", "--json"], true);
99
+ let found;
100
+ try {
101
+ found = JSON.parse(result?.out ?? "");
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ if (!isObject(found) || !Array.isArray(found.labels))
107
+ return null;
108
+ return found.labels.filter((label) => isObject(label) && label.status === "to-create").map((label) => String(label.name));
109
+ }
110
+ /**
111
+ * The step that makes in GitHub the labels the flow lacks, with its own command. It only
112
+ * runs if the repo has that version of the flow by then: not when its install or update
113
+ * was left out or failed.
114
+ * @param version - The flow's version.
115
+ * @param missing - The labels.
116
+ * @returns The step.
117
+ */
118
+ function labelsStep(version, missing) {
119
+ return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${version}`], label: `Crear en GitHub los labels que faltan: ${missing.join(", ")}`, run: async (context) => {
120
+ if (flowVersion(context.root) !== version) {
121
+ context.note(`no se crearon: el repo no tiene issue-flow ${version}`);
122
+ return true;
123
+ }
124
+ return runFlow(context, version, ["labels", "sync"]);
125
+ } };
126
+ }
83
127
  /**
84
128
  * The step the repo needs, if any.
85
129
  * @param installed - The repo's version, or null.
@@ -110,9 +154,13 @@ export async function issueFlowStatus(root) {
110
154
  const installed = flowVersion(root);
111
155
  const latest = await npmLatest(FLOW_PACKAGE);
112
156
  const step = stepFor(installed, latest, root);
113
- const known = latest === null ? " (npm no responde)" : "";
114
- const state = installed === null ? `no instalado${known}` : latest !== null && compareVersions(latest, installed) > 0 ? `${installed}, hay ${latest}` : `${installed}, al día${known}`;
115
- return { name: NAME, summary, installed: installed !== null, state, steps: step === null ? [] : [step] };
157
+ const behind = installed !== null && latest !== null && compareVersions(latest, installed) > 0;
158
+ const target = installed === null || behind ? latest : installed;
159
+ const missing = target === null ? [] : await missingLabels(root, target);
160
+ const known = `${latest === null ? " (npm no responde)" : ""}${missing === null ? " (no se pudieron leer los labels de GitHub)" : ""}`;
161
+ const state = installed === null ? `no instalado${known}` : behind ? `${installed}, hay ${latest}${known}` : `${installed}, al día${known}`;
162
+ const steps = [...(step === null ? [] : [step]), ...(target !== null && missing !== null && missing.length > 0 ? [labelsStep(target, missing)] : [])];
163
+ return { name: NAME, summary, installed: installed !== null, state, steps };
116
164
  }
117
165
  /**
118
166
  * The flow's own doctor at the repo's version, and whether its skills are at that tag.
@@ -128,7 +176,8 @@ export async function issueFlowChecks(root) {
128
176
  if (installed === null)
129
177
  return [{ name: NAME, status: "info", detail: `package.json tiene ${spec}, no una versión exacta` }];
130
178
  if (compareVersions(installed, JSON_DOCTOR) < 0)
131
- return [{ name: NAME, status: "info", detail: `${installed} no tiene doctor --json: corre npx agent-kitchen para actualizarlo` }];
179
+ return [{ name: NAME, status: "info", detail: `${installed} no tiene doctor --json: corre npx agent-kitchen apply para actualizarlo` }];
180
+ const labelsRead = missingLabels(root, installed);
132
181
  const result = await captureNpx(root, [`${FLOW_PACKAGE}@${installed}`, "doctor", "--json"]);
133
182
  let found;
134
183
  try {
@@ -146,6 +195,14 @@ export async function issueFlowChecks(root) {
146
195
  ? { name: `${NAME}: skills`, status: "info", detail: "ninguna instalada" }
147
196
  : off.length === 0
148
197
  ? { name: `${NAME}: skills`, status: "ok", detail: `${count} en v${installed}` }
149
- : { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}; issue-flow está en ${installed}: corre npx agent-kitchen` };
150
- return [...own, skills];
198
+ : { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}; issue-flow está en ${installed}: corre npx agent-kitchen apply` };
199
+ const missing = await labelsRead;
200
+ const labels = compareVersions(installed, LABELS_SINCE) < 0
201
+ ? []
202
+ : missing === null
203
+ ? [{ name: `${NAME}: labels`, status: "info", detail: "no se pudieron leer de GitHub" }]
204
+ : missing.length > 0
205
+ ? [{ name: `${NAME}: labels`, status: "info", detail: `faltan ${missing.join(", ")} en GitHub; corre npx agent-kitchen apply` }]
206
+ : [{ name: `${NAME}: labels`, status: "ok", detail: "GitHub tiene los que espera el flujo" }];
207
+ return [...own, skills, ...labels];
151
208
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file The version of `skills-manager` in the repo's package.json. agent-kitchen runs it to
3
+ * install and move the skills, so agent-kitchen is the one that knows the oldest version it
4
+ * works with: it adds it when missing and raises an exact version below it, never lowers
5
+ * one. A range or `workspace:` is the repo's own choice: it is left alone, with a warning if
6
+ * the version it runs is too old. Only in a repo with package.json that uses skills.
7
+ */
8
+ import { type Check, type PieceStatus } from "../piece.js";
9
+ /** The piece's name, as `--skip` takes it. */
10
+ export declare const NAME = "skills-manager";
11
+ /** The oldest `skills-manager` agent-kitchen works with: the first with `--replace`. */
12
+ export declare const MIN_VERSION = "1.2.0";
13
+ /**
14
+ * What the repo has of skills-manager, and the step that raises it, if any. The step reads
15
+ * package.json again when it runs.
16
+ * @param root - The repo.
17
+ * @param withSkills - Whether this run may install skills.
18
+ * @returns The piece's status, or null where the piece does not apply.
19
+ * @example skillsManagerStatus(root, true)?.state // "1.1.0, bajo 1.2.0"
20
+ */
21
+ export declare function skillsManagerStatus(root: string, withSkills: boolean): PieceStatus | null;
22
+ /**
23
+ * Whether the version of skills-manager the repo runs is the one agent-kitchen needs.
24
+ * @param root - The repo.
25
+ * @returns The checks; none where the piece does not apply.
26
+ * @example skillsManagerChecks(root)[0]?.status // "ok"
27
+ */
28
+ export declare function skillsManagerChecks(root: string): Check[];
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @file The version of `skills-manager` in the repo's package.json. agent-kitchen runs it to
3
+ * install and move the skills, so agent-kitchen is the one that knows the oldest version it
4
+ * works with: it adds it when missing and raises an exact version below it, never lowers
5
+ * one. A range or `workspace:` is the repo's own choice: it is left alone, with a warning if
6
+ * the version it runs is too old. Only in a repo with package.json that uses skills.
7
+ */
8
+ import { SKILLS_MANAGER, SKILLS_MANIFEST } from "../piece.js";
9
+ import { dependencyVersion, installedVersion, readJson, setDependency } from "../repo.js";
10
+ import { compareVersions, isRelease } from "../tools.js";
11
+ /** The piece's name, as `--skip` takes it. */
12
+ export const NAME = "skills-manager";
13
+ /** The oldest `skills-manager` agent-kitchen works with: the first with `--replace`. */
14
+ export const MIN_VERSION = "1.2.0";
15
+ /**
16
+ * What package.json has of skills-manager and, for a range, the version that runs: the one
17
+ * in node_modules, or the first x.y.z of the range.
18
+ * @param root - The repo.
19
+ * @returns What it has.
20
+ */
21
+ function pinned(root) {
22
+ const spec = dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER);
23
+ if (spec === null)
24
+ return { kind: "missing" };
25
+ if (isRelease(spec))
26
+ return { kind: "exact", version: spec };
27
+ const floor = spec.startsWith("workspace:") ? null : (/\d+\.\d+\.\d+/.exec(spec)?.[0] ?? null);
28
+ return { kind: "other", spec, runs: installedVersion(root, SKILLS_MANAGER) ?? floor };
29
+ }
30
+ /**
31
+ * Whether a version is older than the one agent-kitchen needs.
32
+ * @param version - The version.
33
+ * @returns True if it is below the minimum.
34
+ */
35
+ function below(version) {
36
+ return compareVersions(version, MIN_VERSION) < 0;
37
+ }
38
+ /**
39
+ * Whether the piece applies: the repo has package.json and uses skills, or this run may
40
+ * install them.
41
+ * @param root - The repo.
42
+ * @param withSkills - Whether this run may install skills (`skills` is not left out).
43
+ * @returns True if it applies.
44
+ */
45
+ function applies(root, withSkills) {
46
+ return readJson(root, "package.json") !== null && (withSkills || readJson(root, SKILLS_MANIFEST) !== null);
47
+ }
48
+ /**
49
+ * What the repo has of skills-manager, and the step that raises it, if any. The step reads
50
+ * package.json again when it runs.
51
+ * @param root - The repo.
52
+ * @param withSkills - Whether this run may install skills.
53
+ * @returns The piece's status, or null where the piece does not apply.
54
+ * @example skillsManagerStatus(root, true)?.state // "1.1.0, bajo 1.2.0"
55
+ */
56
+ export function skillsManagerStatus(root, withSkills) {
57
+ if (!applies(root, withSkills))
58
+ return null;
59
+ const summary = "el CLI de las skills, que corre agent-kitchen";
60
+ const now = pinned(root);
61
+ if (now.kind === "other") {
62
+ const old = now.runs !== null && below(now.runs) ? `; corre ${now.runs}, bajo ${MIN_VERSION}` : "";
63
+ return { name: NAME, summary, installed: true, state: `package.json tiene ${now.spec}, no una versión exacta; no se toca${old}`, steps: [] };
64
+ }
65
+ if (now.kind === "exact" && !below(now.version))
66
+ return { name: NAME, summary, installed: true, state: `${now.version}, al día`, steps: [] };
67
+ const label = now.kind === "missing" ? `Agregar skills-manager ${MIN_VERSION} a package.json` : `Subir skills-manager ${now.version} → ${MIN_VERSION}, el mínimo de agent-kitchen`;
68
+ const state = now.kind === "missing" ? "no está en package.json" : `${now.version}, bajo ${MIN_VERSION}`;
69
+ const step = {
70
+ piece: NAME,
71
+ checked: true,
72
+ downloads: [`${SKILLS_MANAGER}@${MIN_VERSION}`],
73
+ label,
74
+ run: async (context) => {
75
+ const current = pinned(context.root);
76
+ if (current.kind === "missing" || (current.kind === "exact" && below(current.version))) {
77
+ setDependency(context.root, SKILLS_MANAGER, MIN_VERSION);
78
+ context.note(`${SKILLS_MANAGER} ${MIN_VERSION} en package.json`);
79
+ }
80
+ else
81
+ context.note("package.json ya tenía una versión que sirve");
82
+ return true;
83
+ },
84
+ };
85
+ return { name: NAME, summary, installed: now.kind !== "missing", state, steps: [step] };
86
+ }
87
+ /**
88
+ * Whether the version of skills-manager the repo runs is the one agent-kitchen needs.
89
+ * @param root - The repo.
90
+ * @returns The checks; none where the piece does not apply.
91
+ * @example skillsManagerChecks(root)[0]?.status // "ok"
92
+ */
93
+ export function skillsManagerChecks(root) {
94
+ if (!applies(root, false))
95
+ return [];
96
+ const now = pinned(root);
97
+ if (now.kind === "missing")
98
+ return [{ name: NAME, status: "info", detail: `no está en package.json; corre npx agent-kitchen apply para fijar ${MIN_VERSION}` }];
99
+ if (now.kind === "exact")
100
+ return below(now.version)
101
+ ? [{ name: NAME, status: "fail", detail: `${now.version}, bajo ${MIN_VERSION}, el mínimo de agent-kitchen: corre npx agent-kitchen apply` }]
102
+ : [{ name: NAME, status: "ok", detail: `${now.version} (mínimo ${MIN_VERSION})` }];
103
+ if (now.runs !== null && below(now.runs))
104
+ return [{ name: NAME, status: "fail", detail: `${now.spec} corre ${now.runs}, bajo ${MIN_VERSION}, el mínimo de agent-kitchen: sube el rango` }];
105
+ return [{ name: NAME, status: "ok", detail: `${now.spec}${now.runs === null ? "" : `, corre ${now.runs}`}` }];
106
+ }
@@ -181,7 +181,7 @@ async function updateStep(root, catalog, ref) {
181
181
  const failed = await updateSkills(context.root, behind.map((skill) => skill.name));
182
182
  if (failed === null)
183
183
  return true;
184
- context.fail(`skills-manager no actualizó ${catalog}; corre '${failed.command}' y luego agent-kitchen otra vez.`, failed.run.output);
184
+ context.fail(`skills-manager no actualizó ${catalog}; corre '${failed.command}' y luego agent-kitchen apply otra vez.`, failed.run.output);
185
185
  return false;
186
186
  },
187
187
  };
package/dist/repo.d.ts CHANGED
@@ -46,3 +46,23 @@ export declare function dependencyVersion(pkg: Record<string, unknown> | null, n
46
46
  * @example packageManager(root) // "pnpm"
47
47
  */
48
48
  export declare function packageManager(root: string): string;
49
+ /**
50
+ * The version of a package installed in the repo's `node_modules`.
51
+ * @param root - The repo.
52
+ * @param name - The package.
53
+ * @returns Its version, or null if it is not installed.
54
+ * @example installedVersion(root, "@ramon-sg/skills-manager") // "1.3.0"
55
+ */
56
+ export declare function installedVersion(root: string, name: string): string | null;
57
+ /**
58
+ * Sets the version of a dependency in package.json, changing nothing else: the file keeps
59
+ * its indentation, line endings and key order. It changes the entry `dependencyVersion`
60
+ * reads (`devDependencies` first, then `dependencies`); a new one goes into
61
+ * `devDependencies`, in alphabetical order, as npm adds it.
62
+ * @param root - The repo.
63
+ * @param name - The package.
64
+ * @param version - The version.
65
+ * @throws KitError if package.json is missing or not a JSON object.
66
+ * @example setDependency(root, "@ramon-sg/skills-manager", "1.2.0")
67
+ */
68
+ export declare function setDependency(root: string, name: string, version: string): void;
package/dist/repo.js CHANGED
@@ -115,3 +115,54 @@ export function packageManager(root) {
115
115
  return field;
116
116
  return LOCKFILES.find(([file]) => existsSync(join(root, file)))?.[1] ?? "npm";
117
117
  }
118
+ /**
119
+ * The version of a package installed in the repo's `node_modules`.
120
+ * @param root - The repo.
121
+ * @param name - The package.
122
+ * @returns Its version, or null if it is not installed.
123
+ * @example installedVersion(root, "@ramon-sg/skills-manager") // "1.3.0"
124
+ */
125
+ export function installedVersion(root, name) {
126
+ const file = join(root, "node_modules", name, "package.json");
127
+ if (!existsSync(file))
128
+ return null;
129
+ try {
130
+ const pkg = JSON.parse(readFileSync(file, "utf8"));
131
+ return isObject(pkg) && typeof pkg.version === "string" ? pkg.version : null;
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ }
137
+ /**
138
+ * Sets the version of a dependency in package.json, changing nothing else: the file keeps
139
+ * its indentation, line endings and key order. It changes the entry `dependencyVersion`
140
+ * reads (`devDependencies` first, then `dependencies`); a new one goes into
141
+ * `devDependencies`, in alphabetical order, as npm adds it.
142
+ * @param root - The repo.
143
+ * @param name - The package.
144
+ * @param version - The version.
145
+ * @throws KitError if package.json is missing or not a JSON object.
146
+ * @example setDependency(root, "@ramon-sg/skills-manager", "1.2.0")
147
+ */
148
+ export function setDependency(root, name, version) {
149
+ const pkg = readJson(root, "package.json");
150
+ if (pkg === null)
151
+ throw new KitError("no hay package.json.");
152
+ const text = readFileSync(join(root, "package.json"), "utf8");
153
+ const indent = /^([ \t]+)"/m.exec(text)?.[1] ?? " ";
154
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
155
+ const [deps, dev] = [pkg.dependencies, pkg.devDependencies];
156
+ if (isObject(dev) && typeof dev[name] === "string")
157
+ dev[name] = version;
158
+ else if (isObject(deps) && typeof deps[name] === "string")
159
+ deps[name] = version;
160
+ else {
161
+ const entries = Object.entries(isObject(dev) ? dev : {});
162
+ const at = entries.findIndex(([key]) => key > name);
163
+ entries.splice(at === -1 ? entries.length : at, 0, [name, version]);
164
+ pkg.devDependencies = Object.fromEntries(entries);
165
+ }
166
+ const json = JSON.stringify(pkg, null, indent).replace(/\n/g, eol);
167
+ writeFileSync(join(root, "package.json"), /\r?\n$/.test(text) || text === "" ? `${json}${eol}` : json);
168
+ }
@@ -66,10 +66,12 @@ export declare function manifestCatalogs(root: string): {
66
66
  */
67
67
  export declare function pinCatalog(root: string, catalog: string, tag: string): boolean;
68
68
  /**
69
- * The `skills-manager` to run: the version the repo pins in package.json, so the lock is
70
- * written by the same version every time, or the latest 1.x.
69
+ * The `skills-manager` to run, so the lock is written by the same version every time: the
70
+ * exact version package.json pins (the one its install leaves, even before it runs), else
71
+ * the one in node_modules, else the range package.json has (npx cannot take `workspace:`),
72
+ * else the latest 1.x.
71
73
  * @param root - The repo.
72
74
  * @returns The npx spec.
73
- * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.1.0"
75
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.2.0"
74
76
  */
75
77
  export declare function skillsManagerSpec(root: string): string;
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * @file What agent-kitchen reads of the files of `skills-manager`: which skills of a catalog
3
3
  * the lock has and at which tag (the lock is where the manifest's `paths.lock` says), the
4
- * catalog's source in the manifest, which `skills-manager` to run (the one the repo pins,
5
- * or the latest 1.x), the repo's own skills (folders the lock does not know), and updating
6
- * skills one by one, because `skills-manager update` takes a single name.
4
+ * catalog's source in the manifest, which `skills-manager` to run (the one the repo pins or
5
+ * has installed, or the latest 1.x), the repo's own skills (folders the lock does not know),
6
+ * and updating skills one by one, because `skills-manager update` takes a single name.
7
7
  */
8
8
  import { existsSync, readdirSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { dependencyVersion, isObject, readJson, writeJson } from "./repo.js";
10
+ import { dependencyVersion, installedVersion, isObject, readJson, writeJson } from "./repo.js";
11
11
  import { SKILLS_LOCK, SKILLS_MANAGER, SKILLS_MANIFEST } from "./piece.js";
12
- import { runNpx } from "./tools.js";
12
+ import { isRelease, runNpx } from "./tools.js";
13
13
  /** Where `skills-manager` puts the skills when the manifest does not say. */
14
14
  const SKILLS_DIR = ".agents/skills";
15
15
  /**
@@ -105,12 +105,18 @@ export function pinCatalog(root, catalog, tag) {
105
105
  return true;
106
106
  }
107
107
  /**
108
- * The `skills-manager` to run: the version the repo pins in package.json, so the lock is
109
- * written by the same version every time, or the latest 1.x.
108
+ * The `skills-manager` to run, so the lock is written by the same version every time: the
109
+ * exact version package.json pins (the one its install leaves, even before it runs), else
110
+ * the one in node_modules, else the range package.json has (npx cannot take `workspace:`),
111
+ * else the latest 1.x.
110
112
  * @param root - The repo.
111
113
  * @returns The npx spec.
112
- * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.1.0"
114
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.2.0"
113
115
  */
114
116
  export function skillsManagerSpec(root) {
115
- return `${SKILLS_MANAGER}@${dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER) ?? "1"}`;
117
+ const pinned = dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER);
118
+ if (pinned !== null && isRelease(pinned))
119
+ return `${SKILLS_MANAGER}@${pinned}`;
120
+ const range = pinned === null || pinned.startsWith("workspace:") ? null : pinned;
121
+ return `${SKILLS_MANAGER}@${installedVersion(root, SKILLS_MANAGER) ?? range ?? "1"}`;
116
122
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-kitchen",
3
- "version": "0.3.1",
4
- "description": "One command to install, update and check a repo's agent tooling: the issue flow and its skills, the general skills, Claude cloud sessions and claude-repo-guard.",
3
+ "version": "0.5.0",
4
+ "description": "One command to install, update and check a repo's agent tooling: the issue flow and its skills, the general skills, Claude cloud sessions, claude-repo-guard and the installed-versions rule in AGENTS.md.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {