agent-cloud-kit 0.0.0-stage → 1.0.0-rc.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/dist/skill.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @file The `cloud-ready` skill that `init` copies into the repo, and the link Claude Code needs
3
+ * to discover it.
4
+ */
5
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ /** Repo-relative directory of the `cloud-ready` skill. */
8
+ export const SKILL_DIR = ".agents/skills/cloud-ready";
9
+ /** Directory of the skill inside this package. */
10
+ const PACKAGE_SKILL_DIR = "skills/cloud-ready";
11
+ /** Repo-relative folder of the skills that Claude Code discovers. */
12
+ const CLAUDE_SKILLS_DIR = ".claude/skills";
13
+ /** Repo-relative folder of the skills that skills-manager installs. */
14
+ const AGENTS_SKILLS_DIR = ".agents/skills";
15
+ /** Repo-relative path of the link to the skill that Claude Code discovers. */
16
+ const SKILL_LINK = `${CLAUDE_SKILLS_DIR}/cloud-ready`;
17
+ /** Target of `SKILL_LINK`, relative to the folder that holds it. */
18
+ const SKILL_LINK_TARGET = `../../${SKILL_DIR}`;
19
+ /** Matches the frontmatter at the start of a skill file; group 1 is its body. */
20
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
21
+ /** Matches the `metadata:` block of the frontmatter, up to the next top-level key or the end. */
22
+ const METADATA_BLOCK = /^metadata:[ \t]*\n((?:[ \t]+.*(?:\n|$))*)/m;
23
+ /** Matches an indented `version:` line inside the metadata block; the value may be quoted. */
24
+ const VERSION_LINE = /^\s+version:\s*"?([^"\n]+)"?$/m;
25
+ /**
26
+ * Reads `metadata.version` from the frontmatter of the installed skill.
27
+ *
28
+ * @param repo - Repo root.
29
+ * @returns The version, or undefined when the skill or its version is missing.
30
+ * @example
31
+ * skillVersion("/home/user/app"); // "1.0.0"
32
+ */
33
+ export function skillVersion(repo) {
34
+ const file = join(repo, SKILL_DIR, "SKILL.md");
35
+ if (!existsSync(file))
36
+ return undefined;
37
+ const frontmatter = FRONTMATTER.exec(readFileSync(file, "utf8"))?.[1] ?? "";
38
+ const block = METADATA_BLOCK.exec(frontmatter)?.[1] ?? "";
39
+ return VERSION_LINE.exec(block)?.[1]?.trim();
40
+ }
41
+ /**
42
+ * Sets `metadata.version` in the frontmatter of a skill file, adding the block when it is missing.
43
+ *
44
+ * @param text - Content of SKILL.md.
45
+ * @param version - Version to write.
46
+ * @returns The new content; unchanged when the file has no frontmatter.
47
+ * @example
48
+ * withVersion('---\nname: x\nmetadata:\n version: "0.0.0"\n---\n', "1.0.0"); // version "1.0.0"
49
+ */
50
+ function withVersion(text, version) {
51
+ const match = FRONTMATTER.exec(text);
52
+ if (!match)
53
+ return text;
54
+ const frontmatter = match[1] ?? "";
55
+ const line = ` version: "${version}"`;
56
+ const block = METADATA_BLOCK.exec(frontmatter);
57
+ let updated;
58
+ if (!block) {
59
+ updated = `${frontmatter}\nmetadata:\n${line}`;
60
+ }
61
+ else {
62
+ const body = block[1] ?? "";
63
+ const newBody = VERSION_LINE.test(body) ? body.replace(VERSION_LINE, line) : `${line}\n${body}`;
64
+ const head = block[0].slice(0, block[0].length - body.length);
65
+ updated = frontmatter.slice(0, block.index) + head + newBody + frontmatter.slice(block.index + block[0].length);
66
+ }
67
+ return `---\n${updated}\n---${text.slice(match[0].length)}`;
68
+ }
69
+ /**
70
+ * Copies the `cloud-ready` skill of this package into the repo, with `metadata.version` set to the
71
+ * package version. Leaves it untouched when it is already at that version; otherwise replaces the
72
+ * whole folder atomically (copy to a temp folder, then rename).
73
+ *
74
+ * @param repo - Repo root.
75
+ * @param packageRoot - Root of this package, which holds `skills/cloud-ready`.
76
+ * @param packageVersion - Version of this package.
77
+ * @returns A line describing the outcome.
78
+ * @throws When the package has no skill or the copy fails (the installed skill is kept).
79
+ * @example
80
+ * installSkill("/home/user/app", "/usr/lib/node_modules/agent-cloud-kit", "1.0.0");
81
+ * // "installed .agents/skills/cloud-ready 1.0.0"
82
+ */
83
+ export function installSkill(repo, packageRoot, packageVersion) {
84
+ const current = skillVersion(repo);
85
+ if (current === packageVersion)
86
+ return `${SKILL_DIR} already at ${packageVersion}`;
87
+ const source = join(packageRoot, PACKAGE_SKILL_DIR);
88
+ if (!existsSync(join(source, "SKILL.md")))
89
+ throw new Error(`${PACKAGE_SKILL_DIR}/SKILL.md not found in ${packageRoot}`);
90
+ const target = join(repo, SKILL_DIR);
91
+ const temp = `${target}.${process.pid}.tmp`;
92
+ const old = `${target}.${process.pid}.old`;
93
+ mkdirSync(dirname(target), { recursive: true });
94
+ try {
95
+ rmSync(temp, { recursive: true, force: true });
96
+ cpSync(source, temp, { recursive: true });
97
+ const skillFile = join(temp, "SKILL.md");
98
+ writeFileSync(skillFile, withVersion(readFileSync(skillFile, "utf8"), packageVersion));
99
+ if (existsSync(target))
100
+ renameSync(target, old);
101
+ renameSync(temp, target);
102
+ }
103
+ catch (err) {
104
+ rmSync(temp, { recursive: true, force: true });
105
+ if (existsSync(old) && !existsSync(target))
106
+ renameSync(old, target);
107
+ throw err;
108
+ }
109
+ rmSync(old, { recursive: true, force: true });
110
+ return current === undefined ? `installed ${SKILL_DIR} ${packageVersion}` : `updated ${SKILL_DIR} from ${current} to ${packageVersion}`;
111
+ }
112
+ /**
113
+ * Tells whether a path exists, counting a broken symlink as existing.
114
+ *
115
+ * @param path - Absolute path.
116
+ * @returns True when something is there.
117
+ */
118
+ function present(path) {
119
+ try {
120
+ lstatSync(path);
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ /**
128
+ * Links the skill where Claude Code discovers it: Claude Code does not read `.agents/skills/`, so
129
+ * this creates the relative symlink `.claude/skills/cloud-ready -> ../../.agents/skills/cloud-ready`.
130
+ * Does nothing when `.claude/skills` already resolves to `.agents/skills` or the entry exists.
131
+ *
132
+ * @param repo - Repo root.
133
+ * @returns A line describing what was done, or undefined when nothing was needed.
134
+ * @example
135
+ * linkSkill("/home/user/app"); // "linked .claude/skills/cloud-ready to .agents/skills/cloud-ready"
136
+ */
137
+ export function linkSkill(repo) {
138
+ const claudeSkills = join(repo, CLAUDE_SKILLS_DIR);
139
+ const agentsSkills = join(repo, AGENTS_SKILLS_DIR);
140
+ if (existsSync(claudeSkills) && existsSync(agentsSkills) && realpathSync(claudeSkills) === realpathSync(agentsSkills))
141
+ return undefined;
142
+ const link = join(repo, SKILL_LINK);
143
+ if (present(link))
144
+ return undefined;
145
+ mkdirSync(claudeSkills, { recursive: true });
146
+ symlinkSync(SKILL_LINK_TARGET, link);
147
+ return `linked ${SKILL_LINK} to ${SKILL_DIR}`;
148
+ }
package/dist/state.js ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @file Machine fingerprint and the per-repo state file that records what setup last did.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
+ import { basename, join } from "node:path";
7
+ import { TOOLS } from "./detect.js";
8
+ /** Directory, inside the tools dir, that holds one state file per repo. */
9
+ const STATE_DIR = "state";
10
+ /** Prefix of every fingerprint. */
11
+ const FINGERPRINT_PREFIX = "sha256-";
12
+ /** Hash algorithm used for fingerprints. */
13
+ const HASH_ALGORITHM = "sha256";
14
+ /** Indentation of the state file. */
15
+ const JSON_INDENT = 2;
16
+ /**
17
+ * Serializes a value as JSON with object keys sorted recursively; arrays keep their order.
18
+ *
19
+ * @param value - Any JSON-compatible value.
20
+ * @returns A canonical JSON string.
21
+ */
22
+ function stableJson(value) {
23
+ if (Array.isArray(value))
24
+ return `[${value.map(stableJson).join(",")}]`;
25
+ if (typeof value === "object" && value !== null) {
26
+ const entries = Object.entries(value)
27
+ .filter(([, v]) => v !== undefined)
28
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
29
+ .map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`);
30
+ return `{${entries.join(",")}}`;
31
+ }
32
+ return JSON.stringify(value) ?? "null";
33
+ }
34
+ /**
35
+ * Hashes everything that decides how the machine is built, so a change asks for a rebuild.
36
+ * `session`, `requiredEnv` and `notes` are left out: changing them does not need a rebuild.
37
+ *
38
+ * @param config - Repo config.
39
+ * @param versions - Pinned tool versions.
40
+ * @param images - Docker images to pull.
41
+ * @param packageVersion - Version of this package.
42
+ * @returns `sha256-<hex>`.
43
+ * @example
44
+ * fingerprint(config, { node: "20.1.0" }, ["postgres:16"], "1.0.0"); // "sha256-ab12..."
45
+ */
46
+ export function fingerprint(config, versions, images, packageVersion) {
47
+ const { version, aptRepos, apt, docker, setup } = config;
48
+ const json = stableJson({ version, aptRepos, apt, docker, setup, versions, images, packageVersion });
49
+ return FINGERPRINT_PREFIX + createHash(HASH_ALGORITHM).update(json).digest("hex");
50
+ }
51
+ /**
52
+ * Locates the state file of a repo.
53
+ *
54
+ * @param toolsDir - Tools directory.
55
+ * @param repo - Repo root path.
56
+ * @returns `<toolsDir>/state/<basename(repo)>.json`.
57
+ * @example
58
+ * statePath("/opt/agent-cloud-kit", "/home/user/app"); // "/opt/agent-cloud-kit/state/app.json"
59
+ */
60
+ export function statePath(toolsDir, repo) {
61
+ return join(toolsDir, STATE_DIR, `${basename(repo)}.json`);
62
+ }
63
+ /**
64
+ * Writes the state file atomically (temp file in the same dir, then rename).
65
+ *
66
+ * @param toolsDir - Tools directory.
67
+ * @param repo - Repo root path.
68
+ * @param state - State to record.
69
+ * @example
70
+ * writeState("/opt/agent-cloud-kit", "/home/user/app", state);
71
+ */
72
+ export function writeState(toolsDir, repo, state) {
73
+ const file = statePath(toolsDir, repo);
74
+ mkdirSync(join(toolsDir, STATE_DIR), { recursive: true });
75
+ const temp = `${file}.${process.pid}.tmp`;
76
+ writeFileSync(temp, `${JSON.stringify(state, null, JSON_INDENT)}\n`);
77
+ renameSync(temp, file);
78
+ }
79
+ /**
80
+ * Tells whether a value is a plain JSON object.
81
+ *
82
+ * @param value - Any parsed JSON value.
83
+ * @returns True for non-null, non-array objects.
84
+ */
85
+ function isRecord(value) {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value);
87
+ }
88
+ /**
89
+ * Tells whether a parsed value has the shape of a `StepResult`.
90
+ *
91
+ * @param value - Any parsed JSON value.
92
+ * @returns True for a string `name`, a boolean `ok`, a finite numeric `ms` and an optional string `error`.
93
+ */
94
+ function isStepResult(value) {
95
+ if (!isRecord(value))
96
+ return false;
97
+ return (typeof value.name === "string" &&
98
+ typeof value.ok === "boolean" &&
99
+ typeof value.ms === "number" &&
100
+ Number.isFinite(value.ms) &&
101
+ (value.error === undefined || typeof value.error === "string"));
102
+ }
103
+ /**
104
+ * Tells whether a parsed value is a map of known tools to version strings.
105
+ *
106
+ * @param value - Any parsed JSON value.
107
+ * @returns True when every key is a tool and every value a string.
108
+ */
109
+ function isVersionMap(value) {
110
+ if (!isRecord(value))
111
+ return false;
112
+ return Object.entries(value).every(([k, v]) => TOOLS.includes(k) && typeof v === "string");
113
+ }
114
+ /**
115
+ * Tells whether a parsed value has the shape of a `MachineState`; a missing `resolved` is accepted.
116
+ *
117
+ * @param value - Any parsed JSON value.
118
+ * @returns True when all required fields are present with the right types.
119
+ */
120
+ function isMachineState(value) {
121
+ if (!isRecord(value))
122
+ return false;
123
+ return (typeof value.fingerprint === "string" &&
124
+ typeof value.packageVersion === "string" &&
125
+ typeof value.finishedAt === "string" &&
126
+ Array.isArray(value.steps) &&
127
+ value.steps.every(isStepResult) &&
128
+ (value.resolved === undefined || isVersionMap(value.resolved)));
129
+ }
130
+ /**
131
+ * Reads a repo's state file; never throws because it runs at session start.
132
+ *
133
+ * @param toolsDir - Tools directory.
134
+ * @param repo - Repo root path.
135
+ * @returns The state, or undefined when the file is missing, unreadable, invalid or incomplete.
136
+ * @example
137
+ * readState("/opt/agent-cloud-kit", "/home/user/app"); // undefined before the first setup
138
+ */
139
+ export function readState(toolsDir, repo) {
140
+ try {
141
+ const parsed = JSON.parse(readFileSync(statePath(toolsDir, repo), "utf8"));
142
+ return isMachineState(parsed) ? { ...parsed, resolved: parsed.resolved ?? {} } : undefined;
143
+ }
144
+ catch {
145
+ return undefined;
146
+ }
147
+ }
package/dist/steps.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @file Step results: runs one unit of work and reports its outcome without ever throwing.
3
+ */
4
+ /**
5
+ * Runs a step, timing it and converting any failure into a result.
6
+ *
7
+ * @param name - Name reported in the result.
8
+ * @param fn - Work to run.
9
+ * @returns The outcome; this promise never rejects.
10
+ * @example
11
+ * await step("pull images", async () => pull()); // { name: "pull images", ok: true, ms: 1200 }
12
+ */
13
+ export async function step(name, fn) {
14
+ const start = Date.now();
15
+ try {
16
+ await fn();
17
+ return { name, ok: true, ms: Date.now() - start };
18
+ }
19
+ catch (err) {
20
+ const error = err instanceof Error ? err.message : String(err);
21
+ return { name, ok: false, ms: Date.now() - start, error };
22
+ }
23
+ }
24
+ /** Prefix of the name given to a lane whose function rejected. */
25
+ const LANE_NAME_PREFIX = "lane ";
26
+ /**
27
+ * Runs lanes of steps concurrently and joins their results in lane order.
28
+ *
29
+ * @param laneFns - Lane functions, each resolving to the results of its own steps.
30
+ * @returns Results ordered by lane, then by position within the lane; never rejects.
31
+ * @example
32
+ * await lanes(async () => [await step("a", fa)], async () => [await step("b", fb)]);
33
+ */
34
+ export async function lanes(...laneFns) {
35
+ const groups = await Promise.all(laneFns.map(async (laneFn, index) => {
36
+ const start = Date.now();
37
+ try {
38
+ return await laneFn();
39
+ }
40
+ catch (err) {
41
+ const error = err instanceof Error ? err.message : String(err);
42
+ return [{ name: `${LANE_NAME_PREFIX}${index}`, ok: false, ms: Date.now() - start, error }];
43
+ }
44
+ }));
45
+ return groups.flat();
46
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "agent-cloud-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-rc.0",
4
+ "description": "Zero-dependency CLI that prepares Claude Code cloud sessions.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ramon-sg/agent-cloud-kit.git"
10
+ },
11
+ "homepage": "https://github.com/ramon-sg/agent-cloud-kit#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/ramon-sg/agent-cloud-kit/issues"
14
+ },
15
+ "keywords": [
16
+ "cloud",
17
+ "sandbox",
18
+ "setup",
19
+ "session-start",
20
+ "cli"
21
+ ],
22
+ "bin": {
23
+ "agent-cloud-kit": "dist/cli.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "schema.json",
28
+ "skills",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ },
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "typecheck": "tsc -p tsconfig.test.json",
42
+ "test": "node --test 'test/**/*.test.ts'"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "20.19.43",
46
+ "typescript": "7.0.2"
47
+ }
48
+ }
package/schema.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "agent-cloud-kit config",
4
+ "description": "Contents of .claude/cloud.json for agent-cloud-kit.",
5
+ "type": "object",
6
+ "required": ["version"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": { "type": "string", "description": "Path or URL of this schema." },
10
+ "version": {
11
+ "type": "string",
12
+ "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$",
13
+ "description": "Exact agent-cloud-kit version to run (no ranges)."
14
+ },
15
+ "aptRepos": {
16
+ "type": "array",
17
+ "items": { "type": "string" },
18
+ "default": [],
19
+ "description": "HTTPS URLs of setup scripts that add APT repositories (piped to bash)."
20
+ },
21
+ "apt": {
22
+ "type": "array",
23
+ "items": { "type": "string" },
24
+ "default": [],
25
+ "description": "APT packages to install."
26
+ },
27
+ "docker": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "default": { "pull": "auto" },
31
+ "properties": {
32
+ "pull": {
33
+ "description": "\"auto\" detects images, false disables pulling, or list images explicitly.",
34
+ "oneOf": [
35
+ { "const": "auto" },
36
+ { "const": false },
37
+ { "type": "array", "items": { "type": "string" } }
38
+ ],
39
+ "default": "auto"
40
+ }
41
+ }
42
+ },
43
+ "setup": {
44
+ "type": "array",
45
+ "items": { "type": "string" },
46
+ "default": [],
47
+ "description": "Shell commands run once when the environment is set up."
48
+ },
49
+ "session": {
50
+ "type": "array",
51
+ "items": { "type": "string" },
52
+ "default": [],
53
+ "description": "Shell commands run at the start of every session."
54
+ },
55
+ "requiredEnv": {
56
+ "type": "array",
57
+ "items": { "type": "string" },
58
+ "default": [],
59
+ "description": "Environment variables that must be set; a warning is shown when missing."
60
+ },
61
+ "domains": {
62
+ "type": "array",
63
+ "items": { "type": "string", "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$" },
64
+ "default": [],
65
+ "description": "Extra hosts the cloud environment must allow; doctor probes them and the manual lists them."
66
+ },
67
+ "notes": {
68
+ "type": "string",
69
+ "description": "What does and does not run in the cloud: free text, or a path, relative to the repo, of a text file inside it whose contents are used instead. The session prints it at start and the manual shows it."
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,179 @@
1
+ ---
2
+ name: cloud-ready
3
+ description: Prepara o verifica un repo Node/TypeScript para las sesiones en la nube de Claude Code (claude.ai/code). Usa `configure` en local, con el usuario, para escribir `.claude/cloud.json` y decidir qué corre en la nube; usa `verify` dentro de una sesión en la nube para comprobar la preparación, arreglarla y reportar en el PR.
4
+ argument-hint: "configure [respuestas] | verify"
5
+ metadata:
6
+ version: "1.0.0"
7
+ ---
8
+
9
+ # cloud-ready
10
+
11
+ Deja el repo listo para la nube de Claude Code y lo comprueba en la nube real. No impone una
12
+ forma de testear: se respeta lo que el repo ya hace. Un repo sin tests solo necesita la máquina
13
+ preparada.
14
+
15
+ - **Modo:** el primer argumento. `configure` corre en local con el usuario; `verify`, en la nube.
16
+ Sin argumento: `verify` si `CLAUDE_CODE_REMOTE=true`, si no `configure`.
17
+ - **Antes de empezar:** lee `reference/knowledge.md` (lo que se sabe de la nube) y, si el repo
18
+ tiene tests, `reference/compat.md` (cómo hacerlos compatibles).
19
+ - **El archivo de `notes`:** el que diga `notes` en `.claude/cloud.json`; si no hay, usa
20
+ `.claude/cloud-notes.md` y pon `"notes": ".claude/cloud-notes.md"` en la config. Se muestra al
21
+ empezar cada sesión en la nube y se copia en `docs/agent-cloud.md`: que sea corto.
22
+ - **La CLI:** `agent-cloud-kit <comando>`. Si no está en el `PATH`, usa `npx -y agent-cloud-kit <comando>`.
23
+
24
+ ## Archivos de la preparación
25
+
26
+ Son los únicos que `verify` puede cambiar:
27
+
28
+ - `.claude/cloud.json`
29
+ - el archivo de `notes`
30
+ - `docs/agent-cloud.md` (lo regenera `agent-cloud-kit manual`; a mano solo la sección "Hallazgos")
31
+ - `.agents/skills/test-audit/project.md` (solo su sección "Nube", y solo si el archivo existe)
32
+
33
+ ## `configure` (local, con el usuario)
34
+
35
+ Si `CLAUDE_CODE_REMOTE=true`, detente: este modo necesita al usuario en una terminal local.
36
+ Las respuestas que traiga el argumento (variables secretas, dominios extra) son el punto de
37
+ partida: no las vuelvas a preguntar.
38
+
39
+ 1. **Lee qué usa el repo:** `package.json` (scripts, `packageManager`, `engines`), el lockfile,
40
+ `.tool-versions`, `mise.toml`, `.nvmrc`, archivos de Docker compose, `Dockerfile`,
41
+ `.env.example`, la CI (`.github/workflows/`) y el README. Lee `.claude/cloud.json` si existe.
42
+ 2. **Escribe la preparación en `.claude/cloud.json`** (no toques `version` ni `$schema`):
43
+ - `setup`: la instalación de dependencias con el gestor del repo y el lockfile congelado
44
+ (`npm ci`, `pnpm install --frozen-lockfile`, `bun install --frozen-lockfile`…), más lo que
45
+ haga falta una sola vez (p. ej. navegadores de Playwright). Node y los gestores fijados en
46
+ `.tool-versions`, `mise.toml`, `.nvmrc` o `packageManager` los instala el paquete solo: no
47
+ los agregues. Cada paso tiene 180 s y el setup entero debe quedar bajo ~5 min.
48
+ - `apt` y `aptRepos`: solo lo que el repo necesite y la imagen no traiga.
49
+ - `session`: comandos rápidos de cada sesión (2 min como máximo cada uno, sin depender de la red).
50
+ - `domains`: los hosts que el setup o los tests necesitan y que no están en la lista por
51
+ defecto de gestores de paquetes.
52
+ - `requiredEnv`: solo los nombres de las variables secretas que de verdad hacen falta en la nube.
53
+ - `notes`: la ruta del archivo de `notes`.
54
+ 3. **Busca los tests del repo:** scripts `test*` y `e2e*` de `package.json`, configuración de
55
+ runners (`vitest.config.*`, `jest.config.*`, `playwright.config.*`, `.mocharc*`),
56
+ Testcontainers en las dependencias, compose de tests y los jobs de la CI que corren tests.
57
+ 4. **Sin tests:** díselo al usuario y no hagas nada más con tests. Anota en `notes` que el repo
58
+ no tiene tests y que en la nube solo se prepara la máquina. Sigue en el paso 7.
59
+ 5. **Con tests:** agrúpalos (p. ej. unitarios, con base de datos, de navegador) y, para cada
60
+ grupo, di si correría en la nube tal cual. Si no, propón, en este orden
61
+ (`reference/compat.md`):
62
+ 1. Adaptar la preparación: `setup`, `session`, `domains`, `requiredEnv`.
63
+ 2. Solo si eso no basta, una variante para la nube: un script aparte (p. ej. `cloud:services`)
64
+ o una rama por `CLAUDE_CODE_REMOTE` en un script existente.
65
+ 3. Si nada de eso alcanza, déjalo fuera de la nube y explica por qué.
66
+
67
+ Muestra cada propuesta con el cambio exacto (archivo y contenido).
68
+ 6. **Pregunta y aplica solo lo aprobado.** Un cambio fuera de la config (scripts, compose,
69
+ configuración de tests) necesita un sí explícito del usuario. Nunca cambies lo que un test
70
+ comprueba ni crees scripts de tests nuevos sin aprobación.
71
+ 7. **Escribe el archivo de `notes`:** qué correr en la nube (el comando tal como lo corre el
72
+ repo, lo que hay que levantar antes, cuánto tarda y si va en segundo plano), qué no y por qué.
73
+ Por ejemplo:
74
+
75
+ ```markdown
76
+ En la nube: `npm test` (~1 min). Antes de `npm run test:db`, `docker compose up -d db`
77
+ (~3 min, en segundo plano). No corre `npm run test:e2e:prod`: pega a una API externa que el
78
+ entorno no permite.
79
+ ```
80
+
81
+ 8. **Si existe `.agents/skills/test-audit/project.md`:** escribe o actualiza su sección "Nube"
82
+ con el mismo resumen; no toques el resto del archivo.
83
+ 9. **Corre `agent-cloud-kit manual`** y muéstrale al usuario la sección "Configurar el entorno en
84
+ claude.ai" de `docs/agent-cloud.md` (setup script, dominios, variables).
85
+ 10. **No hagas commit:** `agent-cloud-kit prepare` lo hace al salir. Si te invocaron fuera de
86
+ `prepare`, pregunta al usuario.
87
+ 11. **Termina diciéndole al usuario que salga:** dile al usuario, con estas palabras o parecidas,
88
+ "configure terminó: sal de claude con `/exit` y `prepare` sigue (commit, push y PR)". Si te
89
+ invocaron fuera de `prepare`, omite este paso.
90
+
91
+ ## `verify` (en la nube)
92
+
93
+ Solo con `CLAUDE_CODE_REMOTE=true`; si no, detente y dilo. El prompt trae la rama de `prepare`
94
+ (`chore/cloud-ready-…`) y puede traer el número del PR. La sesión trabaja en su propia rama
95
+ `claude/…`: no la cambies; al final empujas a la rama de `prepare`.
96
+
97
+ 1. **Contexto:** lee `.claude/cloud.json`, el archivo de `notes` y `docs/agent-cloud.md`. Anota
98
+ las líneas `agent-cloud-kit:` de falla que mostró el inicio de la sesión ("did not run",
99
+ pasos fallidos, "another configuration", variables faltantes).
100
+ 2. **Corre `agent-cloud-kit doctor`** y guarda la salida. Si `agent-cloud-kit` no está en el
101
+ `PATH`, el setup no corrió: usa `npx -y agent-cloud-kit doctor` y repórtalo.
102
+ 3. **Comprueba la preparación:** dependencias instaladas (p. ej. `node_modules` existe), las
103
+ versiones fijadas (`node -v` y los demás gestores contra `.tool-versions`, `mise.toml`,
104
+ `.nvmrc` o `packageManager`), las variables de `requiredEnv` presentes (solo nombres; nunca
105
+ imprimas valores) y Docker, si el repo lo usa.
106
+ 4. **Tests, solo si las `notes` dicen qué correr en la nube.** Córrelos tal como el repo los
107
+ corre, con lo que haya que levantar antes. Las corridas largas van en segundo plano, con la
108
+ salida a un log que sigues hasta que terminen. Si las `notes` no dicen qué correr, no corras
109
+ tests y dilo en el reporte.
110
+ 5. **Si algo falla,** diagnostica con `reference/knowledge.md` y `reference/compat.md`, y
111
+ clasifica:
112
+ - **Se arregla en la preparación:** cambia solo los archivos de la preparación y vuelve a
113
+ comprobar. Un cambio de `setup` no se re-ejecuta en esta sesión (el setup ya corrió): corre
114
+ el comando a mano para probarlo y anota que la máquina guardada se rehará en una sesión nueva.
115
+ - **Solo se cambia en claude.ai** (dominios de la red, variables, entorno por defecto con
116
+ `/remote-env`, rehacer la máquina editando el setup script, el Claude GitHub App): no lo
117
+ intentes; deja la instrucción exacta para el reporte y deja de intentar arreglos.
118
+ - **Requiere cambiar código, tests o scripts:** no lo cambies; deja el cambio propuesto
119
+ (archivo y diff) para el comentario del PR.
120
+
121
+ Hasta **3 intentos** de arreglo en total. Tras el tercero, detente y reporta lo que queda.
122
+ 6. **Hallazgos:** ver "Hallazgos", abajo.
123
+ 7. **Corre `agent-cloud-kit manual`.**
124
+ 8. **Antes del commit,** revisa `git diff --name-only HEAD` y `git status --porcelain`. Todo lo
125
+ que no sea un archivo de la preparación se revierte con `git checkout -- <archivo>` (si está
126
+ versionado) o se borra (si es nuevo y lo creaste tú). Un cambio necesario al código, tests o
127
+ scripts queda como propuesta en el comentario del PR.
128
+ 9. **Commit y push:** agrega solo los archivos de la preparación que cambiaron, por nombre
129
+ (`git add -- .claude/cloud.json <archivo de notes> docs/agent-cloud.md`, más
130
+ `.agents/skills/test-audit/project.md` si lo tocaste); si hay algo en stage,
131
+ `git commit -m "chore(cloud): verify cloud-ready"`. Luego
132
+ `git push origin HEAD:<rama de prepare>`. Si el push se rechaza, `git fetch origin <rama de
133
+ prepare>`, `git rebase FETCH_HEAD` (el clon es parcial: `origin/<rama>` puede no existir) y
134
+ reintenta una vez.
135
+ 10. **Comentario en el PR,** por REST con `gh api` (`gh pr …` usa GraphQL y está bloqueado;
136
+ `gh auth status` dice que el token es inválido, pero `gh api` funciona):
137
+ - `owner` y `repo`: de `git remote get-url origin` (las dos últimas partes de la ruta, sin `.git`).
138
+ - Número del PR: el del prompt; si no viene,
139
+ `gh api "repos/{owner}/{repo}/pulls?head={owner}:{rama de prepare}&state=open" --jq '.[0].number'`.
140
+ - Comentario: escribe el cuerpo en un archivo temporal y
141
+ `gh api repos/{owner}/{repo}/issues/{n}/comments -f body="$(cat <archivo>)"`.
142
+ - Si no hay PR, el resultado queda solo en `docs/agent-cloud.md` y en tu resumen final.
143
+ - No saques el PR de draft: lo decide el usuario.
144
+ 11. **Resumen final** en la sesión, con lo mismo que el comentario.
145
+
146
+ ### El comentario del PR
147
+
148
+ ```markdown
149
+ ## cloud-ready verify
150
+
151
+ **Comprobado:** `agent-cloud-kit doctor` (salida 0 / qué falló), dependencias, versiones, variables.
152
+ **Corrió:** cada comando, su resultado y cuánto tardó.
153
+ **No corrió:** qué y por qué.
154
+ **Cambiar en claude.ai:** la instrucción exacta (dónde y qué), o "nada".
155
+ **Cambios propuestos al código o los tests:** archivo y diff, o "ninguno".
156
+ **Arreglado en la preparación:** archivos y por qué. Intentos: n de 3.
157
+ **Hallazgos:** los nuevos, o "ninguno".
158
+ ```
159
+
160
+ ## Hallazgos
161
+
162
+ En ambos modos: todo hecho nuevo sobre la nube (algo que no está en `reference/knowledge.md`,
163
+ o que lo contradice) va a la sección `## Hallazgos` de `docs/agent-cloud.md`, fuera de las
164
+ marcas de `agent-cloud-kit` (`manual` la conserva). Una línea por hallazgo, marcada
165
+ "proponer al paquete":
166
+
167
+ ```markdown
168
+ - (proponer al paquete) `docker compose` necesita `--wait` para que la base acepte conexiones.
169
+ Visto en `verify`, 2026-11-02, con `docker compose up -d db`.
170
+ ```
171
+
172
+ No edites `reference/knowledge.md` del repo: se reemplaza al actualizar el paquete.
173
+
174
+ ## Límites
175
+
176
+ - `verify` nunca cambia código, tests ni scripts; `configure`, solo con aprobación del usuario.
177
+ - Nunca imprimas ni escribas el valor de una variable secreta; solo su nombre.
178
+ - Nada de `gh pr` ni `gh issue` en la nube: usa `gh api` (REST).
179
+ - No configures claude.ai: da la instrucción exacta y que la haga el usuario.