@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.
@@ -0,0 +1,212 @@
1
+ import path from "node:path";
2
+ export function validateWorkItemIdentity(input) {
3
+ const workItems = input.config.work_items;
4
+ if (!workItems) {
5
+ return {
6
+ ok: false,
7
+ error: "Selected work item requires configured [work_items]"
8
+ };
9
+ }
10
+ if (!isRecord(input.workItem)) {
11
+ return { ok: false, error: "Selected work item must be an object" };
12
+ }
13
+ const source = input.workItem.source;
14
+ if (!isRecord(source)) {
15
+ return { ok: false, error: "Selected work item source must be an object" };
16
+ }
17
+ const key = input.workItem.key;
18
+ const sourceType = source.type;
19
+ const locator = source.locator;
20
+ if (typeof key !== "string" || key.length === 0) {
21
+ return { ok: false, error: "Selected work item key must be a non-empty string" };
22
+ }
23
+ if (input.seenWorkItemKeys?.includes(key) && key !== input.allowKnownKey) {
24
+ return {
25
+ ok: false,
26
+ error: `Selected work item key "${key}" was already seen in this run`
27
+ };
28
+ }
29
+ if (input.completedWorkItemKeys?.includes(key) &&
30
+ key !== input.allowKnownKey) {
31
+ return {
32
+ ok: false,
33
+ error: `Selected work item key "${key}" is already completed`
34
+ };
35
+ }
36
+ if (sourceType !== workItems.source) {
37
+ return {
38
+ ok: false,
39
+ error: `Selected work item source.type must be "${workItems.source}"`
40
+ };
41
+ }
42
+ if (typeof locator !== "string" || locator.length === 0) {
43
+ return {
44
+ ok: false,
45
+ error: "Selected work item source.locator must be a non-empty string"
46
+ };
47
+ }
48
+ if (key !== `${workItems.source}:${locator}`) {
49
+ return {
50
+ ok: false,
51
+ error: `Selected work item key must be "${workItems.source}:${locator}"`
52
+ };
53
+ }
54
+ if (workItems.source === "local") {
55
+ const localValidation = validateLocalLocator(locator, workItems.path);
56
+ if (!localValidation.ok) {
57
+ return localValidation;
58
+ }
59
+ }
60
+ else {
61
+ const githubValidation = validateGitHubLocator(locator, workItems.repository);
62
+ if (!githubValidation.ok) {
63
+ return githubValidation;
64
+ }
65
+ }
66
+ if (input.availableWorkItems) {
67
+ return validateCandidateMembership({
68
+ key,
69
+ sourceType,
70
+ locator,
71
+ availableWorkItems: input.availableWorkItems
72
+ });
73
+ }
74
+ return { ok: true };
75
+ }
76
+ export function validateWorkItemQueue(input) {
77
+ if (!Array.isArray(input.workItems)) {
78
+ return {
79
+ ok: false,
80
+ error: "Selected work_items must be an array"
81
+ };
82
+ }
83
+ if (input.workItems.length === 0) {
84
+ return {
85
+ ok: false,
86
+ error: "Selected work_items must contain at least one item"
87
+ };
88
+ }
89
+ const seen = new Set();
90
+ const normalized = [];
91
+ for (const [index, workItem] of input.workItems.entries()) {
92
+ const validation = validateWorkItemIdentity({
93
+ config: input.config,
94
+ workItem,
95
+ availableWorkItems: input.availableWorkItems,
96
+ seenWorkItemKeys: input.seenWorkItemKeys,
97
+ completedWorkItemKeys: input.completedWorkItemKeys
98
+ });
99
+ if (!validation.ok) {
100
+ return {
101
+ ok: false,
102
+ error: `Selected work_items[${index}] ${validation.error}`
103
+ };
104
+ }
105
+ const key = readObjectProperty(workItem, "key");
106
+ if (typeof key !== "string") {
107
+ return {
108
+ ok: false,
109
+ error: `Selected work_items[${index}] key must be a string`
110
+ };
111
+ }
112
+ if (seen.has(key)) {
113
+ return {
114
+ ok: false,
115
+ error: `Selected work_items contains duplicate key "${key}"`
116
+ };
117
+ }
118
+ seen.add(key);
119
+ const candidate = input.availableWorkItems?.find((item) => item.key === key);
120
+ normalized.push(candidate ?? workItem);
121
+ }
122
+ return { ok: true, workItems: normalized };
123
+ }
124
+ function validateCandidateMembership(input) {
125
+ const candidate = input.availableWorkItems.find((item) => item.key === input.key);
126
+ if (!candidate) {
127
+ return {
128
+ ok: false,
129
+ error: `Selected work item key "${input.key}" is not in available_work_items`
130
+ };
131
+ }
132
+ if (candidate.source.type !== input.sourceType ||
133
+ candidate.source.locator !== input.locator) {
134
+ return {
135
+ ok: false,
136
+ error: `Selected work item source does not match available_work_items entry "${input.key}"`
137
+ };
138
+ }
139
+ return { ok: true };
140
+ }
141
+ function validateLocalLocator(locator, configuredPath) {
142
+ const normalizedLocator = normalizeRelativePath(locator);
143
+ const normalizedPath = configuredPath
144
+ ? normalizeRelativePath(configuredPath)
145
+ : undefined;
146
+ if (!normalizedLocator || normalizedLocator !== locator) {
147
+ return {
148
+ ok: false,
149
+ error: "Local work item locator must be a canonical relative path"
150
+ };
151
+ }
152
+ if (!normalizedPath) {
153
+ return {
154
+ ok: false,
155
+ error: "Local work item validation requires [work_items].path"
156
+ };
157
+ }
158
+ if (normalizedLocator !== normalizedPath &&
159
+ !normalizedLocator.startsWith(`${normalizedPath}/`)) {
160
+ return {
161
+ ok: false,
162
+ error: `Local work item locator must stay under "${normalizedPath}"`
163
+ };
164
+ }
165
+ return { ok: true };
166
+ }
167
+ function validateGitHubLocator(locator, repository) {
168
+ if (!repository) {
169
+ return {
170
+ ok: false,
171
+ error: "GitHub work item validation requires [work_items].repository"
172
+ };
173
+ }
174
+ const match = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#([1-9][0-9]*)$/.exec(locator);
175
+ if (!match) {
176
+ return {
177
+ ok: false,
178
+ error: 'GitHub work item locator must use "owner/repo#number"'
179
+ };
180
+ }
181
+ if (match[1] !== repository) {
182
+ return {
183
+ ok: false,
184
+ error: `GitHub work item locator must use repository "${repository}"`
185
+ };
186
+ }
187
+ return { ok: true };
188
+ }
189
+ function normalizeRelativePath(value) {
190
+ if (value.length === 0 ||
191
+ value.includes("\\") ||
192
+ path.posix.isAbsolute(value) ||
193
+ /^[A-Za-z]:[\\/]/.test(value)) {
194
+ return undefined;
195
+ }
196
+ const normalized = path.posix.normalize(value);
197
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
198
+ return undefined;
199
+ }
200
+ return normalized;
201
+ }
202
+ function isRecord(value) {
203
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
204
+ }
205
+ function readObjectProperty(value, key) {
206
+ if (value &&
207
+ typeof value === "object" &&
208
+ Object.prototype.hasOwnProperty.call(value, key)) {
209
+ return value[key];
210
+ }
211
+ return undefined;
212
+ }
@@ -0,0 +1,212 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execa } from "execa";
4
+ export async function listWorkItemCandidates(input) {
5
+ const workItems = input.config.work_items;
6
+ if (!workItems) {
7
+ return [];
8
+ }
9
+ if (workItems.source === "local") {
10
+ if (!workItems.path) {
11
+ throw new Error('Local work items require [work_items].path');
12
+ }
13
+ return listLocalWorkItemCandidates({
14
+ projectRoot: input.projectRoot,
15
+ workItemsPath: workItems.path,
16
+ maxCandidates: workItems.max_candidates,
17
+ excerptChars: workItems.excerpt_chars
18
+ });
19
+ }
20
+ if (!workItems.repository) {
21
+ throw new Error('GitHub work items require [work_items].repository');
22
+ }
23
+ return listGitHubWorkItemCandidates({
24
+ repository: workItems.repository,
25
+ maxCandidates: workItems.max_candidates,
26
+ excerptChars: workItems.excerpt_chars
27
+ });
28
+ }
29
+ export function filterAvailableWorkItems(input) {
30
+ const seen = new Set(input.seenWorkItemKeys);
31
+ const completed = new Set(input.completedWorkItemKeys);
32
+ return input.candidates.filter((candidate) => !seen.has(candidate.key) && !completed.has(candidate.key));
33
+ }
34
+ export async function listLocalWorkItemCandidates(input) {
35
+ const normalizedPath = normalizeProjectRelativePath(input.workItemsPath);
36
+ if (!normalizedPath) {
37
+ throw new Error("[work_items].path must be a canonical relative path");
38
+ }
39
+ const root = path.resolve(input.projectRoot);
40
+ const workItemsDir = path.resolve(root, normalizedPath);
41
+ const relative = path.relative(root, workItemsDir);
42
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
43
+ throw new Error("[work_items].path must stay inside the project root");
44
+ }
45
+ const dirStat = await stat(workItemsDir).catch((error) => {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ throw new Error(`Local work item path does not exist: ${normalizedPath} (${message})`);
48
+ });
49
+ if (!dirStat.isDirectory()) {
50
+ throw new Error(`Local work item path is not a directory: ${normalizedPath}`);
51
+ }
52
+ const files = (await collectMarkdownFiles(workItemsDir))
53
+ .map((filePath) => toPosixPath(path.relative(root, filePath)))
54
+ .sort((left, right) => left.localeCompare(right));
55
+ const candidates = [];
56
+ for (const locator of files.slice(0, input.maxCandidates)) {
57
+ const absolutePath = path.join(root, locator);
58
+ const content = await readFile(absolutePath, "utf8");
59
+ candidates.push({
60
+ key: `local:${locator}`,
61
+ title: inferMarkdownTitle(content, locator),
62
+ source: {
63
+ type: "local",
64
+ locator
65
+ },
66
+ excerpt: buildExcerpt(content, input.excerptChars)
67
+ });
68
+ }
69
+ return candidates;
70
+ }
71
+ export async function listGitHubWorkItemCandidates(input) {
72
+ const result = await execa("gh", [
73
+ "issue",
74
+ "list",
75
+ "--repo",
76
+ input.repository,
77
+ "--state",
78
+ "open",
79
+ "--limit",
80
+ String(input.maxCandidates),
81
+ "--json",
82
+ "number,title,url,labels,updatedAt,body"
83
+ ], { reject: false });
84
+ if (result.exitCode !== 0) {
85
+ const detail = (result.stderr || result.stdout || "unknown error").trim();
86
+ throw new Error(`Failed to list GitHub work items with gh: ${detail}`);
87
+ }
88
+ let issues;
89
+ try {
90
+ issues = JSON.parse(result.stdout);
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ throw new Error(`Failed to parse gh issue list JSON: ${message}`);
95
+ }
96
+ if (!Array.isArray(issues)) {
97
+ throw new Error("gh issue list returned JSON that is not an array");
98
+ }
99
+ return issues.slice(0, input.maxCandidates).map((issue) => normalizeGitHubIssue({
100
+ issue,
101
+ repository: input.repository,
102
+ excerptChars: input.excerptChars
103
+ }));
104
+ }
105
+ function normalizeGitHubIssue(input) {
106
+ if (!isRecord(input.issue)) {
107
+ throw new Error("gh issue list returned a non-object issue");
108
+ }
109
+ const number = input.issue.number;
110
+ const title = input.issue.title;
111
+ if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) {
112
+ throw new Error("GitHub issue is missing a positive integer number");
113
+ }
114
+ if (typeof title !== "string" || title.length === 0) {
115
+ throw new Error(`GitHub issue #${number} is missing a title`);
116
+ }
117
+ const locator = `${input.repository}#${number}`;
118
+ const candidate = {
119
+ key: `github:${locator}`,
120
+ title,
121
+ source: {
122
+ type: "github",
123
+ locator
124
+ },
125
+ labels: normalizeLabels(input.issue.labels)
126
+ };
127
+ if (typeof input.issue.url === "string" && input.issue.url.length > 0) {
128
+ candidate.url = input.issue.url;
129
+ }
130
+ if (typeof input.issue.updatedAt === "string" &&
131
+ input.issue.updatedAt.length > 0) {
132
+ candidate.updated_at = input.issue.updatedAt;
133
+ }
134
+ if (typeof input.issue.body === "string") {
135
+ candidate.excerpt = buildExcerpt(input.issue.body, input.excerptChars);
136
+ }
137
+ return candidate;
138
+ }
139
+ async function collectMarkdownFiles(dir) {
140
+ const entries = await readdir(dir, { withFileTypes: true });
141
+ const files = [];
142
+ for (const entry of entries) {
143
+ const filePath = path.join(dir, entry.name);
144
+ if (entry.isDirectory()) {
145
+ files.push(...(await collectMarkdownFiles(filePath)));
146
+ continue;
147
+ }
148
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
149
+ files.push(filePath);
150
+ }
151
+ }
152
+ return files;
153
+ }
154
+ function inferMarkdownTitle(content, locator) {
155
+ const heading = /^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/m.exec(content);
156
+ if (heading?.[1]) {
157
+ return heading[1].trim();
158
+ }
159
+ return path.posix.basename(locator, ".md");
160
+ }
161
+ function buildExcerpt(content, maxChars) {
162
+ if (maxChars <= 0) {
163
+ return undefined;
164
+ }
165
+ const compact = content.replace(/\s+/g, " ").trim();
166
+ if (!compact) {
167
+ return undefined;
168
+ }
169
+ if (compact.length <= maxChars) {
170
+ return compact;
171
+ }
172
+ if (maxChars <= 3) {
173
+ return compact.slice(0, maxChars);
174
+ }
175
+ return `${compact.slice(0, maxChars - 3).trimEnd()}...`;
176
+ }
177
+ function normalizeLabels(labels) {
178
+ if (!Array.isArray(labels)) {
179
+ return undefined;
180
+ }
181
+ const normalized = labels
182
+ .map((label) => {
183
+ if (typeof label === "string") {
184
+ return label;
185
+ }
186
+ if (isRecord(label) && typeof label.name === "string") {
187
+ return label.name;
188
+ }
189
+ return undefined;
190
+ })
191
+ .filter((label) => Boolean(label));
192
+ return normalized.length > 0 ? normalized : undefined;
193
+ }
194
+ function normalizeProjectRelativePath(value) {
195
+ if (value.length === 0 ||
196
+ value.includes("\\") ||
197
+ path.posix.isAbsolute(value) ||
198
+ /^[A-Za-z]:[\\/]/.test(value)) {
199
+ return undefined;
200
+ }
201
+ const normalized = path.posix.normalize(value);
202
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
203
+ return undefined;
204
+ }
205
+ return normalized;
206
+ }
207
+ function toPosixPath(value) {
208
+ return value.split(path.sep).join(path.posix.sep);
209
+ }
210
+ function isRecord(value) {
211
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
212
+ }
@@ -16,7 +16,8 @@ configuration.
16
16
  ## Goals
