@nyxa/nyx-agent 0.8.1 → 0.9.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.
package/README.md CHANGED
@@ -7,9 +7,9 @@ GitHub issue at a time, each phase with fresh context.
7
7
 
8
8
  For every run NyxAgent:
9
9
 
10
- 1. **Selects** executable open GitHub issues to work on (read-only), grouping
11
- GitHub sub-issues under non-executable parent PRD/plan issues, then asks the
12
- user to confirm the proposed checklist.
10
+ 1. **Selects** executable open GitHub issues to work on (read-only), presenting
11
+ GitHub sub-issues in parent PRD/plan sections, then asks the user to confirm
12
+ the proposed checklist with the same section order.
13
13
  2. For each selected issue, in an isolated git **worktree**:
14
14
  - **implements** it (the agent — the only customizable prompt),
15
15
  - optionally **reviews** it in bounded discovery rounds, then revises only
@@ -21,9 +21,10 @@ For every run NyxAgent:
21
21
  The agent only implements, reviews, and revises. Every git/gh side effect —
22
22
  commit, push, pull request — is performed by the engine, so closing the loop
23
23
  never depends on the model. Issues are closed by GitHub when the PR merges
24
- (`Closes #n` in the PR body); parent PRD/plan issues are closed only when
25
- NyxAgent can prove the PR completes their remaining open executable children.
26
- The human merges the PR.
24
+ (`Closes #n` in the PR body); parent PRD/plan issues referenced explicitly from
25
+ child descriptions are grouped for selection, while parent close lines are added
26
+ only when NyxAgent can prove the PR completes their remaining open executable
27
+ children. The human merges the PR.
27
28
 
28
29
  The workflow shape is fixed (not configurable). Only `.nyxagent/prompts/execution.md`
29
30
  is editable.
@@ -11,7 +11,7 @@ import { createRunReporter } from "./reporter.js";
11
11
  import { runAgentPhase, } from "./runPhase.js";
12
12
  import { REVIEW_CHALLENGE_SCHEMA, REVIEW_DISCOVERY_SCHEMA, GLOBAL_REVIEW_SCHEMA, REVIEW_VALIDATION_SCHEMA, SELECTION_SCHEMA, } from "./schemas.js";
13
13
  import { commitAll, commitsAhead, createPullRequest, pushBranch, rangeDiff, stageAllAndDiff, } from "./scm.js";
14
- import { confirmWorkItemSelection, } from "./selectionConfirmation.js";
14
+ import { buildSelectionSections, confirmWorkItemSelection, } from "./selectionConfirmation.js";
15
15
  import { createRunId } from "./time.js";
16
16
  import { filterAvailable, listGitHubWorkItemInventory, resolveSelectedQueue, } from "./workItems.js";
17
17
  const MAX_CANDIDATES = 50;
