Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .claude/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Architecture — flowspec-cli

## Overview
flowspec-cli is a Python CLI package. The entry point is `src/flowspec_cli/__init__.py` — a Typer app that wires together all subcommands. New features go in dedicated submodules and register at the bottom of `__init__.py`.

## Module Structure
```
src/flowspec_cli/
├── __init__.py # Main Typer app, COPILOT_AGENT_TEMPLATES, all top-level commands
│ # 10,541 lines — do not add more; extract instead (see issue #1226)
├── doctor/ # Health-check command (TASK-606, in progress)
├── workflow/ # State machine: assessor, validator, transition, config, state_guard
├── security/ # SAST tools, MCP security server
├── memory/ # Task memory CLI (backlog/memory/)
├── hooks/ # Claude Code hook system: events, schema, cli
├── telemetry/ # Telemetry events, config, CLI
├── backlog/ # Backlog shim and integration
├── satellite/ # Migration and audit utilities
├── skills/ # Skill scaffold system
├── quality/ # Quality assessors, scorer, config
├── vscode/ # VS Code settings generator
├── logging/ # Structured logging utilities
├── deprecated.py # Deprecated-file cleanup for upgrade-repo
├── placeholders.py # Template placeholder substitution
├── task_context.py # Task context extraction
├── validation_agents.py # Validation agent orchestration
└── templates/ # Files deployed to user repos (agents, commands, hooks)
```

## Adding a New Command
1. Create `src/flowspec_cli/<name>/` with `__init__.py`, `checks.py`/core logic, `cli.py`
2. Register in `__init__.py` near the bottom:
- Simple command: `@app.command(name="<name>")` wrapping `from flowspec_cli.<name>.cli import run_<name>`
- Sub-app: `from flowspec_cli.<name>.cli import <name>_app; app.add_typer(<name>_app, name="<name>")`
3. Write tests in `tests/test_<name>.py`

## Existing Utilities (reuse, don't reinvent)
All in `src/flowspec_cli/__init__.py`:
- `check_backlog_installed_version()` — `backlog --version` via subprocess
- `check_beads_installed_version()` — `bd --version` via subprocess
- `get_github_latest_release(owner, repo)` — GitHub API, returns version string or None
- `get_npm_latest_version(package)` — npm registry, returns version string or None
- `compare_semver(a, b)` — returns -1/0/1
- `show_banner()` — prints the flowspec banner
- `console` — shared `rich.Console` singleton
- `COPILOT_AGENT_TEMPLATES` — embedded agent file content dict
- `CONSTITUTION_TEMPLATES` — embedded constitution template dict
- `SOURCE_REPO_MARKER` — sentinel file name for source repo detection

## Key Boundaries
- **No circular imports**: submodules import from `__init__.py` only for constants/utilities; `__init__.py` imports submodules only at the bottom (lazy, inside command wrappers, or via `app.add_typer`)
- **No global state mutation**: the module-level `client` (httpx) and `console` (Rich) are the only singletons
- **Template files are king**: `templates/` and `src/flowspec_cli/templates/` must stay in sync; `COPILOT_AGENT_TEMPLATES` is the embedded fallback

## Anti-Patterns
- Do not add more top-level code to `__init__.py` — extract to a submodule
- Do not mock subprocess in tests when a real temp file works — use `tmp_path`
- Do not use `os.path` — always `pathlib.Path`
- Do not hardcode version strings outside `__version__` and `get_backlog_validated_version()`
32 changes: 32 additions & 0 deletions .claude/history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# History — Decision and Reference Logging

## Decision Log Format
Log non-trivial decisions to `.logs/decisions/<topic>.jsonl`:

```json
{
"timestamp": "2026-05-23T10:00:00Z",
"task": "TASK-606",
"phase": "implementation",
"decision": "Use dataclass CheckResult instead of TypedDict",
"rationale": "dataclass gives free __repr__ and supports default values; CheckResult is instantiated 8x per run",
"actor": "@claude",
"alternatives_considered": ["TypedDict", "NamedTuple"],
"references": [".claude/plans/task-606-flowspec-doctor.md"]
}
```

## When to Log
- Architecture choices (why a submodule over inline code)
- Trade-offs with meaningful alternatives (why not X)
- Rejected approaches and the reason
- Discovered constraints that changed the plan

## When NOT to Log
- Obvious implementation steps
- Minor style choices covered by ruff
- Things that are self-evident from the code

## Task Notes vs Decision Log
- **backlog task notes** (`backlog task edit <id> --append-notes`) — PR-ready summary, what was built
- **decision log** (`.logs/decisions/`) — architectural/trade-off reasoning for future agents
68 changes: 68 additions & 0 deletions .claude/language.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Language — Python

## Version
Python 3.11+ (enforced by `pyproject.toml`)

## Formatter
`ruff format` — line length 88, configured in `pyproject.toml`

