Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ vite.config.ts.timestamp-*
CLAUDE.md
.DS_Store

# Cursor
.cursor/

# Pnpm
.pnpm-store/
result
Expand Down
22 changes: 18 additions & 4 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ openspec update

### `openspec list`

List changes or specs in your project.
List changes, specs, or archived changes in your project.

```
openspec list [options]
Expand All @@ -175,9 +175,12 @@ openspec list [options]
|--------|-------------|
| `--specs` | List specs instead of changes |
| `--changes` | List changes (default) |
| `--archive` | List archived changes |
| `--sort <order>` | Sort by `recent` (default) or `name` |
| `--json` | Output as JSON |

Only one mode applies per run. If you pass more than one of `--changes`, `--specs`, or `--archive`, precedence is: `--archive` over `--specs` over default (changes). With `--json`, the root key matches the mode: `{ "changes": [...] }`, `{ "specs": [...] }` (each item has `id`, `requirementCount`), or `{ "archivedChanges": [...] }` (same shape as changes: name, completedTasks, totalTasks, lastModified, status).

**Examples:**

```bash
Expand All @@ -189,16 +192,27 @@ openspec list --specs

# JSON output for scripts
openspec list --json

# List archived changes
openspec list --archive

# JSON for specs (script/agent use)
openspec list --specs --json

# JSON for archived changes
openspec list --archive --json
```

**Output (text):**

```
Active changes:
add-dark-mode UI theme switching support
fix-login-bug Session timeout handling
Changes:
add-dark-mode 2/5 tasks 2h ago
fix-login-bug ✓ Complete 1d ago
```
Comment on lines 206 to 212
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fenced code block missing language specifier.

The static analysis tool (markdownlint MD040) flags this code block. Add a language identifier for consistency with the rest of the file.

Proposed fix
 **Output (text):**
 
-```
+```text
 Changes:
   add-dark-mode     2/5 tasks      2h ago
   fix-login-bug     ✓ Complete     1d ago
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.20.0)</summary>

[warning] 199-199: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @docs/cli.md around lines 197 - 203, The fenced code block under the "Output
(text):" section lacks a language specifier which triggers markdownlint MD040;
update the opening fence to include a language identifier (e.g., change ``` to

explicitly marked as text and the linter error is resolved.


With `--archive`, the header is "Archived changes:" and rows show archived change names with task progress and last modified.

---

### `openspec view`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-13
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Design: Add Spec title and list JSON fields

## Context

The Spec type is defined in `src/core/schemas/spec.schema.ts` (name, overview, requirements, metadata). `MarkdownParser.parseSpec(name)` in `src/core/parsers/markdown-parser.ts` builds a Spec from spec.md: it requires `## Purpose` and `## Requirements`, maps Purpose content to `overview`, and passes through the `name` argument (spec id) as the spec’s `name`. The document’s first `# H1` is not currently parsed or stored. Multiple callers depend on the Spec shape: validation (SpecSchema.safeParse), list command (parseSpec for specs mode), view dashboard, deprecated spec show/list, and json-converter. Changing the schema or parser return shape can break any of these if not updated consistently.

## Goals / Non-Goals

**Goals:**

- Add a required `title` field to the Spec type, set from the first `# H1` in the spec document, with fallback to the spec id (`name`) when H1 is missing or unparseable.
- Keep `openspec list --specs --json` output unchanged (each item: `id`, `requirementCount` only). Add a `--detail` flag so that when used (e.g. `openspec list --specs --json --detail`), each spec entry additionally includes `title` and `overview`, enabling LLMs and scripts to optionally discover and select specs without reading each file.
- Ensure all existing callers of SpecSchema and `parseSpec()` are identified, updated if they construct or consume Spec, and verified so that existing behavior is preserved and new behavior is consistent.

**Non-Goals:**

- Adding a separate `summary` field (overview remains the description).
- Changing required sections (Purpose, Requirements) or validation rules beyond accommodating `title`.
- Frontmatter or other overrides for title in this change.

## Call-site audit (mandatory before implementation)

Before changing the schema or parser, every consumer of the Spec type or `parseSpec()` must be checked and updated as needed so that existing functionality is not broken.

