Contrato del agente de OpenSpec
Superficies legibles por máquina de la CLI openspec, verificadas contra src/ (auditoría de cierre, 2026-06-11). Cada forma documentada a continuación proviene del código emisor.
1. Convenciones generales
- Un documento JSON por invocación. En el modo
--json, stdout contiene exactamente un documento JSON (formateado con 2 espacios). El texto humano, los indicadores de progreso y el banner del almacén se envían a stderr. - Banner del almacén. En el modo humano, una raíz seleccionada del almacén imprime
Using OpenSpec root: <id> (<path>)en stderr. Nunca se imprime en el modo JSON. - La capitalización de las claves depende de la superficie (véase Inconsistencias conocidas): los payloads de store/doctor/context usan
snake_case; los payloads de workflow (status,instructions,new change,validate,list) usancamelCase, excepto el objetorootincrustado, que siempre usastore_id. - Las claves opcionales se omiten, no se establecen en null, en la mayoría de los payloads (p. ej.,
root.store_id,member.path). Las excepciones que usannullexplícito se señalan por cada forma (store doctorgit.*, payloads de falla).
2. El envoltorio de diagnóstico
Toda máquina legible de diagnóstico (StoreDiagnostic) comparte una misma forma de envoltorio:
{
"severity": "error" | "warning" | "info",
"code": "snake_case_string",
"message": "human sentence",
"target": "dotted.surface (optional)",
"fix": "one actionable sentence/command (optional)"
}Los diagnósticos aparecen en dos posiciones: arreglos de estado (status: StoreDiagnostic[] a nivel superior o por entrada) para hallazgos de salud, y errores lanzados convertidos en un arreglo de status de un solo elemento cuando falla un comando.
3. Selección de raíz y RootOutput
Todos los comandos que resuelven la raíz (list, show, validate, status, instructions, instructions apply, instructions archive, new change, archive, doctor, context, schemas) resuelven una sola raíz de OpenSpec con una precedencia:
--store <id>→ la raíz del almacén registrado (source: "store").- De lo contrario, el ancestro más cercano con
openspec/: forma de planificación →source: "nearest"(un punterostore:se ignora con una advertencia en stderr); un directorio solo de configuración con un punterostore:válido → ese almacén,source: "declared". - Sin raíz más cercana +
defaultStoreglobal establecido (openspec config set defaultStore <id>) → ese almacén,source: "global_default"; un id obsoleto falla con el error del almacén subyacente y unfixque nombraopenspec config unset defaultStore. - Sin raíz más cercana, sin predeterminado + almacenes registrados existentes → error
no_root_with_registered_stores. - Sin raíz, sin predeterminado, sin almacenes: los comandos pueden tratar el cwd como
source: "implicit";doctor,context,listyvalidatepara múltiples elementos fallan conno_openspec_rooten su lugar.listconserva el fallback implícito para proyectos heredados conopenspec/project.md.
Los payloads JSON exitosos normalmente incrustan la raíz; schemas --json exitoso deliberadamente permanece como el arreglo desnudo de compatibilidad documentado en §4.13:
"root": { "path": "/abs/path", "source": "store" | "declared" | "global_default" | "nearest" | "implicit", "store_id": "id (only when store-selected)" }Contrato de fallo de raíz: en modo JSON, una falla de resolución imprime { ...commandNullShape, "status": [diagnostic] } en stdout y sale con código 1.
4. Formas JSON de los comandos
4.1 list --json
{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress" } ], "root": RootOutput } — nota que el status por cambio es un enum de cadena aquí. --specs: { "specs": [ { "id", "requirementCount" } ], "root" }.
4.2 show <item> --json
Cambio: { "id", "title", "deltaCount", "deltas": [...], "root" }. Especificación: { "id", "title", "overview", "requirementCount", "requirements": [...], "metadata": { "version", "format", "sourcePath"? }, "root" }.
4.3 validate --json
{ "items": [ { "id", "type": "change"|"spec", "valid", "issues": [ { "level", "path", "message", "line"?, "column"? } ], "durationMs" } ], "summary": { "totals": {items,passed,failed}, "byType": {...} }, "version": "1.0", "root" }. Sale con código 1 cuando falla algún elemento.
4.4 status --json
{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isPlanningComplete", "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", requires, missingDeps?} ], "root" }. isPlanningComplete significa que todo artefacto de planificación no omitido existe; los artefactos omitidos cuentan como satisfechos sin ser creados. No significa que las tareas de implementación estén completas. isComplete se conserva como un alias de compatibilidad con el mismo valor. El requires de cada artefacto son sus ids de dependencia directa (presente para cada estado, por lo que el conjunto transitivo requerido es computable incluso cuando el artefacto está done); missingDeps aparece solo cuando está blocked. El arreglo artifacts está en orden de dependencias, con el orden de declaración artifacts: del esquema desempatando entre artefactos que se vuelven listos al mismo tiempo (nunca alfabético), por lo que la primera entrada ready es el artefacto a escribir a continuación; missingDeps usa ese mismo orden. "skipped" marca un artefacto cuyo camino generates está bajo specs/ en un cambio cuyo .openspec.yaml declara skip_specs: true; satisface dependencias pero no debe crearse. Sin cambios activos: { "changes": [], "message", "root" }, sale con código 0.
--all (lote, mutuamente excluyente con --change — combinarlos es un error con la forma nula { "changes": [], "root": null, "status": [d] }): { "changes": [ <objeto de estado por cambio, sin raíz por cambio>, ... ], "root" }, ordenado por nombre de cambio. Un cambio que falla al cargar contribuye { "changeName", "status": [d] } en su lugar; la barrida continúa, conserva el envoltorio completo y sale con código 1 tanto en modo texto como JSON. Un --schema inválido hace fallar toda la invocación con la forma nula, incluso cuando no existen cambios.
4.5 instructions <artifact> --json
{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "skipped"?, "warning"?, "template", "dependencies": [{id,done,path,description,skipped?}], "unlocks", "root" }. unlocks lista los artefactos que este hace listos, en el orden de declaración del esquema (el mismo orden que status recomienda). "skipped": true (con "warning") aparece cuando el cambio declara skip_specs: true y este artefacto está omitido — no crees sus archivos. Una entrada de dependencia con skipped: true está satisfecha sin archivos — no intentes leer sus rutas.
ReferenceIndexEntry: { "store_id", "root"?, "specs"?: [{id,summary}], "fetch"?, "status": [] } — las entradas resueltas llevan root/specs/fetch; las no resueltas llevan store_id + estado de advertencia. Índice limitado a 50KB (reference_index_truncated).
4.6 instructions apply --json
{ "changeName", "changeDir", "schemaName", "contextFiles": { "<artifactId>": ["/abs", ...] }, "progress": {total,complete,remaining}, "tasks": [{id,description,done}], "state": "blocked"|"all_done"|"ready", "missingArtifacts"?, "missingPrerequisites"?, "warnings"?, "instruction", "references"?, "context"?, "operationGuidance"?, "root" }. missingArtifacts es en lo que apply se bloquea (el apply.requires del esquema); missingPrerequisites es todo lo que aún se debe construir antes de que apply pueda ejecutarse, en orden de construcción: el cierre transitivo de esos requires, por lo que puede ser la lista más larga. warnings lista problemas no bloqueantes con el cambio mismo: hoy, un cambio listo para implementarse sin especificaciones delta y sin skip_specs: true, el estado que openspec validate rechaza. Ambos campos raíz opcionales (context, operationGuidance) se leen de la raíz seleccionada en cada invocación. context es una entrada a nivel de prompt requerida cuyos hechos relevantes del proyecto, convenciones y restricciones deben aplicarse; operationGuidance es una entrada de asesoramiento cuyas entradas se siguen solo cuando son aplicables y compatibles con el flujo de trabajo integrado. Ambos permanecen separados del estado, tareas, progreso, archivos de contexto y la instrucción integrada.
4.7 instructions archive --json
{ "changeName", "context"?, "operationGuidance"?, "root" }. Requiere un --change válido en la raíz del repo/almacén resuelto y usa las mismas semánticas de contexto requerido/guía de asesoramiento que apply. Esta es una superficie de entrada de solo lectura en tiempo de ejecución: no devuelve el flujo de trabajo estático de archivo, no inspecciona ni fusiona especificaciones delta, no escribe especificaciones principales ni mueve el cambio.
4.8 new change <name> --json
Éxito: { "change": { "id", "path", "metadataPath", "schema" }, "root" }. Falla: { "change": null, "status": [d] }, sale con código 1.
4.9 archive <name> --json
Éxito: { "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"?, "warnings"? }, "root" }. Falla: { "archive": null, "root"?, "status": [d] }, sale con código 1. specsUpdated es verdadero solo cuando al menos un archivo de especificación fue escrito o retirado (una capacidad cuya última requisito el cambio eliminó hace que su especificación sea eliminada, lo que requiere retire_capabilities: true en el .openspec.yaml del cambio; cada retiro se nombra en warnings, con un comando de recuperación de Git pegado solo cuando la especificación vivía en el checkout de quien invoca); un cambio ya sincronizado se archiva con totales en cero y los omisiones enumerados en warnings. El modo JSON es estrictamente no interactivo: cada punto de prompt se convierte en un código archive_*.
4.10 doctor --json
{ "root": { "path", "source", "store_id"?, "healthy", "status": [] }, "store": { "id", "metadata": {present,valid,remote?}, "origin_url"?, "drift"?: {ahead,behind}, "status": [] } | null, "references": [...], "status": [] }. drift (presente solo para un checkout de almacén respaldado por Git que tiene una referencia de seguimiento ascendente) es conteos adelante/atrás contra el upstream obtenido por última vez, no el remoto en vivo. Hallazgos de salud de cualquier severidad salen con código 0. Payload de falla: { "root": null, "store": null, "references": [], "status": [d] }, sale con código 1.
4.11 context --json
{ "root": { "path", "source", "store_id"?, "role": "openspec_root" }, "members": [ { "role": "referenced_store", "id", "path"?, "remote"?, "fetch"?, "status": [] } ], "status": [] }. DISPONIBLE = ruta presente Y estado vacío. --code-workspace <path> escribe {folders:[{name,path}]} (solo almacenes referenciados disponibles, prefijos ref:); en modo JSON, la escritura se ejecuta antes de imprimir, por lo que stdout contiene exactamente un documento incluso en falla de escritura. Falla: { "root": null, "members": [], "status": [d] }, sale con código 1.
4.12 store ... --json
setup/register: { "store": {id, root, metadata_path?}, "registry": {path, registered, already_registered}, "git": {is_repository, initialized, committed}, "created_files": [], "status": [] }. unregister/remove: { "store", "registry": {path, removed}, "files": {deleted, deleted_path, left_on_disk}, "status": [] }. list: { "stores": [{id, root}], "status": [] }. doctor: { "stores": [ { id, root, metadata_path?, openspec_root: {...healthy, status}, metadata: {present, valid, id?, remote}, git: {is_repository, has_commits, has_uncommitted_changes, has_remote, origin_url}, status } ], "status": [] } (null = desconocido/no probado). Hallazgos de salud salen con código 0; fallas salen con código 1 y la forma nula correspondiente. Cancelación del prompt sale con código 130.
4.13 schemas --json / templates --json
schemas: el éxito permanece como un arreglo desnudo [ {name, description, artifacts, source} ]; resuelve la precedencia canónica de selección de raíz y acepta --store <id>. Falla de selección de raíz: { "schemas": [], "root": null, "status": [d] }, sale con código 1. templates: objeto claveado { "<artifactId>": {path, source} }, aún basado en cwd sin claves de raíz/estado.
5. Contrato de códigos de salida
| Situación | Salida | Stdout |
|---|---|---|
| Éxito, incluidas las detecciones de salud (doctor/context/store doctor) | 0 | el payload |
Fallo del comando en modo --json | 1 | un documento JSON con status: [d] y la forma nula del comando |
validate con elementos fallidos | 1 | informe completo |
Cancelación del prompt (grupo store, modo humano) | 130 | solo stderr |
6. Catálogo de códigos de diagnóstico
Resolución
no_openspec_root, no_root_with_registered_stores, no_registered_stores, unknown_store, store_identity_mismatch, unhealthy_store_root, store_path_not_supported, invalid_store_pointer, initiative_option_removed, areas_option_removed; paso directo: invalid_store_id, invalid_store_registry, invalid_store_metadata.
Salud de la raíz OpenSpec (error, sin corrección)
openspec_store_root_missing, openspec_store_root_not_directory, openspec_root_missing, openspec_root_not_directory, openspec_config_missing, openspec_config_not_file, openspec_specs_not_directory, openspec_changes_not_directory, openspec_archive_not_directory. Durante la beta de stores, openspec/specs/, openspec/changes/ y openspec/changes/archive/ pueden estar ausentes en una raíz saludable; solo constituyen errores de salud cuando están presentes pero no son directorios.
Registro/identidad/estado de store
invalid_store_id, invalid_store_registry, invalid_store_metadata, store_registry_busy, store_not_found, no_store_registry, store_registry_changed, store_metadata_missing, store_metadata_id_mismatch, store_metadata_invalid, store_id_conflict, store_path_conflict, store_already_registered (info).
Configuración/registro/eliminación de store
store_setup_id_required, store_setup_path_required, store_setup_path_not_directory, store_setup_inside_git_repo, store_setup_non_empty_directory, store_setup_cancelled, store_path_required, store_path_missing, store_path_not_directory, store_root_pointer_declared, store_register_root_unhealthy, store_register_identity_confirmation_required, store_register_cancelled, store_remote_empty, store_remote_requires_hand_edit, store_remove_confirmation_required, store_remove_cancelled, store_remove_path_not_directory, store_remove_metadata_missing, store_root_missing (aviso en remove, error en doctor), store_root_not_directory.
Git de store
store_git_init_failed, store_git_identity_missing, store_git_commit_failed, store_git_no_commits (aviso), store_clone_fragile_directories (aviso), store_remote_divergence (info, doctor), store_checkout_drift (info, doctor).
Referencias (aviso)
reference_invalid_id, reference_registry_unreadable, reference_unresolved, reference_root_unhealthy, reference_index_truncated.
Relaciones (aviso; doctor; context conserva solo la del registro)
relationship_registry_unreadable, root_pointer_ignored, root_pointer_invalid, pointer_declarations_inert.
Archivo (modo JSON)
archive_change_name_required, archive_change_not_found, archive_change_symlink, archive_validation_failed, archive_confirmation_required, archive_tasks_incomplete, archive_spec_update_failed, archive_spec_validation_failed, archive_target_exists, archive_error.
Escrituras de context
context_file_exists, context_output_dir_missing.
Respaldos
doctor_failed, context_failed, store_error, change_error, archive_error.
Inconsistencias conocidas
Registradas por la auditoría final; los renombramientos de claves publicadas son decisiones de producto aplazadas más allá de esta versión:
En modoCorregido en la ronda de gauntlet de la versión final: elementos desconocidos y ambiguos en--json, varias rutas de fallo imprimían solo stderr sin documento JSON.show/validateemiten{status:[{code: unknown_item | ambiguous_item, ...}]}; los errores lanzados enstatus/instructions/list/show/validatese enrutan a través del helper de fallo consciente de JSON (la forma nula del comando +status);store <subcomando desconocido> --jsonemite{status:[{code: unknown_store_subcommand}]};listlleva su forma nula{changes|specs: [], root: null}en fallos de resolución.store_root_missingse emite con dos severidades (aviso en remove, error en store doctor) — dependiente del contexto, documentado arriba.- snake_case (familia store) vs camelCase (familia workflow) en la capitalización de claves;
root.store_ides snake_case en todas partes. - Existen cuatro declaraciones paralelas de tipos de sobre en src; los diagnósticos de archive nunca llevan
target. list --jsonreutiliza la clavestatuscomo enumeración de cadena por cambio.- Solo la salida de
validatelleva un campoversion. templatesignora la selección de raíz (basado en cwd, sin--store).- Las formas nominales deprecadas (subcomandos
change/spec) emiten payloads sin sobre sinroot/status.