@nyxa/nyx-agent 0.2.1 → 0.3.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/dist/cli.js +2 -1
- package/dist/commands/init.js +51 -39
- package/dist/config/schema.js +37 -1
- package/dist/runtime/buildPrompt.js +25 -0
- package/dist/runtime/ledger.js +118 -0
- package/dist/runtime/runPhase.js +64 -0
- package/dist/runtime/runWorkflow.js +375 -43
- package/dist/runtime/validateWorkItem.js +212 -0
- package/dist/runtime/workItems.js +212 -0
- package/docs/nyxagent-v0-spec.md +80 -24
- package/package.json +1 -1
- package/templates/default/prompts/selection.md +28 -10
- package/templates/default/schemas/selection.schema.json +52 -8
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function validateWorkItemIdentity(input) {
|
|
3
|
+
const workItems = input.config.work_items;
|
|
4
|
+
if (!workItems) {
|
|
5
|
+
return {
|
|
6
|
+
ok: false,
|
|
7
|
+
error: "Selected work item requires configured [work_items]"
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (!isRecord(input.workItem)) {
|
|
11
|
+
return { ok: false, error: "Selected work item must be an object" };
|
|
12
|
+
}
|
|
13
|
+
const source = input.workItem.source;
|
|
14
|
+
if (!isRecord(source)) {
|
|
15
|
+
return { ok: false, error: "Selected work item source must be an object" };
|
|
16
|
+
}
|
|
17
|
+
const key = input.workItem.key;
|
|
18
|
+
const sourceType = source.type;
|
|
19
|
+
const locator = source.locator;
|
|
20
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
21
|
+
return { ok: false, error: "Selected work item key must be a non-empty string" };
|
|
22
|
+
}
|
|
23
|
+
if (input.seenWorkItemKeys?.includes(key) && key !== input.allowKnownKey) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: `Selected work item key "${key}" was already seen in this run`
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (input.completedWorkItemKeys?.includes(key) &&
|
|
30
|
+
key !== input.allowKnownKey) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
error: `Selected work item key "${key}" is already completed`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (sourceType !== workItems.source) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: `Selected work item source.type must be "${workItems.source}"`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (typeof locator !== "string" || locator.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: "Selected work item source.locator must be a non-empty string"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (key !== `${workItems.source}:${locator}`) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: `Selected work item key must be "${workItems.source}:${locator}"`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (workItems.source === "local") {
|
|
55
|
+
const localValidation = validateLocalLocator(locator, workItems.path);
|
|
56
|
+
if (!localValidation.ok) {
|
|
57
|
+
return localValidation;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const githubValidation = validateGitHubLocator(locator, workItems.repository);
|
|
62
|
+
if (!githubValidation.ok) {
|
|
63
|
+
return githubValidation;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (input.availableWorkItems) {
|
|
67
|
+
return validateCandidateMembership({
|
|
68
|
+
key,
|
|
69
|
+
sourceType,
|
|
70
|
+
locator,
|
|
71
|
+
availableWorkItems: input.availableWorkItems
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
export function validateWorkItemQueue(input) {
|
|
77
|
+
if (!Array.isArray(input.workItems)) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: "Selected work_items must be an array"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (input.workItems.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: "Selected work_items must contain at least one item"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const normalized = [];
|
|
91
|
+
for (const [index, workItem] of input.workItems.entries()) {
|
|
92
|
+
const validation = validateWorkItemIdentity({
|
|
93
|
+
config: input.config,
|
|
94
|
+
workItem,
|
|
95
|
+
availableWorkItems: input.availableWorkItems,
|
|
96
|
+
seenWorkItemKeys: input.seenWorkItemKeys,
|
|
97
|
+
completedWorkItemKeys: input.completedWorkItemKeys
|
|
98
|
+
});
|
|
99
|
+
if (!validation.ok) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `Selected work_items[${index}] ${validation.error}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const key = readObjectProperty(workItem, "key");
|
|
106
|
+
if (typeof key !== "string") {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `Selected work_items[${index}] key must be a string`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (seen.has(key)) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: `Selected work_items contains duplicate key "${key}"`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
seen.add(key);
|
|
119
|
+
const candidate = input.availableWorkItems?.find((item) => item.key === key);
|
|
120
|
+
normalized.push(candidate ?? workItem);
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, workItems: normalized };
|
|
123
|
+
}
|
|
124
|
+
function validateCandidateMembership(input) {
|
|
125
|
+
const candidate = input.availableWorkItems.find((item) => item.key === input.key);
|
|
126
|
+
if (!candidate) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: `Selected work item key "${input.key}" is not in available_work_items`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (candidate.source.type !== input.sourceType ||
|
|
133
|
+
candidate.source.locator !== input.locator) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `Selected work item source does not match available_work_items entry "${input.key}"`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { ok: true };
|
|
140
|
+
}
|
|
141
|
+
function validateLocalLocator(locator, configuredPath) {
|
|
142
|
+
const normalizedLocator = normalizeRelativePath(locator);
|
|
143
|
+
const normalizedPath = configuredPath
|
|
144
|
+
? normalizeRelativePath(configuredPath)
|
|
145
|
+
: undefined;
|
|
146
|
+
if (!normalizedLocator || normalizedLocator !== locator) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: "Local work item locator must be a canonical relative path"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!normalizedPath) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
error: "Local work item validation requires [work_items].path"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (normalizedLocator !== normalizedPath &&
|
|
159
|
+
!normalizedLocator.startsWith(`${normalizedPath}/`)) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: `Local work item locator must stay under "${normalizedPath}"`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { ok: true };
|
|
166
|
+
}
|
|
167
|
+
function validateGitHubLocator(locator, repository) {
|
|
168
|
+
if (!repository) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: "GitHub work item validation requires [work_items].repository"
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const match = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#([1-9][0-9]*)$/.exec(locator);
|
|
175
|
+
if (!match) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
error: 'GitHub work item locator must use "owner/repo#number"'
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (match[1] !== repository) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: `GitHub work item locator must use repository "${repository}"`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { ok: true };
|
|
188
|
+
}
|
|
189
|
+
function normalizeRelativePath(value) {
|
|
190
|
+
if (value.length === 0 ||
|
|
191
|
+
value.includes("\\") ||
|
|
192
|
+
path.posix.isAbsolute(value) ||
|
|
193
|
+
/^[A-Za-z]:[\\/]/.test(value)) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const normalized = path.posix.normalize(value);
|
|
197
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return normalized;
|
|
201
|
+
}
|
|
202
|
+
function isRecord(value) {
|
|
203
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
204
|
+
}
|
|
205
|
+
function readObjectProperty(value, key) {
|
|
206
|
+
if (value &&
|
|
207
|
+
typeof value === "object" &&
|
|
208
|
+
Object.prototype.hasOwnProperty.call(value, key)) {
|
|
209
|
+
return value[key];
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
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) {
|
|
72
|
+
const result = await execa("gh", [
|
|
73
|
+
"issue",
|
|
74
|
+
"list",
|
|
75
|
+
"--repo",
|
|
76
|
+
input.repository,
|
|
77
|
+
"--state",
|
|
78
|
+
"open",
|
|
79
|
+
"--limit",
|
|
80
|
+
String(input.maxCandidates),
|
|
81
|
+
"--json",
|
|
82
|
+
"number,title,url,labels,updatedAt,body"
|
|
83
|
+
], { reject: false });
|
|
84
|
+
if (result.exitCode !== 0) {
|
|
85
|
+
const detail = (result.stderr || result.stdout || "unknown error").trim();
|
|
86
|
+
throw new Error(`Failed to list GitHub work items with gh: ${detail}`);
|
|
87
|
+
}
|
|
88
|
+
let issues;
|
|
89
|
+
try {
|
|
90
|
+
issues = JSON.parse(result.stdout);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
throw new Error(`Failed to parse gh issue list JSON: ${message}`);
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(issues)) {
|
|
97
|
+
throw new Error("gh issue list returned JSON that is not an array");
|
|
98
|
+
}
|
|
99
|
+
return issues.slice(0, input.maxCandidates).map((issue) => normalizeGitHubIssue({
|
|
100
|
+
issue,
|
|
101
|
+
repository: input.repository,
|
|
102
|
+
excerptChars: input.excerptChars
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
function normalizeGitHubIssue(input) {
|
|
106
|
+
if (!isRecord(input.issue)) {
|
|
107
|
+
throw new Error("gh issue list returned a non-object issue");
|
|
108
|
+
}
|
|
109
|
+
const number = input.issue.number;
|
|
110
|
+
const title = input.issue.title;
|
|
111
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) {
|
|
112
|
+
throw new Error("GitHub issue is missing a positive integer number");
|
|
113
|
+
}
|
|
114
|
+
if (typeof title !== "string" || title.length === 0) {
|
|
115
|
+
throw new Error(`GitHub issue #${number} is missing a title`);
|
|
116
|
+
}
|
|
117
|
+
const locator = `${input.repository}#${number}`;
|
|
118
|
+
const candidate = {
|
|
119
|
+
key: `github:${locator}`,
|
|
120
|
+
title,
|
|
121
|
+
source: {
|
|
122
|
+
type: "github",
|
|
123
|
+
locator
|
|
124
|
+
},
|
|
125
|
+
labels: normalizeLabels(input.issue.labels)
|
|
126
|
+
};
|
|
127
|
+
if (typeof input.issue.url === "string" && input.issue.url.length > 0) {
|
|
128
|
+
candidate.url = input.issue.url;
|
|
129
|
+
}
|
|
130
|
+
if (typeof input.issue.updatedAt === "string" &&
|
|
131
|
+
input.issue.updatedAt.length > 0) {
|
|
132
|
+
candidate.updated_at = input.issue.updatedAt;
|
|
133
|
+
}
|
|
134
|
+
if (typeof input.issue.body === "string") {
|
|
135
|
+
candidate.excerpt = buildExcerpt(input.issue.body, input.excerptChars);
|
|
136
|
+
}
|
|
137
|
+
return candidate;
|
|
138
|
+
}
|
|
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
|
+
function buildExcerpt(content, maxChars) {
|
|
162
|
+
if (maxChars <= 0) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const compact = content.replace(/\s+/g, " ").trim();
|
|
166
|
+
if (!compact) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (compact.length <= maxChars) {
|
|
170
|
+
return compact;
|
|
171
|
+
}
|
|
172
|
+
if (maxChars <= 3) {
|
|
173
|
+
return compact.slice(0, maxChars);
|
|
174
|
+
}
|
|
175
|
+
return `${compact.slice(0, maxChars - 3).trimEnd()}...`;
|
|
176
|
+
}
|
|
177
|
+
function normalizeLabels(labels) {
|
|
178
|
+
if (!Array.isArray(labels)) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const normalized = labels
|
|
182
|
+
.map((label) => {
|
|
183
|
+
if (typeof label === "string") {
|
|
184
|
+
return label;
|
|
185
|
+
}
|
|
186
|
+
if (isRecord(label) && typeof label.name === "string") {
|
|
187
|
+
return label.name;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
})
|
|
191
|
+
.filter((label) => Boolean(label));
|
|
192
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
193
|
+
}
|
|
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
|
+
function isRecord(value) {
|
|
211
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
212
|
+
}
|
package/docs/nyxagent-v0-spec.md
CHANGED
|
@@ -16,7 +16,8 @@ configuration.
|
|
|
16
16
|
## Goals
|
|
17
17
|
|
|
18
18
|
- Install a `.nyxagent/` folder into a project with sensible templates.
|
|
19
|
-
-
|
|
19
|
+
- Select an ordered work-item queue once, then run a configurable phase workflow
|
|
20
|
+
for up to `max_iterations` confirmed work items.
|
|
20
21
|
- Launch a fresh harness process for each phase.
|
|
21
22
|
- Keep workflow structure generic through phase transitions and outcomes.
|
|
22
23
|
- Keep prompts focused on agent behavior, not runtime mechanics.
|
|
@@ -26,7 +27,7 @@ configuration.
|
|
|
26
27
|
|
|
27
28
|
## Non-Goals For v0
|
|
28
29
|
|
|
29
|
-
- No
|
|
30
|
+
- No native classification of work items as plans, PRDs, or tasks.
|
|
30
31
|
- No native Git commit or issue close logic in the engine.
|
|
31
32
|
- No resume command.
|
|
32
33
|
- No fully general DAG/workflow engine.
|
|
@@ -62,6 +63,7 @@ If `.nyxagent/` already exists:
|
|
|
62
63
|
```text
|
|
63
64
|
.nyxagent/
|
|
64
65
|
config.toml
|
|
66
|
+
state.json
|
|
65
67
|
prompts/
|
|
66
68
|
selection.md
|
|
67
69
|
execution.md
|
|
@@ -113,8 +115,10 @@ max_attempts = 1
|
|
|
113
115
|
prompt = "prompts/repair-result.md"
|
|
114
116
|
|
|
115
117
|
[work_items]
|
|
116
|
-
source = "local
|
|
118
|
+
source = "local"
|
|
117
119
|
path = "issues"
|
|
120
|
+
max_candidates = 50
|
|
121
|
+
excerpt_chars = 800
|
|
118
122
|
|
|
119
123
|
[[phases]]
|
|
120
124
|
id = "selection"
|
|
@@ -168,15 +172,34 @@ max_visits_per_iteration = 1
|
|
|
168
172
|
|
|
169
173
|
### Config Semantics
|
|
170
174
|
|
|
171
|
-
- `workflow.max_iterations` is the maximum number of distinct work
|
|
172
|
-
run.
|
|
175
|
+
- `workflow.max_iterations` is the maximum number of distinct confirmed work
|
|
176
|
+
items in a run.
|
|
173
177
|
- `phases[*].max_visits_per_iteration` prevents infinite loops inside one work
|
|
174
178
|
item.
|
|
175
179
|
- `model.reasoning_level` is a harness-neutral string.
|
|
176
180
|
- Harness args are declarative and may interpolate config/runtime variables.
|
|
177
181
|
- Per-phase `model` and `harness` blocks override global values.
|
|
178
|
-
- `work_items`
|
|
179
|
-
|
|
182
|
+
- `work_items` supports only `local` and `github` in v0.
|
|
183
|
+
- `work_items.max_candidates` defaults to `50` and caps the inventory sent to
|
|
184
|
+
the initial selection prompt.
|
|
185
|
+
- `work_items.excerpt_chars` defaults to `800` and bounds candidate excerpts.
|
|
186
|
+
- At run start, the engine scans the configured provider, normalizes candidates
|
|
187
|
+
into `available_work_items`, and sends that inventory to the selection phase.
|
|
188
|
+
- The selection phase returns an ordered `work_items` queue. NyxAgent validates
|
|
189
|
+
each selected identity against `[work_items]`, rejects duplicates, and
|
|
190
|
+
requires every selected key to exist in `available_work_items`.
|
|
191
|
+
- Before executing, NyxAgent shows the selected queue in an interactive
|
|
192
|
+
checkbox prompt. The user can uncheck work items; only confirmed items are
|
|
193
|
+
executed.
|
|
194
|
+
- Local markdown work items do not have a required frontmatter schema. The
|
|
195
|
+
provider infers titles from the first heading or filename and includes a
|
|
196
|
+
bounded content excerpt.
|
|
197
|
+
- Local keys use `local:<relative-path-under-work-items-path>`.
|
|
198
|
+
- GitHub keys use `github:<owner>/<repo>#<issue-number>` and must match the
|
|
199
|
+
configured repository.
|
|
200
|
+
- NyxAgent does not decide whether an item is a plan, PRD, or task. The
|
|
201
|
+
selection agent makes that semantic choice from the deterministic inventory
|
|
202
|
+
and may include optional `selection_groups` for user review.
|
|
180
203
|
|
|
181
204
|
## Workflow Model
|
|
182
205
|
|
|
@@ -196,10 +219,12 @@ Reserved next targets:
|
|
|
196
219
|
The engine does not know about development, review, approval, or closure. It
|
|
197
220
|
only knows phases, outcomes, transitions, and visit limits.
|
|
198
221
|
|
|
199
|
-
The default template expresses the standard
|
|
222
|
+
The default template expresses the standard run:
|
|
200
223
|
|
|
201
224
|
```text
|
|
202
|
-
selection ->
|
|
225
|
+
selection -> user confirms queue
|
|
226
|
+
for each confirmed work item:
|
|
227
|
+
execution -> review
|
|
203
228
|
review.approved -> closure -> next_iteration
|
|
204
229
|
review.changes_requested -> execution
|
|
205
230
|
selection.no_work -> stop_run
|
|
@@ -273,6 +298,11 @@ The runtime contract includes:
|
|
|
273
298
|
- required structured output contract
|
|
274
299
|
- work item context, when selected
|
|
275
300
|
- work item config from `[work_items]`
|
|
301
|
+
- `available_work_items`
|
|
302
|
+
- `selected_work_item_queue`
|
|
303
|
+
- `seen_work_item_keys`
|
|
304
|
+
- `completed_work_item_keys`
|
|
305
|
+
- `last_completed_work_item`
|
|
276
306
|
|
|
277
307
|
The user prompt remains focused on domain behavior.
|
|
278
308
|
|
|
@@ -284,6 +314,7 @@ Prompts may use simple interpolation:
|
|
|
284
314
|
{{iteration_dir}}
|
|
285
315
|
{{phase_dir}}
|
|
286
316
|
{{state_file}}
|
|
317
|
+
{{selected_work_item_queue}}
|
|
287
318
|
{{work_item.key}}
|
|
288
319
|
{{work_item.title}}
|
|
289
320
|
{{model.name}}
|
|
@@ -300,17 +331,20 @@ Each run creates a timestamped directory:
|
|
|
300
331
|
.nyxagent/runs/2026-05-23T12-30-00/
|
|
301
332
|
run.json
|
|
302
333
|
state.json
|
|
334
|
+
selection/
|
|
335
|
+
state.json
|
|
336
|
+
phases/
|
|
337
|
+
selection/
|
|
338
|
+
attempt-001/
|
|
339
|
+
prompt.md
|
|
340
|
+
stdout.log
|
|
341
|
+
stderr.log
|
|
342
|
+
meta.json
|
|
343
|
+
result.json
|
|
303
344
|
iterations/
|
|
304
345
|
001/
|
|
305
346
|
state.json
|
|
306
347
|
phases/
|
|
307
|
-
selection/
|
|
308
|
-
attempt-001/
|
|
309
|
-
prompt.md
|
|
310
|
-
stdout.log
|
|
311
|
-
stderr.log
|
|
312
|
-
meta.json
|
|
313
|
-
result.json
|
|
314
348
|
execution/
|
|
315
349
|
attempt-001/
|
|
316
350
|
prompt.md
|
|
@@ -346,12 +380,28 @@ Each run creates a timestamped directory:
|
|
|
346
380
|
- run status
|
|
347
381
|
- current iteration
|
|
348
382
|
- completed iterations
|
|
383
|
+
- available work items seen by the initial selection phase
|
|
384
|
+
- selected work item queue confirmed for the run
|
|
385
|
+
- skipped selected work item keys
|
|
386
|
+
- selection groups returned for user review
|
|
349
387
|
- seen work item keys
|
|
388
|
+
- completed work item keys
|
|
389
|
+
- last completed work item
|
|
390
|
+
|
|
391
|
+
`.nyxagent/state.json` records persistent work item completion state:
|
|
392
|
+
|
|
393
|
+
- completed work item keys
|
|
394
|
+
- last completed work item
|
|
395
|
+
- minimal completion history
|
|
350
396
|
|
|
351
397
|
`iterations/NNN/state.json` records per-work-item state:
|
|
352
398
|
|
|
353
399
|
- iteration number
|
|
354
400
|
- work item
|
|
401
|
+
- selected work item queue for the run
|
|
402
|
+
- seen work item keys
|
|
403
|
+
- completed work item keys
|
|
404
|
+
- last completed work item
|
|
355
405
|
- phase results
|
|
356
406
|
- phase visit counts
|
|
357
407
|
- current phase status
|
|
@@ -392,9 +442,14 @@ Default prompts should be concise but operational.
|
|
|
392
442
|
|
|
393
443
|
Selection:
|
|
394
444
|
|
|
395
|
-
-
|
|
396
|
-
-
|
|
397
|
-
-
|
|
445
|
+
- choose an ordered queue from `available_work_items`
|
|
446
|
+
- treat candidates agnostically: they may be plans, PRDs, or tasks
|
|
447
|
+
- prefer the most actionable items based on candidate titles and excerpts
|
|
448
|
+
- if a plan references concrete candidate tasks, choose the concrete task when
|
|
449
|
+
that is the best next work
|
|
450
|
+
- avoid keys already present in `seen_work_item_keys` or
|
|
451
|
+
`completed_work_item_keys`
|
|
452
|
+
- optionally include `selection_groups` to present related work by plan or PRD
|
|
398
453
|
- return `selected` or `no_work`
|
|
399
454
|
|
|
400
455
|
Execution:
|
|
@@ -429,17 +484,18 @@ Closure:
|
|
|
429
484
|
- model name
|
|
430
485
|
- reasoning level
|
|
431
486
|
- max iterations
|
|
432
|
-
- work item source template: `local
|
|
487
|
+
- work item source template: `local` or `github`
|
|
433
488
|
|
|
434
|
-
For `local
|
|
489
|
+
For `local`, init asks for a work item path.
|
|
490
|
+
For `github`, init asks for a repository in `owner/repo` format.
|
|
435
491
|
|
|
436
492
|
Default path selection:
|
|
437
493
|
|
|
438
494
|
- use `issues/` if it exists
|
|
439
495
|
- otherwise suggest `.nyxagent/tasks/`
|
|
440
496
|
|
|
441
|
-
If the chosen local
|
|
442
|
-
sample
|
|
497
|
+
If the chosen local work item path does not exist, init creates the directory but
|
|
498
|
+
does not create sample work items.
|
|
443
499
|
|
|
444
500
|
## Implementation Stack
|
|
445
501
|
|
|
@@ -479,7 +535,7 @@ src/
|
|
|
479
535
|
|
|
480
536
|
- `nyxagent resume`
|
|
481
537
|
- `nyxagent import-tasks`
|
|
482
|
-
- tracker adapters
|
|
538
|
+
- additional tracker adapters
|
|
483
539
|
- explicit Git commit adapter
|
|
484
540
|
- stricter artifact redaction
|
|
485
541
|
- richer phase retry policy
|
package/package.json
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
Select
|
|
1
|
+
Select the ordered work item queue for this run.
|
|
2
2
|
|
|
3
|
-
Use
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `available_work_items` from the runtime contract as the complete inventory
|
|
4
|
+
for selection. Some candidates may be plans, PRDs, or concrete tasks. Build the
|
|
5
|
+
most actionable queue for this run, up to `workflow.max_iterations` items.
|
|
6
6
|
|
|
7
|
-
If
|
|
7
|
+
If candidates appear to belong to the same plan or PRD, keep related concrete
|
|
8
|
+
tasks together and use `selection_groups` to describe the grouping. The grouping
|
|
9
|
+
is only for user review; every executable item must still be included in
|
|
10
|
+
`work_items`.
|
|
11
|
+
|
|
12
|
+
Prefer concrete tasks over their parent plan when both are present and the task
|
|
13
|
+
is ready to execute. Choose the plan itself only when it is the best next work.
|
|
14
|
+
If no candidate is exploitable, return `no_work`.
|
|
15
|
+
|
|
16
|
+
Do not choose any key listed in `seen_work_item_keys` or
|
|
17
|
+
`completed_work_item_keys`. Every selected work item must be copied from
|
|
18
|
+
`available_work_items`; do not invent keys.
|
|
19
|
+
|
|
20
|
+
The selected work item identities must match the inventory entries:
|
|
21
|
+
|
|
22
|
+
- local: `source.type` is `local`, `source.locator` is the project-relative
|
|
23
|
+
markdown path, and `key` is `local:<source.locator>`.
|
|
24
|
+
- github: `source.type` is `github`, `source.locator` is
|
|
25
|
+
`owner/repo#number` for the configured repository, and `key` is
|
|
26
|
+
`github:<source.locator>`.
|
|
8
27
|
|
|
9
28
|
Return one of these outcomes:
|
|
10
29
|
|
|
11
|
-
- `selected`: include
|
|
30
|
+
- `selected`: include ordered `work_items`; optionally include
|
|
31
|
+
`selection_groups` with `title`, optional `kind`, and `work_item_keys`
|
|
12
32
|
- `no_work`: include a short `reason`
|
|
13
33
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- `github:owner/repo#42`
|
|
17
|
-
- `local:issues/TASK-0007.md`
|
|
34
|
+
For compatibility, `work_item` is still accepted for a single selected item, but
|
|
35
|
+
new results should use `work_items`.
|
|
18
36
|
|
|
19
37
|
Do not modify project files or task files during selection.
|