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.
- package/CHANGELOG.md +211 -0
- package/README.md +3 -3
- package/VERSION +1 -1
- package/adopt.sh +86 -34
- package/kit/CLAUDE.md +1 -1
- package/kit/README.md +1 -1
- package/kit/agents/crew-commit-agent.md +7 -3
- package/kit/agents/crew-frontend-expert.md +4 -4
- package/kit/agents/crew-performance-expert.md +1 -1
- package/kit/agents/crew-review-agent.md +1 -1
- package/kit/eval/doctor.sh +57 -3
- package/kit/eval/golden-routing.txt +13 -0
- package/kit/eval/lib/git-bash.sh +11 -2
- package/kit/eval/parser-conformance.sh +1 -0
- package/kit/eval/routing-eval.sh +7 -3
- package/kit/eval/smoke-test.sh +2545 -214
- package/kit/hooks/commit-msg +1 -1
- package/kit/hooks/context-usage.sh +7 -1
- package/kit/hooks/guard-bash.sh +1608 -103
- package/kit/hooks/guard-commit-scan.sh +138 -19
- package/kit/hooks/guard-powershell.sh +89 -15
- package/kit/hooks/guard-write.sh +98 -22
- package/kit/hooks/no-bash-guard.ps1 +71 -0
- package/kit/hooks/pre-commit +5 -2
- package/kit/hooks/prompt-approval.sh +484 -0
- package/kit/hooks/route-hint.sh +13 -5
- package/kit/hooks/session-rehydrate.sh +18 -0
- package/kit/hooks/skill-trust.sh +17 -2
- package/kit/owned-blobs.tsv +43 -0
- package/kit/settings.json +20 -4
- package/kit/skills/a11y/SKILL.md +1 -0
- package/kit/skills/crew-doctor/SKILL.md +2 -2
- package/kit/skills/frontend/SKILL.md +3 -3
- package/kit/skills/frontend-flutter/SKILL.md +94 -0
- package/kit/skills/frontend-flutter/references/adaptive-and-accessible.md +55 -0
- package/kit/skills/frontend-flutter/references/platform-async-release.md +59 -0
- package/kit/skills/frontend-flutter/references/rebuilds-and-performance.md +36 -0
- package/kit/studio/server/hooks/studio-gate.sh +35 -1
- package/kit/studio/server/index.js +14 -4
- package/kit/studio/server/lib/graph.js +64 -3
- package/kit/studio/server/lib/kit.js +16 -2
- package/kit/studio/server/lib/permissions.js +77 -2
- package/kit/studio/server/lib/projects.js +60 -7
- package/kit/studio/server/lib/session.js +16 -2
- package/kit/studio/web/app.js +1765 -573
- package/kit/studio/web/approvals.js +124 -0
- package/kit/studio/web/canvas.js +932 -885
- package/kit/studio/web/chat.js +322 -145
- package/kit/studio/web/convo.js +174 -0
- package/kit/studio/web/dock.js +221 -0
- package/kit/studio/web/graph-plan.js +457 -0
- package/kit/studio/web/icons/builtin.svg +1 -0
- package/kit/studio/web/index.html +116 -50
- package/kit/studio/web/inspect.js +110 -0
- package/kit/studio/web/list-plan.js +112 -0
- package/kit/studio/web/list.js +157 -0
- package/kit/studio/web/liveness.js +66 -0
- package/kit/studio/web/nav.js +237 -0
- package/kit/studio/web/newsession.js +235 -0
- package/kit/studio/web/press.js +65 -0
- package/kit/studio/web/style.css +1281 -1014
- package/kit/studio/web/theme.js +43 -0
- package/kit/studio/web/timeline-plan.js +291 -0
- package/kit/studio/web/timeline.js +400 -0
- package/kit/studio/web/tokens.css +131 -0
- package/kit/studio/web/toolbar-fit.js +35 -0
- 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,
|
|
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`
|
|
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
|
[](https://www.npmjs.com/package/crewforth)
|
|
15
15
|
[](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
|
-
- **
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
1168
|
-
# `/crew-skill` opens with `Read .claude/AGENT_TEMPLATE.md`,
|
|
1169
|
-
#
|
|
1170
|
-
#
|
|
1171
|
-
#
|
|
1172
|
-
#
|
|
1173
|
-
#
|
|
1174
|
-
|
|
1175
|
-
#
|
|
1176
|
-
#
|
|
1177
|
-
#
|
|
1178
|
-
#
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
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
|
|
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 **
|
|
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.
|
|
90
|
-
the
|
|
91
|
-
|
|
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 (
|
|
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
|
|
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
|
```
|
package/kit/eval/doctor.sh
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
270
|
-
|
|
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"
|