@jjlabsio/claude-crew 0.1.32 → 0.1.34

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 (40) 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/package.json +8 -3
  24. package/scripts/crew-agent-runner.mjs +323 -0
  25. package/scripts/lib/build.mjs +213 -0
  26. package/scripts/lib/cli.mjs +30 -0
  27. package/scripts/lib/config.mjs +33 -0
  28. package/scripts/lib/contracts.mjs +146 -0
  29. package/scripts/lib/dispatch.mjs +241 -0
  30. package/scripts/lib/installHooks.mjs +136 -0
  31. package/scripts/lib/pluginRoot.mjs +10 -0
  32. package/scripts/lib/render.mjs +110 -0
  33. package/scripts/lib/renderFollowup.mjs +51 -0
  34. package/scripts/lib/resolve.mjs +72 -0
  35. package/scripts/lib/validate.mjs +100 -0
  36. package/skills/crew-agent-runner/SKILL.md +115 -0
  37. package/skills/crew-dev/SKILL.md +162 -780
  38. package/skills/crew-interview/SKILL.md +135 -44
  39. package/skills/crew-plan/SKILL.md +217 -414
  40. package/skills/crew-setup/SKILL.md +32 -19
@@ -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
+ }
@@ -0,0 +1,146 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { pluginPath } from "./pluginRoot.mjs";
5
+
6
+ const WORKSPACE_ACCESS_VALUES = new Set(["read-only", "workspace-write"]);
7
+
8
+ export function loadContracts(filePath) {
9
+ const resolvedPath =
10
+ filePath === undefined
11
+ ? pluginPath("data", "agent-contracts.json")
12
+ : resolve(process.cwd(), filePath);
13
+ const contracts = JSON.parse(readFileSync(resolvedPath, "utf8"));
14
+ validateContracts(contracts);
15
+ return contracts;
16
+ }
17
+
18
+ export function validateContracts(obj) {
19
+ const diagnostics = [];
20
+
21
+ if (!isPlainObject(obj)) {
22
+ throw new Error("Invalid agent contracts:\n- <root>: expected object");
23
+ }
24
+
25
+ if (typeof obj.version !== "number") {
26
+ diagnostics.push("<root>: version must be a number");
27
+ }
28
+
29
+ if (!Array.isArray(obj.roles)) {
30
+ diagnostics.push("<root>: roles must be an array");
31
+ throwContractsError(diagnostics);
32
+ }
33
+
34
+ if (obj.roles.length === 0) {
35
+ diagnostics.push("<root>: roles must not be empty");
36
+ }
37
+
38
+ const seenRoles = new Set();
39
+ for (const [index, contract] of obj.roles.entries()) {
40
+ if (isPlainObject(contract) && typeof contract.role === "string") {
41
+ if (seenRoles.has(contract.role)) {
42
+ diagnostics.push(`${contract.role}: duplicate role name`);
43
+ }
44
+ seenRoles.add(contract.role);
45
+ }
46
+ validateRole(contract, index, diagnostics);
47
+ }
48
+
49
+ if (diagnostics.length > 0) {
50
+ throwContractsError(diagnostics);
51
+ }
52
+ }
53
+
54
+ function validateRole(contract, index, diagnostics) {
55
+ const role = getRoleLabel(contract, index);
56
+
57
+ if (!isPlainObject(contract)) {
58
+ diagnostics.push(`${role}: role contract must be an object`);
59
+ return;
60
+ }
61
+
62
+ requireField(contract, "role", role, diagnostics);
63
+ requireField(contract, "inputs", role, diagnostics);
64
+ requireField(contract, "outputs", role, diagnostics);
65
+ requireField(contract, "capabilities", role, diagnostics);
66
+ requireField(contract, "policy", role, diagnostics);
67
+ requireField(contract, "claudeSubagent", role, diagnostics);
68
+
69
+ if (typeof contract.role !== "string") {
70
+ diagnostics.push(`${role}: role must be a string`);
71
+ }
72
+ validateCapabilities(contract.capabilities, role, diagnostics);
73
+ validatePolicy(contract.policy, role, diagnostics);
74
+ validateClaudeSubagent(contract.claudeSubagent, role, diagnostics);
75
+ }
76
+
77
+ function validateCapabilities(capabilities, role, diagnostics) {
78
+ if (!isPlainObject(capabilities)) {
79
+ diagnostics.push(`${role}: capabilities must be an object`);
80
+ return;
81
+ }
82
+
83
+ if (!WORKSPACE_ACCESS_VALUES.has(capabilities.workspaceAccess)) {
84
+ diagnostics.push(
85
+ `${role}: capabilities.workspaceAccess must be one of ${Array.from(WORKSPACE_ACCESS_VALUES).join(", ")}`
86
+ );
87
+ }
88
+ }
89
+
90
+ function validatePolicy(policy, role, diagnostics) {
91
+ if (!isPlainObject(policy)) {
92
+ diagnostics.push(`${role}: policy must be an object`);
93
+ return;
94
+ }
95
+
96
+ if (!Number.isInteger(policy.maxAttempts) || policy.maxAttempts < 1) {
97
+ diagnostics.push(`${role}: policy.maxAttempts must be an integer >= 1`);
98
+ }
99
+ }
100
+
101
+ function validateClaudeSubagent(claudeSubagent, role, diagnostics) {
102
+ if (!isPlainObject(claudeSubagent)) {
103
+ diagnostics.push(`${role}: claudeSubagent must be an object`);
104
+ return;
105
+ }
106
+
107
+ validateStringArray(
108
+ claudeSubagent.tools,
109
+ `${role}: claudeSubagent.tools`,
110
+ diagnostics
111
+ );
112
+ }
113
+
114
+ function validateStringArray(value, field, diagnostics) {
115
+ if (!Array.isArray(value)) {
116
+ diagnostics.push(`${field} must be a string array`);
117
+ return;
118
+ }
119
+
120
+ for (const [index, item] of value.entries()) {
121
+ if (typeof item !== "string") {
122
+ diagnostics.push(`${field}[${index}] must be a string`);
123
+ }
124
+ }
125
+ }
126
+
127
+ function requireField(contract, field, role, diagnostics) {
128
+ if (!Object.hasOwn(contract, field)) {
129
+ diagnostics.push(`${role}: missing required field ${field}`);
130
+ }
131
+ }
132
+
133
+ function throwContractsError(diagnostics) {
134
+ throw new Error(`Invalid agent contracts:\n- ${diagnostics.join("\n- ")}`);
135
+ }
136
+
137
+ function getRoleLabel(contract, index) {
138
+ if (isPlainObject(contract) && typeof contract.role === "string") {
139
+ return contract.role;
140
+ }
141
+ return `<role-${index}>`;
142
+ }
143
+
144
+ function isPlainObject(value) {
145
+ return value !== null && typeof value === "object" && !Array.isArray(value);
146
+ }
@@ -0,0 +1,241 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { renderPrompt } from "./render.mjs";
8
+
9
+ const DEFAULT_COMPANION = fileURLToPath(
10
+ new URL("../crew-codex-companion.mjs", import.meta.url)
11
+ );
12
+ const AGENT_RESULT_STATUSES = new Set([
13
+ "complete",
14
+ "blocked_on_user",
15
+ "needs_agent",
16
+ "needs_tool",
17
+ "failed"
18
+ ]);
19
+
20
+ export class DispatchError extends Error {
21
+ constructor(message, options = {}) {
22
+ super(message);
23
+ this.name = "DispatchError";
24
+ this.agentResult = options.agentResult ?? null;
25
+ this.companionPayload = options.companionPayload ?? null;
26
+ this.exitCode = options.exitCode ?? 1;
27
+ }
28
+ }
29
+
30
+ export function formatDispatchProviderGuardMessage(role, provider) {
31
+ return `dispatch is for Codex provider only. Resolved provider for role '${role}' is '${provider}'. Use 'render' + Agent tool for Claude provider (see crew-agent-runner SKILL.md).`;
32
+ }
33
+
34
+ export async function dispatch(input) {
35
+ if (input.resolved?.provider !== "codex") {
36
+ throw new DispatchError(
37
+ formatDispatchProviderGuardMessage(
38
+ input.role,
39
+ input.resolved?.provider ?? "unknown"
40
+ ),
41
+ { exitCode: 2 }
42
+ );
43
+ }
44
+
45
+ const companion = resolveCompanion(input);
46
+ await assertResumeCandidate(input.resumeHandle, companion);
47
+
48
+ const tmpDir = await mkdtemp(join(tmpdir(), "claude-crew-dispatch-"));
49
+ const promptFile = join(tmpDir, `${input.role}-prompt.md`);
50
+
51
+ try {
52
+ await writeFile(
53
+ promptFile,
54
+ renderPrompt({
55
+ role: input.role,
56
+ request: input.request,
57
+ contract: input.contract
58
+ }),
59
+ "utf8"
60
+ );
61
+
62
+ const args = buildTaskArgs(input, promptFile);
63
+ const execution = await runCompanion(companion, args);
64
+ const payload = parseCompanionJson(execution.stdout, "task");
65
+
66
+ if (payload.crewAgentResultError) {
67
+ throw new DispatchError(
68
+ `Companion returned crewAgentResultError: ${payload.crewAgentResultError}`,
69
+ { companionPayload: payload }
70
+ );
71
+ }
72
+
73
+ const agentResult = normalizeAgentResult(payload.crewAgentResult, payload.threadId);
74
+ if (!agentResult) {
75
+ throw new DispatchError("Companion did not return crewAgentResult.", {
76
+ companionPayload: payload
77
+ });
78
+ }
79
+
80
+ if (execution.status !== 0 && agentResult.status !== "failed") {
81
+ throw new DispatchError(`Companion exited with status ${execution.status}.`, {
82
+ agentResult,
83
+ companionPayload: payload
84
+ });
85
+ }
86
+
87
+ return agentResult;
88
+ } finally {
89
+ await rm(tmpDir, { recursive: true, force: true });
90
+ }
91
+ }
92
+
93
+ export async function runCompanion(companion, args) {
94
+ return new Promise((resolve, reject) => {
95
+ const child = spawn(companion.command, [...companion.prefixArgs, ...args], {
96
+ cwd: process.cwd(),
97
+ env: process.env,
98
+ stdio: ["ignore", "pipe", "pipe"],
99
+ windowsHide: true
100
+ });
101
+
102
+ let stdout = "";
103
+ let stderr = "";
104
+ child.stdout.setEncoding("utf8");
105
+ child.stderr.setEncoding("utf8");
106
+ child.stdout.on("data", (chunk) => {
107
+ stdout += chunk;
108
+ });
109
+ child.stderr.on("data", (chunk) => {
110
+ stderr += chunk;
111
+ });
112
+ child.on("error", reject);
113
+ child.on("close", (status, signal) => {
114
+ resolve({
115
+ status: status ?? 1,
116
+ signal,
117
+ stdout,
118
+ stderr
119
+ });
120
+ });
121
+ });
122
+ }
123
+
124
+ async function assertResumeCandidate(resumeHandle, companion) {
125
+ if (!resumeHandle) {
126
+ return;
127
+ }
128
+
129
+ const execution = await runCompanion(companion, [
130
+ "task-resume-candidate",
131
+ "--json"
132
+ ]);
133
+ const payload = parseCompanionJson(execution.stdout, "task-resume-candidate");
134
+ const candidateThreadId = payload?.candidate?.threadId ?? null;
135
+
136
+ if (execution.status !== 0) {
137
+ throw new DispatchError(
138
+ `Companion resume candidate lookup failed with status ${execution.status}.`,
139
+ { companionPayload: payload }
140
+ );
141
+ }
142
+
143
+ if (!candidateThreadId) {
144
+ throw new DispatchError(`No resume candidate found for ${resumeHandle}.`, {
145
+ companionPayload: payload
146
+ });
147
+ }
148
+
149
+ if (candidateThreadId !== resumeHandle) {
150
+ throw new DispatchError(
151
+ `Resume handle ${resumeHandle} does not match candidate ${candidateThreadId}.`,
152
+ { companionPayload: payload }
153
+ );
154
+ }
155
+ }
156
+
157
+ function buildTaskArgs(input, promptFile) {
158
+ const args = [
159
+ "task",
160
+ "--json",
161
+ "--expect-crew-result",
162
+ "--prompt-file",
163
+ promptFile
164
+ ];
165
+
166
+ if (input.resumeHandle) {
167
+ args.push("--resume-last");
168
+ }
169
+
170
+ if (input.resolved?.codex_sandbox === "workspace-write") {
171
+ args.push("--write");
172
+ }
173
+
174
+ if (input.resolved?.model) {
175
+ args.push("--model", input.resolved.model);
176
+ }
177
+
178
+ if (input.resolved?.reasoning) {
179
+ args.push("--effort", input.resolved.reasoning);
180
+ }
181
+
182
+ return args;
183
+ }
184
+
185
+ function normalizeAgentResult(value, threadId) {
186
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
187
+ return null;
188
+ }
189
+ if (!AGENT_RESULT_STATUSES.has(value.status)) {
190
+ return null;
191
+ }
192
+
193
+ return {
194
+ ...value,
195
+ questions: Array.isArray(value.questions) ? value.questions : [],
196
+ requests: Array.isArray(value.requests) ? value.requests : [],
197
+ error: value.error ?? null,
198
+ agent_handle: threadId ?? value.agent_handle ?? null
199
+ };
200
+ }
201
+
202
+ function parseCompanionJson(stdout, command) {
203
+ try {
204
+ return JSON.parse(stdout);
205
+ } catch (error) {
206
+ throw new DispatchError(
207
+ `Companion ${command} did not return JSON: ${
208
+ error instanceof Error ? error.message : String(error)
209
+ }`
210
+ );
211
+ }
212
+ }
213
+
214
+ function resolveCompanion(input = {}) {
215
+ const nodeScript =
216
+ input.companionBin ?? process.env.CREW_COMPANION_NODE_BIN ?? null;
217
+ if (nodeScript) {
218
+ return {
219
+ command: process.execPath,
220
+ prefixArgs: [nodeScript]
221
+ };
222
+ }
223
+
224
+ const binary = process.env.CREW_COMPANION_BIN ?? DEFAULT_COMPANION;
225
+ if (isNodeScript(binary)) {
226
+ return {
227
+ command: process.execPath,
228
+ prefixArgs: [binary]
229
+ };
230
+ }
231
+
232
+ return {
233
+ command: binary,
234
+ prefixArgs: []
235
+ };
236
+ }
237
+
238
+ function isNodeScript(value) {
239
+ const name = basename(String(value));
240
+ return name.endsWith(".mjs") || name.endsWith(".js");
241
+ }