crewforth 3.4.0 → 3.5.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 (153) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/README.md +3 -2
  3. package/VERSION +1 -1
  4. package/adopt.sh +40 -5
  5. package/bin/cli.js +138 -2
  6. package/kit/AGENT_TEMPLATE.md +15 -0
  7. package/kit/CLAUDE.md +49 -17
  8. package/kit/agents/crew-backend-expert.md +4 -3
  9. package/kit/agents/crew-commit-agent.md +2 -0
  10. package/kit/agents/crew-database-expert.md +2 -1
  11. package/kit/agents/crew-devops-expert.md +1 -0
  12. package/kit/agents/crew-docs-agent.md +3 -1
  13. package/kit/agents/crew-frontend-expert.md +2 -0
  14. package/kit/agents/crew-performance-expert.md +7 -2
  15. package/kit/agents/crew-planner.md +15 -3
  16. package/kit/agents/crew-privacy-agent.md +2 -0
  17. package/kit/agents/crew-review-agent.md +8 -1
  18. package/kit/agents/crew-security-expert.md +4 -3
  19. package/kit/agents/crew-test-expert.md +6 -2
  20. package/kit/eval/doctor.sh +201 -1
  21. package/kit/eval/gate-report.sh +28 -0
  22. package/kit/eval/golden-holdout.txt +2 -0
  23. package/kit/eval/golden-routing.txt +17 -0
  24. package/kit/eval/lib/crew-map/contracts.mjs +77 -0
  25. package/kit/eval/lib/crew-map/crew-map.sh +2 -1
  26. package/kit/eval/lib/crew-map/detect.mjs +2 -1
  27. package/kit/eval/lib/crew-map/narrative.mjs +14 -3
  28. package/kit/eval/lib/crew-map/project.mjs +45 -6
  29. package/kit/eval/lib/crew-map/scan.mjs +153 -14
  30. package/kit/eval/lib/findings/findings.schema.json +11 -1
  31. package/kit/eval/lib/findings/findings.sh +24 -1
  32. package/kit/eval/lib/hook-inject.sh +159 -0
  33. package/kit/eval/lib/lockfiles.sh +71 -0
  34. package/kit/eval/lib/payload-check.sh +29 -0
  35. package/kit/eval/lib/preview.sh +3 -3
  36. package/kit/eval/lib/review-bench/bench.mjs +14 -2
  37. package/kit/eval/lib/review-prepass/prepass.mjs +32 -3
  38. package/kit/eval/lib/settings-json.awk +18 -0
  39. package/kit/eval/lib/untrusted-text.awk +38 -0
  40. package/kit/eval/scan-skill.sh +67 -20
  41. package/kit/eval/smoke-test.sh +1700 -40
  42. package/kit/eval/utilization.sh +77 -3
  43. package/kit/hooks/agent-outcome.sh +9 -1
  44. package/kit/hooks/board-sync.sh +49 -3
  45. package/kit/hooks/board.sh +8 -8
  46. package/kit/hooks/branch-fresh.sh +101 -0
  47. package/kit/hooks/floor-blocklist.txt +19 -1
  48. package/kit/hooks/guard-bash.sh +92 -8
  49. package/kit/hooks/guard-question.sh +2 -1
  50. package/kit/hooks/loop-guard.sh +445 -0
  51. package/kit/hooks/map-stale.sh +437 -0
  52. package/kit/hooks/pre-commit +48 -7
  53. package/kit/hooks/precompact-snapshot.sh +102 -6
  54. package/kit/hooks/prompt-approval.sh +45 -9
  55. package/kit/hooks/route-hint.sh +36 -7
  56. package/kit/hooks/secret-blocklist.txt +55 -1
  57. package/kit/hooks/session-guard.sh +45 -15
  58. package/kit/hooks/session-rehydrate.sh +34 -11
  59. package/kit/hooks/session-stats.sh +169 -8
  60. package/kit/hooks/session-update-check.sh +5 -1
  61. package/kit/hooks/skill-trust.sh +23 -14
  62. package/kit/hooks/turn-check.sh +190 -0
  63. package/kit/owned-blobs.tsv +4 -4
  64. package/kit/settings.json +64 -0
  65. package/kit/skills/a11y/SKILL.md +50 -3
  66. package/kit/skills/a11y/references/recipes.md +41 -0
  67. package/kit/skills/adr/SKILL.md +14 -2
  68. package/kit/skills/api-design/SKILL.md +6 -1
  69. package/kit/skills/api-design/references/idempotency.md +39 -0
  70. package/kit/skills/brainstorm/SKILL.md +4 -2
  71. package/kit/skills/brainstorm/references/asking.md +47 -0
  72. package/kit/skills/ci-pipeline/SKILL.md +11 -1
  73. package/kit/skills/ci-pipeline/references/agent-job.md +33 -0
  74. package/kit/skills/commit-message/SKILL.md +9 -3
  75. package/kit/skills/confidence-check/SKILL.md +9 -7
  76. package/kit/skills/confidence-check/references/source-check.md +37 -0
  77. package/kit/skills/crew-approve/SKILL.md +3 -0
  78. package/kit/skills/crew-audit/SKILL.md +63 -0
  79. package/kit/skills/crew-board/SKILL.md +2 -2
  80. package/kit/skills/crew-code-review/SKILL.md +27 -4
  81. package/kit/skills/crew-code-review/references/panel-mode.md +9 -9
  82. package/kit/skills/crew-code-review/references/simplify.md +28 -0
  83. package/kit/skills/crew-doctor/SKILL.md +3 -2
  84. package/kit/skills/crew-gates/SKILL.md +3 -0
  85. package/kit/skills/crew-gates/references/enforcers.md +30 -0
  86. package/kit/skills/crew-interview/SKILL.md +42 -0
  87. package/kit/skills/crew-map/SKILL.md +26 -6
  88. package/kit/skills/crew-plan/SKILL.md +9 -2
  89. package/kit/skills/crew-pr/SKILL.md +57 -0
  90. package/kit/skills/crew-pr/references/checks.md +35 -0
  91. package/kit/skills/crew-pr/references/threads.md +23 -0
  92. package/kit/skills/crew-review/SKILL.md +34 -1
  93. package/kit/skills/crew-ship/SKILL.md +5 -2
  94. package/kit/skills/crew-skill/SKILL.md +26 -1
  95. package/kit/skills/crew-update/SKILL.md +4 -1
  96. package/kit/skills/db-migration/SKILL.md +15 -0
  97. package/kit/skills/db-migration/references/expand-contract.md +52 -0
  98. package/kit/skills/dependency-audit/SKILL.md +34 -1
  99. package/kit/skills/dependency-audit/scripts/record.sh +37 -0
  100. package/kit/skills/dependency-upgrade/SKILL.md +29 -1
  101. package/kit/skills/deploy/SKILL.md +18 -0
  102. package/kit/skills/deploy/references/topology-a.md +7 -0
  103. package/kit/skills/deploy/references/topology-b.md +2 -2
  104. package/kit/skills/docs-writer/SKILL.md +37 -2
  105. package/kit/skills/eval-grader/SKILL.md +9 -0
  106. package/kit/skills/eval-grader/references/method.md +26 -0
  107. package/kit/skills/frontend/SKILL.md +5 -2
  108. package/kit/skills/frontend/references/from-design.md +29 -0
  109. package/kit/skills/frontend/references/page-metadata.md +17 -0
  110. package/kit/skills/frontend/references/verify-contract.md +18 -1
  111. package/kit/skills/frontend/references/worst-data.md +35 -0
  112. package/kit/skills/frontend-design/SKILL.md +20 -4
  113. package/kit/skills/frontend-design/references/design-review.md +21 -0
  114. package/kit/skills/frontend-design/references/detection.md +44 -0
  115. package/kit/skills/frontend-design/references/motion.md +54 -0
  116. package/kit/skills/frontend-design/references/ui-copy.md +35 -0
  117. package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
  118. package/kit/skills/handoff/SKILL.md +28 -3
  119. package/kit/skills/handoff/references/cold-reader.md +13 -6
  120. package/kit/skills/i18n-integrity/SKILL.md +3 -0
  121. package/kit/skills/iterate/SKILL.md +7 -2
  122. package/kit/skills/mcp-builder/SKILL.md +21 -4
  123. package/kit/skills/mcp-builder/references/building.md +74 -13
  124. package/kit/skills/mcp-builder/references/cli-for-agents.md +26 -0
  125. package/kit/skills/observability/SKILL.md +13 -1
  126. package/kit/skills/performance/SKILL.md +25 -1
  127. package/kit/skills/performance/references/bottlenecks.md +11 -0
  128. package/kit/skills/performance/references/web-vitals.md +31 -0
  129. package/kit/skills/red-team/SKILL.md +25 -3
  130. package/kit/skills/reflect/SKILL.md +7 -2
  131. package/kit/skills/release/SKILL.md +13 -0
  132. package/kit/skills/security-scan/SKILL.md +6 -2
  133. package/kit/skills/security-scan/references/ai-agents.md +16 -0
  134. package/kit/skills/security-scan/references/fronts.md +59 -2
  135. package/kit/skills/security-scan/references/reporting.md +13 -0
  136. package/kit/skills/security-scan/references/verify.md +9 -0
  137. package/kit/skills/sonarqube-check/SKILL.md +32 -2
  138. package/kit/skills/sonarqube-check/references/ratchet.md +32 -0
  139. package/kit/skills/spec-planning/SKILL.md +25 -8
  140. package/kit/skills/systematic-debugging/SKILL.md +15 -7
  141. package/kit/skills/systematic-debugging/references/techniques.md +5 -0
  142. package/kit/skills/teamboard/SKILL.md +3 -3
  143. package/kit/skills/testing/SKILL.md +18 -3
  144. package/kit/skills/testing/references/browser-headless.md +21 -0
  145. package/kit/skills/token-budget/SKILL.md +26 -5
  146. package/kit/skills/worktree/SKILL.md +13 -5
  147. package/kit/studio/server/lib/session.js +23 -2
  148. package/kit/studio/server/lib/worktree-changes.js +64 -0
  149. package/kit/studio/web/convo.js +14 -0
  150. package/kit/studio/web/map-plan.js +54 -8
  151. package/kit/studio/web/map.js +15 -3
  152. package/package.json +5 -4
  153. package/start.sh +11 -2
