<!-- source: openspec/current/agents/muse.md -->

---
title: Muse (CLI agent)
status: stable
owner: pavel
audience: [pavel, ai, engineer]
tldr: Capability spec for the muse CLI binary — turns Linear issues into OpenSpec change proposals via the draft command.
---
# Muse (CLI agent)

> The spec-authoring CLI. Turns Linear issues into OpenSpec change proposals.

---

## Status

**MVP CLI shipped 2026-05-12 from change `bootstrap-cli-orchestra-mvp` Phase 4.**

This spec covers the `muse` CLI binary (`packages/muse/bin/muse/`). The broader Muse AI substrate (Architect mode, Builder mode, Canvas, Scout) is documented in [`openspec/current/muse.md`](../muse.md). This file is scoped to the CLI orchestration surface that Phase 4 adds.

---

## MVP CLI — draft command (applied 2026-05-12 from change `bootstrap-cli-orchestra-mvp` Phase 4)

Phase 4 of `bootstrap-cli-orchestra-mvp` ships the `muse draft` command in `packages/muse/bin/muse/`.

### `muse draft <linear-issue-key>`

Drafts an OpenSpec change triplet from a Linear issue via three sequential LLM calls, then writes the files to `openspec/changes/<slug>/`.

```
muse draft RAP-456
muse draft RAP-456 --dry-run
muse --capabilities
muse --help
```

**Flow:**

1. Fetch Linear issue (title, description) via Linear GraphQL API
2. Generate change slug: kebab-case from issue title, ≤6 words
3. Load prompts from `packages/muse/src/prompts/`
4. Three LLM calls (sequential):
   - **Architect** (`claude-opus-4-8`) → `design.md`
   - **Storyteller** (`claude-sonnet-4-6`) → `proposal.md`
   - **Planner** (`claude-sonnet-4-6`) → `tasks.md`
5. Write triplet to `openspec/changes/<slug>/`
6. Emit `SPEC_DRAFTED` event to `~/.rapoport/events/YYYY-MM-DD.jsonl`
7. Print path + per-file token/cost summary

**No auto-PR, no auto-branch.** Human reviews every Muse output in MVP.

**Output (TTY):**

```
Change drafted at: openspec/changes/<slug>/

  design.md    <N> in / <N> out  $<cost>
  proposal.md  <N> in / <N> out  $<cost>
  tasks.md     <N> in / <N> out  $<cost>

  Total: <N> in / <N> out  $<total>

Next: git diff openspec/changes/<slug>/ → review → open PR manually
```

### Environment variables

| Variable | Required | Notes |
|---|---|---|
| `LINEAR_BOT_API_KEY` | yes | Linear API key for issue fetching |
| `ANTHROPIC_API_KEY` | yes | Anthropic API key for LLM calls |

### Prompts

Stored as Markdown in `packages/muse/src/prompts/`:

| File | Role | Model |
|---|---|---|
| `system.md` | Shared system prompt — Muse's identity and output conventions | — |
| `design.md` | User prompt template for design.md generation | architect / opus |
| `proposal.md` | User prompt template for proposal.md generation | storyteller / sonnet |
| `tasks.md` | User prompt template for tasks.md generation | planner / sonnet |

### Event emitted

```
spec.drafted: { changeSlug: string, files: string[] }
```

Written to `~/.rapoport/events/YYYY-MM-DD.jsonl` tagged with the Linear ID. Observable via `maestro lineage <linear-id>`.

### Implementation location

`packages/muse/bin/muse/` — follows same binary pattern as `packages/muse/bin/maestro/`. Compiled by `tsconfig.bin.json` to `dist/bin/muse/`. Registered in `package.json` as `bin.muse: dist/bin/muse/index.js`.

---

## Access scope

Following the access scope declared in [`bootstrap-cli-orchestra-mvp/design.md § Access scope — Muse`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md).

| Resource | Read | Write |
|---|---|---|
| `openspec/current/` | yes (all) | no |
| `openspec/changes/<slug>/` | yes | yes (proposal.md, design.md, tasks.md only) |
| `openspec/archive/` | yes | no |
| `~/.rapoport/events/` | no | yes (append via cli-core/events) |
| Code | read for context | no |
| Linear | issue read | no |
| Anthropic API | yes | yes |
| `LINEAR_BOT_API_KEY` env | read | no |
| `ANTHROPIC_API_KEY` env | read | no |

### Untrusted input boundary

Untrusted text inputs (Linear bodies, PR review comments, PR-branch repo content, Telegram messages, downstream API responses) are wrapped in `<untrusted_input source="...">` sentinel tags at prompt-build time. The agent's system prompt forbids following instructions inside the block. Convention is documented in [`openspec/_methodology/security/untrusted-input-convention.md`](../../_methodology/security/untrusted-input-convention.md).

