crewforth 3.0.3 → 3.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/README.md +3 -3
  3. package/VERSION +1 -1
  4. package/adopt.sh +86 -34
  5. package/kit/CLAUDE.md +1 -1
  6. package/kit/README.md +1 -1
  7. package/kit/agents/crew-commit-agent.md +7 -3
  8. package/kit/agents/crew-frontend-expert.md +4 -4
  9. package/kit/agents/crew-performance-expert.md +1 -1
  10. package/kit/agents/crew-review-agent.md +1 -1
  11. package/kit/eval/doctor.sh +57 -3
  12. package/kit/eval/golden-routing.txt +13 -0
  13. package/kit/eval/lib/git-bash.sh +11 -2
  14. package/kit/eval/parser-conformance.sh +1 -0
  15. package/kit/eval/routing-eval.sh +7 -3
  16. package/kit/eval/smoke-test.sh +2545 -214
  17. package/kit/hooks/commit-msg +1 -1
  18. package/kit/hooks/context-usage.sh +7 -1
  19. package/kit/hooks/guard-bash.sh +1608 -103
  20. package/kit/hooks/guard-commit-scan.sh +138 -19
  21. package/kit/hooks/guard-powershell.sh +89 -15
  22. package/kit/hooks/guard-write.sh +98 -22
  23. package/kit/hooks/no-bash-guard.ps1 +71 -0
  24. package/kit/hooks/pre-commit +5 -2
  25. package/kit/hooks/prompt-approval.sh +484 -0
  26. package/kit/hooks/route-hint.sh +13 -5
  27. package/kit/hooks/session-rehydrate.sh +18 -0
  28. package/kit/hooks/skill-trust.sh +17 -2
  29. package/kit/owned-blobs.tsv +43 -0
  30. package/kit/settings.json +20 -4
  31. package/kit/skills/a11y/SKILL.md +1 -0
  32. package/kit/skills/crew-doctor/SKILL.md +2 -2
  33. package/kit/skills/frontend/SKILL.md +3 -3
  34. package/kit/skills/frontend-flutter/SKILL.md +94 -0
  35. package/kit/skills/frontend-flutter/references/adaptive-and-accessible.md +55 -0
  36. package/kit/skills/frontend-flutter/references/platform-async-release.md +59 -0
  37. package/kit/skills/frontend-flutter/references/rebuilds-and-performance.md +36 -0
  38. package/kit/studio/server/hooks/studio-gate.sh +35 -1
  39. package/kit/studio/server/index.js +14 -4
  40. package/kit/studio/server/lib/graph.js +64 -3
  41. package/kit/studio/server/lib/kit.js +16 -2
  42. package/kit/studio/server/lib/permissions.js +77 -2
  43. package/kit/studio/server/lib/projects.js +60 -7
  44. package/kit/studio/server/lib/session.js +16 -2
  45. package/kit/studio/web/app.js +1765 -573
  46. package/kit/studio/web/approvals.js +124 -0
  47. package/kit/studio/web/canvas.js +932 -885
  48. package/kit/studio/web/chat.js +322 -145
  49. package/kit/studio/web/convo.js +174 -0
  50. package/kit/studio/web/dock.js +221 -0
  51. package/kit/studio/web/graph-plan.js +457 -0
  52. package/kit/studio/web/icons/builtin.svg +1 -0
  53. package/kit/studio/web/index.html +116 -50
  54. package/kit/studio/web/inspect.js +110 -0
  55. package/kit/studio/web/list-plan.js +112 -0
  56. package/kit/studio/web/list.js +157 -0
  57. package/kit/studio/web/liveness.js +66 -0
  58. package/kit/studio/web/nav.js +237 -0
  59. package/kit/studio/web/newsession.js +235 -0
  60. package/kit/studio/web/press.js +65 -0
  61. package/kit/studio/web/style.css +1281 -1014
  62. package/kit/studio/web/theme.js +43 -0
  63. package/kit/studio/web/timeline-plan.js +291 -0
  64. package/kit/studio/web/timeline.js +400 -0
  65. package/kit/studio/web/tokens.css +131 -0
  66. package/kit/studio/web/toolbar-fit.js +35 -0
  67. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -5,6 +5,217 @@ Crewforth was named Claude Starter Kit until 3.0.0.
