@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.
Files changed (44) hide show
  1. package/README.md +58 -9
  2. package/dist/cli.js +13 -16
  3. package/dist/commands/init.js +112 -462
  4. package/dist/commands/run.js +17 -3
  5. package/dist/commands/update.js +1 -0
  6. package/dist/config/loadConfig.js +17 -3
  7. package/dist/config/schema.js +29 -146
  8. package/dist/runtime/files.js +1 -0
  9. package/dist/runtime/git.js +1 -0
  10. package/dist/runtime/gitLifecycle.js +19 -57
  11. package/dist/runtime/harness.js +26 -0
  12. package/dist/runtime/ledger.js +1 -0
  13. package/dist/runtime/paths.js +1 -12
  14. package/dist/runtime/prompts.js +103 -0
  15. package/dist/runtime/runPhase.js +85 -254
  16. package/dist/runtime/runPipeline.js +479 -0
  17. package/dist/runtime/schemas.js +52 -0
  18. package/dist/runtime/scm.js +80 -0
  19. package/dist/runtime/time.js +1 -0
  20. package/dist/runtime/validateResult.js +2 -3
  21. package/dist/runtime/workItems.js +43 -118
  22. package/package.json +2 -5
  23. package/dist/runtime/buildPrompt.js +0 -54
  24. package/dist/runtime/effectiveConfig.js +0 -14
  25. package/dist/runtime/renderTemplate.js +0 -28
  26. package/dist/runtime/runWorkflow.js +0 -680
  27. package/dist/runtime/validateWorkItem.js +0 -212
  28. package/dist/runtime/workItemAnnotations.js +0 -39
  29. package/docs/nyxagent-v0-spec.md +0 -742
  30. package/templates/default/prompts/closure.md +0 -30
  31. package/templates/default/prompts/execution.md +0 -11
  32. package/templates/default/prompts/finalize.md +0 -7
  33. package/templates/default/prompts/global-review.md +0 -24
  34. package/templates/default/prompts/global-revision.md +0 -9
  35. package/templates/default/prompts/pull-request.md +0 -23
  36. package/templates/default/prompts/repair-result.md +0 -29
  37. package/templates/default/prompts/review.md +0 -18
  38. package/templates/default/prompts/revision.md +0 -7
  39. package/templates/default/prompts/selection.md +0 -46
  40. package/templates/default/schemas/closure.schema.json +0 -35
  41. package/templates/default/schemas/global-review.schema.json +0 -60
  42. package/templates/default/schemas/pull-request.schema.json +0 -44
  43. package/templates/default/schemas/review.schema.json +0 -60
  44. package/templates/default/schemas/selection.schema.json +0 -135
@@ -1,122 +1,65 @@
1
1
  import path from "node:path";
2
2
  import { execa } from "execa";
3
- import { buildPhasePrompt, phaseRequiresStructuredResult } from "./buildPrompt.js";
4
- import { getEffectiveHarness, getEffectiveModel } from "./effectiveConfig.js";
5
- import { ensureDir, readText, writeJson, writeText } from "./files.js";
3
+ import { ensureDir, writeJson, writeText } from "./files.js";
6
4
  import { getGitSnapshot } from "./git.js";
7
- import { parseNyxAgentResult, getOutcome } from "./parseResult.js";
8
- import { resolveNyxPath } from "./paths.js";
9
- import { renderTemplate } from "./renderTemplate.js";
5
+ import { buildHarnessInvocation } from "./harness.js";
6
+ import { parseNyxAgentResult } from "./parseResult.js";
10
7
  import { validateAgainstSchema } from "./validateResult.js";
