@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,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const YAML = require("yaml");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load a plan from a YAML file
|
|
9
|
+
* @param {string} filePath - Absolute path to the plan YAML file
|
|
10
|
+
* @returns {Object} - Parsed plan object with helper methods
|
|
11
|
+
*/
|
|
12
|
+
function loadPlan(filePath) {
|
|
13
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
14
|
+
const doc = YAML.parseDocument(content);
|
|
15
|
+
const data = doc.toJS();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
filePath,
|
|
19
|
+
fileName: path.basename(filePath),
|
|
20
|
+
doc,
|
|
21
|
+
metadata: data.metadata || {},
|
|
22
|
+
steps: data.steps || [],
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get set of completed step IDs
|
|
26
|
+
* @returns {Set<string>}
|
|
27
|
+
*/
|
|
28
|
+
getCompletedSteps() {
|
|
29
|
+
return new Set(
|
|
30
|
+
this.steps.filter((s) => s.status === "complete").map((s) => s.id)
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get set of blocked step IDs
|
|
36
|
+
* @returns {Set<string>}
|
|
37
|
+
*/
|
|
38
|
+
getBlockedSteps() {
|
|
39
|
+
return new Set(
|
|
40
|
+
this.steps.filter((s) => s.status === "blocked").map((s) => s.id)
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if all steps are complete or blocked
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
isComplete() {
|
|
49
|
+
return this.steps.every(
|
|
50
|
+
(s) => s.status === "complete" || s.status === "blocked"
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if all steps are complete (none blocked)
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
isSuccessful() {
|
|
59
|
+
return this.steps.every((s) => s.status === "complete");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Save a plan back to its YAML file (preserves comments)
|
|
66
|
+
* @param {Object} plan - The plan object (with filePath, doc, metadata, steps)
|
|
67
|
+
*/
|
|
68
|
+
function savePlan(plan) {
|
|
69
|
+
const metadataNode = plan.doc.get("metadata", true);
|
|
70
|
+
const stepsNode = plan.doc.get("steps", true);
|
|
71
|
+
|
|
72
|
+
// Update metadata fields in-place
|
|
73
|
+
if (metadataNode) {
|
|
74
|
+
for (const [key, value] of Object.entries(plan.metadata)) {
|
|
75
|
+
metadataNode.set(key, value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Update step fields in-place
|
|
80
|
+
if (stepsNode && stepsNode.items) {
|
|
81
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
82
|
+
const stepData = plan.steps[i];
|
|
83
|
+
const stepNode = stepsNode.items[i];
|
|
84
|
+
if (stepNode) {
|
|
85
|
+
for (const [key, value] of Object.entries(stepData)) {
|
|
86
|
+
stepNode.set(key, value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(plan.filePath, plan.doc.toString(), "utf8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Update a specific step's status in a plan file (preserves comments)
|
|
97
|
+
* @param {string} filePath - Path to the plan file
|
|
98
|
+
* @param {string} stepId - ID of the step to update
|
|
99
|
+
* @param {string} status - New status value (pending, in_progress, complete, blocked)
|
|
100
|
+
* @param {Object} [extras] - Optional extra fields to update (e.g., blockedReason)
|
|
101
|
+
*/
|
|
102
|
+
function updateStepStatus(filePath, stepId, status, extras = {}) {
|
|
103
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
104
|
+
const doc = YAML.parseDocument(content);
|
|
105
|
+
const stepsNode = doc.get("steps", true);
|
|
106
|
+
|
|
107
|
+
if (!stepsNode || !stepsNode.items) return;
|
|
108
|
+
|
|
109
|
+
for (const stepNode of stepsNode.items) {
|
|
110
|
+
const idNode = stepNode.get("id", true);
|
|
111
|
+
if (idNode && String(idNode) === stepId) {
|
|
112
|
+
stepNode.set("status", status);
|
|
113
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
114
|
+
stepNode.set(key, value);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(filePath, doc.toString(), "utf8");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update multiple steps' status at once (preserves comments)
|
|
125
|
+
* @param {string} filePath - Path to the plan file
|
|
126
|
+
* @param {Array<{stepId: string, status: string, extras?: Object}>} updates - Array of updates
|
|
127
|
+
*/
|
|
128
|
+
function updateStepsStatus(filePath, updates) {
|
|
129
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
130
|
+
const doc = YAML.parseDocument(content);
|
|
131
|
+
const stepsNode = doc.get("steps", true);
|
|
132
|
+
|
|
133
|
+
if (!stepsNode || !stepsNode.items) return;
|
|
134
|
+
|
|
135
|
+
const updateMap = new Map(updates.map((u) => [u.stepId, u]));
|
|
136
|
+
|
|
137
|
+
for (const stepNode of stepsNode.items) {
|
|
138
|
+
const idNode = stepNode.get("id", true);
|
|
139
|
+
const stepId = idNode ? String(idNode) : null;
|
|
140
|
+
const update = updateMap.get(stepId);
|
|
141
|
+
|
|
142
|
+
if (update) {
|
|
143
|
+
stepNode.set("status", update.status);
|
|
144
|
+
for (const [key, value] of Object.entries(update.extras || {})) {
|
|
145
|
+
if (value === null) {
|
|
146
|
+
stepNode.delete(key); // Remove field when null
|
|
147
|
+
} else {
|
|
148
|
+
stepNode.set(key, value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(filePath, doc.toString(), "utf8");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get all YAML plan files in a directory
|
|
159
|
+
* @param {string} dir - Directory to scan
|
|
160
|
+
* @returns {string[]} - Array of absolute file paths, sorted by name
|
|
161
|
+
*/
|
|
162
|
+
function getPlanFiles(dir) {
|
|
163
|
+
if (!fs.existsSync(dir)) return [];
|
|
164
|
+
|
|
165
|
+
return fs
|
|
166
|
+
.readdirSync(dir)
|
|
167
|
+
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
168
|
+
.map((f) => path.join(dir, f))
|
|
169
|
+
.sort();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Move a completed plan to the completed directory
|
|
174
|
+
* @param {string} planFile - Current path of the plan
|
|
175
|
+
* @param {string} completedDir - Destination directory
|
|
176
|
+
*/
|
|
177
|
+
function movePlanToCompleted(planFile, completedDir) {
|
|
178
|
+
const fileName = path.basename(planFile);
|
|
179
|
+
const destPath = path.join(completedDir, fileName);
|
|
180
|
+
|
|
181
|
+
if (!fs.existsSync(completedDir)) {
|
|
182
|
+
fs.mkdirSync(completedDir, { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fs.renameSync(planFile, destPath);
|
|
186
|
+
return destPath;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get filenames of completed plans (for exclusion filtering)
|
|
191
|
+
* @param {string} completedDir - Path to completed directory
|
|
192
|
+
* @returns {Set<string>} - Set of filenames
|
|
193
|
+
*/
|
|
194
|
+
function getCompletedPlanNames(completedDir) {
|
|
195
|
+
if (!fs.existsSync(completedDir)) return new Set();
|
|
196
|
+
|
|
197
|
+
return new Set(
|
|
198
|
+
fs
|
|
199
|
+
.readdirSync(completedDir)
|
|
200
|
+
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
loadPlan,
|
|
206
|
+
savePlan,
|
|
207
|
+
updateStepStatus,
|
|
208
|
+
updateStepsStatus,
|
|
209
|
+
getPlanFiles,
|
|
210
|
+
movePlanToCompleted,
|
|
211
|
+
getCompletedPlanNames
|
|
212
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Tracker for Plan Orchestration
|
|
3
|
+
*
|
|
4
|
+
* Tracks step completion, elapsed time, and provides ETA estimates
|
|
5
|
+
* during plan execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class ProgressTracker {
|
|
9
|
+
/**
|
|
10
|
+
* @param {number} totalSteps - Total number of steps in the plan
|
|
11
|
+
* @param {string} planFileName - Name of the plan file for display
|
|
12
|
+
*/
|
|
13
|
+
constructor(totalSteps, planFileName) {
|
|
14
|
+
this.totalSteps = totalSteps;
|
|
15
|
+
this.planFileName = planFileName;
|
|
16
|
+
this.completedCount = 0;
|
|
17
|
+
this.blockedCount = 0;
|
|
18
|
+
this.startTime = Date.now();
|
|
19
|
+
this.stepCompletionTimes = []; // Duration of each completed step
|
|
20
|
+
this.stepStartTimes = new Map(); // stepId -> startTime
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize counts from existing plan state (for resume mode)
|
|
25
|
+
* @param {Object} plan - The loaded plan object
|
|
26
|
+
*/
|
|
27
|
+
initializeFromPlan(plan) {
|
|
28
|
+
for (const step of plan.steps) {
|
|
29
|
+
if (step.status === "complete") {
|
|
30
|
+
this.completedCount++;
|
|
31
|
+
} else if (step.status === "blocked") {
|
|
32
|
+
this.blockedCount++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Record when step(s) begin execution
|
|
39
|
+
* @param {string[]} stepIds - Array of step IDs starting
|
|
40
|
+
*/
|
|
41
|
+
recordStart(stepIds) {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
for (const stepId of stepIds) {
|
|
44
|
+
this.stepStartTimes.set(stepId, now);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Record a step completing successfully
|
|
50
|
+
* @param {string} stepId - The completed step ID
|
|
51
|
+
*/
|
|
52
|
+
recordComplete(stepId) {
|
|
53
|
+
this.completedCount++;
|
|
54
|
+
const startTime = this.stepStartTimes.get(stepId);
|
|
55
|
+
if (startTime) {
|
|
56
|
+
const duration = Date.now() - startTime;
|
|
57
|
+
this.stepCompletionTimes.push(duration);
|
|
58
|
+
this.stepStartTimes.delete(stepId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Record a step becoming blocked
|
|
64
|
+
* @param {string} stepId - The blocked step ID
|
|
65
|
+
*/
|
|
66
|
+
recordBlocked(stepId) {
|
|
67
|
+
this.blockedCount++;
|
|
68
|
+
this.stepStartTimes.delete(stepId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get elapsed time since tracking started
|
|
73
|
+
* @returns {number} Elapsed time in milliseconds
|
|
74
|
+
*/
|
|
75
|
+
getElapsed() {
|
|
76
|
+
return Date.now() - this.startTime;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get estimated time remaining based on average step duration
|
|
81
|
+
* @returns {number|null} Estimated remaining time in ms, or null if not calculable
|
|
82
|
+
*/
|
|
83
|
+
getEstimatedRemaining() {
|
|
84
|
+
if (this.stepCompletionTimes.length === 0) {
|
|
85
|
+
return null; // No data yet
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const avgStepTime =
|
|
89
|
+
this.stepCompletionTimes.reduce((a, b) => a + b, 0) /
|
|
90
|
+
this.stepCompletionTimes.length;
|
|
91
|
+
|
|
92
|
+
const remainingSteps =
|
|
93
|
+
this.totalSteps - this.completedCount - this.blockedCount;
|
|
94
|
+
|
|
95
|
+
return avgStepTime * remainingSteps;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format duration in human-readable format
|
|
100
|
+
* @param {number} ms - Duration in milliseconds
|
|
101
|
+
* @returns {string} Formatted string like "2m 30s" or "1h 5m"
|
|
102
|
+
*/
|
|
103
|
+
formatDuration(ms) {
|
|
104
|
+
if (ms < 1000) {
|
|
105
|
+
return "<1s";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const seconds = Math.floor(ms / 1000);
|
|
109
|
+
const minutes = Math.floor(seconds / 60);
|
|
110
|
+
const hours = Math.floor(minutes / 60);
|
|
111
|
+
|
|
112
|
+
if (hours > 0) {
|
|
113
|
+
const remainingMinutes = minutes % 60;
|
|
114
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
115
|
+
} else if (minutes > 0) {
|
|
116
|
+
const remainingSeconds = seconds % 60;
|
|
117
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
118
|
+
} else {
|
|
119
|
+
return `${seconds}s`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get completion percentage
|
|
125
|
+
* @returns {number} Percentage complete (0-100)
|
|
126
|
+
*/
|
|
127
|
+
getPercentComplete() {
|
|
128
|
+
const processed = this.completedCount + this.blockedCount;
|
|
129
|
+
return Math.round((processed / this.totalSteps) * 100);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Log the start of plan execution
|
|
134
|
+
*/
|
|
135
|
+
logStart() {
|
|
136
|
+
const initialComplete = this.completedCount;
|
|
137
|
+
const initialBlocked = this.blockedCount;
|
|
138
|
+
const pending = this.totalSteps - initialComplete - initialBlocked;
|
|
139
|
+
|
|
140
|
+
console.log(`[Progress] Starting plan: ${this.planFileName}`);
|
|
141
|
+
console.log(
|
|
142
|
+
`[Progress] Total steps: ${this.totalSteps} (${pending} pending, ${initialComplete} complete, ${initialBlocked} blocked)`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Log when step(s) start
|
|
148
|
+
* @param {string[]} stepIds - Array of step IDs starting
|
|
149
|
+
*/
|
|
150
|
+
logStepStart(stepIds) {
|
|
151
|
+
this.recordStart(stepIds);
|
|
152
|
+
const processed = this.completedCount + this.blockedCount;
|
|
153
|
+
const stepList = stepIds.join(", ");
|
|
154
|
+
|
|
155
|
+
if (stepIds.length === 1) {
|
|
156
|
+
console.log(
|
|
157
|
+
`[Progress] Starting ${stepList} (${processed + 1} of ${this.totalSteps})`
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(
|
|
161
|
+
`[Progress] Starting ${stepIds.length} steps: ${stepList} (${processed + 1}-${processed + stepIds.length} of ${this.totalSteps})`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Log current progress after step completion
|
|
168
|
+
*/
|
|
169
|
+
logProgress() {
|
|
170
|
+
const processed = this.completedCount + this.blockedCount;
|
|
171
|
+
const percent = this.getPercentComplete();
|
|
172
|
+
const elapsed = this.formatDuration(this.getElapsed());
|
|
173
|
+
const eta = this.getEstimatedRemaining();
|
|
174
|
+
|
|
175
|
+
let progressLine = `[Progress] ${processed}/${this.totalSteps} steps (${percent}%) | Elapsed: ${elapsed}`;
|
|
176
|
+
|
|
177
|
+
if (eta !== null) {
|
|
178
|
+
progressLine += ` | ETA: ${this.formatDuration(eta)}`;
|
|
179
|
+
} else {
|
|
180
|
+
progressLine += " | ETA: Calculating...";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(progressLine);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Log final summary when plan execution ends
|
|
188
|
+
*/
|
|
189
|
+
logSummary() {
|
|
190
|
+
const totalTime = this.formatDuration(this.getElapsed());
|
|
191
|
+
const avgTime =
|
|
192
|
+
this.stepCompletionTimes.length > 0
|
|
193
|
+
? this.formatDuration(
|
|
194
|
+
this.stepCompletionTimes.reduce((a, b) => a + b, 0) /
|
|
195
|
+
this.stepCompletionTimes.length
|
|
196
|
+
)
|
|
197
|
+
: "N/A";
|
|
198
|
+
|
|
199
|
+
console.log(`[Progress] === Summary ===`);
|
|
200
|
+
console.log(
|
|
201
|
+
`[Progress] Total: ${this.totalSteps} steps (${this.completedCount} complete, ${this.blockedCount} blocked)`
|
|
202
|
+
);
|
|
203
|
+
console.log(`[Progress] Time: ${totalTime}`);
|
|
204
|
+
console.log(`[Progress] Avg step time: ${avgTime}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { ProgressTracker };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Format Definition (Single Source of Truth)
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: Any changes to the structure or instructions below MUST be manually
|
|
5
|
+
* reflected in: agent/skills/orrery-report/SKILL.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// 1. Define the Expected Shape (The Contract)
|
|
9
|
+
const REPORT_FIELDS = {
|
|
10
|
+
stepId: "The ID of the step being executed (string)",
|
|
11
|
+
status: "One of: 'complete', 'blocked'",
|
|
12
|
+
summary: "A concise summary of what was done (string)",
|
|
13
|
+
artifacts: "Array of file paths created or modified (string[])",
|
|
14
|
+
testResults: "Optional: Test outcome summary (string, e.g., '2/2 passed')",
|
|
15
|
+
blockedReason: "Required if status is 'blocked' (string)",
|
|
16
|
+
commitMessage:
|
|
17
|
+
"A meaningful commit message (string, e.g., 'feat: add login endpoint')"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// 2. Generate the Instruction for the Agent
|
|
21
|
+
function getFormatInstructions() {
|
|
22
|
+
return `## Output Contract
|
|
23
|
+
|
|
24
|
+
Output one JSON object per step to stdout. You may output multiple objects if processing multiple steps.
|
|
25
|
+
|
|
26
|
+
Success Example:
|
|
27
|
+
{"stepId": "step-1", "status": "complete", "summary": "Implemented login", "artifacts": ["src/auth.js"], "testResults": "5/5 passed", "commitMessage": "feat: add user authentication"}
|
|
28
|
+
|
|
29
|
+
Blocked Example:
|
|
30
|
+
{"stepId": "step-2", "status": "blocked", "blockedReason": "API is down", "summary": "Could not verify"}
|
|
31
|
+
|
|
32
|
+
Rules:
|
|
33
|
+
- JSON must be valid and on a single line.
|
|
34
|
+
- Do not wrap in markdown blocks (just raw JSON).
|
|
35
|
+
- Each step result must be a separate JSON object.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. Validate/Normalize the incoming data
|
|
39
|
+
function validateAgentOutput(data) {
|
|
40
|
+
if (!data || typeof data !== "object") {
|
|
41
|
+
throw new Error("Report data must be an object");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Required fields
|
|
45
|
+
if (!data.stepId || typeof data.stepId !== "string") {
|
|
46
|
+
throw new Error("Report missing required field: stepId (string)");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!data.status || !["complete", "blocked"].includes(data.status)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid status: ${data.status}. Must be 'complete' or 'blocked'`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Conditional requirements
|
|
56
|
+
if (data.status === "blocked" && !data.blockedReason) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Report with status 'blocked' must include 'blockedReason'"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Normalize optional fields
|
|
63
|
+
return {
|
|
64
|
+
stepId: data.stepId,
|
|
65
|
+
status: data.status,
|
|
66
|
+
summary:
|
|
67
|
+
data.summary ||
|
|
68
|
+
(data.status === "blocked" ? "Step blocked" : "Step completed"),
|
|
69
|
+
artifacts: Array.isArray(data.artifacts) ? data.artifacts : [],
|
|
70
|
+
testResults: data.testResults || null,
|
|
71
|
+
blockedReason: data.blockedReason || null,
|
|
72
|
+
commitMessage: data.commitMessage || `feat: complete step ${data.stepId}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
REPORT_FIELDS,
|
|
78
|
+
getFormatInstructions,
|
|
79
|
+
validateAgentOutput
|
|
80
|
+
};
|