@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.
- package/.devcontainer.example/Dockerfile +149 -0
- package/.devcontainer.example/devcontainer.json +61 -0
- package/.devcontainer.example/init-firewall.sh +175 -0
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/agent/skills/discovery/SKILL.md +428 -0
- package/agent/skills/discovery/schemas/plan-schema.yaml +138 -0
- package/agent/skills/orrery-execute/SKILL.md +107 -0
- package/agent/skills/orrery-report/SKILL.md +119 -0
- package/agent/skills/orrery-review/SKILL.md +105 -0
- package/agent/skills/orrery-verify/SKILL.md +105 -0
- package/agent/skills/refine-plan/SKILL.md +291 -0
- package/agent/skills/simulate-plan/SKILL.md +244 -0
- package/bin/orrery.js +5 -0
- package/lib/cli/commands/help.js +21 -0
- package/lib/cli/commands/ingest-plan.js +56 -0
- package/lib/cli/commands/init.js +21 -0
- package/lib/cli/commands/install-devcontainer.js +97 -0
- package/lib/cli/commands/install-skills.js +182 -0
- package/lib/cli/commands/orchestrate.js +27 -0
- package/lib/cli/commands/resume.js +146 -0
- package/lib/cli/commands/status.js +137 -0
- package/lib/cli/commands/validate-plan.js +288 -0
- package/lib/cli/index.js +57 -0
- package/lib/orchestration/agent-invoker.js +595 -0
- package/lib/orchestration/condensed-plan.js +128 -0
- package/lib/orchestration/config.js +213 -0
- package/lib/orchestration/dependency-resolver.js +149 -0
- package/lib/orchestration/edit-invoker.js +115 -0
- package/lib/orchestration/index.js +1065 -0
- package/lib/orchestration/plan-loader.js +212 -0
- package/lib/orchestration/progress-tracker.js +208 -0
- package/lib/orchestration/report-format.js +80 -0
- package/lib/orchestration/review-invoker.js +305 -0
- package/lib/utils/agent-detector.js +47 -0
- package/lib/utils/git.js +297 -0
- package/lib/utils/paths.js +43 -0
- package/lib/utils/plan-detect.js +24 -0
- package/lib/utils/skill-copier.js +79 -0
- package/package.json +58 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const { findPlanForCurrentBranch } = require("../../utils/plan-detect");
|
|
4
|
+
const { updateStepsStatus } = require("../../orchestration/plan-loader");
|
|
5
|
+
const { commit } = require("../../utils/git");
|
|
6
|
+
const { orchestrate } = require("../../orchestration");
|
|
7
|
+
|
|
8
|
+
function supportsColor() {
|
|
9
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function colorize(text, color) {
|
|
13
|
+
if (!supportsColor()) return text;
|
|
14
|
+
const colors = {
|
|
15
|
+
green: "\x1b[32m",
|
|
16
|
+
yellow: "\x1b[33m",
|
|
17
|
+
red: "\x1b[31m",
|
|
18
|
+
reset: "\x1b[0m"
|
|
19
|
+
};
|
|
20
|
+
return `${colors[color] || ""}${text}${colors.reset}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = function registerResumeCommand(program) {
|
|
24
|
+
program
|
|
25
|
+
.command("resume")
|
|
26
|
+
.description("Unblock steps and resume orchestration")
|
|
27
|
+
.option("--step <id>", "Unblock a specific step before resuming")
|
|
28
|
+
.option("--all", "Unblock all blocked steps (default behavior)")
|
|
29
|
+
.option(
|
|
30
|
+
"--dry-run",
|
|
31
|
+
"Preview what would be unblocked without making changes"
|
|
32
|
+
)
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
// 1. Find plan for current branch
|
|
35
|
+
let match;
|
|
36
|
+
try {
|
|
37
|
+
match = findPlanForCurrentBranch();
|
|
38
|
+
} catch {
|
|
39
|
+
console.error("Error detecting plan from current branch.");
|
|
40
|
+
console.log(
|
|
41
|
+
"Make sure you're on a work branch (e.g., plan/feature-name)."
|
|
42
|
+
);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!match) {
|
|
48
|
+
console.error(
|
|
49
|
+
"Not on a work branch. No plan found for current branch."
|
|
50
|
+
);
|
|
51
|
+
console.log("\nTo resume a plan:");
|
|
52
|
+
console.log(" 1. git checkout <work-branch>");
|
|
53
|
+
console.log(" 2. orrery resume");
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { planFile, plan } = match;
|
|
59
|
+
const planFileName = path.basename(planFile);
|
|
60
|
+
console.log(`(detected plan: ${planFileName})\n`);
|
|
61
|
+
|
|
62
|
+
// 2. Check for blocked steps
|
|
63
|
+
const blockedSteps = plan.steps.filter((s) => s.status === "blocked");
|
|
64
|
+
|
|
65
|
+
// 3. If no blocked steps, skip to resume
|
|
66
|
+
if (blockedSteps.length === 0) {
|
|
67
|
+
console.log("No blocked steps to unblock.\n");
|
|
68
|
+
|
|
69
|
+
if (options.dryRun) {
|
|
70
|
+
console.log("Dry run: would resume orchestration.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log("Resuming orchestration...\n");
|
|
75
|
+
await orchestrate({ resume: true });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Determine which steps to unblock
|
|
80
|
+
let stepsToUnblock = [];
|
|
81
|
+
|
|
82
|
+
if (options.step) {
|
|
83
|
+
// Unblock specific step
|
|
84
|
+
const step = blockedSteps.find((s) => s.id === options.step);
|
|
85
|
+
if (!step) {
|
|
86
|
+
console.error(
|
|
87
|
+
`Step "${options.step}" is not blocked or does not exist.`
|
|
88
|
+
);
|
|
89
|
+
console.log("\nBlocked steps:");
|
|
90
|
+
for (const s of blockedSteps) {
|
|
91
|
+
console.log(` - ${s.id}`);
|
|
92
|
+
}
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
stepsToUnblock = [step];
|
|
97
|
+
} else {
|
|
98
|
+
// Default: unblock all (--all is implicit)
|
|
99
|
+
stepsToUnblock = blockedSteps;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 5. Dry-run mode: show preview
|
|
103
|
+
if (options.dryRun) {
|
|
104
|
+
console.log("Dry run - would unblock the following steps:\n");
|
|
105
|
+
for (const step of stepsToUnblock) {
|
|
106
|
+
console.log(` ${step.id}`);
|
|
107
|
+
if (step.blocked_reason) {
|
|
108
|
+
console.log(` (was blocked: ${step.blocked_reason})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log("\nThen would commit and resume orchestration.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 6. Perform the unblock
|
|
116
|
+
const updates = stepsToUnblock.map((step) => ({
|
|
117
|
+
stepId: step.id,
|
|
118
|
+
status: "pending",
|
|
119
|
+
extras: { blocked_reason: null } // Clear the blocked_reason
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
updateStepsStatus(planFile, updates);
|
|
123
|
+
|
|
124
|
+
console.log(`Unblocked ${stepsToUnblock.length} step(s):\n`);
|
|
125
|
+
for (const step of stepsToUnblock) {
|
|
126
|
+
console.log(` ${colorize("pending", "yellow")} ${step.id}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 7. Commit the plan file changes
|
|
130
|
+
const planName = planFileName.replace(/\.ya?ml$/, "");
|
|
131
|
+
const commitMessage = `chore: unblock steps in ${planName}`;
|
|
132
|
+
const commitHash = commit(commitMessage, [planFile], process.cwd());
|
|
133
|
+
|
|
134
|
+
if (commitHash) {
|
|
135
|
+
console.log(
|
|
136
|
+
`\nCommitted: ${commitMessage} (${commitHash.slice(0, 7)})`
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
console.log("\n(no changes to commit)");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 8. Resume orchestration
|
|
143
|
+
console.log("\nResuming orchestration...\n");
|
|
144
|
+
await orchestrate({ resume: true });
|
|
145
|
+
});
|
|
146
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const { getPlansDir } = require("../../utils/paths");
|
|
4
|
+
const { getPlanFiles, loadPlan } = require("../../orchestration/plan-loader");
|
|
5
|
+
const { findPlanForCurrentBranch } = require("../../utils/plan-detect");
|
|
6
|
+
const { getCurrentBranch } = require("../../utils/git");
|
|
7
|
+
|
|
8
|
+
function supportsColor() {
|
|
9
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function colorize(text, color) {
|
|
13
|
+
if (!supportsColor()) return text;
|
|
14
|
+
const colors = {
|
|
15
|
+
green: "\x1b[32m",
|
|
16
|
+
yellow: "\x1b[33m",
|
|
17
|
+
red: "\x1b[31m",
|
|
18
|
+
reset: "\x1b[0m"
|
|
19
|
+
};
|
|
20
|
+
return `${colors[color] || ""}${text}${colors.reset}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getPlanStatus(plan) {
|
|
24
|
+
if (plan.isSuccessful()) return "complete";
|
|
25
|
+
if (plan.steps.some((step) => step.status === "blocked")) return "blocked";
|
|
26
|
+
if (plan.steps.some((step) => step.status === "in_progress")) {
|
|
27
|
+
return "in_progress";
|
|
28
|
+
}
|
|
29
|
+
if (plan.metadata && plan.metadata.work_branch) return "in_flight";
|
|
30
|
+
return "pending";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getStatusColor(status) {
|
|
34
|
+
if (status === "complete") return "green";
|
|
35
|
+
if (status === "in_progress" || status === "in_flight") return "yellow";
|
|
36
|
+
if (status === "blocked") return "red";
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatStatusLabel(status) {
|
|
41
|
+
const color = getStatusColor(status);
|
|
42
|
+
const label = status.replace("_", " ");
|
|
43
|
+
return color ? colorize(label, color) : label;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolvePlanPath(planArg) {
|
|
47
|
+
if (!planArg) return null;
|
|
48
|
+
if (path.isAbsolute(planArg)) return planArg;
|
|
49
|
+
if (planArg.includes(path.sep)) return path.resolve(process.cwd(), planArg);
|
|
50
|
+
return path.join(getPlansDir(), planArg);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function summarizePlans(plans) {
|
|
54
|
+
let pendingSteps = 0;
|
|
55
|
+
let completedSteps = 0;
|
|
56
|
+
|
|
57
|
+
for (const plan of plans) {
|
|
58
|
+
for (const step of plan.steps) {
|
|
59
|
+
if (step.status === "pending") pendingSteps += 1;
|
|
60
|
+
if (step.status === "complete") completedSteps += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { pendingSteps, completedSteps };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderPlanList(plans) {
|
|
68
|
+
for (const plan of plans) {
|
|
69
|
+
const status = getPlanStatus(plan);
|
|
70
|
+
const label = formatStatusLabel(status);
|
|
71
|
+
console.log(`${label} ${plan.fileName}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderPlanDetail(plan) {
|
|
76
|
+
const status = getPlanStatus(plan);
|
|
77
|
+
console.log(`${formatStatusLabel(status)} ${plan.fileName}`);
|
|
78
|
+
for (const step of plan.steps) {
|
|
79
|
+
const stepLabel = formatStatusLabel(step.status || "pending");
|
|
80
|
+
const description = step.description ? ` - ${step.description}` : "";
|
|
81
|
+
console.log(` ${stepLabel} ${step.id}${description}`);
|
|
82
|
+
// Display blocked reason if step is blocked
|
|
83
|
+
if (step.status === "blocked" && step.blocked_reason) {
|
|
84
|
+
console.log(` Reason: ${step.blocked_reason}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = function registerStatusCommand(program) {
|
|
90
|
+
program
|
|
91
|
+
.command("status")
|
|
92
|
+
.description("Show orchestration status for plans in the current project")
|
|
93
|
+
.option("--plan <file>", "Show detailed status for a specific plan")
|
|
94
|
+
.action((options) => {
|
|
95
|
+
const plansDir = getPlansDir();
|
|
96
|
+
const planArg = options.plan;
|
|
97
|
+
|
|
98
|
+
if (planArg) {
|
|
99
|
+
const planPath = resolvePlanPath(planArg);
|
|
100
|
+
if (!planPath || !require("fs").existsSync(planPath)) {
|
|
101
|
+
console.error(`Plan not found: ${planArg}`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const plan = loadPlan(planPath);
|
|
107
|
+
renderPlanDetail(plan);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Auto-detect plan when on a work branch
|
|
112
|
+
try {
|
|
113
|
+
const match = findPlanForCurrentBranch();
|
|
114
|
+
if (match) {
|
|
115
|
+
const currentBranch = getCurrentBranch(process.cwd());
|
|
116
|
+
console.log(`(detected plan for branch: ${currentBranch})\n`);
|
|
117
|
+
renderPlanDetail(match.plan);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Not a git repo or other git error - fall through to list all plans
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const planFiles = getPlanFiles(plansDir);
|
|
125
|
+
if (planFiles.length === 0) {
|
|
126
|
+
console.log("No plans found");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const plans = planFiles.map((planFile) => loadPlan(planFile));
|
|
131
|
+
const { pendingSteps, completedSteps } = summarizePlans(plans);
|
|
132
|
+
console.log(
|
|
133
|
+
`${plans.length} plans, ${pendingSteps} pending steps, ${completedSteps} completed`
|
|
134
|
+
);
|
|
135
|
+
renderPlanList(plans);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const YAML = require("yaml");
|
|
4
|
+
|
|
5
|
+
const { loadPlan, savePlan } = require("../../orchestration/plan-loader");
|
|
6
|
+
|
|
7
|
+
const REQUIRED_STEP_FIELDS = ["id", "description"];
|
|
8
|
+
const VALID_STATUSES = ["pending", "in_progress", "complete", "blocked"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a file path is a plan file that should be validated
|
|
12
|
+
*/
|
|
13
|
+
function isPlanFile(filePath) {
|
|
14
|
+
return (
|
|
15
|
+
filePath &&
|
|
16
|
+
filePath.includes("work/plans/") &&
|
|
17
|
+
(filePath.endsWith(".yaml") || filePath.endsWith(".yml"))
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read JSON from stdin (for hook mode)
|
|
23
|
+
*/
|
|
24
|
+
function readStdin() {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let data = "";
|
|
27
|
+
process.stdin.setEncoding("utf8");
|
|
28
|
+
process.stdin.on("readable", () => {
|
|
29
|
+
let chunk;
|
|
30
|
+
while ((chunk = process.stdin.read()) !== null) {
|
|
31
|
+
data += chunk;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
process.stdin.on("end", () => {
|
|
35
|
+
resolve(data);
|
|
36
|
+
});
|
|
37
|
+
if (process.stdin.isTTY) {
|
|
38
|
+
resolve("");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate plan structure and return errors/warnings
|
|
45
|
+
*/
|
|
46
|
+
function validatePlanStructure(filePath) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
const warnings = [];
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(filePath)) {
|
|
51
|
+
return { errors: [`File not found: ${filePath}`], warnings, data: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
55
|
+
|
|
56
|
+
let data;
|
|
57
|
+
try {
|
|
58
|
+
data = YAML.parse(content);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
let errorMsg = `YAML Parse Error in ${path.basename(filePath)}:\n`;
|
|
61
|
+
errorMsg += ` ${err.reason}`;
|
|
62
|
+
if (err.mark) {
|
|
63
|
+
errorMsg += `\n Line ${err.mark.line + 1}, Column ${err.mark.column + 1}`;
|
|
64
|
+
|
|
65
|
+
const lines = content.split("\n");
|
|
66
|
+
const errorLine = err.mark.line;
|
|
67
|
+
const startLine = Math.max(0, errorLine - 2);
|
|
68
|
+
const endLine = Math.min(lines.length - 1, errorLine + 2);
|
|
69
|
+
|
|
70
|
+
errorMsg += "\n\n Context:";
|
|
71
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
72
|
+
const prefix = i === errorLine ? ">>> " : " ";
|
|
73
|
+
errorMsg += `\n ${prefix}${i + 1}: ${lines[i]}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
errorMsg += "\n\nCommon causes:";
|
|
77
|
+
errorMsg += "\n - Unquoted strings containing colons (use double quotes)";
|
|
78
|
+
errorMsg += "\n - Inconsistent indentation (use 2 spaces)";
|
|
79
|
+
errorMsg += "\n - Special characters in values (quote the entire value)";
|
|
80
|
+
errorMsg += "\n\nExample fix:";
|
|
81
|
+
errorMsg += "\n BAD: criteria: Output shows: timestamp";
|
|
82
|
+
errorMsg += '\n GOOD: criteria: "Output shows: timestamp"';
|
|
83
|
+
|
|
84
|
+
return { errors: [errorMsg], warnings, data: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!data) {
|
|
88
|
+
errors.push("Plan file is empty");
|
|
89
|
+
return { errors, warnings, data: null };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for steps array
|
|
93
|
+
if (!data.steps) {
|
|
94
|
+
errors.push("Missing required 'steps' array");
|
|
95
|
+
} else if (!Array.isArray(data.steps)) {
|
|
96
|
+
errors.push("'steps' must be an array");
|
|
97
|
+
} else {
|
|
98
|
+
const stepIds = new Set();
|
|
99
|
+
|
|
100
|
+
data.steps.forEach((step, index) => {
|
|
101
|
+
const stepLabel = step.id
|
|
102
|
+
? `step '${step.id}'`
|
|
103
|
+
: `step at index ${index}`;
|
|
104
|
+
|
|
105
|
+
for (const field of REQUIRED_STEP_FIELDS) {
|
|
106
|
+
if (!step[field]) {
|
|
107
|
+
errors.push(`${stepLabel}: missing required field '${field}'`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (step.id) {
|
|
112
|
+
if (stepIds.has(step.id)) {
|
|
113
|
+
errors.push(`Duplicate step ID: '${step.id}'`);
|
|
114
|
+
}
|
|
115
|
+
stepIds.add(step.id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (step.status && !VALID_STATUSES.includes(step.status)) {
|
|
119
|
+
errors.push(
|
|
120
|
+
`${stepLabel}: invalid status '${step.status}' (must be one of: ${VALID_STATUSES.join(", ")})`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (step.deps) {
|
|
125
|
+
if (!Array.isArray(step.deps)) {
|
|
126
|
+
errors.push(`${stepLabel}: 'deps' must be an array`);
|
|
127
|
+
} else {
|
|
128
|
+
step.deps.forEach((dep) => {
|
|
129
|
+
if (typeof dep !== "string") {
|
|
130
|
+
errors.push(
|
|
131
|
+
`${stepLabel}: dependency must be a string, got ${typeof dep}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (step.files && !Array.isArray(step.files)) {
|
|
139
|
+
errors.push(`${stepLabel}: 'files' must be an array`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (step.commands && !Array.isArray(step.commands)) {
|
|
143
|
+
errors.push(`${stepLabel}: 'commands' must be an array`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
step.criteria &&
|
|
148
|
+
step.criteria.includes(": ") &&
|
|
149
|
+
!step.criteria.startsWith('"')
|
|
150
|
+
) {
|
|
151
|
+
warnings.push(
|
|
152
|
+
`${stepLabel}: 'criteria' contains ': ' - ensure this field is properly quoted in source`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Validate dependency references
|
|
158
|
+
data.steps.forEach((step) => {
|
|
159
|
+
if (step.deps && Array.isArray(step.deps)) {
|
|
160
|
+
step.deps.forEach((dep) => {
|
|
161
|
+
if (!stepIds.has(dep)) {
|
|
162
|
+
errors.push(
|
|
163
|
+
`step '${step.id}': references unknown dependency '${dep}'`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!data.metadata) {
|
|
172
|
+
warnings.push("Missing 'metadata' section (recommended)");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { errors, warnings, data };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate a plan file and optionally re-save to normalize formatting
|
|
180
|
+
*/
|
|
181
|
+
function validatePlan(filePath, options = {}) {
|
|
182
|
+
const { errors, warnings, data } = validatePlanStructure(filePath);
|
|
183
|
+
|
|
184
|
+
console.log(`\nValidating: ${path.basename(filePath)}\n`);
|
|
185
|
+
|
|
186
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
187
|
+
console.log("✓ Plan is valid\n");
|
|
188
|
+
const stepCount = data.steps ? data.steps.length : 0;
|
|
189
|
+
console.log(` Steps: ${stepCount}`);
|
|
190
|
+
if (data.steps) {
|
|
191
|
+
data.steps.forEach((step) => {
|
|
192
|
+
const status = step.status || "pending";
|
|
193
|
+
const desc = step.description || "";
|
|
194
|
+
console.log(
|
|
195
|
+
` - ${step.id}: ${desc.substring(0, 50)}${desc.length > 50 ? "..." : ""} [${status}]`
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
console.log();
|
|
200
|
+
|
|
201
|
+
// Re-save to normalize formatting (uses same format as orchestrator)
|
|
202
|
+
if (!options.skipResave) {
|
|
203
|
+
const plan = loadPlan(filePath);
|
|
204
|
+
savePlan(plan);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (errors.length > 0) {
|
|
211
|
+
console.error("✗ Validation errors:\n");
|
|
212
|
+
errors.forEach((err) => console.error(` - ${err}`));
|
|
213
|
+
console.error();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (warnings.length > 0) {
|
|
217
|
+
console.warn("⚠ Warnings:\n");
|
|
218
|
+
warnings.forEach((warn) => console.warn(` - ${warn}`));
|
|
219
|
+
console.warn();
|
|
220
|
+
|
|
221
|
+
// Re-save valid plans with warnings too
|
|
222
|
+
if (errors.length === 0 && !options.skipResave) {
|
|
223
|
+
const plan = loadPlan(filePath);
|
|
224
|
+
savePlan(plan);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return errors.length === 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Run in hook mode - read file path from stdin JSON
|
|
233
|
+
*/
|
|
234
|
+
async function runAsHook() {
|
|
235
|
+
const input = await readStdin();
|
|
236
|
+
|
|
237
|
+
if (!input.trim()) {
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let hookData;
|
|
242
|
+
try {
|
|
243
|
+
hookData = JSON.parse(input);
|
|
244
|
+
} catch {
|
|
245
|
+
process.exit(0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const filePath = hookData?.tool_input?.file_path;
|
|
249
|
+
|
|
250
|
+
if (!isPlanFile(filePath)) {
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const isValid = validatePlan(filePath);
|
|
255
|
+
process.exit(isValid ? 0 : 2);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = registerValidatePlanCommand;
|
|
259
|
+
module.exports.validatePlanStructure = validatePlanStructure;
|
|
260
|
+
|
|
261
|
+
function registerValidatePlanCommand(program) {
|
|
262
|
+
program
|
|
263
|
+
.command("validate-plan")
|
|
264
|
+
.description("Validate a plan YAML file and normalize its formatting")
|
|
265
|
+
.argument(
|
|
266
|
+
"[file]",
|
|
267
|
+
"Path to the plan file (or reads from stdin for hook mode)"
|
|
268
|
+
)
|
|
269
|
+
.option("--no-resave", "Skip re-saving the file after validation")
|
|
270
|
+
.action(async (file, options) => {
|
|
271
|
+
if (!file) {
|
|
272
|
+
// Hook mode - read from stdin
|
|
273
|
+
await runAsHook();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const filePath = path.resolve(file);
|
|
278
|
+
|
|
279
|
+
if (!fs.existsSync(filePath)) {
|
|
280
|
+
console.error(`File not found: ${file}`);
|
|
281
|
+
process.exitCode = 2;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const isValid = validatePlan(filePath, { skipResave: !options.resave });
|
|
286
|
+
process.exitCode = isValid ? 0 : 2;
|
|
287
|
+
});
|
|
288
|
+
}
|
package/lib/cli/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { Command } = require("commander");
|
|
3
|
+
|
|
4
|
+
const registerInit = require("./commands/init");
|
|
5
|
+
const registerInstallSkills = require("./commands/install-skills");
|
|
6
|
+
const registerInstallDevcontainer = require("./commands/install-devcontainer");
|
|
7
|
+
const registerOrchestrate = require("./commands/orchestrate");
|
|
8
|
+
const registerStatus = require("./commands/status");
|
|
9
|
+
const registerResume = require("./commands/resume");
|
|
10
|
+
const registerValidatePlan = require("./commands/validate-plan");
|
|
11
|
+
const registerIngestPlan = require("./commands/ingest-plan");
|
|
12
|
+
const registerHelp = require("./commands/help");
|
|
13
|
+
|
|
14
|
+
function getPackageVersion() {
|
|
15
|
+
const pkgPath = path.join(__dirname, "..", "..", "package.json");
|
|
16
|
+
const pkg = require(pkgPath);
|
|
17
|
+
return pkg.version || "0.0.0";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildProgram() {
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name("orrery")
|
|
25
|
+
.description("Structured workflow orchestration for AI agents")
|
|
26
|
+
.version(getPackageVersion(), "-v, --version")
|
|
27
|
+
.showHelpAfterError();
|
|
28
|
+
|
|
29
|
+
registerInit(program);
|
|
30
|
+
registerInstallSkills(program);
|
|
31
|
+
registerInstallDevcontainer(program);
|
|
32
|
+
registerOrchestrate(program);
|
|
33
|
+
registerStatus(program);
|
|
34
|
+
registerResume(program);
|
|
35
|
+
registerValidatePlan(program);
|
|
36
|
+
registerIngestPlan(program);
|
|
37
|
+
registerHelp(program);
|
|
38
|
+
|
|
39
|
+
program.on("command:*", (operands) => {
|
|
40
|
+
const [command] = operands;
|
|
41
|
+
console.error(`Unknown command: ${command}`);
|
|
42
|
+
program.outputHelp();
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return program;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run(argv) {
|
|
50
|
+
const program = buildProgram();
|
|
51
|
+
program.parse(argv);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
buildProgram,
|
|
56
|
+
run
|
|
57
|
+
};
|