Skip to content

feat: list cmd support json for specs and archive#700

Open
RandyZ wants to merge 5 commits intoFission-AI:mainfrom
RandyZ:feature/list-cmd-support-json-for-specs-and-archive
Open

feat: list cmd support json for specs and archive#700
RandyZ wants to merge 5 commits intoFission-AI:mainfrom
RandyZ:feature/list-cmd-support-json-for-specs-and-archive

Conversation

@RandyZ
Copy link

@RandyZ RandyZ commented Feb 12, 2026

Suggested English PR description:


Add JSON output for list --specs and list --archive for archived changes

Summary

  • openspec list --specs --json now returns JSON: { "specs": [ { "id", "requirementCount" } ] } instead of the human-readable table. Empty result is { "specs": [] }.
  • openspec list --archive lists archived changes (directories under openspec/changes/archive/). It supports --sort (recent | name) and --json; with --json it outputs { "archivedChanges": [...] } with the same shape as active changes (name, completedTasks, totalTasks, lastModified, status).
  • Mode precedence: If more than one of --changes, --specs, or --archive is passed, the effective mode is: --archive over --specs over default (changes).

Motivation

  • Scripts and tooling could not consume specs listing because list --specs --json was ignored and only the table was printed.
  • There was no CLI way to list archived changes; users had to inspect openspec/changes/archive/ manually.

Changes

Area Changes
CLI New --archive option on openspec list; mode resolution with precedence (archive > specs > changes).
Core ListCommand in src/core/list.ts: specs branch outputs JSON when options.json is true; new archive mode scans openspec/changes/archive/, reuses task progress and last-modified, supports --sort and --json.
Spec openspec/specs/cli-list/spec.md updated with JSON output for specs, archive mode, precedence, and empty-archive behavior.
Tests New tests in test/core/list.test.ts for list --specs --json (shape, empty), list --archive / list --archive --json (shape, empty state), and CLI precedence (--specs --archive → archive list).
Docs docs/cli.md: list section updated with --archive, mode/JSON note, and examples for --archive, --specs --json, and --archive --json.

Testing

  • pnpm run build && pnpm exec vitest run test/core/list.test.ts — all 17 list tests pass.
  • Manual: openspec list --specs --json, openspec list --archive --json, and openspec list --archive produce the expected output.

Summary by CodeRabbit

  • New Features

    • Added --archive to list archived changes (table + --json) and --detail for --specs --json to include spec title and overview
    • Improved --specs --json output shape and mode precedence: archive > specs > changes
    • Spec display titles now come from the spec's first H1 (fallback to id)
  • Documentation

    • CLI docs updated to cover archive mode, JSON outputs, detail flag, sorting, and precedence
  • Tests

    • Added coverage for specs JSON, detail, archive mode, empty states, and precedence
  • Chores

    • Updated ignore patterns (.cursor/ and result)

ignore cursor
Enable JSON output for specs and add an archive listing mode to `openspec list`. Updated CLI to accept `--archive` and compute mode precedence (archive > specs > changes). ListCommand (src/core/list.ts) now supports mode `'archive'`, emits `{ "specs": [...] }` for `--specs --json` and `{ "archivedChanges": [...] }` for `--archive --json` (archived items reuse change fields: name, completedTasks, totalTasks, lastModified, status). Documentation and specs were added/updated (docs/cli.md, openspec/specs/cli-list/spec.md) along with design/proposal/tasks metadata. Tests (test/core/list.test.ts) were extended to cover specs JSON, archive listing, JSON shapes, empty states, and mode precedence.
Move the add-list-specs-json-and-archive change files into a dated archive folder at openspec/changes/archive/2026-02-12-add-list-specs-json-and-archive/. Files moved: .openspec.yaml, design.md, proposal.md, specs/cli-list/spec.md, and tasks.md. This archives the change entry to keep active changes cleaned up.
@RandyZ RandyZ requested a review from TabishB as a code owner February 12, 2026 08:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

This PR adds a new --archive mode and expands the openspec list command to support JSON output for specs (including an optional --detail) and archived changes, introduces mode precedence (archive > specs > changes), updates CLI flags and ListCommand.execute signature, and adds tests and docs for these behaviors.

Changes

Cohort / File(s) Summary
Configuration
\.gitignore
Added .cursor/ and result ignore patterns.
User Documentation
docs/cli.md
Updated list command text to document --archive, --specs --json shapes, precedence rules, and examples for archived/spec JSON outputs.
Change Record & Design
openspec/changes/archive/2026-02-12-add-list-specs-json-and-archive/.openspec.yaml, .../design.md, .../proposal.md, .../tasks.md
Added change record, design, proposal, and tasks describing archive mode, specs JSON shape, precedence rules, path handling, and testing tasks.
Production Spec (CLI)
openspec/changes/.../specs/cli-list/spec.md, openspec/specs/cli-list/spec.md
Expanded spec to define --archive, --detail, JSON shapes (specs, archivedChanges), empty-state behavior, sorting rules, and precedence.
CLI Implementation
src/cli/index.ts
Added --archive and --detail options, extended action signature, and resolved mode with precedence (archive > specs > changes); passes new options to ListCommand.
Core List Logic
src/core/list.ts
Extended ListOptions with detail, expanded execute signature to include 'archive', implemented archive-mode scanning of openspec/changes/archive, JSON output shapes for specs and archivedChanges, sorting/status calculation, and non-JSON messaging.
Spec Title & Validation
src/core/parsers/markdown-parser.ts, src/core/schemas/spec.schema.ts, src/core/validation/constants.ts
Parser now extracts first H1 as title (fallback to name); Spec schema requires title; added validation message SPEC_TITLE_EMPTY.
Spec Command Changes
src/commands/spec.ts
Propagated title into outputs and replaced display uses of name with title for spec presentation in JSON/plaintext.
Tests
test/core/list.test.ts, test/commands/spec.test.ts, test/core/parsers/markdown-parser.test.ts, test/core/validation.test.ts
Added tests for specs JSON (with/without --detail), archive mode (table and JSON), empty archive behaviors, precedence tests, parser/title extraction, and updated schema validation tests.
Openspec Metadata
openspec/changes/archive/2025-02-12-add-spec-title-and-list-json-fields/...
Added related change records, design, proposal, and tasks documenting title extraction and detail-enabled JSON fields.

