@nyxa/nyx-agent 0.3.1 → 0.3.3

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.
@@ -41,11 +41,21 @@ async function buildRuntimeContract(input) {
41
41
  JSON.stringify(input.context.available_work_items ?? [], null, 2),
42
42
  "```",
43
43
  "",
44
+ "Recommended work item queue:",
45
+ "```json",
46
+ JSON.stringify(input.context.recommended_work_item_queue ?? [], null, 2),
47
+ "```",
48
+ "",
44
49
  "Selected work item queue:",
45
50
  "```json",
46
51
  JSON.stringify(input.context.selected_work_item_queue ?? [], null, 2),
47
52
  "```",
48
53
  "",
54
+ "Work item annotations:",
55
+ "```json",
56
+ JSON.stringify(input.context.work_item_annotations ?? [], null, 2),
57
+ "```",
58
+ "",
49
59
  "Seen work item keys:",
50
60
  "```json",
51
61
  JSON.stringify(input.context.seen_work_item_keys ?? [], null, 2),
@@ -83,7 +83,9 @@ function buildContext(input) {
83
83
  workflow: input.config.workflow,
84
84
  work_items: input.config.work_items ?? {},
85
85
  available_work_items: input.state.available_work_items ?? [],
86
+ recommended_work_item_queue: input.state.recommended_work_item_queue ?? [],
86
87
  selected_work_item_queue: input.state.selected_work_item_queue ?? [],
88
+ work_item_annotations: input.state.work_item_annotations ?? [],
87
89
  work_item: input.state.work_item ?? {},
88
90
  seen_work_item_keys: input.state.seen_work_item_keys ?? [],
89
91
  completed_work_item_keys: input.state.completed_work_item_keys ?? [],
@@ -9,6 +9,7 @@ import { getNyxDir, relativeToProject } from "./paths.js";
9
9
  import { runPhase } from "./runPhase.js";
10
10
  import { createRunId } from "./time.js";
11
11
  import { validateWorkItemQueue } from "./validateWorkItem.js";
12
+ import { buildWorkItemKindMap, formatWorkItemChoiceName, normalizeWorkItemAnnotations } from "./workItemAnnotations.js";
12
13
  import { filterAvailableWorkItems, listWorkItemCandidates } from "./workItems.js";
13
14
  export async function runWorkflow(options) {
14
15
  const projectRoot = path.resolve(options.projectRoot);
@@ -27,7 +28,9 @@ export async function runWorkflow(options) {
27
28
  current_iteration: 0,
28
29
  completed_iterations: 0,
29
30
  available_work_items: [],
31
+ recommended_work_item_queue: [],
30
32
  selected_work_item_queue: [],
33
+ work_item_annotations: [],
31
34
  skipped_work_item_keys: [],
32
35
  selection_groups: [],
33
36
  seen_work_item_keys: [],
@@ -64,14 +67,19 @@ export async function runWorkflow(options) {
64
67
  return;
65
68
  }
66
69
  runState.available_work_items = selection.availableWorkItems;
70
+ runState.recommended_work_item_queue = selection.workItems;
71
+ runState.work_item_annotations = selection.workItemAnnotations;
67
72
  runState.selection_groups = selection.selectionGroups;
68
73
  await writeJson(path.join(runDir, "state.json"), runState);
69
74
  let confirmedWorkItems;
70
75
  try {
71
76
  confirmedWorkItems = normalizeConfirmedWorkItems({
72
- queue: selection.workItems,
77
+ availableWorkItems: selection.availableWorkItems,
73
78
  confirmed: await (options.confirmWorkItems ?? confirmWorkItemsWithCheckbox)({
74
- workItems: selection.workItems,
79
+ availableWorkItems: selection.availableWorkItems,
80
+ recommendedWorkItems: selection.workItems,
81
+ workItems: selection.availableWorkItems,
82
+ workItemAnnotations: selection.workItemAnnotations,
75
83
  selectionGroups: selection.selectionGroups,
76
84
  maxIterations: config.workflow.max_iterations
77
85
  })
@@ -113,7 +121,9 @@ export async function runWorkflow(options) {
113
121
  status: "running",
114
122
  work_item: workItem,
115
123
  available_work_items: selectedWorkItems,
124
+ recommended_work_item_queue: selection.workItems,
116
125
  selected_work_item_queue: selectedWorkItems,
126
+ work_item_annotations: selection.workItemAnnotations,
117
127
  seen_work_item_keys: [...runState.seen_work_item_keys],
118
128
  completed_work_item_keys: [...ledger.completed_work_item_keys],
119
129
  last_completed_work_item: ledger.last_completed_work_item,
@@ -202,7 +212,9 @@ async function runSelectionPhase(input) {
202
212
  iteration: 0,
203
213
  status: "running",
204
214
  available_work_items: availableWorkItems,
215
+ recommended_work_item_queue: [],
205
216
  selected_work_item_queue: [],
217
+ work_item_annotations: [],
206
218
  seen_work_item_keys: [...input.runState.seen_work_item_keys],
207
219
  completed_work_item_keys: [...input.ledger.completed_work_item_keys],
208
220
  last_completed_work_item: input.ledger.last_completed_work_item,
@@ -250,13 +262,20 @@ async function runSelectionPhase(input) {
250
262
  completedWorkItemKeys: selectionState.completed_work_item_keys
251
263
  });
252
264
  const selectionGroups = readSelectionGroups(phaseResult.result);
265
+ const workItemAnnotations = readWorkItemAnnotations({
266
+ result: phaseResult.result,
267
+ availableWorkItems
268
+ });
253
269
  selectionState.status = "completed";
270
+ selectionState.recommended_work_item_queue = workItems;
254
271
  selectionState.selected_work_item_queue = workItems;
272
+ selectionState.work_item_annotations = workItemAnnotations;
255
273
  await writeJson(selectionStateFile, selectionState);
256
274
  await writeJson(path.join(input.runDir, "state.json"), input.runState);
257
275
  return {
258
276
  status: "selected",
259
277
  workItems,
278
+ workItemAnnotations,
260
279
  selectionGroups,
261
280
  availableWorkItems,
262
281
  nextPhaseId: nextTarget
@@ -319,24 +338,30 @@ async function runWorkflowPhase(input) {
319
338
  return phaseResult;
320
339
  }
321
340
  export async function confirmWorkItemsWithCheckbox(input) {
322
- if (input.workItems.length === 0) {
341
+ if (input.availableWorkItems.length === 0) {
323
342
  return [];
324
343
  }
325
344
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
326
345
  throw new Error("Work item selection confirmation requires an interactive TTY");
327
346
  }
347
+ const confirmationView = buildConfirmationView(input);
328
348
  const selectedKeys = await checkbox({
329
349
  message: `Confirm work items to run (max ${input.maxIterations})`,
330
- choices: buildConfirmationChoices(input),
350
+ choices: confirmationView.choices,
331
351
  required: false,
332
- pageSize: Math.min(Math.max(input.workItems.length + 2, 5), 20)
352
+ pageSize: Math.min(Math.max(input.availableWorkItems.length + 2, 5), 20),
353
+ validate: (selected) => selected.length <= input.maxIterations ||
354
+ `Select at most ${input.maxIterations} work items.`
333
355
  });
334
356
  const selected = new Set(selectedKeys);
335
- return input.workItems.filter((item) => selected.has(item.key));
357
+ return confirmationView.workItems.filter((item) => selected.has(item.key));
336
358
  }
337
- function buildConfirmationChoices(input) {
359
+ function buildConfirmationView(input) {
338
360
  const choices = [];
339
- const byKey = new Map(input.workItems.map((item) => [item.key, item]));
361
+ const workItems = [];
362
+ const byKey = new Map(input.availableWorkItems.map((item) => [item.key, item]));
363
+ const recommendedKeys = new Set(input.recommendedWorkItems.map((item) => item.key));
364
+ const annotationsByKey = buildWorkItemKindMap(input.workItemAnnotations ?? []);
340
365
  const displayed = new Set();
341
366
  for (const group of input.selectionGroups) {
342
367
  const groupItems = group.work_item_keys
@@ -352,25 +377,30 @@ function buildConfirmationChoices(input) {
352
377
  }
353
378
  choices.push(new Separator(`-- ${group.title} --`));
354
379
  for (const item of groupItems) {
355
- choices.push(buildConfirmationChoice(item));
380
+ choices.push(buildConfirmationChoice({ item, recommendedKeys, annotationsByKey }));
381
+ workItems.push(item);
356
382
  displayed.add(item.key);
357
383
  }
358
384
  }
359
- const ungroupedItems = input.workItems.filter((item) => !displayed.has(item.key));
385
+ const ungroupedItems = input.availableWorkItems.filter((item) => !displayed.has(item.key));
360
386
  if (input.selectionGroups.length > 0 && ungroupedItems.length > 0) {
361
387
  choices.push(new Separator("-- Ungrouped --"));
362
388
  }
363
389
  for (const item of ungroupedItems) {
364
- choices.push(buildConfirmationChoice(item));
390
+ choices.push(buildConfirmationChoice({ item, recommendedKeys, annotationsByKey }));
391
+ workItems.push(item);
365
392
  }
366
- return choices;
393
+ return { choices, workItems };
367
394
  }
368
- function buildConfirmationChoice(item) {
395
+ function buildConfirmationChoice(input) {
369
396
  return {
370
- value: item.key,
371
- name: `${item.title} (${item.key})`,
372
- description: item.excerpt,
373
- checked: true
397
+ value: input.item.key,
398
+ name: formatWorkItemChoiceName({
399
+ item: input.item,
400
+ annotationsByKey: input.annotationsByKey
401
+ }),
402
+ description: input.item.excerpt,
403
+ checked: input.recommendedKeys.has(input.item.key)
374
404
  };
375
405
  }
376
406
  function resolveNextTarget(phase, outcome) {
@@ -429,22 +459,28 @@ function readSelectedWorkItems(input) {
429
459
  return validation.workItems;
430
460
  }
431
461
  function normalizeConfirmedWorkItems(input) {
432
- const queuedByKey = new Map(input.queue.map((item) => [item.key, item]));
462
+ const availableByKey = new Map(input.availableWorkItems.map((item) => [item.key, item]));
433
463
  const selected = new Set();
434
464
  const normalized = [];
435
465
  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`);
466
+ const available = availableByKey.get(item.key);
467
+ if (!available) {
468
+ throw new Error(`Confirmed work item key "${item.key}" is not in available_work_items`);
439
469
  }
440
470
  if (selected.has(item.key)) {
441
471
  throw new Error(`Confirmed work item key "${item.key}" was selected twice`);
442
472
  }
443
473
  selected.add(item.key);
444
- normalized.push(queued);
474
+ normalized.push(available);
445
475
  }
446
476
  return normalized;
447
477
  }
478
+ function readWorkItemAnnotations(input) {
479
+ return normalizeWorkItemAnnotations({
480
+ annotations: readObjectProperty(input.result, "work_item_annotations"),
481
+ availableWorkItems: input.availableWorkItems
482
+ });
483
+ }
448
484
  function readSelectionGroups(result) {
449
485
  const groups = readObjectProperty(result, "selection_groups");
450
486
  if (!Array.isArray(groups)) {
@@ -0,0 +1,39 @@
1
+ export const workItemKinds = ["task", "plan", "prd", "work"];
2
+ export function normalizeWorkItemAnnotations(input) {
3
+ if (!Array.isArray(input.annotations)) {
4
+ return [];
5
+ }
6
+ const availableKeys = new Set(input.availableWorkItems.map((item) => item.key));
7
+ const seen = new Set();
8
+ const normalized = [];
9
+ for (const annotation of input.annotations) {
10
+ if (!isRecord(annotation) || typeof annotation.key !== "string") {
11
+ continue;
12
+ }
13
+ if (!availableKeys.has(annotation.key) || seen.has(annotation.key)) {
14
+ continue;
15
+ }
16
+ seen.add(annotation.key);
17
+ normalized.push({
18
+ key: annotation.key,
19
+ kind: normalizeWorkItemKind(annotation.kind)
20
+ });
21
+ }
22
+ return normalized;
23
+ }
24
+ export function buildWorkItemKindMap(annotations) {
25
+ return new Map(annotations.map((annotation) => [annotation.key, annotation.kind]));
26
+ }
27
+ export function formatWorkItemChoiceName(input) {
28
+ const kind = input.annotationsByKey.get(input.item.key) ?? "work";
29
+ return `[${kind}] ${input.item.title} (${input.item.key})`;
30
+ }
31
+ function normalizeWorkItemKind(value) {
32
+ return typeof value === "string" && isWorkItemKind(value) ? value : "work";
33
+ }
34
+ function isWorkItemKind(value) {
35
+ return workItemKinds.includes(value);
36
+ }
37
+ function isRecord(value) {
38
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
39
+ }
@@ -185,12 +185,17 @@ max_visits_per_iteration = 1
185
185
  - `work_items.excerpt_chars` defaults to `800` and bounds candidate excerpts.
186
186
  - At run start, the engine scans the configured provider, normalizes candidates
187
187
  into `available_work_items`, and sends that inventory to the selection phase.
188
- - The selection phase returns an ordered `work_items` queue. NyxAgent validates
189
- each selected identity against `[work_items]`, rejects duplicates, and
190
- requires every selected key to exist in `available_work_items`.
191
- - Before executing, NyxAgent shows the selected queue in an interactive
192
- checkbox prompt. The user can uncheck work items; only confirmed items are
193
- executed.
188
+ - The selection phase returns an ordered recommended `work_items` queue.
189
+ NyxAgent validates each recommended identity against `[work_items]`, rejects
190
+ duplicates, and requires every selected key to exist in
191
+ `available_work_items`.
192
+ - Before executing, NyxAgent shows the full `available_work_items` inventory in
193
+ an interactive checkbox prompt. The recommended queue is pre-checked, and the
194
+ user can add or remove available items. Only confirmed items are executed.
195
+ - The selection phase may return optional `work_item_annotations` for display.
196
+ Supported kinds are `task`, `plan`, `prd`, and `work`. Unknown or missing
197
+ kinds are shown as `work`; annotations never affect work item identity or
198
+ ledger completion.
194
199
  - Local markdown work items do not have a required frontmatter schema. The
195
200
  provider infers titles from the first heading or filename and includes a
196
201
  bounded content excerpt.
@@ -199,7 +204,8 @@ max_visits_per_iteration = 1
199
204
  configured repository.
200
205
  - NyxAgent does not decide whether an item is a plan, PRD, or task. The
201
206
  selection agent makes that semantic choice from the deterministic inventory
202
- and may include optional `selection_groups` for user review.
207
+ and may include optional `selection_groups` for user review. Groups may cover
208
+ the full available inventory, not only the recommended queue.
203
209
 
204
210
  ## Workflow Model
205
211
 
@@ -222,7 +228,7 @@ only knows phases, outcomes, transitions, and visit limits.
222
228
  The default template expresses the standard run:
223
229
 
224
230
  ```text
225
- selection -> user confirms queue
231
+ selection -> user confirms inventory with recommended queue pre-checked
226
232
  for each confirmed work item:
227
233
  execution -> review
228
234
  review.approved -> closure -> next_iteration
@@ -299,7 +305,9 @@ The runtime contract includes:
299
305
  - work item context, when selected
300
306
  - work item config from `[work_items]`
301
307
  - `available_work_items`
308
+ - `recommended_work_item_queue`
302
309
  - `selected_work_item_queue`
310
+ - `work_item_annotations`
303
311
  - `seen_work_item_keys`
304
312
  - `completed_work_item_keys`
305
313
  - `last_completed_work_item`
@@ -314,7 +322,9 @@ Prompts may use simple interpolation:
314
322
  {{iteration_dir}}
315
323
  {{phase_dir}}
316
324
  {{state_file}}
325
+ {{recommended_work_item_queue}}
317
326
  {{selected_work_item_queue}}
327
+ {{work_item_annotations}}
318
328
  {{work_item.key}}
319
329
  {{work_item.title}}
320
330
  {{model.name}}
@@ -381,8 +391,10 @@ Each run creates a timestamped directory:
381
391
  - current iteration
382
392
  - completed iterations
383
393
  - available work items seen by the initial selection phase
394
+ - recommended work item queue returned by the selection agent
384
395
  - selected work item queue confirmed for the run
385
- - skipped selected work item keys
396
+ - work item annotations returned by the selection agent
397
+ - skipped recommended work item keys
386
398
  - selection groups returned for user review
387
399
  - seen work item keys
388
400
  - completed work item keys
@@ -442,14 +454,17 @@ Default prompts should be concise but operational.
442
454
 
443
455
  Selection:
444
456
 
445
- - choose an ordered queue from `available_work_items`
457
+ - recommend an ordered queue from `available_work_items`
446
458
  - treat candidates agnostically: they may be plans, PRDs, or tasks
447
459
  - prefer the most actionable items based on candidate titles and excerpts
448
460
  - if a plan references concrete candidate tasks, choose the concrete task when
449
461
  that is the best next work
450
462
  - avoid keys already present in `seen_work_item_keys` or
451
463
  `completed_work_item_keys`
452
- - optionally include `selection_groups` to present related work by plan or PRD
464
+ - optionally include `selection_groups` to present related work by the most
465
+ useful grouping the agent can infer
466
+ - optionally include `work_item_annotations` with display kinds `task`, `plan`,
467
+ `prd`, or `work`
453
468
  - return `selected` or `no_work`
454
469
 
455
470
  Execution:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyxa/nyx-agent",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "A lightweight phase orchestrator for repeatedly launching coding agents with fresh context.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,13 +1,21 @@
1
- Select the ordered work item queue for this run.
1
+ Recommend the ordered work item queue for this run.
2
2
 
3
3
  Use `available_work_items` from the runtime contract as the complete inventory
4
- for selection. Some candidates may be plans, PRDs, or concrete tasks. Build the
5
- most actionable queue for this run, up to `workflow.max_iterations` items.
6
-
7
- If candidates appear to belong to the same plan or PRD, keep related concrete
8
- tasks together and use `selection_groups` to describe the grouping. The grouping
9
- is only for user review; every executable item must still be included in
10
- `work_items`.
4
+ for selection. Some candidates may be plans, PRDs, or concrete tasks.
5
+ Recommend the most actionable queue for this run, up to
6
+ `workflow.max_iterations` items. NyxAgent will show the complete inventory to
7
+ the user afterward with your recommendation pre-selected.
8
+
9
+ If candidates appear to belong together, use `selection_groups` to describe the
10
+ most useful review grouping. Infer grouping from titles, paths, excerpts, and
11
+ declared relationships when available, but do not assume every project uses
12
+ plan/PRD subdirectories. Groups may include any key from `available_work_items`,
13
+ including items that are not part of your recommended `work_items` queue.
14
+
15
+ When useful, include `work_item_annotations` to label available items for user
16
+ review. Each annotation should include `key` and `kind`. Use `kind` values:
17
+ `task`, `plan`, `prd`, or `work`. Use `work` when you are not sure. You may
18
+ annotate items that are not in the recommended `work_items` queue.
11
19
 
12
20
  Prefer concrete tasks over their parent plan when both are present and the task
13
21
  is ready to execute. Choose the plan itself only when it is the best next work.
@@ -27,8 +35,9 @@ The selected work item identities must match the inventory entries:
27
35
 
28
36
  Return one of these outcomes:
29
37
 
30
- - `selected`: include ordered `work_items`; optionally include
31
- `selection_groups` with `title`, optional `kind`, and `work_item_keys`
38
+ - `selected`: include recommended ordered `work_items`; optionally include
39
+ `selection_groups` with `title`, optional `kind`, and `work_item_keys`; and
40
+ optionally include `work_item_annotations` with `key` and `kind`
32
41
  - `no_work`: include a short `reason`
33
42
 
34
43
  For compatibility, `work_item` is still accepted for a single selected item, but
@@ -77,6 +77,23 @@
77
77
  "additionalProperties": true
78
78
  }
79
79
  },
80
+ "work_item_annotations": {
81
+ "type": "array",
82
+ "items": {
83
+ "type": "object",
84
+ "required": ["key"],
85
+ "properties": {
86
+ "key": {
87
+ "type": "string",
88
+ "minLength": 1
89
+ },
90
+ "kind": {
91
+ "type": "string"
92
+ }
93
+ },
94
+ "additionalProperties": true
95
+ }
96
+ },
80
97
  "reason": {
81
98
  "type": "string"
82
99
  }