@nyxa/nyx-agent 0.2.1 → 0.3.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/dist/cli.js +2 -1
- package/dist/commands/init.js +51 -39
- package/dist/config/schema.js +37 -1
- package/dist/runtime/buildPrompt.js +25 -0
- package/dist/runtime/ledger.js +118 -0
- package/dist/runtime/runPhase.js +64 -0
- package/dist/runtime/runWorkflow.js +375 -43
- package/dist/runtime/validateWorkItem.js +212 -0
- package/dist/runtime/workItems.js +212 -0
- package/docs/nyxagent-v0-spec.md +80 -24
- package/package.json +1 -1
- package/templates/default/prompts/selection.md +28 -10
- package/templates/default/schemas/selection.schema.json +52 -8
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2
3
|
import pc from "picocolors";
|
|
3
4
|
import { loadConfig } from "../config/loadConfig.js";
|
|
4
5
|
import { ensureDir, writeJson } from "./files.js";
|
|
5
6
|
import { getGitSnapshot } from "./git.js";
|
|
7
|
+
import { markWorkItemCompleted, readWorkItemLedger, writeWorkItemLedger } from "./ledger.js";
|
|
6
8
|
import { getNyxDir, relativeToProject } from "./paths.js";
|
|
7
9
|
import { runPhase } from "./runPhase.js";
|
|
8
10
|
import { createRunId } from "./time.js";
|
|
11
|
+
import { validateWorkItemQueue } from "./validateWorkItem.js";
|
|
12
|
+
import { filterAvailableWorkItems, listWorkItemCandidates } from "./workItems.js";
|
|
9
13
|
export async function runWorkflow(options) {
|
|
10
14
|
const projectRoot = path.resolve(options.projectRoot);
|
|
11
15
|
const nyxDir = getNyxDir(projectRoot);
|
|
@@ -14,13 +18,21 @@ export async function runWorkflow(options) {
|
|
|
14
18
|
const runId = createRunId();
|
|
15
19
|
const runDir = path.join(nyxDir, "runs", runId);
|
|
16
20
|
await ensureDir(runDir);
|
|
21
|
+
let ledger = await readWorkItemLedger(nyxDir);
|
|
22
|
+
await writeWorkItemLedger(nyxDir, ledger);
|
|
17
23
|
const git = await getGitSnapshot(projectRoot);
|
|
18
24
|
const runState = {
|
|
19
25
|
run_id: runId,
|
|
20
26
|
status: "running",
|
|
21
27
|
current_iteration: 0,
|
|
22
28
|
completed_iterations: 0,
|
|
23
|
-
|
|
29
|
+
available_work_items: [],
|
|
30
|
+
selected_work_item_queue: [],
|
|
31
|
+
skipped_work_item_keys: [],
|
|
32
|
+
selection_groups: [],
|
|
33
|
+
seen_work_item_keys: [],
|
|
34
|
+
completed_work_item_keys: [...ledger.completed_work_item_keys],
|
|
35
|
+
last_completed_work_item: ledger.last_completed_work_item
|
|
24
36
|
};
|
|
25
37
|
await writeJson(path.join(runDir, "run.json"), {
|
|
26
38
|
run_id: runId,
|
|
@@ -40,7 +52,58 @@ export async function runWorkflow(options) {
|
|
|
40
52
|
console.log(`Model: ${config.model.name}`);
|
|
41
53
|
console.log("");
|
|
42
54
|
const phasesById = new Map(config.phases.map((phase) => [phase.id, phase]));
|
|
43
|
-
|
|
55
|
+
const selection = await runSelectionPhase({
|
|
56
|
+
projectRoot,
|
|
57
|
+
runDir,
|
|
58
|
+
config,
|
|
59
|
+
phasesById,
|
|
60
|
+
runState,
|
|
61
|
+
ledger
|
|
62
|
+
});
|
|
63
|
+
if (selection.status === "stopped") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
runState.available_work_items = selection.availableWorkItems;
|
|
67
|
+
runState.selection_groups = selection.selectionGroups;
|
|
68
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
69
|
+
let confirmedWorkItems;
|
|
70
|
+
try {
|
|
71
|
+
confirmedWorkItems = normalizeConfirmedWorkItems({
|
|
72
|
+
queue: selection.workItems,
|
|
73
|
+
confirmed: await (options.confirmWorkItems ?? confirmWorkItemsWithCheckbox)({
|
|
74
|
+
workItems: selection.workItems,
|
|
75
|
+
selectionGroups: selection.selectionGroups,
|
|
76
|
+
maxIterations: config.workflow.max_iterations
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
runState.status = "failed";
|
|
82
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
const selectedWorkItems = confirmedWorkItems.slice(0, config.workflow.max_iterations);
|
|
86
|
+
const selectedKeys = new Set(selectedWorkItems.map((item) => item.key));
|
|
87
|
+
runState.selected_work_item_queue = selectedWorkItems;
|
|
88
|
+
runState.skipped_work_item_keys = selection.workItems
|
|
89
|
+
.filter((item) => !selectedKeys.has(item.key))
|
|
90
|
+
.map((item) => item.key);
|
|
91
|
+
runState.seen_work_item_keys = selectedWorkItems.map((item) => item.key);
|
|
92
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
93
|
+
if (confirmedWorkItems.length > config.workflow.max_iterations) {
|
|
94
|
+
console.log(pc.yellow(`Only the first ${config.workflow.max_iterations} selected work items will run.`));
|
|
95
|
+
}
|
|
96
|
+
if (selectedWorkItems.length === 0) {
|
|
97
|
+
runState.status = "completed_no_selected_work";
|
|
98
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(`Done: ${runState.status}`);
|
|
101
|
+
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (let queueIndex = 0; queueIndex < selectedWorkItems.length; queueIndex += 1) {
|
|
105
|
+
const iterationNumber = queueIndex + 1;
|
|
106
|
+
const workItem = selectedWorkItems[queueIndex];
|
|
44
107
|
runState.current_iteration = iterationNumber;
|
|
45
108
|
await writeJson(path.join(runDir, "state.json"), runState);
|
|
46
109
|
const iterationDir = path.join(runDir, "iterations", String(iterationNumber).padStart(3, "0"));
|
|
@@ -48,68 +111,42 @@ export async function runWorkflow(options) {
|
|
|
48
111
|
const iterationState = {
|
|
49
112
|
iteration: iterationNumber,
|
|
50
113
|
status: "running",
|
|
114
|
+
work_item: workItem,
|
|
115
|
+
available_work_items: selectedWorkItems,
|
|
116
|
+
selected_work_item_queue: selectedWorkItems,
|
|
51
117
|
seen_work_item_keys: [...runState.seen_work_item_keys],
|
|
118
|
+
completed_work_item_keys: [...ledger.completed_work_item_keys],
|
|
119
|
+
last_completed_work_item: ledger.last_completed_work_item,
|
|
52
120
|
phase_results: {},
|
|
53
121
|
phase_visit_counts: {}
|
|
54
122
|
};
|
|
55
123
|
const iterationStateFile = path.join(iterationDir, "state.json");
|
|
56
124
|
await writeJson(iterationStateFile, iterationState);
|
|
57
|
-
let currentPhaseId =
|
|
125
|
+
let currentPhaseId = selection.nextPhaseId;
|
|
58
126
|
while (currentPhaseId) {
|
|
59
127
|
const phase = phasesById.get(currentPhaseId);
|
|
60
128
|
if (!phase) {
|
|
61
129
|
throw new Error(`Unknown phase "${currentPhaseId}"`);
|
|
62
130
|
}
|
|
63
|
-
const
|
|
64
|
-
iterationState.phase_visit_counts[phase.id] = visits;
|
|
65
|
-
if (visits > phase.max_visits_per_iteration) {
|
|
66
|
-
iterationState.status = "failed";
|
|
67
|
-
runState.status = "failed";
|
|
68
|
-
await writeJson(iterationStateFile, iterationState);
|
|
69
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
70
|
-
throw new Error(`Phase "${phase.id}" exceeded max_visits_per_iteration=${phase.max_visits_per_iteration}`);
|
|
71
|
-
}
|
|
72
|
-
process.stdout.write(`[${iterationNumber}/${config.workflow.max_iterations}] ${phase.id} ... `);
|
|
73
|
-
await writeJson(iterationStateFile, iterationState);
|
|
74
|
-
const phaseResult = await runPhase({
|
|
131
|
+
const phaseResult = await runWorkflowPhase({
|
|
75
132
|
projectRoot,
|
|
76
133
|
runDir,
|
|
77
134
|
iterationDir,
|
|
78
|
-
|
|
135
|
+
iterationStateFile,
|
|
79
136
|
config,
|
|
80
137
|
phase,
|
|
81
|
-
|
|
138
|
+
iterationState,
|
|
139
|
+
runState,
|
|
140
|
+
iterationNumber,
|
|
141
|
+
maxIterations: selectedWorkItems.length
|
|
82
142
|
});
|
|
83
143
|
if (!phaseResult.ok) {
|
|
84
|
-
console.log(pc.red("failed"));
|
|
85
|
-
iterationState.status = "failed";
|
|
86
|
-
iterationState.phase_results[phase.id] = {
|
|
87
|
-
status: "failed",
|
|
88
|
-
error: phaseResult.error
|
|
89
|
-
};
|
|
90
|
-
runState.status = "failed";
|
|
91
|
-
await writeJson(iterationStateFile, iterationState);
|
|
92
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
93
144
|
throw new Error(phaseResult.error);
|
|
94
145
|
}
|
|
95
|
-
iterationState.phase_results[phase.id] =
|
|
96
|
-
phaseResult.result ?? { status: "ok" };
|
|
97
|
-
const workItem = readObjectProperty(phaseResult.result, "work_item");
|
|
98
|
-
if (workItem !== undefined) {
|
|
99
|
-
iterationState.work_item = workItem;
|
|
100
|
-
const key = readObjectProperty(workItem, "key");
|
|
101
|
-
if (typeof key === "string" && !runState.seen_work_item_keys.includes(key)) {
|
|
102
|
-
runState.seen_work_item_keys.push(key);
|
|
103
|
-
iterationState.seen_work_item_keys = [...runState.seen_work_item_keys];
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
146
|
const nextTarget = resolveNextTarget(phase, phaseResult.outcome);
|
|
107
|
-
const label = phaseResult.outcome ?? "ok";
|
|
108
|
-
console.log(pc.green(label));
|
|
109
|
-
await writeJson(iterationStateFile, iterationState);
|
|
110
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
111
147
|
if (nextTarget === "stop_run") {
|
|
112
|
-
iterationState.status =
|
|
148
|
+
iterationState.status =
|
|
149
|
+
phaseResult.outcome === "no_work" ? "no_work" : "stopped";
|
|
113
150
|
runState.status =
|
|
114
151
|
phaseResult.outcome === "no_work" ? "completed_no_work" : "stopped";
|
|
115
152
|
await writeJson(iterationStateFile, iterationState);
|
|
@@ -122,6 +159,16 @@ export async function runWorkflow(options) {
|
|
|
122
159
|
if (nextTarget === "stop_iteration" || nextTarget === "next_iteration") {
|
|
123
160
|
iterationState.status = "completed";
|
|
124
161
|
runState.completed_iterations += 1;
|
|
162
|
+
ledger = await completeIterationWorkItem({
|
|
163
|
+
nyxDir,
|
|
164
|
+
ledger,
|
|
165
|
+
runState,
|
|
166
|
+
iterationState
|
|
167
|
+
});
|
|
168
|
+
iterationState.completed_work_item_keys = [
|
|
169
|
+
...ledger.completed_work_item_keys
|
|
170
|
+
];
|
|
171
|
+
iterationState.last_completed_work_item = ledger.last_completed_work_item;
|
|
125
172
|
await writeJson(iterationStateFile, iterationState);
|
|
126
173
|
await writeJson(path.join(runDir, "state.json"), runState);
|
|
127
174
|
break;
|
|
@@ -129,12 +176,203 @@ export async function runWorkflow(options) {
|
|
|
129
176
|
currentPhaseId = nextTarget;
|
|
130
177
|
}
|
|
131
178
|
}
|
|
132
|
-
runState.status =
|
|
179
|
+
runState.status =
|
|
180
|
+
selectedWorkItems.length >= config.workflow.max_iterations
|
|
181
|
+
? "completed_max_iterations"
|
|
182
|
+
: "completed_queue";
|
|
133
183
|
await writeJson(path.join(runDir, "state.json"), runState);
|
|
134
184
|
console.log("");
|
|
135
185
|
console.log(`Done: ${runState.status}`);
|
|
136
186
|
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
137
187
|
}
|
|
188
|
+
async function runSelectionPhase(input) {
|
|
189
|
+
const selectionPhase = input.phasesById.get(input.config.workflow.entry_phase);
|
|
190
|
+
if (!selectionPhase) {
|
|
191
|
+
throw new Error(`Unknown phase "${input.config.workflow.entry_phase}"`);
|
|
192
|
+
}
|
|
193
|
+
const availableWorkItems = await loadAvailableWorkItems({
|
|
194
|
+
projectRoot: input.projectRoot,
|
|
195
|
+
config: input.config,
|
|
196
|
+
runState: input.runState,
|
|
197
|
+
ledger: input.ledger
|
|
198
|
+
});
|
|
199
|
+
const selectionDir = path.join(input.runDir, "selection");
|
|
200
|
+
await ensureDir(selectionDir);
|
|
201
|
+
const selectionState = {
|
|
202
|
+
iteration: 0,
|
|
203
|
+
status: "running",
|
|
204
|
+
available_work_items: availableWorkItems,
|
|
205
|
+
selected_work_item_queue: [],
|
|
206
|
+
seen_work_item_keys: [...input.runState.seen_work_item_keys],
|
|
207
|
+
completed_work_item_keys: [...input.ledger.completed_work_item_keys],
|
|
208
|
+
last_completed_work_item: input.ledger.last_completed_work_item,
|
|
209
|
+
phase_results: {},
|
|
210
|
+
phase_visit_counts: {}
|
|
211
|
+
};
|
|
212
|
+
const selectionStateFile = path.join(selectionDir, "state.json");
|
|
213
|
+
await writeJson(selectionStateFile, selectionState);
|
|
214
|
+
const phaseResult = await runWorkflowPhase({
|
|
215
|
+
projectRoot: input.projectRoot,
|
|
216
|
+
runDir: input.runDir,
|
|
217
|
+
iterationDir: selectionDir,
|
|
218
|
+
iterationStateFile: selectionStateFile,
|
|
219
|
+
config: input.config,
|
|
220
|
+
phase: selectionPhase,
|
|
221
|
+
iterationState: selectionState,
|
|
222
|
+
runState: input.runState,
|
|
223
|
+
iterationNumber: "selection",
|
|
224
|
+
maxIterations: input.config.workflow.max_iterations
|
|
225
|
+
});
|
|
226
|
+
if (!phaseResult.ok) {
|
|
227
|
+
throw new Error(phaseResult.error);
|
|
228
|
+
}
|
|
229
|
+
const nextTarget = resolveNextTarget(selectionPhase, phaseResult.outcome);
|
|
230
|
+
if (nextTarget === "stop_run") {
|
|
231
|
+
selectionState.status =
|
|
232
|
+
phaseResult.outcome === "no_work" ? "no_work" : "stopped";
|
|
233
|
+
input.runState.status =
|
|
234
|
+
phaseResult.outcome === "no_work" ? "completed_no_work" : "stopped";
|
|
235
|
+
await writeJson(selectionStateFile, selectionState);
|
|
236
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log(`Done: ${input.runState.status}`);
|
|
239
|
+
console.log(`Run dir: ${relativeToProject(input.projectRoot, input.runDir)}`);
|
|
240
|
+
return { status: "stopped" };
|
|
241
|
+
}
|
|
242
|
+
if (!nextTarget || isReservedTarget(nextTarget)) {
|
|
243
|
+
throw new Error(`Selection phase must transition to a runnable phase, got "${nextTarget ?? "none"}"`);
|
|
244
|
+
}
|
|
245
|
+
const workItems = readSelectedWorkItems({
|
|
246
|
+
config: input.config,
|
|
247
|
+
result: phaseResult.result,
|
|
248
|
+
availableWorkItems,
|
|
249
|
+
seenWorkItemKeys: selectionState.seen_work_item_keys,
|
|
250
|
+
completedWorkItemKeys: selectionState.completed_work_item_keys
|
|
251
|
+
});
|
|
252
|
+
const selectionGroups = readSelectionGroups(phaseResult.result);
|
|
253
|
+
selectionState.status = "completed";
|
|
254
|
+
selectionState.selected_work_item_queue = workItems;
|
|
255
|
+
await writeJson(selectionStateFile, selectionState);
|
|
256
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
257
|
+
return {
|
|
258
|
+
status: "selected",
|
|
259
|
+
workItems,
|
|
260
|
+
selectionGroups,
|
|
261
|
+
availableWorkItems,
|
|
262
|
+
nextPhaseId: nextTarget
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async function runWorkflowPhase(input) {
|
|
266
|
+
const visits = (input.iterationState.phase_visit_counts[input.phase.id] ?? 0) + 1;
|
|
267
|
+
input.iterationState.phase_visit_counts[input.phase.id] = visits;
|
|
268
|
+
if (visits > input.phase.max_visits_per_iteration) {
|
|
269
|
+
input.iterationState.status = "failed";
|
|
270
|
+
input.runState.status = "failed";
|
|
271
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
272
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
error: `Phase "${input.phase.id}" exceeded max_visits_per_iteration=${input.phase.max_visits_per_iteration}`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
process.stdout.write(`[${input.iterationNumber}/${input.maxIterations}] ${input.phase.id} ... `);
|
|
279
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
280
|
+
const phaseResult = await runPhase({
|
|
281
|
+
projectRoot: input.projectRoot,
|
|
282
|
+
runDir: input.runDir,
|
|
283
|
+
iterationDir: input.iterationDir,
|
|
284
|
+
stateFile: input.iterationStateFile,
|
|
285
|
+
config: input.config,
|
|
286
|
+
phase: input.phase,
|
|
287
|
+
state: input.iterationState
|
|
288
|
+
});
|
|
289
|
+
if (!phaseResult.ok) {
|
|
290
|
+
console.log(pc.red("failed"));
|
|
291
|
+
input.iterationState.status = "failed";
|
|
292
|
+
input.iterationState.phase_results[input.phase.id] = {
|
|
293
|
+
status: "failed",
|
|
294
|
+
error: phaseResult.error
|
|
295
|
+
};
|
|
296
|
+
input.runState.status = "failed";
|
|
297
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
298
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
299
|
+
return phaseResult;
|
|
300
|
+
}
|
|
301
|
+
input.iterationState.phase_results[input.phase.id] =
|
|
302
|
+
phaseResult.result ?? { status: "ok" };
|
|
303
|
+
const workItem = readObjectProperty(phaseResult.result, "work_item");
|
|
304
|
+
if (workItem !== undefined && input.iterationNumber !== "selection") {
|
|
305
|
+
input.iterationState.work_item = workItem;
|
|
306
|
+
const key = readObjectProperty(workItem, "key");
|
|
307
|
+
if (typeof key === "string" &&
|
|
308
|
+
!input.runState.seen_work_item_keys.includes(key)) {
|
|
309
|
+
input.runState.seen_work_item_keys.push(key);
|
|
310
|
+
input.iterationState.seen_work_item_keys = [
|
|
311
|
+
...input.runState.seen_work_item_keys
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const label = phaseResult.outcome ?? "ok";
|
|
316
|
+
console.log(pc.green(label));
|
|
317
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
318
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
319
|
+
return phaseResult;
|
|
320
|
+
}
|
|
321
|
+
export async function confirmWorkItemsWithCheckbox(input) {
|
|
322
|
+
if (input.workItems.length === 0) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
326
|
+
throw new Error("Work item selection confirmation requires an interactive TTY");
|
|
327
|
+
}
|
|
328
|
+
const selectedKeys = await checkbox({
|
|
329
|
+
message: `Confirm work items to run (max ${input.maxIterations})`,
|
|
330
|
+
choices: buildConfirmationChoices(input),
|
|
331
|
+
required: false,
|
|
332
|
+
pageSize: Math.min(Math.max(input.workItems.length + 2, 5), 20)
|
|
333
|
+
});
|
|
334
|
+
const selected = new Set(selectedKeys);
|
|
335
|
+
return input.workItems.filter((item) => selected.has(item.key));
|
|
336
|
+
}
|
|
337
|
+
function buildConfirmationChoices(input) {
|
|
338
|
+
const choices = [];
|
|
339
|
+
const byKey = new Map(input.workItems.map((item) => [item.key, item]));
|
|
340
|
+
const displayed = new Set();
|
|
341
|
+
for (const group of input.selectionGroups) {
|
|
342
|
+
const groupItems = group.work_item_keys
|
|
343
|
+
.map((key) => byKey.get(key))
|
|
344
|
+
.filter((item) => {
|
|
345
|
+
if (!item) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
return !displayed.has(item.key);
|
|
349
|
+
});
|
|
350
|
+
if (groupItems.length === 0) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
choices.push(new Separator(`-- ${group.title} --`));
|
|
354
|
+
for (const item of groupItems) {
|
|
355
|
+
choices.push(buildConfirmationChoice(item));
|
|
356
|
+
displayed.add(item.key);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const ungroupedItems = input.workItems.filter((item) => !displayed.has(item.key));
|
|
360
|
+
if (input.selectionGroups.length > 0 && ungroupedItems.length > 0) {
|
|
361
|
+
choices.push(new Separator("-- Ungrouped --"));
|
|
362
|
+
}
|
|
363
|
+
for (const item of ungroupedItems) {
|
|
364
|
+
choices.push(buildConfirmationChoice(item));
|
|
365
|
+
}
|
|
366
|
+
return choices;
|
|
367
|
+
}
|
|
368
|
+
function buildConfirmationChoice(item) {
|
|
369
|
+
return {
|
|
370
|
+
value: item.key,
|
|
371
|
+
name: `${item.title} (${item.key})`,
|
|
372
|
+
description: item.excerpt,
|
|
373
|
+
checked: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
138
376
|
function resolveNextTarget(phase, outcome) {
|
|
139
377
|
if (phase.transitions) {
|
|
140
378
|
if (!outcome) {
|
|
@@ -148,6 +386,97 @@ function resolveNextTarget(phase, outcome) {
|
|
|
148
386
|
}
|
|
149
387
|
return phase.next;
|
|
150
388
|
}
|
|
389
|
+
async function loadAvailableWorkItems(input) {
|
|
390
|
+
const candidates = await listWorkItemCandidates({
|
|
391
|
+
projectRoot: input.projectRoot,
|
|
392
|
+
config: input.config
|
|
393
|
+
});
|
|
394
|
+
return filterAvailableWorkItems({
|
|
395
|
+
candidates,
|
|
396
|
+
seenWorkItemKeys: input.runState.seen_work_item_keys,
|
|
397
|
+
completedWorkItemKeys: input.ledger.completed_work_item_keys
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
async function completeIterationWorkItem(input) {
|
|
401
|
+
const ledger = markWorkItemCompleted({
|
|
402
|
+
ledger: input.ledger,
|
|
403
|
+
workItem: input.iterationState.work_item
|
|
404
|
+
});
|
|
405
|
+
input.runState.completed_work_item_keys = [
|
|
406
|
+
...ledger.completed_work_item_keys
|
|
407
|
+
];
|
|
408
|
+
input.runState.last_completed_work_item = ledger.last_completed_work_item;
|
|
409
|
+
await writeWorkItemLedger(input.nyxDir, ledger);
|
|
410
|
+
return ledger;
|
|
411
|
+
}
|
|
412
|
+
function readSelectedWorkItems(input) {
|
|
413
|
+
const workItems = readObjectProperty(input.result, "work_items");
|
|
414
|
+
const legacyWorkItem = readObjectProperty(input.result, "work_item");
|
|
415
|
+
const selection = workItems ?? (legacyWorkItem === undefined ? undefined : [legacyWorkItem]);
|
|
416
|
+
if (selection === undefined) {
|
|
417
|
+
throw new Error('Selection phase returned "selected" without work_items');
|
|
418
|
+
}
|
|
419
|
+
const validation = validateWorkItemQueue({
|
|
420
|
+
config: input.config,
|
|
421
|
+
workItems: selection,
|
|
422
|
+
availableWorkItems: input.availableWorkItems,
|
|
423
|
+
seenWorkItemKeys: input.seenWorkItemKeys,
|
|
424
|
+
completedWorkItemKeys: input.completedWorkItemKeys
|
|
425
|
+
});
|
|
426
|
+
if (!validation.ok) {
|
|
427
|
+
throw new Error(validation.error);
|
|
428
|
+
}
|
|
429
|
+
return validation.workItems;
|
|
430
|
+
}
|
|
431
|
+
function normalizeConfirmedWorkItems(input) {
|
|
432
|
+
const queuedByKey = new Map(input.queue.map((item) => [item.key, item]));
|
|
433
|
+
const selected = new Set();
|
|
434
|
+
const normalized = [];
|
|
435
|
+
for (const item of input.confirmed) {
|
|
436
|
+
const queued = queuedByKey.get(item.key);
|
|
437
|
+
if (!queued) {
|
|
438
|
+
throw new Error(`Confirmed work item key "${item.key}" is not in selected queue`);
|
|
439
|
+
}
|
|
440
|
+
if (selected.has(item.key)) {
|
|
441
|
+
throw new Error(`Confirmed work item key "${item.key}" was selected twice`);
|
|
442
|
+
}
|
|
443
|
+
selected.add(item.key);
|
|
444
|
+
normalized.push(queued);
|
|
445
|
+
}
|
|
446
|
+
return normalized;
|
|
447
|
+
}
|
|
448
|
+
function readSelectionGroups(result) {
|
|
449
|
+
const groups = readObjectProperty(result, "selection_groups");
|
|
450
|
+
if (!Array.isArray(groups)) {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
return groups
|
|
454
|
+
.map((group) => {
|
|
455
|
+
if (!isRecord(group) || typeof group.title !== "string") {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
const keys = Array.isArray(group.work_item_keys)
|
|
459
|
+
? group.work_item_keys.filter((key) => typeof key === "string")
|
|
460
|
+
: [];
|
|
461
|
+
if (keys.length === 0) {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
const normalized = {
|
|
465
|
+
title: group.title,
|
|
466
|
+
work_item_keys: keys
|
|
467
|
+
};
|
|
468
|
+
if (typeof group.kind === "string") {
|
|
469
|
+
normalized.kind = group.kind;
|
|
470
|
+
}
|
|
471
|
+
return normalized;
|
|
472
|
+
})
|
|
473
|
+
.filter((group) => Boolean(group));
|
|
474
|
+
}
|
|
475
|
+
function isReservedTarget(target) {
|
|
476
|
+
return (target === "stop_run" ||
|
|
477
|
+
target === "stop_iteration" ||
|
|
478
|
+
target === "next_iteration");
|
|
479
|
+
}
|
|
151
480
|
function readObjectProperty(value, key) {
|
|
152
481
|
if (value &&
|
|
153
482
|
typeof value === "object" &&
|
|
@@ -156,3 +485,6 @@ function readObjectProperty(value, key) {
|
|
|
156
485
|
}
|
|
157
486
|
return undefined;
|
|
158
487
|
}
|
|
488
|
+
function isRecord(value) {
|
|
489
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
490
|
+
}
|