Sequence Diagram(s)

sequenceDiagram
  participant User as "User (CLI)"
  participant CLI as "src/cli/index.ts"
  participant List as "ListCommand (src/core/list.ts)"
  participant FS as "Filesystem (openspec/changes, openspec/specs)"
  participant Output as "Stdout / JSON"

  User->>CLI: run `openspec list [--specs|--archive] [--json] [--detail]`
  CLI->>List: execute(path='.', mode, {sort, json, detail})
  List->>FS: read openspec/changes or openspec/specs or openspec/changes/archive
  FS-->>List: return dirs/files + tasks.md + timestamps
  List->>List: compute progress, lastModified, status, (spec titles if specs mode)
  List->>Output: render table or emit JSON { specs: [...] } / { archivedChanges: [...] }
  Output-->>User: display results / exit 0
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • TabishB

Poem

🐰 I hopped through folders, sniffed each spec and name,
Found H1 titles, archived histories to tame.
I sorted, counted tasks, and wrapped them up in JSON cheer,
Archive first, specs next — now the listings are clear! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the main changes: adding JSON output support for specs mode and introducing archive mode to the list command.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

This PR adds JSON output support for list --specs and introduces a new --archive flag to list archived changes, with mode precedence (archive > specs > changes).

Major changes:

  • JSON for specs: openspec list --specs --json now outputs { "specs": [{"id", "requirementCount"}] } instead of only table format
  • Archive mode: New --archive flag lists archived changes from openspec/changes/archive/ with task progress and timestamps, supports both text and JSON output
  • Mode precedence: When multiple mode flags provided, --archive overrides --specs, which overrides default changes mode
  • Comprehensive tests: Added 7 new tests covering specs JSON output, archive mode (text/JSON), empty states, and CLI precedence

Critical issue:

  • Indentation bug in src/core/list.ts: Lines 154-272 (specs and archive mode blocks) are incorrectly indented with 2 fewer spaces than required. The if (mode === 'specs') block and archive mode block should be at the same indentation level as the if (mode === 'changes') block above them, not nested within it. This will cause a syntax error and prevent compilation.

Confidence Score: 1/5

  • This PR cannot be merged due to a critical syntax error that will prevent compilation
  • The indentation bug in src/core/list.ts lines 154-272 will cause a syntax error - the specs and archive mode blocks are missing 2 spaces of indentation, making them incorrectly nested. While the feature implementation logic is sound, the tests are comprehensive, and the documentation is thorough, this formatting issue prevents the code from compiling and must be fixed before merge.
  • Pay immediate attention to src/core/list.ts - fix indentation on lines 154-272

Important Files Changed

Filename Overview
src/core/list.ts Added JSON output for specs mode and new archive mode, but critical indentation bug will cause syntax error
src/cli/index.ts Added --archive flag and mode precedence logic - clean implementation
test/core/list.test.ts Added comprehensive tests for specs JSON, archive mode, and precedence - good coverage
openspec/specs/cli-list/spec.md Spec updated with archive mode requirements, JSON outputs, and mode precedence scenarios

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant ListCommand
    participant FileSystem

    User->>CLI: openspec list --archive --json
    CLI->>CLI: Parse flags (archive > specs > changes)
    CLI->>ListCommand: execute(path, 'archive', {json: true, sort: 'recent'})
    
    ListCommand->>FileSystem: access(openspec/changes/archive/)
    alt Directory exists
        FileSystem-->>ListCommand: Success
        ListCommand->>FileSystem: readdir(archive/)
        FileSystem-->>ListCommand: [dir1, dir2, ...]
        
        loop For each directory
            ListCommand->>FileSystem: Read tasks.md
            FileSystem-->>ListCommand: Task content
            ListCommand->>ListCommand: Parse task progress
            ListCommand->>FileSystem: Get last modified time
            FileSystem-->>ListCommand: Timestamp
        end
        
        ListCommand->>ListCommand: Sort by recent/name
        ListCommand->>ListCommand: Format JSON output
        ListCommand->>User: {"archivedChanges": [...]}
    else Directory missing
        FileSystem-->>ListCommand: Error
        ListCommand->>User: {"archivedChanges": []}
    end
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

11 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 154 to 272
@@ -183,12 +191,83 @@ export class ListCommand {
}

specs.sort((a, b) => a.id.localeCompare(b.id));
if (json) {
console.log(JSON.stringify({ specs }, null, 2));
return;
}
console.log('Specs:');
const padding = ' ';
const nameWidth = Math.max(...specs.map(s => s.id.length));
for (const spec of specs) {
const padded = spec.id.padEnd(nameWidth);
console.log(`${padding}${padded} requirements ${spec.requirementCount}`);
}
return;
}

// archive mode
const archiveDir = path.join(targetPath, 'openspec', 'changes', 'archive');
try {
await fs.access(archiveDir);
} catch {
if (json) {
console.log(JSON.stringify({ archivedChanges: [] }, null, 2));
} else {
console.log('No archived changes found.');
}
return;
}