---

## Related capabilities

- [`openspec/current/muse.md`](../muse.md) — Muse AI substrate (Architect, Builder, Canvas, Scout modes)
- [`openspec/current/agents/maestro.md`](./maestro.md) — Maestro CLI; `maestro lineage` surfaces Muse draft events
- [`openspec/archive/2026-05-12-bootstrap-cli-orchestra-mvp/`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/) — full MVP design and task history
- `packages/muse/bin/muse/` — implementation
- `packages/cli-core/` — shared event emission and LLM call primitives

---

## MVP draft command shipped (applied 2026-05-13 from change `bootstrap-cli-orchestra-mvp`)

### Command surface

`muse draft <linear-issue-key>` — the sole command in the MVP release. Full interface, flow, prompts, and environment variables are documented in [§ MVP CLI — draft command](#mvp-cli--draft-command-applied-2026-05-12-from-change-bootstrap-cli-orchestra-mvp-phase-4) above.

### Model bindings

The `muse draft` command makes three sequential LLM calls. Each is bound to a row in [`openspec/_methodology/tools/agent-model-mapping.md § Per-agent default + fallback`](../../_methodology/tools/agent-model-mapping.md#per-agent-default--fallback):

| LLM call role | Methodology row | Default model | Fallback | Output file |
|---|---|---|---|---|
| Architect | **Muse — Architect** | `claude-opus-4-8` | `claude-sonnet-4-6` | `design.md` |
| Storyteller | **Muse — Builder** | `claude-sonnet-4-6` | `claude-opus-4-8` | `proposal.md` |
| Planner | **Muse — Builder** | `claude-sonnet-4-6` | `claude-opus-4-8` | `tasks.md` |

The Storyteller and Planner roles are sub-roles within Muse's Builder mode for the `draft` pipeline. Consumers resolving "which model does Muse use?" follow the methodology doc rows; identifier strings update in `_methodology/tools/agent-model-mapping.md § Model identifier table` independently of this spec.

### Access scope

Access scope declared as an explicit allowlist following [`openspec/_methodology/agent-access-scope.md`](../../_methodology/agent-access-scope.md). Anything not declared is denied. Future tool additions are denied by default unless explicitly added to the allowlist below.

The overview-level three-column table is at [`openspec/_methodology/agent-access-scope.md § Muse`](../../_methodology/agent-access-scope.md#muse). The operative five-column form:

| Resource | Read | Write | Notes | Source of truth |
|---|---|---|---|---|
| `openspec/current/` | yes (all) | no | Input context for LLM calls | CLAUDE.md § Hard rule 7 |
| `openspec/changes/<slug>/` | yes | yes | `proposal.md`, `design.md`, `tasks.md` only | [`bootstrap-cli-orchestra-mvp/design.md § Access scope — Muse`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md) |
| `openspec/archive/` | yes | no | Reference context only | [`bootstrap-cli-orchestra-mvp/design.md § Access scope — Muse`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md) |
| `~/.rapoport/events/` | no | yes (append) | `SPEC_DRAFTED` event via `cli-core/events` | [`openspec/current/cli-core.md § Event schema`](../cli-core.md) |
| Code (`apps/`, `packages/`) | read for context | no | LLM prompt context only; no code writes | [`bootstrap-cli-orchestra-mvp/design.md § Access scope — Muse`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md) |
| GitHub | issue/PR read | no | Pavel opens all PRs in MVP | [`bootstrap-cli-orchestra-mvp/design.md § No auto-PR`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md) |
| Linear | issue read | no | `LINEAR_BOT_API_KEY` env; via forge/core Linear client (see forge/core carve-out below) | [`bootstrap-cli-orchestra-mvp/design.md § Access scope — Muse`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md) |
| Anthropic API | yes | yes | `ANTHROPIC_API_KEY` env; via forge/core Anthropic wrapper | [`bootstrap-cli-orchestra-mvp/design.md § Access scope — Muse`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md) |
| Infisical | `/muse/*` only | no | Deferred — reads from env for MVP | [`bootstrap-cli-orchestra-mvp/design.md § Access scope — Muse`](../../archive/2026-05-12-bootstrap-cli-orchestra-mvp/design.md) |

**Forge/core carve-out — Linear and Anthropic clients:** `packages/muse` imports the `@rapoport-studio/forge/core` subpath for the Linear SDK client (`packages/forge/src/core/linear-client.ts`) and the Anthropic wrapper (`packages/forge/src/core/anthropic.ts`). This reuses already-vetted primitives rather than reimplementing them inside Muse, keeps a single auth path and a single Octokit/Linear/Anthropic surface across both CLIs, and defers lifting these into a dedicated `packages/cli-core/` library until a third consumer triggers the rule-of-three. The carve-out is defined in CLAUDE.md § Package boundaries as a pre-extract arrangement.

**Why this list, not more:** Muse in MVP is a read-from-Linear, write-to-openspec-changes, emit-an-event command. It does not write code, does not open PRs, does not write to Linear, and does not read from the database. The scope is the minimum required to draft a spec triplet and surface it for human review before any further automation.

### Untrusted input boundary

Untrusted text inputs (Linear bodies, PR review comments, PR-branch repo content, Telegram messages, downstream API responses) are wrapped in `<untrusted_input source="...">` sentinel tags at prompt-build time. The agent's system prompt forbids following instructions inside the block. Convention is documented in [`openspec/_methodology/security/untrusted-input-convention.md`](../../_methodology/security/untrusted-input-convention.md).


### `muse draft` CLI substrate access (applied 2026-05-24 from change complete-idea-to-forge-loop)

**RAP-1206 (Phase 4 of complete-idea-to-forge-loop).** The `muse draft` CLI's access scope widens to read/write the studio substrate directly. After Phase 4 lands, the CLI's authoritative output is no longer disk-side files — it's an idea row + four file rows in Supabase.

Substrate operations the CLI performs against `SUPABASE_SERVICE_ROLE_KEY` (bypassing RLS):

- `projects`: SELECT by slug; INSERT if missing (uses `STUDIO_DEFAULT_PROJECT_SLUG` env, default `studio`).
- `specs`: SELECT by project_id; INSERT if missing.
- `ideas`: SELECT by `rap_id`; INSERT if missing with `source='agent'`, `status='captured'`. Idempotent (D-IFL-5).
- `files`: UPSERT by `(idea_id, path)` at `status='accepted'` (D-IFL-4 — no human-in-the-loop acceptance for CLI emissions).
- `ideas` PATCH (when `--convert`): writes `github_branch`, `github_repo_full_name`, `converted_to_project_at`, `base_sha_at_conversion`.
- `files` PATCH (when `--convert`): writes `base_sha`, `base_content`, `status='applied'` per row.
- `ideas` PATCH (when `--open-pr`): writes `last_pr_url`.

GitHub operations (via `@rapoport-studio/forge/core` `GitHubClient`, allowed by CLAUDE.md package boundaries — muse may import forge/core's read-only clients):

- `branchExists` pre-flight (errors loudly on collision — muse v1 does not orphan-recover; the studio handles that path).
- `getRefSha('main')` + `getFileContent('main', path)` for base capture.
- `createBranch` + `createTreeCommit` for the four artefacts.
- `createPullRequest` (idempotent on `ideas.last_pr_url`).
- `pulls.get` for the `drafted → applied` transition merged-state check (read by the studio poll action, not the CLI).

Required env: `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, `GITHUB_BOT_TOKEN` (for `--convert` / `--open-pr` only), `LINEAR_BOT_API_KEY`, `ANTHROPIC_API_KEY`. Source via `infisical run --env=prod --path=/`.

Source: `openspec/archive/2026-05-24-complete-idea-to-forge-loop/`.

## Storyteller prompt teaches canonical proposal.md header (applied 2026-05-26 from change `teach-muse-proposal-header`)

The Storyteller prompt (`packages/muse/src/prompts/proposal.md`) — the user-prompt template the `muse draft <linear-issue-key>` CLI dispatches for `proposal.md` generation per `§ MVP CLI — draft command` above — now teaches the canonical header (H1 in sentence case + `**Affects capability spec:**` blockquote) per [`openspec/AGENTS.md § proposal.md canonical shape`](../../AGENTS.md). Every `muse draft` invocation produces a `proposal.md` ready for the studio's spec-preview surface — no manual header insertion needed.

Per **D-MPH-7** the extension was minimal: the existing YAML frontmatter scaffold is preserved (it has live human-reader signal, dropping it is a separate convention call), the four body sections are unchanged, and the H1 + Affects blockquote are inserted between frontmatter close and `## Why`. The legacy `# Proposal — <short title>` H1 form was replaced with `# <Change title in sentence case>` so the parser captures a clean title.

A drift-guard test at `packages/muse/src/prompts/proposal.test.ts` asserts the Storyteller template continues to teach the canonical header — parallel guard to the Architect mode test at `packages/muse/src/modes/architect/prompts/system.test.ts § proposal.md template teaches the canonical header (…)`. Both must pass; a CI fail on either means the contract drifted on one of the two emission paths.

Source: `openspec/archive/2026-05-26-teach-muse-proposal-header/` (PR-B #1804). Decision D-MPH-7 ratified.
