openspec-aware

Par chorus-aidlc · chorus

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

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

Authoring sensible aux OpenSpec (plugin Pi)

Cette skill est une sous-procédure partagée invoquée par les skills de l'étape 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 vrais (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 skill appelante revient à son comportement existant en mode libre.

Quand vous atteignez un point dans proposal / develop / yolo où cette skill est référencée, lisez la valeur de CHORUS_OPENSPEC_ACTIVE depuis le contexte session_start (voir §1) et branchez sur elle. Ne réexécutez pas le bloc de détection — le gestionnaire session_start l'a déjà fait une fois pour cette session.


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

Le gestionnaire session_start de l'extension Chorus calcule CHORUS_OPENSPEC_ACTIVE une fois à l'ouverture de la session et écrit une section ## OpenSpec Mode dans le contexte injecté par l'extension. La valeur de CHORUS_OPENSPEC_ACTIVE vaut 1 seulement quand les trois conditions suivantes sont vraies :

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

Les deux signaux (2) et (3) sont obligatoires car le chemin d'authoring OpenSpec nécessite le répertoire de travail et la CLI — avoir l'un sans l'autre laisse le workflow non fonctionnel. Si le signal (2) est vrai mais (3) ne l'est pas, le gestionnaire session_start affiche un indice « Dépôt OpenSpec détecté — installez avec : npm i -g @fission-ai/openspec » à l'utilisateur ; l'agent devrait le transmettre s'il est posé plutôt que de choisir silencieusement le mode libre.

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 de la skill appelante. Ne scaffoldez pas openspec/changes/. N'ajoutez pas la ligne slug à la description de la proposal.

Fallback manuel

Si vous êtes dans une sous-shell, un sous-agent, ou une session qui n'a pas vu le contexte session_start (par ex. vous avez été engendré en milieu de session et le contexte du parent n'a pas été transféré), reconstruisez la valeur vous-même avec les mêmes trois vérifications :

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

Utilisez ceci seulement quand le contexte session_start est vraiment 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 releases précédentes.

Règle 1 — Mirror via le wrapper, jamais retaper le contenu du document à partir de la sortie de l'agent

Les appels de mirror document/brouillon (chorus_pm_add_document_draft, chorus_pm_update_document_draft, chorus_pm_update_document) DOIVENT passer par :

chorus-mcp-call.sh <tool_name> "$PAYLOAD"

chorus-mcp-call.sh est fourni avec le package chorus-pi (déclaré comme un bin). Invoquez-le via l'outil bash. L'extension résout le chemin du wrapper au démarrage et le déclare dans la Quick Reference injectée (ligne - **OpenSpec wrapper**: … is at <PATH>). Préférez ce chemin injecté :

CHORUS_BIN="<injected path from the Quick Reference>"   # copy from the `- **OpenSpec wrapper**` line
# …or if it wasn't injected, resolve it once:
CHORUS_BIN=$(find ~/.pi/agent/npm -path '*chorus-pi/bin/chorus-mcp-call.sh' -type f 2>/dev/null | head -1)
# for local-path installs (pi install ./chorus-pi) the script lives next to the package:
CHORUS_BIN="$(dirname "$(realpath chorus-pi/bin/chorus-mcp-call.sh 2>/dev/null)")/chorus-mcp-call.sh"
"$CHORUS_BIN" <tool> '<json>'

