@nyxa/nyx-agent 0.3.5 → 0.4.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 +1 -0
- package/dist/cli.js +17 -0
- package/dist/commands/init.js +248 -41
- package/dist/commands/update.js +148 -0
- package/dist/config/schema.js +19 -0
- package/dist/runtime/buildPrompt.js +12 -50
- package/dist/runtime/gitLifecycle.js +109 -0
- package/dist/runtime/runPhase.js +8 -6
- package/dist/runtime/runWorkflow.js +157 -3
- package/docs/nyxagent-v0-spec.md +188 -3
- package/package.json +1 -1
- package/templates/default/prompts/closure.md +22 -8
- package/templates/default/prompts/finalize.md +7 -0
- package/templates/default/prompts/global-review.md +24 -0
- package/templates/default/prompts/global-revision.md +9 -0
- package/templates/default/prompts/pull-request.md +23 -0
- package/templates/default/prompts/revision.md +7 -0
- package/templates/default/schemas/closure.schema.json +35 -0
- package/templates/default/schemas/global-review.schema.json +60 -0
- package/templates/default/schemas/pull-request.schema.json +44 -0
|
@@ -13,6 +13,10 @@ async function buildRuntimeContract(input) {
|
|
|
13
13
|
const schemaText = input.phase.output_schema
|
|
14
14
|
? await readFile(resolveNyxPath(input.projectRoot, input.phase.output_schema, `output_schema for phase "${input.phase.id}"`), "utf8")
|
|
15
15
|
: undefined;
|
|
16
|
+
const workdir = typeof input.context.workdir === "string"
|
|
17
|
+
? input.context.workdir
|
|
18
|
+
: input.projectRoot;
|
|
19
|
+
const git = input.context.git;
|
|
16
20
|
const lines = [
|
|
17
21
|
"# NyxAgent Runtime Contract",
|
|
18
22
|
"",
|
|
@@ -20,57 +24,15 @@ async function buildRuntimeContract(input) {
|
|
|
20
24
|
"Follow the phase prompt, but obey this runtime contract first.",
|
|
21
25
|
"",
|
|
22
26
|
`Project root: ${input.projectRoot}`,
|
|
23
|
-
`
|
|
24
|
-
`Iteration dir: ${input.iterationDir}`,
|
|
25
|
-
`Phase dir: ${input.phaseDir}`,
|
|
26
|
-
`State file: ${input.stateFile}`,
|
|
27
|
-
`Phase id: ${input.phase.id}`,
|
|
28
|
-
"",
|
|
29
|
-
"Current state:",
|
|
30
|
-
"```json",
|
|
31
|
-
JSON.stringify(input.context.state ?? {}, null, 2),
|
|
32
|
-
"```",
|
|
33
|
-
"",
|
|
34
|
-
"Work item configuration:",
|
|
35
|
-
"```json",
|
|
36
|
-
JSON.stringify(input.config.work_items ?? {}, null, 2),
|
|
37
|
-
"```",
|
|
38
|
-
"",
|
|
39
|
-
"Available work items:",
|
|
40
|
-
"```json",
|
|
41
|
-
JSON.stringify(input.context.available_work_items ?? [], null, 2),
|
|
42
|
-
"```",
|
|
43
|
-
"",
|
|
44
|
-
"Recommended work item queue:",
|
|
45
|
-
"```json",
|
|
46
|
-
JSON.stringify(input.context.recommended_work_item_queue ?? [], null, 2),
|
|
47
|
-
"```",
|
|
48
|
-
"",
|
|
49
|
-
"Selected work item queue:",
|
|
50
|
-
"```json",
|
|
51
|
-
JSON.stringify(input.context.selected_work_item_queue ?? [], null, 2),
|
|
52
|
-
"```",
|
|
53
|
-
"",
|
|
54
|
-
"Work item annotations:",
|
|
55
|
-
"```json",
|
|
56
|
-
JSON.stringify(input.context.work_item_annotations ?? [], null, 2),
|
|
57
|
-
"```",
|
|
58
|
-
"",
|
|
59
|
-
"Seen work item keys:",
|
|
60
|
-
"```json",
|
|
61
|
-
JSON.stringify(input.context.seen_work_item_keys ?? [], null, 2),
|
|
62
|
-
"```",
|
|
63
|
-
"",
|
|
64
|
-
"Completed work item keys:",
|
|
65
|
-
"```json",
|
|
66
|
-
JSON.stringify(input.context.completed_work_item_keys ?? [], null, 2),
|
|
67
|
-
"```",
|
|
68
|
-
"",
|
|
69
|
-
"Last completed work item:",
|
|
70
|
-
"```json",
|
|
71
|
-
JSON.stringify(input.context.last_completed_work_item ?? null, null, 2),
|
|
72
|
-
"```"
|
|
27
|
+
`Working directory: ${workdir}`
|
|
73
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), "```");
|
|
74
36
|
if (input.phase.transitions) {
|
|
75
37
|
lines.push("", "Configured outcome transitions:", "```json", JSON.stringify(input.phase.transitions, null, 2), "```");
|
|
76
38
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import { renderTemplate } from "./renderTemplate.js";
|
|
5
|
+
/**
|
|
6
|
+
* Set up the run-scoped git context (one branch per run = one PRD = one PR).
|
|
7
|
+
*
|
|
8
|
+
* The engine only performs generic git plumbing (branch + worktree). All
|
|
9
|
+
* GitHub semantics (pushing, opening the PR, closing issues) stay in the
|
|
10
|
+
* phase prompts, keeping the engine agnostic.
|
|
11
|
+
*
|
|
12
|
+
* Returns `undefined` when git management is disabled (`mode = "off"`), in
|
|
13
|
+
* which case phases run in `projectRoot` exactly as before.
|
|
14
|
+
*/
|
|
15
|
+
export async function setUpGitContext(input) {
|
|
16
|
+
const mode = input.git.mode;
|
|
17
|
+
if (mode === "off") {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
await assertGitRepository(input.projectRoot);
|
|
21
|
+
const branch = sanitizeBranch(renderTemplate(input.git.branch_template, { run_id: input.runId }));
|
|
22
|
+
if (!branch) {
|
|
23
|
+
throw new Error(`[git].branch_template "${input.git.branch_template}" produced an empty branch name`);
|
|
24
|
+
}
|
|
25
|
+
const base = input.git.base ?? (await currentRef(input.projectRoot));
|
|
26
|
+
const branchExists = await refExists(input.projectRoot, branch);
|
|
27
|
+
if (mode === "branch") {
|
|
28
|
+
const args = branchExists
|
|
29
|
+
? ["checkout", branch]
|
|
30
|
+
: ["checkout", "-b", branch, base];
|
|
31
|
+
await runGit(input.projectRoot, args, "create branch");
|
|
32
|
+
return { mode, branch, base, worktree: input.projectRoot };
|
|
33
|
+
}
|
|
34
|
+
// mode === "worktree"
|
|
35
|
+
const worktree = path.resolve(input.projectRoot, input.git.worktree_dir, input.runId);
|
|
36
|
+
const args = branchExists
|
|
37
|
+
? ["worktree", "add", worktree, branch]
|
|
38
|
+
: ["worktree", "add", worktree, "-b", branch, base];
|
|
39
|
+
await runGit(input.projectRoot, args, "create worktree");
|
|
40
|
+
return { mode, branch, base, worktree };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Tear down a run-scoped git context. The branch is always kept (it holds the
|
|
44
|
+
* committed work and any pull request); only the worktree working directory is
|
|
45
|
+
* removed, according to the cleanup policy.
|
|
46
|
+
*/
|
|
47
|
+
export async function tearDownGitContext(input) {
|
|
48
|
+
if (input.context.mode !== "worktree") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const shouldRemove = input.cleanup === "always" ||
|
|
52
|
+
(input.cleanup === "on_success" && input.success);
|
|
53
|
+
if (!shouldRemove) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const removal = await execa("git", ["worktree", "remove", input.context.worktree, "--force"], { cwd: input.projectRoot, reject: false });
|
|
57
|
+
if (removal.exitCode !== 0) {
|
|
58
|
+
// Fall back to pruning the directory and the worktree registry so a failed
|
|
59
|
+
// run never leaves the next run unable to reuse the path.
|
|
60
|
+
await rm(input.context.worktree, { recursive: true, force: true });
|
|
61
|
+
await execa("git", ["worktree", "prune"], {
|
|
62
|
+
cwd: input.projectRoot,
|
|
63
|
+
reject: false
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function sanitizeBranch(name) {
|
|
68
|
+
return name
|
|
69
|
+
.trim()
|
|
70
|
+
.replace(/\s+/g, "-")
|
|
71
|
+
.replace(/[~^:?*[\]\\]/g, "-")
|
|
72
|
+
.replace(/\.{2,}/g, ".")
|
|
73
|
+
.replace(/\/{2,}/g, "/")
|
|
74
|
+
.replace(/-{2,}/g, "-")
|
|
75
|
+
.replace(/^[-/.]+/, "")
|
|
76
|
+
.replace(/[-/.]+$/, "");
|
|
77
|
+
}
|
|
78
|
+
async function assertGitRepository(cwd) {
|
|
79
|
+
const result = await execa("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
80
|
+
cwd,
|
|
81
|
+
reject: false
|
|
82
|
+
});
|
|
83
|
+
if (result.exitCode !== 0 || result.stdout.trim() !== "true") {
|
|
84
|
+
throw new Error(`[git].mode is enabled but ${cwd} is not a git repository. Run "git init" or set [git].mode = "off".`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function currentRef(cwd) {
|
|
88
|
+
const named = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
89
|
+
cwd,
|
|
90
|
+
reject: false
|
|
91
|
+
});
|
|
92
|
+
const name = named.stdout.trim();
|
|
93
|
+
if (named.exitCode === 0 && name && name !== "HEAD") {
|
|
94
|
+
return name;
|
|
95
|
+
}
|
|
96
|
+
const sha = await execa("git", ["rev-parse", "HEAD"], { cwd, reject: false });
|
|
97
|
+
return sha.stdout.trim();
|
|
98
|
+
}
|
|
99
|
+
async function refExists(cwd, ref) {
|
|
100
|
+
const result = await execa("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${ref}`], { cwd, reject: false });
|
|
101
|
+
return result.exitCode === 0;
|
|
102
|
+
}
|
|
103
|
+
async function runGit(cwd, args, label) {
|
|
104
|
+
const result = await execa("git", args, { cwd, reject: false });
|
|
105
|
+
if (result.exitCode !== 0) {
|
|
106
|
+
const detail = (result.stderr || result.stdout || "unknown error").trim();
|
|
107
|
+
throw new Error(`Failed to ${label} with git: ${detail}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/runtime/runPhase.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function runPhase(input) {
|
|
|
29
29
|
context,
|
|
30
30
|
config: input.config,
|
|
31
31
|
phase: input.phase,
|
|
32
|
-
|
|
32
|
+
workdir: input.workdir ?? input.projectRoot
|
|
33
33
|
});
|
|
34
34
|
if (attempt.exitCode !== 0) {
|
|
35
35
|
await writeAttemptMeta(attempt, {
|
|
@@ -93,7 +93,9 @@ function buildContext(input) {
|
|
|
93
93
|
phase_results: input.state.phase_results ?? {},
|
|
94
94
|
state: input.state,
|
|
95
95
|
model,
|
|
96
|
-
harness
|
|
96
|
+
harness,
|
|
97
|
+
workdir: input.workdir ?? input.projectRoot,
|
|
98
|
+
git: input.git ?? {}
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
async function runHarnessAttempt(input) {
|
|
@@ -107,13 +109,13 @@ async function runHarnessAttempt(input) {
|
|
|
107
109
|
const args = harness.args.map((arg) => renderTemplate(arg, input.context));
|
|
108
110
|
const startedAt = new Date().toISOString();
|
|
109
111
|
const started = Date.now();
|
|
110
|
-
const gitBefore = await getGitSnapshot(input.
|
|
112
|
+
const gitBefore = await getGitSnapshot(input.workdir);
|
|
111
113
|
let stdout = "";
|
|
112
114
|
let stderr = "";
|
|
113
115
|
let exitCode = 0;
|
|
114
116
|
try {
|
|
115
117
|
const result = await execa(command, args, {
|
|
116
|
-
cwd: input.
|
|
118
|
+
cwd: input.workdir,
|
|
117
119
|
input: input.prompt,
|
|
118
120
|
reject: false
|
|
119
121
|
});
|
|
@@ -127,7 +129,7 @@ async function runHarnessAttempt(input) {
|
|
|
127
129
|
}
|
|
128
130
|
const endedAt = new Date().toISOString();
|
|
129
131
|
const durationMs = Date.now() - started;
|
|
130
|
-
const gitAfter = await getGitSnapshot(input.
|
|
132
|
+
const gitAfter = await getGitSnapshot(input.workdir);
|
|
131
133
|
await writeText(path.join(input.attemptDir, "stdout.log"), stdout);
|
|
132
134
|
await writeText(path.join(input.attemptDir, "stderr.log"), stderr);
|
|
133
135
|
const attempt = {
|
|
@@ -250,7 +252,7 @@ async function repairStructuredResult(input) {
|
|
|
250
252
|
context: repairContext,
|
|
251
253
|
config: input.input.config,
|
|
252
254
|
phase: input.input.phase,
|
|
253
|
-
|
|
255
|
+
workdir: input.input.workdir ?? input.input.projectRoot
|
|
254
256
|
});
|
|
255
257
|
if (repairAttempt.exitCode !== 0) {
|
|
256
258
|
lastError = `Repair harness exited with code ${repairAttempt.exitCode}`;
|
|
@@ -4,6 +4,7 @@ import pc from "picocolors";
|
|
|
4
4
|
import { loadConfig } from "../config/loadConfig.js";
|
|
5
5
|
import { ensureDir, writeJson } from "./files.js";
|
|
6
6
|
import { getGitSnapshot } from "./git.js";
|
|
7
|
+
import { setUpGitContext, tearDownGitContext } from "./gitLifecycle.js";
|
|
7
8
|
import { markWorkItemCompleted, readWorkItemLedger, writeWorkItemLedger } from "./ledger.js";
|
|
8
9
|
import { getNyxDir, relativeToProject } from "./paths.js";
|
|
9
10
|
import { runPhase } from "./runPhase.js";
|
|
@@ -109,6 +110,53 @@ export async function runWorkflow(options) {
|
|
|
109
110
|
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
113
|
+
let gitContext;
|
|
114
|
+
if (config.git && config.git.mode !== "off") {
|
|
115
|
+
gitContext = await setUpGitContext({ projectRoot, git: config.git, runId });
|
|
116
|
+
}
|
|
117
|
+
if (gitContext) {
|
|
118
|
+
console.log(`Git: ${gitContext.mode} on "${gitContext.branch}" (base "${gitContext.base}")`);
|
|
119
|
+
if (gitContext.mode === "worktree") {
|
|
120
|
+
console.log(`Worktree: ${relativeToProject(projectRoot, gitContext.worktree)}`);
|
|
121
|
+
}
|
|
122
|
+
console.log("");
|
|
123
|
+
}
|
|
124
|
+
const cleanup = config.git?.cleanup ?? "on_success";
|
|
125
|
+
let runSucceeded = false;
|
|
126
|
+
try {
|
|
127
|
+
runSucceeded = await runSelectedQueue({
|
|
128
|
+
projectRoot,
|
|
129
|
+
nyxDir,
|
|
130
|
+
runDir,
|
|
131
|
+
config,
|
|
132
|
+
phasesById,
|
|
133
|
+
selection,
|
|
134
|
+
selectedWorkItems,
|
|
135
|
+
runState,
|
|
136
|
+
ledger,
|
|
137
|
+
workdir: gitContext?.worktree ?? projectRoot,
|
|
138
|
+
gitContext
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (gitContext) {
|
|
143
|
+
await tearDownGitContext({
|
|
144
|
+
projectRoot,
|
|
145
|
+
context: gitContext,
|
|
146
|
+
cleanup,
|
|
147
|
+
success: runSucceeded
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Run the confirmed work item queue and, when configured, the run-scoped final
|
|
154
|
+
* phase (e.g. opening the PRD pull request). Returns true when the whole run
|
|
155
|
+
* completed normally, false when it stopped early (stop_run).
|
|
156
|
+
*/
|
|
157
|
+
async function runSelectedQueue(input) {
|
|
158
|
+
const { projectRoot, nyxDir, runDir, config, phasesById, selection, selectedWorkItems, runState, workdir, gitContext } = input;
|
|
159
|
+
let ledger = input.ledger;
|
|
112
160
|
for (let queueIndex = 0; queueIndex < selectedWorkItems.length; queueIndex += 1) {
|
|
113
161
|
const iterationNumber = queueIndex + 1;
|
|
114
162
|
const workItem = selectedWorkItems[queueIndex];
|
|
@@ -148,7 +196,9 @@ export async function runWorkflow(options) {
|
|
|
148
196
|
iterationState,
|
|
149
197
|
runState,
|
|
150
198
|
iterationNumber,
|
|
151
|
-
maxIterations: selectedWorkItems.length
|
|
199
|
+
maxIterations: selectedWorkItems.length,
|
|
200
|
+
workdir,
|
|
201
|
+
git: gitContext
|
|
152
202
|
});
|
|
153
203
|
if (!phaseResult.ok) {
|
|
154
204
|
throw new Error(phaseResult.error);
|
|
@@ -164,7 +214,7 @@ export async function runWorkflow(options) {
|
|
|
164
214
|
console.log("");
|
|
165
215
|
console.log(`Done: ${runState.status}`);
|
|
166
216
|
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
167
|
-
return;
|
|
217
|
+
return false;
|
|
168
218
|
}
|
|
169
219
|
if (nextTarget === "stop_iteration" || nextTarget === "next_iteration") {
|
|
170
220
|
iterationState.status = "completed";
|
|
@@ -186,6 +236,23 @@ export async function runWorkflow(options) {
|
|
|
186
236
|
currentPhaseId = nextTarget;
|
|
187
237
|
}
|
|
188
238
|
}
|
|
239
|
+
if (config.workflow.final_phase) {
|
|
240
|
+
const finalized = await runFinalPhase({
|
|
241
|
+
projectRoot,
|
|
242
|
+
runDir,
|
|
243
|
+
config,
|
|
244
|
+
phasesById,
|
|
245
|
+
runState,
|
|
246
|
+
selectedWorkItems,
|
|
247
|
+
ledger,
|
|
248
|
+
workdir,
|
|
249
|
+
gitContext,
|
|
250
|
+
workItemAnnotations: selection.workItemAnnotations
|
|
251
|
+
});
|
|
252
|
+
if (!finalized) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
189
256
|
runState.status =
|
|
190
257
|
selectedWorkItems.length >= config.workflow.max_iterations
|
|
191
258
|
? "completed_max_iterations"
|
|
@@ -194,6 +261,91 @@ export async function runWorkflow(options) {
|
|
|
194
261
|
console.log("");
|
|
195
262
|
console.log(`Done: ${runState.status}`);
|
|
196
263
|
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Execute the run-scoped finalization flow after all iterations complete. This
|
|
268
|
+
* is a self-contained phase graph (symmetric to the iteration flow started by
|
|
269
|
+
* `entry_phase`): it begins at `workflow.final_phase` and follows each phase's
|
|
270
|
+
* `next`/`transitions` until it reaches a leaf phase (success) or `stop_run`
|
|
271
|
+
* (abort). This is where, for example, a review -> revision -> pull request
|
|
272
|
+
* chain runs. The engine only provides the branch/worktree context and
|
|
273
|
+
* run-level state; the GitHub/PR semantics live in the phase prompts, keeping
|
|
274
|
+
* the engine agnostic. A single leaf `final_phase` simply runs once.
|
|
275
|
+
*
|
|
276
|
+
* Returns true when finalization completed normally, false when a phase routed
|
|
277
|
+
* to `stop_run`.
|
|
278
|
+
*/
|
|
279
|
+
async function runFinalPhase(input) {
|
|
280
|
+
const finalPhaseId = input.config.workflow.final_phase;
|
|
281
|
+
if (!finalPhaseId) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
const finalizationDir = path.join(input.runDir, "finalization");
|
|
285
|
+
await ensureDir(finalizationDir);
|
|
286
|
+
const finalizationState = {
|
|
287
|
+
iteration: 0,
|
|
288
|
+
status: "running",
|
|
289
|
+
available_work_items: input.selectedWorkItems,
|
|
290
|
+
recommended_work_item_queue: input.selectedWorkItems,
|
|
291
|
+
selected_work_item_queue: input.selectedWorkItems,
|
|
292
|
+
work_item_annotations: input.workItemAnnotations,
|
|
293
|
+
seen_work_item_keys: [...input.runState.seen_work_item_keys],
|
|
294
|
+
completed_work_item_keys: [...input.ledger.completed_work_item_keys],
|
|
295
|
+
last_completed_work_item: input.ledger.last_completed_work_item,
|
|
296
|
+
phase_results: {},
|
|
297
|
+
phase_visit_counts: {}
|
|
298
|
+
};
|
|
299
|
+
const finalizationStateFile = path.join(finalizationDir, "state.json");
|
|
300
|
+
await writeJson(finalizationStateFile, finalizationState);
|
|
301
|
+
let currentPhaseId = finalPhaseId;
|
|
302
|
+
let lastResult;
|
|
303
|
+
while (currentPhaseId) {
|
|
304
|
+
const phase = input.phasesById.get(currentPhaseId);
|
|
305
|
+
if (!phase) {
|
|
306
|
+
throw new Error(`Unknown final phase "${currentPhaseId}"`);
|
|
307
|
+
}
|
|
308
|
+
const phaseResult = await runWorkflowPhase({
|
|
309
|
+
projectRoot: input.projectRoot,
|
|
310
|
+
runDir: input.runDir,
|
|
311
|
+
iterationDir: finalizationDir,
|
|
312
|
+
iterationStateFile: finalizationStateFile,
|
|
313
|
+
config: input.config,
|
|
314
|
+
phase,
|
|
315
|
+
iterationState: finalizationState,
|
|
316
|
+
runState: input.runState,
|
|
317
|
+
iterationNumber: "final",
|
|
318
|
+
maxIterations: input.selectedWorkItems.length,
|
|
319
|
+
workdir: input.workdir,
|
|
320
|
+
git: input.gitContext
|
|
321
|
+
});
|
|
322
|
+
if (!phaseResult.ok) {
|
|
323
|
+
throw new Error(phaseResult.error);
|
|
324
|
+
}
|
|
325
|
+
if (phaseResult.result !== undefined) {
|
|
326
|
+
lastResult = phaseResult.result;
|
|
327
|
+
}
|
|
328
|
+
const nextTarget = resolveNextTarget(phase, phaseResult.outcome);
|
|
329
|
+
if (nextTarget === "stop_run") {
|
|
330
|
+
finalizationState.status = "stopped";
|
|
331
|
+
input.runState.status = "stopped";
|
|
332
|
+
input.runState.final_result = phaseResult.result ?? { status: "stopped" };
|
|
333
|
+
await writeJson(finalizationStateFile, finalizationState);
|
|
334
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
335
|
+
console.log("");
|
|
336
|
+
console.log(`Done: ${input.runState.status}`);
|
|
337
|
+
console.log(`Run dir: ${relativeToProject(input.projectRoot, input.runDir)}`);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (nextTarget === "next_iteration" || nextTarget === "stop_iteration") {
|
|
341
|
+
throw new Error(`Final phase "${phase.id}" routed to iteration-only target "${nextTarget}"; finalization phases must end at a leaf phase (no next/transitions) or "stop_run"`);
|
|
342
|
+
}
|
|
343
|
+
currentPhaseId = nextTarget;
|
|
344
|
+
}
|
|
345
|
+
finalizationState.status = "completed";
|
|
346
|
+
input.runState.final_result = lastResult ?? { status: "ok" };
|
|
347
|
+
await writeJson(finalizationStateFile, finalizationState);
|
|
348
|
+
return true;
|
|
197
349
|
}
|
|
198
350
|
async function runSelectionPhase(input) {
|
|
199
351
|
const selectionPhase = input.phasesById.get(input.config.workflow.entry_phase);
|
|
@@ -303,7 +455,9 @@ async function runWorkflowPhase(input) {
|
|
|
303
455
|
stateFile: input.iterationStateFile,
|
|
304
456
|
config: input.config,
|
|
305
457
|
phase: input.phase,
|
|
306
|
-
state: input.iterationState
|
|
458
|
+
state: input.iterationState,
|
|
459
|
+
workdir: input.workdir,
|
|
460
|
+
git: input.git
|
|
307
461
|
});
|
|
308
462
|
if (!phaseResult.ok) {
|
|
309
463
|
console.log(pc.red("failed"));
|