openspec-aware

Par chorus-aidlc · chorus

Création en mode OpenSpec opt-in pour les workflows Chorus PM dans Claude Code. Détecte le CLI `openspec` local, génère l'arborescence `openspec/changes/<slug>/` sur le disque et synchronise les fichiers Markdown dans les brouillons de documents Chorus via le wrapper `chorus-api.sh`. Lecture obligatoire pour les skills proposal, develop et yolo lorsque l'utilisateur dispose du CLI `openspec`.

npx skills add https://github.com/chorus-aidlc/chorus --skill openspec-aware

Authoring conscient d'OpenSpec (plugin Claude Code)

Cette compétence est une sous-procédure partagée invoquée par les compétences de la phase Chorus (proposal, develop, yolo) chaque fois que l'utilisateur souhaite une authoring guidée par spec via la CLI OpenSpec. Elle est optionnelle :

  • S'active quand les trois signaux sont présents (voir §1) : CHORUS_OPENSPEC_MODE n'est pas off, un répertoire openspec/ existe à la racine du projet, et la CLI openspec est sur PATH.
  • 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 depuis le contexte SessionStart (voir §1) et branchez selon elle. N'exécutez pas à nouveau le bloc de détection — le hook SessionStart l'a déjà fait une fois pour cette session.


§1. Détection — déjà effectuée au SessionStart

