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

Authorship conscient d'OpenSpec (plugin Pi)

Cette compétence est une sous-procédure partagée appelée par les compétences de stage Chorus (proposal, develop, yolo) chaque fois que l'utilisateur souhaite une authoring pilotée par spec via la CLI OpenSpec. C'est un opt-in :

  • 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 free-form 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 session_start (voir §1) et branchez dessus. Ne relancez 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é de l'extension. La valeur de CHORUS_OPENSPEC_ACTIVE est 1 seulement si les trois conditions tiennent :

  1. CHORUS_OPENSPEC_MODE n'est pas défini sur off (l'opt-out explicite prime).
  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 requis 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 workflow inexécutable. Si le signal (2) tient mais (3) non, le gestionnaire session_start affiche un conseil « OpenSpec repo détecté — installer avec : npm i -g @fission-ai/openspec » à l'utilisateur ; l'agent doit le transmettre s'il est demandé plutôt que de silencieusement choisir free-form.

Comment lire la valeur

Vous devriez déjà voir quelque chose comme ceci dans votre contexte (cherchez la section ## OpenSpec Mode près du début 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 → suivre §3 (authoring OpenSpec).
  • CHORUS_OPENSPEC_ACTIVE=0 → revenir au chemin free-form de la compétence appelante. Ne pas créer d'échafaudage pour openspec/changes/. Ne pas ajouter la ligne slug à la description de la proposal.

Fallback manuel

Si vous êtes dans un sous-shell, sous-agent ou session qui n'a pas vu le contexte session_start (p. ex. vous avez été lancé 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

N'utilisez ceci que si le contexte session_start est véritablement indisponible — dupliquer la détection est du gaspillage 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 revue. Les deux ont causé des incidents dans les versions antérieures.

Règle 1 — Mettre en miroir via le wrapper, jamais retaper le contenu du document à partir de 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-mcp-call.sh <tool_name> "$PAYLOAD"

chorus-mcp-call.sh est livré avec le paquet chorus-pi (déclaré en tant que bin). Invoquez-le via l'outil bash. L'extension résout le chemin du wrapper au démarrage et l'indique 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
# …ou si ce n'était pas injecté, résolvez-le une fois :
CHORUS_BIN=$(find ~/.pi/agent/npm -path '*chorus-pi/bin/chorus-mcp-call.sh' -type f 2>/dev/null | head -1)
# pour les installations en local-path (pi install ./packages/chorus-pi) le script vit à côté du paquet :
CHORUS_BIN="$(dirname "$(realpath packages/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 que sur PATH pour les installations npm/git — pour une installation en local-path (pi install ./packages/chorus-pi) elle n'est pas linké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 tapé à la main est une violation de protocole pour le mode OpenSpec et échouera la revue. Raisons :

  1. Coût en tokens. Retaper un corps markdown de plusieurs milliers de lignes à travers l'LLM consomme des tokens en entrée + sortie pour chaque brouillon. Le wrapper envoie les octets via jq -Rs '.' — le contenu n'entre jamais dans le contexte de l'LLM. Une mirror typique de 3-doc proposal via le script coûte à peu près zéro token de contenu ; via MCP direct cela coûte routinément 20k+.
  2. Égalité octets. jq -Rs '.' est un encodeur fidèle aux octets : les backslashes, guillemets, sauts de ligne, contenu des clôtures de code, caractères de largeur zéro survivent tous. L'émission par l'LLM a un taux d'échec non nul sur le markdown long — l'alignement des tableaux dérive, les échappements de clôture se font "corriger", les longues URLs s'enroulent. La garantie d'égalité exacte des octets tient seulement sur le chemin du wrapper.
  3. Source unique de vérité. Avec le wrapper, le fichier openspec/changes/<slug>/*.md local est autoritaire et Chorus est un miroir. Avec retappage d'agent, l'autorité se divise entre le fichier local et ce que l'LLM s'est avéré émettre — un diff futur ne peut pas dire lequel est correct.

Règle 2 — Arrêt sur erreur via chorus_check_response

Chaque appel au wrapper doit vérifier trois signaux : code de sortie du wrapper, "error": dans le corps, corps vide. Bare RC=$? est insuffisant — le wrapper quitte 0 sur HTTP 401 (échec d'auth) avec un corps vide, donc un check à signal unique rate silencieusement l'échec runtime le plus commun. 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'Idea source,
  • unique dans openspec/changes/.

Enregistrez-le pour les étapes ultérieures :

SLUG="add-export-csv"

3.2 Créer l'échafaudage du 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 Why + What Changes + Capabilities + Impact prd
design.md Architecture, contracts, risks tech_design
specs/<capability>/spec.md Delta spec (## ADDED Requirements + Scenarios) spec (un brouillon par capability)
tasks.md Liste des tâches OpenSpec (non mirrée — 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 delta spec liste 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 réellement besoin.

## ADDED Requirements

Ajouter une brand-new Requirement à la spec long-terme.

## ADDED Requirements

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

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

## MODIFIED Requirements

Remplacement de bloc entier, pas fusion. Tout ce que vous écrivez ici remplace complètement la même Requirement nommée dans la spec long-terme — titre, description, et tous les scénarios. La demi-écrire 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 tous les scénarios 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 long-terme. Le bloc sous l'en-tête est juste le ou les noms 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 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 à 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 patchent pas.
  • Utilisez SHALL / MUST pour les requirements normatives ; évitez should / may.
  • La fusion dans openspec/specs/<capability>/spec.md se fait au moment du openspec archive (§3.9), pas au moment de la proposal. Tant que la proposal est en vol, Chorus ne voit que le fichier delta comme un seul Document spec — il n'y a pas d'état semi-fusionné pour que la compétence raisonne.

Optionnel :

openspec validate "$SLUG"

3.4 Helper : json_encode_file

Définissez une fois en haut de la session d'authoring. Avec jq disponible, cela envoie le fichier dans une chaîne JSON ; le fallback correspond à l'échappement propre de chorus-mcp-call.sh quand jq est manquant.

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 round-trip est exacte : une différence de saut de ligne final est une vraie dérive et NE DOIT PAS être normalisée ou ignorée.

3.5 Créer le conteneur de proposal avec la ligne de provenance 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 l'LLM va bien). La description doit porter 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, un seul espace après le deux-points),
  • pas de ponctuation finale,
  • la valeur correspond au slug passé à openspec new change.

Cette ligne est grep-able machine par les exécutions futures de cette compétence et par le trigger archive de §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éfinissez le helper d'arrêt-sur-erreur de §6 une fois en haut, puis lancez un appel par fichier :

# chorus-mcp-call.sh est livré avec chorus-pi ; résolvez CHORUS_BIN une fois (voir §2), puis appelez via l'outil bash.
# 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.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 capability 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, aucun wrapper nécessaire) sont la source de vérité pour les tâches.

Pourquoi le parsing utilise printf '%s' "$RESULT" | grep pas echo "$RESULT" | jq : echo interprète les séquences de backslash à l'intérieur du JSON capturé, transformant \n embarqué en un 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 pattern s'applique à tout parsing de résultat du wrapper dans cette compétence.

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 check 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-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 approbation de proposal

Une fois la proposal approuvée, les brouillons se matérialisent en Documents avec leurs propres UUIDs. 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.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'un shell frais, regardez-le via chorus_get_documents pour le projet de la proposal et associez-le par title + type. Re-dérivez $SLUG en greppant la description de la proposal 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, l'extension (le gestionnaire tool_execution_end) injecte un rappel additionalContext contenant le substring littéral openspec archive <slug> pour que vous puissiez agir sans relire le slug.

Le hook est read-only ; vous (l'agent) effectuez l'archive :

  1. Lancez l'archive localement. Utilisez --yes pour le mode non-interactif. Ne pas passer --skip-specs (annule le mirror-back) ou --no-validate (permet aux deltas mal formé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 capability. (Lancez openspec archive --help contre votre version installée pour confirmer l'ensemble de flags actuel — les flags peuvent dériver entre les releases.)

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

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

  4. Confirmez le succès. Résolvez chaque Document UUID correspondant et lancez verify-document-roundtrip.sh <local-spec-path> <document-uuid>. Ceci effectue une comparaison exacte octets et des diagnostiques de mismatch métadonnées uniquement. Ne la 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 le shell local n'a pas de CLI openspec, le hook quitte 0 silencieusement et aucun rappel d'archive n'est injecté. Le comportement free-form existant est préservé.


§4. Authoring en fallback (pas d'openspec)

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

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

Fichier local Chorus Document.type Mirrée?
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 capability)
openspec/changes/<slug>/tasks.md (not mapped) 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é d'é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-mcp-call.sh capture le corps d'erreur JSON-RPC en interne, le pipe via un filtre jq .result.content[]? qui ne produit pas de sortie quand .result est absent, et quitte 0 avec stdout vide. Un check RC=$? bare n'arrêterait pas dessus — le mode d'échec runtime le plus commun serait invisible.

Définissez ce helper une fois en haut de la session d'authoring et utilisez-le après chaque appel au 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 en || true.
  • Rediriger stderr vers /dev/null.
  • Enterrer l'appel au wrapper à l'intérieur d'un pipeline (masque $?).
  • Sauter la capture de $RESULT dans une variable ; le helper a besoin du corps.
  • Utiliser seulement if [ "$RC" -ne 0 ]; then ... — cela rate le chemin d'erreur HTTP.

Forme minimale du site d'appel :

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

C'est une politique au niveau du projet : pas d'erreurs silencieuses.


§7. Checklist de référence rapide

Quand invoquée par une compétence de stage (proposal / develop / yolo) :

  1. Lisez CHORUS_OPENSPEC_ACTIVE depuis la section ## OpenSpec Mode dans le contexte session_start (§1). Si ce n'est pas là, revenez à la sonde manuelle en §1.
  2. Si CHORUS_OPENSPEC_ACTIVE=0 → revenir au chemin free-form 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 ; souvenez-vous que MODIFIED écrase 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éfinissez les helpers json_encode_file, chorus_check_response, et résolvez CHORUS_BIN vers le chorus-mcp-call.sh bundled (voir §2). Puis utilisez "$CHORUS_BIN" à la place d'un chorus-mcp-call.sh bare dans chaque appel ci-dessous. g. Pour chaque ligne dans §5 avec « yes » — mirror via chorus-mcp-call.sh chorus_pm_add_document_draft (§3.6). Enregistrez chaque $DRAFT_UUID. h. Sur tout chorus_check_response échoué — arrêtez, surfacez l'erreur, ne pas procéder.
  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