@@ -125,6 +125,7 @@ _mt() {
125
125
  'the advisor (%s) is below the critical tier (opus): Claude Code does not attach an advisor below the model it would advise, so critical work runs without one') s="advisor (%s) kritik seviyenin (opus) altında: Claude Code danışacağı modelin altındaki bir advisor'ı eklemiyor, yani kritik iş advisor'sız koşuyor" ;;
126
126
  "settings.json wires prompt-approval.sh but the script is missing — in auto and dontAsk '/crew-approve commit' records nothing; switch mode with Shift+Tab or commit in your own terminal (update Crewforth)") s="settings.json prompt-approval.sh'i bağlıyor ama betik yok — auto ve dontAsk modunda '/crew-approve commit' hiçbir şey kaydetmez; Shift+Tab ile mod değiştirin ya da kendi terminalinizden commit edin (Crewforth'u güncelleyin)" ;;
127
127
  "prompt-approval.sh is not wired on UserPromptSubmit — in auto and dontAsk '/crew-approve commit' records nothing; switch mode with Shift+Tab or commit in your own terminal (update Crewforth)") s="prompt-approval.sh UserPromptSubmit'e bağlı değil — auto ve dontAsk modunda '/crew-approve commit' hiçbir şey kaydetmez; Shift+Tab ile mod değiştirin ya da kendi terminalinizden commit edin (Crewforth'u güncelleyin)" ;;
128
+ "worktree %s has no .claude/settings.json — a Claude Code session resumed there runs none of Crewforth's hooks (entering it with the EnterWorktree tool copies .claude/ in; otherwise copy it yourself)") s="%s worktree'sinde .claude/settings.json yok — orada sürdürülen bir Claude Code oturumu Crewforth'un hiçbir hook'unu koşmaz (EnterWorktree aracıyla girilince .claude/ oraya kopyalanır; yoksa kendiniz kopyalayın)" ;;
128
129
  "worktree %s carries Crewforth %s, this one %s — a session there runs that version's gates and approval path (update Crewforth there)") s="%s worktree'sinde Crewforth %s var, burada %s — orada açılan oturum o sürümün kapılarını ve onay yolunu koşar (Crewforth'u orada güncelleyin)" ;;
129
130
  "fix the syntax by hand — restoring Crewforth's file would drop any hooks you added") s="sözdizimini elle düzeltin — Crewforth'un dosyasını geri yüklemek eklediğiniz hook'ları siler" ;;
130
131
  'settings.json wires hooks through the %s placeholder — on Windows its separators are stripped before bash runs, so NO hook launches and every gate is silently absent') s="settings.json hook'ları %s yer tutucusuyla bağlıyor — Windows'ta ayırıcılar bash çalışmadan silinir, bu yüzden HİÇBİR hook başlamaz ve bütün kapılar sessizce yok olur" ;;
@@ -153,6 +154,13 @@ _mt() {
153
154
  'CLAUDE.md (or a doc it references) names auto-delegated agent(s) that no installed agent matches — delegation to them silently fails') s='CLAUDE.md (ya da başvurduğu bir belge) kurulu hiçbir ajanla eşleşmeyen, otomatik devredilen ajan(lar) anıyor — onlara devretmek sessizce başarısız olur' ;;
154
155
  'rename each bare reference to its `crew-` id:') s='her çıplak anmayı `crew-` kimliğiyle değiştirin:' ;;
155
156
  'CLAUDE.md (or a referenced doc) names pull-only agent(s) by their old bare id — invoked explicitly, so delegation still works; rename for consistency:%s') s='CLAUDE.md (ya da başvurduğu bir belge) yalnız açıkça çağrılan ajan(lar)ı eski çıplak kimliğiyle anıyor — açıkça çağrıldıkları için devretme yine çalışır; tutarlılık için yeniden adlandırın:%s' ;;
