orcastrator 0.2.7 → 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) {
@@ -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, {
@@ -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
- if (error.code === "ENOENT") {
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) => entry.isDirectory())
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"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orcastrator",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "orca": "dist/cli/index.js"