@nyxa/nyx-agent 0.4.1 → 0.6.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 +58 -9
- package/dist/cli.js +13 -16
- package/dist/commands/init.js +112 -462
- package/dist/commands/run.js +17 -3
- package/dist/commands/update.js +1 -0
- package/dist/config/loadConfig.js +17 -3
- package/dist/config/schema.js +29 -146
- package/dist/runtime/files.js +1 -0
- package/dist/runtime/git.js +1 -0
- package/dist/runtime/gitLifecycle.js +19 -57
- package/dist/runtime/harness.js +26 -0
- package/dist/runtime/ledger.js +1 -0
- package/dist/runtime/paths.js +1 -12
- package/dist/runtime/prompts.js +103 -0
- package/dist/runtime/runPhase.js +85 -254
- package/dist/runtime/runPipeline.js +479 -0
- package/dist/runtime/schemas.js +52 -0
- package/dist/runtime/scm.js +80 -0
- package/dist/runtime/time.js +1 -0
- package/dist/runtime/validateResult.js +2 -3
- package/dist/runtime/workItems.js +43 -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 -30
- 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,171 @@
|
|
|
1
|
-
|
|
1
|
+
/** `nyxagent init`: scaffolds .nyxagent/config.json, the editable execution prompt, and .gitignore entries. */
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { confirm, input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
3
|
+
import { input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
5
4
|
import pc from "picocolors";
|
|
5
|
+
import { harnessNames, reviewModes } from "../config/schema.js";
|
|
6
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
|
+
const DEFAULT_CODEX_MODEL = "gpt-5.5";
|
|
9
10
|
const GITIGNORE_MARKER = "# NyxAgent runtime artifacts";
|
|
11
|
+
// Everything under .nyxagent/ is NyxAgent's own plumbing. Ignoring it keeps it
|
|
12
|
+
// out of the agent's worktree checkout and out of the commits/diffs it reviews,
|
|
13
|
+
// so the agent can never accidentally edit (or review) NyxAgent's own files.
|
|
10
14
|
const GITIGNORE_ENTRIES = [
|
|
11
15
|
".nyxagent/runs/",
|
|
16
|
+
".nyxagent/worktrees/",
|
|
12
17
|
".nyxagent/state.json",
|
|
13
|
-
".nyxagent/
|
|
18
|
+
".nyxagent/config.json",
|
|
19
|
+
".nyxagent/config.toml",
|
|
20
|
+
".nyxagent/prompts/"
|
|
14
21
|
];
|
|
15
22
|
export async function initCommand(options, projectRoot = process.cwd()) {
|
|
16
23
|
const root = path.resolve(projectRoot);
|
|
17
24
|
const nyxDir = getNyxDir(root);
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
throw new Error(
|
|
25
|
+
const configPath = path.join(nyxDir, "config.json");
|
|
26
|
+
if ((await pathExists(configPath)) && !options.force) {
|
|
27
|
+
throw new Error('.nyxagent/config.json already exists. Use "nyxagent init --force" to overwrite it.');
|
|
21
28
|
}
|
|
22
|
-
const
|
|
23
|
-
const shouldCreateConfig = !options.missing || !(await pathExists(configPath));
|
|
24
|
-
const resolved = shouldCreateConfig
|
|
25
|
-
? await resolveInitOptions(options, root)
|
|
26
|
-
: undefined;
|
|
29
|
+
const resolved = await resolveInitOptions(options);
|
|
27
30
|
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);
|
|
31
|
+
await writeText(configPath, `${JSON.stringify(buildConfig(resolved), null, 2)}\n`);
|
|
32
|
+
const executionPromptPath = path.join(nyxDir, "prompts", "execution.md");
|
|
33
|
+
if (!(await pathExists(executionPromptPath))) {
|
|
34
|
+
await ensureDir(path.dirname(executionPromptPath));
|
|
35
|
+
await writeText(executionPromptPath, EXECUTION_PROMPT_FILE);
|
|
40
36
|
}
|
|
37
|
+
await ensureGitignoreEntries(root);
|
|
41
38
|
console.log(pc.green("NyxAgent initialized."));
|
|
42
39
|
console.log(`Config: ${relativeToProject(root, configPath)}`);
|
|
40
|
+
console.log(`Editable prompt: ${relativeToProject(root, executionPromptPath)}`);
|
|
43
41
|
}
|
|
44
|
-
async function resolveInitOptions(options
|
|
45
|
-
const harness = options.harness
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
async function resolveInitOptions(options) {
|
|
43
|
+
const harness = options.harness
|
|
44
|
+
? normalizeHarness(options.harness)
|
|
45
|
+
: await select({
|
|
46
|
+
message: "Default harness",
|
|
48
47
|
choices: [
|
|
49
48
|
{ name: "codex", value: "codex" },
|
|
50
|
-
{ name: "claude", value: "claude" }
|
|
51
|
-
{ name: "custom", value: "custom" }
|
|
49
|
+
{ name: "claude", value: "claude" }
|
|
52
50
|
]
|
|
53
|
-
})
|
|
51
|
+
});
|
|
54
52
|
const model = options.model ??
|
|
55
53
|
(await input({
|
|
56
54
|
message: "Model",
|
|
57
|
-
default: harness === "codex" ?
|
|
55
|
+
default: harness === "codex" ? DEFAULT_CODEX_MODEL : "",
|
|
56
|
+
validate: (value) => value.trim().length > 0 || "Model is required"
|
|
58
57
|
}));
|
|
59
|
-
const
|
|
58
|
+
const reasoning_effort = options.reasoningEffort ??
|
|
59
|
+
(await input({ message: "Reasoning effort", default: "medium" }));
|
|
60
|
+
const review = options.review
|
|
61
|
+
? normalizeReview(options.review)
|
|
62
|
+
: await select({
|
|
63
|
+
message: "Review strategy",
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: "After each task", value: "each" },
|
|
66
|
+
{ name: "After all tasks (global review)", value: "all" },
|
|
67
|
+
{ name: "Both per-task and global", value: "both" },
|
|
68
|
+
{ name: "No review", value: "none" }
|
|
69
|
+
],
|
|
70
|
+
default: "each"
|
|
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)" }));
|
|
84
|
+
validateRepository(repo);
|
|
85
|
+
const baseBranchInput = options.baseBranch ??
|
|
60
86
|
(await input({
|
|
61
|
-
message: "
|
|
62
|
-
default: "
|
|
87
|
+
message: "Base branch (blank = current branch at run time)",
|
|
88
|
+
default: ""
|
|
63
89
|
}));
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const parsedMaxIterations = options.maxIterations
|
|
67
|
-
? Number.parseInt(options.maxIterations, 10)
|
|
68
|
-
: undefined;
|
|
69
|
-
const maxIterations = parsedMaxIterations ??
|
|
90
|
+
const base_branch = baseBranchInput.trim() ? baseBranchInput.trim() : undefined;
|
|
91
|
+
const max_iterations = parseMaxIterations(options.maxIterations) ??
|
|
70
92
|
(await numberPrompt({
|
|
71
|
-
message: "Max
|
|
93
|
+
message: "Max work items per run",
|
|
72
94
|
default: 5,
|
|
73
95
|
required: true
|
|
74
96
|
}));
|
|
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) {
|
|
97
|
+
if (!Number.isInteger(max_iterations) || max_iterations <= 0) {
|
|
111
98
|
throw new Error("max iterations must be a positive integer");
|
|
112
99
|
}
|
|
113
100
|
return {
|
|
114
101
|
harness,
|
|
115
|
-
model,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
reviewMode,
|
|
123
|
-
pullRequest
|
|
102
|
+
model: model.trim(),
|
|
103
|
+
reasoning_effort: reasoning_effort.trim() || "medium",
|
|
104
|
+
review,
|
|
105
|
+
review_max_attempts,
|
|
106
|
+
repo,
|
|
107
|
+
base_branch,
|
|
108
|
+
max_iterations
|
|
124
109
|
};
|
|
125
110
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
function buildConfig(options) {
|
|
112
|
+
const config = {
|
|
113
|
+
harness: options.harness,
|
|
114
|
+
model: options.model,
|
|
115
|
+
reasoning_effort: options.reasoning_effort,
|
|
116
|
+
review: options.review,
|
|
117
|
+
tracker: { type: "github", repo: options.repo },
|
|
118
|
+
max_iterations: options.max_iterations
|
|
119
|
+
};
|
|
120
|
+
// No point persisting an attempts cap when reviews are disabled.
|
|
121
|
+
if (options.review !== "none") {
|
|
122
|
+
config.review_max_attempts = options.review_max_attempts;
|
|
133
123
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
choices: [
|
|
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;
|
|
124
|
+
if (options.base_branch) {
|
|
125
|
+
config.base_branch = options.base_branch;
|
|
157
126
|
}
|
|
158
|
-
|
|
127
|
+
return config;
|
|
159
128
|
}
|
|
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;
|
|
129
|
+
function normalizeHarness(value) {
|
|
130
|
+
if (harnessNames.includes(value)) {
|
|
131
|
+
return value;
|
|
171
132
|
}
|
|
172
|
-
|
|
173
|
-
message: "Skill name (as you invoke it, e.g. /tdd)"
|
|
174
|
-
})).trim();
|
|
175
|
-
return skill.length > 0 ? skill : undefined;
|
|
133
|
+
throw new Error(`harness must be one of: ${harnessNames.join(", ")}`);
|
|
176
134
|
}
|
|
177
|
-
function
|
|
178
|
-
if (
|
|
179
|
-
return
|
|
135
|
+
function normalizeReview(value) {
|
|
136
|
+
if (reviewModes.includes(value)) {
|
|
137
|
+
return value;
|
|
180
138
|
}
|
|
181
|
-
|
|
182
|
-
return source;
|
|
183
|
-
}
|
|
184
|
-
throw new Error('work item source must be "local" or "github"');
|
|
139
|
+
throw new Error(`review must be one of: ${reviewModes.join(", ")}`);
|
|
185
140
|
}
|
|
186
|
-
function
|
|
187
|
-
if (
|
|
141
|
+
function validateRepository(repo) {
|
|
142
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
188
143
|
throw new Error('GitHub repository must use "owner/repo"');
|
|
189
144
|
}
|
|
190
145
|
}
|
|
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));
|
|
146
|
+
function parseMaxIterations(value) {
|
|
147
|
+
if (value === undefined) {
|
|
148
|
+
return undefined;
|
|
209
149
|
}
|
|
150
|
+
return Number.parseInt(value, 10);
|
|
210
151
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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}`);
|
|
152
|
+
function parseReviewAttempts(value) {
|
|
153
|
+
if (value === undefined) {
|
|
154
|
+
return undefined;
|
|
219
155
|
}
|
|
156
|
+
return Number.parseInt(value, 10);
|
|
220
157
|
}
|
|
221
158
|
async function ensureGitignoreEntries(root) {
|
|
222
159
|
const gitignorePath = path.join(root, ".gitignore");
|
|
223
160
|
const current = (await pathExists(gitignorePath))
|
|
224
161
|
? await readText(gitignorePath)
|
|
225
162
|
: "";
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
if (missingEntries.length === 0) {
|
|
163
|
+
const existing = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
164
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !existing.has(entry));
|
|
165
|
+
if (missing.length === 0) {
|
|
230
166
|
return;
|
|
231
167
|
}
|
|
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, '\\"');
|
|
168
|
+
const prefix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
169
|
+
const block = `${prefix}${current.length === 0 ? "" : "\n"}${GITIGNORE_MARKER}\n${missing.join("\n")}\n`;
|
|
170
|
+
await writeText(gitignorePath, `${current}${block}`);
|
|
521
171
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
/** `nyxagent run`: normalizes CLI options and hands off to the pipeline runner. */
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
+
import { harnessNames } from "../config/schema.js";
|
|
4
|
+
import { runPipeline } from "../runtime/runPipeline.js";
|
|
3
5
|
export async function runCommand(options, projectRoot = process.cwd()) {
|
|
4
|
-
await
|
|
6
|
+
await runPipeline({
|
|
5
7
|
projectRoot,
|
|
6
|
-
configPath: options.config
|
|
8
|
+
configPath: options.config
|
|
9
|
+
? path.resolve(projectRoot, options.config)
|
|
10
|
+
: undefined,
|
|
11
|
+
harness: normalizeHarness(options.harness)
|
|
7
12
|
});
|
|
8
13
|
}
|
|
14
|
+
function normalizeHarness(value) {
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (harnessNames.includes(value)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`--harness must be one of: ${harnessNames.join(", ")}`);
|
|
22
|
+
}
|
package/dist/commands/update.js
CHANGED