crewforth 0.0.1 → 3.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +3805 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -7
  4. package/VERSION +1 -0
  5. package/adopt.sh +1739 -0
  6. package/bin/cli.js +497 -0
  7. package/kit/AGENT_TEMPLATE.md +92 -0
  8. package/kit/CLAUDE.md +228 -0
  9. package/kit/README.md +107 -0
  10. package/kit/agents/crew-backend-expert.md +95 -0
  11. package/kit/agents/crew-commit-agent.md +96 -0
  12. package/kit/agents/crew-database-expert.md +84 -0
  13. package/kit/agents/crew-devops-expert.md +82 -0
  14. package/kit/agents/crew-frontend-expert.md +85 -0
  15. package/kit/agents/crew-performance-expert.md +78 -0
  16. package/kit/agents/crew-planner.md +60 -0
  17. package/kit/agents/crew-privacy-agent.md +71 -0
  18. package/kit/agents/crew-review-agent.md +95 -0
  19. package/kit/agents/crew-security-expert.md +73 -0
  20. package/kit/agents/crew-session-manager.md +77 -0
  21. package/kit/agents/crew-test-expert.md +64 -0
  22. package/kit/eval/doctor.sh +639 -0
  23. package/kit/eval/gate-report.sh +137 -0
  24. package/kit/eval/golden-holdout.txt +53 -0
  25. package/kit/eval/golden-routing.txt +199 -0
  26. package/kit/eval/lib/crew-env.sh +14 -0
  27. package/kit/eval/lib/settings-json.awk +154 -0
  28. package/kit/eval/lib/skill-listing.awk +43 -0
  29. package/kit/eval/lib/star.sh +70 -0
  30. package/kit/eval/parser-conformance.sh +612 -0
  31. package/kit/eval/preflight.sh +186 -0
  32. package/kit/eval/routing-eval.sh +209 -0
  33. package/kit/eval/scan-skill.sh +162 -0
  34. package/kit/eval/smoke-test.sh +6736 -0
  35. package/kit/eval/update-guard.sh +81 -0
  36. package/kit/eval/utilization.sh +130 -0
  37. package/kit/hooks/board-sync.sh +106 -0
  38. package/kit/hooks/board.sh +962 -0
  39. package/kit/hooks/commit-msg +49 -0
  40. package/kit/hooks/context-usage.sh +353 -0
  41. package/kit/hooks/floor-blocklist.txt +124 -0
  42. package/kit/hooks/guard-bash.sh +1433 -0
  43. package/kit/hooks/guard-commit-scan.sh +511 -0
  44. package/kit/hooks/guard-write.sh +513 -0
  45. package/kit/hooks/pre-commit +577 -0
  46. package/kit/hooks/route-hint.sh +185 -0
  47. package/kit/hooks/secret-blocklist.txt +82 -0
  48. package/kit/hooks/session-guard.sh +149 -0
  49. package/kit/hooks/session-rehydrate.sh +41 -0
  50. package/kit/hooks/session-stats.sh +252 -0
  51. package/kit/hooks/session-update-check.sh +236 -0
  52. package/kit/hooks/skill-trust.sh +163 -0
  53. package/kit/hooks/trace-blocklist.txt +53 -0
  54. package/kit/settings.json +132 -0
  55. package/kit/skills/a11y/SKILL.md +44 -0
  56. package/kit/skills/adr/SKILL.md +74 -0
  57. package/kit/skills/api-design/SKILL.md +50 -0
  58. package/kit/skills/automode-policy/SKILL.md +91 -0
  59. package/kit/skills/automode-policy/references/policy.json +13 -0
  60. package/kit/skills/automode-policy/scripts/apply.sh +84 -0
  61. package/kit/skills/automode-policy/scripts/check.sh +102 -0
  62. package/kit/skills/backend-architecture/SKILL.md +103 -0
  63. package/kit/skills/brainstorm/SKILL.md +47 -0
  64. package/kit/skills/ci-pipeline/SKILL.md +38 -0
  65. package/kit/skills/commit-message/SKILL.md +82 -0
  66. package/kit/skills/commit-message/references/examples.md +8 -0
  67. package/kit/skills/confidence-check/SKILL.md +86 -0
  68. package/kit/skills/crew-board/SKILL.md +55 -0
  69. package/kit/skills/crew-brainstorm/SKILL.md +19 -0
  70. package/kit/skills/crew-code-review/SKILL.md +151 -0
  71. package/kit/skills/crew-code-review/references/history-search.md +19 -0
  72. package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
  73. package/kit/skills/crew-code-review/references/receiving.md +23 -0
  74. package/kit/skills/crew-doctor/SKILL.md +46 -0
  75. package/kit/skills/crew-gates/SKILL.md +25 -0
  76. package/kit/skills/crew-handoff/SKILL.md +14 -0
  77. package/kit/skills/crew-plan/SKILL.md +18 -0
  78. package/kit/skills/crew-review/SKILL.md +26 -0
  79. package/kit/skills/crew-ship/SKILL.md +19 -0
  80. package/kit/skills/crew-skill/SKILL.md +62 -0
  81. package/kit/skills/crew-studio/SKILL.md +129 -0
  82. package/kit/skills/crew-update/SKILL.md +43 -0
  83. package/kit/skills/db-migration/SKILL.md +120 -0
  84. package/kit/skills/db-migration/references/backup-restore.md +33 -0
  85. package/kit/skills/db-migration/references/raw-sql.md +30 -0
  86. package/kit/skills/db-migration/references/tool-matrix.md +35 -0
  87. package/kit/skills/dependency-audit/SKILL.md +61 -0
  88. package/kit/skills/dependency-upgrade/SKILL.md +82 -0
  89. package/kit/skills/deploy/SKILL.md +100 -0
  90. package/kit/skills/deploy/references/proxy-ssl.md +56 -0
  91. package/kit/skills/deploy/references/rollback.md +18 -0
  92. package/kit/skills/deploy/references/scripts.md +9 -0
  93. package/kit/skills/deploy/references/topology-a.md +128 -0
  94. package/kit/skills/deploy/references/topology-b.md +44 -0
  95. package/kit/skills/docs-writer/SKILL.md +53 -0
  96. package/kit/skills/docs-writer/references/coauthoring.md +34 -0
  97. package/kit/skills/eval-grader/SKILL.md +75 -0
  98. package/kit/skills/eval-grader/references/method.md +51 -0
  99. package/kit/skills/frontend/SKILL.md +56 -0
  100. package/kit/skills/frontend/references/verify-contract.md +41 -0
  101. package/kit/skills/frontend-design/SKILL.md +71 -0
  102. package/kit/skills/frontend-design/references/design-review.md +34 -0
  103. package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
  104. package/kit/skills/handoff/SKILL.md +71 -0
  105. package/kit/skills/handoff/references/cold-reader.md +51 -0
  106. package/kit/skills/i18n-integrity/SKILL.md +31 -0
  107. package/kit/skills/incident-runbook/SKILL.md +40 -0
  108. package/kit/skills/incident-runbook/references/postmortem.md +16 -0
  109. package/kit/skills/iterate/SKILL.md +40 -0
  110. package/kit/skills/mcp-builder/SKILL.md +54 -0
  111. package/kit/skills/mcp-builder/references/building.md +71 -0
  112. package/kit/skills/observability/SKILL.md +50 -0
  113. package/kit/skills/performance/SKILL.md +38 -0
  114. package/kit/skills/performance/references/bottlenecks.md +10 -0
  115. package/kit/skills/privacy-compliance/SKILL.md +75 -0
  116. package/kit/skills/red-team/SKILL.md +48 -0
  117. package/kit/skills/reflect/SKILL.md +57 -0
  118. package/kit/skills/release/SKILL.md +44 -0
  119. package/kit/skills/security-scan/SKILL.md +99 -0
  120. package/kit/skills/security-scan/references/ai-agents.md +107 -0
  121. package/kit/skills/security-scan/references/fronts.md +99 -0
  122. package/kit/skills/security-scan/references/prompting.md +23 -0
  123. package/kit/skills/security-scan/references/reporting.md +75 -0
  124. package/kit/skills/security-scan/references/verify.md +131 -0
  125. package/kit/skills/sonarqube-check/SKILL.md +108 -0
  126. package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
  127. package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
  128. package/kit/skills/spec-planning/SKILL.md +92 -0
  129. package/kit/skills/systematic-debugging/SKILL.md +57 -0
  130. package/kit/skills/systematic-debugging/references/techniques.md +44 -0
  131. package/kit/skills/teamboard/SKILL.md +88 -0
  132. package/kit/skills/teamboard/references/setup.md +37 -0
  133. package/kit/skills/testing/SKILL.md +49 -0
  134. package/kit/skills/testing/references/flaky-triage.md +51 -0
  135. package/kit/skills/threat-model/SKILL.md +57 -0
  136. package/kit/skills/threat-model/references/interview.md +51 -0
  137. package/kit/skills/threat-model/references/schema.md +42 -0
  138. package/kit/skills/token-budget/SKILL.md +51 -0
  139. package/kit/skills/trace-scan/SKILL.md +43 -0
  140. package/kit/skills/worktree/SKILL.md +40 -0
  141. package/kit/studio/README.md +266 -0
  142. package/kit/studio/ensure-node.sh +471 -0
  143. package/kit/studio/package.json +14 -0
  144. package/kit/studio/server/hooks/studio-gate.sh +169 -0
  145. package/kit/studio/server/index.js +902 -0
  146. package/kit/studio/server/lib/crew-env.js +11 -0
  147. package/kit/studio/server/lib/fleet.js +146 -0
  148. package/kit/studio/server/lib/graph.js +533 -0
  149. package/kit/studio/server/lib/instance.js +135 -0
  150. package/kit/studio/server/lib/kit-telemetry.js +182 -0
  151. package/kit/studio/server/lib/kit.js +158 -0
  152. package/kit/studio/server/lib/palette.js +118 -0
  153. package/kit/studio/server/lib/peers.js +72 -0
  154. package/kit/studio/server/lib/permissions.js +150 -0
  155. package/kit/studio/server/lib/projects.js +294 -0
  156. package/kit/studio/server/lib/roster.js +191 -0
  157. package/kit/studio/server/lib/session.js +309 -0
  158. package/kit/studio/server/lib/terminal.js +91 -0
  159. package/kit/studio/server/lib/transcript.js +100 -0
  160. package/kit/studio/web/app.js +1098 -0
  161. package/kit/studio/web/canvas.js +1179 -0
  162. package/kit/studio/web/chat.js +543 -0
  163. package/kit/studio/web/index.html +86 -0
  164. package/kit/studio/web/md.js +110 -0
  165. package/kit/studio/web/storage-migrate.js +23 -0
  166. package/kit/studio/web/style.css +1453 -0
  167. package/package.json +46 -8
  168. package/start.sh +764 -0
  169. package/bin/crewforth.js +0 -10
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # commit-msg gate, two independent checks:
3
+ # (1) §4.1 trace scanner — AI-authorship traces / vendor names / co-author in the commit message.
4
+ # (2) team board claim gate — a commit that names an item must be backed by a live claim held by YOU.
5
+ # Skipping either requires an explicit request per §4.5 (git commit --no-verify).
6
+ set -euo pipefail
7
+ HERE="$(cd "$(dirname "$0")" && pwd)"
8
+ BL="$HERE/trace-blocklist.txt"
9
+ MSG_FILE="${1:-}"
10
+ [ -n "$MSG_FILE" ] && [ -f "$MSG_FILE" ] || exit 0
11
+
12
+ # (2) Board claim gate. It is a no-op in every repo that never ran `/crew-board init` — board.sh returns 0 when
13
+ # the board ref does not exist, so solo projects and Crewforth's own repo behave exactly as before. It reads the
14
+ # LOCAL ref only: committing must never require the network.
15
+ if [ -f "$HERE/board.sh" ]; then
16
+ bash "$HERE/board.sh" gate "$MSG_FILE" || exit 1
17
+ fi
18
+
19
+ [ -f "$BL" ] || exit 0
20
+
21
+ # Repo-root exception list (optional): a FULL-line blocklist pattern is SKIPPED (not part of the payload).
22
+ AL=""
23
+ GITROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
24
+ [ -n "$GITROOT" ] && [ -f "$GITROOT/.trace-allowlist.txt" ] && AL="$GITROOT/.trace-allowlist.txt"
25
+
26
+ HIT=0
27
+ while IFS= read -r pat || [ -n "$pat" ]; do
28
+ pat="${pat%$'\r'}" # tolerate a CRLF blocklist — pre-commit:176 has had this
29
+ case "$pat" in ''|\#*) continue;; esac # since the gate was written; its twin here did not, and
30
+ # the message scan was the ONLY reader with no fallback:
31
+ # pre-commit never sees the message, so a co-author
32
+ # trailer had exactly one gate and it was blind. Verified
33
+ # on two machines: LF blocklist rejects (rc=1), the same
34
+ # blocklist saved CRLF accepts (rc=0). Reachable without
35
+ # trying: Crewforth writes no .gitattributes into a user's
36
+ # project, Git for Windows defaults core.autocrlf=true,
37
+ # and this file is meant to be edited by hand.
38
+ if [ -n "$AL" ] && grep -qxF -- "$pat" "$AL"; then continue; fi
39
+ if grep -iqE -- "$pat" "$MSG_FILE"; then
40
+ echo "TRACE-SCANNER (message): forbidden expression -> '$pat' (§4.1)"
41
+ HIT=1
42
+ fi
43
+ done < "$BL"
44
+
45
+ if [ "$HIT" -ne 0 ]; then
46
+ echo "Commit message rejected. Write a human, technical message; leave no trace/brand."
47
+ exit 1
48
+ fi
49
+ exit 0
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env bash
2
+ # Measures session context fill for REAL (NOT a guess): reads the API usage of the last main-context
3
+ # turn in the transcript JSONL -> input + cache_read + cache_creation = tokens in the context window.
4
+ # This is the number /context shows; since the assistant cannot run /context, it reads directly from here.
5
+ #
6
+ # Usage:
7
+ # bash context-usage.sh [transcript.jsonl] # if no arg is given, auto-finds from pwd
8
+ # bash context-usage.sh --verbose [transcript.jsonl] # long form, with the raw token counts
9
+ # echo '{"transcript_path":"..."}' | bash context-usage.sh # also accepts hook stdin JSON
10
+ # Window size: CONTEXT_WINDOW env (default 1000000).
11
+ #
12
+ # The default output is COMPACT on purpose. The UserPromptSubmit hook injects this line into the model's
13
+ # context on EVERY turn, and it stays there for the rest of the session — a long line is a per-turn tax that
14
+ # compounds through cache reads. The percentage carries all the signal; the raw counts are for humans, so they
15
+ # live behind --verbose (which is what session-guard.sh uses for its once-per-threshold user warning).
16
+ set -uo pipefail
17
+ # The script's own directory without `dirname`: an external a stripped PATH may lack, and a process on a hook that
18
+ # runs before every prompt. The JSON reader is found relative to it (../eval/lib).
19
+ case "$0" in */*) HERE="${0%/*}" ;; *) HERE=. ;; esac
20
+ HERE="$(cd "$HERE" && pwd)"
21
+ WINDOW="${CONTEXT_WINDOW:-1000000}"
22
+ VERBOSE=0
23
+ case "${1:-}" in --verbose|-v) VERBOSE=1; shift ;; esac
24
+ TR="${1:-}"
25
+
26
+ # A path that arrived inside the hook's stdin JSON is JSON-ENCODED, and on Windows that matters: the real value
27
+ # `C:\Users\me\.claude\projects\p\a.jsonl` is transmitted as `C:\\Users\\me\\...`. Slicing it out with sed hands
28
+ # back the doubled backslashes verbatim, so every `[ -f "$TR" ]` failed and the hook reported "transcript not
29
+ # found" on every single turn — on Windows CLI and on Claude Desktop alike. It read like the hook was being
30
+ # called without stdin; it was being called correctly and then throwing the answer away.
31
+ #
32
+ # So: undo the JSON escaping, then fold the separators. `\\` -> `/` first (the encoded form), then any lone `\`
33
+ # (a value that was never encoded, e.g. an argument typed by hand). Forward slashes resolve on every platform
34
+ # including Git Bash. A POSIX path contains neither, so this is a no-op there.
35
+ unjson_path(){ p="${1//\\\\//}"; p="${p//\\//}"; printf '%s' "$p"; }
36
+
37
+ IN=""
38
+ # 1) transcript_path from hook stdin (if present)
39
+ #
40
+ # `cat` on a pipe blocks until EOF, and "not a tty" does not mean "data is coming". Claude Code always writes
41
+ # the event JSON and closes, so this is invisible in normal use — but any wrapper that leaves stdin open and
42
+ # silent (a background shell, a CI runner, Crewforth's own suite launched as a job) hangs the hook forever, and
43
+ # it is a UserPromptSubmit hook: a hang here is a hang on every turn. Measured 2026-08-24: real JSON stdin 0s,
44
+ # closed stdin 0s, open-and-silent pipe still running after 20 minutes.
45
+ #
46
+ # So bound the wait. `read -t` on the first line costs nothing when the data is already there (the common case
47
+ # by far) and gives up otherwise; the rest is drained without a timeout because a producer that sent a first
48
+ # line is sending the whole object. Timeout is deliberately short — this runs before every prompt.
49
+ if [ -z "$TR" ] && [ ! -t 0 ]; then
50
+ # `read` returns NON-ZERO when it hits EOF without a trailing newline — and it still assigns what it read.
51
+ # Claude Code sends the event JSON with no trailing newline, so gating on the exit status alone throws the
52
+ # whole payload away and every measurement reports "unmeasurable". Measured the wrong way once already: the
53
+ # first version of this fix was timed, not checked for output, so it looked fine and was silently blind.
54
+ _first=""
55
+ IFS= read -r -t "${CREW_STDIN_TIMEOUT:-2}" _first 2>/dev/null || true
56
+ if [ -n "$_first" ]; then
57
+ IN="$_first
58
+ $(cat 2>/dev/null || true)"
59
+ fi
60
+ # Shortest-match parameter expansion, not `sed | head`: same FIRST-occurrence semantics, two processes
61
+ # cheaper, on a hook that runs before every prompt.
62
+ if [ -n "$IN" ]; then
63
+ _r="${IN#*\"transcript_path\"}"
64
+ if [ "$_r" != "$IN" ]; then _r="${_r#*\"}"; TR="${_r%%\"*}"; fi
65
+ fi
66
+ fi
67
+ # --- Permission mode: say it once, while it still saves a turn -----------------------------------------
68
+ # §4.4 already states that the commit gate FAILS CLOSED in auto, dontAsk, plan and bypassPermissions, because
69
+ # in those modes software answers the prompt and nothing can prove a person did. What the session had no way to
70
+ # know was which mode it was IN. guard-bash.sh reads permission_mode, but only when it is already refusing --
71
+ # which is one turn too late. Measured in the field: the user said "commit", the guard refused, the mode was
72
+ # switched, and the command ran again. Everything behaved correctly and a turn was still spent on a fact that
73
+ # was available from the start.
74
+ #
75
+ # The published UserPromptSubmit schema carries `permission_mode`, with a JSON example showing it, so this is
76
+ # read rather than assumed; if it is ever absent the slice comes back empty and nothing is printed.
77
+ #
78
+ # ONCE PER SESSION, not once per turn. The model needs this fact, not a reminder of it, and a line repeated
79
+ # before every prompt in a mode people leave on all day is pure tax. Keyed by the mode itself, so switching
80
+ # modes mid-session announces the new one -- the same marker pattern the stale-discipline gate below uses.
81
+ # Modes where the gate does NOT fail closed say nothing at all: there the prompt reaches a person and there is
82
+ # no turn to save.
83
+ # Placed HERE, before the transcript work, because it must not depend on it. The first version sat further
84
+ # down and never ran on a session whose transcript could not be read -- the hook exits early for that, and a
85
+ # session with no usable transcript is exactly one that has no other way to learn its own mode. It derives its
86
+ # own session id for the same reason: everything below it is allowed to give up.
87
+ if [ -n "${IN:-}" ]; then
88
+ PSID="${IN#*\"session_id\"}"
89
+ [ "$PSID" = "$IN" ] && PSID="" || { PSID="${PSID#*:}"; PSID="${PSID#*\"}"; PSID="${PSID%%\"*}"; }
90
+ case "$PSID" in ''|*[!A-Za-z0-9._-]*) PSID="" ;; esac
91
+ fi
92
+ if [ -n "${IN:-}" ] && [ -n "${PSID:-}" ]; then
93
+ PM="${IN#*\"permission_mode\"}"
94
+ if [ "$PM" != "$IN" ]; then
95
+ PM="${PM#*:}"; PM="${PM#*\"}"; PM="${PM%%\"*}"
96
+ case "$PM" in
97
+ auto|dontAsk|plan|bypassPermissions)
98
+ PMARK="${TMPDIR:-/tmp}/crew-permmode.${PSID}"
99
+ WASM=""
100
+ [ -f "$PMARK" ] && IFS= read -r WASM < "$PMARK" 2>/dev/null
101
+ if [ "$WASM" != "$PM" ]; then
102
+ printf '%s' "$PM" > "$PMARK" 2>/dev/null || true
103
+ echo "🔒 Permission mode: $PM. git commit/push fail closed in this mode (§4.4). Only when a commit or push comes up: say so before you ask for approval; a real yes still comes first, then the user switches mode (or exports CLAUDE_GIT_OK=1 in headless/CI). Otherwise do not mention it."
104
+ fi
105
+ ;;
106
+ esac
107
+ fi
108
+ fi
109
+
110
+
111
+ [ -n "$TR" ] && TR="$(unjson_path "$TR")"
112
+ # 2) still missing: derive the project dir from pwd.
113
+ # ---- CREW-TRANSCRIPT-DIR (kept byte-identical in context-usage.sh and session-stats.sh; smoke-test §6i3 pins it)
114
+ # Claude Code stores a session under $HOME/.claude/projects/<cwd encoded as a directory name>. Reproducing that
115
+ # encoding is the ONLY way a by-hand call (no hook payload on stdin) can find its own transcript.
116
+ crew_project_dirs(){ # candidate directory names for the current cwd, best first, one per line
117
+ local w p
118
+ # Windows first. Claude Code sees the NATIVE cwd (C:\repo\app) while Git Bash's `pwd` reports /c/repo/app, so
119
+ # the drive letter never matched and the by-hand call could not resolve a transcript on Windows AT ALL — the
120
+ # same "transcript not found" was reported from that machine three separate times before the cause was found.
121
+ # `pwd -W` is the MSYS builtin that returns the native form; elsewhere it just fails and is discarded.
122
+ w="$(pwd -W 2>/dev/null || true)"
123
+ # The encoder folds : \ / . AND _ to '-'. The underscore is the one that hides: a project named `report_api`
124
+ # lands in `...-report-api`, so folding only slashes and dots misses every path containing an underscore — on
125
+ # every platform, not just Windows.
126
+ for p in "$w" "$(pwd)"; do
127
+ [ -n "$p" ] && printf '%s\n' "$(printf '%s' "$p" | sed 's#[:\\/._]#-#g')"
128
+ done
129
+ # Legacy shapes, kept so a directory written by an older client still resolves.
130
+ printf '%s\n' "$(pwd | sed 's#[/.]#-#g')" "$(pwd | sed 's#/#-#g')"
131
+ }
132
+ if [ -z "$TR" ]; then
133
+ while IFS= read -r esc; do
134
+ [ -n "$esc" ] || continue
135
+ cand="$(ls -t "$HOME/.claude/projects/$esc"/*.jsonl 2>/dev/null | head -1)"
136
+ [ -n "$cand" ] && { TR="$cand"; break; }
137
+ done <<CREWEOF
138
+ $(crew_project_dirs)
139
+ CREWEOF
140
+ fi
141
+ # ---- /CREW-TRANSCRIPT-DIR
142
+ # Cannot measure. The two call sites want opposite things here, so they get opposite answers.
143
+ #
144
+ # Called BY HAND (a transcript passed as an argument): complain on stderr and exit non-zero. A person who typed a
145
+ # path wants to know it was wrong, and the suite asserts this — "never invent a fill" is checked by watching for
146
+ # a non-zero exit rather than by trusting the absence of output.
147
+ #
148
+ # Called AS A HOOK (payload on stdin, no argument): stay silent and exit 0. Nothing downstream reads the status —
149
+ # session-guard.sh parses the line and falls open without it — while a non-zero exit is a visible error in the
150
+ # user's session, once per turn, for a condition the discipline already handles (no 🔋 line, say so once, drop
151
+ # it). It also stops depending on the `|| true` in the hook command to hide it, which exec-form hooks cannot use.
152
+ if [ -z "$TR" ] || [ ! -f "$TR" ]; then
153
+ [ -n "$IN" ] && exit 0 # hook payload on stdin -> quiet
154
+ echo "context-usage: transcript not found (pass an arg or use hook stdin)" >&2
155
+ exit 1
156
+ fi
157
+
158
+ # Sum of usage for the last main-context turn: a non-sidechain ASSISTANT record that has a cache_read.
159
+ #
160
+ # Both engines require `"type":"assistant"`. When a subagent returns, its tool_result lands in the MAIN context
161
+ # (`isSidechain:false`) as a `type:"user"` record whose `toolUseResult.usage` is raw, unescaped JSON. jq is
162
+ # anchored at `.message.usage` and never saw it, but awk only sees text: it read the SUBAGENT's tokens as the
163
+ # session's. A 92.2%-full context reported 0.9% — "continue" — so the handoff gate stayed silent exactly when it
164
+ # was needed. Reachable by interrupting a subagent, which leaves that record last. The same predicate now guards
165
+ # both engines so they cannot drift; if a record ever lacks `.type` both go quiet, and a hook that says nothing
166
+ # is recoverable in a way that a hook confidently reporting 0.9% is not.
167
+ # ONE ENGINE on every machine: Crewforth's awk JSON reader (eval/lib/settings-json.awk, op=usage). There used to be a
168
+ # jq branch in front of a regex-over-the-line awk scan; the regex matched key names ANYWHERE, so a tool input or
169
+ # result holding an object with "input_tokens" or "type":"assistant" could replace the record's own usage — found in
170
+ # review, 50% read as 0% or 90% on constructed transcripts (0 of 507 real ones differed). The reader parses each
171
+ # candidate record and applies the jq predicate exactly, and it skips the byte window's cut first line.
172
+ # `scan` prints the total of the LAST matching record; callers take the last line with `${x##*NL}`, which is
173
+ # shell, where `| tail -1` was a process on a hook that runs before every prompt.
174
+ last_line() { printf '%s' "${1##*$'\n'}"; }
175
+ scan() { # reads a JSONL tail on stdin, prints the total of the last qualifying record
176
+ # Crewforth's JSON reader, which the plugin also ships at ../eval/lib. Without it there is nothing to read with:
177
+ # print nothing, and the caller reports "usage not found" rather than a number it did not measure.
178
+ [ -f "$HERE/../eval/lib/settings-json.awk" ] || { cat >/dev/null; return 0; }
179
+ awk -v op=usage -f "$HERE/../eval/lib/settings-json.awk" -
180
+ }
181
+
182
+ # Read the TAIL, not the file, and bound it by BYTES not lines. We want the LAST match; a tail hands back the
183
+ # bytes closest to EOF, so a match inside the window IS the last match, and too small a window can only come back
184
+ # empty, never stale. Bytes beat lines because one pasted payload can make a SINGLE JSONL record tens of MB: a
185
+ # line-based `tail -n 200` then drags that whole blob through the scanner (measured: 1.4s for a 60MB paste on a
186
+ # fast box; multiply by a slow Windows fork with no jq and it crosses the 10s hook timeout). A byte tail caps the
187
+ # work no matter how fat the lines are — the same 60MB case scans in ~12ms. A front-truncated partial first line
188
+ # simply fails to match, and since we keep the LAST match that is harmless. Widen once (256 KiB covers even a
189
+ # large subagent fan-out of small usage records), then a SIZE-GUARDED whole-file last resort.
190
+ #
191
+ # The partial first line has to go before the scan, and that is not a tidiness point — it is why the
192
+ # window never worked. The comment above reasons that a front-truncated line "simply fails to match",
193
+ # which is true of awk and FALSE of jq: measured, jq ABORTS the whole stream on a malformed first
194
+ # record and prints nothing, while awk skips the line and carries on. So on every machine that has jq
195
+ # — which is most of them, and every developer laptop — both windows returned empty and the hook fell
196
+ # through to scanning the ENTIRE transcript, every single turn. On a 45 MB session that is 45 MB read
197
+ # per prompt, and the byte bound that this block exists to enforce was never once in force.
198
+ #
199
+ # Dropped with `tail -n +2`, NOT with `${W#*$'\n'}`. The parameter expansion looks free and is a trap:
200
+ # on a window with no newline in it — a malformed transcript that is one enormous line, which the
201
+ # suite fixtures on purpose — bash walks every prefix looking for a match it will never find. Measured
202
+ # on a 4 MiB single-line file: it had not finished after 20 seconds, and it hung the gate for 36
203
+ # minutes before that was noticed. One streaming process is the cheap option here, not the expensive
204
+ # one. Only correct to drop when the tail actually truncated, hence the size test.
205
+ # The size is asked ONCE, up front, and it pays for itself: it is the only exact way to know whether
206
+ # a tail truncated, and knowing that is what lets the window work at all. Comparing the captured
207
+ # length against the window does NOT work — `$( )` strips the trailing newline, so the capture is
208
+ # never quite the window size and the drop never fires.
209
+ SZ="$(wc -c < "$TR" 2>/dev/null)"; SZ="${SZ//[!0-9]/}"; SZ="${SZ:-0}"
210
+ TOTAL=""
211
+ for B in 262144 4194304; do # 256 KiB, then 4 MiB
212
+ if [ "$SZ" -gt "$B" ]; then
213
+ TOTAL="$(tail -c "$B" "$TR" | tail -n +2 | scan)" # the tail cut a line in half; drop it
214
+ else
215
+ TOTAL="$(tail -c "$B" "$TR" | scan)" # whole file: the first line is intact
216
+ fi
217
+ TOTAL="$(last_line "$TOTAL")"
218
+ [ -n "$TOTAL" ] && break
219
+ done
220
+ if [ -z "$TOTAL" ]; then
221
+ # The record is further from EOF than 4 MiB — almost always because the CURRENT turn pasted a huge payload that
222
+ # now sits between EOF and the last assistant record. The whole file WOULD find it, but on a pathologically
223
+ # large transcript that scan is exactly what blows the hook timeout. So bound it: scan the whole file only when
224
+ # it is small enough to finish well inside the timeout (180MB ~= 4.7s under awk, so 200MB is safe under 30s);
225
+ # past the cap, fail OPEN. A missing 🔋 line is recoverable — the model answers "could not measure" — whereas a
226
+ # timed-out hook is just discarded noise.
227
+ CAP="${CREW_CONTEXT_MAX_BYTES:-209715200}" # 200 MiB; override per-repo
228
+ [ "${SZ:-0}" -le "$CAP" ] && TOTAL="$(last_line "$(scan < "$TR")")"
229
+ fi
230
+ # Same split as the missing-transcript case above: a hook stays quiet and exits 0, a by-hand call explains
231
+ # itself and exits non-zero. Exec-form hook commands carry no `|| true` to swallow a status, so anything that
232
+ # exits non-zero here becomes an error banner in the user's session once per turn.
233
+ if [ -z "${TOTAL:-}" ]; then
234
+ [ -n "$IN" ] && exit 0
235
+ # Name the missing reader rather than blaming the transcript's size: without it nothing was read at all.
236
+ if [ ! -f "$HERE/../eval/lib/settings-json.awk" ]; then
237
+ echo "context-usage: Crewforth's JSON reader is missing ($HERE/../eval/lib/settings-json.awk) — update Crewforth" >&2
238
+ exit 1
239
+ fi
240
+ echo "context-usage: usage not found in the byte-bounded window (transcript too large to scan within the hook timeout)" >&2
241
+ exit 1
242
+ fi
243
+
244
+ # LC_ALL=C: force a '.' decimal separator regardless of locale (tr_TR etc. would emit '77,2' and could
245
+ # mis-parse the percentage). Generation AND every comparison below run under C so they stay consistent.
246
+ # Integer arithmetic, not awk, and it ROUNDS rather than truncates — which is the whole reason awk was
247
+ # here. `(t*1000 + w/2) / w` is round-half-up on tenths; verified against awk's %.1f across the range
248
+ # before the swap, not after. That removes the single most expensive process this hook starts on Git
249
+ # Bash (measured there: 57 ms idle, 404 ms under load, against 62 ms for a bare `true`).
250
+ PCT_T=$(( (TOTAL * 1000 + WINDOW / 2) / WINDOW ))
251
+ PCT="$((PCT_T / 10)).$((PCT_T % 10))"
252
+ # The COMPARISONS take the integer part: the thresholds are whole numbers, so nothing below needs the tenth,
253
+ # exactly as session-guard.sh already argues for its own two. This paragraph used to end "the DISPLAYED
254
+ # percentage stays awk's, because %.1f rounds and shell arithmetic truncates" — a true statement about the
255
+ # obvious integer division, and the reason awk survived here for so long. It stopped being true one commit
256
+ # ago: the expression above rounds. A comment that describes a tool the code no longer calls is worse than
257
+ # no comment, because the next reader trusts it instead of the line.
258
+ PCTI="${PCT%%.*}"; case "$PCTI" in ''|*[!0-9]*) PCTI=0 ;; esac
259
+ if [ "$PCTI" -lt 50 ]; then LEVEL="continue"
260
+ elif [ "$PCTI" -lt 75 ]; then LEVEL="medium"
261
+ else LEVEL="handoff+clear"
262
+ fi
263
+ # The '%<number>' shape is a contract: session-guard.sh reads the percentage back out of this line.
264
+ if [ "$VERBOSE" = 1 ]; then
265
+ echo "🔋 Session: %$PCT ($TOTAL/$WINDOW token) → $LEVEL"
266
+ else
267
+ echo "🔋 Session %$PCT → $LEVEL"
268
+ fi
269
+ # >=75%: a short nudge for the model. The USER already gets the full, once-per-threshold warning from the
270
+ # Stop hook (session-guard.sh), so repeating the whole sentence here every turn would be pure duplication.
271
+ if [ "$PCTI" -ge 75 ]; then
272
+ echo "⚠️ >75% — hand off (handoff skill) then /clear; not automatic."
273
+ fi
274
+
275
+ # --- Stale-discipline gate ---------------------------------------------------------------------------
276
+ # CLAUDE.md and the discipline it imports are read ONCE, when the session starts. Update Crewforth while a
277
+ # session is running and every file on disk changes while the rules already in the model's context stay at
278
+ # the old version — it keeps quoting rules that no longer exist, and nothing says so. This does.
279
+ #
280
+ # Only meaningful on the UserPromptSubmit call: session-guard.sh pipes a Stop payload through this same
281
+ # script, and a by-hand run has no stdin at all. Fails open — no stdin, no session_id, no VERSION: silent.
282
+ case "$IN" in *'"hook_event_name"'*UserPromptSubmit*) ;; *) exit 0 ;; esac
283
+
284
+ # The session id is used twice below; computing it once is not a tidiness question. Measured on a corporate
285
+ # Windows machine: a process costs ~290ms there against ~2ms on Linux, so every spawn removed from a hook that
286
+ # runs on EVERY turn is a third of a second the user waits before the model has even started.
287
+ SID=""
288
+ case "$IN" in *'"session_id"'*)
289
+ # Extracted with parameter expansion, not `printf | sed | head | tr`. Four spawns, on every turn, to lift a
290
+ # UUID out of a string the shell already holds. It reads worse and costs ~1.2s a turn on a machine where a
291
+ # process is 290ms. Handles both `"session_id":"x"` and `"session_id": "x"` — the cut is at the next quote
292
+ # after the colon either way.
293
+ SID="${IN#*\"session_id\"}"; SID="${SID#*:}"; SID="${SID#*\"}"; SID="${SID%%\"*}"
294
+ # It becomes a filename, so it is VALIDATED rather than trusted. Anything unexpected falls back to the old
295
+ # scrub — correctness first, and the fallback costs nothing in the normal case because it never runs.
296
+ case "$SID" in
297
+ ''|*[!A-Za-z0-9._-]*) SID="$(printf '%s' "$IN" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 | tr -cd 'A-Za-z0-9._-')" ;;
298
+ esac ;;
299
+ esac
300
+
301
+ # --- Publish the measurement so the Stop hook does not have to repeat it ------------------------------
302
+ # session-guard.sh needs the same number, and it used to get it by running THIS script again as a nested
303
+ # `bash context-usage.sh --verbose` — a second shell startup plus a second full transcript scan, at the end of
304
+ # every single turn. On the machine above that pair costs seconds per turn, to recompute a figure that was
305
+ # already sitting in memory one hook earlier.
306
+ #
307
+ # The honest cost of sharing it: this reading is taken at the START of the turn, so the Stop hook sees a fill
308
+ # that does not include the turn's own output. It can therefore cross a threshold one turn later than a fresh
309
+ # measurement would. That is acceptable for an advisory warning and is NOT acceptable silently — session-guard
310
+ # falls back to measuring for itself whenever this file is missing, so the accurate path always exists.
311
+ if [ -n "$SID" ]; then
312
+ printf '%s %s %s %s\n' "$PCT" "$TOTAL" "$WINDOW" "$LEVEL" > "${TMPDIR:-/tmp}/crew-context.${SID}" 2>/dev/null || true
313
+ fi
314
+
315
+ # --- Stale-WIRING gate -------------------------------------------------------------------------------
316
+ # A resumed session keeps the hook wiring it was started with. Measured on Windows: settings.json on disk had
317
+ # already been corrected, and `--resume` still produced the error naming the OLD, mangled path — while the same
318
+ # event under a fresh session was clean. So after a kit update, `--resume` carries the previous wiring, and on
319
+ # the release that fixed the Windows path that means the gates are still the broken ones.
320
+ #
321
+ # The obvious gate cannot work: a hook cannot report its own absence, and under the old wiring on Windows NO
322
+ # hook launched at all. What this catches is the other half — a session where the hooks DO run, but not the way
323
+ # the file on disk says they should. `$0` is the evidence: Crewforth wires `bash .claude/hooks/<name>.sh`, so a
324
+ # correctly-launched hook sees a relative `$0`. Anything else means this session was launched from a different
325
+ # settings.json than the one now on disk.
326
+ #
327
+ # Silent unless settings.json actually carries Crewforth's current shape — a project that rewired its hooks by
328
+ # hand is not wrong, and warning it every turn would be noise it cannot fix.
329
+ CREWSET="$HERE/../settings.json"
330
+ if [ -f "$CREWSET" ] && grep -q 'bash \.claude/hooks/context-usage\.sh' "$CREWSET" 2>/dev/null; then
331
+ case "$0" in
332
+ .claude/hooks/*) ;; # launched exactly as the file on disk wires it
333
+ *) echo "⚠️ this session is running OLDER hook wiring than .claude/settings.json on disk (resumed across a Crewforth update). The gates in force are the previous ones — ask the user to quit the CLI and start a NEW session; --resume will not pick up the change." ;;
334
+ esac
335
+ fi
336
+
337
+ # --- Stale-discipline gate ---------------------------------------------------------------------------
338
+ KITVER="$HERE/../VERSION" # hooks live in .claude/hooks -> .claude/VERSION
339
+ [ -f "$KITVER" ] || exit 0
340
+ NOW="$(head -1 "$KITVER" 2>/dev/null | tr -cd '0-9A-Za-z.-')"
341
+ [ -n "$NOW" ] || exit 0
342
+ [ -n "$SID" ] || exit 0 # computed once, above
343
+ MARK="${TMPDIR:-/tmp}/crew-kit-version.${SID}"
344
+ if [ ! -e "$MARK" ]; then
345
+ printf '%s' "$NOW" > "$MARK" 2>/dev/null || true # first turn: remember the version, say nothing
346
+ else
347
+ WAS="$(head -1 "$MARK" 2>/dev/null)"
348
+ # Repeated on every turn on purpose: the loaded context stays stale until a new session starts (/clear or a
349
+ # relaunch). /compact re-reads CLAUDE.md but keeps the session id, so this warning would not stop after it.
350
+ [ -n "$WAS" ] && [ "$WAS" != "$NOW" ] && \
351
+ echo "⚠️ Crewforth updated $WAS → $NOW mid-session. The discipline in your context is the OLD one — do not act on it; ask the user to run /clear (or quit and relaunch Claude Code): a new session loads the new one."
352
+ fi
353
+ exit 0
@@ -0,0 +1,124 @@
1
+ # Floor guard — moves that LOWER the quality bar on the way to a green check (ERE, case-SENSITIVE).
2
+ #
3
+ # An agent that hits a red check does not invent a clever loophole; it takes the cheapest road to green. That road
4
+ # is nearly always one of a handful of shapes, and every one of them is visible in the lines a commit ADDS: a
5
+ # checker switched off where it fired, a test made to stop running, a stub or a swallowed error standing where the
6
+ # work should be. Tightening the bar needs no gate. Loosening it should be loud. This list is the loud part.
7
+ #
8
+ # Only ADDED lines in project code are scanned. Documentation (.md .mdx .txt .rst .adoc) is not: a suppression
9
+ # written in prose silences nothing. Generated files are not either — a file whose first lines carry a generator
10
+ # marker (`<auto-generated`, `Code generated … DO NOT EDIT`, `@generated`) was not written by the author of this
11
+ # commit, and EF Core alone puts a warning pragma in every model snapshot it emits (395k such files on GitHub,
12
+ # measured), so without that exemption an EF Core project could not commit a migration.
13
+ #
14
+ # A legitimate exception is a tracked decision, not a loosened rule: `.floor-allowlist.txt` at the repo root, one
15
+ # entry per line — `path:<glob>` exempts a path, `rule:<name>` turns one rule off, and any other line exempts the
16
+ # EXACT pattern line below. The allowlist change sits in the same diff as the code it excuses, where review sees it.
17
+ #
18
+ # `path:` IS A SHELL PATTERN, NOT A .gitignore GLOB, and it is wider than it reads. `*` also matches `/`, so
19
+ # `path:*.cs` exempts EVERY C# file in the repository, not the ones at the root; and `**` means nothing special, so
20
+ # `path:**/gen/*` exempts `a/gen/x.cs` but not a root-level `gen/x.cs`. Both were measured on Windows 11 and both err
21
+ # toward exempting more than the author meant, which is the dangerous direction for an exception. Write the narrowest
22
+ # path that names the thing: `path:src/legacy/*`, not `path:*legacy*`.
23
+ #
24
+ # Each pattern carries its own case below it (`#test:` must be caught, `#test-clean:` must not), and smoke-test.sh
25
+ # drives every one through the REAL pre-commit. A `# rule:` line names the rule for the patterns that follow it.
26
+ #
27
+ # A PATTERN MUST NOT BEGIN WITH `#`. Every loop that reads this file treats such a line as a comment and skips
28
+ # it, so `#pragma …` or `# noqa` written plainly is a pattern that looks armed and matches nothing. Write the
29
+ # character as a one-member bracket instead — `[#]pragma` is the same ERE and is read as a pattern.
30
+ #
31
+ # Written for Crewforth:
32
+ # bash rather than Node, the staged diff instead of a merge base, per-ecosystem patterns for the stacks Crewforth
33
+ # supports, and the generated-file and documentation exemptions that stack needs.
34
+
35
+ # rule: silenced-checker
36
+ @ts-(ignore|nocheck)
37
+ #test: const total = compute(order); // @ts-ignore
38
+ #test: /* @ts-nocheck */
39
+ eslint-disable
40
+ #test: // eslint-disable-next-line no-explicit-any
41
+ biome-ignore
42
+ #test: // biome-ignore lint/suspicious/noExplicitAny: legacy
43
+ (istanbul|c8) ignore
44
+ #test: /* istanbul ignore next */
45
+ [#][[:space:]]*noqa
46
+ #test: import os, sys # noqa: E401
47
+ [#][[:space:]]*type:[[:space:]]*ignore
48
+ #test: value = parse(raw) # type: ignore[arg-type]
49
+ [#][[:space:]]*pragma:[[:space:]]*no cover
50
+ #test: def debug_dump(): # pragma: no cover
51
+ pylint:[[:space:]]*disable
52
+ #test: # pylint: disable=broad-except
53
+ [#]pragma[[:space:]]+warning[[:space:]]+disable
54
+ #test: #pragma warning disable CS8618
55
+ SuppressMessage\(
56
+ #test: [SuppressMessage("Design", "CA1031")]
57
+ NOSONAR
58
+ #test: catch (Exception) { throw; } // NOSONAR
59
+ ExcludeFromCodeCoverage
60
+ #test: [ExcludeFromCodeCoverage]
61
+ //[[:space:]]*nolint
62
+ #test: if err := run(); err != nil { //nolint:errcheck
63
+ @SuppressWarnings
64
+ #test: @SuppressWarnings("unchecked")
65
+ @Suppress\(
66
+ #test: @Suppress("UNCHECKED_CAST")
67
+ [#]!?\[allow\(
68
+ #test: #[allow(dead_code)]
69
+ nosemgrep
70
+ #test: eval(userInput) // nosemgrep
71
+ gitleaks:allow
72
+ #test: const sample = "abc"; // gitleaks:allow
73
+ (^|[^A-Za-z0-9_])nosec([^A-Za-z0-9_]|$)
74
+ #test: subprocess.call(cmd, shell=True) # nosec
75
+ Stryker disable
76
+ #test: // Stryker disable next-line all
77
+ #test-clean: // @ts-expect-error: the upstream type is wrong, tracked upstream
78
+ #test-clean: const config = loadEslintConfig(root);
79
+ #test-clean: # the type annotation is ignored by the serializer
80
+ #test-clean: [JsonIgnore]
81
+ #test-clean: const nosecret = readVault();
82
+
83
+ # rule: test-made-easier
84
+ (^|[^A-Za-z0-9_])(it|test|describe|context|suite)\.(skip|only)\(
85
+ #test: it.skip('rejects an expired token', async () => {
86
+ #test: describe.only('checkout', () => {
87
+ (^|[^A-Za-z0-9_.])x(it|describe|test)\(
88
+ #test: xit('rounds half-cents up', () => {
89
+ @pytest\.mark\.skip
90
+ #test: @pytest.mark.skip(reason="flaky on CI")
91
+ (^|[^A-Za-z0-9_])t\.Skip(Now|f)?\(
92
+ #test: t.Skip("needs a database")
93
+ \[(Fact|Theory)\([^]]*Skip[[:space:]]*=
94
+ #test: [Fact(Skip = "broken after the refactor")]
95
+ \[Ignore(\(|\])
96
+ #test: [Ignore("revisit")]
97
+ @(Disabled|Ignore)([^A-Za-z0-9_]|$)
98
+ #test: @Disabled
99
+ #test-clean: return events.skip(1).take(3);
100
+ #test-clean: var page = query.Skip(20).Take(10);
101
+ #test-clean: [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
102
+ #test-clean: it('rejects an expired token', async () => {
103
+ #test-clean: const exit = await shell.exec('npm test');
104
+
105
+ # rule: unfinished-work
106
+ throw[[:space:]]+new[[:space:]]+NotImplementedException
107
+ #test: public Task Handle() => throw new NotImplementedException();
108
+ throw[[:space:]]+new[[:space:]]+Error\([^)]*[Nn]ot[[:space:]]+implemented
109
+ #test: throw new Error("not implemented yet");
110
+ (^|[^A-Za-z0-9_])(todo|unimplemented)!\(
111
+ #test: fn settle(&self) -> Amount { todo!() }
112
+ panic\([^)]*[Nn]ot[[:space:]]+implemented
113
+ #test: panic("not implemented")
114
+ catch[[:space:]]*(\([^)]*\))?[[:space:]]*\{[[:space:]]*\}
115
+ #test: try { save(); } catch (e) {}
116
+ #test: } catch {}
117
+ except[^:]*:[[:space:]]*pass([[:space:]]|$)
118
+ #test: except Exception: pass
119
+ \.catch\([[:space:]]*\([^)]*\)[[:space:]]*=>[[:space:]]*\{[[:space:]]*\}[[:space:]]*\)
120
+ #test: fetchRates().catch(() => {});
121
+ #test-clean: catch (err) { logger.error(err); throw err; }
122
+ #test-clean: raise NotImplementedError # abstract: every provider overrides this
123
+ #test-clean: // TODO(ENG-441): tighten once the migration lands
124
+ #test-clean: const implemented = features.filter(f => f.done);