17
17
 
18
18
  - Install a `.nyxagent/` folder into a project with sensible templates.
19
- - Run a configurable phase workflow for up to `max_iterations` work items.
19
+ - Select an ordered work-item queue once, then run a configurable phase workflow
20
+ for up to `max_iterations` confirmed work items.
20
21
  - Launch a fresh harness process for each phase.
21
22
  - Keep workflow structure generic through phase transitions and outcomes.
22
23
  - Keep prompts focused on agent behavior, not runtime mechanics.
@@ -26,7 +27,7 @@ configuration.
26
27
 
27
28
  ## Non-Goals For v0
28
29
 
29
- - No built-in GitHub, Jira, Linear, or local task tracker adapter.
30
+ - No native classification of work items as plans, PRDs, or tasks.
30
31
  - No native Git commit or issue close logic in the engine.
31
32
  - No resume command.
32
33
  - No fully general DAG/workflow engine.
@@ -62,6 +63,7 @@ If `.nyxagent/` already exists:
62
63
  ```text
63
64
  .nyxagent/
64
65
  config.toml
66
+ state.json
65
67
  prompts/
66
68
  selection.md
67
69
  execution.md
@@ -113,8 +115,10 @@ max_attempts = 1
113
115
  prompt = "prompts/repair-result.md"
114
116
 
115
117
  [work_items]
116
- source = "local-markdown"
118
+ source = "local"
117
119
  path = "issues"
120
+ max_candidates = 50
121
+ excerpt_chars = 800
118
122
 
119
123
  [[phases]]
120
124
  id = "selection"
@@ -168,15 +172,34 @@ max_visits_per_iteration = 1
168
172
 
169
173
  ### Config Semantics
170
174
 
171
- - `workflow.max_iterations` is the maximum number of distinct work items in a
172
- run.
175
+ - `workflow.max_iterations` is the maximum number of distinct confirmed work
176
+ items in a run.
173
177
  - `phases[*].max_visits_per_iteration` prevents infinite loops inside one work
174
178
  item.
175
179
  - `model.reasoning_level` is a harness-neutral string.
176
180
  - Harness args are declarative and may interpolate config/runtime variables.
177
181
  - Per-phase `model` and `harness` blocks override global values.
178
- - `work_items` is informative in v0. It is injected into runtime context and
179
- used by prompts, not scanned by the engine.
182
+ - `work_items` supports only `local` and `github` in v0.
183
+ - `work_items.max_candidates` defaults to `50` and caps the inventory sent to
184
+ the initial selection prompt.
185
+ - `work_items.excerpt_chars` defaults to `800` and bounds candidate excerpts.
186
+ - At run start, the engine scans the configured provider, normalizes candidates
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.
194
+ - Local markdown work items do not have a required frontmatter schema. The
195
+ provider infers titles from the first heading or filename and includes a
196
+ bounded content excerpt.
197
+ - Local keys use `local:<relative-path-under-work-items-path>`.
198
+ - GitHub keys use `github:<owner>/<repo>#<issue-number>` and must match the
199
+ configured repository.
200
+ - NyxAgent does not decide whether an item is a plan, PRD, or task. The
201
+ selection agent makes that semantic choice from the deterministic inventory
202
+ and may include optional `selection_groups` for user review.
180
203
 