@@ -285,13 +285,9 @@ async function runSelection(input) {
285
285
  ["Max work items this run", input.config.max_iterations],
286
286
  [
287
287
  "Available candidates",
288
- input.candidates.map((candidate) => ({
289
- key: candidate.key,
290
- number: candidate.number,
291
- title: candidate.title,
292
- labels: candidate.labels,
293
- parent: candidate.parent,
294
- excerpt: candidate.excerpt,
288
+ buildSelectionSections(input.candidates).map((section) => ({
289
+ section: section.label ?? "Ungrouped issues",
290
+ candidates: section.candidates.map(selectionCandidateSummary),
295
291
  })),
296
292
  ],
297
293
  ]);
@@ -880,6 +876,16 @@ function workItemSummary(item) {
880
876
  parent: item.parent,
881
877
  };
882
878
  }
879
+ function selectionCandidateSummary(candidate) {
880
+ return {
881
+ key: candidate.key,
882
+ number: candidate.number,
883
+ title: candidate.title,
884
+ labels: candidate.labels,
885
+ parent: candidate.parent,
886
+ excerpt: candidate.excerpt,
887
+ };
888
+ }
883
889
  function buildCommitMessage(item) {
884
890
  return `${item.title}\n\nWork item: ${item.source.locator}`;
885
891
  }
@@ -7,10 +7,11 @@ export async function confirmWorkItemSelection(input) {
7
7
  if (!isInteractive) {
8
8
  throw new Error('Interactive work item selection requires a TTY. Re-run with "nyxagent run --yes" to accept the agent selection.');
9
9
  }
10
+ const choiceItems = buildSelectionChoiceItems(input);
10
11
  const selectedKeys = await checkbox({
11
12
  message: `Select work items to run (max ${input.maxItems})`,
12
- choices: buildSelectionChoiceItems(input).map(toInquirerChoice),
13
- pageSize: Math.min(Math.max(input.candidates.length, 7), 20),
13
+ choices: choiceItems.map(toInquirerChoice),
14
+ pageSize: Math.min(Math.max(choiceItems.length, 7), 20),
14
15
  required: false,
15
16
  validate: (selected) => selected.length <= input.maxItems ||
16
17
  `Select at most ${input.maxItems} work item(s).`,
@@ -20,28 +21,58 @@ export async function confirmWorkItemSelection(input) {
20
21
  },
21
22
  });
22
23
  const selected = new Set(selectedKeys);
23
- return input.candidates.filter((candidate) => selected.has(candidate.key));
24
+ const candidatesByKey = new Map(input.candidates.map((candidate) => [candidate.key, candidate]));
25
+ return choiceItems.flatMap((item) => {
26
+ if (item.type !== "choice" || !selected.has(item.value)) {
27
+ return [];
28
+ }
29
+ const candidate = candidatesByKey.get(item.value);
30
+ return candidate ? [candidate] : [];
31
+ });
24
32
  }
25
33
  export function buildSelectionChoiceItems(input) {
26
34
  const proposedKeys = new Set(input.proposed.map((item) => item.key));
27
35
  const items = [];
28
- let currentGroup;
29
- for (const candidate of input.candidates) {
30
- const group = detectPlanGroup(candidate);
31
- if (group && group !== currentGroup) {
32
- items.push({ type: "separator", label: group });
36
+ for (const section of buildSelectionSections(input.candidates)) {
37
+ if (section.label) {
38
+ items.push({ type: "separator", label: section.label });
39
+ }
40
+ for (const candidate of section.candidates) {
41
+ const proposed = proposedKeys.has(candidate.key);
42
+ items.push({
43
+ type: "choice",
44
+ value: candidate.key,
45
+ name: `#${candidate.number} ${candidate.title}${proposed ? " (agent)" : ""}`,
46
+ checked: proposed,
47
+ });
33
48
  }
34
- currentGroup = group;
35
- const proposed = proposedKeys.has(candidate.key);
36
- items.push({
37
- type: "choice",
38
- value: candidate.key,
39
- name: `#${candidate.number} ${candidate.title}${proposed ? " (agent)" : ""}`,
40
- checked: proposed,
41
- });
42
49
  }
43
50
  return items;
44
51
  }
52
+ export function buildSelectionSections(candidates) {
53
+ const sections = [];
54
+ const groupedSections = new Map();
55
+ let ungroupedSection;
56
+ for (const candidate of candidates) {
57
+ const group = detectSelectionGroup(candidate);
58
+ if (!group) {
59
+ if (!ungroupedSection) {
60
+ ungroupedSection = { candidates: [] };
61
+ sections.push(ungroupedSection);
62
+ }
63
+ ungroupedSection.candidates.push(candidate);
64
+ continue;
65
+ }
66
+ let section = groupedSections.get(group.key);
67
+ if (!section) {
68
+ section = { label: group.label, candidates: [] };
69
+ groupedSections.set(group.key, section);
70
+ sections.push(section);
71
+ }
72
+ section.candidates.push(candidate);
73
+ }
74
+ return sections;
75
+ }
45
76
  function toInquirerChoice(item) {
46
77
  if (item.type === "separator") {
47
78
  return new Separator(item.label);
@@ -52,25 +83,36 @@ function toInquirerChoice(item) {
52
83
  checked: item.checked,
53
84
  };
54
85
  }
55
- function detectPlanGroup(candidate) {
86
+ function detectSelectionGroup(candidate) {
56
87
  if (candidate.parent) {
57
- return formatParentGroup(candidate.parent);
88
+ return {
89
+ key: `parent:${candidate.parent.key}`,
90
+ label: formatParentGroup(candidate.parent),
91
+ };
58
92
  }
59
93
  for (const label of candidate.labels ?? []) {
60
94
  const group = parseGroupLabel(label);
61
95
  if (group) {
62
- return group;
96
+ return { key: `display:${group.toLowerCase()}`, label: group };
63
97
  }
64
98
  }
65
- return parseBracketedTitleGroup(candidate.title);
99
+ const titleGroup = parseBracketedTitleGroup(candidate.title);
100
+ return titleGroup
101
+ ? { key: `display:${titleGroup.toLowerCase()}`, label: titleGroup }
102
+ : undefined;
66
103
  }
67
104
  function formatParentGroup(parent) {
68
105
  if (!parent) {
69
106
  return "Parent";
70
107
  }
71
- return parent.title
72
- ? `Parent #${parent.number}: ${parent.title}`
73
- : `Parent #${parent.number}`;
108
+ if (!parent.title) {
109
+ return `Parent #${parent.number}`;
110
+ }
111
+ const titledGroup = parseLeadingTitleGroup(parent.title);
112
+ if (titledGroup) {
113
+ return `${titledGroup.kind} #${parent.number}: ${titledGroup.name}`;
114
+ }
115
+ return `Parent #${parent.number}: ${parent.title}`;
74
116
  }
75
117
  function parseGroupLabel(label) {
76
118
  const match = /^(plan|prd)\s*[:/=-]\s*(.+)$/i.exec(label.trim());
@@ -86,6 +128,20 @@ function parseBracketedTitleGroup(title) {
86
128
  }
87
129
  return formatGroupLabel(match[1], match[2]);
88
130
  }
131
+ function parseLeadingTitleGroup(title) {
132
+ const match = /^(plan|prd)\s*[:/=-]\s*(.+)$/i.exec(title.trim());
133
+ if (!match) {
134
+ return undefined;
135
+ }
136
+ const name = match[2].trim();
137
+ if (!name) {
138
+ return undefined;
139
+ }
140
+ return {
141
+ kind: match[1].toLowerCase() === "prd" ? "PRD" : "Plan",
142
+ name,
143
+ };
144
+ }
89
145
  function formatGroupLabel(kind, rawName) {
90
146
  const name = rawName.trim();
91
147
  if (!name) {
@@ -376,7 +376,7 @@ function parseExplicitParentReferenceLine(line) {
376
376
  .replace(/^[-*]\s+/, "")
377
377
  .replace(/^>\s+/, "");
378
378
  const issueRef = "(?:#|[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#|https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/issues/)(\\d+)";
379
- const match = new RegExp(`^(?:parent(?:\\s+issue)?|prd|plan)\\s*(?::|=|-)?\\s*${issueRef}\\s*$`, "i").exec(trimmed);
379
+ const match = new RegExp(`^(?:parent(?:\\s+(?:issue|prd|plan))?|prd|plan)\\s*(?::|=|-)?\\s*${issueRef}\\s*$`, "i").exec(trimmed);
380
380
  if (!match) {
381
381
  return undefined;
382
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyxa/nyx-agent",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "A lightweight phase orchestrator for repeatedly launching coding agents with fresh context.",
5
5
  "type": "module",
6
6
  "repository": {