@nyxa/nyx-agent 0.4.0 → 0.5.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 +52 -9
- package/dist/cli.js +11 -16
- package/dist/commands/init.js +87 -466
- package/dist/commands/run.js +16 -3
- package/dist/config/loadConfig.js +16 -3
- package/dist/config/schema.js +27 -146
- package/dist/runtime/gitLifecycle.js +19 -57
- package/dist/runtime/harness.js +26 -0
- package/dist/runtime/paths.js +0 -12
- package/dist/runtime/prompts.js +103 -0
- package/dist/runtime/runPhase.js +85 -254
- package/dist/runtime/runPipeline.js +395 -0
- package/dist/runtime/schemas.js +52 -0
- package/dist/runtime/scm.js +76 -0
- package/dist/runtime/validateResult.js +1 -3
- package/dist/runtime/workItems.js +42 -118
- package/package.json +2 -5
- package/dist/runtime/buildPrompt.js +0 -54
- package/dist/runtime/effectiveConfig.js +0 -14
- package/dist/runtime/renderTemplate.js +0 -28
- package/dist/runtime/runWorkflow.js +0 -680
- package/dist/runtime/validateWorkItem.js +0 -212
- package/dist/runtime/workItemAnnotations.js +0 -39
- package/docs/nyxagent-v0-spec.md +0 -742
- package/templates/default/prompts/closure.md +0 -25
- package/templates/default/prompts/execution.md +0 -11
- package/templates/default/prompts/finalize.md +0 -7
- package/templates/default/prompts/global-review.md +0 -24
- package/templates/default/prompts/global-revision.md +0 -9
- package/templates/default/prompts/pull-request.md +0 -23
- package/templates/default/prompts/repair-result.md +0 -29
- package/templates/default/prompts/review.md +0 -18
- package/templates/default/prompts/revision.md +0 -7
- package/templates/default/prompts/selection.md +0 -46
- package/templates/default/schemas/closure.schema.json +0 -35
- package/templates/default/schemas/global-review.schema.json +0 -60
- package/templates/default/schemas/pull-request.schema.json +0 -44
- package/templates/default/schemas/review.schema.json +0 -60
- package/templates/default/schemas/selection.schema.json +0 -135
package/dist/commands/init.js
CHANGED
|
@@ -1,521 +1,142 @@
|
|
|
1
|
-
import { readdir, stat } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { confirm, input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
2
|
+
import { input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
5
3
|
import pc from "picocolors";
|
|
4
|
+
import { harnessNames, reviewModes } from "../config/schema.js";
|
|
6
5
|
import { ensureDir, pathExists, readText, writeText } from "../runtime/files.js";
|
|
7
6
|
import { getNyxDir, relativeToProject } from "../runtime/paths.js";
|
|
8
|
-
|
|
7
|
+
import { EXECUTION_PROMPT_FILE } from "../runtime/prompts.js";
|
|
8
|
+
const DEFAULT_CODEX_MODEL = "gpt-5.5";
|
|
9
9
|
const GITIGNORE_MARKER = "# NyxAgent runtime artifacts";
|
|
10
10
|
const GITIGNORE_ENTRIES = [
|
|
11
11
|
".nyxagent/runs/",
|
|
12
|
-
".nyxagent/
|
|
13
|
-
".nyxagent/
|
|
12
|
+
".nyxagent/worktrees/",
|
|
13
|
+
".nyxagent/state.json"
|
|
14
14
|
];
|
|
15
15
|
export async function initCommand(options, projectRoot = process.cwd()) {
|
|
16
16
|
const root = path.resolve(projectRoot);
|
|
17
17
|
const nyxDir = getNyxDir(root);
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
throw new Error(
|
|
18
|
+
const configPath = path.join(nyxDir, "config.json");
|
|
19
|
+
if ((await pathExists(configPath)) && !options.force) {
|
|
20
|
+
throw new Error('.nyxagent/config.json already exists. Use "nyxagent init --force" to overwrite it.');
|
|
21
21
|
}
|
|
22
|
-
const
|
|
23
|
-
const shouldCreateConfig = !options.missing || !(await pathExists(configPath));
|
|
24
|
-
const resolved = shouldCreateConfig
|
|
25
|
-
? await resolveInitOptions(options, root)
|
|
26
|
-
: undefined;
|
|
22
|
+
const resolved = await resolveInitOptions(options);
|
|
27
23
|
await ensureDir(nyxDir);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
await writeText(
|
|
33
|
-
}
|
|
34
|
-
if (resolved?.implementationSkill) {
|
|
35
|
-
const executionPromptPath = path.join(nyxDir, "prompts", "execution.md");
|
|
36
|
-
await writeText(executionPromptPath, buildSkillExecutionPrompt(resolved.implementationSkill));
|
|
37
|
-
}
|
|
38
|
-
if (resolved?.workItemsSource === "local" && resolved.workItemsPath) {
|
|
39
|
-
await ensureWorkItemsDirectory(root, resolved.workItemsPath);
|
|
24
|
+
await writeText(configPath, `${JSON.stringify(buildConfig(resolved), null, 2)}\n`);
|
|
25
|
+
const executionPromptPath = path.join(nyxDir, "prompts", "execution.md");
|
|
26
|
+
if (!(await pathExists(executionPromptPath))) {
|
|
27
|
+
await ensureDir(path.dirname(executionPromptPath));
|
|
28
|
+
await writeText(executionPromptPath, EXECUTION_PROMPT_FILE);
|
|
40
29
|
}
|
|
30
|
+
await ensureGitignoreEntries(root);
|
|
41
31
|
console.log(pc.green("NyxAgent initialized."));
|
|
42
32
|
console.log(`Config: ${relativeToProject(root, configPath)}`);
|
|
33
|
+
console.log(`Editable prompt: ${relativeToProject(root, executionPromptPath)}`);
|
|
43
34
|
}
|
|
44
|
-
async function resolveInitOptions(options
|
|
45
|
-
const harness = options.harness
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
async function resolveInitOptions(options) {
|
|
36
|
+
const harness = options.harness
|
|
37
|
+
? normalizeHarness(options.harness)
|
|
38
|
+
: await select({
|
|
39
|
+
message: "Default harness",
|
|
48
40
|
choices: [
|
|
49
41
|
{ name: "codex", value: "codex" },
|
|
50
|
-
{ name: "claude", value: "claude" }
|
|
51
|
-
{ name: "custom", value: "custom" }
|
|
42
|
+
{ name: "claude", value: "claude" }
|
|
52
43
|
]
|
|
53
|
-
})
|
|
44
|
+
});
|
|
54
45
|
const model = options.model ??
|
|
55
46
|
(await input({
|
|
56
47
|
message: "Model",
|
|
57
|
-
default: harness === "codex" ?
|
|
48
|
+
default: harness === "codex" ? DEFAULT_CODEX_MODEL : "",
|
|
49
|
+
validate: (value) => value.trim().length > 0 || "Model is required"
|
|
58
50
|
}));
|
|
59
|
-
const
|
|
51
|
+
const reasoning_effort = options.reasoningEffort ??
|
|
52
|
+
(await input({ message: "Reasoning effort", default: "medium" }));
|
|
53
|
+
const review = options.review
|
|
54
|
+
? normalizeReview(options.review)
|
|
55
|
+
: await select({
|
|
56
|
+
message: "Review strategy",
|
|
57
|
+
choices: [
|
|
58
|
+
{ name: "After each task", value: "each" },
|
|
59
|
+
{ name: "After all tasks (global review)", value: "all" },
|
|
60
|
+
{ name: "Both per-task and global", value: "both" },
|
|
61
|
+
{ name: "No review", value: "none" }
|
|
62
|
+
],
|
|
63
|
+
default: "each"
|
|
64
|
+
});
|
|
65
|
+
const repo = options.repo ?? (await input({ message: "GitHub repository (owner/repo)" }));
|
|
66
|
+
validateRepository(repo);
|
|
67
|
+
const baseBranchInput = options.baseBranch ??
|
|
60
68
|
(await input({
|
|
61
|
-
message: "
|
|
62
|
-
default: "
|
|
69
|
+
message: "Base branch (blank = current branch at run time)",
|
|
70
|
+
default: ""
|
|
63
71
|
}));
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const parsedMaxIterations = options.maxIterations
|
|
67
|
-
? Number.parseInt(options.maxIterations, 10)
|
|
68
|
-
: undefined;
|
|
69
|
-
const maxIterations = parsedMaxIterations ??
|
|
72
|
+
const base_branch = baseBranchInput.trim() ? baseBranchInput.trim() : undefined;
|
|
73
|
+
const max_iterations = parseMaxIterations(options.maxIterations) ??
|
|
70
74
|
(await numberPrompt({
|
|
71
|
-
message: "Max
|
|
75
|
+
message: "Max work items per run",
|
|
72
76
|
default: 5,
|
|
73
77
|
required: true
|
|
74
78
|
}));
|
|
75
|
-
|
|
76
|
-
? normalizeWorkItemsSource(options.workItemsSource)
|
|
77
|
-
: await select({
|
|
78
|
-
message: "Work item source template",
|
|
79
|
-
choices: [
|
|
80
|
-
{ name: "local", value: "local" },
|
|
81
|
-
{ name: "github", value: "github" }
|
|
82
|
-
]
|
|
83
|
-
});
|
|
84
|
-
let workItemsPath = options.workItemsPath;
|
|
85
|
-
if (workItemsSource === "local" && !workItemsPath) {
|
|
86
|
-
const issuesPath = path.join(root, "issues");
|
|
87
|
-
workItemsPath = await input({
|
|
88
|
-
message: "Local work item path",
|
|
89
|
-
default: (await pathExists(issuesPath)) ? "issues" : ".nyxagent/tasks"
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (workItemsSource !== "local" && workItemsPath) {
|
|
93
|
-
throw new Error("--work-items-path can only be used with local work items");
|
|
94
|
-
}
|
|
95
|
-
let workItemsRepository = options.workItemsRepository;
|
|
96
|
-
if (workItemsSource === "github" && !workItemsRepository) {
|
|
97
|
-
workItemsRepository = await input({
|
|
98
|
-
message: "GitHub repository",
|
|
99
|
-
default: ""
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (workItemsSource === "github") {
|
|
103
|
-
validateGitHubRepository(workItemsRepository);
|
|
104
|
-
}
|
|
105
|
-
// GitHub PRDs are closed with a pull request by default (a dedicated branch +
|
|
106
|
-
// worktree). Opt out with --no-pull-request or by removing [git]/final_phase
|
|
107
|
-
// from the generated config. Pull requests are GitHub-specific, so local work
|
|
108
|
-
// item sources never get the finalization phase.
|
|
109
|
-
const pullRequest = workItemsSource === "github" ? options.pullRequest ?? true : false;
|
|
110
|
-
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
79
|
+
if (!Number.isInteger(max_iterations) || max_iterations <= 0) {
|
|
111
80
|
throw new Error("max iterations must be a positive integer");
|
|
112
81
|
}
|
|
113
82
|
return {
|
|
114
83
|
harness,
|
|
115
|
-
model,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
implementationSkill,
|
|
122
|
-
reviewMode,
|
|
123
|
-
pullRequest
|
|
84
|
+
model: model.trim(),
|
|
85
|
+
reasoning_effort: reasoning_effort.trim() || "medium",
|
|
86
|
+
review,
|
|
87
|
+
repo,
|
|
88
|
+
base_branch,
|
|
89
|
+
max_iterations
|
|
124
90
|
};
|
|
125
91
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
{
|
|
138
|
-
name: "After each task (review + correction inside the loop)",
|
|
139
|
-
value: "each"
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
name: "After all tasks (global review + correction)",
|
|
143
|
-
value: "all"
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
name: "Both (per-task and a final global review)",
|
|
147
|
-
value: "both"
|
|
148
|
-
},
|
|
149
|
-
{ name: "No review", value: "none" }
|
|
150
|
-
],
|
|
151
|
-
default: "each"
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
function normalizeReviewMode(mode) {
|
|
155
|
-
if (mode === "each" || mode === "all" || mode === "both" || mode === "none") {
|
|
156
|
-
return mode;
|
|
92
|
+
function buildConfig(options) {
|
|
93
|
+
const config = {
|
|
94
|
+
harness: options.harness,
|
|
95
|
+
model: options.model,
|
|
96
|
+
reasoning_effort: options.reasoning_effort,
|
|
97
|
+
review: options.review,
|
|
98
|
+
tracker: { type: "github", repo: options.repo },
|
|
99
|
+
max_iterations: options.max_iterations
|
|
100
|
+
};
|
|
101
|
+
if (options.base_branch) {
|
|
102
|
+
config.base_branch = options.base_branch;
|
|
157
103
|
}
|
|
158
|
-
|
|
104
|
+
return config;
|
|
159
105
|
}
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
return provided.length > 0 ? provided : undefined;
|
|
164
|
-
}
|
|
165
|
-
const useSkill = await confirm({
|
|
166
|
-
message: "Use an implementation skill?",
|
|
167
|
-
default: false
|
|
168
|
-
});
|
|
169
|
-
if (!useSkill) {
|
|
170
|
-
return undefined;
|
|
106
|
+
function normalizeHarness(value) {
|
|
107
|
+
if (harnessNames.includes(value)) {
|
|
108
|
+
return value;
|
|
171
109
|
}
|
|
172
|
-
|
|
173
|
-
message: "Skill name (as you invoke it, e.g. /tdd)"
|
|
174
|
-
})).trim();
|
|
175
|
-
return skill.length > 0 ? skill : undefined;
|
|
110
|
+
throw new Error(`harness must be one of: ${harnessNames.join(", ")}`);
|
|
176
111
|
}
|
|
177
|
-
function
|
|
178
|
-
if (
|
|
179
|
-
return
|
|
112
|
+
function normalizeReview(value) {
|
|
113
|
+
if (reviewModes.includes(value)) {
|
|
114
|
+
return value;
|
|
180
115
|
}
|
|
181
|
-
|
|
182
|
-
return source;
|
|
183
|
-
}
|
|
184
|
-
throw new Error('work item source must be "local" or "github"');
|
|
116
|
+
throw new Error(`review must be one of: ${reviewModes.join(", ")}`);
|
|
185
117
|
}
|
|
186
|
-
function
|
|
187
|
-
if (
|
|
118
|
+
function validateRepository(repo) {
|
|
119
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
188
120
|
throw new Error('GitHub repository must use "owner/repo"');
|
|
189
121
|
}
|
|
190
122
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
async function copyTemplateTree(sourceDir, destinationDir, missingOnly) {
|
|
196
|
-
await ensureDir(destinationDir);
|
|
197
|
-
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
198
|
-
for (const entry of entries) {
|
|
199
|
-
const source = path.join(sourceDir, entry.name);
|
|
200
|
-
const destination = path.join(destinationDir, entry.name);
|
|
201
|
-
if (entry.isDirectory()) {
|
|
202
|
-
await copyTemplateTree(source, destination, missingOnly);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (missingOnly && (await pathExists(destination))) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
await writeText(destination, await readText(source));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
async function ensureWorkItemsDirectory(root, taskPath) {
|
|
212
|
-
const absoluteTaskPath = path.resolve(root, taskPath);
|
|
213
|
-
if (!(await pathExists(absoluteTaskPath))) {
|
|
214
|
-
await ensureDir(absoluteTaskPath);
|
|
215
|
-
}
|
|
216
|
-
const taskPathStat = await stat(absoluteTaskPath);
|
|
217
|
-
if (!taskPathStat.isDirectory()) {
|
|
218
|
-
throw new Error(`Work item path is not a directory: ${taskPath}`);
|
|
123
|
+
function parseMaxIterations(value) {
|
|
124
|
+
if (value === undefined) {
|
|
125
|
+
return undefined;
|
|
219
126
|
}
|
|
127
|
+
return Number.parseInt(value, 10);
|
|
220
128
|
}
|
|
221
129
|
async function ensureGitignoreEntries(root) {
|
|
222
130
|
const gitignorePath = path.join(root, ".gitignore");
|
|
223
131
|
const current = (await pathExists(gitignorePath))
|
|
224
132
|
? await readText(gitignorePath)
|
|
225
133
|
: "";
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
if (missingEntries.length === 0) {
|
|
134
|
+
const existing = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
135
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !existing.has(entry));
|
|
136
|
+
if (missing.length === 0) {
|
|
230
137
|
return;
|
|
231
138
|
}
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const nextLines = [...lines];
|
|
236
|
-
nextLines.splice(markerIndex + 1, 0, ...missingEntries);
|
|
237
|
-
updated = normalizeGitignoreContent(nextLines.join("\n"));
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
const prefix = getGitignoreAppendPrefix(current);
|
|
241
|
-
updated = `${current}${prefix}${GITIGNORE_MARKER}\n${missingEntries.join("\n")}\n`;
|
|
242
|
-
}
|
|
243
|
-
await writeText(gitignorePath, updated);
|
|
244
|
-
}
|
|
245
|
-
function getGitignoreAppendPrefix(current) {
|
|
246
|
-
if (current.length === 0) {
|
|
247
|
-
return "";
|
|
248
|
-
}
|
|
249
|
-
if (current.endsWith("\n\n")) {
|
|
250
|
-
return "";
|
|
251
|
-
}
|
|
252
|
-
if (current.endsWith("\n")) {
|
|
253
|
-
return "\n";
|
|
254
|
-
}
|
|
255
|
-
return "\n\n";
|
|
256
|
-
}
|
|
257
|
-
function normalizeGitignoreContent(value) {
|
|
258
|
-
return `${value.replace(/\n*$/, "")}\n`;
|
|
259
|
-
}
|
|
260
|
-
function buildSkillExecutionPrompt(skill) {
|
|
261
|
-
return `Implement the selected work item using the ${skill} skill.
|
|
262
|
-
|
|
263
|
-
Do not commit. Do not close or mark the work item done. Leave clear validation
|
|
264
|
-
evidence in your final response.
|
|
265
|
-
`;
|
|
266
|
-
}
|
|
267
|
-
function buildConfigToml(options) {
|
|
268
|
-
const harness = buildHarnessToml(options.harness);
|
|
269
|
-
const workItems = buildWorkItemsToml(options);
|
|
270
|
-
const perTaskReview = options.reviewMode === "each" || options.reviewMode === "both";
|
|
271
|
-
const globalReview = options.reviewMode === "all" || options.reviewMode === "both";
|
|
272
|
-
const executionNext = perTaskReview ? "review" : "closure";
|
|
273
|
-
const reviewPhases = perTaskReview
|
|
274
|
-
? `
|
|
275
|
-
[[phases]]
|
|
276
|
-
id = "revision"
|
|
277
|
-
prompt = "prompts/revision.md"
|
|
278
|
-
next = "review"
|
|
279
|
-
max_visits_per_iteration = 3
|
|
280
|
-
|
|
281
|
-
[[phases]]
|
|
282
|
-
id = "review"
|
|
283
|
-
prompt = "prompts/review.md"
|
|
284
|
-
output_schema = "schemas/review.schema.json"
|
|
285
|
-
required_output = true
|
|
286
|
-
max_visits_per_iteration = 3
|
|
287
|
-
|
|
288
|
-
[phases.harness]
|
|
289
|
-
args = ${formatTomlArray(buildHarnessArgs(options.harness, "readonly"))}
|
|
290
|
-
|
|
291
|
-
[phases.transitions]
|
|
292
|
-
approved = "closure"
|
|
293
|
-
changes_requested = "revision"
|
|
294
|
-
`
|
|
295
|
-
: "";
|
|
296
|
-
// The finalization terminal leaf the global review approves into: a pushed
|
|
297
|
-
// pull request for GitHub, otherwise a read-only finalize phase.
|
|
298
|
-
const finalizationTerminal = options.pullRequest
|
|
299
|
-
? "pull_request"
|
|
300
|
-
: globalReview
|
|
301
|
-
? "finalize"
|
|
302
|
-
: undefined;
|
|
303
|
-
// The entry of the finalization flow: the global review when enabled, else
|
|
304
|
-
// the pull request (when configured), else nothing.
|
|
305
|
-
const finalPhaseEntry = globalReview
|
|
306
|
-
? "global_review"
|
|
307
|
-
: options.pullRequest
|
|
308
|
-
? "pull_request"
|
|
309
|
-
: undefined;
|
|
310
|
-
const finalPhaseLine = finalPhaseEntry
|
|
311
|
-
? `\nfinal_phase = "${finalPhaseEntry}"`
|
|
312
|
-
: "";
|
|
313
|
-
const gitSection = options.pullRequest ? `${buildGitToml()}\n\n` : "";
|
|
314
|
-
const pullRequestPhase = options.pullRequest
|
|
315
|
-
? `\n${buildPullRequestPhaseToml(options.harness)}\n`
|
|
316
|
-
: "";
|
|
317
|
-
const globalReviewPhases = globalReview
|
|
318
|
-
? `\n${buildGlobalReviewPhasesToml(options.harness, finalizationTerminal ?? "finalize")}\n`
|
|
319
|
-
: "";
|
|
320
|
-
const finalizePhase = globalReview && !options.pullRequest
|
|
321
|
-
? `\n${buildFinalizePhaseToml(options.harness)}\n`
|
|
322
|
-
: "";
|
|
323
|
-
return `[workflow]
|
|
324
|
-
entry_phase = "selection"${finalPhaseLine}
|
|
325
|
-
max_iterations = ${options.maxIterations}
|
|
326
|
-
|
|
327
|
-
[model]
|
|
328
|
-
name = "${escapeTomlString(options.model)}"
|
|
329
|
-
reasoning_level = "${escapeTomlString(options.reasoningLevel)}"
|
|
330
|
-
|
|
331
|
-
${harness}
|
|
332
|
-
|
|
333
|
-
${gitSection}[repair]
|
|
334
|
-
max_attempts = 1
|
|
335
|
-
prompt = "prompts/repair-result.md"
|
|
336
|
-
|
|
337
|
-
${workItems}
|
|
338
|
-
|
|
339
|
-
[[phases]]
|
|
340
|
-
id = "selection"
|
|
341
|
-
prompt = "prompts/selection.md"
|
|
342
|
-
output_schema = "schemas/selection.schema.json"
|
|
343
|
-
required_output = true
|
|
344
|
-
max_visits_per_iteration = 1
|
|
345
|
-
|
|
346
|
-
[phases.harness]
|
|
347
|
-
args = ${formatTomlArray(buildHarnessArgs(options.harness, "readonly"))}
|
|
348
|
-
|
|
349
|
-
[phases.transitions]
|
|
350
|
-
selected = "execution"
|
|
351
|
-
no_work = "stop_run"
|
|
352
|
-
|
|
353
|
-
[[phases]]
|
|
354
|
-
id = "execution"
|
|
355
|
-
prompt = "prompts/execution.md"
|
|
356
|
-
next = "${executionNext}"
|
|
357
|
-
max_visits_per_iteration = 1
|
|
358
|
-
${reviewPhases}
|
|
359
|
-
[[phases]]
|
|
360
|
-
id = "closure"
|
|
361
|
-
prompt = "prompts/closure.md"
|
|
362
|
-
output_schema = "schemas/closure.schema.json"
|
|
363
|
-
required_output = true
|
|
364
|
-
max_visits_per_iteration = 1
|
|
365
|
-
|
|
366
|
-
[phases.harness]
|
|
367
|
-
args = ${formatTomlArray(buildHarnessArgs(options.harness, "write_network"))}
|
|
368
|
-
|
|
369
|
-
[phases.transitions]
|
|
370
|
-
closed = "next_iteration"
|
|
371
|
-
failed = "stop_run"
|
|
372
|
-
${pullRequestPhase}${globalReviewPhases}${finalizePhase}`;
|
|
373
|
-
}
|
|
374
|
-
function buildHarnessToml(harness) {
|
|
375
|
-
if (harness === "codex") {
|
|
376
|
-
return `[harness]
|
|
377
|
-
preset = "codex"
|
|
378
|
-
command = "codex"
|
|
379
|
-
args = ${formatTomlArray(buildHarnessArgs("codex", "write"))}
|
|
380
|
-
prompt_input = "stdin"`;
|
|
381
|
-
}
|
|
382
|
-
if (harness === "claude") {
|
|
383
|
-
return `[harness]
|
|
384
|
-
preset = "claude"
|
|
385
|
-
command = "claude"
|
|
386
|
-
args = ${formatTomlArray(buildHarnessArgs("claude", "write"))}
|
|
387
|
-
prompt_input = "stdin"`;
|
|
388
|
-
}
|
|
389
|
-
return `[harness]
|
|
390
|
-
preset = "custom"
|
|
391
|
-
command = "your-agent-command"
|
|
392
|
-
args = []
|
|
393
|
-
prompt_input = "stdin"`;
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Build the harness CLI args for a phase capability. This is where the
|
|
397
|
-
* harness-specific network/permission posture lives, keeping the runtime
|
|
398
|
-
* engine itself agnostic (it only runs `command + args`).
|
|
399
|
-
*
|
|
400
|
-
* - readonly: no writes, no network (selection, review).
|
|
401
|
-
* - write: edit files locally (execution, revision).
|
|
402
|
-
* - write_network: edit files AND reach the network for `gh`/`git push`
|
|
403
|
-
* (closure, pull_request). This is the fix for issues never being closed:
|
|
404
|
-
* codex's default workspace-write sandbox blocks the network, so closing a
|
|
405
|
-
* GitHub issue silently failed.
|
|
406
|
-
*/
|
|
407
|
-
function buildHarnessArgs(harness, capability) {
|
|
408
|
-
if (harness === "codex") {
|
|
409
|
-
const args = [
|
|
410
|
-
"exec",
|
|
411
|
-
"--model",
|
|
412
|
-
"{{model.name}}",
|
|
413
|
-
"-c",
|
|
414
|
-
'model_reasoning_effort="{{model.reasoning_level}}"'
|
|
415
|
-
];
|
|
416
|
-
if (capability === "readonly") {
|
|
417
|
-
args.push("--sandbox", "read-only");
|
|
418
|
-
}
|
|
419
|
-
else if (capability === "write_network") {
|
|
420
|
-
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
421
|
-
}
|
|
422
|
-
args.push("-");
|
|
423
|
-
return args;
|
|
424
|
-
}
|
|
425
|
-
if (harness === "claude") {
|
|
426
|
-
const args = ["-p", "--model", "{{model.name}}", "--output-format", "text"];
|
|
427
|
-
if (capability === "readonly") {
|
|
428
|
-
args.push("--permission-mode", "plan");
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
args.push("--dangerously-skip-permissions");
|
|
432
|
-
}
|
|
433
|
-
return args;
|
|
434
|
-
}
|
|
435
|
-
return [];
|
|
436
|
-
}
|
|
437
|
-
function buildGitToml() {
|
|
438
|
-
return `[git]
|
|
439
|
-
mode = "worktree"
|
|
440
|
-
# base = "main" # defaults to the current branch
|
|
441
|
-
branch_template = "nyxagent/{{run_id}}"
|
|
442
|
-
worktree_dir = ".nyxagent/worktrees"
|
|
443
|
-
cleanup = "on_success"`;
|
|
444
|
-
}
|
|
445
|
-
function buildPullRequestPhaseToml(harness) {
|
|
446
|
-
return `[[phases]]
|
|
447
|
-
id = "pull_request"
|
|
448
|
-
prompt = "prompts/pull-request.md"
|
|
449
|
-
output_schema = "schemas/pull-request.schema.json"
|
|
450
|
-
required_output = true
|
|
451
|
-
max_visits_per_iteration = 1
|
|
452
|
-
|
|
453
|
-
[phases.harness]
|
|
454
|
-
args = ${formatTomlArray(buildHarnessArgs(harness, "write_network"))}`;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Build the run-level "global review + correction" finalization sub-graph:
|
|
458
|
-
*
|
|
459
|
-
* global_review --approved-----------> <terminal leaf>
|
|
460
|
-
* --changes_requested--> global_revision --> global_review
|
|
461
|
-
*
|
|
462
|
-
* `global_revision` relies on the default write harness (like the per-task
|
|
463
|
-
* `revision`/`execution` phases) and commits its own corrections, since no
|
|
464
|
-
* `closure` phase follows it before the terminal. `global_review` is read-only.
|
|
465
|
-
* The loop is bounded by max_visits_per_iteration = 3.
|
|
466
|
-
*/
|
|
467
|
-
function buildGlobalReviewPhasesToml(harness, terminal) {
|
|
468
|
-
return `[[phases]]
|
|
469
|
-
id = "global_revision"
|
|
470
|
-
prompt = "prompts/global-revision.md"
|
|
471
|
-
next = "global_review"
|
|
472
|
-
max_visits_per_iteration = 3
|
|
473
|
-
|
|
474
|
-
[[phases]]
|
|
475
|
-
id = "global_review"
|
|
476
|
-
prompt = "prompts/global-review.md"
|
|
477
|
-
output_schema = "schemas/global-review.schema.json"
|
|
478
|
-
required_output = true
|
|
479
|
-
max_visits_per_iteration = 3
|
|
480
|
-
|
|
481
|
-
[phases.harness]
|
|
482
|
-
args = ${formatTomlArray(buildHarnessArgs(harness, "readonly"))}
|
|
483
|
-
|
|
484
|
-
[phases.transitions]
|
|
485
|
-
approved = "${terminal}"
|
|
486
|
-
changes_requested = "global_revision"`;
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* The terminal leaf of a global-review finalization flow when there is no pull
|
|
490
|
-
* request to land on (local sources, or GitHub with --no-pull-request). It is
|
|
491
|
-
* read-only and makes no changes; it just confirms/summarizes the run.
|
|
492
|
-
*/
|
|
493
|
-
function buildFinalizePhaseToml(harness) {
|
|
494
|
-
return `[[phases]]
|
|
495
|
-
id = "finalize"
|
|
496
|
-
prompt = "prompts/finalize.md"
|
|
497
|
-
max_visits_per_iteration = 1
|
|
498
|
-
|
|
499
|
-
[phases.harness]
|
|
500
|
-
args = ${formatTomlArray(buildHarnessArgs(harness, "readonly"))}`;
|
|
501
|
-
}
|
|
502
|
-
function buildWorkItemsToml(options) {
|
|
503
|
-
if (options.workItemsSource === "local") {
|
|
504
|
-
return `[work_items]
|
|
505
|
-
source = "local"
|
|
506
|
-
path = "${escapeTomlString(options.workItemsPath ?? ".nyxagent/tasks")}"
|
|
507
|
-
max_candidates = 50
|
|
508
|
-
excerpt_chars = 800`;
|
|
509
|
-
}
|
|
510
|
-
return `[work_items]
|
|
511
|
-
source = "github"
|
|
512
|
-
repository = "${escapeTomlString(options.workItemsRepository ?? "")}"
|
|
513
|
-
max_candidates = 50
|
|
514
|
-
excerpt_chars = 800`;
|
|
515
|
-
}
|
|
516
|
-
function formatTomlArray(values) {
|
|
517
|
-
return `[${values.map((value) => `"${escapeTomlString(value)}"`).join(", ")}]`;
|
|
518
|
-
}
|
|
519
|
-
function escapeTomlString(value) {
|
|
520
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
139
|
+
const prefix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
140
|
+
const block = `${prefix}${current.length === 0 ? "" : "\n"}${GITIGNORE_MARKER}\n${missing.join("\n")}\n`;
|
|
141
|
+
await writeText(gitignorePath, `${current}${block}`);
|
|
521
142
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { harnessNames } from "../config/schema.js";
|
|
3
|
+
import { runPipeline } from "../runtime/runPipeline.js";
|
|
3
4
|
export async function runCommand(options, projectRoot = process.cwd()) {
|
|
4
|
-
await
|
|
5
|
+
await runPipeline({
|
|
5
6
|
projectRoot,
|
|
6
|
-
configPath: options.config
|
|
7
|
+
configPath: options.config
|
|
8
|
+
? path.resolve(projectRoot, options.config)
|
|
9
|
+
: undefined,
|
|
10
|
+
harness: normalizeHarness(options.harness)
|
|
7
11
|
});
|
|
8
12
|
}
|
|
13
|
+
function normalizeHarness(value) {
|
|
14
|
+
if (value === undefined) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (harnessNames.includes(value)) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`--harness must be one of: ${harnessNames.join(", ")}`);
|
|
21
|
+
}
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { parse } from "smol-toml";
|
|
3
2
|
import { nyxConfigSchema } from "./schema.js";
|
|
4
3
|
export async function loadConfig(configPath) {
|
|
5
4
|
const raw = await readFile(configPath, "utf8");
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(raw);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
throw new Error(`Invalid JSON in ${configPath}: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
const result = nyxConfigSchema.safeParse(parsed);
|
|
14
|
+
if (!result.success) {
|
|
15
|
+
const detail = result.error.issues
|
|
16
|
+
.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
|
|
17
|
+
.join("; ");
|
|
18
|
+
throw new Error(`Invalid NyxAgent config (${configPath}): ${detail}`);
|
|
19
|
+
}
|
|
20
|
+
return result.data;
|
|
8
21
|
}
|