puis appelez "$CHORUS_BIN" <tool> '<json>'. La commande simple chorus-mcp-call.sh n'est sur PATH que pour les installs npm/git — pour une install local-path (pi install ./chorus-pi) elle n'est PAS liée, donc toujours utiliser le $CHORUS_BIN résolu.

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 saisi à la main est une violation du 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 fait traverser les octets via jq -Rs '.' — le contenu n'entre jamais dans le contexte du LLM. Un mirror de proposal typique de 3 docs via le script coûte approximativement zéro content-token ; via MCP direct il coûte couramment 20k+.
  2. Égalité des octets. jq -Rs '.' est un encodeur fidèle aux octets : les antislash, guillemets, sauts de ligne, contenu des fence de code, caractères de largeur zéro — tout survit. L'émission du LLM a un taux d'échec non-zéro sur le markdown long — l'alignement des tableaux dérive, les échappements de fence sont « corrigés », les longues URL wrappent. La garantie d'égalité des octets tient seulement sur le chemin du wrapper.
  3. Source unique de vérité. Avec le wrapper, le fichier local openspec/changes/<slug>/*.md est autoritaire et Chorus en est un mirror. Avec la retape de 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 : le code de sortie du wrapper, "error": dans le corps, le corps vide. Un simple RC=$? est insuffisant — le wrapper sort 0 sur HTTP 401 (échec d'auth) avec un corps vide, donc une vérification sur un seul signal rate silencieusement l'échec runtime le plus courant. Voir §6 pour la définition de l'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, pas addExportCsv ou add_export_csv),
  • dérivé du titre de l'Idea source,
  • unique au sein de openspec/changes/.

Enregistrez-le pour les étapes ultérieures :

SLUG="add-export-csv"

3.2 Scaffolder le dossier de changement

openspec new change "$SLUG" --description "<one-line idea summary>"

Ceci crée openspec/changes/$SLUG/ avec README.md et .openspec.yaml. Puis authoring à la main :

Fichier local Objectif Mirror en tant que Document.type
proposal.md Pourquoi + Quoi Change + Capacités + Impact prd
design.md Architecture, contrats, risques tech_design
specs/<capability>/spec.md Delta spec (## ADDED Requirements + Scenarios) spec (un brouillon par capacité)
tasks.md Liste de tâches OpenSpec (pas mirrored — 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)

Un delta spec énumère un ou plusieurs en-têtes de bloc — ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements, ## RENAMED Requirements — et dans chacun, des entrées ### Requirement:. Mélangez librement dans le même fichier ; incluez seulement les blocs dont vous avez vraiment besoin.

## ADDED Requirements

Ajouter une Requirement complètement nouvelle aux specs long-terme.

## ADDED Requirements

### Requirement: <name>
<requirement text — use SHALL / MUST for normative behavior>

#### 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 du 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>
<full updated requirement text>

#### Scenario: <name>
- **WHEN** <condition>
- **THEN** <expected outcome>

#### Scenario: <other name>
- **WHEN** <condition>
- **THEN** <expected outcome>

Incluez toujours chaque scénario que vous voulez que la spec post-archive contienne, 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 le titre est juste le(s) nom(s) de la requirement que vous supprimez — pas de scénarios nécessaires.

## REMOVED Requirements

### Requirement: <existing name>

## RENAMED Requirements

Renommer le titre d'une Requirement. Le corps et les scénarios sont préservés tel-quel dans la spec long-terme ; utilisez MODIFIED à la place si vous avez besoin de 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 de 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 overwrite, pas patch.
  • Utilisez SHALL / MUST pour les requirements normatives ; évitez should / may.
  • La fusion dans openspec/specs/<capability>/spec.md se produit au moment de openspec archive (§3.9), pas au moment de la proposal. Pendant que la proposal est en cours, Chorus ne voit que le fichier delta en tant qu'un seul Document spec — il n'y a pas d'état partiellement fusionné pour que la skill raisonne dessus.

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 fait traverser le fichier dans une chaîne JSON ; le fallback correspond à l'échappement propre de chorus-mcp-call.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
}

La vérification de la boucle est exacte : une différence de saut de ligne de fin est une vraie dérive et NE DOIT PAS être normalisée ou ignorée.

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

Utiliser l'outil MCP habituel chorus_pm_create_proposal (pas de wrapper nécessaire pour cet appel unique — la description est courte, la version émise par le LLM est 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 simple après le deux-points),
  • pas de ponctuation de fin,
  • la valeur correspond au slug passé à openspec new change.

Cette ligne est grep-able par machine par les futures exécutions de cette skill et par le trigger d'archive §3.9.

3.6 Mirror chaque brouillon de document via le wrapper

Rappel Règle 1 : ces appels passent par chorus-mcp-call.sh, pas par MCP direct. L'agent ne doit pas retaper le corps du document.

Définir l'helper halt-on-error de §6 une fois au début, puis exécuter un appel par fichier :

# chorus-mcp-call.sh ships with chorus-pi; resolve CHORUS_BIN once (see §2), then call via the bash tool.
# PRD draft
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.sh 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 mirror tasks.md — les brouillons de tâches Chorus (créés via l'outil MCP chorus_pm_add_task_draft, pas de wrapper nécessaire) sont source de vérité pour les tâches.

Pourquoi le parsing 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 un \n imbriqué en vrai saut de ligne. jq avorte 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 à tout parsing de résultat wrapper dans cette skill.

3.7 Édition d'un brouillon après le premier mirror

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.sh 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 l'approbation de la proposal

Une fois que la proposal est approuvée, les brouillons se matérialisent en Documents avec leurs propres UUIDs. Pour garder openspec/changes/$SLUG/ et le Document Chorus synchronisés, mirror 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.sh chorus_pm_update_document "$PAYLOAD")
RC=$?
chorus_check_response "chorus_pm_update_document" "$RC" "$RESULT"

Pour re-dériver $SPEC_DOCUMENT_UUID à partir d'une nouvelle shell, le chercher via chorus_get_documents pour le projet de la proposal et matcher par title + type. Re-dériver $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

Quand la DERNIÈRE tâche d'une idea en mode OpenSpec est admin-vérifiée via chorus_admin_verify_task, l'extension (le handler tool_execution_end) injecte un rappel additionalContext contenant la substring littérale openspec archive <slug> pour que vous puissiez agir sans relire le slug.

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

  1. Exécutez l'archive localement. Utilisez --yes pour le mode non-interactif. Ne pas passer --skip-specs (contredit le mirror-back) ou --no-validate (permet aux deltas malformés de corrompre les specs cumulatives).

    openspec archive "$SLUG" --yes

    Ceci déplace openspec/changes/$SLUG/ sous openspec/changes/archive/<date>-<slug>/ et émet/met à jour openspec/specs/<capability>/spec.md pour chaque capacité. (Exécutez openspec archive --help contre votre version installée pour confirmer l'ensemble des flags courants — les flags peuvent dériver entre les releases.)

  2. Mirror chaque openspec/specs/<capability>/spec.md mise à jour vers le Document Chorus post-approbation correspondant (contrat §3.8). chorus_get_documents ne supporte les filtres server-side que projectUuid + type ; filtrez par title côté client. Un appel chorus_pm_update_document par capacité.

  3. Arrêtez sur toute erreur de openspec archive ou chorus_pm_update_document. Imprimez stderr verbatim, postez un commentaire sur la proposal enregistrant l'échec (chorus_add_comment avec targetType: "proposal", targetUuid: <proposalUuid>), puis arrêtez. Pas de retry. Correspond à §6 « pas d'erreurs silencieuses ». (Commentez la proposal, pas l'idea : l'échec est dans l'archivage des specs dérivées de la proposal, et les proposals peuvent être inputType: "document" sans idea attachée.)

  4. Confirmez le succès. Résolvez chaque UUID de Document correspondant et exécutez verify-document-roundtrip.sh <local-spec-path> <document-uuid>. Ceci effectue une comparaison exacte des octets et des diagnostiques de désaccord metadata-only. Ne le remplacez pas par jq récursif, head, substitution de commande, ou normalisation de saut de ligne.

Opt-in strict : si la tâche vérifiée n'est pas la dernière de son idea, OU la description de la proposal ne porte pas de ligne OpenSpec change slug: <slug>, OU la shell locale n'a pas de CLI openspec, le hook sort 0 silencieusement et aucun rappel d'archive n'est injecté. Le comportement existant libre-forme est préservé.


§4. Authoring fallback (pas d'openspec)

Quand la détection place l'agent en mode fallback (CHORUS_OPENSPEC_ACTIVE=0), cette skill est une no-op. Revenez au chemin libre-forme de la skill 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 proposal.
  • Les brouillons de documents sont authoriés via des appels MCP directs chorus_pm_add_document_draft avec content inline — comme avant l'existence de cette skill.
  • La Règle 1 (mirror wrapper-only) ne s'applique pas — il n'y a pas de source de vérité locale.
  • Le hook d'archive §3.9 ne fait rien (pas de slug → sortie silencieuse).

§5. Tableau de référence du mapping des types de document

Fichier local Chorus Document.type Mirrored?
openspec/changes/<slug>/proposal.md prd yes
openspec/changes/<slug>/design.md tech_design yes
openspec/changes/<slug>/specs/<capability>/spec.md spec yes (un brouillon par capacité)
openspec/changes/<slug>/tasks.md (not mapped) no — 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 défaillances — l'helper chorus_check_response

Il y a un edge case wrapper connu : quand le serveur retourne HTTP 4xx (par ex. 401 d'une mauvaise CHORUS_API_KEY), chorus-mcp-call.sh capture le corps d'erreur JSON-RPC en interne, le fait traverser 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 n'arrêterait pas sur ceci — le mode de défaillance runtime le plus courant serait invisible.

Définir cet helper une fois au début 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-patterns — ne pas:

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

Forme minimale du site d'appel :

RESULT=$(chorus-mcp-call.sh <tool_name> "$PAYLOAD")
RC=$?
chorus_check_response "<tool_name>" "$RC" "$RESULT"
# ...if we reach here, the call succeeded; parse RESULT and continue.

C'est une politique project-wide : pas d'erreurs silencieuses.


§7. Checklist de référence rapide

Quand invoquée depuis une skill de l'étape (proposal / develop / yolo) :

  1. Lire CHORUS_OPENSPEC_ACTIVE depuis la section ## OpenSpec Mode dans le contexte session_start (§1). Si elle n'y est pas, tomber back sur la sonde manuelle en §1.
  2. Si CHORUS_OPENSPEC_ACTIVE=0 → revenir au chemin libre-forme de l'appelant (§4).
  3. Sinon: a. Choisir $SLUG (§3.1). b. openspec new change "$SLUG" (§3.2). c. Authorer proposal.md, design.md, specs/<capability>/spec.md (§3.2–§3.3). Mélanger les blocs ADDED / MODIFIED / REMOVED / RENAMED selon les besoins ; se souvenir que MODIFIED overwrite la Requirement entière. 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éfinir les helpers json_encode_file, chorus_check_response, et résoudre CHORUS_BIN vers le chorus-mcp-call.sh bundlé (voir §2). Puis utiliser "$CHORUS_BIN" au lieu d'un simple chorus-mcp-call.sh dans chaque appel ci-dessous. g. Pour chaque ligne en §5 avec « yes » — mirror via chorus-mcp-call.sh chorus_pm_add_document_draft (§3.6). Enregistrer chaque $DRAFT_UUID. h. Sur toute chorus_check_response échouée — arrêter, surfacer l'erreur, ne pas continuer.
  4. Éditions avant approbation → §3.7. Éditions après approbation → §3.8.
  5. Dernière tâche vérifiée → hook tire → exécuter le flux d'archive §3.9.

Skills similaires