@caseyharalson/orrery 0.7.1

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/.devcontainer.example/Dockerfile +149 -0
  2. package/.devcontainer.example/devcontainer.json +61 -0
  3. package/.devcontainer.example/init-firewall.sh +175 -0
  4. package/LICENSE +21 -0
  5. package/README.md +139 -0
  6. package/agent/skills/discovery/SKILL.md +428 -0
  7. package/agent/skills/discovery/schemas/plan-schema.yaml +138 -0
  8. package/agent/skills/orrery-execute/SKILL.md +107 -0
  9. package/agent/skills/orrery-report/SKILL.md +119 -0
  10. package/agent/skills/orrery-review/SKILL.md +105 -0
  11. package/agent/skills/orrery-verify/SKILL.md +105 -0
  12. package/agent/skills/refine-plan/SKILL.md +291 -0
  13. package/agent/skills/simulate-plan/SKILL.md +244 -0
  14. package/bin/orrery.js +5 -0
  15. package/lib/cli/commands/help.js +21 -0
  16. package/lib/cli/commands/ingest-plan.js +56 -0
  17. package/lib/cli/commands/init.js +21 -0
  18. package/lib/cli/commands/install-devcontainer.js +97 -0
  19. package/lib/cli/commands/install-skills.js +182 -0
  20. package/lib/cli/commands/orchestrate.js +27 -0
  21. package/lib/cli/commands/resume.js +146 -0
  22. package/lib/cli/commands/status.js +137 -0
  23. package/lib/cli/commands/validate-plan.js +288 -0
  24. package/lib/cli/index.js +57 -0
  25. package/lib/orchestration/agent-invoker.js +595 -0
  26. package/lib/orchestration/condensed-plan.js +128 -0
  27. package/lib/orchestration/config.js +213 -0
  28. package/lib/orchestration/dependency-resolver.js +149 -0
  29. package/lib/orchestration/edit-invoker.js +115 -0
  30. package/lib/orchestration/index.js +1065 -0
  31. package/lib/orchestration/plan-loader.js +212 -0
  32. package/lib/orchestration/progress-tracker.js +208 -0
  33. package/lib/orchestration/report-format.js +80 -0
  34. package/lib/orchestration/review-invoker.js +305 -0
  35. package/lib/utils/agent-detector.js +47 -0
  36. package/lib/utils/git.js +297 -0
  37. package/lib/utils/paths.js +43 -0
  38. package/lib/utils/plan-detect.js +24 -0
  39. package/lib/utils/skill-copier.js +79 -0
  40. package/package.json +58 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Orchestrator Configuration
