@nyxa/nyx-agent 0.4.1 → 0.6.0
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/README.md +58 -9
- package/dist/cli.js +13 -16
- package/dist/commands/init.js +112 -462
- package/dist/commands/run.js +17 -3
- package/dist/commands/update.js +1 -0
- package/dist/config/loadConfig.js +17 -3
- package/dist/config/schema.js +29 -146
- package/dist/runtime/files.js +1 -0
- package/dist/runtime/git.js +1 -0
- package/dist/runtime/gitLifecycle.js +19 -57
- package/dist/runtime/harness.js +26 -0
- package/dist/runtime/ledger.js +1 -0
- package/dist/runtime/paths.js +1 -12
- package/dist/runtime/prompts.js +103 -0
- package/dist/runtime/runPhase.js +85 -254
- package/dist/runtime/runPipeline.js +479 -0
- package/dist/runtime/schemas.js +52 -0
- package/dist/runtime/scm.js +80 -0
- package/dist/runtime/time.js +1 -0
- package/dist/runtime/validateResult.js +2 -3
- package/dist/runtime/workItems.js +43 -118
- package/package.json +2 -5
- package/dist/runtime/buildPrompt.js +0 -54
- package/dist/runtime/effectiveConfig.js +0 -14
- package/dist/runtime/renderTemplate.js +0 -28
- package/dist/runtime/runWorkflow.js +0 -680
- package/dist/runtime/validateWorkItem.js +0 -212
- package/dist/runtime/workItemAnnotations.js +0 -39
- package/docs/nyxagent-v0-spec.md +0 -742
- package/templates/default/prompts/closure.md +0 -30
- package/templates/default/prompts/execution.md +0 -11
- package/templates/default/prompts/finalize.md +0 -7
- package/templates/default/prompts/global-review.md +0 -24
- package/templates/default/prompts/global-revision.md +0 -9
- package/templates/default/prompts/pull-request.md +0 -23
- package/templates/default/prompts/repair-result.md +0 -29
- package/templates/default/prompts/review.md +0 -18
- package/templates/default/prompts/revision.md +0 -7
- package/templates/default/prompts/selection.md +0 -46
- package/templates/default/schemas/closure.schema.json +0 -35
- package/templates/default/schemas/global-review.schema.json +0 -60
- package/templates/default/schemas/pull-request.schema.json +0 -44
- package/templates/default/schemas/review.schema.json +0 -60
- package/templates/default/schemas/selection.schema.json +0 -135
|
@@ -1,680 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { checkbox, Separator } from "@inquirer/prompts";
|
|
3
|
-
import pc from "picocolors";
|
|
4
|
-
import { loadConfig } from "../config/loadConfig.js";
|
|
5
|
-
import { ensureDir, writeJson } from "./files.js";
|
|
6
|
-
import { getGitSnapshot } from "./git.js";
|
|
7
|
-
import { setUpGitContext, tearDownGitContext } from "./gitLifecycle.js";
|
|
8
|
-
import { markWorkItemCompleted, readWorkItemLedger, writeWorkItemLedger } from "./ledger.js";
|
|
9
|
-
import { getNyxDir, relativeToProject } from "./paths.js";
|
|
10
|
-
import { runPhase } from "./runPhase.js";
|
|
11
|
-
import { createRunId } from "./time.js";
|
|
12
|
-
import { validateWorkItemQueue } from "./validateWorkItem.js";
|
|
13
|
-
import { buildWorkItemKindMap, formatWorkItemChoiceName, normalizeWorkItemAnnotations } from "./workItemAnnotations.js";
|
|
14
|
-
import { filterAvailableWorkItems, listWorkItemCandidates } from "./workItems.js";
|
|
15
|
-
export async function runWorkflow(options) {
|
|
16
|
-
const projectRoot = path.resolve(options.projectRoot);
|
|
17
|
-
const nyxDir = getNyxDir(projectRoot);
|
|
18
|
-
const configPath = options.configPath ?? path.join(nyxDir, "config.toml");
|
|
19
|
-
const config = await loadConfig(configPath);
|
|
20
|
-
const runId = createRunId();
|
|
21
|
-
const runDir = path.join(nyxDir, "runs", runId);
|
|
22
|
-
await ensureDir(runDir);
|
|
23
|
-
let ledger = await readWorkItemLedger(nyxDir);
|
|
24
|
-
await writeWorkItemLedger(nyxDir, ledger);
|
|
25
|
-
const git = await getGitSnapshot(projectRoot);
|
|
26
|
-
const runState = {
|
|
27
|
-
run_id: runId,
|
|
28
|
-
status: "running",
|
|
29
|
-
current_iteration: 0,
|
|
30
|
-
completed_iterations: 0,
|
|
31
|
-
available_work_items: [],
|
|
32
|
-
recommended_work_item_queue: [],
|
|
33
|
-
selected_work_item_queue: [],
|
|
34
|
-
work_item_annotations: [],
|
|
35
|
-
skipped_work_item_keys: [],
|
|
36
|
-
selection_groups: [],
|
|
37
|
-
seen_work_item_keys: [],
|
|
38
|
-
completed_work_item_keys: [...ledger.completed_work_item_keys],
|
|
39
|
-
last_completed_work_item: ledger.last_completed_work_item
|
|
40
|
-
};
|
|
41
|
-
await writeJson(path.join(runDir, "run.json"), {
|
|
42
|
-
run_id: runId,
|
|
43
|
-
project_root: projectRoot,
|
|
44
|
-
started_at: new Date().toISOString(),
|
|
45
|
-
config_path: relativeToProject(projectRoot, configPath),
|
|
46
|
-
harness: {
|
|
47
|
-
preset: config.harness.preset,
|
|
48
|
-
command: config.harness.command
|
|
49
|
-
},
|
|
50
|
-
git
|
|
51
|
-
});
|
|
52
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
53
|
-
console.log(pc.bold(`NyxAgent run ${runId}`));
|
|
54
|
-
console.log(`Project: ${projectRoot}`);
|
|
55
|
-
console.log(`Harness: ${config.harness.preset ?? "custom"} (${config.harness.command})`);
|
|
56
|
-
console.log(`Model: ${config.model.name}`);
|
|
57
|
-
console.log("");
|
|
58
|
-
const phasesById = new Map(config.phases.map((phase) => [phase.id, phase]));
|
|
59
|
-
const selection = await runSelectionPhase({
|
|
60
|
-
projectRoot,
|
|
61
|
-
runDir,
|
|
62
|
-
config,
|
|
63
|
-
phasesById,
|
|
64
|
-
runState,
|
|
65
|
-
ledger
|
|
66
|
-
});
|
|
67
|
-
if (selection.status === "stopped") {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
runState.available_work_items = selection.availableWorkItems;
|
|
71
|
-
runState.recommended_work_item_queue = selection.workItems;
|
|
72
|
-
runState.work_item_annotations = selection.workItemAnnotations;
|
|
73
|
-
runState.selection_groups = selection.selectionGroups;
|
|
74
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
75
|
-
let confirmedWorkItems;
|
|
76
|
-
try {
|
|
77
|
-
confirmedWorkItems = normalizeConfirmedWorkItems({
|
|
78
|
-
availableWorkItems: selection.availableWorkItems,
|
|
79
|
-
confirmed: await (options.confirmWorkItems ?? confirmWorkItemsWithCheckbox)({
|
|
80
|
-
availableWorkItems: selection.availableWorkItems,
|
|
81
|
-
recommendedWorkItems: selection.workItems,
|
|
82
|
-
workItems: selection.availableWorkItems,
|
|
83
|
-
workItemAnnotations: selection.workItemAnnotations,
|
|
84
|
-
selectionGroups: selection.selectionGroups,
|
|
85
|
-
maxIterations: config.workflow.max_iterations
|
|
86
|
-
})
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
runState.status = "failed";
|
|
91
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
const selectedWorkItems = confirmedWorkItems.slice(0, config.workflow.max_iterations);
|
|
95
|
-
const selectedKeys = new Set(selectedWorkItems.map((item) => item.key));
|
|
96
|
-
runState.selected_work_item_queue = selectedWorkItems;
|
|
97
|
-
runState.skipped_work_item_keys = selection.workItems
|
|
98
|
-
.filter((item) => !selectedKeys.has(item.key))
|
|
99
|
-
.map((item) => item.key);
|
|
100
|
-
runState.seen_work_item_keys = selectedWorkItems.map((item) => item.key);
|
|
101
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
102
|
-
if (confirmedWorkItems.length > config.workflow.max_iterations) {
|
|
103
|
-
console.log(pc.yellow(`Only the first ${config.workflow.max_iterations} selected work items will run.`));
|
|
104
|
-
}
|
|
105
|
-
if (selectedWorkItems.length === 0) {
|
|
106
|
-
runState.status = "completed_no_selected_work";
|
|
107
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
108
|
-
console.log("");
|
|
109
|
-
console.log(`Done: ${runState.status}`);
|
|
110
|
-
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
let gitContext;
|
|
114
|
-
if (config.git && config.git.mode !== "off") {
|
|
115
|
-
gitContext = await setUpGitContext({ projectRoot, git: config.git, runId });
|
|
116
|
-
}
|
|
117
|
-
if (gitContext) {
|
|
118
|
-
console.log(`Git: ${gitContext.mode} on "${gitContext.branch}" (base "${gitContext.base}")`);
|
|
119
|
-
if (gitContext.mode === "worktree") {
|
|
120
|
-
console.log(`Worktree: ${relativeToProject(projectRoot, gitContext.worktree)}`);
|
|
121
|
-
}
|
|
122
|
-
console.log("");
|
|
123
|
-
}
|
|
124
|
-
const cleanup = config.git?.cleanup ?? "on_success";
|
|
125
|
-
let runSucceeded = false;
|
|
126
|
-
try {
|
|
127
|
-
runSucceeded = await runSelectedQueue({
|
|
128
|
-
projectRoot,
|
|
129
|
-
nyxDir,
|
|
130
|
-
runDir,
|
|
131
|
-
config,
|
|
132
|
-
phasesById,
|
|
133
|
-
selection,
|
|
134
|
-
selectedWorkItems,
|
|
135
|
-
runState,
|
|
136
|
-
ledger,
|
|
137
|
-
workdir: gitContext?.worktree ?? projectRoot,
|
|
138
|
-
gitContext
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
finally {
|
|
142
|
-
if (gitContext) {
|
|
143
|
-
await tearDownGitContext({
|
|
144
|
-
projectRoot,
|
|
145
|
-
context: gitContext,
|
|
146
|
-
cleanup,
|
|
147
|
-
success: runSucceeded
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Run the confirmed work item queue and, when configured, the run-scoped final
|
|
154
|
-
* phase (e.g. opening the PRD pull request). Returns true when the whole run
|
|
155
|
-
* completed normally, false when it stopped early (stop_run).
|
|
156
|
-
*/
|
|
157
|
-
async function runSelectedQueue(input) {
|
|
158
|
-
const { projectRoot, nyxDir, runDir, config, phasesById, selection, selectedWorkItems, runState, workdir, gitContext } = input;
|
|
159
|
-
let ledger = input.ledger;
|
|
160
|
-
for (let queueIndex = 0; queueIndex < selectedWorkItems.length; queueIndex += 1) {
|
|
161
|
-
const iterationNumber = queueIndex + 1;
|
|
162
|
-
const workItem = selectedWorkItems[queueIndex];
|
|
163
|
-
runState.current_iteration = iterationNumber;
|
|
164
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
165
|
-
const iterationDir = path.join(runDir, "iterations", String(iterationNumber).padStart(3, "0"));
|
|
166
|
-
await ensureDir(iterationDir);
|
|
167
|
-
const iterationState = {
|
|
168
|
-
iteration: iterationNumber,
|
|
169
|
-
status: "running",
|
|
170
|
-
work_item: workItem,
|
|
171
|
-
available_work_items: selectedWorkItems,
|
|
172
|
-
recommended_work_item_queue: selection.workItems,
|
|
173
|
-
selected_work_item_queue: selectedWorkItems,
|
|
174
|
-
work_item_annotations: selection.workItemAnnotations,
|
|
175
|
-
seen_work_item_keys: [...runState.seen_work_item_keys],
|
|
176
|
-
completed_work_item_keys: [...ledger.completed_work_item_keys],
|
|
177
|
-
last_completed_work_item: ledger.last_completed_work_item,
|
|
178
|
-
phase_results: {},
|
|
179
|
-
phase_visit_counts: {}
|
|
180
|
-
};
|
|
181
|
-
const iterationStateFile = path.join(iterationDir, "state.json");
|
|
182
|
-
await writeJson(iterationStateFile, iterationState);
|
|
183
|
-
let currentPhaseId = selection.nextPhaseId;
|
|
184
|
-
while (currentPhaseId) {
|
|
185
|
-
const phase = phasesById.get(currentPhaseId);
|
|
186
|
-
if (!phase) {
|
|
187
|
-
throw new Error(`Unknown phase "${currentPhaseId}"`);
|
|
188
|
-
}
|
|
189
|
-
const phaseResult = await runWorkflowPhase({
|
|
190
|
-
projectRoot,
|
|
191
|
-
runDir,
|
|
192
|
-
iterationDir,
|
|
193
|
-
iterationStateFile,
|
|
194
|
-
config,
|
|
195
|
-
phase,
|
|
196
|
-
iterationState,
|
|
197
|
-
runState,
|
|
198
|
-
iterationNumber,
|
|
199
|
-
maxIterations: selectedWorkItems.length,
|
|
200
|
-
workdir,
|
|
201
|
-
git: gitContext
|
|
202
|
-
});
|
|
203
|
-
if (!phaseResult.ok) {
|
|
204
|
-
throw new Error(phaseResult.error);
|
|
205
|
-
}
|
|
206
|
-
const nextTarget = resolveNextTarget(phase, phaseResult.outcome);
|
|
207
|
-
if (nextTarget === "stop_run") {
|
|
208
|
-
iterationState.status =
|
|
209
|
-
phaseResult.outcome === "no_work" ? "no_work" : "stopped";
|
|
210
|
-
runState.status =
|
|
211
|
-
phaseResult.outcome === "no_work" ? "completed_no_work" : "stopped";
|
|
212
|
-
await writeJson(iterationStateFile, iterationState);
|
|
213
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
214
|
-
console.log("");
|
|
215
|
-
console.log(`Done: ${runState.status}`);
|
|
216
|
-
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
if (nextTarget === "stop_iteration" || nextTarget === "next_iteration") {
|
|
220
|
-
iterationState.status = "completed";
|
|
221
|
-
runState.completed_iterations += 1;
|
|
222
|
-
ledger = await completeIterationWorkItem({
|
|
223
|
-
nyxDir,
|
|
224
|
-
ledger,
|
|
225
|
-
runState,
|
|
226
|
-
iterationState
|
|
227
|
-
});
|
|
228
|
-
iterationState.completed_work_item_keys = [
|
|
229
|
-
...ledger.completed_work_item_keys
|
|
230
|
-
];
|
|
231
|
-
iterationState.last_completed_work_item = ledger.last_completed_work_item;
|
|
232
|
-
await writeJson(iterationStateFile, iterationState);
|
|
233
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
currentPhaseId = nextTarget;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (config.workflow.final_phase) {
|
|
240
|
-
const finalized = await runFinalPhase({
|
|
241
|
-
projectRoot,
|
|
242
|
-
runDir,
|
|
243
|
-
config,
|
|
244
|
-
phasesById,
|
|
245
|
-
runState,
|
|
246
|
-
selectedWorkItems,
|
|
247
|
-
ledger,
|
|
248
|
-
workdir,
|
|
249
|
-
gitContext,
|
|
250
|
-
workItemAnnotations: selection.workItemAnnotations
|
|
251
|
-
});
|
|
252
|
-
if (!finalized) {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
runState.status =
|
|
257
|
-
selectedWorkItems.length >= config.workflow.max_iterations
|
|
258
|
-
? "completed_max_iterations"
|
|
259
|
-
: "completed_queue";
|
|
260
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
261
|
-
console.log("");
|
|
262
|
-
console.log(`Done: ${runState.status}`);
|
|
263
|
-
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Execute the run-scoped finalization flow after all iterations complete. This
|
|
268
|
-
* is a self-contained phase graph (symmetric to the iteration flow started by
|
|
269
|
-
* `entry_phase`): it begins at `workflow.final_phase` and follows each phase's
|
|
270
|
-
* `next`/`transitions` until it reaches a leaf phase (success) or `stop_run`
|
|
271
|
-
* (abort). This is where, for example, a review -> revision -> pull request
|
|
272
|
-
* chain runs. The engine only provides the branch/worktree context and
|
|
273
|
-
* run-level state; the GitHub/PR semantics live in the phase prompts, keeping
|
|
274
|
-
* the engine agnostic. A single leaf `final_phase` simply runs once.
|
|
275
|
-
*
|
|
276
|
-
* Returns true when finalization completed normally, false when a phase routed
|
|
277
|
-
* to `stop_run`.
|
|
278
|
-
*/
|
|
279
|
-
async function runFinalPhase(input) {
|
|
280
|
-
const finalPhaseId = input.config.workflow.final_phase;
|
|
281
|
-
if (!finalPhaseId) {
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
const finalizationDir = path.join(input.runDir, "finalization");
|
|
285
|
-
await ensureDir(finalizationDir);
|
|
286
|
-
const finalizationState = {
|
|
287
|
-
iteration: 0,
|
|
288
|
-
status: "running",
|
|
289
|
-
available_work_items: input.selectedWorkItems,
|
|
290
|
-
recommended_work_item_queue: input.selectedWorkItems,
|
|
291
|
-
selected_work_item_queue: input.selectedWorkItems,
|
|
292
|
-
work_item_annotations: input.workItemAnnotations,
|
|
293
|
-
seen_work_item_keys: [...input.runState.seen_work_item_keys],
|
|
294
|
-
completed_work_item_keys: [...input.ledger.completed_work_item_keys],
|
|
295
|
-
last_completed_work_item: input.ledger.last_completed_work_item,
|
|
296
|
-
phase_results: {},
|
|
297
|
-
phase_visit_counts: {}
|
|
298
|
-
};
|
|
299
|
-
const finalizationStateFile = path.join(finalizationDir, "state.json");
|
|
300
|
-
await writeJson(finalizationStateFile, finalizationState);
|
|
301
|
-
let currentPhaseId = finalPhaseId;
|
|
302
|
-
let lastResult;
|
|
303
|
-
while (currentPhaseId) {
|
|
304
|
-
const phase = input.phasesById.get(currentPhaseId);
|
|
305
|
-
if (!phase) {
|
|
306
|
-
throw new Error(`Unknown final phase "${currentPhaseId}"`);
|
|
307
|
-
}
|
|
308
|
-
const phaseResult = await runWorkflowPhase({
|
|
309
|
-
projectRoot: input.projectRoot,
|
|
310
|
-
runDir: input.runDir,
|
|
311
|
-
iterationDir: finalizationDir,
|
|
312
|
-
iterationStateFile: finalizationStateFile,
|
|
313
|
-
config: input.config,
|
|
314
|
-
phase,
|
|
315
|
-
iterationState: finalizationState,
|
|
316
|
-
runState: input.runState,
|
|
317
|
-
iterationNumber: "final",
|
|
318
|
-
maxIterations: input.selectedWorkItems.length,
|
|
319
|
-
workdir: input.workdir,
|
|
320
|
-
git: input.gitContext
|
|
321
|
-
});
|
|
322
|
-
if (!phaseResult.ok) {
|
|
323
|
-
throw new Error(phaseResult.error);
|
|
324
|
-
}
|
|
325
|
-
if (phaseResult.result !== undefined) {
|
|
326
|
-
lastResult = phaseResult.result;
|
|
327
|
-
}
|
|
328
|
-
const nextTarget = resolveNextTarget(phase, phaseResult.outcome);
|
|
329
|
-
if (nextTarget === "stop_run") {
|
|
330
|
-
finalizationState.status = "stopped";
|
|
331
|
-
input.runState.status = "stopped";
|
|
332
|
-
input.runState.final_result = phaseResult.result ?? { status: "stopped" };
|
|
333
|
-
await writeJson(finalizationStateFile, finalizationState);
|
|
334
|
-
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
335
|
-
console.log("");
|
|
336
|
-
console.log(`Done: ${input.runState.status}`);
|
|
337
|
-
console.log(`Run dir: ${relativeToProject(input.projectRoot, input.runDir)}`);
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
if (nextTarget === "next_iteration" || nextTarget === "stop_iteration") {
|
|
341
|
-
throw new Error(`Final phase "${phase.id}" routed to iteration-only target "${nextTarget}"; finalization phases must end at a leaf phase (no next/transitions) or "stop_run"`);
|
|
342
|
-
}
|
|
343
|
-
currentPhaseId = nextTarget;
|
|
344
|
-
}
|
|
345
|
-
finalizationState.status = "completed";
|
|
346
|
-
input.runState.final_result = lastResult ?? { status: "ok" };
|
|
347
|
-
await writeJson(finalizationStateFile, finalizationState);
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
async function runSelectionPhase(input) {
|
|
351
|
-
const selectionPhase = input.phasesById.get(input.config.workflow.entry_phase);
|
|
352
|
-
if (!selectionPhase) {
|
|
353
|
-
throw new Error(`Unknown phase "${input.config.workflow.entry_phase}"`);
|
|
354
|
-
}
|
|
355
|
-
const availableWorkItems = await loadAvailableWorkItems({
|
|
356
|
-
projectRoot: input.projectRoot,
|
|
357
|
-
config: input.config,
|
|
358
|
-
runState: input.runState,
|
|
359
|
-
ledger: input.ledger
|
|
360
|
-
});
|
|
361
|
-
const selectionDir = path.join(input.runDir, "selection");
|
|
362
|
-
await ensureDir(selectionDir);
|
|
363
|
-
const selectionState = {
|
|
364
|
-
iteration: 0,
|
|
365
|
-
status: "running",
|
|
366
|
-
available_work_items: availableWorkItems,
|
|
367
|
-
recommended_work_item_queue: [],
|
|
368
|
-
selected_work_item_queue: [],
|
|
369
|
-
work_item_annotations: [],
|
|
370
|
-
seen_work_item_keys: [...input.runState.seen_work_item_keys],
|
|
371
|
-
completed_work_item_keys: [...input.ledger.completed_work_item_keys],
|
|
372
|
-
last_completed_work_item: input.ledger.last_completed_work_item,
|
|
373
|
-
phase_results: {},
|
|
374
|
-
phase_visit_counts: {}
|
|
375
|
-
};
|
|
376
|
-
const selectionStateFile = path.join(selectionDir, "state.json");
|
|
377
|
-
await writeJson(selectionStateFile, selectionState);
|
|
378
|
-
const phaseResult = await runWorkflowPhase({
|
|
379
|
-
projectRoot: input.projectRoot,
|
|
380
|
-
runDir: input.runDir,
|
|
381
|
-
iterationDir: selectionDir,
|
|
382
|
-
iterationStateFile: selectionStateFile,
|
|
383
|
-
config: input.config,
|
|
384
|
-
phase: selectionPhase,
|
|
385
|
-
iterationState: selectionState,
|
|
386
|
-
runState: input.runState,
|
|
387
|
-
iterationNumber: "selection",
|
|
388
|
-
maxIterations: input.config.workflow.max_iterations
|
|
389
|
-
});
|
|
390
|
-
if (!phaseResult.ok) {
|
|
391
|
-
throw new Error(phaseResult.error);
|
|
392
|
-
}
|
|
393
|
-
const nextTarget = resolveNextTarget(selectionPhase, phaseResult.outcome);
|
|
394
|
-
if (nextTarget === "stop_run") {
|
|
395
|
-
selectionState.status =
|
|
396
|
-
phaseResult.outcome === "no_work" ? "no_work" : "stopped";
|
|
397
|
-
input.runState.status =
|
|
398
|
-
phaseResult.outcome === "no_work" ? "completed_no_work" : "stopped";
|
|
399
|
-
await writeJson(selectionStateFile, selectionState);
|
|
400
|
-
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
401
|
-
console.log("");
|
|
402
|
-
console.log(`Done: ${input.runState.status}`);
|
|
403
|
-
console.log(`Run dir: ${relativeToProject(input.projectRoot, input.runDir)}`);
|
|
404
|
-
return { status: "stopped" };
|
|
405
|
-
}
|
|
406
|
-
if (!nextTarget || isReservedTarget(nextTarget)) {
|
|
407
|
-
throw new Error(`Selection phase must transition to a runnable phase, got "${nextTarget ?? "none"}"`);
|
|
408
|
-
}
|
|
409
|
-
const workItems = readSelectedWorkItems({
|
|
410
|
-
config: input.config,
|
|
411
|
-
result: phaseResult.result,
|
|
412
|
-
availableWorkItems,
|
|
413
|
-
seenWorkItemKeys: selectionState.seen_work_item_keys,
|
|
414
|
-
completedWorkItemKeys: selectionState.completed_work_item_keys
|
|
415
|
-
});
|
|
416
|
-
const selectionGroups = readSelectionGroups(phaseResult.result);
|
|
417
|
-
const workItemAnnotations = readWorkItemAnnotations({
|
|
418
|
-
result: phaseResult.result,
|
|
419
|
-
availableWorkItems
|
|
420
|
-
});
|
|
421
|
-
selectionState.status = "completed";
|
|
422
|
-
selectionState.recommended_work_item_queue = workItems;
|
|
423
|
-
selectionState.selected_work_item_queue = workItems;
|
|
424
|
-
selectionState.work_item_annotations = workItemAnnotations;
|
|
425
|
-
await writeJson(selectionStateFile, selectionState);
|
|
426
|
-
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
427
|
-
return {
|
|
428
|
-
status: "selected",
|
|
429
|
-
workItems,
|
|
430
|
-
workItemAnnotations,
|
|
431
|
-
selectionGroups,
|
|
432
|
-
availableWorkItems,
|
|
433
|
-
nextPhaseId: nextTarget
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
async function runWorkflowPhase(input) {
|
|
437
|
-
const visits = (input.iterationState.phase_visit_counts[input.phase.id] ?? 0) + 1;
|
|
438
|
-
input.iterationState.phase_visit_counts[input.phase.id] = visits;
|
|
439
|
-
if (visits > input.phase.max_visits_per_iteration) {
|
|
440
|
-
input.iterationState.status = "failed";
|
|
441
|
-
input.runState.status = "failed";
|
|
442
|
-
await writeJson(input.iterationStateFile, input.iterationState);
|
|
443
|
-
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
444
|
-
return {
|
|
445
|
-
ok: false,
|
|
446
|
-
error: `Phase "${input.phase.id}" exceeded max_visits_per_iteration=${input.phase.max_visits_per_iteration}`
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
process.stdout.write(`[${input.iterationNumber}/${input.maxIterations}] ${input.phase.id} ... `);
|
|
450
|
-
await writeJson(input.iterationStateFile, input.iterationState);
|
|
451
|
-
const phaseResult = await runPhase({
|
|
452
|
-
projectRoot: input.projectRoot,
|
|
453
|
-
runDir: input.runDir,
|
|
454
|
-
iterationDir: input.iterationDir,
|
|
455
|
-
stateFile: input.iterationStateFile,
|
|
456
|
-
config: input.config,
|
|
457
|
-
phase: input.phase,
|
|
458
|
-
state: input.iterationState,
|
|
459
|
-
workdir: input.workdir,
|
|
460
|
-
git: input.git
|
|
461
|
-
});
|
|
462
|
-
if (!phaseResult.ok) {
|
|
463
|
-
console.log(pc.red("failed"));
|
|
464
|
-
input.iterationState.status = "failed";
|
|
465
|
-
input.iterationState.phase_results[input.phase.id] = {
|
|
466
|
-
status: "failed",
|
|
467
|
-
error: phaseResult.error
|
|
468
|
-
};
|
|
469
|
-
input.runState.status = "failed";
|
|
470
|
-
await writeJson(input.iterationStateFile, input.iterationState);
|
|
471
|
-
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
472
|
-
return phaseResult;
|
|
473
|
-
}
|
|
474
|
-
input.iterationState.phase_results[input.phase.id] =
|
|
475
|
-
phaseResult.result ?? { status: "ok" };
|
|
476
|
-
const workItem = readObjectProperty(phaseResult.result, "work_item");
|
|
477
|
-
if (workItem !== undefined && input.iterationNumber !== "selection") {
|
|
478
|
-
input.iterationState.work_item = workItem;
|
|
479
|
-
const key = readObjectProperty(workItem, "key");
|
|
480
|
-
if (typeof key === "string" &&
|
|
481
|
-
!input.runState.seen_work_item_keys.includes(key)) {
|
|
482
|
-
input.runState.seen_work_item_keys.push(key);
|
|
483
|
-
input.iterationState.seen_work_item_keys = [
|
|
484
|
-
...input.runState.seen_work_item_keys
|
|
485
|
-
];
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
const label = phaseResult.outcome ?? "ok";
|
|
489
|
-
console.log(pc.green(label));
|
|
490
|
-
await writeJson(input.iterationStateFile, input.iterationState);
|
|
491
|
-
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
492
|
-
return phaseResult;
|
|
493
|
-
}
|
|
494
|
-
export async function confirmWorkItemsWithCheckbox(input) {
|
|
495
|
-
if (input.availableWorkItems.length === 0) {
|
|
496
|
-
return [];
|
|
497
|
-
}
|
|
498
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
499
|
-
throw new Error("Work item selection confirmation requires an interactive TTY");
|
|
500
|
-
}
|
|
501
|
-
const confirmationView = buildConfirmationView(input);
|
|
502
|
-
const selectedKeys = await checkbox({
|
|
503
|
-
message: `Confirm work items to run (max ${input.maxIterations})`,
|
|
504
|
-
choices: confirmationView.choices,
|
|
505
|
-
required: false,
|
|
506
|
-
pageSize: Math.min(Math.max(input.availableWorkItems.length + 2, 5), 20),
|
|
507
|
-
validate: (selected) => selected.length <= input.maxIterations ||
|
|
508
|
-
`Select at most ${input.maxIterations} work items.`
|
|
509
|
-
});
|
|
510
|
-
const selected = new Set(selectedKeys);
|
|
511
|
-
return confirmationView.workItems.filter((item) => selected.has(item.key));
|
|
512
|
-
}
|
|
513
|
-
function buildConfirmationView(input) {
|
|
514
|
-
const choices = [];
|
|
515
|
-
const workItems = [];
|
|
516
|
-
const byKey = new Map(input.availableWorkItems.map((item) => [item.key, item]));
|
|
517
|
-
const recommendedKeys = new Set(input.recommendedWorkItems.map((item) => item.key));
|
|
518
|
-
const annotationsByKey = buildWorkItemKindMap(input.workItemAnnotations ?? []);
|
|
519
|
-
const displayed = new Set();
|
|
520
|
-
for (const group of input.selectionGroups) {
|
|
521
|
-
const groupItems = group.work_item_keys
|
|
522
|
-
.map((key) => byKey.get(key))
|
|
523
|
-
.filter((item) => {
|
|
524
|
-
if (!item) {
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
527
|
-
return !displayed.has(item.key);
|
|
528
|
-
});
|
|
529
|
-
if (groupItems.length === 0) {
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
choices.push(new Separator(`-- ${group.title} --`));
|
|
533
|
-
for (const item of groupItems) {
|
|
534
|
-
choices.push(buildConfirmationChoice({ item, recommendedKeys, annotationsByKey }));
|
|
535
|
-
workItems.push(item);
|
|
536
|
-
displayed.add(item.key);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const ungroupedItems = input.availableWorkItems.filter((item) => !displayed.has(item.key));
|
|
540
|
-
if (input.selectionGroups.length > 0 && ungroupedItems.length > 0) {
|
|
541
|
-
choices.push(new Separator("-- Ungrouped --"));
|
|
542
|
-
}
|
|
543
|
-
for (const item of ungroupedItems) {
|
|
544
|
-
choices.push(buildConfirmationChoice({ item, recommendedKeys, annotationsByKey }));
|
|
545
|
-
workItems.push(item);
|
|
546
|
-
}
|
|
547
|
-
return { choices, workItems };
|
|
548
|
-
}
|
|
549
|
-
function buildConfirmationChoice(input) {
|
|
550
|
-
return {
|
|
551
|
-
value: input.item.key,
|
|
552
|
-
name: formatWorkItemChoiceName({
|
|
553
|
-
item: input.item,
|
|
554
|
-
annotationsByKey: input.annotationsByKey
|
|
555
|
-
}),
|
|
556
|
-
description: input.item.excerpt,
|
|
557
|
-
checked: input.recommendedKeys.has(input.item.key)
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
function resolveNextTarget(phase, outcome) {
|
|
561
|
-
if (phase.transitions) {
|
|
562
|
-
if (!outcome) {
|
|
563
|
-
throw new Error(`Phase "${phase.id}" requires an outcome`);
|
|
564
|
-
}
|
|
565
|
-
const target = phase.transitions[outcome];
|
|
566
|
-
if (!target) {
|
|
567
|
-
throw new Error(`Phase "${phase.id}" returned unknown outcome "${outcome}"`);
|
|
568
|
-
}
|
|
569
|
-
return target;
|
|
570
|
-
}
|
|
571
|
-
return phase.next;
|
|
572
|
-
}
|
|
573
|
-
async function loadAvailableWorkItems(input) {
|
|
574
|
-
const candidates = await listWorkItemCandidates({
|
|
575
|
-
projectRoot: input.projectRoot,
|
|
576
|
-
config: input.config
|
|
577
|
-
});
|
|
578
|
-
return filterAvailableWorkItems({
|
|
579
|
-
candidates,
|
|
580
|
-
seenWorkItemKeys: input.runState.seen_work_item_keys,
|
|
581
|
-
completedWorkItemKeys: input.ledger.completed_work_item_keys
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
async function completeIterationWorkItem(input) {
|
|
585
|
-
const ledger = markWorkItemCompleted({
|
|
586
|
-
ledger: input.ledger,
|
|
587
|
-
workItem: input.iterationState.work_item
|
|
588
|
-
});
|
|
589
|
-
input.runState.completed_work_item_keys = [
|
|
590
|
-
...ledger.completed_work_item_keys
|
|
591
|
-
];
|
|
592
|
-
input.runState.last_completed_work_item = ledger.last_completed_work_item;
|
|
593
|
-
await writeWorkItemLedger(input.nyxDir, ledger);
|
|
594
|
-
return ledger;
|
|
595
|
-
}
|
|
596
|
-
function readSelectedWorkItems(input) {
|
|
597
|
-
const workItems = readObjectProperty(input.result, "work_items");
|
|
598
|
-
const legacyWorkItem = readObjectProperty(input.result, "work_item");
|
|
599
|
-
const selection = workItems ?? (legacyWorkItem === undefined ? undefined : [legacyWorkItem]);
|
|
600
|
-
if (selection === undefined) {
|
|
601
|
-
throw new Error('Selection phase returned "selected" without work_items');
|
|
602
|
-
}
|
|
603
|
-
const validation = validateWorkItemQueue({
|
|
604
|
-
config: input.config,
|
|
605
|
-
workItems: selection,
|
|
606
|
-
availableWorkItems: input.availableWorkItems,
|
|
607
|
-
seenWorkItemKeys: input.seenWorkItemKeys,
|
|
608
|
-
completedWorkItemKeys: input.completedWorkItemKeys
|
|
609
|
-
});
|
|
610
|
-
if (!validation.ok) {
|
|
611
|
-
throw new Error(validation.error);
|
|
612
|
-
}
|
|
613
|
-
return validation.workItems;
|
|
614
|
-
}
|
|
615
|
-
function normalizeConfirmedWorkItems(input) {
|
|
616
|
-
const availableByKey = new Map(input.availableWorkItems.map((item) => [item.key, item]));
|
|
617
|
-
const selected = new Set();
|
|
618
|
-
const normalized = [];
|
|
619
|
-
for (const item of input.confirmed) {
|
|
620
|
-
const available = availableByKey.get(item.key);
|
|
621
|
-
if (!available) {
|
|
622
|
-
throw new Error(`Confirmed work item key "${item.key}" is not in available_work_items`);
|
|
623
|
-
}
|
|
624
|
-
if (selected.has(item.key)) {
|
|
625
|
-
throw new Error(`Confirmed work item key "${item.key}" was selected twice`);
|
|
626
|
-
}
|
|
627
|
-
selected.add(item.key);
|
|
628
|
-
normalized.push(available);
|
|
629
|
-
}
|
|
630
|
-
return normalized;
|
|
631
|
-
}
|
|
632
|
-
function readWorkItemAnnotations(input) {
|
|
633
|
-
return normalizeWorkItemAnnotations({
|
|
634
|
-
annotations: readObjectProperty(input.result, "work_item_annotations"),
|
|
635
|
-
availableWorkItems: input.availableWorkItems
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
function readSelectionGroups(result) {
|
|
639
|
-
const groups = readObjectProperty(result, "selection_groups");
|
|
640
|
-
if (!Array.isArray(groups)) {
|
|
641
|
-
return [];
|
|
642
|
-
}
|
|
643
|
-
return groups
|
|
644
|
-
.map((group) => {
|
|
645
|
-
if (!isRecord(group) || typeof group.title !== "string") {
|
|
646
|
-
return undefined;
|
|
647
|
-
}
|
|
648
|
-
const keys = Array.isArray(group.work_item_keys)
|
|
649
|
-
? group.work_item_keys.filter((key) => typeof key === "string")
|
|
650
|
-
: [];
|
|
651
|
-
if (keys.length === 0) {
|
|
652
|
-
return undefined;
|
|
653
|
-
}
|
|
654
|
-
const normalized = {
|
|
655
|
-
title: group.title,
|
|
656
|
-
work_item_keys: keys
|
|
657
|
-
};
|
|
658
|
-
if (typeof group.kind === "string") {
|
|
659
|
-
normalized.kind = group.kind;
|
|
660
|
-
}
|
|
661
|
-
return normalized;
|
|
662
|
-
})
|
|
663
|
-
.filter((group) => Boolean(group));
|
|
664
|
-
}
|
|
665
|
-
function isReservedTarget(target) {
|
|
666
|
-
return (target === "stop_run" ||
|
|
667
|
-
target === "stop_iteration" ||
|
|
668
|
-
target === "next_iteration");
|
|
669
|
-
}
|
|
670
|
-
function readObjectProperty(value, key) {
|
|
671
|
-
if (value &&
|
|
672
|
-
typeof value === "object" &&
|
|
673
|
-
Object.prototype.hasOwnProperty.call(value, key)) {
|
|
674
|
-
return value[key];
|
|
675
|
-
}
|
|
676
|
-
return undefined;
|
|
677
|
-
}
|
|
678
|
-
function isRecord(value) {
|
|
679
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
680
|
-
}
|