chorus-openspec-aware

Par chorus-aidlc · chorus

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

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

Authoring OpenSpec (plugin CLI Kiro)

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

  • 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 le 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 (voir §1) et déviez en fonction.


§1. Détection

Le hook agentSpawn de l'agent principal chorus de Chorus peut calculer CHORUS_OPENSPEC_ACTIVE une fois au démarrage et écrire une section ## OpenSpec Mode dans votre contexte de démarrage. Si vous voyez cette section, utilisez sa valeur ; sinon, exécutez la sonde manuelle ci-dessous. La valeur de CHORUS_OPENSPEC_ACTIVE est 1 uniquement quand tous les trois éléments suivants sont vrais :

  1. CHORUS_OPENSPEC_MODE n'est pas défini sur off (l'opt-out explicite gagne).
  2. La racine du projet contient un répertoire openspec/ (c.-à-d. que quelqu'un a exécuté openspec init ici).
  3. Le 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 le CLI — avoir l'un sans l'autre rend le workflow non exécutable. Si le signal (2) est vrai mais (3) ne l'est pas, proposez un indice « Dépôt OpenSpec détecté — installer avec : npm i -g @fission-ai/openspec » à l'utilisateur plutôt que de choisir silencieusement la forme libre.

Comment lire la valeur

Si votre contexte agentSpawn l'inclut, vous verrez quelque chose comme :

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

Déviez :

  • CHORUS_OPENSPEC_ACTIVE=1 → suivre §3 (authoring OpenSpec).
  • CHORUS_OPENSPEC_ACTIVE=0 → revenir au chemin libre de la compétence appelante. Ne pas créer openspec/changes/. Ne pas ajouter la ligne slug à la description de la proposition.

Sonde manuelle

Si vous n'avez pas vu de section ## OpenSpec Mode dans votre contexte (par ex. le hook agentSpawn ne l'a pas injecté, ou vous êtes un sous-agent), calculez la valeur vous-même avec les trois mêmes vérifications. Kiro CLI ne définit pas de variable d'env pour le répertoire du projet, donc sondez le répertoire de travail actuel :

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

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

Les deux sont appliquées au moment de l'examen. Les deux ont causé des incidents dans les versions passées.

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

Les appels de miroir de 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 installé aux côtés des scripts de hook sous le répertoire chorus-bin/ de Kiro et est sur PATH — appelez-le par son nom.

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 retapé à la main est une violation de protocole pour le mode OpenSpec et échouera l'examen. Raisons :

  1. Coût des tokens. Retaper un corps markdown multi-millier de lignes à travers le LLM brûle les tokens d'entrée + sortie pour chaque brouillon. Le wrapper diffuse les bytes via jq -Rs '.' — le contenu ne rentre jamais dans le contexte du LLM. Un miroir de proposition typique à 3 docs via le script coûte à peu près zéro content-tokens ; via MCP direct, il coûte régulièrement 20k+.
  2. Égalité octet-par-octet. jq -Rs '.' est un encodeur fidèle aux bytes : les antislashs, les guillemets, les retours à la ligne, le contenu des délimiteurs de code, les caractères de largeur zéro survivent tous. L'émission par LLM a un taux d'échec non nul sur long markdown — l'alignement du tableau dévie, les échappements de délimiteur sont « corrigés », les longues URL s'enroulent. La garantie d'égalité octet-par-octet (modulo \n final) ne tient que sur le chemin du wrapper.
  3. Source unique de vérité. Avec le wrapper, le openspec/changes/<slug>/*.md local est autoritaire et Chorus en est le miroir. Avec la retape par agent, l'autorité se divise entre le fichier local et tout ce que le LLM a émis — un futur diff 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. Bare RC=$? est insuffisant — le wrapper sort 0 sur HTTP 401 (échec d'authentification) avec le 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 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 :

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

Enregistrez-le pour les étapes ultérieures :

SLUG="add-export-csv"

3.2 Créer le dossier de changement

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

Cela crée openspec/changes/$SLUG/ avec README.md et .openspec.yaml. Ensuite, authorer à 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 + Scenarios) spec (un brouillon par capacité)
tasks.md Liste des tâches OpenSpec (pas en miroir — 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 spec delta liste un ou plusieurs en-têtes de bloc — ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements, ## RENAMED Requirements — et dans chacun, des entrées ### Requirement:. Mixez librement dans le même fichier ; n'incluez que les blocs dont vous avez réellement besoin.

## ADDED Requirements

Ajouter une nouvelle Requirement à la spec de long terme.

## ADDED Requirements

### Requirement: <name>
<requirement text — utilisez SHALL / MUST pour le comportement normatif>

#### 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 de même nom existante dans la spec de long terme — titre, description et tous les scénarios. Demi-écriture la supprime.

## MODIFIED Requirements

### Requirement: <existing name>
<full updated requirement text>

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

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

Toujours inclure chaque scénario 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 de 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: <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 de long terme ; utilisez MODIFIED à la place si vous devez 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 mis à jour complet — ils réécrivent, ne corrigent pas.
  • Utilisez SHALL / MUST pour les requirements normatifs ; évitez should / may.
  • La fusion dans openspec/specs/<capability>/spec.md se fait à l'heure openspec archive (§3.9), non à l'heure de la proposition. Tandis que la proposition est en vol, Chorus ne voit que le fichier delta comme un Document spec — il n'y a pas d'état demi-fusionné pour que la compétence raisonne.

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 diffuse 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 seul \n au contenu du brouillon lors de l'écriture, donc le content serveur est octet-égal modulo une newline finale. Les examinateurs comparant le fichier local avec le serveur doivent ignorer cet octet.

3.5 Créer le conteneur de proposition avec la ligne de provenance du 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 le LLM va 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 unique après les deux points),
  • pas de ponctuation finale,
  • la valeur correspond au slug passé à openspec new change.

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

Définir le helper halt-on-error du §6 une fois en haut, puis exécuter 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 mettre tasks.md en miroir — 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 d'antislash à l'intérieur du JSON capturé, transformant \n intégré en vrai saut de ligne. jq s'arrête alors avec Invalid string: control characters from U+0000 through U+001F must be escaped. printf '%s' émet les bytes capturés verbatim. Le même motif s'applique à tout parsing de résultat wrapper dans cette compétence.

3.7 Édition d'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-api.sh mcp-tool 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 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, mettez en miroir 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-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 depuis un shell frais, regardez-le via chorus_get_documents pour le projet de la proposition et correspondez par title + type. Re-dérivez $SLUG en grepping la description de la proposition 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 idée en mode OpenSpec est admin-vérifiée via chorus_admin_verify_task, le hook postToolUse de l'agent principal chorus injecte un rappel contenant la sous-chaîne littérale openspec archive <slug> pour que vous puissiez agir 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 PAS passer --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é. (Exécutez openspec archive --help contre votre version installée pour confirmer l'ensemble actuel de drapeaux — les drapeaux peuvent changer entre les versions.)

  2. Mettez en miroir 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 côté serveur projectUuid + type ; filtrez par titre 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 proposition 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 proposition, pas l'idée : l'échec est dans l'archivage des specs dérivées de proposition, et les propositions peuvent être inputType: "document" sans idée attachée.)

  4. Confirmez le succès. Listez les fichiers openspec/specs/<capability>/spec.md et vérifiez qu'ils font un aller-retour octet-é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 proposition ne porte pas de ligne OpenSpec change slug: <slug>, OU le shell local n'a pas le CLI openspec, le hook sort 0 silencieusement et aucun rappel d'archive n'est injecté. Le comportement libre existant est préservé.


§4. Authoring fallback (pas d'openspec)

Quand la détection place l'agent en mode fallback (CHORUS_OPENSPEC_ACTIVE=0), cette compétence est un no-op. Revenez au chemin libre 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 authoriés via des appels MCP directs chorus_pm_add_document_draft avec content en ligne — comme avant qu'il n'existe cette compétence.
  • La Règle 1 (miroir wrapper-only) 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. Tableau de référence de mappage des types de document

Fichier local Document.type Chorus Mis en 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 (pas 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é des échecs — le helper chorus_check_response

Il y a un cas limite connu du wrapper : quand le serveur retourne HTTP 4xx (par ex. 401 d'une mauvaise CHORUS_API_KEY), chorus-api.sh mcp-tool capture le corps d'erreur JSON-RPC en interne, le piped via un filtre jq .result.content[]? qui ne produit aucune sortie quand .result est absent, et sort 0 avec stdout vide. Une vérification de bare RC=$? ne s'arrêterait pas sur ceci — le mode d'échec d'exécution le plus courant serait invisible.

Définir ce helper une fois en haut 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-motifs — ne pas :

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

Forme minimaliste du site d'appel :

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

C'est la politique de projet : pas d'erreurs silencieuses.


§7. Checklist de référence rapide

Quand invoqué depuis une compétence de phase (/chorus-proposal / /chorus-develop / /chorus-yolo) :

  1. Lisez CHORUS_OPENSPEC_ACTIVE depuis la section ## OpenSpec Mode dans votre contexte agentSpawn (§1). Si ce n'est pas là, revenez à la sonde manuelle de §1.
  2. Si CHORUS_OPENSPEC_ACTIVE=0 → revenez au chemin libre 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élangez les blocs ADDED / MODIFIED / REMOVED / RENAMED selon les besoins ; souvenez-vous que MODIFIED réécrit 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. (chorus-api.sh est sur PATH — aucune variable $API nécessaire.) g. Pour chaque ligne dans §5 avec « oui » — mettez en miroir via chorus-api.sh mcp-tool chorus_pm_add_document_draft (§3.6). Enregistrez chaque $DRAFT_UUID. h. Sur tout chorus_check_response échoué — arrêtez, surface l'erreur, ne continuez 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 → exécutez le flux d'archive §3.9.

Skills similaires