@nyxa/nyx-agent 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +52 -9
  2. package/dist/cli.js +11 -16
  3. package/dist/commands/init.js +87 -466
  4. package/dist/commands/run.js +16 -3
  5. package/dist/config/loadConfig.js +16 -3
  6. package/dist/config/schema.js +27 -146
  7. package/dist/runtime/gitLifecycle.js +19 -57
  8. package/dist/runtime/harness.js +26 -0
  9. package/dist/runtime/paths.js +0 -12
  10. package/dist/runtime/prompts.js +103 -0
  11. package/dist/runtime/runPhase.js +85 -254
  12. package/dist/runtime/runPipeline.js +395 -0
  13. package/dist/runtime/schemas.js +52 -0
  14. package/dist/runtime/scm.js +76 -0
  15. package/dist/runtime/validateResult.js +1 -3
  16. package/dist/runtime/workItems.js +42 -118
  17. package/package.json +2 -5
  18. package/dist/runtime/buildPrompt.js +0 -54
  19. package/dist/runtime/effectiveConfig.js +0 -14
  20. package/dist/runtime/renderTemplate.js +0 -28
  21. package/dist/runtime/runWorkflow.js +0 -680
  22. package/dist/runtime/validateWorkItem.js +0 -212
  23. package/dist/runtime/workItemAnnotations.js +0 -39
  24. package/docs/nyxagent-v0-spec.md +0 -742
  25. package/templates/default/prompts/closure.md +0 -30
  26. package/templates/default/prompts/execution.md +0 -11
  27. package/templates/default/prompts/finalize.md +0 -7
  28. package/templates/default/prompts/global-review.md +0 -24
  29. package/templates/default/prompts/global-revision.md +0 -9
  30. package/templates/default/prompts/pull-request.md +0 -23
  31. package/templates/default/prompts/repair-result.md +0 -29
  32. package/templates/default/prompts/review.md +0 -18
  33. package/templates/default/prompts/revision.md +0 -7
  34. package/templates/default/prompts/selection.md +0 -46
  35. package/templates/default/schemas/closure.schema.json +0 -35
  36. package/templates/default/schemas/global-review.schema.json +0 -60
  37. package/templates/default/schemas/pull-request.schema.json +0 -44
  38. package/templates/default/schemas/review.schema.json +0 -60
  39. package/templates/default/schemas/selection.schema.json +0 -135
@@ -1,79 +1,10 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
2
- import path from "node:path";
3
1
  import { execa } from "execa";
4
- export async function listWorkItemCandidates(input) {
5
- const workItems = input.config.work_items;
6
- if (!workItems) {
7
- return [];
8
- }
9
- if (workItems.source === "local") {
10
- if (!workItems.path) {
11
- throw new Error('Local work items require [work_items].path');
12
- }
13
- return listLocalWorkItemCandidates({
14
- projectRoot: input.projectRoot,
15
- workItemsPath: workItems.path,
16
- maxCandidates: workItems.max_candidates,
17
- excerptChars: workItems.excerpt_chars
18
- });
19
- }
20
- if (!workItems.repository) {
21
- throw new Error('GitHub work items require [work_items].repository');
22
- }
23
- return listGitHubWorkItemCandidates({
24
- repository: workItems.repository,
25
- maxCandidates: workItems.max_candidates,
26
- excerptChars: workItems.excerpt_chars
27
- });
28
- }
29
- export function filterAvailableWorkItems(input) {
30
- const seen = new Set(input.seenWorkItemKeys);
31
- const completed = new Set(input.completedWorkItemKeys);
32
- return input.candidates.filter((candidate) => !seen.has(candidate.key) && !completed.has(candidate.key));
33
- }
34
- export async function listLocalWorkItemCandidates(input) {
35
- const normalizedPath = normalizeProjectRelativePath(input.workItemsPath);
36
- if (!normalizedPath) {
37
- throw new Error("[work_items].path must be a canonical relative path");
38
- }
39
- const root = path.resolve(input.projectRoot);
40
- const workItemsDir = path.resolve(root, normalizedPath);
41
- const relative = path.relative(root, workItemsDir);
42
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
43
- throw new Error("[work_items].path must stay inside the project root");
44
- }
45
- const dirStat = await stat(workItemsDir).catch((error) => {
46
- const message = error instanceof Error ? error.message : String(error);
47
- throw new Error(`Local work item path does not exist: ${normalizedPath} (${message})`);
48
- });
49
- if (!dirStat.isDirectory()) {
50
- throw new Error(`Local work item path is not a directory: ${normalizedPath}`);
51
- }
52
- const files = (await collectMarkdownFiles(workItemsDir))
53
- .map((filePath) => toPosixPath(path.relative(root, filePath)))
54
- .sort((left, right) => left.localeCompare(right));
55
- const candidates = [];
56
- for (const locator of files.slice(0, input.maxCandidates)) {
57
- const absolutePath = path.join(root, locator);
58
- const content = await readFile(absolutePath, "utf8");
59
- candidates.push({
60
- key: `local:${locator}`,
61
- title: inferMarkdownTitle(content, locator),
62
- source: {
63
- type: "local",
64
- locator
65
- },
66
- excerpt: buildExcerpt(content, input.excerptChars)
67
- });
68
- }
69
- return candidates;
70
- }
71
- export async function listGitHubWorkItemCandidates(input) {
2
+ export async function listGitHubIssues(input) {
72
3
  const result = await execa("gh", [
73
4
  "issue",
74
5
  "list",
75
6
  "--repo",
76
- input.repository,
7
+ input.repo,
77
8
  "--state",
78
9
  "open",
79
10
  "--limit",
@@ -83,7 +14,7 @@ export async function listGitHubWorkItemCandidates(input) {
83
14
  ], { reject: false });
84
15
  if (result.exitCode !== 0) {
85
16
  const detail = (result.stderr || result.stdout || "unknown error").trim();
86
- throw new Error(`Failed to list GitHub work items with gh: ${detail}`);
17
+ throw new Error(`Failed to list GitHub issues with gh: ${detail}`);
87
18
  }
88
19
  let issues;
89
20
  try {
@@ -96,13 +27,46 @@ export async function listGitHubWorkItemCandidates(input) {
96
27
  if (!Array.isArray(issues)) {
97
28
  throw new Error("gh issue list returned JSON that is not an array");
98
29
  }
99
- return issues.slice(0, input.maxCandidates).map((issue) => normalizeGitHubIssue({
30
+ return issues
31
+ .slice(0, input.maxCandidates)
32
+ .map((issue) => normalizeIssue({
100
33
  issue,
101
- repository: input.repository,
34
+ repo: input.repo,
102
35
  excerptChars: input.excerptChars
103
36
  }));
104
37
  }
105
- function normalizeGitHubIssue(input) {
38
+ export function filterAvailable(input) {
39
+ const completed = new Set(input.completedKeys);
40
+ return input.candidates.filter((candidate) => !completed.has(candidate.key));
41
+ }
42
+ /** Resolve the selection phase's ordered keys against the available candidates. */
43
+ export function resolveSelectedQueue(input) {
44
+ if (!Array.isArray(input.keys)) {
45
+ return { ok: false, error: "selection work_item_keys must be an array" };
46
+ }
47
+ const byKey = new Map(input.candidates.map((c) => [c.key, c]));
48
+ const seen = new Set();
49
+ const queue = [];
50
+ for (const key of input.keys) {
51
+ if (typeof key !== "string") {
52
+ return { ok: false, error: "selection work_item_keys must be strings" };
53
+ }
54
+ if (seen.has(key)) {
55
+ return { ok: false, error: `selection contains duplicate key "${key}"` };
56
+ }
57
+ const candidate = byKey.get(key);
58
+ if (!candidate) {
59
+ return {
60
+ ok: false,
61
+ error: `selection key "${key}" is not an available candidate`
62
+ };
63
+ }
64
+ seen.add(key);
65
+ queue.push(candidate);
66
+ }
67
+ return { ok: true, queue };
68
+ }
69
+ function normalizeIssue(input) {
106
70
  if (!isRecord(input.issue)) {
107
71
  throw new Error("gh issue list returned a non-object issue");
108
72
  }
@@ -114,14 +78,12 @@ function normalizeGitHubIssue(input) {
114
78
  if (typeof title !== "string" || title.length === 0) {
115
79
  throw new Error(`GitHub issue #${number} is missing a title`);
116
80
  }
117
- const locator = `${input.repository}#${number}`;
81
+ const locator = `${input.repo}#${number}`;
118
82
  const candidate = {
119
83
  key: `github:${locator}`,
120
84
  title,
121
- source: {
122
- type: "github",
123
- locator
124
- },
85
+ number,
86
+ source: { type: "github", locator },
125
87
  labels: normalizeLabels(input.issue.labels)
126
88
  };
127
89
  if (typeof input.issue.url === "string" && input.issue.url.length > 0) {
@@ -136,28 +98,6 @@ function normalizeGitHubIssue(input) {
136
98
  }
137
99
  return candidate;
138
100
  }
139
- async function collectMarkdownFiles(dir) {
140
- const entries = await readdir(dir, { withFileTypes: true });
141
- const files = [];
142
- for (const entry of entries) {
143
- const filePath = path.join(dir, entry.name);
144
- if (entry.isDirectory()) {
145
- files.push(...(await collectMarkdownFiles(filePath)));
146
- continue;
147
- }
148
- if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
149
- files.push(filePath);
150
- }
151
- }
152
- return files;
153
- }
154
- function inferMarkdownTitle(content, locator) {
155
- const heading = /^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/m.exec(content);
156
- if (heading?.[1]) {
157
- return heading[1].trim();
158
- }
159
- return path.posix.basename(locator, ".md");
160
- }
161
101
  function buildExcerpt(content, maxChars) {
162
102
  if (maxChars <= 0) {
163
103
  return undefined;
@@ -191,22 +131,6 @@ function normalizeLabels(labels) {
191
131
  .filter((label) => Boolean(label));
192
132
  return normalized.length > 0 ? normalized : undefined;
193
133
  }
194
- function normalizeProjectRelativePath(value) {
195
- if (value.length === 0 ||
196
- value.includes("\\") ||
197
- path.posix.isAbsolute(value) ||
198
- /^[A-Za-z]:[\\/]/.test(value)) {
199
- return undefined;
200
- }
201
- const normalized = path.posix.normalize(value);
202
- if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
203
- return undefined;
204
- }
205
- return normalized;
206
- }
207
- function toPosixPath(value) {
208
- return value.split(path.sep).join(path.posix.sep);
209
- }
210
134
  function isRecord(value) {
211
135
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
212
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyxa/nyx-agent",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "A lightweight phase orchestrator for repeatedly launching coding agents with fresh context.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,9 +11,7 @@
11
11
  "nyxagent": "dist/cli.js"
12
12
  },
13
13
  "files": [
14
- "dist",
15
- "templates",
16
- "docs"
14
+ "dist"
17
15
  ],
18
16
  "publishConfig": {
19
17
  "registry": "https://registry.npmjs.org",
@@ -34,7 +32,6 @@
34
32
  "commander": "^14.0.0",
35
33
  "execa": "^9.6.0",
36
34
  "picocolors": "^1.1.0",
37
- "smol-toml": "^1.3.0",
38
35
  "zod": "^4.0.0"
39
36
  },
40
37
  "devDependencies": {
@@ -1,54 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { resolveNyxPath } from "./paths.js";
3
- import { renderTemplate } from "./renderTemplate.js";
4
- export async function buildPhasePrompt(input) {
5
- const promptPath = resolveNyxPath(input.projectRoot, input.phase.prompt, `prompt for phase "${input.phase.id}"`);
6
- const promptTemplate = await readFile(promptPath, "utf8");
7
- const userPrompt = renderTemplate(promptTemplate, input.context);
8
- const runtimeContract = await buildRuntimeContract(input);
9
- return `${runtimeContract}\n\n# Phase Prompt\n\n${userPrompt}`;
10
- }
11
- async function buildRuntimeContract(input) {
12
- const requiresResult = phaseRequiresStructuredResult(input.phase);
13
- const schemaText = input.phase.output_schema
14
- ? await readFile(resolveNyxPath(input.projectRoot, input.phase.output_schema, `output_schema for phase "${input.phase.id}"`), "utf8")
15
- : undefined;
16
- const workdir = typeof input.context.workdir === "string"
17
- ? input.context.workdir
18
- : input.projectRoot;
19
- const git = input.context.git;
20
- const lines = [
21
- "# NyxAgent Runtime Contract",
22
- "",
23
- "You are running as one isolated phase in a NyxAgent workflow.",
24
- "Follow the phase prompt, but obey this runtime contract first.",
25
- "",
26
- `Project root: ${input.projectRoot}`,
27
- `Working directory: ${workdir}`
28
- ];
29
- if (workdir !== input.projectRoot) {
30
- lines.push("Run all commands and edit files from the working directory above (an", "isolated git worktree). The project root only holds NyxAgent run artifacts.");
31
- }
32
- if (git?.branch) {
33
- lines.push(`Git branch: ${git.branch} (base ${git.base ?? "unknown"})`);
34
- }
35
- lines.push(`Run dir: ${input.runDir}`, `Iteration dir: ${input.iterationDir}`, `Phase dir: ${input.phaseDir}`, `State file: ${input.stateFile}`, `Phase id: ${input.phase.id}`, "", "Current state:", "```json", JSON.stringify(input.context.state ?? {}, null, 2), "```", "", "Work item configuration:", "```json", JSON.stringify(input.config.work_items ?? {}, null, 2), "```", "", "Available work items:", "```json", JSON.stringify(input.context.available_work_items ?? [], null, 2), "```", "", "Recommended work item queue:", "```json", JSON.stringify(input.context.recommended_work_item_queue ?? [], null, 2), "```", "", "Selected work item queue:", "```json", JSON.stringify(input.context.selected_work_item_queue ?? [], null, 2), "```", "", "Work item annotations:", "```json", JSON.stringify(input.context.work_item_annotations ?? [], null, 2), "```", "", "Seen work item keys:", "```json", JSON.stringify(input.context.seen_work_item_keys ?? [], null, 2), "```", "", "Completed work item keys:", "```json", JSON.stringify(input.context.completed_work_item_keys ?? [], null, 2), "```", "", "Last completed work item:", "```json", JSON.stringify(input.context.last_completed_work_item ?? null, null, 2), "```");
36
- if (input.phase.transitions) {
37
- lines.push("", "Configured outcome transitions:", "```json", JSON.stringify(input.phase.transitions, null, 2), "```");
38
- }
39
- if (requiresResult) {
40
- lines.push("", "Structured result is required.", "Return the structured result in stdout inside the final <nyxagent_result> XML block.", "NyxAgent will parse the last <nyxagent_result> block, validate it, and write result.json.", "Do not write result.json yourself.", "", "Result format:", "```xml", "<nyxagent_result>", "{", ' "outcome": "one_of_the_configured_outcomes"', "}", "</nyxagent_result>", "```");
41
- if (input.phase.transitions) {
42
- lines.push("", `The JSON object must include an "outcome" matching one of: ${Object.keys(input.phase.transitions).join(", ")}.`);
43
- }
44
- }
45
- if (schemaText) {
46
- lines.push("", "Expected JSON Schema:", "```json", schemaText.trim(), "```");
47
- }
48
- return lines.join("\n");
49
- }
50
- export function phaseRequiresStructuredResult(phase) {
51
- return Boolean(phase.required_output ||
52
- phase.output_schema ||
53
- (phase.transitions && Object.keys(phase.transitions).length > 0));
54
- }
@@ -1,14 +0,0 @@
1
- export function getEffectiveModel(config, phase) {
2
- return {
3
- ...config.model,
4
- ...(phase.model ?? {})
5
- };
6
- }
7
- export function getEffectiveHarness(config, phase) {
8
- return {
9
- ...config.harness,
10
- ...(phase.harness ?? {}),
11
- args: phase.harness?.args ?? config.harness.args,
12
- prompt_input: phase.harness?.prompt_input ?? config.harness.prompt_input
13
- };
14
- }
@@ -1,28 +0,0 @@
1
- const templatePattern = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
2
- export function renderTemplate(input, context) {
3
- return input.replace(templatePattern, (_match, key) => {
4
- const value = readPath(context, key);
5
- if (value === undefined || value === null) {
6
- return "";
7
- }
8
- if (typeof value === "string") {
9
- return value;
10
- }
11
- if (typeof value === "number" ||
12
- typeof value === "boolean" ||
13
- typeof value === "bigint") {
14
- return String(value);
15
- }
16
- return JSON.stringify(value, null, 2);
17
- });
18
- }
19
- export function readPath(context, key) {
20
- return key.split(".").reduce((current, segment) => {
21
- if (current &&
22
- typeof current === "object" &&
23
- Object.prototype.hasOwnProperty.call(current, segment)) {
24
- return current[segment];
25
- }
26
- return undefined;
27
- }, context);
28
- }