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,81 @@
1
+ #!/usr/bin/env bash
2
+ # The two measured halves around an update, for /crew-update. Run from the project root.
3
+ #
4
+ # update-guard.sh pre before the update: names uncommitted changes in .claude/ and CLAUDE.md (it WARNS, it
5
+ # never stops anything — whether to go ahead is the user's call, asked by /crew-update),
6
+ # then records what is on disk now.
7
+ # update-guard.sh post after the update: lists what the update added, changed, moved and removed, and points
8
+ # at the release notes the updater extracted from the package's own CHANGELOG.
9
+ #
10
+ # It writes exactly one file, .claude/.state/update-snapshot, and changes nothing else. The snapshot is one
11
+ # `cksum` over every file (one process for the lot, not one per file — that is 200 forks on Git Bash), so a
12
+ # move is a removed path and an added path with the same checksum. The state directory and the gate log are
13
+ # left out: they change on every run and say nothing about the update.
14
+ set -u
15
+ MODE="${1:-}"
16
+ [ -d .claude ] || { echo "update-guard: no .claude/ here — run it from the project root" >&2; exit 2; }
17
+ SNAP=".claude/.state/update-snapshot"
18
+
19
+ snapshot(){ # -> "<crc> <size> <path>" per file, sorted by path
20
+ { find .claude -type f ! -path '.claude/.state/*' ! -name gate-log.tsv -print0 2>/dev/null
21
+ [ -f CLAUDE.md ] && printf 'CLAUDE.md\0'; } | xargs -0 cksum 2>/dev/null | LC_ALL=C sort -k3
22
+ }
23
+
24
+ case "$MODE" in
25
+ pre)
26
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
27
+ # What git does not track, it cannot call uncommitted: a private install (the default) gitignores .claude/
28
+ # and CLAUDE.md, and then "clean" would be a claim nobody measured. Say which paths are outside git instead.
29
+ UNTR=""; for _p in .claude CLAUDE.md; do [ -e "$_p" ] && git check-ignore -q "$_p" 2>/dev/null && UNTR="$UNTR $_p"; done
30
+ # Crewforth's own runtime state is left out: it changes on its own, and a repo that committed it before it
31
+ # was ignored would otherwise warn on every update about a cache nobody edited.
32
+ DIRTY="$(git status --porcelain --untracked-files=all -- .claude CLAUDE.md ':(exclude).claude/.state' 2>/dev/null)"
33
+ if [ -n "$DIRTY" ]; then
34
+ echo "UNCOMMITTED: .claude/ or CLAUDE.md has changes that are not committed:"
35
+ printf '%s\n' "$DIRTY" | sed 's/^/ /' | head -n 20
36
+ [ "$(printf '%s\n' "$DIRTY" | wc -l | tr -d ' ')" -gt 20 ] && echo " …"
37
+ echo "The update rewrites Crewforth's files under .claude/. Commit or stash first, or go ahead knowingly."
38
+ elif [ -n "$UNTR" ]; then
39
+ echo "NOT IN GIT:$UNTR — gitignored here, so uncommitted changes cannot be detected. The update replaces Crewforth's"
40
+ echo "files under .claude/ (CLAUDE.md and project skills are kept); edits made to Crewforth files there are overwritten."
41
+ else
42
+ echo "clean: no uncommitted changes in .claude/ or CLAUDE.md"
43
+ fi
44
+ else
45
+ echo "not a git repository: nothing to compare against — the file list after the update is the record"
46
+ fi
47
+ mkdir -p .claude/.state 2>/dev/null
48
+ snapshot > "$SNAP" || { echo "update-guard: could not write $SNAP" >&2; exit 1; }
49
+ ;;
50
+ post)
51
+ [ -f "$SNAP" ] || { echo "update-guard: no snapshot — run \`update-guard.sh pre\` before the update" >&2; exit 2; }
52
+ # One tagged line per path (A added · C changed · M moved · R removed), sorted, then printed as four lists.
53
+ OUT="$(snapshot | awk '
54
+ FNR==NR { p=$0; sub(/^[^ ]+ [^ ]+ /,"",p); old[p]=$1" "$2; next }
55
+ { p=$0; sub(/^[^ ]+ [^ ]+ /,"",p); new[p]=$1" "$2 }
56
+ END {
57
+ for (p in new) if (!(p in old)) added[p]=new[p]; else if (old[p]!=new[p]) print "C\t" p
58
+ for (p in old) if (!(p in new)) removed[p]=old[p]
59
+ # a move: gone from one path, the same bytes at another — only when those bytes are unambiguous on both
60
+ # sides (one removed and one added file carry them) and not empty; otherwise it stays removed + added.
61
+ for (r in removed) nr[removed[r]]++
62
+ for (a in added) na[added[a]]++
63
+ for (r in removed) if (nr[removed[r]]==1 && removed[r] !~ / 0$/) for (a in added) if (added[a]==removed[r] && na[added[a]]==1) { moved[r]=a; break }
64
+ for (r in moved) { print "M\t" r " -> " moved[r]; delete removed[r]; delete added[moved[r]] }
65
+ for (p in added) print "A\t" p
66
+ for (p in removed) print "R\t" p
67
+ }' "$SNAP" - | LC_ALL=C sort)"
68
+ for _t in "A added" "C changed" "M moved" "R removed"; do
69
+ _k="${_t%% *}"; _l="$(printf '%s\n' "$OUT" | sed -n "s/^$_k //p")"
70
+ printf '%s (%s):\n' "${_t#* }" "$(printf '%s' "$_l" | grep -c . || true)"
71
+ [ -n "$_l" ] && printf '%s\n' "$_l" | sed 's/^/ /'
72
+ done
73
+ if [ -s .claude/.state/whats-new.md ]; then
74
+ echo "what changed: .claude/.state/whats-new.md (the release notes from the installed package's own CHANGELOG)"
75
+ else
76
+ echo "what changed: no release notes were extracted (the package carried no CHANGELOG, or nothing is newer)"
77
+ fi
78
+ ;;
79
+ *) echo "usage: update-guard.sh pre|post" >&2; exit 2 ;;
80
+ esac
81
+ exit 0
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bash
2
+ # What Crewforth LOADS versus what it actually FIRES.
3
+ #
4
+ # Every installed skill spends its name and description in EVERY session's skill listing, forever, whether or not
5
+ # it is ever used. doctor.sh §4a already measures the loaded half and reports it against the budget. This
6
+ # measures the other half, because the documented remedy for an over-budget listing — setting rarely-used skills
7
+ # to "name-only" in skillOverrides — needs a list of WHICH skills, and nothing produced one. A cold skill is not
8
+ # automatically a bad skill (an incident runbook earns its keep by existing), so this REPORTS and never enforces.
9
+ #
10
+ # Usage: bash utilization.sh [--all-projects]
11
+ # Exit: 0 always. Measuring context cost must never fail a build.
12
+ #
13
+ # PRIVACY (§4.3): the current project only, unless --all-projects is asked for explicitly. What is printed is
14
+ # skill NAMES and COUNTS — never a path, never the encoded project directory, never a prompt or a file content.
15
+ # Nothing is written anywhere; this script only reads.
16
+ set -uo pipefail
17
+
18
+ ALL=0
19
+ case "${1:-}" in
20
+ --all-projects) ALL=1 ;;
21
+ ""|--help|-h) [ "${1:-}" != "" ] && { sed -n '2,12p' "$0"; exit 0; } ;;
22
+ esac
23
+
24
+ # Where the skills live: an installed project, or this repo's payload when run from Crewforth's own checkout.
25
+ SKILLS=""; OWNREPO=0
26
+ for d in .claude/skills kit/skills; do [ -d "$d" ] && { SKILLS="$d"; break; }; done
27
+ [ "$SKILLS" = "kit/skills" ] && OWNREPO=1
28
+ if [ -z "$SKILLS" ]; then echo "utilization: no skills directory here (looked for .claude/skills)"; exit 0; fi
29
+
30
+ TR=""
31
+ # ---- CREW-TRANSCRIPT-DIR (kept byte-identical in context-usage.sh and session-stats.sh; smoke-test §6i3 pins it)
32
+ # Claude Code stores a session under $HOME/.claude/projects/<cwd encoded as a directory name>. Reproducing that
33
+ # encoding is the ONLY way a by-hand call (no hook payload on stdin) can find its own transcript.
34
+ crew_project_dirs(){ # candidate directory names for the current cwd, best first, one per line
35
+ local w p
36
+ # Windows first. Claude Code sees the NATIVE cwd (C:\repo\app) while Git Bash's `pwd` reports /c/repo/app, so
37
+ # the drive letter never matched and the by-hand call could not resolve a transcript on Windows AT ALL — the
38
+ # same "transcript not found" was reported from that machine three separate times before the cause was found.
39
+ # `pwd -W` is the MSYS builtin that returns the native form; elsewhere it just fails and is discarded.
40
+ w="$(pwd -W 2>/dev/null || true)"
41
+ # The encoder folds : \ / . AND _ to '-'. The underscore is the one that hides: a project named `report_api`
42
+ # lands in `...-report-api`, so folding only slashes and dots misses every path containing an underscore — on
43
+ # every platform, not just Windows.
44
+ for p in "$w" "$(pwd)"; do
45
+ [ -n "$p" ] && printf '%s\n' "$(printf '%s' "$p" | sed 's#[:\\/._]#-#g')"
46
+ done
47
+ # Legacy shapes, kept so a directory written by an older client still resolves.
48
+ printf '%s\n' "$(pwd | sed 's#[/.]#-#g')" "$(pwd | sed 's#/#-#g')"
49
+ }
50
+ if [ -z "$TR" ]; then
51
+ while IFS= read -r esc; do
52
+ [ -n "$esc" ] || continue
53
+ cand="$(ls -t "$HOME/.claude/projects/$esc"/*.jsonl 2>/dev/null | head -1)"
54
+ [ -n "$cand" ] && { TR="$cand"; break; }
55
+ done <<CREWEOF
56
+ $(crew_project_dirs)
57
+ CREWEOF
58
+ fi
59
+ # ---- /CREW-TRANSCRIPT-DIR
60
+
61
+ # THE TRANSCRIPTS ARE NOT ONE FILE, AND THAT IS THE WHOLE MEASUREMENT. A project directory holds
62
+ # <session>.jsonl at the top AND a <session>/ directory per session whose subagents/ tree carries one transcript
63
+ # per delegated agent. Measured on this machine while writing this: 14 transcripts at the top level, 306 nested.
64
+ # A parser that reads only the top level therefore misses most of the work — and delegated agents are precisely
65
+ # where skills fire, because that is what they are delegated with. Counting only the top level reported 2 of 40
66
+ # skills as used; counting the tree reported 21. The difference is not a rounding error, it is the answer.
67
+ if [ "$ALL" = 1 ]; then
68
+ DIRS="$HOME/.claude/projects"
69
+ [ -d "$DIRS" ] || { echo "utilization: NOT MEASURED — no transcripts directory"; exit 0; }
70
+ SCOPE="all projects"
71
+ else
72
+ [ -n "$TR" ] || { echo "utilization: NOT MEASURED — no transcript found for this project."
73
+ echo " (a fresh checkout, a first session, or a project directory the encoder cannot reproduce)"
74
+ echo " NOT MEASURED is not the same as 'nothing fired'; it means nothing was read."; exit 0; }
75
+ DIRS="$(dirname "$TR")"
76
+ SCOPE="this project"
77
+ fi
78
+
79
+ # One grep over the tree, not one per skill: 40 greps × 320 files is the cost pattern Crewforth keeps removing.
80
+ #
81
+ # TWO SHAPES COUNT AS A FIRING, and only these two:
82
+ # "file_path":"…/skills/<name>/SKILL.md" — the skill was READ (how a skill is actually consumed)
83
+ # "name":"Skill","input":{"skill":"<name>" — the Skill tool was invoked by name
84
+ # THE ANCHOR IS THE POINT. Matching a bare skill name anywhere in the JSON would count Crewforth measuring itself:
85
+ # a single `grep -rn description: skills/` tool result echoes all 40 paths on one line, and every skill would
86
+ # report as used forever. Both patterns above require the name to sit in a position only a real invocation puts
87
+ # it in. Lines carrying "is_error":true are dropped first — a read that failed is not a use. That drop is
88
+ # line-level, so it catches the error record itself; correlating a failed result back to its own tool_use id
89
+ # would need a second pass over every line, and a Read of a file that is installed rarely fails.
90
+ HITS="$(grep -rhI --include='*.jsonl' -e '"file_path":"' -e '"name":"Skill"' "$DIRS" 2>/dev/null \
91
+ | grep -v '"is_error":true' \
92
+ | grep -ohE '/skills/[A-Za-z0-9._-]+/SKILL\.md|"name":"Skill","input":\{"skill":"[A-Za-z0-9._-]+"' 2>/dev/null \
93
+ | sed -e 's#^/skills/##' -e 's#/SKILL\.md$##' -e 's#.*"skill":"##' -e 's#"$##' \
94
+ | sort | uniq -c | sort -rn)"
95
+
96
+ FIRED=0; COLD=0; COLDLIST=""; COLDBYTES=0; FIREDLIST=""
97
+ for f in "$SKILLS"/*/SKILL.md; do
98
+ [ -e "$f" ] || continue
99
+ n="$(basename "$(dirname "$f")")"
100
+ c="$(printf '%s\n' "$HITS" | awk -v s="$n" '$2==s {print $1; exit}')"
101
+ if [ -n "$c" ]; then
102
+ FIRED=$((FIRED+1)); FIREDLIST="$FIREDLIST $n($c)"
103
+ else
104
+ COLD=$((COLD+1)); COLDLIST="$COLDLIST $n"
105
+ b="$(awk '/^---$/{k++; next} k==1' "$f" | awk '/^(name|description):/,0' | wc -c | tr -d ' ')"
106
+ COLDBYTES=$((COLDBYTES + b))
107
+ fi
108
+ done
109
+ TOTAL=$((FIRED+COLD))
110
+ [ "$TOTAL" -gt 0 ] || { echo "utilization: NOT MEASURED — no skills installed"; exit 0; }
111
+
112
+ NSESS="$(find "$DIRS" -name '*.jsonl' 2>/dev/null | wc -l | tr -d ' ')"
113
+ echo "== skill utilization ($SCOPE, $NSESS transcript file(s)) =="
114
+ echo " fired : $FIRED/$TOTAL"
115
+ echo " cold : $COLD/$TOTAL (${COLDBYTES} B of always-on listing spent on skills nothing reached)"
116
+ [ -n "$FIREDLIST" ] && { echo " --- fired (times reached) ---"; printf '%s\n' "$FIREDLIST" | tr ' ' '\n' | grep -v '^$' | sort | paste -sd' ' - | fold -s -w 100 | sed 's/^/ /'; }
117
+ [ -n "$COLDLIST" ] && { echo " --- cold ---"; printf '%s\n' "$COLDLIST" | tr ' ' '\n' | grep -v '^$' | sort | paste -sd' ' - | fold -s -w 100 | sed 's/^/ /'; }
118
+ echo " ---"
119
+ # Crewforth's own repository is the one place where this number lies, so it says so rather than being quoted out
120
+ # of context later. Here a SKILL.md is opened to EDIT it, and an edit is indistinguishable from an invocation at
121
+ # the transcript level — both are a Read of the same path. In a project that USES Crewforth there is no such
122
+ # traffic, which is the case the measurement is for.
123
+ [ "$OWNREPO" = 1 ] && {
124
+ echo " NOTE: read from Crewforth's OWN repository, where a SKILL.md is opened to be EDITED. Those edits are"
125
+ echo " indistinguishable from invocations here, so 'fired' is inflated. Run this in a project that USES the"
126
+ echo " Crewforth for a number that means what it says."
127
+ echo " ---"; }
128
+ echo " A cold skill is not a bad skill — a runbook earns its place by existing. This is the evidence for the"
129
+ echo " skillOverrides \"name-only\" decision doctor.sh §4a asks you to make, not the decision itself."
130
+ exit 0
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook — put the TEAM's state into a session that would otherwise only know its own.
3
+ #
4
+ # The problem it closes: every teammate runs Crewforth locally, so "Ali took item #1 two hours ago" exists
5
+ # nowhere this session can see. This hook injects the board summary at the start of every session, which is the
6
+ # one moment the answer changes what happens next: which item to pick up, which one is blocked, which claim has
7
+ # gone quiet.
8
+ #
9
+ # Deliberate scope:
10
+ # - Same event and channel as session-rehydrate.sh: SessionStart + hookSpecificOutput.additionalContext is the
11
+ # documented way to put text into the model's context.
12
+ # - Matched on startup|resume|clear|compact. Unlike the handover hook this DOES include `startup`: a stale
13
+ # handover nags, but "who holds what right now" is exactly what a fresh session is missing.
14
+ # - NO NETWORK IN THE FOREGROUND. The visible half reads one cache file and exits. When that cache is older
15
+ # than CREW_BOARD_MAX_AGE the hook starts a DETACHED refresher (fetch + heartbeat) whose result is used by the
16
+ # NEXT session. On an unreachable remote the session-start cost stays at zero.
17
+ # - Fails OPEN and SILENT: no repo, no board, no cache -> no output, exit 0. It never blocks a session.
18
+ set -uo pipefail
19
+ # The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
20
+ _crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
21
+ [ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"; unset _crew_d
22
+
23
+ # Two intervals, because two very different repos run this hook. Where a board exists, 15 minutes keeps the view
24
+ # worth acting on. Where none does — every solo project, and every install that upgraded into this feature — the
25
+ # only thing a refresh can do is ask the remote for a ref nobody has created, so it backs off to once a day.
26
+ # Without that split, a repo that will never have a board opened a background fetch at every session start.
27
+ MAX_AGE="${CREW_BOARD_MAX_AGE:-900}"
28
+ MAX_AGE_NOBOARD="${CREW_BOARD_MAX_AGE_NOBOARD:-86400}"
29
+
30
+ HERE="$(cd "$(dirname "$0")" && pwd)"
31
+ SELF="$HERE/$(basename "$0")"
32
+
33
+ # ---- detached half: the only place this hook is allowed to touch the network -----------------------------------
34
+ if [ "${1:-}" = "--refresh" ]; then
35
+ cd "${2:-.}" 2>/dev/null || exit 0
36
+ bash "$HERE/board.sh" sync >/dev/null 2>&1 || exit 0
37
+ bash "$HERE/board.sh" beat >/dev/null 2>&1 || true
38
+ exit 0
39
+ fi
40
+
41
+ # ---- foreground half: one file read, then a decision -----------------------------------------------------------
42
+ [ -n "${CREW_NO_BOARD:-}" ] && exit 0
43
+
44
+ IN=""
45
+ [ ! -t 0 ] && IN="$(cat 2>/dev/null || true)"
46
+ ROOT="${CLAUDE_PROJECT_DIR:-}"
47
+ [ -n "$ROOT" ] || ROOT="$(printf '%s' "$IN" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
48
+ [ -n "$ROOT" ] || ROOT="$PWD"
49
+ # Windows hands this over as a native path and the stdin copy arrives JSON-encoded on top (`C:\\Repos\\app`).
50
+ # Undo the escaping, then fold the separators; both are no-ops on POSIX.
51
+ ROOT="${ROOT//\\\\//}"; ROOT="${ROOT//\\//}"
52
+
53
+ cd "$ROOT" 2>/dev/null || exit 0
54
+ GITDIR="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
55
+ [ -n "$GITDIR" ] || exit 0
56
+
57
+ CACHE="$GITDIR/crew-board-cache"
58
+ STAMP=0
59
+ [ -f "$CACHE.at" ] && STAMP="$(tr -cd '0-9' < "$CACHE.at" 2>/dev/null)"
60
+ [ -n "$STAMP" ] || STAMP=0
61
+
62
+ # A non-empty cache means a board was found here; an absent or empty one means there is nothing to keep fresh.
63
+ # Costs no extra process: the same test decides, below, whether there is anything to say.
64
+ [ -s "$CACHE" ] || MAX_AGE="$MAX_AGE_NOBOARD"
65
+
66
+ NOW="$(date -u +%s 2>/dev/null || echo 0)"
67
+ if [ "$NOW" -gt 0 ] && [ "$((NOW - STAMP))" -gt "$MAX_AGE" ]; then
68
+ # stdin/stdout/stderr all detached: an inherited stdout keeps the hook's pipe open after it exits, and the
69
+ # caller then waits to EOF on a fetch that has nothing to do with it.
70
+ bash "$SELF" --refresh "$ROOT" </dev/null >/dev/null 2>&1 &
71
+ fi
72
+
73
+ # Never invent a board state. No cache (no board here, or every refresh so far failed) -> stay silent.
74
+ [ -s "$CACHE" ] || exit 0
75
+
76
+ # /crew-board is user-only, so the command Claude can run itself comes first (the plugin's copy names its path).
77
+ if [ -f "$HERE/../.claude-plugin/plugin.json" ]; then _BS="bash \"$HERE/board.sh\" sync"; else _BS='bash .claude/hooks/board.sh sync'; fi
78
+ MSG="$(cat "$CACHE")
79
+ Board state above is a cached snapshot; $_BS (or the user can type /crew-board sync) refreshes it."
80
+
81
+ # ONE PATH, no jq. The JSON is built here on every machine, so a Mac with jq and a Windows box without it emit the
82
+ # same bytes. The escaper is jq-identical, measured on 15 inputs (quote, backslash, tab, CR, C0 controls, DEL,
83
+ # UTF-8, leading/trailing newlines, 5000 chars): 15/15 equal to `jq -cn --arg m`. The one it replaced escaped only
84
+ # the quote, the backslash and the newline, so a tab in an item title produced JSON the CLI cannot parse
85
+ # (measured: jq rc=5) and the board silently vanished from the session — on exactly the no-jq machine this
86
+ # branch existed for. The trailing newline on the printf closes the last record, so a value ending in a newline
87
+ # keeps it. One awk, no subshell per character. A CR that ENDS a line is dropped on purpose: it is a CRLF line
88
+ # ending, not content, and MSYS gawk already drops it on read (text mode) while BSD awk keeps it — measured on
89
+ # Windows. Stripping it here makes every OS emit the same bytes; a CR inside a line is still escaped as \r.
90
+ ESC="$(printf '%s\n' "$MSG" | LC_ALL=C awk 'BEGIN { ORS = ""
91
+ for (i = 1; i < 32; i++) ctl[i] = sprintf("%c", i)
92
+ nm[8] = "\\b"; nm[9] = "\\t"; nm[12] = "\\f"; nm[13] = "\\r" }
93
+ NR > 1 { print "\\n" }
94
+ { s = $0; sub(/\r$/, "", s)
95
+ gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s)
96
+ for (i = 1; i < 32; i++) {
97
+ if (i == 10 || !index(s, ctl[i])) continue
98
+ r = (i in nm) ? nm[i] : sprintf("\\u%04x", i)
99
+ out = ""; while ((p = index(s, ctl[i])) > 0) { out = out substr(s, 1, p - 1) r; s = substr(s, p + 1) }
100
+ s = out s
101
+ }
102
+ d = sprintf("%c", 127)
103
+ if (index(s, d)) { out = ""; while ((p = index(s, d)) > 0) { out = out substr(s, 1, p - 1) "\\u007f"; s = substr(s, p + 1) }; s = out s }
104
+ print s }')"
105
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$ESC"
106
+ exit 0