@nyxa/nyx-agent 0.6.1 → 0.7.0

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
@@ -31,6 +31,7 @@ nyxagent init # create .nyxagent/config.json (interactive)
31
31
  nyxagent run # run the pipeline, confirming selected work items
32
32
  nyxagent run --yes # accept the agent selection without prompting
33
33
  nyxagent run --harness claude # override the configured harness for one run
34
+ nyxagent run --verbose # stream agent output and runtime details
34
35
  nyxagent update # self-update to the latest published version
35
36
  ```
36
37
 
package/dist/cli.js CHANGED
@@ -34,6 +34,7 @@ program
34
34
  .option("--config <path>", "config path (default: .nyxagent/config.json)")
35
35
  .option("--harness <name>", "override the configured harness: codex or claude")
36
36
  .option("-y, --yes", "accept the agent-selected work items without prompting")
37
+ .option("--verbose", "stream agent output and NyxAgent runtime details")
37
38
  .action(async (options) => {
38
39
  await runCommand(options);
39
40
  });
@@ -9,7 +9,8 @@ export async function runCommand(options, projectRoot = process.cwd()) {
9
9
  ? path.resolve(projectRoot, options.config)
10
10
  : undefined,
11
11
  harness: normalizeHarness(options.harness),
12
- autoAcceptSelection: options.yes ?? false
12
+ autoAcceptSelection: options.yes ?? false,
13
+ verbose: options.verbose ?? false,
13
14
  });
14
15
  }
15
16
  function normalizeHarness(value) {
@@ -0,0 +1,65 @@
1
+ import path from "node:path";
2
+ import pc from "picocolors";
3
+ export function createRunReporter(options = {}) {
4
+ const verbose = options.verbose ?? false;
5
+ const writeStdout = options.writeStdout ?? ((line) => console.log(line));
6
+ const writeStderr = options.writeStderr ??
7
+ ((line) => {
8
+ process.stderr.write(`${line}\n`);
9
+ });
10
+ const stdout = (line) => writeStdout(line);
11
+ const stderr = (line) => writeStderr(line);
12
+ return {
13
+ verbose,
14
+ heading: (message) => stdout(pc.bold(message)),
15
+ info: (message) => stdout(message),
16
+ section: (message) => stdout(pc.cyan(message)),
17
+ success: (message) => stdout(pc.green(message)),
18
+ warn: (message) => stdout(pc.yellow(message)),
19
+ error: (message) => stdout(pc.red(message)),
20
+ detail: (message) => {
21
+ if (verbose) {
22
+ stderr(pc.dim(message));
23
+ }
24
+ },
25
+ phaseStarted: (event) => {
26
+ if (!verbose) {
27
+ return;
28
+ }
29
+ const attempt = attemptLabel(event.attemptDir);
30
+ const command = [event.invocation.command, ...event.invocation.args].join(" ");
31
+ stderr(pc.dim(`[${event.phaseId} ${attempt}] start ${command} (cwd ${event.workdir}, capability ${event.capability}, model ${event.model}, reasoning ${event.reasoning})`));
32
+ },
33
+ phaseFinished: (event) => {
34
+ if (!verbose) {
35
+ return;
36
+ }
37
+ stderr(pc.dim(`[${event.phaseId} ${attemptLabel(event.attemptDir)}] exit ${event.exitCode} in ${formatDuration(event.durationMs)}`));
38
+ },
39
+ phaseArtifact: (event) => {
40
+ if (!verbose) {
41
+ return;
42
+ }
43
+ const prefix = event.attemptDir
44
+ ? `${event.phaseId} ${attemptLabel(event.attemptDir)}`
45
+ : event.phaseId;
46
+ stderr(pc.dim(`[${prefix}] artifact ${event.filePath}`));
47
+ },
48
+ agentOutput: (event) => {
49
+ if (!verbose) {
50
+ return;
51
+ }
52
+ const outputType = event.stream ?? event.eventType;
53
+ stderr(`[${event.phaseId} ${attemptLabel(event.attemptDir)} ${outputType}] ${event.message}`);
54
+ },
55
+ };
56
+ }
57
+ function attemptLabel(attemptDirOrPath) {
58
+ return path.basename(attemptDirOrPath);
59
+ }
60
+ function formatDuration(durationMs) {
61
+ if (durationMs < 1000) {
62
+ return `${durationMs}ms`;
63
+ }
64
+ return `${(durationMs / 1000).toFixed(1)}s`;
65
+ }
@@ -15,12 +15,12 @@ export async function runAgentPhase(input) {
15
15
  const attempt = await invokeHarness({
16
16
  attemptDir: path.join(input.phaseDir, "attempt-001"),
17
17
  input,
18
- prompt: input.prompt
18
+ prompt: input.prompt,
19
19
  });
20
20
  if (attempt.exitCode !== 0) {
21
21
  return {
22
22
  ok: false,
23
- error: `Phase "${input.phaseId}" failed with exit code ${attempt.exitCode}`
23
+ error: `Phase "${input.phaseId}" failed with exit code ${attempt.exitCode}`,
24
24
  };
25
25
  }
26
26
  if (!input.schema) {
@@ -28,16 +28,26 @@ export async function runAgentPhase(input) {
28
28
  }
29
29
  const parsed = parseAndValidate(input.schema, attempt.stdout);
30
30
  if (parsed.ok) {
31
- await writeJson(path.join(input.phaseDir, "result.json"), parsed.result);
31
+ const resultPath = path.join(input.phaseDir, "result.json");
32
+ await writeJson(resultPath, parsed.result);
33
+ input.reporter?.phaseArtifact({
34
+ phaseId: input.phaseId,
35
+ filePath: resultPath,
36
+ });
32
37
  return parsed;
33
38
  }
34
39
  const repaired = await repairResult({
35
40
  input,
36
41
  originalStdout: attempt.stdout,
37
- validationError: parsed.error
42
+ validationError: parsed.error,
38
43
  });
39
44
  if (repaired.ok && repaired.result !== undefined) {
40
- await writeJson(path.join(input.phaseDir, "result.json"), repaired.result);
45
+ const resultPath = path.join(input.phaseDir, "result.json");
46
+ await writeJson(resultPath, repaired.result);
47
+ input.reporter?.phaseArtifact({
48
+ phaseId: input.phaseId,
49
+ filePath: resultPath,
50
+ });
41
51
  }
42
52
  return repaired;
43
53
  }
@@ -48,19 +58,53 @@ async function invokeHarness(args) {
48
58
  harness: args.input.harness,
49
59
  capability: args.forceReadonly ? "readonly" : args.input.capability,
50
60
  model: args.input.model,
51
- reasoning: args.input.reasoning
61
+ reasoning: args.input.reasoning,
52
62
  });
53
63
  const startedAt = new Date().toISOString();
54
64
  const started = Date.now();
55
65
  const gitBefore = await getGitSnapshot(args.input.workdir);
66
+ args.input.reporter?.phaseStarted({
67
+ phaseId: args.input.phaseId,
68
+ attemptDir: args.attemptDir,
69
+ workdir: args.input.workdir,
70
+ capability: args.forceReadonly ? "readonly" : args.input.capability,
71
+ model: args.input.model,
72
+ reasoning: args.input.reasoning,
73
+ invocation,
74
+ });
56
75
  let stdout = "";
57
76
  let stderr = "";
58
77
  let exitCode = 0;
59
78
  try {
79
+ const verbose = args.input.reporter?.verbose
80
+ ? {
81
+ stdout: (line, event) => {
82
+ args.input.reporter?.agentOutput({
83
+ phaseId: args.input.phaseId,
84
+ attemptDir: args.attemptDir,
85
+ eventType: event.type,
86
+ stream: event.type === "output" ? "stdout" : undefined,
87
+ message: line,
88
+ });
89
+ return "";
90
+ },
91
+ stderr: (line, event) => {
92
+ args.input.reporter?.agentOutput({
93
+ phaseId: args.input.phaseId,
94
+ attemptDir: args.attemptDir,
95
+ eventType: event.type,
96
+ stream: event.type === "output" ? "stderr" : undefined,
97
+ message: line,
98
+ });
99
+ return "";
100
+ },
101
+ }
102
+ : "none";
60
103
  const result = await execa(invocation.command, invocation.args, {
61
104
  cwd: args.input.workdir,
62
105
  input: args.prompt,
63
- reject: false
106
+ reject: false,
107
+ verbose,
64
108
  });
65
109
  stdout = result.stdout;
66
110
  stderr = result.stderr;
@@ -71,18 +115,35 @@ async function invokeHarness(args) {
71
115
  stderr = error instanceof Error ? error.message : String(error);
72
116
  }
73
117
  const gitAfter = await getGitSnapshot(args.input.workdir);
74
- await writeText(path.join(args.attemptDir, "stdout.log"), stdout);
75
- await writeText(path.join(args.attemptDir, "stderr.log"), stderr);
76
- await writeJson(path.join(args.attemptDir, "meta.json"), {
118
+ const durationMs = Date.now() - started;
119
+ const stdoutPath = path.join(args.attemptDir, "stdout.log");
120
+ const stderrPath = path.join(args.attemptDir, "stderr.log");
121
+ const metaPath = path.join(args.attemptDir, "meta.json");
122
+ await writeText(stdoutPath, stdout);
123
+ await writeText(stderrPath, stderr);
124
+ await writeJson(metaPath, {
77
125
  command: invocation.command,
78
126
  args: invocation.args,
79
127
  started_at: startedAt,
80
128
  ended_at: new Date().toISOString(),
81
- duration_ms: Date.now() - started,
129
+ duration_ms: durationMs,
82
130
  exit_code: exitCode,
83
131
  git_before: gitBefore,
84
- git_after: gitAfter
132
+ git_after: gitAfter,
133
+ });
134
+ args.input.reporter?.phaseFinished({
135
+ phaseId: args.input.phaseId,
136
+ attemptDir: args.attemptDir,
137
+ durationMs,
138
+ exitCode,
85
139
  });
140
+ for (const filePath of [stdoutPath, stderrPath, metaPath]) {
141
+ args.input.reporter?.phaseArtifact({
142
+ phaseId: args.input.phaseId,
143
+ attemptDir: args.attemptDir,
144
+ filePath,
145
+ });
146
+ }
86
147
  return { stdout, stderr, exitCode };
87
148
  }
88
149
  function parseAndValidate(schema, stdout) {
@@ -118,13 +179,13 @@ async function repairResult(args) {
118
179
  "",
119
180
  "Original prompt:",
120
181
  "",
121
- args.input.prompt
182
+ args.input.prompt,
122
183
  ].join("\n");
123
184
  const attempt = await invokeHarness({
124
185
  attemptDir: path.join(args.input.phaseDir, `repair-${String(attemptNumber).padStart(3, "0")}`),
125
186
  input: args.input,
126
187
  prompt: repairPrompt,
127
- forceReadonly: true
188
+ forceReadonly: true,
128
189
  });
129
190
  if (attempt.exitCode !== 0) {
130
191
  lastError = `Repair harness exited with code ${attempt.exitCode}`;
@@ -138,6 +199,6 @@ async function repairResult(args) {
138
199
  }
139
200
  return {
140
201
  ok: false,
141
- error: `Phase "${args.input.phaseId}" produced an invalid result: ${lastError}`
202
+ error: `Phase "${args.input.phaseId}" produced an invalid result: ${lastError}`,
142
203
  };
143
204
  }
@@ -1,17 +1,17 @@
1
1
  import path from "node:path";
2
- import pc from "picocolors";
3
2
  import { loadConfig } from "../config/loadConfig.js";
4
3
  import { ensureDir, pathExists, readText } from "./files.js";
5
- import { deleteBranch, removeRunWorktree, setUpRunWorktree } from "./gitLifecycle.js";
6
- import { markWorkItemCompleted, readWorkItemLedger, writeWorkItemLedger } from "./ledger.js";
4
+ import { deleteBranch, removeRunWorktree, setUpRunWorktree, } from "./gitLifecycle.js";
5
+ import { markWorkItemCompleted, readWorkItemLedger, writeWorkItemLedger, } from "./ledger.js";
7
6
  import { getNyxDir, relativeToProject } from "./paths.js";
8
- import { buildContextBlock, buildPhasePrompt, EXECUTION_PROMPT, GLOBAL_REVIEW_PROMPT, GLOBAL_REVISION_PROMPT, REVIEW_PROMPT, REVISION_PROMPT, SELECTION_PROMPT, truncateForPrompt } from "./prompts.js";
9
- import { runAgentPhase } from "./runPhase.js";
10
- import { GLOBAL_REVIEW_SCHEMA, REVIEW_SCHEMA, SELECTION_SCHEMA } from "./schemas.js";
11
- import { commitAll, commitsAhead, createPullRequest, pushBranch, rangeDiff, stageAllAndDiff } from "./scm.js";
12
- import { confirmWorkItemSelection } from "./selectionConfirmation.js";
7
+ import { buildContextBlock, buildPhasePrompt, EXECUTION_PROMPT, GLOBAL_REVIEW_PROMPT, GLOBAL_REVISION_PROMPT, REVIEW_PROMPT, REVISION_PROMPT, SELECTION_PROMPT, truncateForPrompt, } from "./prompts.js";
8
+ import { createRunReporter } from "./reporter.js";
9
+ import { runAgentPhase, } from "./runPhase.js";
10
+ import { GLOBAL_REVIEW_SCHEMA, REVIEW_SCHEMA, SELECTION_SCHEMA, } from "./schemas.js";
11
+ import { commitAll, commitsAhead, createPullRequest, pushBranch, rangeDiff, stageAllAndDiff, } from "./scm.js";
12
+ import { confirmWorkItemSelection, } from "./selectionConfirmation.js";
13
13
  import { createRunId } from "./time.js";
14
- import { filterAvailable, listGitHubIssues, resolveSelectedQueue } from "./workItems.js";
14
+ import { filterAvailable, listGitHubIssues, resolveSelectedQueue, } from "./workItems.js";
15
15
  const MAX_CANDIDATES = 50;
16
16
  const EXCERPT_CHARS = 800;
17
17
  export function defaultPipelineDependencies() {
@@ -20,7 +20,7 @@ export function defaultPipelineDependencies() {
20
20
  runPhase: runAgentPhase,
21
21
  pushBranch,
22
22
  createPullRequest,
23
- confirmSelection: confirmWorkItemSelection
23
+ confirmSelection: confirmWorkItemSelection,
24
24
  };
25
25
  }
26
26
  /**
@@ -41,20 +41,26 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
41
41
  const runId = createRunId();
42
42
  const runDir = path.join(nyxDir, "runs", runId);
43
43
  await ensureDir(runDir);
44
- console.log(pc.bold(`NyxAgent run ${runId}`));
45
- console.log(`Harness: ${harness} · model: ${config.model} · review: ${config.review}`);
44
+ const reporter = input.reporter ?? createRunReporter({ verbose: input.verbose ?? false });
45
+ reporter.heading(`NyxAgent run ${runId}`);
46
+ reporter.info(`Harness: ${harness} · model: ${config.model} · review: ${config.review}`);
47
+ reporter.detail(`Config: ${relativeToProject(projectRoot, configPath)}`);
48
+ reporter.detail(`Artifacts: ${relativeToProject(projectRoot, runDir)}`);
49
+ reporter.detail(`Tracker: ${config.tracker.repo}`);
46
50
  const ledger = await readWorkItemLedger(nyxDir);
51
+ reporter.detail(`Completed work items already in ledger: ${ledger.completed_work_item_keys.length}`);
47
52
  // 1. Selection runs read-only in the main checkout, before any branch exists.
48
53
  const candidates = filterAvailable({
49
54
  candidates: await deps.listIssues({
50
55
  repo: config.tracker.repo,
51
56
  maxCandidates: MAX_CANDIDATES,
52
- excerptChars: EXCERPT_CHARS
57
+ excerptChars: EXCERPT_CHARS,
53
58
  }),
54
- completedKeys: ledger.completed_work_item_keys
59
+ completedKeys: ledger.completed_work_item_keys,
55
60
  });
61
+ reporter.detail(`Available work item candidates: ${candidates.length}`);
56
62
  if (candidates.length === 0) {
57
- console.log("No open work items available. Nothing to do.");
63
+ reporter.info("No open work items available. Nothing to do.");
58
64
  return;
59
65
  }
60
66
  const proposed = await runSelection({
@@ -63,34 +69,36 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
63
69
  harness,
64
70
  config,
65
71
  candidates,
66
- runPhase: deps.runPhase
72
+ runPhase: deps.runPhase,
73
+ reporter,
67
74
  });
68
75
  if (proposed.length === 0) {
69
- console.log("Selection chose no work items. Nothing to do.");
76
+ reporter.info("Selection chose no work items. Nothing to do.");
70
77
  return;
71
78
  }
72
79
  const selected = await deps.confirmSelection({
73
80
  candidates,
74
81
  proposed,
75
82
  maxItems: config.max_iterations,
76
- autoAccept: input.autoAcceptSelection ?? false
83
+ autoAccept: input.autoAcceptSelection ?? false,
77
84
  });
78
85
  if (selected.length === 0) {
79
- console.log("No work items selected. Nothing to do.");
86
+ reporter.info("No work items selected. Nothing to do.");
80
87
  return;
81
88
  }
82
89
  const planned = selected.slice(0, config.max_iterations);
83
- console.log(`Selected ${planned.length} work item(s):`);
90
+ reporter.info(`Selected ${planned.length} work item(s):`);
84
91
  for (const item of planned) {
85
- console.log(` - ${item.title} (#${item.number})`);
92
+ reporter.info(` - ${item.title} (#${item.number})`);
86
93
  }