181
204
  ## Workflow Model
182
205
 
@@ -196,10 +219,12 @@ Reserved next targets:
196
219
  The engine does not know about development, review, approval, or closure. It
197
220
  only knows phases, outcomes, transitions, and visit limits.
198
221
 
199
- The default template expresses the standard loop:
222
+ The default template expresses the standard run:
200
223
 
201
224
  ```text
202
- selection -> execution -> review
225
+ selection -> user confirms queue
226
+ for each confirmed work item:
227
+ execution -> review
203
228
  review.approved -> closure -> next_iteration
204
229
  review.changes_requested -> execution
205
230
  selection.no_work -> stop_run
@@ -273,6 +298,11 @@ The runtime contract includes:
273
298
  - required structured output contract
274
299
  - work item context, when selected
275
300
  - work item config from `[work_items]`
301
+ - `available_work_items`
302
+ - `selected_work_item_queue`
303
+ - `seen_work_item_keys`
304
+ - `completed_work_item_keys`
305
+ - `last_completed_work_item`
276
306
 
277
307
  The user prompt remains focused on domain behavior.
278
308
 
@@ -284,6 +314,7 @@ Prompts may use simple interpolation:
284
314
  {{iteration_dir}}
285
315
  {{phase_dir}}
286
316
  {{state_file}}
317
+ {{selected_work_item_queue}}
287
318
  {{work_item.key}}
288
319
  {{work_item.title}}
289
320
  {{model.name}}
@@ -300,17 +331,20 @@ Each run creates a timestamped directory:
300
331
  .nyxagent/runs/2026-05-23T12-30-00/
301
332
  run.json
302
333
  state.json
334
+ selection/
335
+ state.json
336
+ phases/
337
+ selection/
338
+ attempt-001/
339
+ prompt.md
340
+ stdout.log
341
+ stderr.log
342
+ meta.json
343
+ result.json
303
344
  iterations/
304
345
  001/
305
346
  state.json
306
347
  phases/
307
- selection/
308
- attempt-001/
309
- prompt.md
310
- stdout.log
311
- stderr.log
312
- meta.json
313
- result.json
314
348
  execution/
315
349
  attempt-001/
316
350
  prompt.md
@@ -346,12 +380,28 @@ Each run creates a timestamped directory:
346
380
  - run status
347
381
  - current iteration
348
382
  - completed iterations
383
+ - available work items seen by the initial selection phase
384
+ - selected work item queue confirmed for the run
385
+ - skipped selected work item keys
386
+ - selection groups returned for user review
349
387
  - seen work item keys
388
+ - completed work item keys
389
+ - last completed work item
390
+
391
+ `.nyxagent/state.json` records persistent work item completion state:
392
+
393
+ - completed work item keys
394
+ - last completed work item
395
+ - minimal completion history
350
396
 
351
397
  `iterations/NNN/state.json` records per-work-item state:
352
398
 
353
399
  - iteration number
354
400
  - work item
401
+ - selected work item queue for the run
402
+ - seen work item keys
403
+ - completed work item keys
404
+ - last completed work item
355
405
  - phase results
356
406
  - phase visit counts
357
407
  - current phase status
@@ -392,9 +442,14 @@ Default prompts should be concise but operational.
392
442
 
393
443
  Selection:
394
444
 
395
- - inspect configured work item source
396
- - choose one work item
397
- - avoid keys already present in `seen_work_item_keys`
445
+ - choose an ordered queue from `available_work_items`
446
+ - treat candidates agnostically: they may be plans, PRDs, or tasks
447
+ - prefer the most actionable items based on candidate titles and excerpts
448
+ - if a plan references concrete candidate tasks, choose the concrete task when
449
+ that is the best next work
450
+ - avoid keys already present in `seen_work_item_keys` or
451
+ `completed_work_item_keys`
452
+ - optionally include `selection_groups` to present related work by plan or PRD
398
453
  - return `selected` or `no_work`
399
454
 
400
455
  Execution:
@@ -429,17 +484,18 @@ Closure:
429
484
  - model name
430
485
  - reasoning level
431
486
  - max iterations
432
- - work item source template: `local-markdown`, `github`, or `custom`
487
+ - work item source template: `local` or `github`
433
488
 
434
- For `local-markdown`, init asks for a task path.
489
+ For `local`, init asks for a work item path.
490
+ For `github`, init asks for a repository in `owner/repo` format.
435
491
 
436
492
  Default path selection:
437
493
 
438
494
  - use `issues/` if it exists
439
495
  - otherwise suggest `.nyxagent/tasks/`
440
496
 
441
- If the chosen local task path does not exist, init may create it and add a
442
- sample task.
497
+ If the chosen local work item path does not exist, init creates the directory but
498
+ does not create sample work items.
443
499
 
444
500
  ## Implementation Stack
445
501
 
@@ -479,7 +535,7 @@ src/
479
535
 
480
536
  - `nyxagent resume`
481
537
  - `nyxagent import-tasks`
482
- - tracker adapters for GitHub, Jira, Linear, or local frontmatter tasks
538
+ - additional tracker adapters
483
539
  - explicit Git commit adapter
484
540
  - stricter artifact redaction
485
541
  - richer phase retry policy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyxa/nyx-agent",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "A lightweight phase orchestrator for repeatedly launching coding agents with fresh context.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,19 +1,37 @@
1
- Select exactly one work item for this iteration.
1
+ Select the ordered work item queue for this run.
2
2
 
3
- Use the work item configuration from the runtime contract. Prefer open,
4
- well-scoped work that has not already been selected in this run. Avoid any item
5
- whose key appears in `seen_work_item_keys`.
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
6
 
7
- If no suitable work item exists, return `no_work`.
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`.
11
+
12
+ Prefer concrete tasks over their parent plan when both are present and the task
13
+ is ready to execute. Choose the plan itself only when it is the best next work.
14
+ If no candidate is exploitable, return `no_work`.
15
+
16
+ Do not choose any key listed in `seen_work_item_keys` or
17
+ `completed_work_item_keys`. Every selected work item must be copied from
18
+ `available_work_items`; do not invent keys.
19
+
20
+ The selected work item identities must match the inventory entries:
21
+
22
+ - local: `source.type` is `local`, `source.locator` is the project-relative
23
+ markdown path, and `key` is `local:<source.locator>`.
24
+ - github: `source.type` is `github`, `source.locator` is
25
+ `owner/repo#number` for the configured repository, and `key` is
26
+ `github:<source.locator>`.
8
27
 
9
28
  Return one of these outcomes:
10
29
 
11
- - `selected`: include a stable `work_item`
30
+ - `selected`: include ordered `work_items`; optionally include
31
+ `selection_groups` with `title`, optional `kind`, and `work_item_keys`
12
32
  - `no_work`: include a short `reason`
13
33
 
14
- The selected work item must have a stable key. Examples:
15
-
16
- - `github:owner/repo#42`
17
- - `local:issues/TASK-0007.md`
34
+ For compatibility, `work_item` is still accepted for a single selected item, but
35
+ new results should use `work_items`.
18
36
 
19
37
  Do not modify project files or task files during selection.