Authoring OpenSpec (plugin CLI Kiro)
Cette compétence est une sous-procédure partagée invoquée par les compétences de la phase Chorus (/chorus-proposal, /chorus-develop, /chorus-yolo) chaque fois que l'utilisateur souhaite une authoring pilotée par spec via OpenSpec CLI. Elle est optionnelle :
- S'active quand les trois signaux sont vrais (voir §1) :
CHORUS_OPENSPEC_MODEn'est pasoff, un répertoireopenspec/existe à la racine du projet, et le CLIopenspecest surPATH. - Sinon, la compétence appelante revient à son comportement libre existant.
Quand vous atteignez un point dans proposal / develop / yolo où cette compétence est référencée, lisez la valeur de CHORUS_OPENSPEC_ACTIVE (voir §1) et déviez en fonction.
§1. Détection
Le hook agentSpawn de l'agent principal chorus de Chorus peut calculer CHORUS_OPENSPEC_ACTIVE une fois au démarrage et écrire une section ## OpenSpec Mode dans votre contexte de démarrage. Si vous voyez cette section, utilisez sa valeur ; sinon, exécutez la sonde manuelle ci-dessous. La valeur de CHORUS_OPENSPEC_ACTIVE est 1 uniquement quand tous les trois éléments suivants sont vrais :
CHORUS_OPENSPEC_MODEn'est pas défini suroff(l'opt-out explicite gagne).- La racine du projet contient un répertoire
openspec/(c.-à-d. que quelqu'un a exécutéopenspec initici). - Le CLI
openspecest surPATH.
Les deux signaux (2) et (3) sont obligatoires car le chemin d'authoring OpenSpec nécessite le répertoire de travail et le CLI — avoir l'un sans l'autre rend le workflow non exécutable. Si le signal (2) est vrai mais (3) ne l'est pas, proposez un indice « Dépôt OpenSpec détecté — installer avec : npm i -g @fission-ai/openspec » à l'utilisateur plutôt que de choisir silencieusement la forme libre.
Comment lire la valeur
Si votre contexte agentSpawn l'inclut, vous verrez quelque chose comme :
## OpenSpec Mode
CHORUS_OPENSPEC_ACTIVE=1 (openspec/ directory + openspec CLI both present)
ou :
## OpenSpec Mode
CHORUS_OPENSPEC_ACTIVE=0 (no openspec/ directory at /path/to/repo/openspec)
Déviez :
CHORUS_OPENSPEC_ACTIVE=1→ suivre §3 (authoring OpenSpec).CHORUS_OPENSPEC_ACTIVE=0→ revenir au chemin libre de la compétence appelante. Ne pas créeropenspec/changes/. Ne pas ajouter la ligne slug à la description de la proposition.
Sonde manuelle
Si vous n'avez pas vu de section ## OpenSpec Mode dans votre contexte (par ex. le hook agentSpawn ne l'a pas injecté, ou vous êtes un sous-agent), calculez la valeur vous-même avec les trois mêmes vérifications. Kiro CLI ne définit pas de variable d'env pour le répertoire du projet, donc sondez le répertoire de travail actuel :
if [ "${CHORUS_OPENSPEC_MODE:-}" = "off" ]; then
CHORUS_OPENSPEC_ACTIVE=0
elif [ ! -d "$PWD/openspec" ]; then
CHORUS_OPENSPEC_ACTIVE=0
elif ! openspec --version >/dev/null 2>&1; then
CHORUS_OPENSPEC_ACTIVE=0
else
CHORUS_OPENSPEC_ACTIVE=1
fi
§2. ⛔ Deux règles non négociables
Les deux sont appliquées au moment de l'examen. Les deux ont causé des incidents dans les versions passées.
Règle 1 — Miroir via le wrapper, jamais retaper le contenu du document depuis la sortie de l'agent
Les appels de miroir de document/brouillon (chorus_pm_add_document_draft, chorus_pm_update_document_draft, chorus_pm_update_document) DOIVENT passer par :
chorus-api.sh mcp-tool <tool_name> "$PAYLOAD"
chorus-api.sh est installé aux côtés des scripts de hook sous le répertoire chorus-bin/ de Kiro et est sur PATH — appelez-le par son nom.
avec $PAYLOAD construit en utilisant json_encode_file (défini en §3.4). Appeler ces outils directement depuis le harness MCP de l'agent avec un champ content retapé à la main est une violation de protocole pour le mode OpenSpec et échouera l'examen. Raisons :
- Coût des tokens. Retaper un corps markdown multi-millier de lignes à travers le LLM brûle les tokens d'entrée + sortie pour chaque brouillon. Le wrapper diffuse les bytes via
jq -Rs '.'— le contenu ne rentre jamais dans le contexte du LLM. Un miroir de proposition typique à 3 docs via le script coûte à peu près zéro content-tokens ; via MCP direct, il coûte régulièrement 20k+. - Égalité octet-par-octet.
jq -Rs '.'est un encodeur fidèle aux bytes : les antislashs, les guillemets, les retours à la ligne, le contenu des délimiteurs de code, les caractères de largeur zéro survivent tous. L'émission par LLM a un taux d'échec non nul sur long markdown — l'alignement du tableau dévie, les échappements de délimiteur sont « corrigés », les longues URL s'enroulent. La garantie d'égalité octet-par-octet (modulo\nfinal) ne tient que sur le chemin du wrapper. - Source unique de vérité. Avec le wrapper, le
openspec/changes/<slug>/*.mdlocal est autoritaire et Chorus en est le miroir. Avec la retape par agent, l'autorité se divise entre le fichier local et tout ce que le LLM a émis — un futur diff ne peut pas dire lequel est correct.
Règle 2 — Arrêter en cas d'erreur via chorus_check_response
Chaque appel wrapper doit vérifier trois signaux : le code de sortie du wrapper, "error": dans le corps, le corps vide. Bare RC=$? est insuffisant — le wrapper sort 0 sur HTTP 401 (échec d'authentification) avec le corps vide, donc une vérification à un seul signal manque silencieusement l'échec d'exécution le plus courant. Voir §6 pour la définition du helper.
§3. Authoring en mode OpenSpec
3.1 Choisir un slug
openspec/changes/<slug>/ est le dossier de changement local. Le slug doit être :
- en kebab-case (
add-export-csv, pasaddExportCsvouadd_export_csv), - dérivé du titre de l'Idée source,
- unique dans
openspec/changes/.
Enregistrez-le pour les étapes ultérieures :
SLUG="add-export-csv"
3.2 Créer le dossier de changement
openspec new change "$SLUG" --description "<résumé d'une ligne de l'idée>"
Cela crée openspec/changes/$SLUG/ avec README.md et .openspec.yaml. Ensuite, authorer à la main :
| Fichier local | Objectif | Miroir comme Document.type |
|---|---|---|
proposal.md |
Pourquoi + Quels Changements + Capacités + Impact | prd |
design.md |
Architecture, contrats, risques | tech_design |
specs/<capability>/spec.md |
Spec delta (## ADDED Requirements + Scenarios) |
spec (un brouillon par capacité) |
tasks.md |
Liste des tâches OpenSpec | (pas en miroir — les brouillons de tâches Chorus sont source de vérité) |
Utilisez openspec instructions <artifact> --change "$SLUG" (artifacts : proposal, specs, design, tasks) pour les templates.
3.3 Forme du fichier spec (vérifiée contre openspec instructions specs)
Une spec delta liste un ou plusieurs en-têtes de bloc — ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements, ## RENAMED Requirements — et dans chacun, des entrées ### Requirement:. Mixez librement dans le même fichier ; n'incluez que les blocs dont vous avez réellement besoin.
## ADDED Requirements
Ajouter une nouvelle Requirement à la spec de long terme.
## ADDED Requirements
### Requirement: <name>
<requirement text — utilisez SHALL / MUST pour le comportement normatif>
#### Scenario: <name>
- **WHEN** <condition>
- **THEN** <expected outcome>
## MODIFIED Requirements
Remplacement du bloc entier, pas fusion. Tout ce que vous écrivez ici remplace complètement la Requirement de même nom existante dans la spec de long terme — titre, description et tous les scénarios. Demi-écriture la supprime.
## MODIFIED Requirements
### Requirement: <existing name>
<full updated requirement text>
#### Scenario: <name>
- **WHEN** <condition>
- **THEN** <expected outcome>
#### Scenario: <other name>
- **WHEN** <condition>
- **THEN** <expected outcome>
Toujours inclure chaque scénario que vous voulez que la spec post-archive ait, même ceux qui étaient déjà présents et inchangés.
## REMOVED Requirements
Supprimer une Requirement de la spec de long terme. Le bloc sous l'en-tête est juste le(s) nom(s) de requirement que vous supprimez — aucun scénario nécessaire.
## REMOVED Requirements
### Requirement: <existing name>
## RENAMED Requirements
Renommer le titre d'une Requirement. Le corps et les scénarios sont préservés tels quels dans la spec de long terme ; utilisez MODIFIED à la place si vous devez changer autre chose que le titre.
## RENAMED Requirements
### Requirement: <old name> -> <new name>
Règles de formatage critiques (vérifiées) :
- Les Scenarios DOIVENT utiliser exactement 4 hashtags (
#### Scenario:). 3 hashtags ou une liste à puces échouent silencieusement la validation. - Chaque
### Requirement:sousADDEDouMODIFIEDDOIT avoir au moins un#### Scenario:. - Les blocs
MODIFIEDDOIVENT inclure le contenu mis à jour complet — ils réécrivent, ne corrigent pas. - Utilisez
SHALL/MUSTpour les requirements normatifs ; évitezshould/may. - La fusion dans
openspec/specs/<capability>/spec.mdse fait à l'heureopenspec archive(§3.9), non à l'heure de la proposition. Tandis que la proposition est en vol, Chorus ne voit que le fichier delta comme un Documentspec— il n'y a pas d'état demi-fusionné pour que la compétence raisonne.
Optionnel :
openspec validate "$SLUG"
3.4 Helper : json_encode_file
Définir une fois au début de la session d'authoring. Avec jq disponible, il diffuse le fichier dans une chaîne JSON ; le fallback correspond à l'échappement propre de chorus-api.sh quand jq manque.
json_encode_file() {
local _path="$1"
if command -v jq >/dev/null 2>&1; then
jq -Rs '.' < "$_path"
else
local _content
_content=$(cat "$_path")
_content=${_content//\\/\\\\}
_content=${_content//\"/\\\"}
_content=${_content//$'\n'/\\n}
printf '"%s"' "$_content"
fi
}
Aller-retour : le backend Chorus ajoute un seul \n au contenu du brouillon lors de l'écriture, donc le content serveur est octet-égal modulo une newline finale. Les examinateurs comparant le fichier local avec le serveur doivent ignorer cet octet.
3.5 Créer le conteneur de proposition avec la ligne de provenance du slug
Utilisez l'outil MCP chorus_pm_create_proposal régulier (aucun wrapper requis pour cet appel unique — la description est courte, la version émise par le LLM va bien). La description doit porter exactement une ligne :
OpenSpec change slug: <slug>
- sur sa propre ligne (pas d'autre texte sur cette ligne),
- préfixe littéral
OpenSpec change slug:(O capital, S capital, espace unique après les deux points), - pas de ponctuation finale,
- la valeur correspond au slug passé à
openspec new change.
Cette ligne est grep-able par machine par les exécutions futures de cette compétence et par le déclencheur d'archive §3.9.
3.6 Miroir chaque brouillon de document via le wrapper
Rappel de la Règle 1 : ces appels passent par
chorus-api.sh, pas MCP direct. L'agent ne doit pas retaper le corps du document.
Définir le helper halt-on-error du §6 une fois en haut, puis exécuter un appel par fichier :
# chorus-api.sh est sur PATH — aucun chemin absolu nécessaire.
# Brouillon PRD
CONTENT=$(json_encode_file "openspec/changes/$SLUG/proposal.md")
PAYLOAD=$(cat <<JSON
{
"proposalUuid": "$PROPOSAL_UUID",
"type": "prd",
"title": "PRD: $HUMAN_TITLE",
"content": $CONTENT
}
JSON
)
RESULT=$(chorus-api.sh mcp-tool chorus_pm_add_document_draft "$PAYLOAD")
RC=$?
chorus_check_response "chorus_pm_add_document_draft (prd)" "$RC" "$RESULT"
PRD_DRAFT_UUID=$(printf '%s' "$RESULT" | grep -o '"draftUuid"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
Répétez avec type: "tech_design" pour design.md, et un appel par capacité avec type: "spec" pour chaque specs/<capability>/spec.md. Ne pas mettre tasks.md en miroir — les brouillons de tâches Chorus (créés via l'outil MCP chorus_pm_add_task_draft, aucun wrapper nécessaire) sont la source de vérité pour les tâches.
Pourquoi le parsing utilise
printf '%s' "$RESULT" | greppasecho "$RESULT" | jq:echointerprète les séquences d'antislash à l'intérieur du JSON capturé, transformant\nintégré en vrai saut de ligne.jqs'arrête alors avecInvalid string: control characters from U+0000 through U+001F must be escaped.printf '%s'émet les bytes capturés verbatim. Le même motif s'applique à tout parsing de résultat wrapper dans cette compétence.
3.7 Édition d'un brouillon après le premier miroir
Les changements de fichier local se propagent via chorus_pm_update_document_draft — même wrapper, même json_encode_file, même vérification halt.
CONTENT=$(json_encode_file "openspec/changes/$SLUG/proposal.md")
PAYLOAD=$(cat <<JSON
{
"proposalUuid": "$PROPOSAL_UUID",
"draftUuid": "$PRD_DRAFT_UUID",
"content": $CONTENT
}
JSON
)
RESULT=$(chorus-api.sh mcp-tool chorus_pm_update_document_draft "$PAYLOAD")
RC=$?
chorus_check_response "chorus_pm_update_document_draft" "$RC" "$RESULT"
3.8 Édition d'un Document après approbation de proposition
Une fois la proposition approuvée, les brouillons se matérialisent en Documents avec leurs propres UUID. Pour garder openspec/changes/$SLUG/ et le Document Chorus synchronisés, mettez en miroir les éditions de fichier via chorus_pm_update_document :
CONTENT=$(json_encode_file "openspec/changes/$SLUG/specs/<capability>/spec.md")
PAYLOAD=$(cat <<JSON
{
"documentUuid": "$SPEC_DOCUMENT_UUID",
"content": $CONTENT
}
JSON
)
RESULT=$(chorus-api.sh mcp-tool chorus_pm_update_document "$PAYLOAD")
RC=$?
chorus_check_response "chorus_pm_update_document" "$RC" "$RESULT"
Pour re-dériver $SPEC_DOCUMENT_UUID depuis un shell frais, regardez-le via chorus_get_documents pour le projet de la proposition et correspondez par title + type. Re-dérivez $SLUG en grepping la description de la proposition pour ^OpenSpec change slug:.
3.9 Archive après vérification de la dernière tâche
Quand la DERNIÈRE tâche d'une idée en mode OpenSpec est admin-vérifiée via chorus_admin_verify_task, le hook postToolUse de l'agent principal chorus injecte un rappel contenant la sous-chaîne littérale openspec archive <slug> pour que vous puissiez agir sans re-lire le slug.
Le hook est en lecture seule ; vous (l'agent) effectuez l'archive :
-
Exécutez l'archive localement. Utilisez
--yespour le mode non interactif. NE PAS passer--skip-specs(annule le miroir-retour) ou--no-validate(laisse les deltas malformés corrompre les specs cumulatives).openspec archive "$SLUG" --yesCela déplace
openspec/changes/$SLUG/sousopenspec/changes/archive/<date>-<slug>/et émet/met à jouropenspec/specs/<capability>/spec.mdpour chaque capacité. (Exécutezopenspec archive --helpcontre votre version installée pour confirmer l'ensemble actuel de drapeaux — les drapeaux peuvent changer entre les versions.) -
Mettez en miroir chaque
openspec/specs/<capability>/spec.mdmis à jour vers le Document Chorus post-approbation correspondant (contrat §3.8).chorus_get_documentsne supporte que les filtres côté serveurprojectUuid+type; filtrez par titre côté client. Un appelchorus_pm_update_documentpar capacité. -
Arrêtez sur toute erreur de
openspec archiveouchorus_pm_update_document. Imprimez stderr verbatim, postez un commentaire sur la proposition enregistrant l'échec (chorus_add_commentavectargetType: "proposal",targetUuid: <proposalUuid>), puis arrêtez. Pas de retry. Correspond à §6 « pas d'erreurs silencieuses ». (Commentez la proposition, pas l'idée : l'échec est dans l'archivage des specs dérivées de proposition, et les propositions peuvent êtreinputType: "document"sans idée attachée.) -
Confirmez le succès. Listez les fichiers
openspec/specs/<capability>/spec.mdet vérifiez qu'ils font un aller-retour octet-égal (modulo newline finale) avec leurs homologues Document Chorus.
Opt-in strict : si la tâche vérifiée n'est pas la dernière de son idée, OU la description de la proposition ne porte pas de ligne OpenSpec change slug: <slug>, OU le shell local n'a pas le CLI openspec, le hook sort 0 silencieusement et aucun rappel d'archive n'est injecté. Le comportement libre existant est préservé.
§4. Authoring fallback (pas d'openspec)
Quand la détection place l'agent en mode fallback (CHORUS_OPENSPEC_ACTIVE=0), cette compétence est un no-op. Revenez au chemin libre de la compétence appelante :
- Aucun dossier
openspec/changes/n'est créé ou référencé. - Aucune ligne
OpenSpec change slug: …n'est ajoutée à la description de la proposition. - Les brouillons de document sont authoriés via des appels MCP directs
chorus_pm_add_document_draftaveccontenten ligne — comme avant qu'il n'existe cette compétence. - La Règle 1 (miroir wrapper-only) ne s'applique pas — il n'y a pas de source de vérité fichier local.
- Le hook d'archive §3.9 ne fait rien (pas de slug → sortie silencieuse).
§5. Tableau de référence de mappage des types de document
| Fichier local | Document.type Chorus |
Mis en miroir ? |
|---|---|---|
openspec/changes/<slug>/proposal.md |
prd |
oui |
openspec/changes/<slug>/design.md |
tech_design |
oui |
openspec/changes/<slug>/specs/<capability>/spec.md |
spec |
oui (un brouillon par capacité) |
openspec/changes/<slug>/tasks.md |
(pas mappé) | non — les brouillons de tâches Chorus sont source de vérité |
prd, tech_design, spec sont des valeurs Document.type valides pré-existantes — aucun changement de schéma requis.
§6. Visibilité des échecs — le helper chorus_check_response
Il y a un cas limite connu du wrapper : quand le serveur retourne HTTP 4xx (par ex. 401 d'une mauvaise CHORUS_API_KEY), chorus-api.sh mcp-tool capture le corps d'erreur JSON-RPC en interne, le piped via un filtre jq .result.content[]? qui ne produit aucune sortie quand .result est absent, et sort 0 avec stdout vide. Une vérification de bare RC=$? ne s'arrêterait pas sur ceci — le mode d'échec d'exécution le plus courant serait invisible.
Définir ce helper une fois en haut de la session d'authoring et l'utiliser après chaque appel wrapper :
chorus_check_response() {
local _tool="$1"
local _rc="$2"
local _body="$3"
local _has_error=0
local _is_empty=0
local _trimmed
_trimmed=$(printf '%s' "$_body" | tr -d ' \t\n\r')
[ -z "$_trimmed" ] && _is_empty=1
if [ "$_is_empty" -eq 0 ]; then
if command -v jq >/dev/null 2>&1; then
if printf '%s' "$_body" | jq -e 'try ([.. | objects | has("error")] | any) catch false' >/dev/null 2>&1; then
_has_error=1
fi
else
printf '%s' "$_body" | grep -qE '"error"[[:space:]]*:' && _has_error=1
fi
fi
if [ "$_rc" -ne 0 ] || [ "$_has_error" -eq 1 ] || [ "$_is_empty" -eq 1 ]; then
echo "ERROR: $_tool failed (exit=$_rc, error_in_body=$_has_error, empty_body=$_is_empty)" >&2
echo "Output: $_body" >&2
[ "$_rc" -ne 0 ] && exit "$_rc" || exit 1
fi
}
Anti-motifs — ne pas :
- S'effondrer à
|| true. - Rediriger stderr vers
/dev/null. - Enterrer l'appel wrapper à l'intérieur d'un pipeline (masque
$?). - Oublier de capturer
$RESULTdans une variable ; le helper a besoin du corps. - Utiliser uniquement
if [ "$RC" -ne 0 ]; then ...— ça rate le chemin d'erreur HTTP.
Forme minimaliste du site d'appel :
RESULT=$(chorus-api.sh mcp-tool <tool_name> "$PAYLOAD")
RC=$?
chorus_check_response "<tool_name>" "$RC" "$RESULT"
# ...si on arrive ici, l'appel a réussi ; parsez RESULT et continuez.
C'est la politique de projet : pas d'erreurs silencieuses.
§7. Checklist de référence rapide
Quand invoqué depuis une compétence de phase (/chorus-proposal / /chorus-develop / /chorus-yolo) :
- Lisez
CHORUS_OPENSPEC_ACTIVEdepuis la section## OpenSpec Modedans votre contexteagentSpawn(§1). Si ce n'est pas là, revenez à la sonde manuelle de §1. - Si
CHORUS_OPENSPEC_ACTIVE=0→ revenez au chemin libre de l'appelant (§4). - Sinon :
a. Choisir
$SLUG(§3.1). b.openspec new change "$SLUG"(§3.2). c. Authorerproposal.md,design.md,specs/<capability>/spec.md(§3.2–§3.3). Mélangez les blocsADDED/MODIFIED/REMOVED/RENAMEDselon les besoins ; souvenez-vous queMODIFIEDréécrit la Requirement entière. d. Optionnel :openspec validate "$SLUG". e.chorus_pm_create_proposal(MCP direct) avec la ligneOpenSpec change slug: $SLUGdans la description (§3.5). f. Définir les helpersjson_encode_file,chorus_check_response. (chorus-api.shest sur PATH — aucune variable$APInécessaire.) g. Pour chaque ligne dans §5 avec « oui » — mettez en miroir viachorus-api.sh mcp-tool chorus_pm_add_document_draft(§3.6). Enregistrez chaque$DRAFT_UUID. h. Sur toutchorus_check_responseéchoué — arrêtez, surface l'erreur, ne continuez PAS. - Éditions avant approbation → §3.7. Éditions après approbation → §3.8.
- Dernière tâche vérifiée → hook se déclenche → exécutez le flux d'archive §3.9.