```bash
uv run ruff format . # format in place
uv run ruff format --check . # CI check (no changes)
```

## Linter
`ruff check` — replaces flake8, isort, pyflakes

```bash
uv run ruff check . # lint
uv run ruff check . --fix # auto-fix safe issues
```

## Type Hints
- Required on all public API functions and class methods
- Not enforced with a type checker (no pyright/mypy in CI)
- Use `from __future__ import annotations` when needed for forward refs

## Test Framework
**pytest** — run with:
```bash
uv run pytest tests/ -x -q # fail-fast, quiet
uv run pytest tests/ -x -q -k foo # filter by name
```

Current stats: ~3473 tests, ~37s runtime

### Test Patterns
- File: `tests/test_<module>.py`
- Classes: `class TestFeatureName:` — group related tests
- Methods: `def test_<behaviour>(self) -> None:` — explicit return type
- Pattern: Arrange → Act → Assert
- Fixtures: `tmp_path` for file isolation, `monkeypatch` for subprocess/env mocking
- Never use relative paths — always `Path(__file__).resolve().parent.parent` for project root

### Coverage Target
>80% on core functionality (not enforced in CI, but expected)

### What NOT to mock
- Real YAML parsing — use `tmp_path` fixtures with actual files
- Real file I/O — prefer real files over StringIO

## Naming Conventions
- Never shadow Python builtins: no `type`, `list`, `dict`, `input`, `filter`, `map`, `hash`
- `id` acceptable in public APIs where required
- Never shadow imported modules: if `import html` exists, don't do `html = generate_html()`

## File I/O
Always specify encoding:
```python
path.read_text(encoding="utf-8")
path.write_text(content, encoding="utf-8")
with open(path, encoding="utf-8") as f: ...
```

## Imports
All imports at module level (top of file). Never inline inside functions.

## Comments
Default: none. Add only when the WHY is non-obvious — a hidden constraint, subtle invariant, or known workaround.
175 changes: 175 additions & 0 deletions .claude/plans/task-606-flowspec-doctor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Plan: `flowspec doctor` — Setup Health Check and Diagnostics

