-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: add --spaceline output format to list command #471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Add compact, emoji-rich horizontal format for displaying changes with progress bars, task status, and Git statistics. Usage: openspec list --spaceline Example output: π add-feature | σ°·« ββββββββββ 80% σ°·« Implementation (4/5) | β 2 | π 1 open | σ°¬ main (β3 β0) Co-Authored-By: Claude <[email protected]>
π WalkthroughWalkthroughThis PR introduces a new Spaceline output format for the openspec list command. It adds documentation (design, proposal, and specifications), implements Git statistics utilities, creates a spaceline formatter module, and integrates both into the list command and CLI with a new --spaceline flag that is mutually exclusive with --json. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant CLI
participant ListCommand
participant FileSystem
participant GitStats
participant SpacelineFormatter
User->>CLI: openspec list --spaceline
CLI->>ListCommand: execute(targetPath, 'changes', {spaceline: true})
ListCommand->>FileSystem: Read changes directory
FileSystem-->>ListCommand: Change entries list
loop For each change
ListCommand->>FileSystem: Read proposal.md for title
FileSystem-->>ListCommand: Proposal content
ListCommand->>ListCommand: extractTitle(content, changeName)
ListCommand->>FileSystem: Count deltas via ChangeParser
FileSystem-->>ListCommand: Delta count
ListCommand->>GitStats: getGitDiffStatsForPath(changePath)
GitStats-->>ListCommand: {added, removed, branch}
end
ListCommand->>SpacelineFormatter: formatSpaceline(change, stats)
SpacelineFormatter-->>ListCommand: [line1, line2] with emojis & progress
ListCommand->>User: Print formatted spaceline output
Estimated code review effortπ― 4 (Complex) | β±οΈ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
π₯ Pre-merge checks | β 3β Passed checks (3 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing touches
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. Comment |
Review CompleteYour review story is ready! Comment !reviewfast on this PR to re-generate the story. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and canβt be posted inline due to platform limitations.
β οΈ Outside diff range comments (2)
src/cli/index.ts (1)
95-119: Decide how to handle--specs --spaceline(currently silently ignored).If spaceline is changes-only, consider treating
--specs --spacelineas misuse (exit code 2) rather than silently falling back to specs output.Proposed CLI guard
.action(async (options?: { specs?: boolean; changes?: boolean; sort?: string; json?: boolean; spaceline?: boolean }) => { try { // Check for mutually exclusive flags if (options?.spaceline && options?.json) { console.error('Error: Cannot use --spaceline with --json'); process.exit(2); } + if (options?.spaceline && options?.specs) { + console.error('Error: Cannot use --spaceline with --specs'); + process.exit(2); + } const listCommand = new ListCommand(); const mode: 'changes' | 'specs' = options?.specs ? 'specs' : 'changes'; const sort = options?.sort === 'name' ? 'name' : 'recent'; await listCommand.execute('.', mode, { sort, json: options?.json, spaceline: options?.spaceline }); } catch (error) {openspec/specs/cli-list/spec.md (1)
49-65: Spec drift: spaceline example formatting is incorrect, and--longflag reference doesn't match the CLI.
Spaceline format issues:
- Line 1 is missing the status icon between the change ID and progress bar. Should be:
π {change-id} | σ°·« {progress-bar} {percentage}%- Git stats notation is reversed. Should be
β{added} β{removed}(not{added}β {removed}β)- Add language identifier
textto the fenced code block (lines 117β122)
--longflag reference (line 159): The spec requirement "Spaceline Flag Compatibility" mentions that spaceline should "take precedence over--long", butopenspec listdoes not support a--longflag (only the deprecatedopenspec change listhas it). Remove the reference to--longor clarify this applies only to the deprecated command.
π€ Fix all issues with AI agents
In
@openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.md:
- Around line 15-33: The spec example block is inconsistent with the implemented
spaceline: move {status-icon} to Line 1 before {progress-bar} and match git stat
ordering to (β{added} β{removed}); also add a language tag to the fenced block
(use "text") and adjust the second line to mirror the formatter: "π {change-id}
| {status-icon} {progress-bar} {percentage}%" and "{status-icon} {category}
({completed}/{total}) | {delta-icon} {count} | {open-icon} {open} open |
{branch-icon} {branch} (β{added} β{removed})", updating the WHERE list entries
for β{added} and β{removed} accordingly.
In @openspec/project.md:
- Around line 19-46: The fenced code block under the "## Project Structure"
section is missing a language tag (MD040); update the opening fence from ``` to
```text so the tree block is explicitly tagged (i.e., replace the triple
backticks before the src/ tree with ```text) to satisfy the markdownlint rule.
In @src/core/list.ts:
- Around line 153-156: The ternary for building sorted in src/core/list.ts
currently sorts by changeData.id in both branches; update the non-'name' branch
(the "recent" case) so it sorts by lastModified instead β change the comparator
used in changesData.sort for the else branch to compare
b.changeData.lastModified and a.changeData.lastModified (or parse them as Dates
if they are strings) to produce descending (most recent first) order; keep the
'name' branch using changeData.id.localeCompare and only change the comparator
for the recent sort.
In @src/utils/git-stats.ts:
- Around line 123-158: The getGitDiffStatsForPath function interpolates
relativePath into a shell command causing possible command injection and
failures with spaces/special chars; replace the execSync interpolation with a
safe child_process spawnSync call (or spawn) that passes arguments as an array
(e.g., ['diff','--numstat', base, '--', relativePath]) and read stdout with the
same encoding, updating the function to import spawnSync from 'child_process'
and handle errors the same way so parsing of output, added/removed calculation,
and returned shape remain unchanged.
In @src/utils/spaceline-formatter.ts:
- Around line 6-21: Update the README Prerequisites to note that certain glyphs
used in the UI require a Nerd Font (e.g., FiraCode Nerd Font or Hack Nerd Font)
so the characters for STATUS_IMPLEMENTATION and BRANCH in EMOJI render
correctly; add a brief line in the Prerequisites section naming Nerd Font as a
requirement and suggest a couple of example fonts and how to verify
installation.
In @test/utils/git-stats.test.ts:
- Around line 43-65: The test assumes the repo branch is 'main' which can be
'master' by default; update the test in git-stats.test.ts (the it block that
calls getGitDiffStats) to either initialize the repo with an explicit branch
(use execSync('git init -b main', ...) when setting up the temp repo) or make
the assertion accept both names (e.g., assert stats?.branch is 'main' or
'master' or match /^(main|master)$/) so the test passes regardless of Git's
default branch.
π§Ή Nitpick comments (6)
src/utils/git-stats.ts (1)
79-115: Document the edge case for equal additions and deletions.When a file has equal additions and deletions (
additions === deletions), it's not counted in either theaddedorremovedcategory. While this may be intentional for neutral changes, it should be documented in the function's JSDoc comment to clarify the classification logic.π Suggested documentation enhancement
/** * Get Git diff statistics for the current repository. * * Uses `git diff --numstat` to count file additions and removals. * Files with net additions (additions > deletions) count as "added". * Files with net removals (deletions > additions) count as "removed". + * Files with equal additions and deletions are not counted in either category. * * @returns GitStats with added/removed counts, or null if Git is unavailableopenspec/changes/archive/2026-01-10-add-list-spaceline/design.md (1)
19-19: Add language identifier to fenced code block.The code block at line 19 should specify a language for proper syntax highlighting and linting compliance.
π Proposed fix
-``` +```text 123 45 path/to/file.ts # 123 additions, 45 deletions β counts as "added" (β) 10 50 another/file.ts # 10 additions, 50 deletions β counts as "removed" (β)</details> </blockquote></details> <details> <summary>test/utils/spaceline-formatter.test.ts (1)</summary><blockquote> `23-49`: **Consider defining behavior for non-positive / non-integer `width`.** Right now tests only cover a positive integer width; adding a couple of cases (e.g., `width=0`, `width<0`, `width=3.5`) would lock in expected behavior and catch accidental RangeErrors if implementation changes. </blockquote></details> <details> <summary>src/utils/spaceline-formatter.ts (1)</summary><blockquote> `131-141`: **Consider consistent formatting for missing Git stats.** When Git stats are unavailable, line 140 shows `'| (?)'`, which omits the branch emoji present when stats are available (line 138: `| ${EMOJI.BRANCH} ${branch} (...)`). This inconsistency could make the output harder to parse visually or programmatically. Consider using a consistent format: <details> <summary>β»οΈ Suggested consistent fallback</summary> ```diff } else { - gitStats = '| (?)'; + gitStats = `| ${EMOJI.BRANCH} (no git data)`; }src/core/list.ts (2)
146-146: Clarify the openItems calculation.The calculation
progress.total - progress.completedrepresents incomplete tasks, which may not accurately reflect "open items (TODOs, PRs, etc.)" as described in theSpacelineStatsinterface documentation (line 45 in spaceline-formatter.ts).Consider either:
- Updating the interface documentation to clarify this is incomplete tasks, or
- Implementing a more comprehensive open items count if the current approach doesn't meet requirements
125-133: Consider logging parse errors for debugging.The try-catch block silently ignores all errors when reading or parsing the proposal. While using fallback values is appropriate, completely silent failures could make debugging difficult if proposal files are malformed.
Consider adding a debug log or comment explaining why errors are intentionally ignored.
β»οΈ Optional: Add minimal error context
} catch { - // Unable to read proposal or parse deltas + // Unable to read proposal or parse deltas - using defaults (title=changeDir, deltaCount=0) }
π Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
π Files selected for processing (12)
openspec/changes/archive/2026-01-10-add-list-spaceline/design.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/tasks.mdopenspec/project.mdopenspec/specs/cli-list/spec.mdsrc/cli/index.tssrc/core/list.tssrc/utils/git-stats.tssrc/utils/spaceline-formatter.tstest/utils/git-stats.test.tstest/utils/spaceline-formatter.test.ts
π§° Additional context used
π Path-based instructions (3)
openspec/changes/**/*.md
π CodeRabbit inference engine (openspec/AGENTS.md)
Scaffold proposal using
proposal.md,tasks.md, optionaldesign.md, and delta specs underopenspec/changes/<id>/
Files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/design.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/tasks.md
openspec/changes/**/specs/**/spec.md
π CodeRabbit inference engine (openspec/AGENTS.md)
openspec/changes/**/specs/**/spec.md: Use## ADDED|MODIFIED|REMOVED|RENAMED Requirementsheaders in spec delta files
Include at least one#### Scenario:per requirement in spec delta files
Use#### Scenario: Nameformat (4 hashtags) for scenario headers, not bullets or bold text
Use## ADDED Requirementsfor new orthogonal capabilities that can stand alone; use## MODIFIED Requirementsfor behavior changes of existing requirements
When using MODIFIED Requirements, paste the full requirement block including header and all scenarios
Files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.md
openspec/specs/**/spec.md
π CodeRabbit inference engine (openspec/AGENTS.md)
Use SHALL/MUST for normative requirements in spec files; avoid should/may unless intentionally non-normative
Files:
openspec/specs/cli-list/spec.md
π§ Learnings (14)
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/**/specs/**/spec.md : Use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements` headers in spec delta files
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/design.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/*/proposal.md : Ensure `proposal.md` includes sections: Why (1-2 sentences), What Changes (bullet list with breaking change markers), and Impact (affected specs and code)
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/design.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/tasks.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/**/specs/**/spec.md : Use `## ADDED Requirements` for new orthogonal capabilities that can stand alone; use `## MODIFIED Requirements` for behavior changes of existing requirements
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/**/*.md : Scaffold proposal using `proposal.md`, `tasks.md`, optional `design.md`, and delta specs under `openspec/changes/<id>/`
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/design.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/project.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/**/specs/**/spec.md : Include at least one `#### Scenario:` per requirement in spec delta files
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:02.839Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:02.839Z
Learning: Use `@/openspec/AGENTS.md` to learn how to create and apply change proposals, spec format and conventions, and project structure and guidelines
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/project.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Search existing work using `openspec spec list --long`, `openspec list` for enumerating changes; use `rg` only for full-text search
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/**/specs/**/spec.md : When using MODIFIED Requirements, paste the full requirement block including header and all scenarios
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Check `openspec/project.md` for project conventions before creating specs
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/project.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/**/specs/**/spec.md : Use `#### Scenario: Name` format (4 hashtags) for scenario headers, not bullets or bold text
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/changes/*/tasks.md : Ensure `tasks.md` contains implementation checklist with numbered sections and checkbox items
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/proposal.mdopenspec/changes/archive/2026-01-10-add-list-spaceline/tasks.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Applies to openspec/specs/**/spec.md : Use SHALL/MUST for normative requirements in spec files; avoid should/may unless intentionally non-normative
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.mdopenspec/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Run `openspec validate [change-id] --strict` before requesting approval
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.md
π Learning: 2025-11-25T01:08:19.004Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-11-25T01:08:19.004Z
Learning: Create `design.md` only when needed: cross-cutting changes, new external dependencies, significant data model changes, security/performance complexity, or pre-coding ambiguity
Applied to files:
openspec/changes/archive/2026-01-10-add-list-spaceline/design.md
𧬠Code graph analysis (4)
src/cli/index.ts (1)
src/core/list.ts (1)
ListCommand(81-289)
src/utils/spaceline-formatter.ts (1)
src/utils/git-stats.ts (1)
GitStatsResult(19-19)
test/utils/spaceline-formatter.test.ts (1)
src/utils/spaceline-formatter.ts (5)
EMOJI(6-21)generateProgressBar(81-87)ChangeData(26-37)SpacelineStats(42-47)formatSpaceline(115-146)
test/utils/git-stats.test.ts (1)
src/utils/git-stats.ts (2)
getGitDiffStats(79-115)getGitDiffStatsForPath(123-158)
πͺ markdownlint-cli2 (0.18.1)
openspec/changes/archive/2026-01-10-add-list-spaceline/design.md
19-19: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
openspec/project.md
20-20: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
openspec/specs/cli-list/spec.md
117-117: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
π Additional comments (15)
openspec/changes/archive/2026-01-10-add-list-spaceline/proposal.md (1)
1-33: LGTM! Documentation structure follows guidelines.The proposal document correctly includes all required sections (Why, What Changes, and Impact) with appropriate detail. The structure aligns with the expected format for change proposals.
Based on learnings, the proposal structure is correct.
openspec/changes/archive/2026-01-10-add-list-spaceline/tasks.md (1)
1-47: LGTM! Task checklist follows guidelines.The tasks document correctly uses numbered sections with checkbox items, covering Implementation, Testing, and Documentation. The deferred item (3.3) is clearly marked with rationale.
Based on learnings, the task structure is correct.
src/utils/git-stats.ts (1)
1-41: LGTM! Helper functions are well-structured.The type definitions, constants, and helper functions (
getCurrentBranch,getDiffBase) are cleanly implemented with appropriate error handling.test/utils/git-stats.test.ts (1)
27-41: LGTM! Core test structure is solid.The test setup, teardown, and structure validation tests are well-implemented with appropriate error handling and assertions.
Also applies to: 67-74, 102-113
test/utils/spaceline-formatter.test.ts (1)
1-178: Solid end-to-end coverage for spaceline formatting.The suite hits constants, progress-bar rounding/clamping, category mapping, and git-stats present/absent paths without overfitting to full-line string equality.
src/cli/index.ts (1)
95-119: Nice:--spacelineis wired cleanly and--jsonconflict exits with code 2.This matches the documented βmisuseβ exit code and prevents ambiguous output formats.
openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.md (1)
75-92: No action required. The delta spec already includes the complete "Requirement: Flags" block with all three scenarios (Selecting specs, Selecting changes, and Spaceline output format) in their entirety. The MODIFIED Requirements section is correctly formatted and fully complies with the guideline to paste the complete requirement block including all scenarios.Likely an incorrect or invalid review comment.
src/utils/spaceline-formatter.ts (3)
23-47: LGTM!The type definitions are well-structured and properly documented with clear JSDoc comments.
49-65: LGTM!The category mapping logic handles edge cases appropriately. The
split('-')[0]approach gracefully handles change IDs without hyphens, and the default fallback to 'Implementation' is reasonable.
67-87: LGTM!The progress bar generation logic correctly clamps the percentage, handles edge cases (0%, 100%), and includes helpful examples in the JSDoc.
src/core/list.ts (5)
7-9: LGTM!The new imports are necessary for the spaceline functionality and are properly utilized in the
executeSpacelinemethod.
21-21: LGTM!The
spacelineoption follows the established pattern for optional boolean flags in the interface.
82-88: LGTM!The title extraction logic uses an appropriate regex pattern with good fallback behavior. The case-insensitive matching for "Change:" is a nice touch for robustness.
169-174: LGTM!The integration of the spaceline mode uses a clean early-return pattern and properly validates both the mode and spaceline flag before delegating to the new method.
143-143: No action required. The functiongetGitDiffStatsForPathis confirmed to be synchronous (returnsGitStatsResultdirectly, not a Promise). Calling it withoutawaitat line 143 is correct and intentional.
| #### Scenario: Spaceline with all elements | ||
|
|
||
| - **WHEN** a change has all available information | ||
| - **THEN** display: | ||
| ``` | ||
| π {change-id} | {progress-bar} {percentage}% | ||
| {status-icon} {category} ({completed}/{total} tasks) | {delta-icon} {count} | {open-icon} {open} | {branch-icon} {branch} ({added}β {removed}β) | ||
| ``` | ||
| - **WHERE**: | ||
| - `progress-bar` is a 10-character visual bar using `β` and `β` | ||
| - `percentage` is task completion percentage | ||
| - `status-icon` is implementation stage emoji (σ°·« for implementation, etc.) | ||
| - `category` is derived from change ID prefix (e.g., "add" β "Implementation") | ||
| - `delta-icon` `β` shows spec delta count | ||
| - `open-icon` `π` shows open TODO/pr items count | ||
| - `branch-icon` `σ°¬` shows current Git branch | ||
| - `addedβ` shows number of files with net additions | ||
| - `removedβ` shows number of files with net removals | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spec example output doesnβt match the implemented spaceline line format (status icon + git stats ordering).
The formatter output (and PR example) includes the status icon on Line 1 and uses β{added} β{removed} ordering; the spec example currently differs. Also, add a language to the fenced block (MD040).
Proposed spec fix
#### Scenario: Spaceline with all elements
- **WHEN** a change has all available information
- **THEN** display:
- ```
- π {change-id} | {progress-bar} {percentage}%
- {status-icon} {category} ({completed}/{total} tasks) | {delta-icon} {count} | {open-icon} {open} | {branch-icon} {branch} ({added}β {removed}β)
- ```
+ ```text
+ π {change-id} | {status-icon} {progress-bar} {percentage}%
+ {status-icon} {category} ({completed}/{total}) | {delta-icon} {count} | {open-icon} {open} open | {branch-icon} {branch} (β{added} β{removed})
+ ```
- **WHERE**:
- `progress-bar` is a 10-character visual bar using `β` and `β`
- `percentage` is task completion percentage
- `status-icon` is implementation stage emoji (σ°·« for implementation, etc.)
- `category` is derived from change ID prefix (e.g., "add" β "Implementation")
- `delta-icon` `β` shows spec delta count
- `open-icon` `π` shows open TODO/pr items count
- `branch-icon` `σ°¬` shows current Git branch
- - `addedβ` shows number of files with net additions
- - `removedβ` shows number of files with net removals
+ - `β{added}` shows number of files with net additions
+ - `β{removed}` shows number of files with net removalsAlso applies to: 34-40
π€ Prompt for AI Agents
In
@openspec/changes/archive/2026-01-10-add-list-spaceline/specs/cli-list/spec.md
around lines 15 - 33, The spec example block is inconsistent with the
implemented spaceline: move {status-icon} to Line 1 before {progress-bar} and
match git stat ordering to (β{added} β{removed}); also add a language tag to the
fenced block (use "text") and adjust the second line to mirror the formatter:
"π {change-id} | {status-icon} {progress-bar} {percentage}%" and "{status-icon}
{category} ({completed}/{total}) | {delta-icon} {count} | {open-icon} {open}
open | {branch-icon} {branch} (β{added} β{removed})", updating the WHERE list
entries for β{added} and β{removed} accordingly.
| ## Project Structure | ||
| ``` | ||
| src/ | ||
| βββ cli/ # CLI command implementations | ||
| βββ core/ # Core OpenSpec logic (templates, structure) | ||
| βββ utils/ # Shared utilities (file operations, rollback) | ||
| βββ commands/ # CLI command handlers | ||
| β βββ change.ts # Change proposal commands | ||
| β βββ spec.ts # Spec management commands | ||
| β βββ completion.ts # Shell completion commands | ||
| β βββ config.ts # Configuration commands | ||
| β βββ validate.ts # Validation commands | ||
| β βββ show.ts # Display commands | ||
| βββ core/ | ||
| β βββ configurators/ # AI tool configuration | ||
| β β βββ registry.ts | ||
| β β βββ [agent].ts | ||
| β β βββ slash/ # Slash command configurators | ||
| β βββ completions/ # Shell completion system | ||
| β β βββ generators/ # Bash, Zsh, Fish, PowerShell | ||
| β β βββ installers/ | ||
| β β βββ templates/ | ||
| β βββ artifact-graph/ # Artifact dependency resolution | ||
| β βββ templates/ # AI instruction templates | ||
| β βββ schemas/ # OpenSpec schema definitions | ||
| β βββ parsers/ # Markdown/change parsers | ||
| β βββ validation/ # Spec validation logic | ||
| βββ utils/ # Shared utilities | ||
| dist/ # Compiled output (gitignored) | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a language tag to the fenced project-structure block (MD040).
Proposed markdownlint fix
## Project Structure
-```
+```text
src/
βββ commands/ # CLI command handlers
β βββ change.ts # Change proposal commands
β βββ spec.ts # Spec management commands
β βββ completion.ts # Shell completion commands
β βββ config.ts # Configuration commands
β βββ validate.ts # Validation commands
β βββ show.ts # Display commands
βββ core/
β βββ configurators/ # AI tool configuration
β β βββ registry.ts
β β βββ [agent].ts
β β βββ slash/ # Slash command configurators
β βββ completions/ # Shell completion system
β β βββ generators/ # Bash, Zsh, Fish, PowerShell
β β βββ installers/
β β βββ templates/
β βββ artifact-graph/ # Artifact dependency resolution
β βββ templates/ # AI instruction templates
β βββ schemas/ # OpenSpec schema definitions
β βββ parsers/ # Markdown/change parsers
β βββ validation/ # Spec validation logic
βββ utils/ # Shared utilities
dist/ # Compiled output (gitignored)</details>
<details>
<summary>π§° Tools</summary>
<details>
<summary>πͺ markdownlint-cli2 (0.18.1)</summary>
20-20: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>π€ Prompt for AI Agents</summary>
In @openspec/project.md around lines 19 - 46, The fenced code block under the
"## Project Structure" section is missing a language tag (MD040); update the
opening fence from totext so the tree block is explicitly tagged (i.e.,
replace the triple backticks before the src/ tree with ```text) to satisfy the
markdownlint rule.
</details>
<!-- This is an auto-generated comment by CodeRabbit -->
| // Sort by preference (spaceline uses alphabetical by default for readability) | ||
| const sorted = sort === 'name' | ||
| ? changesData.sort((a, b) => a.changeData.id.localeCompare(b.changeData.id)) | ||
| : changesData.sort((a, b) => a.changeData.id.localeCompare(b.changeData.id)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the sorting logic bug.
Both branches of the ternary operator perform the same alphabetical sort by changeData.id. The recent sort should order by lastModified instead.
π Proposed fix for sorting logic
- const sorted = sort === 'name'
- ? changesData.sort((a, b) => a.changeData.id.localeCompare(b.changeData.id))
- : changesData.sort((a, b) => a.changeData.id.localeCompare(b.changeData.id));
+ const sorted = sort === 'name'
+ ? changesData.sort((a, b) => a.changeData.id.localeCompare(b.changeData.id))
+ : changesData.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());π€ Prompt for AI Agents
In @src/core/list.ts around lines 153 - 156, The ternary for building sorted in
src/core/list.ts currently sorts by changeData.id in both branches; update the
non-'name' branch (the "recent" case) so it sorts by lastModified instead β
change the comparator used in changesData.sort for the else branch to compare
b.changeData.lastModified and a.changeData.lastModified (or parse them as Dates
if they are strings) to produce descending (most recent first) order; keep the
'name' branch using changeData.id.localeCompare and only change the comparator
for the recent sort.
| export function getGitDiffStatsForPath(filePath: string): GitStatsResult { | ||
| try { | ||
| const branch = getCurrentBranch(); | ||
| if (branch === '(no branch)') { | ||
| return { ...DEFAULT_GIT_STATS, branch }; | ||
| } | ||
|
|
||
| const base = getDiffBase(); | ||
| const relativePath = path.relative(process.cwd(), filePath); | ||
| const output = execSync(`git diff --numstat ${base} -- ${relativePath}`, { encoding: 'utf-8' }); | ||
|
|
||
| let added = 0; | ||
| let removed = 0; | ||
|
|
||
| const lines = output.trim().split('\n'); | ||
| for (const line of lines) { | ||
| if (!line) continue; | ||
|
|
||
| const parts = line.split('\t'); | ||
| if (parts.length < 2) continue; | ||
|
|
||
| const additions = parseInt(parts[0], 10) || 0; | ||
| const deletions = parseInt(parts[1], 10) || 0; | ||
|
|
||
| if (additions > deletions) { | ||
| added++; | ||
| } else if (deletions > additions) { | ||
| removed++; | ||
| } | ||
| } | ||
|
|
||
| return { added, removed, branch }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escape or quote the path argument to prevent command injection.
Line 132 directly interpolates relativePath into the shell command without escaping or quoting. If the path contains spaces, quotes, or other special characters, the command could fail or behave unexpectedly. Consider using execSync with an array of arguments or properly escaping the path.
π Proposed fix using array arguments or proper escaping
Option 1: Use array arguments (if execSync supports it via spawn)
Note: execSync doesn't directly support array arguments, but you can use spawn or spawnSync from child_process for safer argument passing.
Option 2: Escape the path for shell usage
const base = getDiffBase();
const relativePath = path.relative(process.cwd(), filePath);
- const output = execSync(`git diff --numstat ${base} -- ${relativePath}`, { encoding: 'utf-8' });
+ // Escape path for shell by wrapping in quotes and escaping internal quotes
+ const escapedPath = relativePath.replace(/'/g, "'\\''");
+ const output = execSync(`git diff --numstat ${base} -- '${escapedPath}'`, { encoding: 'utf-8' });Alternatively, for more robust handling, consider using a utility library for shell escaping or restructuring to avoid shell interpolation entirely.
π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function getGitDiffStatsForPath(filePath: string): GitStatsResult { | |
| try { | |
| const branch = getCurrentBranch(); | |
| if (branch === '(no branch)') { | |
| return { ...DEFAULT_GIT_STATS, branch }; | |
| } | |
| const base = getDiffBase(); | |
| const relativePath = path.relative(process.cwd(), filePath); | |
| const output = execSync(`git diff --numstat ${base} -- ${relativePath}`, { encoding: 'utf-8' }); | |
| let added = 0; | |
| let removed = 0; | |
| const lines = output.trim().split('\n'); | |
| for (const line of lines) { | |
| if (!line) continue; | |
| const parts = line.split('\t'); | |
| if (parts.length < 2) continue; | |
| const additions = parseInt(parts[0], 10) || 0; | |
| const deletions = parseInt(parts[1], 10) || 0; | |
| if (additions > deletions) { | |
| added++; | |
| } else if (deletions > additions) { | |
| removed++; | |
| } | |
| } | |
| return { added, removed, branch }; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export function getGitDiffStatsForPath(filePath: string): GitStatsResult { | |
| try { | |
| const branch = getCurrentBranch(); | |
| if (branch === '(no branch)') { | |
| return { ...DEFAULT_GIT_STATS, branch }; | |
| } | |
| const base = getDiffBase(); | |
| const relativePath = path.relative(process.cwd(), filePath); | |
| // Escape path for shell by wrapping in quotes and escaping internal quotes | |
| const escapedPath = relativePath.replace(/'/g, "'\\''"); | |
| const output = execSync(`git diff --numstat ${base} -- '${escapedPath}'`, { encoding: 'utf-8' }); | |
| let added = 0; | |
| let removed = 0; | |
| const lines = output.trim().split('\n'); | |
| for (const line of lines) { | |
| if (!line) continue; | |
| const parts = line.split('\t'); | |
| if (parts.length < 2) continue; | |
| const additions = parseInt(parts[0], 10) || 0; | |
| const deletions = parseInt(parts[1], 10) || 0; | |
| if (additions > deletions) { | |
| added++; | |
| } else if (deletions > additions) { | |
| removed++; | |
| } | |
| } | |
| return { added, removed, branch }; | |
| } catch { | |
| return null; | |
| } | |
| } |
π€ Prompt for AI Agents
In @src/utils/git-stats.ts around lines 123 - 158, The getGitDiffStatsForPath
function interpolates relativePath into a shell command causing possible command
injection and failures with spaces/special chars; replace the execSync
interpolation with a safe child_process spawnSync call (or spawn) that passes
arguments as an array (e.g., ['diff','--numstat', base, '--', relativePath]) and
read stdout with the same encoding, updating the function to import spawnSync
from 'child_process' and handle errors the same way so parsing of output,
added/removed calculation, and returned shape remain unchanged.
| export const EMOJI = { | ||
| /** Change/document icon */ | ||
| CHANGE: 'π', | ||
| /** Progress bar filled character */ | ||
| PROGRESS_FULL: 'β', | ||
| /** Progress bar empty character */ | ||
| PROGRESS_EMPTY: 'β', | ||
| /** Implementation status */ | ||
| STATUS_IMPLEMENTATION: 'σ°·«', | ||
| /** Delta count icon */ | ||
| DELTA: 'β', | ||
| /** Open items icon */ | ||
| OPEN: 'π', | ||
| /** Git branch icon */ | ||
| BRANCH: 'σ°¬', | ||
| } as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Script executed:
#!/bin/bash
# Search for mentions of Nerd Font or font requirements in documentation
echo "=== Searching for font/Nerd Font mentions ==="
rg -i -C3 'nerd.?font|font.?requirement|prerequisites|requirements' README.md --type=md 2>/dev/null || echo "No matches in README.md"
echo -e "\n=== Checking if docs directory exists ==="
if [ -d "docs" ]; then
rg -i -C3 'nerd.?font|font.?requirement|prerequisites|requirements' docs/ --type=md 2>/dev/null || echo "No matches in docs/"
else
echo "docs/ directory not found"
fi
echo -e "\n=== Broader search for installation or setup docs ==="
find . -maxdepth 2 -type f \( -name "INSTALL*" -o -name "SETUP*" -o -name "*.md" \) ! -path "*/node_modules/*" ! -path "*/.git/*" 2>/dev/null | head -20Repository: Fission-AI/OpenSpec
Length of output: 5773
Document the Nerd Font requirement.
The EMOJI constants include Nerd Font icons (STATUS_IMPLEMENTATION: 'σ°·«' and BRANCH: 'σ°¬'), which require users to have a Nerd Font installed. Without it, these glyphs render as broken characters or boxes. This requirement is not currently documented in the README or installation prerequisites.
Add a note to the README's Prerequisites section specifying that a Nerd Font (such as FiraCode Nerd Font or Hack Nerd Font) is required for proper display of UI elements.
π€ Prompt for AI Agents
In @src/utils/spaceline-formatter.ts around lines 6 - 21, Update the README
Prerequisites to note that certain glyphs used in the UI require a Nerd Font
(e.g., FiraCode Nerd Font or Hack Nerd Font) so the characters for
STATUS_IMPLEMENTATION and BRANCH in EMOJI render correctly; add a brief line in
the Prerequisites section naming Nerd Font as a requirement and suggest a couple
of example fonts and how to verify installation.
| it('should return stats when in a Git repository with changes', async () => { | ||
| // Initialize a new Git repo in temp dir | ||
| process.chdir(tempDir); | ||
| execSync('git init', { stdio: 'pipe', cwd: tempDir }); | ||
| execSync('git config user.email "[email protected]"', { stdio: 'pipe', cwd: tempDir }); | ||
| execSync('git config user.name "Test User"', { stdio: 'pipe', cwd: tempDir }); | ||
|
|
||
| // Create initial commit | ||
| const initialFile = path.join(tempDir, 'initial.txt'); | ||
| await fs.writeFile(initialFile, 'initial content'); | ||
| execSync('git add .', { stdio: 'pipe', cwd: tempDir }); | ||
| execSync('git commit -m "initial"', { stdio: 'pipe', cwd: tempDir }); | ||
|
|
||
| // Create a new file with additions (staged but not committed) | ||
| const newFile = path.join(tempDir, 'new-file.txt'); | ||
| await fs.writeFile(newFile, 'new content\nmore content'); | ||
|
|
||
| const stats = getGitDiffStats(); | ||
| expect(stats).not.toBeNull(); | ||
| // Note: git diff --numstat against HEAD shows staged changes | ||
| // The new file should appear as an addition | ||
| expect(stats?.branch).toBe('main'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Script executed:
# First, let's find and examine the test file
find . -name "git-stats.test.ts" -type fRepository: Fission-AI/OpenSpec
Length of output: 93
π Script executed:
# Also search for the getGitDiffStats function to understand what it does
find . -name "*.ts" -type f | head -20Repository: Fission-AI/OpenSpec
Length of output: 728
π Script executed:
# List the test file to verify content and line numbers
ls -la test/utils/git-stats.test.ts 2>/dev/null || echo "File not found at expected location"Repository: Fission-AI/OpenSpec
Length of output: 140
π Script executed:
# Read the test file to verify the actual code at lines 43-65
cat -n test/utils/git-stats.test.ts | sed -n '40,70p'Repository: Fission-AI/OpenSpec
Length of output: 1579
π Script executed:
# Also find and examine the getGitDiffStats function implementation
find . -name "*.ts" -type f -not -path "*/node_modules/*" | xargs rg "export.*getGitDiffStats|function getGitDiffStats" -A 10Repository: Fission-AI/OpenSpec
Length of output: 1349
π Script executed:
# Examine the getCurrentBranch function and full git-stats.ts implementation
cat -n ./src/utils/git-stats.tsRepository: Fission-AI/OpenSpec
Length of output: 5100
π Script executed:
# Check if there's any git configuration setup in the test setup
cat -n test/utils/git-stats.test.ts | head -50Repository: Fission-AI/OpenSpec
Length of output: 2289
π Script executed:
# Check the full test to see if there's any explicit branch setting
cat -n test/utils/git-stats.test.ts | sed -n '43,100p'Repository: Fission-AI/OpenSpec
Length of output: 2996
π Script executed:
# Verify git init behavior and whether it supports -b flag for setting initial branch
git init --help 2>/dev/null | grep -A 5 "\-b" | head -20 || echo "Checking alternative..."Repository: Fission-AI/OpenSpec
Length of output: 45
π Script executed:
# Test git init default branch behavior in a sandbox
mkdir -p /tmp/git-test-1
cd /tmp/git-test-1
git init 2>&1 | head -5
git branch --show-current
cd /tmp
rm -rf git-test-1Repository: Fission-AI/OpenSpec
Length of output: 371
π Script executed:
# Verify that git init -b flag is available as a solution
git init --help 2>/dev/null | grep -E "^\s+-b," || git --versionRepository: Fission-AI/OpenSpec
Length of output: 81
Fix branch name assertion for test reliability.
Line 64 assumes the branch name is 'main', but git init creates a 'master' branch by default. This will cause the test to fail in environments where Git is configured to use the standard default branch name.
Consider either:
- Explicitly setting the default branch during test setup using
git init -b main, or - Making the assertion flexible to accept either branch name
Suggested fix for flexible branch name handling
const stats = getGitDiffStats();
expect(stats).not.toBeNull();
- // Note: git diff --numstat against HEAD shows staged changes
- // The new file should appear as an addition
- expect(stats?.branch).toBe('main');
+ // Branch could be 'main' or 'master' depending on git config
+ expect(['main', 'master']).toContain(stats?.branch);π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should return stats when in a Git repository with changes', async () => { | |
| // Initialize a new Git repo in temp dir | |
| process.chdir(tempDir); | |
| execSync('git init', { stdio: 'pipe', cwd: tempDir }); | |
| execSync('git config user.email "[email protected]"', { stdio: 'pipe', cwd: tempDir }); | |
| execSync('git config user.name "Test User"', { stdio: 'pipe', cwd: tempDir }); | |
| // Create initial commit | |
| const initialFile = path.join(tempDir, 'initial.txt'); | |
| await fs.writeFile(initialFile, 'initial content'); | |
| execSync('git add .', { stdio: 'pipe', cwd: tempDir }); | |
| execSync('git commit -m "initial"', { stdio: 'pipe', cwd: tempDir }); | |
| // Create a new file with additions (staged but not committed) | |
| const newFile = path.join(tempDir, 'new-file.txt'); | |
| await fs.writeFile(newFile, 'new content\nmore content'); | |
| const stats = getGitDiffStats(); | |
| expect(stats).not.toBeNull(); | |
| // Note: git diff --numstat against HEAD shows staged changes | |
| // The new file should appear as an addition | |
| expect(stats?.branch).toBe('main'); | |
| }); | |
| it('should return stats when in a Git repository with changes', async () => { | |
| // Initialize a new Git repo in temp dir | |
| process.chdir(tempDir); | |
| execSync('git init', { stdio: 'pipe', cwd: tempDir }); | |
| execSync('git config user.email "[email protected]"', { stdio: 'pipe', cwd: tempDir }); | |
| execSync('git config user.name "Test User"', { stdio: 'pipe', cwd: tempDir }); | |
| // Create initial commit | |
| const initialFile = path.join(tempDir, 'initial.txt'); | |
| await fs.writeFile(initialFile, 'initial content'); | |
| execSync('git add .', { stdio: 'pipe', cwd: tempDir }); | |
| execSync('git commit -m "initial"', { stdio: 'pipe', cwd: tempDir }); | |
| // Create a new file with additions (staged but not committed) | |
| const newFile = path.join(tempDir, 'new-file.txt'); | |
| await fs.writeFile(newFile, 'new content\nmore content'); | |
| const stats = getGitDiffStats(); | |
| expect(stats).not.toBeNull(); | |
| // Branch could be 'main' or 'master' depending on git config | |
| expect(['main', 'master']).toContain(stats?.branch); | |
| }); |
π€ Prompt for AI Agents
In @test/utils/git-stats.test.ts around lines 43 - 65, The test assumes the repo
branch is 'main' which can be 'master' by default; update the test in
git-stats.test.ts (the it block that calls getGitDiffStats) to either initialize
the repo with an explicit branch (use execSync('git init -b main', ...) when
setting up the temp repo) or make the assertion accept both names (e.g., assert
stats?.branch is 'main' or 'master' or match /^(main|master)$/) so the test
passes regardless of Git's default branch.
|
@zpershuai Whats the use case for this? |
|
@TabishB I use claude codeοΌclaude code has a feature that the user can customize the statusline, so I can use this feature to see the status of openspec immediately. |

Add compact, emoji-rich horizontal format for displaying changes with progress bars, task status, and Git statistics.
Usage:
openspec list --spaceline
Example output:
π add-feature | σ°·« ββββββββββ 80%
σ°·« Implementation (4/5) | β 2 | π 1 open | σ°¬ main (β3 β0)
Summary by CodeRabbit
New Features
Documentation
βοΈ Tip: You can customize this high-level summary in your review settings.