crewforth 0.0.1 → 3.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +3784 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -7
  4. package/VERSION +1 -0
  5. package/adopt.sh +1670 -0
  6. package/bin/cli.js +497 -0
  7. package/kit/AGENT_TEMPLATE.md +92 -0
  8. package/kit/CLAUDE.md +228 -0
  9. package/kit/README.md +107 -0
  10. package/kit/agents/crew-backend-expert.md +95 -0
  11. package/kit/agents/crew-commit-agent.md +96 -0
  12. package/kit/agents/crew-database-expert.md +84 -0
  13. package/kit/agents/crew-devops-expert.md +82 -0
  14. package/kit/agents/crew-frontend-expert.md +85 -0
  15. package/kit/agents/crew-performance-expert.md +78 -0
  16. package/kit/agents/crew-planner.md +60 -0
  17. package/kit/agents/crew-privacy-agent.md +71 -0
  18. package/kit/agents/crew-review-agent.md +95 -0
  19. package/kit/agents/crew-security-expert.md +73 -0
  20. package/kit/agents/crew-session-manager.md +77 -0
  21. package/kit/agents/crew-test-expert.md +64 -0
  22. package/kit/eval/doctor.sh +465 -0
  23. package/kit/eval/gate-report.sh +137 -0
  24. package/kit/eval/golden-holdout.txt +53 -0
  25. package/kit/eval/golden-routing.txt +199 -0
  26. package/kit/eval/lib/crew-env.sh +14 -0
  27. package/kit/eval/lib/settings-json.awk +154 -0
  28. package/kit/eval/lib/star.sh +70 -0
  29. package/kit/eval/parser-conformance.sh +612 -0
  30. package/kit/eval/preflight.sh +186 -0
  31. package/kit/eval/routing-eval.sh +209 -0
  32. package/kit/eval/scan-skill.sh +162 -0
  33. package/kit/eval/smoke-test.sh +6693 -0
  34. package/kit/eval/update-guard.sh +81 -0
  35. package/kit/eval/utilization.sh +130 -0
  36. package/kit/hooks/board-sync.sh +106 -0
  37. package/kit/hooks/board.sh +962 -0
  38. package/kit/hooks/commit-msg +49 -0
  39. package/kit/hooks/context-usage.sh +353 -0
  40. package/kit/hooks/floor-blocklist.txt +124 -0
  41. package/kit/hooks/guard-bash.sh +1431 -0
  42. package/kit/hooks/guard-commit-scan.sh +511 -0
  43. package/kit/hooks/guard-write.sh +513 -0
  44. package/kit/hooks/pre-commit +577 -0
  45. package/kit/hooks/route-hint.sh +185 -0
  46. package/kit/hooks/secret-blocklist.txt +82 -0
  47. package/kit/hooks/session-guard.sh +149 -0
  48. package/kit/hooks/session-rehydrate.sh +41 -0
  49. package/kit/hooks/session-stats.sh +252 -0
  50. package/kit/hooks/session-update-check.sh +236 -0
  51. package/kit/hooks/skill-trust.sh +150 -0
  52. package/kit/hooks/trace-blocklist.txt +53 -0
  53. package/kit/settings.json +132 -0
  54. package/kit/skills/a11y/SKILL.md +44 -0
  55. package/kit/skills/adr/SKILL.md +74 -0
  56. package/kit/skills/api-design/SKILL.md +50 -0
  57. package/kit/skills/automode-policy/SKILL.md +91 -0
  58. package/kit/skills/automode-policy/references/policy.json +13 -0
  59. package/kit/skills/automode-policy/scripts/apply.sh +84 -0
  60. package/kit/skills/automode-policy/scripts/check.sh +102 -0
  61. package/kit/skills/backend-architecture/SKILL.md +103 -0
  62. package/kit/skills/brainstorm/SKILL.md +47 -0
  63. package/kit/skills/ci-pipeline/SKILL.md +38 -0
  64. package/kit/skills/commit-message/SKILL.md +82 -0
  65. package/kit/skills/commit-message/references/examples.md +8 -0
  66. package/kit/skills/confidence-check/SKILL.md +86 -0
  67. package/kit/skills/crew-board/SKILL.md +55 -0
  68. package/kit/skills/crew-brainstorm/SKILL.md +19 -0
  69. package/kit/skills/crew-code-review/SKILL.md +151 -0
  70. package/kit/skills/crew-code-review/references/history-search.md +19 -0
  71. package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
  72. package/kit/skills/crew-code-review/references/receiving.md +23 -0
  73. package/kit/skills/crew-doctor/SKILL.md +36 -0
  74. package/kit/skills/crew-gates/SKILL.md +25 -0
  75. package/kit/skills/crew-handoff/SKILL.md +14 -0
  76. package/kit/skills/crew-plan/SKILL.md +18 -0
  77. package/kit/skills/crew-review/SKILL.md +23 -0
  78. package/kit/skills/crew-ship/SKILL.md +19 -0
  79. package/kit/skills/crew-skill/SKILL.md +61 -0
  80. package/kit/skills/crew-studio/SKILL.md +129 -0
  81. package/kit/skills/crew-update/SKILL.md +43 -0
  82. package/kit/skills/db-migration/SKILL.md +120 -0
  83. package/kit/skills/db-migration/references/backup-restore.md +33 -0
  84. package/kit/skills/db-migration/references/raw-sql.md +30 -0
  85. package/kit/skills/db-migration/references/tool-matrix.md +35 -0
  86. package/kit/skills/dependency-audit/SKILL.md +61 -0
  87. package/kit/skills/dependency-upgrade/SKILL.md +82 -0
  88. package/kit/skills/deploy/SKILL.md +100 -0
  89. package/kit/skills/deploy/references/proxy-ssl.md +56 -0
  90. package/kit/skills/deploy/references/rollback.md +18 -0
  91. package/kit/skills/deploy/references/scripts.md +9 -0
  92. package/kit/skills/deploy/references/topology-a.md +128 -0
  93. package/kit/skills/deploy/references/topology-b.md +44 -0
  94. package/kit/skills/docs-writer/SKILL.md +53 -0
  95. package/kit/skills/docs-writer/references/coauthoring.md +34 -0
  96. package/kit/skills/eval-grader/SKILL.md +75 -0
  97. package/kit/skills/eval-grader/references/method.md +51 -0
  98. package/kit/skills/frontend/SKILL.md +56 -0
  99. package/kit/skills/frontend/references/verify-contract.md +41 -0
  100. package/kit/skills/frontend-design/SKILL.md +71 -0
  101. package/kit/skills/frontend-design/references/design-review.md +34 -0
  102. package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
  103. package/kit/skills/handoff/SKILL.md +71 -0
  104. package/kit/skills/handoff/references/cold-reader.md +51 -0
  105. package/kit/skills/i18n-integrity/SKILL.md +31 -0
  106. package/kit/skills/incident-runbook/SKILL.md +40 -0
  107. package/kit/skills/incident-runbook/references/postmortem.md +16 -0
  108. package/kit/skills/iterate/SKILL.md +40 -0
  109. package/kit/skills/mcp-builder/SKILL.md +54 -0
  110. package/kit/skills/mcp-builder/references/building.md +71 -0
  111. package/kit/skills/observability/SKILL.md +50 -0
  112. package/kit/skills/performance/SKILL.md +38 -0
  113. package/kit/skills/performance/references/bottlenecks.md +10 -0
  114. package/kit/skills/privacy-compliance/SKILL.md +75 -0
  115. package/kit/skills/red-team/SKILL.md +48 -0
  116. package/kit/skills/reflect/SKILL.md +57 -0
  117. package/kit/skills/release/SKILL.md +44 -0
  118. package/kit/skills/security-scan/SKILL.md +99 -0
  119. package/kit/skills/security-scan/references/ai-agents.md +107 -0
  120. package/kit/skills/security-scan/references/fronts.md +99 -0
  121. package/kit/skills/security-scan/references/prompting.md +23 -0
  122. package/kit/skills/security-scan/references/reporting.md +75 -0
  123. package/kit/skills/security-scan/references/verify.md +131 -0
  124. package/kit/skills/sonarqube-check/SKILL.md +108 -0
  125. package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
  126. package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
  127. package/kit/skills/spec-planning/SKILL.md +92 -0
  128. package/kit/skills/systematic-debugging/SKILL.md +57 -0
  129. package/kit/skills/systematic-debugging/references/techniques.md +44 -0
  130. package/kit/skills/teamboard/SKILL.md +88 -0
  131. package/kit/skills/teamboard/references/setup.md +37 -0
  132. package/kit/skills/testing/SKILL.md +49 -0
  133. package/kit/skills/testing/references/flaky-triage.md +51 -0
  134. package/kit/skills/threat-model/SKILL.md +57 -0
  135. package/kit/skills/threat-model/references/interview.md +51 -0
  136. package/kit/skills/threat-model/references/schema.md +42 -0
  137. package/kit/skills/token-budget/SKILL.md +51 -0
  138. package/kit/skills/trace-scan/SKILL.md +43 -0
  139. package/kit/skills/worktree/SKILL.md +40 -0
  140. package/kit/studio/README.md +266 -0
  141. package/kit/studio/ensure-node.sh +471 -0
  142. package/kit/studio/package.json +14 -0
  143. package/kit/studio/server/hooks/studio-gate.sh +169 -0
  144. package/kit/studio/server/index.js +902 -0
  145. package/kit/studio/server/lib/crew-env.js +11 -0
  146. package/kit/studio/server/lib/fleet.js +146 -0
  147. package/kit/studio/server/lib/graph.js +533 -0
  148. package/kit/studio/server/lib/instance.js +135 -0
  149. package/kit/studio/server/lib/kit-telemetry.js +182 -0
  150. package/kit/studio/server/lib/kit.js +158 -0
  151. package/kit/studio/server/lib/palette.js +118 -0
  152. package/kit/studio/server/lib/peers.js +72 -0
  153. package/kit/studio/server/lib/permissions.js +150 -0
  154. package/kit/studio/server/lib/projects.js +294 -0
  155. package/kit/studio/server/lib/roster.js +191 -0
  156. package/kit/studio/server/lib/session.js +309 -0
  157. package/kit/studio/server/lib/terminal.js +91 -0
  158. package/kit/studio/server/lib/transcript.js +100 -0
  159. package/kit/studio/web/app.js +1098 -0
  160. package/kit/studio/web/canvas.js +1179 -0
  161. package/kit/studio/web/chat.js +543 -0
  162. package/kit/studio/web/index.html +86 -0
  163. package/kit/studio/web/md.js +110 -0
  164. package/kit/studio/web/storage-migrate.js +23 -0
  165. package/kit/studio/web/style.css +1453 -0
  166. package/package.json +46 -8
  167. package/start.sh +748 -0
  168. package/bin/crewforth.js +0 -10
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env bash
2
+ # ensure-node.sh — find a Node the panel can run on, and when asked, go and get one.
3
+ #
4
+ # Every gate in Crewforth is bash and needs nothing installed. The panel is the one
5
+ # component that needs a runtime, and a machine without Node used to get a dead end:
6
+ # "install Node 18+, then come back". This script is Crewforth doing that itself.
7
+ #
8
+ # ensure-node.sh print a usable node's path on stdout; exit 1 if there is none
9
+ # ensure-node.sh --explain the same, narrating every place it looked (on stderr)
10
+ # ensure-node.sh --plan say exactly what --install would fetch and where; change nothing
11
+ # ensure-node.sh --install fetch one into ~/.claude/studio-runtime (asks first)
12
+ # ensure-node.sh --install --yes fetch without asking (for scripts and CI)
13
+ # ensure-node.sh --probe <path> exit 0 if that binary is a Node at or above the floor
14
+ #
15
+ # What it will not do: touch the system. No admin rights, no package manager, no PATH
16
+ # edit, no shell profile. The runtime lands in one directory under $HOME and is used by
17
+ # its full path. Removing that directory undoes everything this script ever did.
18
+ #
19
+ # Env: CREW_STUDIO_NODE a node binary to trust ahead of any search
20
+ # CREW_STUDIO_RUNTIME where fetched runtimes live (default ~/.claude/studio-runtime)
21
+ # CREW_STUDIO_NODE_VERSION pin a version instead of asking nodejs.org for the newest LTS
22
+ set -u
23
+ # The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
24
+ _crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
25
+ [ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"; unset _crew_d
26
+
27
+ # The floor is stated here and in eval/preflight.sh. Two copies, because preflight ships
28
+ # in editions that carry no panel — so selfcheck pins them equal instead.
29
+ FLOOR=18
30
+ DIST=https://nodejs.org/dist
31
+ # Used only when nodejs.org cannot be reached to ask which release is current.
32
+ FALLBACK_VERSION=v24.21.0
33
+
34
+ # Windows caps a full file path at 260 characters and PowerShell's Expand-Archive enforces it.
35
+ # Measured: a 141-character target failed with PathTooLongException at __generated__\google\api\…
36
+ # after 17 seconds and left nothing behind, while unzip extracted the same archive to the same
37
+ # place in 3 seconds. So the fallback is not merely slower than unzip, it is strictly weaker,
38
+ # and the difference only shows on a long path.
39
+ WIN_MAX_PATH=260
40
+ NODE_DEEPEST_ENTRY=125 # measured inside v24.21.0's zip; a deeper future release makes
41
+ # this under-warn rather than over-warn, which is the safe side
42
+ # Minus two, not one: the cap is exclusive. Measured on Windows 11 — a 133-character unpack
43
+ # directory works and 134 fails, because 134 + 1 + 125 is exactly 260 and Windows wants less.
44
+ WIN_DIR_BUDGET=$((WIN_MAX_PATH - NODE_DEEPEST_ENTRY - 2))
45
+
46
+ # Under $HOME, and deliberately so in BOTH editions — including the plugin, where writing into the
47
+ # user's ~/.claude looks at first glance like the thing we ruled out. It is not the same thing. What
48
+ # was ruled out is a plugin installing KIT FILES there: an update would leave orphans nobody owns.
49
+ # This is a runtime cache the user says yes to, self-contained, and documented as "delete it and this
50
+ # never happened". A plugin update does not touch it. The alternative is worse: a runtime inside the
51
+ # plugin directory would be thrown away and re-fetched, 36 MB at a time, on every plugin update.
52
+ RUNTIME="${CREW_STUDIO_RUNTIME:-$HOME/.claude/studio-runtime}"
53
+ EXPLAIN=0
54
+ YES=0
55
+ MODE=resolve
56
+ PROBE=""
57
+
58
+ say() { [ "$EXPLAIN" = 1 ] && printf '%s\n' "$*" >&2; return 0; }
59
+ die() { printf '%s\n' "$*" >&2; exit 1; }
60
+
61
+ # A name that resolves is not a working interpreter: the Windows Store ships a stub that
62
+ # satisfies `command -v`, prints nothing and exits 49. So run it and read the major it
63
+ # reports. The cheap tests come first so an unmatched glob never costs a process — on
64
+ # Git Bash a fork is 62-135 ms idle and around 400 ms on a busy machine, and this list is long.
65
+ works() {
66
+ [ -n "${1:-}" ] || return 1
67
+ case "$1" in
68
+ */*) [ -f "$1" ] || return 1 ;;
69
+ *) command -v "$1" >/dev/null 2>&1 || return 1 ;;
70
+ esac
71
+ v="$("$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null)" || return 1
72
+ case "$v" in ''|*[!0-9]*) return 1 ;; esac
73
+ [ "$v" -ge "$FLOOR" ]
74
+ }
75
+
76
+ # Everywhere a Node might be that a non-interactive shell would not see. A version manager
77
+ # puts its node on PATH from a login shell only, so "nvm is installed" and "this script can
78
+ # find node" are different facts, and the second one is the one that matters here.
79
+ candidates() {
80
+ [ -n "${CREW_STUDIO_NODE:-}" ] && printf '%s\n' "$CREW_STUDIO_NODE"
81
+ printf 'node\n'
82
+ # Guarded, because that pipeline costs three processes whether or not anything is there,
83
+ # and a process is not always cheap: a bare /usr/bin/true measured 880-1483 ms on two machines
84
+ # against 3-5 ms on a healthy one, and one session traced that to endpoint software inspecting every
85
+ # process creation. A directory test costs none.
86
+ if [ -d "$RUNTIME" ]; then
87
+ ls -1d "$RUNTIME"/node-v*/ 2>/dev/null | sort -r | while read -r d; do
88
+ printf '%s\n' "${d}bin/node" "${d}node.exe"
89
+ done
90
+ fi
91
+ printf '%s\n' \
92
+ "$HOME"/.nvm/versions/node/*/bin/node \
93
+ "$HOME"/.local/share/fnm/node-versions/*/installation/bin/node \
94
+ "$HOME"/.fnm/node-versions/*/installation/bin/node \
95
+ "$HOME"/.volta/tools/image/node/*/bin/node \
96
+ "$HOME"/.asdf/installs/nodejs/*/bin/node \
97
+ "$HOME"/n/bin/node \
98
+ /opt/homebrew/bin/node \
99
+ /usr/local/bin/node \
100
+ /usr/bin/node \
101
+ "${LOCALAPPDATA:-$HOME/AppData/Local}/Programs/nodejs/node.exe" \
102
+ "${ProgramFiles:-/c/Program Files}/nodejs/node.exe" \
103
+ "/c/Program Files/nodejs/node.exe" 2>/dev/null
104
+ }
105
+
106
+ resolve() {
107
+ candidates | while read -r c; do
108
+ [ -n "$c" ] || continue
109
+ if works "$c"; then printf '%s\n' "$c"; return 0; fi
110
+ done | head -1
111
+ }
112
+
113
+ # --- platform ------------------------------------------------------------------
114
+
115
+ plat_arch() {
116
+ p="" a=""
117
+ case "$(uname -s 2>/dev/null)" in
118
+ Darwin) p=darwin ;;
119
+ Linux) p=linux ;;
120
+ MINGW*|MSYS*|CYGWIN*|Windows_NT) p=win ;;
121
+ *) return 1 ;;
122
+ esac
123
+ case "$(uname -m 2>/dev/null)" in
124
+ arm64|aarch64) a=arm64 ;;
125
+ x86_64|amd64) a=x64 ;;
126
+ armv7l) a=armv7l ;;
127
+ *) return 1 ;;
128
+ esac
129
+ printf '%s %s\n' "$p" "$a"
130
+ }
131
+
132
+ fetcher() {
133
+ if command -v curl >/dev/null 2>&1; then printf 'curl\n'
134
+ elif command -v wget >/dev/null 2>&1; then printf 'wget\n'
135
+ else return 1; fi
136
+ }
137
+
138
+ get() { # get <url> <dest|->
139
+ case "$(fetcher)" in
140
+ curl) curl -fsSL --max-time 600 -o "$2" "$1" ;;
141
+ wget) wget -q -T 600 -O "$2" "$1" ;;
142
+ *) return 1 ;;
143
+ esac
144
+ }
145
+
146
+ # No checksum tool means no install. An unverified runtime is a worse answer than an honest
147
+ # refusal, so this fails closed rather than trusting the transfer.
148
+ #
149
+ # And the tool is not trusted for resolving either. A name that resolves is not a working
150
+ # program — the same lesson as the Windows Store's node stub — and a hash tool that returns
151
+ # something plausible but wrong would turn the verification step into theatre. So each
152
+ # candidate is run against an answer already known, and the first one that gets it right is
153
+ # the one used.
154
+ SHA_TOOL=""
155
+ SHA_VECTOR=ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad # sha256("abc")
156
+
157
+ sha256_with() { # sha256_with <tool> <file>
158
+ # Three of the four read stdin, and that is why they are fed that way: given a path they
159
+ # print it beside the digest, and a path is attacker-shaped input as far as a parser is
160
+ # concerned. Measured: with the file under a directory whose name is 64 hex characters,
161
+ # openssl and certutil put that path where a first-match rule finds it and the wrong value
162
+ # gets compared. From stdin there is no path in the output at all.
163
+ #
164
+ # certutil cannot be fed stdin, so it is read from the end instead: it prints the path in a
165
+ # header line before the digest, and nothing after the digest is hex.
166
+ pick=first
167
+ case "$1" in
168
+ sha256sum) out="$(sha256sum < "$2" 2>/dev/null)" ;;
169
+ shasum) out="$(shasum -a 256 < "$2" 2>/dev/null)" ;;
170
+ openssl) out="$(openssl dgst -sha256 < "$2" 2>/dev/null)" ;;
171
+ certutil) out="$(certutil -hashfile "$2" SHA256 2>/dev/null)"; pick=last ;;
172
+ *) return 1 ;;
173
+ esac
174
+ # Four tools, four shapes, and a per-tool field rule gets one of them wrong every time:
175
+ # sha256sum / shasum <hash> *file hash is field 1
176
+ # openssl 3.x SHA2-256(file)= <hash>\r hash is the LAST field, label is not "SHA256"
177
+ # certutil a header line, then <hash>\r, then a footer not a field at all
178
+ # Windows builds also append CR, and a trailing CR is enough to fail the comparison and
179
+ # silently drop a tool that works. One rule covers all four shapes AND the CR: take the
180
+ # first 64-hex token anywhere in the output. CR is not a hex character, so it ends a token
181
+ # rather than joining one — an explicit strip here would be a line no test could fail.
182
+ printf '%s' "$out" | tr 'A-Z' 'a-z' | tr -cs '0-9a-f' '\n' |
183
+ awk -v pick="$pick" '
184
+ length($0) == 64 { h = $0; if (pick == "first") { print; exit } }
185
+ END { if (pick == "last" && h != "") print h }'
186
+ }
187
+
188
+ sha256_calibrate() {
189
+ [ -n "$SHA_TOOL" ] && return 0
190
+ probe="$(mktemp 2>/dev/null || printf '%s/crew-sha-%s' "${TMPDIR:-/tmp}" "$$")" || return 1
191
+ printf abc > "$probe" || { rm -f "$probe"; return 1; }
192
+ for t in sha256sum shasum openssl certutil; do
193
+ command -v "$t" >/dev/null 2>&1 || continue
194
+ if [ "$(sha256_with "$t" "$probe")" = "$SHA_VECTOR" ]; then SHA_TOOL="$t"; break; fi
195
+ done
196
+ rm -f "$probe"
197
+ [ -n "$SHA_TOOL" ]
198
+ }
199
+
200
+ sha256_of() {
201
+ sha256_calibrate || return 1
202
+ sha256_with "$SHA_TOOL" "$1"
203
+ }
204
+
205
+ # index.tab is the same release list as index.json in a form that needs no JSON parser —
206
+ # which matters, because the machine running this is by definition the one without Node.
207
+ # Column 10 is `lts`, and `-` there means the release is not one.
208
+ newest_lts() {
209
+ [ -n "${CREW_STUDIO_NODE_VERSION:-}" ] && { printf '%s\n' "$CREW_STUDIO_NODE_VERSION"; return 0; }
210
+ tab="$(get "$DIST/index.tab" - 2>/dev/null)" || return 1
211
+ printf '%s\n' "$tab" | awk -F'\t' 'NR>1 && $10!="-" && $10!="" { print $1; exit }'
212
+ }
213
+
214
+ # Exit codes are not the answer here, so every branch is judged by what landed on disk.
215
+ # Measured on Windows 11: Info-ZIP's unzip returns 1 for "extracted, with warnings" — files
216
+ # present, status 1 — so `unzip || fallback` runs the fallback after a success and
217
+ # `unzip && ok` calls a success a failure. What the archive was meant to leave behind is the
218
+ # only thing worth checking.
219
+ extract() { # extract <archive> <into> <expected-dir-name>
220
+ case "$1" in
221
+ *.zip)
222
+ tar -xf "$1" -C "$2" 2>/dev/null # bsdtar reads zip; GNU tar, on Git Bash, does not
223
+ [ -d "$2/$3" ] && return 0
224
+
225
+ if command -v unzip >/dev/null 2>&1; then
226
+ unzip -q -o "$1" -d "$2" >/dev/null 2>&1
227
+ [ -d "$2/$3" ] && return 0
228
+ fi
229
+
230
+ if command -v powershell >/dev/null 2>&1; then
231
+ # Expand-Archive is a Windows program and reads Windows paths. Handed a POSIX one it
232
+ # exits 1 having written nothing — measured, not assumed — so convert first.
233
+ #
234
+ # -wa, not -w: -w leaves a relative path relative, and measured on Windows, a relative
235
+ # path handed to a Windows program that is not running from the same directory fails
236
+ # to find the file. That failure would arrive as "the download is broken", which is
237
+ # the wrong suspect.
238
+ pa="$1"; pb="$2"
239
+ if command -v cygpath >/dev/null 2>&1; then
240
+ pa="$(cygpath -wa "$1" 2>/dev/null || printf '%s' "$1")"
241
+ pb="$(cygpath -wa "$2" 2>/dev/null || printf '%s' "$2")"
242
+ fi
243
+ # Said BEFORE, not after. Measured on Windows 11: Expand-Archive takes 26 s on Node's
244
+ # zip where unzip takes 3 s — and this branch only runs on machines with no unzip, so
245
+ # the person who waits is the one with no alternative. A minute of silence reads as a
246
+ # hang, and by the time an explanation arrives they have already decided that.
247
+ printf 'unpacking with PowerShell (no unzip on this machine) — this can take up to a minute\n' >&2
248
+ powershell -NoProfile -Command "Expand-Archive -LiteralPath '$pa' -DestinationPath '$pb' -Force" >/dev/null 2>&1
249
+ [ -d "$2/$3" ] && return 0
250
+ fi
251
+ return 1 ;;
252
+ *)
253
+ tar -xzf "$1" -C "$2" 2>/dev/null
254
+ [ -d "$2/$3" ] && return 0
255
+ return 1 ;;
256
+ esac
257
+ }
258
+
259
+ # --- the plan ------------------------------------------------------------------
260
+
261
+ # Everything --install would do, computed without doing any of it. Kept as one function so
262
+ # --plan and --install cannot describe different downloads.
263
+ plan() {
264
+ pa="$(plat_arch)" || die "ensure-node.sh: unsupported platform $(uname -s 2>/dev/null)/$(uname -m 2>/dev/null) — nodejs.org publishes no build for it."
265
+ PLAT="${pa%% *}"; ARCH="${pa##* }"
266
+ fetcher >/dev/null || die "ensure-node.sh: neither curl nor wget is here, so nothing can be fetched. Install Node yourself: https://nodejs.org"
267
+ VERSION="$(newest_lts)" || VERSION=""
268
+ if [ -z "$VERSION" ]; then
269
+ VERSION="$FALLBACK_VERSION"
270
+ VERSION_SOURCE="pinned fallback — nodejs.org could not be reached to ask"
271
+ elif [ -n "${CREW_STUDIO_NODE_VERSION:-}" ]; then
272
+ VERSION_SOURCE="pinned by CREW_STUDIO_NODE_VERSION"
273
+ else
274
+ VERSION_SOURCE="newest LTS on nodejs.org"
275
+ fi
276
+ case "$PLAT" in win) EXT=zip ;; *) EXT=tar.gz ;; esac
277
+ ARTIFACT="node-$VERSION-$PLAT-$ARCH.$EXT"
278
+ URL="$DIST/$VERSION/$ARTIFACT"
279
+ SUMS="$DIST/$VERSION/SHASUMS256.txt"
280
+ TARGET="$RUNTIME/node-$VERSION-$PLAT-$ARCH"
281
+
282
+ # Asked here, before anything is fetched. Learning that the runtime cannot fit after a 37 MB
283
+ # download and a 17-second failure is the wrong order, and the failure names the archive when
284
+ # the archive is fine. Only when unzip is absent: unzip is not bound by the cap, so where it
285
+ # exists the chain stops there and this does not apply.
286
+ TOO_LONG=""; TOO_LONG_PATH=""
287
+ if [ "$PLAT" = win ] && ! command -v unzip >/dev/null 2>&1; then
288
+ # What to measure, and against what, have to be the same thing — this is where the two
289
+ # halves of the arithmetic were mismatched twice.
290
+ #
291
+ # NODE_DEEPEST_ENTRY is counted from the ARCHIVE ROOT, so it already contains the
292
+ # "node-<ver>-<plat>-<arch>/" segment. The object that pairs with it is therefore the
293
+ # directory the archive is expanded INTO — $RUNTIME/.tmp.<pid> — and not that directory
294
+ # plus the segment, which counts the same 22 characters twice and refuses paths that work.
295
+ # $TARGET is the wrong object for a different reason: the unpack happens under .tmp.<pid>
296
+ # and is moved afterwards, so $TARGET is shorter than what actually exists mid-install.
297
+ #
298
+ # A fixed widest tail, deliberately, and NOT the real `$$`. Using the actual pid looks
299
+ # more precise and is worse: measured on Windows, MSYS runs its own small pid space —
300
+ # three digits after a fresh boot, growing with uptime — so the accepted length would
301
+ # drift day to day on one machine, and `--plan` and `--install` are separate processes
302
+ # with different pids, so at the exact boundary they could disagree with each other about
303
+ # the same path. This check exists to give one definite answer before a 37 MB download.
304
+ # Seven digits is the widest a pid gets here; the cost is a couple of characters of
305
+ # conservatism, which is cheaper than a limit nobody can reproduce.
306
+ probe="$RUNTIME/.tmp.0000000"
307
+ if command -v cygpath >/dev/null 2>&1; then
308
+ probe="$(cygpath -wa "$probe" 2>/dev/null || printf '%s' "$probe")"
309
+ fi
310
+ # Both, because a message that names one path and reports another's length sends the
311
+ # reader to count the wrong thing — the same mismatch as the arithmetic, surviving in prose.
312
+ if [ "${#probe}" -gt "$WIN_DIR_BUDGET" ]; then TOO_LONG="${#probe}"; TOO_LONG_PATH="$probe"; fi
313
+ fi
314
+ }
315
+
316
+ human_size() {
317
+ b="$1"
318
+ case "$b" in ''|*[!0-9]*) printf 'size unknown\n'; return ;; esac
319
+ printf '%s MB\n' "$(( (b + 524288) / 1048576 ))"
320
+ }
321
+
322
+ remote_size() {
323
+ command -v curl >/dev/null 2>&1 || { printf 'unknown\n'; return; }
324
+ curl -fsSLI --max-time 30 "$1" 2>/dev/null |
325
+ awk 'tolower($1)=="content-length:"{v=$2} END{gsub(/\r/,"",v); print (v==""?"unknown":v)}'
326
+ }
327
+
328
+ # --- modes ---------------------------------------------------------------------
329
+
330
+ # Everything above is definitions, so the file can be sourced to reach one function at a
331
+ # time. The checksum refusal and the empty-search branch are only reachable on a machine
332
+ # with no Node at all, and the tests run on machines that have one.
333
+ main() {
334
+
335
+ while [ $# -gt 0 ]; do
336
+ case "$1" in
337
+ --resolve) MODE=resolve ;;
338
+ --explain) MODE=resolve; EXPLAIN=1 ;;
339
+ --plan) MODE=plan ;;
340
+ --install) MODE=install ;;
341
+ --probe) MODE=probe; shift; PROBE="${1:-}" ;;
342
+ --yes|-y) YES=1 ;;
343
+ -h|--help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
344
+ *) printf 'ensure-node.sh: unknown argument %s\n' "$1" >&2; exit 2 ;;
345
+ esac
346
+ shift
347
+ done
348
+
349
+ case "$MODE" in
350
+ probe)
351
+ works "${PROBE:-}" && exit 0 || exit 1
352
+ ;;
353
+
354
+ resolve)
355
+ say "looking for a Node $FLOOR or newer"
356
+ found="$(resolve)"
357
+ if [ -n "$found" ]; then
358
+ # A bare name is not an answer. The caller hands this string to a launcher that may not share this PATH,
359
+ # and /crew-studio tells the model "use the path it prints" -- while this printed `node`, measured. Only one
360
+ # candidate can be a name (PATH's own), and works() has already run it, so `command -v` names the very file
361
+ # that answered. If it somehow resolves to nothing, keep the name rather than lose a working runtime.
362
+ case "$found" in
363
+ */*) : ;;
364
+ *) _abs="$(command -v "$found" 2>/dev/null)"; [ -n "$_abs" ] && found="$_abs" ;;
365
+ esac
366
+ say "found: $found"
367
+ printf '%s\n' "$found"
368
+ exit 0
369
+ fi
370
+ say "nothing usable on PATH, in $RUNTIME, or in any version manager's directory"
371
+ exit 1
372
+ ;;
373
+
374
+ plan)
375
+ plan
376
+ printf 'Crewforth Studio needs Node %s+. This machine has none that runs.\n\n' "$FLOOR"
377
+ printf ' fetch %s\n' "$URL"
378
+ printf ' size %s (%s)\n' "$(human_size "$(remote_size "$URL")")" "$VERSION_SOURCE"
379
+ printf ' verify %s (refuses to install if the hash does not match)\n' "$SUMS"
380
+ printf ' unpack %s\n' "$TARGET"
381
+ if [ -n "$TOO_LONG" ]; then
382
+ printf '\n %s\n' "WILL NOT WORK HERE. The archive is expanded here first:"
383
+ printf ' %s\n' "$TOO_LONG_PATH"
384
+ printf ' %s\n' "which is $TOO_LONG characters. Windows caps a full file path at $WIN_MAX_PATH, Node's deepest"
385
+ printf ' %s\n' "entry inside the zip is $NODE_DEEPEST_ENTRY, and this machine has no unzip — only PowerShell's"
386
+ printf ' %s\n' "Expand-Archive, which enforces the cap. Point CREW_STUDIO_RUNTIME somewhere shorter, or"
387
+ printf ' %s\n' "install unzip, which is not bound by it."
388
+ fi
389
+ printf '\nNothing outside that directory is touched: no admin rights, no package manager,\n'
390
+ printf 'no PATH or profile edit. Delete it and this never happened.\n'
391
+ printf '\n do it bash %s --install\n' "$0"
392
+ printf ' or your own way, if you would rather the system owned it:\n'
393
+ printf ' Windows winget install OpenJS.NodeJS.LTS\n'
394
+ printf ' macOS brew install node\n'
395
+ printf ' Linux see https://nodejs.org/en/download/package-manager\n'
396
+ exit 0
397
+ ;;
398
+
399
+ install)
400
+ found="$(resolve)"
401
+ if [ -n "$found" ]; then printf '%s\n' "$found"; exit 0; fi
402
+ plan
403
+
404
+ [ -z "$TOO_LONG" ] ||
405
+ die "ensure-node.sh: the archive would be expanded into
406
+ $TOO_LONG_PATH
407
+ which is $TOO_LONG characters. Windows caps a full file path at $WIN_MAX_PATH, Node's deepest entry inside the zip
408
+ is $NODE_DEEPEST_ENTRY, and this machine has no unzip — only PowerShell's Expand-Archive, which enforces that cap.
409
+ Nothing was downloaded. Point CREW_STUDIO_RUNTIME somewhere shorter, or install unzip."
410
+
411
+ sha256_calibrate ||
412
+ die "ensure-node.sh: no SHA-256 tool here that returns the right answer for a known input (tried sha256sum, shasum, openssl, certutil), so a download could not be verified. Refusing to install one unchecked. Install Node yourself: https://nodejs.org"
413
+
414
+ if [ "$YES" != 1 ]; then
415
+ # `[ -r /dev/tty ]` is true on a machine that has one even when this process cannot
416
+ # open it. Opening it is the only honest test, and getting that wrong printed a
417
+ # prompt nobody could answer into a pipe.
418
+ reply=""
419
+ ask="Fetch $ARTIFACT ($(human_size "$(remote_size "$URL")")) into $RUNTIME? [y/N] "
420
+ if [ -t 0 ]; then
421
+ printf '%s' "$ask" >&2; read -r reply || reply=""
422
+ elif ( exec 3< /dev/tty ) 2>/dev/null; then
423
+ printf '%s' "$ask" >&2; read -r reply < /dev/tty || reply=""
424
+ else
425
+ die "ensure-node.sh: no terminal to ask on, and --yes was not given. See what it would do: bash $0 --plan"
426
+ fi
427
+ case "$reply" in y|Y|yes|YES) ;; *) die "ensure-node.sh: not fetching anything." ;; esac
428
+ fi
429
+
430
+ mkdir -p "$RUNTIME" || die "ensure-node.sh: cannot create $RUNTIME"
431
+ tmp="$RUNTIME/.tmp.$$"
432
+ rm -rf "$tmp"; mkdir -p "$tmp" || die "ensure-node.sh: cannot create $tmp"
433
+ trap 'rm -rf "$tmp"' EXIT INT TERM
434
+
435
+ printf 'fetching %s\n' "$URL" >&2
436
+ get "$URL" "$tmp/$ARTIFACT" || die "ensure-node.sh: download failed — $URL"
437
+ get "$SUMS" "$tmp/SHASUMS256.txt" || die "ensure-node.sh: could not fetch the checksum list, so the download cannot be verified. Nothing installed."
438
+
439
+ want="$(awk -v f="$ARTIFACT" '$2==f || $2=="*"f {print $1; exit}' "$tmp/SHASUMS256.txt")"
440
+ [ -n "$want" ] || die "ensure-node.sh: $ARTIFACT is not listed in SHASUMS256.txt. Nothing installed."
441
+ got="$(sha256_of "$tmp/$ARTIFACT")" || die "ensure-node.sh: could not hash the download. Nothing installed."
442
+ [ "$want" = "$got" ] || die "ensure-node.sh: checksum mismatch for $ARTIFACT
443
+ expected $want
444
+ got $got
445
+ Nothing installed."
446
+ printf 'checksum ok\n' >&2
447
+
448
+ src="node-$VERSION-$PLAT-$ARCH"
449
+ extract "$tmp/$ARTIFACT" "$tmp" "$src" ||
450
+ die "ensure-node.sh: unpacking $ARTIFACT did not produce $src (tried tar, unzip and powershell).
451
+ The archive matched its published checksum a moment ago, so this is the unpacker on this machine, not the download.
452
+ Nothing installed."
453
+ src="$tmp/$src"
454
+
455
+ rm -rf "$TARGET"
456
+ mkdir -p "$(dirname "$TARGET")"
457
+ mv "$src" "$TARGET" || die "ensure-node.sh: could not move the runtime into $TARGET"
458
+
459
+ bin="$TARGET/bin/node"; [ -f "$bin" ] || bin="$TARGET/node.exe"
460
+ works "$bin" || die "ensure-node.sh: unpacked $TARGET but its node does not run. Left in place so you can look at it."
461
+ printf 'installed %s\n' "$TARGET" >&2
462
+ printf '%s\n' "$bin"
463
+ exit 0
464
+ ;;
465
+ esac
466
+
467
+ }
468
+
469
+ # Sourced for a test, or run for real. `${BASH_SOURCE[0]}` is this file either way; `$0` is
470
+ # this file only when it was executed.
471
+ if [ "${BASH_SOURCE[0]:-$0}" = "$0" ]; then main "$@"; fi
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@crewforth/studio",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Visual orchestration panel for Claude Code — see which agent is running and what it is doing.",
6
+ "type": "module",
7
+ "bin": {
8
+ "crewforth-studio": "server/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
13
+ "license": "MIT"
14
+ }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse bridge: park the decision, ask the panel, answer with an exit code.
3
+ #
4
+ # The harness kills a hook that outruns its timeout and then lets the tool
5
+ # proceed — measured on this machine: a hook sleeping past a 3s limit produced
6
+ # permission_denials=0 and the command ran. So this never reaches that limit.
7
+ # It decides for itself at CREW_GATE_WAIT seconds, well inside the timeout
8
+ # configured in the settings file, and a hook the harness never has to kill
9
+ # fails CLOSED.
10
+ #
11
+ # No jq: Crewforth's hooks run on stock Git Bash, which has none. The fields
12
+ # needed here are flat strings, and parameter expansion reads them without
13
+ # spawning anything.
14
+ #
15
+ # Usage: studio-gate.sh <spool-dir>
16
+ set -u
17
+
18
+ SPOOL="${1:-}"
19
+ [ -n "$SPOOL" ] || exit 0 # misconfigured: do not stand in the way
20
+
21
+ WAIT="${CREW_GATE_WAIT:-45}"
22
+ POLL_MS=150
23
+
24
+ IN=""
25
+ # Bounded read: an open-but-silent stdin must not hang the session.
26
+ while IFS= read -r -t "${CREW_STDIN_TIMEOUT:-5}" line || [ -n "$line" ]; do
27
+ IN="$IN$line"
28
+ line=""
29
+ done
30
+
31
+ # ---- flat-string extraction, no subprocesses -------------------------------
32
+ _field() { # _field <key> -> value on stdout
33
+ local r="${IN#*\"$1\":\"}"
34
+ [ "$r" = "$IN" ] && return 1
35
+ printf '%s' "${r%%\"*}"
36
+ }
37
+
38
+ TOOL="$(_field tool_name || true)"
39
+ TUID="$(_field tool_use_id || true)"
40
+ SID="$(_field session_id || true)"
41
+ [ -n "$TOOL" ] || exit 0 # nothing to ask about
42
+
43
+ # Ids become filenames; anything unexpected is refused rather than sanitised
44
+ # into something that might escape the spool.
45
+ case "$TUID" in
46
+ ''|*[!A-Za-z0-9_-]*) TUID="unknown-$$" ;;
47
+ esac
48
+
49
+ REQ="$SPOOL/req"; ANS="$SPOOL/ans"; ALWAYS="$SPOOL/always"
50
+ mkdir -p "$REQ" "$ANS" "$ALWAYS" 2>/dev/null || exit 0
51
+
52
+ # A tool the panel already blanket-approved for this session skips the round trip.
53
+ case "$TOOL" in
54
+ *[!A-Za-z0-9_-]*) ;; # odd tool name: never treated as approved
55
+ *) [ -f "$ALWAYS/$TOOL" ] && exit 0 ;;
56
+ esac
57
+
58
+ # The request carries the whole hook payload, so the panel can show the command
59
+ # itself rather than just the tool's name.
60
+ printf '%s' "$IN" > "$REQ/$TUID.json" 2>/dev/null || exit 0
61
+
62
+ # The wait must not cost a process per poll. On Git Bash a fork is 20-50 ms, so
63
+ # a naive `date` + `sleep` loop spends seconds of pure overhead on every prompt
64
+ # and freezes the session it is meant to be guarding.
65
+ #
66
+ # deadline -> SECONDS, a shell builtin, instead of `date +%s`
67
+ # the pause -> a bounded read on a fifo held open read-write, which blocks for
68
+ # the timeout and never sees EOF; zero processes per iteration
69
+ #
70
+ # The fifo is probed rather than assumed: it is emulated on Windows and may
71
+ # return immediately. If it does, fall back to sleep at a coarser interval.
72
+ FIFO="$SPOOL/.wait.$$"
73
+
74
+ # Pausing without forking. On Git Bash a fork is 20-50 ms, so a `sleep` per poll
75
+ # spends seconds of overhead on a prompt that is supposed to feel instant.
76
+ #
77
+ # A bounded read on a fifo held open read-write blocks for its timeout and never
78
+ # sees EOF, which costs no process at all. Two things about it have to be
79
+ # measured rather than assumed:
80
+ #
81
+ # - the fifo may be emulated (Windows) and return immediately
82
+ # - bash 3.2, still the system shell on macOS, ignores a FRACTIONAL -t and
83
+ # returns instantly, turning the poll into a hot spin on a whole core
84
+ #
85
+ # So the probe runs exactly what the loop will run. Probing with a different
86
+ # timeout than the loop uses is how the first version of this passed while
87
+ # spinning 185,000 times in five seconds.
88
+ fifo_fast() { read -r -t 0.15 _ <&9 2>/dev/null || :; }
89
+ fifo_slow() { read -r -t 1 _ <&9 2>/dev/null || :; }
90
+ sleep_pause() { sleep 1; }
91
+
92
+ PAUSE=sleep_pause
93
+
94
+ # The probe costs about a second, and it answers the same question every time
95
+ # for a given machine. Measure once per spool and remember.
96
+ CACHE="$SPOOL/.pause-mode"
97
+ if [ -f "$CACHE" ]; then
98
+ read -r _cached < "$CACHE" 2>/dev/null || _cached=""
99
+ case "$_cached" in
100
+ fifo_fast|fifo_slow|sleep_pause) PAUSE="$_cached" ;;
101
+ esac
102
+ fi
103
+
104
+ _probe() { # _probe <fn> <min-ms>; 0 if it really waited
105
+ local t0 t1
106
+ t0=$SECONDS
107
+ "$1"
108
+ t1=$SECONDS
109
+ [ $(( t1 - t0 )) -ge "$2" ]
110
+ }
111
+
112
+ if [ "$PAUSE" = sleep_pause ] && [ ! -f "$CACHE" ] && mkfifo "$FIFO" 2>/dev/null; then
113
+ # `exec` with redirections and no command rebinds the SHELL's descriptors, so
114
+ # `exec 9<>"$FIFO" 2>/dev/null` silences stderr for the rest of the hook's
115
+ # life — every denial message with it. Save it, then put it back.
116
+ exec 7>&2
117
+ if exec 9<>"$FIFO" 2>/dev/null; then
118
+ exec 2>&7 7>&-
119
+ rm -f "$FIFO" 2>/dev/null # unlinked; the open descriptor keeps it alive
120
+ # Fractional first. Ten of them must add up to at least a second, or this
121
+ # shell is not honouring the fraction.
122
+ _t0=$SECONDS
123
+ fifo_fast; fifo_fast; fifo_fast; fifo_fast; fifo_fast
124
+ fifo_fast; fifo_fast; fifo_fast; fifo_fast; fifo_fast
125
+ if [ $(( SECONDS - _t0 )) -ge 1 ]; then
126
+ PAUSE=fifo_fast
127
+ elif _probe fifo_slow 1; then
128
+ PAUSE=fifo_slow # whole seconds, still no forks
129
+ fi
130
+ else
131
+ exec 2>&7 7>&-
132
+ rm -f "$FIFO" 2>/dev/null
133
+ fi
134
+ printf '%s\n' "$PAUSE" > "$CACHE" 2>/dev/null || :
135
+ fi
136
+
137
+ # A cached fifo mode still needs its descriptor in this process.
138
+ case "$PAUSE" in
139
+ fifo_*)
140
+ if [ ! -e /dev/fd/9 ]; then
141
+ if mkfifo "$FIFO" 2>/dev/null; then
142
+ exec 7>&2
143
+ exec 9<>"$FIFO" 2>/dev/null || PAUSE=sleep_pause
144
+ exec 2>&7 7>&-
145
+ rm -f "$FIFO" 2>/dev/null
146
+ else
147
+ PAUSE=sleep_pause
148
+ fi
149
+ fi
150
+ ;;
151
+ esac
152
+
153
+ START=$SECONDS
154
+ while :; do
155
+ if [ -f "$ANS/$TUID" ]; then
156
+ read -r VERDICT < "$ANS/$TUID" 2>/dev/null || VERDICT=deny
157
+ rm -f "$ANS/$TUID" "$REQ/$TUID.json" 2>/dev/null
158
+ case "$VERDICT" in
159
+ allow|always) exit 0 ;;
160
+ *) printf 'studio: denied in the panel\n' >&2; exit 2 ;;
161
+ esac
162
+ fi
163
+ [ $(( SECONDS - START )) -ge "$WAIT" ] && break
164
+ "$PAUSE"
165
+ done
166
+
167
+ rm -f "$REQ/$TUID.json" 2>/dev/null
168
+ printf 'studio: no answer within %ss — denied\n' "$WAIT" >&2
169
+ exit 2