87
94
  // 2. One branch + worktree per run (created only now that there is work).
88
95
  const git = await setUpRunWorktree({
89
96
  projectRoot,
90
97
  runId,
91
- base: config.base_branch
98
+ base: config.base_branch,
92
99
  });
93
- console.log(`Branch ${git.branch} (base ${git.base})`);
100
+ reporter.info(`Branch ${git.branch} (base ${git.base})`);
101
+ reporter.detail(`Worktree: ${relativeToProject(projectRoot, git.worktree)}`);
94
102
  let success = false;
95
103
  let producedCommits = false;
96
104
  let currentLedger = ledger;
@@ -100,7 +108,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
100
108
  for (const [index, item] of planned.entries()) {
101
109
  const iterationNumber = index + 1;
102
110
  const iterationDir = path.join(runDir, "iterations", String(iterationNumber).padStart(3, "0"));
103
- console.log(pc.cyan(`\n[${iterationNumber}/${planned.length}] ${item.title} (#${item.number})`));
111
+ reporter.section(`\n[${iterationNumber}/${planned.length}] ${item.title} (#${item.number})`);
104
112
  await runExecution({
105
113
  iterationDir,
106
114
  item,
@@ -108,7 +116,8 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
108
116
  git,
109
117
  harness,
110
118
  config,
111
- runPhase: deps.runPhase
119
+ runPhase: deps.runPhase,
120
+ reporter,
112
121
  });
113
122
  if (config.review === "each" || config.review === "both") {
114
123
  await runReviewLoop({
@@ -118,22 +127,24 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
118
127
  harness,
119
128
  config,
120
129
  maxAttempts: config.review_max_attempts,
121
- runPhase: deps.runPhase
130
+ runPhase: deps.runPhase,
131
+ reporter,
122
132
  });
123
133
  }
124
134
  const { committed } = await commitAll({
125
135
  cwd: git.worktree,
126
- message: buildCommitMessage(item)
136
+ message: buildCommitMessage(item),
127
137
  });
128
138
  if (committed) {
129
139
  producedCommits = true;
140
+ reporter.detail(`Committed work item #${item.number}.`);
130
141
  }
131
142
  else {
132
- console.log(pc.yellow(" No changes to commit for this item."));
143
+ reporter.warn(" No changes to commit for this item.");
133
144
  }
134
145
  currentLedger = markWorkItemCompleted({
135
146
  ledger: currentLedger,
136
- workItem: item
147
+ workItem: item,
137
148
  });
138
149
  await writeWorkItemLedger(nyxDir, currentLedger);
139
150
  completed.push(item);
@@ -145,17 +156,20 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
145
156
  harness,
146
157
  config,
147
158
  maxAttempts: config.review_max_attempts,
148
- runPhase: deps.runPhase
159
+ runPhase: deps.runPhase,
160
+ reporter,
149
161
  });
