@nyxa/nyx-agent 0.7.0 → 0.8.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,11 +7,13 @@ GitHub issue at a time, each phase with fresh context.
7
7
 
8
8
  For every run NyxAgent:
9
9
 
10
- 1. **Selects** open GitHub issues to work on (read-only), then asks the user to
11
- confirm the proposed checklist.
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.
12
13
  2. For each selected issue, in an isolated git **worktree**:
13
14
  - **implements** it (the agent — the only customizable prompt),
14
- - optionally **reviews** and **revises** it (bounded loop),
15
+ - optionally **reviews** it in bounded discovery rounds, then revises only
16
+ verified blockers with locked validation,
15
17
  - **commits** the change (the engine, deterministically).
16
18
  3. Optionally runs a **global review** across the whole run.
17
19
  4. **Pushes** the run branch and **opens one pull request** (the engine).
@@ -19,7 +21,9 @@ For every run NyxAgent:
19
21
  The agent only implements, reviews, and revises. Every git/gh side effect —
20
22
  commit, push, pull request — is performed by the engine, so closing the loop
21
23
  never depends on the model. Issues are closed by GitHub when the PR merges
22
- (`Closes #n` in the PR body); the human merges the PR.
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.
23
27
 
24
28
  The workflow shape is fixed (not configurable). Only `.nyxagent/prompts/execution.md`
25
29
  is editable.
@@ -45,7 +49,7 @@ nyxagent update # self-update to the latest published version
45
49
  "model": "gpt-5.5",
46
50
  "reasoning_effort": "medium",
47
51
  "review": "each",
48
- "review_max_attempts": 4,
52
+ "review_rounds": { "each": 1, "global": 1 },
49
53
  "tracker": { "type": "github", "repo": "owner/repo" },
50
54
  "base_branch": "main",
51
55
  "max_iterations": 5
@@ -54,12 +58,19 @@ nyxagent update # self-update to the latest published version
54
58
 
55
59
  - `harness`: `codex` or `claude` (override per run with `--harness`).
56
60
  - `review`: `each` (per task), `all` (global only), `both`, or `none`.
57
- - `review_max_attempts`: review+revise rounds per stage before the run fails (default 4).
61
+ - `review_rounds.each`: fresh per-task discovery rounds (default 1).
62
+ - `review_rounds.global`: fresh global discovery rounds (default 1).
63
+ - `review_max_attempts`: deprecated; accepted for old configs with a warning, but
64
+ ignored by the review loop.
65
+ - `agents.execution`, `agents.review`, `agents.global_review`, and
66
+ `agents.global_review.roles.<role>` can override `harness`, `model`, and
67
+ `reasoning_effort` for specialized phases. Global review roles are
68
+ `diff-contract`, `integration`, `domain-invariants`, and `tests-validation`.
58
69
  - `base_branch`: optional; defaults to the current branch at run time.
59
70
 
60
- If a run fails review after exhausting its attempts but has already produced
71
+ If a run fails review validation but has already produced
61
72
  commits, NyxAgent pushes the branch and opens a **draft** pull request with the
62
- unresolved feedback, so the work is never stranded on an orphaned branch.
73
+ unresolved blockers, so the work is never stranded on an orphaned branch.
63
74
 
64
75
  ## Requirements
65
76
 
package/dist/cli.js CHANGED
@@ -20,7 +20,9 @@ program
20
20
  .option("--model <name>", "model name")
21
21
  .option("--reasoning-effort <level>", "reasoning effort (default: medium)")
22
22
  .option("--review <mode>", "review strategy: each, all, both, or none")
23
- .option("--review-attempts <count>", "max review attempts per stage (default: 4)")
23
+ .option("--review-rounds-each <count>", "per-work-item review discovery rounds (default: 1)")
24
+ .option("--review-rounds-global <count>", "global review discovery rounds (default: 1)")
25
+ .option("--review-attempts <count>", "deprecated alias for both review round counts")
24
26
  .option("--repo <owner/repo>", "GitHub repository")
25
27
  .option("--base-branch <branch>", "base branch (default: current branch)")
