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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

Diagnose, generate, and maintain the AI agent setup of your projects — CLAUDE.md, skills, subagents, rules, and supporting docs.

**Tech stack:** Python 3.10+, `argh` (CLI), `string.Template` (templates), `dataclasses`. Lightweight by design — no heavy deps.
**Tech stack:** Python 3.10+, `cw` (CLI), `string.Template` (templates), `dataclasses`. Lightweight by design — no heavy deps.

## Build & Test Commands

Expand All @@ -25,7 +25,7 @@ pytest --doctest-modules opsward/ # doctests embedded in the package
- `generate.py` — template rendering + file generation (never overwrites; dry-run by default)
- `maintain.py` — staleness / drift detection
- `recommend.py` — tech-stack → curated ecosystem-skill recommendations
- `cli.py` / `__main__.py` — `argh` dispatch
- `cli.py` / `__main__.py` — `cw` dispatch (surface pinned by `tests/test_cli_parity.py`)
- `base.py` — dataclasses (`ScanResult`, `DiagnosisReport`, `ComponentScore`, …)
- `data/templates/` — bundled templates (`shared/`, `python/`, `jsts/`), accessed via `importlib.resources`
- `tests/` — pytest tests + sample-project fixtures under `tests/fixtures/`
Expand Down
48 changes: 38 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ There is also an `ow` PyPI package that is a thin re-export shim for `opsward`.
## Tech Stack

- **Language:** Python 3.10+
- **CLI:** `argh` for dispatching functions to CLI commands
- **CLI:** `cw` for dispatching functions to CLI commands (MIT; reproduces argh's grammar)
- **Templates:** String-based (`string.Template` or simple f-string/jinja2-minimal) — keep deps light
- **Data structures:** `dataclasses` for models, `Mapping`/`MutableMapping` where storage is involved
- **File access:** `importlib.resources.files` for bundled templates in `opsward/data/`
Expand Down Expand Up @@ -39,7 +39,7 @@ Key docs to read before starting work:
- `generate.py` — template rendering and file generation (never overwrites without confirmation). Also provides `generate_skills()` for targeted skill/agent installation.
- `maintain.py` — staleness detection, update suggestions, drift analysis
- `recommend.py` — map detected tech-stack signals (deps, frameworks) to curated ecosystem skill recommendations
- `cli.py` — argh-based CLI entry point
- `cli.py` — CLI command functions (the `_dispatch_funcs` SSOT)
- `util.py` — internal helpers (underscore-prefixed)
- `data/` — bundled package resources (accessed via `importlib.resources.files`)
- `templates/` — generation templates organized by target project type
Expand Down Expand Up @@ -84,24 +84,52 @@ opsward install-skills --global-install --write # Install into ~/.claude/

### CLI Pattern

Follow the argh SSOT dispatch pattern:
Follow the SSOT dispatch pattern: `cli.py` owns the command list, `__main__.py`
only runs it.

```python
# In cli.py
_dispatch_funcs = [diagnose, generate, maintain, recommend, install_skills]

if __name__ == "__main__":
import argh
argh.dispatch_commands(_dispatch_funcs)
_dispatch_funcs = [diagnose, generate, maintain, recommend, install_skills, find]
```

```python
# In __main__.py
import cw
from opsward.cli import _dispatch_funcs
import argh
argh.dispatch_commands(_dispatch_funcs)

def main():
raise SystemExit(cw.dispatch(_dispatch_funcs))
```

Do not pass `prog=` — leaving it to `argparse` is what keeps `python -m opsward`
reporting `__main__.py` and the console script reporting `opsward`.

**The CLI surface is pinned.** `misc/cli_golden_py<major><minor>.json` records every
argv in `misc/cli_cases.txt` — exit code, stdout, stderr, `usage:` line and the full
`--help` body — and `tests/test_cli_parity.py` replays the one matching the running
CPython. Adding a command or a flag will fail that test. Re-record **every** golden,
each on its own interpreter:

```bash
python3.12 -m cw.testing characterize opsward --cases misc/cli_cases.txt \
-o misc/cli_golden_py312.json
```

and put the resulting diff in the PR. Never re-record to make a red test green without
first reading what changed.

Two rules for the corpus:

- **No case may print an absolute path.** The goldens are committed and have to replay
on someone else's machine, so `--format json` and `install-skills` happy paths are
deliberately excluded (both name the *resolved* project root).
- **One golden per CPython version in the CI matrix.** `argparse` is stdlib and rewrites
its own text between versions — 3.12 stopped listing `nargs='*'` positionals among
"the following arguments are required", and changed how `invalid choice` quotes the
choices. Those are the only two cases that differ between the 3.10 and 3.12
recordings. Adding a version to `[tool.wads.ci.testing]` means recording a golden for
it; the test fails loudly rather than skipping if one is missing.

### Template Pattern

Templates live in `opsward/data/templates/`. They are plain markdown files with `${variable}` placeholders (using `string.Template`). The generator reads them via `importlib.resources`, substitutes variables from the scan results, and writes to the target project.
Expand Down
61 changes: 61 additions & 0 deletions misc/cli_cases.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# opsward CLI characterization corpus. Replayed by tests/test_cli_parity.py;
# re-recording procedure is in CLAUDE.md, "CLI Pattern".
#
# Recorded from the argh-based CLI *before* the cw migration and replayed after, so a
# dispatcher swap that changes anything a shell can see fails loudly.
#
# Rules for adding a case:
# * paths are relative to the repo root (characterize/replay run with cwd=repo root);
# * no case may print an absolute path -- the golden is committed and must be portable;
# * no case may write to the filesystem or reach the network;
# * no case may need an optional extra (`find`'s toolery), so the corpus replays on a
# bare install. `find --help` still pins find's grammar at tier 2.

# -- tier 3: the whole help surface (top level + every subcommand) --
--help
diagnose --help
generate --help
maintain --help
recommend --help
install-skills --help
find --help

# -- tier 1: one happy path per subcommand (all dry-run / read-only) --
diagnose tests/fixtures/bare_project
diagnose tests/fixtures/python_project
generate tests/fixtures/bare_project
maintain tests/fixtures/stale_project
recommend tests/fixtures/python_project

# -- tier 1: option grammar. argh derives a short flag per parameter and dashes the long
# one; both spellings, the "=" form and repeated positionals are all pinned here.
diagnose tests/fixtures/bare_project --min-score 0
diagnose tests/fixtures/bare_project --min-score=0
diagnose tests/fixtures/bare_project -m 0
diagnose --verbose tests/fixtures/python_project
diagnose -v tests/fixtures/python_project
# NOTE: `--format json` is deliberately absent. Its output embeds the *resolved*
# (absolute) project root, which would put a machine path in a committed golden and
# make it unreplayable anywhere else. tests/test_cli.py covers the JSON surface.
diagnose tests/fixtures/bare_project --format json-ish-typo --min-score 0
diagnose tests/fixtures/bare_project tests/fixtures/python_project --min-score 0
generate tests/fixtures/bare_project --agents-md --hooks
generate tests/fixtures/bare_project -a
# NOTE: `install-skills` has no happy-path case for the same reason as `--format json`:
# every one of its lines names the *resolved* target directory. `install-skills --help`
# pins its grammar at tier 2; tests/ covers its behaviour.
install-skills --target
install-skills -t

# -- tier 1: usage errors. These must keep argparse's exit code 2; a dispatcher that
# starts exiting 0 on a bad command line breaks every CI step that checks it.
[]
nosuchcommand
diagnose --nosuchflag
diagnose --min-score
diagnose tests/fixtures/bare_project --min-score notanint
generate --write --nosuchflag tests/fixtures/bare_project
find

# -- tier 1: a runtime error path (opsward's own sys.exit(2)) --
diagnose ./no/such/directory/opsward-characterization
Loading
Loading