| Location | Usage | Required change / check |
|----------|--------|---------------------------|
| `src/core/schemas/spec.schema.ts` | Defines Spec type | Add required `title: z.string().min(1, …)`. |
| `src/core/parsers/markdown-parser.ts` | `parseSpec(name): Spec` | Extract first level-1 heading; set `title` to that or `name`. Return `title` in the Spec object. |
| `src/core/validation/validator.ts` | `parser.parseSpec(specName)` then `SpecSchema.safeParse(spec)`; `applySpecRules(spec)` uses `spec.overview` | Parser will always return `title`. Validator only needs to pass through; confirm no code builds a Spec manually without `title`. |
| `src/core/list.ts` | `parser.parseSpec(id)`; uses `spec.requirements.length` for specs mode JSON | Default (no `--detail`): keep current shape (id, requirementCount only). When `options.json` and `options.detail` are both true, add `title: spec.title` and `overview: spec.overview` to each item in the `specs` array. CLI: add `--detail` option to list command. |
| `src/core/view.ts` | `parser.parseSpec(entry.name)`; uses `spec.requirements.length` | No change required for this change; optional later: show `spec.title` in dashboard. |
| `src/commands/spec.ts` | `parseSpecFromFile` → `parseSpec`; `show` uses `parsed.name` as `title`, `parsed.overview`; `list` uses `spec.name` as `title` | **show**: Use `parsed.title` for the output `title` field instead of `parsed.name`. **list**: Use `spec.title` instead of `spec.name` for the displayed/list title. |
| `src/core/converters/json-converter.ts` | `parser.parseSpec(specName)` then spreads `...spec` into JSON | No code change; once parser returns `title`, the converted JSON will include it. |

**Implementation order:** (1) Schema + parser (so every Spec has `title`), (2) list.ts (add `--detail` option and conditional title/overview in specs JSON), (3) spec.ts (deprecated show/list use `title`), (4) run tests and any code that constructs Spec or validates with SpecSchema to confirm no regressions.

## Decisions

1. **Title required in schema**
`title` is a required field so that every Spec is guaranteed to have a display name. The parser is the single place that sets it (H1 or fallback to name), so no caller has to handle missing title.

2. **H1 extraction in parser**
Reuse the existing section parse: the first top-level section with `level === 1` from `parseSections()` is the document title. Use its `title` property (the heading text). If there is no level-1 section, use the `name` argument. This avoids a separate first-line regex and keeps parsing in one place.

3. **List JSON: default unchanged; `--detail` adds title and overview**
Default `openspec list --specs --json` is unchanged: each element in `specs` has only `id` and `requirementCount`. When `--detail` is passed (e.g. `openspec list --specs --json --detail`), each element additionally includes `title` and `overview`. Use `overview` (not a new name like `purpose`) so the field name matches the Spec type and existing `openspec spec show --json` output.

4. **Deprecated spec list / show**
`openspec spec show --json` already outputs `title` and `overview`; switch the source of `title` from `parsed.name` to `parsed.title`. `openspec spec list` (and --long/--json) currently use `spec.name` as the display title; use `spec.title` so behavior aligns with the new model and with list --specs --json.

## Risks / Trade-offs

- **[Risk]** Existing tests or code that build a Spec object manually (e.g. mocks) may omit `title` and fail validation.
**Mitigation:** Grep for `Spec` construction and `SpecSchema.safeParse`/`parseSpec`; add `title` to any fixture or mock that returns a Spec.

- **[Risk]** Specs with no `# H1` (only `## Purpose`, etc.) will get `title = name`; that may be less readable than a proper H1.
**Mitigation:** Acceptable; conventions can recommend adding an H1. No change to required sections.

- **[Trade-off]** Parser does slightly more work (scan for first H1). Cost is one pass over already-parsed sections; negligible.

## Migration Plan

- No data migration. Existing spec.md files need no change; missing H1 is handled by fallback to name.
- After implementation: run full test suite; manually run `openspec list --specs --json` (confirm unchanged: id, requirementCount only), `openspec list --specs --json --detail` (confirm title and overview present), `openspec spec show <id> --json`, and `openspec spec list --json` to confirm shape and that title/overview appear as expected where applicable.

## Open Questions

- None for this change. Optional follow-up: document in openspec-conventions that the first `# H1` is the canonical display title and is used in list/APIs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Why

