@nyxa/nyx-agent 0.4.0 → 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.
- package/README.md +52 -9
- package/dist/cli.js +11 -16
- package/dist/commands/init.js +87 -466
- package/dist/commands/run.js +16 -3
- package/dist/config/loadConfig.js +16 -3
- package/dist/config/schema.js +27 -146
- package/dist/runtime/gitLifecycle.js +19 -57
- package/dist/runtime/harness.js +26 -0
- package/dist/runtime/paths.js +0 -12
- package/dist/runtime/prompts.js +103 -0
- package/dist/runtime/runPhase.js +85 -254
- package/dist/runtime/runPipeline.js +395 -0
- package/dist/runtime/schemas.js +52 -0
- package/dist/runtime/scm.js +76 -0
- package/dist/runtime/validateResult.js +1 -3
- package/dist/runtime/workItems.js +42 -118
- package/package.json +2 -5
- package/dist/runtime/buildPrompt.js +0 -54
- package/dist/runtime/effectiveConfig.js +0 -14
- package/dist/runtime/renderTemplate.js +0 -28
- package/dist/runtime/runWorkflow.js +0 -680
- package/dist/runtime/validateWorkItem.js +0 -212
- package/dist/runtime/workItemAnnotations.js +0 -39
- package/docs/nyxagent-v0-spec.md +0 -742
- package/templates/default/prompts/closure.md +0 -25
- package/templates/default/prompts/execution.md +0 -11
- package/templates/default/prompts/finalize.md +0 -7
- package/templates/default/prompts/global-review.md +0 -24
- package/templates/default/prompts/global-revision.md +0 -9
- package/templates/default/prompts/pull-request.md +0 -23
- package/templates/default/prompts/repair-result.md +0 -29
- package/templates/default/prompts/review.md +0 -18
- package/templates/default/prompts/revision.md +0 -7
- package/templates/default/prompts/selection.md +0 -46
- package/templates/default/schemas/closure.schema.json +0 -35
- package/templates/default/schemas/global-review.schema.json +0 -60
- package/templates/default/schemas/pull-request.schema.json +0 -44
- package/templates/default/schemas/review.schema.json +0 -60
- package/templates/default/schemas/selection.schema.json +0 -135
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
30
|
+
return issues
|
|
31
|
+
.slice(0, input.maxCandidates)
|
|
32
|
+
.map((issue) => normalizeIssue({
|
|
100
33
|
issue,
|
|
101
|
-
|
|
34
|
+
repo: input.repo,
|
|
102
35
|
excerptChars: input.excerptChars
|
|
103
36
|
}));
|
|
104
37
|
}
|
|
105
|
-
function
|
|
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.
|
|
81
|
+
const locator = `${input.repo}#${number}`;
|
|
118
82
|
const candidate = {
|
|
119
83
|
key: `github:${locator}`,
|
|
120
84
|
title,
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
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
|
-
}
|