150
162
  if (corrections) {
151
163
  producedCommits = true;
152
164
  }
153
165
  }
154
- if (!producedCommits || (await commitsAhead(git.worktree, git.base)) === 0) {
155
- console.log("\nRun produced no commits; skipping pull request.");
166
+ if (!producedCommits ||
167
+ (await commitsAhead(git.worktree, git.base)) === 0) {
168
+ reporter.info("\nRun produced no commits; skipping pull request.");
156
169
  success = true;
157
170
  return;
158
171
  }
172
+ reporter.detail(`Pushing branch ${git.branch}.`);
159
173
  await deps.pushBranch({ cwd: git.worktree, branch: git.branch });
160
174
  const prUrl = await deps.createPullRequest({
161
175
  cwd: git.worktree,
@@ -163,9 +177,9 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
163
177
  base: git.base,
164
178
  head: git.branch,
165
179
  title: buildPrTitle(completed),
166
- body: buildPrBody(completed)
180
+ body: buildPrBody(completed),
167
181
  });
168
- console.log(pc.green(`\nPull request opened: ${prUrl}`));
182
+ reporter.success(`\nPull request opened: ${prUrl}`);
169
183
  success = true;
170
184
  }
171
185
  catch (error) {
@@ -179,7 +193,8 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
179
193
  producedCommits,
180
194
  completed,
181
195
  config,
182
- deps
196
+ deps,
197
+ reporter,
183
198
  });
