Authoring compatible avec OpenSpec (plugin OpenClaw)
Ce skill est une sous-procédure partagée invoquée par les skills de la scène Chorus (proposal, develop, yolo) chaque fois que l'utilisateur souhaite un authoring piloté par spec via la CLI OpenSpec. C'est un opt-in :
- 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 dansPATH. - Sinon le skill appelant revient à son comportement free-form existant.
Espace de noms des outils : les outils Chorus MCP sont exposés sous un préfixe
chorus__sur OpenClaw (ex.chorus__chorus_pm_create_proposal). Des noms nus sont utilisés en prose pour la lisibilité — ajoutezchorus__en invoquant directement les outils MCP. Les appels document-mirror ne passent PAS du tout par le harnais MCP — ils passent par le wrapperchorus-api.sh(voir §2 Règle 1), qui communique avec le endpoint Chorus MCP via HTTP en utilisant votre clé API, indépendamment du namespacingchorus__.
§1. Détection — exécution inline, à chaque fois (pas de hook SessionStart sur OpenClaw)
Différence OpenClaw : le plugin Claude Code pré-calcule
CHORUS_OPENSPEC_ACTIVEune fois dans un hook SessionStart et l'injecte en contexte. OpenClaw n'exécute pas ce hook. Vous DEVEZ calculer l'activité vous-même, inline, dès que vous atteignez ce skill à partir d'un skill de scène. Ne cherchez pas une valeur injectéeCHORUS_OPENSPEC_ACTIVE— elle n'existera pas sur OpenClaw.
Calculez la valeur avec les trois vérifications. CHORUS_OPENSPEC_ACTIVE vaut 1 uniquement quand les trois conditions sont vraies :
CHORUS_OPENSPEC_MODEn'est pas défini àoff(l'opt-out explicite prime).- La racine du projet contient un répertoire
openspec/(c.-à-d. quelqu'un a exécutéopenspec initici). - La CLI
openspecest dansPATH.
Les deux signaux (2) et (3) sont requis car le chemin authoring OpenSpec a besoin du répertoire de travail et de la CLI — avoir l'un sans l'autre rend le workflow inutilisable. Si le signal (2) est présent mais (3) ne l'est pas, affichez un conseil à l'utilisateur — "Dépôt OpenSpec détecté — installez avec : npm i -g @fission-ai/openspec" — plutôt que de choisir silencieusement le free-form.
Bloc de détection inline (exécutez ceci)
# Exécutez les trois vérifications directement. PROJECT_DIR est votre racine de projet
# (OpenClaw n'exporte pas CLAUDE_PROJECT_DIR — utilisez par défaut $PWD).
PROJECT_DIR="${PWD}"
if [ "${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 # envisagez d'afficher le conseil d'installation à l'utilisateur
else
CHORUS_OPENSPEC_ACTIVE=1
fi
echo "CHORUS_OPENSPEC_ACTIVE=$CHORUS_OPENSPEC_ACTIVE"
Branchez selon le résultat :
CHORUS_OPENSPEC_ACTIVE=1→ suivez §3 (authoring OpenSpec).CHORUS_OPENSPEC_ACTIVE=0→ retournez au chemin free-form du skill appelant. Ne créez pas d'échafaudageopenspec/changes/. N'ajoutez pas la ligne slug à la description de la proposal.
Exécutez cette détection inline chaque fois que proposal / develop / yolo font référence à ce skill. Il n'y a pas de valeur injectée par l'hôte à lire sur OpenClaw ; recalculer les trois vérifications est le contrat.
§2. ⛔ Deux règles non négociables
Les deux sont appliquées au moment de la review. Les deux ont causé des incidents dans les versions antérieures.
Règle 1 — Mirroir via le wrapper, ne jamais retaper le contenu du document depuis la sortie de l'agent
Les appels document/draft mirror (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"
avec $PAYLOAD construit en utilisant json_encode_file (défini en §3.4). Appeler ces outils directement depuis le harnais MCP de l'agent avec un champ content retapé à la main est une violation de protocole pour le mode OpenSpec et échouera la review. Raisons :
- Coût en tokens. Retaper un corps markdown de plusieurs milliers de lignes via le LLM brûle les tokens d'entrée + sortie pour chaque draft. Le wrapper fait circuler les octets via
jq -Rs '.'— le contenu n'entre jamais en contexte LLM. Un mirroir de proposal typique à 3 docs via le script coûte à peu près zéro tokens de contenu ; via MCP direct cela coûte régulièrement 20k+. - Égalité au niveau des octets.
jq -Rs '.'est un encodeur byte-faithful : les antislashs, guillemets, sauts de ligne, contenu des délimiteurs de code, caractères de largeur zéro survivent tous. La ré-émission par le LLM a un taux d'échec non nul sur du long markdown — l'alignement des tableaux dérive, les échappements des délimiteurs se font « corriger », les longues URLs s'enroulent. La garantie d'égalité au niveau des octets (modulo\nfinal) tient uniquement sur le chemin du wrapper. - Source unique de vérité. Avec le wrapper, le
openspec/changes/<slug>/*.mdlocal est autoritaire et Chorus est un miroir. Avec la ré-émission par l'agent, l'autorité se partage entre le fichier local et ce que le LLM a émis — un diff futur ne peut pas dire quel est le bon.
Disponibilité de
chorus-api.shsur OpenClaw. Ce wrapper est le transport document-mirror. Il doit être accessible souschorus-api.shdansPATH(le bundle standalone skill Chorus l'inclut ; si vous avez installé via ce bundle il est déjà dansPATH). S'il n'est pas dansPATHdans votre environnement OpenClaw, faites l'une des choses suivantes :
- l'appeler par son chemin absolu (ex.
"$HOME/.chorus/bin/chorus-api.sh" mcp-tool ...), ou- reproduire son unique comportement inline — POSTez un JSON-RPC
tools/callpour<tool_name>avec arguments$PAYLOADà"$CHORUS_URL/api/mcp"avec en-têteAuthorization: Bearer $CHORUS_API_KEYen utilisantcurl, capturant le corps brut pour la vérification halt-on-error de §6.Le wrapper requiert
CHORUS_URLetCHORUS_API_KEYdans l'environnement (mêmes valeurs que votre config de pluginchorusUrl/apiKey). Exportez-les avant le premier appel s'ils ne sont pas déjà définis. Ce que vous ne DEVEZ PAS faire c'est retaper le corps du document via le modèle — la règle wrapper-only tient indépendamment de comment vous invoquez le wrapper.
Règle 2 — Halt on error 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 sort 0 sur HTTP 401 (échec auth) avec corps vide, donc une vérification single-signal manque silencieusement l'échec runtime 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 local de changement. Le slug doit être :
- kebab-case (
add-export-csv, pasaddExportCsvouadd_export_csv), - dérivé du titre Idea source,
- unique dans
openspec/changes/.
Enregistrez-le pour les étapes suivantes :
SLUG="add-export-csv"
3.2 Créer l'échafaudage du dossier de changement
openspec new change "$SLUG" --description "<résumé idée d'une ligne>"
Cela crée openspec/changes/$SLUG/ avec README.md et .openspec.yaml. Puis authoritez à la main :
| Fichier local | Objectif | Mirroir en tant que Document.type |
|---|---|---|
proposal.md |
Pourquoi + Quels Changements + Capacités + Impact | prd |
design.md |
Architecture, contrats, risques | tech_design |
specs/<capability>/spec.md |
Delta spec (## ADDED Requirements + Scenarios) |
spec (un draft par capacité) |
tasks.md |
Liste des tâches OpenSpec | (non mirrorisé — les task drafts Chorus sont la 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 delta spec liste un ou plusieurs en-têtes de bloc — ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements, ## RENAMED Requirements — et dedans, des entrées ### Requirement:. Mélangez librement dans le même fichier ; n'incluez que les blocs dont vous avez vraiment besoin.
## ADDED Requirements
Ajoutez une Requirement toute neuve au spec long terme.
## ADDED Requirements
### Requirement: <nom>
<texte requirement — utilisez SHALL / MUST pour le comportement normatif>
#### Scenario: <nom>
- **WHEN** <condition>
- **THEN** <résultat attendu>
## 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 le spec long terme — titre, description, et tous les scénarios. La moitié-écrire supprime le reste.
## MODIFIED Requirements
### Requirement: <nom existant>
<texte 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 le spec post-archive ait, même ceux qui étaient déjà présents et inchangés.
## REMOVED Requirements
Supprimez une Requirement du spec long terme. Le bloc sous l'en-tête est juste le(s) nom(s) requirement que vous supprimez — pas de scénarios nécessaires.
## REMOVED Requirements
### Requirement: <nom existant>
## RENAMED Requirements
Renommez le titre d'une Requirement. Le corps et les scénarios sont préservés tels quels dans le spec long terme ; utilisez MODIFIED à la place si vous avez besoin de changer autre chose que le titre.
## RENAMED Requirements
### Requirement: <ancien nom> -> <nouveau nom>
Règles de formatage critiques (vérifiées) :
- Les Scénarios 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, ne corrigent pas. - Utilisez
SHALL/MUSTpour les requirements normatives ; évitezshould/may. - La fusion dans
openspec/specs/<capability>/spec.mdse fait au moment deopenspec archive(§3.9), pas au moment de proposal. Tant que la proposal est en vol, Chorus voit uniquement le fichier delta comme un Documentspec— il n'y a pas d'état semi-fusionné pour le skill de raisonner.
Optionnel :
openspec validate "$SLUG"
3.4 Helper : json_encode_file
Définissez une fois au début de la session authoring. Avec jq disponible il fait circuler 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 \n unique au contenu draft en écriture, donc content serveur est byte-égal modulo une newline finale. Les reviewers diffant fichier local vs serveur devraient ignorer cet unique octet.
3.5 Créer le conteneur proposal avec la ligne de provenance slug
Utilisez le tool 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 fine). La description doit porter exactement une ligne :
OpenSpec change slug: <slug>
- sur sa propre ligne (pas autre texte sur cette ligne),
- préfixe littéral
OpenSpec change slug:(O majuscule, S majuscule, espace unique après deux-points), - pas de ponctuation finale,
- la valeur correspond au slug passé à
openspec new change.
Cette ligne est grepp-able en machine par les exécutions futures de ce skill et par le déclencheur archive de §3.9.
3.6 Mirroir chaque document draft via le wrapper
Rappel Règle 1 : ces appels passent par
chorus-api.sh, pas MCP direct. L'agent ne doit pas retaper le corps du document.
Assurez-vous que CHORUS_URL et CHORUS_API_KEY sont exportés (note Règle 1). Définissez le helper halt-on-error de §6 une fois au sommet, puis exécutez un appel par fichier :
# Draft 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 mirroisez pas tasks.md — les task drafts Chorus (créés via le tool MCP chorus_pm_add_task_draft, pas de wrapper requis) sont la source de vérité pour les tâches.
Pourquoi le parsing utilise
printf '%s' "$RESULT" | grepet pasecho "$RESULT" | jq:echointerprète les séquences antislash dedans le JSON capturé, transformant\nembarqué en vraie newline.jqabandonne 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 pattern s'applique à tout parsing de résultat wrapper dans ce skill.
3.7 Éditer un draft après le premier mirroir
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 Éditer un Document après approbation de proposal
Une fois la proposal approuvée, les drafts se matérialisent en Documents avec leurs propres UUIDs. Pour garder openspec/changes/$SLUG/ et le Document Chorus synchronisés, mirrorez les édits 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 ré-dériver $SPEC_DOCUMENT_UUID depuis un shell frais, cherchez-le via chorus_get_documents pour le project de la proposal et faites correspondre par title + type. Ré-dérivez $SLUG en greppant la description de la proposal pour ^OpenSpec change slug:.
3.9 Archive après vérification de la dernière tâche
Différence OpenClaw : le plugin Claude Code a un hook PostToolUse (
bin/on-post-verify-task.sh) qui se déclenche aprèschorus_admin_verify_tasket injecte un rappelopenspec archive <slug>. OpenClaw 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 qui vient d'être vérifiée était la DERNIÈRE tâche de son idée mode OpenSpec (chaque Task de chaque Proposal approuvée de cette idée est maintenantdone/closed, et la proposaldescriptionporte une ligneOpenSpec change slug: <slug>). Si oui, lancez le flux archive ci-dessous. Sinon, ne faites rien.
Quand le déclencheur tient, vous effectuez l'archive :
-
Exécutez l'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" --yesCela déplace
openspec/changes/$SLUG/sousopenspec/changes/archive/<date>-<slug>/et émet/met à jouropenspec/specs/<capability>/spec.mdpour chaque capacité. (Lancezopenspec archive --helpcontre votre version installée pour confirmer le set de drapeaux courant — les drapeaux peuvent dériver entre les versions.) -
Mirrorez chaque
openspec/specs/<capability>/spec.mdmis à jour vers le Document Chorus post-approbation correspondant (contrat §3.8).chorus_get_documentsne supporte que les filtres serveurprojectUuid+type; filtrez par title client-side. Un appelchorus_pm_update_documentpar capacité. -
Halte sur toute erreur de
openspec archiveouchorus_pm_update_document. Affichez stderr verbatim, postez un commentaire sur la proposal enregistrant l'échec (chorus_add_commentavectargetType: "proposal",targetUuid: <proposalUuid>), puis arrêtez. Pas de retry. Correspond à §6 "pas d'erreurs silencieuses." (Commentez sur la proposal, pas l'idée : l'échec est dans l'archivage des specs dérivées de proposal, et les proposals peuvent êtreinputType: "document"sans idée attachée.) -
Confirmez succès. Listez les fichiers
openspec/specs/<capability>/spec.mdet vérifiez qu'ils font aller-retour byte-é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 proposal ne porte pas de ligne OpenSpec change slug: <slug>, OU le shell local n'a pas de CLI openspec, ne faites rien — pas d'archive. Le comportement free-form existant est préservé.
§4. Authoring fallback (sans openspec)
Quand la détection de §1 place l'agent en mode fallback (CHORUS_OPENSPEC_ACTIVE=0), ce skill est un no-op. Retournez au chemin free-form 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 proposal. - Les document drafts sont authorisés via des appels MCP
chorus_pm_add_document_draftdirects aveccontentinline — comme avant que ce skill existe. - La Règle 1 (mirroir wrapper-only) ne s'applique pas — il n'y a pas de source de vérité fichier local.
- Le flux archive de §3.9 ne fait rien (pas de slug → pas d'archive).
§5. Mappage de type de document (tableau de référence)
| Fichier local | Chorus Document.type |
Mirrorisé ? |
|---|---|---|
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 draft par capacité) |
openspec/changes/<slug>/tasks.md |
(non mappé) | non — les task drafts Chorus sont la source de vérité |
prd, tech_design, spec sont des valeurs Document.type valides pré-existantes — aucun changement de schema requis.
§6. Visibilité des défaillances — le helper chorus_check_response
Il y a un cas limite connu du wrapper : quand le serveur retourne HTTP 4xx (ex. 401 d'une mauvaise CHORUS_API_KEY), chorus-api.sh mcp-tool capture le corps d'erreur JSON-RPC en interne, le pipe à travers un filtre jq .result.content[]? qui ne produit aucune sortie quand .result est absent, et sort 0 avec stdout vide. Une vérification RC=$? seule ne halterait pas cela — le mode d'échec runtime le plus courant serait invisible. (Si vous aviez reproduit le wrapper inline via curl par la note Règle 1, la même vérification à trois signaux s'applique au corps HTTP brut.)
Définissez ce helper une fois au top de la session authoring et utilisez-le 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-patterns — ne pas :
- Réduire à
|| true. - Rediriger stderr vers
/dev/null. - Enterrer l'appel wrapper dedans une pipeline (masque
$?). - Passer de capturer
$RESULTdans une variable ; le helper a besoin du corps. - Utiliser uniquement
if [ "$RC" -ne 0 ]; then ...— cela manque le chemin erreur-HTTP.
Forme d'appel site minimale :
RESULT=$(chorus-api.sh mcp-tool <tool_name> "$PAYLOAD")
RC=$?
chorus_check_response "<tool_name>" "$RC" "$RESULT"
# ...si nous atteignons ici, l'appel a réussi ; parsez RESULT et continuez.
C'est la politique projet-wide : pas d'erreurs silencieuses.
§7. Checklist de référence rapide
Quand invoqué depuis un skill de scène (proposal / develop / yolo) :
- Exécutez la détection à trois vérifications inline de §1 vous-même (pas de hook SessionStart sur OpenClaw). Calculez
CHORUS_OPENSPEC_ACTIVEde :CHORUS_OPENSPEC_MODE != off+ répertoireopenspec/présent + CLIopenspecsur PATH. - Si
CHORUS_OPENSPEC_ACTIVE=0→ retournez au chemin free-form du caller (§4). - Sinon :
a. Choisissez
$SLUG(§3.1). b.openspec new change "$SLUG"(§3.2). c. Authorisezproposal.md,design.md,specs/<capability>/spec.md(§3.2–§3.3). Mélangez les blocsADDED/MODIFIED/REMOVED/RENAMEDselon les besoins ; souvenez-vous queMODIFIEDécrase la Requirement entière. d. Optionnel :openspec validate "$SLUG". e.chorus_pm_create_proposal(MCP direct) avec la ligneOpenSpec change slug: $SLUGen description (§3.5). f. ExportezCHORUS_URL/CHORUS_API_KEY; définissez les helpersjson_encode_file,chorus_check_response; confirmez quechorus-api.shest accessible (note Règle 1). g. Pour chaque ligne en §5 avec "oui" — mirrorez viachorus-api.sh mcp-tool chorus_pm_add_document_draft(§3.6). Enregistrez chaque$DRAFT_UUID. h. Sur toutechorus_check_responseéchouée — halte, surfacez l'erreur, ne procédez pas. - Éditions avant approbation → §3.7. Éditions après approbation → §3.8.
- Dernière tâche vérifiée → détectez le déclencheur vous-même (pas de hook) → lancez le flux archive §3.9.