157
+ 'CLAUDE.md imports file(s) that do not exist — they load nothing, silently:%s') s='CLAUDE.md var olmayan dosya(lar)ı içe aktarıyor — hiçbir şey yüklenmiyor ve bu söylenmiyor:%s' ;;
158
+ 'CLAUDE.md points at path(s) that do not exist (moved or deleted?):%s') s='CLAUDE.md var olmayan yol(lar)ı gösteriyor (taşındı ya da silindi mi?):%s' ;;
159
+ 'set in this environment, read by no Crewforth file (a typo?):%s — it changes nothing') s="bu ortamda ayarlı ama hiçbir Crewforth dosyası okumuyor (yazım hatası mı?):%s — hiçbir şeyi değiştirmiyor" ;;
160
+ 'Crewforth switch(es) that take a whole number set to something else:%s — a hook may ignore it and use its default') s='tam sayı bekleyen Crewforth anahtar(lar)ı başka bir şeye ayarlı:%s — hook onu yok sayıp varsayılanını kullanabilir' ;;
161
+ 'CLAUDE.md line(s)%s read as loosening a §4 rule (approval, review or hooks) — a project CLAUDE.md can tighten §4, never loosen it, and the gates still apply; reword them so the model and the gates agree') s="CLAUDE.md satır(lar)ı%s bir §4 kuralını (onay, review ya da hook) gevşetiyor gibi okunuyor — proje CLAUDE.md'si §4'ü sıkılaştırabilir, asla gevşetemez, kapılar yine işler; model ile kapılar aynı şeyi söylesin diye bu satırları yeniden yazın" ;;
162
+ '%s points at path(s) that do not exist (moved or deleted?):%s') s='%s var olmayan yol(lar)ı gösteriyor (taşındı ya da silindi mi?):%s' ;;
163
+ 'every import and file path in CLAUDE.md resolves') s="CLAUDE.md'deki her içe aktarma ve dosya yolu çözülüyor" ;;
156
164
  'agent references resolve to installed agents (CLAUDE.md + referenced docs)') s='ajan anmaları kurulu ajanlara çözülüyor (CLAUDE.md ve başvurduğu belgeler)' ;;
157
165
  'CLAUDE.md missing — .claude/DISCIPLINE.md is never loaded (routing / DoD / session rules absent)') s='CLAUDE.md yok — .claude/DISCIPLINE.md hiç yüklenmiyor (yönlendirme / DoD / oturum kuralları yok)' ;;
158
166
  'create CLAUDE.md with this as its own line: @.claude/DISCIPLINE.md') s="CLAUDE.md'yi oluşturun ve şunu ayrı bir satır olarak yazın: @.claude/DISCIPLINE.md" ;;
@@ -189,6 +197,9 @@ _mt() {
189
197
  'CLAUDE.md project section is still the template (placeholders left in)') s='CLAUDE.md proje bölümü hâlâ şablon (yer tutucular duruyor)' ;;
190
198
  'fill in Project / Stack / Project skills — agents read the stack from there') s='Project / Stack / Project skills bölümlerini doldurun — ajanlar yığını oradan okur' ;;
191
199
  'CLAUDE.md project section is filled in') s='CLAUDE.md proje bölümü doldurulmuş' ;;
200
+ 'quality analysis recorded: %s') s='kalite analizi kayıtlı: %s' ;;
201
+ 'CLAUDE.md names no quality analysis (## Stack → Quality analysis: %s) — the quality gate cannot tell a skipped analysis from a declined one') s='CLAUDE.md kalite analizi belirtmiyor (## Stack → Quality analysis: %s) — kalite kapısı atlanmış analizi reddedilmiş olandan ayıramıyor' ;;
202
+ 'add "Quality analysis: sonarqube-local" (or sonarqube-server, or declined) under ## Stack; sonarqube-check stands a local server up') s='## Stack altına "Quality analysis: sonarqube-local" (ya da sonarqube-server, ya da declined) ekleyin; sonarqube-check yerel sunucuyu kurar' ;;
192
203
  "%s project-specific skill(s) alongside Crewforth's") s="Crewforth'unkilerin yanında %s projeye özel skill" ;;
193
204
  "no project-specific skill — every skill installed here ships with Crewforth") s="projeye özel skill yok — burada kurulu her skill Crewforth ile geliyor" ;;
194
205
  "no project-specific skill — %s, which CLAUDE.md lists under Project skills, ships with Crewforth") s="projeye özel skill yok — CLAUDE.md'nin Project skills altında saydığı %s Crewforth ile geliyor" ;;
@@ -205,6 +216,18 @@ _mt() {
205
216
  're-read it against the code and update Stack / Project skills; keep the @.claude/DISCIPLINE.md line and your own rule sections as they are') s='kodla karşılaştırarak yeniden okuyun ve Stack / Project skills bölümlerini güncelleyin; @.claude/DISCIPLINE.md satırını ve kendi kural bölümlerinizi olduğu gibi bırakın' ;;
206
217
  'freshness signal skipped (cannot read CLAUDE.md mtime on this platform)') s="güncellik sinyali atlandı (bu platformda CLAUDE.md'nin değişme zamanı okunamıyor)" ;;
207
218
  ' → readiness %s/%s') s=' → hazırlık %s/%s' ;;
219
+ 'hook injection every turn (UserPromptSubmit): %s bytes, ~%s tokens (estimate: bytes/4), %s hook(s) run') s="hook enjeksiyonu her turda (UserPromptSubmit): %s bayt, ~%s token (tahmin: bayt/4), %s hook çalıştı" ;;
220
+ 'hook injection each session start (SessionStart): %s bytes, ~%s tokens (estimate: bytes/4), %s hook(s) run') s="hook enjeksiyonu her oturum başında (SessionStart): %s bayt, ~%s token (tahmin: bayt/4), %s hook çalıştı" ;;
221
+ '%s (%s): %s bytes') s='%s (%s): %s bayt' ;;
222
+ '%s (%s): 0 bytes for this fixture') s='%s (%s): bu fixture için 0 bayt' ;;
223
+ 'not measured: %s (%s) — %s') s='ölçülmedi: %s (%s) — %s' ;;
224
+ 'writes .claude/state/crew-critical-paths.auto in the project') s="projede .claude/state/crew-critical-paths.auto dosyasını yazıyor" ;;
225
+ 'writes .claude/.state/update-asked (it would silence the real update question) and starts a network check') s="projede .claude/.state/update-asked dosyasını yazıyor (gerçek güncelleme sorusunu susturur) ve bir ağ denetimi başlatıyor" ;;
226
+ 'its scratch folder is inside a git repository, where it would empty a recorded approval') s="geçici klasörü bir git deposunun içinde; orada kayıtlı bir onayı boşaltırdı" ;;
227
+ 'the script is not in .claude/hooks') s=".claude/hooks içinde betik yok" ;;
228
+ "not one of Crewforth's checked hooks, so what it writes is unknown") s="Crewforth'un denetlenmiş hook'larından değil; ne yazdığı bilinmiyor" ;;
229
+ 'a floor for one fixture (prompt: %s; a new session), not a maximum: a hook that speaks only for some prompts or states says nothing here') s='tek bir fixture için alt sınır (istem: %s; yeni oturum), üst sınır değil: yalnız bazı istemlerde ya da durumlarda konuşan bir hook burada hiçbir şey söylemez' ;;
230
+ 'hook injection NOT MEASURED — the measurement printed no totals (eval/lib/hook-inject.sh)') s='hook enjeksiyonu ÖLÇÜLMEDİ — ölçüm toplam basmadı (eval/lib/hook-inject.sh)' ;;
208
231
  'npx crewforth adopt') ;; # identifier, printed as is
209
232
  'chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg') ;; # identifier, printed as is
210
233
  'git config core.hooksPath .claude/hooks') ;; # identifier, printed as is
@@ -450,7 +473,12 @@ if [ -f .claude/VERSION ] && git rev-parse --git-dir >/dev/null 2>&1; then
450
473
  _wv0="$(head -1 .claude/VERSION 2>/dev/null | tr -cd '0-9A-Za-z.-')"; _wt0="$(git rev-parse --show-toplevel 2>/dev/null)"
451
474
  while IFS= read -r _wl; do