11
- import { validateWorkItemIdentity, validateWorkItemQueue } from "./validateWorkItem.js";
12
- export async function runPhase(input) {
13
- const phaseDir = path.join(input.iterationDir, "phases", input.phase.id);
14
- await ensureDir(phaseDir);
15
- const context = buildContext({ ...input, phaseDir });
16
- const prompt = await buildPhasePrompt({
17
- projectRoot: input.projectRoot,
18
- runDir: input.runDir,
19
- iterationDir: input.iterationDir,
20
- phaseDir,
21
- stateFile: input.stateFile,
22
- config: input.config,
23
- phase: input.phase,
24
- context
25
- });
26
- const attempt = await runHarnessAttempt({
27
- attemptDir: path.join(phaseDir, "attempt-001"),
28
- prompt,
29
- context,
30
- config: input.config,
31
- phase: input.phase,
32
- workdir: input.workdir ?? input.projectRoot
8
+ /**
9
+ * Run one agent phase: invoke the harness with the prompt on stdin, and — when a
10
+ * schema is given — parse, validate, and (on failure) repair the structured
11
+ * result. Decoupled from any workflow graph: the pipeline owns control flow.
12
+ */
13
+ export async function runAgentPhase(input) {
14
+ await ensureDir(input.phaseDir);
15
+ const attempt = await invokeHarness({
16
+ attemptDir: path.join(input.phaseDir, "attempt-001"),
17
+ input,
18
+ prompt: input.prompt
33
19
  });
34
20
  if (attempt.exitCode !== 0) {
35
- await writeAttemptMeta(attempt, {
36
- failure: `Harness exited with code ${attempt.exitCode}`
37
- });
38
21
  return {
39
22
  ok: false,
40
- error: `Phase "${input.phase.id}" failed with exit code ${attempt.exitCode}`
23
+ error: `Phase "${input.phaseId}" failed with exit code ${attempt.exitCode}`
41
24
  };
42
25
  }
43
- if (!phaseRequiresStructuredResult(input.phase)) {
44
- await writeAttemptMeta(attempt);
26
+ if (!input.schema) {
45
27
  return { ok: true };
46
28
  }
47
- const parsed = await parseAndValidatePhaseResult({
48
- input,
49
- phaseDir,
50
- attempt
51
- });
29
+ const parsed = parseAndValidate(input.schema, attempt.stdout);
52
30
  if (parsed.ok) {
31
+ await writeJson(path.join(input.phaseDir, "result.json"), parsed.result);
53
32
  return parsed;
54
33
  }
55
- await writeAttemptMeta(attempt, {
56
- result_error: parsed.error
57
- });
58
- const repaired = await repairStructuredResult({
34
+ const repaired = await repairResult({
59
35
  input,
60
- phaseDir,
61
- originalPrompt: prompt,
62
- originalAttempt: attempt,
36
+ originalStdout: attempt.stdout,
63
37
  validationError: parsed.error
64
38
  });
65
- if (repaired.ok) {
66
- return repaired;
39
+ if (repaired.ok && repaired.result !== undefined) {
40
+ await writeJson(path.join(input.phaseDir, "result.json"), repaired.result);
67
41
  }
68
- return {
69
- ok: false,
70
- error: repaired.error
71
- };
72
- }
73
- function buildContext(input) {
74
- const model = getEffectiveModel(input.config, input.phase);
75
- const harness = getEffectiveHarness(input.config, input.phase);
76
- return {
77
- project_root: input.projectRoot,
78
- run_dir: input.runDir,
79
- iteration_dir: input.iterationDir,
80
- phase_dir: input.phaseDir,
81
- state_file: input.stateFile,
82
- phase: input.phase,
83
- workflow: input.config.workflow,
84
- work_items: input.config.work_items ?? {},
85
- available_work_items: input.state.available_work_items ?? [],
86
- recommended_work_item_queue: input.state.recommended_work_item_queue ?? [],
87
- selected_work_item_queue: input.state.selected_work_item_queue ?? [],
88
- work_item_annotations: input.state.work_item_annotations ?? [],
89
- work_item: input.state.work_item ?? {},
90
- seen_work_item_keys: input.state.seen_work_item_keys ?? [],
91
- completed_work_item_keys: input.state.completed_work_item_keys ?? [],
92
- last_completed_work_item: input.state.last_completed_work_item ?? null,
93
- phase_results: input.state.phase_results ?? {},
94
- state: input.state,
95
- model,
96
- harness,
97
- workdir: input.workdir ?? input.projectRoot,
98
- git: input.git ?? {}
99
- };
42
+ return repaired;
100
43
  }
101
- async function runHarnessAttempt(input) {
102
- await ensureDir(input.attemptDir);
103
- await writeText(path.join(input.attemptDir, "prompt.md"), input.prompt);
104
- const harness = getEffectiveHarness(input.config, input.phase);
105
- if (harness.prompt_input !== "stdin") {
106
- throw new Error(`Unsupported prompt_input "${harness.prompt_input}"`);
107
- }
108
- const command = renderTemplate(harness.command, input.context);
109
- const args = harness.args.map((arg) => renderTemplate(arg, input.context));
44
+ async function invokeHarness(args) {
45
+ await ensureDir(args.attemptDir);
46
+ await writeText(path.join(args.attemptDir, "prompt.md"), args.prompt);
47
+ const invocation = buildHarnessInvocation({
48
+ harness: args.input.harness,
49
+ capability: args.forceReadonly ? "readonly" : args.input.capability,
50
+ model: args.input.model,
51
+ reasoning: args.input.reasoning
52
+ });
110
53
  const startedAt = new Date().toISOString();
111
54
  const started = Date.now();
112
- const gitBefore = await getGitSnapshot(input.workdir);
55
+ const gitBefore = await getGitSnapshot(args.input.workdir);
113
56
  let stdout = "";
114
57
  let stderr = "";
115
58
  let exitCode = 0;
116
59
  try {
117
- const result = await execa(command, args, {
118
- cwd: input.workdir,
119
- input: input.prompt,
60
+ const result = await execa(invocation.command, invocation.args, {
61
+ cwd: args.input.workdir,
62
+ input: args.prompt,
120
63
  reject: false
121
64
  });
122
65
  stdout = result.stdout;
@@ -127,186 +70,74 @@ async function runHarnessAttempt(input) {
127
70
  exitCode = 127;
128
71
  stderr = error instanceof Error ? error.message : String(error);
129
72
  }
130
- const endedAt = new Date().toISOString();
131
- const durationMs = Date.now() - started;
132
- const gitAfter = await getGitSnapshot(input.workdir);
133
- await writeText(path.join(input.attemptDir, "stdout.log"), stdout);
134
- await writeText(path.join(input.attemptDir, "stderr.log"), stderr);
135
- const attempt = {
136
- attemptDir: input.attemptDir,
137
- metaPath: path.join(input.attemptDir, "meta.json"),
138
- stdout,
139
- stderr,
140
- exitCode,
141
- command,
142
- args,
143
- startedAt,
144
- endedAt,
145
- durationMs,
146
- gitBefore,
147
- gitAfter
148
- };
149
- await writeAttemptMeta(attempt);
150
- return attempt;
151
- }
152
- async function writeAttemptMeta(attempt, extra = {}) {
153
- await writeJson(attempt.metaPath, {
154
- command: attempt.command,
155
- args: attempt.args,
156
- started_at: attempt.startedAt,
157
- ended_at: attempt.endedAt,
158
- duration_ms: attempt.durationMs,
159
- exit_code: attempt.exitCode,
160
- git_before: attempt.gitBefore,
161
- git_after: attempt.gitAfter,
162
- ...extra
73
+ const gitAfter = await getGitSnapshot(args.input.workdir);
74
+ await writeText(path.join(args.attemptDir, "stdout.log"), stdout);
75
+ await writeText(path.join(args.attemptDir, "stderr.log"), stderr);
76
+ await writeJson(path.join(args.attemptDir, "meta.json"), {
77
+ command: invocation.command,
78
+ args: invocation.args,
79
+ started_at: startedAt,
80
+ ended_at: new Date().toISOString(),
81
+ duration_ms: Date.now() - started,
82
+ exit_code: exitCode,
83
+ git_before: gitBefore,
84
+ git_after: gitAfter
163
85
  });
86
+ return { stdout, stderr, exitCode };
164
87
  }
165
- async function parseAndValidatePhaseResult(input) {
166
- const parsed = parseNyxAgentResult(input.attempt.stdout);
88
+ function parseAndValidate(schema, stdout) {
89
+ const parsed = parseNyxAgentResult(stdout);
167
90
  if (!parsed.ok) {
168
91
  return parsed;
169
92
  }
170
- if (input.input.phase.output_schema) {
171
- const schemaPath = resolveNyxPath(input.input.projectRoot, input.input.phase.output_schema, `output_schema for phase "${input.input.phase.id}"`);
172
- const validation = await validateAgainstSchema(schemaPath, parsed.value);
173
- if (!validation.ok) {
174
- return validation;
175
- }
176
- }
177
- if (input.input.phase.transitions && !getOutcome(parsed.value)) {
178
- return {
179
- ok: false,
180
- error: `Phase "${input.input.phase.id}" has transitions but result has no string outcome`
181
- };
182
- }
183
- const workItem = readObjectProperty(parsed.value, "work_item");
184
- if (workItem !== undefined) {
185
- const currentWorkItemKey = readObjectProperty(readObjectProperty(input.input.state, "work_item"), "key");
186
- const workItemValidation = validateWorkItemIdentity({
187
- config: input.input.config,
188
- workItem,
189
- availableWorkItems: readWorkItemCandidates(input.input.state.available_work_items),
190
- seenWorkItemKeys: readStringArray(input.input.state.seen_work_item_keys),
191
- completedWorkItemKeys: readStringArray(input.input.state.completed_work_item_keys),
192
- allowKnownKey: typeof currentWorkItemKey === "string" ? currentWorkItemKey : undefined
193
- });
194
- if (!workItemValidation.ok) {
195
- return workItemValidation;
196
- }
93
+ const validation = validateAgainstSchema(schema, parsed.value);
94
+ if (!validation.ok) {
95
+ return validation;
197
96
  }
198
- const workItems = readObjectProperty(parsed.value, "work_items");
199
- if (workItems !== undefined) {
200
- const workItemQueueValidation = validateWorkItemQueue({
201
- config: input.input.config,
202
- workItems,
203
- availableWorkItems: readWorkItemCandidates(input.input.state.available_work_items),
204
- seenWorkItemKeys: readStringArray(input.input.state.seen_work_item_keys),
205
- completedWorkItemKeys: readStringArray(input.input.state.completed_work_item_keys)
206
- });
207
- if (!workItemQueueValidation.ok) {
208
- return workItemQueueValidation;
209
- }
210
- }
211
- await writeJson(path.join(input.phaseDir, "result.json"), parsed.value);
212
- return {
213
- ok: true,
214
- result: parsed.value,
215
- outcome: getOutcome(parsed.value)
216
- };
97
+ return { ok: true, result: parsed.value };
217
98
  }
218
- async function repairStructuredResult(input) {
219
- const maxAttempts = input.input.config.repair.max_attempts;
220
- if (maxAttempts <= 0) {
221
- return {
222
- ok: false,
223
- error: input.validationError
224
- };
225
- }
226
- const repairPromptTemplate = await readText(resolveNyxPath(input.input.projectRoot, input.input.config.repair.prompt, "repair prompt"));
227
- let lastError = input.validationError;
99
+ async function repairResult(args) {
100
+ const maxAttempts = args.input.repairAttempts ?? 1;
101
+ let lastError = args.validationError;
228
102
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
229
- const repairContext = buildContext({
230
- ...input.input,
231
- phaseDir: input.phaseDir
232
- });
233
- const renderedRepairPrompt = renderTemplate(repairPromptTemplate, {
234
- ...repairContext,
235
- original_prompt: input.originalPrompt,
236
- original_stdout: input.originalAttempt.stdout,
237
- original_stderr: input.originalAttempt.stderr,
238
- validation_error: lastError
239
- });
240
103
  const repairPrompt = [
241
- "# NyxAgent Result Repair",
104
+ "# NyxAgent result repair",
105
+ "",
106
+ "Your previous response did not produce a valid structured result.",
107
+ "Do not redo the work and do not modify any files. Re-emit only the result.",
242
108
  "",
243
- "Repair only the structured result for the previous phase attempt.",
244
- "Do not modify project files. Do not redo the phase work.",
245
- "Return a valid result inside the final <nyxagent_result> block.",
109
+ "Validation error:",
110
+ "```",
111
+ lastError,
112
+ "```",
246
113
  "",
247
- renderedRepairPrompt
114
+ "Previous response:",
115
+ "```",
116
+ args.originalStdout,
117
+ "```",
118
+ "",
119
+ "Original prompt:",
120
+ "",
121
+ args.input.prompt
248
122
  ].join("\n");
249
- const repairAttempt = await runHarnessAttempt({
250
- attemptDir: path.join(input.phaseDir, `repair-${String(attemptNumber).padStart(3, "0")}`),
123
+ const attempt = await invokeHarness({
124
+ attemptDir: path.join(args.input.phaseDir, `repair-${String(attemptNumber).padStart(3, "0")}`),
125
+ input: args.input,
251
126
  prompt: repairPrompt,
252
- context: repairContext,
253
- config: input.input.config,
254
- phase: input.input.phase,
255
- workdir: input.input.workdir ?? input.input.projectRoot
127
+ forceReadonly: true
256
128
  });
257
- if (repairAttempt.exitCode !== 0) {
258
- lastError = `Repair harness exited with code ${repairAttempt.exitCode}`;
259
- await writeAttemptMeta(repairAttempt, {
260
- failure: lastError
261
- });
129
+ if (attempt.exitCode !== 0) {
130
+ lastError = `Repair harness exited with code ${attempt.exitCode}`;
262
131
  continue;
263
132
  }
264
- const repaired = await parseAndValidatePhaseResult({
265
- input: input.input,
266
- phaseDir: input.phaseDir,
267
- attempt: repairAttempt
268
- });
269
- if (repaired.ok) {
270
- return repaired;
133
+ const parsed = parseAndValidate(args.input.schema, attempt.stdout);
134
+ if (parsed.ok) {
135
+ return parsed;
271
136
  }
272
- lastError = repaired.error;
273
- await writeAttemptMeta(repairAttempt, {
274
- result_error: repaired.error
275
- });
137
+ lastError = parsed.error;
276
138
  }
277
139
  return {
278
140
  ok: false,
279
- error: lastError
141
+ error: `Phase "${args.input.phaseId}" produced an invalid result: ${lastError}`
280
142
  };
281
143
  }
282
- function readObjectProperty(value, key) {
283
- if (value &&
284
- typeof value === "object" &&
285
- Object.prototype.hasOwnProperty.call(value, key)) {
286
- return value[key];
287
- }
288
- return undefined;
289
- }
290
- function readStringArray(value) {
291
- if (!Array.isArray(value)) {
292
- return undefined;
293
- }
294
- return value.filter((item) => typeof item === "string");
295
- }
296
- function readWorkItemCandidates(value) {
297
- if (!Array.isArray(value)) {
298
- return undefined;
299
- }
300
- return value.filter(isWorkItemCandidate);
301
- }
302
- function isWorkItemCandidate(value) {
303
- const key = readObjectProperty(value, "key");
304
- const title = readObjectProperty(value, "title");
305
- const source = readObjectProperty(value, "source");
306
- const type = readObjectProperty(source, "type");
307
- const locator = readObjectProperty(source, "locator");
308
- return (typeof key === "string" &&
309
- typeof title === "string" &&
310
- (type === "local" || type === "github") &&
311
- typeof locator === "string");
312
- }