Contrato do Agente OpenSpec
Superfícies legíveis por máquina da CLI openspec, verificadas contra src/ (auditoria final, 2026-06-11). Cada estrutura abaixo é documentada a partir do código emissor.
1. Convenções gerais
- Um documento JSON por invocação. No modo
--json, o stdout carrega exatamente um documento JSON (formatado com 2 espaços). Texto humano, spinners e o banner da store vão para o stderr. - Banner da store. No modo humano, uma raiz selecionada pela store imprime
Using OpenSpec root: <id> (<path>)no stderr. Nunca impresso no modo JSON. - O formato das chaves depende da superfície (veja Inconsistências conhecidas): payloads de store/doctor/context usam
snake_case; payloads de workflow (status,instructions,new change,validate,list) usamcamelCase, exceto o objetorootincorporado, que sempre usastore_id. - Chaves opcionais são omitidas, não nulas, na maioria dos payloads (ex.:
root.store_id,member.path). As exceções que usamnullexplícito são destacadas por estrutura (store doctorgit.*, payloads de falha).
2. O envelope de diagnóstico
Uma forma de envelope é compartilhada por todos os diagnósticos legíveis por máquina (StoreDiagnostic):
{
"severity": "error" | "warning" | "info",
"code": "snake_case_string",
"message": "human sentence",
"target": "dotted.surface (optional)",
"fix": "one actionable sentence/command (optional)"
}Os diagnósticos aparecem em duas posições: arrays de status (status: StoreDiagnostic[] no nível superior ou por entrada) para descobertas de saúde, e erros lançados convertidos em um array status de elemento único em caso de falha do comando.
3. Seleção da raiz e RootOutput
Todos os comandos que resolvem a raiz (list, show, validate, status, instructions, instructions apply, instructions archive, new change, archive, doctor, context, schemas) resolvem uma única raiz OpenSpec com uma única precedência:
--store <id>→ a raiz da loja registrada (source: "store").- Caso contrário, o ancestral mais próximo com
openspec/: forma de planejamento →source: "nearest"(um ponteirostore:é ignorado com um aviso stderr); diretório apenas de configuração com um ponteirostore:válido → essa loja,source: "declared". - Nenhuma raiz próxima +
defaultStoreglobal definido (openspec config set defaultStore <id>) → essa loja,source: "global_default"; um id obsoleto falha com o erro subjacente da loja e umfixindicandoopenspec config unset defaultStore. - Nenhuma raiz próxima, nenhum padrão + lojas registradas existem → erro
no_root_with_registered_stores. - Sem raiz, sem padrão, sem lojas: os comandos podem tratar o cwd como
source: "implicit";doctor,context,listevalidateem lote falham em vez disso comno_openspec_root.listpreserva o fallback implícito para projetos legados comopenspec/project.md.
Payloads JSON bem-sucedidos normalmente incorporam a raiz; schemas --json bem-sucedido permanece deliberadamente como o array nu compatível documentado na §4.13:
"root": { "path": "/abs/path", "source": "store" | "declared" | "global_default" | "nearest" | "implicit", "store_id": "id (only when store-selected)" }Contrato de falha da raiz: no modo JSON, uma falha de resolução imprime { ...commandNullShape, "status": [diagnostic] } no stdout e sai com código 1.
4. Formas JSON dos comandos
4.1 list --json
{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress" } ], "root": RootOutput } — note que o status por mudança é um enum de string aqui. --specs: { "specs": [ { "id", "requirementCount" } ], "root" }.
4.2 show <item> --json
Mudança: { "id", "title", "deltaCount", "deltas": [...], "root" }. Especificação: { "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" }. Sai com 1 quando qualquer item falhar.
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 artefato de planejamento não pulado existe; artefatos pulados contam como satisfeitos sem serem criados. Isso não significa que as tarefas de implementação estejam completas. isComplete é mantido como um alias de compatibilidade com o mesmo valor. O requires de cada artefato são seus ids de dependência direta (presentes para todos os status, portanto o conjunto necessário transitivo é computável mesmo quando o artefato está done); missingDeps aparece apenas quando blocked. O array artifacts está em ordem de dependência, com a ordem de declaração artifacts: do esquema quebrando empates entre artefatos que se tornam prontos ao mesmo tempo (nunca alfabética), então a primeira entrada ready é o artefato a ser escrito em seguida; missingDeps usa essa mesma ordem. "skipped" marca um artefato cujo caminho generates está sob specs/ em uma mudança cuja .openspec.yaml declara skip_specs: true; ele satisfaz dependências, mas não deve ser criado. Sem mudanças ativas: { "changes": [], "message", "root" }, sai com 0.
--all (lote, mutuamente exclusivo com --change — combiná-los é um erro com a forma nula { "changes": [], "root": null, "status": [d] }): { "changes": [ <objeto de status por mudança, sem root por mudança>, ... ], "root" }, ordenado por nome da mudança. Uma mudança que falha ao carregar contribui com { "changeName", "status": [d] } no lugar; a varredura continua, preserva o envelope completo e sai com 1 tanto no modo texto quanto no JSON. Um --schema inválido falha toda a invocação com a forma nula, mesmo quando não há mudanças.
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 os artefatos que este torna prontos, na ordem de declaração do esquema (a mesma ordem que status recomenda-os). "skipped": true (com "warning") aparece quando a mudança declara skip_specs: true e este artefato é pulado — não crie seus arquivos. Uma entrada de dependência com skipped: true é satisfeita sem arquivos — não tente ler seus caminhos.
ReferenceIndexEntry: { "store_id", "root"?, "specs"?: [{id,summary}], "fetch"?, "status": [] } — entradas resolvidas carregam root/specs/fetch; não resolvidas carregam store_id + status de aviso. Í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 é o que o apply bloqueia (o apply.requires do esquema); missingPrerequisites é tudo o que ainda precisa ser construído antes que o apply possa ser executado, em ordem de construção — o fecho transitivo desses requires, portanto pode ser a lista maior. warnings lista problemas não bloqueantes com a própria mudança — hoje, uma mudança que está pronta para implementar sem specs delta e sem skip_specs: true, o estado que openspec validate rejeita. Ambos os campos root opcionais (context, operationGuidance) são lidos da raiz selecionada em toda invocação. context é uma entrada obrigatória em nível de prompt cujos fatos relevantes do projeto, convenções e restrições devem ser aplicados; operationGuidance é uma entrada consultiva cujas entradas são seguidas apenas quando aplicáveis e compatíveis com o workflow integrado. Ambos permanecem separados do estado, tarefas, progresso, arquivos de contexto e da instrução integrada.
4.7 instructions archive --json
{ "changeName", "context"?, "operationGuidance"?, "root" }. Requer um --change válido na raiz do repositório/loja resolvida e usa a mesma semântica de contexto obrigatório/orientação consultiva do apply. Esta é uma superfície de entrada de tempo de execução somente leitura: ela não retorna o workflow de arquivo estático, inspeciona ou mescla specs delta, escreve specs principais ou move a mudança.
4.8 new change <name> --json
Sucesso: { "change": { "id", "path", "metadataPath", "schema" }, "root" }. Falha: { "change": null, "status": [d] }, sai com 1.
4.9 archive <name> --json
Sucesso: { "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"?, "warnings"? }, "root" }. Falha: { "archive": null, "root"?, "status": [d] }, sai com 1. specsUpdated é verdadeiro apenas quando pelo menos um arquivo de spec foi escrito ou aposentado (uma capacidade cuja última requisito a mudança removeu tem sua spec deletada, o que requer retire_capabilities: true na .openspec.yaml da mudança; cada aposentadoria é nomeada em warnings, com um comando de recuperação Git colável apenas quando a spec vivia no checkout do chamador); uma mudança já sincronizada arquivada com totais zero e as skips listadas em warnings. O modo JSON é estritamente não interativo: cada ponto de prompt torna-se um 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 apenas para uma checkout de loja baseada em git que tenha uma ref de rastreamento upstream) são contagens ahead/behind contra o último upstream buscado, não o remoto atual. Descobertas de saúde de qualquer severidade saem com 0. Payload de falha: { "root": null, "store": null, "references": [], "status": [d] }, sai com 1.
4.11 context --json
{ "root": { "path", "source", "store_id"?, "role": "openspec_root" }, "members": [ { "role": "referenced_store", "id", "path"?, "remote"?, "fetch"?, "status": [] } ], "status": [] }. AVAILABLE = path presente E status vazio. --code-workspace <path> escreve {folders:[{name,path}]} (apenas lojas referenciadas disponíveis, prefixos ref:); no modo JSON, a escrita ocorre antes da impressão, então o stdout contém exatamente um documento mesmo em caso de falha de escrita. Falha: { "root": null, "members": [], "status": [d] }, sai com 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 = desconhecido/não sondado). Descobertas de saúde saem com 0; falhas saem com 1 com a forma nula correspondente. Cancelamento de prompt sai com 130.
4.13 schemas --json / templates --json
schemas: o sucesso permanece como um array nu [ {name, description, artifacts, source} ]; resolve a precedência canônica de seleção de raiz e aceita --store <id>. Falha de seleção de raiz: { "schemas": [], "root": null, "status": [d] }, sai com 1. templates: objeto chaveado { "<artifactId>": {path, source} }, ainda baseado no cwd sem chaves root/status.
5. Contrato de código de saída
| Situação | Saída | Stdout |
|---|---|---|
| Sucesso, incluindo achados de saúde (doctor/context/store doctor) | 0 | o payload |
Falha de comando no modo --json | 1 | um documento JSON com status: [d] e o formato nulo do comando |
validate com itens com falha | 1 | relatório completo |
Cancelamento de prompt (grupo store, modo humano) | 130 | apenas stderr |
6. Catálogo de códigos de diagnóstico
Resolução
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; pass-through: invalid_store_id, invalid_store_registry, invalid_store_metadata.
Saúde da raiz OpenSpec (erro, sem correção)
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 o beta de stores, openspec/specs/, openspec/changes/ e openspec/changes/archive/ podem estar ausentes em uma raiz saudável; eles só são erro de saúde quando presentes mas não são diretórios.
Registro/identidade/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).
Configuração/registro/remoção 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 em remove, erro em store 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).
Referências (aviso)
reference_invalid_id, reference_registry_unreadable, reference_unresolved, reference_root_unhealthy, reference_index_truncated.
Relacionamentos (aviso; doctor; context mantém apenas o de registro)
relationship_registry_unreadable, root_pointer_ignored, root_pointer_invalid, pointer_declarations_inert.
Arquivo (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.
Escritas de contexto
context_file_exists, context_output_dir_missing.
Fallbacks
doctor_failed, context_failed, store_error, change_error, archive_error.
Inconsistências conhecidas
Registradas pela auditoria capstone; renomeações de chaves publicadas são decisões de produto adiadas para além desta versão:
No modoCorrigido na rodada capstone gauntlet: itens desconhecidos e ambíguos em--json, vários caminhos de falha imprimiam apenas stderr sem documento JSON.show/validateemitem{status:[{code: unknown_item | ambiguous_item, ...}]}; erros lançados emstatus/instructions/list/show/validatepassam pelo helper de falha consciente de JSON (o formato nulo do comando +status);store <unknown subcommand> --jsonemite{status:[{code: unknown_store_subcommand}]};listcarrega seu formato nulo{changes|specs: [], root: null}em falhas de resolução.store_root_missingé emitido com duas severidades (aviso em remove, erro em store doctor) — dependente de contexto, documentado acima.- Casing snake_case (família store) vs camelCase (família workflow);
root.store_idé snake_case em todos os lugares. - Quatro declarações paralelas de tipos de envelope existem em src; diagnósticos de archive nunca carregam
target. list --jsonreutiliza a chavestatuscomo enum de string por change.- Apenas a saída de
validatecarrega um campoversion. templatesignora a seleção de raiz (baseado em cwd, sem--store).- Formas de substantivo descontinuadas (subcomandos
change/spec) emitem payloads sem envelope semroot/status.