@nyxa/nyx-agent 0.3.4 → 0.4.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 +18 -1
- package/dist/commands/init.js +249 -48
- package/dist/commands/update.js +148 -0
- package/dist/config/schema.js +19 -0
- package/dist/runtime/buildPrompt.js +12 -50
- package/dist/runtime/gitLifecycle.js +109 -0
- package/dist/runtime/runPhase.js +8 -6
- package/dist/runtime/runWorkflow.js +157 -3
- package/docs/nyxagent-v0-spec.md +193 -10
- package/package.json +1 -1
- package/templates/default/prompts/closure.md +22 -8
- package/templates/default/prompts/finalize.md +7 -0
- package/templates/default/prompts/global-review.md +24 -0
- package/templates/default/prompts/global-revision.md +9 -0
- package/templates/default/prompts/pull-request.md +23 -0
- package/templates/default/prompts/revision.md +7 -0
- package/templates/default/schemas/closure.schema.json +35 -0
- package/templates/default/schemas/global-review.schema.json +60 -0
- package/templates/default/schemas/pull-request.schema.json +44 -0
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { initCommand } from "./commands/init.js";
|
|
6
6
|
import { runCommand } from "./commands/run.js";
|
|
7
|
+
import { updateCommand } from "./commands/update.js";
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
const packageJson = require("../package.json");
|
|
9
10
|
const program = new Command();
|
|
@@ -17,11 +18,17 @@ program
|
|
|
17
18
|
.option("--missing", "only add missing template files")
|
|
18
19
|
.option("--harness <preset>", "harness preset: codex, claude, or custom")
|
|
19
20
|
.option("--model <name>", "model name")
|
|
20
|
-
.option("--reasoning-level <level>", "harness-neutral reasoning level")
|
|
21
|
+
.option("--reasoning-level <level>", "default harness-neutral reasoning level")
|
|
21
22
|
.option("--max-iterations <count>", "maximum work items per run")
|
|
22
23
|
.option("--work-items-source <source>", "work item source template: local or github")
|
|
23
24
|
.option("--work-items-path <path>", "local markdown work item directory")
|
|
24
25
|
.option("--work-items-repository <owner/repo>", "GitHub work item repository")
|
|
26
|
+
.option("--implementation-skill <name>", "implementation skill to reference in the execution prompt")
|
|
27
|
+
.option("--review", "include a review phase (alias for --review-mode each)")
|
|
28
|
+
.option("--no-review", "skip the review phase (alias for --review-mode none)")
|
|
29
|
+
.option("--review-mode <mode>", "review strategy: each, all, both, or none")
|
|
30
|
+
.option("--pull-request", "close the PRD with a pull request (GitHub work items)")
|
|
31
|
+
.option("--no-pull-request", "skip the pull request finalization phase")
|
|
25
32
|
.action(async (options) => {
|
|
26
33
|
await initCommand(options);
|
|
27
34
|
});
|
|
@@ -32,6 +39,16 @@ program
|
|
|
32
39
|
.action(async (options) => {
|
|
33
40
|
await runCommand(options);
|
|
34
41
|
});
|
|
42
|
+
program
|
|
43
|
+
.command("update")
|
|
44
|
+
.description("Update nyxagent to the latest published version")
|
|
45
|
+
.argument("[version]", "version or dist-tag to install (default: latest)")
|
|
46
|
+
.option("--check", "only check for a newer version, do not install")
|
|
47
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
48
|
+
.option("--package-manager <pm>", "force a package manager: npm, pnpm, yarn, or bun")
|
|
49
|
+
.action(async (version, options) => {
|
|
50
|
+
await updateCommand(version, options);
|
|
51
|
+
});
|
|
35
52
|
await program.parseAsync().catch((error) => {
|
|
36
53
|
const message = error instanceof Error ? error.message : String(error);
|
|
37
54
|
console.error(pc.red(`Error: ${message}`));
|
package/dist/commands/init.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
4
|
+
import { confirm, input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
import { ensureDir, pathExists, readText, writeText } from "../runtime/files.js";
|
|
7
7
|
import { getNyxDir, relativeToProject } from "../runtime/paths.js";
|
|
8
8
|
const DEFAULT_OPENAI_MODEL = "gpt-5.5";
|
|
9
9
|
const GITIGNORE_MARKER = "# NyxAgent runtime artifacts";
|
|
10
|
-
const GITIGNORE_ENTRIES = [
|
|
10
|
+
const GITIGNORE_ENTRIES = [
|
|
11
|
+
".nyxagent/runs/",
|
|
12
|
+
".nyxagent/state.json",
|
|
13
|
+
".nyxagent/worktrees/"
|
|
14
|
+
];
|
|
11
15
|
export async function initCommand(options, projectRoot = process.cwd()) {
|
|
12
16
|
const root = path.resolve(projectRoot);
|
|
13
17
|
const nyxDir = getNyxDir(root);
|
|
@@ -27,6 +31,10 @@ export async function initCommand(options, projectRoot = process.cwd()) {
|
|
|
27
31
|
if (resolved) {
|
|
28
32
|
await writeText(configPath, buildConfigToml(resolved));
|
|
29
33
|
}
|
|
34
|
+
if (resolved?.implementationSkill) {
|
|
35
|
+
const executionPromptPath = path.join(nyxDir, "prompts", "execution.md");
|
|
36
|
+
await writeText(executionPromptPath, buildSkillExecutionPrompt(resolved.implementationSkill));
|
|
37
|
+
}
|
|
30
38
|
if (resolved?.workItemsSource === "local" && resolved.workItemsPath) {
|
|
31
39
|
await ensureWorkItemsDirectory(root, resolved.workItemsPath);
|
|
32
40
|
}
|
|
@@ -50,9 +58,11 @@ async function resolveInitOptions(options, root) {
|
|
|
50
58
|
}));
|
|
51
59
|
const reasoningLevel = options.reasoningLevel ??
|
|
52
60
|
(await input({
|
|
53
|
-
message: "
|
|
61
|
+
message: "Default reasoning level",
|
|
54
62
|
default: "medium"
|
|
55
63
|
}));
|
|
64
|
+
const implementationSkill = await resolveImplementationSkill(options);
|
|
65
|
+
const reviewMode = await resolveReviewMode(options);
|
|
56
66
|
const parsedMaxIterations = options.maxIterations
|
|
57
67
|
? Number.parseInt(options.maxIterations, 10)
|
|
58
68
|
: undefined;
|
|
@@ -92,6 +102,11 @@ async function resolveInitOptions(options, root) {
|
|
|
92
102
|
if (workItemsSource === "github") {
|
|
93
103
|
validateGitHubRepository(workItemsRepository);
|
|
94
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;
|
|
95
110
|
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
96
111
|
throw new Error("max iterations must be a positive integer");
|
|
97
112
|
}
|
|
@@ -102,9 +117,63 @@ async function resolveInitOptions(options, root) {
|
|
|
102
117
|
maxIterations,
|
|
103
118
|
workItemsSource,
|
|
104
119
|
workItemsPath,
|
|
105
|
-
workItemsRepository
|
|
120
|
+
workItemsRepository,
|
|
121
|
+
implementationSkill,
|
|
122
|
+
reviewMode,
|
|
123
|
+
pullRequest
|
|
106
124
|
};
|
|
107
125
|
}
|
|
126
|
+
async function resolveReviewMode(options) {
|
|
127
|
+
if (options.reviewMode !== undefined) {
|
|
128
|
+
return normalizeReviewMode(options.reviewMode);
|
|
129
|
+
}
|
|
130
|
+
// Backward compatibility with the boolean --review / --no-review flags.
|
|
131
|
+
if (options.review !== undefined) {
|
|
132
|
+
return options.review ? "each" : "none";
|
|
133
|
+
}
|
|
134
|
+
return select({
|
|
135
|
+
message: "Review strategy",
|
|
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;
|
|
157
|
+
}
|
|
158
|
+
throw new Error('review mode must be "each", "all", "both", or "none"');
|
|
159
|
+
}
|
|
160
|
+
async function resolveImplementationSkill(options) {
|
|
161
|
+
if (options.implementationSkill !== undefined) {
|
|
162
|
+
const provided = options.implementationSkill.trim();
|
|
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;
|
|
171
|
+
}
|
|
172
|
+
const skill = (await input({
|
|
173
|
+
message: "Skill name (as you invoke it, e.g. /tdd)"
|
|
174
|
+
})).trim();
|
|
175
|
+
return skill.length > 0 ? skill : undefined;
|
|
176
|
+
}
|
|
108
177
|
function normalizeWorkItemsSource(source) {
|
|
109
178
|
if (source === "local" || source === "local-markdown") {
|
|
110
179
|
return "local";
|
|
@@ -188,11 +257,71 @@ function getGitignoreAppendPrefix(current) {
|
|
|
188
257
|
function normalizeGitignoreContent(value) {
|
|
189
258
|
return `${value.replace(/\n*$/, "")}\n`;
|
|
190
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
|
+
}
|
|
191
267
|
function buildConfigToml(options) {
|
|
192
268
|
const harness = buildHarnessToml(options.harness);
|
|
193
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
|
+
: "";
|
|
194
323
|
return `[workflow]
|
|
195
|
-
entry_phase = "selection"
|
|
324
|
+
entry_phase = "selection"${finalPhaseLine}
|
|
196
325
|
max_iterations = ${options.maxIterations}
|
|
197
326
|
|
|
198
327
|
[model]
|
|
@@ -201,7 +330,7 @@ reasoning_level = "${escapeTomlString(options.reasoningLevel)}"
|
|
|
201
330
|
|
|
202
331
|
${harness}
|
|
203
332
|
|
|
204
|
-
[repair]
|
|
333
|
+
${gitSection}[repair]
|
|
205
334
|
max_attempts = 1
|
|
206
335
|
prompt = "prompts/repair-result.md"
|
|
207
336
|
|
|
@@ -215,7 +344,7 @@ required_output = true
|
|
|
215
344
|
max_visits_per_iteration = 1
|
|
216
345
|
|
|
217
346
|
[phases.harness]
|
|
218
|
-
args = ${formatTomlArray(
|
|
347
|
+
args = ${formatTomlArray(buildHarnessArgs(options.harness, "readonly"))}
|
|
219
348
|
|
|
220
349
|
[phases.transitions]
|
|
221
350
|
selected = "execution"
|
|
@@ -224,49 +353,37 @@ no_work = "stop_run"
|
|
|
224
353
|
[[phases]]
|
|
225
354
|
id = "execution"
|
|
226
355
|
prompt = "prompts/execution.md"
|
|
227
|
-
next = "
|
|
228
|
-
max_visits_per_iteration =
|
|
229
|
-
|
|
230
|
-
[phases.model]
|
|
231
|
-
reasoning_level = "high"
|
|
232
|
-
|
|
356
|
+
next = "${executionNext}"
|
|
357
|
+
max_visits_per_iteration = 1
|
|
358
|
+
${reviewPhases}
|
|
233
359
|
[[phases]]
|
|
234
|
-
id = "
|
|
235
|
-
prompt = "prompts/
|
|
236
|
-
output_schema = "schemas/
|
|
360
|
+
id = "closure"
|
|
361
|
+
prompt = "prompts/closure.md"
|
|
362
|
+
output_schema = "schemas/closure.schema.json"
|
|
237
363
|
required_output = true
|
|
238
|
-
max_visits_per_iteration =
|
|
239
|
-
|
|
240
|
-
[phases.model]
|
|
241
|
-
reasoning_level = "high"
|
|
364
|
+
max_visits_per_iteration = 1
|
|
242
365
|
|
|
243
366
|
[phases.harness]
|
|
244
|
-
args = ${formatTomlArray(
|
|
367
|
+
args = ${formatTomlArray(buildHarnessArgs(options.harness, "write_network"))}
|
|
245
368
|
|
|
246
369
|
[phases.transitions]
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
[[phases]]
|
|
251
|
-
id = "closure"
|
|
252
|
-
prompt = "prompts/closure.md"
|
|
253
|
-
next = "next_iteration"
|
|
254
|
-
max_visits_per_iteration = 1
|
|
255
|
-
`;
|
|
370
|
+
closed = "next_iteration"
|
|
371
|
+
failed = "stop_run"
|
|
372
|
+
${pullRequestPhase}${globalReviewPhases}${finalizePhase}`;
|
|
256
373
|
}
|
|
257
374
|
function buildHarnessToml(harness) {
|
|
258
375
|
if (harness === "codex") {
|
|
259
376
|
return `[harness]
|
|
260
377
|
preset = "codex"
|
|
261
378
|
command = "codex"
|
|
262
|
-
args = ${formatTomlArray(
|
|
379
|
+
args = ${formatTomlArray(buildHarnessArgs("codex", "write"))}
|
|
263
380
|
prompt_input = "stdin"`;
|
|
264
381
|
}
|
|
265
382
|
if (harness === "claude") {
|
|
266
383
|
return `[harness]
|
|
267
384
|
preset = "claude"
|
|
268
385
|
command = "claude"
|
|
269
|
-
args =
|
|
386
|
+
args = ${formatTomlArray(buildHarnessArgs("claude", "write"))}
|
|
270
387
|
prompt_input = "stdin"`;
|
|
271
388
|
}
|
|
272
389
|
return `[harness]
|
|
@@ -275,28 +392,112 @@ command = "your-agent-command"
|
|
|
275
392
|
args = []
|
|
276
393
|
prompt_input = "stdin"`;
|
|
277
394
|
}
|
|
278
|
-
|
|
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) {
|
|
279
408
|
if (harness === "codex") {
|
|
280
|
-
|
|
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;
|
|
281
424
|
}
|
|
282
425
|
if (harness === "claude") {
|
|
283
|
-
|
|
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;
|
|
284
434
|
}
|
|
285
435
|
return [];
|
|
286
436
|
}
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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"))}`;
|
|
300
501
|
}
|
|
301
502
|
function buildWorkItemsToml(options) {
|
|
302
503
|
if (options.workItemsSource === "local") {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { confirm } from "@inquirer/prompts";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
const PACKAGE_NAME = "@nyxa/nyx-agent";
|
|
7
|
+
export async function updateCommand(versionSpec, options, deps = defaultUpdateDependencies()) {
|
|
8
|
+
const spec = (versionSpec ?? "").trim() || "latest";
|
|
9
|
+
const current = deps.getCurrentVersion();
|
|
10
|
+
const target = await deps.resolveVersion(spec);
|
|
11
|
+
if (current === target) {
|
|
12
|
+
console.log(pc.green(`nyxagent is already up to date (${current}).`));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Only guard against downgrades for moving dist-tags like "latest"; an
|
|
16
|
+
// explicit version (e.g. "0.3.9") is treated as a deliberate pin.
|
|
17
|
+
const isDistTag = !/^\d/.test(spec);
|
|
18
|
+
if (isDistTag && compareVersions(current, target) > 0) {
|
|
19
|
+
console.log(pc.yellow(`Installed nyxagent ${current} is newer than ${spec} (${target}); nothing to do.`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const verb = compareVersions(target, current) >= 0 ? "Update" : "Downgrade";
|
|
23
|
+
console.log(`${verb} available: ${pc.dim(current)} -> ${pc.cyan(target)}`);
|
|
24
|
+
if (options.check) {
|
|
25
|
+
console.log(`Run ${pc.bold("nyxagent update")} to install it.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const packageManager = options.packageManager
|
|
29
|
+
? normalizePackageManager(options.packageManager)
|
|
30
|
+
: deps.detectPackageManager();
|
|
31
|
+
if (!options.yes) {
|
|
32
|
+
const confirmed = await deps.confirm(`${verb} nyxagent to ${target} with ${packageManager}?`);
|
|
33
|
+
if (!confirmed) {
|
|
34
|
+
console.log("Update cancelled.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log(`Installing ${PACKAGE_NAME}@${target} with ${packageManager}...`);
|
|
39
|
+
await deps.install({ packageManager, version: target });
|
|
40
|
+
console.log(pc.green(`nyxagent updated to ${target}. Run "nyxagent --version" to confirm.`));
|
|
41
|
+
}
|
|
42
|
+
function normalizePackageManager(value) {
|
|
43
|
+
if (value === "npm" ||
|
|
44
|
+
value === "pnpm" ||
|
|
45
|
+
value === "yarn" ||
|
|
46
|
+
value === "bun") {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
throw new Error('package manager must be "npm", "pnpm", "yarn", or "bun"');
|
|
50
|
+
}
|
|
51
|
+
export function defaultUpdateDependencies() {
|
|
52
|
+
return {
|
|
53
|
+
getCurrentVersion: readPackageVersion,
|
|
54
|
+
resolveVersion: resolveVersionFromRegistry,
|
|
55
|
+
install: runGlobalInstall,
|
|
56
|
+
detectPackageManager: detectPackageManagerFromPath,
|
|
57
|
+
confirm: confirmInteractive
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function readPackageVersion() {
|
|
61
|
+
const require = createRequire(import.meta.url);
|
|
62
|
+
const pkg = require("../../package.json");
|
|
63
|
+
return pkg.version;
|
|
64
|
+
}
|
|
65
|
+
async function resolveVersionFromRegistry(spec) {
|
|
66
|
+
const result = await execa("npm", ["view", `${PACKAGE_NAME}@${spec}`, "version"], { reject: false });
|
|
67
|
+
if (result.exitCode !== 0) {
|
|
68
|
+
const detail = (result.stderr || result.stdout || "unknown error").trim();
|
|
69
|
+
throw new Error(`Failed to query the npm registry: ${detail}`);
|
|
70
|
+
}
|
|
71
|
+
const lines = result.stdout
|
|
72
|
+
.trim()
|
|
73
|
+
.split(/\r?\n/)
|
|
74
|
+
.map((line) => line.trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
if (lines.length === 0) {
|
|
77
|
+
throw new Error(`No published version matches ${PACKAGE_NAME}@${spec}`);
|
|
78
|
+
}
|
|
79
|
+
// For an exact version or dist-tag npm prints a bare version; for a range it
|
|
80
|
+
// prints `<name>@<version> '<version>'` lines, so take the last match.
|
|
81
|
+
const last = lines[lines.length - 1];
|
|
82
|
+
const match = /([0-9][^\s'"]*)\s*$/.exec(last);
|
|
83
|
+
return match ? match[1] : last;
|
|
84
|
+
}
|
|
85
|
+
async function runGlobalInstall(input) {
|
|
86
|
+
const spec = `${PACKAGE_NAME}@${input.version}`;
|
|
87
|
+
await execa(input.packageManager, installArgs(input.packageManager, spec), {
|
|
88
|
+
stdio: "inherit"
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function installArgs(packageManager, spec) {
|
|
92
|
+
switch (packageManager) {
|
|
93
|
+
case "npm":
|
|
94
|
+
return ["install", "-g", spec];
|
|
95
|
+
case "pnpm":
|
|
96
|
+
return ["add", "-g", spec];
|
|
97
|
+
case "yarn":
|
|
98
|
+
return ["global", "add", spec];
|
|
99
|
+
case "bun":
|
|
100
|
+
return ["add", "-g", spec];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function detectPackageManagerFromPath() {
|
|
104
|
+
const here = fileURLToPath(import.meta.url).replace(/\\/g, "/").toLowerCase();
|
|
105
|
+
if (here.includes("/pnpm/") || here.includes("/.pnpm/")) {
|
|
106
|
+
return "pnpm";
|
|
107
|
+
}
|
|
108
|
+
if (here.includes("/.bun/") || here.includes("/bun/")) {
|
|
109
|
+
return "bun";
|
|
110
|
+
}
|
|
111
|
+
if (here.includes("/yarn/") || here.includes("/.config/yarn/")) {
|
|
112
|
+
return "yarn";
|
|
113
|
+
}
|
|
114
|
+
return "npm";
|
|
115
|
+
}
|
|
116
|
+
async function confirmInteractive(message) {
|
|
117
|
+
// In a non-interactive context the user already opted in by running the
|
|
118
|
+
// command, so proceed without a prompt that would otherwise fail.
|
|
119
|
+
if (!process.stdin.isTTY) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return confirm({ message, default: true });
|
|
123
|
+
}
|
|
124
|
+
function compareVersions(a, b) {
|
|
125
|
+
const pa = parseVersion(a);
|
|
126
|
+
const pb = parseVersion(b);
|
|
127
|
+
for (let i = 0; i < 3; i += 1) {
|
|
128
|
+
if (pa.core[i] !== pb.core[i]) {
|
|
129
|
+
return pa.core[i] - pb.core[i];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// A final release outranks a prerelease sharing the same core (1.0.0 > 1.0.0-rc).
|
|
133
|
+
if (pa.pre === pb.pre) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
if (pa.pre === "") {
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
if (pb.pre === "") {
|
|
140
|
+
return -1;
|
|
141
|
+
}
|
|
142
|
+
return pa.pre < pb.pre ? -1 : 1;
|
|
143
|
+
}
|
|
144
|
+
function parseVersion(value) {
|
|
145
|
+
const [main, pre = ""] = value.replace(/^v/, "").split("-", 2);
|
|
146
|
+
const parts = main.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
147
|
+
return { core: [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0], pre };
|
|
148
|
+
}
|
package/dist/config/schema.js
CHANGED
|
@@ -29,6 +29,15 @@ const phaseSchema = z
|
|
|
29
29
|
})
|
|
30
30
|
.passthrough();
|
|
31
31
|
const workItemsSourceSchema = z.preprocess((value) => (value === "local-markdown" ? "local" : value), z.enum(["local", "github"]));
|
|
32
|
+
const gitSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
mode: z.enum(["off", "branch", "worktree"]).default("off"),
|
|
35
|
+
base: z.string().min(1).optional(),
|
|
36
|
+
branch_template: z.string().min(1).default("nyxagent/{{run_id}}"),
|
|
37
|
+
worktree_dir: z.string().min(1).default(".nyxagent/worktrees"),
|
|
38
|
+
cleanup: z.enum(["always", "on_success", "never"]).default("on_success")
|
|
39
|
+
})
|
|
40
|
+
.passthrough();
|
|
32
41
|
const githubRepositoryPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
33
42
|
const workItemsSchema = z
|
|
34
43
|
.object({
|
|
@@ -68,10 +77,12 @@ export const nyxConfigSchema = z
|
|
|
68
77
|
.object({
|
|
69
78
|
workflow: z.object({
|
|
70
79
|
entry_phase: z.string().min(1),
|
|
80
|
+
final_phase: z.string().min(1).optional(),
|
|
71
81
|
max_iterations: z.number().int().positive()
|
|
72
82
|
}),
|
|
73
83
|
model: modelSchema,
|
|
74
84
|
harness: harnessSchema,
|
|
85
|
+
git: gitSchema.optional(),
|
|
75
86
|
repair: z
|
|
76
87
|
.object({
|
|
77
88
|
max_attempts: z.number().int().nonnegative().default(1),
|
|
@@ -110,6 +121,14 @@ export const nyxConfigSchema = z
|
|
|
110
121
|
message: `Unknown entry phase "${config.workflow.entry_phase}"`
|
|
111
122
|
});
|
|
112
123
|
}
|
|
124
|
+
if (config.workflow.final_phase &&
|
|
125
|
+
!phaseIds.has(config.workflow.final_phase)) {
|
|
126
|
+
ctx.addIssue({
|
|
127
|
+
code: "custom",
|
|
128
|
+
path: ["workflow", "final_phase"],
|
|
129
|
+
message: `Unknown final phase "${config.workflow.final_phase}"`
|
|
130
|
+
});
|
|
131
|
+
}
|
|
113
132
|
const reservedTargets = new Set([
|
|
114
133
|
"stop_run",
|
|
115
134
|
"stop_iteration",
|