Skip to content
Back to skills

Openspec Aware

BSecurity

OpenSpec-mode authoring for Chorus PM workflows on OpenClaw — the default whenever OpenSpec is usable. Resolves the whole spec-mode contract (lite/openspec/off) by sourcing the plugin's shipped `bin/resolve-spec-mode.sh` (no SessionStart hook on OpenClaw), scaffolds `openspec/changes/<slug>/` on disk, and mirrors Markdown files into Chorus document drafts via `chorus mcp call --arg-file` (bash `chorus-api.sh` wrapper as fallback). When OpenSpec isn't usable the mode resolves to spec-lite (see...

  • 1,192 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 2, 2026
ai-agentsgoshellbashnodegitapibackend

Works with

  • claude code
  • cli
  • api
  • mcp

Security analysis

B88/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 7, 2026

npx -y skills add Chorus-AIDLC/Chorus --skill openspec-aware --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Openspec Aware?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Openspec Aware
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/chorus-aidlc-openspec-aware-a5e70e44/badge)](https://www.skillsdirectory.com/skills/chorus-aidlc-openspec-aware-a5e70e44)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: openspec-aware
description: OpenSpec-mode authoring for Chorus PM workflows on OpenClaw — the default whenever OpenSpec is usable. Resolves the whole spec-mode contract (lite/openspec/off) by sourcing the plugin's shipped `bin/resolve-spec-mode.sh` (no SessionStart hook on OpenClaw), scaffolds `openspec/changes/<slug>/` on disk, and mirrors Markdown files into Chorus document drafts via `chorus mcp call --arg-file` (bash `chorus-api.sh` wrapper as fallback). When OpenSpec isn't usable the mode resolves to spec-lite (see the spec-lite skill). Required reading for the proposal, develop, and yolo skills.
license: AGPL-3.0
metadata:
  author: chorus
  version: "0.22.0"
  category: project-management
  mcp_server: chorus
---

# OpenSpec-aware Authoring (OpenClaw plugin)

This skill is a **shared sub-procedure** invoked by the Chorus stage skills (proposal, develop, yolo) when the resolved spec mode is a **usable OpenSpec** — spec-driven authoring through the [OpenSpec CLI](https://github.com/Fission-AI/OpenSpec):

- Activates when the resolved spec mode is a **usable OpenSpec** (see §1): `CHORUS_SPEC_MODE=openspec` *or* unset, **and** `CHORUS_OPENSPEC_MODE` not `off`, an `openspec/` directory at the project root, and the `openspec` CLI on `PATH`.
- Otherwise the calling skill follows the resolved mode — **spec-lite** (the default when OpenSpec isn't usable) or free-form (`CHORUS_SPEC_MODE=off`).

> **See also — `spec-lite` (the lightweight fallback):** OpenSpec (this skill) stays the default whenever it is usable. When OpenSpec is absent or disabled — or `CHORUS_SPEC_MODE=lite` — the mode resolves to **spec-lite**: a durable local `.chorus/specs/<slug>/spec.md` (never synced) + per-change dated folders `<slug>/<YYYY-MM-DD>-<change-slug>/` of Chorus-typed docs mirrored 1:1 into Chorus via the same `--arg-file` transport. See the `spec-lite` skill.

> **Tool namespace:** Chorus MCP tools are exposed under a `chorus__` prefix on OpenClaw (e.g. `chorus__chorus_pm_create_proposal`). Bare names are used in prose for readability — prepend `chorus__` when invoking the MCP tools directly. **Document-mirror calls do NOT go through the MCP harness at all** — they go through the `chorus` CLI (`chorus mcp call`, preferred) or the `chorus-api.sh` wrapper (fallback) (see §2 Rule 1), which talk to the Chorus MCP endpoint over HTTP using your API key, independent of the `chorus__` namespacing.

---

## §1. Resolve the spec mode — run the shipped resolver, every time (no SessionStart hook on OpenClaw)

> **OpenClaw difference:** the Claude Code / Codex / Pi ports precompute the spec mode once (in a SessionStart hook or an extension handler) and inject a `## Spec Mode` value into context. **OpenClaw does none of that.** You MUST resolve the mode yourself, at the moment you reach this skill from a stage skill. Do not look for an injected `CHORUS_OPENSPEC_ACTIVE` / `## Spec Mode` value — it will not exist on OpenClaw. Resolve the **whole** contract (lite / openspec / off), not just "is OpenSpec active".
>
> **Never hand-roll the rule in this Markdown.** The plugin ships the *canonical* resolver — `bin/resolve-spec-mode.sh`, byte-identical to the Claude Code copy and enforced so by `test-resolver-drift.sh` — precisely so this skill can **source** it instead of reproducing it. A prose copy of the rule drifts from the real one (it has); a sourced script cannot.

**The rule:** an explicit `CHORUS_SPEC_MODE` (`lite`/`openspec`/`off`) wins; when unset, **OpenSpec is the default whenever it is usable** — else **spec-lite**. OpenSpec is *usable* only when **all three** hold:

1. OpenSpec is not disabled (`CHORUS_OPENSPEC_MODE` **not** `off`; the `enableOpenSpec` plugin toggle not `false`).
2. The project root contains an `openspec/` directory (i.e. someone ran `openspec init` here).
3. The `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. When OpenSpec is *requested but usable=false* (signal 2 holds but 3 does not, say), surface a hint to the user — "OpenSpec repo detected — install with: `npm i -g @fission-ai/openspec`".

### Resolution block (run this)

```bash
# PROJECT_ROOT is your project root — OpenClaw does not export
# CLAUDE_PROJECT_DIR, so default to $PWD. The resolver reads it.
PROJECT_ROOT="${PWD}"

# Locate the resolver the plugin ships. Covers the npm install, a global npm
# install, and a linked dev checkout; the first hit wins.
RESOLVER=""
for candidate in \
  "$PWD/node_modules/@chorus-aidlc/chorus-openclaw-plugin/bin/resolve-spec-mode.sh" \
  "$(npm root -g 2>/dev/null)/@chorus-aidlc/chorus-openclaw-plugin/bin/resolve-spec-mode.sh" \
  "$PWD/packages/openclaw-plugin/bin/resolve-spec-mode.sh"
do
  [ -f "$candidate" ] && { RESOLVER="$candidate"; break; }
done

# shellcheck source=/dev/null
. "$RESOLVER"    # sets SPEC_MODE, SPEC_REASON, SPEC_FAIL, OPENSPEC_HINT, CHORUS_OPENSPEC_ACTIVE
echo "SPEC_MODE=$SPEC_MODE CHORUS_OPENSPEC_ACTIVE=$CHORUS_OPENSPEC_ACTIVE SPEC_FAIL=${SPEC_FAIL} REASON=${SPEC_REASON}"
```

If `RESOLVER` came out empty (the loop found nothing), **halt** and ask the user to run `/chorus spec` — which resolves the same contract from the plugin's `src/spec-mode.ts` — and paste the result. Do **not** substitute your own detection.

Branch on the result:

- `SPEC_FAIL` non-empty (explicit `CHORUS_SPEC_MODE=openspec` that can't be honored — either a config conflict or OpenSpec not installed; `OPENSPEC_HINT` carries the install command when it is merely missing) → the caller MUST **halt** and surface it verbatim. Do not silently fall back.
- `CHORUS_OPENSPEC_ACTIVE=1` → follow §3 (OpenSpec authoring).
- Otherwise (`SPEC_MODE=lite` or `off`, no fail) → this skill is a **no-op**; return to the calling skill, which follows the resolved mode: **spec-lite** (the default when OpenSpec isn't usable — see the `spec-lite` skill) or free-form (`off`). **Do not** scaffold `openspec/changes/`. **Do not** add the slug line to the proposal description.

Run this resolution whenever proposal / develop / yolo reference this skill. There is no host-injected value to read on OpenClaw — but "resolve it yourself" means *run the shipped resolver*, never re-derive the rule from this document's prose.

---

## §2. ⛔ Two non-negotiable rules

Both are enforced at review time. Both have caused incidents in past releases.

### Rule 1 — Fill `content` from the file (CLI preferred, bash-wrapper fallback); never re-type document content from agent output

Document/draft mirror calls (`chorus_pm_add_document_draft`, `chorus_pm_update_document_draft`, `chorus_pm_update_document`) **MUST** fill the `content` field from the local file's bytes, never from a hand-typed body. 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. Use whichever transport is available, preferred first:

- **Primary — the `chorus` CLI:** `chorus mcp call <tool_name> '<json-without-content>' --arg-file content=<file>`. `--arg-file content=<path>` reads the file's raw bytes and injects them as the JSON `content` string, byte-exact — the CLI's built-in replacement for `json_encode_file`, so no helper is needed. `chorus mcp call` reads the same `CHORUS_URL` / `CHORUS_API_KEY` from the environment (availability note below). See §3.6. **Requires chorus >= 0.17.0** (the `chorus mcp` subcommand was added then; an older CLI errors with "unknown command"); on any version or unknown-command failure, upgrade with `npm install -g @chorus-aidlc/chorus`.
- **Fallback — the `chorus-api.sh` wrapper, when `chorus` is not on `PATH`:** build `$PAYLOAD` with the `json_encode_file` helper and call `chorus-api.sh mcp-tool <tool_name> "$PAYLOAD"`. Defined in the §3.6 fallback block.

> **New to the `chorus` CLI?** See the **`chorus-cli`** skill for install, configuring agents (`chorus agents add|remove|list`), the connection env vars, and `chorus mcp` basics.

> **Acting identity — which agent the call acts as.** `chorus mcp call` resolves the agent from, in order: `CHORUS_AGENT_PROFILE` (a name or UUID) → `CHORUS_URL` + `CHORUS_API_KEY` in the environment → the single agent configured in `~/.chorus/daemon.json`. A daemon-woken session already has `CHORUS_AGENT_PROFILE` set. If a mirror call fails with `Multiple agents … specify --agent` (several agents configured and no profile/creds in the env), pass your own identity explicitly: `chorus mcp call <tool> … --agent <your-agentUuid>` — your UUID is in your `chorus_checkin` result, and `chorus agents` lists every configured name/UUID.

Reasons (they apply to both paths):

1. **Token cost.** Re-typing a multi-thousand-line markdown body through the LLM burns input + output tokens for every draft. Both the CLI's `--arg-file` and the fallback's `json_encode_file` stream the file's bytes into the JSON string — content never enters LLM context. A typical 3-doc proposal mirror costs roughly zero content-tokens this way; via direct MCP with a re-typed body it routinely costs 20k+.
2. **Byte-equality.** A file-fill path (CLI `--arg-file`, or the fallback's `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 byte-equality guarantee (modulo trailing `\n`) holds **only** on a file-fill path, never on LLM re-emission.
3. **Single source of truth.** With a file-fill mirror, the local `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` / `chorus-api.sh` availability on OpenClaw.** The document-mirror transport is either the `chorus` CLI (preferred) or the `chorus-api.sh` wrapper (fallback). The wrapper must be reachable as `chorus-api.sh` on `PATH` (the Chorus standalone skill bundle ships it; if you installed via that bundle it is already on `PATH`). If neither `chorus` nor `chorus-api.sh` is on `PATH` in your OpenClaw environment, do one of:
> - call the wrapper by its absolute path (e.g. `"$HOME/.chorus/bin/chorus-api.sh" mcp-tool ...`), or
> - reproduce its single behavior inline — POST a JSON-RPC `tools/call` for `<tool_name>` with arguments `$PAYLOAD` to `"$CHORUS_URL/api/mcp"` with header `Authorization: Bearer $CHORUS_API_KEY` using `curl`, capturing the raw body for the §6 halt-on-error check.
>
> Both `chorus mcp call` and the wrapper read `CHORUS_URL` and `CHORUS_API_KEY` from the environment (same values as your plugin config `chorusUrl` / `apiKey`). Export them before the first call if they are not already set. What you must NOT do is re-type the document body through the model — the file-fill rule stands regardless of which transport you use.

### Rule 2 — Halt on error via `chorus_check_response`

Every 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.

---

## §3. OpenSpec mode authoring

### 3.1 Pick a slug

`openspec/changes/<slug>/` is the local change folder. The slug must be:

- kebab-case (`add-export-csv`, not `addExportCsv` or `add_export_csv`),
- derived from the source Idea title,
- unique within `openspec/changes/`.

Record it for later steps:

```bash
SLUG="add-export-csv"
```

### 3.2 Scaffold the change folder

```bash
openspec 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.

### 3.3 Spec file shape (verified against `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 Requirements`

Append 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 Requirements`

**Whole-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 Requirements`

Delete 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 Requirements`

Rename 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):**

- Scenarios MUST use **exactly 4 hashtags** (`#### Scenario:`). 3 hashtags or a bullet list silently fail validation.
- Every `### Requirement:` under `ADDED` or `MODIFIED` MUST have at least one `#### Scenario:`.
- `MODIFIED` blocks MUST include the **full updated content** — they overwrite, not patch.
- Use `SHALL` / `MUST` for normative requirements; avoid `should` / `may`.
- The merge into `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:

```bash
openspec validate "$SLUG"
```

### 3.4 Filling the `content` field byte-exact

The document `content` must be inserted **byte-for-byte** from the local file — never re-typed by the LLM. Two mechanisms, preferred first:

- **Primary — `chorus mcp call … --arg-file content=<path>`** (§3.6). The CLI reads the file's raw bytes and injects them as the JSON `content` string. This is the byte-faithful replacement for `json_encode_file`, so on the CLI path **no helper is needed** — pass the base JSON without a `content` field and let `--arg-file` fill it.
- **Fallback — `json_encode_file`** (defined in the §3.6 fallback block, used only when `chorus` is not on `PATH`). With `jq` available it streams the file through `jq -Rs '.'`; the pure-shell branch matches `chorus-api.sh`'s own escaping when `jq` is missing.

Round-trip: the Chorus backend appends a single `\n` to draft content on write, so server `content` is **byte-equal modulo a trailing newline**. Reviewers diffing local file vs server should ignore that one byte.

### 3.5 Create the proposal container with the slug provenance line

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>
```

- on its own line (no other text on that line),
- literal prefix `OpenSpec change slug: ` (capital O, capital S, single space after colon),
- no trailing punctuation,
- value matches the slug passed to `openspec new change`.

This line is machine-grep-able by future runs of this skill and by the §3.9 archive trigger.

### 3.6 Mirror each document draft (CLI primary, wrapper fallback)

> **Rule 1 reminder:** `content` comes from the file's bytes, never a hand-typed body. The agent must not retype the document body.

Ensure `CHORUS_URL` and `CHORUS_API_KEY` are exported (Rule 1 note) — both the CLI and the wrapper read them. Define the halt-on-error helper from §6 once at the top. **Primary path — the `chorus` CLI:** pass the base JSON *without* a `content` field and let `--arg-file content=<file>` fill it byte-exact. One call per file:

```bash
# PRD draft — --arg-file fills content byte-exact from the file; no json_encode_file needed.
RESULT=$(chorus mcp call chorus_pm_add_document_draft \
  "{\"proposalUuid\":\"$PROPOSAL_UUID\",\"type\":\"prd\",\"title\":\"PRD: $HUMAN_TITLE\"}" \
  --arg-file content="openspec/changes/$SLUG/proposal.md")
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.

#### Fallback — when the `chorus` CLI is not on `PATH`

If `command -v chorus` fails, mirror through the `chorus-api.sh` wrapper (or the absolute-path / inline-`curl` variants from the Rule 1 availability note). Define `json_encode_file` here (it is used **only** on this fallback path), then build `$PAYLOAD` with an embedded `content`. The `chorus_check_response` halt-on-error check applies exactly as on the primary path.

```bash
# Define once, fallback-only: byte-faithful file → JSON string.
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
}

# 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/')
```

### 3.7 Editing a draft after the first mirror

Local file changes propagate via `chorus_pm_update_document_draft` — same primary/fallback split as §3.6, same halt check. Primary (CLI):

```bash
RESULT=$(chorus mcp call chorus_pm_update_document_draft \
  "{\"proposalUuid\":\"$PROPOSAL_UUID\",\"draftUuid\":\"$PRD_DRAFT_UUID\"}" \
  --arg-file content="openspec/changes/$SLUG/proposal.md")
RC=$?
chorus_check_response "chorus_pm_update_document_draft" "$RC" "$RESULT"
```

Fallback (no `chorus` on `PATH`) — `json_encode_file` from the §3.6 fallback block:

```bash
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 Editing a Document after proposal approval

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`. Primary (CLI):

```bash
RESULT=$(chorus mcp call chorus_pm_update_document \
  "{\"documentUuid\":\"$SPEC_DOCUMENT_UUID\"}" \
  --arg-file content="openspec/changes/$SLUG/specs/<capability>/spec.md")
RC=$?
chorus_check_response "chorus_pm_update_document" "$RC" "$RESULT"
```

Fallback (no `chorus` on `PATH`) — `json_encode_file` from the §3.6 fallback block:

```bash
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"
```

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: `.

### 3.9 Archive after the last task is verified

> **OpenClaw difference:** the Claude Code plugin has a PostToolUse hook (`bin/on-post-verify-task.sh`) that fires after `chorus_admin_verify_task` and injects an `openspec archive <slug>` reminder. **OpenClaw has no such hook.** You (the agent) must detect the trigger yourself: after each `chorus_admin_verify_task`, check whether the just-verified task was the **LAST** task of its OpenSpec-mode idea (every Task across every approved Proposal of that idea is now `done`/`closed`, and the proposal `description` carries an `OpenSpec change slug: <slug>` line). If so, run the archive flow below. If not, do nothing.

When the trigger holds, you perform the archive:

1. **Run archive locally.** Use `--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.)

2. **Mirror each updated `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.

3. **Halt on any error** from `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.)

4. **Confirm success.** List `openspec/specs/<capability>/spec.md` files and verify they round-trip byte-equal (modulo trailing newline) with their Chorus Document counterparts.

**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, do nothing — no archive. Existing free-form behavior is preserved.

---

## §4. Fallback authoring (resolved mode is not a usable OpenSpec)

When the §1 resolution does not yield a usable OpenSpec (`CHORUS_OPENSPEC_ACTIVE=0`, no `SPEC_FAIL`), this skill is a **no-op** — return to the calling skill, which follows the resolved mode: **spec-lite** (the default when OpenSpec isn't usable — see the `spec-lite` skill) or free-form (`CHORUS_SPEC_MODE=off`). From this skill's side, regardless of which:

- No `openspec/changes/` folder is created or referenced.
- No `OpenSpec change slug: …` line is added to the proposal description.
- The §3.9 archive flow does nothing (no OpenSpec slug → no archive).

In **spec-lite** the caller mirrors the dated-folder docs under `.chorus/specs/<slug>/` via the same `--arg-file` transport + Rule 1 / Rule 2 (that is the `spec-lite` skill's job). In **free-form** (`off`) there is no local file source of truth, so document drafts are authored via direct MCP `chorus_pm_add_document_draft` calls with inline `content` — same as before this skill existed, and Rule 1 (file-fill mirror) does not apply.

---

## §5. Document type mapping (reference table)

| 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.

---

## §6. Failure visibility — the `chorus_check_response` helper

This helper guards **both** the primary CLI path and the fallback wrapper path. On the fallback path there 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. (If you reproduced the wrapper inline via `curl` per the Rule 1 note, the same three-signal check still applies to the raw HTTP body. `chorus mcp call` exits non-zero on tool/transport errors, so `RC` is reliable on the primary path — but run the same three-signal check on both, as defense in depth.)

Define this helper **once** at the top of the authoring session and use it after every mirror call (CLI or wrapper):

```bash
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** — do **not**:

- Collapse to `|| true`.
- Redirect stderr to `/dev/null`.
- Bury the wrapper call inside a pipeline (masks `$?`).
- Skip capturing `$RESULT` into a variable; the helper needs the body.
- Use only `if [ "$RC" -ne 0 ]; then ...` — that misses the HTTP-error path.

**Minimal call site shape (both paths):**

```bash
# Primary — chorus CLI:
RESULT=$(chorus mcp call <tool_name> '<json-without-content>' --arg-file content=<file>)
RC=$?
chorus_check_response "<tool_name>" "$RC" "$RESULT"

# Fallback — chorus-api.sh wrapper (chorus not on PATH):
RESULT=$(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.

---

## §7. Quick reference checklist

When invoked from a stage skill (proposal / develop / yolo):

1. **Run the §1 resolution yourself** (no SessionStart hook on OpenClaw) by sourcing the shipped `bin/resolve-spec-mode.sh`. It resolves the whole contract (lite / openspec / off); do NOT hand-roll an OpenSpec-only check or re-derive the rule from prose.
2. If `SPEC_FAIL` is set (explicit `CHORUS_SPEC_MODE=openspec` unusable) → **halt** and surface it. If `CHORUS_OPENSPEC_ACTIVE=0` (resolved mode = spec-lite or free-form) → no-op; return to the caller per the resolved mode (§4).
3. Otherwise (`CHORUS_OPENSPEC_ACTIVE=1`):
   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. Export `CHORUS_URL` / `CHORUS_API_KEY` (both the CLI and the wrapper read them); define the `chorus_check_response` helper. Prefer `chorus mcp call … --arg-file content=<file>` for mirrors (§3.6) — no `json_encode_file` needed on that path; define `json_encode_file` and confirm `chorus-api.sh` is reachable (Rule 1 note) only when falling back to the wrapper because `chorus` is not on `PATH`.
   g. For each row in §5 with "yes" — mirror via `chorus mcp call chorus_pm_add_document_draft … --arg-file content=<file>` (§3.6; fallback = `chorus-api.sh mcp-tool`). Record each `$DRAFT_UUID`.
   h. On any failed `chorus_check_response` — halt, surface the error, do NOT proceed.
4. Edits before approval → §3.7. Edits after approval → §3.8.
5. Last task verified → detect the trigger yourself (no hook) → run §3.9 archive flow.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.