orcastrator 0.2.6 → 0.2.8
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.
|
@@ -50,7 +50,7 @@ function extractAssistantText(message) {
|
|
|
50
50
|
.trim();
|
|
51
51
|
return text.length > 0 ? text : null;
|
|
52
52
|
}
|
|
53
|
-
function parseTaskArray(raw) {
|
|
53
|
+
export function parseTaskArray(raw) {
|
|
54
54
|
const parsed = JSON.parse(raw);
|
|
55
55
|
if (!Array.isArray(parsed)) {
|
|
56
56
|
throw new Error("Claude plan response was not a JSON array");
|
|
@@ -63,11 +63,18 @@ function parseTaskArray(raw) {
|
|
|
63
63
|
dependencies: Array.isArray(task.dependencies)
|
|
64
64
|
? task.dependencies.map(String)
|
|
65
65
|
: [],
|
|
66
|
+
// Apply sensible defaults for required fields the LLM may have omitted.
|
|
67
|
+
name: typeof task.name === "string" && task.name.length > 0 ? task.name : "Unnamed task",
|
|
68
|
+
description: typeof task.description === "string" ? task.description : "",
|
|
69
|
+
acceptance_criteria: Array.isArray(task.acceptance_criteria) ? task.acceptance_criteria : [],
|
|
70
|
+
status: typeof task.status === "string" && task.status.length > 0 ? task.status : "pending",
|
|
71
|
+
retries: typeof task.retries === "number" ? task.retries : 0,
|
|
72
|
+
maxRetries: typeof task.maxRetries === "number" ? task.maxRetries : 3,
|
|
66
73
|
}));
|
|
67
74
|
}
|
|
68
|
-
function parseTaskExecution(raw) {
|
|
75
|
+
export function parseTaskExecution(raw) {
|
|
69
76
|
const parsed = JSON.parse(raw);
|
|
70
|
-
if (!parsed || typeof parsed !== "object") {
|
|
77
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
71
78
|
throw new Error("Claude task response was not a JSON object");
|
|
72
79
|
}
|
|
73
80
|
const candidate = parsed;
|
|
@@ -112,9 +119,9 @@ async function collectSessionResult(session) {
|
|
|
112
119
|
function getModel(config) {
|
|
113
120
|
return config?.claude?.model ?? process.env.ORCA_CLAUDE_MODEL ?? "claude-sonnet-4-5";
|
|
114
121
|
}
|
|
115
|
-
export async function planSpec(spec, systemContext) {
|
|
122
|
+
export async function planSpec(spec, systemContext, config) {
|
|
116
123
|
const session = unstable_v2_createSession({
|
|
117
|
-
model: getModel()
|
|
124
|
+
model: getModel(config)
|
|
118
125
|
});
|
|
119
126
|
try {
|
|
120
127
|
const streamPromise = collectSessionResult(session);
|
|
@@ -49,6 +49,11 @@ function coerceConfig(candidate) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
if ("executor" in candidate && candidate.executor !== undefined) {
|
|
53
|
+
if (candidate.executor !== "claude" && candidate.executor !== "codex") {
|
|
54
|
+
throw new Error(`Config.executor must be 'claude' or 'codex', got ${String(candidate.executor)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
52
57
|
return candidate;
|
|
53
58
|
}
|
|
54
59
|
export async function loadConfig(configPath) {
|
|
@@ -62,7 +67,7 @@ export async function loadConfig(configPath) {
|
|
|
62
67
|
const configCandidate = "default" in importedModule ? importedModule.default : importedModule;
|
|
63
68
|
return coerceConfig(configCandidate);
|
|
64
69
|
}
|
|
65
|
-
const TOP_LEVEL_SCALARS = ["runsDir", "sessionLogs", "maxRetries", "anthropicApiKey", "openaiApiKey"];
|
|
70
|
+
const TOP_LEVEL_SCALARS = ["runsDir", "sessionLogs", "maxRetries", "anthropicApiKey", "openaiApiKey", "executor"];
|
|
66
71
|
export function mergeConfigs(...configs) {
|
|
67
72
|
const presentConfigs = configs.filter((config) => config !== undefined);
|
|
68
73
|
if (presentConfigs.length === 0) {
|
package/dist/core/planner.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function runPlanner(specPath, store, runId, config) {
|
|
|
24
24
|
const systemContext = skills.length === 0
|
|
25
25
|
? DEFAULT_SYSTEM_CONTEXT
|
|
26
26
|
: `${DEFAULT_SYSTEM_CONTEXT}\n\n${formatSkillsSection(skills)}`;
|
|
27
|
-
const result = await planSpecImpl(spec, systemContext);
|
|
27
|
+
const result = await planSpecImpl(spec, systemContext, config);
|
|
28
28
|
validateDAG(result.tasks);
|
|
29
29
|
await store.writeTasks(runId, result.tasks);
|
|
30
30
|
await store.updateRun(runId, {
|
package/dist/core/task-runner.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
|
-
import
|
|
3
|
+
import * as claudeAgent from "../agents/claude/session.js";
|
|
4
|
+
import { createCodexSession } from "../agents/codex/session.js";
|
|
4
5
|
import { loadSkills } from "../utils/skill-loader.js";
|
|
5
6
|
import { getRunnable, validateDAG } from "./dependency-graph.js";
|
|
6
7
|
import { shouldRetry } from "./retry-policy.js";
|
|
7
|
-
|
|
8
|
+
// Non-null only when set by tests — null means "use real executor logic"
|
|
9
|
+
let testExecuteTaskOverride = null;
|
|
8
10
|
export function setExecuteTaskForTests(fn) {
|
|
9
|
-
|
|
11
|
+
testExecuteTaskOverride = fn;
|
|
10
12
|
}
|
|
11
13
|
function toErrorMessage(error) {
|
|
12
14
|
if (error instanceof Error) {
|
|
@@ -94,15 +96,39 @@ async function writeSessionSummary(store, runId, sessionLogsDir) {
|
|
|
94
96
|
}
|
|
95
97
|
export async function runTaskRunner(options) {
|
|
96
98
|
const emitHook = options.emitHook ?? defaultEmitHook;
|
|
97
|
-
const executeTaskFn = options.executeTask ?? executeTaskImpl;
|
|
98
99
|
const { runId, store, config } = options;
|
|
99
100
|
const skills = await loadSkills(config);
|
|
100
101
|
const taskSystemContext = skills.length === 0 ? undefined : formatSkillsSection(skills);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// Test mocks bypass all executor logic entirely — no real sessions created.
|
|
103
|
+
const mockFn = options.executeTask ?? testExecuteTaskOverride;
|
|
104
|
+
// Build real executor (Codex persistent session or Claude stateless fallback).
|
|
105
|
+
// Only runs in production — skipped completely when a mock is active.
|
|
106
|
+
let codexSession;
|
|
107
|
+
let executeTaskFn;
|
|
108
|
+
if (mockFn) {
|
|
109
|
+
executeTaskFn = mockFn;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const executor = config?.executor ?? "codex";
|
|
113
|
+
if (executor === "codex") {
|
|
114
|
+
try {
|
|
115
|
+
codexSession = await createCodexSession(process.cwd(), config);
|
|
116
|
+
executeTaskFn = (task, taskRunId, _cfg, systemContext) => codexSession.executeTask(task, taskRunId, systemContext);
|
|
117
|
+
}
|
|
118
|
+
catch (sessionError) {
|
|
119
|
+
console.warn(`[orca] Codex session init failed, falling back to Claude: ${toErrorMessage(sessionError)}`);
|
|
120
|
+
executeTaskFn = claudeAgent.executeTask;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
executeTaskFn = claudeAgent.executeTask;
|
|
125
|
+
}
|
|
104
126
|
}
|
|
105
127
|
try {
|
|
128
|
+
let run = await store.getRun(runId);
|
|
129
|
+
if (!run) {
|
|
130
|
+
throw new Error(`Run not found: ${runId}`);
|
|
131
|
+
}
|
|
106
132
|
validateDAG(run.tasks);
|
|
107
133
|
await emitHook({
|
|
108
134
|
runId: run.runId,
|
|
@@ -320,4 +346,14 @@ export async function runTaskRunner(options) {
|
|
|
320
346
|
await writeSessionSummary(store, runId, config?.sessionLogs);
|
|
321
347
|
throw error;
|
|
322
348
|
}
|
|
349
|
+
finally {
|
|
350
|
+
if (codexSession) {
|
|
351
|
+
try {
|
|
352
|
+
await codexSession.disconnect();
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// Best-effort cleanup — don't mask the real error.
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
323
359
|
}
|
|
@@ -59,7 +59,12 @@ export async function loadSkill(skillDirPath) {
|
|
|
59
59
|
skillFileContent = await readFile(skillFilePath, "utf8");
|
|
60
60
|
}
|
|
61
61
|
catch (error) {
|
|
62
|
-
|
|
62
|
+
const errorCode = error.code;
|
|
63
|
+
if (errorCode === "ENOENT") {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (errorCode === "EACCES" || errorCode === "EPERM") {
|
|
67
|
+
console.warn(`Skipping skill at ${expandedDirPath}: unable to read SKILL.md (${errorCode})`);
|
|
63
68
|
return null;
|
|
64
69
|
}
|
|
65
70
|
throw error;
|
|
@@ -87,13 +92,21 @@ export async function loadSkillsFromDir(skillsDirPath) {
|
|
|
87
92
|
throw error;
|
|
88
93
|
}
|
|
89
94
|
const subdirectories = entries
|
|
90
|
-
.filter((entry) =>
|
|
95
|
+
.filter((entry) => {
|
|
96
|
+
if (entry.isSymbolicLink()) {
|
|
97
|
+
console.warn(`Skipping symlinked skill entry: ${path.join(expandedDirPath, entry.name)}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return entry.isDirectory();
|
|
101
|
+
})
|
|
91
102
|
.map((entry) => entry.name)
|
|
92
103
|
.sort((a, b) => a.localeCompare(b));
|
|
93
104
|
const loaded = await Promise.all(subdirectories.map((subdirName) => loadSkill(path.join(expandedDirPath, subdirName))));
|
|
94
105
|
return loaded.filter((skill) => skill !== null);
|
|
95
106
|
}
|
|
96
107
|
export async function loadSkills(config) {
|
|
108
|
+
// NOTE: config.skills paths are currently resolved relative to process.cwd().
|
|
109
|
+
// A future improvement can resolve them relative to the config file location.
|
|
97
110
|
const fromConfig = await Promise.all((config?.skills ?? []).map((skillPath) => loadSkill(skillPath)));
|
|
98
111
|
const projectSkills = await loadSkillsFromDir(path.join(process.cwd(), ".orca", "skills"));
|
|
99
112
|
const globalSkills = await loadSkillsFromDir(path.join(os.homedir(), ".orca", "skills"));
|