orcastrator 0.2.11 → 0.2.13
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.
- package/README.md +9 -1
- package/dist/agents/claude/session.js +213 -70
- package/dist/agents/codex/session.js +46 -17
- package/dist/cli/commands/help.js +16 -0
- package/dist/cli/commands/resume.js +55 -2
- package/dist/cli/commands/run.js +91 -32
- package/dist/cli/commands/setup.js +134 -4
- package/dist/cli/commands/skills.js +75 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/config-loader.js +23 -0
- package/dist/core/planner.js +12 -3
- package/dist/types/effort.js +17 -0
- package/dist/utils/agent-json.js +84 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,10 @@ Global:
|
|
|
128
128
|
- `--spec <path>`
|
|
129
129
|
- `--plan <path>`
|
|
130
130
|
- `--config <path>`
|
|
131
|
+
- `--codex-only` (force Codex executor for this run)
|
|
132
|
+
- `--claude-only` (force Claude executor for this run)
|
|
133
|
+
- `--codex-effort <low|medium|high>`
|
|
134
|
+
- `--claude-effort <low|medium|high|max>`
|
|
131
135
|
- `--on-milestone <cmd>`
|
|
132
136
|
- `--on-task-complete <cmd>`
|
|
133
137
|
- `--on-task-fail <cmd>`
|
|
@@ -150,6 +154,10 @@ Global:
|
|
|
150
154
|
- `--run <run-id>`
|
|
151
155
|
- `--last`
|
|
152
156
|
- `--config <path>`
|
|
157
|
+
- `--codex-only`
|
|
158
|
+
- `--claude-only`
|
|
159
|
+
- `--codex-effort <low|medium|high>`
|
|
160
|
+
- `--claude-effort <low|medium|high|max>`
|
|
153
161
|
|
|
154
162
|
`orca cancel`:
|
|
155
163
|
|
|
@@ -180,7 +188,7 @@ Global:
|
|
|
180
188
|
|
|
181
189
|
- `--anthropic-key <key>`
|
|
182
190
|
- `--openai-key <key>`
|
|
183
|
-
- `--check`
|
|
191
|
+
- `--check` (API key lookup order: CLI flag → process env → `~/.openclaw/openclaw.json` `env.vars` → `~/.claude/.env` → `~/.config/claude/.env` → `./.env`)
|
|
184
192
|
- `--global`
|
|
185
193
|
- `--project`
|
|
186
194
|
|
|
@@ -1,16 +1,114 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { parseAgentJson } from "../../utils/agent-json.js";
|
|
4
|
+
const PlannedTaskSchema = z.object({
|
|
5
|
+
id: z.string().min(1),
|
|
6
|
+
name: z.string().min(1),
|
|
7
|
+
description: z.string(),
|
|
8
|
+
dependencies: z.array(z.string()),
|
|
9
|
+
acceptance_criteria: z.array(z.string()),
|
|
10
|
+
status: z.literal("pending"),
|
|
11
|
+
retries: z.literal(0),
|
|
12
|
+
maxRetries: z.literal(3),
|
|
13
|
+
}).strict();
|
|
14
|
+
const PlanPayloadSchema = z.object({
|
|
15
|
+
tasks: z.array(PlannedTaskSchema).min(1),
|
|
16
|
+
}).strict();
|
|
17
|
+
const TaskExecutionPayloadSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
outcome: z.enum(["done", "failed"]),
|
|
20
|
+
error: z.string().optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict()
|
|
23
|
+
.superRefine((value, ctx) => {
|
|
24
|
+
if (value.outcome === "failed" && !value.error) {
|
|
25
|
+
ctx.addIssue({
|
|
26
|
+
code: z.ZodIssueCode.custom,
|
|
27
|
+
message: "error is required when outcome=failed",
|
|
28
|
+
path: ["error"],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (value.outcome === "done" && value.error !== undefined) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: z.ZodIssueCode.custom,
|
|
34
|
+
message: "error must be omitted when outcome=done",
|
|
35
|
+
path: ["error"],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const PLAN_OUTPUT_SCHEMA = {
|
|
40
|
+
type: "object",
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
required: ["tasks"],
|
|
43
|
+
properties: {
|
|
44
|
+
tasks: {
|
|
45
|
+
type: "array",
|
|
46
|
+
minItems: 1,
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
required: [
|
|
51
|
+
"id",
|
|
52
|
+
"name",
|
|
53
|
+
"description",
|
|
54
|
+
"dependencies",
|
|
55
|
+
"acceptance_criteria",
|
|
56
|
+
"status",
|
|
57
|
+
"retries",
|
|
58
|
+
"maxRetries",
|
|
59
|
+
],
|
|
60
|
+
properties: {
|
|
61
|
+
id: { type: "string", minLength: 1 },
|
|
62
|
+
name: { type: "string", minLength: 1 },
|
|
63
|
+
description: { type: "string" },
|
|
64
|
+
dependencies: { type: "array", items: { type: "string" } },
|
|
65
|
+
acceptance_criteria: { type: "array", items: { type: "string" } },
|
|
66
|
+
status: { type: "string", enum: ["pending"] },
|
|
67
|
+
retries: { type: "number", enum: [0] },
|
|
68
|
+
maxRetries: { type: "number", enum: [3] },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const EXECUTION_OUTPUT_SCHEMA = {
|
|
75
|
+
type: "object",
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
required: ["outcome"],
|
|
78
|
+
properties: {
|
|
79
|
+
outcome: { type: "string", enum: ["done", "failed"] },
|
|
80
|
+
error: { type: "string" },
|
|
81
|
+
},
|
|
82
|
+
// eslint-disable-next-line unicorn/no-thenable
|
|
83
|
+
allOf: [
|
|
84
|
+
{
|
|
85
|
+
if: { properties: { outcome: { const: "failed" } } },
|
|
86
|
+
// eslint-disable-next-line unicorn/no-thenable
|
|
87
|
+
then: { required: ["error"] },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
if: { properties: { outcome: { const: "done" } } },
|
|
91
|
+
// eslint-disable-next-line unicorn/no-thenable
|
|
92
|
+
then: { not: { required: ["error"] } },
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
const PLAN_OUTPUT_FORMAT = {
|
|
97
|
+
type: "json_schema",
|
|
98
|
+
schema: PLAN_OUTPUT_SCHEMA,
|
|
99
|
+
};
|
|
100
|
+
const EXECUTION_OUTPUT_FORMAT = {
|
|
101
|
+
type: "json_schema",
|
|
102
|
+
schema: EXECUTION_OUTPUT_SCHEMA,
|
|
103
|
+
};
|
|
2
104
|
function buildPlanningPrompt(spec, systemContext) {
|
|
3
105
|
return [
|
|
4
106
|
systemContext,
|
|
5
107
|
"You are decomposing a spec into an ordered task graph.",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"Set status to \"pending\", retries to 0, and maxRetries to 3 for every task.",
|
|
9
|
-
"dependencies must be an array of task IDs.",
|
|
10
|
-
"acceptance_criteria must be an array of strings.",
|
|
11
|
-
"Return ONLY valid JSON. No markdown fences. No explanation.",
|
|
108
|
+
"Use the configured structured output schema only.",
|
|
109
|
+
"Do not include prose or markdown.",
|
|
12
110
|
"Spec:",
|
|
13
|
-
spec
|
|
111
|
+
spec,
|
|
14
112
|
].join("\n\n");
|
|
15
113
|
}
|
|
16
114
|
function buildTaskExecutionPrompt(task, runId, cwd, systemContext) {
|
|
@@ -25,10 +123,8 @@ function buildTaskExecutionPrompt(task, runId, cwd, systemContext) {
|
|
|
25
123
|
task.description,
|
|
26
124
|
"Acceptance Criteria:",
|
|
27
125
|
...task.acceptance_criteria.map((criterion, index) => `${index + 1}. ${criterion}`),
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"{\"outcome\":\"done\"|\"failed\",\"error\"?:\"string\"}",
|
|
31
|
-
"If you cannot complete the task, return outcome=failed with a short error reason."
|
|
126
|
+
"Use the configured structured output schema only.",
|
|
127
|
+
"If you cannot complete the task, set outcome=failed and provide a concise error.",
|
|
32
128
|
].join("\n\n");
|
|
33
129
|
}
|
|
34
130
|
function extractAssistantText(message) {
|
|
@@ -50,107 +146,154 @@ function extractAssistantText(message) {
|
|
|
50
146
|
.trim();
|
|
51
147
|
return text.length > 0 ? text : null;
|
|
52
148
|
}
|
|
149
|
+
function formatSchemaError(prefix, error) {
|
|
150
|
+
const details = error.issues
|
|
151
|
+
.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`)
|
|
152
|
+
.join("; ");
|
|
153
|
+
return new Error(`${prefix}. ${details}`);
|
|
154
|
+
}
|
|
53
155
|
export function parseTaskArray(raw) {
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
58
|
-
throw
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// LLMs sometimes emit numeric IDs (1, 2, 3) even when we ask for strings.
|
|
62
|
-
return parsed.map((task) => ({
|
|
63
|
-
...task,
|
|
64
|
-
id: String(task.id),
|
|
65
|
-
dependencies: Array.isArray(task.dependencies)
|
|
66
|
-
? task.dependencies.map(String)
|
|
67
|
-
: [],
|
|
68
|
-
// Apply sensible defaults for required fields the LLM may have omitted.
|
|
69
|
-
name: typeof task.name === "string" && task.name.length > 0 ? task.name : "Unnamed task",
|
|
70
|
-
description: typeof task.description === "string" ? task.description : "",
|
|
71
|
-
acceptance_criteria: Array.isArray(task.acceptance_criteria) ? task.acceptance_criteria : [],
|
|
72
|
-
status: typeof task.status === "string" && task.status.length > 0 ? task.status : "pending",
|
|
73
|
-
retries: typeof task.retries === "number" ? task.retries : 0,
|
|
74
|
-
maxRetries: typeof task.maxRetries === "number" ? task.maxRetries : 3,
|
|
75
|
-
}));
|
|
156
|
+
const parsed = parseAgentJson(raw);
|
|
157
|
+
const normalized = Array.isArray(parsed) ? { tasks: parsed } : parsed;
|
|
158
|
+
const result = PlanPayloadSchema.safeParse(normalized);
|
|
159
|
+
if (!result.success) {
|
|
160
|
+
throw formatSchemaError("Claude plan response failed schema validation", result.error);
|
|
161
|
+
}
|
|
162
|
+
return result.data.tasks;
|
|
76
163
|
}
|
|
77
164
|
export function parseTaskExecution(raw) {
|
|
78
|
-
const parsed =
|
|
79
|
-
|
|
80
|
-
|
|
165
|
+
const parsed = parseAgentJson(raw);
|
|
166
|
+
const result = TaskExecutionPayloadSchema.safeParse(parsed);
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
throw formatSchemaError("Claude task response failed schema validation", result.error);
|
|
81
169
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
170
|
+
return {
|
|
171
|
+
outcome: result.data.outcome,
|
|
172
|
+
rawResponse: raw,
|
|
173
|
+
...(result.data.error ? { error: result.data.error } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function parseStructuredPlanPayload(payload) {
|
|
177
|
+
const result = PlanPayloadSchema.safeParse(payload);
|
|
178
|
+
if (!result.success) {
|
|
179
|
+
throw formatSchemaError("Claude structured plan payload failed schema validation", result.error);
|
|
85
180
|
}
|
|
86
|
-
|
|
87
|
-
|
|
181
|
+
return result.data.tasks;
|
|
182
|
+
}
|
|
183
|
+
export function parseStructuredTaskExecutionPayload(payload, rawResponse = "") {
|
|
184
|
+
const result = TaskExecutionPayloadSchema.safeParse(payload);
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
throw formatSchemaError("Claude structured task payload failed schema validation", result.error);
|
|
88
187
|
}
|
|
89
188
|
return {
|
|
90
|
-
outcome:
|
|
91
|
-
rawResponse
|
|
92
|
-
...(
|
|
189
|
+
outcome: result.data.outcome,
|
|
190
|
+
rawResponse,
|
|
191
|
+
...(result.data.error ? { error: result.data.error } : {}),
|
|
93
192
|
};
|
|
94
193
|
}
|
|
95
|
-
async function collectSessionResult(
|
|
194
|
+
async function collectSessionResult(claudeQuery) {
|
|
96
195
|
const assistantMessages = [];
|
|
97
196
|
let resultText = null;
|
|
98
|
-
|
|
197
|
+
let structuredOutput;
|
|
198
|
+
for await (const message of claudeQuery) {
|
|
99
199
|
const assistantText = extractAssistantText(message);
|
|
100
200
|
if (assistantText) {
|
|
101
201
|
assistantMessages.push(assistantText);
|
|
102
202
|
}
|
|
103
|
-
if (message.type === "result" &&
|
|
104
|
-
message.
|
|
105
|
-
|
|
106
|
-
|
|
203
|
+
if (message.type === "result" && message.subtype === "success") {
|
|
204
|
+
if (typeof message.result === "string") {
|
|
205
|
+
resultText = message.result;
|
|
206
|
+
}
|
|
207
|
+
if ("structured_output" in message) {
|
|
208
|
+
structuredOutput = message.structured_output;
|
|
209
|
+
}
|
|
107
210
|
}
|
|
108
211
|
if (message.type === "result" && message.subtype !== "success") {
|
|
109
212
|
const details = "errors" in message ? message.errors.join("; ") : "unknown error";
|
|
110
213
|
throw new Error(`Claude session failed (${message.subtype}): ${details}`);
|
|
111
214
|
}
|
|
112
215
|
}
|
|
113
|
-
const rawResponse = assistantMessages.length > 0
|
|
114
|
-
|
|
115
|
-
: resultText;
|
|
116
|
-
if (!rawResponse) {
|
|
216
|
+
const rawResponse = assistantMessages.length > 0 ? assistantMessages[assistantMessages.length - 1] : resultText;
|
|
217
|
+
if (!rawResponse && structuredOutput === undefined) {
|
|
117
218
|
throw new Error("Claude response was empty");
|
|
118
219
|
}
|
|
119
|
-
return
|
|
220
|
+
return {
|
|
221
|
+
rawResponse: rawResponse ?? "",
|
|
222
|
+
structuredOutput,
|
|
223
|
+
};
|
|
120
224
|
}
|
|
121
225
|
function getModel(config) {
|
|
122
226
|
return config?.claude?.model ?? process.env.ORCA_CLAUDE_MODEL ?? "claude-sonnet-4-5";
|
|
123
227
|
}
|
|
124
|
-
|
|
125
|
-
|
|
228
|
+
function getEffort(config) {
|
|
229
|
+
return config?.claude?.effort;
|
|
230
|
+
}
|
|
231
|
+
function buildClaudeQueryOptions(config, outputFormat) {
|
|
232
|
+
const options = {
|
|
126
233
|
model: getModel(config),
|
|
127
234
|
permissionMode: "bypassPermissions",
|
|
235
|
+
outputFormat,
|
|
236
|
+
};
|
|
237
|
+
const effort = getEffort(config);
|
|
238
|
+
if (effort) {
|
|
239
|
+
options.effortValue = effort;
|
|
240
|
+
}
|
|
241
|
+
return options;
|
|
242
|
+
}
|
|
243
|
+
function shouldAllowTextJsonFallback(config) {
|
|
244
|
+
if (config?.claude?.allowTextJsonFallback !== undefined) {
|
|
245
|
+
return config.claude.allowTextJsonFallback;
|
|
246
|
+
}
|
|
247
|
+
const env = process.env.ORCA_CLAUDE_ALLOW_TEXT_JSON_FALLBACK;
|
|
248
|
+
return env === "1" || env === "true";
|
|
249
|
+
}
|
|
250
|
+
function throwMissingStructuredOutput(kind) {
|
|
251
|
+
throw new Error(`Claude structured_output missing for ${kind}. Refusing freeform JSON parsing on critical path. ` +
|
|
252
|
+
"Set ORCA_CLAUDE_ALLOW_TEXT_JSON_FALLBACK=1 (or config.claude.allowTextJsonFallback=true) to temporarily enable fallback.");
|
|
253
|
+
}
|
|
254
|
+
export async function planSpec(spec, systemContext, config) {
|
|
255
|
+
const claudeQuery = query({
|
|
256
|
+
prompt: buildPlanningPrompt(spec, systemContext),
|
|
257
|
+
options: buildClaudeQueryOptions(config, PLAN_OUTPUT_FORMAT),
|
|
128
258
|
});
|
|
129
259
|
try {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
260
|
+
const { rawResponse, structuredOutput } = await collectSessionResult(claudeQuery);
|
|
261
|
+
if (structuredOutput !== undefined) {
|
|
262
|
+
return {
|
|
263
|
+
tasks: parseStructuredPlanPayload(structuredOutput),
|
|
264
|
+
rawResponse,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (!shouldAllowTextJsonFallback(config)) {
|
|
268
|
+
throwMissingStructuredOutput("planner");
|
|
269
|
+
}
|
|
270
|
+
console.warn("[orca][claude] structured_output missing for planner; fallback text JSON parser enabled via explicit flag");
|
|
133
271
|
return {
|
|
134
272
|
tasks: parseTaskArray(rawResponse),
|
|
135
|
-
rawResponse
|
|
273
|
+
rawResponse,
|
|
136
274
|
};
|
|
137
275
|
}
|
|
138
276
|
finally {
|
|
139
|
-
|
|
277
|
+
claudeQuery.close();
|
|
140
278
|
}
|
|
141
279
|
}
|
|
142
280
|
export async function executeTask(task, runId, config, systemContext) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
281
|
+
const claudeQuery = query({
|
|
282
|
+
prompt: buildTaskExecutionPrompt(task, runId, process.cwd(), systemContext),
|
|
283
|
+
options: buildClaudeQueryOptions(config, EXECUTION_OUTPUT_FORMAT),
|
|
146
284
|
});
|
|
147
285
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
286
|
+
const { rawResponse, structuredOutput } = await collectSessionResult(claudeQuery);
|
|
287
|
+
if (structuredOutput !== undefined) {
|
|
288
|
+
return parseStructuredTaskExecutionPayload(structuredOutput, rawResponse);
|
|
289
|
+
}
|
|
290
|
+
if (!shouldAllowTextJsonFallback(config)) {
|
|
291
|
+
throwMissingStructuredOutput("task execution");
|
|
292
|
+
}
|
|
293
|
+
console.warn("[orca][claude] structured_output missing for task execution; fallback text JSON parser enabled via explicit flag");
|
|
151
294
|
return parseTaskExecution(rawResponse);
|
|
152
295
|
}
|
|
153
296
|
finally {
|
|
154
|
-
|
|
297
|
+
claudeQuery.close();
|
|
155
298
|
}
|
|
156
299
|
}
|
|
@@ -158,6 +158,9 @@ function getCodexPath() {
|
|
|
158
158
|
return (process.env.ORCA_CODEX_PATH ??
|
|
159
159
|
`${process.env.HOME}/.nvm/versions/node/v22.22.0/bin/codex`);
|
|
160
160
|
}
|
|
161
|
+
function getEffort(config) {
|
|
162
|
+
return config?.codex?.effort;
|
|
163
|
+
}
|
|
161
164
|
export async function createCodexSession(cwd, config) {
|
|
162
165
|
const client = new CodexClient({
|
|
163
166
|
codexPath: getCodexPath(),
|
|
@@ -172,10 +175,17 @@ export async function createCodexSession(cwd, config) {
|
|
|
172
175
|
return {
|
|
173
176
|
threadId,
|
|
174
177
|
async planSpec(spec, systemContext) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
const effort = getEffort(config);
|
|
179
|
+
const result = effort
|
|
180
|
+
? await client.runTurn({
|
|
181
|
+
threadId,
|
|
182
|
+
effort,
|
|
183
|
+
input: [{ type: "text", text: buildPlanningPrompt(spec, systemContext) }],
|
|
184
|
+
})
|
|
185
|
+
: await client.runTurn({
|
|
186
|
+
threadId,
|
|
187
|
+
input: [{ type: "text", text: buildPlanningPrompt(spec, systemContext) }],
|
|
188
|
+
});
|
|
179
189
|
const rawResponse = extractAgentText(result);
|
|
180
190
|
return {
|
|
181
191
|
tasks: parseTaskArray(rawResponse),
|
|
@@ -183,15 +193,27 @@ export async function createCodexSession(cwd, config) {
|
|
|
183
193
|
};
|
|
184
194
|
},
|
|
185
195
|
async executeTask(task, runId, systemContext) {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
const effort = getEffort(config);
|
|
197
|
+
const result = effort
|
|
198
|
+
? await client.runTurn({
|
|
199
|
+
threadId,
|
|
200
|
+
effort,
|
|
201
|
+
input: [
|
|
202
|
+
{
|
|
203
|
+
type: "text",
|
|
204
|
+
text: buildTaskExecutionPrompt(task, runId, cwd, systemContext),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
: await client.runTurn({
|
|
209
|
+
threadId,
|
|
210
|
+
input: [
|
|
211
|
+
{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: buildTaskExecutionPrompt(task, runId, cwd, systemContext),
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
});
|
|
195
217
|
const rawResponse = extractAgentText(result);
|
|
196
218
|
// Primary signal: use the SDK's structured turn status.
|
|
197
219
|
const status = result.turn.status;
|
|
@@ -226,10 +248,17 @@ export async function createCodexSession(cwd, config) {
|
|
|
226
248
|
"Task graph:",
|
|
227
249
|
taskGraphJson,
|
|
228
250
|
].join("\n");
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
const effort = getEffort(config);
|
|
252
|
+
const result = effort
|
|
253
|
+
? await client.runTurn({
|
|
254
|
+
threadId,
|
|
255
|
+
effort,
|
|
256
|
+
input: [{ type: "text", text: prompt }],
|
|
257
|
+
})
|
|
258
|
+
: await client.runTurn({
|
|
259
|
+
threadId,
|
|
260
|
+
input: [{ type: "text", text: prompt }],
|
|
261
|
+
});
|
|
233
262
|
const rawResponse = extractAgentText(result);
|
|
234
263
|
const json = extractJson(rawResponse);
|
|
235
264
|
const parsed = JSON.parse(json);
|
|
@@ -45,6 +45,22 @@ function printStyledHelpPage() {
|
|
|
45
45
|
command: "--config <path>",
|
|
46
46
|
description: "explicit config file (auto-discovered by default)"
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
command: "--codex-only",
|
|
50
|
+
description: "override executor to Codex for the current run"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
command: "--claude-only",
|
|
54
|
+
description: "override executor to Claude for the current run"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
command: "--codex-effort <value>",
|
|
58
|
+
description: "override Codex effort for the current run"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
command: "--claude-effort <value>",
|
|
62
|
+
description: "override Claude effort for the current run"
|
|
63
|
+
},
|
|
48
64
|
{ command: "--full-auto", description: "skip all questions, proceed autonomously" },
|
|
49
65
|
{ command: "--on-complete <cmd>", description: "shell hook on run complete" },
|
|
50
66
|
{ command: "--on-error <cmd>", description: "shell hook on run error" },
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { InvalidArgumentError } from "commander";
|
|
2
|
+
import { resolveConfig } from "../../core/config-loader.js";
|
|
1
3
|
import { runTaskRunner } from "../../core/task-runner.js";
|
|
2
4
|
import { RunStore } from "../../state/store.js";
|
|
5
|
+
import { parseClaudeEffort, parseCodexEffort } from "../../types/effort.js";
|
|
3
6
|
import { getLastRun } from "../../utils/last-run.js";
|
|
4
7
|
function createStore() {
|
|
5
8
|
const runsDir = process.env.ORCA_RUNS_DIR;
|
|
@@ -14,8 +17,45 @@ function formatRunIds(runs) {
|
|
|
14
17
|
function getActiveRuns(runs) {
|
|
15
18
|
return runs.filter((run) => run.overallStatus === "planning" || run.overallStatus === "running");
|
|
16
19
|
}
|
|
20
|
+
function parseCodexEffortOption(value) {
|
|
21
|
+
try {
|
|
22
|
+
return parseCodexEffort(value);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parseClaudeEffortOption(value) {
|
|
29
|
+
try {
|
|
30
|
+
return parseClaudeEffort(value);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function applyExecutorOverrideForResume(config, options) {
|
|
37
|
+
const nextConfig = { ...config };
|
|
38
|
+
if (options.codexOnly || options.claudeOnly) {
|
|
39
|
+
nextConfig.executor = options.codexOnly ? "codex" : "claude";
|
|
40
|
+
}
|
|
41
|
+
if (options.codexEffort !== undefined) {
|
|
42
|
+
nextConfig.codex = { ...nextConfig.codex, effort: options.codexEffort };
|
|
43
|
+
}
|
|
44
|
+
if (options.claudeEffort !== undefined) {
|
|
45
|
+
nextConfig.claude = { ...nextConfig.claude, effort: options.claudeEffort };
|
|
46
|
+
}
|
|
47
|
+
if (config === undefined && Object.keys(nextConfig).length === 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return nextConfig;
|
|
51
|
+
}
|
|
17
52
|
export async function resumeCommandHandler(options) {
|
|
53
|
+
if (options.codexOnly && options.claudeOnly) {
|
|
54
|
+
throw new Error("--codex-only and --claude-only are mutually exclusive; choose only one executor override.");
|
|
55
|
+
}
|
|
18
56
|
const store = createStore();
|
|
57
|
+
const resolvedConfig = await resolveConfig(options.config);
|
|
58
|
+
const effectiveConfig = applyExecutorOverrideForResume(resolvedConfig, options);
|
|
19
59
|
if (options.last) {
|
|
20
60
|
const lastRun = await getLastRun(store);
|
|
21
61
|
if (!lastRun) {
|
|
@@ -59,7 +99,8 @@ export async function resumeCommandHandler(options) {
|
|
|
59
99
|
});
|
|
60
100
|
await runTaskRunner({
|
|
61
101
|
runId: run.runId,
|
|
62
|
-
store
|
|
102
|
+
store,
|
|
103
|
+
...(effectiveConfig ? { config: effectiveConfig } : {})
|
|
63
104
|
});
|
|
64
105
|
const refreshed = await store.getRun(run.runId);
|
|
65
106
|
if (!refreshed) {
|
|
@@ -74,5 +115,17 @@ export function registerResumeCommand(program) {
|
|
|
74
115
|
.option("--run <run-id>", "Run ID to resume")
|
|
75
116
|
.option("--last", "Use the most recent run")
|
|
76
117
|
.option("--config <path>", "Path to orca config file")
|
|
77
|
-
.
|
|
118
|
+
.option("--codex-only", "Force Codex executor for this resumed run (overrides config)")
|
|
119
|
+
.option("--claude-only", "Force Claude executor for this resumed run (overrides config)")
|
|
120
|
+
.option("--codex-effort <value>", "Codex effort override for this resumed run", parseCodexEffortOption)
|
|
121
|
+
.option("--claude-effort <value>", "Claude effort override for this resumed run", parseClaudeEffortOption)
|
|
122
|
+
.action(async (options) => {
|
|
123
|
+
try {
|
|
124
|
+
await resumeCommandHandler(options);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
78
131
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import { access, unlink, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { InvalidArgumentError } from "commander";
|
|
6
7
|
import { createCodexSession } from "../../agents/codex/session.js";
|
|
7
8
|
import { ensureCodexMultiAgent } from "../../core/codex-config.js";
|
|
8
9
|
import { resolveConfig } from "../../core/config-loader.js";
|
|
@@ -12,6 +13,7 @@ import { createOpenclawHookHandler, detectOpenclawAvailability } from "../../hoo
|
|
|
12
13
|
import { createStdoutHookHandler } from "../../hooks/adapters/stdout.js";
|
|
13
14
|
import { HookDispatcher } from "../../hooks/dispatcher.js";
|
|
14
15
|
import { RunStore } from "../../state/store.js";
|
|
16
|
+
import { parseClaudeEffort, parseCodexEffort } from "../../types/effort.js";
|
|
15
17
|
import { generateRunId } from "../../utils/ids.js";
|
|
16
18
|
const ALL_HOOKS = [
|
|
17
19
|
"onMilestone",
|
|
@@ -30,6 +32,22 @@ const VALID_HOOK_NAMES = new Set([
|
|
|
30
32
|
function isHookName(value) {
|
|
31
33
|
return VALID_HOOK_NAMES.has(value);
|
|
32
34
|
}
|
|
35
|
+
function parseCodexEffortOption(value) {
|
|
36
|
+
try {
|
|
37
|
+
return parseCodexEffort(value);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function parseClaudeEffortOption(value) {
|
|
44
|
+
try {
|
|
45
|
+
return parseClaudeEffort(value);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
33
51
|
function createStore() {
|
|
34
52
|
const runsDir = process.env.ORCA_RUNS_DIR;
|
|
35
53
|
return runsDir ? new RunStore(runsDir) : new RunStore();
|
|
@@ -49,7 +67,26 @@ function buildCliCommandHooks(options) {
|
|
|
49
67
|
...(options.onError ? { onError: options.onError } : {})
|
|
50
68
|
};
|
|
51
69
|
}
|
|
70
|
+
function applyExecutorOverrideForRun(config, options) {
|
|
71
|
+
const nextConfig = { ...config };
|
|
72
|
+
if (options.codexOnly || options.claudeOnly) {
|
|
73
|
+
nextConfig.executor = options.codexOnly ? "codex" : "claude";
|
|
74
|
+
}
|
|
75
|
+
if (options.codexEffort !== undefined) {
|
|
76
|
+
nextConfig.codex = { ...nextConfig.codex, effort: options.codexEffort };
|
|
77
|
+
}
|
|
78
|
+
if (options.claudeEffort !== undefined) {
|
|
79
|
+
nextConfig.claude = { ...nextConfig.claude, effort: options.claudeEffort };
|
|
80
|
+
}
|
|
81
|
+
if (config === undefined && Object.keys(nextConfig).length === 0) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return nextConfig;
|
|
85
|
+
}
|
|
52
86
|
export async function runCommandHandler(options) {
|
|
87
|
+
if (options.codexOnly && options.claudeOnly) {
|
|
88
|
+
throw new Error("--codex-only and --claude-only are mutually exclusive; choose only one executor override.");
|
|
89
|
+
}
|
|
53
90
|
const inlineTask = options.task ?? options.prompt ?? options.goal;
|
|
54
91
|
const inputSpecPath = options.spec ?? options.plan;
|
|
55
92
|
if (options.goal !== undefined && (options.task || options.prompt)) {
|
|
@@ -76,11 +113,12 @@ export async function runCommandHandler(options) {
|
|
|
76
113
|
throw new Error(`Spec file not found or not readable: ${specPath}`);
|
|
77
114
|
});
|
|
78
115
|
const orcaConfig = await resolveConfig(options.config);
|
|
116
|
+
const effectiveConfig = applyExecutorOverrideForRun(orcaConfig, options);
|
|
79
117
|
const runId = generateRunId(specPath);
|
|
80
118
|
console.log(`Run ID: ${runId}`);
|
|
81
119
|
const store = createStore();
|
|
82
120
|
await store.createRun(runId, specPath);
|
|
83
|
-
await runPlanner(specPath, store, runId,
|
|
121
|
+
await runPlanner(specPath, store, runId, effectiveConfig);
|
|
84
122
|
await store.updateRun(runId, {
|
|
85
123
|
mode: "run",
|
|
86
124
|
overallStatus: "running"
|
|
@@ -124,45 +162,57 @@ export async function runCommandHandler(options) {
|
|
|
124
162
|
const emitHook = async (event) => {
|
|
125
163
|
await dispatcher.dispatch(event);
|
|
126
164
|
};
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
// Phase 4: Codex consults the task graph before execution begins.
|
|
135
|
-
const plannedRun = await store.getRun(runId);
|
|
136
|
-
if (!plannedRun) {
|
|
137
|
-
throw new Error(`Run not found after planning: ${runId}`);
|
|
165
|
+
const executor = effectiveConfig?.executor ?? "codex";
|
|
166
|
+
if (executor === "codex") {
|
|
167
|
+
const cwd = process.cwd();
|
|
168
|
+
const multiAgentResult = await ensureCodexMultiAgent(effectiveConfig);
|
|
169
|
+
if (multiAgentResult.action === "created" || multiAgentResult.action === "appended") {
|
|
170
|
+
console.log(`Multi-agent: enabled (updated ${multiAgentResult.path})`);
|
|
138
171
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
const codexSession = await createCodexSession(cwd, effectiveConfig);
|
|
173
|
+
try {
|
|
174
|
+
// Phase 4: Codex consults the task graph before execution begins.
|
|
175
|
+
const plannedRun = await store.getRun(runId);
|
|
176
|
+
if (!plannedRun) {
|
|
177
|
+
throw new Error(`Run not found after planning: ${runId}`);
|
|
145
178
|
}
|
|
179
|
+
console.log("Phase 4: Codex reviewing task graph...");
|
|
180
|
+
const consultation = await codexSession.consultTaskGraph(plannedRun.tasks);
|
|
181
|
+
if (consultation.issues.length > 0) {
|
|
182
|
+
console.log("Codex consultation issues:");
|
|
183
|
+
for (const issue of consultation.issues) {
|
|
184
|
+
console.log(` - ${issue}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!consultation.ok) {
|
|
188
|
+
console.error("Codex flagged the task graph as not OK. Aborting.");
|
|
189
|
+
await store.updateRun(runId, { overallStatus: "failed" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
console.log("Codex consultation passed. Starting execution...");
|
|
193
|
+
await runTaskRunner({
|
|
194
|
+
runId,
|
|
195
|
+
store,
|
|
196
|
+
...(effectiveConfig ? { config: effectiveConfig } : {}),
|
|
197
|
+
emitHook,
|
|
198
|
+
executeTask: (task, taskRunId, _config, systemContext) => codexSession.executeTask(task, taskRunId, systemContext),
|
|
199
|
+
});
|
|
200
|
+
const reviewText = await codexSession.reviewChanges();
|
|
201
|
+
console.log("Codex post-execution review:");
|
|
202
|
+
console.log(reviewText);
|
|
146
203
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await store.updateRun(runId, { overallStatus: "failed" });
|
|
150
|
-
return;
|
|
204
|
+
finally {
|
|
205
|
+
await codexSession.disconnect();
|
|
151
206
|
}
|
|
152
|
-
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log("Phase 4: Skipping Codex consultation because executor is set to Claude.");
|
|
153
210
|
await runTaskRunner({
|
|
154
211
|
runId,
|
|
155
212
|
store,
|
|
156
|
-
...(
|
|
157
|
-
emitHook
|
|
158
|
-
executeTask: (task, runId, _config, systemContext) => codexSession.executeTask(task, runId, systemContext),
|
|
213
|
+
...(effectiveConfig ? { config: effectiveConfig } : {}),
|
|
214
|
+
emitHook
|
|
159
215
|
});
|
|
160
|
-
const reviewText = await codexSession.reviewChanges();
|
|
161
|
-
console.log("Codex post-execution review:");
|
|
162
|
-
console.log(reviewText);
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
await codexSession.disconnect();
|
|
166
216
|
}
|
|
167
217
|
const run = await store.getRun(runId);
|
|
168
218
|
if (!run) {
|
|
@@ -196,6 +246,10 @@ export function registerRunCommand(program) {
|
|
|
196
246
|
.option("--task <text>", "Inline task text (alternative to --spec)")
|
|
197
247
|
.option("-p, --prompt <text>", "Inline task text (alias for --task)")
|
|
198
248
|
.option("--config <path>", "Path to orca config file")
|
|
249
|
+
.option("--codex-only", "Force Codex executor for this run (overrides config)")
|
|
250
|
+
.option("--claude-only", "Force Claude executor for this run (overrides config)")
|
|
251
|
+
.option("--codex-effort <value>", "Codex effort override for this run", parseCodexEffortOption)
|
|
252
|
+
.option("--claude-effort <value>", "Claude effort override for this run", parseClaudeEffortOption)
|
|
199
253
|
.option("--on-milestone <cmd>", "Shell hook command for onMilestone")
|
|
200
254
|
.option("--on-task-complete <cmd>", "Shell hook command for onTaskComplete")
|
|
201
255
|
.option("--on-task-fail <cmd>", "Shell hook command for onTaskFail")
|
|
@@ -207,6 +261,11 @@ export function registerRunCommand(program) {
|
|
|
207
261
|
...commandOptions,
|
|
208
262
|
...(goal !== undefined ? { goal } : {})
|
|
209
263
|
};
|
|
264
|
+
if (normalizedOptions.codexOnly && normalizedOptions.claudeOnly) {
|
|
265
|
+
console.error("Error: --codex-only and --claude-only are mutually exclusive; choose only one.");
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
210
269
|
const inlineTask = normalizedOptions.task ?? normalizedOptions.prompt ?? normalizedOptions.goal;
|
|
211
270
|
const inputSpecPath = normalizedOptions.spec ?? normalizedOptions.plan;
|
|
212
271
|
if (normalizedOptions.goal !== undefined && (normalizedOptions.task || normalizedOptions.prompt)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync, spawnSync } from "node:child_process";
|
|
2
|
-
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { constants as fsConstants, readFileSync } from "node:fs";
|
|
3
3
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -48,7 +48,7 @@ function commandExists(command) {
|
|
|
48
48
|
const result = spawnSync("which", [command], { stdio: "ignore" });
|
|
49
49
|
return result.status === 0;
|
|
50
50
|
}
|
|
51
|
-
export function resolveApiKey(flagValue, envVarName) {
|
|
51
|
+
export function resolveApiKey(flagValue, envVarName, openclawConfigPathOrOptions, maybeOptions) {
|
|
52
52
|
if (flagValue && flagValue.trim().length > 0) {
|
|
53
53
|
return flagValue.trim();
|
|
54
54
|
}
|
|
@@ -56,8 +56,138 @@ export function resolveApiKey(flagValue, envVarName) {
|
|
|
56
56
|
if (envValue && envValue.trim().length > 0) {
|
|
57
57
|
return envValue.trim();
|
|
58
58
|
}
|
|
59
|
+
const options = typeof openclawConfigPathOrOptions === "string"
|
|
60
|
+
? { ...(maybeOptions ?? {}), openclawConfigPath: openclawConfigPathOrOptions }
|
|
61
|
+
: (openclawConfigPathOrOptions ?? {});
|
|
62
|
+
const homedir = options.homedir ?? os.homedir();
|
|
63
|
+
const openclawValue = readOpenclawEnvVar(envVarName, options.openclawConfigPath, homedir);
|
|
64
|
+
if (openclawValue) {
|
|
65
|
+
return openclawValue;
|
|
66
|
+
}
|
|
67
|
+
const dotenvValue = readDotEnvFallback(envVarName, {
|
|
68
|
+
cwd: options.cwd ?? process.cwd(),
|
|
69
|
+
homedir
|
|
70
|
+
});
|
|
71
|
+
if (dotenvValue) {
|
|
72
|
+
return dotenvValue;
|
|
73
|
+
}
|
|
59
74
|
return undefined;
|
|
60
75
|
}
|
|
76
|
+
function readOpenclawEnvVar(envVarName, openclawConfigPath, homedir = os.homedir()) {
|
|
77
|
+
const configPath = openclawConfigPath ?? path.join(homedir, ".openclaw", "openclaw.json");
|
|
78
|
+
try {
|
|
79
|
+
const fileText = readFileSync(configPath, "utf8");
|
|
80
|
+
const parsed = JSON.parse(fileText);
|
|
81
|
+
const value = parsed.env?.vars?.[envVarName];
|
|
82
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
83
|
+
return value.trim();
|
|
84
|
+
}
|
|
85
|
+
if (value && typeof value === "object") {
|
|
86
|
+
const candidateObject = value;
|
|
87
|
+
for (const candidate of [
|
|
88
|
+
candidateObject.value,
|
|
89
|
+
candidateObject.ref,
|
|
90
|
+
candidateObject.opRef,
|
|
91
|
+
candidateObject.reference
|
|
92
|
+
]) {
|
|
93
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
94
|
+
return candidate.trim();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function readDotEnvFallback(envVarName, options) {
|
|
105
|
+
const candidatePaths = [
|
|
106
|
+
path.join(options.homedir, ".claude", ".env"),
|
|
107
|
+
path.join(options.homedir, ".config", "claude", ".env"),
|
|
108
|
+
path.join(options.cwd, ".env")
|
|
109
|
+
];
|
|
110
|
+
for (const candidatePath of candidatePaths) {
|
|
111
|
+
const value = readEnvVarFromDotEnvFile(candidatePath, envVarName);
|
|
112
|
+
if (value !== undefined) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function readEnvVarFromDotEnvFile(filePath, envVarName) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = parseDotEnv(readFileSync(filePath, "utf8"));
|
|
121
|
+
const value = parsed[envVarName];
|
|
122
|
+
if (value !== undefined && value.trim().length > 0) {
|
|
123
|
+
return value.trim();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function parseDotEnv(text) {
|
|
132
|
+
const values = {};
|
|
133
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
134
|
+
const line = rawLine.trim();
|
|
135
|
+
if (!line || line.startsWith("#")) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const normalized = line.startsWith("export ") ? line.slice(7).trimStart() : line;
|
|
139
|
+
const separatorIndex = normalized.indexOf("=");
|
|
140
|
+
if (separatorIndex <= 0) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
144
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let rawValue = normalized.slice(separatorIndex + 1).trim();
|
|
148
|
+
if (!rawValue) {
|
|
149
|
+
values[key] = "";
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (rawValue.startsWith('"') || rawValue.startsWith("'")) {
|
|
153
|
+
const quote = rawValue.charAt(0);
|
|
154
|
+
const closingQuoteIndex = findClosingQuote(rawValue, quote);
|
|
155
|
+
if (closingQuoteIndex > 0) {
|
|
156
|
+
rawValue = rawValue.slice(1, closingQuoteIndex);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
rawValue = rawValue.slice(1);
|
|
160
|
+
}
|
|
161
|
+
values[key] = quote === '"' ? unescapeDoubleQuoted(rawValue) : unescapeSingleQuoted(rawValue);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const commentStart = rawValue.search(/\s#/);
|
|
165
|
+
if (commentStart >= 0) {
|
|
166
|
+
rawValue = rawValue.slice(0, commentStart).trimEnd();
|
|
167
|
+
}
|
|
168
|
+
values[key] = rawValue;
|
|
169
|
+
}
|
|
170
|
+
return values;
|
|
171
|
+
}
|
|
172
|
+
function findClosingQuote(value, quote) {
|
|
173
|
+
for (let i = 1; i < value.length; i += 1) {
|
|
174
|
+
if (value[i] === quote && value[i - 1] !== "\\") {
|
|
175
|
+
return i;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
function unescapeDoubleQuoted(value) {
|
|
181
|
+
return value
|
|
182
|
+
.replace(/\\n/g, "\n")
|
|
183
|
+
.replace(/\\r/g, "\r")
|
|
184
|
+
.replace(/\\t/g, "\t")
|
|
185
|
+
.replace(/\\"/g, '"')
|
|
186
|
+
.replace(/\\\\/g, "\\");
|
|
187
|
+
}
|
|
188
|
+
function unescapeSingleQuoted(value) {
|
|
189
|
+
return value.replace(/\\'/g, "'").replace(/\\\\/g, "\\");
|
|
190
|
+
}
|
|
61
191
|
export function detectPackageManager(exists = commandExists) {
|
|
62
192
|
if (exists("brew")) {
|
|
63
193
|
return "brew";
|
|
@@ -228,7 +358,7 @@ export async function setupCommandHandler(options) {
|
|
|
228
358
|
console.log(chalk.green("✓ Anthropic API key found"));
|
|
229
359
|
}
|
|
230
360
|
else {
|
|
231
|
-
results.push({ name: "ANTHROPIC_API_KEY", status: "warn", detail:
|
|
361
|
+
results.push({ name: "ANTHROPIC_API_KEY", status: "warn", detail: "not set" });
|
|
232
362
|
if (!checkMode)
|
|
233
363
|
console.log(chalk.yellow("! ANTHROPIC_API_KEY not set"));
|
|
234
364
|
}
|
|
@@ -241,7 +371,7 @@ export async function setupCommandHandler(options) {
|
|
|
241
371
|
console.log(chalk.green("✓ OpenAI API key found"));
|
|
242
372
|
}
|
|
243
373
|
else {
|
|
244
|
-
results.push({ name: "OPENAI_API_KEY", status: "warn", detail:
|
|
374
|
+
results.push({ name: "OPENAI_API_KEY", status: "warn", detail: "not set" });
|
|
245
375
|
if (!checkMode)
|
|
246
376
|
console.log(chalk.yellow("! OPENAI_API_KEY not set"));
|
|
247
377
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveConfig } from "../../core/config-loader.js";
|
|
4
|
+
import { loadSkills } from "../../utils/skill-loader.js";
|
|
5
|
+
function pad(value, width) {
|
|
6
|
+
return value.padEnd(width, " ");
|
|
7
|
+
}
|
|
8
|
+
function formatTable(headers, rows) {
|
|
9
|
+
const widths = headers.map((header, index) => {
|
|
10
|
+
const rowWidths = rows.map((row) => row[index]?.length ?? 0);
|
|
11
|
+
return Math.max(header.length, ...rowWidths);
|
|
12
|
+
});
|
|
13
|
+
const headerLine = headers.map((header, index) => pad(header, widths[index] ?? header.length)).join(" ");
|
|
14
|
+
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
15
|
+
const body = rows.map((row) => row.map((cell, index) => pad(cell, widths[index] ?? cell.length)).join(" "));
|
|
16
|
+
return [headerLine, separator, ...body].join("\n");
|
|
17
|
+
}
|
|
18
|
+
function expandHome(inputPath) {
|
|
19
|
+
if (inputPath === "~") {
|
|
20
|
+
return os.homedir();
|
|
21
|
+
}
|
|
22
|
+
if (inputPath.startsWith("~/") || inputPath.startsWith("~\\")) {
|
|
23
|
+
return path.join(os.homedir(), inputPath.slice(2));
|
|
24
|
+
}
|
|
25
|
+
return inputPath;
|
|
26
|
+
}
|
|
27
|
+
function resolveConfigSkillDir(inputPath) {
|
|
28
|
+
return path.resolve(expandHome(inputPath));
|
|
29
|
+
}
|
|
30
|
+
function pathWithin(targetPath, parentPath) {
|
|
31
|
+
const relative = path.relative(parentPath, targetPath);
|
|
32
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
33
|
+
}
|
|
34
|
+
function detectSkillSource(skill, configSkillDirs) {
|
|
35
|
+
if (configSkillDirs.has(skill.dirPath)) {
|
|
36
|
+
return "config";
|
|
37
|
+
}
|
|
38
|
+
const projectSkillsRoot = path.join(process.cwd(), ".orca", "skills");
|
|
39
|
+
if (pathWithin(skill.dirPath, projectSkillsRoot)) {
|
|
40
|
+
return "project";
|
|
41
|
+
}
|
|
42
|
+
const globalSkillsRoot = path.join(os.homedir(), ".orca", "skills");
|
|
43
|
+
if (pathWithin(skill.dirPath, globalSkillsRoot)) {
|
|
44
|
+
return "global";
|
|
45
|
+
}
|
|
46
|
+
// loadSkills currently only returns config/project/global entries.
|
|
47
|
+
return "config";
|
|
48
|
+
}
|
|
49
|
+
function formatSkillsTable(skills, configSkillDirs) {
|
|
50
|
+
const headers = ["Name", "Description", "Source", "Path"];
|
|
51
|
+
const rows = skills.map((skill) => [
|
|
52
|
+
skill.name,
|
|
53
|
+
skill.description,
|
|
54
|
+
detectSkillSource(skill, configSkillDirs),
|
|
55
|
+
skill.dirPath
|
|
56
|
+
]);
|
|
57
|
+
return formatTable(headers, rows);
|
|
58
|
+
}
|
|
59
|
+
export async function skillsCommandHandler(options) {
|
|
60
|
+
const config = await resolveConfig(options.config);
|
|
61
|
+
const configSkillDirs = new Set((config?.skills ?? []).map((skillPath) => resolveConfigSkillDir(skillPath)));
|
|
62
|
+
const skills = await loadSkills(config);
|
|
63
|
+
if (skills.length === 0) {
|
|
64
|
+
console.log("No skills found.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(formatSkillsTable(skills, configSkillDirs));
|
|
68
|
+
}
|
|
69
|
+
export function registerSkillsCommand(program) {
|
|
70
|
+
program
|
|
71
|
+
.command("skills")
|
|
72
|
+
.description("List available skills")
|
|
73
|
+
.option("--config <path>", "Path to orca config file")
|
|
74
|
+
.action(async (options) => skillsCommandHandler(options));
|
|
75
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { registerPrFinalizeCommand } from "./commands/pr-finalize.js";
|
|
|
13
13
|
import { registerResumeCommand } from "./commands/resume.js";
|
|
14
14
|
import { registerRunCommand } from "./commands/run.js";
|
|
15
15
|
import { registerSetupCommand } from "./commands/setup.js";
|
|
16
|
+
import { registerSkillsCommand } from "./commands/skills.js";
|
|
16
17
|
import { registerStatusCommand } from "./commands/status.js";
|
|
17
18
|
const program = new Command();
|
|
18
19
|
program.name("orca").description("Orca CLI: coordinated agent run harness").version(version);
|
|
@@ -21,6 +22,7 @@ registerAnswerCommand(program);
|
|
|
21
22
|
registerPlanCommand(program);
|
|
22
23
|
registerStatusCommand(program);
|
|
23
24
|
registerListCommand(program);
|
|
25
|
+
registerSkillsCommand(program);
|
|
24
26
|
registerResumeCommand(program);
|
|
25
27
|
registerCancelCommand(program);
|
|
26
28
|
registerPrCommand(program);
|
|
@@ -3,6 +3,7 @@ import { access } from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { parseClaudeEffort, parseCodexEffort } from "../types/effort.js";
|
|
6
7
|
function isObject(value) {
|
|
7
8
|
return typeof value === "object" && value !== null;
|
|
8
9
|
}
|
|
@@ -54,6 +55,28 @@ function coerceConfig(candidate) {
|
|
|
54
55
|
throw new Error(`Config.executor must be 'claude' or 'codex', got ${String(candidate.executor)}`);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
if ("claude" in candidate && candidate.claude !== undefined) {
|
|
59
|
+
if (!isObject(candidate.claude)) {
|
|
60
|
+
throw new Error(`Config.claude must be an object, got ${describeType(candidate.claude)}`);
|
|
61
|
+
}
|
|
62
|
+
if ("effort" in candidate.claude && candidate.claude.effort !== undefined) {
|
|
63
|
+
if (typeof candidate.claude.effort !== "string") {
|
|
64
|
+
throw new Error(`Config.claude.effort must be a string, got ${describeType(candidate.claude.effort)}`);
|
|
65
|
+
}
|
|
66
|
+
parseClaudeEffort(candidate.claude.effort);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if ("codex" in candidate && candidate.codex !== undefined) {
|
|
70
|
+
if (!isObject(candidate.codex)) {
|
|
71
|
+
throw new Error(`Config.codex must be an object, got ${describeType(candidate.codex)}`);
|
|
72
|
+
}
|
|
73
|
+
if ("effort" in candidate.codex && candidate.codex.effort !== undefined) {
|
|
74
|
+
if (typeof candidate.codex.effort !== "string") {
|
|
75
|
+
throw new Error(`Config.codex.effort must be a string, got ${describeType(candidate.codex.effort)}`);
|
|
76
|
+
}
|
|
77
|
+
parseCodexEffort(candidate.codex.effort);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
57
80
|
return candidate;
|
|
58
81
|
}
|
|
59
82
|
export async function loadConfig(configPath) {
|
package/dist/core/planner.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
-
import { planSpec } from "../agents/claude/session.js";
|
|
2
|
+
import { planSpec as planSpecWithClaude } from "../agents/claude/session.js";
|
|
3
|
+
import { planSpec as planSpecWithCodex } from "../agents/codex/session.js";
|
|
3
4
|
import { logger } from "../utils/logger.js";
|
|
4
5
|
import { loadSkills } from "../utils/skill-loader.js";
|
|
5
6
|
import { validateDAG } from "./dependency-graph.js";
|
|
6
7
|
const DEFAULT_SYSTEM_CONTEXT = "You are Orca planner.";
|
|
7
|
-
let
|
|
8
|
+
let testPlanSpecOverride = null;
|
|
8
9
|
export function setPlanSpecForTests(fn) {
|
|
9
|
-
|
|
10
|
+
testPlanSpecOverride = fn;
|
|
11
|
+
}
|
|
12
|
+
function resolvePlanSpecImpl(config) {
|
|
13
|
+
if (testPlanSpecOverride) {
|
|
14
|
+
return testPlanSpecOverride;
|
|
15
|
+
}
|
|
16
|
+
const executor = config?.executor ?? "codex";
|
|
17
|
+
return executor === "claude" ? planSpecWithClaude : planSpecWithCodex;
|
|
10
18
|
}
|
|
11
19
|
function formatSkillsSection(skills) {
|
|
12
20
|
const formattedSkills = skills.map((skill) => [
|
|
@@ -24,6 +32,7 @@ export async function runPlanner(specPath, store, runId, config) {
|
|
|
24
32
|
const systemContext = skills.length === 0
|
|
25
33
|
? DEFAULT_SYSTEM_CONTEXT
|
|
26
34
|
: `${DEFAULT_SYSTEM_CONTEXT}\n\n${formatSkillsSection(skills)}`;
|
|
35
|
+
const planSpecImpl = resolvePlanSpecImpl(config);
|
|
27
36
|
const result = await planSpecImpl(spec, systemContext, config);
|
|
28
37
|
validateDAG(result.tasks);
|
|
29
38
|
await store.writeTasks(runId, result.tasks);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const CODEX_EFFORT_VALUES = ["low", "medium", "high"];
|
|
2
|
+
export const CLAUDE_EFFORT_VALUES = ["low", "medium", "high", "max"];
|
|
3
|
+
function formatAllowed(values) {
|
|
4
|
+
return values.map((value) => `'${value}'`).join(", ");
|
|
5
|
+
}
|
|
6
|
+
function parseEffort(raw, allowed, label) {
|
|
7
|
+
if (allowed.includes(raw)) {
|
|
8
|
+
return raw;
|
|
9
|
+
}
|
|
10
|
+
throw new Error(`${label} must be one of ${formatAllowed(allowed)}, got '${raw}'`);
|
|
11
|
+
}
|
|
12
|
+
export function parseCodexEffort(raw) {
|
|
13
|
+
return parseEffort(raw, CODEX_EFFORT_VALUES, "Codex effort");
|
|
14
|
+
}
|
|
15
|
+
export function parseClaudeEffort(raw) {
|
|
16
|
+
return parseEffort(raw, CLAUDE_EFFORT_VALUES, "Claude effort");
|
|
17
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function tryParseJson(input) {
|
|
2
|
+
try {
|
|
3
|
+
return JSON.parse(input);
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function extractFencedCandidates(text) {
|
|
10
|
+
const candidates = [];
|
|
11
|
+
const fenceRegex = /```(?:json)?\s*\n?([\s\S]*?)\n?```/gi;
|
|
12
|
+
for (const match of text.matchAll(fenceRegex)) {
|
|
13
|
+
const body = match[1]?.trim();
|
|
14
|
+
if (body) {
|
|
15
|
+
candidates.push(body);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return candidates;
|
|
19
|
+
}
|
|
20
|
+
function extractFirstJsonObjectOrArray(text) {
|
|
21
|
+
for (let start = 0; start < text.length; start += 1) {
|
|
22
|
+
const first = text[start];
|
|
23
|
+
if (first !== "{" && first !== "[") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
let depth = 0;
|
|
27
|
+
let inString = false;
|
|
28
|
+
let escaped = false;
|
|
29
|
+
for (let end = start; end < text.length; end += 1) {
|
|
30
|
+
const ch = text[end];
|
|
31
|
+
if (inString) {
|
|
32
|
+
if (escaped) {
|
|
33
|
+
escaped = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (ch === "\\") {
|
|
37
|
+
escaped = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ch === '"') {
|
|
41
|
+
inString = false;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '"') {
|
|
46
|
+
inString = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ch === "{" || ch === "[") {
|
|
50
|
+
depth += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === "}" || ch === "]") {
|
|
54
|
+
depth -= 1;
|
|
55
|
+
if (depth === 0) {
|
|
56
|
+
const candidate = text.slice(start, end + 1).trim();
|
|
57
|
+
if (tryParseJson(candidate) !== null) {
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
export function parseAgentJson(raw) {
|
|
68
|
+
const text = raw.trim();
|
|
69
|
+
const candidates = [
|
|
70
|
+
text,
|
|
71
|
+
...extractFencedCandidates(text),
|
|
72
|
+
];
|
|
73
|
+
const extracted = extractFirstJsonObjectOrArray(text);
|
|
74
|
+
if (extracted) {
|
|
75
|
+
candidates.push(extracted);
|
|
76
|
+
}
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
const parsed = tryParseJson(candidate);
|
|
79
|
+
if (parsed !== null) {
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw new Error("Response did not contain valid JSON object/array");
|
|
84
|
+
}
|