@nyxa/nyx-agent 0.4.1 → 0.5.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.
Files changed (39) hide show
  1. package/README.md +52 -9
  2. package/dist/cli.js +11 -16
  3. package/dist/commands/init.js +87 -466
  4. package/dist/commands/run.js +16 -3
  5. package/dist/config/loadConfig.js +16 -3
  6. package/dist/config/schema.js +27 -146
  7. package/dist/runtime/gitLifecycle.js +19 -57
  8. package/dist/runtime/harness.js +26 -0
  9. package/dist/runtime/paths.js +0 -12
  10. package/dist/runtime/prompts.js +103 -0
  11. package/dist/runtime/runPhase.js +85 -254
  12. package/dist/runtime/runPipeline.js +395 -0
  13. package/dist/runtime/schemas.js +52 -0
  14. package/dist/runtime/scm.js +76 -0
  15. package/dist/runtime/validateResult.js +1 -3
  16. package/dist/runtime/workItems.js +42 -118
  17. package/package.json +2 -5
  18. package/dist/runtime/buildPrompt.js +0 -54
  19. package/dist/runtime/effectiveConfig.js +0 -14
  20. package/dist/runtime/renderTemplate.js +0 -28
  21. package/dist/runtime/runWorkflow.js +0 -680
  22. package/dist/runtime/validateWorkItem.js +0 -212
  23. package/dist/runtime/workItemAnnotations.js +0 -39
  24. package/docs/nyxagent-v0-spec.md +0 -742
  25. package/templates/default/prompts/closure.md +0 -30
  26. package/templates/default/prompts/execution.md +0 -11
  27. package/templates/default/prompts/finalize.md +0 -7
  28. package/templates/default/prompts/global-review.md +0 -24
  29. package/templates/default/prompts/global-revision.md +0 -9
  30. package/templates/default/prompts/pull-request.md +0 -23
  31. package/templates/default/prompts/repair-result.md +0 -29
  32. package/templates/default/prompts/review.md +0 -18
  33. package/templates/default/prompts/revision.md +0 -7
  34. package/templates/default/prompts/selection.md +0 -46
  35. package/templates/default/schemas/closure.schema.json +0 -35
  36. package/templates/default/schemas/global-review.schema.json +0 -60
  37. package/templates/default/schemas/pull-request.schema.json +0 -44
  38. package/templates/default/schemas/review.schema.json +0 -60
  39. 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
- }