@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 +1 -0
- package/dist/cli.js +1 -0
- package/dist/commands/run.js +2 -1
- package/dist/runtime/reporter.js +65 -0
- package/dist/runtime/runPhase.js +76 -15
- package/dist/runtime/runPipeline.js +98 -70
- package/package.json +1 -1
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
|
});
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/runtime/runPhase.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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:
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
+
reporter.info(`Selected ${planned.length} work item(s):`);
|
|
84
91
|
for (const item of planned) {
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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. */
|