clone

a hermes agent plugin for openspec: tools for spec workflows and a dashboard tab for browsing changes and specs

Merge dev

dami etoile committed d185b4a
commit
d185b4abdb80c4e198658c2518b5bc35ab0ce3ab
parent
3cf148b1fcc8ee7ebaff307af88cc27869fc4cfd
parent
d183a3e7264db5a2c3fc0c3666ae312ab0ceefe6
author
dami etoile <dami.etoile@gmail.com>
20 files changed, +770 -15
MREADME.md4+-
M__init__.py8++-
Mdashboard/plugin_api.py20+++--
Aopenspec/changes/archive/.gitkeep0
Aopenspec/changes/archive/2026-06-22-fix-init-instructions-workflow/.openspec.yaml2+
Aopenspec/changes/archive/2026-06-22-fix-init-instructions-workflow/README.md3+
Aopenspec/changes/archive/2026-06-22-fix-init-instructions-workflow/design.md35+++++++++
Aopenspec/changes/archive/2026-06-22-fix-init-instructions-workflow/proposal.md32++++++++
Aopenspec/changes/archive/2026-06-22-fix-init-instructions-workflow/specs/agent-tools/spec.md29++++++++
Aopenspec/changes/archive/2026-06-22-fix-init-instructions-workflow/specs/dashboard-sources/spec.md24++++++
Aopenspec/changes/archive/2026-06-22-fix-init-instructions-workflow/tasks.md24++++++
Aopenspec/ideas/.gitkeep0
Aopenspec/specs/agent-tools/spec.md88+++++++++++++++++++++++
Aopenspec/specs/change-board/spec.md114++++++++++++++++++++++++++++++
Aopenspec/specs/dashboard-sources/spec.md103+++++++++++++++++++++++++++
Aopenspec/specs/source-registry/spec.md82++++++++++++++++++++++
Aopenspec/specs/spec-browser/spec.md97++++++++++++++++++++++++++
Mschemas.py2+-
Atests/test_openspec_workflow_fixes.py60++++++++++++++++
Mtools.py58+++++++++++++++-
M README.md +2 -2
@@ -31,7 +31,7 @@ This plugin closes both gaps:
3131 - Register repos by path — each gets a vanity name and stable tokens (`name/os_a1b2c3`).3232 - **Changes view** — Kanban-style board: ideas → draft → todo → in_progress → done → archived. Click any change to read its proposal, tasks, design, and spec deltas. Spec deltas render side-by-side (current vs proposed) with structured requirement/scenario parsing.3333 - **Specs view** — browse current specs in the worktree, sorted alphabetically or by last git commit date. Compare against HEAD (dirty mode) or arbitrary git refs (before/after).34-- **Source initialization** — register a repo before it has an `openspec/` directory, then initialize it from the dashboard (runs `openspec init --tools none`, or creates the directory structure as a fallback).34+- **Source initialization** — register a repo before it has an `openspec/` directory, then initialize it from the dashboard. CLI-backed and fallback initialization both normalize the plugin-supported layout: `openspec/changes/`, `openspec/changes/archive/`, `openspec/specs/`, and `openspec/ideas/`.3535 - Deep-linking via URL hash (`#project-name/token#anchor` — the second `#` selects a tab: proposal, tasks, design, or specs).3636 3737 ![Change board](screenshots/board.png)
@@ -62,7 +62,7 @@ To register a repo in the dashboard, open the OpenSpec tab and click **Add sourc
6262 openspec_context(identifier="/path/to/your/repo")6363 ```6464 65-The repo doesn't need an `openspec/` directory upfront — if it's missing, the dashboard shows an **Initialize** button that creates it. Once initialized, the dashboard shows changes and specs live from the filesystem.65+The repo doesn't need an `openspec/` directory upfront — if it's missing, the dashboard shows an **Initialize** button that creates the plugin-supported OpenSpec roots. Once initialized, the dashboard shows changes, ideas, and specs live from the filesystem.6666 6767 ## Verify6868
M __init__.py +6 -2
M dashboard/plugin_api.py +12 -8
@@ -351,6 +351,14 @@ def _source_payload(source: dict[str, Any]) -> dict[str, Any]:
351351 }352352 353353 354+def _ensure_openspec_layout(root: Path) -> None:355+ """Create every OpenSpec directory root used by this plugin."""356+ openspec_root = root / "openspec"357+ (openspec_root / "changes" / "archive").mkdir(parents=True, exist_ok=True)358+ (openspec_root / "specs").mkdir(parents=True, exist_ok=True)359+ (openspec_root / "ideas").mkdir(parents=True, exist_ok=True)360+361+354362 # ---------------------------------------------------------------------------355363 # Detail helpers356364 # ---------------------------------------------------------------------------
@@ -803,14 +811,10 @@ def init_source(source_id: str):
803811 raise HTTPException(status_code=500, detail=f"openspec init failed: {detail}")804812 except subprocess.TimeoutExpired:805813 raise HTTPException(status_code=504, detail="openspec init timed out")806- else:807- # Fallback: create the minimal directory structure.808- try:809- (root / "openspec" / "changes").mkdir(parents=True, exist_ok=True)810- (root / "openspec" / "specs").mkdir(parents=True, exist_ok=True)811- (root / "openspec" / "ideas").mkdir(parents=True, exist_ok=True)812- except OSError as exc:813- raise HTTPException(status_code=500, detail=f"Failed to create openspec/ structure: {exc}")814+ try:815+ _ensure_openspec_layout(root)816+ except OSError as exc:817+ raise HTTPException(status_code=500, detail=f"Failed to create openspec/ structure: {exc}")814818 return {"ok": True, "message": "OpenSpec initialized", "source": _source_payload(source)}815819 816820
A openspec/changes/archive/.gitkeep +0 -0

File mode 100644

A openspec/changes/archive/2026-06-22-fix-init-instructions-workflow/.openspec.yaml +2 -0
A openspec/changes/archive/2026-06-22-fix-init-instructions-workflow/README.md +3 -0
A openspec/changes/archive/2026-06-22-fix-init-instructions-workflow/design.md +35 -0
@@ -0,0 +1,35 @@
1+## Context2+3+The plugin wraps the upstream OpenSpec CLI, but the CLI currently has two behaviors that are awkward for the Hermes plugin UX:4+5+- `openspec init --tools none` does not create every directory the plugin supports (`ideas/` in particular).6+- `openspec instructions` requires at least one change even when the requested artifact guidance can be derived from schema templates.7+8+The plugin should not fork OpenSpec behavior, but it can normalize its own integration boundaries so dashboard and agent workflows are predictable.9+10+## Goals / Non-Goals11+12+**Goals:**13+14+- Make dashboard-initialized repos immediately compatible with plugin scans and normal OpenSpec list commands.15+- Keep fallback initialization and CLI-backed initialization producing the same directory layout.16+- Let agents request spec-writing guidance in a fresh repository without first creating a dummy change.17+- Preserve raw CLI command behavior wherever the CLI successfully returns useful output.18+19+**Non-Goals:**20+21+- Change the upstream OpenSpec CLI.22+- Add a new agent tool for initialization.23+- Invent a full replacement for `openspec instructions`.24+25+## Decisions26+27+- Add a local `_ensure_openspec_layout(root)` helper in the dashboard API and call it after both CLI init and fallback init.28+- Keep `.gitkeep` files out of runtime init paths; create directories only. Repo-local `.gitkeep` files are only for versioning empty directories in this plugin repository.29+- Normalize only the high-confidence artifact alias `spec -> specs`.30+- If `openspec instructions` fails because no changes exist, call `openspec templates --json`, read the requested artifact template, and return it as structured fallback stdout while preserving the original reason.31+32+## Risks / Trade-offs33+34+- Template fallback is less rich than CLI enriched instructions tied to a concrete change. The response explicitly marks `fallback: template` so callers know the difference.35+- Fallback depends on the OpenSpec CLI's `templates --json` output. If that output changes, the regression test should catch the break.
A openspec/changes/archive/2026-06-22-fix-init-instructions-workflow/proposal.md +32 -0
@@ -0,0 +1,32 @@
1+## Why2+3+Fresh OpenSpec repositories exposed three maintainer-visible workflow problems:4+5+1. Before initialization, OpenSpec list operations fail with `No OpenSpec changes directory found`; the plugin should make dashboard initialization create a complete, CLI-listable OpenSpec layout.6+2. `openspec init --tools none` creates `changes/` and `specs/` but not the plugin-supported `ideas/` directory, and may omit `changes/archive/` in fallback paths; the dashboard should normalize the layout after either CLI init or fallback init.7+3. `openspec_instructions` can fail in freshly initialized repositories because the OpenSpec CLI requires an existing change, and the tool description advertised `spec` even though the CLI artifact is `specs`; the agent tool should accept the natural alias and return useful template guidance when no change exists.8+9+## What Changes10+11+- Add layout normalization after dashboard source initialization so initialized sources always contain `openspec/changes/`, `openspec/changes/archive/`, `openspec/specs/`, and `openspec/ideas/`.12+- Add fallback behavior to `openspec_instructions` for no-change repositories by returning the package template for the requested artifact.13+- Normalize `artifact="spec"` to `artifact="specs"` for the tool and schema description.14+- Add regression tests for the initialization layout and fresh-repo instruction fallback.15+- Make the plugin package importable under pytest from the repository root so regression tests can run locally.16+17+## Capabilities18+19+### New Capabilities20+- None21+22+### Modified Capabilities23+- `dashboard-sources`: source initialization must normalize all plugin-supported OpenSpec directories.24+- `agent-tools`: instructions lookup must tolerate fresh repositories and the `spec` alias.25+26+## Impact27+28+- `dashboard/plugin_api.py`29+- `tools.py`30+- `schemas.py`31+- `__init__.py`32+- `tests/test_openspec_workflow_fixes.py`
A openspec/changes/archive/2026-06-22-fix-init-instructions-workflow/specs/agent-tools/spec.md +29 -0
@@ -0,0 +1,29 @@
1+## MODIFIED Requirements2+3+### Requirement: Artifact listing and inspection4+The plugin SHALL provide tools that map directly to OpenSpec list/show/status/instructions workflows while smoothing known CLI rough edges that would otherwise block fresh-repository agent workflows.5+6+#### Scenario: List changes or specs7+- **WHEN** an agent calls `openspec_list` with kind `changes` or `specs`8+- **THEN** the tool invokes the OpenSpec CLI JSON list command for that kind and respects supported sort options9+10+#### Scenario: Show a change or spec11+- **WHEN** an agent calls `openspec_show` with a target name and optional flags12+- **THEN** the tool invokes the OpenSpec CLI JSON show command with the requested type, delta, requirements, scenario, or requirement filters13+14+#### Scenario: Validate artifacts15+- **WHEN** an agent calls `openspec_validate`16+- **THEN** the tool invokes the OpenSpec CLI validation command with strict JSON non-interactive output and the requested target or scope17+18+#### Scenario: Retrieve implementation instructions19+- **WHEN** an agent calls `openspec_instructions`20+- **THEN** the tool returns OpenSpec's enriched instructions for the requested artifact, schema, and optional change21+22+#### Scenario: Accept singular spec artifact alias23+- **WHEN** an agent calls `openspec_instructions` with artifact `spec`24+- **THEN** the tool treats it as the OpenSpec CLI artifact `specs`25+26+#### Scenario: Fresh repository instructions fallback27+- **GIVEN** a repository has been initialized but contains no changes28+- **WHEN** an agent calls `openspec_instructions` for a known artifact29+- **THEN** the tool returns `ok: true` with template-backed guidance for that artifact instead of failing only because no change exists
A openspec/changes/archive/2026-06-22-fix-init-instructions-workflow/specs/dashboard-sources/spec.md +24 -0
@@ -0,0 +1,24 @@
1+## MODIFIED Requirements2+3+### Requirement: Source initialization4+The dashboard API SHALL initialize a registered source that exists but lacks an `openspec/` directory and SHALL normalize the resulting OpenSpec layout to include all directory roots used by the plugin.5+6+#### Scenario: OpenSpec CLI available7+- **GIVEN** the OpenSpec CLI binary is available8+- **WHEN** the dashboard initializes an uninitialized source9+- **THEN** the API runs `openspec init <repo-root> --tools none`, ensures `openspec/changes`, `openspec/changes/archive`, `openspec/specs`, and `openspec/ideas` all exist, and returns the refreshed source payload10+11+#### Scenario: OpenSpec CLI unavailable12+- **GIVEN** the OpenSpec CLI binary is unavailable13+- **WHEN** the dashboard initializes an uninitialized source14+- **THEN** the API creates `openspec/changes`, `openspec/changes/archive`, `openspec/specs`, and `openspec/ideas` and returns the refreshed source payload15+16+#### Scenario: Already initialized source17+- **GIVEN** the source already has an `openspec/` directory18+- **WHEN** the dashboard initializes the source19+- **THEN** the API returns success with an `Already initialized` message and does not modify existing artifacts20+21+#### Scenario: CLI-listable initialized source22+- **GIVEN** a source has been initialized through the dashboard API23+- **WHEN** an agent or maintainer runs OpenSpec list commands in that source24+- **THEN** the required OpenSpec directories exist so list operations are not blocked by a missing `changes` root
A openspec/changes/archive/2026-06-22-fix-init-instructions-workflow/tasks.md +24 -0
A openspec/ideas/.gitkeep +0 -0

File mode 100644

A openspec/specs/agent-tools/spec.md +88 -0
@@ -0,0 +1,88 @@
1+# Agent OpenSpec Tools2+3+## Purpose4+5+The plugin SHALL expose OpenSpec-aware Hermes tools that let agents discover, inspect, validate, and act on repo-local OpenSpec artifacts without manually assembling shell commands. Tools are expected to be safe for maintainers to use across multiple registered repositories and predictable enough for other agents to depend on.6+## Requirements7+### Requirement: Registered-source context resolution8+The plugin SHALL provide an `openspec_context` tool that resolves user-facing OpenSpec identifiers into the source repository path and relevant artifact content.9+10+#### Scenario: Resolve a bare source name11+- **GIVEN** a source is registered with a vanity name or falls back to its path basename12+- **WHEN** an agent calls `openspec_context` with that source name13+- **THEN** the tool returns the repository path, workdir hint, active changes with tokens, and current specs with tokens14+15+#### Scenario: Resolve a change token16+- **GIVEN** a registered source contains an active or archived change directory with `proposal.md`17+- **WHEN** an agent calls `openspec_context` with `<source>/<os_token>` for that change18+- **THEN** the tool returns the change name, archive status, proposal content, tasks content, design content, and each delta spec file's relative path and content19+20+#### Scenario: Resolve a spec token21+- **GIVEN** a registered source contains a spec under `openspec/specs/`22+- **WHEN** an agent calls `openspec_context` with `<source>/<os_token>` for that spec23+- **THEN** the tool returns the spec path and full spec content24+25+#### Scenario: Missing source or artifact26+- **WHEN** an agent calls `openspec_context` with an unknown source or unknown artifact token27+- **THEN** the tool returns `ok: false` and a direct error explaining what could not be resolved28+29+### Requirement: CLI-backed tool gating30+The plugin SHALL only expose CLI-backed tools when an executable OpenSpec CLI binary is available.31+32+#### Scenario: OpenSpec binary available33+- **GIVEN** `OPENSPEC_BIN`, `PATH`, or the user's `~/.npm-global/bin/openspec` points to an executable binary34+- **WHEN** Hermes registers plugin tools35+- **THEN** `openspec_list`, `openspec_show`, `openspec_validate`, `openspec_status`, and `openspec_instructions` are available36+37+#### Scenario: OpenSpec binary unavailable38+- **GIVEN** no executable OpenSpec CLI binary is found39+- **WHEN** Hermes registers plugin tools40+- **THEN** only `openspec_context` remains available because it reads the registry database and filesystem directly41+42+### Requirement: CLI command execution contract43+CLI-backed tools SHALL execute OpenSpec commands in a resolved project workdir and return structured JSON-compatible results.44+45+#### Scenario: Successful command46+- **WHEN** a CLI-backed tool runs an OpenSpec command successfully47+- **THEN** the tool returns `ok: true`, the command, workdir, exit code, parsed stdout when stdout is JSON, stderr, and truncation status48+49+#### Scenario: Failed command50+- **WHEN** a CLI-backed tool receives a non-zero OpenSpec CLI exit code51+- **THEN** the tool returns `ok: false` with the same command, workdir, stdout, stderr, exit code, and truncation status52+53+#### Scenario: Invalid workdir54+- **WHEN** a CLI-backed tool is called with a missing or non-directory workdir55+- **THEN** the tool returns `ok: false` and does not execute the OpenSpec CLI56+57+#### Scenario: Command timeout58+- **WHEN** an OpenSpec CLI command runs longer than the configured timeout59+- **THEN** the tool returns `ok: false` with the command, workdir, and timeout error60+61+### Requirement: Artifact listing and inspection62+The plugin SHALL provide tools that map directly to OpenSpec list/show/status/instructions workflows while smoothing known CLI rough edges that would otherwise block fresh-repository agent workflows.63+64+#### Scenario: List changes or specs65+- **WHEN** an agent calls `openspec_list` with kind `changes` or `specs`66+- **THEN** the tool invokes the OpenSpec CLI JSON list command for that kind and respects supported sort options67+68+#### Scenario: Show a change or spec69+- **WHEN** an agent calls `openspec_show` with a target name and optional flags70+- **THEN** the tool invokes the OpenSpec CLI JSON show command with the requested type, delta, requirements, scenario, or requirement filters71+72+#### Scenario: Validate artifacts73+- **WHEN** an agent calls `openspec_validate`74+- **THEN** the tool invokes the OpenSpec CLI validation command with strict JSON non-interactive output and the requested target or scope75+76+#### Scenario: Retrieve implementation instructions77+- **WHEN** an agent calls `openspec_instructions`78+- **THEN** the tool returns OpenSpec's enriched instructions for the requested artifact, schema, and optional change79+80+#### Scenario: Accept singular spec artifact alias81+- **WHEN** an agent calls `openspec_instructions` with artifact `spec`82+- **THEN** the tool treats it as the OpenSpec CLI artifact `specs`83+84+#### Scenario: Fresh repository instructions fallback85+- **GIVEN** a repository has been initialized but contains no changes86+- **WHEN** an agent calls `openspec_instructions` for a known artifact87+- **THEN** the tool returns `ok: true` with template-backed guidance for that artifact instead of failing only because no change exists88+
A openspec/specs/change-board/spec.md +114 -0
@@ -0,0 +1,114 @@
1+# OpenSpec Change Board2+3+## Purpose4+5+The dashboard plugin SHALL present repo-local OpenSpec work as a maintainer-facing board that surfaces ideas, active changes, archived changes, task progress, and rich artifact details. The board exists to make OpenSpec state inspectable without leaving the Hermes dashboard.6+7+## Requirements8+9+### Requirement: Board scan model10+The dashboard API SHALL scan each initialized source's `openspec/` directory into board data without persisting change or spec records in the plugin database.11+12+#### Scenario: Active change scan13+- **GIVEN** a directory exists under `openspec/changes/` with a `proposal.md`14+- **WHEN** the source is scanned15+- **THEN** the API includes it as a change with name, token, title, status, artifact presence flags, and task stats when available16+17+#### Scenario: Archived change scan18+- **GIVEN** a directory exists under `openspec/changes/archive/` with a `proposal.md`19+- **WHEN** the source is scanned20+- **THEN** the API includes it as an archived change and assigns it the archived status21+22+#### Scenario: Idea scan23+- **GIVEN** a markdown file exists under `openspec/ideas/`24+- **WHEN** the source is scanned25+- **THEN** the API includes it as an idea with name, token, title, and ideas status26+27+#### Scenario: Current spec scan28+- **GIVEN** a markdown file exists under `openspec/specs/`29+- **WHEN** the source is scanned30+- **THEN** the API includes it as a current spec with relative path, token, and title31+32+### Requirement: Change status derivation33+The dashboard API SHALL derive change board status from OpenSpec files and task completion rather than storing status manually.34+35+#### Scenario: Draft status36+- **GIVEN** a change has a proposal but no tasks file or no parseable task stats37+- **WHEN** the board scan runs38+- **THEN** the change status is `draft`39+40+#### Scenario: Todo status41+- **GIVEN** a change has tasks and zero completed checklist items42+- **WHEN** the board scan runs43+- **THEN** the change status is `todo`44+45+#### Scenario: In-progress status46+- **GIVEN** a change has both completed and incomplete checklist items47+- **WHEN** the board scan runs48+- **THEN** the change status is `in_progress`49+50+#### Scenario: Done status51+- **GIVEN** a change has tasks and every checklist item is complete52+- **WHEN** the board scan runs53+- **THEN** the change status is `done`54+55+#### Scenario: Archived status56+- **GIVEN** a change lives under `openspec/changes/archive/`57+- **WHEN** the board scan runs58+- **THEN** the change status is `archived` regardless of task completion59+60+### Requirement: Change detail API61+The dashboard API SHALL expose full detail for a selected change.62+63+#### Scenario: Existing change detail64+- **GIVEN** a selected source contains the requested change65+- **WHEN** the dashboard requests change detail66+- **THEN** the API returns the change name, title, status, archive flag, task stats, proposal markdown, design markdown, tasks markdown, and delta spec entries67+68+#### Scenario: Delta spec comparison69+- **GIVEN** a change contains delta spec files under its `specs/` directory70+- **WHEN** the API returns change detail71+- **THEN** each delta spec entry includes relative path, proposed content, current worktree spec content when present, status, and a unified diff when content changed72+73+#### Scenario: Missing change detail74+- **WHEN** the dashboard requests a change that does not exist in the selected source75+- **THEN** the API returns a not-found response76+77+### Requirement: Idea detail API78+The dashboard API SHALL expose full markdown detail for selected idea files.79+80+#### Scenario: Existing idea detail81+- **GIVEN** an idea markdown file exists under `openspec/ideas/`82+- **WHEN** the dashboard requests idea detail83+- **THEN** the API returns the idea name, title, ideas status, and markdown content84+85+#### Scenario: Missing idea detail86+- **WHEN** the dashboard requests an idea that does not exist87+- **THEN** the API returns a not-found response88+89+### Requirement: Board user interface90+The dashboard UI SHALL render changes and ideas in status-oriented columns and let maintainers inspect one selected artifact at a time.91+92+#### Scenario: Board columns93+- **WHEN** the dashboard renders an initialized source94+- **THEN** it displays ideas, draft, todo, in-progress, done, and archived groups using the source scan payload95+96+#### Scenario: Artifact selection97+- **WHEN** the user selects a change or idea card98+- **THEN** the dashboard opens a focused detail panel for that artifact rather than requiring the user to navigate away99+100+#### Scenario: Change artifact tabs101+- **GIVEN** a selected change includes proposal, tasks, design, or spec deltas102+- **WHEN** the detail panel renders103+- **THEN** the user can switch among those artifact views and see missing artifacts clearly104+105+### Requirement: Markdown and task rendering106+The dashboard UI SHALL render OpenSpec markdown and tasks in readable HTML while keeping the plugin self-contained.107+108+#### Scenario: Markdown display109+- **WHEN** proposal, design, idea, or spec markdown is displayed110+- **THEN** the UI renders common markdown elements including headings, bold text, inline code, lists, horizontal rules, and paragraphs111+112+#### Scenario: Task checklist display113+- **WHEN** a tasks document contains OpenSpec checklist items114+- **THEN** the UI displays task structure and completion status in a parsed, readable form instead of raw undifferentiated markdown
A openspec/specs/dashboard-sources/spec.md +103 -0
@@ -0,0 +1,103 @@
1+# Dashboard Source Management2+3+## Purpose4+5+The dashboard plugin SHALL let maintainers register, inspect, initialize, update, and remove OpenSpec-capable repositories from the Hermes dashboard. Source management must work even before a repository has been initialized with an `openspec/` directory.6+## Requirements7+### Requirement: Source listing API8+The dashboard API SHALL list registered sources with live validity, repository, and OpenSpec scan information.9+10+#### Scenario: Valid initialized source11+- **GIVEN** a registered source path resolves to a repository with an `openspec/` directory12+- **WHEN** the dashboard requests the source list13+- **THEN** the API returns the source as valid with repo root, OpenSpec path, scanned changes, scanned ideas, scanned specs, and status counts14+15+#### Scenario: Existing but uninitialized source16+- **GIVEN** a registered source path resolves to an existing repository without an `openspec/` directory17+- **WHEN** the dashboard requests the source list18+- **THEN** the API returns the source as invalid with the repo root and a `No openspec/ directory found` error19+20+#### Scenario: Missing path21+- **GIVEN** a registered source path no longer exists22+- **WHEN** the dashboard requests the source list23+- **THEN** the API returns the source as invalid with an explanatory error instead of dropping the source24+25+### Requirement: Source path resolution26+The dashboard API SHALL normalize user-provided paths and derive the repository root when possible.27+28+#### Scenario: Path inside a git repository29+- **GIVEN** a user submits a path inside a git repository30+- **WHEN** the API registers or updates a source31+- **THEN** it resolves the repository root for validation and payload metadata32+33+#### Scenario: Non-git directory34+- **GIVEN** a user submits an existing directory that is not inside a git repository35+- **WHEN** the API registers or updates a source36+- **THEN** it accepts that directory as the source root37+38+#### Scenario: Invalid path input39+- **WHEN** a user submits an empty path, null-byte path, unparseable path, or non-existent path40+- **THEN** the API rejects the request with a 400-level error and does not modify the registry41+42+### Requirement: Source CRUD operations43+The dashboard API SHALL expose create, update, and delete operations for registry sources.44+45+#### Scenario: Register source46+- **WHEN** the dashboard submits a valid source path and optional display name47+- **THEN** the API stores the source and returns the source payload with live validity metadata48+49+#### Scenario: Update source50+- **GIVEN** a registered source exists51+- **WHEN** the dashboard submits a new valid path or name for that source52+- **THEN** the API updates the registry and returns the refreshed source payload53+54+#### Scenario: Duplicate path conflict55+- **GIVEN** another source already uses a submitted path56+- **WHEN** the dashboard creates or updates a source with that path57+- **THEN** the API returns a conflict response and preserves the existing registry state58+59+#### Scenario: Remove source60+- **GIVEN** a registered source exists61+- **WHEN** the dashboard deletes the source62+- **THEN** the API removes it from the registry and subsequent source lists omit it63+64+### Requirement: Source initialization65+The dashboard API SHALL initialize a registered source that exists but lacks an `openspec/` directory and SHALL normalize the resulting OpenSpec layout to include all directory roots used by the plugin.66+67+#### Scenario: OpenSpec CLI available68+- **GIVEN** the OpenSpec CLI binary is available69+- **WHEN** the dashboard initializes an uninitialized source70+- **THEN** the API runs `openspec init <repo-root> --tools none`, ensures `openspec/changes`, `openspec/changes/archive`, `openspec/specs`, and `openspec/ideas` all exist, and returns the refreshed source payload71+72+#### Scenario: OpenSpec CLI unavailable73+- **GIVEN** the OpenSpec CLI binary is unavailable74+- **WHEN** the dashboard initializes an uninitialized source75+- **THEN** the API creates `openspec/changes`, `openspec/changes/archive`, `openspec/specs`, and `openspec/ideas` and returns the refreshed source payload76+77+#### Scenario: Already initialized source78+- **GIVEN** the source already has an `openspec/` directory79+- **WHEN** the dashboard initializes the source80+- **THEN** the API returns success with an `Already initialized` message and does not modify existing artifacts81+82+#### Scenario: CLI-listable initialized source83+- **GIVEN** a source has been initialized through the dashboard API84+- **WHEN** an agent or maintainer runs OpenSpec list commands in that source85+- **THEN** the required OpenSpec directories exist so list operations are not blocked by a missing `changes` root86+87+### Requirement: Dashboard source controls88+The dashboard UI SHALL present compact controls for selecting and managing registered sources.89+90+#### Scenario: Source selection91+- **GIVEN** one or more registered sources exist92+- **WHEN** the dashboard renders the OpenSpec tab93+- **THEN** the user can select the active source from a top-level control and see invalid sources labelled as invalid94+95+#### Scenario: Source actions96+- **WHEN** the user opens source management controls97+- **THEN** the user can refresh sources, add a source, edit the active source, remove the active source, and initialize an uninitialized active source when applicable98+99+#### Scenario: Empty source list100+- **GIVEN** no sources are registered101+- **WHEN** the dashboard renders the OpenSpec tab102+- **THEN** it shows an empty-state path to add a source instead of rendering a broken board103+
A openspec/specs/source-registry/spec.md +82 -0
@@ -0,0 +1,82 @@
1+# OpenSpec Source Registry2+3+## Purpose4+5+The plugin SHALL maintain a lightweight registry of repositories that the OpenSpec dashboard and agent tools can address by stable, copyable identifiers. The registry is intentionally separate from OpenSpec artifact storage: sources are stored in SQLite, while changes and specs are read live from each repository's filesystem.6+7+## Requirements8+9+### Requirement: Profile-local source storage10+The registry SHALL store OpenSpec sources in the active Hermes home so each Hermes profile has an independent source list.11+12+#### Scenario: Registry database path13+- **WHEN** the registry opens its database14+- **THEN** it uses `<hermes_home>/openspec.db` from Hermes' profile-aware home resolver15+16+#### Scenario: Database initialization17+- **WHEN** source data is first read or written18+- **THEN** the registry creates the source table and unique path index if they do not already exist19+20+### Requirement: Source record identity21+Each registered source SHALL have a stable internal token, an optional vanity name, an absolute or user-expanded path string, and a creation timestamp.22+23+#### Scenario: Add new source24+- **GIVEN** a path is not already registered25+- **WHEN** the registry adds the source26+- **THEN** it generates an `os_` token, stores the source, and returns token/id aliases, name, path, and created_at27+28+#### Scenario: Duplicate source path29+- **GIVEN** a source path is already registered30+- **WHEN** the registry attempts to add the same normalized path again31+- **THEN** it rejects the operation with a duplicate-source error32+33+#### Scenario: Update source34+- **GIVEN** an existing source token35+- **WHEN** the registry updates the source path or vanity name36+- **THEN** it preserves the token and creation timestamp while replacing the path and name37+38+#### Scenario: Remove source39+- **GIVEN** an existing source token40+- **WHEN** the registry removes the source41+- **THEN** the source no longer appears in registry listings or identifier resolution42+43+### Requirement: Human-facing source names44+The registry SHALL resolve sources by explicit vanity name first and by path basename when no vanity name is set.45+46+#### Scenario: Explicit vanity name47+- **GIVEN** a source has a non-empty `name`48+- **WHEN** an agent resolves that name case-insensitively49+- **THEN** the registry returns the matching source50+51+#### Scenario: Basename fallback52+- **GIVEN** a source has no explicit `name`53+- **WHEN** an agent resolves the basename of the source path case-insensitively54+- **THEN** the registry returns the matching source55+56+### Requirement: Deterministic artifact tokens57+The registry SHALL derive copyable `os_` artifact tokens deterministically from change names and spec paths rather than storing per-artifact rows.58+59+#### Scenario: Change token derivation60+- **WHEN** a change folder name is converted into a token61+- **THEN** the token is a stable `os_` value derived from the change name62+63+#### Scenario: Spec token derivation64+- **WHEN** a spec path is converted into a token65+- **THEN** the token is a stable `os_` value derived from the `spec:<relative-path>` key66+67+#### Scenario: Filesystem stays authoritative68+- **GIVEN** a change or spec is renamed, added, archived, or removed on disk69+- **WHEN** tools scan the repository70+- **THEN** the current filesystem state determines which artifact tokens resolve71+72+### Requirement: Legacy source migration73+The registry SHALL support one-time migration from legacy dashboard configuration sources into the SQLite registry.74+75+#### Scenario: Legacy sources exist76+- **GIVEN** the Hermes dashboard config contains legacy OpenSpec source entries77+- **WHEN** the plugin API lists registered sources for the first time in a process78+- **THEN** it attempts to migrate those sources into the registry idempotently79+80+#### Scenario: Legacy migration failure81+- **WHEN** legacy source migration raises an exception82+- **THEN** source listing continues using the SQLite registry without crashing the dashboard API
A openspec/specs/spec-browser/spec.md +97 -0
@@ -0,0 +1,97 @@
1+# Spec Browser and Diff View2+3+## Purpose4+5+The dashboard plugin SHALL provide a spec browser for current OpenSpec specs and diffs across working tree and git refs. The browser must help maintainers inspect requirements, scenarios, metadata, and changes without reading raw markdown unless they choose to.6+7+## Requirements8+9+### Requirement: Current spec browser mode10+The dashboard API SHALL return the current worktree spec set for an initialized source when no diff parameters are requested.11+12+#### Scenario: List current specs13+- **GIVEN** markdown specs exist under `openspec/specs/`14+- **WHEN** the dashboard requests the spec browser without before/after refs and without dirty mode15+- **THEN** the API returns mode `current`, current branch, each spec path, token, title, status `current`, content, and changed flag `false`16+17+#### Scenario: Current spec timestamps18+- **GIVEN** git history or filesystem metadata is available for a current spec19+- **WHEN** the API returns current spec browser entries20+- **THEN** each entry includes most-recent edit time when available and creation time when git history can determine it21+22+### Requirement: Dirty worktree diff mode23+The dashboard API SHALL compare a git ref to current worktree specs when dirty mode is requested.24+25+#### Scenario: Dirty diff defaults26+- **WHEN** dirty mode is requested without an explicit before ref27+- **THEN** the API compares `HEAD` to the working tree28+29+#### Scenario: Dirty diff filters unchanged specs30+- **GIVEN** some specs match the selected before ref exactly31+- **WHEN** dirty mode is requested32+- **THEN** unchanged specs are omitted from the returned files list33+34+#### Scenario: Dirty diff entry35+- **GIVEN** a spec was added, modified, or deleted relative to the before ref36+- **WHEN** dirty mode is requested37+- **THEN** the API returns before content, after content, status, changed flag, unified diff, and available timestamps for that spec38+39+### Requirement: Ref-to-ref diff mode40+The dashboard API SHALL compare specs between two explicit git refs when both before and after refs are supplied.41+42+#### Scenario: Valid ref comparison43+- **GIVEN** both before and after refs are supplied44+- **WHEN** the dashboard requests the spec browser45+- **THEN** the API returns mode `refs`, both labels, all spec paths present in either ref, per-spec status, before content, after content, changed flag, and unified diff when changed46+47+#### Scenario: Incomplete ref comparison48+- **WHEN** only one of before or after is supplied outside dirty mode49+- **THEN** the API rejects the request with a 400-level error explaining that both refs are required50+51+### Requirement: Safe spec path handling52+The API SHALL prevent spec detail and git content reads from escaping `openspec/specs/`.53+54+#### Scenario: Worktree spec path traversal55+- **WHEN** a spec detail request contains an absolute path or parent-directory traversal outside the specs root56+- **THEN** the API returns not found and does not read outside the specs directory57+58+#### Scenario: Ref spec path traversal59+- **WHEN** a git-ref spec read is requested for an absolute path or a path containing `..`60+- **THEN** the API returns no content and does not invoke git with an escaped specs path61+62+### Requirement: Spec detail API63+The dashboard API SHALL expose the content of a selected current spec.64+65+#### Scenario: Existing spec detail66+- **GIVEN** a requested spec path exists under `openspec/specs/`67+- **WHEN** the dashboard requests spec detail68+- **THEN** the API returns the path, title, and markdown content69+70+#### Scenario: Missing spec detail71+- **WHEN** the dashboard requests a spec path that does not exist under `openspec/specs/`72+- **THEN** the API returns a not-found response73+74+### Requirement: Structured requirement display75+The dashboard UI SHALL render OpenSpec requirements and scenarios as structured content in spec and diff views when the format is recognized.76+77+#### Scenario: Requirement section parsing78+- **GIVEN** spec content contains OpenSpec requirement headings and scenario headings79+- **WHEN** the UI renders the spec or diff view80+- **THEN** it displays requirement names, requirement text, scenario names, and scenario bullets as structured elements instead of a raw markdown block81+82+#### Scenario: Unknown markdown fallback83+- **GIVEN** spec content does not match the recognized OpenSpec requirement format84+- **WHEN** the UI renders the spec or diff view85+- **THEN** it falls back to safe markdown rendering rather than hiding content86+87+### Requirement: Copyable identifiers88+The spec browser SHALL expose copyable identifiers for sources, changes, and specs that agents can resolve later.89+90+#### Scenario: Copy spec token91+- **GIVEN** a spec browser entry has a source name and spec token92+- **WHEN** the user copies the identifier chip93+- **THEN** the copied text is `<source-name>/<os_token>` and can be resolved by `openspec_context`94+95+#### Scenario: Copy feedback96+- **WHEN** a user copies an identifier chip97+- **THEN** the UI gives brief visible feedback that the value was copied
M schemas.py +1 -1
A tests/test_openspec_workflow_fixes.py +60 -0
@@ -0,0 +1,60 @@
1+import json2+import os3+import shutil4+import subprocess5+from pathlib import Path6+7+import pytest8+9+import tools10+from dashboard import plugin_api11+12+13+def test_openspec_instructions_accepts_spec_alias_without_changes(tmp_path):14+ exe = tools._openspec_bin()15+ if not exe:16+ pytest.skip("openspec CLI is not available")17+18+ subprocess.run([exe, "init", "--tools", "none", str(tmp_path)], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)19+20+ result = json.loads(tools.openspec_instructions({"workdir": str(tmp_path), "artifact": "spec"}))21+22+ assert result["ok"] is True23+ assert result["stdout"]["artifact"] == "specs"24+ assert result["stdout"]["fallback"] == "template"25+ assert "## ADDED Requirements" in result["stdout"]["content"]26+ assert "No changes found" in result["stdout"]["reason"]27+28+29+def test_dashboard_init_layout_ensures_plugin_supported_directories(tmp_path):30+ openspec_root = tmp_path / "openspec"31+ (openspec_root / "changes").mkdir(parents=True)32+ (openspec_root / "specs").mkdir()33+34+ plugin_api._ensure_openspec_layout(tmp_path)35+36+ assert (openspec_root / "changes").is_dir()37+ assert (openspec_root / "changes" / "archive").is_dir()38+ assert (openspec_root / "specs").is_dir()39+ assert (openspec_root / "ideas").is_dir()40+41+42+def test_dashboard_init_fallback_layout_matches_supported_scan_roots(tmp_path, monkeypatch):43+ monkeypatch.setattr(plugin_api, "_find_openspec_bin", lambda: None)44+ source = {"token": "os_test", "id": "os_test", "name": "demo", "path": str(tmp_path), "created_at": 0}45+46+ class FakeRegistry:47+ def get_source(self, source_id):48+ return source if source_id == "os_test" else None49+50+ monkeypatch.setattr(plugin_api, "_registry", FakeRegistry())51+52+ response = plugin_api.init_source("os_test")53+54+ assert response["ok"] is True55+ openspec_root = tmp_path / "openspec"56+ assert (openspec_root / "changes").is_dir()57+ assert (openspec_root / "changes" / "archive").is_dir()58+ assert (openspec_root / "specs").is_dir()59+ assert (openspec_root / "ideas").is_dir()60+ assert response["source"]["valid"] is True
M tools.py +56 -2
@@ -12,6 +12,8 @@ from typing import Any
1212 1313 MAX_OUTPUT_CHARS = 50_0001414 15+_ARTIFACT_ALIASES = {"spec": "specs"}16+1517 1618 def _openspec_bin() -> str | None:1719 configured = os.getenv("OPENSPEC_BIN", "").strip()
@@ -48,6 +50,11 @@ def _json_or_text(text: str) -> Any:
4850 return stripped4951 5052 53+def _normalize_artifact(value: str) -> str:54+ artifact = (value or "").strip()55+ return _ARTIFACT_ALIASES.get(artifact, artifact)56+57+5158 def _run(args: list[str], workdir: Any = None) -> str:5259 exe = _openspec_bin()5360 if not exe:
@@ -104,6 +111,46 @@ def _run(args: list[str], workdir: Any = None) -> str:
104111 })105112 106113 114+def _template_instruction_fallback(artifact: str, reason: str, workdir: Any = None, schema: str = "") -> str | None:115+ """Return template-backed instructions when OpenSpec needs a change first."""116+ if "No changes found" not in reason:117+ return None118+ artifact = _normalize_artifact(artifact)119+ if not artifact:120+ return None121+122+ templates_args = ["templates", "--json"]123+ if schema:124+ templates_args.extend(["--schema", schema])125+ templates_result = json.loads(_run(templates_args, workdir))126+ if not templates_result.get("ok") or not isinstance(templates_result.get("stdout"), dict):127+ return None128+129+ entry = templates_result["stdout"].get(artifact)130+ if not isinstance(entry, dict):131+ return None132+ path = Path(str(entry.get("path") or "")).expanduser()133+ if not path.is_file():134+ return None135+ try:136+ content = path.read_text(encoding="utf-8", errors="replace")137+ except OSError:138+ return None139+140+ fallback = dict(templates_result)141+ fallback["ok"] = True142+ fallback["stdout"] = {143+ "artifact": artifact,144+ "fallback": "template",145+ "reason": reason,146+ "templatePath": str(path),147+ "source": entry.get("source"),148+ "content": content,149+ }150+ fallback["stderr"] = ""151+ return json.dumps(fallback)152+153+107154 def _registry_module():108155 """Import the plugin-local OpenSpec registry (``plugins.openspec.registry``).109156
@@ -338,7 +385,7 @@ def openspec_status(args: dict, **kwargs) -> str:
338385 339386 def openspec_instructions(args: dict, **kwargs) -> str:340387 cmd = ["instructions", "--json"]341- artifact = str(args.get("artifact") or "").strip()388+ artifact = _normalize_artifact(str(args.get("artifact") or ""))342389 if artifact:343390 cmd.append(artifact)344391 change = str(args.get("change") or "").strip()
@@ -347,4 +394,11 @@ def openspec_instructions(args: dict, **kwargs) -> str:
347394 schema = str(args.get("schema") or "").strip()348395 if schema:349396 cmd.extend(["--schema", schema])350- return _run(cmd, args.get("workdir"))397+398+ result = _run(cmd, args.get("workdir"))399+ parsed = json.loads(result)400+ if parsed.get("ok"):401+ return result402+ reason = "\n".join(str(parsed.get(key) or "") for key in ("stderr", "stdout", "error"))403+ fallback = _template_instruction_fallback(artifact, reason, args.get("workdir"), schema)404+ return fallback or result