Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Opt-in OpenSpec-mode authoring for Chorus PM workflows in Pi. Detects the local `openspec` CLI, scaffolds `openspec/changes/<slug>/` on disk, and mirrors Markdown files into Chorus document drafts via the `chorus-mcp-call.sh` wrapper. Required reading for the proposal, develop, and yolo skills whenever the user has the `openspec` CLI installed.
.claude/skills/chorus-aidlc-openspec-aware/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 259% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 308% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 373% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 589% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 488% | 0% |
This skill is a shared sub-procedure invoked by the Chorus stage skills (proposal, develop, yolo) whenever the user wants spec-driven authoring through the OpenSpec CLI. It is opt-in:
CHORUS_OPENSPEC_MODE is not off, an openspec/ directory exists at the project root, and the openspec CLI is on PATH.When you reach a point in proposal / develop / yolo where this skill is referenced, read the value of CHORUS_OPENSPEC_ACTIVE from the SessionStart context (see §1) and branch on it. Do not re-run the detection block — the SessionStart hook has already done it once for this session.
The Chorus plugin's SessionStart hook (bin/on-session-start.sh) computes CHORUS_OPENSPEC_ACTIVE once when the session opens and writes a ## OpenSpec Mode section into the plugin's injected context. The value of CHORUS_OPENSPEC_ACTIVE is 1 only when all three of these hold:
CHORUS_OPENSPEC_MODE is not set to off (explicit opt-out wins).openspec/ directory (i.e. someone ran openspec init here).openspec CLI is on PATH.Both signals (2) and (3) are required because the OpenSpec authoring path needs the working directory and the CLI — having one without the other leaves the workflow unrunnable. If signal (2) holds but (3) does not, the SessionStart hook surfaces a "OpenSpec repo detected — install with: npm i -g @fission-ai/openspec" hint to the user; the agent should pass this through if asked rather than silently choosing free-form.
You should already see something like this in your context (look for the ## OpenSpec Mode section near the top of the conversation):
## OpenSpec Mode
CHORUS_OPENSPEC_ACTIVE=1 (openspec/ directory + openspec CLI both present)or:
## OpenSpec Mode
CHORUS_OPENSPEC_ACTIVE=0 (no openspec/ directory at /path/to/repo/openspec)Branch:
CHORUS_OPENSPEC_ACTIVE=1 → follow §3 (OpenSpec authoring).CHORUS_OPENSPEC_ACTIVE=0 → return to the calling skill's free-form path. Do not scaffold openspec/changes/. Do not add the slug line to the proposal description.If you're in a sub-shell, sub-agent, or session that did not see SessionStart context (e.g. you were spawned mid-session and the parent's context was not forwarded), reconstruct the value yourself with the same three checks:
bashif [ "${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
Use this only when SessionStart context is genuinely unavailable — duplicating the detection is wasteful when the hook already computed it.
Both are enforced at review time. Both have caused incidents in past releases.
Document/draft mirror calls (chorus_pm_add_document_draft, chorus_pm_update_document_draft, chorus_pm_update_document) MUST go through:
chorus-api.sh mcp-tool <tool_name> "$PAYLOAD"chorus-api.sh is on PATH — call it by name.
with $PAYLOAD built using json_encode_file (defined in §3.4). Calling these tools directly from the agent's MCP harness with a hand-typed content field is a protocol violation for OpenSpec mode and will fail review. Reasons:
jq -Rs '.' — content never enters LLM context. A typical 3-doc proposal mirror via the script costs roughly zero content-tokens; via direct MCP it routinely costs 20k+.jq -Rs '.' is a byte-faithful encoder: backslashes, quotes, newlines, code-fence content, zero-width chars all survive. LLM re-emission has a non-zero failure rate on long markdown — table alignment drifts, fence escapes get "fixed", long URLs wrap. The exact byte-equality guarantee holds only on the wrapper path.openspec/changes/<slug>/*.md is authoritative and Chorus is a mirror. With agent re-typing, authority splits between local file and whatever the LLM happened to output — a future diff cannot tell which one is correct.chorus_check_responseEvery wrapper call must check three signals: wrapper exit code, "error": in body, empty body. Bare RC=$? is insufficient — the wrapper exits 0 on HTTP 401 (auth failure) with empty body, so a single-signal check silently misses the most common runtime failure. See §6 for the helper definition.
openspec/changes/<slug>/ is the local change folder. The slug must be:
add-export-csv, not addExportCsv or add_export_csv),openspec/changes/.Record it for later steps:
bashSLUG="add-export-csv"
bashopenspec new change "$SLUG" --description "<one-line idea summary>"
This creates openspec/changes/$SLUG/ with README.md and .openspec.yaml. Then author by hand:
| Local file | Purpose | Mirror as 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 (one draft per capability) | | tasks.md | OpenSpec tasks list | _(not mirrored — Chorus task drafts are source of truth)_ |
Use openspec instructions <artifact> --change "$SLUG" (artifacts: proposal, specs, design, tasks) for templates.
openspec instructions specs)A delta spec lists one or more block headers — ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements, ## RENAMED Requirements — and within each, ### Requirement: entries. Mix freely in the same file; only include the blocks you actually need.
## ADDED RequirementsAppend a brand-new Requirement to the long-term spec.
## ADDED Requirements
### Requirement: <name>
<requirement text — use SHALL / MUST for normative behavior>
#### Scenario: <name>
- **WHEN** <condition>
- **THEN** <expected outcome>## MODIFIED RequirementsWhole-block replacement, not merge. Whatever you write here completely replaces the existing same-named Requirement in the long-term spec — title, description, and all scenarios. Half-writing it deletes the rest.
## MODIFIED Requirements
### Requirement: <existing name>
<full updated requirement text>
#### Scenario: <name>
- **WHEN** <condition>
- **THEN** <expected outcome>
#### Scenario: <other name>
- **WHEN** <condition>
- **THEN** <expected outcome>Always include every scenario you want the post-archive spec to have, even ones that were already present and unchanged.
## REMOVED RequirementsDelete a Requirement from the long-term spec. The block under the heading is just the requirement name(s) you're removing — no scenarios needed.
## REMOVED Requirements
### Requirement: <existing name>## RENAMED RequirementsRename a Requirement's title. Body and scenarios are preserved as-is in the long-term spec; use MODIFIED instead if you need to change anything besides the title.
## RENAMED Requirements
### Requirement: <old name> -> <new name>Critical formatting rules (verified):
#### Scenario:). 3 hashtags or a bullet list silently fail validation.### Requirement: under ADDED or MODIFIED MUST have at least one #### Scenario:.MODIFIED blocks MUST include the full updated content — they overwrite, not patch.SHALL / MUST for normative requirements; avoid should / may.openspec/specs/<capability>/spec.md happens at openspec archive time (§3.9), not at proposal time. While the proposal is in flight, Chorus only sees the delta file as one spec Document — there is no half-merged state for the skill to reason about.Optional:
bashopenspec validate "$SLUG"
json_encode_fileDefine once at the top of the authoring session. With jq available it streams the file into a JSON string; the fallback matches chorus-api.sh's own escaping when jq is missing.
bashjson_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 verification is exact: a trailing newline difference is real drift and MUST NOT be normalized or ignored.
Use the regular chorus_pm_create_proposal MCP tool (no wrapper required for this single call — the description is short, the LLM-emitted version is fine). The description must carry exactly one line:
OpenSpec change slug: <slug>OpenSpec change slug: (capital O, capital S, single space after colon),openspec new change.This line is machine-grep-able by future runs of this skill and by the §3.9 archive trigger.
> Rule 1 reminder: these calls go through chorus-api.sh, not direct MCP. The agent must not retype the document body.
Define the halt-on-error helper from §6 once at the top, then run one call per file:
bash# chorus-api.sh is on PATH — no absolute path needed. # 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-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/')
Repeat with type: "tech_design" for design.md, and one call per capability with type: "spec" for each specs/<capability>/spec.md. Do not mirror tasks.md — Chorus task drafts (created via the chorus_pm_add_task_draft MCP tool, no wrapper needed) are the source of truth for tasks.
> Why parsing uses printf '%s' "$RESULT" | grep not echo "$RESULT" | jq: echo interprets backslash sequences inside the captured JSON, turning embedded \n into a real newline. jq then aborts with Invalid string: control characters from U+0000 through U+001F must be escaped. printf '%s' emits the captured bytes verbatim. Same pattern applies to all wrapper-result parsing in this skill.
Local file changes propagate via chorus_pm_update_document_draft — same wrapper, same json_encode_file, same halt check.
bashCONTENT=$(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"
Once the proposal is approved, drafts materialize into Documents with their own UUIDs. To keep openspec/changes/$SLUG/ and the Chorus Document in sync, mirror file edits via chorus_pm_update_document:
bashCONTENT=$(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"
To re-derive $SPEC_DOCUMENT_UUID from a fresh shell, look it up via chorus_get_documents for the proposal's project and match by title + type. Re-derive $SLUG by grepping the proposal's description for ^OpenSpec change slug: .
When the LAST task of an OpenSpec-mode idea is admin-verified via chorus_admin_verify_task, the plugin's PostToolUse hook (bin/on-post-verify-task.sh) injects an additionalContext reminder containing the literal substring openspec archive <slug> so you can act without re-reading the slug.
The hook is read-only; you (the agent) perform the archive:
--yes for non-interactive mode. Do NOT pass --skip-specs (defeats the mirror-back) or --no-validate (lets malformed deltas corrupt cumulative specs).bash openspec archive "$SLUG" --yes
This moves openspec/changes/$SLUG/ under openspec/changes/archive/<date>-<slug>/ and emits/updates openspec/specs/<capability>/spec.md for each capability. (Run openspec archive --help against your installed version to confirm the current flag set — flags can drift between releases.)
openspec/specs/<capability>/spec.md back to the matching post-approval Chorus Document (§3.8 contract). chorus_get_documents only supports projectUuid + type server-side filters; filter by title client-side. One chorus_pm_update_document call per capability.openspec archive or chorus_pm_update_document. Print stderr verbatim, post a comment on the proposal recording the failure (chorus_add_comment with targetType: "proposal", targetUuid: <proposalUuid>), then stop. No retry. Matches §6 "no silent errors." (Comment on the proposal, not the idea: the failure is in archiving proposal-derived specs, and proposals can be inputType: "document" with no idea attached.)verify-document-roundtrip.sh <local-spec-path> <document-uuid>. This performs exact-byte comparison and metadata-only mismatch diagnostics. Do not replace it with recursive jq, head, command substitution, or newline normalization.
Strict opt-in: if the verified task is not the last of its idea, OR the proposal description carries no OpenSpec change slug: <slug> line, OR the local shell has no openspec CLI, the hook exits 0 silently and no archive reminder is injected. Existing free-form behavior is preserved.
When detection puts the agent in fallback mode (CHORUS_OPENSPEC_ACTIVE=0), this skill is a no-op. Return to the calling skill's free-form path:
openspec/changes/ folder is created or referenced.OpenSpec change slug: … line is added to the proposal description.chorus_pm_add_document_draft calls with inline content — same as before this skill existed.| Local file | 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 (one draft per capability) | | openspec/changes/<slug>/tasks.md | _(not mapped)_ | no — Chorus task drafts are source of truth |
prd, tech_design, spec are pre-existing valid Document.type values — no schema change required.
chorus_check_response helperThere is a known wrapper edge case: when the server returns HTTP 4xx (e.g. 401 from a bad CHORUS_API_KEY), chorus-api.sh mcp-tool captures the JSON-RPC error body internally, pipes it through a .result.content[]? jq filter that produces no output when .result is absent, and exits 0 with empty stdout. A bare RC=$? check would not halt on this — the most common runtime failure mode would be invisible.
Define this helper once at the top of the authoring session and use it after every wrapper call:
bashchorus_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 — do not:
|| true./dev/null.$?).$RESULT into a variable; the helper needs the body.if [ "$RC" -ne 0 ]; then ... — that misses the HTTP-error path.Minimal call site shape:
bashRESULT=$(chorus-api.sh mcp-tool <tool_name> "$PAYLOAD") RC=$? chorus_check_response "<tool_name>" "$RC" "$RESULT" # ...if we reach here, the call succeeded; parse RESULT and continue.
This is project-wide policy: no silent errors.
When invoked from a stage skill (proposal / develop / yolo):
CHORUS_OPENSPEC_ACTIVE from the ## OpenSpec Mode section in the SessionStart context (§1). If it isn't there, fall back to the manual probe in §1.CHORUS_OPENSPEC_ACTIVE=0 → return to caller's free-form path (§4).a. Pick $SLUG (§3.1). b. openspec new change "$SLUG" (§3.2). c. Author proposal.md, design.md, specs/<capability>/spec.md (§3.2–§3.3). Mix ADDED / MODIFIED / REMOVED / RENAMED blocks as needed; remember MODIFIED overwrites the whole Requirement. d. Optional: openspec validate "$SLUG". e. chorus_pm_create_proposal (direct MCP) with the OpenSpec change slug: $SLUG line in description (§3.5). f. Define json_encode_file, chorus_check_response helpers. (chorus-api.sh is on PATH — no $API variable needed.) g. For each row in §5 with "yes" — mirror via chorus-api.sh mcp-tool chorus_pm_add_document_draft (§3.6). Record each $DRAFT_UUID. h. On any failed chorus_check_response — halt, surface the error, do NOT proceed.
Other measured skills in the registry, with their headline benchmark lift.