26
28
  .option("--max-iterations <count>", "maximum work items per run")
@@ -2,8 +2,8 @@
2
2
  import path from "node:path";
3
3
  import { input, number as numberPrompt, select } from "@inquirer/prompts";
4
4
  import pc from "picocolors";
5
- import { harnessNames, reviewModes } from "../config/schema.js";
6
- import { ensureDir, pathExists, readText, writeText } from "../runtime/files.js";
5
+ import { harnessNames, reviewModes, } from "../config/schema.js";
6
+ import { ensureDir, pathExists, readText, writeText, } from "../runtime/files.js";
7
7
  import { getNyxDir, relativeToProject } from "../runtime/paths.js";
8
8
  import { EXECUTION_PROMPT_FILE } from "../runtime/prompts.js";
9
9
  const DEFAULT_CODEX_MODEL = "gpt-5.5";
@@ -17,7 +17,7 @@ const GITIGNORE_ENTRIES = [
17
17
  ".nyxagent/state.json",
18
18
  ".nyxagent/config.json",
19
19
  ".nyxagent/config.toml",
20
- ".nyxagent/prompts/"
20
+ ".nyxagent/prompts/",
21
21
  ];
22
22
  export async function initCommand(options, projectRoot = process.cwd()) {
23
23
  const root = path.resolve(projectRoot);
@@ -46,14 +46,14 @@ async function resolveInitOptions(options) {
46
46
  message: "Default harness",
47
47
  choices: [
48
48
  { name: "codex", value: "codex" },
49
- { name: "claude", value: "claude" }
50
- ]
49
+ { name: "claude", value: "claude" },
50
+ ],
51
51
  });
52
52
  const model = options.model ??
53
53
  (await input({
54
54
  message: "Model",
55
55
  default: harness === "codex" ? DEFAULT_CODEX_MODEL : "",
56
- validate: (value) => value.trim().length > 0 || "Model is required"
56
+ validate: (value) => value.trim().length > 0 || "Model is required",
57
57
  }));
58
58
  const reasoning_effort = options.reasoningEffort ??
59
59
  (await input({ message: "Reasoning effort", default: "medium" }));
@@ -65,34 +65,27 @@ async function resolveInitOptions(options) {
65
65
  { name: "After each task", value: "each" },
66
66
  { name: "After all tasks (global review)", value: "all" },
67
67
  { name: "Both per-task and global", value: "both" },
68
- { name: "No review", value: "none" }
68
+ { name: "No review", value: "none" },
69
69
  ],
70
- default: "each"
70
+ default: "each",
71
71
  });
72
- const review_max_attempts = review === "none"
73
- ? 4
74
- : parseReviewAttempts(options.reviewAttempts) ??
75
- (await numberPrompt({
76
- message: "Max review attempts per stage",
77
- default: 4,
78
- required: true
79
- }));
80
- if (!Number.isInteger(review_max_attempts) || review_max_attempts <= 0) {
81
- throw new Error("review attempts must be a positive integer");
82
- }
83
- const repo = options.repo ?? (await input({ message: "GitHub repository (owner/repo)" }));
72
+ const review_rounds = await resolveReviewRounds(options, review);
73
+ const repo = options.repo ??
74
+ (await input({ message: "GitHub repository (owner/repo)" }));
84
75
  validateRepository(repo);
85
76
  const baseBranchInput = options.baseBranch ??
86
77
  (await input({
87
78
  message: "Base branch (blank = current branch at run time)",
88
- default: ""
79
+ default: "",
89
80
  }));
90
- const base_branch = baseBranchInput.trim() ? baseBranchInput.trim() : undefined;
81
+ const base_branch = baseBranchInput.trim()
82
+ ? baseBranchInput.trim()
83
+ : undefined;
91
84
  const max_iterations = parseMaxIterations(options.maxIterations) ??
92
85
  (await numberPrompt({
93
86
  message: "Max work items per run",
94
87
  default: 5,
95
- required: true
88
+ required: true,
96
89
  }));
