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
package/bin/cli.js ADDED
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Runner for the Crewforth. The kit is a set of bash scripts (start.sh / adopt.sh)
4
+ // plus the kit/ payload, all bundled in this npm package. This wrapper stages the
5
+ // payload in a temp dir and runs the requested script with the user's project as the CWD, so
6
+ // the script's self-cleanup only ever removes the temp copies — never the package or the CWD.
7
+
8
+ const { spawnSync } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ const pkgDir = path.join(__dirname, '..');
14
+ const argv = process.argv.slice(2);
15
+ const sub = argv[0];
16
+
17
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
18
+ console.log(`Crewforth
19
+
20
+ Usage:
21
+ npx crewforth [init] [--private|--shared] [--yes]
22
+ Set up Crewforth in a fresh project (start.sh wizard). Every install ships everything and
23
+ is stack-agnostic: the stack is recorded per project in CLAUDE.md ## Stack.
24
+ (--dotnet was removed in 3.0; it is still accepted, warns, and installs the same setup.)
25
+ npx crewforth adopt
26
+ Hand Crewforth over onto an existing project (adopt.sh).
27
+ npx crewforth@latest update
28
+ Refresh a project that already has Crewforth. Alias of 'adopt': it reads .claude/kit.conf,
29
+ migrates a pre-3.0 .NET install (its pattern skill stays, as a project skill), and
30
+ restores anything missing. Your CLAUDE.md is never touched.
31
+
32
+ npx crewforth studio [--no-open] [--port <n>] [--selftest]
33
+ Open the Studio panel on your own Claude Code sessions — nothing is installed. Needs Node 18+.
34
+ npx crewforth add <name...> | add --list
35
+ Copy one agent or skill (and the skills an agent uses) into ./.claude — not the full install.
36
+ --list shows the catalogue; --force replaces a file that differs; --no-deps adds the agent alone.
37
+
38
+ npx crewforth --version
39
+ Print the Crewforth version and exit.
40
+
41
+ Run any of them at the root of your target project.
42
+ On Windows, run inside Git Bash for the smoothest experience (WSL works as a fallback).`);
43
+ process.exit(0);
44
+ }
45
+
46
+ // Answered here, before anything is staged. It used to fall through to start.sh, which cost a copy of the whole
47
+ // payload into a temp dir and then refused the flag as an unknown parameter.
48
+ if (sub === '--version' || sub === '-v') {
49
+ let version = 'unknown';
50
+ try { version = fs.readFileSync(path.join(pkgDir, 'VERSION'), 'utf8').split(/\r?\n/)[0].trim() || 'unknown'; } catch (_) { /* no VERSION shipped */ }
51
+ console.log(version);
52
+ process.exit(0);
53
+ }
54
+
55
+ // ================================ add ================================
56
+ // In this file, not a module beside it: the kit's own repo-bloat gate refuses any NEW file under bin/ (bin/ is build
57
+ // output in .NET and Java). Tracked files are exempt, which is how bin/cli.js itself passes.
58
+ // `add <name...>` — copy one agent or skill (and the skills an agent names) into a project, without the full
59
+ // install. Pure Node: no bash, no temp dir, nothing outside ./.claude is ever written.
60
+ //
61
+ // Paths are built from CATALOGUE names only. A name the user typed is looked up in the catalogue and never used as a
62
+ // path segment itself, so `..`, an absolute path or a separator cannot reach the file system.
63
+
64
+ // The one place the component prefix is spelled. The user types the bare name (`add security-expert`); the
65
+ // prefixed name (`crew-security-expert`) and the pre-3.0 suffixed one (`security-expert-csk`) resolve to the same entry.
66
+ const AGENT_PREFIX = 'crew-';
67
+ const LEGACY_SUFFIX = '-csk'; // 2.x names, accepted on input only
68
+ const bare = (n) => (n.startsWith(AGENT_PREFIX) ? n.slice(AGENT_PREFIX.length) : n);
69
+ const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
70
+ const RECORD = 'crewforth-added.json';
71
+
72
+ function payloadDir(pkgDir) { return path.join(pkgDir, 'kit'); }
73
+
74
+ function catalogue(pkgDir) {
75
+ const root = payloadDir(pkgDir);
76
+ const agents = fs.readdirSync(path.join(root, 'agents'))
77
+ .filter((f) => f.endsWith('.md'))
78
+ .map((f) => f.slice(0, -3))
79
+ .sort();
80
+ const skillsDir = path.join(root, 'skills');
81
+ const all = fs.readdirSync(skillsDir)
82
+ .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory() && fs.existsSync(path.join(skillsDir, d, 'SKILL.md')))
83
+ .sort();
84
+ // Since 3.0 the slash commands are skills (Claude Code merged the two); the kit marks them in frontmatter with
85
+ // `metadata: kind: command`, so they are listed — and counted — as the commands people know them as.
86
+ const isCommand = (d) => /\n {2}kind: command\r?\n/.test(fs.readFileSync(path.join(skillsDir, d, 'SKILL.md'), 'utf8').split(/\r?\n---\r?\n/)[0] + '\n'); // CRLF too
87
+ const commands = all.filter(isCommand);
88
+ const skills = all.filter((d) => !commands.includes(d));
89
+ // `metadata: experimental: true` installs and answers when named, but is not listed or counted until it is done.
90
+ const experimental = all.filter((d) => /\n {2}experimental: true\r?\n/.test(fs.readFileSync(path.join(skillsDir, d, 'SKILL.md'), 'utf8').split(/\r?\n---\r?\n/)[0] + '\n'));
91
+ return { root, agents, skills, commands, experimental };
92
+ }
93
+
94
+ // Resolve what the user typed to a catalogue entry. A typed prefix (`crew-x`) or legacy suffix (`x-csk`) is stripped
95
+ // first; then: agent `crew-<name>`, skill `<name>`, skill `crew-<name>` (the code-review skill carries the prefix).
96
+ function resolveName(cat, typed) {
97
+ if (typeof typed !== 'string' || !NAME_RE.test(typed)) return null;
98
+ let base = typed.endsWith(LEGACY_SUFFIX) ? typed.slice(0, -LEGACY_SUFFIX.length) : typed;
99
+ base = bare(base);
100
+ if (!base) return null;
101
+ if (cat.agents.includes(AGENT_PREFIX + base)) return { type: 'agent', name: AGENT_PREFIX + base };
102
+ if (cat.skills.includes(base)) return { type: 'skill', name: base };
103
+ if (cat.skills.includes(AGENT_PREFIX + base)) return { type: 'skill', name: AGENT_PREFIX + base };
104
+ if (cat.commands.includes(AGENT_PREFIX + base)) return { type: 'skill', name: AGENT_PREFIX + base }; // a command is a skill on disk
105
+ return null;
106
+ }
107
+
108
+ // THE dependency rule, and the only place it lives: every catalogue skill an agent's text names in backticks.
109
+ // Measured over the 12 agents when this was written: 1 to 11 skills (devops-expert 11, backend-expert 9); the list
110
+ // is always printed before anything is written, and --no-deps installs the agent alone.
111
+ function inferSkills(agentText, skillNames) {
112
+ const known = new Set(skillNames);
113
+ const out = [];
114
+ for (const m of agentText.matchAll(/`([a-z0-9][a-z0-9-]*)`/g)) {
115
+ if (known.has(m[1]) && !out.includes(m[1])) out.push(m[1]);
116
+ }
117
+ return out;
118
+ }
119
+
120
+ // Other agents an agent hands work to. Listed, never installed.
121
+ function pairedAgents(agentText, agentNames, self) {
122
+ return agentNames.filter((a) => a !== self && new RegExp(`(^|[^a-z0-9-])${a}([^a-z0-9-]|$)`).test(agentText));
123
+ }
124
+
125
+ // Levenshtein, for "did you mean". Names are short; the quadratic cost is irrelevant.
126
+ function distance(a, b) {
127
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
128
+ for (let j = 1; j <= b.length; j += 1) d[0][j] = j;
129
+ for (let i = 1; i <= a.length; i += 1) {
130
+ for (let j = 1; j <= b.length; j += 1) {
131
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
132
+ }
133
+ }
134
+ return d[a.length][b.length];
135
+ }
136
+ function suggest(cat, typed) {
137
+ const shown = [...cat.agents.map(bare), ...cat.skills];
138
+ const t = String(typed).toLowerCase();
139
+ return shown.map((n) => [n, distance(t, n)]).sort((x, y) => x[1] - y[1] || x[0].localeCompare(y[0])).slice(0, 3).map((x) => x[0]);
140
+ }
141
+
142
+ // First sentence of a frontmatter description (block scalar `|` or inline).
143
+ function firstSentence(file) {
144
+ let text = '';
145
+ try { text = fs.readFileSync(file, 'utf8'); } catch (_) { return ''; }
146
+ const fm = text.replace(/^\uFEFF/, '').split(/^---[ \t]*\r?$/m)[1] || '';
147
+ const lines = fm.split(/\r?\n/);
148
+ const i = lines.findIndex((l) => /^description:/.test(l));
149
+ if (i < 0) return '';
150
+ let desc = lines[i].replace(/^description:\s*\|?\s*/, '');
151
+ for (let k = i + 1; k < lines.length && /^\s+\S/.test(lines[k]); k += 1) desc += ` ${lines[k].trim()}`;
152
+ desc = desc.replace(/\*\*/g, '').trim();
153
+ const m = desc.match(/^(.+?[.!?])(\s|$)/);
154
+ return (m ? m[1] : desc).trim();
155
+ }
156
+
157
+ // Every file under a source directory, as paths relative to it. Symlinks in the payload are refused, not followed.
158
+ function listFiles(dir, rel = '') {
159
+ const out = [];
160
+ for (const e of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
161
+ const r = path.join(rel, e.name);
162
+ if (e.isSymbolicLink()) throw new Error(`refusing a symlink in the payload: ${r}`);
163
+ if (e.isDirectory()) out.push(...listFiles(dir, r));
164
+ else if (e.isFile()) out.push(r);
165
+ }
166
+ return out;
167
+ }
168
+
169
+ // Refuse to write through a symlink anywhere between the project root and the target, and anywhere outside it.
170
+ // Project-relative, always with `/` — one spelling in every message and in the record, on Windows too.
171
+ function shown(projectRoot, p) { return path.relative(projectRoot, p).split(path.sep).join('/'); }
172
+
173
+ function assertSafeTarget(projectRoot, target) {
174
+ const rel = path.relative(projectRoot, target);
175
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) throw new Error(`refusing to write outside the project: ${target}`);
176
+ let cur = projectRoot;
177
+ for (const part of rel.split(path.sep)) {
178
+ cur = path.join(cur, part);
179
+ let st;
180
+ try { st = fs.lstatSync(cur); } catch (_) { return; } // the rest does not exist yet
181
+ if (st.isSymbolicLink()) throw new Error(`refusing to write through a symlink: ${shown(projectRoot, cur)}`);
182
+ }
183
+ }
184
+
185
+ // Every existing component between the project root and a target's directory must BE a directory. Checked while
186
+ // planning, so a regular file in the way (`.claude/skills` as a file) stops the run before anything is written.
187
+ function assertDirsOrAbsent(projectRoot, dir) {
188
+ let cur = projectRoot;
189
+ for (const part of path.relative(projectRoot, dir).split(path.sep).filter(Boolean)) {
190
+ cur = path.join(cur, part);
191
+ let st;
192
+ try { st = fs.statSync(cur); } catch (_) { return; }
193
+ if (!st.isDirectory()) throw new Error(`${shown(projectRoot, cur)} exists and is not a directory`);
194
+ }
195
+ }
196
+
197
+ // The 3.0 name migration for projects that used `add` rather than the full install: every record item still named
198
+ // <x>-csk (a 2.x add) moves to its crew- name — files included — and the record follows. Same rule as the updater:
199
+ // move, never delete; if the new name already exists nothing moves and the user is told. Paths come from the record
200
+ // and are rewritten only in the component segment, then checked like every other write.
201
+ function removeEmptyDirs(dir) {
202
+ let entries;
203
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
204
+ for (const e of entries) if (e.isDirectory() && !e.isSymbolicLink()) removeEmptyDirs(path.join(dir, e.name));
205
+ try { fs.rmdirSync(dir); } catch (_) { /* not empty — something of the user's is in it */ }
206
+ }
207
+
208
+ function migrateAdded(projectRoot, cat, log, err) {
209
+ const recFile = path.join(projectRoot, '.claude', RECORD);
210
+ let rec;
211
+ try { rec = JSON.parse(fs.readFileSync(recFile, 'utf8')); } catch (_) { return 0; }
212
+ if (!rec || typeof rec !== 'object' || !Array.isArray(rec.items)) return 0;
213
+ let changed = 0;
214
+ for (const it of rec.items) {
215
+ if (!it || typeof it.name !== 'string' || !it.name.endsWith(LEGACY_SUFFIX)) continue;
216
+ const r = resolveName(cat, it.name);
217
+ if (!r || r.type !== it.type) continue;
218
+ const oldSeg = it.type === 'agent' ? `agents/${it.name}.md` : `skills/${it.name}/`;
219
+ const newSeg = it.type === 'agent' ? `agents/${r.name}.md` : `skills/${r.name}/`;
220
+ const files = Array.isArray(it.files) ? it.files : [];
221
+ const moves = files.map((f) => ({ from: f, to: String(f).replace(oldSeg, newSeg) }));
222
+ const clash = moves.filter((m) => m.to !== m.from && fs.existsSync(path.join(projectRoot, m.to)));
223
+ if (clash.length) { err(`add: ${it.name} → ${r.name}: both names exist (${clash[0].to}) — nothing moved; keep one`); continue; }
224
+ for (const m of moves) {
225
+ const src = path.join(projectRoot, m.from); const dst = path.join(projectRoot, m.to);
226
+ if (m.to === m.from || !fs.existsSync(src)) continue;
227
+ assertSafeTarget(projectRoot, src); assertSafeTarget(projectRoot, dst);
228
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
229
+ fs.renameSync(src, dst);
230
+ }
231
+ // An emptied old skill directory goes too, deepest first; a directory still holding a user's own file stays.
232
+ if (it.type === 'skill') removeEmptyDirs(path.join(projectRoot, '.claude', 'skills', it.name));
233
+ // Moved, not upgraded: the files still hold the content of the version that added them.
234
+ log(`3.0 rename: ${it.name} → ${r.name} (content from ${rec.version || '2.x'}; \`npx crewforth add ${r.name} --force\` updates it)`);
235
+ it.name = r.name; it.files = moves.map((m) => m.to); changed += 1;
236
+ }
237
+ if (changed) fs.writeFileSync(recFile, `${JSON.stringify(rec, null, 2)}\n`);
238
+ return changed;
239
+ }
240
+
241
+ function listCommand(pkgDir, log) {
242
+ const cat = catalogue(pkgDir);
243
+ const shown = (list) => list.filter((x) => !cat.experimental.includes(x));
244
+ log(`Agents (${cat.agents.length}) — npx crewforth add <name> (the crew- prefix is optional when you type it)`);
245
+ for (const a of cat.agents) log(` ${a.padEnd(24)} ${firstSentence(path.join(cat.root, 'agents', `${a}.md`))}`);
246
+ log('');
247
+ log(`Skills (${shown(cat.skills).length})`);
248
+ for (const s of shown(cat.skills)) log(` ${s.padEnd(24)} ${firstSentence(path.join(cat.root, 'skills', s, 'SKILL.md'))}`);
249
+ log('');
250
+ log(`Commands (${shown(cat.commands).length}) — you start them with /<name>`);
251
+ for (const c of shown(cat.commands)) log(` ${('/' + c).padEnd(24)} ${firstSentence(path.join(cat.root, 'skills', c, 'SKILL.md'))}`);
252
+ return 0;
253
+ }
254
+
255
+ function addCommand(pkgDir, args, opts = {}) {
256
+ const log = opts.log || ((s) => process.stdout.write(`${s}\n`));
257
+ const err = opts.err || ((s) => process.stderr.write(`${s}\n`));
258
+ const projectRoot = path.resolve(opts.cwd || process.cwd());
259
+
260
+ if (args.includes('--help') || args.includes('-h')) {
261
+ log('Usage: npx crewforth add <name...> [--force] [--no-deps] · npx crewforth add --list');
262
+ log(' Copies an agent (and the skills it names) or a skill into ./.claude. Not the full install: no hooks, no gates.');
263
+ log(' --force replaces a file that differs · --no-deps adds an agent without its skills · --list shows the catalogue');
264
+ return 0;
265
+ }
266
+ if (args.includes('--list')) return listCommand(pkgDir, log);
267
+ const force = args.includes('--force');
268
+ const noDeps = args.includes('--no-deps');
269
+ const names = args.filter((a) => !a.startsWith('-'));
270
+ const unknownFlags = args.filter((a) => a.startsWith('-') && !['--force', '--no-deps', '--list'].includes(a));
271
+ if (unknownFlags.length) { err(`add: unknown option ${unknownFlags.join(' ')}`); return 2; }
272
+ if (!names.length) { err('add: name at least one agent or skill — see `npx crewforth add --list`'); return 2; }
273
+
274
+ if (fs.existsSync(path.join(projectRoot, '.claude', 'kit.conf'))) {
275
+ log('This project has the full install; use `npx crewforth update` instead. Nothing was written.');
276
+ return 0;
277
+ }
278
+
279
+ const cat = catalogue(pkgDir);
280
+ // All or nothing: every name must resolve before anything is planned — or migrated.
281
+ const resolved = []; const unknown = [];
282
+ for (const n of names) { const r = resolveName(cat, n); if (r) resolved.push(r); else unknown.push(n); }
283
+ if (unknown.length) {
284
+ for (const u of unknown) err(`add: unknown agent or skill '${u}' — did you mean: ${suggest(cat, u).join(', ')}?`);
285
+ err('Nothing was written.');
286
+ return 2;
287
+ }
288
+ const migrated = migrateAdded(projectRoot, cat, log, err);
289
+
290
+ // Expand agents into their skills; keep a stable, de-duplicated order.
291
+ const items = []; const pairs = new Set();
292
+ const push = (it) => { if (!items.some((x) => x.type === it.type && x.name === it.name)) items.push(it); };
293
+ for (const r of resolved) {
294
+ push(r);
295
+ if (r.type !== 'agent') continue;
296
+ const text = fs.readFileSync(path.join(cat.root, 'agents', `${r.name}.md`), 'utf8');
297
+ const deps = noDeps ? [] : inferSkills(text, cat.skills);
298
+ if (deps.length) log(`${bare(r.name)} uses ${deps.length} skill(s), adding them too: ${deps.join(', ')}`);
299
+ for (const d of deps) push({ type: 'skill', name: d });
300
+ for (const p of pairedAgents(text, cat.agents, r.name)) pairs.add(p);
301
+ }
302
+ for (const it of items) if (it.type === 'agent') pairs.delete(it.name);
303
+
304
+ // Plan every file, then decide, then write — so a conflict leaves the tree exactly as it was.
305
+ const plan = [];
306
+ for (const it of items) {
307
+ const files = it.type === 'agent'
308
+ ? [{ src: path.join(cat.root, 'agents', `${it.name}.md`), dst: path.join(projectRoot, '.claude', 'agents', `${it.name}.md`) }]
309
+ : listFiles(path.join(cat.root, 'skills', it.name)).map((r) => ({
310
+ src: path.join(cat.root, 'skills', it.name, r), dst: path.join(projectRoot, '.claude', 'skills', it.name, r) }));
311
+ for (const f of files) {
312
+ assertSafeTarget(projectRoot, f.dst);
313
+ assertDirsOrAbsent(projectRoot, path.dirname(f.dst));
314
+ const body = fs.readFileSync(f.src);
315
+ let state = 'new';
316
+ if (fs.existsSync(f.dst)) state = Buffer.compare(fs.readFileSync(f.dst), body) === 0 ? 'same' : 'differs';
317
+ plan.push({ ...f, body, state, item: it });
318
+ }
319
+ }
320
+ const conflicts = plan.filter((p) => p.state === 'differs');
321
+ if (conflicts.length && !force) {
322
+ for (const c of conflicts) err(`add: ${shown(projectRoot, c.dst)} exists with different content — not overwritten (use --force to replace it)`);
323
+ err(migrated ? 'Nothing was added (only the 3.0 renames above were made).' : 'Nothing was written.');
324
+ return 1;
325
+ }
326
+
327
+ // The record is computed BEFORE anything is written, from whatever is there now — so a malformed record
328
+ // (`[]`, `null`, a string) is replaced by a well-formed one instead of failing after the files landed.
329
+ const recFile = path.join(projectRoot, '.claude', RECORD);
330
+ assertSafeTarget(projectRoot, recFile);
331
+ let prev = null;
332
+ try { prev = JSON.parse(fs.readFileSync(recFile, 'utf8')); } catch (_) { /* first add, or unreadable */ }
333
+ const rec = { version: 'unknown', items: (prev && !Array.isArray(prev) && typeof prev === 'object' && Array.isArray(prev.items)) ? prev.items : [] };
334
+ try { rec.version = fs.readFileSync(path.join(pkgDir, 'VERSION'), 'utf8').split(/\r?\n/)[0].trim() || 'unknown'; } catch (_) { /* keep 'unknown' */ }
335
+ for (const it of items) {
336
+ const files = plan.filter((p) => p.item === it).map((p) => shown(projectRoot, p.dst));
337
+ rec.items = rec.items.filter((x) => !(x && x.type === it.type && x.name === it.name));
338
+ rec.items.push({ type: it.type, name: it.name, files });
339
+ }
340
+ const recBody = `${JSON.stringify(rec, null, 2)}\n`;
341
+ let recOld = null; try { recOld = fs.readFileSync(recFile, 'utf8'); } catch (_) { /* none */ }
342
+ const writes = plan.filter((p) => p.state !== 'same');
343
+ if (recOld !== recBody) writes.push({ dst: recFile, body: Buffer.from(recBody), record: true });
344
+
345
+ // All or nothing, for real: a write that fails midway (permissions, a locked file on Windows) undoes the ones
346
+ // before it — new files are removed, replaced files get their old bytes back — and the run says nothing changed.
347
+ const done = []; const madeDirs = [];
348
+ try {
349
+ for (const w of writes) {
350
+ const before = fs.existsSync(w.dst) ? fs.readFileSync(w.dst) : null;
351
+ const made = fs.mkdirSync(path.dirname(w.dst), { recursive: true }); // the first directory it CREATED, if any
352
+ if (made) madeDirs.push(made);
353
+ fs.writeFileSync(w.dst, w.body);
354
+ done.push({ dst: w.dst, before });
355
+ }
356
+ } catch (e) {
357
+ for (const d of done.reverse()) {
358
+ try { if (d.before === null) fs.unlinkSync(d.dst); else fs.writeFileSync(d.dst, d.before); } catch (_) { /* best effort */ }
359
+ }
360
+ // Directories this run created hold only files it has just removed — take them away too, deepest first.
361
+ for (const m of madeDirs.reverse()) { try { fs.rmSync(m, { recursive: true, force: true }); } catch (_) { /* best effort */ } }
362
+ err(`add: could not write (${e.code || e.message}) — the files written so far were rolled back. Nothing was changed.`);
363
+ return 1;
364
+ }
365
+ const wrote = writes.filter((w) => !w.record).length; const same = plan.length - wrote;
366
+
367
+ for (const it of items) log(` ${it.type === 'agent' ? 'agent' : 'skill'} ${it.name}`);
368
+ log(wrote ? `${wrote} file(s) written${same ? `, ${same} already up to date` : ''}.` : `Already up to date — ${same} file(s) unchanged.`);
369
+ if (pairs.size) log(`Works well with: ${[...pairs].sort().map(bare).join(', ')} (not installed — add them the same way).`);
370
+ log("Added the agent's instructions. The gates (hooks, commit checks) come with the full install: `npx crewforth init`.");
371
+ return 0;
372
+ }
373
+ // ============================== /add ================================
374
+
375
+ // ---- studio and add: pure Node, handled BEFORE the bash probe below, so neither needs Git Bash on Windows. ----
376
+
377
+ // `studio`: run the panel straight from the package — no stage, no copy. It only READS its own directory (web/,
378
+ // hooks/); its state goes to ~/.claude/studio-runtime and os.tmpdir(), so a read-only npx cache is fine.
379
+ // IN THIS PROCESS, not as a child: with a child, anything that terminates this wrapper without a signal handler —
380
+ // every programmatic kill on Windows is TerminateProcess — would leave the panel running and holding its port.
381
+ // One process means Ctrl-C, SIGTERM and the panel's own cleanup (clearStateSync, stopping its sessions) are the
382
+ // panel's, with nothing in between. index.js starts main() only when argv[1] is its own path, so argv is set first.
383
+ if (sub === 'studio') {
384
+ const major = Number(process.versions.node.split('.')[0]);
385
+ if (major < 18) {
386
+ console.error(`crewforth studio needs Node 18 or newer (this is ${process.version}).`);
387
+ process.exit(1);
388
+ }
389
+ const rest = argv.slice(1);
390
+ const noOpen = rest.includes('--no-open');
391
+ const pass = rest.filter((a) => a !== '--no-open');
392
+ // --open is the default; not added to an offline or informational run.
393
+ if (!noOpen && !pass.some((a) => ['--open', '-o', '--selftest', '--help', '-h'].includes(a))) pass.push('--open');
394
+ const entry = path.join(pkgDir, 'kit', 'studio', 'server', 'index.js');
395
+ process.argv = [process.argv[0], entry, ...pass];
396
+ import(require('url').pathToFileURL(entry).href).catch((e) => {
397
+ console.error(`crewforth studio: ${e && e.message ? e.message : e}`);
398
+ process.exit(1);
399
+ });
400
+ return;
401
+ }
402
+
403
+ if (sub === 'add') {
404
+ let code;
405
+ try { code = addCommand(pkgDir, argv.slice(1)); } catch (e) { console.error(`crewforth add: ${e.message}`); code = 1; }
406
+ process.exit(code);
407
+ }
408
+
409
+ const isAdopt = sub === 'adopt' || sub === 'update';
410
+ const isInit = sub === 'init';
411
+ const script = isAdopt ? 'adopt.sh' : 'start.sh';
412
+ const passArgs = (isAdopt || isInit) ? argv.slice(1) : argv;
413
+
414
+ // On Windows, prefer Git Bash (MINGW: shares the Win32 namespace, accepts C:/... natively) over WSL's
415
+ // System32 bash.exe (its drvfs / 8.3 / /mnt/c handling is the fragile path). Fall back to 'bash' on PATH.
416
+ function findBash() {
417
+ if (process.platform !== 'win32') return 'bash';
418
+ const roots = [
419
+ process.env['ProgramFiles'],
420
+ process.env['ProgramW6432'],
421
+ process.env['ProgramFiles(x86)'],
422
+ process.env['LOCALAPPDATA'] && path.join(process.env['LOCALAPPDATA'], 'Programs'),
423
+ ].filter(Boolean);
424
+ for (const r of roots) {
425
+ for (const rel of ['Git\\bin\\bash.exe', 'Git\\usr\\bin\\bash.exe']) {
426
+ const p = path.join(r, rel);
427
+ try { if (fs.existsSync(p)) return p; } catch (_) { /* ignore */ }
428
+ }
429
+ }
430
+ return 'bash'; // likely WSL; the runner below converts C:/... -> /mnt/c/... for that case
431
+ }
432
+ const BASH = findBash();
433
+
434
+ // bash is required (macOS / Linux have it; on Windows: Git Bash or WSL)
435
+ const probe = spawnSync(BASH, ['-c', 'exit 0']);
436
+ if (probe.error) {
437
+ console.error('Crewforth needs bash — macOS/Linux have it; on Windows install Git Bash (git-scm.com) or use WSL.');
438
+ process.exit(1);
439
+ }
440
+
441
+ // Expand 8.3 short names (e.g. C:\Users\LONGNA~1.DEV) to their real long form before staging — WSL's drvfs
442
+ // exposes only long names, so an unexpanded short path yields a "correct-looking" /mnt/c/... that still ENOENTs.
443
+ // No-op / safe on macOS & Linux.
444
+ const realpath = (p) => { try { return fs.realpathSync.native(p); } catch (_) { return p; } };
445
+
446
+ // Stage the bundled payload in a temp dir so the script's self-cleanup is harmless.
447
+ const stage = fs.mkdtempSync(path.join(realpath(os.tmpdir()), 'crewforth-'));
448
+ try {
449
+ for (const item of [script, 'kit', 'VERSION', 'CHANGELOG.md']) { // CHANGELOG: the updater extracts what changed from it
450
+ const src = path.join(pkgDir, item);
451
+ if (fs.existsSync(src)) fs.cpSync(src, path.join(stage, item), { recursive: true });
452
+ }
453
+
454
+ let res;
455
+ if (process.platform !== 'win32') {
456
+ // macOS / Linux: run the staged script directly. cwd = the user's project so it installs there;
457
+ // $0 resolves to the stage so the payload (kit/) is found next to it. No path munging —
458
+ // a backslash is a legal Unix filename char, and rewriting it would corrupt real paths.
459
+ res = spawnSync(BASH, [path.join(stage, script), ...passArgs], {
460
+ stdio: 'inherit',
461
+ cwd: process.cwd(),
462
+ });
463
+ } else {
464
+ // Windows: forward-slash so bash's argv parsing doesn't eat '\'; then translate the Windows paths to
465
+ // the running shell's convention INSIDE bash, dispatched deterministically by shell flavour:
466
+ // Git Bash / MSYS / Cygwin -> cygpath -u (C:/... also works as-is); WSL -> wslpath -u -> /mnt/c/...
467
+ const fwd = (p) => p.replace(/\\/g, '/');
468
+ const stageFwd = fwd(realpath(stage));
469
+ const projFwd = fwd(realpath(process.cwd()));
470
+ const runner = [
471
+ 'conv(){',
472
+ // The `return` used to fire whether or not the converter WORKED, so a cygpath/wslpath that resolves and
473
+ // produces nothing yielded an empty path and the pass-through below — which the comment promises works —
474
+ // was never reached. The user then got "bash cannot read the staged script at /adopt.sh" and went hunting
475
+ // for 8.3 names and TEMP settings, for what was a path-converter failure.
476
+ ' _o=""',
477
+ ' case "$(uname -s)" in',
478
+ ' MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && _o="$(cygpath -u \"$1\" 2>/dev/null)" && [ -n "$_o" ] && { printf %s "$_o"; return; } ;;',
479
+ ' *) command -v wslpath >/dev/null 2>&1 && _o="$(wslpath -u \"$1\" 2>/dev/null)" && [ -n "$_o" ] && { printf %s "$_o"; return; } ;;',
480
+ ' esac',
481
+ ' printf %s "$1"',
482
+ '}',
483
+ 'S=$(conv "$1"); C=$(conv "$2"); shift 2',
484
+ '[ -r "$S/' + script + '" ] || { echo "crewforth: bash cannot read the staged script at $S/' + script +
485
+ ' — run inside Git Bash, or set TEMP to a long (non-8.3, ASCII) path." >&2; exit 127; }',
486
+ 'cd "$C" || { echo "crewforth: cannot enter the project directory $C" >&2; exit 1; }',
487
+ 'exec bash "$S/' + script + '" "$@"',
488
+ ].join('\n');
489
+ res = spawnSync(BASH, ['-c', runner, 'crewforth', stageFwd, projFwd, ...passArgs], {
490
+ stdio: 'inherit',
491
+ });
492
+ }
493
+
494
+ process.exitCode = res.status == null ? 1 : res.status;
495
+ } finally {
496
+ try { fs.rmSync(stage, { recursive: true, force: true }); } catch (_) { /* best effort */ }
497
+ }
@@ -0,0 +1,92 @@
1
+ # Agent Template Contract (Claude Code)
2
+
3
+ All expert agents conform to this skeleton. Canonical reference: **`crew-backend-expert`**.
4
+ Principle: **agent = thin trigger** ("who / when"), **skill = "how"**. Knowledge lives in the skill, the trigger in the agent.
5
+
6
+ ## Frontmatter (required fields)
7
+ - `name`: kebab-case, exactly matching the file name.
8
+ - `description`: Claude Code makes its delegation decision **by looking at this**. It must say two things:
9
+ (1) what it does, (2) **WHEN** it kicks in.
10
+ - **The `Trigger phrases:` line (English key phrases) sits in the BODY**, right under the frontmatter, marked by a one-line
11
+ `<!-- routing-eval reads the next line … -->`; routing-eval reads it there. Why not in the description: a skill's
12
+ description is in the always-on listing, which has a budget (1% of the context window by default), and an
13
+ overflowing listing loses descriptions and with them the keywords a match depends on; an agent's description
14
+ stays focused on WHEN to delegate, the field Claude reads. Keep the rationale here, not in every file — the
15
+ comment is loaded with the body on every invocation.
16
+ - `tools`: least-privilege principle. Read-only auditor → `Read, Grep, Glob (+Bash)`; writing expert → `+ Edit, Write`.
17
+ - `model`: cost routing (table below). If the field is absent, the main session model is inherited (inherit).
18
+
19
+ ## Body sections (fixed order)
20
+ 1. **When** — triggering context.
21
+ 2. **Expertise stance — recommended.** 3-5 **role-specific** concrete behaviors that the best in that role does differently (not a generic "be an expert"). It raises the decision/stance; the mechanical "how" stays in the skill.
22
+ 3. **How (follow its skill)** — which skill + that skill's exit points specific to this agent. The skill is the **single source of truth**; do not copy the "how" into the agent — at most a quick reminder, and on conflict the skill wins (§2 "no repetition").
23
+ 3b. **Before writing any of it — writing experts only.** Two pre-flight checks, both **model discipline**; no hook enforces either:
24
+ - `confidence-check` — the only check in Crewforth that comes BEFORE implementation. Any "no" is a stop, not a caveat.
25
+ - **A design summary, when the change carries architecture** — a new or changed data model/schema, a new or changed API contract, or 2+ domains touched. Three to five lines (which table/endpoint/integration point moves · which pattern · what the alternative was), put to the user with `AskUserQuestion` before the first line of code. Trivial single-domain work skips it: RISK decides, not size.
26
+ 4. **Coordination (cross-agent) — recommended for writing experts.** Whom this work is delegated to: security→crew-security-expert, schema→crew-database-expert, tests→crew-test-expert, messages→i18n, personal data→crew-privacy-agent, hot path/query/render/payload→crew-performance-expert, findings at closure→crew-review-agent. It turns the agent into an orchestrator; usually unnecessary for read-only auditors.
27
+ - **The read-only audits go out in parallel** — several `Agent` calls in ONE message. None of them writes product code, so there is nothing to serialise (discipline Workflow §3).
28
+ - **No unbounded ping-pong.** More than 3 handovers between the SAME two agents on one task (e.g. `crew-backend-expert` ↔ `crew-database-expert`) is a loop, not coordination: stop before the fourth, summarise what each round changed and what is still open, and put it to the user with `AskUserQuestion`. Model discipline — nothing counts the hops for you.
29
+ 5. **DoD** — closure responsibility: `/simplify` + tests green + `sonarqube-check` (0/0/0/0, build 0/0).
30
+ 6. **Output & context (token)** — what returns to the main thread: a **short summary**, not raw logs/dumps; heavy output goes to `docs/*.md` (token-budget skill).
31
+ 7. **Errors/escalation** — when stuck/unsure, **stop and report** or hand off to the relevant expert; do not proceed on a guess.
32
+ 8. **Example delegation** — 1 ✅ triggers / 1 ❌ does-not-trigger line (delegation accuracy).
33
+ 9. **Constraints** — read-only or not, what it does not do, platform/policy limits.
34
+
35
+ ## Model routing (cost calibration)
36
+ **Use a tier ALIAS, not a dated model ID** (`haiku`/`sonnet`/`opus`/`inherit`). An alias resolves automatically to the current tier; when a model is renamed/deprecated, agents do not silently break. Use a full ID (`claude-sonnet-…`) only if pinning to a specific version is required.
37
+
38
+ | Agent | Role | model | Why |
39
+ |---|---|---|---|
40
+ | crew-session-manager | assessment | `haiku` | lightweight, writes no code |
41
+ | crew-security-expert | audit | `sonnet` | decision-heavy (auth/IDOR) |
42
+ | crew-review-agent | audit | `haiku` | read-only findings |
43
+ | crew-commit-agent | message generation | `haiku` | lightweight, writes no code |
44
+ | crew-privacy-agent | audit | `sonnet` | decision-heavy (KVKK/GDPR) |
45
+ | crew-planner | planning | `inherit` | wants stable reasoning |
46
+ | crew-backend-expert | writing | `inherit` | complex code, main model |
47
+ | crew-database-expert | writing | `inherit` | migration/schema risk |
48
+ | crew-test-expert | writing | `inherit` | behavioral correctness |
49
+ | crew-frontend-expert | writing | `inherit` | UI + native bridge |
50
+
51
+ Pulling the read-only trio down to Haiku lowers token/cost; the writing experts stay at full power.
52
+ (Aliases are valid in Claude Code frontmatter; if the field is empty, `inherit` is assumed.)
53
+
54
+ ## Placement
55
+ - Project-local (10): `./.claude/agents/` — crew-session-manager, backend/database/security/test/crew-frontend-expert, crew-review-agent, crew-commit-agent, crew-planner, crew-privacy-agent. Everything stays inside the repo; no dependency on home (`~/.claude`) (handover §3).
56
+ - No extra agent is needed; stack-specific "hows" live under `./.claude/skills/` (the frontend's "how" is in the project's frontend skill / CLAUDE.md).
57
+
58
+ ## Decompose along the cost axis (tool < skill < subagent)
59
+ A monolithic prompt is a smell. Move each responsibility to the **cheapest primitive that suffices**:
60
+
61
+ ```
62
+ cheaper, weaker ◀──────────────────────────────▶ more power, more cost
63
+ TOOL / code-exec SKILL SUBAGENT
64
+ one call, stateless, instructions read its own context window
65
+ deterministic on demand & its own goal
66
+ ```
67
+
68
+ Smell tests:
69
+ - A tool that dumps **>2k tokens** into context → replace it with **code execution** over the data (compute over context, not a data dump).
70
+ - Writing **"always do X before Y"** into an agent prompt → that belongs in a **skill**, not copied prose.
71
+ - A **subagent whose output is one number/line** → it shouldn't be a subagent; inline it. Delegate for *isolation*, not by default (see `token-budget`).
72
+
73
+ **Typed contract between stages.** A prose handoff drops data — a confidence number gets lost when the orchestrator
74
+ re-parses a paragraph. Require a **typed contract** at every stage boundary (e.g. `{value, confidence, method,
75
+ flags}`) and **validate-and-clamp** it before trusting it (known fields only, enums constrained, list lengths
76
+ capped). *Anchor the number, not just the narrative.*
77
+
78
+ ## Test-first (add the eval before the skill/agent)
79
+ Treat a new skill/agent like code under TDD: write the checkable expectation **first**, watch it fail, then build
80
+ until it passes. Crewforth's evals ARE those tests.
81
+ 1. **Golden routing** — add a line to `eval/golden-routing.txt` (`<a realistic prompt>|<this target>`) *before* writing
82
+ the skill. Run `routing-eval.sh`: it FAILS (target missing / no trigger). That failure defines the trigger phrases
83
+ you must choose — you're designing the description against a concrete prompt, not guessing.
84
+ 2. **Negative guard** — if a trigger risks over-firing (a generic word like "design", "review", "model"), add a
85
+ `<prompt>|!<target>` line so an over-broad trigger fails the eval. This is how a trim stays trimmed.
86
+ 3. **Budget & spec** — `smoke-test.sh` gates name==dir, description ≤1024, and the always-on byte budget; a verbose
87
+ description fails the suite rather than quietly taxing every session. Add the Turkish summary line
88
+ (`packaging/skill-summaries.tr.tsv`; agents and commands have their own `.tr.tsv`) — the site build fails without it.
89
+ 4. **Only then** write `SKILL.md` (+ `references/` for depth) until all three go green. Red → green, never green-by-assertion-weakening (that's the Verifier-integrity anti-pattern the review skill itself flags).
90
+
91
+ ## Reference example
92
+ `crew-backend-expert.md` is this contract applied verbatim; when creating a new agent, copy it and fill it in.