184
199
  throw error;
185
200
  }
@@ -211,14 +226,15 @@ async function salvageFailedRun(input) {
211
226
  }
212
227
  }
213
228
  if (ahead === 0) {
214
- console.log(pc.red(`\nRun failed. Branch ${input.git.branch} and worktree kept for debugging: ${location}`));
229
+ input.reporter.error(`\nRun failed. Branch ${input.git.branch} and worktree kept for debugging: ${location}`);
215
230
  return;
216
231
  }
217
232
  const reason = input.error instanceof Error ? input.error.message : String(input.error);
218
233
  try {
234
+ input.reporter.detail(`Pushing failed run branch ${input.git.branch}.`);
219
235
  await input.deps.pushBranch({
220
236
  cwd: input.git.worktree,
221
- branch: input.git.branch
237
+ branch: input.git.branch,
222
238
  });
223
239
  const url = await input.deps.createPullRequest({
224
240
  cwd: input.git.worktree,
@@ -227,17 +243,17 @@ async function salvageFailedRun(input) {
227
243
  head: input.git.branch,
228
244
  title: buildDraftPrTitle(input.completed),
229
245
  body: buildDraftPrBody(input.completed, reason),
230
- draft: true
246
+ draft: true,
231
247
  });
232
- console.log(pc.yellow(`\nRun failed, but the work was salvaged into a DRAFT pull request: ${url}`));
233
- console.log(pc.yellow(`Branch ${input.git.branch} and worktree kept: ${location}`));
248
+ input.reporter.warn(`\nRun failed, but the work was salvaged into a DRAFT pull request: ${url}`);
249
+ input.reporter.warn(`Branch ${input.git.branch} and worktree kept: ${location}`);
234
250
  }
235
251
  catch (salvageError) {
236
252
  const detail = salvageError instanceof Error
237
253
  ? salvageError.message
238
254
  : String(salvageError);
239
- console.log(pc.red(`\nRun failed, and salvaging the work into a draft pull request also failed: ${detail}`));
240
- console.log(pc.red(`Branch ${input.git.branch} and worktree kept for debugging: ${location}`));
255
+ input.reporter.error(`\nRun failed, and salvaging the work into a draft pull request also failed: ${detail}`);
256
+ input.reporter.error(`Branch ${input.git.branch} and worktree kept for debugging: ${location}`);
241
257
  }
242
258
  }