5
5
  Notable changes to this project are recorded here. Format follows [Keep a Changelog](https://keepachangelog.com/en/),
6
6
  versioning follows [SemVer](https://semver.org/).
7
7
 
8
+ ## [Unreleased] — 3.1.0
9
+
10
+ ### Security
11
+
12
+ - **A git subcommand the shell fills in walked past every gate.** Each rule about a git command finds it by its name,
13
+ and with an expansion inside the word the name is not in the text: `git com${z}mit`, `c=commit; git "$c"`, `git
14
+ $(printf com)mit`, a backtick, and the same for `git pu${z}sh --force`, `git re$(:)set --hard`, `git cl${z}ean -fdx`
15
+ and `git co${z}nfig core.hooksPath …`. Measured: 20 of 22 such calls passed silently, and git ran them. A git call
16
+ whose subcommand holds an expansion is refused now, in every session. In PowerShell, git started through
17
+ `System.Diagnostics.Process` (`[Diagnostics.Process]::Start('git','commit -n …')`, a `ProcessStartInfo`) is refused
18
+ as well: its arguments are one string no rule reads. An expansion anywhere else in a git call is untouched: of 4986
19
+ real commands that hold git, 2850 have one, none in the subcommand, and none changed its verdict.
20
+ - **In PowerShell, a force push handed to git as arguments was not read.** `Start-Process git -ArgumentList
21
+ 'push','--force',…` and `& $g push --force …` run the same git command as the plain form; of 23 such forms of
22
+ `push --force` (`-f`, `+ref`, `--force-with-lease`), `reset --hard`, `clean -f` and `core.hooksPath`, 21 passed. The
23
+ arguments are judged now as the git command they make, by the same rules that judge the plain form, and the refusal
24
+ says what was read.
25
+ - **A `git commit` is read the way the shell and git read it.** The rules for `--no-verify`, `--amend` and a commit
26
+ that takes its content from the working tree looked for those words and walked the command token by token. Each of
27
+ these reached the approval prompt with no rule firing (and with `CLAUDE_GIT_OK` ran with nobody asked), measured by
28
+ running them: `git commit -n` and `--no-verif` skip the hooks; `--amen` amends; `-m x 2>&1 -a`, `-mxm b.txt`,
29
+ `-m 'a"' -a ; echo 'b"'` and `-F - <<END -a` commit the working tree; `cd ../other && git commit` and
30
+ `GIT_INDEX_FILE=… git commit` commit something the review record does not describe; `bash -c 'git commit -am x'`
31
+ hides the whole command. The command is now read left to right — quotes as the shell pairs them, here-document
32
+ bodies as data, redirections as redirections — and every `git commit` in it against git's own option table,
33
+ wherever it sits (braces, `if`, a wrapper, a quoted command word). Refused with the reason: an abbreviated or
34
+ unknown option, an argument the shell would expand into something else, a `-c` setting or a `GIT_` / `HOME`
35
+ variable that changes what git reads, and a commit after a `cd` into another repository. Nothing that was refused
36
+ before is let through.
37
+ - **`core.hooksPath` could be switched off without `git config`.** `printf '[core]\n\thooksPath = /dev/null\n' >> .git/config`
38
+ passed, and after it a commit from your own terminal skipped the trace and secret scans. `.git/config` (a worktree's
39
+ and a submodule's too), your own `~/.gitconfig` and `~/.config/git/config`, and `.claude/git-shim` are gate files now: a shell write and the file tools are refused,
40
+ reading is not. `git config include.path`, `includeIf` and `GIT_CONFIG_` variables that carry `hooksPath` are
41
+ refused as well, and so is an abbreviated `--no-verify` on any git command.
42
+ - **A large commit command could run past the gate's timeout, and a hook that times out does not block.** `guard-bash.sh`
43
+ stripped and padded the whole command with `${text//x/y}`, ten times over, and bash rebuilds the string for every
44
+ match: a 46 KB commit whose message is a quote-dense here-document took 335 s on macOS and 14 s at 18 KB on Windows,
45
+ against a 600 s timeout. The substitutions are done a piece at a time now; a 30 KB commit of that shape is judged in
46
+ about a second. Other shapes still cost the square of their size (a 64 KB commit took 154 s to read), so a command
47
+ that holds a `git commit` is read up to 32768 bytes and refused unread above, with the advice to put the message in
48
+ a file and use `git commit -F <file>`. And whatever the command, taking it out of the hook's JSON costs the square of
49
+ its size (256 KB of an escape-dense command: 49 s, 512 KB: 200 s), in `guard-bash.sh` and `guard-commit-scan.sh` alike,
50
+ so a Bash or PowerShell call above 262144 bytes is refused before it is parsed, by size alone, with the advice to put
51
+ the long content in a file. The largest of 12387 real commands is 31639 bytes. Write and Edit are not limited.
52
+ - **A Write or Edit of a few megabytes ran `guard-write.sh` past its timeout, a gate file included.** The look for a
53
+ second path key walked the content with an expansion that costs the square of the size when nothing matches: 1 MB
54
+ took 24 s on macOS, and a 6 MB Write to `.claude/hooks/guard-bash.sh` was refused after 636 s, which is after the
55
+ 600 s timeout, so it was not refused. The key search walks the payload a piece at a time now: the same Write is
56
+ refused in 2.6 s, and an ordinary 6 MB file passes in the same time. The file tools stay unlimited in size.
57
+ - A review record could vouch for any staged diff once `git config diff.external true` was set: the staged diff then
58
+ prints nothing, and every change got the same id. The id is computed with `--no-ext-diff --no-textconv`.
59
+ - A recursive forced delete of `.` or `..` passed in both shells (`rm -rf .`, `rm -rf ..`,
60
+ `Remove-Item -Recurse -Force .`): the rule looked for `/`, `*` or `~` in the target, and these carry none. They are
61
+ stopped now, in any flag spelling, with `sudo`, and across a backslash line break.
62
+ - In PowerShell a path was a target only when it began with a drive letter, a UNC prefix or `$HOME`:
63
+ `Remove-Item -Recurse -Force src\app` and `"$env:TEMP\x"` passed, while `rm -rf src/app` was stopped. A target
64
+ with a path separator is stopped now, and so is a recursive forced removal fed by a pipeline.
65
+ - **With no Git Bash, nothing guarded the session.** On Windows, when Claude Code finds no Git Bash, every Crewforth
66
+ gate fails to start (they are bash scripts; the failure does not block), and PowerShell is the only shell. One hook
67
+ now names no shell: Claude Code runs it through bash where there is one — it leaves at once, starting no process —
68
+ and through PowerShell where there is none, and there it stops every call of the Bash, PowerShell, Write, Edit and
69
+ NotebookEdit tools, with the `CLAUDE_CODE_GIT_BASH_PATH` to set. Reading and searching keep working.
70
+ - Two spellings of `-Recurse` and `-Force` walked past the PowerShell delete rule, which wanted a hyphen and then
71
+ whitespace: a value after a colon (`-Recurse:$true -Force:$true`, `-Rec:1 -Fo:1`), and an en dash, an em dash or a
72
+ horizontal bar in place of the hyphen. PowerShell 5.1 deletes the tree with each of them (measured). They are
73
+ recognised now, whether the dash arrives as itself or as a JSON `\u` escape.
74
+
75
+ ### Added
76
+
77
+ - **`frontend-flutter`: a stack layer for Flutter**, beside `frontend-rn-expo`. It applies only in a project whose
78
+ `pubspec.yaml` depends on the Flutter SDK, and `crew-frontend-expert` adds it on top of `frontend` there. It leaves
79
+ state management, routing and the lint set to the project, and holds what Flutter itself asks for: the layers and
80
+ rules of its architecture guide, the rebuild cost model, layout by window size and never by device type, isolates,
81
+ platform channels, generated localisations, the mix of unit, widget, integration and golden tests, measuring in
82
+ profile mode on a real device, and a release without a secret in it. Its done-definition is `dart format
83
+ --set-exit-if-changed .`, `flutter analyze`, `flutter test` and the accessibility guideline test. Every rule was
84
+ checked against Flutter's and Dart's own documentation; the sources are in the skill's `references/`. 40 skills now.
85
+ - **In `auto` and `dontAsk`, your own message approves a commit or a push.** Those modes answer a permission prompt
86
+ with software, so the commit gate fails closed there and the only way through was to switch mode for every commit.
87
+ Now a message that is nothing but `approve: commit`, `approve: push` or `approve: commit+push` (`onay:` works too)
88
+ is recorded by a new hook, `prompt-approval.sh`, with what git reports at that moment: the tree of what is staged
89
+ and `HEAD` for a commit; `HEAD`, the branch, its remote and the address that remote pushes to for a push; and the
90
+ session you wrote in. The confirmation you see names that address. `guard-bash.sh` then allows one call and no other: `git commit -m …` alone (a single-quoted
91
+ message, or one read from a here-document with a quoted delimiter; `-q`, `-s`, `-v`), or
92
+ `git push <remote> <branch>` alone. A changed index, a moved `HEAD`, a second commit, another branch, remote or
93
+ address, a refspec, another option, a `cd`, a pipe or a second command is refused, with the reason. The record
94
+ lasts 30 minutes and your next message ends it. It opens neither the review-before-commit rule nor any destructive
95
+ command, and `plan` and `bypassPermissions` stay closed.
96
+ A session cannot produce the approval it would use: the file tools and any shell command that names the record
97
+ are refused, and so are a command that feeds the hook a payload of its own and, in those two modes, one that
98
+ starts a Claude Code session continuing this one. Those rules match names, as the rules for the gate files do.
99
+ - **Studio: two more views of a session.** The **Timeline** draws the same agents against the clock, one row each,
100
+ with a drawer for an agent's last error; the **List** sorts them by what needs someone, and is the view a narrow
101
+ window opens on. `g`, `t` and `l` switch between Graph, Timeline and List. The time an agent waited for you is drawn
102
+ only for a session Studio started, and only since the panel started; for any other session the Timeline says
103
+ `Not measured: this session's approvals are not seen by Studio`.
104
+ - **Studio: an approval dock.** A tool call in a session Studio started waits along the bottom of the panel,
105
+ whichever session and view are on screen, with the agent that asked, the tool and its input: Allow once, Allow the
106
+ tool for this session, or Deny (`a`, `s`, `d`); denied at 45 s when nobody answers. A session allowance is listed
107
+ in the inspector's Gates tab and can be revoked there.
108
+ - **Studio: a New session panel.** The project, the permission mode and an optional first message. Plan is the
109
+ default; Accept edits and Default are the other two, and modes that skip Crewforth's gates are not offered.
110
+ - **Studio: a layout for a narrow window.** Below 640 px the panel is one column: the navigator is a drawer, the
111
+ inspector and the conversation take the whole stage and stop above the dock. Checked in a narrow desktop browser
112
+ window; not measured on a phone.
113
+
114
+ ### Changed
115
+
116
+ - **After a final release, npm's `next` no longer names something older than `latest`.** A final moved `latest`
117
+ only, so `npx crewforth@next` went on installing the rc before it. The release now moves `next` to the final when
118
+ what `next` names is older; an rc of a later version stays where it is.
119
+ - **A command that names a gate file must be one that reads it.** The rule that guards the hooks, `settings.json`,
120
+ the rulebook and the git hooks looked for a write verb (`rm`, `mv`, `cp`, `tee`, …) in front of the path, and a
121
+ list of verbs is never complete: of 39 ways to write or delete a gate file, 32 passed (in 3.0.3 as well: `curl -o`,
122
+ `wget -O`, `sort -o`, `git log --output=`, `tar -C`, `unzip -d`, `unlink`, `shred`, `touch`, `patch`, an editor,
123
+ `find … -delete`, `git checkout <rev> -- <file>`). The question is turned round. A command that names a gate path
124
+ passes when it is a reader (`grep`, `cat`, `head`, `tail`, `wc`, `ls`, `less`, `diff`, `jq`, `sed` without `-i`,
125
+ `find` without `-delete` or `-exec`, …), a runner of a hook script (`bash .claude/hooks/<x>.sh`), `chmod +x` on
126
+ one, or a git command that only reads or stages (`status`, `log`, `diff`, `show`, `add`, `commit`, a `git config`
127
+ that reads); anything else is refused, with the reason. A path that is hidden is followed where the text allows
128
+ it: quoting inside the name, the `.claude` folder itself, a glob, a variable or a loop that holds the path, a `cd`
129
+ into the folder, a command substitution, `xargs`. `bash -c`, `eval`, `source`, a changed `PATH`, an alias or a
130
+ function in such a call, and an interpreter fed a here-document that names a gate file are refused.
131
+ **Reading is freer than before:** `grep -n rm .claude/hooks/guard-bash.sh` and `cat /Users/ed/p/.claude/hooks/x`
132
+ were refused for the word `rm` and the folder `ed`, and pass now. Bash tool; a PowerShell call keeps the verb
133
+ rule, without counting a verb that is a name in a path.
134
+ - **A quoted argument is no longer read as a command.** `git commit -m "drop the rm -rf /tmp/build step"` deletes
135
+ nothing and `claude -p "… git push --force origin main"` pushes nothing, yet the §4.5 rules read the command as
136
+ text and refused both (measured: 14 of 20 such commands). The gate now takes the quoted argument out before those
137
+ rules read, for a short list of commands that do not run their argument: `git commit` / `git tag -m`, `gh pr|issue|release
138
+ --title` / `--body`, `claude -p`, the grep family, and `echo` / `printf` when the call has no pipe and no
139
+ redirection. A `claude -p` prompt is taken out only when nothing in the call changes the directory (`cd`, `pushd`,
140
+ `popd`, `git -C`) and its other options are `--output-format`, `--model` or `--max-turns`: the session it starts is
141
+ judged by the gates of where it starts and with the settings it is given. The
142
+ word must be wholly quoted, and a double-quoted one must hold no `$(` and no backtick. Everything else is as
143
+ before: the same words outside the quotes, after the argument, printed into a shell or a file, handed to `bash
144
+ -c`, `eval`, `ssh` or `xargs`, stored with `printf -v`, or anywhere in a call that changes what a command word
145
+ means (`alias`, `hash`, a function, `PATH=`) are refused. Bash tool only.
146
+ - **Trusting or declining a component is confirmed.** `skill-trust.sh --trust-one` and `--decline-one` printed nothing
147
+ on success, and in the field a session that had just declined a skill said it had not checked the record. Each
148
+ prints one line now (the component, the file, the first characters of the digest it holds), and only after the
149
+ line is read back from the file; when the answer could not be written the command exits 1 and says so.
150
+ - **Reading `core.hooksPath` is no longer refused on Windows.** `git config --get core.hooksPath` is how a person
151
+ checks that the gate is armed, and a read is let through only when the payload holds no escape the reader drops.
152
+ That was asked of the whole payload, and a Windows `cwd` is full of them once JSON-encoded (`C:\\repos\\app` holds
153
+ a backslash and an r), so in such a directory every read was refused as tampering. It is asked of the command
154
+ now, with every escaped backslash taken out first. Of 250 measured cells (7 reads, 18 writes, a POSIX `cwd` and
155
+ four Windows ones, Bash and PowerShell) 40 reads changed from refused to passed; every write, a real CR, form
156
+ feed or backspace escape in the command among them, is refused as before.
157
+ - **A project's own hook chain survives the second update.** The first run puts `.claude/git-shim` in front of it
158
+ and points git there; the second read that as "no chain" and pointed git at `.claude/hooks`, so the project's own
159
+ `pre-commit` never ran again, with nothing said (a project with `.husky` was found again, as `.husky` and not the
160
+ `.husky/_` it had). The chain is read back from the shim.
161
+ - **The doctor knows `.claude/git-shim`.** On a project with hooks of its own it answered "not Crewforth's hooks" and
162
+ advised `git config core.hooksPath .claude/hooks`, the command that disconnects the project's hooks. A shim that
163
+ runs Crewforth's hook is healthy; where a chain of the project's own exists the advice is the update, which
164
+ keeps it.
165
+ - **The session is told when the git hooks are not connected.** An install made before `git init`, and every clone
166
+ of a repository that shares `.claude/`, starts with `core.hooksPath` unset: a commit typed in a terminal is then
167
+ scanned by nothing. Crewforth does not change git's configuration by itself; the session start says so, and the
168
+ fix stays the user's.
169
+ - **The doctor's Git Bash advice ends with "close the terminal and Claude Code and open them again".** An open
170
+ terminal keeps the old value of `CLAUDE_CODE_GIT_BASH_PATH`; in the field the fix was right and the proof came two
171
+ rounds late.
172
+ - **The doctor reads §4.2's list.** `trace-blocklist.txt` ships the vendor section as a commented placeholder, and
173
+ until a name stands there that rule looks for nothing: the doctor says so, as information. The placeholder made
174
+ active as it shipped is a warning.
175
+ - **An update no longer loses an edit of the three files Crewforth owns.** `adopt` and `update` rewrote
176
+ `.claude/AGENT_TEMPLATE.md` and `.claude/DISCIPLINE.md` on every run with no look at what was there, so a copy the
177
+ user had edited was gone without a word (`README.md` was already kept when it differed). One rule for the three now:
178
+ a copy that is this version's, or exactly what an earlier release shipped (line endings aside), is refreshed and
179
+ nothing is kept; anything else goes to `.claude/.legacy-backup/<time>/` first, and the update names it. What "an
180
+ earlier release shipped" is comes from `kit/owned-blobs.tsv`, generated from the release tags: 6 versions of
181
+ AGENT_TEMPLATE.md, 8 of README.md and 24 of DISCIPLINE.md, so an untouched copy from any of them does not pile up
182
+ in the backup directory on each update. A copy that cannot be backed up is left as it is.
183
+ - **A gate that times out does not block, so the gates got faster and their timeout longer.** Claude Code runs the
184
+ tool call when a `PreToolUse` hook reaches its timeout; one field session on 3.0.0 showed nine such calls in a day.
185
+ - Every read-only Bash call, every Write and Edit, and every PowerShell call now costs **no process at all** in the
186
+ four gates (it was 5 in `guard-bash.sh`, 7 in `guard-commit-scan.sh` and 4 in `guard-write.sh`, counting the
187
+ `$( )` subshells the 3.0.1 count did not see). On Git Bash a process costs 62–135 ms, and seconds on a loaded
188
+ machine.
189
+ - The four gates' timeout is 600 s, Claude Code's own default; Crewforth had set 60. The other hooks keep 60.
190
+ - **Studio has a new interface.** The graph is a left-to-right tree: the session on the left, its agents to the right,
191
+ a workflow run as one group, and a strip above the canvas naming what needs attention. Selecting an agent opens an
192
+ inspector with four tabs (Overview, Conversation, Gates, Stats), and the inspector, the conversation and the New
193
+ session panel share one place on the right.
194
+ - **Allow in Studio is an explicit answer to Claude Code.** In the Accept edits and Default modes the approval hook
195
+ returns `permissionDecision: allow`; before, it only stayed silent, and Claude Code then refused a write in Default
196
+ mode (measured on Windows). In Plan it still says nothing. A Crewforth gate or a `deny` rule refuses the call all
197
+ the same (measured on Windows, in a real session), and the conversation says who refused it. Known limit: the dock
198
+ can ask about a call that another gate refuses anyway.
199
+
200
+ ## [3.0.4] — 2026-10-05
201
+
202
+ ### Security
203
+
204
+ - **A command continued on the next line is read as one command.** A backslash at the end of a line continues it, and
205
+ the gates read each line on its own: `rm -f \` + newline + `.claude/hooks/guard-bash.sh` deleted a gate file, `rm \`
206
+ + newline + `-rf …`, `git push \` + newline + `--force` and `curl … \` + newline + `| bash` were not refused, and
207
+ `git \` + newline + `push` or `commit` reached neither the approval prompt nor the review gate, nor the commit
208
+ content scan (measured on 3.0.3, macOS and Windows). The command is now joined before anything reads it, the way
209
+ the shell joins it: an odd number of backslashes continues the line, a PowerShell call continues with a backtick,
210
+ and a line that may be a comment is read both ways.
211
+ - **A gate path spelled with backslashes is the gate path.** In a PowerShell call `Remove-Item
212
+ .claude\hooks\guard-bash.sh` and `Set-Content .claude\settings.json …` were not recognised and passed.
213
+ - **A gate that stops on an error of its own refuses the call.** Claude Code blocks a tool call on exit 2 only, so a
214
+ hook that died with status 1, or went on past the rule that broke and left with 0, had allowed the call. Each
215
+ PreToolUse gate (`guard-bash.sh`, `guard-commit-scan.sh`, `guard-write.sh`, `guard-powershell.sh`) now answers 2,
216
+ and says the error was its own, when it leaves with a status that is neither 0 nor 2 and when a command of it is
217
+ abandoned in the middle. Not covered: an error inside a command substitution, and a command that is not found.
218
+
8
219
  ## [3.0.3] — 2026-10-03
9
220
 
10
221
  ### Security
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center"><img src="https://raw.githubusercontent.com/Crewforth/crewforth/main/assets/logo.svg" alt="Crewforth" width="420"></p>
2
2
 
3
- **Crewforth is your engineering crew for Claude Code.** It adds subagents, skills, commands and hooks to Claude Code: 12 specialist agents that each own a domain, 39 skills that hold the method, 10 commands you start with `/crew-…`, and hooks that enforce the rules that matter.
3
+ **Crewforth is your engineering crew for Claude Code.** It adds subagents, skills, commands and hooks to Claude Code: 12 specialist agents that each own a domain, 40 skills that hold the method, 10 commands you start with `/crew-…`, and hooks that enforce the rules that matter.
4
4
 
5
5
  ```bash
6
6
  npx crewforth init # set up a new project
@@ -9,7 +9,7 @@ npx crewforth add <agent|skill> # copy one agent or skill into ./.claude
9
9
  npx crewforth studio # open the Studio panel; installs nothing
10
10
  ```
11
11
 
12
- Then open Claude Code and run `/crew-doctor` to confirm the setup. Requires bash and git; on Windows, use Git Bash (the gates run only when Claude Code finds it; `/crew-doctor` says when it does not).
12
+ Then open Claude Code and run `/crew-doctor` to confirm the setup. Requires bash and git; on Windows, use Git Bash (the gates run only when Claude Code finds it; when it does not, the tools that write or run commands are stopped and `/crew-doctor` names the path to set).
13
13
 
14
14
  [![npm](https://img.shields.io/npm/v/crewforth?style=flat-square&color=6D28D9)](https://www.npmjs.com/package/crewforth)
15
15
  [![License](https://img.shields.io/badge/license-MIT-5c6472?style=flat-square)](https://github.com/Crewforth/crewforth/blob/main/LICENSE)
@@ -29,7 +29,7 @@ Then open Claude Code and run `/crew-doctor` to confirm the setup. Requires bash
29
29
  ## What you get
30
30
 
31
31
  - **12 specialist agents**, from `crew-planner` to `crew-commit-agent`, each owning one domain.
32
- - **39 skills** holding the method: testing, migrations, API contracts, observability, accessibility, deployment.
32
+ - **40 skills** holding the method: testing, migrations, API contracts, observability, accessibility, deployment.
33
33
  - **10 commands**, each started with its `/crew-…` name: `/crew-plan`, `/crew-review`, `/crew-ship`, `/crew-handoff`, `/crew-brainstorm`, `/crew-update`, `/crew-doctor`, `/crew-gates`, `/crew-skill`, `/crew-studio`.
34
34
  - **Safe adoption.** `adopt` lands everything on a branch, staged and uncommitted; `main` is never touched.
35
35
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.3
1
+ 3.1.0
package/adopt.sh CHANGED
@@ -220,9 +220,10 @@ _mt() { # $1 = English text (the key); further args fill %s; result in _M
220
220
  ".NET pattern skill renamed: devarch-module -> cqrs-aop-module (content kept)") s=".NET desen skill'inin adı değişti: devarch-module -> cqrs-aop-module (içerik korundu)" ;;
221
221
  "⚠️ both devarch-module and cqrs-aop-module are present — nothing moved; remove the old one when ready") s='⚠️ devarch-module ve cqrs-aop-module ikisi birden var — hiçbir şey taşınmadı; hazır olduğunuzda eskisini silin' ;;
222
222
  "AGENT_TEMPLATE.md written (owned by Crewforth; refreshed on every update)") s="AGENT_TEMPLATE.md yazıldı (Crewforth'un dosyası; her güncellemede yenilenir)" ;;
223
- "README.md differed from this version's — the old one is kept in %s/README.md") s="README.md bu sürümünkinden farklıydı — eskisi %s/README.md içinde saklandı" ;;
223
+ '%s is not a copy Crewforth shipped before this version — it is kept in %s/%s') s="%s, Crewforth'un bu sürümden önce gönderdiği bir kopya değil — %s/%s içinde saklandı" ;;
224
+ '%s could not be checked against what Crewforth shipped (git or the shipped list is missing) — the old copy is kept in %s/%s') s="%s, Crewforth'un gönderdikleriyle karşılaştırılamadı (git ya da gönderilen liste yok) — eski kopya %s/%s içinde saklandı" ;;
224
225
  'README.md refreshed (describes this version of Crewforth)') s="README.md yenilendi (Crewforth'un bu sürümünü anlatıyor)" ;;
225
- "README.md differs from this version's and could not be backed up — left as it is") s='README.md bu sürümünkinden farklı ve yedeklenemedi — olduğu gibi bırakıldı' ;;
226
+ '%s is not a copy Crewforth shipped before this version and could not be backed up — left as it is') s="%s, Crewforth'un bu sürümden önce gönderdiği bir kopya değil ve yedeklenemedi — olduğu gibi bırakıldı" ;;
226
227
  "pre-2.0 install (profile=%s): profile pruning was removed — completing the install") s='2.0 öncesi kurulum (profile=%s): profil budama kalktı — eksikler tamamlanıyor' ;;
227
228
  "pre-2.0 install (profile=%s): nothing was missing — the full set was already present") s='2.0 öncesi kurulum (profile=%s): eksik yok — tam set zaten kuruluydu' ;;
228
229
  "overlap: %s -> skill '%s' already present (kept); original re-backed up") s="çakışma: %s -> '%s' skill'i zaten var (korundu); orijinal yeniden yedeklendi" ;;
@@ -523,11 +524,29 @@ fi
523
524
  rowm 'git' "$GITKIND" # every GITKIND value is a literal with its own table row
524
525
 
525
526
  # existing hook system (decision #5 — single-hooksPath clash with husky/lefthook)
526
- HOOKSYS="none"
527
+ HOOKSYS="none"; SHIM_CHAIN=""
527
528
  CURHP="$(git config --get core.hooksPath 2>/dev/null || true)"
528
529
  case "$CURHP" in
529
530
  "") : ;;
530
- .claude/hooks|.claude/git-shim) HOOKSYS="Crewforth (already armed)"; CURHP="" ;; # kit's OWN path — not a foreign chain (re-adopt must not shim itself)
531
+ .claude/hooks) HOOKSYS="Crewforth (already armed)"; CURHP="" ;; # kit's OWN path — not a foreign chain (re-adopt must not shim itself)
532
+ .claude/git-shim)
533
+ # Crewforth's shim, from an earlier run: git is pointed at it, and IT holds the project's own chain. That chain
534
+ # is read back from the shim, or the next lines would see "no chain" and point git straight at .claude/hooks:
535
+ # measured, the second update of a project with core.hooksPath=myhooks left its own pre-commit never running
536
+ # again, with nothing said. (A project with .husky was found again by its directory; one with any other
537
+ # directory was not.)
538
+ HOOKSYS="Crewforth (already armed)"; CURHP=""
539
+ for _sf in .claude/git-shim/pre-commit .claude/git-shim/commit-msg; do
540
+ [ -f "$_sf" ] || continue
541
+ while IFS= read -r _sl || [ -n "$_sl" ]; do
542
+ _sl="${_sl%$'\r'}"
543
+ case "$_sl" in 'P="'*'/$H"') _sl="${_sl#P=\"}"; _sl="${_sl%/\$H\"}"; _sl="${_sl#\$ROOT/}"
544
+ case "$_sl" in ''|.claude/hooks|.claude/git-shim) ;; *) SHIM_CHAIN="$_sl" ;; esac ;;
545
+ esac
546
+ done < "$_sf"
547
+ [ -n "$SHIM_CHAIN" ] && break
548
+ done
549
+ [ -n "$SHIM_CHAIN" ] && HOOKSYS="core.hooksPath=$SHIM_CHAIN" ;;
531
550
  *) HOOKSYS="core.hooksPath=$CURHP" ;;
532
551
  esac
533
552
  [ -d .husky ] && HOOKSYS="husky (.husky/)"
@@ -1164,36 +1183,66 @@ copy_noclobber "$SRC/studio" .claude/studio "$KIT_PRESENT"; T_ADD=$ret_add;
1164
1183
  # lives in packaging/studio-test/, outside the payload, so nothing has to be deleted here and
1165
1184
  # the count is simply what landed.
1166
1185
  chmod +x .claude/studio/server/hooks/*.sh 2>/dev/null || true
1167
- # AGENT_TEMPLATE.md — a kit-owned flat file, so it is written on every run rather than never-overwritten.
1168
- # `/crew-skill` opens with `Read .claude/AGENT_TEMPLATE.md`, and until now only start.sh copied it
1169
- # (start.sh:486). That left the command pointing at a file that does not exist on an adopted install, and
1170
- # `update` is an alias of this script (bin/cli.js:48), so a copy placed by start.sh was never refreshed
1171
- # either — it went stale from the day it landed and nothing ever noticed, because §3b iterates skills and
1172
- # agents and this is neither. Overwriting is right for the same reason DISCIPLINE.md is overwritten: the
1173
- # file states the kit's own contract, a project does not author it, and a stale contract is worse than none.
1174
- cp "$SRC/AGENT_TEMPLATE.md" .claude/ 2>/dev/null && say 'AGENT_TEMPLATE.md written (owned by Crewforth; refreshed on every update)'
1175
- # README.md — Crewforth's description of what lives under .claude/. start.sh copied it; this script never did, so an
1176
- # updated project kept the README of the version it was first installed with (RC-1 field: both projects still
1177
- # described 3.0.0). It is refreshed on every run now, and never at the cost of an edit: a copy that differs from this
1178
- # version's (line endings aside) is kept first in .claude/.legacy-backup/<time>/README.md and named.
1179
- if [ -f "$SRC/README.md" ]; then
1180
- if [ -f .claude/README.md ] && ! cmp -s "$SRC/README.md" .claude/README.md \
1181
- && ! tr -d '\r' < .claude/README.md 2>/dev/null | cmp -s "$SRC/README.md" -; then
1182
- _rbk="$LEG_BK"
1183
- if [ -z "$_rbk" ] || [ ! -d "$_rbk" ]; then
1184
- _rbk=".claude/.legacy-backup/$(date +%Y%m%d-%H%M%S)"; _b="$_rbk"; i=2
1185
- while [ -e "$_b" ]; do _b="$_rbk-$i"; i=$((i+1)); done; _rbk="$_b"
1186
- fi
1187
- if mkdir -p "$_rbk" 2>/dev/null && cp .claude/README.md "$_rbk/README.md" 2>/dev/null; then
1188
- [ -f .claude/.legacy-backup/.gitignore ] || printf '*\n' > .claude/.legacy-backup/.gitignore 2>/dev/null
1189
- say "README.md differed from this version's — the old one is kept in %s/README.md" "$_rbk"
1190
- cp "$SRC/README.md" .claude/ 2>/dev/null && say 'README.md refreshed (describes this version of Crewforth)'
1191
- else
1192
- warnm "README.md differs from this version's and could not be backed up — left as it is"
1186
+ # THE FLAT FILES CREWFORTH OWNS — AGENT_TEMPLATE.md, README.md and (in stage 3) DISCIPLINE.md. Each states the kit's own
1187
+ # contract, so it is rewritten on every run: `/crew-skill` opens with `Read .claude/AGENT_TEMPLATE.md`, the README
1188
+ # describes this version, and a stale discipline is worse than none. Until 3.1 two of the three were rewritten with
1189
+ # no look at what was there, so a copy the user had edited was lost without a word. One rule for the three now:
1190
+ # * the same as this version's (line endings aside): written, nothing said about the old one;
1191
+ # * a copy an earlier release shipped, untouched — its bytes, or its bytes with CR removed, are an id
1192
+ # kit/owned-blobs.tsv holds for that file: refreshed, nothing kept (it is in the release it came from);
1193
+ # * anything else is kept first in .claude/.legacy-backup/<time>/ and named, then refreshed: an edit of the user's,
1194
+ # or a copy from a release the list does not hold (it holds the final releases below this VERSION, so a patch
1195
+ # release made after this one, or a release candidate, is not in it). The line says what is known — "not a copy
1196
+ # Crewforth shipped before this version" — and not that somebody edited it. When git or the list is missing
1197
+ # nothing can be told, and the copy is kept the same way.
1198
+ # A copy that cannot be backed up is left as it is.
1199
+ _owned_bkdir(){ # -> LEG_BK: this run's backup directory (the legacy sweep's when it made one), created; 1 when it cannot be
1200
+ local b i=2
1201
+ if [ -z "$LEG_BK" ] || [ ! -d "$LEG_BK" ]; then
1202
+ LEG_BK=".claude/.legacy-backup/$(date +%Y%m%d-%H%M%S)"; b="$LEG_BK"
1203
+ while [ -e "$b" ]; do b="$LEG_BK-$i"; i=$((i+1)); done; LEG_BK="$b"
1204
+ fi
1205
+ mkdir -p "$LEG_BK" 2>/dev/null || return 1
1206
+ # A team that shares .claude/ should not commit the backup by accident.
1207
+ [ -f .claude/.legacy-backup/.gitignore ] || printf '*\n' > .claude/.legacy-backup/.gitignore 2>/dev/null
1208
+ return 0
1209
+ }
1210
+ _owned_shipped(){ # $1 = name -> 0 when .claude/<name> is a copy an earlier release shipped · 1 it is not · 2 cannot tell
1211
+ local f=".claude/$1" lst="$SRC/owned-blobs.tsv" h hl cr crl
1212
+ [ -f "$lst" ] && command -v git >/dev/null 2>&1 || return 2
1213
+ h="$(git hash-object --no-filters -- "$f" 2>/dev/null)" || return 2
1214
+ [ -n "$h" ] || return 2
1215
+ hl="$h"
1216
+ # CR is stripped only when every CR ends a line (a Windows editor, an autocrlf copy); no shipped copy holds one.
1217
+ # Whether there is a CR at all is asked with tr, not `grep -q`: Git for Windows' grep 3.0 answers "no" to
1218
+ # `grep -q $'\r'` on a CRLF file (measured there: rc 1 on a file where `grep -c` counts 107), and an untouched copy
1219
+ # checked out with core.autocrlf=true was then kept as if it had been edited.
1220
+ cr="$(tr -dc '\r' < "$f" 2>/dev/null | wc -c | tr -d ' ')"
1221
+ if [ "${cr:-0}" != 0 ]; then
1222
+ crl="$(grep -c $'\r$' "$f" 2>/dev/null)"
1223
+ [ "$cr" = "${crl:-x}" ] && hl="$(tr -d '\r' < "$f" 2>/dev/null | git hash-object --no-filters --stdin 2>/dev/null)"
1224
+ fi
1225
+ awk -F'\t' -v p="$f" -v a="$h" -v b="${hl:-$h}" '!/^#/ && $1 == p && ($2 == a || $2 == b) { f = 1 } END { exit !f }' "$lst"
1226
+ }
1227
+ _owned_refresh(){ # $1 = name under .claude/, $2 = a file holding this version's content -> 0 written · 1 left as it is
1228
+ local n="$1" new="$2" cur=".claude/$1" rc
1229
+ if [ -f "$cur" ] && ! cmp -s "$new" "$cur" && ! tr -d '\r' < "$cur" 2>/dev/null | cmp -s "$new" -; then
1230
+ _owned_shipped "$n"; rc=$?
1231
+ if [ "$rc" != 0 ]; then
1232
+ if _owned_bkdir && cp "$cur" "$LEG_BK/$n" 2>/dev/null; then
1233
+ if [ "$rc" = 2 ]; then say '%s could not be checked against what Crewforth shipped (git or the shipped list is missing) — the old copy is kept in %s/%s' "$n" "$LEG_BK" "$n"
1234
+ else say '%s is not a copy Crewforth shipped before this version — it is kept in %s/%s' "$n" "$LEG_BK" "$n"; fi
1235
+ else
1236
+ warnm '%s is not a copy Crewforth shipped before this version and could not be backed up — left as it is' "$n"
1237
+ return 1
1238
+ fi
1193
1239
  fi
1194
- else
1195
- cp "$SRC/README.md" .claude/ 2>/dev/null
1196
1240
  fi
1241
+ cp "$new" "$cur" 2>/dev/null
1242
+ }
1243
+ _owned_refresh AGENT_TEMPLATE.md "$SRC/AGENT_TEMPLATE.md" && say 'AGENT_TEMPLATE.md written (owned by Crewforth; refreshed on every update)'
1244
+ if [ -f "$SRC/README.md" ]; then
1245
+ _owned_refresh README.md "$SRC/README.md" && say 'README.md refreshed (describes this version of Crewforth)'
1197
1246
  fi
1198
1247
  # Report the migration by what LANDED, not by what was missing: a component the payload lists can still be kept
1199
1248
  # out (EXCL_S), and must not be announced as restored when it was.
@@ -1482,8 +1531,10 @@ h1m "Stage 3 — activate the Crewforth discipline (without touching the project
1482
1531
  # everything above the sentinel line. Contains NO @import (leaf) -> no 4-hop trap.
1483
1532
  if [ -f "$SRC/CLAUDE.md" ]; then
1484
1533
  kit_require_sentinel "$SRC/CLAUDE.md"
1485
- kit_discipline_of "$SRC/CLAUDE.md" > .claude/DISCIPLINE.md
1486
- say 'DISCIPLINE.md written (Crewforth discipline only; the project template stays out of it)'
1534
+ kit_discipline_of "$SRC/CLAUDE.md" > .claude/DISCIPLINE.md.crew-new
1535
+ _owned_refresh DISCIPLINE.md .claude/DISCIPLINE.md.crew-new \
1536
+ && say 'DISCIPLINE.md written (Crewforth discipline only; the project template stays out of it)'
1537
+ rm -f .claude/DISCIPLINE.md.crew-new
1487
1538
  fi
1488
1539
 
1489
1540
  # 3b) single-line @import into the project CLAUDE.md (if present DON'T touch content, only prepend; if absent create).
@@ -1578,6 +1629,7 @@ h1m 'Stage 4 — arm the git gates (SHIM via husky) + PROOF'
1578
1629
  # 4a) location of the existing hook chain (the shim calls this too)
1579
1630
  ORIG_HOOKS=""
1580
1631
  if [ -n "$CURHP" ]; then ORIG_HOOKS="$CURHP"
1632
+ elif [ -n "$SHIM_CHAIN" ]; then ORIG_HOOKS="$SHIM_CHAIN" # the chain the shim of an earlier run already runs
1581
1633
  elif [ -d .husky ]; then ORIG_HOOKS=".husky"
1582
1634
  elif [ -x .git/hooks/pre-commit ] || [ -x .git/hooks/commit-msg ]; then ORIG_HOOKS=".git/hooks"; fi
1583
1635
  # never shim the kit onto its OWN hooks (re-adopt) — the shim would exec itself and recurse on every commit
package/kit/CLAUDE.md CHANGED
@@ -170,7 +170,7 @@ a team that adopted Crewforth may have chosen to share them instead, and the tra
170
170
  No `git commit` / `git push` unless the user says "commit" / "push". `git add` and new branches: no approval, any
171
171
  mode (`add -f` is §4.5). "Done / we can proceed" is **not** approval. **Present the message FIRST** — even in
172
172
  auto/fast mode. `guard-bash.sh` asks in `default`/`acceptEdits`; in `auto`, `dontAsk`, `plan`, `bypassPermissions`
173
- nobody sees a prompt, so commit/push FAIL CLOSED — get a real yes, then switch mode. Never hand the user a command
173
+ nobody sees a prompt, so commit/push FAIL CLOSED: get a real yes; the guard says how. Never hand the user a command
174
174
  to paste. **Headless/CI:** if `printenv CLAUDE_GIT_OK` is set AND this request itself says "commit"/"push", that IS
175
175
  the approval — commit (a review blocker still stops you) and show the message in your report.
176
176
 
package/kit/README.md CHANGED
@@ -36,7 +36,7 @@ summarizes what lives under `.claude/` and how it works.
36
36
  comes back. Silent when nothing is new, and silent without a `kit-manifest.txt` rather than guessing.
37
37
  - **settings.json** — permissions and the hook chain (PreToolUse · UserPromptSubmit · Stop · SessionStart).
38
38
  - **`DISCIPLINE.md`** — behavior, four principles, workflow, definition of done, token discipline, and prohibitions.
39
- Crewforth-owned: an update **overwrites** it, so keep nothing of your own here. Your `./CLAUDE.md` pulls it in with a
39
+ Crewforth-owned: an update **refreshes** it, so keep nothing of your own here (a copy you edited is kept in `.claude/.legacy-backup/` first, and named). Your `./CLAUDE.md` pulls it in with a
40
40
  single `@.claude/DISCIPLINE.md` line and holds your project rules, which win on conflict; in DISCIPLINE.md §4 they can only tighten, never loosen (its §4.1 names the one exception).
41
41
  - **`kit.conf`** — which installer ran and at what version. `stack=` is always `generic` since 3.0 (kept for older
42
42
  updaters); the project's stack itself lives in the `## Stack` section of `./CLAUDE.md`.
@@ -86,9 +86,13 @@ On a mixed/non-atomic diff, **propose a split**; do not commit before approval (
86
86
  proceed" is not approval (§4.4).
87
87
  The tool-level gate `guard-bash.sh` intercepts commit/push in **every** permission mode: in normal modes it raises an
88
88
  approval prompt only the user can answer — so present the message FIRST, then run the commit yourself and let the user
89
- approve it at the prompt. Never hand the user a command to paste into their own terminal. Under `bypassPermissions`
90
- the gate fails closed; there the user must switch modes or pre-authorise with `CLAUDE_GIT_OK=1` (which never
91
- substitutes for approval).
89
+ approve it at the prompt. Never hand the user a command to paste into their own terminal. In `auto` and `dontAsk`
90
+ the prompt reaches nobody: stage, present the message, and the user approves by sending a message that is only
91
+ `approve: commit` (`approve: push`, `approve: commit+push`; `onay:` too). It covers what is staged on that HEAD.
92
+ Then run `git commit -m '…'` alone in its call (single-quoted, or from a here-document with a quoted delimiter),
93
+ and a push as `git push <remote> <branch>`. Never write that message yourself. Under `plan` and
94
+ `bypassPermissions` the gate fails closed; there the user must switch modes or pre-authorise with
95
+ `CLAUDE_GIT_OK=1` (which never substitutes for approval).
92
96
  - **No AI trace:** the message contains no co-author trailer, auto-generation footer, robot emoji, AI-assistant/tool name,
93
97
  or the `.claude` name; the message is human, technical prose (§4.1).
94
98
  - **No vendor name:** the third-party template name and any "cleanup/vendor copy" disclosure are not written into the message (§4.2).
@@ -3,14 +3,14 @@ name: crew-frontend-expert
3
3
  color: purple
4
4
  description: |
5
5
  Stack-agnostic frontend expert — web, mobile (native, Flutter, React Native, MAUI, KMP), desktop.
6
- The "how" lives in the `frontend` skill (an RN/Expo project adds `frontend-rn-expo`). **Use proactively — owns everything the user sees or
6
+ The "how" lives in the `frontend` skill (RN/Expo adds `frontend-rn-expo`, Flutter `frontend-flutter`). **Use proactively — owns everything the user sees or
7
7
  interacts with:** screens, components, navigation, client state, i18n, accessibility, responsive, native
8
8
  bridges, and all visual work — design systems, token/theme layers, typography, dark mode, "it doesn't look
9
9
  premium". Any request about it is yours whatever its size, wording or language.
10
10
  tools: Read, Grep, Glob, Edit, Write, Bash, PowerShell
11
11
  metadata:
12
12
  stage: produce
13
- skills: [a11y, confidence-check, dependency-audit, frontend, frontend-design, frontend-rn-expo, i18n-integrity, observability, performance]
13
+ skills: [a11y, confidence-check, dependency-audit, frontend, frontend-design, frontend-flutter, frontend-rn-expo, i18n-integrity, observability, performance]
14
14
  ---
15
15
 
16
16
  # Frontend Expert (stack-agnostic)
@@ -48,8 +48,8 @@ On UI, component/page, navigation/routing, state, i18n interface, responsive, or
48
48
 
49
49
  ## How (applies the `frontend` skill + stack-specific layer)
50
50
  1. **Generic discipline:** the **`frontend`** skill applies on every stack — architecture, state, state-complete UI, i18n, a11y, performance.
51
- 2. **Detect the stack:** `package.json` + repo structure → web (React/Next/Vue/Svelte/Angular), mobile (React Native/Flutter), desktop.
52
- 3. **Stack-specific layer:** apply that stack's frontend skill. Ready example in Crewforth: **`frontend-rn-expo`** for mobile RN+Expo (optional). For a web/desktop project, the project's own frontend skill / CLAUDE.md.
51
+ 2. **Detect the stack:** `package.json` / `pubspec.yaml` + repo structure → web (React/Next/Vue/Svelte/Angular), mobile (React Native/Flutter), desktop.
52
+ 3. **Stack-specific layer:** apply that stack's frontend skill. Ready in Crewforth: **`frontend-rn-expo`** for mobile RN+Expo and **`frontend-flutter`** for Flutter (both optional, neither a default). For a web/desktop project, the project's own frontend skill / CLAUDE.md.
53
53
  4. **Also apply:** `frontend-design` (visual/UX quality — hierarchy, spacing, type, states) · `a11y` (accessibility gate) · `i18n-integrity` (translation integrity) · `observability` (client log/error) · `performance` (render/bundle) · `dependency-audit` (packages).
54
54
 
55
55
  ## DoD
@@ -47,7 +47,7 @@ Static reading is legitimate for finding *where to point the measurement*:
47
47
  - **Allocation**: per-item allocation in a loop, a large buffer copied instead of streamed, string building in a loop.
48
48
  - **Payload/cache**: an oversized response, no caching where the input is stable, a cache keyed so it never hits.
49
49
  - **Client/render**: a re-render loop, work in a render path, an effect that re-triggers itself, an unbounded list
50
- without virtualisation, a heavy import on the startup path ([[frontend]], [[frontend-rn-expo]]).
50
+ without virtualisation, a heavy import on the startup path ([[frontend]], [[frontend-rn-expo]], [[frontend-flutter]]).
51
51
  - **Startup**: work at module load that belongs behind first use.
52
52
 
53
53
  ## How
@@ -58,7 +58,7 @@ refuses a commit without it (§4.6). Use exactly this recipe; the hook hashes th
58
58
  ```bash
59
59
  # CREW-REVIEW-PASS (kept identical in guard-bash.sh; smoke-test pins the pair)
60
60
  mkdir -p .claude
61
- D=$(git diff --cached | git hash-object --stdin)
61
+ D=$(git diff --cached --no-ext-diff --no-textconv | git hash-object --stdin)
62
62
  H=$(git rev-parse --verify --quiet HEAD 2>/dev/null || echo NONE)
63
63
  printf '{"diff_oid":"%s","head":"%s","ts":"%s"}\n' "$D" "$H" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .claude/review-pass.json
64
64
  ```
@@ -78,6 +78,14 @@ _mt() {
78
78
  'core.hooksPath -> %s (commit-time gates active)') s='core.hooksPath -> %s (commit anındaki kapılar devrede)' ;;
79
79
  'core.hooksPath is unset — commit trace/secret/bloat gates are INACTIVE') s='core.hooksPath ayarlı değil — commit iz, gizli bilgi ve şişkinlik kapıları DEVRE DIŞI' ;;
80
80
  "core.hooksPath -> %s (not Crewforth's hooks)") s="core.hooksPath -> %s (Crewforth'un hook'ları değil)" ;;
81
+ "core.hooksPath -> %s (Crewforth's hooks run first, then the project's own — commit-time gates active)") s="core.hooksPath -> %s (önce Crewforth'un hook'ları, sonra projenin kendi hook'ları çalışır — commit anındaki kapılar devrede)" ;;
82
+ "core.hooksPath -> %s, but the shim there does not run Crewforth's hook:%s") s="core.hooksPath -> %s, ama oradaki shim Crewforth'un hook'unu çalıştırmıyor:%s" ;;
83
+ 'update Crewforth (npx crewforth update), which writes the shim again') s="Crewforth'u güncelleyin (npx crewforth update); güncelleme shim'i yeniden yazar" ;;
84
+ "update Crewforth (npx crewforth update): this project has hooks of its own, and the update runs Crewforth's before them instead of replacing them") s="Crewforth'u güncelleyin (npx crewforth update): bu projenin kendi hook'ları var; güncelleme Crewforth'unkileri onların yerine koymaz, önlerinde çalıştırır" ;;
85
+ "update Crewforth (npx crewforth update): it keeps that chain and runs Crewforth's hooks before it (.claude/git-shim)") s="Crewforth'u güncelleyin (npx crewforth update): o zinciri korur ve Crewforth'un hook'larını önünde çalıştırır (.claude/git-shim)" ;;
86
+ "trace-blocklist.txt: the placeholder %s is active as it shipped — §4.2 looks for those characters and for no template name; write the template's name in its place, or put the # back") s="trace-blocklist.txt: %s yer tutucusu gönderildiği hâliyle etkin — §4.2 o karakterleri arıyor, bir şablon adını değil; yerine şablonun adını yazın ya da başına # koyun" ;;
87
+ "§4.2 names no vendor template: .claude/hooks/trace-blocklist.txt holds only its placeholder, so that rule looks for nothing — if this project came from a template, write its name there") s="§4.2 hiçbir üçüncü taraf şablon adı içermiyor: .claude/hooks/trace-blocklist.txt'te yalnız yer tutucu var, yani o kural hiçbir şey aramıyor — proje bir şablondan geldiyse adını oraya yazın" ;;
88
+ '§4.2 names %s vendor template pattern(s) in trace-blocklist.txt') s="§4.2, trace-blocklist.txt'te %s üçüncü taraf şablon deseni içeriyor" ;;
81
89
  'not a git repo — commit gates need: git init && git config core.hooksPath .claude/hooks') s='git deposu değil — commit kapıları için: git init && git config core.hooksPath .claude/hooks' ;;
