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.
- package/CHANGELOG.md +3784 -0
- package/LICENSE +21 -0
- package/README.md +37 -7
- package/VERSION +1 -0
- package/adopt.sh +1670 -0
- package/bin/cli.js +497 -0
- package/kit/AGENT_TEMPLATE.md +92 -0
- package/kit/CLAUDE.md +228 -0
- package/kit/README.md +107 -0
- package/kit/agents/crew-backend-expert.md +95 -0
- package/kit/agents/crew-commit-agent.md +96 -0
- package/kit/agents/crew-database-expert.md +84 -0
- package/kit/agents/crew-devops-expert.md +82 -0
- package/kit/agents/crew-frontend-expert.md +85 -0
- package/kit/agents/crew-performance-expert.md +78 -0
- package/kit/agents/crew-planner.md +60 -0
- package/kit/agents/crew-privacy-agent.md +71 -0
- package/kit/agents/crew-review-agent.md +95 -0
- package/kit/agents/crew-security-expert.md +73 -0
- package/kit/agents/crew-session-manager.md +77 -0
- package/kit/agents/crew-test-expert.md +64 -0
- package/kit/eval/doctor.sh +465 -0
- package/kit/eval/gate-report.sh +137 -0
- package/kit/eval/golden-holdout.txt +53 -0
- package/kit/eval/golden-routing.txt +199 -0
- package/kit/eval/lib/crew-env.sh +14 -0
- package/kit/eval/lib/settings-json.awk +154 -0
- package/kit/eval/lib/star.sh +70 -0
- package/kit/eval/parser-conformance.sh +612 -0
- package/kit/eval/preflight.sh +186 -0
- package/kit/eval/routing-eval.sh +209 -0
- package/kit/eval/scan-skill.sh +162 -0
- package/kit/eval/smoke-test.sh +6693 -0
- package/kit/eval/update-guard.sh +81 -0
- package/kit/eval/utilization.sh +130 -0
- package/kit/hooks/board-sync.sh +106 -0
- package/kit/hooks/board.sh +962 -0
- package/kit/hooks/commit-msg +49 -0
- package/kit/hooks/context-usage.sh +353 -0
- package/kit/hooks/floor-blocklist.txt +124 -0
- package/kit/hooks/guard-bash.sh +1431 -0
- package/kit/hooks/guard-commit-scan.sh +511 -0
- package/kit/hooks/guard-write.sh +513 -0
- package/kit/hooks/pre-commit +577 -0
- package/kit/hooks/route-hint.sh +185 -0
- package/kit/hooks/secret-blocklist.txt +82 -0
- package/kit/hooks/session-guard.sh +149 -0
- package/kit/hooks/session-rehydrate.sh +41 -0
- package/kit/hooks/session-stats.sh +252 -0
- package/kit/hooks/session-update-check.sh +236 -0
- package/kit/hooks/skill-trust.sh +150 -0
- package/kit/hooks/trace-blocklist.txt +53 -0
- package/kit/settings.json +132 -0
- package/kit/skills/a11y/SKILL.md +44 -0
- package/kit/skills/adr/SKILL.md +74 -0
- package/kit/skills/api-design/SKILL.md +50 -0
- package/kit/skills/automode-policy/SKILL.md +91 -0
- package/kit/skills/automode-policy/references/policy.json +13 -0
- package/kit/skills/automode-policy/scripts/apply.sh +84 -0
- package/kit/skills/automode-policy/scripts/check.sh +102 -0
- package/kit/skills/backend-architecture/SKILL.md +103 -0
- package/kit/skills/brainstorm/SKILL.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +38 -0
- package/kit/skills/commit-message/SKILL.md +82 -0
- package/kit/skills/commit-message/references/examples.md +8 -0
- package/kit/skills/confidence-check/SKILL.md +86 -0
- package/kit/skills/crew-board/SKILL.md +55 -0
- package/kit/skills/crew-brainstorm/SKILL.md +19 -0
- package/kit/skills/crew-code-review/SKILL.md +151 -0
- package/kit/skills/crew-code-review/references/history-search.md +19 -0
- package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
- package/kit/skills/crew-code-review/references/receiving.md +23 -0
- package/kit/skills/crew-doctor/SKILL.md +36 -0
- package/kit/skills/crew-gates/SKILL.md +25 -0
- package/kit/skills/crew-handoff/SKILL.md +14 -0
- package/kit/skills/crew-plan/SKILL.md +18 -0
- package/kit/skills/crew-review/SKILL.md +23 -0
- package/kit/skills/crew-ship/SKILL.md +19 -0
- package/kit/skills/crew-skill/SKILL.md +61 -0
- package/kit/skills/crew-studio/SKILL.md +129 -0
- package/kit/skills/crew-update/SKILL.md +43 -0
- package/kit/skills/db-migration/SKILL.md +120 -0
- package/kit/skills/db-migration/references/backup-restore.md +33 -0
- package/kit/skills/db-migration/references/raw-sql.md +30 -0
- package/kit/skills/db-migration/references/tool-matrix.md +35 -0
- package/kit/skills/dependency-audit/SKILL.md +61 -0
- package/kit/skills/dependency-upgrade/SKILL.md +82 -0
- package/kit/skills/deploy/SKILL.md +100 -0
- package/kit/skills/deploy/references/proxy-ssl.md +56 -0
- package/kit/skills/deploy/references/rollback.md +18 -0
- package/kit/skills/deploy/references/scripts.md +9 -0
- package/kit/skills/deploy/references/topology-a.md +128 -0
- package/kit/skills/deploy/references/topology-b.md +44 -0
- package/kit/skills/docs-writer/SKILL.md +53 -0
- package/kit/skills/docs-writer/references/coauthoring.md +34 -0
- package/kit/skills/eval-grader/SKILL.md +75 -0
- package/kit/skills/eval-grader/references/method.md +51 -0
- package/kit/skills/frontend/SKILL.md +56 -0
- package/kit/skills/frontend/references/verify-contract.md +41 -0
- package/kit/skills/frontend-design/SKILL.md +71 -0
- package/kit/skills/frontend-design/references/design-review.md +34 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
- package/kit/skills/handoff/SKILL.md +71 -0
- package/kit/skills/handoff/references/cold-reader.md +51 -0
- package/kit/skills/i18n-integrity/SKILL.md +31 -0
- package/kit/skills/incident-runbook/SKILL.md +40 -0
- package/kit/skills/incident-runbook/references/postmortem.md +16 -0
- package/kit/skills/iterate/SKILL.md +40 -0
- package/kit/skills/mcp-builder/SKILL.md +54 -0
- package/kit/skills/mcp-builder/references/building.md +71 -0
- package/kit/skills/observability/SKILL.md +50 -0
- package/kit/skills/performance/SKILL.md +38 -0
- package/kit/skills/performance/references/bottlenecks.md +10 -0
- package/kit/skills/privacy-compliance/SKILL.md +75 -0
- package/kit/skills/red-team/SKILL.md +48 -0
- package/kit/skills/reflect/SKILL.md +57 -0
- package/kit/skills/release/SKILL.md +44 -0
- package/kit/skills/security-scan/SKILL.md +99 -0
- package/kit/skills/security-scan/references/ai-agents.md +107 -0
- package/kit/skills/security-scan/references/fronts.md +99 -0
- package/kit/skills/security-scan/references/prompting.md +23 -0
- package/kit/skills/security-scan/references/reporting.md +75 -0
- package/kit/skills/security-scan/references/verify.md +131 -0
- package/kit/skills/sonarqube-check/SKILL.md +108 -0
- package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
- package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
- package/kit/skills/spec-planning/SKILL.md +92 -0
- package/kit/skills/systematic-debugging/SKILL.md +57 -0
- package/kit/skills/systematic-debugging/references/techniques.md +44 -0
- package/kit/skills/teamboard/SKILL.md +88 -0
- package/kit/skills/teamboard/references/setup.md +37 -0
- package/kit/skills/testing/SKILL.md +49 -0
- package/kit/skills/testing/references/flaky-triage.md +51 -0
- package/kit/skills/threat-model/SKILL.md +57 -0
- package/kit/skills/threat-model/references/interview.md +51 -0
- package/kit/skills/threat-model/references/schema.md +42 -0
- package/kit/skills/token-budget/SKILL.md +51 -0
- package/kit/skills/trace-scan/SKILL.md +43 -0
- package/kit/skills/worktree/SKILL.md +40 -0
- package/kit/studio/README.md +266 -0
- package/kit/studio/ensure-node.sh +471 -0
- package/kit/studio/package.json +14 -0
- package/kit/studio/server/hooks/studio-gate.sh +169 -0
- package/kit/studio/server/index.js +902 -0
- package/kit/studio/server/lib/crew-env.js +11 -0
- package/kit/studio/server/lib/fleet.js +146 -0
- package/kit/studio/server/lib/graph.js +533 -0
- package/kit/studio/server/lib/instance.js +135 -0
- package/kit/studio/server/lib/kit-telemetry.js +182 -0
- package/kit/studio/server/lib/kit.js +158 -0
- package/kit/studio/server/lib/palette.js +118 -0
- package/kit/studio/server/lib/peers.js +72 -0
- package/kit/studio/server/lib/permissions.js +150 -0
- package/kit/studio/server/lib/projects.js +294 -0
- package/kit/studio/server/lib/roster.js +191 -0
- package/kit/studio/server/lib/session.js +309 -0
- package/kit/studio/server/lib/terminal.js +91 -0
- package/kit/studio/server/lib/transcript.js +100 -0
- package/kit/studio/web/app.js +1098 -0
- package/kit/studio/web/canvas.js +1179 -0
- package/kit/studio/web/chat.js +543 -0
- package/kit/studio/web/index.html +86 -0
- package/kit/studio/web/md.js +110 -0
- package/kit/studio/web/storage-migrate.js +23 -0
- package/kit/studio/web/style.css +1453 -0
- package/package.json +46 -8
- package/start.sh +748 -0
- 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
|