243
259
  async function runSelection(input) {
@@ -251,9 +267,9 @@ async function runSelection(input) {
251
267
  number: candidate.number,
252
268
  title: candidate.title,
253
269
  labels: candidate.labels,
254
- excerpt: candidate.excerpt
255
- }))
256
- ]
270
+ excerpt: candidate.excerpt,
271
+ })),
272
+ ],
257
273
  ]);
258
274
  const result = await input.runPhase({
259
275
  phaseId: "selection",
@@ -266,9 +282,10 @@ async function runSelection(input) {
266
282
  prompt: buildPhasePrompt({
267
283
  guidance: SELECTION_PROMPT,
268
284
  context,
269
- schema: SELECTION_SCHEMA
285
+ schema: SELECTION_SCHEMA,
270
286
  }),
271
- schema: SELECTION_SCHEMA
287
+ schema: SELECTION_SCHEMA,
288
+ reporter: input.reporter,
272
289
  });
273
290
  if (!result.ok) {
274
291
  throw new Error(result.error);
@@ -279,7 +296,7 @@ async function runSelection(input) {
279
296
  }
280
297
  const resolved = resolveSelectedQueue({
281
298
  keys: value.work_item_keys,
282
- candidates: input.candidates
299
+ candidates: input.candidates,
283
300
  });
284
301
  if (!resolved.ok) {
285
302
  throw new Error(`Selection produced an invalid queue: ${resolved.error}`);
@@ -291,7 +308,7 @@ async function runExecution(input) {
291
308
  ["Work item", workItemSummary(input.item)],
292
309
  ["Issue description", input.item.excerpt ?? "(no description provided)"],
293
310
  ["Working directory", input.git.worktree],
294
- ["Branch", `${input.git.branch} (base ${input.git.base})`]
311
+ ["Branch", `${input.git.branch} (base ${input.git.base})`],
295
312
  ]);
296
313
  const result = await input.runPhase({
297
314
  phaseId: "execution",
@@ -301,7 +318,8 @@ async function runExecution(input) {
301
318
  model: input.config.model,
302
319
  reasoning: input.config.reasoning_effort,
303
320
  capability: "write",
304
- prompt: buildPhasePrompt({ guidance: input.guidance, context })
321
+ prompt: buildPhasePrompt({ guidance: input.guidance, context }),
322
+ reporter: input.reporter,
305
323
  });
306
324
  if (!result.ok) {
307
325
  throw new Error(result.error);
@@ -322,17 +340,21 @@ async function runReviewLoop(input) {
322
340
  guidance: REVIEW_PROMPT,
323
341
  context: buildContextBlock([
324
342
  ["Work item", workItemSummary(input.item)],
325
- ["Uncommitted changes (diff)", truncateForPrompt(diff || "(no changes)")]
343
+ [
344
+ "Uncommitted changes (diff)",
345
+ truncateForPrompt(diff || "(no changes)"),
346
+ ],
326
347
  ]),
327
- schema: REVIEW_SCHEMA
348
+ schema: REVIEW_SCHEMA,
328
349
  }),
329
- schema: REVIEW_SCHEMA
350
+ schema: REVIEW_SCHEMA,
351
+ reporter: input.reporter,
330
352
  });
331
353
  if (!reviewResult.ok) {
332
354
  throw new Error(reviewResult.error);
333
355
  }
334
356
  const review = reviewResult.result;
335
- console.log(` review: ${review.outcome}`);
357
+ input.reporter.info(` review: ${review.outcome}`);
336
358
  if (review.outcome === "approved") {
337
359
  return;
338
360
  }
@@ -351,9 +373,10 @@ async function runReviewLoop(input) {
351
373
  guidance: REVISION_PROMPT,
352
374
  context: buildContextBlock([
353
375
  ["Work item", workItemSummary(input.item)],
354
- ["Required changes", review.required_changes ?? []]
355
- ])
356
- })
376
+ ["Required changes", review.required_changes ?? []],
377
+ ]),
378
+ }),
379
+ reporter: input.reporter,
357
380
  });
