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
62 changes: 62 additions & 0 deletions .claude/hooks/lint-changed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/sh
# Lint the file Claude just wrote, and report a failure instead of hiding it.
#
# `Backend (pint + tests)` is one of the required checks on this repository, so a style
# violation that reaches a push costs a CI round. This reports it at the moment it is written.
#
# Why a script rather than a settings one-liner: a JSON-escaped command is where a malformed
# settings file comes from, and a malformed one is skipped silently in -p and CI runs.
#
# Why the linter call is not wrapped to discard stderr and swallow the exit code: a suppressed
# hook is indistinguishable from a hook that never fired.
#
# `set -e` is deliberately absent: the linter's nonzero exit is the case worth reporting, and
# -e would exit before the report is written. Every condition the hook cannot judge exits 0.

set -u

TOOL=pint # baked in at construction time, never read from repository content
CHECK_ARGS='--test' # the flag that makes pint REPORT rather than rewrite

command -v jq >/dev/null 2>&1 || exit 0
payload=$(cat) || exit 0
file=$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty') || exit 0
[ -n "$file" ] || exit 0

# file_path is absolute, but nothing promises `..` is collapsed or symlinks resolved. Resolve
# BOTH sides the same way before comparing: `cd` collapses `..` and `pwd -P` resolves symlinks.
root=$(cd "${CLAUDE_PROJECT_DIR:-.}" && pwd -P) || exit 0
dir=$(cd -- "$(dirname -- "$file")" && pwd -P) || exit 0
real="$dir/$(basename -- "$file")"