97
90
  if (!Number.isInteger(max_iterations) || max_iterations <= 0) {
98
91
  throw new Error("max iterations must be a positive integer");
@@ -102,10 +95,10 @@ async function resolveInitOptions(options) {
102
95
  model: model.trim(),
103
96
  reasoning_effort: reasoning_effort.trim() || "medium",
104
97
  review,
105
- review_max_attempts,
98
+ review_rounds,
106
99
  repo,
107
100
  base_branch,
108
- max_iterations
101
+ max_iterations,
109
102
  };
110
103
  }
111
104
  function buildConfig(options) {
@@ -115,11 +108,11 @@ function buildConfig(options) {
115
108
  reasoning_effort: options.reasoning_effort,
116
109
  review: options.review,
117
110
  tracker: { type: "github", repo: options.repo },
118
- max_iterations: options.max_iterations
111
+ max_iterations: options.max_iterations,
119
112
  };
120
- // No point persisting an attempts cap when reviews are disabled.
113
+ // No point persisting review rounds when reviews are disabled.
121
114
  if (options.review !== "none") {
122
- config.review_max_attempts = options.review_max_attempts;
115
+ config.review_rounds = options.review_rounds;
123
116
  }
124
117
  if (options.base_branch) {
125
118
  config.base_branch = options.base_branch;
@@ -149,7 +142,38 @@ function parseMaxIterations(value) {
149
142
  }
150
143
  return Number.parseInt(value, 10);
151
144
  }
152
- function parseReviewAttempts(value) {
145
+ async function resolveReviewRounds(options, review) {
146
+ if (review === "none") {
147
+ return { each: 1, global: 1 };
148
+ }
149
+ const deprecatedAttempts = parsePositiveInteger(options.reviewAttempts);
150
+ const each = parsePositiveInteger(options.reviewRoundsEach) ??
151
+ deprecatedAttempts ??
152
+ (review === "each" || review === "both"
153
+ ? await numberPrompt({
154
+ message: "Review rounds per work item",
155
+ default: 1,
156
+ required: true,
157
+ })
158
+ : 1);
159
+ const global = parsePositiveInteger(options.reviewRoundsGlobal) ??
160
+ deprecatedAttempts ??
161
+ (review === "all" || review === "both"
162
+ ? await numberPrompt({
163
+ message: "Global review rounds",
164
+ default: 1,
165
+ required: true,
166
+ })
167
+ : 1);
168
+ if (!Number.isInteger(each) || each <= 0) {
169
+ throw new Error("review_rounds.each must be a positive integer");
170
+ }
171
+ if (!Number.isInteger(global) || global <= 0) {
172
+ throw new Error("review_rounds.global must be a positive integer");
173
+ }
174
+ return { each, global };
175
+ }
176
+ function parsePositiveInteger(value) {
153
177
  if (value === undefined) {
154
178
  return undefined;
155
179
  }
@@ -7,7 +7,37 @@ import { z } from "zod";
7
7
  */
8
8
  export const harnessNames = ["codex", "claude"];
9
9
  export const reviewModes = ["each", "all", "both", "none"];
10
+ export const globalReviewRoles = [
11
+ "diff-contract",
12
+ "integration",
13
+ "domain-invariants",
14
+ "tests-validation",
15
+ ];
10
16
  const githubRepositoryPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
17
+ const reviewRoundsSchema = z
18
+ .object({
19
+ each: z.number().int().positive().default(1),
20
+ global: z.number().int().positive().default(1),
21
+ })
22
+ .default({ each: 1, global: 1 });
23
+ const agentOverrideSchema = z
24
+ .object({
25
+ harness: z.enum(harnessNames).optional(),
26
+ model: z.string().min(1).optional(),
27
+ reasoning_effort: z.string().min(1).optional(),
28
+ })
29
+ .strict();
30
+ const globalReviewAgentOverrideSchema = agentOverrideSchema.extend({
31
+ roles: z
32
+ .object({
33
+ "diff-contract": agentOverrideSchema.optional(),
34
+ integration: agentOverrideSchema.optional(),
35
+ "domain-invariants": agentOverrideSchema.optional(),
36
+ "tests-validation": agentOverrideSchema.optional(),
37
+ })
38
+ .strict()
39
+ .optional(),
40
+ });
11
41
  export const nyxConfigSchema = z
12
42
  .object({
13
43
  /** Which agent CLI runs each phase. Overridable per run via `run --harness`. */
@@ -18,18 +48,29 @@ export const nyxConfigSchema = z
18
48
  reasoning_effort: z.string().min(1).default("medium"),
19
49
  /** When the agent reviews its own work. */
20
50
  review: z.enum(reviewModes).default("each"),
21
- /** How many review+revise rounds a review stage gets before the run fails. */
22
- review_max_attempts: z.number().int().positive().default(4),
51
+ /** How many fresh discovery rounds each review stage may run. */
52
+ review_rounds: reviewRoundsSchema,
53
+ /** Deprecated: accepted for existing configs, but no longer drives reviews. */
54
+ review_max_attempts: z.number().int().positive().optional(),
55
+ /** Optional agent overrides by phase and global-review role. */
56
+ agents: z
57
+ .object({
58
+ execution: agentOverrideSchema.optional(),
59
+ review: agentOverrideSchema.optional(),
60
+ global_review: globalReviewAgentOverrideSchema.optional(),
61
+ })
62
+ .strict()
63
+ .optional(),
23
64
  /** Work item tracker. GitHub issues only in this version. */
24
65
  tracker: z.object({
25
66
  type: z.literal("github"),
26
67
  repo: z
27
68
  .string()
28
- .regex(githubRepositoryPattern, 'tracker.repo must be "owner/repo"')
69
+ .regex(githubRepositoryPattern, 'tracker.repo must be "owner/repo"'),
29
70
  }),
30
71
  /** Base branch the run branch is cut from. Defaults to the current branch. */
31
72
  base_branch: z.string().min(1).optional(),
32
73
  /** Maximum work items processed in a single run. */
33
- max_iterations: z.number().int().positive().default(5)
74
+ max_iterations: z.number().int().positive().default(5),
34
75
  })
35
76
  .strict();
@@ -23,39 +23,72 @@ test, implement the smallest change that satisfies it, then tidy the result.
23
23
 
24
24
  Do not commit and do not touch git — NyxAgent commits your changes for you. Leave
25
25
  clear validation evidence (commands run and their results) in your final response.`;
26
- export const REVIEW_PROMPT = `Review the implementation of the selected work item.
26
+ export const REVIEW_PROMPT = `Discover findings in the implementation of the selected work item.
27
27
 
28
- The uncommitted changes for this item are shown as a diff in the context above; you
29
- may also read files in the working directory. Stay read-only and do not modify
30
- anything.
28
+ Use the review-context artifact paths in the context above. Inspect the patch file,
29
+ diffstat, changed-files list, and the working directory as needed. Stay read-only
30
+ and do not modify anything.
31
31
 
32
- Assess: alignment with the work item, correctness and regression risk, test or
33
- validation evidence, design fit, and security or data-safety concerns.
32
+ This is discovery for the current review round only. Assess alignment with the work
33
+ item, correctness and regression risk, test or validation evidence, design fit, and
34
+ security or data-safety concerns.
34
35
 
35
- Set outcome to "approved" when the work is ready, or "changes_requested" with a
36
- concrete, actionable list in required_changes. Always include a short summary.`;
36
+ Put only must-fix issues in blockers. Put missing or weak validation in test_gaps,
37
+ non-blocking concerns in advisory_findings, uncertain suspicions in
38
+ uncertain_findings, and explicitly refuted candidates in rejected_findings.`;
39
+ export const REVIEW_CHALLENGE_PROMPT = `Challenge the proposed blockers for the selected work item.
40
+
41
+ Stay read-only. Try to refute each proposed blocker using the current code,
42
+ review-context artifacts, and concrete evidence. Return only blockers that remain
43
+ valid and actionable. Move false positives or already-satisfied findings to
44
+ rejected_findings with evidence. Do not introduce new findings in this phase.`;
37
45
  export const REVISION_PROMPT = `Apply the changes requested by the review for the selected work item.
38
46
 
39
- The required changes are listed in the context above. Address exactly those, keeping
40
- the work focused. Do not commit — NyxAgent commits your changes for you.`;
47
+ The verified blockers are listed in the context above. Address exactly those,
48
+ keeping the work focused. Do not commit — NyxAgent commits your changes for you.`;
49
+ export const REVIEW_VALIDATION_PROMPT = `Validate the correction for the previously verified blockers.
50
+
51
+ Stay read-only. Validate only the blockers listed in the context above. Do not run a
52
+ new review and do not introduce unrelated new findings. For each blocker, return one
53
+ status: resolved, unresolved, false_positive, or regression_from_correction.
54
+
55
+ Use regression_from_correction only when the correction itself directly created a
56
+ new blocker and the evidence proves that causal link.`;
41
57
  export const GLOBAL_REVIEW_PROMPT = `Review the entire run as a whole, now that every selected work item is implemented
42
58
  and committed.
43
59
 
44
- The combined diff for the run is shown in the context above; you may also read files
45
- in the working directory. Stay read-only and do not modify anything.
60
+ Use the review-context artifact paths in the context above. Inspect the patch file,
61
+ diffstat, changed-files list, commit list, and the working directory as needed. Stay
62
+ read-only and do not modify anything.
46
63
 
47
64
  Focus on cross-cutting concerns a per-item review cannot see: integration between
48
65
  items, regressions one item introduced in another, overall design coherence,
49
66
  duplication, and gaps versus the issues' intent.
50
67
 
51
- Set outcome to "approved" when the run is coherent and ready, or
52
- "changes_requested" with a concrete, actionable list in required_changes. Always
53
- include a short summary.`;
68
+ Return typed findings. Put only must-fix issues in blockers. Put missing or weak
69
+ validation in test_gaps, non-blocking concerns in advisory_findings, uncertain
70
+ suspicions in uncertain_findings, and explicitly refuted candidates in
71
+ rejected_findings.`;
72
+ export const GLOBAL_REVIEW_CHALLENGE_PROMPT = `Challenge the aggregated global-review blockers.
73
+
74
+ Stay read-only. Try to refute each proposed blocker using the current code,
75
+ review-context artifacts, and concrete evidence. Return only blockers that remain
76
+ valid and actionable. Move false positives or already-satisfied findings to
77
+ rejected_findings with evidence. Do not introduce new findings in this phase.`;
54
78
  export const GLOBAL_REVISION_PROMPT = `Apply the changes requested by the global review of the whole run.
55
79
 
56
- The required changes are listed in the context above. Address exactly those, across
80
+ The verified blockers are listed in the context above. Address exactly those, across
57
81
  whichever work items are affected. Do not commit — NyxAgent commits your corrections
58
82
  for you.`;
83
+ export const GLOBAL_REVIEW_VALIDATION_PROMPT = `Validate the global review correction for the previously verified blockers.
84
+
85
+ Stay read-only. Validate only the blockers listed in the context above. Do not run a
86
+ new global review and do not introduce unrelated new findings. For each blocker,
87
+ return one status: resolved, unresolved, false_positive, or
88
+ regression_from_correction.
89
+
90
+ Use regression_from_correction only when the correction itself directly created a
91
+ new blocker and the evidence proves that causal link.`;
59
92
  /** Rendered into .nyxagent/prompts/execution.md at init; the only editable prompt. */
60
93
  export const EXECUTION_PROMPT_FILE = `${EXECUTION_PROMPT}
61
94
  `;
@@ -88,7 +121,7 @@ export function buildPhasePrompt(input) {
88
121
  "",
89
122
  "## Instructions",
90
123
  "",
91
- input.guidance.trim()
124
+ input.guidance.trim(),
92
125
  ];
93
126
  if (input.schema) {
94
127
  parts.push("", "## Required result", "", "End your response with a single <nyxagent_result> block containing JSON that", "matches this schema. NyxAgent parses the last such block, validates it, and", "ignores everything else for control flow.", "", "```json", JSON.stringify(input.schema, null, 2), "```", "", "<nyxagent_result>", "{ ... }", "</nyxagent_result>");