452
475
  case "$_wl" in "worktree "*) _wp="${_wl#worktree }" ;; *) continue ;; esac
453
- [ "$_wp" != "$_wt0" ] && [ -f "$_wp/.claude/VERSION" ] || continue
476
+ [ "$_wp" != "$_wt0" ] || continue
477
+ # A linked worktree with no settings.json loads no project hook in a session resumed there (measured, see
478
+ # worktree-provision.sh). One Claude Code makes under .claude/worktrees/ still runs this checkout's hooks.
479
+ case "$_wp" in "$_wt0"/.claude/worktrees/*) ;; *)
480
+ [ -d "$_wp" ] && [ ! -f "$_wp/.claude/settings.json" ] && warn "worktree %s has no .claude/settings.json — a Claude Code session resumed there runs none of Crewforth's hooks (entering it with the EnterWorktree tool copies .claude/ in; otherwise copy it yourself)" "$_wp" ;; esac
481
+ [ -f "$_wp/.claude/VERSION" ] || continue
454
482
  _wv="$(head -1 "$_wp/.claude/VERSION" 2>/dev/null | tr -cd '0-9A-Za-z.-')"
455
483
  [ "$_wv" = "$_wv0" ] || warn "worktree %s carries Crewforth %s, this one %s — a session there runs that version's gates and approval path (update Crewforth there)" "$_wp" "${_wv:-?}" "$_wv0"
456
484
  done <<EOF_WT
@@ -512,6 +540,56 @@ EOF_SLA
512
540
  fi
513
541
  fi
514
542
 
543
+ # 4c) Per-turn hook injection. What the hooks on UserPromptSubmit (every turn) and SessionStart (each start, resume,
544
+ # clear, compact) add to the context, in bytes: eval/lib/hook-inject.sh runs each one on a fixture payload with a
545
+ # throwaway HOME/TMPDIR and a session id no real session has, and skips, by name, a hook it cannot run without a
546
+ # lasting write. A FLOOR for that fixture, not a maximum: a hook that speaks only for some prompts or states says
547
+ # nothing here, and Claude Code's own framing around each injection is not counted. Tokens are bytes/4, an
548
+ # estimate, not a tokenizer count. Advisory, never a failure.
549
+ _HI="$_DOC_LIB/hook-inject.sh"; [ -f "$_HI" ] || _HI=.claude/eval/lib/hook-inject.sh
550
+ if [ -f .claude/settings.json ] && [ -f "$_HI" ]; then
551
+ _hio="$(bash "$_HI" . 2>/dev/null)"
552
+ _hip=""; _hiu=""; _his=""; _hil=""
553
+ while IFS=$'\t' read -r _k _e _h _b _r; do
554
+ case "$_k" in
555
+ prompt) _hip="$_e" ;;
556
+ total) case "$_e" in UserPromptSubmit) _hiu="$_h $_b" ;; *) _his="$_h $_b" ;; esac ;;
557
+ run) _hil="$_hil$_e $_h $_b
558
+ " ;;
559
+ skip) case "$_b" in
560
+ writes-state) _mt "writes .claude/state/crew-critical-paths.auto in the project" ;;
561
+ writes-update) _mt "writes .claude/.state/update-asked (it would silence the real update question) and starts a network check" ;;
562
+ scratch-in-repo) _mt "its scratch folder is inside a git repository, where it would empty a recorded approval" ;;
563
+ missing) _mt "the script is not in .claude/hooks" ;;
564
+ *) _mt "not one of Crewforth's checked hooks, so what it writes is unknown" ;;
565
+ esac
566
+ _hil="$_hil$_e $_h !$_M
567
+ " ;;
568
+ esac
569
+ done <<EOF_HI
570
+ $_hio
571
+ EOF_HI
572
+ if [ -n "$_hiu" ] && [ -n "$_his" ]; then
573
+ read -r _ub _un <<< "$_hiu"; read -r _sb _sn <<< "$_his"
574
+ ok "hook injection every turn (UserPromptSubmit): %s bytes, ~%s tokens (estimate: bytes/4), %s hook(s) run" "$_ub" "$((_ub / 4))" "$_un"
575
+ ok "hook injection each session start (SessionStart): %s bytes, ~%s tokens (estimate: bytes/4), %s hook(s) run" "$_sb" "$((_sb / 4))" "$_sn"
576
+ while IFS=$'\t' read -r _e _h _v; do
577
+ [ -n "$_h" ] || continue
578
+ case "$_v" in
579
+ !*) _mt "not measured: %s (%s) — %s" "$_h" "$_e" "${_v#!}" ;;
580
+ 0) _mt "%s (%s): 0 bytes for this fixture" "$_h" "$_e" ;;
581
+ *) _mt "%s (%s): %s bytes" "$_h" "$_e" "$_v" ;;
582
+ esac
583
+ echo " ↳ $_M"
584
+ done <<EOF_HL
585
+ $_hil
586
+ EOF_HL
587
+ _mt "a floor for one fixture (prompt: %s; a new session), not a maximum: a hook that speaks only for some prompts or states says nothing here" "$_hip"; echo " ↳ $_M"
588
+ else
589
+ skip "hook injection NOT MEASURED — the measurement printed no totals (eval/lib/hook-inject.sh)"
590
+ fi
591
+ fi
592
+
515
593
  # 4b) Is delegation itself switched off? The documented way to stop Claude using ANY subagent is to deny the `Agent`
516
594
  # tool in permissions.deny. A project that does that keeps twelve agents on disk that can never run, and the only
517
595
  # symptom is that every task quietly happens on the main thread — which reads as "Crewforth does nothing" rather
@@ -637,6 +715,94 @@ if [ -f .claude/DISCIPLINE.md ]; then
637
715
  fi
638
716
  fi
639
717
 
718
+ # 6a) Pointers in ./CLAUDE.md that lead nowhere. An `@path` import whose file is gone loads nothing and says nothing;
719
+ # a backticked path to a file that was moved or deleted sends the model to read what is not there. Imports are read
720
+ # where Claude Code reads them (its memory docs: `@` anywhere in the file, not inside a code span or block):
721
+ # a line of its own is always one; inside a sentence only a word shaped like a file (an extension), so `@types/node`
722
+ # or `@agent-x` is not mistaken for one. `~/` and absolute paths live outside the repository and are not judged,
723
+ # nor is a path with an escaped space. A backticked path counts when it is shaped like one (a `/`, an extension, no
724
+ # placeholder, glob or space); a `:42` or `#L10` after it is a location in the file, not part of its name. An
725
+ # example path in prose that is shaped like a real one is still named: doctor cannot tell an example from a pointer.
726
+ # Nested instruction files (a CLAUDE.md or AGENTS.md below the root, and an AGENTS.md at it) are read the same
727
+ # way: Claude Code loads a subdirectory's CLAUDE.md when it works there, and AGENTS.md is the file other tools
728
+ # read. A path in one resolves against its own folder first, then the root. Found by find, not git ls-files: an
729
+ # install gitignores CLAUDE.md, and the pattern hides the nested ones too. Dependency and build folders are not
730
+ # walked, and at most 20 files are read, so a monorepo does not make the doctor slow. Information only.
731
+ # _ptrs FILE BASE: the imports and backticked paths in FILE that resolve neither under BASE nor at the root.
732
+ _ptrs() {
733
+ _imp=""; _btp=""; local _fen=0 _l _r _p _t _own _w _ws _b="$2"
734
+ while IFS= read -r _l || [ -n "$_l" ]; do
735
+ _l="${_l%$'\r'}"
736
+ case "$_l" in '```'*|'~~~'*) _fen=$((1-_fen)); continue ;; esac
737
+ [ "$_fen" = 1 ] && continue
738
+ # Backticked spans: collected as path candidates, and removed from the text the imports are read in.
739
+ _r="$_l"; _p=""
740
+ while case "$_r" in *'`'*'`'*) true ;; *) false ;; esac; do
741
+ _p="$_p ${_r%%\`*}"; _r="${_r#*\`}"; _t="${_r%%\`*}"; _r="${_r#*\`}"
742
+ _t="${_t%%#*}"; case "$_t" in *:[0-9]*) _t="${_t%%:[0-9]*}" ;; esac
743
+ case "$_t" in *[[:space:]\<\>\*\{\}\$\|]*|'~'*|@*|/*|http*|-*|*://*) continue ;; */*.[A-Za-z]*) ;; *) continue ;; esac
744
+ _t="${_t#./}"; [ -e "$_b/$_t" ] || [ -e "$_t" ] || case "$_btp" in *" $_t"*) ;; *) _btp="$_btp $_t" ;; esac
745
+ done
746
+ _p="$_p $_r"
747
+ case "$_l" in @*) _own=1 ;; *) _own=0 ;; esac
748
+ read -r -a _ws <<< "$_p"
749
+ for _w in "${_ws[@]+"${_ws[@]}"}"; do
750
+ case "$_w" in @?*) ;; *) continue ;; esac
751
+ _t="${_w#@}"; while case "$_t" in *[.,\;:\)\]]) true ;; *) false ;; esac; do _t="${_t%?}"; done
752
+ case "$_t" in ''|'~/'*|/*|*\\*) continue ;; esac
753
+ [ "$_own" = 1 ] || case "$_t" in *.[A-Za-z]*) ;; *) continue ;; esac
754
+ _own=0
755
+ [ -e "$_b/$_t" ] || case "$_imp" in *" @$_t"*) ;; *) _imp="$_imp @$_t" ;; esac
756
+ done
757
+ done < "$1"
758
+ # A path written relative to a sub-project (`services/x.py` for backend/app/services/x.py) is not missing: one
759
+ # `git ls-files`, asked only when something looked missing, and a tracked file that ends with the path resolves it.
760
+ if [ -n "$_btp" ] && [ -n "${_ls:=$(git ls-files 2>/dev/null)}" ]; then
761
+ local _nl=$'\n' _k=""
762
+ for _t in $_btp; do case "$_nl$_ls$_nl" in *"/$_t$_nl"*|*"$_nl$_t$_nl"*) ;; *) _k="$_k $_t" ;; esac; done
763
+ _btp="$_k"
764
+ fi
765
+ }
766
+ _ls=""
767
+ if [ -f CLAUDE.md ]; then
768
+ _ptrs CLAUDE.md .
769
+ [ -n "$_imp" ] && warn "CLAUDE.md imports file(s) that do not exist — they load nothing, silently:%s" "$_imp"
770
+ [ -n "$_btp" ] && warn "CLAUDE.md points at path(s) that do not exist (moved or deleted?):%s" "$_btp"
771
+ [ -z "$_imp$_btp" ] && ok "every import and file path in CLAUDE.md resolves"
772
+ fi
773
+ _nst=0
774
+ while IFS= read -r _nf; do
775
+ _nf="${_nf#./}"; [ "$_nf" = CLAUDE.md ] && continue
776
+ _nst=$((_nst+1)); [ "$_nst" -gt 20 ] && break
777
+ _ptrs "$_nf" "$(dirname "$_nf")"
778
+ [ -n "$_imp$_btp" ] && warn "%s points at path(s) that do not exist (moved or deleted?):%s" "$_nf" "$_imp$_btp"
779
+ done < <(find . -maxdepth 6 \( -name .git -o -name .claude -o -name node_modules -o -name vendor -o -name dist \
780
+ -o -name build -o -name target -o -name .venv -o -name venv -o -name Pods \) -prune \
781
+ -o \( -name CLAUDE.md -o -name AGENTS.md \) -type f -print 2>/dev/null | LC_ALL=C sort)
782
+
783
+ # 6b) A line in ./CLAUDE.md that reads as loosening §4. The discipline says a project CLAUDE.md can tighten §4, never
784
+ # loosen it, and the gates hold either way; but a line like "no need to ask before committing" tells the model
785
+ # the opposite of what the gate will do, and the session then argues with the gate. Information only, and a
786
+ # phrase list, not a judge: a loosening phrase on a line about commits, pushes, reviews or hooks, with no
787
+ # "never", "do not", "don't" or "must not" before it. Code fences and the @import line are not read.
788
+ if [ -f CLAUDE.md ]; then
789
+ _lz="$(LC_ALL=C awk '
790
+ /^[[:space:]]*(```|~~~)/ { f = !f; next }
791
+ f || /^@/ { next }
792
+ { l = tolower($0) }
793
+ l !~ /commit|push|review|hook|verify|approv|force|reset/ { next }
794
+ {
795
+ n = split("no need to ask|no need for approval|no approval needed|without asking|without approval|without confirmation|skip the review|skip review|skip the hook|auto-commit|autocommit|commit directly|push directly|commit freely|push freely|you may commit|you may push|use --no-verify|--no-verify is fine|--no-verify is ok|force-push is fine|force push is fine", ph, "|")
796
+ for (i = 1; i <= n; i++) {
797
+ p = index(l, ph[i]); if (!p) continue
798
+ pre = substr(l, 1, p - 1)
799
+ if (pre ~ /never|do not|don.t|must not|not allowed|forbidden/) continue
800
+ printf " %d", NR; break
801
+ }
802
+ }' CLAUDE.md 2>/dev/null)"
803
+ [ -n "$_lz" ] && warn "CLAUDE.md line(s)%s read as loosening a §4 rule (approval, review or hooks) — a project CLAUDE.md can tighten §4, never loosen it, and the gates still apply; reword them so the model and the gates agree" "$_lz"
804
+ fi
805
+
640
806
  # 8b) The shell matcher. Claude Code's hooks reference is explicit: inspect shell commands with
641
807
  # `Bash|PowerShell`, because wherever the PowerShell tool is enabled it IS the shell — and it is on by
642
808
  # default for claude.ai and Console accounts on Windows. An install from before 2.5.0 watches only `Bash`,
@@ -858,6 +1024,27 @@ fi
858
1024
  _pre=""; [ -n "$_DOC_LIB" ] && [ -f "$_DOC_LIB/preset.sh" ] && _pre="$(bash "$_DOC_LIB/preset.sh" --show 2>/dev/null)"
859
1025
  ok "organisation preset: %s" "${_pre:-none}"
860
1026
 
1027
+ # 9f) Crewforth's own switches, as set in this environment. A CREW_ variable no Crewforth file reads is almost always
1028
+ # a typo (a misspelt routing switch set to off switches nothing off, and nothing says so), and a switch that
1029
+ # takes a number set to something else may be ignored for its default without a word. The names are read from the installed files
1030
+ # themselves, so the list cannot go stale; a value is judged only for names that end like a number (_MAX, _MIN,
1031
+ # _PCT, _N, _TIMEOUT, _AGE, _BYTES, _CHARS, _TURNS, _TOOLS, _ERRORS, _TOLERANCE). Information only.
1032
+ # Read where this doctor runs from too (its own eval/ and hooks/ beside it): a suite that drives it sets names of its own.
1033
+ _kd="${_DOC_LIB:+${_DOC_LIB%/lib}}"
1034
+ _kn="$(grep -rhoE 'CREW_[A-Z0-9_]+' .claude/hooks .claude/eval .claude/skills .claude/studio ${_kd:+"$_kd" "${_kd%/eval}/hooks"} 2>/dev/null | LC_ALL=C sort -u)"
1035
+ if [ -n "$_kn" ]; then
1036
+ _nl=$'\n'; _uk=""; _nv=""
1037
+ while IFS='=' read -r _vn _vv; do
1038
+ case "$_vn" in CREW_[A-Z0-9_]*) ;; *) continue ;; esac
1039
+ case "$_nl$_kn$_nl" in *"$_nl$_vn$_nl"*) ;; *) _uk="$_uk $_vn"; continue ;; esac
1040
+ case "$_vn" in *_MAX|*_MIN|*_PCT|*_N|*_TIMEOUT|*_AGE|*_BYTES|*_CHARS|*_TURNS|*_TOOLS|*_ERRORS|*_TOLERANCE|*_MAX_*|*_MIN_*)
1041
+ case "$_vv" in ''|*[!0-9]*) _nv="$_nv $_vn=$_vv" ;; esac ;;
1042
+ esac
1043
+ done < <(env 2>/dev/null | LC_ALL=C sort)
1044
+ [ -n "$_uk" ] && warn "set in this environment, read by no Crewforth file (a typo?):%s — it changes nothing" "$_uk"
1045
+ [ -n "$_nv" ] && warn "Crewforth switch(es) that take a whole number set to something else:%s — a hook may ignore it and use its default" "$_nv"
1046
+ fi
1047
+
861
1048
  # 10) Gate activity. The suite proves the gates CAN fire; this reports whether anything actually tripped them.
862
1049
  # Recording is on by default (rule names only, never the command), so "no log" here means no gate has
863
1050
  # fired yet — a measured zero, not a gap. Never a failure either way.
@@ -906,6 +1093,19 @@ if [ -f CLAUDE.md ]; then
906
1093
  gap "CLAUDE.md project section is still the template (placeholders left in)" \
907
1094
  "fill in Project / Stack / Project skills — agents read the stack from there"
908
1095
  else rdy "CLAUDE.md project section is filled in"; fi
1096
+ # R1b) Which quality analysis the DoD's quality gate runs. It was written six times as "(if SonarQube is in use)",
1097
+ # and on a project with no server that condition was simply false: one project measured 0 analyses in its
1098
+ # first 8 days. The answer is now a line in ## Stack; a missing or empty one is said here, by value.
1099
+ QA="<missing>"
1100
+ while IFS= read -r _l || [ -n "$_l" ]; do
1101
+ case "$_l" in 'Quality analysis:'*) _l="${_l#Quality analysis:}"; _l="${_l%$'\r'}"; _l="${_l# }"; QA="${_l:-<empty>}"; break ;; esac
1102
+ done < CLAUDE.md
1103
+ case "$QA" in '<missing>'|'<empty>') ;; '<'*) QA='<template>' ;; esac
1104
+ case "$QA" in
1105
+ sonarqube-local*|sonarqube-server*|declined*) rdy "quality analysis recorded: %s" "${QA%% *}" ;;
1106
+ *) gap "CLAUDE.md names no quality analysis (## Stack → Quality analysis: %s) — the quality gate cannot tell a skipped analysis from a declined one" \
1107
+ 'add "Quality analysis: sonarqube-local" (or sonarqube-server, or declined) under ## Stack; sonarqube-check stands a local server up' "${QA%% *}" ;;
1108
+ esac
909
1109
  fi
910
1110
 
911
1111
  # R2) A project-specific skill — Crewforth ships the generic 'how's; the domain ones (payment-contract,
@@ -69,6 +69,30 @@ awk '
69
69
  sort -u "$TMP/inv" -o "$TMP/inv"
70
70
  NRULES=$(grep -c . "$TMP/inv" 2>/dev/null); NRULES=$(( ${NRULES:-0} + 0 ))
71
71
 
72
+ # ---- the exceptions a project wrote down. A gate with many exceptions is a rule the project disagrees with, and that
73
+ # is worth an ADR or a change to the rule rather than one more line. Counted per rule: a `rule:<name>` entry, and an
74
+ # exact pattern line, credited to the rule heading it sits under in floor-blocklist.txt; a `path:` entry names no rule.
75
+ allowlists(){
76
+ local fa=./.floor-allowlist.txt pa=./.private-allowlist.txt fb="$HOOKS/floor-blocklist.txt"
77
+ [ -f "$fa" ] || [ -f "$pa" ] || return 0
78
+ echo " exceptions written down:"
79
+ if [ -f "$fa" ]; then
80
+ awk -v fb="$fb" '
81
+ FILENAME == fb { sub(/\r$/, ""); if ($0 ~ /^# rule: /) { r = substr($0, 9); next } if ($0 ~ /^#/ || $0 == "") next; rule[$0] = r; next }
82
+ { sub(/\r$/, ""); if ($0 ~ /^#/ || $0 == "") next
83
+ e = $0; i = index(e, " # "); if (i) e = substr(e, 1, i - 1); sub(/[[:space:]]+$/, "", e); n++
84
+ if (e ~ /^rule:/) c[substr(e, 6)]++; else if (e !~ /^path:/ && (e in rule)) c[rule[e]]++ }
85
+ END { printf " .floor-allowlist.txt %d entr%s\n", n, (n == 1 ? "y" : "ies")
86
+ for (k in c) if (c[k] >= 3) printf " \342\232\240 rule %s is allowlisted %d times: write an ADR or change the rule\n", k, c[k] }
87
+ ' "$fb" "$fa" 2>/dev/null
88
+ fi
89
+ if [ -f "$pa" ]; then
90
+ local np; np="$(grep -cvE '^[[:space:]]*(#|$)' "$pa" 2>/dev/null)"; np="${np:-0}"
91
+ printf ' .private-allowlist.txt %s entr%s\n' "$np" "$([ "$np" = 1 ] && echo y || echo ies)"
92
+ fi
93
+ echo
94
+ }
95
+
72
96
  # ---- no log. Since 2.5.0 recording is ON by default, so the absence of a file means one of two DIFFERENT
73
97
  # things and the report must not blur them:
74
98
  # .claude/ exists -> recording had somewhere to go and nothing has tripped a gate yet. That is a real
@@ -81,6 +105,7 @@ if [ -z "$LOG" ] || [ ! -f "$LOG" ]; then
81
105
  echo " ✅ no gate has fired in this project yet — $NRULES rules wired, recording on,"
82
106
  echo " nothing has tripped one. (Recording writes .claude/gate-log.tsv, which is gitignored;"
83
107
  echo " rule names and verdicts only, never the command — CREW_GATE_LOG_CMD=1 adds it for debugging.)"
108
+ echo; allowlists
84
109
  fi
85
110
  exit 0
86
111
  fi
@@ -137,3 +162,6 @@ echo
137
162
  echo " Not observed is NOT dead. It means one of: nobody attempted it, the model never reached for that"
138
163
  echo " command, or the rule cannot match. Only the first two are healthy, and this report cannot tell them"
139
164
  echo " apart — smoke-test is what proves a rule CAN fire; this shows whether anything tripped it."
165
+
166
+ echo
167
+ allowlists
@@ -51,3 +51,5 @@ we need to put this build on the server tonight|deploy
51
51
  # in the same change — so these are weaker held-out evidence than the rows above.
52
52
  empty repo, nobody has picked a language or framework for the api yet|backend-architecture
53
53
  we cannot agree whether this service should be layered or split per feature|backend-architecture
54
+ # P14 row for the docs-writer "diagram" trigger: same honesty note, written in the change that added it.
55
+ sketch a diagram of how the services call each other|docs-writer
@@ -36,6 +36,8 @@ flutter ekranı ekle|crew-frontend-expert
36
36
  add a SwiftUI view for the profile|crew-frontend-expert
37
37
  build a Jetpack Compose screen for settings|crew-frontend-expert
38
38
  run an OWASP check and a full security scan|crew-security-expert
39
+ check the login flow for session fixation|crew-security-expert
40
+ the oauth redirect accepts any callback url|crew-security-expert
39
41
  we need a kvkk privacy audit|crew-privacy-agent
40
42
  commit the changes|crew-commit-agent
41
43
  run tests and give me a coverage report|crew-test-expert
@@ -50,7 +52,9 @@ check i18n for any missing translation|i18n-integrity
50
52
  run the sonarqube quality gate|sonarqube-check
51
53
  prepare a handoff with the current session state|handoff
52
54
  add a structured log with a correlation id|observability
55
+ add metrics to the checkout service|observability
53
56
  update the README and write the API docs|docs-writer
57
+ draw an architecture diagram for the README|docs-writer
54
58
  run an accessibility audit and check WCAG|a11y
55
59
  handle api versioning with an openapi contract|api-design
56
60
  the page is slow, do a performance optimization|performance
@@ -62,6 +66,7 @@ keep going until the tests pass and the review is clean|iterate
62
66
  debug this and find the root cause of the intermittent bug|systematic-debugging
63
67
  improve the visual design — the spacing and typography look off|frontend-design
64
68
  build an MCP server to expose these tools to Claude|mcp-builder
69
+ add an MCP tool to my agent|mcp-builder
65
70
  build a threat model and map the attack surface before we scan|threat-model
66
71
  set up an eval to measure output quality against a baseline|eval-grader
67
72
 
@@ -96,6 +101,7 @@ the settings screen needs a state management fix|frontend
96
101
  we need a consent flow before we store any of this|privacy-compliance
97
102
  let us step back and see what we missed this week|reflect
98
103
  scan for vulnerabilities in the auth flow|security-scan
104
+ review our github actions security before we add pull_request_target|security-scan
99
105
  how do we split this into tasks with acceptance criteria|spec-planning
100
106
  are the tests green after that change|testing
101
107
  check for AI trace before the commit|trace-scan
@@ -140,6 +146,7 @@ the screen froze during the demo|!crew-frontend-expert
140
146
  fix the layout of this json file|!frontend-design
141
147
  design the database schema|!frontend-design
142
148
  read the model context and summarize it|!mcp-builder
149
+ redirect the user to the dashboard after login|!crew-security-expert
143
150
  optimize the slow sql query performance|!systematic-debugging
144
151
  # `build` contains `ui`, and `UI` used to be a trigger, so a CI failure routed to the frontend expert. A wrong
145
152
  # route is worse than none: it looks like Crewforth worked. Fixed by word-bounded matching AND by retiring the
@@ -150,8 +157,10 @@ the docker build is broken in the pipeline|!crew-frontend-expert
150
157
  # session-context skill. A one-word trigger for a word that lives in two vocabularies is not a trigger.
151
158
  add a dark mode and a design token layer|!token-budget
152
159
  isolate this work in a git worktree before the parallel agents run|worktree
160
+ remove the worktree once the experiment is done|worktree
153
161
  before I start, am I sure enough to implement this|confidence-check
154
162
  upgrade dependencies to current versions|dependency-upgrade
163
+ land the open dependabot PRs|dependency-upgrade
155
164
  is this endpoint fast enough on the hot path|crew-performance-expert
156
165
  who is working on which sprint item right now|teamboard
157
166
  audit this against ccpa and lgpd for data protection regulation|crew-privacy-agent
@@ -237,3 +246,11 @@ fix the failing unit test in the payment module|!crew-docs-agent
237
246
  # Positive twins of the read-only prompts smoke-test pins silent (route-hint.sh): the same README, an action verb.
238
247
  update the README with the notes at the end|crew-docs-agent
239
248
  tuval README'nin sonundaki notları güncelle|crew-docs-agent
249
+ # Dependency added inside the request (the trigger is the diff, §4.7; these catch the request that names it), each with its negative twin.
250
+ the lockfile changed after the merge, check the new dependency|dependency-audit
251
+ the build changed after the merge, check the new module|!dependency-audit
252
+ axios paketini ekle ve sürümünü sabitle|dependency-audit
253
+ axios isteğinin zaman aşımını düzelt|!dependency-audit
254
+ # WCAG 2.2 criteria a 2.1 checklist misses.
255
+ make the buttons at least 24 pixels so the target size passes|a11y
256
+ the focus is hidden under the sticky header when tabbing|a11y
@@ -0,0 +1,77 @@
1
+ // contracts.mjs — the rules a project writes down for its own code, and what holds them to the tree.
2
+ //
3
+ // .crewforth/contracts.md is written by the team (or by /crew-map when the user names a rule), never by the scanner,
4
+ // so a rescan cannot drop it. One section per contract:
5
+ //
6
+ // ## totals-round-once
7
+ // Order totals are rounded once, at the end, so a split order adds up to the same cents as the whole.
8
+ // - Core/Orders.cs:Total
9
+ // - Core/Invoices.cs:Sum
10
+ //
11
+ // The heading is the key (kebab case, unique in the file); the first paragraph is the rule, one or two sentences;
12
+ // each `- path:symbol` line is where the rule lives: a file of the project and a name that appears in it as a word.
13
+ // A symbol, not a line number: a rule outlives the edits that move it. Text before the first heading is free.
14
+ //
15
+ // Who reads it: map-stale.sh tells an agent, the moment it edits a cited file, which rules live there; at Stop it
16
+ // tells the user which keys the session's edits touched; crew-review-agent gives a verdict per touched key. The
17
+ // validator below (scan.mjs --contracts, and --validate when the file exists) keeps every cite pointing at code.
18
+
19
+ export const CONTRACTS_FILE = '.crewforth/contracts.md';
20
+ const KEY = /^[a-z0-9]+(-[a-z0-9]+)*$/;
21
+ const SYMBOL = /^[A-Za-z_$/][\w$./-]*$/;
22
+ const MAX_RULE = 300;
23
+ const MAX_KEY = 60;
24
+
25
+ /** Parse the file: [{ key, line, rule, cites: [{ path, symbol, line }] }] and the problems a parse alone can see. */
26
+ export function parse(text) {
27
+ const out = []; const problems = [];
28
+ let cur = null; let para = null;
29
+ String(text).split('\n').forEach((raw, i) => {
30
+ const l = raw.replace(/\r$/, '');
31
+ const h = /^##\s+(.*?)\s*$/.exec(l);
32
+ if (h) { cur = { key: h[1], line: i + 1, rule: '', cites: [] }; out.push(cur); para = 'want'; return; }
33
+ if (!cur) return;
34
+ const c = /^\s*[-*]\s+(\S+)\s*$/.exec(l);
35
+ if (c) {
36
+ const at = c[1].lastIndexOf(':');
37
+ cur.cites.push({ path: at > 0 ? c[1].slice(0, at) : c[1], symbol: at > 0 ? c[1].slice(at + 1) : '', line: i + 1 });
38
+ if (para === 'in') para = 'done';
39
+ return;
40
+ }
41
+ if (!l.trim()) { if (para === 'in') para = 'done'; return; }
42
+ if (para === 'want') { cur.rule = l.trim(); para = 'in'; }
43
+ else if (para === 'in') cur.rule += ` ${l.trim()}`;
44
+ else problems.push({ at: `${cur.key}`, message: `line ${i + 1} is neither the rule nor a "- path:symbol" cite` });
45
+ });
46
+ return { contracts: out, problems };
47
+ }
48
+
49
+ const sentences = (t) => (String(t).trim().match(/[.!?](\s|$)/g) ?? []).length;
50
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+
52
+ /**
53
+ * Hold the file to the tree. `files` is the scanner's listing (a Set), `read(rel)` its reader ({ text } or null).
54
+ * Returns { contracts, problems: [{ at, message }] }; no problem means every rule is stated and every cite lands.
55
+ */
56
+ export function validate(text, files, read) {
57
+ const { contracts, problems } = parse(text);
58
+ const seen = new Set();
59
+ for (const c of contracts) {
60
+ const at = c.key || `line ${c.line}`;
61
+ if (!KEY.test(c.key) || c.key.length > MAX_KEY) problems.push({ at, message: `the key is not kebab case (a-z, 0-9 and single dashes, ${MAX_KEY} characters at most)` });
62
+ if (seen.has(c.key)) problems.push({ at, message: 'the key is used twice: a review gives one verdict per key' });
63
+ seen.add(c.key);
64
+ if (!c.rule) problems.push({ at, message: 'states no rule: the first paragraph under the heading is the rule' });
65
+ else if (c.rule.length > MAX_RULE || sentences(c.rule) > 2) problems.push({ at, message: `the rule runs past two sentences or ${MAX_RULE} characters` });
66
+ if (!c.cites.length) problems.push({ at, message: 'cites nothing: a rule says where it lives with "- path:symbol"' });
67
+ for (const x of c.cites) {
68
+ const where = `${x.path}:${x.symbol}`;
69
+ if (!x.symbol || !SYMBOL.test(x.symbol) || /^\d+$/.test(x.symbol)) { problems.push({ at, message: `${where} names no symbol (a function, type, key or route that appears in the file, not a line number)` }); continue; }
70
+ if (x.path.startsWith('/') || /^[A-Za-z]:/.test(x.path) || x.path.split('/').includes('..') || !files.has(x.path)) { problems.push({ at, message: `${where}: ${x.path} is not a file of this project` }); continue; }
71
+ const src = read(x.path);
72
+ if (!src) { problems.push({ at, message: `${where}: ${x.path} could not be read` }); continue; }
73
+ if (!new RegExp(`(^|[^\\w$])${escape(x.symbol)}($|[^\\w$])`, 'm').test(src.text)) problems.push({ at, message: `${where}: ${x.symbol} does not appear in ${x.path} (renamed or moved?)` });
74
+ }
75
+ }
76
+ return { contracts, problems };
77
+ }
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # crew-map.sh — the shell entry to scan.mjs. It does one thing: find a Node that RUNS and hand over.
3
3
  #