case "$real" in
"$root"/*) ;; # the trailing slash stops /root-evil matching
*) exit 0 ;;
esac
case "$real" in
*/.git/*|*/.env|*/.env.*|*/vendor/*|*.pem|*.key|*id_rsa*) exit 0 ;;
esac
# Pint governs `backend/` only, and it is the only linter installed on disk here. Dart is left
# out on purpose: `flutter analyze` is a whole-project pass costing seconds per call, and the
# Dart language server already surfaces the same diagnostics without a hook.
case "$real" in
*.php) ;;
*) exit 0 ;;
esac

bin=''
for candidate in "backend/vendor/bin/$TOOL" "vendor/bin/$TOOL"; do
if [ -x "$root/$candidate" ]; then bin="$root/$candidate"; break; fi
done
[ -n "$bin" ] || bin=$(command -v "$TOOL") || exit 0

# Run from `backend/`, which is where a `pint.json` would be read from if one is ever added.
# `--` is not passed: pint takes the path as a plain argument.
output=$(cd "$root/backend" && "$bin" $CHECK_ARGS "$real" 2>&1)
status=$?
[ "$status" -eq 0 ] && exit 0

printf '%s' "$output" | head -c 4000 | jq -Rs --arg f "$real" \
'{hookSpecificOutput: {hookEventName: "PostToolUse",
additionalContext: ("Pint failed on \($f):\n" + .)}}'
exit 0
23 changes: 23 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"enabledPlugins": {
"fluttersdk@fluttersdk-marketplace": true
},
"worktree": {
"baseRef": "fresh"
},
"hooks": {
"PostToolUse": [
{
"matcher": "^(Write|Edit|MultiEdit)$",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/lint-changed.sh",
"timeout": 60
}
]
}
]
}
}
11 changes: 8 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@ That override file is also why a green local run can be a red CI: with it, this
## One task, one worktree, one PR

- Branch from `main` as `feature/<slug>` or `fix/<slug>`, and work in a worktree under `.claude/worktrees/<slug>`.
- A fresh worktree lacks three gitignored files it needs in order to run: `pubspec_overrides.yaml`, `backend/.env`, `.artisan/plugins.json`. Two mechanisms copy them from the main worktree and neither covers every path on its own: `.worktreeinclude` runs when Claude Code creates the worktree, `bin/check` on its first run there. Do not hand-author them.
- A fresh worktree lacks three gitignored files it needs in order to run: `pubspec_overrides.yaml`, `backend/.env`, `.artisan/plugins.json`. Two mechanisms copy them from the main worktree and neither covers every path on its own: `.worktreeinclude` runs when Claude Code creates the worktree, `bin/check` on its first run there. Both carry all three now; `.worktreeinclude` used to carry only the overrides, so the `EnterWorktree` path silently shipped a worktree with no `backend/.env` and no plugin commands. Do not hand-author them.
- Without `pubspec_overrides.yaml` nothing errors: the siblings resolve from pub.dev and the suite passes against the PUBLISHED packages while the diff under review is of the local ones. `bin/check` refuses rather than measuring the wrong thing. A FORK has no sibling checkouts and hosted resolution is correct there, which is what `CHECK_ALLOW_HOSTED=1 bin/check` is for; inside this workspace, reaching for it means you are about to certify the wrong packages.
- The paths inside `pubspec_overrides.yaml` must be ABSOLUTE. A worktree lives at `.claude/worktrees/<slug>`, so the conventional relative `../magic` resolves to `.claude/worktrees/magic` and version solving fails on the first path dependency. That failure is loud, unlike the one above it.
- Land the work as a PR. A suite that only ran on one machine is not evidence.

## Verifying a change

`bin/check` is the gate. It fans the suites out across cores and prints one line per job:

- `bin/check` runs `flutter analyze`, `flutter test`, `pint --test`, and the PHP suite.
- `bin/check` runs `flutter analyze`, the design-token scan, the component-registry check, the overrides-parser shape table, `flutter test`, `pint --test`, and the PHP suite.
- `bin/check --fast` runs only the static passes.
- `bin/check flutter|backend` scopes it to one half.

Two gates are NOT in `bin/check`: the `.github/` instruction mirrors and the package skill copies are checked by CI, so a stale one passes locally and blocks the merge there. Run `bin/sync-instructions` after editing AGENTS.md or a rule, and `bin/sync-skills` after pulling a sibling package.

A `.php` file Claude writes is linted at that moment by a `PostToolUse` hook (`.claude/hooks/lint-changed.sh`), which reports a `pint --test` failure back into the session instead of letting it cost a CI round. It is registered in `.claude/settings.json` rather than `settings.local.json`, because the latter is gitignored and a worktree would therefore never have it. The hook exits silently when `backend/vendor/` is absent, which is a real state in a fresh clone rather than a fault.

A green suite is the floor, not the finish line. Anything a person clicks gets driven for real with `fluttersdk_dusk` against a running Chrome, at desktop and at mobile width both, because the shell swaps widget trees at `lg` (1024px) and each side can break alone. `docs/verification-loop.md` is the procedure: the three layers, how to boot the app, how to resize a viewport correctly, and the measurement traps that produce confident wrong answers.

## Running it
Expand All @@ -45,7 +50,7 @@ A green suite is the floor, not the finish line. Anything a person clicks gets d

## Off-limits

- Generated files are regenerated, never edited: `lib/config/wind_theme.g.dart` (`design:sync`), `lib/preview/_previews.g.dart` (`previews:refresh`), `lib/app/commands/_index.g.dart` (`commands:refresh`), `.artisan/plugins.json`, and everything `bin/sync-instructions` writes under `.github/`.
- Generated files are regenerated, never edited: `docs/component-registry.md` (`bin/sync-registry`), `.github/skills/{magic-framework,wind-ui}/SKILL.md` (`bin/sync-skills`, each carrying the hash CI checks it against), `lib/config/wind_theme.g.dart` (`design:sync`), `lib/preview/_previews.g.dart` (`previews:refresh`), `lib/app/commands/_index.g.dart` (`commands:refresh`), `.artisan/plugins.json`, and everything `bin/sync-instructions` writes under `.github/`.
- `backend/vendor/`, `build/`, `.dart_tool/`.
- The fluttersdk packages are separate repositories. Reading them is expected; changing one is a PR in that repo under its own rules. `design:sync`, `design:lint`, `make:component`, and `previews:refresh` are `magic`'s commands, not this project's, and there is no `magic_example:artisan`.

Expand Down
Loading