82
90
  "Crewforth's JSON reader is missing (%s) — settings.json cannot be checked") s="Crewforth'un JSON okuyucusu yok (%s) — settings.json denetlenemiyor" ;;
83
91
  'update Crewforth') s="Crewforth'u güncelleyin" ;;
@@ -261,13 +269,59 @@ if [ -x .claude/hooks/guard-bash.sh ]; then
261
269
  esac
262
270
  fi
263
271
 
264
- # 3) core.hooksPath — without it the §4.1/§4.2 commit trace + secret/bloat scan never runs
272
+ # 2c) §4.2's list. trace-blocklist.txt ships the vendor section with a placeholder, `# <vendor-template-name>`,
273
+ # commented out: until a name stands there §4.2 looks for nothing, and nothing said so (a field note: it stays
274
+ # unfilled). Two states are told apart. The placeholder made ACTIVE as it shipped (the # removed, the text left)
275
+ # is a mistake: the rule then looks for those characters. No active line at all is the ordinary state of a
276
+ # project that came from no template, so it is said once, as information, and is not counted.
277
+ _tb=.claude/hooks/trace-blocklist.txt
278
+ if [ -f "$_tb" ]; then
279
+ _tbn=0; _tbp=""; _tbon=0
280
+ while IFS= read -r _l || [ -n "$_l" ]; do
281
+ _l="${_l%$'\r'}"
282
+ case "$_l" in
283
+ '# --- '*4.2*) _tbon=1; continue ;;
284
+ '# --- '*) _tbon=0; continue ;;
285
+ esac
286
+ [ "$_tbon" = 1 ] || continue
287
+ case "$_l" in ''|'#'*) continue ;; esac
288
+ _tbn=$((_tbn+1))
289
+ case "$_l" in '<'*'>') _tbp="$_l" ;; esac
290
+ done < "$_tb"
291
+ if [ -n "$_tbp" ]; then
292
+ warn "trace-blocklist.txt: the placeholder %s is active as it shipped — §4.2 looks for those characters and for no template name; write the template's name in its place, or put the # back" "$_tbp"
293
+ elif [ "$_tbn" = 0 ]; then
294
+ skip "§4.2 names no vendor template: .claude/hooks/trace-blocklist.txt holds only its placeholder, so that rule looks for nothing — if this project came from a template, write its name there"
295
+ else
296
+ ok "§4.2 names %s vendor template pattern(s) in trace-blocklist.txt" "$_tbn"
297
+ fi
298
+ fi
299
+
300
+ # 3) core.hooksPath — without it the §4.1/§4.2 commit trace + secret/bloat scan never runs on a commit.
301
+ # .claude/git-shim is Crewforth's too: it is where the updater points git when the project has a hook chain of
302
+ # its own (husky, .git/hooks), and each shim there runs Crewforth's hook and then the project's. Doctor did not
303
+ # know it: on such a project it answered ❌ "not Crewforth's hooks" and advised `git config core.hooksPath
304
+ # .claude/hooks`, the one command that disconnects the project's own hooks. The advice for a project with a chain
305
+ # of its own is the updater, which keeps the chain.
265
306
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
266
307
  HP="$(git config --get core.hooksPath 2>/dev/null || true)"
308
+ # A hook chain of the project's own that an unset or foreign core.hooksPath would be running today.
309
+ _own=0; { [ -d .husky ] || [ -x .git/hooks/pre-commit ] || [ -x .git/hooks/commit-msg ]; } && _own=1
267
310
  case "$HP" in
268
311
  */.claude/hooks|.claude/hooks) ok "core.hooksPath -> %s (commit-time gates active)" "$HP" ;;
269
- "") bad "core.hooksPath is unset — commit trace/secret/bloat gates are INACTIVE" "git config core.hooksPath .claude/hooks" ;;
270
- *) bad "core.hooksPath -> %s (not Crewforth's hooks)" "git config core.hooksPath .claude/hooks" "$HP" ;;
312
+ */.claude/git-shim|.claude/git-shim)
313
+ _shb=""
314
+ for h in pre-commit commit-msg; do
315
+ { [ -x "$HP/$h" ] && grep -q '\.claude/hooks/' "$HP/$h" 2>/dev/null; } || _shb="$_shb $h"
316
+ done
317
+ if [ -z "$_shb" ]; then ok "core.hooksPath -> %s (Crewforth's hooks run first, then the project's own — commit-time gates active)" "$HP"
318
+ else bad "core.hooksPath -> %s, but the shim there does not run Crewforth's hook:%s" "update Crewforth (npx crewforth update), which writes the shim again" "$HP" "$_shb"; fi ;;
319
+ "") if [ "$_own" = 1 ]; then
320
+ bad "core.hooksPath is unset — commit trace/secret/bloat gates are INACTIVE" "update Crewforth (npx crewforth update): this project has hooks of its own, and the update runs Crewforth's before them instead of replacing them"
321
+ else
322
+ bad "core.hooksPath is unset — commit trace/secret/bloat gates are INACTIVE" "git config core.hooksPath .claude/hooks"
323
+ fi ;;
324
+ *) bad "core.hooksPath -> %s (not Crewforth's hooks)" "update Crewforth (npx crewforth update): it keeps that chain and runs Crewforth's hooks before it (.claude/git-shim)" "$HP" ;;
271
325
  esac
272
326
  else
273
327
  warn "not a git repo — commit gates need: git init && git config core.hooksPath .claude/hooks"