@nyxa/nyx-agent 0.3.5 → 0.4.1
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 +17 -0
- package/dist/commands/init.js +248 -41
- 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 +188 -3
- package/package.json +1 -1
- package/templates/default/prompts/closure.md +27 -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();
|
|
@@ -22,6 +23,12 @@ program
|
|
|
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
|
}
|
|
@@ -53,6 +61,8 @@ async function resolveInitOptions(options, root) {
|
|
|
53
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,43 +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
|
-
|
|
356
|
+
next = "${executionNext}"
|
|
357
|
+
max_visits_per_iteration = 1
|
|
358
|
+
${reviewPhases}
|
|
230
359
|
[[phases]]
|
|
231
|
-
id = "
|
|
232
|
-
prompt = "prompts/
|
|
233
|
-
output_schema = "schemas/
|
|
360
|
+
id = "closure"
|
|
361
|
+
prompt = "prompts/closure.md"
|
|
362
|
+
output_schema = "schemas/closure.schema.json"
|
|
234
363
|
required_output = true
|
|
235
|
-
max_visits_per_iteration =
|
|
364
|
+
max_visits_per_iteration = 1
|
|
236
365
|
|
|
237
366
|
[phases.harness]
|
|
238
|
-
args = ${formatTomlArray(
|
|
367
|
+
args = ${formatTomlArray(buildHarnessArgs(options.harness, "write_network"))}
|
|
239
368
|
|
|
240
369
|
[phases.transitions]
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
[[phases]]
|
|
245
|
-
id = "closure"
|
|
246
|
-
prompt = "prompts/closure.md"
|
|
247
|
-
next = "next_iteration"
|
|
248
|
-
max_visits_per_iteration = 1
|
|
249
|
-
`;
|
|
370
|
+
closed = "next_iteration"
|
|
371
|
+
failed = "stop_run"
|
|
372
|
+
${pullRequestPhase}${globalReviewPhases}${finalizePhase}`;
|
|
250
373
|
}
|
|
251
374
|
function buildHarnessToml(harness) {
|
|
252
375
|
if (harness === "codex") {
|
|
253
376
|
return `[harness]
|
|
254
377
|
preset = "codex"
|
|
255
378
|
command = "codex"
|
|
256
|
-
args = ${formatTomlArray(
|
|
379
|
+
args = ${formatTomlArray(buildHarnessArgs("codex", "write"))}
|
|
257
380
|
prompt_input = "stdin"`;
|
|
258
381
|
}
|
|
259
382
|
if (harness === "claude") {
|
|
260
383
|
return `[harness]
|
|
261
384
|
preset = "claude"
|
|
262
385
|
command = "claude"
|
|
263
|
-
args =
|
|
386
|
+
args = ${formatTomlArray(buildHarnessArgs("claude", "write"))}
|
|
264
387
|
prompt_input = "stdin"`;
|
|
265
388
|
}
|
|
266
389
|
return `[harness]
|
|
@@ -269,28 +392,112 @@ command = "your-agent-command"
|
|
|
269
392
|
args = []
|
|
270
393
|
prompt_input = "stdin"`;
|
|
271
394
|
}
|
|
272
|
-
|
|
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) {
|
|
273
408
|
if (harness === "codex") {
|
|
274
|
-
|
|
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;
|
|
275
424
|
}
|
|
276
425
|
if (harness === "claude") {
|
|
277
|
-
|
|
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;
|
|
278
434
|
}
|
|
279
435
|
return [];
|
|
280
436
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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"))}`;
|
|
294
501
|
}
|
|
295
502
|
function buildWorkItemsToml(options) {
|
|
296
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",
|