358
381
  if (!revision.ok) {
359
382
  throw new Error(revision.error);
@@ -378,18 +401,19 @@ async function runGlobalReviewLoop(input) {
378
401
  ["Run branch", `${input.git.branch} (base ${input.git.base})`],
379
402
  [
380
403
  "Combined run diff (base...HEAD)",
381
- truncateForPrompt(diff || "(no changes)")
382
- ]
404
+ truncateForPrompt(diff || "(no changes)"),
405
+ ],
383
406
  ]),
384
- schema: GLOBAL_REVIEW_SCHEMA
407
+ schema: GLOBAL_REVIEW_SCHEMA,
385
408
  }),
386
- schema: GLOBAL_REVIEW_SCHEMA
409
+ schema: GLOBAL_REVIEW_SCHEMA,
410
+ reporter: input.reporter,
387
411
  });
388
412
  if (!reviewResult.ok) {
389
413
  throw new Error(reviewResult.error);
390
414
  }
391
415
  const review = reviewResult.result;
392
- console.log(`global review: ${review.outcome}`);
416
+ input.reporter.info(`global review: ${review.outcome}`);
393
417
  if (review.outcome === "approved") {
394
418
  return committedCorrections;
395
419
  }
@@ -407,19 +431,21 @@ async function runGlobalReviewLoop(input) {
407
431
  prompt: buildPhasePrompt({
408
432
  guidance: GLOBAL_REVISION_PROMPT,
409
433
  context: buildContextBlock([
410
- ["Required changes", review.required_changes ?? []]
411
- ])
412
- })
434
+ ["Required changes", review.required_changes ?? []],
435
+ ]),
436
+ }),
437
+ reporter: input.reporter,
413
438
  });
