diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..6bb4fee --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,28 @@ +{ + "name": "vgv-ai-flutter-plugin", + "version": "0.0.5", + "description": "Best-practice skills for Flutter and Dart development from Very Good Ventures.", + "author": { + "name": "Very Good Ventures", + "email": "tools@verygood.ventures", + "url": "https://verygood.ventures" + }, + "homepage": "https://github.com/VeryGoodOpenSource/vgv-ai-flutter-plugin", + "repository": "https://github.com/VeryGoodOpenSource/vgv-ai-flutter-plugin", + "license": "MIT", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "VGV AI Flutter Plugin", + "shortDescription": "Flutter and Dart best practices from Very Good Ventures", + "longDescription": "Best-practice skills for Flutter and Dart covering accessibility, animations, BLoC, testing, theming, navigation, security, internationalization, layered architecture, license compliance, UI packages, project creation, SDK/lint upgrades, and an autonomous quality-gate loop that drives analyze, format, test, and coverage to green — plus automated dart analyze and format hooks.", + "developerName": "Very Good Ventures", + "category": "Productivity", + "capabilities": ["Write"], + "defaultPrompt": [ + "Create a new Flutter app with Very Good CLI", + "Add a bloc for user authentication", + "Drive this package to green" + ], + "websiteURL": "https://verygood.ventures" + } +} diff --git a/.release-please-config.json b/.release-please-config.json index 9dbfd32..3dc37cd 100644 --- a/.release-please-config.json +++ b/.release-please-config.json @@ -22,6 +22,11 @@ "type": "json", "path": ".claude-plugin/plugin.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".codex-plugin/plugin.json", + "jsonpath": "$.version" } ] } diff --git a/AGENTS.md b/AGENTS.md index f70cd55..d171801 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,11 +7,16 @@ VGV AI Flutter Plugin provides best-practices skills for Flutter and Dart develo ## Repository Structure ```text -.mcp.json # MCP server configuration (Dart and Very Good CLI) +.mcp.json # MCP server configuration (Dart and Very Good CLI); read by both harnesses .claude-plugin/ - plugin.json # Plugin manifest (name, version, keywords) + plugin.json # Claude Code plugin manifest (name, version, keywords) +.codex-plugin/ + plugin.json # Codex plugin manifest (interface metadata + mcpServers -> ./.mcp.json) agents/ flutter-reviewer.md # Read-only Flutter code reviewer subagent +codex/ # The only Codex-specific assets; skills, MCP and hooks are shared + agents/ + flutter-reviewer.toml # Codex port of agents/flutter-reviewer.md — users copy it to ~/.codex/agents/ docs/ plan/ # Planning and design documents evals/ @@ -167,10 +172,27 @@ documentation in the same change: automatically, so verify each one by hand. - **Adding or changing a hook** in `hooks/hooks.json` — update the **Hooks** section in `README.md` (and the `## Hooks` section in `CLAUDE.md` if behavior - changes). + changes). The file is shared with Codex, so keep any `PostToolUse` matcher + covering `apply_patch` as well as `Edit|Write`, or the hook stops firing there. - **Adding or changing an MCP tool** — update the **MCP Integration** tools table in `README.md`, and check whether any skill's `allowed-tools` names a tool that - was renamed or removed. Nothing validates those names. + was renamed or removed. Nothing validates those names. A new **server** goes in + `.mcp.json` only; both harnesses read that file. +- **Editing either plugin manifest** — `.claude-plugin/plugin.json` and + `.codex-plugin/plugin.json` describe the same plugin. Keep + `interface.longDescription` in the Codex manifest in step with `description` in + the Claude Code one; release-please bumps `version` in both. Renaming the plugin + changes the skill namespace on both harnesses. +- **Changing what a hook script reads from its payload** — the two harnesses + describe an edit differently (Claude Code `tool_input.file_path`, Codex + `tool_input.command` holding an apply_patch envelope). `analyze.sh` and + `format.sh` each read both shapes with the same inline `jq` expression — keep + the two copies identical. +- **Changing `agents/flutter-reviewer.md`** — port the same change to + `codex/agents/flutter-reviewer.toml`. A Codex plugin cannot ship a subagent, so + that file is a separate copy users install by hand. Its output contract (the + four-column findings table) is consumed verbatim by callers on both harnesses, + so the two must not drift. ## Evals diff --git a/CLAUDE.md b/CLAUDE.md index dbaa815..50da965 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,25 @@ from `vgv-cli-common.sh`. The following hook is **agent-scoped** — it is decla These run **after** a tool call completes: -- `Edit|Write` matcher → `analyze.sh` — runs `dart analyze` on the modified `.dart` file; exits 2 on failure (blocking — Claude must fix the issue) -- `Edit|Write` matcher → `format.sh` — runs `dart format` on the modified `.dart` file; always exits 0 (non-blocking) +- `apply_patch|Edit|Write` matcher → `analyze.sh` — runs `dart analyze` on the modified `.dart` file(s); on failure exits 2, which feeds the analyzer output back to the model as a message. `PostToolUse` runs after the tool, so this does not block or revert the edit +- `apply_patch|Edit|Write` matcher → `format.sh` — runs `dart format` on the modified `.dart` file(s); always exits 0 (non-blocking) + +Both resolve the changed files with the same inline `jq` expression, handling Claude Code's +`tool_input.file_path` and Codex's `tool_input.command` (an `apply_patch` envelope, which can name +several files at once). That is the only harness-specific branch in the hook scripts, and the two +copies must stay identical. All hook scripts require **jq** to parse the hook payload (they skip gracefully if `jq` is not installed). + +### Codex + +This repo installs as a Codex plugin via `.codex-plugin/plugin.json`, with the marketplace entry +living in `very-good-claude-code-marketplace` alongside the Claude Code one. Codex reads +`skills/`, `.mcp.json`, and this same `hooks/hooks.json` — resolving `${CLAUDE_PLUGIN_ROOT}` as a +compatibility alias. That is why the `PostToolUse` matcher says `apply_patch|Edit|Write`: Codex +names its file-editing tool `apply_patch`, and the extra alternative is inert on Claude Code. + +The only Codex-specific asset is `codex/agents/flutter-reviewer.toml`, because Codex has no way to +bundle a subagent in a plugin — users copy it to `~/.codex/agents/` themselves. Change a hook or +the reviewer agent and both harnesses are affected — see `AGENTS.md` → Maintaining Existing +Skills, Hooks, and MCP Tools. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 108322f..f3c1018 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,6 +193,104 @@ frontmatter equivalent, and `interface.short_description`, which takes precedenc spec-legal `metadata: short-description` key. The `SKILL.md` body stays the one source of truth; the sidecar is thin, with no build step. Add one for every new skill. +**Codex runtime** — Codex has its own plugin system, and this repo is a Codex plugin as well as a +Claude Code one. `.codex-plugin/plugin.json` plus the marketplace entry in +`.agents/plugins/marketplace.json` are all it takes; Codex then reads `skills/`, `.mcp.json`, and +`hooks/hooks.json` from the very files Claude Code uses. There is no install script and no second +copy of the hooks. Verified against Codex CLI 0.153.4: + +- **Two manifests, one plugin.** `.codex-plugin/plugin.json` is the Codex twin of + `.claude-plugin/plugin.json`. It is what carries the Codex app presentation metadata + (`interface.displayName`, `category`, `capabilities`, `defaultPrompt`), which has no other home, + and it points `mcpServers` at `./.mcp.json`. Keep `interface.longDescription` in step with + `description` in the Claude Code manifest, and leave `version` to release-please, which bumps + both through `extra-files`. `keywords` is deliberately not duplicated: it only affects plugin + search, and a second copy of a 50-plus entry list would rot. Codex rejects a manifest carrying a + `hooks` key or any field outside its allowed set, so do not add one. +- **Hooks come from default discovery.** Codex looks for a plugin's hooks at + `/hooks/hooks.json` — the same path and file Claude Code uses — and resolves + `${CLAUDE_PLUGIN_ROOT}` inside it, documented as a compatibility alias alongside its own + `PLUGIN_ROOT`. That is the whole reason one hooks file serves both harnesses. A plugin manifest + can override the path with a `hooks` entry, but this repo ships no Codex manifest, so the default + is what applies. +- **Keep the `PostToolUse` matcher on Claude Code's exact-match path.** Claude Code treats a + matcher containing only letters, digits, `_`, `-`, spaces, `,` and `|` as a list of exact tool + names; anything else is an unanchored regex tested with `RegExp.test`. `apply_patch|Edit|Write` + qualifies as exact, so it matches those three tool names and nothing else. Add a `.` or `*` and + it silently becomes a regex that also matches `NotebookEdit`, which this plugin has no reason to + act on. Widen the matcher only together with the payload reading in `analyze.sh` and `format.sh`. +- **Hooks are a stable, default-on feature**, not experimental. The flag is `[features] hooks` + (`codex features list` shows it enabled); there is no `codex_hooks` flag. Codex also runs hooks on + Windows and offers a `commandWindows` override — but these scripts are `bash` and need `jq`, so + Windows means WSL or Git Bash. Codex **silently ignores a malformed `hooks.json`**, which disables + the whole enforcement layer with no error, so validate it by hand after editing it. +- **Five of the six scripts need nothing.** Codex passes `tool_name: "Bash"` with + `tool_input.command` as a plain string and accepts the same `permissionDecision` allow/deny JSON, + so `check-vgv-cli.sh`, `block-cli-workarounds.sh`, and `allow-readonly-git.sh` are untouched; + plain stdout from a `SessionStart` hook is injected as a developer message exactly as on Claude + Code, so `warn-missing-mcp.sh` is too. Only the edit hooks differ: Codex's file-editing tool is + `apply_patch`, so the `PostToolUse` matcher reads `apply_patch|Edit|Write` (the extra alternative + is inert on Claude Code), and the payload hands over the raw patch with no `file_path` and no + changed-file list, so both hooks read the paths out of the patch headers with the same inline + `jq` expression. A rename lists the old and new path and a delete lists none, so an existence + check is all the bookkeeping needed. The expression is duplicated in the two scripts rather than + shared through a third file; keep the copies identical. +- **One marketplace serves both harnesses.** `codex plugin add` only accepts + `PLUGIN@MARKETPLACE`, so a marketplace is mandatory — but it is + `very-good-claude-code-marketplace`, the same repo Claude Code uses, not this one. That + repo carries a Codex manifest at `.agents/plugins/marketplace.json` beside its existing + `.claude-plugin/marketplace.json`, and the Codex entry points back here with a remote + `url` source: + + ```json + { + "name": "vgv-ai-flutter-plugin", + "source": { + "source": "url", + "url": "https://github.com/VeryGoodOpenSource/vgv-ai-flutter-plugin.git" + }, + "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, + "category": "Productivity" + } + ``` + + The source type matters and fails quietly when wrong. Codex resolves `url` and `local` + (with a path inside the marketplace root); it **silently drops** an entry using `github`, + `git`, `git-subdir`, or a `../sibling` path — the marketplace adds fine and + `codex plugin list` just reports "No marketplace plugins found", with no error anywhere. + That is why Codex cannot read the existing `.claude-plugin/marketplace.json`, whose + entries all use `source: github`, and why the two manifests coexist in that repo. + Note that the `url` source always resolves the default branch, so testing an unmerged change + means pointing a throwaway marketplace at your checkout with a `local` source instead. +- **`analyze.sh` and `format.sh` stay two separate hooks.** They are easier to maintain and + reason about apart, which is a deliberate choice over merging them. Two consequences to know. + Claude Code runs every hook in a matcher group **in parallel**, so the two race on the same + file: `dart format` rewrites it while `dart analyze` reads it. Formatting does not change + semantics, so the analyzer reports the same findings either way, though line numbers can refer + to the pre-format file. And because they do not share a helper, the payload-reading `jq` + expression is duplicated in both — keep the copies identical, and add cases to + the two scripts. +- **A plugin cannot ship a Codex subagent.** Codex loads custom agents only from `~/.codex/agents/` + or a project's `.codex/agents/`, and `agents` is not a plugin manifest field or a discovery path. + `codex/agents/flutter-reviewer.toml` is therefore a file users copy, either into + `~/.codex/agents/` for themselves or committed to a project's `.codex/agents/` for a whole team — + the latter is how most repos in the wild do it. Distributing agents any other way currently means + an install script, which this plugin deliberately does not ship. Upstream requests to bundle + agents in a plugin are open ([openai/codex#18988][codex_agents_issue], + [openai/codex#28491][codex_agents_issue_2]); if either lands, the copy step goes away. Codex custom agents are standalone TOML needing `name`, `description`, and + `developer_instructions`, plus any `config.toml` key. There is no per-agent tool allowlist and no + agent-scoped `PreToolUse` hook, so it sets `sandbox_mode = "read-only"` to hold the read-only + contract that `allow-readonly-git.sh` holds on Claude Code. Codex ships no validator for agent + files, so the loader test parses them and asserts `sandbox_mode` is still `read-only`. +- **Do not weaken the Claude Code path** to make Codex simpler. `hooks/hooks.json` and + `agents/flutter-reviewer.md` stay authoritative. + +Nothing in CI exercises Codex, so verify a change to any of it by hand. Install the working tree +into a throwaway `CODEX_HOME` the way a user would (`codex plugin marketplace add` then +`codex plugin add`, with a scratch marketplace whose entry is a `local` path to your checkout), +then check what Codex picked up with `codex debug prompt-input` and `codex doctor --json`. Both +read local state without calling a model, so this needs the `codex` CLI but no credentials. + **Invocation** — every skill in this plugin is **model-invoked**: the model may reach for it autonomously when the context fits (that is the point of a best-practice skill), so neither `disable-model-invocation` (Claude Code) nor a `policy` block (Codex) is set. All trigger @@ -215,6 +313,8 @@ session and exercise it before you commit. - **Dart SDK** and **jq** on your `PATH` — the hooks need both. - **Very Good CLI** ≥ 1.3.0 (`dart pub global activate very_good_cli`) for the Very Good CLI MCP server tools. +- **Codex CLI** (`npm install -g @openai/codex`) only if you touch the hooks or + `codex/`, to verify the change by hand. Everything else runs without it. See the README [Hooks](README.md#hooks) and [MCP Integration](README.md#mcp-integration) sections for the full prerequisite details. @@ -308,7 +408,7 @@ Every pull request runs the following checks automatically: | Spelling | Runs cspell on all `*.md` files | `config/cspell.json` | | Skill validation | Validates **every** `SKILL.md`'s frontmatter and structure against the Agent Skills spec, so a malformed skill fails the build instead of silently vanishing on another host | `Flash-Brew-Digital/validate-skill@v1` | | Plugin validation | Validates and test-installs the plugin | `claude plugin validate .` | -| Script tests | Runs the hook scripts' own test suites | `hooks/scripts/*_test.sh` | +| Script tests | Runs every hook script test suite | `hooks/scripts/*_test.sh` | Evals do **not** run on a pull request. They call real models, so they run after a merge to `main` instead, scoped to the skills that changed: @@ -348,3 +448,6 @@ type(scope): description - Fill out the [PR template](.github/PULL_REQUEST_TEMPLATE.md) completely. - Ensure all CI checks pass before requesting review. - Link any related issues in the PR description. + +[codex_agents_issue]: https://github.com/openai/codex/issues/18988 +[codex_agents_issue_2]: https://github.com/openai/codex/issues/28491 diff --git a/README.md b/README.md index a0075c9..4d490c4 100644 --- a/README.md +++ b/README.md @@ -73,14 +73,49 @@ This plugin includes SessionStart, PreToolUse, and PostToolUse hooks that valida | **Check VGV CLI** (`check-vgv-cli.sh`) | PreToolUse (`mcp__.*very-good-cli__.*`) | Auto-approves Very Good CLI MCP tool calls in every run mode via a PreToolUse `allow` decision, so they never dead-end when the tool isn't on `permissions.allow` (including under `skipAutoPermissionPrompt`); denies with an install/upgrade message if the CLI is missing or < 1.3.0 | | **Block CLI Workarounds** (`block-cli-workarounds.sh`) | PreToolUse (`Bash`) | Blocks direct CLI bypass of Very Good CLI commands through the Bash tool; exits 2 on failure (blocking) | | **Allow Read-only Git** (`allow-readonly-git.sh`) | PreToolUse (`Bash`, `flutter-reviewer` agent only) | Restricts the `flutter-reviewer` agent's Bash to `git diff`/`git status`; exits 2 on anything else (blocking). Scoped via the agent's frontmatter, not `hooks.json` | -| **Analyze** (`analyze.sh`) | PostToolUse (`Edit`/`Write`) | Runs `dart analyze` on the modified `.dart` file; exits 2 on failure (blocking — Claude must fix issues before continuing) | -| **Format** (`format.sh`) | PostToolUse (`Edit`/`Write`) | Runs `dart format` on the modified `.dart` file; always exits 0 (non-blocking — formatting is applied silently) | +| **Analyze** (`analyze.sh`) | PostToolUse (`apply_patch`/`Edit`/`Write`) | Runs `dart analyze` on the modified `.dart` file(s); on failure exits 2, which surfaces the analyzer output to the model as feedback so it fixes the issue. The edit itself already happened and is not reverted | +| **Format** (`format.sh`) | PostToolUse (`apply_patch`/`Edit`/`Write`) | Runs `dart format` on the modified `.dart` file; always exits 0 (non-blocking — formatting is applied silently) | + +Codex runs this same `hooks/hooks.json` and these same scripts — `apply_patch` is its file-editing +tool, which is why that matcher covers it. ### Prerequisites - **Dart SDK** — must be available on your `PATH` - **jq** — used to parse the hook payload; hooks are skipped gracefully if `jq` is not installed +## Codex + +Codex installs from the same marketplace as Claude Code: + +```bash +codex plugin marketplace add VeryGoodOpenSource/very-good-claude-code-marketplace && codex plugin add vgv-ai-flutter-plugin@very-good-claude-code-marketplace +``` + +That one install gives you the skills, both MCP servers, and the hooks. Codex discovers them from +the same files Claude Code uses — `skills/`, `.mcp.json`, and `hooks/hooks.json` — so there is no +Codex-specific configuration in this repo at all. Restart Codex afterwards, then approve the hooks +with `/hooks`, since Codex requires a review before a hook runs for the first time. + +The reviewer agent is the one piece `codex plugin add` does **not** install: Codex has no way to +bundle a subagent in a plugin ([openai/codex#18988][codex_agents_issue]), and loads custom agents +only from `~/.codex/agents/` or a project's `.codex/agents/`. Pick whichever scope fits. + +For yourself, across every project: + +```bash +mkdir -p ~/.codex/agents && cp codex/agents/flutter-reviewer.toml ~/.codex/agents/ +``` + +For a whole team, commit it into the Flutter project instead — then everyone gets the reviewer with +no per-developer setup: + +```bash +mkdir -p .codex/agents && cp codex/agents/flutter-reviewer.toml .codex/agents/ +``` + +Either way, ask Codex to spawn `flutter-reviewer`. + ## Evals Skill evals ask whether Claude routes to a skill and follows it. [promptfoo](https://www.promptfoo.dev) sends each case's prompt through the Claude Agent SDK twice — once with this plugin loaded, once sealed with nothing loaded — so a grader that passes in both columns is measuring the model rather than the skill. They authenticate through your local Claude Code session, so they need no API key. @@ -187,6 +222,11 @@ The Very Good CLI MCP server exposes Very Good CLI commands to Claude. The `.mcp.json` file at the project root registers the `dart` and `very-good-cli` MCP servers using stdio transport. When Claude Code detects this configuration, it connects to both servers and gains access to the tools above. The skills continue to provide knowledge and best practices while the MCP tools handle execution. +On Codex the same two servers are registered in `~/.codex/config.toml` instead — see +[Codex](#codex). Skills that drive an MCP tool always name the equivalent `very_good`, `dart`, or +`flutter` command as a fallback, so they keep working on a host where neither server is connected. + +[codex_agents_issue]: https://github.com/openai/codex/issues/18988 [marketplace_link]: https://github.com/VeryGoodOpenSource/very-good-claude-code-marketplace [claude_code_link]: https://claude.ai/code [vgv_link]: https://verygood.ventures diff --git a/codex/agents/flutter-reviewer.toml b/codex/agents/flutter-reviewer.toml new file mode 100644 index 0000000..1f959f3 --- /dev/null +++ b/codex/agents/flutter-reviewer.toml @@ -0,0 +1,131 @@ +name = "flutter-reviewer" +description = "Read-only Flutter code reviewer. Spawn after writing or changing Dart code to review changed code against VGV bloc, testing, security, and accessibility standards. Never edits files." + +# The Claude Code agent enforces its read-only contract two ways: it declares no +# write tools, and an agent-scoped PreToolUse hook restricts Bash to `git diff` +# and `git status`. Codex has no agent-scoped tool allowlist, so the contract is +# enforced by the sandbox instead. `read-only` is a stronger guarantee than the +# hook it replaces — the OS refuses every write, not just the shell commands a +# matcher anticipated — and the shell restriction is restated as an instruction +# below so the agent does not waste turns on commands the sandbox will reject. +sandbox_mode = "read-only" + +model_reasoning_effort = "high" + +developer_instructions = ''' +You are a read-only Flutter code reviewer for Very Good Ventures. You review changed Dart code +against four VGV standards and report findings as a markdown table. When a parent agent spawns +you, it consumes your table verbatim. + +## Read-only contract + +You **never** edit files. You run in a `read-only` sandbox, so every write is refused: editing a +file, `git checkout`, `git apply`, `sed -i`, and output redirection all fail. Restrict your shell +use to read-only git inspection — only `git diff` and `git status`. Do not attempt to work around +the sandbox; it is intentional. + +If you ever conclude that a fix requires editing a file, describe the fix in the `fix` column of +your findings table. Do not apply it. + +## Standards + +Load these four VGV skills and treat them as your only standards source: + +- **`bloc`** — Bloc/Cubit state management conventions. +- **`testing`** — unit, widget, and golden test conventions. +- **`static-security`** — Flutter static security review. +- **`accessibility`** — WCAG-aligned Flutter accessibility. + +Load all four before you report, and load nothing else as a standard. Every finding you report +must trace back to one of them. If a problem does not map to one of the four, do not report it +(see "What not to report"). + +If a skill will not load, say so in one line before your table and review against the ones that +did — never substitute your own conventions for a standard you could not read. + +## Diff scoping + +Scope your review to changed Dart code only. Never review the whole repository. + +Determine the change set adaptively, from the repository root: + +1. **Uncommitted changes first.** Run `git status` and `git diff` (staged and unstaged). If there + are uncommitted `.dart` changes, review those. +2. **Otherwise, branch-vs-base.** If the working tree is clean, fall back to the branch's changes + against its merge base: `git diff ...HEAD` (typically `main...HEAD`). Use `git status` and + `git diff` to enumerate the changed files. +3. **Include untracked `.dart` files.** `git status` surfaces untracked files; review untracked + `.dart` files as new code. +4. **Monorepo / subdirectory.** Always scope from the repository root and apply the four standards + per affected package. + +Read the changed files in full to review their context, not just the diff hunks. When the Dart MCP +server is connected, you may use its `analyze_files` tool to corroborate a skill-based judgment, +but analyzer output is not itself a findings source (see "What not to report"). If it is +unavailable, rely on the four standards alone — do not run `dart analyze` yourself just to fill the +gap, because analyzer findings are out of scope either way. + +### When scoping fails + +If you cannot determine a change scope — not a git repository, detached HEAD, no merge base, or the +git commands fail — report that you could not determine a change scope and stop. Do not guess and do +not review the whole repository. + +## Output + +Output **exactly one** markdown table, one row per finding. Do **not** split findings into multiple +tables, do **not** group them by file, and do **not** introduce section headings or extra columns +around the table. The table has exactly these four columns, in this order — `location`, `problem`, +`fix`, `standard`: + +```markdown +| location | problem | fix | standard | +| --------------------------------- | ---------------------------------------- | ------------------------------------ | -------------- | +| lib/counter/counter_cubit.dart:12 | Mutable state field breaks immutability | Mark state class fields `final` | bloc | +| test/counter/counter_test.dart:30 | Tautological assertion `expect(x, x)` | Assert against the expected value | testing | +``` + +Rules: + +- `location` — `path:line` of the finding, in a single column. Always include the file path on every + row; never move the path into a heading and never reduce this column to a bare line number. +- `problem` — what is wrong, concisely. +- `fix` — the change you recommend. Describe it; never apply it. +- `standard` — exactly one of `bloc`, `testing`, `static-security`, `accessibility`, in its own + column on every row. Every row must name one of these four. Never convey the standard through a + section heading instead of this column. +- Align the pipe characters vertically (VGV markdown convention). + +A one-line note after the table (per "Out-of-domain changes" below) is allowed. Any other prose, +grouping, or additional tables is not. + +### No changed Dart files + +If the change scope contains no `.dart` files (clean tree, or only non-Dart changes), report +`No changed Dart files to review.` and stop. Never emit an empty table and never invent findings. + +### Out-of-domain changes + +Your four standards do not cover every domain. If changed Dart code touches areas outside them — +for example navigation, theming, internationalization, or layered architecture — you have no loaded +standard to cite, so you stay silent on findings there. Add a one-line note after the table listing +the changed areas that fall outside your four standards, so a clean review is not mistaken for full +coverage. For example: + +> Note: changes in `lib/routing/` and `lib/theme/` are outside the loaded standards (bloc, testing, +> static-security, accessibility) and were not reviewed. + +### What not to report + +- **Analyzer-only findings.** Raw `dart analyze` errors (unused imports, dead null-aware operators, + etc.) do not trace to any of your four standards, so they are out of scope for your table. + Do not report them and do not introduce a `dart-analyzer` pseudo-standard. Such errors are caught + separately by the plugin's PostToolUse `analyze.sh` hook when code is written, not here. Use the + analyzer only to corroborate a skill-based judgment. +- **Untraceable findings.** If a finding cannot name one of the four standards, omit it. + +## Dispatch contract + +When a parent agent spawns you, you self-scope via the adaptive diff procedure above — the caller +does not pass you a file list — and the caller consumes your findings table verbatim. +''' diff --git a/hooks/hooks.json b/hooks/hooks.json index c97b65b..e651aa5 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -36,7 +36,7 @@ ], "PostToolUse": [ { - "matcher": "Edit|Write", + "matcher": "apply_patch|Edit|Write", "hooks": [ { "type": "command", diff --git a/hooks/scripts/analyze.sh b/hooks/scripts/analyze.sh index bba41ca..d7d1a0d 100755 --- a/hooks/scripts/analyze.sh +++ b/hooks/scripts/analyze.sh @@ -10,16 +10,51 @@ if ! command -v jq &>/dev/null; then exit 0 fi -# Extract file path from the tool input -file_path=$(jq -r '.tool_input.file_path // empty' <<< "$input") +# Which files did this edit touch? The two harnesses answer differently: +# +# Claude Code Edit / Write -> .tool_input.file_path (one path) +# Codex apply_patch -> .tool_input.command (a patch envelope, no path) +# +# Codex hook payloads carry no file path and no changed-file list, so the paths +# are read out of the patch headers. A rename emits both the old and the new +# path; the old one no longer exists, so the -f test below drops it. Deleted +# files never match, since only Add/Update/Move headers are selected. +paths=$(jq -r ' + if .tool_input.file_path then .tool_input.file_path + else + (.tool_input.command // "") + | select(startswith("*** Begin Patch")) + | split("\n")[] + | select(test("^\\*\\*\\* (Add File|Update File|Move to): ")) + | sub("^\\*\\*\\* (Add File|Update File|Move to): "; "") + end' <<< "$input") -# Skip if no file path or not a Dart file -if [[ -z "$file_path" || "$file_path" != *.dart ]]; then +cwd=$(jq -r '.cwd // empty' <<< "$input") + +files=() +while IFS= read -r file; do + [ -n "$file" ] || continue + case "$file" in + *.dart) ;; + *) continue ;; + esac + # apply_patch paths may be relative to the session working directory. + case "$file" in + /*) ;; + *) if [ -n "$cwd" ]; then file="$cwd/$file"; fi ;; + esac + if [ -f "$file" ]; then + files+=("$file") + fi +done <<< "$paths" + +# Nothing Dart in this edit +if [ ${#files[@]} -eq 0 ]; then exit 0 fi -# Run dart analyze on the single file -output=$(dart analyze "$file_path" 2>&1) || { +# Run dart analyze on the changed files +output=$(dart analyze "${files[@]}" 2>&1) || { echo "$output" >&2 exit 2 -} \ No newline at end of file +} diff --git a/hooks/scripts/format.sh b/hooks/scripts/format.sh index e58b601..ac161b8 100755 --- a/hooks/scripts/format.sh +++ b/hooks/scripts/format.sh @@ -10,13 +10,48 @@ if ! command -v jq &>/dev/null; then exit 0 fi -# Extract file path from the tool input -file_path=$(jq -r '.tool_input.file_path // empty' <<< "$input") +# Which files did this edit touch? The two harnesses answer differently: +# +# Claude Code Edit / Write -> .tool_input.file_path (one path) +# Codex apply_patch -> .tool_input.command (a patch envelope, no path) +# +# Codex hook payloads carry no file path and no changed-file list, so the paths +# are read out of the patch headers. A rename emits both the old and the new +# path; the old one no longer exists, so the -f test below drops it. Deleted +# files never match, since only Add/Update/Move headers are selected. +paths=$(jq -r ' + if .tool_input.file_path then .tool_input.file_path + else + (.tool_input.command // "") + | select(startswith("*** Begin Patch")) + | split("\n")[] + | select(test("^\\*\\*\\* (Add File|Update File|Move to): ")) + | sub("^\\*\\*\\* (Add File|Update File|Move to): "; "") + end' <<< "$input") -# Skip if no file path or not a Dart file -if [[ -z "$file_path" || "$file_path" != *.dart ]]; then +cwd=$(jq -r '.cwd // empty' <<< "$input") + +files=() +while IFS= read -r file; do + [ -n "$file" ] || continue + case "$file" in + *.dart) ;; + *) continue ;; + esac + # apply_patch paths may be relative to the session working directory. + case "$file" in + /*) ;; + *) if [ -n "$cwd" ]; then file="$cwd/$file"; fi ;; + esac + if [ -f "$file" ]; then + files+=("$file") + fi +done <<< "$paths" + +# Nothing Dart in this edit +if [ ${#files[@]} -eq 0 ]; then exit 0 fi -# Run dart format on the single file (auto-fix, always exit 0) -dart format "$file_path" &>/dev/null || true \ No newline at end of file +# Run dart format on the changed files (auto-fix, always exit 0) +dart format "${files[@]}" &>/dev/null || true