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,252 @@
1
+ #!/usr/bin/env bash
2
+ # Session evidence — what ACTUALLY happened this session, measured from the transcript instead of recalled.
3
+ # A retrospective that asks the model what it did is interviewing the least reliable witness in the room: it
4
+ # reconstructs a tidy story from a context that has already been summarised, and the turns where it span on a
5
+ # failing approach are exactly the ones it remembers least. This reads the record instead.
6
+ #
7
+ # Consumers: the `reflect` skill runs it BEFORE writing findings, and `handoff` runs it when summarising a
8
+ # session. It is NOT wired to a hook event — nothing here is worth a per-turn token tax.
9
+ #
10
+ # Usage:
11
+ # bash session-stats.sh [transcript.jsonl] # no arg -> auto-find from pwd (same rule as context-usage.sh)
12
+ # bash session-stats.sh --raw [transcript.jsonl] # key=value lines instead of the report (tests/scripting)
13
+ # echo '{"transcript_path":"..."}' | bash session-stats.sh
14
+ #
15
+ # Thresholds (env overrides): CREW_RUNAWAY_TOOLS=15 · CREW_DUP_MIN=2 · CREW_ERR_PCT=15 · CREW_INTERRUPT_MIN=3
16
+ #
17
+ # ONE awk engine, no jq path — deliberate. context-usage.sh carries two engines because it reads a nested usage
18
+ # object, and they drifted once (the sidechain bug) with a wrong number, not silence, as the result. Every signal
19
+ # here is a flat per-line text match, so a second engine would buy nothing and add that same failure mode back.
20
+ set -uo pipefail
21
+ RAW=0
22
+ case "${1:-}" in --raw) RAW=1; shift ;; esac
23
+ TR="${1:-}"
24
+
25
+ if [ -z "$TR" ] && [ ! -t 0 ]; then
26
+ IN="$(cat 2>/dev/null || true)"
27
+ [ -n "$IN" ] && TR="$(printf '%s' "$IN" | sed -n 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
28
+ fi
29
+ # ---- CREW-TRANSCRIPT-DIR (kept byte-identical in context-usage.sh and session-stats.sh; smoke-test §6i3 pins it)
30
+ # Claude Code stores a session under $HOME/.claude/projects/<cwd encoded as a directory name>. Reproducing that
31
+ # encoding is the ONLY way a by-hand call (no hook payload on stdin) can find its own transcript.
32
+ crew_project_dirs(){ # candidate directory names for the current cwd, best first, one per line
33
+ local w p
34
+ # Windows first. Claude Code sees the NATIVE cwd (C:\repo\app) while Git Bash's `pwd` reports /c/repo/app, so
35
+ # the drive letter never matched and the by-hand call could not resolve a transcript on Windows AT ALL — the
36
+ # same "transcript not found" was reported from that machine three separate times before the cause was found.
37
+ # `pwd -W` is the MSYS builtin that returns the native form; elsewhere it just fails and is discarded.
38
+ w="$(pwd -W 2>/dev/null || true)"
39
+ # The encoder folds : \ / . AND _ to '-'. The underscore is the one that hides: a project named `report_api`
40
+ # lands in `...-report-api`, so folding only slashes and dots misses every path containing an underscore — on
41
+ # every platform, not just Windows.
42
+ for p in "$w" "$(pwd)"; do
43
+ [ -n "$p" ] && printf '%s\n' "$(printf '%s' "$p" | sed 's#[:\\/._]#-#g')"
44
+ done
45
+ # Legacy shapes, kept so a directory written by an older client still resolves.
46
+ printf '%s\n' "$(pwd | sed 's#[/.]#-#g')" "$(pwd | sed 's#/#-#g')"
47
+ }
48
+ if [ -z "$TR" ]; then
49
+ while IFS= read -r esc; do
50
+ [ -n "$esc" ] || continue
51
+ cand="$(ls -t "$HOME/.claude/projects/$esc"/*.jsonl 2>/dev/null | head -1)"
52
+ [ -n "$cand" ] && { TR="$cand"; break; }
53
+ done <<CREWEOF
54
+ $(crew_project_dirs)
55
+ CREWEOF
56
+ fi
57
+ # ---- /CREW-TRANSCRIPT-DIR
58
+ [ -n "$TR" ] && [ -f "$TR" ] || { echo "session-stats: transcript not found (pass an arg or use hook stdin)" >&2; exit 1; }
59
+
60
+ # Whole-file scan, unlike context-usage.sh's byte-bounded tail: that one runs on EVERY turn inside a hook
61
+ # timeout and only needs the last record, this one runs on demand and every count is a whole-session count.
62
+ #
63
+ # LC_ALL=C is load-bearing, not tidiness. Under a UTF-8 locale BSD awk aborts with "illegal byte sequence" the
64
+ # moment a character class meets a multi-byte character — and a transcript of a session held in Turkish is made
65
+ # of them, so the tool would die on exactly the sessions it was written for. Under C every regex is byte-wise,
66
+ # tolower() folds ASCII only (which is all the duplicate key needs), and non-ASCII bytes pass through untouched.
67
+ STATS="$(LC_ALL=C awk '
68
+ # A subagent has its own window and its own story; counting its tool calls as the main thread\x27s would make
69
+ # every delegating session look like it was spinning.
70
+ /"isSidechain": *true/ { next }
71
+
72
+ # --- compaction: the one event that silently destroys session state -------------------------------------
73
+ /"subtype": *"compact_boundary"/ {
74
+ comp++
75
+ if ($0 ~ /"trigger": *"auto"/) autocomp++
76
+ if (match($0, /"preTokens": *[0-9]+/)) { s=substr($0,RSTART,RLENGTH); gsub(/[^0-9]/,"",s); pre=s }
77
+ if (match($0, /"postTokens": *[0-9]+/)) { s=substr($0,RSTART,RLENGTH); gsub(/[^0-9]/,"",s); post=s }
78
+ next
79
+ }
80
+
81
+ # --- a REFUSED call is not a failed approach, and counting it as one is how this report cried wolf -------
82
+ # `is_error: true` is set for two different things: a tool that RAN and failed, and a call that was never
83
+ # executed because something refused it. The second is a decision ABOUT the call, not an outcome OF it, and
84
+ # the approach the model chose may have been perfectly good. Lumping them together made the report accuse the model
85
+ # of thrashing on a machine where refusals are simply more frequent — a field session produced a "runaway
86
+ # loop" warning whose counted errors were mostly denials. A warning that fires on the innocent is worse
87
+ # than none: the rule in Crewforth is that an ignored warning teaches nothing.
88
+ #
89
+ # MEASURED before it was written, over 14 real transcripts, 101 is_error results:
90
+ # 69.3% a genuine failure — the approach did not work
91
+ # 18.8% protocol: read-before-write, "string to replace not found". The tool RAN and rejected the input,
92
+ # so these stay counted: repeating one is exactly the thrash this report should see.
93
+ # 11.9% REFUSED: the harness blocked the command shape (6.9%), a kit gate blocked it (3.0%), the user
94
+ # declined (2.0%). None of them ran.
95
+ # The field report describes a much larger refusal share on a Windows project; that mix is not measured here
96
+ # and this split does not depend on which one is typical — a refusal is not a failure at any ratio.
97
+ function refusals(line, c, r) {
98
+ c = line; r = gsub(/want to proceed with this tool use/, "", c)
99
+ c = line; r += gsub(/<tool_use_error>Blocked:/, "", c)
100
+ c = line; r += gsub(/requested permissions/, "", c)
101
+ # A PreToolUse hook that blocks (exit 2) and one that errors (exit 1) both surface as "hook error" here.
102
+ # Neither one is a failing approach: the first is the gate doing its job, the second is a defect in Crewforth
103
+ # Both belong outside the error rate, and the second is visible in the gate log instead.
104
+ c = line; r += gsub(/PreToolUse:[^"]*hook error/, "", c)
105
+ return r
106
+ }
107
+
108
+ # --- interrupts: anchored on the content block, never on raw text --------------------------------------
109
+ # The bare phrase also appears inside tool inputs (a grep for it, this very comment) — matching that would
110
+ # count the session\x27s own tooling as user frustration. The `"text":"[` prefix only occurs in a real block.
111
+ {
112
+ c=$0; n=gsub(/"text": *"\[Request interrupted by user/, "", c); if (n>0) ints+=n
113
+ }
114
+
115
+ # --- assistant turns: tool calls belong to the user cycle that is currently open ------------------------
116
+ /"type": *"assistant"/ {
117
+ turns++
118
+ c=$0; cur += gsub(/"type": *"tool_use"/, "", c)
119
+ c=$0; e=gsub(/"is_error": *true/, "", c)
120
+ r=refusals($0); if (r>e) r=e # a line cannot hold more refusals than it holds errors
121
+ errs+=e-r; curerr+=e-r; refused+=r
122
+ c=$0; deleg += gsub(/"name": *"Agent"/, "", c)
123
+ next
124
+ }
125
+
126
+ # --- user records: a real prompt opens a new cycle; a tool_result is just the tail of this one ----------
127
+ /"type": *"user"/ {
128
+ c=$0; e=gsub(/"is_error": *true/, "", c)
129
+ r=refusals($0); if (r>e) r=e
130
+ errs+=e-r; curerr+=e-r; refused+=r
131
+ if ($0 ~ /"type": *"tool_result"/) next
132
+ # Not every user-role record is something a human typed. Slash-command invocations, their stdout, pasted
133
+ # terminal output, image attachments, the caveat banner and the "continued from a previous conversation"
134
+ # summary all arrive wearing the user role — and left in, they DOMINATE the duplicate report: measured on a
135
+ # real 114-prompt session, every single "repeated prompt" was one of these. The signal described the
136
+ # machinery and never the person. Two filters clear them:
137
+ # 1. `"content":"` — a typed prompt is a plain string; pastes, images and attachments come as block arrays.
138
+ # 2. a leading `<tag>` — every machinery wrapper announces itself that way (<command-name>, <bash-stdout>,
139
+ # <local-command-stdout>, <system-reminder>). A prompt that genuinely opens with a tag is lost to the
140
+ # count; that is the cheap side of the trade.
141
+ if ($0 ~ /"isCompactSummary": *true/) next
142
+ if (!match($0, /"content": *"/)) next
143
+ t=substr($0, RSTART+RLENGTH); t=substr(t,1,240); lt=tolower(t)
144
+ if (lt ~ /^<[a-z-]+>/) next
145
+ if (lt ~ /^this session is being continued from a previous conversation/) next
146
+
147
+ if (started) { close_cycle() }
148
+ started=1; cycles++; cur=0; curerr=0
149
+
150
+ # Near-duplicate key: normalise hard and keep a prefix. Punctuation and escapes go, case folds, runs of
151
+ # space collapse — but non-ASCII is LEFT ALONE, because stripping it would fold every prompt in a
152
+ # non-Latin language onto the same key and report a session of distinct questions as one repeated one.
153
+ if (t != "") {
154
+ t=lt
155
+ gsub(/\\[a-z"\\\/]/, " ", t)
156
+ gsub(/[.,;:!?()\[\]{}<>"`*_#|=+-]/, " ", t)
157
+ gsub(/ +/, " ", t); sub(/^ /, "", t)
158
+ k=substr(t,1,60)
159
+ if (length(k) >= 12) seen[k]++ # too short to judge: "ok", "devam" are not a failing prompt
160
+ }
161
+ next
162
+ }
163
+
164
+ # A high tool count alone is NOT spinning — an agentic prompt legitimately runs dozens of calls, and flagging
165
+ # that would fire on the healthiest sessions and teach the reader to skip the whole block. What separates
166
+ # working from spinning is that the calls keep FAILING, so both conditions must hold in the same cycle.
167
+ function close_cycle() {
168
+ tools += cur
169
+ if (cur > maxtools) maxtools = cur
170
+ if (cur >= RUNAWAY && curerr >= RUNERR) { runaway++; if (curerr > runworst) runworst = curerr }
171
+ }
172
+
173
+ END {
174
+ if (started) close_cycle()
175
+ for (k in seen) if (seen[k] > 1) { dupdistinct++; dupextra += seen[k]-1 }
176
+ printf "cycles=%d\nturns=%d\ntools=%d\nmaxtools=%d\nrunaway=%d\nrunaway_errors=%d\nerrors=%d\ninterrupts=%d\ndup_extra=%d\ndup_distinct=%d\ncompactions=%d\nauto_compactions=%d\npre_tokens=%d\npost_tokens=%d\ndelegations=%d\nrefused=%d\n",
177
+ cycles+0, turns+0, tools+0, maxtools+0, runaway+0, runworst+0, errs+0, ints+0, dupextra+0, dupdistinct+0, comp+0, autocomp+0, pre+0, post+0, deleg+0, refused+0
178
+ }
179
+ ' RUNAWAY="${CREW_RUNAWAY_TOOLS:-25}" RUNERR="${CREW_RUNAWAY_ERRORS:-3}" "$TR")"
180
+
181
+ # An awk that died (locale, a truncated file, a transcript format change) leaves this empty, and `set -u` would
182
+ # then fail deep inside the report with an unbound-variable trace. Say what happened instead.
183
+ case "$STATS" in *"cycles="*) ;; *) echo "session-stats: could not read the transcript ($TR)" >&2; exit 1 ;; esac
184
+
185
+ [ "$RAW" = 1 ] && { printf '%s\n' "$STATS"; exit 0; }
186
+
187
+ # shellcheck disable=SC2046 # each line is a bare key=value produced above, deliberately word-split into vars
188
+ eval "$(printf '%s\n' "$STATS" | sed 's/^/S_/')"
189
+
190
+ DUP_MIN="${CREW_DUP_MIN:-2}"; ERR_PCT="${CREW_ERR_PCT:-15}"; INT_MIN="${CREW_INTERRUPT_MIN:-3}"
191
+ AVG="$(LC_ALL=C awk -v t="$S_tools" -v c="$S_cycles" 'BEGIN{ if(c+0>0) printf "%.1f", t/c; else print "0" }')"
192
+ EPCT="$(LC_ALL=C awk -v e="$S_errors" -v t="$S_tools" 'BEGIN{ if(t+0>0) printf "%.0f", (e/t)*100; else print "0" }')"
193
+
194
+ echo "📊 Session evidence (measured from the transcript — not recalled)"
195
+ # Refusals are REPORTED but never warned about: a session that hit a gate twenty times is worth knowing and is
196
+ # not a defect, and the moment it counts toward the error rate the report starts accusing the model of the
197
+ # gate's work. Shown only when there were any, so a clean session's line does not grow a "0 refused".
198
+ if [ "${S_refused:-0}" -gt 0 ]; then
199
+ echo " $S_cycles prompt(s) · $S_tools tool call(s) · avg $AVG per prompt · ${EPCT}% tool errors · $S_refused refused (not counted as errors)"
200
+ else
201
+ echo " $S_cycles prompt(s) · $S_tools tool call(s) · avg $AVG per prompt · ${EPCT}% tool errors"
202
+ fi
203
+
204
+ # Findings only when a threshold is crossed. A clean line for every signal would bury the one that matters and
205
+ # train the reader to skim past the block — the failure mode of every dashboard that reports all-green.
206
+ FOUND=0
207
+ if [ "${S_runaway:-0}" -gt 0 ]; then
208
+ echo " ⚠️ runaway loop: $S_runaway prompt(s) ran ≥${CREW_RUNAWAY_TOOLS:-25} tool calls while failing (worst: $S_runaway_errors errors in one)."
209
+ echo " → an approach that needed that many failing attempts usually needed a different approach (systematic-debugging)."
210
+ FOUND=1
211
+ fi
212
+ if [ "${S_dup_extra:-0}" -ge "$DUP_MIN" ]; then
213
+ echo " ⚠️ repeated prompts: $S_dup_extra near-duplicate(s) across $S_dup_distinct distinct text(s)."
214
+ echo " → re-asking the same way means the context isn't landing; add the missing context instead of retrying."
215
+ FOUND=1
216
+ fi
217
+ if [ "${S_interrupts:-0}" -ge "$INT_MIN" ]; then
218
+ echo " ⚠️ interrupted $S_interrupts time(s) — the user stopped work in progress."
219
+ echo " → each one is a place the plan and the user's intent had already diverged. Name them in the retro."
220
+ FOUND=1
221
+ fi
222
+ if [ "${EPCT:-0}" -ge "$ERR_PCT" ] && [ "${S_tools:-0}" -gt 10 ]; then
223
+ echo " ⚠️ tool error rate ${EPCT}% ($S_errors/$S_tools) — above the ${ERR_PCT}% line."
224
+ echo " → repeated failing calls are usually a wrong assumption about the environment, not bad luck."
225
+ FOUND=1
226
+ fi
227
+ # Delegation. The routing map in the discipline is a RULE, and nothing enforces it — no exit code can judge a
228
+ # delegate-or-not call, so over a long session it decays quietly and the work all ends up on the main thread.
229
+ # This is the only place it becomes visible. Measured across 44 real sessions the rate varies ~9x BETWEEN
230
+ # projects while staying roughly flat within a session, so the number worth reporting is this project's own.
231
+ # Only meaningful where agents are actually installed: in a repo with none, zero is the correct answer.
232
+ if ls .claude/agents/*.md >/dev/null 2>&1 && [ "${S_turns:-0}" -ge "${CREW_DELEG_MIN_TURNS:-30}" ]; then
233
+ DRATE="$(LC_ALL=C awk -v d="$S_delegations" -v t="$S_turns" 'BEGIN{ printf "%.1f", (d/t)*100 }')"
234
+ if [ "${S_delegations:-0}" = 0 ]; then
235
+ echo " ⚠️ no delegation in $S_turns turns — every task ran on the main thread."
236
+ echo " → check the routing map: work that should have gone to an agent (audit, migration, screen) stayed inline."
237
+ FOUND=1
238
+ else
239
+ echo " · $S_delegations delegation(s) across $S_turns turns (${DRATE} per 100)."
240
+ fi
241
+ fi
242
+ if [ "${S_compactions:-0}" -gt 0 ]; then
243
+ if [ "${S_auto_compactions:-0}" -gt 0 ]; then
244
+ echo " ⚠️ $S_auto_compactions AUTO compaction(s) (of $S_compactions; ${S_pre_tokens}→${S_post_tokens} tok on the last one)."
245
+ echo " → auto-compaction drops state nobody chose to drop. Hand off at a phase boundary before it fires (handoff)."
246
+ else
247
+ echo " · $S_compactions manual compaction(s) (${S_pre_tokens}→${S_post_tokens} tok on the last) — deliberate, no state lost unannounced."
248
+ fi
249
+ FOUND=1
250
+ fi
251
+ [ "$FOUND" = 0 ] && echo " ✅ no runaway loop, repeated prompt, interrupt, error spike or auto-compaction in this session."
252
+ exit 0
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook (startup only) — when a newer kit version is published, have Claude ASK the user whether to
3
+ # update now (Update · Later · Skip this version), and never make the session wait to find out.
4
+ #
5
+ # The gap it closes: a fix only reaches a project when somebody REMEMBERS to run /crew-update. Nothing surfaces a
6
+ # release, so shipped fixes sit unused in installs that would want them.
7
+ #
8
+ # The rule that shapes every line below: **this hook performs no network I/O in the foreground.** A SessionStart
9
+ # hook blocks the session until it returns, its timeout is 60s, and the version lookup is exactly the kind of call
10
+ # that hangs behind a corporate proxy or an offline machine. That is the same class of failure as the route-hint
11
+ # fork storm (2.0.1): Crewforth spending the user's session-open budget and getting blamed on the CLI. So:
12
+ # - the foreground reads ONE cache file and exits — no curl, no npm, no subshell loop;
13
+ # - when that cache is older than a day it starts a DETACHED refresher whose result is used by the NEXT session.
14
+ # A version notice is not urgent; being one session late costs nothing, blocking costs everything.
15
+ # - if the refresher is killed (session ends first) nothing is written and the next startup simply tries again.
16
+ # The worst case is a notice that arrives late or not at all — never a hang, never a wrong version.
17
+ #
18
+ # Deliberate scope:
19
+ # - `startup` ONLY. On resume/clear/compact this would re-announce the same thing inside one session, and the
20
+ # session-start channel also carries the rehydrate and trust notices — training the user to skim it is a cost
21
+ # paid by those two, not by this one.
22
+ # - BOTH editions, each on its own channel. A project install compares `.claude/VERSION` against the npm dist-tag
23
+ # and points at `/crew-update`; a plugin install compares its own `.claude-plugin/plugin.json` against the
24
+ # marketplace repo's copy — the number that will actually reach it — and points at `claude plugin update`.
25
+ # The plugin's cache is user-level (`$XDG_CACHE_HOME`), the one case where Crewforth's "everything inside the
26
+ # repo" rule cannot apply, because a plugin install is not inside one. With both present the project install
27
+ # wins and the plugin copy stays quiet, so one release is never announced twice.
28
+ # - A QUESTION, never an action. Nothing in this file installs anything: no npx, no npm, no updater — the update
29
+ # runs only when the user picks "Update", through /crew-update (or the plugin command). A silent auto-update is
30
+ # ruled out on purpose: it rewrites the very gates Crewforth is made of (hooks, settings.json, the discipline), a
31
+ # compromised package would run itself in every project, and a team sharing .claude/ would get a diff nobody
32
+ # asked for. smoke-test.sh pins that this file runs none of those commands.
33
+ # - Asked at most once a day per version. Asking is recorded when the question goes out, so a question the user
34
+ # closes without answering counts as "Later". `--answer later|skip <version>` records the reply (Claude runs
35
+ # it, the hook cannot see the answer): "skip" silences that version for good — a newer one asks again.
36
+ # - Silent when nobody can answer: CI set, CREW_NO_UPDATE_CHECK=1, or a session Claude Code marks unattended
37
+ # (CLAUDE_CODE_SESSION_ATTENDED=0 — measured: `claude -p` sets 0, the terminal and desktop app set 1; the
38
+ # variable is undocumented, so the question text also tells Claude not to ask when nobody can answer).
39
+ # - Opt out entirely with CREW_NO_UPDATE_CHECK=1 — an outbound request nobody asked for is not acceptable in every
40
+ # environment, and the answer to that is a switch, not a justification.
41
+ #
42
+ # The version string is treated as untrusted input: it comes off the network, is filtered to [0-9A-Za-z.-], and must
43
+ # look like a release before it is ever printed into the model's context.
44
+ set -uo pipefail
45
+ # The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
46
+ _crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
47
+ [ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"; unset _crew_d
48
+
49
+ URL="${CREW_UPDATE_URL:-https://registry.npmjs.org/-/package/crewforth/dist-tags}"
50
+ # plugin-stable, not main: the marketplace installs the plugin from that branch, and only an approved release
51
+ # moves it forward. Reading main would announce a release before the plugin channel can deliver it.
52
+ PLUGIN_URL="${CREW_UPDATE_URL:-https://raw.githubusercontent.com/Crewforth/crewforth/plugin-stable/plugin/.claude-plugin/plugin.json}"
53
+ MAX_AGE="${CREW_UPDATE_MAX_AGE:-86400}" # one day between checks
54
+
55
+ # DIGITS AND DOTS, exactly three fields — nothing else survives to be printed. Deliberately stricter than semver:
56
+ # the value arrives off the network and ends up inside a MODEL's context, and `latest` is never a pre-release, so
57
+ # accepting `9.9.9-<anything>` would buy nothing and hand a compromised endpoint a sentence to write. The control
58
+ # characters go first (a newline would let one response become several lines), then the shape decides.
59
+ sane_version(){
60
+ v="${1:-}"; v="${v//[!0-9A-Za-z.-]/}" # same character class `tr -cd` kept, without the process
61
+ case "$v" in
62
+ *[!0-9.]*) return 1 ;; # letters, dashes, anything but a number
63
+ [0-9]*.[0-9]*.[0-9]*.*) return 1 ;; # four or more fields
64
+ [0-9]*.[0-9]*.[0-9]*) printf '%s' "$v" ;;
65
+ *) return 1 ;;
66
+ esac
67
+ }
68
+
69
+ # ---- detached half: fetch and cache. Runs with no terminal, no stdout, nobody waiting on it. ------------------
70
+ # Takes its target explicitly (`--refresh <state-dir> [url] [json-key]`) because the two editions ask different
71
+ # endpoints different questions: the installer edition asks npm for the `latest` dist-tag, the plugin edition asks
72
+ # the marketplace repo's own plugin.json for its `version`. Each edition therefore learns about the release on the
73
+ # channel that will actually deliver it, rather than through a number that merely tends to match.
74
+ if [ "${1:-}" = "--refresh" ]; then
75
+ ST="${2:-}"
76
+ RURL="${3:-$URL}"
77
+ RKEY="${4:-latest}"
78
+ [ -d "$ST" ] || exit 0
79
+ command -v curl >/dev/null 2>&1 || exit 0
80
+ BODY="$(curl -fsS --max-time 10 "$RURL" 2>/dev/null)" || exit 0
81
+ V="$(sane_version "$(printf '%s' "$BODY" | sed -n "s/.*\"$RKEY\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1)")" || exit 0
82
+ T="$(date +%s 2>/dev/null || echo 0)"
83
+ # Written whole or not at all: a refresher killed mid-flight must never leave a half-line the next startup reads.
84
+ TMP="$ST/.update-check.$$"
85
+ printf '%s %s\n' "$V" "$T" > "$TMP" 2>/dev/null && mv -f "$TMP" "$ST/update-check" 2>/dev/null
86
+ rm -f "$TMP" 2>/dev/null
87
+ exit 0
88
+ fi
89
+
90
+ # Where this edition keeps its state: the project install next to its hooks (.claude/.state); the plugin edition,
91
+ # which serves every project and has no repo to write into, at user level.
92
+ HERE="$(cd "$(dirname "$0")" && pwd)"
93
+ SELF="$HERE/$(basename "$0")"
94
+ IS_PLUGIN=0; [ -f "$HERE/../.claude-plugin/plugin.json" ] && IS_PLUGIN=1 # which COPY of this file is running
95
+ if [ "$IS_PLUGIN" = 1 ]; then PSTATE="${XDG_CACHE_HOME:-$HOME/.cache}/crewforth"
96
+ else PSTATE="$HERE/../.state"; fi
97
+
98
+ # ---- the reply: `--answer later|skip <version>`, run by Claude after the user chose. ---------------------------
99
+ # Validated like everything else that came near the network: a version that is not a release number is refused.
100
+ if [ "${1:-}" = "--answer" ]; then
101
+ AV="$(sane_version "${3:-}")" || { echo "session-update-check: not a version: ${3:-}" >&2; exit 2; }
102
+ mkdir -p "$PSTATE" 2>/dev/null || exit 1
103
+ case "${2:-}" in
104
+ later) printf '%s %s\n' "$AV" "$(date +%s 2>/dev/null || echo 0)" > "$PSTATE/update-asked" ;;
105
+ skip) printf '%s\n' "$AV" > "$PSTATE/update-skip" ;;
106
+ *) echo "usage: session-update-check.sh --answer later|skip <version>" >&2; exit 2 ;;
107
+ esac
108
+ exit 0
109
+ fi
110
+
111
+ # ---- foreground half: one file read, then a decision. ---------------------------------------------------------
112
+ [ -n "${CREW_NO_UPDATE_CHECK:-}" ] && exit 0
113
+ [ -n "${CI:-}" ] && exit 0 # a runner has nobody to ask
114
+ [ "${CLAUDE_CODE_SESSION_ATTENDED:-}" = 0 ] && exit 0 # claude -p / an SDK run: nobody to ask
115
+
116
+ IN=""
117
+ [ ! -t 0 ] && IN="$(cat 2>/dev/null || true)"
118
+ # `startup` only. The matcher already says so; this is the same rule in the hook itself, so a wiring that also
119
+ # matched resume, clear, compact or fork (a forked session, Claude Code 2.1.214+) cannot ask the question again.
120
+ case "$IN" in *'"source"'*) _src="${IN#*\"source\"}"; _src="${_src#*\"}"; _src="${_src%%\"*}"
121
+ [ "$_src" = startup ] || exit 0 ;; esac
122
+ ROOT="${CLAUDE_PROJECT_DIR:-}"
123
+ [ -n "$ROOT" ] || { _r="${IN#*\"cwd\"}"; [ "$_r" != "$IN" ] && { _r="${_r#*\"}"; ROOT="${_r%%\"*}"; }; }
124
+ [ -n "$ROOT" ] || ROOT="$PWD"
125
+ # Windows hands this over as a native path, and the stdin copy arrives JSON-encoded on top (`C:\\Repos\\app`).
126
+ # Undo the escaping, then fold the separators; both are no-ops on POSIX. Skipping this is how a hook resolves a
127
+ # directory that cannot exist and then reports nothing, forever.
128
+ ROOT="${ROOT//\\\\//}"; ROOT="${ROOT//\\//}"
129
+
130
+ # Which edition is asking, and therefore: what is installed, what counts as published, and how the user updates.
131
+ # A project install ALWAYS wins — when both are present the plugin copy of this hook goes quiet rather than
132
+ # asking about the same release a second time from the other direction.
133
+ CL="$ROOT/.claude"
134
+ PR_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
135
+ PR_ROOT="${PR_ROOT//\\\\//}"; PR_ROOT="${PR_ROOT//\\//}"
136
+ PJSON="$PR_ROOT/.claude-plugin/plugin.json"
137
+
138
+ # Both SessionStart hooks run at once, so "the project wins" has to be decided by each copy on its own: the
139
+ # plugin's copy steps aside whenever a project install is present, instead of asking the project's question too.
140
+ if [ "$IS_PLUGIN" = 1 ] && [ -f "$CL/VERSION" ]; then exit 0; fi
141
+ if [ -f "$CL/VERSION" ]; then
142
+ read -r _cv < "$CL/VERSION" 2>/dev/null || _cv=""; CUR="$(sane_version "$_cv")" || exit 0
143
+ STATE="$CL/.state" # repo-local: the install lives in the repo
144
+ FEED="${CREW_UPDATE_URL:-$URL}"; KEY=latest; EDITION=project
145
+ ANSWER='bash .claude/hooks/session-update-check.sh --answer'
146
+ elif [ -n "$PR_ROOT" ] && [ -f "$PJSON" ]; then
147
+ # The plugin edition has a version too — its own manifest — and the number that will actually reach it is the one
148
+ # in the marketplace repo, not npm's. They are bumped by the same release commit, but "tends to match" is not a
149
+ # source: a channel should be told about the release by the channel that delivers it.
150
+ CUR="$(sane_version "$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PJSON" 2>/dev/null | head -1)")" || exit 0
151
+ # No repo to write into (a plugin serves every project), so the cache is user-level. This is the one place
152
+ # Crewforth's "everything stays inside the repo" rule does not apply, because a plugin install is not inside one.
153
+ STATE="${XDG_CACHE_HOME:-$HOME/.cache}/crewforth"
154
+ FEED="${CREW_UPDATE_URL:-$PLUGIN_URL}"; KEY=version; EDITION=plugin
155
+ ANSWER="bash \"$PR_ROOT/hooks/session-update-check.sh\" --answer"
156
+ else
157
+ exit 0 # neither edition -> nothing to compare
158
+ fi
159
+
160
+ # The cache must be ONE line, `<version> <epoch>`, and nothing else. Reading only its first line let a second line
161
+ # ride along unchecked: `3.1.0` on line one passed, and whatever followed it was never looked at. A file that is
162
+ # not exactly that shape is ignored whole (the refresher rewrites it whole).
163
+ CACHE="$STATE/update-check"
164
+ LATEST=""; STAMP=0; _raw=""
165
+ if [ -f "$CACHE" ]; then
166
+ IFS= read -r -d '' _raw < "$CACHE" 2>/dev/null || true
167
+ _raw="${_raw%$'\n'}"
168
+ case "$_raw" in
169
+ *$'\n'*|*$'\r'*) _raw="" ;; # more than one line: not ours
170
+ esac
171
+ LATEST="${_raw%% *}"; STAMP="${_raw#* }"
172
+ [ "$LATEST" = "$_raw" ] && STAMP=""
173
+ STAMP="${STAMP//[!0-9]/}"
174
+ fi
175
+ [ -n "$STAMP" ] || STAMP=0
176
+
177
+ NOW="$(date +%s 2>/dev/null || echo 0)"
178
+ if [ "$NOW" -gt 0 ] && [ "$((NOW - STAMP))" -gt "$MAX_AGE" ] && command -v curl >/dev/null 2>&1; then
179
+ mkdir -p "$STATE" 2>/dev/null || true
180
+ # stdin/stdout/stderr all detached. An inherited stdout keeps the hook's pipe open after it exits, and a caller
181
+ # reading to EOF then waits on a curl that has nothing to do with it — a background job that blocks anyway.
182
+ if [ -d "$STATE" ]; then bash "$SELF" --refresh "$STATE" "$FEED" "$KEY" </dev/null >/dev/null 2>&1 & fi
183
+ fi
184
+
185
+ # Nothing cached yet (first run, or every refresh so far failed) -> stay silent. Never guess a version.
186
+ [ -n "$LATEST" ] || exit 0
187
+ LATEST="$(sane_version "$LATEST")" || exit 0
188
+
189
+ # Numeric field compare, one awk. Pre-release suffixes degrade to their numeric prefix (`1-rc2` -> 1), which is
190
+ # the harmless direction: it can suppress a question, never invent one.
191
+ awk -v a="$LATEST" -v b="$CUR" 'BEGIN{
192
+ split(a,x,"."); split(b,y,".");
193
+ for(i=1;i<=3;i++){ if(x[i]+0 > y[i]+0) exit 0; if(x[i]+0 < y[i]+0) exit 1 }
194
+ exit 1 }' || exit 0
195
+
196
+ # Skipped for good? Asked (or answered "later") within the last day?
197
+ if [ -f "$STATE/update-skip" ]; then read -r _sk < "$STATE/update-skip" 2>/dev/null || _sk=""; [ "${_sk//[!0-9.]/}" = "$LATEST" ] && exit 0; fi
198
+ if [ -f "$STATE/update-asked" ]; then
199
+ read -r _av _at < "$STATE/update-asked" 2>/dev/null || { _av=""; _at=0; }
200
+ _at="${_at//[!0-9]/}"; [ -n "$_at" ] || _at=0
201
+ [ "${_av//[!0-9.]/}" = "$LATEST" ] && [ "$((NOW - _at))" -lt 86400 ] && exit 0
202
+ fi
203
+ mkdir -p "$STATE" 2>/dev/null || true
204
+ printf '%s %s\n' "$LATEST" "$NOW" > "$STATE/update-asked" 2>/dev/null || true # asked = "later" until answered
205
+
206
+ # The question speaks the install's language: CREW_LANG, then kit.conf's lang=, then the locale; English otherwise.
207
+ _lg="${CREW_LANG:-}"
208
+ [ -n "$_lg" ] || { [ -f "$CL/kit.conf" ] && _lg="$(sed -n 's/^lang=//p' "$CL/kit.conf" 2>/dev/null | head -1 | tr -d '\r')"; }
209
+ if [ -z "$_lg" ]; then _loc="${LC_ALL:-}"; [ -n "$_loc" ] || _loc="${LC_MESSAGES:-}"; [ -n "$_loc" ] || _loc="${LANG:-}"
210
+ case "$_loc" in tr*|TR*) _lg=tr ;; esac; fi
211
+ MAJOR=0; [ "${LATEST%%.*}" -gt "${CUR%%.*}" ] 2>/dev/null && MAJOR=1
212
+ if [ "$_lg" = tr ]; then
213
+ Q="Crewforth v$LATEST yayında (kurulu: v$CUR). Şimdi güncelleyelim mi?"; O1="Güncelle"; O2="Sonra"; O3="Bu sürümü atla"
214
+ QM="Major sürüm: kırıcı değişiklik içerebilir; güncellemeden önce CHANGELOG'a bakın."
215
+ else
216
+ Q="Crewforth v$LATEST is out (installed: v$CUR). Update now?"; O1="Update"; O2="Later"; O3="Skip this version"
217
+ QM="Major version: it may contain breaking changes; check the CHANGELOG before updating."
218
+ fi
219
+ if [ "$EDITION" = plugin ]; then
220
+ DO="run \`claude plugin update crewforth@crewforth\`, then tell the user to restart Claude Code — the update applies on restart"
221
+ else
222
+ DO="run /crew-update now; only when it has finished, go on to the user's request"
223
+ fi
224
+
225
+ printf 'Crewforth update available: v%s is installed, v%s is published.\n' "$CUR" "$LATEST"
226
+ printf 'Before you answer the user'"'"'s first message, ask them ONE multiple-choice question with your question tool (AskUserQuestion), worded exactly:\n'
227
+ printf ' "%s"%s\n' "$Q" "$( [ "$MAJOR" = 1 ] && printf ' — %s' "$QM" )"
228
+ printf ' options: "%s" · "%s" · "%s"\n' "$O1" "$O2" "$O3"
229
+ printf -- '- "%s" -> %s.\n' "$O1" "$DO"
230
+ printf -- '- "%s", or no answer -> run `%s later %s`, then go on with their request.\n' "$O2" "$ANSWER" "$LATEST"
231
+ printf -- '- "%s" -> run `%s skip %s`, then go on with their request.\n' "$O3" "$ANSWER" "$LATEST"
232
+ # An urgent first message (an error, a broken build) is answered first: the question waits for the end of that reply.
233
+ printf 'Exception: if their first message is an error or an urgent fix, answer that first and ask this question at the end of that reply.\n'
234
+ printf 'Never update unless the user picks "%s". If this session is non-interactive (nobody can answer, e.g. claude -p), do not ask and do not mention it.\n' "$O1"
235
+ printf 'CREW_NO_UPDATE_CHECK=1 turns this check off.\n'
236
+ exit 0