@nyxa/nyx-agent 0.3.2 → 0.3.4

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.
@@ -6,6 +6,8 @@ import pc from "picocolors";
6
6
  import { ensureDir, pathExists, readText, writeText } from "../runtime/files.js";
7
7
  import { getNyxDir, relativeToProject } from "../runtime/paths.js";
8
8
  const DEFAULT_OPENAI_MODEL = "gpt-5.5";
9
+ const GITIGNORE_MARKER = "# NyxAgent runtime artifacts";
10
+ const GITIGNORE_ENTRIES = [".nyxagent/runs/", ".nyxagent/state.json"];
9
11
  export async function initCommand(options, projectRoot = process.cwd()) {
10
12
  const root = path.resolve(projectRoot);
11
13
  const nyxDir = getNyxDir(root);
@@ -21,6 +23,7 @@ export async function initCommand(options, projectRoot = process.cwd()) {
21
23
  await ensureDir(nyxDir);
22
24
  const templatesDir = getTemplatesDir();
23
25
  await copyTemplateTree(templatesDir, nyxDir, Boolean(options.missing));
26
+ await ensureGitignoreEntries(root);
24
27
  if (resolved) {
25
28
  await writeText(configPath, buildConfigToml(resolved));
26
29
  }
@@ -146,6 +149,45 @@ async function ensureWorkItemsDirectory(root, taskPath) {
146
149
  throw new Error(`Work item path is not a directory: ${taskPath}`);
147
150
  }
148
151
  }
152
+ async function ensureGitignoreEntries(root) {
153
+ const gitignorePath = path.join(root, ".gitignore");
154
+ const current = (await pathExists(gitignorePath))
155
+ ? await readText(gitignorePath)
156
+ : "";
157
+ const lines = current.split(/\r?\n/);
158
+ const existingEntries = new Set(lines.map((line) => line.trim()));
159
+ const missingEntries = GITIGNORE_ENTRIES.filter((entry) => !existingEntries.has(entry));
160
+ if (missingEntries.length === 0) {
161
+ return;
162
+ }
163
+ const markerIndex = lines.findIndex((line) => line.trim() === GITIGNORE_MARKER);
164
+ let updated;
165
+ if (markerIndex >= 0) {
166
+ const nextLines = [...lines];
167
+ nextLines.splice(markerIndex + 1, 0, ...missingEntries);
168
+ updated = normalizeGitignoreContent(nextLines.join("\n"));
169
+ }
170
+ else {
171
+ const prefix = getGitignoreAppendPrefix(current);
172
+ updated = `${current}${prefix}${GITIGNORE_MARKER}\n${missingEntries.join("\n")}\n`;
173
+ }
174
+ await writeText(gitignorePath, updated);
175
+ }
176
+ function getGitignoreAppendPrefix(current) {
177
+ if (current.length === 0) {
178
+ return "";
179
+ }
180
+ if (current.endsWith("\n\n")) {
181
+ return "";
182
+ }
183
+ if (current.endsWith("\n")) {
184
+ return "\n";
185
+ }
186
+ return "\n\n";
187
+ }
188
+ function normalizeGitignoreContent(value) {
189
+ return `${value.replace(/\n*$/, "")}\n`;
190
+ }
149
191
  function buildConfigToml(options) {
150
192
  const harness = buildHarnessToml(options.harness);
151
193
  const workItems = buildWorkItemsToml(options);
@@ -51,6 +51,11 @@ async function buildRuntimeContract(input) {
51
51
  JSON.stringify(input.context.selected_work_item_queue ?? [], null, 2),
52
52
  "```",
53
53
  "",
54
+ "Work item annotations:",
55
+ "```json",
56
+ JSON.stringify(input.context.work_item_annotations ?? [], null, 2),
57
+ "```",
58
+ "",
54
59
  "Seen work item keys:",
55
60
  "```json",
56
61
  JSON.stringify(input.context.seen_work_item_keys ?? [], null, 2),
@@ -85,6 +85,7 @@ function buildContext(input) {
85
85
  available_work_items: input.state.available_work_items ?? [],
86
86
  recommended_work_item_queue: input.state.recommended_work_item_queue ?? [],
87
87
  selected_work_item_queue: input.state.selected_work_item_queue ?? [],
88
+ work_item_annotations: input.state.work_item_annotations ?? [],
88
89
  work_item: input.state.work_item ?? {},
89
90
  seen_work_item_keys: input.state.seen_work_item_keys ?? [],
90
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);
@@ -29,6 +30,7 @@ export async function runWorkflow(options) {
29
30
  available_work_items: [],
30
31
  recommended_work_item_queue: [],
31
32
  selected_work_item_queue: [],
33
+ work_item_annotations: [],
32
34
  skipped_work_item_keys: [],
33
35
  selection_groups: [],
34
36
  seen_work_item_keys: [],
@@ -66,6 +68,7 @@ export async function runWorkflow(options) {
66
68
  }
67
69
  runState.available_work_items = selection.availableWorkItems;
68
70
  runState.recommended_work_item_queue = selection.workItems;
71
+ runState.work_item_annotations = selection.workItemAnnotations;
69
72
  runState.selection_groups = selection.selectionGroups;
70
73
  await writeJson(path.join(runDir, "state.json"), runState);
71
74
  let confirmedWorkItems;
@@ -76,6 +79,7 @@ export async function runWorkflow(options) {
76
79
  availableWorkItems: selection.availableWorkItems,
77
80
  recommendedWorkItems: selection.workItems,
78
81
  workItems: selection.availableWorkItems,
82
+ workItemAnnotations: selection.workItemAnnotations,
79
83
  selectionGroups: selection.selectionGroups,
80
84
  maxIterations: config.workflow.max_iterations
81
85
  })
@@ -119,6 +123,7 @@ export async function runWorkflow(options) {
119
123
  available_work_items: selectedWorkItems,
120
124
  recommended_work_item_queue: selection.workItems,
121
125
  selected_work_item_queue: selectedWorkItems,
126
+ work_item_annotations: selection.workItemAnnotations,
122
127
  seen_work_item_keys: [...runState.seen_work_item_keys],
123
128
  completed_work_item_keys: [...ledger.completed_work_item_keys],
124
129
  last_completed_work_item: ledger.last_completed_work_item,
@@ -209,6 +214,7 @@ async function runSelectionPhase(input) {
209
214
  available_work_items: availableWorkItems,
210
215
  recommended_work_item_queue: [],
211
216
  selected_work_item_queue: [],
217
+ work_item_annotations: [],
212
218
  seen_work_item_keys: [...input.runState.seen_work_item_keys],
213
219
  completed_work_item_keys: [...input.ledger.completed_work_item_keys],
214
220
  last_completed_work_item: input.ledger.last_completed_work_item,
@@ -256,14 +262,20 @@ async function runSelectionPhase(input) {
256
262
  completedWorkItemKeys: selectionState.completed_work_item_keys
257
263
  });
258
264
  const selectionGroups = readSelectionGroups(phaseResult.result);
265
+ const workItemAnnotations = readWorkItemAnnotations({
266
+ result: phaseResult.result,
267
+ availableWorkItems
268
+ });
259
269
  selectionState.status = "completed";
260
270
  selectionState.recommended_work_item_queue = workItems;
261
271
  selectionState.selected_work_item_queue = workItems;
272
+ selectionState.work_item_annotations = workItemAnnotations;
262
273
  await writeJson(selectionStateFile, selectionState);
263
274
  await writeJson(path.join(input.runDir, "state.json"), input.runState);
264
275
  return {
265
276
  status: "selected",
266
277
  workItems,
278
+ workItemAnnotations,
267
279
  selectionGroups,
268
280
  availableWorkItems,
269
281
  nextPhaseId: nextTarget
@@ -349,6 +361,7 @@ function buildConfirmationView(input) {
349
361
  const workItems = [];
350
362
  const byKey = new Map(input.availableWorkItems.map((item) => [item.key, item]));
351
363
  const recommendedKeys = new Set(input.recommendedWorkItems.map((item) => item.key));
364
+ const annotationsByKey = buildWorkItemKindMap(input.workItemAnnotations ?? []);
352
365
  const displayed = new Set();
353
366
  for (const group of input.selectionGroups) {
354
367
  const groupItems = group.work_item_keys
@@ -364,7 +377,7 @@ function buildConfirmationView(input) {
364
377
  }
365
378
  choices.push(new Separator(`-- ${group.title} --`));
366
379
  for (const item of groupItems) {
367
- choices.push(buildConfirmationChoice(item, recommendedKeys));
380
+ choices.push(buildConfirmationChoice({ item, recommendedKeys, annotationsByKey }));
368
381
  workItems.push(item);
369
382
  displayed.add(item.key);
370
383
  }
@@ -374,17 +387,20 @@ function buildConfirmationView(input) {
374
387
  choices.push(new Separator("-- Ungrouped --"));
375
388
  }
376
389
  for (const item of ungroupedItems) {
377
- choices.push(buildConfirmationChoice(item, recommendedKeys));
390
+ choices.push(buildConfirmationChoice({ item, recommendedKeys, annotationsByKey }));
378
391
  workItems.push(item);
379
392
  }
380
393
  return { choices, workItems };
381
394
  }
382
- function buildConfirmationChoice(item, recommendedKeys) {
395
+ function buildConfirmationChoice(input) {
383
396
  return {
384
- value: item.key,
385
- name: `${item.title} (${item.key})`,
386
- description: item.excerpt,
387
- checked: recommendedKeys.has(item.key)
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)
388
404
  };
389
405
  }
390
406
  function resolveNextTarget(phase, outcome) {
@@ -459,6 +475,12 @@ function normalizeConfirmedWorkItems(input) {
459
475
  }
460
476
  return normalized;
461
477
  }
478
+ function readWorkItemAnnotations(input) {
479
+ return normalizeWorkItemAnnotations({
480
+ annotations: readObjectProperty(input.result, "work_item_annotations"),
481
+ availableWorkItems: input.availableWorkItems
482
+ });
483
+ }
462
484
  function readSelectionGroups(result) {
463
485
  const groups = readObjectProperty(result, "selection_groups");
464
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
+ }
@@ -192,6 +192,10 @@ max_visits_per_iteration = 1
192
192
  - Before executing, NyxAgent shows the full `available_work_items` inventory in
193
193
  an interactive checkbox prompt. The recommended queue is pre-checked, and the
194
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.
195
199
  - Local markdown work items do not have a required frontmatter schema. The
196
200
  provider infers titles from the first heading or filename and includes a
197
201
  bounded content excerpt.
@@ -303,6 +307,7 @@ The runtime contract includes:
303
307
  - `available_work_items`
304
308
  - `recommended_work_item_queue`
305
309
  - `selected_work_item_queue`
310
+ - `work_item_annotations`
306
311
  - `seen_work_item_keys`
307
312
  - `completed_work_item_keys`
308
313
  - `last_completed_work_item`
@@ -319,6 +324,7 @@ Prompts may use simple interpolation:
319
324
  {{state_file}}
320
325
  {{recommended_work_item_queue}}
321
326
  {{selected_work_item_queue}}
327
+ {{work_item_annotations}}
322
328
  {{work_item.key}}
323
329
  {{work_item.title}}
324
330
  {{model.name}}
@@ -387,6 +393,7 @@ Each run creates a timestamped directory:
387
393
  - available work items seen by the initial selection phase
388
394
  - recommended work item queue returned by the selection agent
389
395
  - selected work item queue confirmed for the run
396
+ - work item annotations returned by the selection agent
390
397
  - skipped recommended work item keys
391
398
  - selection groups returned for user review
392
399
  - seen work item keys
@@ -456,6 +463,8 @@ Selection:
456
463
  `completed_work_item_keys`
457
464
  - optionally include `selection_groups` to present related work by the most
458
465
  useful grouping the agent can infer
466
+ - optionally include `work_item_annotations` with display kinds `task`, `plan`,
467
+ `prd`, or `work`
459
468
  - return `selected` or `no_work`
460
469
 
461
470
  Execution:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyxa/nyx-agent",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "A lightweight phase orchestrator for repeatedly launching coding agents with fresh context.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -12,6 +12,11 @@ declared relationships when available, but do not assume every project uses
12
12
  plan/PRD subdirectories. Groups may include any key from `available_work_items`,
13
13
  including items that are not part of your recommended `work_items` queue.
14
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.
19
+
15
20
  Prefer concrete tasks over their parent plan when both are present and the task
16
21
  is ready to execute. Choose the plan itself only when it is the best next work.
17
22
  If no candidate is exploitable, return `no_work`.
@@ -31,7 +36,8 @@ The selected work item identities must match the inventory entries:
31
36
  Return one of these outcomes:
32
37
 
33
38
  - `selected`: include recommended ordered `work_items`; optionally include
34
- `selection_groups` with `title`, optional `kind`, and `work_item_keys`
39
+ `selection_groups` with `title`, optional `kind`, and `work_item_keys`; and
40
+ optionally include `work_item_annotations` with `key` and `kind`
35
41
  - `no_work`: include a short `reason`
36
42
 
37
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
  }