3
+ *
4
+ * This file configures the plan orchestrator including agent commands,
5
+ * concurrency settings, and directory paths.
6
+ */
7
+
8
+ const { getFormatInstructions } = require("./report-format");
9
+ const { detectInstalledAgents } = require("../utils/agent-detector");
10
+
11
+ // Helper function to get agent priority from environment or default
12
+ function getAgentPriority() {
13
+ const envPriority = process.env.ORRERY_AGENT_PRIORITY;
14
+ if (envPriority && envPriority.trim()) {
15
+ return envPriority
16
+ .trim()
17
+ .split(",")
18
+ .map((s) => s.trim());
19
+ }
20
+ return ["codex", "gemini", "claude"];
21
+ }
22
+
23
+ // Filter agent priority to only include agents that are installed (have config directories)
24
+ function getInstalledAgentPriority() {
25
+ const requestedAgents = getAgentPriority();
26
+ const installedAgents = detectInstalledAgents();
27
+
28
+ const filteredAgents = requestedAgents.filter((agent) =>
29
+ installedAgents.includes(agent)
30
+ );
31
+
32
+ // Warn about agents in priority that aren't installed
33
+ const skippedAgents = requestedAgents.filter(
34
+ (agent) => !installedAgents.includes(agent)
35
+ );
36
+ if (skippedAgents.length > 0) {
37
+ const missingDirs = skippedAgents.map((a) => `~/.${a}`).join(", ");
38
+ console.warn(
39
+ `[config] Skipping unconfigured agents: ${skippedAgents.join(", ")} (missing: ${missingDirs})`
40
+ );
41
+ }
42
+
43
+ if (filteredAgents.length === 0) {
44
+ console.warn(
45
+ `[config] No configured agents found. Install an agent CLI and create its config directory (e.g., ~/.claude, ~/.codex, ~/.gemini)`
46
+ );
47
+ }
48
+
49
+ return filteredAgents;
50
+ }
51
+
52
+ // Shared prompt for all worker agents
53
+ const WORKER_PROMPT = `You are a Worker Agent executing plan steps.
54
+
55
+ Plan file: {planFile}
56
+ Steps to execute: {stepIds}
57
+
58
+ ## Plan Format Note
59
+
60
+ The plan file may be a condensed version containing only your assigned steps
61
+ and their completed dependencies (indicated by \`condensed: true\` in metadata).
62
+ All necessary context is included - do not reference the source plan.
63
+
64
+ ## Workflow
65
+
66
+ For each step:
67
+
68
+ 1. Read the plan file to understand the step's requirements, criteria, and files
69
+ 2. Execute: Implement the changes following project conventions. Do NOT commit - the orchestrator handles commits.
70
+ 3. Verify: Run tests and confirm acceptance criteria are met. Fix issues before proceeding.
71
+ 4. Report: Output a JSON result for the step (see format below)
72
+
73
+ Use /orrery-execute, /orrery-verify, and /orrery-report skills for detailed guidance on each phase.
74
+
75
+ ${getFormatInstructions()}
76
+
77
+ ## Exit Codes
78
+
79
+ - Exit 0: All steps completed successfully
80
+ - Exit 1: One or more steps blocked
81
+
82
+ ## Rules
83
+
84
+ - The plan file is READ-ONLY—never modify it
85
+ - Complete each step fully before starting the next
86
+ - Output clean JSON to stdout—no extra text or markdown wrapping`;
87
+
88
+ const REVIEW_PROMPT = `You are a Review Agent. Evaluate the changes for the completed plan step.
89
+
90
+ ## Context
91
+
92
+ Step context:
93
+ {stepContext}
94
+
95
+ Modified files:
96
+ {files}
97
+
98
+ Diff:
99
+ {diff}
100
+
101
+ ## Instructions
102
+
103
+ - Identify bugs, regressions, missing edge cases, or unmet acceptance criteria.
104
+ - If changes are acceptable, respond with approval.
105
+ - If changes need edits, provide specific, actionable feedback.
106
+ - Keep feedback concise and grounded in the diff.
107
+
108
+ ## Output Format (JSON, single line)
109
+
110
+ {"status":"approved","summary":"..."}
111
+ OR
112
+ {"status":"changes_requested","summary":"...","comments":["...","..."]}`;
113
+
114
+ module.exports = {
115
+ // Agent configurations (keyed by agent name)
116
+ agents: {
117
+ claude: {
118
+ command: "claude",
119
+ args: [
120
+ "--model",
121
+ "sonnet",
122
+ "--dangerously-skip-permissions",
123
+ "-p",
124
+ WORKER_PROMPT
125
+ ]
126
+ },
127
+ codex: {
128
+ command: "codex",
129
+ args: ["exec", "--yolo", WORKER_PROMPT],
130
+ // Codex writes progress to stderr and final result to stdout
131
+ stderrIsProgress: true
132
+ },
133
+ gemini: {
134
+ command: "gemini",
135
+ args: ["--yolo", "-p", WORKER_PROMPT],
136
+ // Gemini writes progress to stderr and final result to stdout
137
+ stderrIsProgress: true
138
+ }
139
+ },
140
+
141
+ // Default agent to use when failover is disabled
142
+ defaultAgent: "codex",
143
+
144
+ // Agent priority list for failover (tried in order, filtered to installed agents)
145
+ agentPriority: getInstalledAgentPriority(),
146
+
147
+ // Failover configuration
148
+ failover: {
149
+ // Enable/disable failover behavior
150
+ enabled: true,
151
+
152
+ // Timeout in milliseconds before trying next agent (15 minutes)
153
+ timeoutMs: 900000,
154
+
155
+ // Patterns to detect failover-triggering errors from stderr
156
+ errorPatterns: {
157
+ // API/connection errors
158
+ apiError: [
159
+ /API error/i,
160
+ /connection refused/i,
161
+ /ECONNRESET/i,
162
+ /ETIMEDOUT/i,
163
+ /network error/i,
164
+ /rate limit/i,
165
+ /429/,
166
+ /502/,
167
+ /503/
168
+ ],
169
+ // Token/context limit errors
170
+ tokenLimit: [
171
+ /token limit/i,
172
+ /context.*(limit|length|exceeded)/i,
173
+ /maximum.*tokens/i,
174
+ /too long/i
175
+ ]
176
+ }
177
+ },
178
+
179
+ // Concurrency control
180
+ concurrency: {
181
+ maxParallel: 1, // Parallel disabled - see TODO below
182
+ // TODO: To re-enable parallel execution:
183
+ // 1. Set maxParallel > 1
184
+ // 2. Bundle commits: after ALL parallel agents complete, commit once with combined messages
185
+ // 3. Update processPlan() to collect all parallel results before committing
186
+ pollInterval: 5000
187
+ },
188
+
189
+ // Retry policy for failed steps
190
+ retry: {
191
+ // Maximum retry attempts per step
192
+ maxAttempts: 1,
193
+ // Delay in ms before retrying
194
+ backoffMs: 5000
195
+ },
196
+
197
+ // Logging options
198
+ logging: {
199
+ // Show agent stdout in real-time
200
+ streamOutput: true,
201
+ // Prefix format for agent output ("[step-1]" or "[step-1,step-2]")
202
+ prefixFormat: "step"
203
+ },
204
+
205
+ // Review/edit loop configuration
206
+ review: {
207
+ enabled: false,
208
+ maxIterations: 3,
209
+ prompt: REVIEW_PROMPT
210
+ },
211
+
212
+ REVIEW_PROMPT
213
+ };
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Dependency resolution utilities for plan step execution ordering.
5
+ * Uses Kahn's algorithm for topological sorting.
6
+ */
7
+
8
+ /**
9
+ * Get steps that are ready to execute (pending with all deps satisfied)
10
+ * @param {Object} plan - The plan object
11
+ * @returns {Array} - Array of step objects ready to execute
12
+ */
13
+ function getReadySteps(plan) {
14
+ const completed = plan.getCompletedSteps();
15
+
16
+ return plan.steps.filter((step) => {
17
+ // Must be pending
18
+ if (step.status !== "pending") return false;
19
+
20
+ // All dependencies must be complete (not pending, not blocked, not in_progress)
21
+ const deps = step.deps || [];
22
+ return deps.every((depId) => completed.has(depId));
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Group steps for parallel execution based on dependency levels.
28
+ * Uses Kahn's algorithm to find execution groups.
29
+ * @param {Array} steps - Array of step objects (with id, deps, parallel fields)
30
+ * @returns {Array<Array>} - Array of step groups, each group can run in parallel
31
+ */
32
+ function resolveExecutionGroups(steps) {
33
+ const stepMap = new Map(steps.map((s) => [s.id, s]));
34
+ const remaining = new Set(steps.map((s) => s.id));
35
+ const groups = [];
36
+
37
+ while (remaining.size > 0) {
38
+ // Find all steps with no remaining dependencies
39
+ const ready = [...remaining].filter((id) => {
40
+ const step = stepMap.get(id);
41
+ const deps = step.deps || [];
42
+ return deps.every((dep) => !remaining.has(dep));
43
+ });
44
+
45
+ if (ready.length === 0 && remaining.size > 0) {
46
+ const cycleSteps = [...remaining].join(", ");
47
+ throw new Error(
48
+ `Circular dependency detected among steps: ${cycleSteps}`
49
+ );
50
+ }
51
+
52
+ // Separate parallel and serial steps
53
+ const parallelSteps = ready
54
+ .filter((id) => stepMap.get(id).parallel === true)
55
+ .map((id) => stepMap.get(id));
56
+
57
+ const serialSteps = ready
58
+ .filter((id) => stepMap.get(id).parallel !== true)
59
+ .map((id) => stepMap.get(id));
60
+
61
+ // Add parallel steps as a single group (can run together)
62
+ if (parallelSteps.length > 0) {
63
+ groups.push(parallelSteps);
64
+ }
65
+
66
+ // Add serial steps as individual groups (run one at a time)
67
+ for (const step of serialSteps) {
68
+ groups.push([step]);
69
+ }
70
+
71
+ // Remove processed steps from remaining
72
+ for (const id of ready) {
73
+ remaining.delete(id);
74
+ }
75
+ }
76
+
77
+ return groups;
78
+ }
79
+
80
+ /**
81
+ * Check if a plan has any circular dependencies
82
+ * @param {Object} plan - The plan object
83
+ * @returns {{hasCycle: boolean, cycleSteps?: string[]}} - Result with cycle info
84
+ */
85
+ function detectCycles(plan) {
86
+ try {
87
+ resolveExecutionGroups(plan.steps);
88
+ return { hasCycle: false };
89
+ } catch (error) {
90
+ if (error.message.includes("Circular dependency")) {
91
+ // Extract step IDs from error message
92
+ const match = error.message.match(/steps: (.+)$/);
93
+ const cycleSteps = match ? match[1].split(", ") : [];
94
+ return { hasCycle: true, cycleSteps };
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get steps that are blocked due to a specific step being blocked
102
+ * (i.e., steps that depend on the blocked step directly or transitively)
103
+ * @param {Object} plan - The plan object
104
+ * @param {string} blockedStepId - ID of the blocked step
105
+ * @returns {string[]} - Array of step IDs that are blocked as a result
106
+ */
107
+ function getBlockedDependents(plan, blockedStepId) {
108
+ const dependents = new Set();
109
+
110
+ function findDependents(stepId) {
111
+ for (const step of plan.steps) {
112
+ const deps = step.deps || [];
113
+ if (deps.includes(stepId) && !dependents.has(step.id)) {
114
+ dependents.add(step.id);
115
+ findDependents(step.id);
116
+ }
117
+ }
118
+ }
119
+
120
+ findDependents(blockedStepId);
121
+ return [...dependents];
122
+ }
123
+
124
+ /**
125
+ * Partition ready steps into groups respecting maxParallel limit
126
+ * @param {Array} readySteps - Steps ready to execute
127
+ * @param {number} maxParallel - Maximum concurrent steps
128
+ * @param {number} currentlyRunning - Number of steps currently in progress
129
+ * @returns {{parallel: Array, serial: Array}} - Steps grouped by execution type
130
+ */
131
+ function partitionSteps(readySteps, maxParallel, currentlyRunning = 0) {
132
+ const availableSlots = Math.max(0, maxParallel - currentlyRunning);
133
+
134
+ const parallel = readySteps.filter((s) => s.parallel === true);
135
+ const serial = readySteps.filter((s) => s.parallel !== true);
136
+
137
+ return {
138
+ parallel: parallel.slice(0, availableSlots),
139
+ serial: serial.slice(0, Math.max(0, availableSlots - parallel.length))
140
+ };
141
+ }
142
+
143
+ module.exports = {
144
+ getReadySteps,
145
+ resolveExecutionGroups,
146
+ detectCycles,
147
+ getBlockedDependents,
148
+ partitionSteps
149
+ };
@@ -0,0 +1,115 @@
1
+ const {
2
+ invokeAgentWithFailover,
3
+ parseAgentResults
4
+ } = require("./agent-invoker");
5
+
6
+ function getWorkerPromptTemplate(config) {
7
+ if (config && typeof config.WORKER_PROMPT === "string") {
8
+ return config.WORKER_PROMPT;
9
+ }
10
+
11
+ const agents = (config && config.agents) || {};
12
+ const preferredAgent =
13
+ (config && config.defaultAgent && agents[config.defaultAgent]) ||
14
+ Object.values(agents)[0];
15
+
16
+ if (preferredAgent && Array.isArray(preferredAgent.args)) {
17
+ const lastArg = preferredAgent.args[preferredAgent.args.length - 1];
18
+ if (typeof lastArg === "string") {
19
+ return lastArg;
20
+ }
21
+ }
22
+
23
+ return "";
24
+ }
25
+
26
+ function formatFeedbackList(feedback) {
27
+ if (!Array.isArray(feedback) || feedback.length === 0) {
28
+ return "No review feedback items provided.";
29
+ }
30
+
31
+ return feedback
32
+ .map((entry, index) => {
33
+ const item =
34
+ entry && typeof entry === "object" ? entry : { comment: String(entry) };
35
+ const file =
36
+ typeof item.file === "string" && item.file.trim()
37
+ ? item.file.trim()
38
+ : "(not specified)";
39
+ const line = Number.isFinite(item.line) ? ` line: ${item.line}` : "";
40
+ const severity =
41
+ typeof item.severity === "string" && item.severity.trim()
42
+ ? item.severity.trim()
43
+ : "suggestion";
44
+ const comment =
45
+ typeof item.comment === "string" && item.comment.trim()
46
+ ? item.comment.trim()
47
+ : "(no comment provided)";
48
+
49
+ return `${index + 1}. file: ${file}${line} severity: ${severity} comment: ${comment}`;
50
+ })
51
+ .join("\n");
52
+ }
53
+
54
+ function buildEditPrompt(template, planFile, stepIds, feedback) {
55
+ const stepIdsStr = Array.isArray(stepIds)
56
+ ? stepIds.join(",")
57
+ : String(stepIds);
58
+ const basePrompt = String(template || "")
59
+ .replace("{planFile}", planFile)
60
+ .replace("{stepIds}", stepIdsStr);
61
+
62
+ const feedbackSection = formatFeedbackList(feedback);
63
+
64
+ return `${basePrompt}\n\n## Review Feedback\n${feedbackSection}\n\n## Instructions\nAddress all review feedback items above before reporting the step as complete.`;
65
+ }
66
+
67
+ function buildEditConfig(config, prompt) {
68
+ const agents = {};
69
+
70
+ for (const [name, agentConfig] of Object.entries(config.agents || {})) {
71
+ const args = Array.isArray(agentConfig.args)
72
+ ? agentConfig.args.slice()
73
+ : [];
74
+ if (args.length > 0) {
75
+ args[args.length - 1] = prompt;
76
+ }
77
+ agents[name] = {
78
+ ...agentConfig,
79
+ args
80
+ };
81
+ }
82
+
83
+ return {
84
+ ...config,
85
+ agents
86
+ };
87
+ }
88
+
89
+ async function invokeEditAgent(
90
+ config,
91
+ planFile,
92
+ stepIds,
93
+ feedback,
94
+ repoRoot,
95
+ options = {}
96
+ ) {
97
+ const template = getWorkerPromptTemplate(config);
98
+ const prompt = buildEditPrompt(template, planFile, stepIds, feedback);
99
+ const editConfig = buildEditConfig(config, prompt);
100
+
101
+ const handle = invokeAgentWithFailover(
102
+ editConfig,
103
+ planFile,
104
+ stepIds,
105
+ repoRoot,
106
+ options
107
+ );
108
+
109
+ const result = await handle.completion;
110
+ return parseAgentResults(result.stdout || "");
111
+ }
112
+
113
+ module.exports = {
114
+ invokeEditAgent
115
+ };