voratiq 0.1.0-beta.3 → 0.1.0-beta.4
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/bin.js +7 -0
- package/dist/cli/review.d.ts +1 -0
- package/dist/cli/review.js +35 -5
- package/dist/commands/review/command.d.ts +6 -0
- package/dist/commands/review/command.js +199 -2
- package/dist/commands/review/errors.d.ts +14 -0
- package/dist/commands/review/errors.js +27 -0
- package/dist/commands/review/manifest.d.ts +46 -0
- package/dist/commands/review/manifest.js +94 -0
- package/dist/commands/review/prompt.d.ts +13 -0
- package/dist/commands/review/prompt.js +74 -0
- package/dist/commands/run/shim/run-agent-shim.js +46 -2
- package/dist/commands/shared/preview.d.ts +5 -0
- package/dist/commands/shared/preview.js +32 -0
- package/dist/commands/shared/prune.d.ts +1 -0
- package/dist/commands/shared/prune.js +4 -0
- package/dist/commands/shared/session-id.d.ts +1 -0
- package/dist/commands/{spec/id.js → shared/session-id.js} +1 -1
- package/dist/commands/spec/command.js +6 -8
- package/dist/commands/spec/preview.js +2 -21
- package/dist/commands/spec/prompt.d.ts +2 -2
- package/dist/commands/spec/prompt.js +3 -3
- package/dist/evals/runner.js +0 -2
- package/dist/preflight/index.d.ts +2 -0
- package/dist/preflight/index.js +3 -1
- package/dist/render/transcripts/review.d.ts +6 -2
- package/dist/render/transcripts/review.js +16 -32
- package/dist/render/utils/records.js +4 -4
- package/dist/render/utils/timezone.d.ts +3 -0
- package/dist/render/utils/timezone.js +45 -0
- package/dist/reviews/records/persistence.d.ts +27 -0
- package/dist/reviews/records/persistence.js +118 -0
- package/dist/reviews/records/types.d.ts +24 -0
- package/dist/reviews/records/types.js +32 -0
- package/dist/workspace/setup.js +28 -1
- package/dist/workspace/structure.d.ts +4 -0
- package/dist/workspace/structure.js +4 -0
- package/package.json +1 -1
- package/dist/commands/spec/id.d.ts +0 -1
package/dist/bin.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createRunCommand } from "./cli/run.js";
|
|
|
16
16
|
import { createSpecCommand } from "./cli/spec.js";
|
|
17
17
|
import { terminateActiveRun } from "./commands/run/lifecycle.js";
|
|
18
18
|
import { renderCliError } from "./render/utils/errors.js";
|
|
19
|
+
import { flushAllReviewRecordBuffers } from "./reviews/records/persistence.js";
|
|
19
20
|
import { flushAllRunRecordBuffers } from "./runs/records/persistence.js";
|
|
20
21
|
import { flushAllSpecRecordBuffers } from "./specs/records/persistence.js";
|
|
21
22
|
import { toErrorMessage } from "./utils/errors.js";
|
|
@@ -75,6 +76,12 @@ async function flushPendingHistory() {
|
|
|
75
76
|
catch (error) {
|
|
76
77
|
console.warn(`[voratiq] Failed to flush run history buffers: ${error.message}`);
|
|
77
78
|
}
|
|
79
|
+
try {
|
|
80
|
+
await flushAllReviewRecordBuffers();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.warn(`[voratiq] Failed to flush review history buffers: ${error.message}`);
|
|
84
|
+
}
|
|
78
85
|
try {
|
|
79
86
|
await flushAllSpecRecordBuffers();
|
|
80
87
|
}
|
package/dist/cli/review.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { type ReviewCommandResult as ReviewExecutionResult } from "../commands/review/command.js";
|
|
3
3
|
export interface ReviewCommandOptions {
|
|
4
4
|
runId: string;
|
|
5
|
+
agentId?: string;
|
|
5
6
|
}
|
|
6
7
|
export interface ReviewCommandResult extends ReviewExecutionResult {
|
|
7
8
|
body: string;
|
package/dist/cli/review.js
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { Command } from "commander";
|
|
4
|
+
import { checkPlatformSupport } from "../agents/runtime/sandbox.js";
|
|
2
5
|
import { executeReviewCommand, } from "../commands/review/command.js";
|
|
3
|
-
import {
|
|
6
|
+
import { buildMarkdownPreviewLines } from "../commands/shared/preview.js";
|
|
7
|
+
import { ensureSandboxDependencies, resolveCliContext, } from "../preflight/index.js";
|
|
4
8
|
import { renderReviewTranscript } from "../render/transcripts/review.js";
|
|
5
9
|
import { writeCommandOutput } from "./output.js";
|
|
6
10
|
export async function runReviewCommand(options) {
|
|
7
|
-
const { runId } = options;
|
|
11
|
+
const { runId, agentId } = options;
|
|
8
12
|
const { root, workspacePaths } = await resolveCliContext();
|
|
13
|
+
checkPlatformSupport();
|
|
14
|
+
ensureSandboxDependencies();
|
|
15
|
+
writeCommandOutput({
|
|
16
|
+
alerts: [{ severity: "info", message: "Generating review..." }],
|
|
17
|
+
});
|
|
9
18
|
const execution = await executeReviewCommand({
|
|
10
19
|
root,
|
|
11
20
|
runsFilePath: workspacePaths.runsFile,
|
|
21
|
+
reviewsFilePath: workspacePaths.reviewsFile,
|
|
12
22
|
runId,
|
|
23
|
+
agentId,
|
|
24
|
+
});
|
|
25
|
+
let previewLines;
|
|
26
|
+
try {
|
|
27
|
+
const reviewContent = await readFile(resolve(root, execution.outputPath), "utf8");
|
|
28
|
+
previewLines = buildMarkdownPreviewLines(reviewContent);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
previewLines = undefined;
|
|
32
|
+
}
|
|
33
|
+
const body = renderReviewTranscript({
|
|
34
|
+
runId: execution.runRecord.runId,
|
|
35
|
+
outputPath: execution.outputPath,
|
|
36
|
+
previewLines,
|
|
37
|
+
...(execution.missingArtifacts.length > 0
|
|
38
|
+
? { missingArtifacts: execution.missingArtifacts }
|
|
39
|
+
: {}),
|
|
13
40
|
});
|
|
14
|
-
const body = renderReviewTranscript(execution.runRecord);
|
|
15
41
|
return {
|
|
16
42
|
...execution,
|
|
17
43
|
body,
|
|
@@ -19,11 +45,15 @@ export async function runReviewCommand(options) {
|
|
|
19
45
|
}
|
|
20
46
|
export function createReviewCommand() {
|
|
21
47
|
return new Command("review")
|
|
22
|
-
.description("
|
|
48
|
+
.description("Generate a one-shot, headless review of run artifacts")
|
|
23
49
|
.requiredOption("--run <run-id>", "Identifier of the recorded run")
|
|
50
|
+
.option("--agent <agent-id>", "Reviewer agent identifier")
|
|
24
51
|
.allowExcessArguments(false)
|
|
25
52
|
.action(async (options) => {
|
|
26
|
-
const result = await runReviewCommand({
|
|
53
|
+
const result = await runReviewCommand({
|
|
54
|
+
runId: options.run,
|
|
55
|
+
agentId: options.agent,
|
|
56
|
+
});
|
|
27
57
|
writeCommandOutput({
|
|
28
58
|
body: result.body,
|
|
29
59
|
stderr: result.stderr,
|
|
@@ -2,9 +2,15 @@ import type { RunRecordEnhanced } from "../../runs/records/enhanced.js";
|
|
|
2
2
|
export interface ReviewCommandInput {
|
|
3
3
|
root: string;
|
|
4
4
|
runsFilePath: string;
|
|
5
|
+
reviewsFilePath: string;
|
|
5
6
|
runId: string;
|
|
7
|
+
agentId?: string;
|
|
6
8
|
}
|
|
7
9
|
export interface ReviewCommandResult {
|
|
10
|
+
reviewId: string;
|
|
8
11
|
runRecord: RunRecordEnhanced;
|
|
12
|
+
agentId: string;
|
|
13
|
+
outputPath: string;
|
|
14
|
+
missingArtifacts: string[];
|
|
9
15
|
}
|
|
10
16
|
export declare function executeReviewCommand(input: ReviewCommandInput): Promise<ReviewCommandResult>;
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectAgentProcessFailureDetail } from "../../agents/runtime/failures.js";
|
|
4
|
+
import { runSandboxedAgent } from "../../agents/runtime/harness.js";
|
|
5
|
+
import { AgentNotFoundError } from "../../configs/agents/errors.js";
|
|
6
|
+
import { loadAgentById, loadAgentCatalog, } from "../../configs/agents/loader.js";
|
|
7
|
+
import { loadEnvironmentConfig } from "../../configs/environment/loader.js";
|
|
8
|
+
import { appendReviewRecord, finalizeReviewRecord, flushReviewRecordBuffer, } from "../../reviews/records/persistence.js";
|
|
1
9
|
import { buildRunRecordView } from "../../runs/records/enhanced.js";
|
|
2
10
|
import { RunRecordNotFoundError } from "../../runs/records/errors.js";
|
|
3
11
|
import { fetchRunsSafely } from "../../runs/records/persistence.js";
|
|
12
|
+
import { toErrorMessage } from "../../utils/errors.js";
|
|
13
|
+
import { normalizePathForDisplay, relativeToRoot } from "../../utils/path.js";
|
|
14
|
+
import { resolveRunWorkspacePaths, scaffoldAgentSessionWorkspace, } from "../../workspace/layout.js";
|
|
15
|
+
import { promoteWorkspaceFile } from "../../workspace/promotion.js";
|
|
16
|
+
import { REVIEW_ARTIFACT_INFO_FILENAME, REVIEW_FILENAME, VORATIQ_REVIEWS_DIR, } from "../../workspace/structure.js";
|
|
4
17
|
import { RunNotFoundCliError } from "../errors.js";
|
|
18
|
+
import { pruneWorkspace } from "../shared/prune.js";
|
|
19
|
+
import { generateSessionId } from "../shared/session-id.js";
|
|
20
|
+
import { ReviewAgentNotFoundError, ReviewGenerationFailedError, ReviewNoAgentsConfiguredError, } from "./errors.js";
|
|
21
|
+
import { buildReviewManifest } from "./manifest.js";
|
|
22
|
+
import { buildReviewPrompt } from "./prompt.js";
|
|
5
23
|
export async function executeReviewCommand(input) {
|
|
6
|
-
const { root, runsFilePath, runId } = input;
|
|
24
|
+
const { root, runsFilePath, reviewsFilePath, runId, agentId } = input;
|
|
7
25
|
const { records } = await fetchRunsSafely({
|
|
8
26
|
root,
|
|
9
27
|
runsFilePath,
|
|
@@ -22,5 +40,184 @@ export async function executeReviewCommand(input) {
|
|
|
22
40
|
const enhanced = await buildRunRecordView(runRecord, {
|
|
23
41
|
workspaceRoot: root,
|
|
24
42
|
});
|
|
25
|
-
|
|
43
|
+
const agent = resolveReviewAgent({ agentId, root });
|
|
44
|
+
const environment = loadEnvironmentConfig({ root });
|
|
45
|
+
const reviewId = generateSessionId();
|
|
46
|
+
const createdAt = new Date().toISOString();
|
|
47
|
+
const workspacePaths = await buildReviewWorkspace({
|
|
48
|
+
root,
|
|
49
|
+
reviewId,
|
|
50
|
+
agentId: agent.id,
|
|
51
|
+
});
|
|
52
|
+
const outputPath = normalizePathForDisplay(relativeToRoot(root, workspacePaths.reviewPath));
|
|
53
|
+
const record = {
|
|
54
|
+
sessionId: reviewId,
|
|
55
|
+
runId,
|
|
56
|
+
createdAt,
|
|
57
|
+
status: "running",
|
|
58
|
+
agentId: agent.id,
|
|
59
|
+
outputPath,
|
|
60
|
+
};
|
|
61
|
+
await appendReviewRecord({
|
|
62
|
+
root,
|
|
63
|
+
reviewsFilePath,
|
|
64
|
+
record,
|
|
65
|
+
});
|
|
66
|
+
const runWorkspaceAbsolute = resolveRunWorkspacePaths(root, runId).absolute;
|
|
67
|
+
let missingArtifacts = [];
|
|
68
|
+
try {
|
|
69
|
+
const buildManifestResult = await buildReviewManifest({
|
|
70
|
+
root,
|
|
71
|
+
run: enhanced,
|
|
72
|
+
});
|
|
73
|
+
const manifest = buildManifestResult.manifest;
|
|
74
|
+
missingArtifacts = buildManifestResult.missingArtifacts;
|
|
75
|
+
const artifactInfoWorkspacePath = join(workspacePaths.workspacePath, REVIEW_ARTIFACT_INFO_FILENAME);
|
|
76
|
+
await writeFile(artifactInfoWorkspacePath, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
});
|
|
79
|
+
const prompt = buildReviewPrompt({
|
|
80
|
+
runId: enhanced.runId,
|
|
81
|
+
runStatus: enhanced.status,
|
|
82
|
+
specPath: enhanced.spec.path,
|
|
83
|
+
baseRevisionSha: enhanced.baseRevisionSha,
|
|
84
|
+
createdAt: enhanced.createdAt,
|
|
85
|
+
completedAt: manifest.run.completedAt,
|
|
86
|
+
artifactInfoPath: REVIEW_ARTIFACT_INFO_FILENAME,
|
|
87
|
+
outputPath: REVIEW_FILENAME,
|
|
88
|
+
repoRootPath: root,
|
|
89
|
+
workspacePath: workspacePaths.workspacePath,
|
|
90
|
+
});
|
|
91
|
+
const result = await runSandboxedAgent({
|
|
92
|
+
root,
|
|
93
|
+
sessionId: reviewId,
|
|
94
|
+
agent,
|
|
95
|
+
prompt,
|
|
96
|
+
environment,
|
|
97
|
+
paths: {
|
|
98
|
+
agentRoot: workspacePaths.agentRoot,
|
|
99
|
+
workspacePath: workspacePaths.workspacePath,
|
|
100
|
+
sandboxHomePath: workspacePaths.sandboxHomePath,
|
|
101
|
+
runtimeManifestPath: workspacePaths.runtimeManifestPath,
|
|
102
|
+
sandboxSettingsPath: workspacePaths.sandboxSettingsPath,
|
|
103
|
+
runtimePath: workspacePaths.runtimePath,
|
|
104
|
+
artifactsPath: workspacePaths.artifactsPath,
|
|
105
|
+
stdoutPath: workspacePaths.stdoutPath,
|
|
106
|
+
stderrPath: workspacePaths.stderrPath,
|
|
107
|
+
},
|
|
108
|
+
captureChat: true,
|
|
109
|
+
extraWriteProtectedPaths: [runWorkspaceAbsolute],
|
|
110
|
+
extraReadProtectedPaths: [],
|
|
111
|
+
});
|
|
112
|
+
if (result.exitCode !== 0 || result.errorMessage) {
|
|
113
|
+
const detectedDetail = result.watchdog?.trigger && result.errorMessage
|
|
114
|
+
? result.errorMessage
|
|
115
|
+
: await detectAgentProcessFailureDetail({
|
|
116
|
+
provider: agent.provider,
|
|
117
|
+
stdoutPath: workspacePaths.stdoutPath,
|
|
118
|
+
stderrPath: workspacePaths.stderrPath,
|
|
119
|
+
});
|
|
120
|
+
const detail = detectedDetail ??
|
|
121
|
+
result.errorMessage ??
|
|
122
|
+
`Agent exited with code ${result.exitCode ?? "unknown"}`;
|
|
123
|
+
throw new ReviewGenerationFailedError([detail], [
|
|
124
|
+
`See stderr: ${normalizePathForDisplay(relativeToRoot(root, workspacePaths.stderrPath))}`,
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
await assertReviewOutputExists(root, workspacePaths, reviewId);
|
|
128
|
+
await promoteWorkspaceFile({
|
|
129
|
+
workspacePath: workspacePaths.workspacePath,
|
|
130
|
+
artifactsPath: workspacePaths.artifactsPath,
|
|
131
|
+
stagedRelativePath: REVIEW_FILENAME,
|
|
132
|
+
artifactRelativePath: REVIEW_FILENAME,
|
|
133
|
+
deleteStaged: true,
|
|
134
|
+
});
|
|
135
|
+
await finalizeReviewRecord({
|
|
136
|
+
root,
|
|
137
|
+
reviewsFilePath,
|
|
138
|
+
sessionId: reviewId,
|
|
139
|
+
status: "succeeded",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const detail = error instanceof ReviewGenerationFailedError &&
|
|
144
|
+
error.detailLines.length > 0
|
|
145
|
+
? error.detailLines.join("\n")
|
|
146
|
+
: toErrorMessage(error);
|
|
147
|
+
await finalizeReviewRecord({
|
|
148
|
+
root,
|
|
149
|
+
reviewsFilePath,
|
|
150
|
+
sessionId: reviewId,
|
|
151
|
+
status: "failed",
|
|
152
|
+
error: detail,
|
|
153
|
+
}).catch(() => { });
|
|
154
|
+
await flushReviewRecordBuffer({
|
|
155
|
+
reviewsFilePath,
|
|
156
|
+
sessionId: reviewId,
|
|
157
|
+
}).catch(() => { });
|
|
158
|
+
await pruneWorkspace(workspacePaths.workspacePath);
|
|
159
|
+
if (error instanceof ReviewGenerationFailedError) {
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
throw new ReviewGenerationFailedError([detail], [
|
|
163
|
+
`See stderr: ${normalizePathForDisplay(relativeToRoot(root, workspacePaths.stderrPath))}`,
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
await flushReviewRecordBuffer({
|
|
167
|
+
reviewsFilePath,
|
|
168
|
+
sessionId: reviewId,
|
|
169
|
+
});
|
|
170
|
+
await pruneWorkspace(workspacePaths.workspacePath);
|
|
171
|
+
return {
|
|
172
|
+
reviewId,
|
|
173
|
+
runRecord: enhanced,
|
|
174
|
+
agentId: agent.id,
|
|
175
|
+
outputPath,
|
|
176
|
+
missingArtifacts,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function resolveReviewAgent(options) {
|
|
180
|
+
const { agentId, root } = options;
|
|
181
|
+
if (agentId) {
|
|
182
|
+
try {
|
|
183
|
+
return loadAgentById(agentId, { root });
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error instanceof AgentNotFoundError) {
|
|
187
|
+
throw new ReviewAgentNotFoundError(error.agentId);
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const catalog = loadAgentCatalog({ root });
|
|
193
|
+
const first = catalog[0];
|
|
194
|
+
if (!first) {
|
|
195
|
+
throw new ReviewNoAgentsConfiguredError();
|
|
196
|
+
}
|
|
197
|
+
return first;
|
|
198
|
+
}
|
|
199
|
+
async function buildReviewWorkspace(options) {
|
|
200
|
+
const { root, reviewId, agentId } = options;
|
|
201
|
+
return await scaffoldAgentSessionWorkspace({
|
|
202
|
+
root,
|
|
203
|
+
domain: VORATIQ_REVIEWS_DIR,
|
|
204
|
+
sessionId: reviewId,
|
|
205
|
+
agentId,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function assertReviewOutputExists(root, workspacePaths, reviewId) {
|
|
209
|
+
const stagedPath = join(workspacePaths.workspacePath, REVIEW_FILENAME);
|
|
210
|
+
try {
|
|
211
|
+
const contents = await readFile(stagedPath, "utf8");
|
|
212
|
+
if (contents.trim().length > 0) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const detail = toErrorMessage(error);
|
|
218
|
+
const stderrDisplay = normalizePathForDisplay(relativeToRoot(root, workspacePaths.stderrPath));
|
|
219
|
+
throw new ReviewGenerationFailedError([`Missing output: ${REVIEW_FILENAME}`], [`Review session: ${reviewId}`, detail, `See stderr: ${stderrDisplay}`]);
|
|
220
|
+
}
|
|
221
|
+
const stderrDisplay = normalizePathForDisplay(relativeToRoot(root, workspacePaths.stderrPath));
|
|
222
|
+
throw new ReviewGenerationFailedError([`Missing output: ${REVIEW_FILENAME}`], [`Review session: ${reviewId}`, `See stderr: ${stderrDisplay}`]);
|
|
26
223
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CliError } from "../../cli/errors.js";
|
|
2
|
+
export declare class ReviewError extends CliError {
|
|
3
|
+
constructor(headline: string, detailLines?: readonly string[], hintLines?: readonly string[]);
|
|
4
|
+
}
|
|
5
|
+
export declare class ReviewAgentNotFoundError extends ReviewError {
|
|
6
|
+
readonly agentId: string;
|
|
7
|
+
constructor(agentId: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class ReviewNoAgentsConfiguredError extends ReviewError {
|
|
10
|
+
constructor();
|
|
11
|
+
}
|
|
12
|
+
export declare class ReviewGenerationFailedError extends ReviewError {
|
|
13
|
+
constructor(detailLines?: readonly string[], hintLines?: readonly string[]);
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CliError } from "../../cli/errors.js";
|
|
2
|
+
export class ReviewError extends CliError {
|
|
3
|
+
constructor(headline, detailLines = [], hintLines = []) {
|
|
4
|
+
super(headline, detailLines, hintLines);
|
|
5
|
+
this.name = "ReviewError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class ReviewAgentNotFoundError extends ReviewError {
|
|
9
|
+
agentId;
|
|
10
|
+
constructor(agentId) {
|
|
11
|
+
super(`Agent "${agentId}" not found in agents.yaml.`, [], ["To add this agent, edit `.voratiq/agents.yaml`."]);
|
|
12
|
+
this.agentId = agentId;
|
|
13
|
+
this.name = "ReviewAgentNotFoundError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class ReviewNoAgentsConfiguredError extends ReviewError {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("No enabled agents found in `.voratiq/agents.yaml`.");
|
|
19
|
+
this.name = "ReviewNoAgentsConfiguredError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ReviewGenerationFailedError extends ReviewError {
|
|
23
|
+
constructor(detailLines = [], hintLines = []) {
|
|
24
|
+
super("Review generation failed.", detailLines, hintLines);
|
|
25
|
+
this.name = "ReviewGenerationFailedError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RunRecordEnhanced } from "../../runs/records/enhanced.js";
|
|
2
|
+
export type ReviewArtifactDescriptor = {
|
|
3
|
+
path: string;
|
|
4
|
+
exists: boolean;
|
|
5
|
+
};
|
|
6
|
+
export interface ReviewManifest {
|
|
7
|
+
version: number;
|
|
8
|
+
run: {
|
|
9
|
+
runId: string;
|
|
10
|
+
status: string;
|
|
11
|
+
specPath: string;
|
|
12
|
+
baseRevisionSha: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
completedAt?: string;
|
|
15
|
+
};
|
|
16
|
+
agents: Array<{
|
|
17
|
+
agentId: string;
|
|
18
|
+
status: string;
|
|
19
|
+
commitSha?: string;
|
|
20
|
+
diffStatistics?: string;
|
|
21
|
+
artifacts: {
|
|
22
|
+
diff?: ReviewArtifactDescriptor;
|
|
23
|
+
chat?: ReviewArtifactDescriptor;
|
|
24
|
+
summary?: ReviewArtifactDescriptor;
|
|
25
|
+
stdout?: ReviewArtifactDescriptor;
|
|
26
|
+
stderr?: ReviewArtifactDescriptor;
|
|
27
|
+
};
|
|
28
|
+
evals: Array<{
|
|
29
|
+
slug: string;
|
|
30
|
+
status: string;
|
|
31
|
+
log?: ReviewArtifactDescriptor;
|
|
32
|
+
error?: string;
|
|
33
|
+
exitCode?: number | null;
|
|
34
|
+
command?: string;
|
|
35
|
+
}>;
|
|
36
|
+
error?: string;
|
|
37
|
+
warnings?: string[];
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export declare function buildReviewManifest(options: {
|
|
41
|
+
root: string;
|
|
42
|
+
run: RunRecordEnhanced;
|
|
43
|
+
}): Promise<{
|
|
44
|
+
manifest: ReviewManifest;
|
|
45
|
+
missingArtifacts: string[];
|
|
46
|
+
}>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { pathExists } from "../../utils/fs.js";
|
|
3
|
+
import { resolvePath } from "../../utils/path.js";
|
|
4
|
+
export async function buildReviewManifest(options) {
|
|
5
|
+
const { root, run } = options;
|
|
6
|
+
const missingNames = [];
|
|
7
|
+
const seenMissing = new Set();
|
|
8
|
+
const noteMissing = (path) => {
|
|
9
|
+
const name = basename(path);
|
|
10
|
+
if (seenMissing.has(name)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
seenMissing.add(name);
|
|
14
|
+
missingNames.push(name);
|
|
15
|
+
};
|
|
16
|
+
const describe = async (repoRelativePath) => {
|
|
17
|
+
if (!repoRelativePath) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const exists = await pathExists(resolvePath(root, repoRelativePath));
|
|
21
|
+
if (!exists) {
|
|
22
|
+
noteMissing(repoRelativePath);
|
|
23
|
+
}
|
|
24
|
+
return { path: repoRelativePath, exists };
|
|
25
|
+
};
|
|
26
|
+
const completedAt = resolveRunCompletedAt(run);
|
|
27
|
+
const agents = await Promise.all(run.agents.map(async (agent) => {
|
|
28
|
+
const artifacts = agent.assets;
|
|
29
|
+
const diff = await describe(artifacts.diffPath);
|
|
30
|
+
const chat = await describe(artifacts.chatPath);
|
|
31
|
+
const summary = await describe(artifacts.summaryPath);
|
|
32
|
+
const stdout = await describe(artifacts.stdoutPath);
|
|
33
|
+
const stderr = await describe(artifacts.stderrPath);
|
|
34
|
+
const evals = await Promise.all(agent.evals.map(async (evaluation) => ({
|
|
35
|
+
slug: evaluation.slug,
|
|
36
|
+
status: evaluation.status,
|
|
37
|
+
...(typeof evaluation.exitCode === "number" ||
|
|
38
|
+
evaluation.exitCode === null
|
|
39
|
+
? { exitCode: evaluation.exitCode }
|
|
40
|
+
: {}),
|
|
41
|
+
...(evaluation.command ? { command: evaluation.command } : {}),
|
|
42
|
+
...(evaluation.error ? { error: evaluation.error } : {}),
|
|
43
|
+
...(evaluation.logPath
|
|
44
|
+
? { log: await describe(evaluation.logPath) }
|
|
45
|
+
: {}),
|
|
46
|
+
})));
|
|
47
|
+
return {
|
|
48
|
+
agentId: agent.agentId,
|
|
49
|
+
status: agent.status,
|
|
50
|
+
...(agent.commitSha ? { commitSha: agent.commitSha } : {}),
|
|
51
|
+
...(agent.diffStatistics
|
|
52
|
+
? { diffStatistics: agent.diffStatistics }
|
|
53
|
+
: {}),
|
|
54
|
+
artifacts: {
|
|
55
|
+
...(diff ? { diff } : {}),
|
|
56
|
+
...(chat ? { chat } : {}),
|
|
57
|
+
...(summary ? { summary } : {}),
|
|
58
|
+
...(stdout ? { stdout } : {}),
|
|
59
|
+
...(stderr ? { stderr } : {}),
|
|
60
|
+
},
|
|
61
|
+
evals,
|
|
62
|
+
...(agent.error ? { error: agent.error } : {}),
|
|
63
|
+
...(agent.warnings ? { warnings: agent.warnings } : {}),
|
|
64
|
+
};
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
manifest: {
|
|
68
|
+
version: 1,
|
|
69
|
+
run: {
|
|
70
|
+
runId: run.runId,
|
|
71
|
+
status: run.status,
|
|
72
|
+
specPath: run.spec.path,
|
|
73
|
+
baseRevisionSha: run.baseRevisionSha,
|
|
74
|
+
createdAt: run.createdAt,
|
|
75
|
+
...(completedAt ? { completedAt } : {}),
|
|
76
|
+
},
|
|
77
|
+
agents,
|
|
78
|
+
},
|
|
79
|
+
missingArtifacts: missingNames,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function resolveRunCompletedAt(run) {
|
|
83
|
+
let latest;
|
|
84
|
+
for (const agent of run.agents) {
|
|
85
|
+
const completedAt = agent.completedAt;
|
|
86
|
+
if (!completedAt) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!latest || completedAt > latest) {
|
|
90
|
+
latest = completedAt;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return latest;
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BuildReviewPromptOptions {
|
|
2
|
+
runId: string;
|
|
3
|
+
runStatus: string;
|
|
4
|
+
specPath: string;
|
|
5
|
+
baseRevisionSha: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
completedAt?: string;
|
|
8
|
+
artifactInfoPath: string;
|
|
9
|
+
outputPath: string;
|
|
10
|
+
repoRootPath: string;
|
|
11
|
+
workspacePath: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildReviewPrompt(options: BuildReviewPromptOptions): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { appendConstraints, appendOutputRequirements, } from "../shared/prompt-helpers.js";
|
|
2
|
+
export function buildReviewPrompt(options) {
|
|
3
|
+
const { runId, runStatus, specPath, baseRevisionSha, createdAt, completedAt, artifactInfoPath, outputPath, repoRootPath, workspacePath, } = options;
|
|
4
|
+
const lines = [
|
|
5
|
+
"You are the reviewer for a completed Voratiq run.",
|
|
6
|
+
"Your job is to judge the quality of each agent's implementation, compare approaches, and recommend what to apply (if anything).",
|
|
7
|
+
"",
|
|
8
|
+
"Review goal:",
|
|
9
|
+
"- Help a human decide whether to apply any agent output.",
|
|
10
|
+
"- Lead with code quality and spec coverage; evals are secondary signals.",
|
|
11
|
+
"",
|
|
12
|
+
"Inputs:",
|
|
13
|
+
`- Run id: ${runId}`,
|
|
14
|
+
`- Status: ${runStatus}`,
|
|
15
|
+
`- Spec path: ${specPath}`,
|
|
16
|
+
`- Base revision: ${baseRevisionSha}`,
|
|
17
|
+
`- Created at: ${createdAt}`,
|
|
18
|
+
...(completedAt ? [`- Completed at: ${completedAt}`] : []),
|
|
19
|
+
"",
|
|
20
|
+
`Run artifact information: \`${artifactInfoPath}\` (JSON, in the workspace root).`,
|
|
21
|
+
"- Use it as the index of what exists and where.",
|
|
22
|
+
"- If an artifact is missing or unreadable, call it out explicitly.",
|
|
23
|
+
"",
|
|
24
|
+
"Workflow (execute in order, no interaction):",
|
|
25
|
+
"1) Read the spec and capture key requirements/constraints.",
|
|
26
|
+
"2) Inspect each agent's artifacts listed in the artifact information (summary, diff, eval logs, stdout/stderr).",
|
|
27
|
+
"3) Evaluate implementation quality, spec coverage, and risks per agent.",
|
|
28
|
+
"4) Compare agents and rank them; weigh trade-offs.",
|
|
29
|
+
"5) Recommend whether to apply one, multiple, or none with explicit commands.",
|
|
30
|
+
"",
|
|
31
|
+
"Evaluation principles:",
|
|
32
|
+
"- Lead with code quality, clarity, and spec adherence.",
|
|
33
|
+
"- Treat evals as secondary diagnostic signals, not the primary decision driver.",
|
|
34
|
+
"- Prefer smaller, safer diffs when quality is otherwise comparable.",
|
|
35
|
+
"- Stay grounded in artifacts; cite file paths/behaviors.",
|
|
36
|
+
"- Call out follow-up work explicitly.",
|
|
37
|
+
"",
|
|
38
|
+
"Output template (use this structure):",
|
|
39
|
+
"",
|
|
40
|
+
`# Review of Run ${runId}`,
|
|
41
|
+
"",
|
|
42
|
+
"## Specification",
|
|
43
|
+
`**Path**: ${specPath}`,
|
|
44
|
+
"**Summary**: <1-2 sentence description of the spec>",
|
|
45
|
+
"",
|
|
46
|
+
"## Agent: <agent-id>",
|
|
47
|
+
"**Status**: <status>",
|
|
48
|
+
"**Quality**: High | Medium | Low",
|
|
49
|
+
"**Eval Signal**: <eval-slug> <status> | <eval-slug> <status> | … (use evals from the artifact information)",
|
|
50
|
+
"**Implementation Review**: <assess code quality, spec coverage, notable choices; cite eval insights as context>",
|
|
51
|
+
"**Follow-up Notes**: <issues, risks, or work needed post-apply>",
|
|
52
|
+
"<Repeat this section for each agent listed in the artifact information>",
|
|
53
|
+
"",
|
|
54
|
+
"## Comparison",
|
|
55
|
+
"<Synthesize differences and trade-offs across agents>",
|
|
56
|
+
"",
|
|
57
|
+
"## Risks / Missing Artifacts",
|
|
58
|
+
"<List missing or unreadable artifacts; explain impact>",
|
|
59
|
+
"",
|
|
60
|
+
"## Recommendation",
|
|
61
|
+
"**Preferred Agent(s)**: <agent-id(s) or `none`>",
|
|
62
|
+
"**Rationale**: <why these are best (or why none qualify)>",
|
|
63
|
+
"**Next Actions**:",
|
|
64
|
+
"<one line per recommendation, e.g. `voratiq apply --run <run-id> --agent <agent-id>`>",
|
|
65
|
+
];
|
|
66
|
+
appendConstraints(lines, {
|
|
67
|
+
readAccess: repoRootPath,
|
|
68
|
+
writeAccess: workspacePath,
|
|
69
|
+
});
|
|
70
|
+
appendOutputRequirements(lines, [
|
|
71
|
+
`- Save the full review to \`${outputPath}\` in the workspace root.`,
|
|
72
|
+
]);
|
|
73
|
+
return `${lines.join("\n")}\n`;
|
|
74
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { composeRestrictedEnvironment } from "../../../utils/env.js";
|
|
6
6
|
import { GIT_AUTHOR_EMAIL, GIT_AUTHOR_NAME, GIT_COMMITTER_EMAIL, GIT_COMMITTER_NAME, } from "../../../utils/git.js";
|
|
@@ -169,6 +169,7 @@ async function launchAgent(options) {
|
|
|
169
169
|
const workspacePath = isAbsolute(manifest.workspace)
|
|
170
170
|
? manifest.workspace
|
|
171
171
|
: resolvePath(agentRoot, manifest.workspace);
|
|
172
|
+
await writeShellProfiles(manifest.env);
|
|
172
173
|
const launchEnv = composeRestrictedEnvironment(manifest.env);
|
|
173
174
|
// Agent commits generated outside the sandbox reuse this persona via
|
|
174
175
|
// resolveSandboxPersona in src/commands/run/agents/lifecycle.ts.
|
|
@@ -214,6 +215,49 @@ async function launchAgent(options) {
|
|
|
214
215
|
});
|
|
215
216
|
});
|
|
216
217
|
}
|
|
218
|
+
async function writeShellProfiles(env) {
|
|
219
|
+
const homeDir = env.HOME;
|
|
220
|
+
if (!homeDir) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const bashrcPath = join(homeDir, ".bashrc");
|
|
224
|
+
const bashProfilePath = join(homeDir, ".bash_profile");
|
|
225
|
+
const exports = formatEnvExports(env);
|
|
226
|
+
const bashrcContents = `# Generated by Voratiq to preserve manifest env\n${exports}\n`;
|
|
227
|
+
const bashProfileContents = "# Generated by Voratiq to preserve manifest env\n" +
|
|
228
|
+
'if [ -f "$HOME/.bashrc" ]; then\n' +
|
|
229
|
+
' . "$HOME/.bashrc"\n' +
|
|
230
|
+
"fi\n";
|
|
231
|
+
try {
|
|
232
|
+
await mkdir(homeDir, { recursive: true });
|
|
233
|
+
await writeFile(bashrcPath, bashrcContents, { encoding: "utf8" });
|
|
234
|
+
await writeFile(bashProfilePath, bashProfileContents, {
|
|
235
|
+
encoding: "utf8",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Best-effort: shell profiles should not block agent startup.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function formatEnvExports(env) {
|
|
243
|
+
const lines = [];
|
|
244
|
+
const keys = Object.keys(env).sort((a, b) => a.localeCompare(b));
|
|
245
|
+
for (const key of keys) {
|
|
246
|
+
const value = env[key];
|
|
247
|
+
if (value === undefined) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
lines.push(`export ${key}=${shellEscape(value)}`);
|
|
251
|
+
}
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
254
|
+
function shellEscape(value) {
|
|
255
|
+
if (value.length === 0) {
|
|
256
|
+
return "''";
|
|
257
|
+
}
|
|
258
|
+
const singleQuoteEscape = "'\"'\"'";
|
|
259
|
+
return `'${value.replace(/'/g, singleQuoteEscape)}'`;
|
|
260
|
+
}
|
|
217
261
|
function logError(message) {
|
|
218
262
|
console.error(`[voratiq] ${message}`);
|
|
219
263
|
}
|