From b090f7fba3c99e90f239330bfabb982d5f3e498e Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 21:16:26 +0200 Subject: [PATCH 1/9] docs(runs): start the agent runs docs branch Co-Authored-By: Claude Opus 5.5 From 6b98cc9e9af6eef3bf032d5604c01d93d9a396ef Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 21:17:09 +0200 Subject: [PATCH 2/9] fix(docs-api): let MDX comments through the docs text extractor A {/* ... */} comment renders nothing, but the extractor behind /api/docs refused every MDX expression, so a screenshot placeholder would have taken the page down. Comment-only expressions are now dropped; anything else is still refused. Co-Authored-By: Claude Opus 5.5 --- src/lib/mdxToText.ts | 8 ++++++ src/tests/lib/mdxToTextComments.test.ts | 33 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/tests/lib/mdxToTextComments.test.ts diff --git a/src/lib/mdxToText.ts b/src/lib/mdxToText.ts index b9bb74e..0a9e5fa 100644 --- a/src/lib/mdxToText.ts +++ b/src/lib/mdxToText.ts @@ -528,6 +528,11 @@ function isMdxExpression(t: string): boolean { return t === 'mdxFlowExpression' || t === 'mdxTextExpression'; } +/** True for an expression that is a single block comment and nothing else. */ +export function isCommentOnly(raw: string): boolean { + return /^\/\*[\s\S]*\*\/$/.test(raw) && raw.indexOf('*/') === raw.length - 2; +} + function promoteHeadingParagraphs(root: { children?: unknown[] }): void { if (!Array.isArray(root.children)) return; const out: unknown[] = []; @@ -573,6 +578,9 @@ function transformTree(root: { children?: unknown[] }, rateLimits: RateLimitsCon if (isMdxExpression(child.type)) { const raw = ((child as unknown as { value?: string }).value ?? '').trim(); + // An MDX comment (`{/* ... */}`) renders nothing, so it serves nothing. + // Anything else in braces is still refused: it would be code we cannot run. + if (isCommentOnly(raw)) continue; throw new Error(`Unexpected MDX expression: {${raw}}`); } diff --git a/src/tests/lib/mdxToTextComments.test.ts b/src/tests/lib/mdxToTextComments.test.ts new file mode 100644 index 0000000..c64d5bf --- /dev/null +++ b/src/tests/lib/mdxToTextComments.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'vitest'; +import { isCommentOnly, mdxToText } from '../../lib/mdxToText'; + +/** + * Docs pages carry `{/* SCREENSHOT: ... *\/}` placeholders where a capture is + * still to come. The extractor behind /api/docs refused every MDX expression, + * so one such comment would have taken the whole page down. A comment renders + * nothing, so it must serve nothing; any other expression is still refused. + */ +describe('MDX comments in the docs text extractor', () => { + test('a block comment on its own line is dropped', async () => { + const text = await mdxToText('# Title\n\n{/* SCREENSHOT: runs tab */}\n\nAfter.\n'); + expect(text).toContain('After.'); + expect(text).not.toContain('SCREENSHOT'); + }); + + test('a multi-line comment is dropped too', async () => { + const text = await mdxToText('Before.\n\n{/*\n TODO: picture\n*/}\n\nAfter.\n'); + expect(text).not.toContain('TODO'); + expect(text).toContain('Before.'); + }); + + test('any other expression is still refused', async () => { + await expect(mdxToText('Value: {1 + 1}\n')).rejects.toThrow(/Unexpected MDX expression/); + await expect(mdxToText('{/* a */ 1}\n')).rejects.toThrow(/Unexpected MDX expression/); + }); + + test('isCommentOnly accepts a single comment and nothing else', () => { + expect(isCommentOnly('/* x */')).toBe(true); + expect(isCommentOnly('/* a */ + /* b */')).toBe(false); + expect(isCommentOnly('x')).toBe(false); + }); +}); From 8a49240b460db9b9f39a197d74e88ed0f2590863 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 21:22:33 +0200 Subject: [PATCH 3/9] docs(runs): add the Agent runs guide and platform tokens New guide in both locales (Guides, right after Workspaces) for running Pi or Hermes on a task inside a workspace from the portal Runs tab, nan run and the /v1/runs API, plus API keys vs nan_pat_ tokens. Linked from Workspaces, the NaN CLI guide and the introduction. Examples and Apps move one place down. Screenshot slots are left as MDX comments for the owner. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/apps.md | 2 +- src/content/docs-es/examples.md | 2 +- src/content/docs-es/intro.md | 1 + src/content/docs-es/nan-cli.mdx | 12 + src/content/docs-es/runs.mdx | 366 +++++++++++++++++++++++++++++ src/content/docs-es/workspaces.mdx | 6 + src/content/docs/apps.md | 2 +- src/content/docs/examples.md | 2 +- src/content/docs/intro.md | 1 + src/content/docs/nan-cli.mdx | 12 + src/content/docs/runs.mdx | 366 +++++++++++++++++++++++++++++ src/content/docs/workspaces.mdx | 6 + src/tests/layouts/runsDocs.test.ts | 227 ++++++++++++++++++ 13 files changed, 1001 insertions(+), 4 deletions(-) create mode 100644 src/content/docs-es/runs.mdx create mode 100644 src/content/docs/runs.mdx create mode 100644 src/tests/layouts/runsDocs.test.ts diff --git a/src/content/docs-es/apps.md b/src/content/docs-es/apps.md index 09529d0..5140472 100644 --- a/src/content/docs-es/apps.md +++ b/src/content/docs-es/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Despliega tus apps desde GitHub a NaN Cloud en minutos. -order: 20 +order: 21 group: Guías --- diff --git a/src/content/docs-es/examples.md b/src/content/docs-es/examples.md index c1cf3ff..d1cac5d 100644 --- a/src/content/docs-es/examples.md +++ b/src/content/docs-es/examples.md @@ -1,7 +1,7 @@ --- title: Ejemplos description: Fragmentos de código para conectarte a la API de NaN con Python, Node.js, curl y más. -order: 19 +order: 20 group: Guías --- diff --git a/src/content/docs-es/intro.md b/src/content/docs-es/intro.md index 1d1fb3a..4d5f140 100644 --- a/src/content/docs-es/intro.md +++ b/src/content/docs-es/intro.md @@ -31,5 +31,6 @@ Los límites van por API key — un tope de peticiones por minuto y un máximo d - [Referencia de la API](/es/docs/api): todos los endpoints, campo a campo. - [Modelos](/es/docs/models): las fichas técnicas y los límites. - [Workspaces](/es/docs/workspaces): tu propia máquina en la nube, por SSH y en el navegador, con agentes de código listos para usar. +- [Runs de agentes](/es/docs/runs): dale una tarea a un agente dentro de tu workspace y deja que trabaje solo. - [Ejemplos](/es/docs/examples): fragmentos en Python, Node.js y curl. - Soporte: reporta incidencias en `#support` de Discord. diff --git a/src/content/docs-es/nan-cli.mdx b/src/content/docs-es/nan-cli.mdx index dd0de9a..b7de7c9 100644 --- a/src/content/docs-es/nan-cli.mdx +++ b/src/content/docs-es/nan-cli.mdx @@ -156,6 +156,18 @@ En Linux con bash, el mismo comando pero contra `~/.bashrc`. +## Runs + +Desde la v0.1.24 la CLI también pone a trabajar agentes en tareas dentro de tus [workspaces](/es/docs/workspaces), y los sigue desde tu terminal: + +```bash +nan run "añade tests al parser de fechas y haz que pasen" +nan runs ls +nan runs logs -f +``` + +El run se ejecuta en el workspace, no en tu máquina, así que sigue aunque cierres la terminal. Los comandos de runs usan tu sesión de `nan auth login`. Todos los flags, los códigos de salida y la API están en [Runs de agentes](/es/docs/runs). + ## Actualizar Vuelve a pasar el instalador. Siempre trae la última versión publicada y sobrescribe la que tengas: diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx new file mode 100644 index 0000000..3c0abdc --- /dev/null +++ b/src/content/docs-es/runs.mdx @@ -0,0 +1,366 @@ +--- +title: Runs de agentes +description: Dale una tarea a un agente dentro de tu workspace y deja que trabaje solo, desde el portal, la terminal o la API. +order: 19 +group: Guías +--- + +import Steps from '../../components/docs/Steps.astro'; +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; + +# Runs de agentes. + +Un **run** le da una tarea a un agente dentro de uno de tus [workspaces](/es/docs/workspaces) y le deja trabajar solo. Escribes lo que quieres que haga, eliges el agente (Pi o Hermes, el que tengas instalado en ese workspace) y la carpeta, y el run se encarga del resto. Sigue adelante aunque cierres la página o la terminal. + +Mientras trabaja, sigues su log en directo. Cuando termina tienes su resultado y, si la carpeta es un repositorio git, una rama con sus cambios lista para que la revises. + +Puedes lanzar un run de tres formas, y las tres ven los mismos runs: + +- Desde la pestaña **Runs** de tu workspace en el portal. +- Desde tu terminal, con `nan run`. +- Desde tus propios scripts, con la API. + +## Antes de empezar + +- **Un workspace encendido**, con Pi o Hermes instalado. Puedes instalar cualquiera de los dos desde el panel de **Software** del workspace. +- **Un plan con inferencia.** Un run usa la clave de inferencia del propio workspace, y lo que gasta el agente cuenta en el uso de tu plan como cualquier otra petición. Si tu plan no incluye inferencia no puedes crear runs: el portal te enseña las opciones para mejorarlo. + +## Lanza un run desde el portal + + + +### Abre la pestaña Runs + +Entra en [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) y abre un workspace. Junto a **Overview**, **Console**, **SSH keys**, **Events** y **Backups** tienes la pestaña **Runs**, con el historial de runs de ese workspace, del más reciente al más antiguo. + +{/* SCREENSHOT: pestaña Runs, el historial de runs de un workspace con el botón New run */} + +### Rellena el nuevo run + +Pulsa **New run** y rellena el formulario: + +| Campo | Qué es | +|---|---| +| Agent | Pi o Hermes, el que tengas instalado en el workspace. | +| Prompt | La tarea, con tus palabras, como se la escribirías al agente en un chat. Hasta 32 KiB. | +| Working directory | La carpeta en la que trabaja el agente. Por defecto es `/home/nan` y tiene que estar dentro de ella. | +| Worktree | `auto` (por defecto), `on` u `off`. Mira [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| Timeout | 15, 30, 60 o 120 minutos. El run se para al llegar a ese límite. | + +{/* SCREENSHOT: formulario de nuevo run, con agente, prompt, carpeta de trabajo, worktree y timeout */} + +### Síguelo en directo + +Haz clic en un run de la lista para abrir su página. El log se va llenando mientras el agente trabaja: lo que dice, las herramientas que usa y lo que le responden. Cuando el run termina, la página muestra cómo ha acabado, el resumen del agente y, si la hay, la rama con sus cambios. + +Puedes cerrar la página cuando quieras. El run sigue, y su página estará ahí cuando vuelvas. + +{/* SCREENSHOT: página de un run con el log en directo, el estado y el botón Cancel */} + +### Cancélalo, si hace falta + +**Cancel** para el run en unos segundos. El portal te pide confirmación antes. + + + +## Dónde trabaja el agente + +Lo que pasa con tus archivos depende de la carpeta que elijas y de la opción **worktree**. + +| La carpeta... | Worktree `auto` (por defecto) | Worktree `on` | Worktree `off` | +|---|---|---|---| +| está dentro de un repositorio git | Worktree y rama aparte | Worktree y rama aparte | Directamente en la carpeta | +| no es un repositorio git | Directamente en la carpeta | El run falla con un error de configuración | Directamente en la carpeta | + +**Con worktree**, el agente no toca tu checkout. Trabaja en una copia aparte del repositorio, en una rama nueva que se llama `nan-run/` seguido de los 8 primeros caracteres del id del run, por ejemplo `nan-run/6f1c2a9b`. Cuando el agente acaba, todo lo que ha cambiado queda en un commit en esa rama. El run **nunca hace push**: la rama se queda en el repositorio dentro de tu workspace y tú decides qué hacer con ella. Si el agente no ha cambiado nada, la rama se borra. + +Para revisar los cambios e incorporarlos, desde el repositorio dentro de tu workspace: + +```bash +git log -p HEAD..nan-run/6f1c2a9b +git merge nan-run/6f1c2a9b +``` + +**Sin worktree**, el agente trabaja directamente en la carpeta y no hay rama. Úsalo con carpetas que no son repositorios, o cuando quieras los cambios ahí mismo. + +La carpeta de trabajo tiene que estar dentro de `/home/nan`, tu home en el workspace. + +## Cómo termina un run + +| Estado | Qué significa | +|---|---| +| `queued` | Esperando un hueco libre. Mira [Cuántos runs a la vez](#cuántos-runs-a-la-vez). | +| `starting` | Se está preparando en el workspace. | +| `running` | El agente está trabajando. | +| `succeeded` | El agente ha terminado su tarea. | +| `failed` | El agente se ha parado con un error, o el run no ha podido empezar (por ejemplo, worktree `on` en una carpeta que no es un repositorio). | +| `cancelled` | Lo has cancelado tú. | +| `timed_out` | Ha llegado a su timeout y se ha parado. | + +Desde la CLI y la API el timeout va de 60 segundos a 2 horas. El portal ofrece 15, 30, 60 y 120 minutos. + +## Cuántos runs a la vez + +Cuántos runs trabajan a la vez depende del tamaño del workspace: + +| Tamaño | Runs a la vez | +|---|---| +| Micro (también el workspace gratis de Premium) | 1 | +| Nano | 2 | +| Basic | 3 | +| Medium | 5 | +| Large | 8 | + +Además, los runs comparten tu concurrencia de inferencia, así que puedes tener como mucho **5 runs trabajando a la vez entre todos tus workspaces** (**8 con Premium**). + +Los runs que pasan de esos límites no se rechazan: esperan en una cola y arrancan por orden de llegada. En la cola caben hasta **20 runs por workspace** y **50 en total**, y puedes crear hasta **60 runs por hora**. + +Cada run trabaja en su propio proceso dentro del workspace, con su parte de CPU y de memoria. Si uno se descontrola, se para solo ese: tus sesiones SSH y tus agentes siempre encendidos siguen funcionando. + +## Inferencia y secretos + +Los runs usan la clave de inferencia del workspace, la misma que usan sus agentes cuando trabajas con ellos a mano. Sus peticiones cuentan en el uso de tu plan como cualquier otra. + +Los valores del archivo de entorno de tu workspace, como esa clave de inferencia o un token de bot de Telegram, aparecen como `***` en los logs y resultados de los runs. Puedes compartir un log sin filtrarlos. + +## Desde la terminal: `nan run` + +La [CLI de NaN](/es/docs/nan-cli) lanza y sigue runs desde tu propia terminal. Necesitas la versión **0.1.24** o posterior. + + + +### Instala o actualiza la CLI + +```bash +curl -fsSL https://nan.builders/install | bash +``` + +### Inicia sesión + +```bash +nan auth login --email tu@correo +``` + +Te llega por correo un enlace de acceso. Cópialo y pégalo de vuelta en el comando sin abrirlo antes en el navegador: solo funciona una vez. Tienes los detalles en la [guía de la CLI](/es/docs/nan-cli). Los comandos de runs usan esa sesión, o la API key de la pestaña Setup de la CLI si no hay sesión. + +### Lanza un run + +```bash +nan run "añade tests al parser de fechas y haz que pasen" +``` + +La CLI te muestra el log en la terminal hasta que el run termina. + + + +Otras formas de lanzarlo: + +```bash +# Un workspace, un agente y una carpeta concretos +nan run --ws develop --agent hermes --cwd /home/nan/projects/api "revisa los TODO pendientes" + +# El prompt desde un archivo, o desde stdin +nan run -f tarea.md +git diff | nan run - + +# Lánzalo y quédate con el id sin esperar +nan run --detach "actualiza las dependencias" +``` + +| Flag | Por defecto | Qué hace | +|---|---|---| +| `--ws NAME` | tu único workspace | El workspace donde se ejecuta. Si tienes más de uno y no lo pones, la CLI te los lista y sale con código 64. | +| `--agent pi\|hermes` | `pi` | El agente. | +| `--model M` | el del agente | El modelo que usa el agente. | +| `--cwd PATH` | `/home/nan` | La carpeta de trabajo en el workspace. | +| `--worktree` / `--no-worktree` | auto | Fuerza el worktree a on u off. Mira [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| `--timeout D` | `30m` | Cuándo pararlo, de `1m` a `2h`. | +| `--detach` | | Lanza el run, imprime su id y vuelve. | +| `--json` | | Imprime los eventos como líneas JSON y después el run terminado. | +| `-f FILE` | | Lee el prompt de un archivo, o de stdin con `-`. | + +Mientras se muestra el log, **Ctrl-C una vez** te desengancha: el run sigue y la CLI te dice cómo retomarlo. **Ctrl-C dos veces en menos de 2 segundos** lo cancela. + +### Gestiona tus runs + +```bash +nan runs ls # tus runs, del más reciente al más antiguo +nan runs show # estado, resultado y prompt +nan runs logs -f # sigue el log hasta que termine +nan runs cancel +``` + +`nan runs logs --json` imprime un evento JSON por línea, cómodo para scripts. + +### Códigos de salida + +`nan run` y `nan runs logs -f` salen con el resultado del run, así que un script puede actuar sin leer el texto: + +| Código | Significado | +|---|---| +| 0 | terminado con éxito | +| 1 | fallido | +| 2 | timeout | +| 3 | cancelado | +| 4 | el workspace no está listo: agente no instalado, sin clave de inferencia o configuración incorrecta | +| 64 | error de uso, también tener más de un workspace y no pasar `--ws` | +| 65 | sin sesión, sesión caducada o sin permiso | +| 69 | nan.builders no disponible, o se ha perdido el stream (el run sigue, y se imprime el id) | +| 75 | el run se ha aceptado y sigue en marcha (`--detach`, o Ctrl-C una vez) | + +Con `set -e`, ten en cuenta el 75 de `--detach`: + +```bash +id=$(nan run --detach "actualiza las dependencias") || [ $? -eq 75 ] +``` + +## Desde la API + +Los mismos runs están disponibles por HTTP, en `https://api.nan.builders/v1/runs`, para tus scripts e integraciones. + +### API keys y tokens + +Hay dos tipos de credenciales que sirven para la API de runs. Las dos están en tus [ajustes del portal](https://cloud.nan.builders/): + +| | **API Keys** | **Tokens** | +|---|---|---| +| Aspecto | `sk-...` | `nan_pat_...` | +| Inferencia (modelos) | Sí | **No, nunca** | +| API de la plataforma (runs, tus workspaces) | Sí | Sí | +| Quién puede crearlas | Planes con inferencia | Cualquier miembro con una suscripción activa | + +Un **token** es lo que conviene darle a un script que solo gestiona runs: no sirve para llamar a los modelos, así que robarlo no da acceso a la inferencia. Créalo en **Settings → Tokens**: + +- Se muestra **una sola vez**, al crearlo. Cópialo en ese momento. +- Puedes tener hasta **10** tokens activos. +- Cada uno caduca a los **30, 90 o 365 días**, o nunca. +- Puedes revocar cualquiera en cualquier momento. + +Para crear runs sigue haciendo falta un plan con inferencia, uses la credencial que uses. + + +La clave que hay dentro de un workspace, la que usan sus agentes para la inferencia, no puede gestionar runs: la API le responde con un `403`. Un agente que trabaja en tu workspace no puede lanzar runs nuevos por su cuenta. + + +### Crea un run + +```bash +export NAN_TOKEN="nan_pat_..." + +curl https://api.nan.builders/v1/runs \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: fix-date-parser-1" \ + -d '{ + "workspace": "develop", + "agent": "pi", + "prompt": "añade tests al parser de fechas y haz que pasen", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800 + }' +``` + +| Campo | Obligatorio | Qué es | +|---|---|---| +| `workspace` | Sí | Nombre o id del workspace. | +| `agent` | Sí | `pi` o `hermes`. | +| `prompt` | Sí | La tarea, hasta 32 KiB. | +| `cwd` | No | Carpeta de trabajo, `/home/nan` por defecto. | +| `git_isolation` | No | `null` para auto (por defecto), `true` para worktree on, `false` para off. | +| `timeout_seconds` | No | De 60 a 7200. 1800 (30 minutos) por defecto. | +| `model` | No | El modelo del agente. Por defecto, el suyo. | + +La respuesta es el run, con su `id` y su `state`. La cabecera `Idempotency-Key` es opcional: si un reintento manda la misma clave con el mismo cuerpo, recibes el run que ya se creó en lugar de uno nuevo. + +### Síguelo + +Los eventos de un run llegan como un stream de server-sent events cuando pides `text/event-stream`. Con `curl -N` los ves según llegan: + +```bash +curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Accept: text/event-stream" +``` + +Cada frame es uno de estos: + +- `event: run_event`, con una línea `id:`: un evento del log (un mensaje, una llamada a una herramienta, su resultado...). Manda el último `id` que hayas visto en la cabecera `Last-Event-ID` para retomar tras un corte, sin perder ni repetir nada. +- `event: state`: el run ha cambiado de estado. +- `event: end`: el run ha terminado y su log se ha enviado entero. Los datos son el run terminado. El stream se cierra después. + +Cada 15 segundos el stream manda una línea de comentario para mantener la conexión abierta. Sin la cabecera `Accept`, el mismo endpoint responde con páginas de JSON: pasa el `next_after` de cada página como `?after=` para pedir la siguiente. + +Para ver cómo ha terminado: + +```bash +curl https://api.nan.builders/v1/runs/$RUN_ID \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### Cancélalo + +```bash +curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### Lista tus runs + +```bash +curl "https://api.nan.builders/v1/runs?workspace=develop&state=running&limit=20" \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +Todos los filtros son opcionales. Si hay más runs de los que caben en una página, la respuesta trae un `next_cursor`: pásalo como `?cursor=` para pedir la siguiente. + +### Errores + +Los errores usan el mismo formato que la API de inferencia: + +```json +{"error": {"message": "...", "type": "...", "param": null, "code": "tier_restricted"}} +``` + +| Estado y código | Qué hacer | +|---|---| +| `403 tier_restricted` | Tu plan no incluye inferencia. Mejóralo para crear runs. | +| `403 workspace_key_not_allowed` | Has usado la clave de dentro de un workspace. Usa una API key o un token. | +| `409 workspace_not_running` | Enciende primero el workspace. | +| `409 agent_not_installed` | Instala el agente desde el panel de **Software**, o elige el otro. | +| `409 no_inference_key` | El workspace no tiene clave de inferencia. Comprueba que tu plan incluye inferencia. | +| `429 run_queue_full` | La cola está llena (20 por workspace, 50 en total). Espera a que terminen algunos runs. | +| `429 rate_limited` | Más de 60 runs en la última hora. Vuelve a intentarlo más tarde. | + +## Preguntas frecuentes + +
+ +No. Con worktree, los cambios se quedan en una rama `nan-run/...` del repositorio dentro de tu workspace. Hacer push, merge o tirarlos es cosa tuya. + +
+ +
+ +Nada: el run sigue en el workspace. Abre su página en la pestaña **Runs**, o ejecuta `nan runs logs -f`, para retomarlo. + +
+ +
+ +No. Los runs usan los modelos a través de la clave de inferencia del workspace, así que para crearlos necesitas un plan con inferencia. Sí puedes crear un token y listar tus workspaces y tus runs. + +
+ +
+ +No. Cada run tiene su propia parte de la CPU y la memoria del workspace. Si uno se descontrola, solo se para ese run. + +
+ +
+ +Escribe en `#support` en Discord. + +
diff --git a/src/content/docs-es/workspaces.mdx b/src/content/docs-es/workspaces.mdx index b8ca31d..3e7fc91 100644 --- a/src/content/docs-es/workspaces.mdx +++ b/src/content/docs-es/workspaces.mdx @@ -215,6 +215,12 @@ gentle-shell es un complemento de Pi (memoria entre sesiones con Engram), así q Con un plan de inferencia, los agentes vienen configurados para usar los modelos de NaN con tu membresía. Las guías de [Pi](/es/docs/pi) y [Hermes](/es/docs/hermes) explican cómo cambiar el modelo o el proveedor a mano. +## Pon a tus agentes a trabajar en tareas + +No hace falta que te quedes conectado mientras un agente trabaja. Un **run** le da una tarea a Pi o a Hermes dentro de tu workspace y le deja trabajar solo: sigues su log en directo, continúa aunque cierres la página y, en un repositorio git, sus cambios acaban en una rama para que los revises. Lánzalo desde la pestaña **Runs** del workspace, desde tu terminal con `nan run` o desde la API. + +Cómo funciona, los límites y los comandos están en [Runs de agentes](/es/docs/runs). + ## Copias de seguridad Los workspaces **no tienen copias de seguridad** salvo que actives el **complemento de backups** de ese workspace, desde su pestaña **Backups**. diff --git a/src/content/docs/apps.md b/src/content/docs/apps.md index 1dd74bf..f5f4d36 100644 --- a/src/content/docs/apps.md +++ b/src/content/docs/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Deploy your apps from GitHub to NaN Cloud in minutes. -order: 20 +order: 21 group: Guides --- diff --git a/src/content/docs/examples.md b/src/content/docs/examples.md index 3a4ab7d..1a59d7b 100644 --- a/src/content/docs/examples.md +++ b/src/content/docs/examples.md @@ -1,7 +1,7 @@ --- title: Examples description: Code snippets to connect to the NaN API with Python, Node.js, curl, and more. -order: 19 +order: 20 group: Guides --- diff --git a/src/content/docs/intro.md b/src/content/docs/intro.md index 979ead2..14f24db 100644 --- a/src/content/docs/intro.md +++ b/src/content/docs/intro.md @@ -31,5 +31,6 @@ Limits are per API key — a cap on requests per minute and a maximum number of - [API reference](/docs/api): every endpoint, field by field. - [Models](/docs/models): the spec sheets and the limits. - [Workspaces](/docs/workspaces): your own cloud machine, over SSH and in the browser, with coding agents ready to use. +- [Agent runs](/docs/runs): give an agent a task inside your workspace and let it work on its own. - [Examples](/docs/examples): snippets in Python, Node.js and curl. - Support: report issues in `#support` on Discord. diff --git a/src/content/docs/nan-cli.mdx b/src/content/docs/nan-cli.mdx index 0d62b44..3f43399 100644 --- a/src/content/docs/nan-cli.mdx +++ b/src/content/docs/nan-cli.mdx @@ -156,6 +156,18 @@ On Linux with bash, the same command but against `~/.bashrc`. +## Runs + +From v0.1.24 the CLI also starts agents on tasks inside your [workspaces](/docs/workspaces), and follows them from your terminal: + +```bash +nan run "add tests for the date parser and make them pass" +nan runs ls +nan runs logs -f +``` + +The run happens in the workspace, not on your machine, so it keeps going if you close the terminal. The runs commands use your `nan auth login` session. Every flag, the exit codes and the API are in [Agent runs](/docs/runs). + ## Updating Run the installer again. It always brings the latest published release and overwrites the one you have: diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx new file mode 100644 index 0000000..d261091 --- /dev/null +++ b/src/content/docs/runs.mdx @@ -0,0 +1,366 @@ +--- +title: Agent runs +description: Give an agent a task inside your workspace and let it work on its own, from the portal, the terminal or the API. +order: 19 +group: Guides +--- + +import Steps from '../../components/docs/Steps.astro'; +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; + +# Agent runs. + +A **run** gives an agent a task inside one of your [workspaces](/docs/workspaces) and lets it work on its own. You write what you want done, pick the agent (Pi or Hermes, whichever is installed in that workspace) and the folder, and the run takes it from there. It keeps going if you close the page or the terminal. + +While it works you follow its log live. When it ends you get its result and, if the folder is a git repository, a branch with its changes, ready for you to review. + +You can start a run in three ways, and all three see the same runs: + +- From the **Runs** tab of your workspace in the portal. +- From your terminal, with `nan run`. +- From your own scripts, with the API. + +## Before you start + +- **A workspace that is running**, with Pi or Hermes installed. You can install either one from the **Software** panel of the workspace. +- **A plan with inference.** A run uses your workspace's own inference key, and what the agent spends counts against your plan's usage like any other request. Without inference in your plan you cannot create runs: the portal shows you the upgrade options instead. + +## Start a run from the portal + + + +### Open the Runs tab + +Go to [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) and open a workspace. Next to **Overview**, **Console**, **SSH keys**, **Events** and **Backups** there is a **Runs** tab with the history of your runs in that workspace, newest first. + +{/* SCREENSHOT: runs tab, the run history of a workspace with the New run button */} + +### Fill in the new run + +Press **New run** and fill in the form: + +| Field | What it is | +|---|---| +| Agent | Pi or Hermes, whichever is installed in the workspace. | +| Prompt | The task, in your own words, as you would write it to the agent in a chat. Up to 32 KiB. | +| Working directory | The folder the agent works in. It defaults to `/home/nan` and must be inside it. | +| Worktree | `auto` (the default), `on` or `off`. See [Where the agent works](#where-the-agent-works). | +| Timeout | 15, 30, 60 or 120 minutes. The run is stopped when it reaches it. | + +{/* SCREENSHOT: new run form, with agent, prompt, working directory, worktree and timeout */} + +### Follow it live + +Click a run in the list to open its page. The log fills in as the agent works: what it says, the tools it calls and what they answer. When the run ends, the page shows how it ended, the agent's summary and, if there is one, the branch with its changes. + +You can close the page at any moment. The run goes on, and its page is there when you come back. + +{/* SCREENSHOT: run page with the live log, the state and the Cancel button */} + +### Cancel it, if you need to + +**Cancel** stops the run within a few seconds. The portal asks you to confirm first. + + + +## Where the agent works + +What happens to your files depends on the folder you choose and on the **worktree** setting. + +| The folder is... | Worktree `auto` (default) | Worktree `on` | Worktree `off` | +|---|---|---|---| +| inside a git repository | Separate worktree and branch | Separate worktree and branch | Directly in the folder | +| not a git repository | Directly in the folder | The run fails with a configuration error | Directly in the folder | + +**With a worktree**, the agent does not touch your checkout. It works in a separate copy of the repository, on a new branch called `nan-run/` followed by the first 8 characters of the run id, for example `nan-run/6f1c2a9b`. When the agent ends, whatever it changed is committed on that branch. The run **never pushes**: the branch stays in the repository inside your workspace, and you decide what to do with it. If the agent changed nothing, the branch is deleted. + +To review the changes and bring them in, from the repository inside your workspace: + +```bash +git log -p HEAD..nan-run/6f1c2a9b +git merge nan-run/6f1c2a9b +``` + +**Without a worktree**, the agent works directly in the folder and there is no branch. Use it for folders that are not repositories, or when you want the changes right there. + +The working directory must be inside `/home/nan`, your home in the workspace. + +## How a run ends + +| State | What it means | +|---|---| +| `queued` | Waiting for a free place. See [How many runs at once](#how-many-runs-at-once). | +| `starting` | Being set up in the workspace. | +| `running` | The agent is working. | +| `succeeded` | The agent finished its task. | +| `failed` | The agent stopped with an error, or the run could not start (for example, worktree `on` in a folder that is not a repository). | +| `cancelled` | You cancelled it. | +| `timed_out` | It reached its timeout and was stopped. | + +Through the CLI and the API the timeout goes from 60 seconds to 2 hours. The portal offers 15, 30, 60 and 120 minutes. + +## How many runs at once + +How many runs work at the same time depends on the size of the workspace: + +| Size | Runs at once | +|---|---| +| Micro (also the free Premium workspace) | 1 | +| Nano | 2 | +| Basic | 3 | +| Medium | 5 | +| Large | 8 | + +On top of that, runs share your inference concurrency, so you can have at most **5 runs working at once across all your workspaces** (**8 on Premium**). + +Runs beyond those limits are not refused: they wait in a queue and start in order, first in, first out. The queue holds up to **20 runs per workspace** and **50 in total**, and you can create up to **60 runs per hour**. + +Each run works in its own process inside the workspace, with its own share of CPU and memory. If one gets out of hand, it is stopped alone: your SSH sessions and your always-on agents keep working. + +## Inference and secrets + +Runs use the inference key of the workspace, the same one its agents use when you work with them by hand. Their requests count against your plan's usage like any other. + +The values in your workspace's environment file, such as that inference key or a Telegram bot token, are shown as `***` in run logs and results. You can share a log without leaking them. + +## From the terminal: `nan run` + +The [NaN CLI](/docs/nan-cli) starts and follows runs from your own terminal. You need version **0.1.24** or newer. + + + +### Install or update the CLI + +```bash +curl -fsSL https://nan.builders/install | bash +``` + +### Sign in + +```bash +nan auth login --email you@yours +``` + +It emails you a sign-in link. Copy it from the email and paste it back into the command without opening it in the browser first: it works once. The [NaN CLI guide](/docs/nan-cli) has the details. The runs commands use that session, or the API key from the CLI's Setup tab when there is no session. + +### Start a run + +```bash +nan run "add tests for the date parser and make them pass" +``` + +The CLI streams the log to your terminal until the run ends. + + + +A few more ways to start one: + +```bash +# A specific workspace, agent and folder +nan run --ws develop --agent hermes --cwd /home/nan/projects/api "review the open TODOs" + +# The prompt from a file, or from stdin +nan run -f task.md +git diff | nan run - + +# Start it and get the id back without waiting +nan run --detach "upgrade the dependencies" +``` + +| Flag | Default | What it does | +|---|---|---| +| `--ws NAME` | your only workspace | The workspace to run in. If you have more than one and leave it out, the CLI lists them and exits with code 64. | +| `--agent pi\|hermes` | `pi` | The agent. | +| `--model M` | the agent's own | The model the agent uses. | +| `--cwd PATH` | `/home/nan` | The working directory in the workspace. | +| `--worktree` / `--no-worktree` | auto | Force a worktree on or off. See [Where the agent works](#where-the-agent-works). | +| `--timeout D` | `30m` | When to stop it, from `1m` to `2h`. | +| `--detach` | | Start the run, print its id and return. | +| `--json` | | Print events as JSON lines, then the finished run. | +| `-f FILE` | | Read the prompt from a file, or from stdin with `-`. | + +While it streams, **Ctrl-C once** detaches: the run keeps going and the CLI tells you how to pick it up again. **Ctrl-C twice within 2 seconds** cancels it. + +### Manage your runs + +```bash +nan runs ls # your runs, newest first +nan runs show # state, result and prompt +nan runs logs -f # follow the log until it ends +nan runs cancel +``` + +`nan runs logs --json` prints one JSON event per line, handy for scripts. + +### Exit codes + +`nan run` and `nan runs logs -f` exit with the outcome of the run, so a script can act on it without reading the text: + +| Code | Meaning | +|---|---| +| 0 | succeeded | +| 1 | failed | +| 2 | timed out | +| 3 | cancelled | +| 4 | the workspace is not ready for it: agent not installed, no inference key or a bad configuration | +| 64 | usage error, including more than one workspace and no `--ws` | +| 65 | not signed in, session expired or not allowed | +| 69 | nan.builders unavailable, or the stream was lost (the run goes on, and the id is printed) | +| 75 | the run was accepted and is still going (`--detach`, or Ctrl-C once) | + +Under `set -e`, allow for the 75 of `--detach`: + +```bash +id=$(nan run --detach "upgrade the dependencies") || [ $? -eq 75 ] +``` + +## From the API + +The same runs are available over HTTP, at `https://api.nan.builders/v1/runs`, for your own scripts and integrations. + +### API keys and tokens + +Two kinds of credentials work with the runs API. Both are in your [settings on the portal](https://cloud.nan.builders/): + +| | **API Keys** | **Tokens** | +|---|---|---| +| Looks like | `sk-...` | `nan_pat_...` | +| Inference (models) | Yes | **No, never** | +| Platform API (runs, your workspaces) | Yes | Yes | +| Who can create them | Plans with inference | Any member with an active subscription | + +A **token** is the one to give a script that only manages runs: it cannot be used to call the models, so it is not worth stealing for that. Create it under **Settings → Tokens**: + +- It is shown **once**, when you create it. Copy it then. +- You can have up to **10** active tokens. +- Each one expires after **30, 90 or 365 days**, or never. +- You can revoke any of them at any time. + +Creating runs still needs a plan with inference, whatever credential you use. + + +The key inside a workspace, the one its agents use for inference, cannot manage runs: the API answers it with `403`. An agent working in your workspace cannot start new runs on its own. + + +### Create a run + +```bash +export NAN_TOKEN="nan_pat_..." + +curl https://api.nan.builders/v1/runs \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: fix-date-parser-1" \ + -d '{ + "workspace": "develop", + "agent": "pi", + "prompt": "add tests for the date parser and make them pass", + "cwd": "/home/nan/projects/api", + "git_isolation": null, + "timeout_seconds": 1800 + }' +``` + +| Field | Required | What it is | +|---|---|---| +| `workspace` | Yes | Name or id of the workspace. | +| `agent` | Yes | `pi` or `hermes`. | +| `prompt` | Yes | The task, up to 32 KiB. | +| `cwd` | No | Working directory, `/home/nan` by default. | +| `git_isolation` | No | `null` for auto (the default), `true` for worktree on, `false` for off. | +| `timeout_seconds` | No | From 60 to 7200. 1800 (30 minutes) by default. | +| `model` | No | The model for the agent. Its own by default. | + +The answer is the run, with its `id` and its `state`. The `Idempotency-Key` header is optional: if a retry sends the same key with the same body, you get the run that was already created instead of a second one. + +### Follow it + +The events of a run come as a stream of server-sent events when you ask for `text/event-stream`. With `curl -N` you see them as they arrive: + +```bash +curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Accept: text/event-stream" +``` + +Each frame is one of these: + +- `event: run_event`, with an `id:` line: one event of the log (a message, a tool call, its result...). Send the last `id` you saw as the `Last-Event-ID` header to resume after a dropped connection, without missing or repeating anything. +- `event: state`: the run changed state. +- `event: end`: the run is over and its log has been sent in full. The data is the finished run. The stream closes after it. + +Every 15 seconds the stream sends a comment line to keep the connection open. Without the `Accept` header, the same endpoint answers pages of JSON instead: pass the `next_after` of each page as `?after=` to get the next one. + +To see how it ended: + +```bash +curl https://api.nan.builders/v1/runs/$RUN_ID \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### Cancel it + +```bash +curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +### List your runs + +```bash +curl "https://api.nan.builders/v1/runs?workspace=develop&state=running&limit=20" \ + -H "Authorization: Bearer $NAN_TOKEN" +``` + +All filters are optional. If there are more runs than fit in a page, the answer has a `next_cursor`: pass it as `?cursor=` to get the next page. + +### Errors + +Errors use the same format as the inference API: + +```json +{"error": {"message": "...", "type": "...", "param": null, "code": "tier_restricted"}} +``` + +| Status and code | What to do | +|---|---| +| `403 tier_restricted` | Your plan does not include inference. Upgrade it to create runs. | +| `403 workspace_key_not_allowed` | You used the key from inside a workspace. Use an API key or a token. | +| `409 workspace_not_running` | Start the workspace first. | +| `409 agent_not_installed` | Install the agent from the **Software** panel, or pick the other one. | +| `409 no_inference_key` | The workspace has no inference key. Check that your plan includes inference. | +| `429 run_queue_full` | The queue is full (20 per workspace, 50 in total). Wait for some runs to end. | +| `429 rate_limited` | More than 60 runs in the last hour. Try again later. | + +## FAQ + +
+ +No. With a worktree, the changes stay on a `nan-run/...` branch in the repository inside your workspace. Pushing, merging or throwing them away is up to you. + +
+ +
+ +Nothing: the run keeps going in the workspace. Open its page in the **Runs** tab, or run `nan runs logs -f`, to pick it up again. + +
+ +
+ +No. Runs use the models through your workspace's inference key, so creating them needs a plan with inference. You can still create a token and list your workspaces and runs. + +
+ +
+ +No. Each run has its own share of the workspace's CPU and memory. If one gets out of hand, only that run is stopped. + +
+ +
+ +Write in `#support` on Discord. + +
diff --git a/src/content/docs/workspaces.mdx b/src/content/docs/workspaces.mdx index 548611d..1f74a54 100644 --- a/src/content/docs/workspaces.mdx +++ b/src/content/docs/workspaces.mdx @@ -215,6 +215,12 @@ gentle-shell is an add-on for Pi (memory between sessions with Engram), so it ne On an inference plan, the agents come set up to use NaN models with your membership. The setup guides for [Pi](/docs/pi) and [Hermes](/docs/hermes) explain how to change the model or the provider by hand. +## Run agents on tasks + +You do not have to stay connected while an agent works. A **run** gives Pi or Hermes a task inside your workspace and lets it work on its own: you follow its log live, it keeps going when you close the page, and in a git repository its changes land on a branch for you to review. Start one from the **Runs** tab of the workspace, from your terminal with `nan run`, or from the API. + +How it works, the limits and the commands are in [Agent runs](/docs/runs). + ## Backups Workspaces have **no backups** unless you turn on the **backup add-on** for that workspace, from its **Backups** tab. diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts new file mode 100644 index 0000000..acee2d6 --- /dev/null +++ b/src/tests/layouts/runsDocs.test.ts @@ -0,0 +1,227 @@ +import { describe, expect, test } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { parseFrontmatter } from '@astrojs/markdown-remark'; +import { mdxToText } from '../../lib/mdxToText'; + +/** + * The Agent runs guide: what members read before they start their first run, + * from the portal, `nan run` or the API. It opens together with Agent + * Automations (Phase 2). + * + * The facts pinned here come from the Phase 1 spec + * (devops AGENT-AUTOMATIONS-PHASE1.md: §2.1.3 API, §2.1.4 admission, §2.2.6 + * worktrees, §2.4 CLI) and the production E2E of 2026-10-10: per-size + * concurrency, the per-member cap shared with inference, the queue limits, the + * CLI exit codes, and the two kinds of credentials (API keys vs `nan_pat_` + * tokens). + */ +const here = dirname(fileURLToPath(import.meta.url)); +const src = resolve(here, '../..'); +const read = (p: string) => readFileSync(resolve(src, p), 'utf-8'); + +const pages = { + en: { dir: 'content/docs/', prefix: '', group: 'Guides', title: 'Agent runs' }, + es: { dir: 'content/docs-es/', prefix: '/es', group: 'Guías', title: 'Runs de agentes' }, +} as const; + +/** Workspace size -> runs at once. */ +const CONCURRENCY: [string, number][] = [ + ['Micro', 1], + ['Nano', 2], + ['Basic', 3], + ['Medium', 5], + ['Large', 8], +]; + +const EXIT_CODES = ['0', '1', '2', '3', '4', '64', '65', '69', '75']; + +const INTERNAL = /firecracker|micro-?vms?\b|\bvm\b|\bkvm\b|kubernetes|\bk8s\b|\br2\b|nan-vmd|systemd|litellm|cgroup/i; + +const strip = (raw: string) => raw.replace(/^---[\s\S]*?\n---\n/, ''); + +/** + * Heading ids as the site renders them. Astro slugs headings with + * github-slugger: lowercase, punctuation dropped, accents kept, spaces to + * hyphens. The build output was checked against this for both pages. + */ +const slug = (h: string) => + h + .toLowerCase() + .replace(/[^\p{L}\p{N}\s_-]/gu, '') + .replace(/ /g, '-'); + +function headingIds(body: string): Set { + const ids = new Set(); + let inFence = false; + for (const line of body.split('\n')) { + if (line.startsWith('```')) inFence = !inFence; + const m = !inFence && /^#{1,6}\s+(.*)$/.exec(line); + if (m) ids.add(slug(m[1])); + } + return ids; +} + +describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => { + const raw = read(page.dir + 'runs.mdx'); + const fm = parseFrontmatter(raw).frontmatter as { order?: number; group?: string; title?: string }; + const body = strip(raw); + const flat = body.replace(/\s+/g, ' '); + + test('sits in the guides group, right after Workspaces', () => { + expect(fm.title).toBe(page.title); + expect(fm.group).toBe(page.group); + const ws = parseFrontmatter(read(page.dir + 'workspaces.mdx')).frontmatter as { order: number }; + expect(fm.order).toBe(ws.order + 1); + }); + + test('names both agents and the three ways in', () => { + expect(flat).toContain('Pi'); + expect(flat).toContain('Hermes'); + expect(body).toContain('**Runs**'); + expect(body).toContain('**New run**'); + expect(body).toContain('nan run'); + expect(body).toContain('https://api.nan.builders/v1/runs'); + }); + + test('describes the portal form with its limits', () => { + expect(flat).toContain('32 KiB'); + expect(body).toContain('`/home/nan`'); + expect(flat).toMatch(/15, 30, 60 (or|o) 120/); + for (const tab of ['Overview', 'Console', 'SSH keys', 'Events', 'Backups']) expect(body).toContain(`**${tab}**`); + }); + + test('explains the worktree rules: branch name, never pushes, no-change branch deleted', () => { + expect(body).toContain('`nan-run/6f1c2a9b`'); + expect(flat).toMatch(/8 (characters|primeros caracteres)/); + expect(flat).toMatch(/\*\*(never pushes|nunca hace push)\*\*/); + expect(flat).toMatch(/(the branch is deleted|la rama se borra)/); + expect(flat).toMatch(/(fails with a configuration error|falla con un error de configuración)/); + }); + + test('lists every run state', () => { + for (const s of ['queued', 'starting', 'running', 'succeeded', 'failed', 'cancelled', 'timed_out']) { + expect(body).toContain(`\`${s}\``); + } + expect(flat).toMatch(/60 (seconds|segundos)/); + expect(flat).toMatch(/2 (hours|horas)/); + }); + + test('publishes the concurrency per size and the per-member caps', () => { + for (const [size, n] of CONCURRENCY) { + expect(body, size).toMatch(new RegExp(`^\\| ${size}\\b[^|]*\\| ${n} \\|$`, 'm')); + } + expect(flat).toMatch(/\*\*5 runs/); + expect(flat).toMatch(/\*\*8 (on|con) Premium\*\*/); + expect(flat).toMatch(/\*\*20 runs (per|por) workspace\*\*/); + expect(flat).toMatch(/\*\*50 (in total|en total)\*\*/); + expect(flat).toMatch(/\*\*60 runs (per hour|por hora)\*\*/); + }); + + test('says runs need inference, count as usage and mask secrets', () => { + expect(body).toContain('tier_restricted'); + expect(body).toContain('`***`'); + }); + + test('documents the CLI: version, flags, Ctrl-C and every exit code', () => { + expect(body).toContain('0.1.24'); + expect(body).toContain('curl -fsSL https://nan.builders/install | bash'); + expect(body).toContain('nan auth login'); + for (const flag of ['--ws', '--agent', '--model', '--cwd', '--worktree', '--no-worktree', '--timeout', '--detach', '--json', '-f']) { + expect(body, flag).toContain(`\`${flag}`); + } + for (const cmd of ['nan runs ls', 'nan runs show', 'nan runs logs -f', 'nan runs cancel', 'nan runs logs --json']) { + expect(body, cmd).toContain(cmd); + } + for (const code of EXIT_CODES) expect(body, code).toMatch(new RegExp(`^\\| ${code} \\|`, 'm')); + expect(flat).toMatch(/2 (seconds|segundos)/); + }); + + test('documents the API with working curl examples: create, follow, cancel', () => { + expect(body).toContain('-H "Idempotency-Key:'); + expect(body).toContain('curl -N https://api.nan.builders/v1/runs/$RUN_ID/events'); + expect(body).toContain('-H "Accept: text/event-stream"'); + expect(body).toContain('curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel'); + for (const field of ['workspace', 'agent', 'prompt', 'cwd', 'git_isolation', 'timeout_seconds', 'model']) { + expect(body, field).toContain(`| \`${field}\` |`); + } + for (const frame of ['event: run_event', 'event: state', 'event: end']) expect(body).toContain(frame); + expect(body).toContain('Last-Event-ID'); + expect(body).toContain('next_cursor'); + expect(body).toContain('"error": {"message"'); + }); + + test('the create example is valid JSON with the documented fields', () => { + const json = /-d '(\{[\s\S]*?\})'/.exec(body)![1]; + const req = JSON.parse(json); + expect(Object.keys(req).sort()).toEqual(['agent', 'cwd', 'git_isolation', 'prompt', 'timeout_seconds', 'workspace']); + expect(req.timeout_seconds).toBeGreaterThanOrEqual(60); + expect(req.timeout_seconds).toBeLessThanOrEqual(7200); + }); + + test('explains API keys vs tokens', () => { + expect(body).toContain('`nan_pat_...`'); + expect(body).toContain('**Settings → Tokens**'); + expect(flat).toMatch(/\*\*(No, never|No, nunca)\*\*/); + expect(flat).toMatch(/\*\*10\*\*/); + expect(flat).toMatch(/\*\*30, 90 (or|o) 365/); + expect(body).toContain('workspace_key_not_allowed'); + }); + + test('never shows a real-looking secret', () => { + expect(body).not.toMatch(/sk-[A-Za-z0-9]{8,}/); + expect(body).not.toMatch(/nan_pat_[A-Za-z0-9]{4,}/); + }); + + test('keeps infrastructure words out of member copy', () => { + expect(body).not.toMatch(INTERNAL); + }); + + test('in-page anchors point at headings that exist', () => { + const ids = headingIds(body); + const anchors = [...body.matchAll(/\]\(#([^)]+)\)/g)].map((m) => m[1]); + expect(anchors.length).toBeGreaterThan(0); + for (const a of anchors) expect(ids.has(a), a).toBe(true); + }); + + test('links only to docs pages that exist in the same locale', () => { + const slugs = [...body.matchAll(new RegExp(`\\(${page.prefix}/docs/([\\w-]+)\\)`, 'g'))].map((m) => m[1]); + expect(slugs).toEqual(expect.arrayContaining(['workspaces', 'nan-cli'])); + for (const slug of slugs) { + expect(existsSync(resolve(src, page.dir, `${slug}.md`)) || existsSync(resolve(src, page.dir, `${slug}.mdx`)), slug).toBe(true); + } + }); + + test('marks where the screenshots go, without shipping broken images', () => { + expect(body).not.toContain(' { + const text = await mdxToText(body); + expect(text).toContain(page.title); + expect(text).not.toContain('SCREENSHOT'); + }); + + test('no em-dashes in the copy', () => { + expect(body).not.toContain('—'); + }); + + test('Workspaces, the CLI guide and the introduction link to it', () => { + const link = `(${page.prefix}/docs/runs)`; + for (const f of ['workspaces.mdx', 'nan-cli.mdx', 'intro.md']) expect(read(page.dir + f), f).toContain(link); + }); +}); + +describe('agent runs guide, both locales', () => { + test('the Spanish page is a translation with the same structure', () => { + const [en, es] = [pages.en, pages.es].map((p) => strip(read(p.dir + 'runs.mdx'))); + const shape = (b: string) => (b.match(/^#{1,3} /gm) ?? []).join(''); + expect(shape(es)).toEqual(shape(en)); + expect(es).not.toEqual(en); + const count = (b: string, re: RegExp) => (b.match(re) ?? []).length; + expect(count(es, /```bash/g)).toBe(count(en, /```bash/g)); + expect(count(es, /
Date: Sat, 10 Oct 2026 21:27:18 +0200 Subject: [PATCH 4/9] docs(runs): address QA and UX review Tokens can still start runs that spend the plan, so they are described as sensitive as an API key. The workspace-key callout notes the exception of handing an agent your own credential. Adds the 30-minute stream cap, --idempotency-key, and the 400/404/idempotency_conflict errors; renames two headings, links the review step to Connect, and polishes the Spanish. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/runs.mdx | 40 ++++++++++++++++-------------- src/content/docs-es/workspaces.mdx | 2 +- src/content/docs/runs.mdx | 20 +++++++++------ src/tests/layouts/runsDocs.test.ts | 4 ++- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 3c0abdc..10d78bf 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -11,7 +11,7 @@ import Callout from '../../components/docs/Callout.astro'; # Runs de agentes. -Un **run** le da una tarea a un agente dentro de uno de tus [workspaces](/es/docs/workspaces) y le deja trabajar solo. Escribes lo que quieres que haga, eliges el agente (Pi o Hermes, el que tengas instalado en ese workspace) y la carpeta, y el run se encarga del resto. Sigue adelante aunque cierres la página o la terminal. +Un **run** le da una tarea a un agente dentro de uno de tus [workspaces](/es/docs/workspaces) y lo deja trabajar solo. Escribes lo que quieres que haga, eliges el agente (Pi o Hermes, el que tengas instalado en ese workspace) y la carpeta, y el run se encarga del resto. Sigue adelante aunque cierres la página o la terminal. Mientras trabaja, sigues su log en directo. Cuando termina tienes su resultado y, si la carpeta es un repositorio git, una rama con sus cambios lista para que la revises. @@ -24,7 +24,7 @@ Puedes lanzar un run de tres formas, y las tres ven los mismos runs: ## Antes de empezar - **Un workspace encendido**, con Pi o Hermes instalado. Puedes instalar cualquiera de los dos desde el panel de **Software** del workspace. -- **Un plan con inferencia.** Un run usa la clave de inferencia del propio workspace, y lo que gasta el agente cuenta en el uso de tu plan como cualquier otra petición. Si tu plan no incluye inferencia no puedes crear runs: el portal te enseña las opciones para mejorarlo. +- **Un plan con inferencia.** Un run usa la clave de inferencia del propio workspace, y lo que gasta el agente cuenta para el uso de tu plan como cualquier otra petición. Si tu plan no incluye inferencia no puedes crear runs: el portal te enseña las opciones para mejorarlo. ## Lanza un run desde el portal @@ -36,7 +36,7 @@ Entra en [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) {/* SCREENSHOT: pestaña Runs, el historial de runs de un workspace con el botón New run */} -### Rellena el nuevo run +### Rellena el formulario Pulsa **New run** y rellena el formulario: @@ -45,7 +45,7 @@ Pulsa **New run** y rellena el formulario: | Agent | Pi o Hermes, el que tengas instalado en el workspace. | | Prompt | La tarea, con tus palabras, como se la escribirías al agente en un chat. Hasta 32 KiB. | | Working directory | La carpeta en la que trabaja el agente. Por defecto es `/home/nan` y tiene que estar dentro de ella. | -| Worktree | `auto` (por defecto), `on` u `off`. Mira [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| Worktree | `auto` (por defecto), `on` u `off`. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | | Timeout | 15, 30, 60 o 120 minutos. El run se para al llegar a ese límite. | {/* SCREENSHOT: formulario de nuevo run, con agente, prompt, carpeta de trabajo, worktree y timeout */} @@ -75,7 +75,7 @@ Lo que pasa con tus archivos depende de la carpeta que elijas y de la opción ** **Con worktree**, el agente no toca tu checkout. Trabaja en una copia aparte del repositorio, en una rama nueva que se llama `nan-run/` seguido de los 8 primeros caracteres del id del run, por ejemplo `nan-run/6f1c2a9b`. Cuando el agente acaba, todo lo que ha cambiado queda en un commit en esa rama. El run **nunca hace push**: la rama se queda en el repositorio dentro de tu workspace y tú decides qué hacer con ella. Si el agente no ha cambiado nada, la rama se borra. -Para revisar los cambios e incorporarlos, desde el repositorio dentro de tu workspace: +Para revisar los cambios e incorporarlos, abre una terminal en tu workspace ([por SSH o desde la pestaña Console](/es/docs/workspaces#conectarte)) y, desde el repositorio: ```bash git log -p HEAD..nan-run/6f1c2a9b @@ -84,13 +84,13 @@ git merge nan-run/6f1c2a9b **Sin worktree**, el agente trabaja directamente en la carpeta y no hay rama. Úsalo con carpetas que no son repositorios, o cuando quieras los cambios ahí mismo. -La carpeta de trabajo tiene que estar dentro de `/home/nan`, tu home en el workspace. +La carpeta de trabajo tiene que estar dentro de `/home/nan`, tu carpeta personal en el workspace. -## Cómo termina un run +## Estados de un run | Estado | Qué significa | |---|---| -| `queued` | Esperando un hueco libre. Mira [Cuántos runs a la vez](#cuántos-runs-a-la-vez). | +| `queued` | Esperando un hueco libre. Consulta [Cuántos runs a la vez](#cuántos-runs-a-la-vez). | | `starting` | Se está preparando en el workspace. | | `running` | El agente está trabajando. | | `succeeded` | El agente ha terminado su tarea. | @@ -120,9 +120,9 @@ Cada run trabaja en su propio proceso dentro del workspace, con su parte de CPU ## Inferencia y secretos -Los runs usan la clave de inferencia del workspace, la misma que usan sus agentes cuando trabajas con ellos a mano. Sus peticiones cuentan en el uso de tu plan como cualquier otra. +Los runs usan la clave de inferencia del workspace, la misma que usan sus agentes cuando trabajas con ellos a mano. Sus peticiones cuentan para el uso de tu plan como cualquier otra. -Los valores del archivo de entorno de tu workspace, como esa clave de inferencia o un token de bot de Telegram, aparecen como `***` en los logs y resultados de los runs. Puedes compartir un log sin filtrarlos. +Los secretos del archivo de entorno de tu workspace, como esa clave de inferencia o un token de bot de Telegram, aparecen como `***` en los logs y resultados de los runs, así que un log no los filtra. ## Desde la terminal: `nan run` @@ -174,13 +174,14 @@ nan run --detach "actualiza las dependencias" | `--agent pi\|hermes` | `pi` | El agente. | | `--model M` | el del agente | El modelo que usa el agente. | | `--cwd PATH` | `/home/nan` | La carpeta de trabajo en el workspace. | -| `--worktree` / `--no-worktree` | auto | Fuerza el worktree a on u off. Mira [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| `--worktree` / `--no-worktree` | auto | Fuerza el worktree a on u off. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | | `--timeout D` | `30m` | Cuándo pararlo, de `1m` a `2h`. | | `--detach` | | Lanza el run, imprime su id y vuelve. | +| `--idempotency-key K` | | Si un reintento manda la misma clave y la misma petición, recibes el run ya creado en lugar de uno nuevo. | | `--json` | | Imprime los eventos como líneas JSON y después el run terminado. | | `-f FILE` | | Lee el prompt de un archivo, o de stdin con `-`. | -Mientras se muestra el log, **Ctrl-C una vez** te desengancha: el run sigue y la CLI te dice cómo retomarlo. **Ctrl-C dos veces en menos de 2 segundos** lo cancela. +Mientras se muestra el log, **Ctrl-C una vez** te desconecta del log: el run sigue y la CLI te dice cómo retomarlo. **Ctrl-C dos veces en menos de 2 segundos** lo cancela. ### Gestiona tus runs @@ -225,12 +226,12 @@ Hay dos tipos de credenciales que sirven para la API de runs. Las dos están en | | **API Keys** | **Tokens** | |---|---|---| -| Aspecto | `sk-...` | `nan_pat_...` | +| Formato | `sk-...` | `nan_pat_...` | | Inferencia (modelos) | Sí | **No, nunca** | | API de la plataforma (runs, tus workspaces) | Sí | Sí | | Quién puede crearlas | Planes con inferencia | Cualquier miembro con una suscripción activa | -Un **token** es lo que conviene darle a un script que solo gestiona runs: no sirve para llamar a los modelos, así que robarlo no da acceso a la inferencia. Créalo en **Settings → Tokens**: +Un **token** es lo que conviene darle a un script que solo gestiona runs. No sirve para llamar a los modelos directamente, pero sí para lanzar runs que gastan tu plan: guárdalo igual de bien que una API key y revócalo si se filtra. Créalo en **Settings → Tokens**: - Se muestra **una sola vez**, al crearlo. Cópialo en ese momento. - Puedes tener hasta **10** tokens activos. @@ -240,7 +241,7 @@ Un **token** es lo que conviene darle a un script que solo gestiona runs: no sir Para crear runs sigue haciendo falta un plan con inferencia, uses la credencial que uses. -La clave que hay dentro de un workspace, la que usan sus agentes para la inferencia, no puede gestionar runs: la API le responde con un `403`. Un agente que trabaja en tu workspace no puede lanzar runs nuevos por su cuenta. +La clave que hay dentro de un workspace, la que usan sus agentes para la inferencia, no puede gestionar runs: la API le responde con un `403`. Un agente que trabaja en tu workspace no puede lanzar runs nuevos por su cuenta, salvo que le des una de tus propias API keys o tokens. ### Crea un run @@ -284,13 +285,13 @@ curl -N https://api.nan.builders/v1/runs/$RUN_ID/events \ -H "Accept: text/event-stream" ``` -Cada frame es uno de estos: +Cada evento del stream es uno de estos: - `event: run_event`, con una línea `id:`: un evento del log (un mensaje, una llamada a una herramienta, su resultado...). Manda el último `id` que hayas visto en la cabecera `Last-Event-ID` para retomar tras un corte, sin perder ni repetir nada. - `event: state`: el run ha cambiado de estado. - `event: end`: el run ha terminado y su log se ha enviado entero. Los datos son el run terminado. El stream se cierra después. -Cada 15 segundos el stream manda una línea de comentario para mantener la conexión abierta. Sin la cabecera `Accept`, el mismo endpoint responde con páginas de JSON: pasa el `next_after` de cada página como `?after=` para pedir la siguiente. +Cada 15 segundos el stream manda una línea de comentario para mantener la conexión abierta. Un stream dura como mucho 30 minutos: si se cierra antes del `end`, vuelve a conectarte con `Last-Event-ID` y sigue donde lo dejaste. Sin la cabecera `Accept`, el mismo endpoint responde con páginas de JSON: pasa el `next_after` de cada página como `?after=` para pedir la siguiente. Para ver cómo ha terminado: @@ -325,8 +326,11 @@ Los errores usan el mismo formato que la API de inferencia: | Estado y código | Qué hacer | |---|---| +| `400 invalid_request` | Hay un campo incorrecto. `param` te dice cuál. | | `403 tier_restricted` | Tu plan no incluye inferencia. Mejóralo para crear runs. | | `403 workspace_key_not_allowed` | Has usado la clave de dentro de un workspace. Usa una API key o un token. | +| `404` | El workspace o el run no existe, o no es tuyo. | +| `409 idempotency_conflict` | Has repetido una `Idempotency-Key` con otro cuerpo. Usa una clave nueva. | | `409 workspace_not_running` | Enciende primero el workspace. | | `409 agent_not_installed` | Instala el agente desde el panel de **Software**, o elige el otro. | | `409 no_inference_key` | El workspace no tiene clave de inferencia. Comprueba que tu plan incluye inferencia. | @@ -337,7 +341,7 @@ Los errores usan el mismo formato que la API de inferencia:
-No. Con worktree, los cambios se quedan en una rama `nan-run/...` del repositorio dentro de tu workspace. Hacer push, merge o tirarlos es cosa tuya. +No. Con worktree, los cambios se quedan en una rama `nan-run/...` del repositorio dentro de tu workspace. Hacer push, merge o descartarlos es cosa tuya.
diff --git a/src/content/docs-es/workspaces.mdx b/src/content/docs-es/workspaces.mdx index 3e7fc91..3a9494e 100644 --- a/src/content/docs-es/workspaces.mdx +++ b/src/content/docs-es/workspaces.mdx @@ -217,7 +217,7 @@ Con un plan de inferencia, los agentes vienen configurados para usar los modelos ## Pon a tus agentes a trabajar en tareas -No hace falta que te quedes conectado mientras un agente trabaja. Un **run** le da una tarea a Pi o a Hermes dentro de tu workspace y le deja trabajar solo: sigues su log en directo, continúa aunque cierres la página y, en un repositorio git, sus cambios acaban en una rama para que los revises. Lánzalo desde la pestaña **Runs** del workspace, desde tu terminal con `nan run` o desde la API. +No hace falta que te quedes conectado mientras un agente trabaja. Un **run** le da una tarea a Pi o a Hermes dentro de tu workspace y lo deja trabajar solo: sigues su log en directo, continúa aunque cierres la página y, en un repositorio git, sus cambios acaban en una rama para que los revises. Lánzalo desde la pestaña **Runs** del workspace, desde tu terminal con `nan run` o desde la API. Cómo funciona, los límites y los comandos están en [Runs de agentes](/es/docs/runs). diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index d261091..a900ccb 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -36,7 +36,7 @@ Go to [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) and {/* SCREENSHOT: runs tab, the run history of a workspace with the New run button */} -### Fill in the new run +### Fill in the form Press **New run** and fill in the form: @@ -75,7 +75,7 @@ What happens to your files depends on the folder you choose and on the **worktre **With a worktree**, the agent does not touch your checkout. It works in a separate copy of the repository, on a new branch called `nan-run/` followed by the first 8 characters of the run id, for example `nan-run/6f1c2a9b`. When the agent ends, whatever it changed is committed on that branch. The run **never pushes**: the branch stays in the repository inside your workspace, and you decide what to do with it. If the agent changed nothing, the branch is deleted. -To review the changes and bring them in, from the repository inside your workspace: +To review the changes and bring them in, open a terminal in your workspace ([over SSH or the Console tab](/docs/workspaces#connect)) and, from the repository: ```bash git log -p HEAD..nan-run/6f1c2a9b @@ -86,7 +86,7 @@ git merge nan-run/6f1c2a9b The working directory must be inside `/home/nan`, your home in the workspace. -## How a run ends +## Run states | State | What it means | |---|---| @@ -122,7 +122,7 @@ Each run works in its own process inside the workspace, with its own share of CP Runs use the inference key of the workspace, the same one its agents use when you work with them by hand. Their requests count against your plan's usage like any other. -The values in your workspace's environment file, such as that inference key or a Telegram bot token, are shown as `***` in run logs and results. You can share a log without leaking them. +Secrets in your workspace's environment file, such as that inference key or a Telegram bot token, are shown as `***` in run logs and results, so a log does not leak them. ## From the terminal: `nan run` @@ -177,10 +177,11 @@ nan run --detach "upgrade the dependencies" | `--worktree` / `--no-worktree` | auto | Force a worktree on or off. See [Where the agent works](#where-the-agent-works). | | `--timeout D` | `30m` | When to stop it, from `1m` to `2h`. | | `--detach` | | Start the run, print its id and return. | +| `--idempotency-key K` | | If a retry sends the same key and request, you get the run already created instead of a second one. | | `--json` | | Print events as JSON lines, then the finished run. | | `-f FILE` | | Read the prompt from a file, or from stdin with `-`. | -While it streams, **Ctrl-C once** detaches: the run keeps going and the CLI tells you how to pick it up again. **Ctrl-C twice within 2 seconds** cancels it. +While it streams, **Ctrl-C once** stops following the log: the run keeps going and the CLI tells you how to pick it up again. **Ctrl-C twice within 2 seconds** cancels it. ### Manage your runs @@ -230,7 +231,7 @@ Two kinds of credentials work with the runs API. Both are in your [settings on t | Platform API (runs, your workspaces) | Yes | Yes | | Who can create them | Plans with inference | Any member with an active subscription | -A **token** is the one to give a script that only manages runs: it cannot be used to call the models, so it is not worth stealing for that. Create it under **Settings → Tokens**: +A **token** is the one to give a script that only manages runs. It cannot call the models directly, but it can start runs that spend your plan, so keep it as private as an API key and revoke it if it leaks. Create it under **Settings → Tokens**: - It is shown **once**, when you create it. Copy it then. - You can have up to **10** active tokens. @@ -240,7 +241,7 @@ A **token** is the one to give a script that only manages runs: it cannot be use Creating runs still needs a plan with inference, whatever credential you use. -The key inside a workspace, the one its agents use for inference, cannot manage runs: the API answers it with `403`. An agent working in your workspace cannot start new runs on its own. +The key inside a workspace, the one its agents use for inference, cannot manage runs: the API answers it with `403`. An agent working in your workspace cannot start new runs on its own, unless you give it one of your own API keys or tokens. ### Create a run @@ -290,7 +291,7 @@ Each frame is one of these: - `event: state`: the run changed state. - `event: end`: the run is over and its log has been sent in full. The data is the finished run. The stream closes after it. -Every 15 seconds the stream sends a comment line to keep the connection open. Without the `Accept` header, the same endpoint answers pages of JSON instead: pass the `next_after` of each page as `?after=` to get the next one. +Every 15 seconds the stream sends a comment line to keep the connection open. A stream lasts 30 minutes at most: if it closes before the `end`, reconnect with `Last-Event-ID` and carry on. Without the `Accept` header, the same endpoint answers pages of JSON instead: pass the `next_after` of each page as `?after=` to get the next one. To see how it ended: @@ -325,8 +326,11 @@ Errors use the same format as the inference API: | Status and code | What to do | |---|---| +| `400 invalid_request` | A field is wrong. `param` says which one. | | `403 tier_restricted` | Your plan does not include inference. Upgrade it to create runs. | | `403 workspace_key_not_allowed` | You used the key from inside a workspace. Use an API key or a token. | +| `404` | The workspace or the run does not exist, or is not yours. | +| `409 idempotency_conflict` | You reused an `Idempotency-Key` with a different body. Use a new key. | | `409 workspace_not_running` | Start the workspace first. | | `409 agent_not_installed` | Install the agent from the **Software** panel, or pick the other one. | | `409 no_inference_key` | The workspace has no inference key. Check that your plan includes inference. | diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index acee2d6..88e0c91 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -128,7 +128,7 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(body).toContain('0.1.24'); expect(body).toContain('curl -fsSL https://nan.builders/install | bash'); expect(body).toContain('nan auth login'); - for (const flag of ['--ws', '--agent', '--model', '--cwd', '--worktree', '--no-worktree', '--timeout', '--detach', '--json', '-f']) { + for (const flag of ['--ws', '--agent', '--model', '--cwd', '--worktree', '--no-worktree', '--timeout', '--detach', '--json', '--idempotency-key', '-f']) { expect(body, flag).toContain(`\`${flag}`); } for (const cmd of ['nan runs ls', 'nan runs show', 'nan runs logs -f', 'nan runs cancel', 'nan runs logs --json']) { @@ -150,6 +150,7 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(body).toContain('Last-Event-ID'); expect(body).toContain('next_cursor'); expect(body).toContain('"error": {"message"'); + for (const code of ['400 invalid_request', '404', '409 idempotency_conflict', '429 run_queue_full', '429 rate_limited']) expect(body, code).toContain(`| \`${code}\` |`); }); test('the create example is valid JSON with the documented fields', () => { @@ -167,6 +168,7 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(flat).toMatch(/\*\*10\*\*/); expect(flat).toMatch(/\*\*30, 90 (or|o) 365/); expect(body).toContain('workspace_key_not_allowed'); + expect(flat).toMatch(/(spend your plan|gastan tu plan)/); }); test('never shows a real-looking secret', () => { From d7c7a04d7c9fed1b5cbc694b6ded4245ae988700 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 21:37:34 +0200 Subject: [PATCH 5/9] docs(runs): add the portal screenshots and use the real UI labels Three captures from the owner (Runs tab, New run form, run page) as WebP under public/docs/runs, metadata stripped, embedded with in both locales in place of the placeholders. The portal copy now uses the labels the UI shows (Task, Folder, Separate branch, Time limit, Start run, Cancel run) and the CLI/API sections map them to --cwd/--worktree/--timeout and cwd/git_isolation/timeout_seconds. Co-Authored-By: Claude Opus 5.5 --- public/docs/runs/runs-01-tab.webp | Bin 0 -> 37468 bytes public/docs/runs/runs-02-new-run.webp | Bin 0 -> 40958 bytes public/docs/runs/runs-03-run-page.webp | Bin 0 -> 16878 bytes src/content/docs-es/runs.mdx | 71 ++++++++++++---------- src/content/docs/runs.mdx | 67 +++++++++++--------- src/tests/layouts/runsDocs.test.ts | 81 +++++++++++++++++++++++-- 6 files changed, 154 insertions(+), 65 deletions(-) create mode 100644 public/docs/runs/runs-01-tab.webp create mode 100644 public/docs/runs/runs-02-new-run.webp create mode 100644 public/docs/runs/runs-03-run-page.webp diff --git a/public/docs/runs/runs-01-tab.webp b/public/docs/runs/runs-01-tab.webp new file mode 100644 index 0000000000000000000000000000000000000000..e2e79a8da7d8450b3c4cc441740989b60c9c0f6d GIT binary patch literal 37468 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�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 iCr>cH# literal 0 HcmV?d00001 diff --git a/public/docs/runs/runs-03-run-page.webp b/public/docs/runs/runs-03-run-page.webp new file mode 100644 index 0000000000000000000000000000000000000000..a0e53d6340dcaa3a5a8fbb4f87d602b7f72b4e5d GIT binary patch literal 16878 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 qA?);GR~1lz@Js+-#|lJ&@H6JLPpQXDE ### Rellena el formulario -Pulsa **New run** y rellena el formulario: +Pulsa **New run** y rellena el formulario (el portal está en inglés): | Campo | Qué es | |---|---| -| Agent | Pi o Hermes, el que tengas instalado en el workspace. | -| Prompt | La tarea, con tus palabras, como se la escribirías al agente en un chat. Hasta 32 KiB. | -| Working directory | La carpeta en la que trabaja el agente. Por defecto es `/home/nan` y tiene que estar dentro de ella. | -| Worktree | `auto` (por defecto), `on` u `off`. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | -| Timeout | 15, 30, 60 o 120 minutos. El run se para al llegar a ese límite. | +| **Agent** | El agente: Pi o Hermes, el que tengas instalado en el workspace. | +| **Task** | La tarea, con tus palabras, como se la escribirías al agente en un chat. Hasta 32 KiB (el contador bajo la caja te dice los bytes que llevas). | +| **Folder** | La carpeta donde trabaja el agente. Por defecto es `/home/nan` y tiene que estar dentro de ella. | +| **Separate branch** | Rama aparte: **Auto** (por defecto), **On** u **Off**. En un repositorio git, el agente trabaja en su propia rama y tu checkout se queda como está. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| **Time limit** | El tiempo máximo: 15, 30, 60 o 120 minutos. El run se para al llegar a él. | -{/* SCREENSHOT: formulario de nuevo run, con agente, prompt, carpeta de trabajo, worktree y timeout */} +Pulsa **Start run**, o `Ctrl+Enter` (`⌘+Enter` en un Mac) desde la caja de la tarea. + + ### Síguelo en directo -Haz clic en un run de la lista para abrir su página. El log se va llenando mientras el agente trabaja: lo que dice, las herramientas que usa y lo que le responden. Cuando el run termina, la página muestra cómo ha acabado, el resumen del agente y, si la hay, la rama con sus cambios. +Haz clic en un run de la lista para abrir su página. Arriba ves su estado, el workspace, el agente, la carpeta, cuánto lleva, los tokens que ha gastado, su tiempo máximo y la tarea. Debajo, en **Output**, el log se va llenando en directo mientras el agente trabaja: lo que dice, las herramientas que usa y lo que le responden. Haz clic en una línea **Tool call** o **Tool result** para desplegarla. Cuando el run termina, la página muestra cómo ha acabado, el resumen del agente y, si la hay, la rama con sus cambios. Puedes cerrar la página cuando quieras. El run sigue, y su página estará ahí cuando vuelvas. -{/* SCREENSHOT: página de un run con el log en directo, el estado y el botón Cancel */} + ### Cancélalo, si hace falta -**Cancel** para el run en unos segundos. El portal te pide confirmación antes. +**Cancel run** para el run en unos segundos. El portal te pide confirmación antes. ## Dónde trabaja el agente -Lo que pasa con tus archivos depende de la carpeta que elijas y de la opción **worktree**. +Lo que pasa con tus archivos depende de la carpeta (**Folder**) que elijas y de la opción **Separate branch**. -| La carpeta... | Worktree `auto` (por defecto) | Worktree `on` | Worktree `off` | +| La carpeta... | **Auto** (por defecto) | **On** | **Off** | |---|---|---|---| -| está dentro de un repositorio git | Worktree y rama aparte | Worktree y rama aparte | Directamente en la carpeta | +| está dentro de un repositorio git | Rama aparte | Rama aparte | Directamente en la carpeta | | no es un repositorio git | Directamente en la carpeta | El run falla con un error de configuración | Directamente en la carpeta | -**Con worktree**, el agente no toca tu checkout. Trabaja en una copia aparte del repositorio, en una rama nueva que se llama `nan-run/` seguido de los 8 primeros caracteres del id del run, por ejemplo `nan-run/6f1c2a9b`. Cuando el agente acaba, todo lo que ha cambiado queda en un commit en esa rama. El run **nunca hace push**: la rama se queda en el repositorio dentro de tu workspace y tú decides qué hacer con ella. Si el agente no ha cambiado nada, la rama se borra. +**Con rama aparte**, el agente no toca tu checkout. Trabaja en una copia aparte del repositorio (un git worktree), en una rama nueva que se llama `nan-run/` seguido de los 8 primeros caracteres del id del run, por ejemplo `nan-run/6f1c2a9b`. Cuando el agente acaba, todo lo que ha cambiado queda en un commit en esa rama. El run **nunca hace push**: la rama se queda en el repositorio dentro de tu workspace y tú decides qué hacer con ella. Si el agente no ha cambiado nada, la rama se borra. Para revisar los cambios e incorporarlos, abre una terminal en tu workspace ([por SSH o desde la pestaña Console](/es/docs/workspaces#conectarte)) y, desde el repositorio: @@ -82,9 +85,9 @@ git log -p HEAD..nan-run/6f1c2a9b git merge nan-run/6f1c2a9b ``` -**Sin worktree**, el agente trabaja directamente en la carpeta y no hay rama. Úsalo con carpetas que no son repositorios, o cuando quieras los cambios ahí mismo. +**Sin rama aparte**, el agente trabaja directamente en la carpeta y no hay rama. Úsalo con carpetas que no son repositorios, o cuando quieras los cambios ahí mismo. -La carpeta de trabajo tiene que estar dentro de `/home/nan`, tu carpeta personal en el workspace. +La carpeta tiene que estar dentro de `/home/nan`, tu carpeta personal en el workspace. ## Estados de un run @@ -94,11 +97,13 @@ La carpeta de trabajo tiene que estar dentro de `/home/nan`, tu carpeta personal | `starting` | Se está preparando en el workspace. | | `running` | El agente está trabajando. | | `succeeded` | El agente ha terminado su tarea. | -| `failed` | El agente se ha parado con un error, o el run no ha podido empezar (por ejemplo, worktree `on` en una carpeta que no es un repositorio). | +| `failed` | El agente se ha parado con un error, o el run no ha podido empezar (por ejemplo, **Separate branch** en **On** en una carpeta que no es un repositorio). | | `cancelled` | Lo has cancelado tú. | -| `timed_out` | Ha llegado a su timeout y se ha parado. | +| `timed_out` | Ha llegado a su tiempo máximo y se ha parado. | + +El portal muestra los mismos estados en inglés y con mayúscula, por ejemplo **Running**, **Succeeded** o **Cancelled**. -Desde la CLI y la API el timeout va de 60 segundos a 2 horas. El portal ofrece 15, 30, 60 y 120 minutos. +Desde la CLI y la API el tiempo máximo va de 60 segundos a 2 horas. El portal ofrece 15, 30, 60 y 120 minutos. ## Cuántos runs a la vez @@ -160,7 +165,7 @@ Otras formas de lanzarlo: # Un workspace, un agente y una carpeta concretos nan run --ws develop --agent hermes --cwd /home/nan/projects/api "revisa los TODO pendientes" -# El prompt desde un archivo, o desde stdin +# La tarea desde un archivo, o desde stdin nan run -f tarea.md git diff | nan run - @@ -173,13 +178,15 @@ nan run --detach "actualiza las dependencias" | `--ws NAME` | tu único workspace | El workspace donde se ejecuta. Si tienes más de uno y no lo pones, la CLI te los lista y sale con código 64. | | `--agent pi\|hermes` | `pi` | El agente. | | `--model M` | el del agente | El modelo que usa el agente. | -| `--cwd PATH` | `/home/nan` | La carpeta de trabajo en el workspace. | -| `--worktree` / `--no-worktree` | auto | Fuerza el worktree a on u off. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | -| `--timeout D` | `30m` | Cuándo pararlo, de `1m` a `2h`. | +| `--cwd PATH` | `/home/nan` | La carpeta en el workspace. | +| `--worktree` / `--no-worktree` | auto | Fuerza la rama aparte a on u off. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | +| `--timeout D` | `30m` | El tiempo máximo, de `1m` a `2h`. | | `--detach` | | Lanza el run, imprime su id y vuelve. | | `--idempotency-key K` | | Si un reintento manda la misma clave y la misma petición, recibes el run ya creado en lugar de uno nuevo. | | `--json` | | Imprime los eventos como líneas JSON y después el run terminado. | -| `-f FILE` | | Lee el prompt de un archivo, o de stdin con `-`. | +| `-f FILE` | | Lee la tarea de un archivo, o de stdin con `-`. | + +Los flags equivalen al formulario del portal: **Folder** es `--cwd`, **Separate branch** es `--worktree` / `--no-worktree` y **Time limit** es `--timeout`. La tarea es el texto entre comillas. Mientras se muestra el log, **Ctrl-C una vez** te desconecta del log: el run sigue y la CLI te dice cómo retomarlo. **Ctrl-C dos veces en menos de 2 segundos** lo cancela. @@ -202,7 +209,7 @@ nan runs cancel |---|---| | 0 | terminado con éxito | | 1 | fallido | -| 2 | timeout | +| 2 | se agotó el tiempo máximo | | 3 | cancelado | | 4 | el workspace no está listo: agente no instalado, sin clave de inferencia o configuración incorrecta | | 64 | error de uso, también tener más de un workspace y no pasar `--ws` | @@ -268,9 +275,11 @@ curl https://api.nan.builders/v1/runs \ | `workspace` | Sí | Nombre o id del workspace. | | `agent` | Sí | `pi` o `hermes`. | | `prompt` | Sí | La tarea, hasta 32 KiB. | -| `cwd` | No | Carpeta de trabajo, `/home/nan` por defecto. | -| `git_isolation` | No | `null` para auto (por defecto), `true` para worktree on, `false` para off. | -| `timeout_seconds` | No | De 60 a 7200. 1800 (30 minutos) por defecto. | +| `cwd` | No | La carpeta, `/home/nan` por defecto. | +| `git_isolation` | No | Rama aparte: `null` para auto (por defecto), `true` para on, `false` para off. | +| `timeout_seconds` | No | El tiempo máximo, de 60 a 7200. 1800 (30 minutos) por defecto. | + +Los campos equivalen al formulario del portal: **Task** es `prompt`, **Folder** es `cwd`, **Separate branch** es `git_isolation` y **Time limit** es `timeout_seconds`. | `model` | No | El modelo del agente. Por defecto, el suyo. | La respuesta es el run, con su `id` y su `state`. La cabecera `Idempotency-Key` es opcional: si un reintento manda la misma clave con el mismo cuerpo, recibes el run que ya se creó en lugar de uno nuevo. @@ -341,7 +350,7 @@ Los errores usan el mismo formato que la API de inferencia:

-No. Con worktree, los cambios se quedan en una rama `nan-run/...` del repositorio dentro de tu workspace. Hacer push, merge o descartarlos es cosa tuya. +No. Con rama aparte, los cambios se quedan en una rama `nan-run/...` del repositorio dentro de tu workspace. Hacer push, merge o descartarlos es cosa tuya.
diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index a900ccb..ffec47d 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -8,6 +8,7 @@ group: Guides import Steps from '../../components/docs/Steps.astro'; import Details from '../../components/docs/Details.astro'; import Callout from '../../components/docs/Callout.astro'; +import Screenshot from '../../components/docs/Screenshot.astro'; # Agent runs. @@ -32,9 +33,9 @@ You can start a run in three ways, and all three see the same runs: ### Open the Runs tab -Go to [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) and open a workspace. Next to **Overview**, **Console**, **SSH keys**, **Events** and **Backups** there is a **Runs** tab with the history of your runs in that workspace, newest first. +Go to [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces) and open a workspace. Next to **Overview**, **Console**, **SSH keys**, **Events** and **Backups** there is a **Runs** tab. Its **Agent runs** card lists the runs of that workspace, newest first, each with its state, the agent, the task, when it started, how long it took and the tokens it used. -{/* SCREENSHOT: runs tab, the run history of a workspace with the New run button */} + ### Fill in the form @@ -42,38 +43,40 @@ Press **New run** and fill in the form: | Field | What it is | |---|---| -| Agent | Pi or Hermes, whichever is installed in the workspace. | -| Prompt | The task, in your own words, as you would write it to the agent in a chat. Up to 32 KiB. | -| Working directory | The folder the agent works in. It defaults to `/home/nan` and must be inside it. | -| Worktree | `auto` (the default), `on` or `off`. See [Where the agent works](#where-the-agent-works). | -| Timeout | 15, 30, 60 or 120 minutes. The run is stopped when it reaches it. | +| **Agent** | Pi or Hermes, whichever is installed in the workspace. | +| **Task** | What you want done, in your own words, as you would write it to the agent in a chat. Up to 32 KiB (the counter under the box shows the bytes used). | +| **Folder** | Where the agent works. It defaults to `/home/nan` and must be inside it. | +| **Separate branch** | **Auto** (the default), **On** or **Off**. In a git repository, the agent works on its own branch and your checkout stays as it is. See [Where the agent works](#where-the-agent-works). | +| **Time limit** | 15, 30, 60 or 120 minutes. The run is stopped when it reaches it. | -{/* SCREENSHOT: new run form, with agent, prompt, working directory, worktree and timeout */} +Press **Start run**, or `Ctrl+Enter` (`⌘+Enter` on a Mac) from the task box. + + ### Follow it live -Click a run in the list to open its page. The log fills in as the agent works: what it says, the tools it calls and what they answer. When the run ends, the page shows how it ended, the agent's summary and, if there is one, the branch with its changes. +Click a run in the list to open its page. At the top you see its state, the workspace, the agent, the folder, how long it has been running, the tokens it has used, its time limit and the task. Below, under **Output**, the log fills in live as the agent works: what it says, the tools it calls and what they answer. Click a **Tool call** or **Tool result** line to unfold it. When the run ends, the page shows how it ended, the agent's summary and, if there is one, the branch with its changes. You can close the page at any moment. The run goes on, and its page is there when you come back. -{/* SCREENSHOT: run page with the live log, the state and the Cancel button */} + ### Cancel it, if you need to -**Cancel** stops the run within a few seconds. The portal asks you to confirm first. +**Cancel run** stops the run within a few seconds. The portal asks you to confirm first. ## Where the agent works -What happens to your files depends on the folder you choose and on the **worktree** setting. +What happens to your files depends on the **Folder** you choose and on the **Separate branch** setting. -| The folder is... | Worktree `auto` (default) | Worktree `on` | Worktree `off` | +| The folder is... | **Auto** (default) | **On** | **Off** | |---|---|---|---| -| inside a git repository | Separate worktree and branch | Separate worktree and branch | Directly in the folder | +| inside a git repository | Separate branch | Separate branch | Directly in the folder | | not a git repository | Directly in the folder | The run fails with a configuration error | Directly in the folder | -**With a worktree**, the agent does not touch your checkout. It works in a separate copy of the repository, on a new branch called `nan-run/` followed by the first 8 characters of the run id, for example `nan-run/6f1c2a9b`. When the agent ends, whatever it changed is committed on that branch. The run **never pushes**: the branch stays in the repository inside your workspace, and you decide what to do with it. If the agent changed nothing, the branch is deleted. +**With a separate branch**, the agent does not touch your checkout. It works in a separate copy of the repository (a git worktree), on a new branch called `nan-run/` followed by the first 8 characters of the run id, for example `nan-run/6f1c2a9b`. When the agent ends, whatever it changed is committed on that branch. The run **never pushes**: the branch stays in the repository inside your workspace, and you decide what to do with it. If the agent changed nothing, the branch is deleted. To review the changes and bring them in, open a terminal in your workspace ([over SSH or the Console tab](/docs/workspaces#connect)) and, from the repository: @@ -82,9 +85,9 @@ git log -p HEAD..nan-run/6f1c2a9b git merge nan-run/6f1c2a9b ``` -**Without a worktree**, the agent works directly in the folder and there is no branch. Use it for folders that are not repositories, or when you want the changes right there. +**Without a separate branch**, the agent works directly in the folder and there is no branch. Use it for folders that are not repositories, or when you want the changes right there. -The working directory must be inside `/home/nan`, your home in the workspace. +The folder must be inside `/home/nan`, your home in the workspace. ## Run states @@ -94,11 +97,13 @@ The working directory must be inside `/home/nan`, your home in the workspace. | `starting` | Being set up in the workspace. | | `running` | The agent is working. | | `succeeded` | The agent finished its task. | -| `failed` | The agent stopped with an error, or the run could not start (for example, worktree `on` in a folder that is not a repository). | +| `failed` | The agent stopped with an error, or the run could not start (for example, **Separate branch** set to **On** in a folder that is not a repository). | | `cancelled` | You cancelled it. | -| `timed_out` | It reached its timeout and was stopped. | +| `timed_out` | It reached its time limit and was stopped. | + +The portal shows the same states with a capital letter, for example **Running**, **Succeeded** or **Cancelled**. -Through the CLI and the API the timeout goes from 60 seconds to 2 hours. The portal offers 15, 30, 60 and 120 minutes. +Through the CLI and the API the time limit goes from 60 seconds to 2 hours. The portal offers 15, 30, 60 and 120 minutes. ## How many runs at once @@ -160,7 +165,7 @@ A few more ways to start one: # A specific workspace, agent and folder nan run --ws develop --agent hermes --cwd /home/nan/projects/api "review the open TODOs" -# The prompt from a file, or from stdin +# The task from a file, or from stdin nan run -f task.md git diff | nan run - @@ -173,13 +178,15 @@ nan run --detach "upgrade the dependencies" | `--ws NAME` | your only workspace | The workspace to run in. If you have more than one and leave it out, the CLI lists them and exits with code 64. | | `--agent pi\|hermes` | `pi` | The agent. | | `--model M` | the agent's own | The model the agent uses. | -| `--cwd PATH` | `/home/nan` | The working directory in the workspace. | -| `--worktree` / `--no-worktree` | auto | Force a worktree on or off. See [Where the agent works](#where-the-agent-works). | -| `--timeout D` | `30m` | When to stop it, from `1m` to `2h`. | +| `--cwd PATH` | `/home/nan` | The folder in the workspace. | +| `--worktree` / `--no-worktree` | auto | Force a separate branch on or off. See [Where the agent works](#where-the-agent-works). | +| `--timeout D` | `30m` | The time limit, from `1m` to `2h`. | | `--detach` | | Start the run, print its id and return. | | `--idempotency-key K` | | If a retry sends the same key and request, you get the run already created instead of a second one. | | `--json` | | Print events as JSON lines, then the finished run. | -| `-f FILE` | | Read the prompt from a file, or from stdin with `-`. | +| `-f FILE` | | Read the task from a file, or from stdin with `-`. | + +The flags match the portal form: **Folder** is `--cwd`, **Separate branch** is `--worktree` / `--no-worktree` and **Time limit** is `--timeout`. The task is the text in quotes. While it streams, **Ctrl-C once** stops following the log: the run keeps going and the CLI tells you how to pick it up again. **Ctrl-C twice within 2 seconds** cancels it. @@ -268,9 +275,11 @@ curl https://api.nan.builders/v1/runs \ | `workspace` | Yes | Name or id of the workspace. | | `agent` | Yes | `pi` or `hermes`. | | `prompt` | Yes | The task, up to 32 KiB. | -| `cwd` | No | Working directory, `/home/nan` by default. | -| `git_isolation` | No | `null` for auto (the default), `true` for worktree on, `false` for off. | -| `timeout_seconds` | No | From 60 to 7200. 1800 (30 minutes) by default. | +| `cwd` | No | The folder, `/home/nan` by default. | +| `git_isolation` | No | Separate branch: `null` for auto (the default), `true` for on, `false` for off. | +| `timeout_seconds` | No | The time limit, from 60 to 7200. 1800 (30 minutes) by default. | + +The fields match the portal form: **Task** is `prompt`, **Folder** is `cwd`, **Separate branch** is `git_isolation` and **Time limit** is `timeout_seconds`. | `model` | No | The model for the agent. Its own by default. | The answer is the run, with its `id` and its `state`. The `Idempotency-Key` header is optional: if a retry sends the same key with the same body, you get the run that was already created instead of a second one. @@ -341,7 +350,7 @@ Errors use the same format as the inference API:
-No. With a worktree, the changes stay on a `nan-run/...` branch in the repository inside your workspace. Pushing, merging or throwing them away is up to you. +No. With a separate branch, the changes stay on a `nan-run/...` branch in the repository inside your workspace. Pushing, merging or throwing them away is up to you.
diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index 88e0c91..9036989 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { parseFrontmatter } from '@astrojs/markdown-remark'; @@ -39,6 +39,43 @@ const EXIT_CODES = ['0', '1', '2', '3', '4', '64', '65', '69', '75']; const INTERNAL = /firecracker|micro-?vms?\b|\bvm\b|\bkvm\b|kubernetes|\bk8s\b|\br2\b|nan-vmd|systemd|litellm|cgroup/i; +const pub = resolve(src, '../public'); +/** The portal captures, in the order the guide shows them. */ +const SHOTS = ['runs-01-tab', 'runs-02-new-run', 'runs-03-run-page']; +/** Things that must never be published: full run or workspace ids (UUIDs) and private addresses. */ +const PRIVATE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}|\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|\bws-[0-9a-f]{8}\b/i; + +interface Shot { + src: string; + alt: string; + caption: string; + width: number; + height: number; +} + +/** Every of a page, in order. */ +function screenshots(body: string): Shot[] { + return [...body.matchAll(/]*?)\/>/g)].map((m) => { + const attr = (name: string) => new RegExp(`${name}="([^"]*)"`).exec(m[1])?.[1] ?? ''; + const num = (name: string) => Number(new RegExp(`${name}=\\{(\\d+)\\}`).exec(m[1])?.[1]); + return { src: attr('src'), alt: attr('alt'), caption: attr('caption'), width: num('width'), height: num('height') }; + }); +} + +/** Pixel size of a WebP (lossy VP8, lossless VP8L or extended VP8X). */ +function webpSize(file: string): { width: number; height: number } { + const b = readFileSync(file); + expect(b.toString('ascii', 0, 4)).toBe('RIFF'); + expect(b.toString('ascii', 8, 12)).toBe('WEBP'); + const chunk = b.toString('ascii', 12, 16); + if (chunk === 'VP8 ') return { width: b.readUInt16LE(26) & 0x3fff, height: b.readUInt16LE(28) & 0x3fff }; + if (chunk === 'VP8L') { + const bits = b.readUInt32LE(21); + return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 }; + } + return { width: b.readUIntLE(24, 3) + 1, height: b.readUIntLE(27, 3) + 1 }; +} + const strip = (raw: string) => raw.replace(/^---[\s\S]*?\n---\n/, ''); /** @@ -85,6 +122,19 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(body).toContain('https://api.nan.builders/v1/runs'); }); + test('uses the labels the portal shows', () => { + for (const label of ['Agent runs', 'New run', 'Agent', 'Task', 'Folder', 'Separate branch', 'Auto', 'On', 'Off', 'Time limit', 'Start run', 'Cancel run', 'Output', 'Tool call', 'Tool result']) { + expect(body, label).toContain(`**${label}**`); + } + // The old names of the form fields are gone from the portal copy. + expect(body).not.toMatch(/\| (Prompt|Working directory|Worktree|Timeout) \|/); + }); + + test('maps the portal labels to the CLI flags and the API fields', () => { + expect(flat).toMatch(/\*\*Folder\*\* (is|es) `--cwd`, \*\*Separate branch\*\* (is|es) `--worktree` \/ `--no-worktree` (and|y) \*\*Time limit\*\* (is|es) `--timeout`/); + expect(flat).toMatch(/\*\*Task\*\* (is|es) `prompt`, \*\*Folder\*\* (is|es) `cwd`, \*\*Separate branch\*\* (is|es) `git_isolation` (and|y) \*\*Time limit\*\* (is|es) `timeout_seconds`/); + }); + test('describes the portal form with its limits', () => { expect(flat).toContain('32 KiB'); expect(body).toContain('`/home/nan`'); @@ -195,15 +245,26 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => } }); - test('marks where the screenshots go, without shipping broken images', () => { - expect(body).not.toContain(' { + expect(screenshots(body).map((s) => s.src)).toEqual(SHOTS.map((n) => `/docs/runs/${n}.webp`)); + expect(body).not.toContain('SCREENSHOT'); + }); + + test.each(SHOTS)('screenshot %s: file exists, size matches, alt and caption present', (name) => { + const shot = screenshots(body).find((s) => s.src === `/docs/runs/${name}.webp`)!; + const file = resolve(pub, `docs/runs/${name}.webp`); + expect(existsSync(file)).toBe(true); + expect(webpSize(file)).toEqual({ width: shot.width, height: shot.height }); + expect(shot.alt.length).toBeGreaterThan(30); + expect(shot.caption.length).toBeGreaterThan(5); + expect(`${shot.src} ${shot.alt} ${shot.caption}`).not.toMatch(PRIVATE); + expect(`${shot.alt} ${shot.caption}`).not.toMatch(INTERNAL); }); test('is served by /api/docs without errors, and the placeholders are not', async () => { const text = await mdxToText(body); expect(text).toContain(page.title); - expect(text).not.toContain('SCREENSHOT'); + expect(text).toContain('/docs/runs/runs-01-tab.webp'); }); test('no em-dashes in the copy', () => { @@ -226,4 +287,14 @@ describe('agent runs guide, both locales', () => { expect(count(es, /```bash/g)).toBe(count(en, /```bash/g)); expect(count(es, /
{ + const [en, es] = [pages.en, pages.es].map((p) => screenshots(strip(read(p.dir + 'runs.mdx')))); + expect(es.map((s) => s.src)).toEqual(en.map((s) => s.src)); + en.forEach((shot, i) => expect(es[i].alt).not.toBe(shot.alt)); + }); + + test('only the published screenshots live in the folder (no raw captures)', () => { + expect(readdirSync(resolve(pub, 'docs/runs')).sort()).toEqual(SHOTS.map((n) => `${n}.webp`).sort()); + }); }); From 6001595d0047d914185189018445cef7f6ba1806 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 21:40:25 +0200 Subject: [PATCH 6/9] docs(runs): keep the API request table whole and map the Agent field The mapping sentence had landed between the timeout_seconds and model rows, closing the table and leaving model as loose text. The sentence now follows the table, both mappings include Agent, and a test pins the request table rows as one contiguous table. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/runs.mdx | 6 +++--- src/content/docs/runs.mdx | 6 +++--- src/tests/layouts/runsDocs.test.ts | 12 +++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 0790271..3a06151 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -186,7 +186,7 @@ nan run --detach "actualiza las dependencias" | `--json` | | Imprime los eventos como líneas JSON y después el run terminado. | | `-f FILE` | | Lee la tarea de un archivo, o de stdin con `-`. | -Los flags equivalen al formulario del portal: **Folder** es `--cwd`, **Separate branch** es `--worktree` / `--no-worktree` y **Time limit** es `--timeout`. La tarea es el texto entre comillas. +Los flags equivalen al formulario del portal: **Agent** es `--agent`, **Folder** es `--cwd`, **Separate branch** es `--worktree` / `--no-worktree` y **Time limit** es `--timeout`. La tarea es el texto entre comillas. Mientras se muestra el log, **Ctrl-C una vez** te desconecta del log: el run sigue y la CLI te dice cómo retomarlo. **Ctrl-C dos veces en menos de 2 segundos** lo cancela. @@ -278,10 +278,10 @@ curl https://api.nan.builders/v1/runs \ | `cwd` | No | La carpeta, `/home/nan` por defecto. | | `git_isolation` | No | Rama aparte: `null` para auto (por defecto), `true` para on, `false` para off. | | `timeout_seconds` | No | El tiempo máximo, de 60 a 7200. 1800 (30 minutos) por defecto. | - -Los campos equivalen al formulario del portal: **Task** es `prompt`, **Folder** es `cwd`, **Separate branch** es `git_isolation` y **Time limit** es `timeout_seconds`. | `model` | No | El modelo del agente. Por defecto, el suyo. | +Los campos equivalen al formulario del portal: **Agent** es `agent`, **Task** es `prompt`, **Folder** es `cwd`, **Separate branch** es `git_isolation` y **Time limit** es `timeout_seconds`. + La respuesta es el run, con su `id` y su `state`. La cabecera `Idempotency-Key` es opcional: si un reintento manda la misma clave con el mismo cuerpo, recibes el run que ya se creó en lugar de uno nuevo. ### Síguelo diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index ffec47d..51f05a0 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -186,7 +186,7 @@ nan run --detach "upgrade the dependencies" | `--json` | | Print events as JSON lines, then the finished run. | | `-f FILE` | | Read the task from a file, or from stdin with `-`. | -The flags match the portal form: **Folder** is `--cwd`, **Separate branch** is `--worktree` / `--no-worktree` and **Time limit** is `--timeout`. The task is the text in quotes. +The flags match the portal form: **Agent** is `--agent`, **Folder** is `--cwd`, **Separate branch** is `--worktree` / `--no-worktree` and **Time limit** is `--timeout`. The task is the text in quotes. While it streams, **Ctrl-C once** stops following the log: the run keeps going and the CLI tells you how to pick it up again. **Ctrl-C twice within 2 seconds** cancels it. @@ -278,10 +278,10 @@ curl https://api.nan.builders/v1/runs \ | `cwd` | No | The folder, `/home/nan` by default. | | `git_isolation` | No | Separate branch: `null` for auto (the default), `true` for on, `false` for off. | | `timeout_seconds` | No | The time limit, from 60 to 7200. 1800 (30 minutes) by default. | - -The fields match the portal form: **Task** is `prompt`, **Folder** is `cwd`, **Separate branch** is `git_isolation` and **Time limit** is `timeout_seconds`. | `model` | No | The model for the agent. Its own by default. | +The fields match the portal form: **Agent** is `agent`, **Task** is `prompt`, **Folder** is `cwd`, **Separate branch** is `git_isolation` and **Time limit** is `timeout_seconds`. + The answer is the run, with its `id` and its `state`. The `Idempotency-Key` header is optional: if a retry sends the same key with the same body, you get the run that was already created instead of a second one. ### Follow it diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index 9036989..2a45dad 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -131,8 +131,8 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => }); test('maps the portal labels to the CLI flags and the API fields', () => { - expect(flat).toMatch(/\*\*Folder\*\* (is|es) `--cwd`, \*\*Separate branch\*\* (is|es) `--worktree` \/ `--no-worktree` (and|y) \*\*Time limit\*\* (is|es) `--timeout`/); - expect(flat).toMatch(/\*\*Task\*\* (is|es) `prompt`, \*\*Folder\*\* (is|es) `cwd`, \*\*Separate branch\*\* (is|es) `git_isolation` (and|y) \*\*Time limit\*\* (is|es) `timeout_seconds`/); + expect(flat).toMatch(/\*\*Agent\*\* (is|es) `--agent`, \*\*Folder\*\* (is|es) `--cwd`, \*\*Separate branch\*\* (is|es) `--worktree` \/ `--no-worktree` (and|y) \*\*Time limit\*\* (is|es) `--timeout`/); + expect(flat).toMatch(/\*\*Agent\*\* (is|es) `agent`, \*\*Task\*\* (is|es) `prompt`, \*\*Folder\*\* (is|es) `cwd`, \*\*Separate branch\*\* (is|es) `git_isolation` (and|y) \*\*Time limit\*\* (is|es) `timeout_seconds`/); }); test('describes the portal form with its limits', () => { @@ -193,9 +193,11 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => expect(body).toContain('curl -N https://api.nan.builders/v1/runs/$RUN_ID/events'); expect(body).toContain('-H "Accept: text/event-stream"'); expect(body).toContain('curl -X POST https://api.nan.builders/v1/runs/$RUN_ID/cancel'); - for (const field of ['workspace', 'agent', 'prompt', 'cwd', 'git_isolation', 'timeout_seconds', 'model']) { - expect(body, field).toContain(`| \`${field}\` |`); - } + // One table, rows back to back: a paragraph between two rows splits it and + // turns the rest into loose text with pipes. + const rows = ['workspace', 'agent', 'prompt', 'cwd', 'git_isolation', 'timeout_seconds', 'model']; + const table = rows.map((f) => `\\| \`${f}\` \\|[^\\n]*`).join('\\n'); + expect(body).toMatch(new RegExp(table)); for (const frame of ['event: run_event', 'event: state', 'event: end']) expect(body).toContain(frame); expect(body).toContain('Last-Event-ID'); expect(body).toContain('next_cursor'); From b85d89e874a8c909d596f69a7eaaeb45450e0224 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 21:52:25 +0200 Subject: [PATCH 7/9] docs(runs): state the portal's 30 min default time limit cloud-ui defaults Time limit to 1800 s (30 min), the same as the CLI and API, with options 15 min, 30 min, 1 hour and 2 hours. The guide now says so in both locales, and the test pins it. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/runs.mdx | 4 ++-- src/content/docs/runs.mdx | 4 ++-- src/tests/layouts/runsDocs.test.ts | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 3a06151..4b684fb 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -47,7 +47,7 @@ Pulsa **New run** y rellena el formulario (el portal está en inglés): | **Task** | La tarea, con tus palabras, como se la escribirías al agente en un chat. Hasta 32 KiB (el contador bajo la caja te dice los bytes que llevas). | | **Folder** | La carpeta donde trabaja el agente. Por defecto es `/home/nan` y tiene que estar dentro de ella. | | **Separate branch** | Rama aparte: **Auto** (por defecto), **On** u **Off**. En un repositorio git, el agente trabaja en su propia rama y tu checkout se queda como está. Consulta [Dónde trabaja el agente](#dónde-trabaja-el-agente). | -| **Time limit** | El tiempo máximo: 15, 30, 60 o 120 minutos. El run se para al llegar a él. | +| **Time limit** | El tiempo máximo: **30 min** por defecto, igual que en la CLI y la API. Las opciones son 15 min, 30 min, 1 hour y 2 hours (1 hora y 2 horas). El run se para al llegar a él. | Pulsa **Start run**, o `Ctrl+Enter` (`⌘+Enter` en un Mac) desde la caja de la tarea. @@ -103,7 +103,7 @@ La carpeta tiene que estar dentro de `/home/nan`, tu carpeta personal en el work El portal muestra los mismos estados en inglés y con mayúscula, por ejemplo **Running**, **Succeeded** o **Cancelled**. -Desde la CLI y la API el tiempo máximo va de 60 segundos a 2 horas. El portal ofrece 15, 30, 60 y 120 minutos. +Desde la CLI y la API el tiempo máximo va de 60 segundos a 2 horas. El portal ofrece 15 min, 30 min, 1 hora y 2 horas, con 30 min por defecto. ## Cuántos runs a la vez diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index 51f05a0..a867508 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -47,7 +47,7 @@ Press **New run** and fill in the form: | **Task** | What you want done, in your own words, as you would write it to the agent in a chat. Up to 32 KiB (the counter under the box shows the bytes used). | | **Folder** | Where the agent works. It defaults to `/home/nan` and must be inside it. | | **Separate branch** | **Auto** (the default), **On** or **Off**. In a git repository, the agent works on its own branch and your checkout stays as it is. See [Where the agent works](#where-the-agent-works). | -| **Time limit** | 15, 30, 60 or 120 minutes. The run is stopped when it reaches it. | +| **Time limit** | **30 min** by default, the same as the CLI and the API. The options are 15 min, 30 min, 1 hour and 2 hours. The run is stopped when it reaches it. | Press **Start run**, or `Ctrl+Enter` (`⌘+Enter` on a Mac) from the task box. @@ -103,7 +103,7 @@ The folder must be inside `/home/nan`, your home in the workspace. The portal shows the same states with a capital letter, for example **Running**, **Succeeded** or **Cancelled**. -Through the CLI and the API the time limit goes from 60 seconds to 2 hours. The portal offers 15, 30, 60 and 120 minutes. +Through the CLI and the API the time limit goes from 60 seconds to 2 hours. The portal offers 15 min, 30 min, 1 hour and 2 hours, with 30 min by default. ## How many runs at once diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index 2a45dad..76c48cf 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -138,7 +138,11 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => test('describes the portal form with its limits', () => { expect(flat).toContain('32 KiB'); expect(body).toContain('`/home/nan`'); - expect(flat).toMatch(/15, 30, 60 (or|o) 120/); + // cloud-ui src/lib/runs.ts: DEFAULT_TIMEOUT_SECONDS = 1800, options 15 min / 30 min / 1 hour / 2 hours. + expect(flat).toMatch(/\| \*\*Time limit\*\* \|[^|]*\*\*30 min\*\* (by default|por defecto), (the same as|igual que en) (the CLI and the API|la CLI y la API)/); + expect(flat).toMatch(/15 min, 30 min, 1 hour (and|y) 2 hours/); + expect(flat).toMatch(/15 min, 30 min, 1 (hour|hora) (and|y) 2 (hours|horas), (with 30 min by default|con 30 min por defecto)/); + expect(flat).not.toMatch(/60 (or|o|and|y) 120 (minutes|minutos)/); for (const tab of ['Overview', 'Console', 'SSH keys', 'Events', 'Backups']) expect(body).toContain(`**${tab}**`); }); From 37f6f885ed2f56f70a730559404d19be462a8899 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:05:01 +0200 Subject: [PATCH 8/9] docs(runs): document CLI v0.1.25 authentication and CI use The runs commands now take NAN_TOKEN, --token-file or a token saved with nan auth login --api-token, before the session and the saved API key. Adds the authentication order and its safety rules, a GitHub Actions example from the CLI README with a secret warning, --token-file in the flags, the main nan runs logs --json event types, and bumps the minimum CLI version to 0.1.25 in the guide and the NaN CLI page. Pinned in tests. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/nan-cli.mdx | 4 +- src/content/docs-es/runs.mdx | 63 ++++++++++++++++++++++++++++-- src/content/docs/nan-cli.mdx | 4 +- src/content/docs/runs.mdx | 63 ++++++++++++++++++++++++++++-- src/tests/layouts/runsDocs.test.ts | 59 +++++++++++++++++++++++++++- 5 files changed, 181 insertions(+), 12 deletions(-) diff --git a/src/content/docs-es/nan-cli.mdx b/src/content/docs-es/nan-cli.mdx index b7de7c9..57d00c4 100644 --- a/src/content/docs-es/nan-cli.mdx +++ b/src/content/docs-es/nan-cli.mdx @@ -158,7 +158,7 @@ En Linux con bash, el mismo comando pero contra `~/.bashrc`. ## Runs -Desde la v0.1.24 la CLI también pone a trabajar agentes en tareas dentro de tus [workspaces](/es/docs/workspaces), y los sigue desde tu terminal: +Desde la v0.1.25 la CLI también pone a trabajar agentes en tareas dentro de tus [workspaces](/es/docs/workspaces), y los sigue desde tu terminal: ```bash nan run "añade tests al parser de fechas y haz que pasen" @@ -166,7 +166,7 @@ nan runs ls nan runs logs -f ``` -El run se ejecuta en el workspace, no en tu máquina, así que sigue aunque cierres la terminal. Los comandos de runs usan tu sesión de `nan auth login`. Todos los flags, los códigos de salida y la API están en [Runs de agentes](/es/docs/runs). +El run se ejecuta en el workspace, no en tu máquina, así que sigue aunque cierres la terminal. Los comandos de runs usan tu sesión de `nan auth login`, o un token de plataforma desde `NAN_TOKEN`, `--token-file` o `nan auth login --api-token` para scripts, CI y servidores. Todos los flags, el orden de autenticación, los códigos de salida y la API están en [Runs de agentes](/es/docs/runs). ## Actualizar diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 4b684fb..3a8053b 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -131,7 +131,7 @@ Los secretos del archivo de entorno de tu workspace, como esa clave de inferenci ## Desde la terminal: `nan run` -La [CLI de NaN](/es/docs/nan-cli) lanza y sigue runs desde tu propia terminal. Necesitas la versión **0.1.24** o posterior. +La [CLI de NaN](/es/docs/nan-cli) lanza y sigue runs desde tu propia terminal. Necesitas la versión **0.1.25** o posterior: `nan --version` imprime `nan `. @@ -147,7 +147,7 @@ curl -fsSL https://nan.builders/install | bash nan auth login --email tu@correo ``` -Te llega por correo un enlace de acceso. Cópialo y pégalo de vuelta en el comando sin abrirlo antes en el navegador: solo funciona una vez. Tienes los detalles en la [guía de la CLI](/es/docs/nan-cli). Los comandos de runs usan esa sesión, o la API key de la pestaña Setup de la CLI si no hay sesión. +Te llega por correo un enlace de acceso. Cópialo y pégalo de vuelta en el comando sin abrirlo antes en el navegador: solo funciona una vez. Tienes los detalles en la [guía de la CLI](/es/docs/nan-cli). Para scripts, CI o un servidor, usa mejor un token: consulta [Autenticación](#autenticación). ### Lanza un run @@ -185,6 +185,7 @@ nan run --detach "actualiza las dependencias" | `--idempotency-key K` | | Si un reintento manda la misma clave y la misma petición, recibes el run ya creado en lugar de uno nuevo. | | `--json` | | Imprime los eventos como líneas JSON y después el run terminado. | | `-f FILE` | | Lee la tarea de un archivo, o de stdin con `-`. | +| `--token-file PATH` | | Se autentica con el token de la primera línea de un archivo. Consulta [Autenticación](#autenticación). | Los flags equivalen al formulario del portal: **Agent** es `--agent`, **Folder** es `--cwd`, **Separate branch** es `--worktree` / `--no-worktree` y **Time limit** es `--timeout`. La tarea es el texto entre comillas. @@ -199,7 +200,17 @@ nan runs logs -f # sigue el log hasta que termine nan runs cancel ``` -`nan runs logs --json` imprime un evento JSON por línea, cómodo para scripts. +`nan runs logs --json` imprime un evento JSON por línea, cómodo para scripts. Cada evento tiene un `type`; estos son los principales: + +| Tipo | Qué es | +|---|---| +| `started` | El run ha empezado: agente, modelo, carpeta y, si la hay, la rama. | +| `message` | Texto del agente. | +| `tool_call` / `tool_result` | Una herramienta que ha usado el agente, y lo que le ha respondido. | +| `log` | Una línea de salida que no es un mensaje. | +| `usage` | Los tokens gastados hasta ahora. | +| `error` | Algo ha ido mal, con un código. | +| `finished` | El run ha terminado: su estado y el resumen del agente. | ### Códigos de salida @@ -223,6 +234,52 @@ Con `set -e`, ten en cuenta el 75 de `--detach`: id=$(nan run --detach "actualiza las dependencias") || [ $? -eq 75 ] ``` +### Autenticación + +`nan run` y `nan runs` usan lo primero que encuentren de esta lista: + +1. **`NAN_TOKEN`**: una variable de entorno con un token de plataforma (`nan_pat_...`) o una API key (`sk-...`). Se lee en cada comando y nunca se escribe en disco. +2. **`--token-file PATH`**: el token en la primera línea de un archivo. El archivo tiene que ser privado (`chmod 600`): en macOS y Linux, si otros usuarios pueden leerlo o escribirlo, se rechaza. +3. **Un token guardado**, con `nan auth login --api-token`. +4. **Tu sesión**, de `nan auth login` (el enlace de acceso por correo). +5. **Una API key guardada**, desde la pestaña Setup (o una clave `sk-...` guardada con `nan auth login --api-token`). + +`nan auth login --api-token` lee el token solo de stdin, oculto mientras lo pegas, lo comprueba contra nan.builders y lo guarda con permisos `0600`. Lo guarda aparte de la API key, así que la pestaña Setup nunca copia un token de plataforma en la configuración de tus herramientas: + +```bash +nan auth login --api-token # pégalo cuando te lo pida +nan auth login --api-token < token.txt # o pásalo por stdin +``` + +No pongas nunca un token en la propia línea de comandos: acabaría en el historial de tu shell y en `ps`. + +Un token de plataforma sirve para los runs y para nada más (consulta [API keys y tokens](#api-keys-y-tokens)). `nan me`, `nan metrics usage` y el panel siguen necesitando una sesión de `nan auth login`. + +### Usa la CLI en CI o en un servidor + +Crea un [token](#api-keys-y-tokens), guárdalo en los secretos de tu CI y pásalo como `NAN_TOKEN`. Por ejemplo, en GitHub Actions: + +```yaml +- name: Review the PR with an agent + env: + NAN_TOKEN: ${{ secrets.NAN_TOKEN }} + run: | + curl -fsSL https://nan.builders/install | bash + nan run --ws ci --timeout 20m -f .github/review-task.md +``` + +Ese paso espera al run y falla si el run falla. Para lanzarlo y seguir sin esperar, usa `--detach` y ten en cuenta su código de salida 75: + +```bash +nan run --ws ci --detach "actualiza las dependencias" || [ $? -eq 75 ] +``` + + +Guarda el token solo como secreto (`secrets.NAN_TOKEN` en GitHub Actions), nunca en el repositorio ni en el archivo del workflow. Puede lanzar runs que gastan tu plan. + + +En un servidor que configuras una vez, guárdalo mejor con `nan auth login --api-token`. + ## Desde la API Los mismos runs están disponibles por HTTP, en `https://api.nan.builders/v1/runs`, para tus scripts e integraciones. diff --git a/src/content/docs/nan-cli.mdx b/src/content/docs/nan-cli.mdx index 3f43399..9fb1916 100644 --- a/src/content/docs/nan-cli.mdx +++ b/src/content/docs/nan-cli.mdx @@ -158,7 +158,7 @@ On Linux with bash, the same command but against `~/.bashrc`. ## Runs -From v0.1.24 the CLI also starts agents on tasks inside your [workspaces](/docs/workspaces), and follows them from your terminal: +From v0.1.25 the CLI also starts agents on tasks inside your [workspaces](/docs/workspaces), and follows them from your terminal: ```bash nan run "add tests for the date parser and make them pass" @@ -166,7 +166,7 @@ nan runs ls nan runs logs -f ``` -The run happens in the workspace, not on your machine, so it keeps going if you close the terminal. The runs commands use your `nan auth login` session. Every flag, the exit codes and the API are in [Agent runs](/docs/runs). +The run happens in the workspace, not on your machine, so it keeps going if you close the terminal. The runs commands use your `nan auth login` session, or a platform token from `NAN_TOKEN`, `--token-file` or `nan auth login --api-token` for scripts, CI and servers. Every flag, the authentication order, the exit codes and the API are in [Agent runs](/docs/runs). ## Updating diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index a867508..70e1e57 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -131,7 +131,7 @@ Secrets in your workspace's environment file, such as that inference key or a Te ## From the terminal: `nan run` -The [NaN CLI](/docs/nan-cli) starts and follows runs from your own terminal. You need version **0.1.24** or newer. +The [NaN CLI](/docs/nan-cli) starts and follows runs from your own terminal. You need version **0.1.25** or newer: `nan --version` prints `nan `. @@ -147,7 +147,7 @@ curl -fsSL https://nan.builders/install | bash nan auth login --email you@yours ``` -It emails you a sign-in link. Copy it from the email and paste it back into the command without opening it in the browser first: it works once. The [NaN CLI guide](/docs/nan-cli) has the details. The runs commands use that session, or the API key from the CLI's Setup tab when there is no session. +It emails you a sign-in link. Copy it from the email and paste it back into the command without opening it in the browser first: it works once. The [NaN CLI guide](/docs/nan-cli) has the details. For scripts, CI or a server, use a token instead: see [Authentication](#authentication). ### Start a run @@ -185,6 +185,7 @@ nan run --detach "upgrade the dependencies" | `--idempotency-key K` | | If a retry sends the same key and request, you get the run already created instead of a second one. | | `--json` | | Print events as JSON lines, then the finished run. | | `-f FILE` | | Read the task from a file, or from stdin with `-`. | +| `--token-file PATH` | | Authenticate with the token on the first line of a file. See [Authentication](#authentication). | The flags match the portal form: **Agent** is `--agent`, **Folder** is `--cwd`, **Separate branch** is `--worktree` / `--no-worktree` and **Time limit** is `--timeout`. The task is the text in quotes. @@ -199,7 +200,17 @@ nan runs logs -f # follow the log until it ends nan runs cancel ``` -`nan runs logs --json` prints one JSON event per line, handy for scripts. +`nan runs logs --json` prints one JSON event per line, handy for scripts. Each event has a `type`; these are the main ones: + +| Type | What it is | +|---|---| +| `started` | The run started: agent, model, folder and, if there is one, the branch. | +| `message` | Text from the agent. | +| `tool_call` / `tool_result` | A tool the agent called, and what it answered. | +| `log` | A line of output that is not a message. | +| `usage` | Tokens used so far. | +| `error` | Something went wrong, with a code. | +| `finished` | The run ended: its state and the agent's summary. | ### Exit codes @@ -223,6 +234,52 @@ Under `set -e`, allow for the 75 of `--detach`: id=$(nan run --detach "upgrade the dependencies") || [ $? -eq 75 ] ``` +### Authentication + +`nan run` and `nan runs` use the first of these they find: + +1. **`NAN_TOKEN`**: an environment variable holding a platform token (`nan_pat_...`) or an API key (`sk-...`). It is read on every command and never written to disk. +2. **`--token-file PATH`**: the token on the first line of a file. The file must be private (`chmod 600`): on macOS and Linux, one that other users can read or write is refused. +3. **A saved token**, from `nan auth login --api-token`. +4. **Your session**, from `nan auth login` (the emailed sign-in link). +5. **A saved API key**, from the Setup tab (or an `sk-...` key saved with `nan auth login --api-token`). + +`nan auth login --api-token` reads the token from stdin only, hidden while you paste it, checks it against nan.builders and saves it with mode `0600`. It keeps it apart from the API key, so the Setup tab never copies a platform token into your tools' configuration: + +```bash +nan auth login --api-token # paste it when asked +nan auth login --api-token < token.txt # or pipe it in +``` + +Never put a token on the command line itself: it would end up in your shell history and in `ps`. + +A platform token works for runs and nothing else (see [API keys and tokens](#api-keys-and-tokens)). `nan me`, `nan metrics usage` and the dashboard still need a session from `nan auth login`. + +### Use the CLI in CI or on a server + +Create a [token](#api-keys-and-tokens), store it in your CI's secret store and expose it as `NAN_TOKEN`. For example, in GitHub Actions: + +```yaml +- name: Review the PR with an agent + env: + NAN_TOKEN: ${{ secrets.NAN_TOKEN }} + run: | + curl -fsSL https://nan.builders/install | bash + nan run --ws ci --timeout 20m -f .github/review-task.md +``` + +That step waits for the run and fails if the run fails. To start the run and move on without waiting, use `--detach` and allow for its exit code 75: + +```bash +nan run --ws ci --detach "upgrade the dependencies" || [ $? -eq 75 ] +``` + + +Store the token only as a secret (`secrets.NAN_TOKEN` in GitHub Actions), never in the repository or in the workflow file. It can start runs that spend your plan. + + +On a server you set up once, save it with `nan auth login --api-token` instead. + ## From the API The same runs are available over HTTP, at `https://api.nan.builders/v1/runs`, for your own scripts and integrations. diff --git a/src/tests/layouts/runsDocs.test.ts b/src/tests/layouts/runsDocs.test.ts index 76c48cf..4ac44ef 100644 --- a/src/tests/layouts/runsDocs.test.ts +++ b/src/tests/layouts/runsDocs.test.ts @@ -179,10 +179,12 @@ describe.each(Object.entries(pages))('agent runs guide (%s)', (locale, page) => }); test('documents the CLI: version, flags, Ctrl-C and every exit code', () => { - expect(body).toContain('0.1.24'); + expect(body).toContain('**0.1.25**'); + expect(body).not.toContain('0.1.24'); + expect(body).toContain('`nan --version` '); expect(body).toContain('curl -fsSL https://nan.builders/install | bash'); expect(body).toContain('nan auth login'); - for (const flag of ['--ws', '--agent', '--model', '--cwd', '--worktree', '--no-worktree', '--timeout', '--detach', '--json', '--idempotency-key', '-f']) { + for (const flag of ['--ws', '--agent', '--model', '--cwd', '--worktree', '--no-worktree', '--timeout', '--detach', '--json', '--idempotency-key', '-f', '--token-file']) { expect(body, flag).toContain(`\`${flag}`); } for (const cmd of ['nan runs ls', 'nan runs show', 'nan runs logs -f', 'nan runs cancel', 'nan runs logs --json']) { @@ -303,4 +305,57 @@ describe('agent runs guide, both locales', () => { test('only the published screenshots live in the folder (no raw captures)', () => { expect(readdirSync(resolve(pub, 'docs/runs')).sort()).toEqual(SHOTS.map((n) => `${n}.webp`).sort()); }); + +}); + +describe.each(Object.entries(pages))('CLI authentication for runs (%s)', (locale, page) => { + const body = strip(read(page.dir + 'runs.mdx')); + const flat = body.replace(/\s+/g, ' '); + + test('lists the sources in the order the CLI (v0.1.25) uses them', () => { + const list = /^1\. \*\*`NAN_TOKEN`\*\*[\s\S]*?^5\. [^\n]*$/m.exec(body)![0]; + const order = [ + '1. **`NAN_TOKEN`**', + '2. **`--token-file PATH`**', + locale === 'en' ? '3. **A saved token**, from `nan auth login --api-token`' : '3. **Un token guardado**, con `nan auth login --api-token`', + locale === 'en' ? '4. **Your session**, from `nan auth login`' : '4. **Tu sesión**, de `nan auth login`', + locale === 'en' ? '5. **A saved API key**' : '5. **Una API key guardada**', + ]; + const at = order.map((o) => list.indexOf(o)); + at.forEach((i, n) => expect(i, order[n]).toBeGreaterThan(-1)); + expect([...at].sort((a, b) => a - b)).toEqual(at); + }); + + test('states the safety rules of each source', () => { + expect(flat).toMatch(/(never written to disk|nunca se escribe en disco)/); + expect(body).toContain('`chmod 600`'); + expect(body).toContain('`0600`'); + expect(body).toContain('nan auth login --api-token < token.txt'); + expect(flat).toMatch(/(never copies a platform token|nunca copia un token de plataforma)/); + expect(flat).toMatch(/(shell history|historial de tu shell)/); + }); + + test('says platform tokens only work for runs, not for nan me / metrics / the dashboard', () => { + expect(flat).toContain('`nan me`, `nan metrics usage`'); + }); + + test('shows the GitHub Actions example from the CLI README, with a secret and a warning', () => { + expect(body).toContain('NAN_TOKEN: ${{ secrets.NAN_TOKEN }}'); + expect(body).toContain('nan run --ws ci --timeout 20m -f .github/review-task.md'); + expect(body).toMatch(/nan run --ws ci --detach "[^"]+" \|\| \[ \$\? -eq 75 \]/); + expect(body).toMatch(//); + }); + + test('lists the main event types of nan runs logs --json', () => { + for (const t of ['started', 'message', 'tool_call', 'tool_result', 'log', 'usage', 'error', 'finished']) { + expect(body, t).toContain(`\`${t}\``); + } + }); + + test('the NaN CLI guide points at v0.1.25 and the token options', () => { + const cli = read(page.dir + 'nan-cli.mdx'); + expect(cli).toContain('v0.1.25'); + expect(cli).not.toContain('v0.1.24'); + expect(cli).toContain('`NAN_TOKEN`, `--token-file` o'.replace(' o', locale === 'en' ? ' or' : ' o')); + }); }); From baa3df43d9056bbe8649ddabb4d82cd8502604d5 Mon Sep 17 00:00:00 2001 From: barckcode Date: Sat, 10 Oct 2026 22:09:16 +0200 Subject: [PATCH 9/9] docs(runs): say platform tokens work for nan run and nan runs in the CLI Avoids contradicting the tokens table, where the platform API also lists your workspaces. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/runs.mdx | 2 +- src/content/docs/runs.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 3a8053b..75ed923 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -253,7 +253,7 @@ nan auth login --api-token < token.txt # o pásalo por stdin No pongas nunca un token en la propia línea de comandos: acabaría en el historial de tu shell y en `ps`. -Un token de plataforma sirve para los runs y para nada más (consulta [API keys y tokens](#api-keys-y-tokens)). `nan me`, `nan metrics usage` y el panel siguen necesitando una sesión de `nan auth login`. +En la CLI, un token de plataforma solo sirve para `nan run` y `nan runs` (consulta [API keys y tokens](#api-keys-y-tokens)). `nan me`, `nan metrics usage` y el panel siguen necesitando una sesión de `nan auth login`. ### Usa la CLI en CI o en un servidor diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index 70e1e57..1fedabe 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -253,7 +253,7 @@ nan auth login --api-token < token.txt # or pipe it in Never put a token on the command line itself: it would end up in your shell history and in `ps`. -A platform token works for runs and nothing else (see [API keys and tokens](#api-keys-and-tokens)). `nan me`, `nan metrics usage` and the dashboard still need a session from `nan auth login`. +In the CLI, a platform token only works for `nan run` and `nan runs` (see [API keys and tokens](#api-keys-and-tokens)). `nan me`, `nan metrics usage` and the dashboard still need a session from `nan auth login`. ### Use the CLI in CI or on a server