@jjlabsio/claude-crew 0.1.33 → 0.1.35

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 (43) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +8 -7
  3. package/README.md +22 -0
  4. package/agents/code-reviewer.md +7 -0
  5. package/agents/dev.md +7 -0
  6. package/agents/explorer.md +7 -0
  7. package/agents/plan-evaluator.md +7 -0
  8. package/agents/planner.md +7 -0
  9. package/agents/pm.md +7 -0
  10. package/agents/qa.md +8 -1
  11. package/agents/researcher.md +7 -0
  12. package/agents/techlead.md +7 -0
  13. package/data/agent-contracts.json +350 -0
  14. package/data/agent-instructions/code-reviewer.md +47 -0
  15. package/data/agent-instructions/dev.md +48 -0
  16. package/data/agent-instructions/explorer.md +14 -0
  17. package/data/agent-instructions/plan-evaluator.md +68 -0
  18. package/data/agent-instructions/planner.md +73 -0
  19. package/data/agent-instructions/pm.md +47 -0
  20. package/data/agent-instructions/qa.md +65 -0
  21. package/data/agent-instructions/researcher.md +15 -0
  22. package/data/agent-instructions/techlead.md +66 -0
  23. package/hooks/enforce-delegation.mjs +51 -0
  24. package/package.json +8 -3
  25. package/scripts/crew-agent-runner.mjs +382 -0
  26. package/scripts/lib/build.mjs +213 -0
  27. package/scripts/lib/cli.mjs +30 -0
  28. package/scripts/lib/config.mjs +33 -0
  29. package/scripts/lib/contracts.mjs +146 -0
  30. package/scripts/lib/dispatch.mjs +241 -0
  31. package/scripts/lib/installHooks.mjs +136 -0
  32. package/scripts/lib/pluginRoot.mjs +10 -0
  33. package/scripts/lib/prepare.mjs +37 -0
  34. package/scripts/lib/render.mjs +138 -0
  35. package/scripts/lib/renderFollowup.mjs +51 -0
  36. package/scripts/lib/resolve.mjs +72 -0
  37. package/scripts/lib/skillDispatchContract.mjs +93 -0
  38. package/scripts/lib/validate.mjs +104 -0
  39. package/skills/crew-agent-runner/SKILL.md +113 -0
  40. package/skills/crew-dev/SKILL.md +171 -776
  41. package/skills/crew-interview/SKILL.md +137 -57
  42. package/skills/crew-plan/SKILL.md +224 -460
  43. package/skills/crew-setup/SKILL.md +32 -19
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+
4
+ import { build } from "./lib/build.mjs";
5
+ import { loadContracts } from "./lib/contracts.mjs";
6
+ import {
7
+ loadCatalog,
8
+ loadProjectConfig,
9
+ loadUserConfig
10
+ } from "./lib/config.mjs";
11
+ import { parseArgv } from "./lib/cli.mjs";
12
+ import {
13
+ dispatch,
14
+ DispatchError,
15
+ formatDispatchProviderGuardMessage
16
+ } from "./lib/dispatch.mjs";
17
+ import { installHooks } from "./lib/installHooks.mjs";
18
+ import { prepareDispatch } from "./lib/prepare.mjs";
19
+ import { renderFollowup } from "./lib/renderFollowup.mjs";
20
+ import { renderPrompt } from "./lib/render.mjs";
21
+ import { resolveRole } from "./lib/resolve.mjs";
22
+ import { validate } from "./lib/validate.mjs";
23
+
24
+ async function main(argv) {
25
+ const { positional, flags } = parseArgv(argv);
26
+ const command = positional[0];
27
+
28
+ if (positional.length !== 1) {
29
+ usage();
30
+ return 2;
31
+ }
32
+
33
+ if (command === "render") {
34
+ return renderCommand(flags);
35
+ }
36
+
37
+ if (command === "prepare") {
38
+ return prepareCommand(flags);
39
+ }
40
+
41
+ if (command === "dispatch") {
42
+ return dispatchCommand(flags);
43
+ }
44
+
45
+ if (command === "render-followup") {
46
+ return renderFollowupCommand(flags);
47
+ }
48
+
49
+ if (command === "build") {
50
+ return buildCommand(flags);
51
+ }
52
+
53
+ if (command === "validate") {
54
+ return validateCommand(flags);
55
+ }
56
+
57
+ if (command === "install-hooks") {
58
+ return installHooksCommand(flags);
59
+ }
60
+
61
+ if (command !== "resolve") {
62
+ usage();
63
+ return 2;
64
+ }
65
+
66
+ if (typeof flags.role !== "string" || flags.role.length === 0) {
67
+ console.error("Missing required --role <name>");
68
+ return 1;
69
+ }
70
+
71
+ try {
72
+ const value = resolveRole({
73
+ role: flags.role,
74
+ catalog: loadCatalog(),
75
+ userConfig: loadUserConfig(),
76
+ projectConfig: loadProjectConfig(),
77
+ contracts: loadContracts()
78
+ });
79
+
80
+ if (flags.json) {
81
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
82
+ } else {
83
+ process.stdout.write(formatTable(value));
84
+ }
85
+ return 0;
86
+ } catch (error) {
87
+ console.error(error.message);
88
+ return 1;
89
+ }
90
+ }
91
+
92
+ function prepareCommand(flags) {
93
+ if (typeof flags.role !== "string" || flags.role.length === 0) {
94
+ console.error("Missing required --role <name>");
95
+ return 1;
96
+ }
97
+
98
+ if (
99
+ typeof flags["request-file"] !== "string" ||
100
+ flags["request-file"].length === 0
101
+ ) {
102
+ console.error("Missing required --request-file <path>");
103
+ return 1;
104
+ }
105
+
106
+ try {
107
+ const contracts = loadContracts();
108
+ const resolved = resolveRole({
109
+ role: flags.role,
110
+ catalog: loadCatalog(),
111
+ userConfig: loadUserConfig(),
112
+ projectConfig: loadProjectConfig(),
113
+ contracts
114
+ });
115
+ const request = JSON.parse(readFileSync(flags["request-file"], "utf8"));
116
+ const prepared = prepareDispatch({
117
+ role: flags.role,
118
+ requestFile: flags["request-file"],
119
+ request,
120
+ resolved
121
+ });
122
+
123
+ if (flags.json) {
124
+ process.stdout.write(`${JSON.stringify(prepared, null, 2)}\n`);
125
+ } else {
126
+ process.stdout.write(formatPrepared(prepared));
127
+ }
128
+ return 0;
129
+ } catch (error) {
130
+ console.error(error.message);
131
+ return 1;
132
+ }
133
+ }
134
+
135
+ async function installHooksCommand(flags) {
136
+ if (
137
+ flags.root !== undefined &&
138
+ (typeof flags.root !== "string" || flags.root.length === 0)
139
+ ) {
140
+ console.error("Missing value for --root <path>");
141
+ return 1;
142
+ }
143
+
144
+ try {
145
+ await installHooks({ root: flags.root ?? process.cwd() });
146
+ return 0;
147
+ } catch (error) {
148
+ console.error(error.message);
149
+ return 1;
150
+ }
151
+ }
152
+
153
+ async function buildCommand(flags) {
154
+ if (
155
+ flags.root !== undefined &&
156
+ (typeof flags.root !== "string" || flags.root.length === 0)
157
+ ) {
158
+ console.error("Missing value for --root <path>");
159
+ return 1;
160
+ }
161
+
162
+ try {
163
+ await build({ root: flags.root ?? process.cwd() });
164
+ return 0;
165
+ } catch (error) {
166
+ console.error(error.message);
167
+ return 1;
168
+ }
169
+ }
170
+
171
+ async function validateCommand(flags) {
172
+ if (
173
+ flags.root !== undefined &&
174
+ (typeof flags.root !== "string" || flags.root.length === 0)
175
+ ) {
176
+ console.error("Missing value for --root <path>");
177
+ return 1;
178
+ }
179
+
180
+ try {
181
+ const result = await validate({ root: flags.root ?? process.cwd() });
182
+ if (result.ok) {
183
+ process.stdout.write("OK\n");
184
+ return 0;
185
+ }
186
+
187
+ for (const error of result.errors) {
188
+ console.error(error);
189
+ }
190
+ return 1;
191
+ } catch (error) {
192
+ console.error(error.message);
193
+ return 1;
194
+ }
195
+ }
196
+
197
+ function renderFollowupCommand(flags) {
198
+ if (
199
+ typeof flags["previous-result"] !== "string" ||
200
+ flags["previous-result"].length === 0
201
+ ) {
202
+ console.error("Missing required --previous-result <file>");
203
+ return 1;
204
+ }
205
+
206
+ if (
207
+ typeof flags["new-input"] !== "string" ||
208
+ flags["new-input"].length === 0
209
+ ) {
210
+ console.error("Missing required --new-input <file>");
211
+ return 1;
212
+ }
213
+
214
+ try {
215
+ const previousResult = JSON.parse(
216
+ readFileSync(flags["previous-result"], "utf8")
217
+ );
218
+ const newInput = readFileSync(flags["new-input"], "utf8");
219
+ process.stdout.write(renderFollowup({ previousResult, newInput }));
220
+ return 0;
221
+ } catch (error) {
222
+ console.error(error.message);
223
+ return 1;
224
+ }
225
+ }
226
+
227
+ async function dispatchCommand(flags) {
228
+ if (typeof flags.role !== "string" || flags.role.length === 0) {
229
+ console.error("Missing required --role <name>");
230
+ return 1;
231
+ }
232
+
233
+ if (
234
+ typeof flags["request-file"] !== "string" ||
235
+ flags["request-file"].length === 0
236
+ ) {
237
+ console.error("Missing required --request-file <path>");
238
+ return 1;
239
+ }
240
+
241
+ if (
242
+ flags["resume-handle"] !== undefined &&
243
+ (typeof flags["resume-handle"] !== "string" ||
244
+ flags["resume-handle"].length === 0)
245
+ ) {
246
+ console.error("Missing value for --resume-handle <thread-id>");
247
+ return 1;
248
+ }
249
+
250
+ try {
251
+ const contracts = loadContracts();
252
+ const resolved = resolveRole({
253
+ role: flags.role,
254
+ catalog: loadCatalog(),
255
+ userConfig: loadUserConfig(),
256
+ projectConfig: loadProjectConfig(),
257
+ contracts
258
+ });
259
+ if (resolved.provider !== "codex") {
260
+ console.error(
261
+ formatDispatchProviderGuardMessage(flags.role, resolved.provider)
262
+ );
263
+ return 2;
264
+ }
265
+ const request = JSON.parse(readFileSync(flags["request-file"], "utf8"));
266
+
267
+ const agentResult = await dispatch({
268
+ role: flags.role,
269
+ request,
270
+ resolved,
271
+ contract: resolved.contract,
272
+ resumeHandle: flags["resume-handle"]
273
+ });
274
+
275
+ writeDispatchResult(agentResult, flags);
276
+ if (agentResult.status === "failed") {
277
+ console.error("Companion returned failed AgentResult.");
278
+ return 1;
279
+ }
280
+ return 0;
281
+ } catch (error) {
282
+ if (error instanceof DispatchError && error.agentResult) {
283
+ writeDispatchResult(error.agentResult, flags);
284
+ }
285
+ console.error(error.message);
286
+ return error instanceof DispatchError ? error.exitCode : 1;
287
+ }
288
+ }
289
+
290
+ function renderCommand(flags) {
291
+ if (typeof flags.role !== "string" || flags.role.length === 0) {
292
+ console.error("Missing required --role <name>");
293
+ return 1;
294
+ }
295
+
296
+ if (
297
+ typeof flags["request-file"] !== "string" ||
298
+ flags["request-file"].length === 0
299
+ ) {
300
+ console.error("Missing required --request-file <path>");
301
+ return 1;
302
+ }
303
+
304
+ try {
305
+ const contracts = loadContracts();
306
+ const contract = findContract(flags.role, contracts);
307
+ if (!contract) {
308
+ throw new Error(`Unknown role: ${flags.role}`);
309
+ }
310
+
311
+ const request = JSON.parse(readFileSync(flags["request-file"], "utf8"));
312
+ process.stdout.write(
313
+ renderPrompt({
314
+ role: flags.role,
315
+ request,
316
+ contract
317
+ })
318
+ );
319
+ return 0;
320
+ } catch (error) {
321
+ console.error(error.message);
322
+ return 1;
323
+ }
324
+ }
325
+
326
+ function formatTable(value) {
327
+ const rows = [
328
+ ["role", value.role],
329
+ ["provider", value.provider],
330
+ ["model", value.model],
331
+ ["reasoning", value.reasoning ?? ""],
332
+ ["codex_sandbox", value.codex_sandbox],
333
+ ["dispatch.path", value.dispatch.path],
334
+ ["dispatch.write", String(value.dispatch.write)]
335
+ ];
336
+
337
+ const keyWidth = Math.max(...rows.map(([key]) => key.length));
338
+ return `${rows
339
+ .map(([key, val]) => `${key.padEnd(keyWidth)} ${val}`)
340
+ .join("\n")}\n`;
341
+ }
342
+
343
+ function formatPrepared(value) {
344
+ if (value.action === "dispatch") {
345
+ return `${value.command.join(" ")}\n`;
346
+ }
347
+
348
+ return value.prompt;
349
+ }
350
+
351
+ function findContract(role, contracts) {
352
+ return contracts.roles.find((contract) => contract.role === role);
353
+ }
354
+
355
+ function writeDispatchResult(agentResult, flags) {
356
+ if (flags.json) {
357
+ process.stdout.write(`${JSON.stringify(agentResult, null, 2)}\n`);
358
+ return;
359
+ }
360
+
361
+ process.stdout.write(`${agentResult.summary ?? agentResult.status}\n`);
362
+ }
363
+
364
+ function usage() {
365
+ console.error("Usage: crew-agent-runner resolve --role <name> [--json]");
366
+ console.error(" crew-agent-runner build [--root <path>]");
367
+ console.error(" crew-agent-runner validate [--root <path>]");
368
+ console.error(" crew-agent-runner install-hooks [--root <path>]");
369
+ console.error(
370
+ " crew-agent-runner prepare --role <name> --request-file <path> [--json]"
371
+ );
372
+ console.error(" crew-agent-runner render --role <name> --request-file <path>");
373
+ console.error(
374
+ " crew-agent-runner render-followup --previous-result <file> --new-input <file>"
375
+ );
376
+ console.error(
377
+ " crew-agent-runner dispatch --role <name> --request-file <path> [--json] [--resume-handle <thread-id>]"
378
+ );
379
+ }
380
+
381
+ const exitCode = await main(process.argv.slice(2));
382
+ process.exitCode = exitCode;
@@ -0,0 +1,213 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+
5
+ import { loadContracts } from "./contracts.mjs";
6
+
7
+ const DEFAULT_CONTRACTS_PATH = "data/agent-contracts.json";
8
+ const FALLBACK_CONTRACTS_PATH = "contracts.json";
9
+ const DEFAULT_CATALOG_PATH = "data/provider-catalog.json";
10
+ const FALLBACK_CATALOG_PATH = "provider-catalog.json";
11
+ const DEFAULT_INSTRUCTIONS_DIR = "data/agent-instructions";
12
+ const FALLBACK_INSTRUCTIONS_DIR = "instructions";
13
+ const DEFAULT_PLUGIN_PATH = ".claude-plugin/plugin.json";
14
+ const FALLBACK_PLUGIN_PATH = "plugin.json";
15
+
16
+ export async function build({ root = process.cwd() } = {}) {
17
+ const inputs = resolveBuildInputs(resolve(root));
18
+ const contracts = loadContracts(inputs.contractsPath);
19
+
20
+ await warnOrphanInstructions({
21
+ instructionsDir: inputs.instructionsDir,
22
+ contracts
23
+ });
24
+
25
+ const derived = await deriveBuildOutput({
26
+ root,
27
+ contracts,
28
+ instructionsDir: inputs.instructionsDir,
29
+ pluginPath: inputs.pluginPath
30
+ });
31
+
32
+ const agentsDir = join(inputs.projectRoot, "agents");
33
+ await mkdir(agentsDir, { recursive: true });
34
+
35
+ for (const [role, content] of derived.agents.entries()) {
36
+ await writeFile(join(agentsDir, `${role}.md`), content, "utf8");
37
+ }
38
+
39
+ await writeFile(inputs.pluginPath, derived.pluginJson, "utf8");
40
+ }
41
+
42
+ export async function deriveBuildOutput({
43
+ root = process.cwd(),
44
+ contracts,
45
+ instructionsDir,
46
+ pluginPath
47
+ } = {}) {
48
+ const inputs = resolveBuildInputs(resolve(root));
49
+ const resolvedContracts =
50
+ contracts ?? loadContracts(inputs.contractsPath);
51
+ const resolvedInstructionsDir = instructionsDir ?? inputs.instructionsDir;
52
+ const resolvedPluginPath = pluginPath ?? inputs.pluginPath;
53
+
54
+ const instructionsByRole = new Map();
55
+ const missingInstructions = [];
56
+ for (const contract of resolvedContracts.roles) {
57
+ const role = contract.role;
58
+ const instructionPath = join(resolvedInstructionsDir, `${role}.md`);
59
+ if (!existsSync(instructionPath)) {
60
+ missingInstructions.push(role);
61
+ continue;
62
+ }
63
+
64
+ instructionsByRole.set(role, await readFile(instructionPath, "utf8"));
65
+ }
66
+
67
+ if (missingInstructions.length > 0) {
68
+ throw new Error(
69
+ `Missing agent instructions: ${missingInstructions.join(", ")}`
70
+ );
71
+ }
72
+
73
+ const agents = new Map();
74
+ for (const contract of resolvedContracts.roles) {
75
+ const role = contract.role;
76
+
77
+ const model = contract.claudeSubagent?.model;
78
+ if (typeof model !== "string" || model.length === 0) {
79
+ throw new Error(`Missing Claude subagent model for role: ${role}`);
80
+ }
81
+
82
+ const agent = renderAgent({
83
+ contract,
84
+ model,
85
+ instructions: instructionsByRole.get(role)
86
+ });
87
+ agents.set(role, agent);
88
+ }
89
+
90
+ const plugin = JSON.parse(await readFile(resolvedPluginPath, "utf8"));
91
+ plugin.agents = resolvedContracts.roles.map(
92
+ (contract) => `./agents/${contract.role}.md`
93
+ );
94
+
95
+ return {
96
+ agents,
97
+ pluginJson: `${JSON.stringify(plugin, null, 2)}\n`
98
+ };
99
+ }
100
+
101
+ export function serializeFrontmatter({ name, model, description, tools }) {
102
+ return [
103
+ "---",
104
+ `name: ${name}`,
105
+ `model: ${model}`,
106
+ `description: ${description}`,
107
+ `tools: [${tools.join(", ")}]`,
108
+ "---"
109
+ ].join("\n");
110
+ }
111
+
112
+ function renderAgent({ contract, model, instructions }) {
113
+ const frontmatter = serializeFrontmatter({
114
+ name: contract.role,
115
+ model,
116
+ description: contract.claudeSubagent.description,
117
+ tools: contract.claudeSubagent.tools
118
+ });
119
+ const body = [renderCapability(contract), normalizeBlockBody(instructions)]
120
+ .filter(Boolean)
121
+ .join("\n\n");
122
+
123
+ return `${frontmatter}\n\n${body}\n`;
124
+ }
125
+
126
+ function renderCapability(contract) {
127
+ const tools = Array.isArray(contract.claudeSubagent?.tools)
128
+ ? contract.claudeSubagent.tools
129
+ : [];
130
+ const outputs = Array.isArray(contract.outputs) ? contract.outputs : [];
131
+
132
+ return [
133
+ "## Capability",
134
+ `workspaceAccess: ${contract.capabilities?.workspaceAccess ?? "unknown"}`,
135
+ `canAskUser: ${String(tools.includes("AskUserQuestion"))}`,
136
+ `canRequestAgent: ${String(tools.includes("Agent"))}`,
137
+ `canUseShell: ${String(tools.includes("Bash"))}`,
138
+ `canWriteCrewFiles: ${String(canWriteCrewFiles(outputs))}`
139
+ ].join("\n");
140
+ }
141
+
142
+ async function warnOrphanInstructions({ instructionsDir, contracts }) {
143
+ const roles = new Set(contracts.roles.map((contract) => contract.role));
144
+ const entries = await readdir(instructionsDir, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
147
+ continue;
148
+ }
149
+
150
+ const role = entry.name.slice(0, -".md".length);
151
+ if (!roles.has(role)) {
152
+ console.error(`Warning: instruction file has no contract role: ${role}`);
153
+ }
154
+ }
155
+ }
156
+
157
+ function resolveInput(root, primary, fallback) {
158
+ const primaryPath = join(root, primary);
159
+ if (existsSync(primaryPath)) {
160
+ return primaryPath;
161
+ }
162
+
163
+ const fallbackPath = join(root, fallback);
164
+ if (existsSync(fallbackPath)) {
165
+ return fallbackPath;
166
+ }
167
+
168
+ return primaryPath;
169
+ }
170
+
171
+ export function resolveBuildInputs(root = process.cwd()) {
172
+ const projectRoot = resolve(root);
173
+ return {
174
+ projectRoot,
175
+ contractsPath: resolveInput(
176
+ projectRoot,
177
+ DEFAULT_CONTRACTS_PATH,
178
+ FALLBACK_CONTRACTS_PATH
179
+ ),
180
+ catalogPath: resolveInput(
181
+ projectRoot,
182
+ DEFAULT_CATALOG_PATH,
183
+ FALLBACK_CATALOG_PATH
184
+ ),
185
+ instructionsDir: resolveInput(
186
+ projectRoot,
187
+ DEFAULT_INSTRUCTIONS_DIR,
188
+ FALLBACK_INSTRUCTIONS_DIR
189
+ ),
190
+ pluginPath: resolveInput(
191
+ projectRoot,
192
+ DEFAULT_PLUGIN_PATH,
193
+ FALLBACK_PLUGIN_PATH
194
+ )
195
+ };
196
+ }
197
+
198
+ function normalizeBlockBody(body) {
199
+ return String(body)
200
+ .replace(/^\uFEFF/, "")
201
+ .replace(/\r\n?/g, "\n")
202
+ .replace(/\n+$/g, "");
203
+ }
204
+
205
+ function canWriteCrewFiles(outputs) {
206
+ return outputs.some((output) => {
207
+ return (
208
+ output?.type === "artifact" &&
209
+ typeof output.target === "string" &&
210
+ output.target.startsWith(".crew/")
211
+ );
212
+ });
213
+ }
@@ -0,0 +1,30 @@
1
+ export function parseArgv(argv) {
2
+ const positional = [];
3
+ const flags = {};
4
+
5
+ for (let index = 0; index < argv.length; index += 1) {
6
+ const arg = argv[index];
7
+
8
+ if (!arg.startsWith("--") || arg === "--") {
9
+ positional.push(arg);
10
+ continue;
11
+ }
12
+
13
+ const key = arg.slice(2);
14
+ if (key.length === 0) {
15
+ positional.push(arg);
16
+ continue;
17
+ }
18
+
19
+ const next = argv[index + 1];
20
+ if (next === undefined || next.startsWith("--")) {
21
+ flags[key] = true;
22
+ continue;
23
+ }
24
+
25
+ flags[key] = next;
26
+ index += 1;
27
+ }
28
+
29
+ return { positional, flags };
30
+ }
@@ -0,0 +1,33 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+
5
+ import { pluginPath } from "./pluginRoot.mjs";
6
+
7
+ export function loadCatalog(filePath) {
8
+ return readJson(
9
+ filePath === undefined
10
+ ? pluginPath("data", "provider-catalog.json")
11
+ : resolve(process.cwd(), filePath),
12
+ true
13
+ );
14
+ }
15
+
16
+ export function loadUserConfig(filePath = join(homedir(), ".claude", "crew", "config.json")) {
17
+ return readJson(filePath, true);
18
+ }
19
+
20
+ export function loadProjectConfig(projectRoot = process.cwd()) {
21
+ return readJson(join(projectRoot, ".crew", "config.json"), true);
22
+ }
23
+
24
+ function readJson(filePath, allowMissing) {
25
+ if (!existsSync(filePath)) {
26
+ if (allowMissing) {
27
+ return {};
28
+ }
29
+ throw new Error(`File not found: ${filePath}`);
30
+ }
31
+
32
+ return JSON.parse(readFileSync(filePath, "utf8"));
33
+ }