const entries = await fs.readdir(archiveDir, { withFileTypes: true });
const archiveDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
if (archiveDirs.length === 0) {
if (json) {
console.log(JSON.stringify({ archivedChanges: [] }, null, 2));
} else {
console.log('No archived changes found.');
}
return;
}

const changes: ChangeInfo[] = [];
for (const changeDir of archiveDirs) {
const progress = await getTaskProgressForChange(archiveDir, changeDir);
const changePath = path.join(archiveDir, changeDir);
const lastModified = await getLastModified(changePath);
changes.push({
name: changeDir,
completedTasks: progress.completed,
totalTasks: progress.total,
lastModified
});
}

if (sort === 'recent') {
changes.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
} else {
changes.sort((a, b) => a.name.localeCompare(b.name));
}

if (json) {
const jsonOutput = changes.map(c => ({
name: c.name,
completedTasks: c.completedTasks,
totalTasks: c.totalTasks,
lastModified: c.lastModified.toISOString(),
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
}));
console.log(JSON.stringify({ archivedChanges: jsonOutput }, null, 2));
return;
}

console.log('Archived changes:');
const padding = ' ';
const nameWidth = Math.max(...changes.map(c => c.name.length));
for (const change of changes) {
const paddedName = change.name.padEnd(nameWidth);
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
const timeAgo = formatRelativeTime(change.lastModified);
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
}
}
Copy link

Choose a reason for hiding this comment

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

Indentation broken for specs and archive mode blocks - these should be at the same level as the changes mode block, not nested

Suggested change
if (mode === 'specs') {
const specsDir = path.join(targetPath, 'openspec', 'specs');
try {
await fs.access(specsDir);
} catch {
if (json) {
console.log(JSON.stringify({ specs: [] }, null, 2));
} else {
console.log('No specs found.');
}
return;
}
const entries = await fs.readdir(specsDir, { withFileTypes: true });
const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
if (specDirs.length === 0) {
if (json) {
console.log(JSON.stringify({ specs: [] }, null, 2));
} else {
console.log('No specs found.');
}
return;
}
type SpecInfo = { id: string; requirementCount: number };
const specs: SpecInfo[] = [];
for (const id of specDirs) {
const specPath = join(specsDir, id, 'spec.md');
try {
const content = readFileSync(specPath, 'utf-8');
const parser = new MarkdownParser(content);
const spec = parser.parseSpec(id);
specs.push({ id, requirementCount: spec.requirements.length });
} catch {
// If spec cannot be read or parsed, include with 0 count
specs.push({ id, requirementCount: 0 });
}
}
specs.sort((a, b) => a.id.localeCompare(b.id));
if (json) {
console.log(JSON.stringify({ specs }, null, 2));
return;
}
console.log('Specs:');
const padding = ' ';
const nameWidth = Math.max(...specs.map(s => s.id.length));
for (const spec of specs) {
const padded = spec.id.padEnd(nameWidth);
console.log(`${padding}${padded} requirements ${spec.requirementCount}`);
}
return;
}
// archive mode
const archiveDir = path.join(targetPath, 'openspec', 'changes', 'archive');
try {
await fs.access(archiveDir);
} catch {
if (json) {
console.log(JSON.stringify({ archivedChanges: [] }, null, 2));
} else {
console.log('No archived changes found.');
}
return;
}
const entries = await fs.readdir(archiveDir, { withFileTypes: true });
const archiveDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
if (archiveDirs.length === 0) {
if (json) {
console.log(JSON.stringify({ archivedChanges: [] }, null, 2));
} else {
console.log('No archived changes found.');
}
return;
}
const changes: ChangeInfo[] = [];
for (const changeDir of archiveDirs) {
const progress = await getTaskProgressForChange(archiveDir, changeDir);
const changePath = path.join(archiveDir, changeDir);
const lastModified = await getLastModified(changePath);
changes.push({
name: changeDir,
completedTasks: progress.completed,
totalTasks: progress.total,
lastModified
});
}
if (sort === 'recent') {
changes.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
} else {
changes.sort((a, b) => a.name.localeCompare(b.name));
}
if (json) {
const jsonOutput = changes.map(c => ({
name: c.name,
completedTasks: c.completedTasks,
totalTasks: c.totalTasks,
lastModified: c.lastModified.toISOString(),
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
}));
console.log(JSON.stringify({ archivedChanges: jsonOutput }, null, 2));
return;
}
console.log('Archived changes:');
const padding = ' ';
const nameWidth = Math.max(...changes.map(c => c.name.length));
for (const change of changes) {
const paddedName = change.name.padEnd(nameWidth);
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
const timeAgo = formatRelativeTime(change.lastModified);
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
}
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/core/list.ts (3)

1-6: 🛠️ Refactor suggestion | 🟠 Major

Duplicate and mixed imports: path / join and sync / async fs.

path is imported as a default on Line 2, and join is destructured from the same module on Line 5. Similarly, async fs (promises) is imported on Line 1 while sync readFileSync is imported on Line 4. This creates unnecessary duplication and mixes I/O paradigms.

Use path.join consistently (already used everywhere except Line 181) and drop the redundant { join } import. Prefer await fs.readFile over readFileSync to avoid blocking (see related comment on Line 183).

♻️ Proposed import cleanup
 import { promises as fs } from 'fs';
 import path from 'path';
 import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
-import { readFileSync } from 'fs';
-import { join } from 'path';
 import { MarkdownParser } from './parsers/markdown-parser.js';

180-191: ⚠️ Potential issue | 🟠 Major

Synchronous file read inside an async method blocks the event loop.

readFileSync on Line 183 is called inside an async execute() method that otherwise uses async I/O throughout. When iterating over many spec directories, this blocks the thread for each file read. Switch to await fs.readFile and update Line 181 to use path.join for consistency.

