Local dashboard for the attune-* documentation family
(attune-rag, attune-help, attune-author). Server-rendered Jinja2
UI ("Cowork dashboard") backed by a FastAPI sidecar — ships clean via
PyPI with no npm step required to run.
Sidebar nav with seven pages, each consuming the existing JSON API:
| Page | What it shows |
|---|---|
| Health | Cross-layer health (rag/help/author/gui versions) + corpus snapshot |
| Templates | Markdown templates with mtime staleness, tags, and a manual-pin toggle |
| Specs | Feature specs in specs/ with phase + status badges. + New spec bootstraps from TEMPLATE.md; + Design / + Tasks inline; status dropdown in Preview |
| Summaries | Inline-editable summaries.json with overwrite warning |
| Living Docs | Workspace editor, scan trigger, document health, review queue, RAG quality bars |
| Commands | Run any registered command from a card grid (RAG queries, regen, maintain, …) |
| Jobs | Job history with per-feature progress, last-output column, Cancel button, auto-refresh |
Click any spec or template to open the Preview / Edit panel — server-side
Markdown rendering plus a raw <textarea> for editing.
A first-class CodeMirror 6 editor for attune-help-style markdown templates.
Triggered by attune-author edit <path> or by navigating to
/editor?corpus=<id>&path=<rel>. Reads from / writes back to any registered
corpus on disk, with all retrieval/lint/refactor smarts coming from
attune-rag's editor toolkit.
Developer features:
| Feature | What it does |
|---|---|
| Schema-driven frontmatter form | Reads attune_rag.editor.load_schema() at request time and renders typed inputs (select / chip-input / textarea / text). Unknown frontmatter keys are preserved verbatim. Raw-YAML toggle round-trips byte-for-byte. |
| CodeMirror 6 + Lezer extension | Composes the standard @codemirror/lang-markdown with a custom Lezer extension for YAML frontmatter delimiters, ## Depth N markers, and [[alias]] refs (excluding fenced code, supporting \[[ escape). |
| Server-side lint | 300 ms debounced POST to /api/corpus/<id>/lint. Diagnostics paint as squiggles in the editor + a clickable strip at the bottom. Local fast-path skips the round-trip for YAML parse errors. |
| Tag + alias autocomplete | Context-aware completions inside tags: / aliases: frontmatter fields and [[…]] body refs. Per-prefix LRU cache invalidates on WS file-change events. |
| Per-hunk save modal | /api/corpus/<id>/template/diff returns stable hunk ids; the modal shows each hunk with a checkbox and runs a projected-state lint so a partial save can't write known-broken frontmatter. Atomic save via /template/save (409 on base_hash mismatch → conflict mode). |
| 3-way merge conflict mode | A WebSocket at /ws/corpus/<id>?path=<rel> pushes file_changed events from watchfiles. On conflict (or 409), a banner offers Reload / Keep / Resolve. Resolve uses node-diff3 for per-region accept-disk / accept-editor / keep-both. |
| Cross-corpus rename refactor | Right-click any tag/alias chip → "Rename …". /api/corpus/<id>/refactor/rename/preview returns a multi-file diff; apply is atomic across files (per-file tempfile + sequential rename + drift-detection rollback). 409 with owning_path on alias collisions. |
| Corpus switcher | Top-bar dropdown lists registered corpora (~/.attune/corpora.json). Search input materializes above 10 corpora. "+ Add corpus…" registers a new root via /api/corpus/register. Switching with unsaved edits prompts Save / Discard / Cancel. |
| Generated-corpus advisory | Persistent, non-dismissible banner when the active corpus has kind: "generated" — flags that edits will be overwritten on the next attune-author maintain. |
| Read-only on duplicate session | A second tab opening the same (corpus, path) receives a duplicate_session WS message and goes read-only with a banner — first tab keeps full control. |
| Keyboard shortcuts | ⌘/Ctrl-S opens the save modal. beforeunload warns on unsaved edits. |
| Pre-bundled, no Node at install | editor-frontend/ is Vite + TypeScript; make build-editor produces a hashed-filename bundle into sidecar/attune_gui/static/editor/ that's checked into the repo. PyPI consumers don't need npm. |
# In one shell — sidecar with auto-reload:
uv run attune-gui --port 8765 --reload
# In another — vitest watch (92 unit tests, ~2s):
cd editor-frontend && npm run test --watch
# Rebuild the bundle (deterministic; output committed):
make build-editor
# Run the Playwright e2e suite against a fresh sidecar
# (auto-spawned by playwright.config.ts):
cd editor-frontend && npm run e2eThe editor bundle is ~210 KB gzipped (budget: 600 KB). Schema and Lezer
grammar parse fixtures live in editor-frontend/src/grammar/; merge
correctness in three-way-merge.test.ts. The Playwright suite under
editor-frontend/e2e/ exercises the end-to-end flows (open→edit→save,
conflict resolve via WS-pushed file_changed, rename refactor
preview+apply, corpus switcher with unsaved-edits guard, save-modal
controls, keyboard shortcuts + advisories) — it spins up a real
sidecar with an isolated ATTUNE_CORPORA_REGISTRY so your dev
registry isn't touched.
| Method | Path | Purpose |
|---|---|---|
| GET | /editor?corpus=<id>&path=<rel> |
Editor shell (Jinja) |
| GET | /api/editor/template-schema |
Frontmatter JSON Schema |
| GET | /api/corpus |
List registered corpora + active id |
| POST | /api/corpus/active |
Switch active corpus |
| POST | /api/corpus/register |
Add a new corpus root |
| POST | /api/corpus/resolve |
Map an absolute path → (corpus_id, rel_path) |
| GET | /api/corpus/<id>/template?path=<rel> |
Read template + base hash + mtime |
| POST | /api/corpus/<id>/template/diff |
Compute unified-diff hunks vs disk |
| POST | /api/corpus/<id>/template/save |
Atomic save (base_hash 409-guarded) |
| POST | /api/corpus/<id>/lint |
Lint a template body |
| GET | /api/corpus/<id>/autocomplete?kind=tag|alias&prefix=… |
Autocomplete |
| POST | /api/corpus/<id>/refactor/rename/preview |
Multi-file rename plan |
| POST | /api/corpus/<id>/refactor/rename/apply |
Atomic rename across files |
| WS | /ws/corpus/<id>?path=<rel> |
file_changed + duplicate_session push |
Looking for AI dev workflows (code review, security audits, refactor planning, multi-agent orchestration)? Those live in
attune-ai— a separate product. attune-gui is deliberately scoped to the documentation lifecycle.
pip install attune-gui
attune-gui
# Or pick a specific port:
attune-gui --port 8765The sidecar binds to 127.0.0.1, prints SIDECAR_URL=…, and serves the
new dashboard at /. Use --open to auto-open your browser.
The sidecar loads KEY=value lines from the first .env it finds, in this order:
./.env(current working directory)<repo-root>/.env(the attune-gui checkout root)~/.attune-gui/.env~/.attune/.env
Existing real env values are preserved; empty/whitespace-only values are treated as unset and overwritten. Common keys:
ANTHROPIC_API_KEY=sk-ant-… # required for author.regen / author.maintain
ATTUNE_SPECS_ROOT=/path/to/your/repo/specs
ATTUNE_WORKSPACE=/path/to/your/project
A single typed config file holds the keys below. Each key has a matching environment variable that overrides the file at runtime — useful for CI or one-off runs. Precedence: env > file > default.
| Key | Env var | Purpose |
|---|---|---|
workspace |
ATTUNE_WORKSPACE |
Project the sidecar watches (Living Docs, templates) |
corpora_registry |
ATTUNE_CORPORA_REGISTRY |
Path to corpora registry (default: ~/.attune/corpora.json) |
specs_root |
ATTUNE_SPECS_ROOT |
Where the Specs page reads from (default: <workspace>/specs/, then walks up from cwd) |
Manage the file from the CLI — no hand-editing JSON:
attune-gui config list # show resolved values + source
attune-gui config get workspace # print one value
attune-gui config set workspace /path/to/project # persist a value
attune-gui config unset specs_root # remove a keyconfig set workspace validates that the path is a real directory; the
others trust you. Workspace can also be set via Living Docs →
Workspace in the UI — both surfaces write to the same file.
git clone https://github.com/Smart-AI-Memory/attune-gui
cd attune-gui
uv sync
uv run attune-gui --port 8765 --reloadTemplates auto-reload — edit anything under sidecar/attune_gui/templates/
and refresh the browser. Python code changes reload automatically with
--reload.
uv run pytest # 124 tests, ~2s
uv run ruff check . # lintIf PR B was opened on top of PR A's branch and A has since merged to main, B will show conflicts against main even though your own commits are clean. The conflicts are A's commits, now reachable from main via the merge, replaying against themselves.
Fix it by replaying only your commits onto main with
git rebase --onto main <old-base-sha> <branch>. <old-base-sha> is
the tip of A's branch at the moment you forked B — find it with
git merge-base <branch> <A-branch> while A's branch still exists
locally, or read it from the PR's "compare" base on github.com before
the branch is deleted.
# B = feature/b, was stacked on feature/a (now merged + deleted)
git fetch origin
old_base=$(git merge-base feature/b origin/main~1) # or paste the SHA
git rebase --onto origin/main "$old_base" feature/bThen force-push with lease — never plain --force:
git push --force-with-lease.
┌──────────────────────────────────────┐
│ Cowork dashboard (Jinja2) / │
└──────────────────┬───────────────────┘
│ /api/*
┌──────────────────▼───────────────────┐
│ FastAPI sidecar — 127.0.0.1 │
│ ├─ routes/system, rag, help, … │
│ ├─ routes/cowork_health │
│ ├─ routes/cowork_specs │
│ ├─ routes/cowork_templates │
│ ├─ routes/cowork_files │
│ └─ routes/cowork_pages (HTML) │
└──────────────────┬───────────────────┘
│
┌──────────────────▼───────────────────┐
│ attune-rag · attune-help │
│ attune-author[ai] │
└──────────────────────────────────────┘
Two rendering surfaces, one rule:
- Template editor at
/editoris a Vite + TypeScript SPA (CodeMirror 6, WebSocket conflict mode, per-hunk diff). Source ineditor-frontend/, pre-bundled intosidecar/attune_gui/static/editor/and committed so PyPI consumers do not need Node. - Everything else (Health, Templates, Specs, Summaries, Living Docs, Commands, Jobs) is server-rendered Jinja2 with light vanilla-JS for inline actions and polling.
New UI defaults to Jinja. Reach for the SPA only when the surface needs editor-grade interactivity — rich text editing, real-time conflict resolution, multi-file refactor previews. Inline actions, polling, and form-driven dashboards stay server-rendered.
attune-gui ships an MCP server (attune-gui-mcp) that exposes a small,
read-mostly tool surface for Claude Code and other MCP clients. Same
data the dashboard renders — specs and living docs — but addressable
programmatically. mcp>=0.9.0 is a core dependency, so pip install attune-gui is enough to get both the dashboard and the MCP server.
| Tool | What it returns |
|---|---|
gui_list_specs |
All feature specs across configured roots (federated). Each tagged with project, root, phase, status. |
gui_get_spec |
All phase-file contents (requirements.md / design.md / tasks.md) for one spec. |
gui_get_spec_status |
The **Status**: value for one phase. Default: most-advanced phase. |
gui_list_living_docs |
Doc registry — generated docs the workspace tracks, with status (current/stale/missing) and persona. |
gui_get_living_doc |
One living-doc's file content. Path-traversal guarded. |
Every tool returns a JSON envelope shaped {"success": bool, ...}.
See docs/specs/mcp-server-scope/ for
the scope decisions and tool schemas.
Add to your project's .mcp.json (or ~/.claude/mcp.json for global):
{
"mcpServers": {
"attune-gui": {
"command": "attune-gui-mcp"
}
}
}The server boots on stdio and logs to
<tmpdir>/attune-gui/attune-gui-mcp.log so the MCP transport stays
clean. Restart Claude Code after editing the config; the five tools
appear under the attune-gui server in /mcp.
attune-ai's ops-specs-features spec is the complement to this one: that work brings attune-gui's spec views into the attune-ai ops dashboard for humans; this MCP server exposes the same views to agents. Both serve the spec-driven workflow, just for different clients.
This is a single-user, local-only app. Not designed for multi-user deployment, not hardened against a motivated attacker on the same machine.
- Binds only to
127.0.0.1— not reachable from other machines - An
Originheader guard rejects browser requests from non-localhost origins - Mutating endpoints require the
X-Attune-Clientheader to match a per-process token (served from/api/session/token) - File API enforces a path-traversal guard against three named roots
(
templates,specs,summaries); writes outside those roots return 400
attune-rag— RAG pipelineattune-help— help runtimeattune-author— doc authoringattune-gui-plugin— Claude Code plugin that launches the dashboard inside Cowork's preview paneattune-ai— separate AI dev workflow product (not used by attune-gui)
Apache-2.0