4
- # bash crew-map.sh [--root DIR] [--stdout | --check] the arguments and exit codes of scan.mjs (0, 1, 2)
4
+ # bash crew-map.sh [--root DIR] [--stdout | --check | --validate] [--force] the arguments and exit codes of
5
+ # scan.mjs (0, 1, 2); --force writes a rescan that would lose most of the map (it refuses with rc 1 otherwise)
5
6
  # rc 3 = no usable Node here, so nothing was scanned. Not a pass: a caller must not read 3 as a fresh map.
6
7
  #
7
8
  # The same resolution as findings.sh beside it: ask node for its version (the Windows Store's stub resolves and
@@ -10,7 +10,7 @@
10
10
 
11
11
  import crypto from 'node:crypto';
12
12
  import { canon } from './surface.mjs';
13
- import { sortEv } from './project.mjs';
13
+ import { sortEv, isTestModule } from './project.mjs';
14
14
 
15
15
  export const RULES = ['cycle', 'module-orphan', 'version-drift', 'dep-undeclared', 'dep-unused', 'config-missing',
16
16
  'config-unread', 'endpoint-missing', 'endpoint-unused', 'external-unowned'];
@@ -85,6 +85,7 @@ export function detect(ctx) {
85
85
  for (const m of mods) {
86
86
  if (m.dir === '.' || m.entry || m.deployed || incoming.has(m.id)) continue;
87
87
  if (m.tech.has('maven:pom')) continue; // an aggregator is a build entry point
88
+ if (isTestModule(m)) continue; // a test project is run by the test runner, never referenced
88
89
  out.push(finding('module-orphan', m.id, `No module references ${m.name}, and it has no entry point`, m.evidence, { module: m.id }));
89
90
  }
90
91
  }