🔧 Proposed fix
     for (const id of specDirs) {
-      const specPath = join(specsDir, id, 'spec.md');
+      const specPath = path.join(specsDir, id, 'spec.md');
       try {
-        const content = readFileSync(specPath, 'utf-8');
+        const content = await fs.readFile(specPath, 'utf-8');
         const parser = new MarkdownParser(content);
         const spec = parser.parseSpec(id);
         specs.push({ id, requirementCount: spec.requirements.length });

154-206: 🛠️ Refactor suggestion | 🟠 Major

Inconsistent indentation in specs and archive blocks.

The specs block (Lines 156–205) drops to 4-space indent despite being inside the if (mode === 'specs') block, and the archive block (Lines 210–271) drops to 2-space indent. This makes the code hard to follow and fragile—an editor auto-format could silently shift brace matching.

Please re-indent the body of both blocks to align consistently with the containing if / method scope.

Also applies to: 208-271

🤖 Fix all issues with AI agents
In `@docs/cli.md`:
- Around line 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 ```text) for the
block containing "Changes: add-dark-mode ..." so the snippet is explicitly
marked as text and the linter error is resolved.

In `@src/core/list.ts`:
- Around line 208-230: The archive mode currently checks only archiveDir
(variable archiveDir) and falls back to "No archived changes found" when the
parent openspec/changes directory is missing; update the logic to first validate
the parent changes directory (e.g., a changesDir derived from targetPath +
'openspec/changes') using fs.access and, if that check fails, emit the same
descriptive init prompt used in changes mode instead of the empty-archive
message; only if changesDir exists should you then check archiveDir and keep the
existing behavior of returning an empty archivedChanges response when the
archive subdirectory is absent.
🧹 Nitpick comments (6)
test/core/list.test.ts (1)

285-302: Consider adding a sort test for archive mode.

The precedence test is a great addition. You might also want a test that verifies --sort name works for archive mode (alphabetical ordering of archived changes), similar to the existing sort=name test for changes at line 119. This would confirm the documented sort behavior for archive mode.

openspec/changes/archive/2026-02-12-add-list-specs-json-and-archive/specs/cli-list/spec.md (1)

134-136: Specs mode sorting is implemented and correct, but tests lack explicit ordering verification.

The implementation at line 193 of src/core/list.ts correctly sorts specs alphabetically by id using localeCompare, which satisfies the requirement. However, the test suite does not include an explicit test case that verifies this alphabetical ordering for specs mode. Consider adding a test similar to the one at line 119-135 (which verifies alphabetical ordering for changes mode) to explicitly confirm specs ordering is maintained.

openspec/specs/cli-list/spec.md (2)

22-25: Implementation detail in spec: path.join reference.

Line 24 prescribes a specific Node.js API (path.join) for path resolution. Specs typically describe what (cross-platform path resolution) rather than how (which library call). Consider rewording to "using platform-appropriate path joining" so the spec stays implementation-agnostic.


67-68: Duplicated precedence rule.

The mode precedence rule is stated identically in the "Flags" requirement (Line 68) and again as a standalone "Mode precedence" scenario (Lines 82-84). Having it in one place and referencing it from the other would reduce drift risk if the rule ever changes.

Also applies to: 82-84

src/core/list.ts (2)

128-138: Duplicated status derivation and JSON-mapping logic between changes and archive modes.

Lines 130–136 (changes JSON) and Lines 252–258 (archive JSON) contain identical mapping logic including the same ternary for status. The human-readable display (Lines 142–150 vs 263–271) is also nearly identical. Consider extracting a shared helper (e.g., toJsonOutput(changes) and printChangeTable(label, changes)) to keep these in sync and reduce copy-paste drift.

♻️ Example helper extraction
function toChangeJsonOutput(changes: ChangeInfo[]) {
  return changes.map(c => ({
    name: c.name,
    completedTasks: c.completedTasks,
    totalTasks: c.totalTasks,
    lastModified: c.lastModified.toISOString(),
    status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress',
  }));
}

function printChangeTable(heading: string, changes: ChangeInfo[]) {
  console.log(`${heading}:`);
  const padding = '  ';
  const nameWidth = Math.max(...changes.map(c => c.name.length));
  for (const change of changes) {
    const paddedName = change.name.padEnd(nameWidth);
    const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
    const timeAgo = formatRelativeTime(change.lastModified);
    console.log(`${padding}${paddedName}     ${status.padEnd(12)}  ${timeAgo}`);
  }
}

Also applies to: 251-261


24-51: getLastModified performs unbounded recursive traversal.

This function walks every file in the directory tree to find the latest mtime. For archive entries with many files this could become slow. Not a blocking issue for this PR, but worth noting for future consideration—a shallow scan (e.g., just tasks.md mtime or directory mtime) may suffice.

Comment on lines 197 to 203
**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
```
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.

Comment on lines +208 to +230
// archive mode
const archiveDir = path.join(targetPath, 'openspec', 'changes', 'archive');
try {
await fs.access(archiveDir);
} catch {
if (json) {
console.log(JSON.stringify({ archivedChanges: [] }, null, 2));
} else {
console.log('No archived changes found.');
}
return;
}

const entries = await fs.readdir(archiveDir, { withFileTypes: true });
const archiveDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
if (archiveDirs.length === 0) {
if (json) {
console.log(JSON.stringify({ archivedChanges: [] }, null, 2));
} else {
console.log('No archived changes found.');
}
return;
}
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

Archive mode doesn't validate that the changes directory exists first.

In changes mode (Line 82–89), if openspec/changes/ doesn't exist, a descriptive error is thrown prompting the user to run openspec init. In archive mode, the code jumps straight to checking the archive subdirectory. If openspec/changes/ itself doesn't exist, fs.access(archiveDir) will fail and silently report "No archived changes found" instead of the more helpful init prompt. Consider checking the parent changes directory first, or at minimum documenting why the behavior diverges.

🤖 Prompt for AI Agents
In `@src/core/list.ts` around lines 208 - 230, The archive mode currently checks
only archiveDir (variable archiveDir) and falls back to "No archived changes
found" when the parent openspec/changes directory is missing; update the logic
to first validate the parent changes directory (e.g., a changesDir derived from
targetPath + 'openspec/changes') using fs.access and, if that check fails, emit
the same descriptive init prompt used in changes mode instead of the
empty-archive message; only if changesDir exists should you then check
archiveDir and keep the existing behavior of returning an empty archivedChanges
response when the archive subdirectory is absent.

@lsmonki
Copy link

lsmonki commented Feb 12, 2026

I was actually working on a change to do exactly the same thing, but after seeing this PR I’ll leave it to you.

Would you consider adding a --title and --summary option to openspec list --specs --json, so that it includes the title (from the # H1) and the spec description (from ## Purpose)? I believe there’s already code that extracts the title in the deprecated openspec spec list --long command.

This would make it easier for the LLM to discover and select relevant specs.

@RandyZ
Copy link
Author

RandyZ commented Feb 13, 2026

I was actually working on a change to do exactly the same thing, but after seeing this PR I’ll leave it to you.

Would you consider adding a --title and --summary option to openspec list --specs --json, so that it includes the title (from the # H1) and the spec description (from ## Purpose)? I believe there’s already code that extracts the title in the deprecated openspec spec list --long command.

This would make it easier for the LLM to discover and select relevant specs.

Good idea, I will support this in my PR. The source of the title is not clearly specified in openspec at the moment. The name used during parseSpec is actually the file ID. This is indeed somewhat unreasonable.

@lsmonki
Copy link

lsmonki commented Feb 13, 2026

I was actually working on a change to do exactly the same thing, but after seeing this PR I’ll leave it to you.

Would you consider adding a --title and --summary option to openspec list --specs --json, so that it includes the title (from the # H1) and the spec description (from ## Purpose)? I believe there’s already code that extracts the title in the deprecated openspec spec list --long command.

This would make it easier for the LLM to discover and select relevant specs.

Good idea, I will support this in my PR. The source of the title is not clearly specified in openspec at the moment. The name used during parseSpec is actually the file ID. This is indeed somewhat unreasonable.

It would be great, just one consideration. Specs might use a custom schema that doesn’t include the title in the first “#” (i.e., # Spec Title), so having a fallback to the spec ID in that case would be helpful.

The same applies to the description section — it might not use ## Purpose, but something like ## Overview, ## Description, or even just the paragraphs immediately following the title.

Introduce a required Spec.title field sourced from the document's first level-1 heading (H1) with fallback to the spec id when missing. Update MarkdownParser.parseSpec to extract the H1 and return title, add SPEC_TITLE_EMPTY validation message and enforce title in SpecSchema. Add a new --detail flag to the list CLI and ListCommand so that openspec list --specs --json remains unchanged by default but openspec list --specs --json --detail includes title and overview for each spec. Update deprecated spec command outputs to use parsed title, adjust tests to cover H1/fallback behavior and the new detail JSON, and add design/proposal/specs documentation for the change.
@RandyZ
Copy link
Author

RandyZ commented Feb 17, 2026

I was actually working on a change to do exactly the same thing, but after seeing this PR I’ll leave it to you.
Would you consider adding a --title and --summary option to openspec list --specs --json, so that it includes the title (from the # H1) and the spec description (from ## Purpose)? I believe there’s already code that extracts the title in the deprecated openspec spec list --long command.
This would make it easier for the LLM to discover and select relevant specs.

Good idea, I will support this in my PR. The source of the title is not clearly specified in openspec at the moment. The name used during parseSpec is actually the file ID. This is indeed somewhat unreasonable.

It would be great, just one consideration. Specs might use a custom schema that doesn’t include the title in the first “#” (i.e., # Spec Title), so having a fallback to the spec ID in that case would be helpful.

The same applies to the description section — it might not use ## Purpose, but something like ## Overview, ## Description, or even just the paragraphs immediately following the title.

The new commit is already supported, but I merged two parameters. Using --detail will add the output of two additional pieces of information.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
test/core/validation.test.ts (1)

87-87: Consider adding a test case for empty/missing title validation.

The title field is now required and validated with SPEC_TITLE_EMPTY, but there's no test that asserts a spec with an empty or missing title is rejected by SpecSchema. The existing tests only add a valid title to passing and other-failure cases.

💡 Suggested test case
it('should reject spec with empty title', () => {
  const spec = {
    name: 'user-auth',
    title: '',
    overview: 'This spec defines user authentication requirements',
    requirements: [
      {
        text: 'The system SHALL provide user authentication',
        scenarios: [
          {
            rawText: 'Given a user with valid credentials\nWhen they submit the login form\nThen they are authenticated',
          },
        ],
      },
    ],
  };

  const result = SpecSchema.safeParse(spec);
  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error.issues[0].message).toBe('Spec title cannot be empty');
  }
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/validation.test.ts` at line 87, Add a unit test verifying
SpecSchema rejects empty or missing title: create a spec object with name
'user-auth' and title set to '' (and optionally another test omitting title
entirely), call SpecSchema.safeParse(spec), assert result.success is false, and
verify the validation error message/issue corresponds to SPEC_TITLE_EMPTY (or
message 'Spec title cannot be empty'); reference SpecSchema and SPEC_TITLE_EMPTY
in the assertion to ensure the correct validation path is tested.
test/commands/spec.test.ts (1)

94-104: This test largely duplicates the test at lines 74–92.

Both tests invoke spec show auth --json and assert json.title === 'auth' and json.id === 'auth'. The only difference is the cwd option vs process.chdir pattern. Consider consolidating or removing the duplicate to keep the test suite focused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/commands/spec.test.ts` around lines 94 - 104, The test "should output
title from parsed spec (spec show --json)" duplicates an earlier test that runs
`spec show auth --json` and asserts title/id; remove or consolidate them so only
one authoritative test remains (either keep the earlier test that uses
process.chdir or keep this one that uses execSync with cwd), and if
consolidating merge any unique assertions into the kept test; locate the
duplicate by the test name and the use of execSync, openspecBin and testDir,
then delete the redundant `it(...)` block or combine its assertions into the
other `it(...)` block.
src/core/list.ts (2)

182-196: Mixing sync and async file I/O in an async method.

readFileSync (Line 184) is used inside an async method, while the rest of the method (and the changes/archive modes) consistently uses await fs.readFile(...). For a small number of spec files this won't be noticeable, but it blocks the event loop during each read and is inconsistent with the pattern used elsewhere in this method.

♻️ Suggested change
-        const content = readFileSync(specPath, 'utf-8');
+        const content = await fs.readFile(specPath, 'utf-8');

This also lets you drop the import { readFileSync } from 'fs' and import { join } from 'path' (use the already-imported path.join).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/list.ts` around lines 182 - 196, The code is blocking the event loop
by using readFileSync inside an async method; replace the synchronous read with
an asynchronous read (use await fs.readFile(specPath, 'utf-8')) when building
specPath (still using path.join(specsDir, id, 'spec.md')), then pass the
resulting content into new MarkdownParser(content) and parseSpec(id) as before;
keep the existing try/catch and the fallback specs.push({ id, requirementCount:
0 }), and remove the readFileSync import since it will no longer be used.

238-277: Significant duplication between archive and changes modes.

The archive block (Lines 238–277) is nearly identical to the changes block (Lines 108–151): gather ChangeInfo[] via getTaskProgressForChange + getLastModified, sort, format JSON with the same status derivation, and render the same table layout. The only differences are the directory path, the JSON root key (archivedChanges vs changes), and the header string.

Consider extracting a shared helper, e.g.:

async function gatherChanges(dir: string, dirNames: string[]): Promise<ChangeInfo[]> { ... }
function formatChangesJson(changes: ChangeInfo[], key: string): string { ... }
function printChangesTable(changes: ChangeInfo[], header: string): void { ... }

This would eliminate ~40 duplicated lines and make future changes (e.g., adding fields) apply in one place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/list.ts` around lines 238 - 277, Extract the duplicated logic into
three helpers and replace both the archive and changes blocks with calls to
them: implement async function gatherChanges(dir: string, dirNames: string[]):
Promise<ChangeInfo[]> which iterates dirNames, calls getTaskProgressForChange
and getLastModified and returns the ChangeInfo[]; implement function
formatChangesJson(changes: ChangeInfo[], rootKey: string): string which maps
status exactly as current code does and returns JSON.stringify({ [rootKey]:
mapped }, null, 2); and implement function printChangesTable(changes:
ChangeInfo[], header: string): void which sorts (by recent or name based on an
argument you pass), computes nameWidth, formats status via formatTaskStatus and
prints the header and rows. Replace the existing archive-specific block and the
changes-specific block to call gatherChanges(...) then either
console.log(formatChangesJson(changes, 'archivedChanges'|'changes')) or
printChangesTable(changes, 'Archived changes:'|'Changes:') as appropriate so
only the directory path, JSON root key, and header differ.
test/core/list.test.ts (1)

309-326: Consider adding a symmetric precedence test for specs > changes.

The test validates archive > specs precedence via CLI. For completeness, consider adding a test that verifies specs > changes when both --specs and --changes flags are passed, to fully exercise the documented precedence chain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/list.test.ts` around lines 309 - 326, Add a symmetric CLI test that
verifies specs take precedence over changes when both flags are passed: create a
new it(...) in the same test suite (mirroring the existing "should apply mode
precedence so archive wins over specs when both flags passed (CLI)" test) that
sets up a specs folder with a spec (e.g., under
tempDir/openspec/specs/some-spec/spec.md) and a changes archive folder (e.g.,
tempDir/openspec/changes/archive/some-change), then call
runCLI(['list','--specs','--changes','--json'], { cwd: tempDir }) and assert
exitCode === 0 and that the parsed JSON contains the specs result (e.g., check
parsed.specs or that the spec name exists) rather than changes; use the same
helpers/variables (runCLI, tempDir) and naming convention as the existing test
to keep consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@openspec/changes/archive/2025-02-12-add-spec-title-and-list-json-fields/specs/openspec-conventions/spec.md`:
- Around line 1-17: Update the archive directory name used in this change from
"2025-02-12-add-spec-title-and-list-json-fields" to
"2026-02-12-add-spec-title-and-list-json-fields" so it matches the PR creation
year; rename the directory and adjust any references (e.g., links or index
entries) that mention the old directory name to the new one to preserve
consistency with the YYYY-MM-DD-[name] convention.

In `@openspec/specs/cli-list/spec.md`:
- Around line 54-64: The new list path produces JSON without title unless
--detail, while the deprecated spec list always includes title; to avoid
surprising migrations either document the difference or make the new command
match the old behavior — to align behavior, update the JSON construction in
src/core/list.ts (the block that builds the specs array for openspec list
--specs --json) to include the same display title extraction used by
src/commands/spec.ts (reuse its title/overview extraction helper) so each spec
object always contains id, requirementCount and title (and include overview only
under --detail if you want to keep that difference); alternately add a clear
note in the CLI help/docs explaining the intentional difference between openspec
spec list --json and openspec list --specs --json.

In `@src/core/list.ts`:
- Around line 155-212: The block guarded by if (mode === 'specs') has its inner
statements (the try/catch around specsDir access, dir reading, loop over
specDirs, sorting and printing) incorrectly unindented; re-indent all code
inside the if (mode === 'specs') { ... } so the try/catch, const entries = await
fs.readdir(...), the for (const id of specDirs) loop that reads spec.md, and the
final return are visually nested under that if and not at top level—apply the
same re-indentation fix to the archive block so its contents are nested under
its mode check (look for the archive-mode conditional and re-indent its
try/catch and processing code accordingly).

---

Duplicate comments:
In `@src/core/list.ts`:
- Around line 214-225: The archive mode only checks archiveDir (archiveDir =
path.join(targetPath, 'openspec', 'changes', 'archive')) and falls back to "No
archived changes found." instead of validating the parent changes directory;
update the logic to first check changesDir = path.join(targetPath, 'openspec',
'changes') and if that parent is missing, emit the same init prompt/response
used by changes mode (the behavior at the changes-mode initialization) rather
than the generic "No archived changes found." message; if changesDir exists then
continue to check archiveDir as currently implemented.

---

Nitpick comments:
In `@src/core/list.ts`:
- Around line 182-196: The code is blocking the event loop by using readFileSync
inside an async method; replace the synchronous read with an asynchronous read
(use await fs.readFile(specPath, 'utf-8')) when building specPath (still using
path.join(specsDir, id, 'spec.md')), then pass the resulting content into new
MarkdownParser(content) and parseSpec(id) as before; keep the existing try/catch
and the fallback specs.push({ id, requirementCount: 0 }), and remove the
readFileSync import since it will no longer be used.
- Around line 238-277: Extract the duplicated logic into three helpers and
replace both the archive and changes blocks with calls to them: implement async
function gatherChanges(dir: string, dirNames: string[]): Promise<ChangeInfo[]>
which iterates dirNames, calls getTaskProgressForChange and getLastModified and
returns the ChangeInfo[]; implement function formatChangesJson(changes:
ChangeInfo[], rootKey: string): string which maps status exactly as current code
does and returns JSON.stringify({ [rootKey]: mapped }, null, 2); and implement
function printChangesTable(changes: ChangeInfo[], header: string): void which
sorts (by recent or name based on an argument you pass), computes nameWidth,
formats status via formatTaskStatus and prints the header and rows. Replace the
existing archive-specific block and the changes-specific block to call
gatherChanges(...) then either console.log(formatChangesJson(changes,
'archivedChanges'|'changes')) or printChangesTable(changes, 'Archived
changes:'|'Changes:') as appropriate so only the directory path, JSON root key,
and header differ.

In `@test/commands/spec.test.ts`:
- Around line 94-104: The test "should output title from parsed spec (spec show
--json)" duplicates an earlier test that runs `spec show auth --json` and
asserts title/id; remove or consolidate them so only one authoritative test
remains (either keep the earlier test that uses process.chdir or keep this one
that uses execSync with cwd), and if consolidating merge any unique assertions
into the kept test; locate the duplicate by the test name and the use of
execSync, openspecBin and testDir, then delete the redundant `it(...)` block or
combine its assertions into the other `it(...)` block.

In `@test/core/list.test.ts`:
- Around line 309-326: Add a symmetric CLI test that verifies specs take
precedence over changes when both flags are passed: create a new it(...) in the
same test suite (mirroring the existing "should apply mode precedence so archive
wins over specs when both flags passed (CLI)" test) that sets up a specs folder
with a spec (e.g., under tempDir/openspec/specs/some-spec/spec.md) and a changes
archive folder (e.g., tempDir/openspec/changes/archive/some-change), then call
runCLI(['list','--specs','--changes','--json'], { cwd: tempDir }) and assert
exitCode === 0 and that the parsed JSON contains the specs result (e.g., check
parsed.specs or that the spec name exists) rather than changes; use the same
helpers/variables (runCLI, tempDir) and naming convention as the existing test
to keep consistency.

In `@test/core/validation.test.ts`:
- Line 87: Add a unit test verifying SpecSchema rejects empty or missing title:
create a spec object with name 'user-auth' and title set to '' (and optionally
another test omitting title entirely), call SpecSchema.safeParse(spec), assert
result.success is false, and verify the validation error message/issue
corresponds to SPEC_TITLE_EMPTY (or message 'Spec title cannot be empty');
reference SpecSchema and SPEC_TITLE_EMPTY in the assertion to ensure the correct
validation path is tested.

Comment on lines +54 to +64
#### 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
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

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the test file to see what the deprecated spec list --json outputs
find . -name "spec.test.ts" -type f 2>/dev/null | head -5

Repository: Fission-AI/OpenSpec

Length of output: 91


🏁 Script executed:

# Check the implementation files
fd -t f "list\.(ts|js)" src/ 2>/dev/null | head -10

Repository: Fission-AI/OpenSpec

Length of output: 79


🏁 Script executed:

# Also look for the spec command implementation
fd -t f "spec\.(ts|js)" src/commands/ 2>/dev/null

Repository: Fission-AI/OpenSpec

Length of output: 83


🏁 Script executed:

# Let's examine the test file around lines 203-205
cat -n test/commands/spec.test.ts | sed -n '195,215p'

Repository: Fission-AI/OpenSpec

Length of output: 968


🏁 Script executed:

# Now let's check the list.ts implementation to see how it constructs the output
cat -n src/core/list.ts | head -150

Repository: Fission-AI/OpenSpec

Length of output: 5984


🏁 Script executed:

# Also check spec.ts for the deprecated command handling
cat -n src/commands/spec.ts | head -100

Repository: Fission-AI/OpenSpec

Length of output: 4252


🏁 Script executed:

# Let's see the specs mode implementation in list.ts
cat -n src/core/list.ts | sed -n '150,300p'

Repository: Fission-AI/OpenSpec

Length of output: 5109


🏁 Script executed:

# Let's also check if there's a deprecated spec list handler in spec.ts
cat -n src/commands/spec.ts | sed -n '100,200p'

Repository: Fission-AI/OpenSpec

Length of output: 4385


🏁 Script executed:

# Search for where the deprecated spec list --json is actually implemented
rg -n "spec list" src/commands/ -A 5 -B 5

Repository: Fission-AI/OpenSpec

Length of output: 45


Clarify the intentional output difference between deprecated spec list --json and new openspec list --specs --json commands.

The deprecated openspec spec list --json command (in src/commands/spec.ts lines 162) always includes title in the output. The new openspec list --specs --json command (in src/core/list.ts lines 188-191) includes title only when --detail is passed. This is an intentional design difference between the two command paths, but it may confuse users migrating from the deprecated to the new command. Consider documenting this distinction explicitly or aligning the behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/specs/cli-list/spec.md` around lines 54 - 64, The new list path
produces JSON without title unless --detail, while the deprecated spec list
always includes title; to avoid surprising migrations either document the
difference or make the new command match the old behavior — to align behavior,
update the JSON construction in src/core/list.ts (the block that builds the
specs array for openspec list --specs --json) to include the same display title
extraction used by src/commands/spec.ts (reuse its title/overview extraction
helper) so each spec object always contains id, requirementCount and title (and
include overview only under --detail if you want to keep that difference);
alternately add a clear note in the CLI help/docs explaining the intentional
difference between openspec spec list --json and openspec list --specs --json.

Comment on lines +155 to +212
if (mode === 'specs') {
const specsDir = path.join(targetPath, 'openspec', 'specs');
try {
await fs.access(specsDir);
} catch {
console.log('No specs found.');
if (json) {
console.log(JSON.stringify({ specs: [] }, null, 2));
} else {
console.log('No specs found.');
}
return;
}

const entries = await fs.readdir(specsDir, { withFileTypes: true });
const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
if (specDirs.length === 0) {
console.log('No specs found.');
if (json) {
console.log(JSON.stringify({ specs: [] }, null, 2));
} else {
console.log('No specs found.');
}
return;
}

type SpecInfo = { id: string; requirementCount: number };
type SpecInfo = { id: string; requirementCount: number; title?: string; overview?: string };
const specs: SpecInfo[] = [];
for (const id of specDirs) {
const specPath = join(specsDir, id, 'spec.md');
try {
const content = readFileSync(specPath, 'utf-8');
const parser = new MarkdownParser(content);
const spec = parser.parseSpec(id);
specs.push({ id, requirementCount: spec.requirements.length });
const entry: SpecInfo = { id, requirementCount: spec.requirements.length };
if (json && detail) {
entry.title = spec.title;
entry.overview = spec.overview;
}
specs.push(entry);
} catch {
// If spec cannot be read or parsed, include with 0 count
specs.push({ id, requirementCount: 0 });
}
}

specs.sort((a, b) => a.id.localeCompare(b.id));
if (json) {
console.log(JSON.stringify({ specs }, null, 2));
return;
}
console.log('Specs:');
const padding = ' ';
const nameWidth = Math.max(...specs.map(s => s.id.length));
for (const spec of specs) {
const padded = spec.id.padEnd(nameWidth);
console.log(`${padding}${padded} requirements ${spec.requirementCount}`);
}
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Inconsistent indentation inside the specs mode block.

The try/catch at Line 157 and the code through Line 211 are indented at the wrong level — they sit at the method-body level rather than inside the if (mode === 'specs') block. The closing } at Line 212 and the return at Line 211 restore control flow correctness, but the visual misalignment makes it hard to follow the nesting and will trip up future maintainers.

The same issue affects the archive block (Lines 216–277).

♻️ Suggested fix: re-indent specs and archive blocks

Indent all code within if (mode === 'specs') { ... } and the archive block by one additional level (2 or 4 spaces, matching project convention) so nesting is visually consistent with the changes block above.

     if (mode === 'specs') {
       const specsDir = path.join(targetPath, 'openspec', 'specs');
-    try {
-      await fs.access(specsDir);
-    } catch {
-      if (json) {
-        console.log(JSON.stringify({ specs: [] }, null, 2));
-      } else {
-        console.log('No specs found.');
-      }
-      return;
-    }
+      try {
+        await fs.access(specsDir);
+      } catch {
+        if (json) {
+          console.log(JSON.stringify({ specs: [] }, null, 2));
+        } else {
+          console.log('No specs found.');
+        }
+        return;
+      }
       // ... (apply same re-indentation to the rest of the specs block)
     }

Apply the same correction to the archive block (Lines 216–277).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/list.ts` around lines 155 - 212, The block guarded by if (mode ===
'specs') has its inner statements (the try/catch around specsDir access, dir
reading, loop over specDirs, sorting and printing) incorrectly unindented;
re-indent all code inside the if (mode === 'specs') { ... } so the try/catch,
const entries = await fs.readdir(...), the for (const id of specDirs) loop that
reads spec.md, and the final return are visually nested under that if and not at
top level—apply the same re-indentation fix to the archive block so its contents
are nested under its mode check (look for the archive-mode conditional and
re-indent its try/catch and processing code accordingly).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments