@nyxa/nyx-agent 0.5.0 → 0.6.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 CHANGED
@@ -7,7 +7,8 @@ GitHub issue at a time, each phase with fresh context.
7
7
 
8
8
  For every run NyxAgent:
9
9
 
10
- 1. **Selects** open GitHub issues to work on (read-only).
10
+ 1. **Selects** open GitHub issues to work on (read-only), then asks the user to
11
+ confirm the proposed checklist.
11
12
  2. For each selected issue, in an isolated git **worktree**:
12
13
  - **implements** it (the agent — the only customizable prompt),
13
14
  - optionally **reviews** and **revises** it (bounded loop),
@@ -27,7 +28,8 @@ is editable.
27
28
 
28
29
  ```bash
29
30
  nyxagent init # create .nyxagent/config.json (interactive)
30
- nyxagent run # run the pipeline
31
+ nyxagent run # run the pipeline, confirming selected work items
32
+ nyxagent run --yes # accept the agent selection without prompting
31
33
  nyxagent run --harness claude # override the configured harness for one run
32
34
  nyxagent update # self-update to the latest published version
33
35
  ```
@@ -42,6 +44,7 @@ nyxagent update # self-update to the latest published version
42
44
  "model": "gpt-5.5",
43
45
  "reasoning_effort": "medium",
44
46
  "review": "each",
47
+ "review_max_attempts": 4,
45
48
  "tracker": { "type": "github", "repo": "owner/repo" },
46
49
  "base_branch": "main",
47
50
  "max_iterations": 5
@@ -50,10 +53,16 @@ nyxagent update # self-update to the latest published version
50
53
 
51
54
  - `harness`: `codex` or `claude` (override per run with `--harness`).
52
55
  - `review`: `each` (per task), `all` (global only), `both`, or `none`.
56
+ - `review_max_attempts`: review+revise rounds per stage before the run fails (default 4).
53
57
  - `base_branch`: optional; defaults to the current branch at run time.
54
58
 
59
+ If a run fails review after exhausting its attempts but has already produced
60
+ commits, NyxAgent pushes the branch and opens a **draft** pull request with the
61
+ unresolved feedback, so the work is never stranded on an orphaned branch.
62
+
55
63
  ## Requirements
56
64
 
57
65
  - A git repository with a GitHub remote.
58
66
  - The `gh` CLI authenticated for the configured repository.
59
67
  - The selected harness CLI (`codex` or `claude`) on `PATH`.
68
+ - An interactive terminal for `nyxagent run`, unless `--yes` is used.
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ /** CLI entry point: declares the `init`, `run`, and `update` commands and dispatches them. */
2
3
  import { Command } from "commander";
3
4
  import { createRequire } from "node:module";
4
5
  import pc from "picocolors";
@@ -19,6 +20,7 @@ program
19
20
  .option("--model <name>", "model name")
20
21
  .option("--reasoning-effort <level>", "reasoning effort (default: medium)")
21
22
  .option("--review <mode>", "review strategy: each, all, both, or none")
23
+ .option("--review-attempts <count>", "max review attempts per stage (default: 4)")
22
24
  .option("--repo <owner/repo>", "GitHub repository")
23
25
  .option("--base-branch <branch>", "base branch (default: current branch)")
24
26
  .option("--max-iterations <count>", "maximum work items per run")
@@ -31,6 +33,7 @@ program
31
33
  .description("Run the NyxAgent pipeline")
32
34
  .option("--config <path>", "config path (default: .nyxagent/config.json)")
33
35
  .option("--harness <name>", "override the configured harness: codex or claude")
36
+ .option("-y, --yes", "accept the agent-selected work items without prompting")
34
37
  .action(async (options) => {
35
38
  await runCommand(options);
36
39
  });
@@ -1,3 +1,4 @@
1
+ /** `nyxagent init`: scaffolds .nyxagent/config.json, the editable execution prompt, and .gitignore entries. */
1
2
  import path from "node:path";
2
3
  import { input, number as numberPrompt, select } from "@inquirer/prompts";
3
4
  import pc from "picocolors";
@@ -7,10 +8,16 @@ import { getNyxDir, relativeToProject } from "../runtime/paths.js";
7
8
  import { EXECUTION_PROMPT_FILE } from "../runtime/prompts.js";
8
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/",
12
16
  ".nyxagent/worktrees/",
13
- ".nyxagent/state.json"
17
+ ".nyxagent/state.json",
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);
@@ -62,6 +69,17 @@ async function resolveInitOptions(options) {
62
69
  ],
63
70
  default: "each"
64
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
+ }
65
83
  const repo = options.repo ?? (await input({ message: "GitHub repository (owner/repo)" }));
66
84
  validateRepository(repo);
67
85
  const baseBranchInput = options.baseBranch ??
@@ -84,6 +102,7 @@ async function resolveInitOptions(options) {
84
102
  model: model.trim(),
85
103
  reasoning_effort: reasoning_effort.trim() || "medium",
86
104
  review,
105
+ review_max_attempts,
87
106
  repo,
88
107
  base_branch,
89
108
  max_iterations
@@ -98,6 +117,10 @@ function buildConfig(options) {
98
117
  tracker: { type: "github", repo: options.repo },
99
118
  max_iterations: options.max_iterations
100
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;
123
+ }
101
124
  if (options.base_branch) {
102
125
  config.base_branch = options.base_branch;
103
126
  }
@@ -126,6 +149,12 @@ function parseMaxIterations(value) {
126
149
  }
127
150
  return Number.parseInt(value, 10);
128
151
  }
152
+ function parseReviewAttempts(value) {
153
+ if (value === undefined) {
154
+ return undefined;
155
+ }
156
+ return Number.parseInt(value, 10);
157
+ }
129
158
  async function ensureGitignoreEntries(root) {
130
159
  const gitignorePath = path.join(root, ".gitignore");
131
160
  const current = (await pathExists(gitignorePath))
@@ -1,3 +1,4 @@
1
+ /** `nyxagent run`: normalizes CLI options and hands off to the pipeline runner. */
1
2
  import path from "node:path";
2
3
  import { harnessNames } from "../config/schema.js";
3
4
  import { runPipeline } from "../runtime/runPipeline.js";
@@ -7,7 +8,8 @@ export async function runCommand(options, projectRoot = process.cwd()) {
7
8
  configPath: options.config
8
9
  ? path.resolve(projectRoot, options.config)
9
10
  : undefined,
10
- harness: normalizeHarness(options.harness)
11
+ harness: normalizeHarness(options.harness),
12
+ autoAcceptSelection: options.yes ?? false
11
13
  });
12
14
  }
13
15
  function normalizeHarness(value) {
@@ -1,3 +1,4 @@
1
+ /** `nyxagent update`: self-update command that resolves the target version from npm and installs it globally. */
1
2
  import { createRequire } from "node:module";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { confirm } from "@inquirer/prompts";
@@ -1,3 +1,4 @@
1
+ /** Reads, JSON-parses, and schema-validates a .nyxagent/config.json file. */
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { nyxConfigSchema } from "./schema.js";
3
4
  export async function loadConfig(configPath) {
@@ -18,6 +18,8 @@ export const nyxConfigSchema = z
18
18
  reasoning_effort: z.string().min(1).default("medium"),
19
19
  /** When the agent reviews its own work. */
20
20
  review: z.enum(reviewModes).default("each"),
21
+ /** How many review+revise rounds a review stage gets before the run fails. */
22
+ review_max_attempts: z.number().int().positive().default(4),
21
23
  /** Work item tracker. GitHub issues only in this version. */
22
24
  tracker: z.object({
23
25
  type: z.literal("github"),
@@ -1,3 +1,4 @@
1
+ /** Small filesystem helpers: ensure a directory, read/write text and JSON, check existence. */
1
2
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  export async function ensureDir(dir) {
@@ -1,3 +1,4 @@
1
+ /** Read-only git snapshot (branch, HEAD, short status) recorded around each phase for the audit trail. */
1
2
  import { execa } from "execa";
2
3
  export async function getGitSnapshot(cwd) {
3
4
  const isRepo = await execa("git", ["rev-parse", "--is-inside-work-tree"], {
@@ -1,3 +1,4 @@
1
+ /** Tracks which work items each run completed (.nyxagent/state.json) so they are skipped on later runs. */
1
2
  import { readFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { writeJson } from "./files.js";
@@ -1,3 +1,4 @@
1
+ /** Project path helpers: locate the .nyxagent directory and render project-relative paths. */
1
2
  import path from "node:path";
2
3
  export function getNyxDir(projectRoot) {
3
4
  return path.join(projectRoot, ".nyxagent");
@@ -9,17 +9,18 @@ import { buildContextBlock, buildPhasePrompt, EXECUTION_PROMPT, GLOBAL_REVIEW_PR
9
9
  import { runAgentPhase } from "./runPhase.js";
10
10
  import { GLOBAL_REVIEW_SCHEMA, REVIEW_SCHEMA, SELECTION_SCHEMA } from "./schemas.js";
11
11
  import { commitAll, commitsAhead, createPullRequest, pushBranch, rangeDiff, stageAllAndDiff } from "./scm.js";
12
+ import { confirmWorkItemSelection } from "./selectionConfirmation.js";
12
13
  import { createRunId } from "./time.js";
13
14
  import { filterAvailable, listGitHubIssues, resolveSelectedQueue } from "./workItems.js";
14
15
  const MAX_CANDIDATES = 50;
15
16
  const EXCERPT_CHARS = 800;
16
- const REVIEW_MAX_ATTEMPTS = 3;
17
17
  export function defaultPipelineDependencies() {
18
18
  return {
19
19
  listIssues: listGitHubIssues,
20
20
  runPhase: runAgentPhase,
21
21
  pushBranch,
22
- createPullRequest
22
+ createPullRequest,
23
+ confirmSelection: confirmWorkItemSelection
23
24
  };
24
25
  }
25
26
  /**
@@ -56,7 +57,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
56
57
  console.log("No open work items available. Nothing to do.");
57
58
  return;
58
59
  }
59
- const selected = await runSelection({
60
+ const proposed = await runSelection({
60
61
  projectRoot,
61
62
  runDir,
62
63
  harness,
@@ -64,10 +65,20 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
64
65
  candidates,
65
66
  runPhase: deps.runPhase
66
67
  });
67
- if (selected.length === 0) {
68
+ if (proposed.length === 0) {
68
69
  console.log("Selection chose no work items. Nothing to do.");
69
70
  return;
70
71
  }
72
+ const selected = await deps.confirmSelection({
73
+ candidates,
74
+ proposed,
75
+ maxItems: config.max_iterations,
76
+ autoAccept: input.autoAcceptSelection ?? false
77
+ });
78
+ if (selected.length === 0) {
79
+ console.log("No work items selected. Nothing to do.");
80
+ return;
81
+ }
71
82
  const planned = selected.slice(0, config.max_iterations);
72
83
  console.log(`Selected ${planned.length} work item(s):`);
73
84
  for (const item of planned) {
@@ -106,6 +117,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
106
117
  git,
107
118
  harness,
108
119
  config,
120
+ maxAttempts: config.review_max_attempts,
109
121
  runPhase: deps.runPhase
110
122
  });
111
123
  }
@@ -132,6 +144,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
132
144
  git,
133
145
  harness,
134
146
  config,
147
+ maxAttempts: config.review_max_attempts,
135
148
  runPhase: deps.runPhase
136
149
  });
137
150
  if (corrections) {
@@ -155,6 +168,21 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
155
168
  console.log(pc.green(`\nPull request opened: ${prUrl}`));
156
169
  success = true;
157
170
  }
171
+ catch (error) {
172
+ // A failed run that already produced commits is salvaged into a draft PR so
173
+ // the work is never stranded on an orphaned branch; the error still
174
+ // propagates so the exit code reflects the failure.
175
+ await salvageFailedRun({
176
+ error,
177
+ projectRoot,
178
+ git,
179
+ producedCommits,
180
+ completed,
181
+ config,
182
+ deps
183
+ });
184
+ throw error;
185
+ }
158
186
  finally {
159
187
  if (success) {
160
188
  await removeRunWorktree({ projectRoot, worktree: git.worktree });
@@ -162,9 +190,54 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
162
190
  await deleteBranch({ projectRoot, branch: git.branch });
163
191
  }
164
192
  }
165
- else {
166
- console.log(pc.red(`\nRun failed. Branch ${git.branch} and worktree kept for debugging: ${relativeToProject(projectRoot, git.worktree)}`));
193
+ }
194
+ }
195
+ /**
196
+ * Failure handling that preserves work. If the run produced commits, push the
197
+ * branch and open a DRAFT pull request describing why it failed, so a human can
198
+ * finish it. Otherwise just keep the branch/worktree for debugging. The branch
199
+ * and worktree are always kept on failure.
200
+ */
201
+ async function salvageFailedRun(input) {
202
+ const location = relativeToProject(input.projectRoot, input.git.worktree);
203
+ // Best-effort: never let salvage throw and mask the original failure.
204
+ let ahead = 0;
205
+ if (input.producedCommits) {
206
+ try {
207
+ ahead = await commitsAhead(input.git.worktree, input.git.base);
167
208
  }
209
+ catch {
210
+ ahead = 0;
211
+ }
212
+ }
213
+ if (ahead === 0) {
214
+ console.log(pc.red(`\nRun failed. Branch ${input.git.branch} and worktree kept for debugging: ${location}`));
215
+ return;
216
+ }
217
+ const reason = input.error instanceof Error ? input.error.message : String(input.error);
218
+ try {
219
+ await input.deps.pushBranch({
220
+ cwd: input.git.worktree,
221
+ branch: input.git.branch
222
+ });
223
+ const url = await input.deps.createPullRequest({
224
+ cwd: input.git.worktree,
225
+ repo: input.config.tracker.repo,
226
+ base: input.git.base,
227
+ head: input.git.branch,
228
+ title: buildDraftPrTitle(input.completed),
229
+ body: buildDraftPrBody(input.completed, reason),
230
+ draft: true
231
+ });
232
+ console.log(pc.yellow(`\nRun failed, but the work was salvaged into a DRAFT pull request: ${url}`));
233
+ console.log(pc.yellow(`Branch ${input.git.branch} and worktree kept: ${location}`));
234
+ }
235
+ catch (salvageError) {
236
+ const detail = salvageError instanceof Error
237
+ ? salvageError.message
238
+ : String(salvageError);
239
+ console.log(pc.red(`\nRun failed, and salvaging the work into a draft pull request also failed: ${detail}`));
240
+ console.log(pc.red(`Branch ${input.git.branch} and worktree kept for debugging: ${location}`));
168
241
  }
169
242
  }
170
243
  async function runSelection(input) {
@@ -235,7 +308,7 @@ async function runExecution(input) {
235
308
  }
236
309
  }
237
310
  async function runReviewLoop(input) {
238
- for (let attempt = 1; attempt <= REVIEW_MAX_ATTEMPTS; attempt += 1) {
311
+ for (let attempt = 1; attempt <= input.maxAttempts; attempt += 1) {
239
312
  const diff = await stageAllAndDiff(input.git.worktree);
240
313
  const reviewResult = await input.runPhase({
241
314
  phaseId: "review",
@@ -263,8 +336,8 @@ async function runReviewLoop(input) {
263
336
  if (review.outcome === "approved") {
264
337
  return;
265
338
  }
266
- if (attempt === REVIEW_MAX_ATTEMPTS) {
267
- throw new Error(`Review for #${input.item.number} not approved after ${REVIEW_MAX_ATTEMPTS} attempts: ${review.summary}`);
339
+ if (attempt === input.maxAttempts) {
340
+ throw new Error(`Review for #${input.item.number} not approved after ${input.maxAttempts} attempts: ${review.summary}${formatRequiredChanges(review.required_changes)}`);
268
341
  }
269
342
  const revision = await input.runPhase({
270
343
  phaseId: "revision",
@@ -289,7 +362,7 @@ async function runReviewLoop(input) {
289
362
  }
290
363
  async function runGlobalReviewLoop(input) {
291
364
  let committedCorrections = false;
292
- for (let attempt = 1; attempt <= REVIEW_MAX_ATTEMPTS; attempt += 1) {
365
+ for (let attempt = 1; attempt <= input.maxAttempts; attempt += 1) {
293
366
  const diff = await rangeDiff(input.git.worktree, input.git.base);
294
367
  const reviewResult = await input.runPhase({
295
368
  phaseId: "global_review",
@@ -320,8 +393,8 @@ async function runGlobalReviewLoop(input) {
320
393
  if (review.outcome === "approved") {
321
394
  return committedCorrections;
322
395
  }
323
- if (attempt === REVIEW_MAX_ATTEMPTS) {
324
- throw new Error(`Global review not approved after ${REVIEW_MAX_ATTEMPTS} attempts: ${review.summary}`);
396
+ if (attempt === input.maxAttempts) {
397
+ throw new Error(`Global review not approved after ${input.maxAttempts} attempts: ${review.summary}${formatRequiredChanges(review.required_changes)}`);
325
398
  }
326
399
  const revision = await input.runPhase({
327
400
  phaseId: "global_revision",
@@ -393,3 +466,26 @@ function buildPrBody(items) {
393
466
  closes
394
467
  ].join("\n");
395
468
  }
469
+ function buildDraftPrTitle(items) {
470
+ return `[Draft] ${buildPrTitle(items)}`;
471
+ }
472
+ function buildDraftPrBody(items, reason) {
473
+ return [
474
+ "> [!WARNING]",
475
+ "> This pull request was opened automatically by NyxAgent after the run",
476
+ "> **failed review**. The work is preserved here for a human to finish.",
477
+ "",
478
+ `**Why the run failed:** ${reason}`,
479
+ "",
480
+ buildPrBody(items)
481
+ ].join("\n");
482
+ }
483
+ /** Render review `required_changes` as a bullet list to append to a failure message. */
484
+ function formatRequiredChanges(changes) {
485
+ if (!changes || changes.length === 0) {
486
+ return "";
487
+ }
488
+ return `\n\nUnresolved review feedback:\n${changes
489
+ .map((change) => `- ${change}`)
490
+ .join("\n")}`;
491
+ }
@@ -54,7 +54,7 @@ export async function pushBranch(input) {
54
54
  await git(input.cwd, ["push", "-u", "origin", input.branch], "push");
55
55
  }
56
56
  export async function createPullRequest(input) {
57
- const result = await execa("gh", [
57
+ const args = [
58
58
  "pr",
59
59
  "create",
60
60
  "--repo",
@@ -67,7 +67,11 @@ export async function createPullRequest(input) {
67
67
  input.title,
68
68
  "--body",
69
69
  input.body
70
- ], { cwd: input.cwd, reject: false });
70
+ ];
71
+ if (input.draft) {
72
+ args.push("--draft");
73
+ }
74
+ const result = await execa("gh", args, { cwd: input.cwd, reject: false });
71
75
  if (result.exitCode !== 0) {
72
76
  const detail = (result.stderr || result.stdout || "unknown error").trim();
73
77
  throw new Error(`gh pr create failed: ${detail}`);
@@ -0,0 +1,85 @@
1
+ import { checkbox, Separator } from "@inquirer/prompts";
2
+ export async function confirmWorkItemSelection(input) {
3
+ if (input.autoAccept) {
4
+ return input.proposed;
5
+ }
6
+ const isInteractive = input.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
7
+ if (!isInteractive) {
8
+ throw new Error('Interactive work item selection requires a TTY. Re-run with "nyxagent run --yes" to accept the agent selection.');
9
+ }
10
+ const selectedKeys = await checkbox({
11
+ message: `Select work items to run (max ${input.maxItems})`,
12
+ choices: buildSelectionChoiceItems(input).map(toInquirerChoice),
13
+ pageSize: Math.min(Math.max(input.candidates.length, 7), 20),
14
+ required: false,
15
+ validate: (selected) => selected.length <= input.maxItems ||
16
+ `Select at most ${input.maxItems} work item(s).`,
17
+ shortcuts: {
18
+ all: null,
19
+ invert: null
20
+ }
21
+ });
22
+ const selected = new Set(selectedKeys);
23
+ return input.candidates.filter((candidate) => selected.has(candidate.key));
24
+ }
25
+ export function buildSelectionChoiceItems(input) {
26
+ const proposedKeys = new Set(input.proposed.map((item) => item.key));
27
+ const items = [];
28
+ let currentGroup;
29
+ for (const candidate of input.candidates) {
30
+ const group = detectPlanGroup(candidate);
31
+ if (group && group !== currentGroup) {
32
+ items.push({ type: "separator", label: group });
33
+ }
34
+ currentGroup = group;
35
+ const proposed = proposedKeys.has(candidate.key);
36
+ items.push({
37
+ type: "choice",
38
+ value: candidate.key,
39
+ name: `#${candidate.number} ${candidate.title}${proposed ? " (agent)" : ""}`,
40
+ checked: proposed
41
+ });
42
+ }
43
+ return items;
44
+ }
45
+ function toInquirerChoice(item) {
46
+ if (item.type === "separator") {
47
+ return new Separator(item.label);
48
+ }
49
+ return {
50
+ value: item.value,
51
+ name: item.name,
52
+ checked: item.checked
53
+ };
54
+ }
55
+ function detectPlanGroup(candidate) {
56
+ for (const label of candidate.labels ?? []) {
57
+ const group = parseGroupLabel(label);
58
+ if (group) {
59
+ return group;
60
+ }
61
+ }
62
+ return parseBracketedTitleGroup(candidate.title);
63
+ }
64
+ function parseGroupLabel(label) {
65
+ const match = /^(plan|prd)\s*[:/=-]\s*(.+)$/i.exec(label.trim());
66
+ if (!match) {
67
+ return undefined;
68
+ }
69
+ return formatGroupLabel(match[1], match[2]);
70
+ }
71
+ function parseBracketedTitleGroup(title) {
72
+ const match = /^\[(plan|prd)\s*[:/=-]\s*([^\]]+)\]/i.exec(title.trim());
73
+ if (!match) {
74
+ return undefined;
75
+ }
76
+ return formatGroupLabel(match[1], match[2]);
77
+ }
78
+ function formatGroupLabel(kind, rawName) {
79
+ const name = rawName.trim();
80
+ if (!name) {
81
+ return undefined;
82
+ }
83
+ const prefix = kind.toLowerCase() === "prd" ? "PRD" : "Plan";
84
+ return `${prefix}: ${name}`;
85
+ }
@@ -1,3 +1,4 @@
1
+ /** Builds a filesystem-safe, lexicographically sortable run id from a timestamp. */
1
2
  export function createRunId(date = new Date()) {
2
3
  return date.toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[:]/g, "-");
3
4
  }
@@ -1,3 +1,4 @@
1
+ /** Validates a parsed phase result against its JSON Schema using Ajv. */
1
2
  import { Ajv2020 } from "ajv/dist/2020.js";
2
3
  export function validateAgainstSchema(schema, value) {
3
4
  const ajv = new Ajv2020({ allErrors: true });
@@ -1,3 +1,4 @@
1
+ /** Work items: lists GitHub issues via `gh`, normalizes them to candidates, and resolves the selected queue. */
1
2
  import { execa } from "execa";
2
3
  export async function listGitHubIssues(input) {
3
4
  const result = await execa("gh", [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyxa/nyx-agent",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "A lightweight phase orchestrator for repeatedly launching coding agents with fresh context.",
5
5
  "type": "module",
6
6
  "repository": {