414
439
  if (!revision.ok) {
415
440
  throw new Error(revision.error);
416
441
  }
417
442
  const { committed } = await commitAll({
418
443
  cwd: input.git.worktree,
419
- message: "Apply global review corrections"
444
+ message: "Apply global review corrections",
420
445
  });
421
446
  if (committed) {
422
447
  committedCorrections = true;
448
+ input.reporter.detail("Committed global review corrections.");
423
449
  }
424
450
  }
425
451
  return committedCorrections;
@@ -441,7 +467,7 @@ function workItemSummary(item) {
441
467
  title: item.title,
442
468
  locator: item.source.locator,
443
469
  url: item.url,
444
- labels: item.labels
470
+ labels: item.labels,
445
471
  };
446
472
  }
447
473
  function buildCommitMessage(item) {
@@ -454,7 +480,9 @@ function buildPrTitle(items) {
454
480
  return `NyxAgent: ${items.length} work items`;
455
481
  }
456
482
  function buildPrBody(items) {
457
- const list = items.map((item) => `- ${item.title} (#${item.number})`).join("\n");
483
+ const list = items
484
+ .map((item) => `- ${item.title} (#${item.number})`)
485
+ .join("\n");
458
486
  const closes = items.map((item) => `Closes #${item.number}`).join("\n");
459
487
  return [
460
488
  "Automated changes by NyxAgent.",
@@ -463,7 +491,7 @@ function buildPrBody(items) {
463
491
  "",
464
492
  list,
465
493
  "",
466
- closes
494
+ closes,
467
495
  ].join("\n");
468
496
  }
469
497
  function buildDraftPrTitle(items) {
@@ -477,7 +505,7 @@ function buildDraftPrBody(items, reason) {
477
505
  "",
478
506
  `**Why the run failed:** ${reason}`,
479
507
  "",
480
- buildPrBody(items)
508
+ buildPrBody(items),
481
509
  ].join("\n");
482
510
  }
483
511
  /** Render review `required_changes` as a bullet list to append to a failure message. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyxa/nyx-agent",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "A lightweight phase orchestrator for repeatedly launching coding agents with fresh context.",
5
5
  "type": "module",
6
6
  "repository": {