@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.
- package/dist/commands/init.js +42 -0
- package/dist/runtime/buildPrompt.js +5 -0
- package/dist/runtime/runPhase.js +1 -0
- package/dist/runtime/runWorkflow.js +29 -7
- package/dist/runtime/workItemAnnotations.js +39 -0
- package/docs/nyxagent-v0-spec.md +9 -0
- package/package.json +1 -1
- package/templates/default/prompts/selection.md +7 -1
- package/templates/default/schemas/selection.schema.json +17 -0
package/dist/commands/init.js
CHANGED
|
@@ -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),
|
package/dist/runtime/runPhase.js
CHANGED
|
@@ -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(
|
|
395
|
+
function buildConfirmationChoice(input) {
|
|
383
396
|
return {
|
|
384
|
-
value: item.key,
|
|
385
|
-
name:
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
}
|
package/docs/nyxagent-v0-spec.md
CHANGED
|
@@ -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
|
@@ -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
|
}
|