Authoring en connaissant OpenSpec (skill dsh plugin)
Ce skill est une sous-procédure partagée invoquée par les skills de stage Chorus (proposal, develop, yolo) chaque fois que l'utilisateur souhaite un authoring piloté par spec via la CLI OpenSpec. C'est un choix explicite :
- S'active quand les trois signaux sont présents (voir §1) :
CHORUS_OPENSPEC_MODEn'est pasoff, un répertoireopenspec/existe à la racine du projet, et la CLIopenspecest surPATH. - Sinon, le skill appelant revient à son comportement existant en mode libre.
Espace de noms des outils : Les outils Chorus MCP sont exposés avec un préfixe
mcp__chorus__sur dsh (ex.mcp__chorus__chorus_pm_create_proposal). Les noms nus sont utilisés dans le texte pour la lisibilité — préfixez avecmcp__chorus__lors de l'invocation directe des outils MCP. Les appels miroir-document ne passent PAS du tout par le harness MCP — ils passent par le wrapper localchorus-mcp-call.mjs(voir §2 Règle 1), qui communique avec l'endpoint Chorus MCP par HTTP en utilisant votre clé API, indépendamment du préfixemcp__chorus__.
§1. Détection — lire la valeur précomputée, sinon exécuter les trois vérifications en ligne
Différence dsh : le plugin Claude Code précompute
CHORUS_OPENSPEC_ACTIVEdans un hook SessionStart. dsh n'a pas de hook SessionStart, mais le bundle chorus-dsh précompute la même valeur au chargement du plugin et l'exporte comme variable d'environnementCHORUS_OPENSPEC_ACTIVE— il exécute les trois vérifications ci-dessous contre le répertoire de travail du processus, avant la porte daemon-origin, de sorte que les sessions interactives et celles déclenchées par daemon en héritent. Lisez cette valeur quand elle est définie ; ne la recalculez en ligne que si elle est absente (ancien bundle, ou la variable a été explicitement effacée).
CHORUS_OPENSPEC_ACTIVE vaut 1 uniquement quand les trois vérifications sont vraies :
CHORUS_OPENSPEC_MODEn'est pas défini àoff(le refus explicite gagne).- La racine du projet contient un répertoire
openspec/(c.-à-d. quelqu'un a exécutéopenspec initici). - La CLI
openspecest surPATH.
Les deux signaux (2) et (3) sont obligatoires car le chemin authoring OpenSpec a besoin du répertoire de travail et de la CLI — en avoir un sans l'autre rend le workflow inutilisable. Si le signal (2) est vrai mais pas (3), affichez un conseil à l'utilisateur — « Repo OpenSpec détecté — installer avec : npm i -g @fission-ai/openspec » — plutôt que de choisir silencieusement le mode libre.
Bloc de détection (exécuter ceci)
# Préférer la valeur que le bundle chorus-dsh a précomputée au chargement ;
# revenir aux trois vérifications quand elle n'est pas définie. PROJECT_DIR est votre racine
# (dsh n'exporte pas CLAUDE_PROJECT_DIR — défaut à $PWD).
PROJECT_DIR="${PWD}"
if [ -n "${CHORUS_OPENSPEC_ACTIVE:-}" ]; then
: # déjà précomputé par le bundle chorus-dsh — l'utiliser tel quel
elif [ "${CHORUS_OPENSPEC_MODE:-}" = "off" ]; then
CHORUS_OPENSPEC_ACTIVE=0
elif [ ! -d "${PROJECT_DIR}/openspec" ]; then
CHORUS_OPENSPEC_ACTIVE=0
elif ! openspec --version >/dev/null 2>&1; then
CHORUS_OPENSPEC_ACTIVE=0 # envisager d'afficher le conseil d'installation à l'utilisateur
else
CHORUS_OPENSPEC_ACTIVE=1
fi
echo "CHORUS_OPENSPEC_ACTIVE=$CHORUS_OPENSPEC_ACTIVE"
Bifurquez sur le résultat :
CHORUS_OPENSPEC_ACTIVE=1→ suivre §3 (authoring OpenSpec).CHORUS_OPENSPEC_ACTIVE=0→ revenir au chemin libre du skill appelant. Ne pas scaffolderopenspec/changes/. Ne pas ajouter la ligne slug à la description de la proposition.
Exécutez ceci chaque fois que proposal / develop / yolo référence ce skill : lisez le CHORUS_OPENSPEC_ACTIVE précomputé s'il est présent, sinon recalculez les trois vérifications.
§2. ⛔ Deux règles non négociables
Les deux sont appliquées au moment de la revue. Les deux ont causé des incidents dans les versions antérieures.
Règle 1 — Miroir via le wrapper, jamais retaper le contenu du document à partir de la sortie de l'agent
Les appels miroir document/brouillon (chorus_pm_add_document_draft, chorus_pm_update_document_draft, chorus_pm_update_document) DOIVENT passer par :
"$CHORUS_MCP_CALL" <tool_name> "$PAYLOAD"
avec $PAYLOAD construit avec json_encode_file (défini en §3.4). Appeler ces outils directement depuis le harness MCP de l'agent avec un champ content dactylographié à la main est une violation de protocole pour le mode OpenSpec et ne passera pas la revue. Raisons :
- Coût en tokens. Retaper un corps markdown multi-milliers de lignes via le LLM brûle des tokens d'entrée + sortie pour chaque brouillon.
json_encode_file(§3.4) encode le fichier avecJSON.stringifyde Node — le contenu n'entre jamais dans le contexte du LLM. Un miroir de proposition typique à 3 docs via le script coûte à peu près zéro token de contenu ; via MCP direct, cela coûte régulièrement 20k+. - Égalité des octets.
JSON.stringifydes octets UTF-8 du fichier est un encodeur fidèle aux octets : les antislash, guillemets, sauts de ligne, contenu de barrière de code, caractères de largeur nulle survivent tous. La réémission LLM a un taux d'échec non nul sur le markdown long — l'alignement du tableau dérive, les échappements de barrière sont « corrigés », les long URLs s'enroulent. La garantie d'égalité des octets (modulo un\nfinal) ne tient que sur le chemin du wrapper. - Source de vérité unique. Avec le wrapper, le fichier local
openspec/changes/<slug>/*.mdfait autorité et Chorus est un miroir. Avec la retape de l'agent, l'autorité se divise entre le fichier local et ce que le LLM a émis — un futur diff ne peut pas dire lequel est correct.
Disponibilité du wrapper sur dsh. Le bundle npm publie le chemin du wrapper dans
CHORUS_MCP_CALLau chargement du plugin. Validez-le avant authoring :if [ -z "${CHORUS_MCP_CALL:-}" ] || [ ! -x "$CHORUS_MCP_CALL" ]; then echo "ERROR: OpenSpec mirroring requires the package-local CHORUS_MCP_CALL wrapper; reload the Chorus dsh bundle." >&2 exit 1 fiLe wrapper lit
CHORUS_URLetCHORUS_API_KEYdepuis l'environnement du processus dsh. S'il est absent, arrêtez visiblement. Ne le reproduisez pas ad hoc et ne retapez pas le contenu du document via le modèle.
Règle 2 — Arrêtez en cas d'erreur via chorus_check_response
Chaque appel wrapper doit vérifier trois signaux : code de sortie du wrapper, "error": dans le corps, corps vide. Un simple RC=$? est insuffisant — le wrapper quitte 0 sur HTTP 401 (échec d'auth) avec un 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 de la fonction d'aide.
§3. Authoring en mode OpenSpec
3.1 Choisir un slug
openspec/changes/<slug>/ est le dossier de changement local. Le slug doit être :
- kebab-case (
add-export-csv, pasaddExportCsvouadd_export_csv), - dérivé du titre de l'Idea source,
- unique au sein de
openspec/changes/.
Enregistrez-le pour les étapes suivantes :
SLUG="add-export-csv"
3.2 Scaffolder le dossier de changement
openspec new change "$SLUG" --description "<résumé en une ligne de l'idée>"
Ceci crée openspec/changes/$SLUG/ avec README.md et .openspec.yaml. Ensuite, écrivez à 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 + Scénarios) |
spec (un brouillon par capacité) |
tasks.md |
Liste des tâches OpenSpec | (non mirrorisée — les brouillons de tâches Chorus font autorité) |
Utilisez openspec instructions <artifact> --change "$SLUG" (artifacts: proposal, specs, design, tasks) pour les modèles.
3.3 Forme du fichier spec (vérifiée contre openspec instructions specs)
Un spec delta liste un ou plusieurs en-têtes de bloc — ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements, ## RENAMED Requirements — et au sein de chacun, des entrées ### Requirement:. Mélangez librement dans le même fichier ; n'incluez que les blocs dont vous avez besoin.
## ADDED Requirements
Ajouter une nouvelle Requirement à la spec à long terme.
## ADDED Requirements
### Requirement: <nom>
<texte de requirement — utiliser SHALL / MUST pour le comportement normatif>
#### Scenario: <nom>
- **WHEN** <condition>
- **THEN** <résultat attendu>
## MODIFIED Requirements
Remplacement de bloc complet, pas fusion. Tout ce que vous écrivez ici remplace complètement la Requirement de même nom existante dans la spec à long terme — titre, description et tous les scénarios. La demi-écrire supprime le reste.
## MODIFIED Requirements
### Requirement: <nom existant>
<texte de requirement complet mis à jour>
#### Scenario: <nom>
- **WHEN** <condition>
- **THEN** <résultat attendu>
#### Scenario: <autre nom>
- **WHEN** <condition>
- **THEN** <résultat attendu>
Incluez toujours chaque scénario que vous voulez que la spec post-archivage ait, même ceux qui étaient déjà présents et inchangés.
## REMOVED Requirements
Supprimer une Requirement de la spec à 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: <nom existant>
## RENAMED Requirements
Renommer le titre d'une Requirement. Le corps et les scénarios sont conservés tels quels dans la spec à long terme ; utilisez MODIFIED à la place si vous devez changer autre chose que le titre.
## RENAMED Requirements
### Requirement: <ancien nom> -> <nouveau nom>
Règles de formatage critiques (vérifiées) :
- Les Scenarios DOIVENT utiliser exactement 4 dièses (
#### Scenario:). 3 dièses ou une liste à puces échouent silencieusement la validation. - Chaque
### Requirement:sousADDEDouMODIFIEDDOIT avoir au moins un#### Scenario:. - Les blocs
MODIFIEDDOIVENT inclure le contenu complet mis à jour — ils écrasent, ils ne corrigent pas. - Utilisez
SHALL/MUSTpour les requirements normatifs ; évitezshould/may. - La fusion dans
openspec/specs/<capability>/spec.mdse fait à l'heure deopenspec archive(§3.9), pas à l'heure de la proposition. Pendant que la proposition est en cours, Chorus ne voit que le fichier delta comme un seul Documentspec— il n'y a pas d'état mi-fusionné pour que le skill raisonne.
Optionnel :
openspec validate "$SLUG"
3.4 Aide : json_encode_file
Définir une fois en haut de la session authoring. Il encode le fichier dans une chaîne JSON fidèle aux octets avec JSON.stringify de Node — Node est garanti présent sous dsh (c'est le runtime du harness), donc aucun jq n'est requis.
json_encode_file() {
# Fidèle aux octets : JSON.stringify du contenu UTF-8 du fichier — guillemets,
# antislash, sauts de ligne, contenu de barrière de code, et caractères de contrôle survivent tous.
# Pas de jq, pas de curl.
node -e 'const fs=require("fs");process.stdout.write(JSON.stringify(fs.readFileSync(process.argv[1],"utf8")))' "$1"
}
Aller-retour : le backend Chorus ajoute un unique \n au contenu du brouillon à l'écriture, donc le content serveur est byte-equal modulo une newline finale. Les relecteurs comparant le fichier local vs le serveur doivent ignorer cet unique octet.
3.5 Créer le conteneur de proposition avec la ligne de provenance du slug
Utiliser l'outil MCP régulier chorus_pm_create_proposal (pas de wrapper requis pour cet appel unique — la description est courte, la version émise par le LLM est correcte). 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 majuscule, S majuscule, un seul espace après les deux points), - pas de ponctuation finale,
- la valeur correspond au slug passé à
openspec new change.
Cette ligne est searchable par grep-machine par les exécutions futures de ce skill et par le déclencheur d'archive §3.9.
3.6 Miroir chaque brouillon de document via le wrapper
Rappel Règle 1 : ces appels passent par
chorus-mcp-call.mjs, pas MCP direct. L'agent ne doit pas retaper le corps du document.
Résolvez CHORUS_MCP_CALL comme spécifié en Règle 1. Définissez la fonction d'aide halt-on-error de §6 une fois en haut, puis exécutez un appel par fichier :
# 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_MCP_CALL" 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 mirroiser tasks.md — les brouillons de tâches Chorus (créés via l'outil MCP chorus_pm_add_task_draft, pas de wrapper nécessaire) font autorité pour les tâches.
Pourquoi le parsing utilise
printf '%s' "$RESULT" | grepet pasecho "$RESULT" | jq:echointerprète les séquences d'antislash à l'intérieur du JSON capturé, transformant\nimbriqué en une vraie newline.jqavorte alors avecInvalid string: control characters from U+0000 through U+001F must be escaped.printf '%s'émet les octets capturés verbatim. Le même motif s'applique à tout parsing de résultat-wrapper dans ce skill.
3.7 Éditer 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_MCP_CALL" chorus_pm_update_document_draft "$PAYLOAD")
RC=$?
chorus_check_response "chorus_pm_update_document_draft" "$RC" "$RESULT"
3.8 Éditer un Document après l'approbation de la 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 en sync, mirroir 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_MCP_CALL" chorus_pm_update_document "$PAYLOAD")
RC=$?
chorus_check_response "chorus_pm_update_document" "$RC" "$RESULT"
Pour re-dériver $SPEC_DOCUMENT_UUID d'un shell frais, cherchez-le via chorus_get_documents pour le projet de la proposition et appariez par title + type. Re-dérivez $SLUG en grepant la description de la proposition pour ^OpenSpec change slug:.
3.9 Archive après la dernière tâche vérifiée
Différence dsh : le plugin Claude Code a un hook PostToolUse (
bin/on-post-verify-task.sh) qui s'exécute aprèschorus_admin_verify_tasket injecte un rappelopenspec archive <slug>. dsh n'a pas de tel hook. Vous (l'agent) devez détecter le déclencheur vous-même : après chaquechorus_admin_verify_task, vérifiez si la tâche juste vérifiée était la DERNIÈRE tâche de son idea en mode OpenSpec (chaque Task sur chaque Proposal approuvée de cette idea est maintenantdone/closed, et la description de la proposition porte une ligneOpenSpec change slug: <slug>). Si oui, exécutez le flux d'archive ci-dessous. Sinon, ne faites rien.
Quand le déclencheur se déclenche, vous effectuez l'archive :
-
Exécutez archive localement. Utilisez
--yespour le mode non-interactif. Ne passez pas--skip-specs(annule le mirror-back) ou--no-validate(laisse les deltas malformés corrompre les specs cumulatives).openspec archive "$SLUG" --yesCeci 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 la série de flags actuelle — les flags peuvent dériver entre les versions.) -
Mirroir 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 title côté client. Un appelchorus_pm_update_documentpar capacité. -
Arrêtez en cas d'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 stoppez. Pas de retry. Correspond à §6 « aucune erreur silencieuse ». (Commentez la proposition, pas l'idea : l'échec est dans l'archivage des specs dérivées de la proposition, et les propositions peuvent êtreinputType: "document"sans idea attachée.) -
Confirmez le succès. Listez les fichiers
openspec/specs/<capability>/spec.mdet vérifiez qu'ils font un aller-retour byte-equal (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 idea, OU la description de la proposition ne porte pas de ligne OpenSpec change slug: <slug>, OU le shell local n'a pas la CLI openspec, ne faites rien — pas d'archive. Le comportement libre existant est préservé.
§4. Authoring de fallback (pas d'openspec)
Quand la détection §1 met l'agent en mode fallback (CHORUS_OPENSPEC_ACTIVE=0), ce skill est un no-op. Revenez au chemin libre du skill appelant :
- 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 documents sont créés via les appels MCP
chorus_pm_add_document_draftdirects aveccontenten ligne — comme avant l'existence de ce skill. - La Règle 1 (mirror wrapper-only) ne s'applique pas — il n'y a pas de source de vérité de fichier local.
- Le flux d'archive §3.9 ne fait rien (pas de slug → pas d'archive).
§5. Tableau de référence de mappage des types de documents
| Fichier local | Document.type Chorus |
Mirroisé ? |
|---|---|---|
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 |
(non mappé) | non — les brouillons de tâches Chorus font autorité |
prd, tech_design, spec sont des valeurs Document.type valides pré-existantes — aucun changement de schéma requis.
§6. Visibilité des échecs — la fonction d'aide chorus_check_response
Le wrapper Node quitte non-zéro sur les échecs de transport, HTTP 4xx/5xx, et les corps JSON-RPC error (ex. un 401 d'une mauvaise CHORUS_API_KEY quitte 2, une erreur au niveau tool quitte 4). Vérifiez quand même les trois signaux ci-dessous en défense en profondeur : un simple RC=$? va bien pour les cas courants mais cette fonction d'aide attrape aussi un corps 200 bien formé qui porte néanmoins un objet "error", et un corps inopinément vide.
Définissez cette fonction d'aide une fois en haut de la session authoring et utilisez-la 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
# Pas de jq requis : une correspondance de sous-chaîne sur une clé "error" suffit pour les
# outils mirror, dont les corps de succès ne portent aucun champ top-level "error".
printf '%s' "$_body" | grep -qE '"error"[[:space:]]*:' && _has_error=1
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 :
- Réduire à
|| true. - Rediriger stderr vers
/dev/null. - Enterrer l'appel wrapper à l'intérieur d'un pipeline (masque
$?). - Sauter la capture de
$RESULTdans une variable ; la fonction d'aide a besoin du corps. - Utiliser seulement
if [ "$RC" -ne 0 ]; then ...— cela manque le chemin d'erreur HTTP.
Forme minimale du site d'appel :
RESULT=$("$CHORUS_MCP_CALL" <tool_name> "$PAYLOAD")
RC=$?
chorus_check_response "<tool_name>" "$RC" "$RESULT"
# ...si nous arrivons ici, l'appel a réussi ; parser RESULT et continuer.
C'est une politique à l'échelle du projet : aucune erreur silencieuse.
§7. Liste de contrôle de référence rapide
Quand invoqué d'un skill de stage (proposal / develop / yolo) :
- Exécutez la détection §1 des trois vérifications vous-même (pas de hook SessionStart sur dsh). Calculez
CHORUS_OPENSPEC_ACTIVEà partir de :CHORUS_OPENSPEC_MODE != off+ répertoireopenspec/présent + CLIopenspecsur PATH. - Si
CHORUS_OPENSPEC_ACTIVE=0→ revenir au chemin libre du caller (§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 ; rappelez-vous queMODIFIEDécrase la Requirement complète. d. Optionnel :openspec validate "$SLUG". e.chorus_pm_create_proposal(MCP direct) avec la ligneOpenSpec change slug: $SLUGdans la description (§3.5). f. Valider le chemin exécutable dans$CHORUS_MCP_CALL; définirjson_encode_fileetchorus_check_response. g. Pour chaque ligne de §5 avec « oui » — miroir via"$CHORUS_MCP_CALL" chorus_pm_add_document_draft(§3.6). Enregistrer chaque$DRAFT_UUID. h. En cas d'échec dechorus_check_response— halte, exposer l'erreur, NE PAS continuer. - Éditions avant approbation → §3.7. Éditions après approbation → §3.8.
- Dernière tâche vérifiée → détecter le déclencheur vous-même (pas de hook) → exécuter le flux d'archive §3.9.