crewforth 3.3.0 → 3.4.0
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 +30 -0
- package/README.md +4 -0
- package/VERSION +1 -1
- package/bin/cli.js +55 -0
- package/kit/eval/smoke-test.sh +92 -12
- package/kit/hooks/session-guard.sh +41 -14
- package/kit/hooks/worktree-provision.sh +91 -0
- package/kit/owned-blobs.tsv +4 -2
- package/kit/settings.json +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ 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
|
+
## [3.4.0] — 2026-10-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **An anonymous install count.** After a successful `npx crewforth init`, Crewforth sends one request, once per
|
|
13
|
+
project: `GET https://ping.crewforth.com/install?v=<version>`, the version and nothing else, after printing a line that says so. No
|
|
14
|
+
ID, path, project, host or user name is sent, and `adopt`, `update`, `add`, `studio`, the hooks and the plugin send
|
|
15
|
+
nothing. `CREW_NO_TELEMETRY` or `DO_NOT_TRACK` set to anything but `0` turns it off, and nothing is sent where `CI`
|
|
16
|
+
is set. The request gets 1.5 seconds and a failure never changes the install. The endpoint is a Cloudflare Worker
|
|
17
|
+
whose source is in the repository and which keeps a count per day and version, nothing else. README → Telemetry.
|
|
18
|
+
- **The site says which release it describes.** The home page shows the version above its heading, and every page
|
|
19
|
+
shows it next to the logo, each linked to the changelog in the page's language. The number is read from `VERSION`
|
|
20
|
+
when the site is built, so a release renames it without anyone editing a page, and a `VERSION` that is not a
|
|
21
|
+
version fails the build.
|
|
22
|
+
- **A guessed fill figure is reported.** The 🔋 figure in a reply is meant to be copied from the hook's measured
|
|
23
|
+
line, and nothing checked that it was. When the measured line stopped arriving, a session went on printing a fill
|
|
24
|
+
that rose from 45% to 86% while its transcript measured about 30%, and the user handed the session off on that
|
|
25
|
+
number. The Stop hook now measures the fill itself, and when the reply's figure is more than 5 points away it
|
|
26
|
+
tells the user both numbers. `CREW_FILL_TOLERANCE` changes the margin.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **A session resumed in a worktree keeps its gates.** `.claude/` is gitignored, so a worktree made with
|
|
31
|
+
`git worktree add ../x` has none. A session that entered one kept its hooks only until it was resumed; from then
|
|
32
|
+
on Claude Code read the worktree's settings, found none, and ran no Crewforth hook at all: no commit approval, no
|
|
33
|
+
write gate, no fill reading. A new hook copies this checkout's `.claude/` into such a worktree when a session
|
|
34
|
+
enters it, without `worktrees/`, `state/`, the review record and the gate log, and refuses the move when the copy
|
|
35
|
+
cannot be made. Worktrees Claude Code creates under `.claude/worktrees/` were not affected and are left alone. The
|
|
36
|
+
plugin edition does not need it: its hooks load in a repository's worktrees too.
|
|
37
|
+
|
|
8
38
|
## [3.3.0] — 2026-10-29
|
|
9
39
|
|
|
10
40
|
### Added
|
package/README.md
CHANGED
|
@@ -41,6 +41,10 @@ Then open Claude Code and run `/crew-doctor` to confirm the setup. Requires bash
|
|
|
41
41
|
|
|
42
42
|
When a new version is published, Claude asks once at the start of a session whether to update; it never updates on its own. Run `/crew-update` inside a session, or `npx crewforth@latest update`.
|
|
43
43
|
|
|
44
|
+
## Telemetry
|
|
45
|
+
|
|
46
|
+
After a successful `npx crewforth init`, Crewforth sends one anonymous count, once per project: `GET https://ping.crewforth.com/install?v=<version>`, the version and nothing else, after printing a line that says so. No ID, path, project, host or user name is sent; `adopt`, `update`, `add` and `studio` send nothing. Turn it off with `CREW_NO_TELEMETRY=1` or `DO_NOT_TRACK=1`; nothing is sent where `CI` is set. Details: [README → Telemetry](https://github.com/Crewforth/crewforth#telemetry).
|
|
47
|
+
|
|
44
48
|
## Links
|
|
45
49
|
|
|
46
50
|
Documentation: [crewforth.com](https://crewforth.com) · Source and issues: [github.com/Crewforth/crewforth](https://github.com/Crewforth/crewforth) · Licence: MIT
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.4.0
|
package/bin/cli.js
CHANGED
|
@@ -496,3 +496,58 @@ try {
|
|
|
496
496
|
} finally {
|
|
497
497
|
try { fs.rmSync(stage, { recursive: true, force: true }); } catch (_) { /* best effort */ }
|
|
498
498
|
}
|
|
499
|
+
|
|
500
|
+
// ================================ install count ================================
|
|
501
|
+
// One anonymous "an install finished" count, so the project can tell how many installs actually happen (npm's
|
|
502
|
+
// download figure also counts mirrors, CI and bots). The single definition of what is sent, when, and how it is
|
|
503
|
+
// turned off; README → Telemetry describes it, and nothing else in the package sends anything.
|
|
504
|
+
// when: a successful init (start.sh exit 0) only — never adopt/update/add/studio, a hook or a session;
|
|
505
|
+
// once per project: the marker lives in the git directory (or .claude/ outside git) and is never sent.
|
|
506
|
+
// what: GET <endpoint>/install?v=<version>, User-Agent "crewforth", no body. No ID of any kind, no path, no
|
|
507
|
+
// project or user name, no host, no language, no OS, no time (the server keeps its own clock).
|
|
508
|
+
// off: CREW_NO_TELEMETRY or DO_NOT_TRACK set to anything but 0, or CI defined at all. Off means nothing is
|
|
509
|
+
// printed, sent or written.
|
|
510
|
+
// cost: the request is given 1.5 s and every failure is swallowed: it never changes the exit code or prints
|
|
511
|
+
// an error. The notice line is printed BEFORE the request.
|
|
512
|
+
// The endpoint is a Cloudflare Worker whose source is packaging/install-ping-worker/, served at ping.crewforth.com.
|
|
513
|
+
// An empty constant sends and prints nothing. CREW_TELEMETRY_URL replaces it for the tests only.
|
|
514
|
+
const INSTALL_COUNT_URL = 'https://ping.crewforth.com';
|
|
515
|
+
const INSTALL_COUNT_NOTICE = {
|
|
516
|
+
en: 'Crewforth sends one anonymous "install worked" count (version only, no IDs or paths). Turn off: CREW_NO_TELEMETRY=1 · details: README → Telemetry',
|
|
517
|
+
tr: 'Crewforth tek seferlik anonim bir "kurulum çalıştı" sayımı gönderir (yalnız sürüm; kimlik ve yol yok). Kapatmak için: CREW_NO_TELEMETRY=1 · ayrıntı: README → Telemetri',
|
|
518
|
+
};
|
|
519
|
+
function installCountOff(env) {
|
|
520
|
+
const set = (v) => v !== undefined && v !== '' && v !== '0';
|
|
521
|
+
return set(env.CREW_NO_TELEMETRY) || set(env.DO_NOT_TRACK) || env.CI !== undefined;
|
|
522
|
+
}
|
|
523
|
+
function countInstall() {
|
|
524
|
+
try {
|
|
525
|
+
const url = process.env.CREW_TELEMETRY_URL || INSTALL_COUNT_URL;
|
|
526
|
+
if (!url || installCountOff(process.env) || typeof fetch !== 'function') return;
|
|
527
|
+
let version = '';
|
|
528
|
+
try { version = fs.readFileSync(path.join(pkgDir, 'VERSION'), 'utf8').split(/\r?\n/)[0].trim(); } catch (_) { /* none */ }
|
|
529
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$/.test(version)) return;
|
|
530
|
+
const project = process.cwd();
|
|
531
|
+
const gitEnv = { ...process.env }; delete gitEnv.GIT_DIR; delete gitEnv.GIT_WORK_TREE;
|
|
532
|
+
const g = spawnSync('git', ['rev-parse', '--git-path', 'crewforth-install-counted'], { cwd: project, env: gitEnv, encoding: 'utf8' });
|
|
533
|
+
const rel = g.status === 0 ? String(g.stdout).trim() : '';
|
|
534
|
+
const marker = rel ? path.resolve(project, rel) : path.join(project, '.claude', 'install-counted');
|
|
535
|
+
if (fs.existsSync(marker)) return;
|
|
536
|
+
let lang = /^(tr)/i.test(process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '') ? 'tr' : 'en';
|
|
537
|
+
if (process.env.CREW_LANG === 'tr' || process.env.CREW_LANG === 'en') lang = process.env.CREW_LANG;
|
|
538
|
+
try {
|
|
539
|
+
const conf = fs.readFileSync(path.join(project, '.claude', 'kit.conf'), 'utf8');
|
|
540
|
+
const m = /^lang=(tr|en)\s*$/m.exec(conf); if (m) lang = m[1];
|
|
541
|
+
} catch (_) { /* no record of the install's language */ }
|
|
542
|
+
console.log(INSTALL_COUNT_NOTICE[lang]);
|
|
543
|
+
try { fs.writeFileSync(marker, version + '\n'); } catch (_) { return; } // no marker, no request: it would repeat
|
|
544
|
+
fetch(`${url.replace(/\/+$/, '')}/install?v=${encodeURIComponent(version)}`, {
|
|
545
|
+
headers: { 'User-Agent': 'crewforth' }, signal: AbortSignal.timeout(1500),
|
|
546
|
+
}).then(
|
|
547
|
+
(r) => { if (r.body) r.body.cancel().catch((_) => { /* the answer is never read: nothing is lost */ }); },
|
|
548
|
+
(_) => { /* no answer within 1.5 s, refused or offline: the count is lost, never the install */ },
|
|
549
|
+
);
|
|
550
|
+
} catch (_) { /* a count is never worth a failed install */ }
|
|
551
|
+
}
|
|
552
|
+
// Called last: the constants above are declared by the time it reads them.
|
|
553
|
+
if (script === 'start.sh' && process.exitCode === 0) countInstall();
|
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -1083,6 +1083,33 @@ fillc 772000 manual 1
|
|
|
1083
1083
|
sgerr="$(mkjson "${SGPFX}-l" "$SGFX" false | CONTEXT_WINDOW=1000000 bash "$HOOKS/session-guard.sh" 2>&1 >/dev/null)"
|
|
1084
1084
|
[ -z "$sgerr" ] && pass "stop-hook: writes nothing to stderr when a compaction IS present" \
|
|
1085
1085
|
|| fail "stop-hook wrote to stderr with a compaction: $(printf '%s' "$sgerr" | tr '\n' ' ' | cut -c1-160)"
|
|
1086
|
+
# (13) THE REPLY'S 🔋 FIGURE IS HELD AGAINST THE MEASUREMENT. The discipline says it is copied from the hook's
|
|
1087
|
+
# line, never guessed, and nothing checked it: a real session printed 45 … 86 for hours while its
|
|
1088
|
+
# transcript measured 30.3, and the user handed it off on that number. The message escapes its newline as
|
|
1089
|
+
# `\n`, as the payload does.
|
|
1090
|
+
mkjsonm(){ printf '{"session_id":"%s","transcript_path":"%s","hook_event_name":"Stop","stop_hook_active":false,"last_assistant_message":"%s"}' "$1" "$SGFX" "$2"; }
|
|
1091
|
+
sgm(){ mkjsonm "$1" "$2" | CONTEXT_WINDOW=1000000 bash "$HOOKS/session-guard.sh" 2>/dev/null; }
|
|
1092
|
+
fill 303000
|
|
1093
|
+
o="$(sgm "${SGPFX}-m1" 'Stopped.\n\n🔋 Oturum %85 → yüksek doluluk · Öneri: yeni oturum')"
|
|
1094
|
+
case "$o" in *'"systemMessage"'*'says %85'*'%30.3'*) pass "stop-hook: a 🔋 figure far from the measurement is reported with the measured one" ;;
|
|
1095
|
+
*) fail "stop-hook let a guessed 🔋 figure (85 against a measured 30.3) through: $o" ;; esac
|
|
1096
|
+
[ -z "$(sgm "${SGPFX}-m2" 'Done.\n\n🔋 Oturum %30,3 → düşük doluluk · Öneri: devam')" ] \
|
|
1097
|
+
&& pass "stop-hook: a 🔋 figure that matches the measurement is silent (a decimal comma included)" \
|
|
1098
|
+
|| fail "stop-hook reported a 🔋 figure that matches the measurement"
|
|
1099
|
+
[ -z "$(sgm "${SGPFX}-m3" 'Done.\n\n🔋 Session %34 → low fill')" ] \
|
|
1100
|
+
&& pass "stop-hook: a figure within the tolerance is silent (an end-of-turn reading runs ahead of the copied one)" \
|
|
1101
|
+
|| fail "stop-hook reported a figure inside the 5-point tolerance"
|
|
1102
|
+
[ -z "$(sgm "${SGPFX}-m4" 'The line reads: 🔋 Session %NN → level')$(sgm "${SGPFX}-m5" 'Done.')" ] \
|
|
1103
|
+
&& pass "stop-hook: no 🔋 figure (a quoted template, or no line at all) says nothing" \
|
|
1104
|
+
|| fail "stop-hook spoke about a reply with no 🔋 figure"
|
|
1105
|
+
fill 800000
|
|
1106
|
+
o="$(sgm "${SGPFX}-m6" 'Done.\n\n🔋 Session %40 → low fill')"
|
|
1107
|
+
case "$o" in *'says %40'*'>75%'*) pass "stop-hook: a wrong figure and a threshold alert share one systemMessage" ;;
|
|
1108
|
+
*) fail "stop-hook dropped one of the two messages, or printed two objects: $o" ;; esac
|
|
1109
|
+
if [ -n "$JSONQ" ]; then
|
|
1110
|
+
printf '%s' "$o" | json_get systemMessage >/dev/null 2>&1 && pass "stop-hook: the combined message is valid JSON (oracle: $JSONQ)" \
|
|
1111
|
+
|| fail "stop-hook's combined message is not valid JSON: $o"
|
|
1112
|
+
else skip tool "stop-hook combined-message JSON check skipped (no working JSON parser)"; fi
|
|
1086
1113
|
rm -f "$SGFX"; rm -f "${TMPDIR:-/tmp}"/crew-session-guard.${SGPFX}-*.* 2>/dev/null
|
|
1087
1114
|
|
|
1088
1115
|
# ---- CREW-NOJQ-PATH ---------------------------------------------------------------------------------------
|
|
@@ -1137,6 +1164,57 @@ crew_nojq_path(){ # $@ = the tools the code under test needs on PATH
|
|
|
1137
1164
|
}
|
|
1138
1165
|
# ---- /CREW-NOJQ-PATH --------------------------------------------------------------------------------------
|
|
1139
1166
|
|
|
1167
|
+
sec "== 6b2) a worktree the session enters carries .claude/ =="
|
|
1168
|
+
# .claude/ is gitignored, so `git worktree add ../x` makes a worktree with none, and a session resumed there loads no
|
|
1169
|
+
# hook at all (measured on Claude Code 2.1.309: a resumed session in such a worktree ran no project hook; the same
|
|
1170
|
+
# worktree with .claude/ copied in ran the Crewforth hooks). worktree-provision.sh copies it on EnterWorktree.
|
|
1171
|
+
if ! command -v git >/dev/null 2>&1; then skip tool "worktree provisioning not checked (no git)" 7
|
|
1172
|
+
else
|
|
1173
|
+
_wp="$(mktemp -d)"; _wpm="$_wp/main"
|
|
1174
|
+
mkdir -p "$_wpm/.claude/state" "$_wpm/.claude/worktrees" "$_wp/other"
|
|
1175
|
+
( cd "$_wpm" && git init -q && git config user.email p@example.test && git config user.name p && printf '.claude/\n' > .gitignore \
|
|
1176
|
+
&& git add .gitignore && git commit -qm init ) >/dev/null 2>&1
|
|
1177
|
+
cp -R "$HOOKS" "$ROOT/eval" "$ROOT/settings.json" "$_wpm/.claude/" 2>/dev/null
|
|
1178
|
+
echo s > "$_wpm/.claude/state/x"; echo '{}' > "$_wpm/.claude/review-pass.json"; echo g > "$_wpm/.claude/gate-log.tsv"
|
|
1179
|
+
git -C "$_wp/other" init -q 2>/dev/null
|
|
1180
|
+
for _w in ../wtA ../wtB ../wtC .claude/worktrees/inner; do git -C "$_wpm" worktree add -q "$_w" -b "b${RANDOM}" >/dev/null 2>&1; done
|
|
1181
|
+
_wpH="$_wpm/.claude/hooks/worktree-provision.sh"
|
|
1182
|
+
_wpcall(){ printf '{"hook_event_name":"%s","cwd":"%s","tool_name":"EnterWorktree","tool_input":{"path":"%s"},"tool_response":{"worktreePath":"%s"}}' "$1" "$_wpm" "$2" "$3" \
|
|
1183
|
+
| ( cd "$_wpm" && bash "$_wpH" ) 2>"$_wp/err"; }
|
|
1184
|
+
if [ ! -d "$_wp/wtA" ] || [ ! -d "$_wpm/.claude/worktrees/inner" ] || [ -e "$_wp/wtA/.claude" ]; then
|
|
1185
|
+
fail "FIXTURE: the worktrees were not made, or wtA already had .claude/ — nothing below would be measured"
|
|
1186
|
+
else
|
|
1187
|
+
_wpcall PreToolUse "$_wp/wtA" ""; _r=$?
|
|
1188
|
+
{ [ "$_r" = 0 ] && [ -f "$_wp/wtA/.claude/settings.json" ] && [ -x "$_wp/wtA/.claude/hooks/guard-bash.sh" ] && [ -f "$_wp/wtA/.claude/eval/lib/settings-json.awk" ]; } \
|
|
1189
|
+
&& pass "worktree: entering a worktree made with git worktree add copies settings, hooks and eval into it (it had none)" \
|
|
1190
|
+
|| fail "worktree: entering ../wtA did not give it a working .claude/ (rc=$_r): $(head -c 200 "$_wp/err")"
|
|
1191
|
+
{ [ ! -e "$_wp/wtA/.claude/state" ] && [ ! -e "$_wp/wtA/.claude/worktrees" ] && [ ! -e "$_wp/wtA/.claude/review-pass.json" ] && [ ! -e "$_wp/wtA/.claude/gate-log.tsv" ]; } \
|
|
1192
|
+
&& pass "worktree: what belongs to one checkout (state/, worktrees/, the review record, the gate log) is not copied" \
|
|
1193
|
+
|| fail "worktree: the copy carried one checkout's own state into another"
|
|
1194
|
+
_wpcall PostToolUse "" "$_wp/wtB"; _r=$?
|
|
1195
|
+
{ [ "$_r" = 0 ] && [ -f "$_wp/wtB/.claude/settings.json" ]; } \
|
|
1196
|
+
&& pass "worktree: after the move, the path the tool reports is provisioned too" \
|
|
1197
|
+
|| fail "worktree: PostToolUse did not provision the reported worktree (rc=$_r)"
|
|
1198
|
+
_wpcall PreToolUse "$_wpm/.claude/worktrees/inner" ""; _r1=$?
|
|
1199
|
+
_wpcall PreToolUse "$_wp/other" ""; _r2=$?
|
|
1200
|
+
{ [ "$_r1$_r2" = 00 ] && [ ! -e "$_wpm/.claude/worktrees/inner/.claude" ] && [ ! -e "$_wp/other/.claude" ]; } \
|
|
1201
|
+
&& pass "worktree: one Claude Code made under .claude/worktrees/ and another repository are left alone" \
|
|
1202
|
+
|| fail "worktree: wrote into a worktree it should leave alone (rc $_r1/$_r2)"
|
|
1203
|
+
_wpcall PreToolUse "$_wp/wtA" ""; _r=$?
|
|
1204
|
+
[ "$_r" = 0 ] && pass "worktree: a worktree that already carries .claude/ is passed through" || fail "worktree: re-entering a provisioned worktree answered $_r"
|
|
1205
|
+
# The copy cannot be made: .claude is a FILE there, so mkdir fails whoever runs the suite (root included).
|
|
1206
|
+
: > "$_wp/wtC/.claude"
|
|
1207
|
+
_wpcall PreToolUse "$_wp/wtC" ""; _r=$?
|
|
1208
|
+
{ [ "$_r" = 2 ] && grep -q 'could not put its .claude/ into the worktree' "$_wp/err"; } \
|
|
1209
|
+
&& pass "worktree: when the copy cannot be made, entering is refused (exit 2) and the reason is said" \
|
|
1210
|
+
|| fail "worktree: a failed copy did not refuse the move (rc=$_r): $(head -c 200 "$_wp/err")"
|
|
1211
|
+
_o="$(_wpcall PostToolUse "" "$_wp/wtC")"; _r=$?
|
|
1212
|
+
case "$_r$_o" in 0*'"systemMessage"'*'could not put its .claude/'*'"additionalContext"'*) pass "worktree: a failed copy after the move tells the user and the model" ;;
|
|
1213
|
+
*) fail "worktree: a failed copy after the move was silent (rc=$_r): $_o" ;; esac
|
|
1214
|
+
fi
|
|
1215
|
+
rm -rf "$_wp"
|
|
1216
|
+
fi
|
|
1217
|
+
|
|
1140
1218
|
sec "== 6c) no-jq fallback: sidechain-safe + full token sum =="
|
|
1141
1219
|
BASHBIN="$(command -v bash 2>/dev/null || echo bash)" # absolute -> a stripped PATH must not hide bash itself
|
|
1142
1220
|
JXBIN="$(crew_nojq_path awk sed grep head tail cat ls tr)"
|
|
@@ -1946,16 +2024,16 @@ if [ "$IS_KIT" = 1 ]; then
|
|
|
1946
2024
|
[ "${HC:-0}" -ge 2 ] && pass "$r states the real hook count ($TH) in both the summary table and the section header" \
|
|
1947
2025
|
|| fail "$r does not state $TH hooks in both places — the count drifted from hooks/ (found $HC of 2)"
|
|
1948
2026
|
done
|
|
1949
|
-
# The plugin channel wires a SUBSET on purpose (skill-trust.sh needs a manifest only an installer writes
|
|
1950
|
-
# Assert the subset is exactly
|
|
2027
|
+
# The plugin channel wires a SUBSET on purpose (skill-trust.sh needs a manifest only an installer writes;
|
|
2028
|
+
# worktree-provision.sh copies an installed .claude/, which a plugin has not). Assert the subset is exactly those, so a hook silently dropped from the plugin fails here.
|
|
1951
2029
|
if [ -f "$KR/plugin/hooks/hooks.json" ] && [ -f "$ROOT/settings.json" ]; then
|
|
1952
2030
|
SET_H="$(grep -oE '[a-z-]+\.sh' "$ROOT/settings.json" | sort -u)"
|
|
1953
2031
|
PLG_H="$(grep -oE '[a-z-]+\.sh' "$KR/plugin/hooks/hooks.json" | sort -u)"
|
|
1954
2032
|
ONLY_SET="$(comm -23 <(printf '%s\n' "$SET_H") <(printf '%s\n' "$PLG_H") | tr '\n' ' ' | sed 's/ *$//')"
|
|
1955
|
-
if [ "$ONLY_SET" = "skill-trust.sh" ]; then
|
|
1956
|
-
pass "plugin wires every settings.json hook except skill-trust.sh (documented
|
|
2033
|
+
if [ "$ONLY_SET" = "skill-trust.sh worktree-provision.sh" ]; then
|
|
2034
|
+
pass "plugin wires every settings.json hook except skill-trust.sh and worktree-provision.sh (documented exclusions)"
|
|
1957
2035
|
else
|
|
1958
|
-
fail "plugin/settings hook sets diverged — only in settings: '${ONLY_SET:-none}' (expected exactly skill-trust.sh)"
|
|
2036
|
+
fail "plugin/settings hook sets diverged — only in settings: '${ONLY_SET:-none}' (expected exactly skill-trust.sh worktree-provision.sh)"
|
|
1959
2037
|
fi
|
|
1960
2038
|
# The two editions must also agree on WHICH TOOLS they watch, not just which scripts they run. Matching
|
|
1961
2039
|
# `Bash` alone leaves the PowerShell tool ungated, and on Windows without Git Bash that tool is the only
|
|
@@ -6929,9 +7007,9 @@ rm -rf "$_hsd"
|
|
|
6929
7007
|
# A gate's timeout is not a comfort setting: a PreToolUse hook that reaches it does not block (Claude Code's hooks
|
|
6930
7008
|
# reference: "doesn't block the tool call … don't count on a stalled hook to act as a gate"), and a field session on
|
|
6931
7009
|
# 3.0.0 showed nine of them in one day — the commands ran with no gate. Crewforth had set 60 s where Claude Code's
|
|
6932
|
-
# own default is 600, and the slowest machine measured needed 32-134 s for one guard-bash call. The
|
|
6933
|
-
# gates (the four bash gates, the no-bash gate of 12e, guard-schedule.sh, guard-agent-model.sh
|
|
6934
|
-
# the approval question, whose `ask` is what keeps a hook from answering it) carry 600 now; every other hook keeps 60 —
|
|
7010
|
+
# own default is 600, and the slowest machine measured needed 32-134 s for one guard-bash call. The nine PreToolUse
|
|
7011
|
+
# gates (the four bash gates, the no-bash gate of 12e, guard-schedule.sh, guard-agent-model.sh, worktree-provision.sh and
|
|
7012
|
+
# prompt-approval.sh on the approval question, whose `ask` is what keeps a hook from answering it) carry 600 now; every other hook keeps 60 —
|
|
6935
7013
|
# when one of those times out, nothing is left unguarded.
|
|
6936
7014
|
# Read per entry, in both editions; the twin lowers one gate back to 60 and must be seen.
|
|
6937
7015
|
gate_timeouts(){ json_hooks "$1" | LC_ALL=C awk -F'\t' '$1 == "PreToolUse" { n++; t = 0
|
|
@@ -6941,13 +7019,15 @@ gate_timeouts(){ json_hooks "$1" | LC_ALL=C awk -F'\t' '$1 == "PreToolUse" { n++
|
|
|
6941
7019
|
_gtd="$(mktemp -d)"
|
|
6942
7020
|
tr -d '\n' < "$ROOT/settings.json" | sed 's/guard-bash\.sh\([^}]*\)"timeout": 600/guard-bash.sh\1"timeout": 60/' > "$_gtd/twin.json"
|
|
6943
7021
|
_gtw="$(gate_timeouts "$_gtd/twin.json")"
|
|
6944
|
-
if [ "$_gtw" != "
|
|
7022
|
+
if [ "$_gtw" != "9 1" ]; then fail "gate timeout pin: the twin with guard-bash.sh back at 60 s read '$_gtw', want '9 1' — the pin sees nothing"
|
|
6945
7023
|
else for _gf in $_gpw_f; do
|
|
6946
7024
|
[ -f "$_gf" ] || continue
|
|
6947
7025
|
set -- $(gate_timeouts "$_gf")
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
7026
|
+
# The plugin edition has no worktree-provision.sh (build-plugin.sh says why), so it carries one gate fewer.
|
|
7027
|
+
_gwant=9; case "$_gf" in */hooks.json) _gwant=8 ;; esac
|
|
7028
|
+
if [ "${1:-0}" != "$_gwant" ]; then fail "${_gf##*/}: $1 PreToolUse gate hooks read, want $_gwant — the pin did not read the file as written"
|
|
7029
|
+
elif [ "${2:-9}" = 0 ]; then pass "${_gf##*/}: all $_gwant PreToolUse gates have a 600 s timeout (a gate that times out does not block)"
|
|
7030
|
+
else fail "${_gf##*/}: $2 of $_gwant PreToolUse gates time out before 600 s — a timed-out gate does not block"; fi
|
|
6951
7031
|
done; fi
|
|
6952
7032
|
rm -rf "$_gtd"
|
|
6953
7033
|
|
|
@@ -117,6 +117,31 @@ if [ -z "$PCT" ]; then
|
|
|
117
117
|
[ -n "$PCT" ] || exit 0
|
|
118
118
|
fi
|
|
119
119
|
|
|
120
|
+
# The reply's own 🔋 line, held against the reading. The discipline says that figure is copied from the hook's
|
|
121
|
+
# line and never guessed, and nothing checked it: a session whose hook line had stopped arriving went on to print
|
|
122
|
+
# 45, 51 … 86 while its transcript measured 30.3, and the user handed the session off on that number. A figure
|
|
123
|
+
# more than CREW_FILL_TOLERANCE points (default 5) from the measurement is reported to the user, every turn it
|
|
124
|
+
# happens. The tolerance covers the one honest gap: a slow-path reading is taken at the END of the turn, so it
|
|
125
|
+
# includes output the model had not produced when it copied the start-of-turn line. No 🔋 line, or none with a
|
|
126
|
+
# number after its '%' (a quoted template, `%NN`), says nothing. Pure expansion: no process.
|
|
127
|
+
WRONG=""
|
|
128
|
+
_said="${IN##*🔋}"
|
|
129
|
+
if [ "$_said" != "$IN" ]; then
|
|
130
|
+
_said="${_said%%\\n*}" # that line only: the payload escapes a newline as \n
|
|
131
|
+
_num="${_said#*%}"
|
|
132
|
+
if [ "$_num" != "$_said" ]; then
|
|
133
|
+
_num="${_num%%[!0-9.,]*}"; _num="${_num/,/.}" # a Turkish reply writes %30,3
|
|
134
|
+
_si="${_num%%.*}"; _sf="${_num#"$_si"}"; _sf="${_sf#.}"; _sf="${_sf:0:1}"
|
|
135
|
+
_mi="${PCT%%.*}"; _mf="${PCT#"$_mi"}"; _mf="${_mf#.}"; _mf="${_mf:0:1}"
|
|
136
|
+
case "$_si$_sf$_mi$_mf" in ''|*[!0-9]*) ;; *)
|
|
137
|
+
[ -n "$_si" ] && [ -n "$_mi" ] && {
|
|
138
|
+
_d=$(( (10#$_si * 10 + 10#${_sf:-0}) - (10#$_mi * 10 + 10#${_mf:-0}) )); [ "$_d" -lt 0 ] && _d=$(( -_d ))
|
|
139
|
+
[ "$_d" -gt $(( ${CREW_FILL_TOLERANCE:-5} * 10 )) ] && WRONG="%$_num"
|
|
140
|
+
} ;;
|
|
141
|
+
esac
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
120
145
|
# Highest threshold crossed. Compared under LC_ALL=C so '77.2' parses identically everywhere.
|
|
121
146
|
# Compared on the integer part with shell arithmetic rather than two `awk` calls. Two spawns at the end of every
|
|
122
147
|
# turn bought nothing here: the thresholds are whole numbers, so truncating is exactly equivalent — 89.9 is below
|
|
@@ -126,24 +151,26 @@ case "$INT" in ''|*[!0-9]*) exit 0 ;; esac
|
|
|
126
151
|
TIER=""
|
|
127
152
|
[ "$INT" -ge 90 ] && TIER=90
|
|
128
153
|
[ -n "$TIER" ] || { [ "$INT" -ge 75 ] && TIER=75; }
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
MSG=""
|
|
131
155
|
# Already warned at this tier this session? Stay silent — one alert per threshold, not one per turn.
|
|
132
|
-
[ -e "$(marker "$TIER")" ]
|
|
133
|
-
# Stamp this tier AND every lower one: jumping 60 -> 92 in a single turn must not let the 75 warning fire
|
|
134
|
-
# later (e.g. once /compact drops the fill back under 90).
|
|
135
|
-
for t in 75 90; do
|
|
136
|
-
|
|
137
|
-
done
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
if [ -n "$TIER" ] && [ ! -e "$(marker "$TIER")" ]; then
|
|
157
|
+
# Stamp this tier AND every lower one: jumping 60 -> 92 in a single turn must not let the 75 warning fire
|
|
158
|
+
# later (e.g. once /compact drops the fill back under 90).
|
|
159
|
+
for t in 75 90; do
|
|
160
|
+
[ "$t" -le "$TIER" ] && { : > "$(marker "$t")" 2>/dev/null || true; }
|
|
161
|
+
done
|
|
162
|
+
if [ "$TIER" = 90 ]; then
|
|
163
|
+
MSG="CRITICAL >90% — hand off NOW: apply the handoff skill, write docs/SESSION_STATE.md, then /clear. Auto-compaction is close and it will cost you the handover."
|
|
164
|
+
else
|
|
165
|
+
MSG=">75% threshold. Recommended: apply the handoff skill, write docs/SESSION_STATE.md, then /clear. Manual; your call."
|
|
166
|
+
fi
|
|
143
167
|
fi
|
|
168
|
+
[ -n "$MSG$WRONG" ] || exit 0
|
|
144
169
|
|
|
145
170
|
# JSON-escape backslash + double-quote in the measured line before embedding it.
|
|
146
171
|
SAFE="$(printf '%s' "$LINE" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
172
|
+
OUT="${MSG:+$SAFE | $MSG}"
|
|
173
|
+
[ -n "$WRONG" ] && OUT="⚠️ The 🔋 line in this reply says $WRONG, but the fill measured from the transcript is: $SAFE. The reply's figure is not a measurement — do not decide a handoff on it.${OUT:+ · $OUT}"
|
|
147
174
|
# exit 0 + systemMessage: a neutral, user-facing warning (NOT a hook error, does NOT force an extra turn).
|
|
148
|
-
printf '{"systemMessage":"%s
|
|
175
|
+
printf '{"systemMessage":"%s"}\n' "$OUT"
|
|
149
176
|
exit 0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse + PostToolUse on EnterWorktree — a worktree the session enters carries Crewforth with it.
|
|
3
|
+
#
|
|
4
|
+
# Why: `.claude/` is gitignored in a fresh install, so a worktree made with `git worktree add ../x` has none. While
|
|
5
|
+
# the session that entered it lives, its hooks keep running from the main checkout. But Claude Code records the
|
|
6
|
+
# session under the worktree once it enters one, and a session resumed there reads its settings from the worktree:
|
|
7
|
+
# with no `.claude/settings.json` there, NO hook is registered — no commit approval, no write gate, no fill line.
|
|
8
|
+
# Measured on Claude Code 2.1.309: a resumed session in such a worktree ran no project hook (0 of 1), the same
|
|
9
|
+
# worktree with `.claude/` copied in ran it (1 of 1). A field session ran four and a half hours that way, printing a
|
|
10
|
+
# fill it had made up. A worktree Claude Code creates under `.claude/worktrees/` is not affected (measured: the
|
|
11
|
+
# resumed session there still ran the main checkout's hooks), so it is left alone.
|
|
12
|
+
#
|
|
13
|
+
# What it does: before the move (PreToolUse, when the call names an existing path) and after it (PostToolUse, the
|
|
14
|
+
# path the tool reports), a linked worktree of THIS repository with no `.claude/settings.json` gets a copy of this
|
|
15
|
+
# checkout's `.claude/`, minus what belongs to one checkout only (`worktrees/`, `state/`, the review record, the
|
|
16
|
+
# gate log). If the copy cannot be made, the PreToolUse call is refused (exit 2) with the reason, and the
|
|
17
|
+
# PostToolUse call tells the user and the model, since the move already happened.
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
# ---- CREW-FAILCLOSED ---------------------------------------------------------------------------------------
|
|
20
|
+
# A gate that stops on an error of its own must refuse the call, not let it through. Claude Code blocks a tool call
|
|
21
|
+
# on exit 2 only; a hook that dies with 1, or runs on past the rule that broke, has allowed it. Both happened in
|
|
22
|
+
# one defect (measured, bash 5): a local array declared and not set is "unbound" under `set -u`, the rule that read
|
|
23
|
+
# it was abandoned in the middle, the script went on with the NEXT top-level command and left with 0, and
|
|
24
|
+
# `rm .claude/hooks/pre-commit` passed. bash does one of two things with such an error, by the kind of error:
|
|
25
|
+
# * it leaves the shell with a status that is neither 0 nor 2 -> the EXIT trap below turns that into a refusal;
|
|
26
|
+
# * it abandons the top-level command it was in and continues -> the whole gate IS one top-level command,
|
|
27
|
+
# _gate_main, which only ever ends by `exit`. If it RETURNS, it was abandoned, and the line after it refuses.
|
|
28
|
+
# Not covered, and said so: an error inside a command substitution (only that subshell ends), and a command
|
|
29
|
+
# that is not found (status 127, the script continues inside the same command).
|
|
30
|
+
# Byte-identical in every PreToolUse gate; the suite pins it. No process is started.
|
|
31
|
+
_crew_stop(){ # $1 = what happened
|
|
32
|
+
declare -F gatelog >/dev/null 2>&1 && gatelog BLOCK 4.5 "the gate stopped on an error of its own"
|
|
33
|
+
echo "GUARD (§4.5): this gate stopped on an error of its own ($1) before it finished judging the call, so the call is refused." >&2
|
|
34
|
+
echo "Nothing about the call itself was found. The error is the line above these two; it is a defect in Crewforth, not in what you ran. Run the doctor (bash .claude/eval/doctor.sh, Bash tool, not PowerShell) and report it." >&2
|
|
35
|
+
exit 2
|
|
36
|
+
}
|
|
37
|
+
trap '_crew_rc=$?; trap - EXIT; case "$_crew_rc" in 0|2) exit "$_crew_rc" ;; esac; _crew_stop "exit status $_crew_rc"' EXIT
|
|
38
|
+
# ---- /CREW-FAILCLOSED
|
|
39
|
+
unq(){ local p="${1#\"}"; p="${p%\"}"; p="${p//\\\\//}"; p="${p//\\//}"; printf '%s' "$p"; } # JSON string -> path; \ -> /
|
|
40
|
+
_gate_main(){
|
|
41
|
+
local IN HERE SRC READER V EV P_IN P_OUT T MINE THEIRS TOP ERR e WHY SAFE
|
|
42
|
+
IFS= read -r -d '' IN || true
|
|
43
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
44
|
+
SRC="$(cd "$HERE/.." && pwd)" # this checkout's .claude/
|
|
45
|
+
READER="$SRC/eval/lib/settings-json.awk"
|
|
46
|
+
[ -f "$READER" ] || exit 0 # no reader, nothing to read the payload with: fail open
|
|
47
|
+
|
|
48
|
+
V="$(printf '%s' "$IN" | awk -v op=gets -v path='hook_event_name|tool_input.path|tool_response.worktreePath' -f "$READER" - 2>/dev/null)"
|
|
49
|
+
EV="${V%%$'\t'*}"; V="${V#*$'\t'}"; P_IN="${V%%$'\t'*}"; V="${V#*$'\t'}"; P_OUT="${V%%$'\t'*}"
|
|
50
|
+
EV="$(unq "$EV")"
|
|
51
|
+
case "$EV" in
|
|
52
|
+
PreToolUse) T="$(unq "$P_IN")" ;;
|
|
53
|
+
PostToolUse) T="$(unq "$P_OUT")" ;;
|
|
54
|
+
*) exit 0 ;;
|
|
55
|
+
esac
|
|
56
|
+
[ -n "$T" ] && [ -d "$T" ] || exit 0 # no path (Claude creates one under .claude/worktrees/) or none yet
|
|
57
|
+
|
|
58
|
+
# Only a linked worktree of THIS repository, asked of git rather than compared as spelled (C:/ against /c/).
|
|
59
|
+
MINE="$(git -C "$SRC/.." rev-parse --git-common-dir 2>/dev/null)" || exit 0
|
|
60
|
+
THEIRS="$(git -C "$T" rev-parse --git-common-dir 2>/dev/null)" || exit 0
|
|
61
|
+
case "$MINE" in /*|?:*) ;; *) MINE="$SRC/../$MINE" ;; esac
|
|
62
|
+
case "$THEIRS" in /*|?:*) ;; *) THEIRS="$T/$THEIRS" ;; esac
|
|
63
|
+
[ "$MINE" -ef "$THEIRS" ] || exit 0
|
|
64
|
+
TOP="$(git -C "$T" rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
|
65
|
+
[ "$TOP" -ef "$SRC/.." ] && exit 0 # the main checkout itself
|
|
66
|
+
case "$(cd "$TOP" && pwd -P)/" in "$(cd "$SRC" && pwd -P)/worktrees/"*) exit 0 ;; esac # Claude Code's own: unaffected
|
|
67
|
+
[ -f "$TOP/.claude/settings.json" ] && exit 0 # already carries it (or .claude/ is tracked)
|
|
68
|
+
|
|
69
|
+
ERR=""
|
|
70
|
+
mkdir -p "$TOP/.claude" 2>/dev/null || ERR="could not create $TOP/.claude"
|
|
71
|
+
if [ -z "$ERR" ]; then
|
|
72
|
+
for e in "$SRC"/* "$SRC"/.[!.]*; do
|
|
73
|
+
[ -e "$e" ] || continue
|
|
74
|
+
case "${e##*/}" in worktrees|state|review-pass.json|gate-log.tsv) continue ;; esac
|
|
75
|
+
cp -R "$e" "$TOP/.claude/" 2>/dev/null || { ERR="could not copy ${e##*/}"; break; }
|
|
76
|
+
done
|
|
77
|
+
fi
|
|
78
|
+
[ -z "$ERR" ] && { [ -f "$TOP/.claude/settings.json" ] && [ -d "$TOP/.claude/hooks" ] || ERR="the copy has no settings.json or hooks/"; }
|
|
79
|
+
[ -z "$ERR" ] && exit 0
|
|
80
|
+
|
|
81
|
+
WHY="Crewforth could not put its .claude/ into the worktree $TOP ($ERR). A session resumed there would load no Crewforth hook: no commit approval, no write gate, no fill reading."
|
|
82
|
+
if [ "$EV" = PreToolUse ]; then
|
|
83
|
+
echo "$WHY Fix the cause above, then enter the worktree again." >&2
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
SAFE="${WHY//\\/\\\\}"; SAFE="${SAFE//\"/\\\"}"
|
|
87
|
+
printf '{"systemMessage":"⚠️ %s","hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s Tell the user, and do not resume this session in that worktree until it is fixed."}}\n' "$SAFE" "$SAFE"
|
|
88
|
+
exit 0
|
|
89
|
+
}
|
|
90
|
+
_gate_main "$@"
|
|
91
|
+
_crew_stop "a command of the gate was abandoned"
|
package/kit/owned-blobs.tsv
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.
|
|
1
|
+
# Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.3.0 (below VERSION 3.4.0) — do not edit by hand.
|
|
2
2
|
# The flat files Crewforth owns: a copy whose bytes (CR aside) are one of these ids was never edited.
|
|
3
3
|
# install_path blob_sha1 first_tag last_tag
|
|
4
4
|
.claude/AGENT_TEMPLATE.md 075e2c26706c06f56af01652cd24c36918513bee v3.0.0 v3.0.4
|
|
5
5
|
.claude/AGENT_TEMPLATE.md 2c8223d811245e3627a3011bb64fb9e4b0bf3b94 v3.2.0 v3.2.1
|
|
6
6
|
.claude/AGENT_TEMPLATE.md 3fdc551d221ee3665b21ef8cc3563de861a9abe6 v1.4.0 v1.6.3
|
|
7
|
+
.claude/AGENT_TEMPLATE.md 59d3af63b0480696dec38136e124bd582066fb16 v3.3.0 v3.3.0
|
|
7
8
|
.claude/AGENT_TEMPLATE.md 686fed306c873b45c245c77a95a8e6de6ee383d5 v1.7.0 v2.11.0
|
|
8
9
|
.claude/AGENT_TEMPLATE.md 7ab19417b6ff3ac732e2c894be17ffb3ec091f7d v3.1.0 v3.1.1
|
|
9
10
|
.claude/AGENT_TEMPLATE.md 981df9ea27b6f1c5ab5cb509d40875a07603858d v2.12.0 v2.13.0
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
.claude/DISCIPLINE.md b7e1066451a8590e0a2607ad0a6fe80b21599c53 v1.1.3 v1.1.8
|
|
30
31
|
.claude/DISCIPLINE.md b90a067ee323a08146862b889293f8feb49f8e7e v3.1.0 v3.1.1
|
|
31
32
|
.claude/DISCIPLINE.md bf062c2b51a9cf3cc4d0caac389c148cf6fdc977 v2.12.0 v2.12.0
|
|
33
|
+
.claude/DISCIPLINE.md c04f53866b84c54aa0e60089cb54cdc7c9aa8df1 v3.3.0 v3.3.0
|
|
32
34
|
.claude/DISCIPLINE.md c693f1bd5832ce570058c6eedcacb5dff13d89e8 v1.10.0 v1.10.0
|
|
33
35
|
.claude/DISCIPLINE.md cf881a5229d85850194a98b8d0d0438a8eaaa9d6 v3.2.0 v3.2.1
|
|
34
36
|
.claude/DISCIPLINE.md d63a01a183c9a80d739c840a511d338dc2765820 v1.4.0 v1.4.4
|
|
@@ -45,4 +47,4 @@
|
|
|
45
47
|
.claude/README.md 97d4dd1f7f1695052cc1d06cc43f5bcdc603c989 v1.0.0 v1.0.4
|
|
46
48
|
.claude/README.md a3419891cfda7385564008e4befc1007bc5364cb v1.1.1 v1.7.0
|
|
47
49
|
.claude/README.md a3b76e0fa31267610d28b742c122a962f6db59c4 v3.0.1 v3.0.4
|
|
48
|
-
.claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.
|
|
50
|
+
.claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.3.0
|
package/kit/settings.json
CHANGED
|
@@ -119,6 +119,17 @@
|
|
|
119
119
|
"timeout": 600
|
|
120
120
|
}
|
|
121
121
|
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"matcher": "EnterWorktree",
|
|
125
|
+
"hooks": [
|
|
126
|
+
{
|
|
127
|
+
"type": "command",
|
|
128
|
+
"shell": "bash",
|
|
129
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/worktree-provision.sh",
|
|
130
|
+
"timeout": 600
|
|
131
|
+
}
|
|
132
|
+
]
|
|
122
133
|
}
|
|
123
134
|
],
|
|
124
135
|
"UserPromptSubmit": [
|
|
@@ -220,6 +231,17 @@
|
|
|
220
231
|
"timeout": 60
|
|
221
232
|
}
|
|
222
233
|
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"matcher": "EnterWorktree",
|
|
237
|
+
"hooks": [
|
|
238
|
+
{
|
|
239
|
+
"type": "command",
|
|
240
|
+
"shell": "bash",
|
|
241
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/worktree-provision.sh",
|
|
242
|
+
"timeout": 60
|
|
243
|
+
}
|
|
244
|
+
]
|
|
223
245
|
}
|
|
224
246
|
],
|
|
225
247
|
"PreCompact": [
|