Skip to content

feat: add OpenSpec as builtin source#320

Merged
rubenmarcus merged 1 commit intomainfrom
feat/openspec-source
Apr 8, 2026
Merged

feat: add OpenSpec as builtin source#320
rubenmarcus merged 1 commit intomainfrom
feat/openspec-source

Conversation

@rubenmarcus
Copy link
Copy Markdown
Member

Summary

  • Adds OpenSpec as a new builtin source that reads structured spec directories
  • No auth required - reads directly from the local openspec/ directory
  • Supports listing changes, fetching specific changes, all changes, or global specs
  • 13 tests covering all code paths

Usage

ralph-starter run --from openspec              # list changes
ralph-starter run --from openspec:my-feature   # specific change
ralph-starter run --from openspec:all          # all active changes
ralph-starter run --from openspec:specs        # global specs only

Files

  • src/sources/builtin/openspec.ts - OpenSpecSource implementation
  • src/sources/__tests__/openspec.test.ts - 13 tests
  • src/sources/index.ts - Register in source registry (2 lines)

Test plan

  • All 13 OpenSpec tests pass
  • Pre-existing tests unaffected (243 passed)
  • Biome lint passes (no new errors)
  • Manual test with real openspec/ directory

🤖 Generated with Claude Code

Adds support for reading structured specs from OpenSpec directories.
OpenSpec organizes specs into changes (proposal, design, tasks, specs)
which map naturally to ralph-starter's source architecture.

Usage:
  ralph-starter run --from openspec           # list changes
  ralph-starter run --from openspec:my-feat   # specific change
  ralph-starter run --from openspec:all       # all active changes
  ralph-starter run --from openspec:specs     # global specs only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

Issue Linking Reminder

This PR doesn't appear to have a linked issue. Consider linking to:

  • This repo: Closes #123
  • ralph-ideas: Closes multivmlabs/ralph-ideas#123

Using Closes, Fixes, or Resolves will auto-close the issue when this PR is merged.


If this PR doesn't need an issue, you can ignore this message.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

✔️ Bundle Size Analysis

Metric Value
Base 2746.81 KB
PR 2787.52 KB
Diff 40.71 KB (1.00%)
Bundle breakdown
156K	dist/auth
80K	dist/automation
4.0K	dist/cli.d.ts
4.0K	dist/cli.d.ts.map
24K	dist/cli.js
16K	dist/cli.js.map
664K	dist/commands
28K	dist/config
4.0K	dist/index.d.ts
4.0K	dist/index.d.ts.map
4.0K	dist/index.js
4.0K	dist/index.js.map
916K	dist/integrations
100K	dist/llm
1.2M	dist/loop
188K	dist/mcp
60K	dist/presets
92K	dist/setup
40K	dist/skills
452K	dist/sources
76K	dist/ui
144K	dist/utils
336K	dist/wizard

Comment thread src/sources/__tests__/openspec.test.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Greptile Summary

This PR adds OpenSpecSource, a new builtin source that reads structured spec directories under openspec/ with no auth required. It supports listing active changes, fetching a specific change, aggregating all changes, and reading global specs, and is registered alongside the other builtin sources in the source registry.

Confidence Score: 5/5

Safe to merge — only P2 findings remain, all non-blocking.

All three findings are P2: a fragile but currently working mock specifier, a metadata field that over-reports spec directories, and missing path-traversal sanitisation that has negligible impact in a local CLI. No logic errors, broken contracts, or data loss risks on the changed path. Test coverage is thorough (13 tests, all passing).

src/sources/builtin/openspec.ts — minor metadata inconsistency and missing traversal guard worth cleaning up post-merge.

Vulnerabilities

No security concerns identified. The source reads only local files and requires no credentials. The changeName path-join behaviour is noted as a style suggestion rather than an exploitable vulnerability in this CLI context.

Important Files Changed