`openspec list --specs --json` currently returns only `id` and `requirementCount` per spec. LLMs and scripts sometimes need a human-readable title and a short description (e.g. from the spec document) to discover and select relevant specs without opening each spec file. The spec document already has a top-level heading (`# H1`) and a Purpose section (`## Purpose`). We should (1) add a required `title` to the Spec model and (2) allow list JSON to optionally include `title` and `overview` via a new flag, without changing the default list output.

## What Changes

- **Spec schema**: Add a required `title` field to the Spec type. When parsing a spec, set `title` from the document's first `# H1`; when that is missing or unparseable, fall back to the spec id (`name`). No new `summary` field — keep using `overview` (## Purpose) for description.
- **Parser**: In `MarkdownParser.parseSpec()` (or the code that builds the Spec object), extract the first level-1 heading and assign it to `title`; otherwise use the passed-in `name`.
- **List JSON**: Keep `openspec list --specs --json` unchanged: each spec entry continues to have only `id` and `requirementCount`. Add a new `--detail` flag; when used with `--specs --json` (e.g. `openspec list --specs --json --detail`), include `title` and `overview` in each spec entry so tooling can optionally get display name and Purpose text.
- **Deprecated `spec list`**: Align with the new model: `openspec spec list --long` and its JSON output should use the parsed `title` (from H1 or name) so behavior is consistent.

## Capabilities

### New Capabilities

None. This change extends the existing spec model and list command only.

### Modified Capabilities

- **openspec-conventions**: Document that a spec's document title (first `# H1`) is the canonical display title and is exposed in list/APIs; when H1 is absent, the spec id is used. No change to required sections (Purpose, Requirements).
- **cli-list**: Extend the list command specification: (1) `openspec list --specs --json` remains unchanged (each item has `id`, `requirementCount` only). (2) Add a `--detail` option; when `openspec list --specs --json --detail` is used, each item in the `specs` array additionally includes `title` (string) and `overview` (string).

## Impact

- **Code**: `src/core/schemas/spec.schema.ts` (add required `title`), `src/core/parsers/markdown-parser.ts` (extract first H1, set title; fallback to name), `src/core/list.ts` (add `--detail`; when `--specs --json --detail`, include `title` and `overview` in each spec entry; default list --specs --json unchanged), `src/commands/spec.ts` (use parsed title in deprecated spec list when available).
- **Validation**: Ensure validators that construct or validate Spec objects supply `title`; existing specs without an explicit H1 will get `title = name` after the parser fallback.
- **Tests**: Update or add tests for parser (H1 → title, no H1 → name), list JSON without --detail (unchanged shape), list JSON with --detail (title, overview), and deprecated spec list output.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## MODIFIED Requirements

### Requirement: Output Format
The command SHALL display items in a clear, readable table format with mode-appropriate progress or counts when `--json` is not provided, or output JSON when `--json` is provided.

#### Scenario: Displaying change list (default)
- **WHEN** displaying the list of changes without `--json`
- **THEN** show a table with columns:
- Change name (directory name)
- Task progress (e.g., "3/5 tasks" or "✓ Complete")

#### Scenario: Displaying spec list
- **WHEN** displaying the list of specs without `--json`
- **THEN** show a table with columns:
- Spec id (directory name)
- Requirement count (e.g., "requirements 12")

#### Scenario: JSON output for specs
- **WHEN** `openspec list --specs --json` is executed without `--detail`
- **THEN** output a JSON object with key `specs` and an array of objects with `id` and `requirementCount` only
- **AND** output `{ "specs": [] }` when no specs exist

#### Scenario: JSON output for specs with detail
- **WHEN** `openspec list --specs --json --detail` is executed
- **THEN** output a JSON object with key `specs` and an array of objects with `id`, `requirementCount`, `title`, and `overview`
- **AND** `title` SHALL be the spec's display title (from document H1 or spec id)
- **AND** `overview` SHALL be the spec's Purpose section content
- **AND** output `{ "specs": [] }` when no specs exist

#### Scenario: Displaying archive list
- **WHEN** displaying the list of archived changes without `--json`
- **THEN** show a table with columns: archived change name (directory name), task progress, and last modified (e.g. relative time)

#### Scenario: JSON output for archive
- **WHEN** `openspec list --archive --json` is executed
- **THEN** output a JSON object with key `archivedChanges` and an array of objects with `name`, `completedTasks`, `totalTasks`, `lastModified` (ISO string), and `status`

### Requirement: Flags
The command SHALL accept flags to select the noun being listed. When more than one of `--changes`, `--specs`, or `--archive` is provided, the effective mode SHALL be determined by precedence: `--archive` overrides `--specs`, `--specs` overrides default (changes). The command SHALL accept a `--detail` flag that, when used with `--specs --json`, causes each spec entry to include `title` and `overview`.

#### Scenario: Selecting specs
- **WHEN** `--specs` is provided
- **THEN** list specs instead of changes

#### Scenario: Selecting changes
- **WHEN** `--changes` is provided
- **THEN** list changes explicitly (same as default behavior)

#### Scenario: Selecting archive
- **WHEN** `--archive` is provided
- **THEN** list archived changes (directories under openspec/changes/archive/)

#### Scenario: Mode precedence
- **WHEN** more than one of `--changes`, `--specs`, or `--archive` is provided
- **THEN** the effective mode SHALL be determined by precedence: `--archive` overrides `--specs`, `--specs` overrides default (changes)

#### Scenario: Requesting detail for spec list JSON
- **WHEN** `--detail` is provided together with `--specs --json`
- **THEN** each object in the `specs` array SHALL include `title` and `overview` in addition to `id` and `requirementCount`
- **AND** when `--detail` is omitted, spec list JSON SHALL remain unchanged (id and requirementCount only)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## ADDED Requirements

### Requirement: Spec document title as canonical display title

A spec's document title SHALL be the first level-1 heading (`# H1`) in the spec file. This title SHALL be the canonical display title for the spec and SHALL be exposed in list output and APIs (e.g. when listing specs with a detail option). When the document has no level-1 heading or it cannot be parsed, the spec id (directory name) SHALL be used as the display title. Required sections (Purpose, Requirements) SHALL remain unchanged.

#### Scenario: Document with H1

- **WHEN** a spec file has a first level-1 heading (e.g. `# List Command Specification`)
- **THEN** that heading text SHALL be used as the spec's display title
- **AND** tooling that exposes spec titles (e.g. list with detail) SHALL show this title

#### Scenario: Document without H1

- **WHEN** a spec file has no level-1 heading (e.g. only `## Purpose`, `## Requirements`)
- **THEN** the spec id (capability directory name) SHALL be used as the display title
- **AND** tooling that exposes spec titles SHALL show the spec id
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## 1. Schema and parser

- [x] 1.1 Add required `title` field to Spec type in `src/core/schemas/spec.schema.ts`
- [x] 1.2 In `markdown-parser.parseSpec()`, extract first level-1 heading and set `title`; fallback to `name` when H1 is missing or unparseable
- [x] 1.3 Add `title` to any fixtures or mocks that construct a Spec (grep for SpecSchema.safeParse / parseSpec / Spec construction)

## 2. List command and --detail

- [x] 2.1 Add `--detail` option to list command (CLI flag and options type passed to list)
- [x] 2.2 In `src/core/list.ts` specs-mode JSON: when `options.json` and `options.detail` are true, include `title` and `overview` in each spec entry; when `--detail` is omitted, keep output shape unchanged (id, requirementCount only)

## 3. Deprecated spec command

- [x] 3.1 In `openspec spec show`: use `parsed.title` for the output title field instead of `parsed.name`
- [x] 3.2 In `openspec spec list` (and --long/--json): use `spec.title` for the displayed/list title instead of `spec.name`

## 4. Tests and verification

- [x] 4.1 Add or update parser tests: document with H1 → title set from heading; document without H1 → title equals name
- [x] 4.2 Add or update list tests: `openspec list --specs --json` output shape unchanged (id, requirementCount only); `openspec list --specs --json --detail` includes title and overview per spec
- [x] 4.3 Update or add tests for deprecated spec list/show to assert title comes from parsed title
- [x] 4.4 Run full test suite; manually run `openspec list --specs --json`, `openspec list --specs --json --detail`, `openspec spec show <id> --json`, and `openspec spec list --json` to confirm shapes and title/overview where applicable
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-12
Loading