**Task:** TASK-606
**Branch:** `galway/task-606/flowspec-doctor`
**Issue:** [#1221](https://github.com/jpoley/flowspec/issues/1221)

---

## Goal

Add `flowspec doctor` CLI command that checks the environment and surfaces setup problems before they cause confusing failures.

```
flowspec doctor # run all checks
flowspec doctor --fix # attempt auto-fix where possible
```

Example output:
```
✅ Python 3.12.2
✅ flowspec v0.4.008 (up to date)
✅ backlog.md v1.21.0
✅ beads v0.29.0
✅ flowspec_workflow.yml present and valid
⚠️ Agent files using old hyphen naming — run: flowspec upgrade-repo
❌ Constitution not found — run: /flow:init
```

---

## Files to Create

```
src/flowspec_cli/doctor/
├── __init__.py — exports run_doctor()
├── checks.py — CheckResult dataclass + 8 check functions
└── cli.py — Typer command + run_doctor()
tests/test_doctor.py — ~15 unit tests (pass/fail/warn per check)
```

## Files to Modify

```
src/flowspec_cli/__init__.py — add thin @app.command("doctor") wrapper (~10 lines)
```

---

## The 8 Checks

| # | Check | Pass | Warn | Fail |
|---|-------|------|------|------|
| 1 | Python version | ≥ 3.11 | — | < 3.11 |
| 2 | flowspec version | current = latest GitHub release | behind latest | cannot fetch |
| 3 | backlog.md | `backlog --version` succeeds | — | not found |
| 4 | beads | `bd --version` succeeds | — | not found |
| 5 | `flowspec_workflow.yml` | exists + valid YAML | — | missing / parse error |
| 6 | Agent naming convention | no `flow-*.agent.md` in `.github/agents/` | old files found | — |
| 7 | Constitution | `memory/constitution.md` exists | missing | — |
| 8 | `.flowspec/` directory | exists | missing | — |

---

## `checks.py` Design

```python
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

class CheckStatus(Enum):
PASS = "pass"
WARN = "warn"
FAIL = "fail"

@dataclass
class CheckResult:
name: str
status: CheckStatus
message: str
fix_cmd: str | None = None # shown in --fix or as hint

def check_python_version() -> CheckResult: ...
def check_flowspec_version(current: str) -> CheckResult: ...
def check_backlog_installed() -> CheckResult: ...
def check_beads_installed() -> CheckResult: ...
def check_workflow_config(project_path: Path) -> CheckResult: ...
def check_agent_naming(project_path: Path) -> CheckResult: ...
def check_constitution(project_path: Path) -> CheckResult: ...
def check_flowspec_dir(project_path: Path) -> CheckResult: ...

def run_all_checks(project_path: Path, current_version: str) -> list[CheckResult]: ...
```

## `cli.py` Design

```python
def run_doctor(project_path: Path, fix: bool = False) -> None:
results = run_all_checks(project_path, current_version=__version__)
# print rich table with ✅/⚠️/❌
# if fix=True: attempt fixes for warn/fail items
```

## `--fix` Behavior

| Check | Fix Action |
|-------|-----------|
| Agent naming (warn) | `subprocess.run(["flowspec", "upgrade-repo"])` |
| Constitution (warn) | Write minimal constitution template to `memory/constitution.md` |
| Others | Print the fix command, do not auto-run |

## `__init__.py` Addition

Near line 9179 (after `app.add_typer(telemetry_app, ...)`):

```python
@app.command(name="doctor")
def doctor_cmd(
fix: bool = typer.Option(False, "--fix", help="Attempt auto-fix where possible"),
):
"""Check flowspec setup health and diagnose configuration issues."""
from flowspec_cli.doctor.cli import run_doctor
run_doctor(project_path=Path.cwd(), fix=fix)
```

---

## Tests (`tests/test_doctor.py`)

```python
class TestCheckPythonVersion:
def test_pass_current_version(self): ... # sys.version_info >= (3, 11)
def test_fail_old_version(self, monkeypatch): ... # mock sys.version_info = (3, 10)

class TestCheckBacklogInstalled:
def test_pass_when_installed(self, monkeypatch): ... # mock subprocess returning version
def test_fail_when_not_found(self, monkeypatch): ... # mock FileNotFoundError

class TestCheckBeadsInstalled:
def test_pass_when_installed(self, monkeypatch): ...
def test_fail_when_not_found(self, monkeypatch): ...

class TestCheckWorkflowConfig:
def test_pass_valid_yml(self, tmp_path): ...
def test_fail_missing(self, tmp_path): ...
def test_fail_invalid_yaml(self, tmp_path): ...

class TestCheckAgentNaming:
def test_pass_no_hyphenated_files(self, tmp_path): ...
def test_warn_hyphenated_files_present(self, tmp_path): ...
def test_pass_no_agents_dir(self, tmp_path): ... # no .github/agents/ = no issue

class TestCheckConstitution:
def test_pass_constitution_exists(self, tmp_path): ...
def test_warn_constitution_missing(self, tmp_path): ...

class TestCheckFlowspecDir:
def test_pass_dir_exists(self, tmp_path): ...
def test_warn_dir_missing(self, tmp_path): ...

class TestRunAllChecks:
def test_returns_eight_checks(self, tmp_path, monkeypatch): ...
```

---

## Definition of Done

- [ ] `uv run ruff check .` passes
- [ ] `uv run ruff format --check .` passes
- [ ] `uv run pytest tests/ -x -q` passes (including new doctor tests)
- [ ] `flowspec doctor` runs and prints formatted output
- [ ] `flowspec doctor --fix` runs without error
- [ ] All 5 ACs in TASK-606 checked
- [ ] PR created, closes #1221
4 changes: 1 addition & 3 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
"Delete(./.env)",
"Delete(./.env.*)",
"Delete(./secrets/**)",
"Write(./CLAUDE.md)",
"Edit(./CLAUDE.md)",
"Delete(./CLAUDE.md)",

"Write(./memory/constitution.md)",
"Edit(./memory/constitution.md)",
"Delete(./memory/constitution.md)",
Expand Down
50 changes: 50 additions & 0 deletions .claude/sourcecontrol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Source Control — flowspec-cli

## Host
github.com/jpoley/flowspec (private)

## Branch Naming
```
{hostname}/task-{id}/{slug}
```
```bash
HOSTNAME=$(hostname -s | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
git checkout -b "${HOSTNAME}/task-606/flowspec-doctor"
```

Current branch: `galway/task-606/flowspec-doctor`

## Commits
All commits require DCO sign-off:
```bash
git commit -s -m "feat: add flowspec doctor command"
```

Conventional prefix:
- `feat:` new feature
- `fix:` bug fix
- `docs:` documentation only
- `refactor:` no behaviour change
- `test:` test-only change
- `chore:` maintenance

## Pre-Commit Gates (run before every commit)
```bash
uv run ruff format --check . # must pass
uv run ruff check . # must pass, zero errors
uv run pytest tests/ -x -q # must pass, zero failures
```

## PRs
- Target branch: `main`
- Title: ≤ 70 chars, conventional prefix, references issue (e.g., `feat: add flowspec doctor command (closes #1221)`)
- Body template: `.github/PULL_REQUEST_TEMPLATE.md`
- **Never merge directly to `main`** — operator merges after CI passes
- **Never update a PR** — close and reopen (Copilot won't re-review amended PRs)
- CI must be green before requesting review

## Never
- `git push --force` to `main`
- `git reset --hard` without operator approval
- Skip `--no-verify` or DCO
- Merge your own PR
Loading
Loading