@@ -24,8 +24,9 @@ export const LAYERS = ['ui', 'api', 'service', 'data', 'infra', 'tool', 'library
24
24
  /** The "file:line" a narrative of this node may cite. */
25
25
  export function citesOf(node) {
26
26
  const out = new Set();
27
- // Its evidence, its entry point and what its README says about it: every line the scan read for this node.
28
- for (const e of [node?.evidence, node?.entryPoint?.evidence, node?.about?.evidence].flatMap((x) => (Array.isArray(x) ? x : []))) {
27
+ // Its evidence, its entry point, what its README says about it and the WHY/HACK/FIXME lines in its code: every
28
+ // line the scan read for this node.
29
+ for (const e of [node?.evidence, node?.entryPoint?.evidence, node?.about?.evidence, node?.markers?.evidence].flatMap((x) => (Array.isArray(x) ? x : []))) {
29
30
  if (e && typeof e.file === 'string' && Number.isInteger(e.line)) out.add(`${e.file}:${e.line}`);
30
31
  }
31
32
  return out;
@@ -67,6 +68,16 @@ export function strip(doc) {
67
68
  return { ...doc, nodes: (doc.nodes ?? []).map((n) => ({ ...n, narrative: null })), overview: null, tour: null };
68
69
  }
69
70
 
71
+ /**
72
+ * What a later scan of the same tree must reproduce byte for byte: the document without what the model wrote, the
73
+ * commit it was scanned at, and how often each module changed. The last two move with every commit, the map's own
74
+ * included, so a committed map is not called stale for them.
75
+ */
76
+ export function steady(doc) {
77
+ const { source, ...rest } = strip(doc);
78
+ return { ...rest, nodes: rest.nodes.map(({ commits, ...n }) => n) };
79
+ }
80
+
70
81
  /** Why a tour is not acceptable, or null when it is (null itself is: not written yet). */
71
82
  export function tourProblem(tour, doc) {
72
83
  if (tour === null || tour === undefined) return null;
@@ -115,7 +126,7 @@ export function carry(old, fresh) {
115
126
  */
116
127
  export function validate(file, fresh, serialize) {
117
128
  const out = [];
118
- const bare = (d) => { const { source, ...rest } = strip(d); return serialize(rest); };
129
+ const bare = (d) => serialize(steady(d));
119
130
  if (bare(file) !== bare(fresh)) {
120
131
  out.push({ at: 'scan', message: 'the map\'s scanner fields differ from a scan of this tree: a field was changed by hand, or the tree changed since (scan again first)' });
121
132
  }