Le hook SessionStart du plugin Chorus (bin/on-session-start.sh) calcule CHORUS_OPENSPEC_ACTIVE une seule fois à l'ouverture de la session et écrit une section ## OpenSpec Mode dans le contexte injecté du plugin. La valeur de CHORUS_OPENSPEC_ACTIVE est 1 uniquement quand les trois conditions suivantes sont vraies :

  1. CHORUS_OPENSPEC_MODE n'est pas défini à off (l'opt-out explicite prévaut).
  2. La racine du projet contient un répertoire openspec/ (c.-à-d. quelqu'un a lancé openspec init ici).
  3. La CLI openspec est sur PATH.

Les deux signaux (2) et (3) sont obligatoires car le chemin d'authoring OpenSpec a besoin du répertoire de travail et de la CLI — en avoir un sans l'autre rend le flux inexécutable. Si le signal (2) tient mais (3) ne tient pas, le hook SessionStart affiche un indice « OpenSpec repo détecté — installer avec : npm i -g @fission-ai/openspec » à l'utilisateur ; l'agent devrait le transmettre s'il est interrogé plutôt que de choisir silencieusement le libre-service.

Comment lire la valeur

Vous devriez déjà voir quelque chose comme ceci dans votre contexte (cherchez la section ## OpenSpec Mode près du haut de la conversation) :

## 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)

Branchez :

  • CHORUS_OPENSPEC_ACTIVE=1 → suivez §3 (authoring OpenSpec).
  • CHORUS_OPENSPEC_ACTIVE=0 → revenez au chemin libre-service de la compétence appelante. Ne créez pas openspec/changes/. N'ajoutez pas la ligne slug à la description de la proposition.

Secours manuel

Si vous êtes dans un sous-shell, un sous-agent, ou une session qui n'a pas vu le contexte SessionStart (p. ex. vous avez été créé en milieu de session et le contexte du parent n'a pas été transmis), reconstruisez la valeur vous-même avec les trois mêmes contrôles :

if [ "${CHORUS_OPENSPEC_MODE:-}" = "off" ]; then
  CHORUS_OPENSPEC_ACTIVE=0
elif [ ! -d "${CLAUDE_PROJECT_DIR:-$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

N'utilisez ceci que quand le contexte SessionStart est véritablement indisponible — dupliquer la détection est gaspilleur quand le hook l'a déjà calculée.


§2. ⛔ Deux règles non négociables

Les deux sont appliquées au moment de la révision. Les deux ont causé des incidents dans les versions antérieures.

Règle 1 — Miroir via le wrapper, ne jamais retaper le contenu de document depuis la sortie de l'agent

Les appels de miroir 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 sur PATH — appelez-le par nom.

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 tapé à la main est une violation de protocole pour le mode OpenSpec et échouera à la révision. Raisons :

  1. Coût en tokens. Retaper un corps markdown de plusieurs milliers de lignes via le LLM consomme des tokens d'entrée + sortie pour chaque brouillon. Le wrapper envoie les octets via jq -Rs '.' — le contenu n'entre jamais dans le contexte LLM. Un miroir de proposition typique avec 3 docs via le script coûte à peu près zéro token de contenu ; via MCP direct, c'est couramment 20k+.
  2. Égalité au byte. jq -Rs '.' est un encodeur fidèle aux octets : les antislashs, guillemets, sauts de ligne, contenu des barrières de code, caractères de largeur nulle survivent tous. La réémission LLM a un taux d'échec non nul sur le long markdown — l'alignement du tableau déraille, les échappements de barrière sont « corrigés », les longues URLs s'enroulent. La garantie d'égalité au byte (modulo \n de fin) tient uniquement sur le chemin du wrapper.
  3. Source unique de vérité. Avec le wrapper, le openspec/changes/<slug>/*.md local est autoritaire et Chorus est un miroir. Avec la retape par l'agent, l'autorité se divise entre le fichier local et ce que le LLM a émis — un diff futur 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 : code de sortie du wrapper, "error": dans le corps, corps vide. Un bare RC=$? est insuffisant — le wrapper sort 0 sur HTTP 401 (échec d'auth) avec un corps vide, donc un contrôle à signal unique manque silencieusement le mode d'é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 de changement local. Le slug doit être :

  • kebab-case (add-export-csv, pas addExportCsv ou add_export_csv),
  • dérivé du titre de l'Idée source,
  • unique au sein de openspec/changes/.

Enregistrez-le pour les étapes ultérieures :

SLUG="add-export-csv"

3.2 Structurer le dossier de changement

openspec new change "$SLUG" --description "<résumé d'idée d'une ligne>"

Cela crée openspec/changes/$SLUG/ avec README.md et .openspec.yaml. Puis authoring à 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 de tâches OpenSpec (non miroir — les brouillons de tâches Chorus sont source de vérité)

Utilisez openspec instructions <artifact> --change "$SLUG" (artefacts : proposal, specs, design, tasks) pour les modèles.

3.3 Forme de 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 à l'intérieur, des entrées ### Requirement:. Mélangez librement dans le même fichier ; incluez uniquement les blocs dont vous avez réellement besoin.

## ADDED Requirements

Ajouter une nouvelle Exigence à la spec à long terme.

## ADDED Requirements

### Requirement: <name>
<texte d'exigence — utilisez SHALL / MUST pour comportement normatif>

#### Scenario: <name>
- **WHEN** <condition>
- **THEN** <résultat attendu>

## MODIFIED Requirements

Remplacement de bloc entier, pas fusion. Tout ce que vous écrivez ici remplace complètement l'Exigence portant le même nom dans la spec à long terme — titre, description, et tous les scénarios. L'écrire partiellement supprime le reste.

## MODIFIED Requirements

### Requirement: <existing name>
<texte d'exigence complet mis à jour>

#### Scenario: <name>
- **WHEN** <condition>
- **THEN** <résultat attendu>

#### Scenario: <other name>
- **WHEN** <condition>
- **THEN** <résultat attendu>

Incluez toujours chaque scénario que vous souhaitez que la spec post-archive ait, même ceux qui étaient déjà présents et inchangés.

## REMOVED Requirements

Supprimer une Exigence de la spec à long terme. Le bloc sous l'en-tête n'est que le(s) nom(s) d'exigence que vous supprimez — aucun scénario nécessaire.

## REMOVED Requirements

### Requirement: <existing name>

## RENAMED Requirements

Renommer le titre d'une Exigence. Le corps et les scénarios sont préservés tels quels dans la spec à long terme ; utilisez MODIFIED à la place si vous devez changer quelque chose d'autre que le titre.

## RENAMED Requirements

### Requirement: <old name> -> <new name>

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: sous ADDED ou MODIFIED DOIT avoir au moins un #### Scenario:.
  • Les blocs MODIFIED DOIVENT inclure le contenu complet mis à jour — ils écrasent, ne corrigent pas.
  • Utilisez SHALL / MUST pour les exigences normatives ; évitez should / may.
  • La fusion dans openspec/specs/<capability>/spec.md se produit au moment openspec archive (§3.9), pas au moment de la proposition. Pendant que la proposition est en vol, Chorus voit uniquement le fichier delta comme un Document spec — il n'y a pas d'état mi-fusionné pour que la compétence raisonne.

Optionnel :

openspec validate "$SLUG"

3.4 Helper : json_encode_file

Définir une seule fois au sommet de la session d'authoring. Avec jq disponible, il envoie le fichier dans une chaîne JSON ; le fallback correspond à l'encodage 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
}

Round-trip : le backend Chorus ajoute un \n unique au contenu du brouillon lors de l'écriture, donc le serveur content est égal au byte modulo un saut de ligne de fin. Les relecteurs comparant un fichier local vs serveur doivent ignorer cet unique octet.

3.5 Créer le conteneur de proposition avec la ligne de provenance du slug

Utilisez l'outil MCP régulier chorus_pm_create_proposal (aucun wrapper requis pour cet appel unique — la description est courte, la version émise par LLM est fine). La description doit contenir exactement une ligne :

OpenSpec change slug: <slug>
  • sur sa propre ligne (aucun autre texte sur cette ligne),
  • préfixe littéral OpenSpec change slug: (O majuscule, S majuscule, espace unique après deux-points),
  • aucune ponctuation de fin,
  • la valeur correspond au slug transmis à openspec new change.

Cette ligne est cherchable par machine-grep 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 Règle 1 : ces appels passent par chorus-api.sh, pas MCP direct. L'agent ne doit pas retaper le corps du document.

Définissez le helper d'arrêt en cas d'erreur de §6 une seule fois au sommet, puis lancez 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 miroir tasks.md — 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 l'analyse utilise printf '%s' "$RESULT" | grep et non echo "$RESULT" | jq : echo interprète les séquences de backslash à l'intérieur du JSON capturé, transformant \n intégré en vrai saut de ligne. jq abandonne alors avec Invalid 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 à toute analyse de résultat wrapper dans cette compétence.

3.7 Éditer un brouillon après le premier miroir

Les modifications de fichier local se propagent via chorus_pm_update_document_draft — même wrapper, même json_encode_file, même vérification d'arrêt.

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 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 synchronisés, miror les modifications 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 d'un shell frais, recherchez-le via chorus_get_documents pour le projet de la proposition et correspondez par title + type. Re-dérivez $SLUG en greppant la description de la proposition pour ^OpenSpec change slug:.

3.9 Archive après la dernière tâche vérifiée

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 du plugin (bin/on-post-verify-task.sh) injecte un rappel additionalContext contenant la sous-chaîne littérale openspec archive <slug> pour que vous agissiez sans re-lire le slug.

Le hook est en lecture seule ; vous (l'agent) effectuez l'archive :

  1. Exécutez l'archive localement. Utilisez --yes pour le mode non-interactif. Ne passez pas --skip-specs (annule le miroir-retour) ou --no-validate (laisse les deltas malformés corrompre les specs cumulatives).

    openspec archive "$SLUG" --yes

    Cela déplace openspec/changes/$SLUG/ sous openspec/changes/archive/<date>-<slug>/ et émet/met à jour openspec/specs/<capability>/spec.md pour chaque capacité. (Lancez openspec archive --help contre votre version installée pour confirmer l'ensemble de drapeaux actuel — les drapeaux peuvent dériver entre les versions.)

  2. Miror chaque openspec/specs/<capability>/spec.md mis à jour vers le Document Chorus post-approbation correspondant (contrat §3.8). chorus_get_documents prend uniquement en charge les filtres serveur projectUuid + type ; filtrez par titre côté client. Un appel chorus_pm_update_document par capacité.

  3. Arrêtez en cas d'erreur depuis openspec archive ou chorus_pm_update_document. Afficher stderr verbatim, publier un commentaire sur la proposition enregistrant l'échec (chorus_add_comment avec targetType: "proposal", targetUuid: <proposalUuid>), puis arrêtez. Aucune réessai. Correspond à §6 « aucune erreur silencieuse ». (Commentez la proposition, pas l'idée : l'échec réside dans l'archivage des specs dérivées de la proposition, et les propositions peuvent être inputType: "document" sans idée attachée.)

  4. Confirmez succès. Listez les fichiers openspec/specs/<capability>/spec.md et vérifiez qu'ils font un round-trip égal au byte (modulo saut de ligne de fin) 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 contient pas de ligne OpenSpec change slug: <slug>, OU le shell local n'a pas de CLI openspec, le hook sort 0 silencieusement et aucun rappel d'archive n'est injecté. Le comportement libre existant est préservé.


§4. Authoring de secours (pas d'openspec)

Quand la détection met l'agent en mode secours (CHORUS_OPENSPEC_ACTIVE=0), cette compétence est un no-op. Revenez au chemin libre-service 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 authoring via appels MCP chorus_pm_add_document_draft directs avec content inline — même qu'avant que cette compétence existe.
  • La Règle 1 (miroir wrapper-uniquement) 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. Table de référence de mappage de type de document

Fichier local Chorus Document.type 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 (non 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é de l'échec — le helper chorus_check_response

Il y a un cas limite connu du wrapper : quand le serveur retourne HTTP 4xx (p. 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. Un contrôle bare RC=$? ne s'arrêterait pas — le mode d'échec runtime le plus courant serait invisible.

Définissez ce helper une seule fois au sommet de la session d'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-motifs — ne pas :

  • Réduire à || true.
  • Rediriger stderr vers /dev/null.
  • Enterrer l'appel wrapper à l'intérieur d'un pipeline (masque $?).
  • Ignorer de capturer $RESULT dans une variable ; le helper a besoin du corps.
  • Utiliser uniquement if [ "$RC" -ne 0 ]; then ... — cela manque le chemin d'erreur HTTP.

Forme de site d'appel minimal :

RESULT=$(chorus-api.sh mcp-tool <tool_name> "$PAYLOAD")
RC=$?
chorus_check_response "<tool_name>" "$RC" "$RESULT"
# ...si nous arrivons ici, l'appel a réussi ; analysez RESULT et continuez.

C'est une politique au niveau du projet : aucune erreur silencieuse.


§7. Checklist de référence rapide

Lors de l'invocation depuis une compétence de phase (proposal / develop / yolo) :

  1. Lisez CHORUS_OPENSPEC_ACTIVE de la section ## OpenSpec Mode du contexte SessionStart (§1). Si elle n'y est pas, revenez à la sonde manuelle en §1.
  2. Si CHORUS_OPENSPEC_ACTIVE=0 → revenez au chemin libre-service de l'appelant (§4).
  3. Sinon : a. Choisissez $SLUG (§3.1). b. openspec new change "$SLUG" (§3.2). c. Authoring proposal.md, design.md, specs/<capability>/spec.md (§3.2–§3.3). Mélangez les blocs ADDED / MODIFIED / REMOVED / RENAMED selon les besoins ; rappelez-vous que MODIFIED écrase toute l'Exigence. d. Optionnel : openspec validate "$SLUG". e. chorus_pm_create_proposal (MCP direct) avec la ligne OpenSpec change slug: $SLUG dans la description (§3.5). f. Définissez les helpers json_encode_file, chorus_check_response. (chorus-api.sh est sur PATH — aucune variable $API nécessaire.) g. Pour chaque ligne en §5 avec « oui » — miroir via chorus-api.sh mcp-tool chorus_pm_add_document_draft (§3.6). Enregistrez chaque $DRAFT_UUID. h. En cas d'échec chorus_check_response — arrêtez, surfacez l'erreur, ne procédez pas.
  4. Éditions avant approbation → §3.7. Éditions après approbation → §3.8.
  5. Dernière tâche vérifiée → hook se déclenche → lancez le flux d'archive §3.9.

Skills similaires