orcastrator 0.2.7 → 0.2.9
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);
|
|
@@ -26,9 +26,11 @@ function buildTaskExecutionPrompt(task, runId, cwd, systemContext) {
|
|
|
26
26
|
"Acceptance Criteria:",
|
|
27
27
|
...task.acceptance_criteria.map((criterion, index) => `${index + 1}. ${criterion}`),
|
|
28
28
|
"Execute this task. You have full shell access — run commands, read/write files, and do whatever is needed.",
|
|
29
|
-
"When done, output
|
|
30
|
-
'{"outcome":"done"
|
|
31
|
-
"
|
|
29
|
+
"IMPORTANT: When done, you MUST output the following JSON on its own line as the very last line of your response (no trailing text after it):",
|
|
30
|
+
'{"outcome":"done"}',
|
|
31
|
+
"Or if the task failed:",
|
|
32
|
+
'{"outcome":"failed","error":"short reason"}',
|
|
33
|
+
"Do not wrap it in markdown fences. Do not add any text after the JSON line. The JSON line is required.",
|
|
32
34
|
].join("\n\n");
|
|
33
35
|
}
|
|
34
36
|
function extractAgentText(result) {
|
|
@@ -83,6 +85,21 @@ const POSITIVE_COMPLETION_PATTERNS = [
|
|
|
83
85
|
/\bwritten\b/i,
|
|
84
86
|
/\bcreated\b/i,
|
|
85
87
|
/\bfinished\b/i,
|
|
88
|
+
// Additional patterns to catch natural-language Codex narration
|
|
89
|
+
/\bapplied\b/i,
|
|
90
|
+
/\bimplemented\b/i,
|
|
91
|
+
/\badded\b/i,
|
|
92
|
+
/\bupdated\b/i,
|
|
93
|
+
/\bmodified\b/i,
|
|
94
|
+
/\binstalled\b/i,
|
|
95
|
+
/\bfixed\b/i,
|
|
96
|
+
/\brefactored\b/i,
|
|
97
|
+
/\bchanges?\s+(?:have\s+been\s+)?made\b/i,
|
|
98
|
+
/\bthe\s+task\s+(?:has\s+been|is)\b/i,
|
|
99
|
+
/\bi\s+have\b/i,
|
|
100
|
+
/\ball\s+(?:tasks?|steps?|criteria)\b/i,
|
|
101
|
+
/\btest(?:s|ing)?\s+pass/i,
|
|
102
|
+
/\bno\s+(?:errors?|issues?|failures?)\b/i,
|
|
86
103
|
];
|
|
87
104
|
const FAILURE_PATTERNS = [
|
|
88
105
|
/\berror\b/i,
|
|
@@ -176,6 +193,22 @@ export async function createCodexSession(cwd, config) {
|
|
|
176
193
|
],
|
|
177
194
|
});
|
|
178
195
|
const rawResponse = extractAgentText(result);
|
|
196
|
+
// Primary signal: use the SDK's structured turn status.
|
|
197
|
+
const status = result.turn.status;
|
|
198
|
+
if (status === "completed") {
|
|
199
|
+
return { outcome: "done", rawResponse };
|
|
200
|
+
}
|
|
201
|
+
if (status === "failed") {
|
|
202
|
+
return {
|
|
203
|
+
outcome: "failed",
|
|
204
|
+
error: result.turn.error?.message ?? "Turn failed",
|
|
205
|
+
rawResponse,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (status === "interrupted") {
|
|
209
|
+
return { outcome: "failed", error: "Turn was interrupted", rawResponse };
|
|
210
|
+
}
|
|
211
|
+
// Fallback: status is unexpected/missing — parse text as before.
|
|
179
212
|
return parseTaskExecution(rawResponse);
|
|
180
213
|
},
|
|
181
214
|
async consultTaskGraph(tasks) {
|
|
@@ -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) {
|
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
|
@@ -124,11 +124,11 @@ export async function runTaskRunner(options) {
|
|
|
124
124
|
executeTaskFn = claudeAgent.executeTask;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
let run = await store.getRun(runId);
|
|
128
|
-
if (!run) {
|
|
129
|
-
throw new Error(`Run not found: ${runId}`);
|
|
130
|
-
}
|
|
131
127
|
try {
|
|
128
|
+
let run = await store.getRun(runId);
|
|
129
|
+
if (!run) {
|
|
130
|
+
throw new Error(`Run not found: ${runId}`);
|
|
131
|
+
}
|
|
132
132
|
validateDAG(run.tasks);
|
|
133
133
|
await emitHook({
|
|
134
134
|
runId: run.runId,
|
|
@@ -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"));
|