Filename Overview
src/sources/builtin/openspec.ts New BuiltinSource that reads structured openspec/ directories; metadata.specs can include directories with no spec.md content and changeName lacks path-traversal guard.
src/sources/tests/openspec.test.ts 13 tests covering all fetch modes; vi.mock targets 'fs' while source and test imports use 'node:fs' — currently works via Vitest aliasing but is fragile.
src/sources/index.ts Two-line registration of OpenSpecSource in the builtin source list; clean and correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["fetch(identifier)"] --> B{id?}
    B -- "empty / 'list'" --> C[listChanges]
    B -- "'specs' / 'global'" --> D[fetchGlobalSpecs]
    B -- "'all'" --> E[fetchAllChanges]
    B -- "other" --> F[fetchChange]

    C --> G[getActiveChanges\nopenspec/changes/*\nexclude archive]
    E --> G
    G --> H{changes\nexist?}
    H -- yes --> I[SourceResult]
    H -- no/empty --> J[error / empty result]

    F --> K[join openspecDir/changes/changeName]
    K --> L{dir exists?}
    L -- no --> M[error]
    L -- yes --> N[Read proposal.md\ndesign.md\ntasks.md\nspecs/*/spec.md]
    N --> I

    D --> O[join openspecDir/specs]
    O --> P{exists?}
    P -- no --> M
    P -- yes --> Q[Read specs/*/spec.md]
    Q --> I
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/sources/__tests__/openspec.test.ts
Line: 5-10

Comment:
**Mock specifier doesn't match import specifier**

The test file imports from `'node:fs'` (line 1) but the mock target is `'fs'`. Vitest currently aliases these, so the tests pass, but this could break if Vitest's module resolution changes. The source under test also uses `'node:fs'`, so aligning the mock is the safer approach.

```suggestion
vi.mock('node:fs', () => ({
  existsSync: vi.fn(),
  readFileSync: vi.fn(),
  readdirSync: vi.fn(),
  statSync: vi.fn(),
}));
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/sources/builtin/openspec.ts
Line: 254-263

Comment:
**`metadata.specs` includes directories without `spec.md`**

`specDirs` is built from all subdirectories, but only those with a `spec.md` are actually read into the output content. If a directory exists but has no `spec.md`, it shows up in `metadata.specs` but contributes nothing to `content`, making `specs.length` larger than `count`. A caller inspecting `metadata.specs` could conclude there is content for those directories when there isn't.

```suggestion
    return {
      content: sections.join('\n'),
      source: 'openspec:specs',
      title: 'OpenSpec Global Specs',
      metadata: {
        type: 'openspec',
        mode: 'specs',
        count: files.length,
        specs: files.map((f) => f.replace('/spec.md', '').replace('specs/', '')),
      },
    };
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/sources/builtin/openspec.ts
Line: 126-130

Comment:
**`changeName` is used unsanitized in path construction**

`changeName` comes directly from the CLI identifier (e.g. `openspec:../../../etc`). `path.join` normalises `..` segments, so a name like `../../../outside` resolves to a directory outside `openspec/changes/`. The `isDirectory()` guard prevents reading arbitrary files, but allows stat/exists checks on arbitrary directories and could leak directory-existence information. Since this is a local CLI tool the practical risk is low, but adding a guard makes the intent explicit:

```suggestion
    const changePath = join(openspecDir, 'changes', changeName);

    // Prevent path traversal outside changes/
    const changesRoot = join(openspecDir, 'changes');
    if (!changePath.startsWith(changesRoot + '/') && changePath !== changesRoot) {
      this.error(`Invalid change name: ${changeName}`);
    }

    if (!existsSync(changePath) || !statSync(changePath).isDirectory()) {
      this.error(`Change not found: ${changeName}`);
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: add OpenSpec as builtin source for..." | Re-trigger Greptile

Comment thread src/sources/__tests__/openspec.test.ts
Comment thread src/sources/builtin/openspec.ts
Comment thread src/sources/builtin/openspec.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ea522246f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/sources/builtin/openspec.ts
Comment thread src/sources/builtin/openspec.ts
@rubenmarcus rubenmarcus merged commit 03a1ed1 into main Apr 8, 2026
12 checks passed
@claude claude Bot mentioned this pull request Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants