@nyxa/nyx-agent 0.4.1 → 0.6.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 +58 -9
- package/dist/cli.js +13 -16
- package/dist/commands/init.js +112 -462
- package/dist/commands/run.js +17 -3
- package/dist/commands/update.js +1 -0
- package/dist/config/loadConfig.js +17 -3
- package/dist/config/schema.js +29 -146
- package/dist/runtime/files.js +1 -0
- package/dist/runtime/git.js +1 -0
- package/dist/runtime/gitLifecycle.js +19 -57
- package/dist/runtime/harness.js +26 -0
- package/dist/runtime/ledger.js +1 -0
- package/dist/runtime/paths.js +1 -12
- package/dist/runtime/prompts.js +103 -0
- package/dist/runtime/runPhase.js +85 -254
- package/dist/runtime/runPipeline.js +479 -0
- package/dist/runtime/schemas.js +52 -0
- package/dist/runtime/scm.js +80 -0
- package/dist/runtime/time.js +1 -0
- package/dist/runtime/validateResult.js +2 -3
- package/dist/runtime/workItems.js +43 -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 -30
- 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,11 @@
|
|
|
1
|
-
|
|
2
|
-
import path from "node:path";
|
|
1
|
+
/** Work items: lists GitHub issues via `gh`, normalizes them to candidates, and resolves the selected queue. */
|
|
3
2
|
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) {
|
|
3
|
+
export async function listGitHubIssues(input) {
|
|
72
4
|
const result = await execa("gh", [
|
|
73
5
|
"issue",
|
|
74
6
|
"list",
|
|
75
7
|
"--repo",
|
|
76
|
-
input.
|
|
8
|
+
input.repo,
|
|
77
9
|
"--state",
|
|
78
10
|
"open",
|
|
79
11
|
"--limit",
|
|
@@ -83,7 +15,7 @@ export async function listGitHubWorkItemCandidates(input) {
|
|
|
83
15
|
], { reject: false });
|
|
84
16
|
if (result.exitCode !== 0) {
|
|
85
17
|
const detail = (result.stderr || result.stdout || "unknown error").trim();
|
|
86
|
-
throw new Error(`Failed to list GitHub
|
|
18
|
+
throw new Error(`Failed to list GitHub issues with gh: ${detail}`);
|
|
87
19
|
}
|
|
88
20
|
let issues;
|
|
89
21
|
try {
|
|
@@ -96,13 +28,46 @@ export async function listGitHubWorkItemCandidates(input) {
|
|
|
96
28
|
if (!Array.isArray(issues)) {
|
|
97
29
|
throw new Error("gh issue list returned JSON that is not an array");
|
|
98
30
|
}
|
|
99
|
-
return issues
|
|
31
|
+
return issues
|
|
32
|
+
.slice(0, input.maxCandidates)
|
|
33
|
+
.map((issue) => normalizeIssue({
|
|
100
34
|
issue,
|
|
101
|
-
|
|
35
|
+
repo: input.repo,
|
|
102
36
|
excerptChars: input.excerptChars
|
|
103
37
|
}));
|
|
104
38
|
}
|
|
105
|
-
function
|
|
39
|
+
export function filterAvailable(input) {
|
|
40
|
+
const completed = new Set(input.completedKeys);
|
|
41
|
+
return input.candidates.filter((candidate) => !completed.has(candidate.key));
|
|
42
|
+
}
|
|
43
|
+
/** Resolve the selection phase's ordered keys against the available candidates. */
|
|
44
|
+
export function resolveSelectedQueue(input) {
|
|
45
|
+
if (!Array.isArray(input.keys)) {
|
|
46
|
+
return { ok: false, error: "selection work_item_keys must be an array" };
|
|
47
|
+
}
|
|
48
|
+
const byKey = new Map(input.candidates.map((c) => [c.key, c]));
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const queue = [];
|
|
51
|
+
for (const key of input.keys) {
|
|
52
|
+
if (typeof key !== "string") {
|
|
53
|
+
return { ok: false, error: "selection work_item_keys must be strings" };
|
|
54
|
+
}
|
|
55
|
+
if (seen.has(key)) {
|
|
56
|
+
return { ok: false, error: `selection contains duplicate key "${key}"` };
|
|
57
|
+
}
|
|
58
|
+
const candidate = byKey.get(key);
|
|
59
|
+
if (!candidate) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: `selection key "${key}" is not an available candidate`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
seen.add(key);
|
|
66
|
+
queue.push(candidate);
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, queue };
|
|
69
|
+
}
|
|
70
|
+
function normalizeIssue(input) {
|
|
106
71
|
if (!isRecord(input.issue)) {
|
|
107
72
|
throw new Error("gh issue list returned a non-object issue");
|
|
108
73
|
}
|
|
@@ -114,14 +79,12 @@ function normalizeGitHubIssue(input) {
|
|
|
114
79
|
if (typeof title !== "string" || title.length === 0) {
|
|
115
80
|
throw new Error(`GitHub issue #${number} is missing a title`);
|
|
116
81
|
}
|
|
117
|
-
const locator = `${input.
|
|
82
|
+
const locator = `${input.repo}#${number}`;
|
|
118
83
|
const candidate = {
|
|
119
84
|
key: `github:${locator}`,
|
|
120
85
|
title,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
locator
|
|
124
|
-
},
|
|
86
|
+
number,
|
|
87
|
+
source: { type: "github", locator },
|
|
125
88
|
labels: normalizeLabels(input.issue.labels)
|
|
126
89
|
};
|
|
127
90
|
if (typeof input.issue.url === "string" && input.issue.url.length > 0) {
|
|
@@ -136,28 +99,6 @@ function normalizeGitHubIssue(input) {
|
|
|
136
99
|
}
|
|
137
100
|
return candidate;
|
|
138
101
|
}
|
|
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
102
|
function buildExcerpt(content, maxChars) {
|
|
162
103
|
if (maxChars <= 0) {
|
|
163
104
|
return undefined;
|
|
@@ -191,22 +132,6 @@ function normalizeLabels(labels) {
|
|
|
191
132
|
.filter((label) => Boolean(label));
|
|
192
133
|
return normalized.length > 0 ? normalized : undefined;
|
|
193
134
|
}
|
|
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
135
|
function isRecord(value) {
|
|
211
136
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
212
137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nyxa/nyx-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
|
-
}
|