Conversation
Create initial package structure for OpenCode provider with stub implementations of the SPI Provider interface. Type definitions and full implementation will follow in subsequent steps. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Define Go structs matching OpenCode's JSON schema including Project, Session, Message, Part, and ToolState types. Add supporting types for time tracking, token usage, and path info. Include constants for part types and tool states to enable type-safe comparisons. Add FullSession and FullMessage assembled types for internal processing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements Step 3 of the OpenCode provider plan with path resolution functions including SHA-1 project hash computation, storage directory lookup, and explicit handling to skip global sessions. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements Step 4 of the OpenCode provider: parser.go with functions to load and assemble OpenCode data from its distributed JSON file structure. Key functions: - LoadProject: Load project metadata from JSON file - LoadSession: Load single session from JSON file - LoadSessionsForProject: Scan and load all sessions for a project - LoadMessagesForSession: Load all messages for a session (sorted oldest-first) - LoadPartsForMessage: Load all parts for a message (sorted by time) - AssembleFullSession: Build complete session with messages and parts - LoadAndAssembleSession: Convenience function to load and assemble by ID - LoadAllSessionsForProject: Load and assemble all sessions for a project - GetFirstUserMessageContent: Extract first user message for slug generation The parser handles OpenCode's distributed file structure where sessions, messages, and parts are stored in separate directories, and assembles them into a hierarchical structure suitable for conversion to SpecStory schema. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements Step 5 of the OpenCode provider plan with functions to convert OpenCode's session/message/part data model to SpecStory's unified schema. Key features: - ConvertToSessionData: Main conversion function with parentID metadata support - ConvertMessage: Converts messages with grouped part handling - ConvertPart: Handles all part types (text, reasoning, tool, compaction, file) - MapToolType: Maps OpenCode tools to SpecStory types (read, write, shell, search, task, generic) - Skips internal markers (step-start, step-finish, patch) - Marks compacted content with [Compacted] prefix - Generates slugs from first user message when needed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds full implementations for all Provider interface methods: - Check: Verifies OpenCode installation via `opencode --version` - DetectAgent: Checks if OpenCode session data exists for project - GetAgentChatSession: Loads single session with messages and parts - GetAgentChatSessions: Loads all sessions for a project - ExecAgentAndWatch: Stub with TODO for Step 7 watcher integration - WatchAgent: Stub with TODO for Step 7 watcher integration Also includes helper functions for command parsing, error classification, user-friendly error messages, and debug file writing. The watch methods are stubs that will be fully implemented in Step 7 (watcher.go). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds watcher.go to monitor OpenCode storage directories and detect session changes in real-time. Updates provider.go to use the watcher in ExecAgentAndWatch and WatchAgent methods, following the pattern established by the Cursor CLI provider. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add the opencode package import and register the OpenCode provider in registerAll() so it's available throughout the application. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Extracts error handling functions into errors.go for better organization: - buildCheckErrorMessage: User-friendly messages for Check failures - printDetectionHelp: Guidance when DetectAgent fails - formatJSONParseError: Messages for corrupt/invalid JSON files Error scenarios covered: not installed, storage missing, project missing, permission denied, global sessions, and JSON parse failures. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This completes the OpenCode provider implementation with thorough test coverage across all components: - paths_test.go: Tests for project hash computation (deterministic, empty path, different paths), storage directory resolution, project directory resolution (storage missing, project missing, success), ListProjectHashes, and helper functions (GetSessionsDir, GetMessagesDir, GetPartsDir, GetProjectFilePath). Uses dependency injection for osGetwd, osUserHomeDir, and osStat functions. - parser_test.go: Tests for JSON loading (LoadProject, LoadSession, LoadSessionsForProject, LoadMessagesForSession, LoadPartsForMessage), session assembly (AssembleFullSession), sorting behavior, and edge cases (missing files, malformed JSON, empty directories). - schema_test.go: Tests for schema conversion (ConvertToSessionData), tool type mapping (MapToolType covering all tool categories), message conversion (all part types including text, reasoning, tool, compaction, file), skipped part types, path hint extraction, and helper functions (isAutoGeneratedSlug, isHexString, isUUIDFormat, normalizePath). - provider_test.go: Tests for Provider methods (Name, DetectAgent, GetAgentChatSession, GetAgentChatSessions), command parsing (parseOpenCodeCommand), error classification (classifyCheckError), and session conversion (convertToAgentChatSession). Test coverage: 55.7% of statements. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
OpenCode uses the git root commit hash as the project identifier, not a SHA-1 hash of the directory path. The algorithm: 1. Find the .git directory by walking up from the project path 2. Check for a cached hash in .git/opencode (OpenCode caches here) 3. Run `git rev-list --max-parents=0 --all` to get root commit(s) 4. Sort commits alphabetically and use the first one 5. Return "global" for non-git directories or repos without commits This matches OpenCode's implementation in project.ts and fixes the project hash mismatch bug where SpecStory was computing different hashes than OpenCode for the same directories. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
@marton78 I haven't had a chance to really dig into this yet, but my first reaction from reading the PR description is around the The CLI works like Can you check if this just needs cleaned up in the PR description, or if there's something more going on in this PR with Cheers, |
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive support for the OpenCode AI coding assistant to SpecStory CLI, enabling users to sync and monitor OpenCode sessions alongside existing providers (Claude Code, Cursor, Codex, Gemini). The implementation achieves full feature parity with existing providers through 8 source files (~2,500 lines) and 4 test files (80+ test cases) achieving 55.7% code coverage.
Changes:
- Added complete OpenCode provider implementation with SPI interface compliance
- Implemented real-time file watching for session updates
- Added schema conversion from OpenCode's data model to SpecStory's unified format
- Registered OpenCode provider in the factory registry
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| registry.go | Registered OpenCode provider in factory |
| provider.go | Core provider implementation (Check, DetectAgent, Get/Watch methods) |
| types.go | OpenCode data structures (Session, Message, Part, etc.) |
| schema.go | Conversion to SpecStory unified schema |
| paths.go | Path resolution and project hash computation |
| parser.go | JSON parsing and data assembly |
| watcher.go | Real-time file system monitoring |
| errors.go | User-friendly error messages |
| *_test.go | Comprehensive unit tests (4 files) |
| go func(sid string) { | ||
| time.Sleep(debounceWindow) | ||
|
|
||
| // Debounce check: If another event arrived for this session while we were sleeping, | ||
| // it would have updated lastProcessed[sid] to a newer timestamp. By comparing against | ||
| // the 'now' value we captured before sleeping, we can detect this: | ||
| // - If timestamps match: no newer event came in, we should proceed with reload | ||
| // - If timestamps differ: a newer event is pending, skip this reload (newer goroutine will handle it) | ||
| // This ensures only the most recent event triggers a reload, preventing duplicate processing. | ||
| w.mu.Lock() | ||
| if w.lastProcessed[sid] != now { | ||
| w.mu.Unlock() | ||
| slog.Debug("Watcher.handleEvent: Skipping reload, newer event pending", "sessionID", sid) | ||
| return | ||
| } | ||
| w.mu.Unlock() | ||
|
|
||
| w.reloadAndCallback(sid) | ||
| }(sessionID) |
There was a problem hiding this comment.
The goroutine captures now by value but compares it against w.lastProcessed[sid] which could be updated by other goroutines between line 291 and line 296. If a new event arrives after line 291 but before the goroutine starts at line 296, both goroutines will have different now values and both will proceed with reload. Consider capturing now inside the goroutine or using a context cancellation mechanism.
There was a problem hiding this comment.
The code actually handles this correctly. The debounce mechanism works as intended:
- Event A sets
lastProcessed[sid] = now_A, spawnsgoroutine A - If Event B arrives during the sleep, it sets
lastProcessed[sid] = now_B - When goroutine A wakes up, it checks
lastProcessed[sid] != now_A→ TRUE (because now_B is there) - Goroutine A correctly skips; goroutine B handles the reload
The check at line 306 is exactly designed to detect this case. The suggested fix of "capturing now inside the goroutine" would actually break the logic since we need to compare against the timestamp from when the event was recorded.
There's a minor theoretical edge case if two events arrive within the same mutex lock/unlock window, but the practical impact is just a redundant reload (not data corruption), and it's extremely unlikely.
| // Note: This function loads ALL sessions with ALL their messages and parts into memory | ||
| // simultaneously. For projects with many large sessions, this could consume significant | ||
| // memory. Consider using LoadSessionsForProject and AssembleFullSession individually | ||
| // if memory usage becomes a concern. |
There was a problem hiding this comment.
The comment accurately warns about memory consumption but should also document the return value behavior when some sessions fail to assemble. Looking at the implementation, failed sessions are silently skipped and only logged. This should be mentioned in the function documentation to clarify that the returned slice may contain fewer sessions than exist on disk.
| // if memory usage becomes a concern. | |
| // if memory usage becomes a concern. | |
| // | |
| // Sessions that fail to assemble are logged and skipped; they are not included in the | |
| // returned slice. As a result, the returned slice may contain fewer sessions than exist | |
| // on disk for the given project. |
| // 3. Run `git rev-list --max-parents=0 --all` to get root commit(s) | ||
| // 4. Sort the commits and use the first one (alphabetically) | ||
| // 5. Returns "global" if not in a git repo or no commits found | ||
| func ComputeProjectHash(projectPath string) (string, error) { |
There was a problem hiding this comment.
The fallback behavior when git commands fail (returning "global") should have explicit test coverage for the case where git is not installed or unavailable. The current tests only cover git repos and non-git directories, but not the case where git exists in the path but executing it fails.
OpenCode stores timestamps as Unix milliseconds (int64), not ISO strings. Also fixes TokenInfo.Cache which is an object with read/write fields, not an integer. These mismatches caused session and message parsing to fail silently, resulting in markdown exports that only contained user messages. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add documentation to LoadAllSessionsForProject clarifying that sessions which fail to assemble are logged and skipped, resulting in potentially fewer sessions in the returned slice than exist on disk - Add test for ComputeProjectHash when git command execution fails, verifying it returns "global" as expected Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@belucid thanks, you were right, that was just an error in the docs. The actual command line still works as you described. I fixed the code review comments. |
|
@marton78 appreciate you engaging with us here! |
…ions Two issues were causing sessions to be repeatedly marked as "updated": 1. Unstable sorting in OpenCode parser - sorts for sessions, messages, and parts lacked tiebreakers, causing non-deterministic ordering when timestamps were equal. Added ID as secondary sort key to all three sorts. 2. Filename collisions - multiple sessions created within the same second with similar content would map to the same filename. Each sync run, they would overwrite each other, causing perpetual "updates". Added collision detection that appends a short session ID suffix when needed. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
just asking, will this be merged into main branch anytime in the next 2 weeks? i am looking forward to trying this out for a project in my masters programme |
|
@simkimsia (cc: @marton78 ) yes, we are targeting getting this in in sooner than the next 2 weeks. We're working on something big and exciting for a "v2" of the SpecStory CLI, so our attention has been elsewhere a bit, but we appreciate the contribution here. @simkimsia also if you follow the instructions its really trivial to build from source so you should be able to easily create a SpecStory CLI from @marton78 's branch w/ OpenCode support ahead of us merging if you want to get a head start. |
|
@intellectronica, we're looking on getting this integrated soon (per: https://github.com/intellectronica/opencode-convodump/) if you're interested or wanna wade in ;) |
|
This looks very promising. Any news on merge intent? I looked for a SpecStory (V2) roadmap but can't find anything. |
OpenCode Provider for SpecStory
Implementation Status: ✅ COMPLETE
Statistics:
Usage:
Overview
Add support for the OpenCode coding agent to SpecStory CLI. OpenCode is a terminal-based AI coding assistant by SST that stores session data in JSON files at
~/.local/share/opencode/storage/.Goals
specstory syncandspecstory run(watch mode)File Structure
OpenCode Data Model Reference
Storage Location
~/.local/share/opencode/storage/Directory Structure
Project Hash Computation
OpenCode uses SHA-1 hash of the absolute worktree path as the project identifier.
Testing Strategy
Unit Tests (
provider_test.go)Test cases:
Use table-driven tests following existing patterns in codebase.
Manual Testing Checklist
Installation Check
Agent Detection
cd /path/to/opencode/project ./specstory sync opencode --debugSession Sync
Watch Mode
./specstory run opencode # In another terminal, use opencode and verify updatesMarkdown Output Verification
Dependencies
No new external dependencies required. Uses:
crypto/sha1(stdlib) - for project hashencoding/json(stdlib) - for JSON parsingfsnotify/fsnotify(existing) - for file watchingRisks and Mitigations
filepathpackage, test on macOS and LinuxDesign Decisions
1. Session Parent Relationships
Decision: Store
parentIDin session metadata without changing structure.OpenCode sessions can have a
parentIDpointing to another session (branching/subagents). We preserve this information in the session metadata for potential future use, but treat each session as independent for now.Implementation: Include
parentIDinSessionData.Metadatawhen present.2. Compaction Handling
Decision: Mark compacted sections with a note indicating summarization.
When encountering a
compactionpart type, include the compaction summary in the output with a clear marker (e.g.,[Compacted: ...]) so users understand they're seeing a summary rather than the full original conversation.Implementation: In
ConvertPart(), handletype: "compaction"by creating a Message with content prefixed by[Compacted]or similar indicator.3. Global Sessions
Decision: Ignore global sessions; only sync project-specific sessions.
SpecStory is project-centric (run from a project directory). Global sessions (stored under hash "global") don't have a clear home for generated markdown. This keeps the implementation simpler and aligned with SpecStory's model.
Implementation: In
ResolveProjectDir(), explicitly skip/ignore the "global" project hash.Estimated Scope
Implementation Summary
Files Created
Source Files (pkg/providers/opencode/):
provider.go- Main Provider interface implementation (SPI methods)types.go- OpenCode type definitions (Project, Session, Message, Part, etc.)paths.go- Path utilities and project hash computationparser.go- JSON parsing and data assemblyschema.go- Conversion to SpecStory unified schemawatcher.go- File system watching for real-time updateserrors.go- User-friendly error messages and help textTest Files (pkg/providers/opencode/):
paths_test.go- Path utilities testsparser_test.go- JSON parsing testsschema_test.go- Schema conversion testsprovider_test.go- Provider methods testsRegistry Update:
pkg/spi/factory/registry.go- Registered OpenCode providerKey Features Implemented
✅ Full SPI Provider Interface
✅ Real-time File Watching
✅ Session Data Assembly
✅ Schema Conversion
✅ Error Handling
✅ Design Decisions Implemented
Test Coverage
Quality Assurance
Each step was:
Ready for Production
The OpenCode provider is fully functional and can be used immediately: