crewforth 0.0.1 → 3.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +3784 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -7
  4. package/VERSION +1 -0
  5. package/adopt.sh +1670 -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 +465 -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/star.sh +70 -0
  29. package/kit/eval/parser-conformance.sh +612 -0
  30. package/kit/eval/preflight.sh +186 -0
  31. package/kit/eval/routing-eval.sh +209 -0
  32. package/kit/eval/scan-skill.sh +162 -0
  33. package/kit/eval/smoke-test.sh +6693 -0
  34. package/kit/eval/update-guard.sh +81 -0
  35. package/kit/eval/utilization.sh +130 -0
  36. package/kit/hooks/board-sync.sh +106 -0
  37. package/kit/hooks/board.sh +962 -0
  38. package/kit/hooks/commit-msg +49 -0
  39. package/kit/hooks/context-usage.sh +353 -0
  40. package/kit/hooks/floor-blocklist.txt +124 -0
  41. package/kit/hooks/guard-bash.sh +1431 -0
  42. package/kit/hooks/guard-commit-scan.sh +511 -0
  43. package/kit/hooks/guard-write.sh +513 -0
  44. package/kit/hooks/pre-commit +577 -0
  45. package/kit/hooks/route-hint.sh +185 -0
  46. package/kit/hooks/secret-blocklist.txt +82 -0
  47. package/kit/hooks/session-guard.sh +149 -0
  48. package/kit/hooks/session-rehydrate.sh +41 -0
  49. package/kit/hooks/session-stats.sh +252 -0
  50. package/kit/hooks/session-update-check.sh +236 -0
  51. package/kit/hooks/skill-trust.sh +150 -0
  52. package/kit/hooks/trace-blocklist.txt +53 -0
  53. package/kit/settings.json +132 -0
  54. package/kit/skills/a11y/SKILL.md +44 -0
  55. package/kit/skills/adr/SKILL.md +74 -0
  56. package/kit/skills/api-design/SKILL.md +50 -0
  57. package/kit/skills/automode-policy/SKILL.md +91 -0
  58. package/kit/skills/automode-policy/references/policy.json +13 -0
  59. package/kit/skills/automode-policy/scripts/apply.sh +84 -0
  60. package/kit/skills/automode-policy/scripts/check.sh +102 -0
  61. package/kit/skills/backend-architecture/SKILL.md +103 -0
  62. package/kit/skills/brainstorm/SKILL.md +47 -0
  63. package/kit/skills/ci-pipeline/SKILL.md +38 -0
  64. package/kit/skills/commit-message/SKILL.md +82 -0
  65. package/kit/skills/commit-message/references/examples.md +8 -0
  66. package/kit/skills/confidence-check/SKILL.md +86 -0
  67. package/kit/skills/crew-board/SKILL.md +55 -0
  68. package/kit/skills/crew-brainstorm/SKILL.md +19 -0
  69. package/kit/skills/crew-code-review/SKILL.md +151 -0
  70. package/kit/skills/crew-code-review/references/history-search.md +19 -0
  71. package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
  72. package/kit/skills/crew-code-review/references/receiving.md +23 -0
  73. package/kit/skills/crew-doctor/SKILL.md +36 -0
  74. package/kit/skills/crew-gates/SKILL.md +25 -0
  75. package/kit/skills/crew-handoff/SKILL.md +14 -0
  76. package/kit/skills/crew-plan/SKILL.md +18 -0
  77. package/kit/skills/crew-review/SKILL.md +23 -0
  78. package/kit/skills/crew-ship/SKILL.md +19 -0
  79. package/kit/skills/crew-skill/SKILL.md +61 -0
  80. package/kit/skills/crew-studio/SKILL.md +129 -0
  81. package/kit/skills/crew-update/SKILL.md +43 -0
  82. package/kit/skills/db-migration/SKILL.md +120 -0
  83. package/kit/skills/db-migration/references/backup-restore.md +33 -0
  84. package/kit/skills/db-migration/references/raw-sql.md +30 -0
  85. package/kit/skills/db-migration/references/tool-matrix.md +35 -0
  86. package/kit/skills/dependency-audit/SKILL.md +61 -0
  87. package/kit/skills/dependency-upgrade/SKILL.md +82 -0
  88. package/kit/skills/deploy/SKILL.md +100 -0
  89. package/kit/skills/deploy/references/proxy-ssl.md +56 -0
  90. package/kit/skills/deploy/references/rollback.md +18 -0
  91. package/kit/skills/deploy/references/scripts.md +9 -0
  92. package/kit/skills/deploy/references/topology-a.md +128 -0
  93. package/kit/skills/deploy/references/topology-b.md +44 -0
  94. package/kit/skills/docs-writer/SKILL.md +53 -0
  95. package/kit/skills/docs-writer/references/coauthoring.md +34 -0
  96. package/kit/skills/eval-grader/SKILL.md +75 -0
  97. package/kit/skills/eval-grader/references/method.md +51 -0
  98. package/kit/skills/frontend/SKILL.md +56 -0
  99. package/kit/skills/frontend/references/verify-contract.md +41 -0
  100. package/kit/skills/frontend-design/SKILL.md +71 -0
  101. package/kit/skills/frontend-design/references/design-review.md +34 -0
  102. package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
  103. package/kit/skills/handoff/SKILL.md +71 -0
  104. package/kit/skills/handoff/references/cold-reader.md +51 -0
  105. package/kit/skills/i18n-integrity/SKILL.md +31 -0
  106. package/kit/skills/incident-runbook/SKILL.md +40 -0
  107. package/kit/skills/incident-runbook/references/postmortem.md +16 -0
  108. package/kit/skills/iterate/SKILL.md +40 -0
  109. package/kit/skills/mcp-builder/SKILL.md +54 -0
  110. package/kit/skills/mcp-builder/references/building.md +71 -0
  111. package/kit/skills/observability/SKILL.md +50 -0
  112. package/kit/skills/performance/SKILL.md +38 -0
  113. package/kit/skills/performance/references/bottlenecks.md +10 -0
  114. package/kit/skills/privacy-compliance/SKILL.md +75 -0
  115. package/kit/skills/red-team/SKILL.md +48 -0
  116. package/kit/skills/reflect/SKILL.md +57 -0
  117. package/kit/skills/release/SKILL.md +44 -0
  118. package/kit/skills/security-scan/SKILL.md +99 -0
  119. package/kit/skills/security-scan/references/ai-agents.md +107 -0
  120. package/kit/skills/security-scan/references/fronts.md +99 -0
  121. package/kit/skills/security-scan/references/prompting.md +23 -0
  122. package/kit/skills/security-scan/references/reporting.md +75 -0
  123. package/kit/skills/security-scan/references/verify.md +131 -0
  124. package/kit/skills/sonarqube-check/SKILL.md +108 -0
  125. package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
  126. package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
  127. package/kit/skills/spec-planning/SKILL.md +92 -0
  128. package/kit/skills/systematic-debugging/SKILL.md +57 -0
  129. package/kit/skills/systematic-debugging/references/techniques.md +44 -0
  130. package/kit/skills/teamboard/SKILL.md +88 -0
  131. package/kit/skills/teamboard/references/setup.md +37 -0
  132. package/kit/skills/testing/SKILL.md +49 -0
  133. package/kit/skills/testing/references/flaky-triage.md +51 -0
  134. package/kit/skills/threat-model/SKILL.md +57 -0
  135. package/kit/skills/threat-model/references/interview.md +51 -0
  136. package/kit/skills/threat-model/references/schema.md +42 -0
  137. package/kit/skills/token-budget/SKILL.md +51 -0
  138. package/kit/skills/trace-scan/SKILL.md +43 -0
  139. package/kit/skills/worktree/SKILL.md +40 -0
  140. package/kit/studio/README.md +266 -0
  141. package/kit/studio/ensure-node.sh +471 -0
  142. package/kit/studio/package.json +14 -0
  143. package/kit/studio/server/hooks/studio-gate.sh +169 -0
  144. package/kit/studio/server/index.js +902 -0
  145. package/kit/studio/server/lib/crew-env.js +11 -0
  146. package/kit/studio/server/lib/fleet.js +146 -0
  147. package/kit/studio/server/lib/graph.js +533 -0
  148. package/kit/studio/server/lib/instance.js +135 -0
  149. package/kit/studio/server/lib/kit-telemetry.js +182 -0
  150. package/kit/studio/server/lib/kit.js +158 -0
  151. package/kit/studio/server/lib/palette.js +118 -0
  152. package/kit/studio/server/lib/peers.js +72 -0
  153. package/kit/studio/server/lib/permissions.js +150 -0
  154. package/kit/studio/server/lib/projects.js +294 -0
  155. package/kit/studio/server/lib/roster.js +191 -0
  156. package/kit/studio/server/lib/session.js +309 -0
  157. package/kit/studio/server/lib/terminal.js +91 -0
  158. package/kit/studio/server/lib/transcript.js +100 -0
  159. package/kit/studio/web/app.js +1098 -0
  160. package/kit/studio/web/canvas.js +1179 -0
  161. package/kit/studio/web/chat.js +543 -0
  162. package/kit/studio/web/index.html +86 -0
  163. package/kit/studio/web/md.js +110 -0
  164. package/kit/studio/web/storage-migrate.js +23 -0
  165. package/kit/studio/web/style.css +1453 -0
  166. package/package.json +46 -8
  167. package/start.sh +748 -0
  168. package/bin/crewforth.js +0 -10
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bash
2
+ # Route hint — name the owning agent next to the request itself.
3
+ #
4
+ # WHY THIS EXISTS, measured rather than assumed. Crewforth's premise is that specialist agents run the work. On a
5
+ # focused, single-domain request that does not happen: 42 measured sessions across the eval suite, two A/B pairs
6
+ # and a twelve-agent domain sweep produced ZERO spontaneous delegations. Two things were tried first and both
7
+ # came back zero — rewriting the agents' `description` into ownership language, and telling the discipline to
8
+ # call the Agent tool concretely. What DOES work is naming the agent explicitly: a command body that @-mentions
9
+ # its agents delegated 3 of 3, and a single request carrying twelve different jobs fanned out to all twelve
10
+ # owners correctly. So the mechanism is sound; the trigger is the shape of the request.
11
+ #
12
+ # The subagents docs list three inputs to the delegation decision: "the task description in your request, the
13
+ # `description` field in subagent configurations, and current context". Crewforth had touched the second and the
14
+ # third. This hook is the first: `UserPromptSubmit` can return `additionalContext`, and for that event the docs
15
+ # place it "alongside the submitted prompt" — the one position we had never used.
16
+ #
17
+ # It states an owner. It never decides FOR the model, never blocks, and stays silent unless a match is clear:
18
+ # a wrong route is worse than none, because it looks like Crewforth worked.
19
+ #
20
+ # ---------------------------------------------------------------------------------------------------------
21
+ # ONE AWK PASS, NOT A SHELL LOOP — this hook runs on EVERY prompt, so its cost is the session's floor.
22
+ #
23
+ # The first version scored the components with nested shell loops: for each of ~50 component files a
24
+ # grep+head+grep+sed, and then for each of its trigger phrases a `norm()` pipeline (sed|tr|sed) plus a
25
+ # `printf|grep -qE` with another `sed` nested inside the pattern. 348 phrases across the shipped payload put
26
+ # that at roughly 2000 process spawns per prompt. Measured on an M-series Mac: 3.35s of pure fork overhead
27
+ # (104% CPU, ~1.7ms per spawn) for a hook whose actual work is substring matching on a few KB of text.
28
+ #
29
+ # That is merely wasteful on macOS/Linux. On Windows it is fatal. Git Bash has no real fork(): every process
30
+ # is a CreateProcess plus the MSYS2 emulation layer plus whatever the AV scanner charges. Measured on a
31
+ # Windows 11 desktop rather than assumed: 62-135 ms per spawn idle, and up to ~400 ms while the machine is
32
+ # busy — forty to eighty times the ~1.7 ms Linux pays, not the "20-50 ms" this comment claimed before anyone
33
+ # ran it here. The same 2000 spawns therefore land at two to four MINUTES, not 40-100 seconds, against a
34
+ # 10s hook timeout. Claude Code blocks on the hook until that timeout expires, on EVERY prompt, and then
35
+ # discards the output — so Crewforth paid the full stall and got no routing for it. That is the "it hangs and
36
+ # nothing works" report from Windows users, and it is not a Claude Code bug: Crewforth was spending the budget.
37
+ #
38
+ # Everything below is therefore one awk invocation over the component files, with the normalisation and the
39
+ # matching done inside awk. Total external processes: FIVE — cat, sed, sed, head, awk. (The count said four
40
+ # until it was traced with `bash -x` on Windows; the `cat` on the next line reads stdin and had been read
41
+ # past.) Measured end to end here: 5 spawns, ~328 ms per prompt idle. eval/smoke-test.sh §7y pins the shape.
42
+ set -uo pipefail
43
+ IN="$(cat)"
44
+ case "$IN" in *'"hook_event_name"'*UserPromptSubmit*) ;; *) exit 0 ;; esac
45
+
46
+ # Two editions, two layouts: a full install puts the components under .claude/, the plugin ships them beside the
47
+ # hook itself. Resolve both or the plugin channel silently loses routing — the same one-channel-weaker failure
48
+ # the commit content gate had before 1.9.0.
49
+ #
50
+ # `${VAR//\\//}` folds backslashes to forward slashes. On Windows both CLAUDE_PROJECT_DIR and CLAUDE_PLUGIN_ROOT
51
+ # arrive as native paths (`C:\Repos\app`), and a native path pasted into a POSIX one produces
52
+ # `C:\Repos\app/.claude/agents` — a shape neither side reliably resolves. Forward slashes work on every
53
+ # platform, including Git Bash, and the substitution is a no-op where there is nothing to fold.
54
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
55
+ PROOT="${CLAUDE_PLUGIN_ROOT//\\//}"
56
+ [ -d "$PROOT/agents" ] && { AGENTS="$PROOT/agents"; SKILLS="$PROOT/skills"; }
57
+ fi
58
+ if [ -z "${AGENTS:-}" ]; then
59
+ PROJ="${CLAUDE_PROJECT_DIR:-.}"; PROJ="${PROJ//\\//}"
60
+ AGENTS="$PROJ/.claude/agents"; SKILLS="$PROJ/.claude/skills"
61
+ fi
62
+ [ -d "$AGENTS" ] || [ -d "$SKILLS" ] || exit 0
63
+
64
+ # The prompt text, taken as a raw slice: no jq dependency, and a partial read is fine because matching is by
65
+ # substring anyway. Everything after `"prompt":"` up to the closing quote that is not escaped. It goes to awk
66
+ # through the ENVIRONMENT, never through `-v`: awk expands escape sequences in a `-v` value, so a prompt
67
+ # containing a literal backslash would be rewritten before the normaliser ever saw it.
68
+ # TWO FIELD NAMES, because the payload has carried both and this hook is the only thing in Crewforth that reads
69
+ # the prompt text at all. The published UserPromptSubmit schema names the field `user_input`; the payload this
70
+ # hook was written against, and the suite that pins it, used `prompt`. Which one a given CLI sends is not a fact
71
+ # this repo can establish from here, and getting it wrong is SILENT: the slice comes back empty, the hook exits
72
+ # 0, and routing is simply gone while every test stays green because the tests choose the name too. So both are
73
+ # accepted. The fallback costs nothing on the path that already worked — it only runs when the first name found
74
+ # nothing. `prompt_id` cannot be mistaken for `prompt`: the pattern includes the closing quote.
75
+ CREW_PROMPT="$(printf '%s' "$IN" | sed -n 's/.*"prompt"[[:space:]]*:[[:space:]]*"\(.*\)/\1/p' | sed 's/","[a-z_]*":.*$//' | head -c 4000)"
76
+ [ -n "$CREW_PROMPT" ] || CREW_PROMPT="$(printf '%s' "$IN" | sed -n 's/.*"user_input"[[:space:]]*:[[:space:]]*"\(.*\)/\1/p' | sed 's/","[a-z_]*":.*$//' | head -c 4000)"
77
+ [ -n "$CREW_PROMPT" ] || exit 0
78
+
79
+ # NOT EVERY TURN CARRIES A REQUEST. A field session watched this hook inject "Use the <x> subagent for this task"
80
+ # on turns where the user had typed nothing at all: a background subagent finished, its REPORT arrived as the
81
+ # turn's text, and the report was scored as if it were the request. Both times the named owner was the agent
82
+ # whose finished work was being reported -- so the hint pushed the main thread to re-delegate work that was
83
+ # already done, at the 10-16k tokens a subagent floor costs. Ten injections that session, zero of them useful.
84
+ #
85
+ # Whether Claude Code really raises UserPromptSubmit for those turns is NOT established here, and this does not
86
+ # depend on the answer: text that OPENS with a notification marker is not a user request under any reading, so
87
+ # staying quiet is right either way. Anchored to the start deliberately -- a person may well write the words
88
+ # "system notification" inside a genuine request, and only a notification BEGINS as one.
89
+ case "$CREW_PROMPT" in
90
+ '<task-notification'*|'<system-reminder'*|'<cross-session-message'*|'<local-command-stdout'*\
91
+ |'[SYSTEM NOTIFICATION'*|'[Task '*|'Another Claude session sent a message'*)
92
+ exit 0 ;;
93
+ esac
94
+ export CREW_PROMPT
95
+
96
+ # Glob expansion is a shell builtin — no forks. Agents FIRST, then skills: awk reads its arguments in order and
97
+ # the tie-break below is first-wins, so the order is part of the contract, not incidental.
98
+ FILES=()
99
+ for f in "$AGENTS"/*.md "$SKILLS"/*/SKILL.md; do [ -e "$f" ] && FILES+=("$f"); done
100
+ [ "${#FILES[@]}" -gt 0 ] || exit 0
101
+
102
+ # Score every installed agent by how many of its trigger phrases appear. Longest phrase wins ties: "state
103
+ # management" is a stronger signal than "state", and a two-word hit beats a one-word hit from another agent.
104
+ # Agents AND skills are scored. The agent answers "who owns this"; the skill answers "how is it done", and the
105
+ # skill is the cheaper, more useful nudge: a subagent re-pays its whole context (measured at 10-16k tokens for a
106
+ # no-op), while a skill just loads its method into the turn already in progress. Measured on a focused request,
107
+ # neither fired on its own — 12 domain tasks produced zero delegations and a skill in only 2 of 12 — so the
108
+ # method Crewforth exists to carry was simply absent from the work.
109
+ RES="$(awk '
110
+ # Same normalisation the routing eval uses: Turkish diacritics folded, lowercased, every run of non-alphanumerics
111
+ # collapsed to one space, and the whole string space-padded so a match is word-bounded. Without the padding a
112
+ # short trigger hides inside a longer word — `UI` inside `build` once sent CI failures to the frontend expert.
113
+ # Folding runs before tolower() because tolower() is ASCII-only in a byte-oriented awk; the gsub pairs cover
114
+ # both cases explicitly so the result does not depend on which awk Git Bash happens to ship.
115
+ function norm(s) {
116
+ gsub(/Ç/,"c",s); gsub(/ç/,"c",s); gsub(/Ğ/,"g",s); gsub(/ğ/,"g",s)
117
+ gsub(/İ/,"i",s); gsub(/ı/,"i",s); gsub(/Ö/,"o",s); gsub(/ö/,"o",s)
118
+ gsub(/Ş/,"s",s); gsub(/ş/,"s",s); gsub(/Ü/,"u",s); gsub(/ü/,"u",s)
119
+ s = tolower(s)
120
+ gsub(/[^a-z0-9]+/," ",s) # also sweeps up any multi-byte leftovers, one space per run
121
+ sub(/^ +/,"",s); sub(/ +$/,"",s)
122
+ return s
123
+ }
124
+ BEGIN { np = " " norm(ENVIRON["CREW_PROMPT"]) " "; bestA=0; bestS=0; nameA=""; nameS="" }
125
+ seen[FILENAME] { next } # one Trigger-phrases line per component, the first
126
+ {
127
+ if (tolower($0) !~ /trigger phrases:/) next
128
+ seen[FILENAME] = 1
129
+ if (FILENAME ~ /\/SKILL\.md$/) { kind="skill"; name=FILENAME; sub(/\/SKILL\.md$/,"",name) }
130
+ else { kind="agent"; name=FILENAME; sub(/\.md$/,"",name) }
131
+ sub(/.*\//,"",name)
132
+ score = 0; rest = $0
133
+ while (match(rest, /"[^"]+"/)) {
134
+ ph = substr(rest, RSTART+1, RLENGTH-2)
135
+ rest = substr(rest, RSTART+RLENGTH)
136
+ p = norm(ph)
137
+ if (p == "") continue
138
+ # Suffix-tolerant, prefix-anchored: the trigger must START at a word boundary, but may carry up to three
139
+ # trailing letters. Word-bounded matching alone silently lost every inflection — `component` did not match
140
+ # "components", `test` did not match "tests" — which is how a hook that looked correct stayed quiet on two
141
+ # thirds of real prompts. The boundary that matters is the LEADING one: it is what keeps `ui` out of `build`.
142
+ # Written as three optional letters rather than {0,3} because interval expressions are not universal in awk.
143
+ # No escaping needed: norm() has already reduced the phrase to [a-z0-9 ], so it carries no metacharacter.
144
+ if (np ~ ("(^| )" p "[a-z]?[a-z]?[a-z]?( |$)")) score += length(ph)
145
+ }
146
+ if (score <= 0) next
147
+ # An AGENT outranks a SKILL when both match: agent = who, skill = how, and the agent applies its own skills
148
+ # anyway, so naming it delivers the method PLUS the isolation and the audit path. A skill is named when no
149
+ # agent owns the domain at all (iterate, reflect, worktree, eval-grader).
150
+ #
151
+ # THE TWO KINDS ARE SCORED SEPARATELY, and that is the fix for a measured silence. The preference used to be
152
+ # applied by OVERWRITING the best match the moment any agent matched, whatever its score — so a one-word
153
+ # agent hit discarded a far stronger skill hit, and the discarded total then fell under the floor below and
154
+ # the hook printed NOTHING. Measured before this change: "this needs an accessibility audit" produced the
155
+ # a11y hint, and "the PAGE needs an accessibility audit" produced silence, because `page` is a
156
+ # crew-frontend-expert trigger worth 4 which replaced the a11y score of ~30 and then failed `>= 6`. Adding a common noun
157
+ # to a request removed the routing entirely, which is the opposite of what a preference is for. Each kind now
158
+ # keeps its own best and the preference is applied at the END, among candidates that clear the floor.
159
+ if (kind == "agent") { if (score > bestA) { bestA=score; nameA=name } }
160
+ else { if (score > bestS) { bestS=score; nameS=name } }
161
+ }
162
+ # Silence is the default. No match, or a match too weak to be sure, means the main thread decides as before.
163
+ # The agent is preferred only when it is credible on its own: a weak agent match no longer buries a strong skill.
164
+ END {
165
+ if (bestA >= 6) print "agent\t" nameA
166
+ else if (bestS >= 6) print "skill\t" nameS
167
+ }
168
+ ' "${FILES[@]}" 2>/dev/null)"
169
+
170
+ [ -n "$RES" ] || exit 0
171
+ BESTKIND="${RES%% *}"; BEST="${RES#* }"
172
+ [ -n "$BEST" ] && [ "$BEST" != "$RES" ] || exit 0
173
+
174
+ # The wording is an INSTRUCTION, not a suggestion, and that distinction is the whole experiment. The first
175
+ # version hedged — "unless it is a one-line edit", "if it is genuinely not that agent's work, say so" — which
176
+ # handed the model two explicit ways to decline, and it took them: 4 of 12. The docs give the phrasing that
177
+ # works verbatim ("Use the test-runner subagent to fix failing tests"), so that is what goes in. A user typing
178
+ # "use crew-frontend-expert for this" gets obeyed without argument; there is no reason to write it more weakly
179
+ # just because a hook is doing the typing.
180
+ if [ "$BESTKIND" = skill ]; then
181
+ MSG="Use the \`$BEST\` skill for this task."
182
+ else
183
+ MSG="Use the $BEST subagent for this task."
184
+ fi
185
+ printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\n' "$MSG"
@@ -0,0 +1,82 @@
1
+ # Secret scanner — high-signal patterns (ERE, case-SENSITIVE). One regex per line; '#' starts a comment.
2
+ # These match secret VALUES (keys / tokens / private keys), not the word "secret".
3
+ # False positive on a real (test) value? Add the EXACT pattern line to .secret-allowlist.txt at the repo root,
4
+ # or replace the value with a placeholder. Deliberate bypass: git commit --no-verify (explicit request only, §4.5).
5
+ #
6
+ # Each pattern carries its own case, on the line right below it:
7
+ # #test: a sample this pattern MUST catch
8
+ # #test-clean: a sample no pattern may catch
9
+ # smoke-test.sh discovers them and drives the REAL hook with them, and it FAILS if a pattern has no case at
10
+ # all — an untested regex is how a typo ships as a gate that silently matches nothing. Samples are deliberately
11
+ # written with a {{A<n>}} placeholder that the test harness expands to n 'A's at run time. The file therefore
12
+ # never contains a contiguous secret-shaped literal — which matters because it ships into every project's
13
+ # .claude/, where their scanners would flag it, and because GitHub push protection blocks such a literal on
14
+ # sight regardless of how low its entropy is (measured: it rejected the Stripe sample).
15
+
16
+ # PEM private keys (RSA / EC / OPENSSH / DSA / PGP ...)
17
+ -----BEGIN ([A-Z]+ )?PRIVATE KEY-----
18
+ #test: -----BEGIN RSA PRIVATE KEY-----
19
+ #test: -----BEGIN PRIVATE KEY-----
20
+
21
+ # AWS access key id
22
+ (AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|A3T)[A-Z0-9]{16}
23
+ #test: aws_access_key_id = AKI{{A17}}
24
+
25
+ # GitHub tokens (personal / oauth / user / server / refresh) + fine-grained PAT
26
+ gh[pousr]_[A-Za-z0-9]{36,}
27
+ #test: token: ghp_{{A36}}
28
+ github_pat_[A-Za-z0-9_]{22,}
29
+ #test: token: github_pat_{{A24}}
30
+
31
+ # Google API key
32
+ AIza[A-Za-z0-9_-]{35}
33
+ #test: key=AIza{{A35}}
34
+
35
+ # Slack token
36
+ xox[baprs]-[A-Za-z0-9-]{10,}
37
+ #test: SLACK_TOKEN=xoxb-{{A12}}
38
+
39
+ # Stripe live secret / restricted key
40
+ (sk|rk)_live_[A-Za-z0-9]{20,}
41
+ #test: STRIPE=sk_live_{{A24}}
42
+
43
+ # OpenAI / Anthropic API keys
44
+ sk-(ant-|proj-)?[A-Za-z0-9_-]{24,}
45
+ #test: API_KEY=sk-ant-{{A24}}
46
+
47
+ # npm access token
48
+ npm_[A-Za-z0-9]{36}
49
+ #test: //registry.npmjs.org/:_authToken=npm_{{A36}}
50
+
51
+ # SendGrid API key
52
+ SG\.[A-Za-z0-9_-]{16,32}\.[A-Za-z0-9_-]{16,}
53
+ #test: SENDGRID_API_KEY=SG.{{A16}}.{{A20}}
54
+
55
+ # JSON Web Token (header.payload.signature)
56
+ eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{10,}
57
+ #test: Authorization: Bearer eyJAAAAAAAAAA.eyJAAAAAA.AAAAAAAAAA
58
+
59
+ # A credential carried in a URL query. THE CLASS EVERY PATTERN ABOVE STRUCTURALLY CANNOT SEE: all eleven
60
+ # recognise a credential by its own SHAPE -- AKIA, ghp_, AIza, xox, sk_live_, sk-ant-, npm_, SG., a JWT, a PEM
61
+ # header -- and a token with no issuer prefix has no shape to recognise. Measured on a published install with
62
+ # the real hook: `http://127.0.0.1:7911/?token=<uuid>`, a bare uuid, and `token=` plus 32 hex all committed
63
+ # cleanly while the calibration lines beside them were caught, so this was a hole rather than a guess.
64
+ # Crewforth produces exactly such a URL itself -- the Studio panel mints one per run and the command hands it to
65
+ # the user -- but the pattern is deliberately about the QUERY KEY, not about that panel: any `?token=` long
66
+ # enough to be real is one, whoever minted it.
67
+ # Anchored on the key and a 20-character floor so the ordinary ways of WRITING one about stay committable:
68
+ # `?token=<your-token>` and `?token=$VAR` carry characters the class excludes, so the run measures zero.
69
+ [?&](token|api_?key|apikey|access_token|auth_token)=[A-Za-z0-9._~-]{20,}
70
+ #test: curl https://api.example.com/v1/items?token={{A20}}
71
+ #test: https://127.0.0.1:7777/?token={{A32}}
72
+ #
73
+ # The other half of the same leak is NOT closable here and is left to `.private-terms.txt` on purpose: a machine
74
+ # name is any word, so no pattern can tell one from prose. That half stays model discipline, and /crew-studio
75
+ # says so where it tells the model what may be quoted.
76
+
77
+ # Values that LOOK secret-adjacent but are not, and must stay committable.
78
+ #test-clean: open http://127.0.0.1:7777/?token=<your-token-here>
79
+ #test-clean: the panel URL carries ?token=$STUDIO_TOKEN from the environment
80
+ #test-clean: const apiKey = process.env.STRIPE_SECRET_KEY
81
+ #test-clean: # set GITHUB_TOKEN in the CI environment, never in the repo
82
+ #test-clean: private key handling is documented in docs/security.md
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook — when session fill crosses a threshold it GUARANTEES the handoff recommendation reaches the USER.
3
+ # Session management = a gate at the threshold, not a wish. But the gate must not nag or burn tokens:
4
+ # - It surfaces the recommendation as a neutral `systemMessage` (user-facing warning), NOT a hook error.
5
+ # (Claude Code renders a BLOCKING Stop hook — exit 2 — as "Stop hook error: ...". We deliberately never block.)
6
+ # - It fires ONCE PER THRESHOLD per session: once at 75% (warn), once more at 90% (critical). Never every turn,
7
+ # so it never forces an extra assistant continuation — which would burn tokens and fill the context faster,
8
+ # i.e. the gate would cause the very thing it exists to prevent.
9
+ # - It NEVER blocks the stop: every path exits 0. A measurement failure fails open, silently.
10
+ # context-usage.sh does the measurement. No automatic /clear — approval is the user's (§4.4).
11
+ # The model is nudged separately every turn by context-usage.sh (UserPromptSubmit); this hook is the
12
+ # guaranteed USER-facing alert. A new session (or /clear) mints a new session_id and re-arms both thresholds.
13
+ set -uo pipefail
14
+ HERE="$(cd "$(dirname "$0")" && pwd)"
15
+ IN="$(cat 2>/dev/null || true)"
16
+
17
+ # Loop guard: if this Stop is already a Stop-hook continuation, do nothing (defensive; we never block anyway).
18
+ case "$IN" in
19
+ *'"stop_hook_active":true'*|*'"stop_hook_active": true'*) exit 0 ;;
20
+ esac
21
+
22
+ # Per-session dedup key: session_id from the Stop stdin JSON (documented field), else the transcript
23
+ # filename. Sanitized to a safe filename fragment. Without any key we fall back to a shared marker
24
+ # (that degenerate case coincides with a measurement failure, which exits silently below anyway).
25
+ #
26
+ # Pure parameter expansion, not `sed | head | tr`. This hook runs on EVERY Stop — once per turn — and on Git
27
+ # Bash a process costs 60-135 ms (measured on a Windows 11 desktop: this hook was 22 processes and 1,805 ms
28
+ # per turn end, of which these three lines were 8). Extraction is shortest-match `#*`, so it takes the FIRST
29
+ # occurrence of the key exactly as the sed did; the `[!…]` class strips to the same safe filename fragment
30
+ # `tr -cd` produced, and that form works on the bash 3.2 macOS ships.
31
+ _slice(){ # $1 = payload, $2 = key -> the value up to the next unescaped-quote-free boundary, "" if absent
32
+ local r="${1#*\"$2\"}"
33
+ [ "$r" != "$1" ] || return 0
34
+ r="${r#*\"}" # past `: "` to the value's opening quote
35
+ printf '%s' "${r%%\"*}"
36
+ }
37
+ TP="$(_slice "$IN" transcript_path)"
38
+ # The path arrives JSON-encoded, so on Windows it carries doubled backslashes. Git Bash happens to accept
39
+ # `C:\\Users\\…` (Windows collapses repeated separators), which is why this was never noticed here — but
40
+ # context-usage.sh already normalises for the same field and does not rely on that, and a check that depends
41
+ # on one shell's tolerance is a check that stops running when someone uses another. Free: no process.
42
+ TP="${TP//\\\\//}"; TP="${TP//\\//}"
43
+ KEY="$(_slice "$IN" session_id)"; KEY="${KEY//[!A-Za-z0-9._-]/}"
44
+ if [ -z "$KEY" ]; then KEY="${TP##*[/\\]}"; KEY="${KEY//[!A-Za-z0-9._-]/}"; fi
45
+ [ -n "$KEY" ] || KEY="unknown"
46
+
47
+ # Compaction generation. A `/compact` does NOT mint a new session_id — only a new session or `/clear` does —
48
+ # so the per-tier markers below survive it, and that silently disarms this gate exactly where it matters most:
49
+ # a session warned at 90% compacts down to ~5%, climbs all the way back, and the user is never told a second
50
+ # time. Keying the markers by compaction COUNT gives every generation its own pair of thresholds.
51
+ # The boundary record is `"subtype":"compact_boundary"` with `compactMetadata.trigger` (verified against real
52
+ # transcripts). grep, not awk: this runs inside a hook timeout on every Stop, and the same size guard
53
+ # context-usage.sh uses applies — past the cap we skip the count and fall back to the old single-generation
54
+ # behaviour rather than risk the timeout.
55
+ COMP=0; AUTOC=0
56
+ if [ -n "$TP" ] && [ -f "$TP" ]; then
57
+ # `$(( ))` instead of `| tr -cd '0-9'`: arithmetic already ignores the surrounding whitespace wc and grep
58
+ # emit, and it costs no process where tr costs one each — three of them, on every turn.
59
+ #
60
+ # NO `|| echo 0` on the greps. `grep -c` with zero matches PRINTS "0" AND EXITS 1, so the fallback
61
+ # fired too and the value became two lines — `0\n0` — which `$(( ))` cannot parse. Measured on a real
62
+ # session: "session-guard.sh: line 61: 0\n0 + 0 : arithmetic syntax error". Every transcript that has
63
+ # not been compacted takes that path, which is every fresh session, so the 75/90 warning this hook
64
+ # exists to emit was failing before it could speak. `wc -c` keeps its fallback: it prints nothing when
65
+ # the file cannot be read, so there the `||` supplies a value rather than appending a second one.
66
+ SZ="$(wc -c < "$TP" 2>/dev/null || echo 0)"; SZ=$(( ${SZ:-0} + 0 ))
67
+ if [ "$SZ" -le "${CREW_CONTEXT_MAX_BYTES:-209715200}" ]; then
68
+ COMP="$(grep -c '"subtype": *"compact_boundary"' "$TP" 2>/dev/null)"; COMP=$(( ${COMP:-0} + 0 ))
69
+ AUTOC="$(grep -c '"compact_boundary".*"trigger": *"auto"' "$TP" 2>/dev/null)"; AUTOC=$(( ${AUTOC:-0} + 0 ))
70
+ fi
71
+ fi
72
+ marker(){ printf '%s/crew-session-guard.%s.c%s.%s' "${TMPDIR:-/tmp}" "$KEY" "$COMP" "$1"; }
73
+
74
+ # An AUTO compaction is not a milestone, it is a loss: state nobody chose to drop is already gone, and the fill
75
+ # reading right after it is reassuringly low precisely because the context was thrown away. Announce it once per
76
+ # generation, independently of the fill tier — waiting for 75% would report the loss long after it happened.
77
+ if [ "${AUTOC:-0}" -gt 0 ] && [ ! -e "$(marker autocompact)" ]; then
78
+ : > "$(marker autocompact)" 2>/dev/null || true
79
+ printf '{"systemMessage":"⚠️ Auto-compaction has fired %s time(s) this session — context was dropped that nobody chose to drop. Check that docs/SESSION_STATE.md still matches reality, and hand off at the next phase boundary instead of riding the fill up to another one."}\n' "$AUTOC"
80
+ exit 0
81
+ fi
82
+
83
+ # Real context%, from context-usage.sh's measurement. Fail-open on any error.
84
+ #
85
+ # FAST PATH — read the figure context-usage.sh already published this turn. That hook runs on UserPromptSubmit,
86
+ # measures exactly this, and now writes it to a session-keyed file. Re-deriving it here meant a second shell
87
+ # startup plus a second full transcript scan at the end of EVERY turn: measured at ~31 processes per Stop, and
88
+ # on a corporate Windows machine a process costs ~290ms, so the turn ended with seconds of silence.
89
+ #
90
+ # Read with `$(<file)` — a builtin, no `cat`. Parsed by word splitting, no `sed`. What it costs: the reading is
91
+ # from the start of the turn, so it excludes this turn's own output and can cross a threshold one turn late.
92
+ # For a warning that never blocks, one turn of lag is worth seconds a turn, and it is not silent: without the
93
+ # file we measure properly below, so the accurate path is always there when the fast one is not.
94
+ PCT=""; TOTAL=""; WINDOW=""; LEVEL=""; LINE=""
95
+ CACHE="${TMPDIR:-/tmp}/crew-context.${KEY}"
96
+ if [ -f "$CACHE" ]; then
97
+ read -r PCT TOTAL WINDOW LEVEL < "$CACHE" 2>/dev/null || true
98
+ case "$PCT" in ''|*[!0-9.]*) PCT="" ;; esac # anything unexpected -> fall through and measure
99
+ [ -n "$PCT" ] && LINE="🔋 Session: %$PCT ($TOTAL/$WINDOW token) → $LEVEL"
100
+ fi
101
+
102
+ if [ -z "$PCT" ]; then
103
+ # SLOW PATH — no published reading (first turn, a by-hand run, or context-usage silent). Measure for real.
104
+ # --verbose here: this line reaches the USER at most twice per session, so the raw token counts are free.
105
+ LINE="$(printf '%s' "$IN" | bash "$HERE/context-usage.sh" --verbose 2>/dev/null || true)"
106
+ LINE="${LINE%%$'\n'*}" # first line, without paying `head` a process for it
107
+ [ -n "$LINE" ] || exit 0
108
+ # Pull the percentage back out of the "🔋 Session: %77.2 (...)" line. context-usage.sh pins LC_ALL=C, so the
109
+ # decimal separator is always '.' whatever the locale. No number -> cannot classify -> stay silent (fail open).
110
+ # Pure expansion rather than `sed | head`: take everything after the first '%', then stop at the first
111
+ # character that is not a digit or a dot. Same first-match semantics as the sed, two processes cheaper, and
112
+ # this runs at the end of every turn.
113
+ PCT="${LINE#*%}"
114
+ [ "$PCT" != "$LINE" ] || PCT=""
115
+ PCT="${PCT%%[!0-9.]*}"
116
+ case "$PCT" in ''|*[!0-9.]*|.*) PCT="" ;; esac
117
+ [ -n "$PCT" ] || exit 0
118
+ fi
119
+
120
+ # Highest threshold crossed. Compared under LC_ALL=C so '77.2' parses identically everywhere.
121
+ # Compared on the integer part with shell arithmetic rather than two `awk` calls. Two spawns at the end of every
122
+ # turn bought nothing here: the thresholds are whole numbers, so truncating is exactly equivalent — 89.9 is below
123
+ # 90 either way — and dropping the decimal also drops the locale hazard the awk calls existed to pin down.
124
+ INT="${PCT%%.*}"; INT="${INT:-0}"
125
+ case "$INT" in ''|*[!0-9]*) exit 0 ;; esac
126
+ TIER=""
127
+ [ "$INT" -ge 90 ] && TIER=90
128
+ [ -n "$TIER" ] || { [ "$INT" -ge 75 ] && TIER=75; }
129
+ [ -n "$TIER" ] || exit 0
130
+
131
+ # Already warned at this tier this session? Stay silent — one alert per threshold, not one per turn.
132
+ [ -e "$(marker "$TIER")" ] && exit 0
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
+ [ "$t" -le "$TIER" ] && { : > "$(marker "$t")" 2>/dev/null || true; }
137
+ done
138
+
139
+ if [ "$TIER" = 90 ]; then
140
+ 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."
141
+ else
142
+ MSG=">75% threshold. Recommended: apply the handoff skill, write docs/SESSION_STATE.md, then /clear. Manual; your call."
143
+ fi
144
+
145
+ # JSON-escape backslash + double-quote in the measured line before embedding it.
146
+ SAFE="$(printf '%s' "$LINE" | sed 's/\\/\\\\/g; s/"/\\"/g')"
147
+ # exit 0 + systemMessage: a neutral, user-facing warning (NOT a hook error, does NOT force an extra turn).
148
+ printf '{"systemMessage":"%s | %s"}\n' "$SAFE" "$MSG"
149
+ exit 0
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook — after a context boundary (compact / clear / resume), point the model back at the handover
3
+ # so in-progress work survives the boundary. This closes Crewforth's handoff loop: `handoff` writes
4
+ # docs/SESSION_STATE.md → the user runs /clear (or context auto-compacts) → a fresh context starts → THIS hook
5
+ # re-surfaces the file so the next turn resumes from it instead of from zero.
6
+ #
7
+ # Deliberate scope (honest, spec-grounded):
8
+ # - SessionStart is the right event: its stdout / hookSpecificOutput.additionalContext is injected into the
9
+ # model's context (docs: "Re-inject context after compaction"). PreCompact is NOT used — it can only block
10
+ # compaction, not add instructions to the summary, and blocking is not what we want.
11
+ # - Matched on compact|clear|resume, NOT startup: those three are explicit "continue where I left off" boundaries.
12
+ # startup fires on every unrelated session open and would keep surfacing a stale handover — that would nag.
13
+ # - CLAUDE.md itself reloads on /compact and /clear on its own, so this hook does NOT re-inject the discipline —
14
+ # only the session-specific state the reload can't recover.
15
+ # - Fails OPEN and SILENT: no handover file, no output. It never blocks the session (always exits 0).
16
+ set -uo pipefail
17
+ IN="$(cat 2>/dev/null || true)"
18
+
19
+ # Project root: CLAUDE_PROJECT_DIR (set for hooks) → cwd from the stdin JSON → PWD.
20
+ ROOT="${CLAUDE_PROJECT_DIR:-}"
21
+ if [ -z "$ROOT" ]; then
22
+ ROOT="$(printf '%s' "$IN" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
23
+ fi
24
+ [ -n "$ROOT" ] || ROOT="$PWD"
25
+ # Windows hands both CLAUDE_PROJECT_DIR and the stdin `cwd` over as native paths (`C:\Repos\app`), and the stdin
26
+ # one arrives JSON-ENCODED on top of that — `C:\\Repos\\app`. Slicing it out with sed keeps the doubled
27
+ # backslashes, so ROOT pointed at a directory that cannot exist and the hook silently rehydrated nothing.
28
+ # Undo the JSON escaping first (`\\`), then fold any lone separator. Both are no-ops on a POSIX path.
29
+ ROOT="${ROOT//\\\\//}"; ROOT="${ROOT//\\//}"
30
+
31
+ STATE="$ROOT/docs/SESSION_STATE.md"
32
+ [ -s "$STATE" ] || exit 0 # no (non-empty) handover → nothing to rehydrate, stay silent
33
+
34
+ MSG="A session handover from before this context boundary exists at docs/SESSION_STATE.md. If the user's request continues that work, read it first — it holds the in-progress task state, open decisions, and the intended next step — and do not restart from scratch. If the request is about something else, do not bring it up."
35
+
36
+ # hookSpecificOutput.additionalContext is the documented channel that injects text into the model's context.
37
+ # ONE PATH, no jq: MSG is a fixed constant with no quote, backslash, control character or newline, so it needs no
38
+ # escaping and every machine emits the same bytes. If you ever make MSG dynamic, escape it the way board-sync.sh
39
+ # does (jq-identical, measured) — a raw quote or tab here makes JSON the CLI silently drops.
40
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$MSG"
41
+ exit 0