From abb31a48f02ee16fe546305462e129a8271127d7 Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:52:58 +0900 Subject: [PATCH 1/7] docs(claude): sync harness docs with the code - kong-cli rule: the "switch on the command string" pattern comes from Kong's own README, not this repository's - kong-cli rule: name VersionCmd.Run as the existing exception to the "no method bodies in cmd/cli.go" rule - upgrade-tools skill: e2e.yml has one bun-version per job, update all - CLAUDE.md: add vhs to the mise tools and the demo / demo-validate tasks to the command list; only commands with cross-field constraints have Validate() Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RKh8XqY1bD6M11zbzS4gVD --- .claude/rules/kong-cli.md | 4 ++-- .claude/skills/upgrade-tools/SKILL.md | 2 +- CLAUDE.md | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.claude/rules/kong-cli.md b/.claude/rules/kong-cli.md index 322486a..547ae98 100644 --- a/.claude/rules/kong-cli.md +++ b/.claude/rules/kong-cli.md @@ -11,7 +11,7 @@ This project uses Kong (`github.com/alecthomas/kong`) for the CLI surface. The p ## Dispatch with `Run(...) error` - Every subcommand struct must implement a `Run(...) error` method. `main.go` dispatches via `ctx.Run(...)`. -- Do not add `switch ctx.Command()` branching in `main.go`. The README's "switch on the command string" pattern is fragile and explicitly avoided here. +- Do not add `switch ctx.Command()` branching in `main.go`. Kong's own README shows that "switch on the command string" pattern; it is fragile and explicitly avoided here. ## Cross-field validation goes in `Validate()`, not `Run()` @@ -21,7 +21,7 @@ This project uses Kong (`github.com/alecthomas/kong`) for the CLI surface. The p ## File layout -- `cmd/cli.go` is reserved for struct definitions, Kong tags, and the top-level `CLI` type. Do not put `Run` or `Validate` method bodies here. +- `cmd/cli.go` is reserved for struct definitions, Kong tags, and the top-level `CLI` type. Do not put `Run` or `Validate` method bodies here; the one existing exception is the three-line `VersionCmd.Run`. - Place `Run` and `Validate` for a subcommand in its domain file (e.g. `cmd/review.go` for `ReviewCmd`, `cmd/pr.go` for `PRCmd`, `cmd/cclocate.go` for `LocateCmd`). - godoc on `Validate` must state what is checked, not just "validates the command". Example: `// Validate requires --output-path when --output=file.` diff --git a/.claude/skills/upgrade-tools/SKILL.md b/.claude/skills/upgrade-tools/SKILL.md index 2429a08..2c81d0a 100644 --- a/.claude/skills/upgrade-tools/SKILL.md +++ b/.claude/skills/upgrade-tools/SKILL.md @@ -22,7 +22,7 @@ Upgrade all dev tools and sync versions across local and CI. | `"github:golangci/golangci-lint"` | `.github/workflows/golangci-lint.yml` | `version: vX.Y.Z` in golangci-lint-action | | `"github:goreleaser/goreleaser"` | `.github/workflows/go-releaser.yml` | `version: vX.Y.Z` in goreleaser-action | | `"github:goreleaser/goreleaser"` | `.github/workflows/go-releaser-check.yml` | `version: vX.Y.Z` in goreleaser-action | -| `bun` | `.github/workflows/e2e.yml` | `bun-version: "X.Y.Z"` in setup-bun | +| `bun` | `.github/workflows/e2e.yml` | every `bun-version: "X.Y.Z"` in setup-bun (one per job) | 4. Run `mise run ci` to verify everything passes diff --git a/CLAUDE.md b/CLAUDE.md index 4790510..24832c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ commd is a Go CLI tool for reviewing Markdown files in an interactive TUI or a w ## Build & Test Commands -Dev tools (Go, golangci-lint, tparse, gofumpt, octocov, goreleaser, bun) are managed by mise (`.mise.toml`). Run `mise install` to set up the toolchain. +Dev tools (Go, golangci-lint, tparse, gofumpt, octocov, goreleaser, bun, vhs) are managed by mise (`.mise.toml`). Run `mise install` to set up the toolchain. ```bash mise run ci # Run full CI pipeline (MUST pass) @@ -29,6 +29,8 @@ mise run fix # Run go fix (modernize) mise run tidy # Run go mod tidy -v mise run vuln # Run go mod verify + govulncheck mise run install-skills # Install tuistory Claude Code skill +mise run demo # Record the README demo GIF (vhs/demo.gif; needs ttyd + ffmpeg) +mise run demo-validate # Validate vhs/demo.tape syntax without recording go test -v ./internal/markdown # Run tests for a specific package go test -run TestParsePreamble ./internal/markdown # Run a single test ``` @@ -41,7 +43,7 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl ### Entry Point & CLI -`main.go` → `cmd/cli.go`: Kong struct-based CLI with 5 subcommands (review, pr, cclocate, cchook, version); `cclocate` and `cchook` are deprecated (hidden from `--help`, kept working until removal). Cross-field constraints are enforced via `Validate()` methods on each command struct (called by Kong post-parse). The GitHub client is wired with `kong.BindToProvider(ghclient.NewClient)` and lazily injected only into commands that take `*ghclient.Client` in their `Run()` signature (currently `PRCmd`). +`main.go` → `cmd/cli.go`: Kong struct-based CLI with 5 subcommands (review, pr, cclocate, cchook, version); `cclocate` and `cchook` are deprecated (hidden from `--help`, kept working until removal). Cross-field constraints are enforced via a `Validate()` method on the command struct that has them (called by Kong post-parse). The GitHub client is wired with `kong.BindToProvider(ghclient.NewClient)` and lazily injected only into commands that take `*ghclient.Client` in their `Run()` signature (currently `PRCmd`). ### Package Layout From f29b4142a92277b11a1afce38a5107e7bc5d44c1 Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:53:18 +0900 Subject: [PATCH 2/7] docs(claude): mark the cchook integration as deprecated cchook and cclocate were deprecated, but the project overview, the hook data flow and three key design decisions still described the plan-mode hook as a current, recommended feature. Label each of them deprecated so new work does not build on the plan-mode flow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RKh8XqY1bD6M11zbzS4gVD --- CLAUDE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24832c8..471b43e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -commd is a Go CLI tool for reviewing Markdown files in an interactive TUI or a web browser. It parses Markdown files, displays them in a 2-pane interface, and supports inline commenting with feedback integration via Claude Code hooks. +commd is a Go CLI tool for reviewing Markdown files in an interactive TUI or a web browser. It parses Markdown files, displays them in a 2-pane interface, and supports inline commenting. Its Claude Code hook integration (`cchook` / `cclocate`) is deprecated. ## Absolute Rule @@ -68,7 +68,7 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl **PR Review**: `cmd/pr.go` → `PRCmd.Validate()` (parses URL) → `Run(client)` (client injected by Kong via `BindToProvider`) → `github.ListMDFiles()` → file picker (multi-select) → for each file: `github.FetchFileContent()` → `markdown.Parse()` → `tui.NewApp()` → `github.BuildPRReview()` → `github.SubmitReview()` -**Hook**: Claude Code (PreToolUse: ExitPlanMode, or PostToolUse: Write|Edit) → stdin JSON → `cchook.Run()` → `pane.SpawnAndWait(commd review ...)` → temp file IPC → exit 0 or 2 +**Hook** (deprecated): Claude Code (PreToolUse: ExitPlanMode, or PostToolUse: Write|Edit) → stdin JSON → `cchook.Run()` → `pane.SpawnAndWait(commd review ...)` → temp file IPC → exit 0 or 2 ## Development Rules @@ -105,11 +105,11 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl - **goldmark AST walk** for Markdown parsing instead of regex to correctly handle `#` inside code fences - **Mermaid → ASCII rendering** via `mermaid-ascii` library; fenced `\`\`\`mermaid` blocks are converted to ASCII art in the TUI detail pane and web view (`internal/mermaid`), with fallback to raw source on error - **Content-hash based viewed tracking** (`state.go`): sections are tracked by title → SHA-256 hash of title+body; if content changes, the viewed mark auto-clears -- **PreToolUse (ExitPlanMode) hook** as the recommended trigger: it fires exactly once per completed plan, before Claude Code's own approval dialog, and exit 2 denies the tool with the review as the reason so Claude revises in plan mode. **PostToolUse (Write|Edit)** remains as an alternative that fires on every plan write. A Stop hook is not an option because Stop doesn't fire during plan mode +- **PreToolUse (ExitPlanMode) hook** (deprecated `cchook`) as the recommended trigger: it fires exactly once per completed plan, before Claude Code's own approval dialog, and exit 2 denies the tool with the review as the reason so Claude revises in plan mode. **PostToolUse (Write|Edit)** remains as an alternative that fires on every plan write. A Stop hook is not an option because Stop doesn't fire during plan mode - **Line comments carry a source quote** (`ReviewComment.Quote`) captured at save time; the output quotes it so Claude can locate the target after line numbers shift, and removed diff lines (which no longer exist in the file) stay readable -- **Temp files for IPC** between hook process and review subprocess (which runs in a separate pane) +- **Temp files for IPC** (deprecated `cchook`) between hook process and review subprocess (which runs in a separate pane) - **Web review is a second front end over the same model**, not a TUI-in-a-browser (no terminal emulation), but it must behave like the TUI — same keys, modes, dialogs and flow; mouse support is additive. The one deliberate difference: no pane focus (vertical keys move the content and step between sections at its edges; the section list is 20% wide by default). The server owns all state (the page only renders JSON and calls the API) and returns the same `ReviewResult`, so output formatting stays in `markdown`. It binds to 127.0.0.1 only and requires the per-session token on the API, which also blocks cross-origin requests (custom header ⇒ preflight, never approved) and DNS rebinding. Vanilla JS with no build step keeps `go build` self-contained -- **Hook always exits 0 on error** — never breaks the Claude Code workflow; uses `CC_PLAN_REVIEW_SKIP=1` env var to disable +- **Hook always exits 0 on error** (deprecated `cchook`) — never breaks the Claude Code workflow; uses `CC_PLAN_REVIEW_SKIP=1` env var to disable ## Testing Patterns From 64e0d803ff37541bfec4c893f1983429380b55b5 Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:53:31 +0900 Subject: [PATCH 3/7] docs(claude): require the Edit tool for file edits In-place perl substitutions have corrupted files in this repository (a wrong delimiter broke a Go test file). Edit them with the Edit tool. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RKh8XqY1bD6M11zbzS4gVD --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 471b43e..5d81153 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,7 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl - Use `go doc` to look up library APIs and usage (more accurate than web searches) - Do not use Python for complex scripting. Use shell scripts instead +- Edit files with the Edit tool, not `perl` / `sed` in-place substitution — a wrong delimiter or escape silently corrupts the file - Do not use `/tmp`. Keep temporary files within the project directory ## Implementation Guidelines From 3558a0fc5dedc181a2f2b6899a5c4788a93cc942 Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:53:31 +0900 Subject: [PATCH 4/7] docs(claude): trim generic rules and move E2E notes to e2e/CLAUDE.md - Drop the generic coding rules (Go idioms, DRY, naming, comments, concise README) that the model follows by default; keep the project-specific ones - Move the E2E test notes to e2e/CLAUDE.md, which Claude Code loads only when working under e2e/, so other sessions do not carry them Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RKh8XqY1bD6M11zbzS4gVD --- CLAUDE.md | 16 +--------------- e2e/CLAUDE.md | 10 ++++++++++ 2 files changed, 11 insertions(+), 15 deletions(-) create mode 100644 e2e/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 5d81153..afea6ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,13 +81,7 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl ### Code -- Follow Go idioms and best practices - Keep design style consistent across subcommands (Kong struct tags, error handling patterns, etc.) -- Follow the DRY principle and eliminate duplicate code -- Keep package and function responsibilities single-purpose. Do not mix multiple responsibilities -- Keep functions unexported (lowercase) unless they are referenced externally -- Use function and variable names that clearly represent their responsibilities -- Add comments for code or logic that is not immediately obvious ### Tests @@ -98,7 +92,6 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl ### Documentation -- Keep the README clear and concise for users - Update the README when code specifications change ## Key Design Decisions @@ -122,11 +115,4 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl ### E2E Tests -E2E tests in `e2e/` use tuistory (terminal UI automation) with bun:test: - -- `launchCommd()` helper spawns the built binary in a virtual terminal and waits for Bubble Tea to render -- Each test runs with a fixed terminal size (120×36 by default) -- Snapshot tests (`snapshot.test.ts`) capture full screen state for layout regression detection -- Fixtures reuse `internal/markdown/testdata/basic.md` -- `addComment()` shared helper for comment creation sequences -- `review --web` is tested at two levels: `web.test.ts` drives the JSON API over HTTP, and `web-ui*.test.ts` drive the page in headless Chromium via Playwright (the `playwright` library under bun:test, not the Playwright test runner). Like the TUI tests, the critical paths (`web-ui.test.ts`) are in the basic tier and the rest (`web-ui-keys` / `web-ui-modes` / `web-ui-edge`) only in the full suite, which aims to cover every key binding and mode. Helpers: `launchWeb()` (`helpers/web.ts`), `useBrowser()` / `openPage()` / `eventually()` (`helpers/browser.ts`; wrap assertions after server-bound keys in `eventually()` since the page updates asynchronously). `mise run e2e-cov` collects V8 coverage of the page scripts (`COMMD_JS_COVERAGE=1`) and prints it per file (`bun scripts/js-coverage.ts --uncovered` lists missed lines). The e2e tasks run `bunx playwright install chromium` first +See `e2e/CLAUDE.md` (loaded when working under `e2e/`). diff --git a/e2e/CLAUDE.md b/e2e/CLAUDE.md new file mode 100644 index 0000000..ad10106 --- /dev/null +++ b/e2e/CLAUDE.md @@ -0,0 +1,10 @@ +# E2E Tests + +E2E tests in `e2e/` use tuistory (terminal UI automation) with bun:test: + +- `launchCommd()` helper spawns the built binary in a virtual terminal and waits for Bubble Tea to render +- Each test runs with a fixed terminal size (120×36 by default) +- Snapshot tests (`snapshot.test.ts`) capture full screen state for layout regression detection +- Fixtures reuse `internal/markdown/testdata/basic.md` +- `addComment()` shared helper for comment creation sequences +- `review --web` is tested at two levels: `web.test.ts` drives the JSON API over HTTP, and `web-ui*.test.ts` drive the page in headless Chromium via Playwright (the `playwright` library under bun:test, not the Playwright test runner). Like the TUI tests, the critical paths (`web-ui.test.ts`) are in the basic tier and the rest (`web-ui-keys` / `web-ui-modes` / `web-ui-edge`) only in the full suite, which aims to cover every key binding and mode. Helpers: `launchWeb()` (`helpers/web.ts`), `useBrowser()` / `openPage()` / `eventually()` (`helpers/browser.ts`; wrap assertions after server-bound keys in `eventually()` since the page updates asynchronously). `mise run e2e-cov` collects V8 coverage of the page scripts (`COMMD_JS_COVERAGE=1`) and prints it per file (`bun scripts/js-coverage.ts --uncovered` lists missed lines). The e2e tasks run `bunx playwright install chromium` first From f44a24ed6f1b9bc1956e3c33c092e0816cec0aba Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:53:32 +0900 Subject: [PATCH 5/7] chore(claude): add a read-only permission allowlist Allow the read-only Go commands that come up most often (go doc, go build ./..., go list -m, go vet ./..., go tool cover -func) so sessions without a local settings file stop prompting for them. Exact matches keep -toolexec, -vettool and -o from being appended. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RKh8XqY1bD6M11zbzS4gVD --- .claude/settings.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..2120c6c --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(go doc *)", + "Bash(go build ./...)", + "Bash(go list -m *)", + "Bash(go vet ./...)", + "Bash(go tool cover -func=coverage.out)" + ] + } +} From 55d1ea5e05cc3e8f9908669527ef925f477c46ae Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:59:34 +0900 Subject: [PATCH 6/7] chore(claude): prompt again for go doc -http and go list -mod flags go doc -http starts an HTTP server, and go list -mod=mod / -modfile can rewrite go.mod, so the allowlisted wildcards were broader than read-only. Route those flags back to a permission prompt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RKh8XqY1bD6M11zbzS4gVD --- .claude/settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index 2120c6c..ecfd298 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,6 +6,10 @@ "Bash(go list -m *)", "Bash(go vet ./...)", "Bash(go tool cover -func=coverage.out)" + ], + "ask": [ + "Bash(go doc *-http*)", + "Bash(go list *-mod*)" ] } } From 67863f15d437f9ceb53a86ae865630d36a2a858c Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:59:34 +0900 Subject: [PATCH 7/7] docs(claude): fix contradictory wording in the harness docs - Describe the deprecated cchook triggers in the past tense instead of calling a deprecated hook "recommended" - VersionCmd.Run is short, not three lines Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RKh8XqY1bD6M11zbzS4gVD --- .claude/rules/kong-cli.md | 2 +- CLAUDE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/rules/kong-cli.md b/.claude/rules/kong-cli.md index 547ae98..3395b8b 100644 --- a/.claude/rules/kong-cli.md +++ b/.claude/rules/kong-cli.md @@ -21,7 +21,7 @@ This project uses Kong (`github.com/alecthomas/kong`) for the CLI surface. The p ## File layout -- `cmd/cli.go` is reserved for struct definitions, Kong tags, and the top-level `CLI` type. Do not put `Run` or `Validate` method bodies here; the one existing exception is the three-line `VersionCmd.Run`. +- `cmd/cli.go` is reserved for struct definitions, Kong tags, and the top-level `CLI` type. Do not put `Run` or `Validate` method bodies here; the one existing exception is the short `VersionCmd.Run`. - Place `Run` and `Validate` for a subcommand in its domain file (e.g. `cmd/review.go` for `ReviewCmd`, `cmd/pr.go` for `PRCmd`, `cmd/cclocate.go` for `LocateCmd`). - godoc on `Validate` must state what is checked, not just "validates the command". Example: `// Validate requires --output-path when --output=file.` diff --git a/CLAUDE.md b/CLAUDE.md index afea6ea..9465046 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,7 +99,7 @@ Linter config: `.golangci.yml` (enabled: asciicheck, gocritic, misspell, nolintl - **goldmark AST walk** for Markdown parsing instead of regex to correctly handle `#` inside code fences - **Mermaid → ASCII rendering** via `mermaid-ascii` library; fenced `\`\`\`mermaid` blocks are converted to ASCII art in the TUI detail pane and web view (`internal/mermaid`), with fallback to raw source on error - **Content-hash based viewed tracking** (`state.go`): sections are tracked by title → SHA-256 hash of title+body; if content changes, the viewed mark auto-clears -- **PreToolUse (ExitPlanMode) hook** (deprecated `cchook`) as the recommended trigger: it fires exactly once per completed plan, before Claude Code's own approval dialog, and exit 2 denies the tool with the review as the reason so Claude revises in plan mode. **PostToolUse (Write|Edit)** remains as an alternative that fires on every plan write. A Stop hook is not an option because Stop doesn't fire during plan mode +- **PreToolUse (ExitPlanMode) hook** (deprecated `cchook`) was the preferred trigger: it fires exactly once per completed plan, before Claude Code's own approval dialog, and exit 2 denies the tool with the review as the reason so Claude revises in plan mode. **PostToolUse (Write|Edit)** was the alternative, firing on every plan write. A Stop hook was not an option because Stop doesn't fire during plan mode - **Line comments carry a source quote** (`ReviewComment.Quote`) captured at save time; the output quotes it so Claude can locate the target after line numbers shift, and removed diff lines (which no longer exist in the file) stay readable - **Temp files for IPC** (deprecated `cchook`) between hook process and review subprocess (which runs in a separate pane) - **Web review is a second front end over the same model**, not a TUI-in-a-browser (no terminal emulation), but it must behave like the TUI — same keys, modes, dialogs and flow; mouse support is additive. The one deliberate difference: no pane focus (vertical keys move the content and step between sections at its edges; the section list is 20% wide by default). The server owns all state (the page only renders JSON and calls the API) and returns the same `ReviewResult`, so output formatting stays in `markdown`. It binds to 127.0.0.1 only and requires the per-session token on the API, which also blocks cross-origin requests (custom header ⇒ preflight, never approved) and DNS rebinding. Vanilla JS with no build step keeps `go build` self-contained