Contrat d'agent OpenSpec
Surfaces lisibles par machine de la CLI openspec, vérifiées par rapport à src/ (audit de synthèse, 2026-06-11). Chaque forme ci-dessous est documentée à partir du code émetteur.
1. Conventions générales
- Un document JSON par invocation. En mode
--json, stdout transporte exactement un document JSON (indentation de 2 espaces). La prose destinée aux humains, les spinners et la bannière du store sont envoyés vers stderr. - Bannière du store. En mode humain, la racine sélectionnée par le store imprime
Using OpenSpec root: <id> (<path>)sur stderr. Jamais imprimée en mode JSON. - La casse des clés dépend de la surface (voir les Incohérences connues) : les charges utiles store/doctor/context utilisent
snake_case; les charges utiles des workflows (status,instructions,new change,validate,list) utilisentcamelCase, sauf l'objetrootincorporé, qui utilise toujoursstore_id. - Les clés optionnelles sont omises, non nulles, dans la plupart des charges utiles (par ex.
root.store_id,member.path). Les exceptions qui utilisent unnullexplicite sont signalées pour chaque forme (store doctorgit.*, charges utiles d'échec).
2. L'enveloppe de diagnostic
Une forme d'enveloppe est partagée par tous les diagnostics lisibles par machine (StoreDiagnostic) :
{
"severity": "error" | "warning" | "info",
"code": "snake_case_string",
"message": "human sentence",
"target": "dotted.surface (optional)",
"fix": "one actionable sentence/command (optional)"
}Les diagnostics apparaissent à deux emplacements : tableaux d'état (status: StoreDiagnostic[] au niveau supérieur ou par entrée) pour les résultats de santé, et erreurs levées converties en un tableau status à un seul élément en cas d'échec de la commande.
3. Sélection de la racine et RootOutput
Toutes les commandes de résolution de racine (list, show, validate, status, instructions, instructions apply, instructions archive, new change, archive, doctor, context, schemas) résolvent une seule racine OpenSpec selon une seule priorité :
--store <id>→ la racine du magasin enregistré (source: "store").- Sinon, l'ancêtre le plus proche contenant
openspec/: forme de planification →source: "nearest"(un pointeurstore:est ignoré avec un avertissement stderr) ; répertoire uniquement config avec un pointeurstore:valide → ce magasin,source: "declared". - Aucune racine proche +
defaultStoreglobal défini (openspec config set defaultStore <id>) → ce magasin,source: "global_default"; un identifiant périmé échoue avec l'erreur sous-jacente du magasin et unfixindiquantopenspec config unset defaultStore. - Aucune racine proche, pas de défaut + magasins enregistrés existants → erreur
no_root_with_registered_stores. - Pas de racine, pas de défaut, pas de magasins : les commandes peuvent traiter le répertoire de travail actuel comme
source: "implicit";doctor,context,listetvalidateen lot échouent plutôt avecno_openspec_root.listconserve la substitution implicite pour les projets hérités avecopenspec/project.md.
Les charges utiles JSON réussies intègrent normalement la racine ; schemas --json réussi reste délibérément le tableau nu documenté dans §4.13 :
"root": { "path": "/abs/path", "source": "store" | "declared" | "global_default" | "nearest" | "implicit", "store_id": "id (only when store-selected)" }Contrat d'échec de racine : en mode JSON, un échec de résolution affiche { ...commandNullShape, "status": [diagnostic] } sur stdout et quitte avec le code 1.
4. Formes JSON des commandes
4.1 list --json
{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress" } ], "root": RootOutput } — notez que le status par changement est ici une énumération de chaînes. --specs: { "specs": [ { "id", "requirementCount" } ], "root" }.
4.2 show <item> --json
Changement : { "id", "title", "deltaCount", "deltas": [...], "root" }. Spécification : { "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" }. Quitte avec le code 1 si un élément échoue.
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 signifie que chaque artefact de planification non ignoré existe ; les artefacts ignorés sont comptés comme satisfaits sans être créés. Cela ne signifie pas que les tâches d'implémentation sont terminées. isComplete est conservé en tant qu'alias de compatibilité avec la même valeur. Le requires de chaque artefact contient ses identifiants de dépendance directe (présents pour chaque statut, afin que l'ensemble requis transitif soit calculable même lorsque l'artefact est done) ; missingDeps n'apparaît que lorsque blocked. Le tableau artifacts est dans l'ordre des dépendances, avec l'ordre de déclaration artifacts: du schéma servant à départager les artefacts qui deviennent prêts en même temps (jamais alphabétique), de sorte que la première entrée ready est l'artefact à écrire ensuite ; missingDeps utilise le même ordre. "skipped" marque un artefact dont le chemin generates se trouve sous specs/ dans un changement dont .openspec.yaml déclare skip_specs: true ; il satisfait les dépendances mais ne doit pas être créé. Aucun changement actif : { "changes": [], "message", "root" }, quitte avec le code 0.
--all (lot, mutuellement exclusif avec --change — leur combinaison est une erreur avec la forme nulle { "changes": [], "root": null, "status": [d] }) : { "changes": [ <objet de statut par changement, sans racine par changement>, ... ], "root" }, trié par nom de changement. Un changement qui échoue à charger contribue { "changeName", "status": [d] } à sa place ; le balayage continue, préserve l'enveloppe complète et quitte avec le code 1 dans les modes texte et JSON. Un --schema invalide fait échouer toute l'invocation avec la forme nulle, même lorsqu'il n'y a aucun changement.
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 liste les artefacts que celui-ci rend prêt, dans l'ordre de déclaration du schéma (le même ordre que status recommande). "skipped": true (avec "warning") apparaît lorsque le changement déclare skip_specs: true et que cet artefact est ignoré — ne pas créer ses fichiers. Une entrée de dépendance avec skipped: true est satisfaite sans fichiers — ne pas essayer de lire ses chemins.
ReferenceIndexEntry : { "store_id", "root"?, "specs"?: [{id,summary}], "fetch"?, "status": [] } — les entrées résolues portent root/specs/fetch ; les non résolues portent store_id + statut d'avertissement. Index limité à 50 Ko (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 est ce sur quoi apply bloque (le apply.requires du schéma) ; missingPrerequisites est tout ce qui reste à construire avant que apply puisse s'exécuter, dans l'ordre de construction - la fermeture transitive de ces requires, donc cela peut être la liste la plus longue. warnings liste les problèmes non bloquants liés au changement lui-même - aujourd'hui, un changement qui est prêt à implémenter sans spécifications delta et sans skip_specs: true, l'état que openspec validate rejette. Les deux champs root optionnels (context, operationGuidance) sont lus depuis la racine sélectionnée à chaque invocation. context est une entrée requise au niveau du prompt dont les faits pertinents du projet, conventions et contraintes doivent être appliqués ; operationGuidance est une entrée consultative dont les entrées ne sont suivies que lorsqu'elles sont applicables et compatibles avec le flux de travail intégré. Ils restent séparés de l'état, des tâches, des progrès, des fichiers contextuels et de l'instruction intégrée.
4.7 instructions archive --json
{ "changeName", "context"?, "operationGuidance"?, "root" }. Nécessite un --change valide dans la racine du dépôt/magasin résolu et utilise les mêmes sémantiques de contexte requis/guidance consultative que apply. Il s'agit d'une surface d'entrée d'exécution en lecture seule : elle ne retourne pas le flux de travail d'archive statique, n'inspecte ni ne fusionne les spécifications delta, n'écrit pas les spécifications principales, ni ne déplace le changement.
4.8 new change <name> --json
Succès : { "change": { "id", "path", "metadataPath", "schema" }, "root" }. Échec : { "change": null, "status": [d] }, quitte avec le code 1.
4.9 archive <name> --json
Succès : { "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"?, "warnings"? }, "root" }. Échec : { "archive": null, "root"?, "status": [d] }, quitte avec le code 1. specsUpdated est vrai uniquement lorsqu'au moins un fichier de spécification a été écrit ou retiré (une capacité dont la dernière exigence a été supprimée par le changement voit sa spécification supprimée, ce qui nécessite retire_capabilities: true dans .openspec.yaml du changement ; chaque retrait est nommé dans warnings, avec une commande Git collable uniquement lorsque la spécification vivait dans le checkout de l'appelant) ; un changement déjà synchronisé s'archive avec des totaux nuls et les sauts listés dans warnings. Le mode JSON est strictement non interactif : chaque point de prompt devient un code 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 (présent uniquement pour un checkout de magasin basé sur git qui dispose d'une référence de suivi amont) est le compte ahead/behind par rapport au dernier amont récupéré, et non par rapport à l'amont en direct. Les résultats de santé de toute gravité quittent avec le code 0. Charge utile d'échec : { "root": null, "store": null, "references": [], "status": [d] }, quitte avec le code 1.
4.11 context --json
{ "root": { "path", "source", "store_id"?, "role": "openspec_root" }, "members": [ { "role": "referenced_store", "id", "path"?, "remote"?, "fetch"?, "status": [] } ], "status": [] }. DISPONIBLE = chemin présent ET statut vide. --code-workspace <path> écrit {folders:[{name,path}]} (magasins référencés disponibles uniquement, préfixes ref:) ; en mode JSON, l'écriture s'exécute avant l'impression afin que stdout contienne exactement un document même en cas d'échec de l'écriture. Échec : { "root": null, "members": [], "status": [d] }, quitte avec le code 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 = inconnu/non sondé). Les résultats de santé quittent avec le code 0 ; les échecs quittent avec le code 1 avec la forme nulle correspondante. L'annulation par invite quitte avec le code 130.
4.13 schemas --json / templates --json
schemas : le succès reste un tableau nu [ {name, description, artifacts, source} ] ; il résout la priorité de sélection de racine canonique et accepte --store <id>. Échec de sélection de racine : { "schemas": [], "root": null, "status": [d] }, quitte avec le code 1. templates : objet indexé { "<artifactId>": {path, source} }, toujours basé sur cwd sans clés root/status.
5. Contrat de code de sortie
| Situation | Code de sortie | Sortie standard |
|---|---|---|
| Succès, y compris les résultats de santé (doctor/context/store doctor) | 0 | la charge utile |
Échec de commande en mode --json | 1 | un document JSON avec status: [d] et la forme nulle de la commande |
validate avec des éléments en échec | 1 | rapport complet |
Annulation du prompt (groupe store, mode humain) | 130 | stderr uniquement |
6. Catalogue des codes de diagnostic
Résolution
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.
Santé de la racine OpenSpec (erreur, sans correctif)
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. Pendant la bêta des stores, openspec/specs/, openspec/changes/ et openspec/changes/archive/ peuvent être absents dans une racine saine ; ils ne constituent des erreurs de santé que s'ils sont présents mais ne sont pas des répertoires.
Registre/identité/état du 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).
Configuration/enregistrement/suppression du 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 (avertissement lors de la suppression, erreur dans doctor), store_root_not_directory.
Git du store
store_git_init_failed, store_git_identity_missing, store_git_commit_failed, store_git_no_commits (avertissement), store_clone_fragile_directories (avertissement), store_remote_divergence (info, doctor), store_checkout_drift (info, doctor).
Références (avertissement)
reference_invalid_id, reference_registry_unreadable, reference_unresolved, reference_root_unhealthy, reference_index_truncated.
Relations (avertissement ; doctor ; context ne conserve que celle du registre)
relationship_registry_unreadable, root_pointer_ignored, root_pointer_invalid, pointer_declarations_inert.
Archive (mode 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.
Écritures de contexte
context_file_exists, context_output_dir_missing.
Repli
doctor_failed, context_failed, store_error, change_error, archive_error.
Incohérences connues
Enregistrées par l'audit capstone ; les renommages de clés publiées sont des décisions produit reportées au-delà de cette version :
En modeCorrigé lors de la manche capstone gauntlet : les éléments inconnus et ambigus de--json, plusieurs chemins d'échec n'affichaient que stderr sans document JSON.show/validateémettent{status:[{code: unknown_item | ambiguous_item, ...}]}; les erreurs levées dansstatus/instructions/list/show/validatepassent par l'assistant d'échec prenant en compte le JSON (la forme nulle de la commande +status) ;store <sous-commande inconnue> --jsonémet{status:[{code: unknown_store_subcommand}]};listporte sa forme nulle{changes|specs: [], root: null}lors des échecs de résolution.store_root_missingest émis avec deux niveaux de gravité (avertissement lors de la suppression, erreur dans store doctor) — dépendant du contexte, documenté ci-dessus.- snake_case (famille store) vs camelCase (famille workflow) pour la casse des clés ;
root.store_idest en snake_case partout. - Quatre déclarations parallèles de types d'enveloppe existent dans src ; les diagnostics d'archive ne portent jamais
target. list --jsonréutilise la cléstatuscomme enum de chaîne par changement.- Seule la sortie de
validateporte un champversion. templatesignore la sélection de racine (basé sur le cwd, sans--store).- Les formes nominales dépréciées (sous-commandes
change/spec) émettent des charges utiles non enveloppées sansroot/status.