voratiq 0.1.0-beta.5 → 0.1.0-beta.7
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 +11 -6
- package/dist/cli/apply.d.ts +1 -0
- package/dist/cli/apply.js +4 -1
- package/dist/cli/prune.d.ts +4 -3
- package/dist/cli/prune.js +29 -5
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.js +28 -3
- package/dist/commands/apply/command.d.ts +1 -0
- package/dist/commands/apply/command.js +72 -3
- package/dist/commands/apply/errors.d.ts +16 -0
- package/dist/commands/apply/errors.js +36 -0
- package/dist/commands/apply/types.d.ts +1 -0
- package/dist/commands/prune/command.d.ts +2 -1
- package/dist/commands/prune/command.js +58 -2
- package/dist/commands/prune/types.d.ts +21 -0
- package/dist/commands/run/agents/run-context.js +3 -20
- package/dist/commands/run/command.js +1 -1
- package/dist/commands/run/lifecycle.d.ts +2 -2
- package/dist/commands/run/lifecycle.js +2 -2
- package/dist/commands/spec/command.js +5 -1
- package/dist/commands/spec/preview.js +7 -1
- package/dist/configs/agents/defaults.js +1 -1
- package/dist/configs/environment/detect.js +9 -4
- package/dist/preflight/branch.d.ts +9 -0
- package/dist/preflight/branch.js +48 -0
- package/dist/preflight/errors.d.ts +3 -0
- package/dist/preflight/errors.js +7 -0
- package/dist/render/transcripts/apply.js +8 -2
- package/dist/render/transcripts/init.js +16 -14
- package/dist/render/transcripts/prune.d.ts +7 -1
- package/dist/render/transcripts/prune.js +58 -11
- package/dist/render/utils/transcript.d.ts +7 -1
- package/dist/render/utils/transcript.js +12 -2
- package/dist/reviews/records/types.d.ts +3 -7
- package/dist/reviews/records/types.js +2 -5
- package/dist/runs/records/types.d.ts +5 -14
- package/dist/runs/records/types.js +4 -20
- package/dist/specs/records/types.d.ts +3 -11
- package/dist/specs/records/types.js +2 -14
- package/dist/status/index.d.ts +58 -7
- package/dist/status/index.js +81 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Voratiq
|
|
2
2
|
|
|
3
|
-
Run agents
|
|
3
|
+
Run coding agents against each other. Merge the winner.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
Voratiq is in public beta. Install via npm:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install -g voratiq
|
|
12
|
+
npm install -g voratiq
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
### Requirements
|
|
@@ -18,7 +18,7 @@ Core:
|
|
|
18
18
|
|
|
19
19
|
- Node 20+
|
|
20
20
|
- git
|
|
21
|
-
- 1+ AI coding agent (Claude [
|
|
21
|
+
- 1+ AI coding agent (Claude [>=2.0.55](https://github.com/anthropics/claude-code?tab=readme-ov-file#get-started), Codex [>=0.66.0](https://github.com/openai/codex?tab=readme-ov-file#quickstart), or Gemini [>=0.19.4](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#quick-install))
|
|
22
22
|
|
|
23
23
|
Platform-specific:
|
|
24
24
|
|
|
@@ -36,7 +36,10 @@ Note: Windows is not currently supported.
|
|
|
36
36
|
voratiq init
|
|
37
37
|
|
|
38
38
|
# Generate a spec
|
|
39
|
-
voratiq spec
|
|
39
|
+
voratiq spec \
|
|
40
|
+
--description "add dark mode toggle with localStorage persistence" \
|
|
41
|
+
--agent <agent-id> \
|
|
42
|
+
--yes
|
|
40
43
|
|
|
41
44
|
# Run agents in parallel
|
|
42
45
|
voratiq run --spec .voratiq/specs/add-dark-mode-toggle.md
|
|
@@ -48,7 +51,9 @@ voratiq review --run <run-id> --agent <agent-id>
|
|
|
48
51
|
voratiq apply --run <run-id> --agent <agent-id>
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
For a full walkthrough, see the [CLI tutorial](https://github.com/voratiq/voratiq/blob/main/docs/tutorial.md).
|
|
55
|
+
|
|
56
|
+
See the [docs](https://github.com/voratiq/voratiq/blob/main/docs/index.md) for core concepts, CLI reference, and configuration guides.
|
|
52
57
|
|
|
53
58
|
## License
|
|
54
59
|
|
package/dist/cli/apply.d.ts
CHANGED
package/dist/cli/apply.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ensureCleanWorkingTree, resolveCliContext, } from "../preflight/index.j
|
|
|
4
4
|
import { renderApplyTranscript } from "../render/transcripts/apply.js";
|
|
5
5
|
import { writeCommandOutput } from "./output.js";
|
|
6
6
|
export async function runApplyCommand(options) {
|
|
7
|
-
const { runId, agentId, ignoreBaseMismatch = false } = options;
|
|
7
|
+
const { runId, agentId, ignoreBaseMismatch = false, commit = false, } = options;
|
|
8
8
|
const { root, workspacePaths } = await resolveCliContext();
|
|
9
9
|
await ensureCleanWorkingTree(root);
|
|
10
10
|
const result = await executeApplyCommand({
|
|
@@ -13,6 +13,7 @@ export async function runApplyCommand(options) {
|
|
|
13
13
|
runId,
|
|
14
14
|
agentId,
|
|
15
15
|
ignoreBaseMismatch,
|
|
16
|
+
commit,
|
|
16
17
|
});
|
|
17
18
|
const body = renderApplyTranscript(result);
|
|
18
19
|
return { result, body };
|
|
@@ -23,12 +24,14 @@ export function createApplyCommand() {
|
|
|
23
24
|
.requiredOption("--run <run-id>", "Identifier of the recorded run")
|
|
24
25
|
.requiredOption("--agent <agent-id>", "Agent id to apply from the run")
|
|
25
26
|
.option("--ignore-base-mismatch", "Apply even if the current HEAD differs from the recorded base", () => true)
|
|
27
|
+
.option("--commit", "Commit the applied diff immediately using the agent summary", () => true)
|
|
26
28
|
.allowExcessArguments(false)
|
|
27
29
|
.action(async (options) => {
|
|
28
30
|
const result = await runApplyCommand({
|
|
29
31
|
runId: options.run,
|
|
30
32
|
agentId: options.agent,
|
|
31
33
|
ignoreBaseMismatch: options.ignoreBaseMismatch ?? false,
|
|
34
|
+
commit: options.commit ?? false,
|
|
32
35
|
});
|
|
33
36
|
writeCommandOutput({
|
|
34
37
|
body: result.body,
|
package/dist/cli/prune.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import type { PruneResult } from "../commands/prune/types.js";
|
|
2
|
+
import type { PruneAllResult, PruneResult } from "../commands/prune/types.js";
|
|
3
3
|
export interface PruneCommandOptions {
|
|
4
|
-
runId
|
|
4
|
+
runId?: string;
|
|
5
|
+
all?: boolean;
|
|
5
6
|
purge?: boolean;
|
|
6
7
|
yes?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface PruneCommandResult {
|
|
9
|
-
result: PruneResult;
|
|
10
|
+
result: PruneResult | PruneAllResult;
|
|
10
11
|
body: string;
|
|
11
12
|
exitCode?: number;
|
|
12
13
|
}
|
package/dist/cli/prune.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { executePruneCommand } from "../commands/prune/command.js";
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { executePruneAllCommand, executePruneCommand, } from "../commands/prune/command.js";
|
|
3
3
|
import { resolveCliContext } from "../preflight/index.js";
|
|
4
|
-
import { renderPruneTranscript } from "../render/transcripts/prune.js";
|
|
4
|
+
import { renderPruneAllTranscript, renderPruneTranscript, } from "../render/transcripts/prune.js";
|
|
5
5
|
import { createConfirmationWorkflow } from "./confirmation.js";
|
|
6
6
|
import { NonInteractiveShellError } from "./errors.js";
|
|
7
7
|
import { writeCommandOutput } from "./output.js";
|
|
8
8
|
export async function runPruneCommand(options) {
|
|
9
9
|
const { runId } = options;
|
|
10
|
+
const all = Boolean(options.all);
|
|
10
11
|
const purge = Boolean(options.purge);
|
|
11
12
|
const assumeYes = Boolean(options.yes);
|
|
12
13
|
const { root, workspacePaths } = await resolveCliContext();
|
|
@@ -17,6 +18,20 @@ export async function runPruneCommand(options) {
|
|
|
17
18
|
},
|
|
18
19
|
});
|
|
19
20
|
try {
|
|
21
|
+
if (all) {
|
|
22
|
+
const result = await executePruneAllCommand({
|
|
23
|
+
root,
|
|
24
|
+
runsDir: workspacePaths.runsDir,
|
|
25
|
+
runsFilePath: workspacePaths.runsFile,
|
|
26
|
+
confirm: confirmation.confirm,
|
|
27
|
+
purge,
|
|
28
|
+
});
|
|
29
|
+
const body = renderPruneAllTranscript(result);
|
|
30
|
+
return { result, body };
|
|
31
|
+
}
|
|
32
|
+
if (!runId) {
|
|
33
|
+
throw new Error("Expected --run <run-id> when --all is not set.");
|
|
34
|
+
}
|
|
20
35
|
const result = await executePruneCommand({
|
|
21
36
|
root,
|
|
22
37
|
runsDir: workspacePaths.runsDir,
|
|
@@ -35,14 +50,23 @@ export async function runPruneCommand(options) {
|
|
|
35
50
|
export function createPruneCommand() {
|
|
36
51
|
return new Command("prune")
|
|
37
52
|
.description("Remove artifacts for a recorded run")
|
|
38
|
-
.
|
|
53
|
+
.addOption(new Option("--run <run-id>", "Identifier of the run to delete").conflicts("all"))
|
|
54
|
+
.addOption(new Option("--all", "Prune all non-pruned runs").conflicts("run"))
|
|
39
55
|
.option("--purge", "Delete all associated configs and artifacts")
|
|
40
56
|
.option("-y, --yes", "Assume yes for all prompts")
|
|
41
57
|
.addHelpText("after", "\nThis command removes agent workspaces, deletes agent branches, and marks runs as pruned. \nPass --purge to also delete all associated configs and artifacts.")
|
|
42
58
|
.allowExcessArguments(false)
|
|
43
|
-
.action(async (options) => {
|
|
59
|
+
.action(async (options, command) => {
|
|
60
|
+
const hasRun = typeof options.run === "string" && options.run.length > 0;
|
|
61
|
+
const wantsAll = Boolean(options.all);
|
|
62
|
+
if (!hasRun && !wantsAll) {
|
|
63
|
+
command.error("error: either --run <run-id> or --all must be provided", {
|
|
64
|
+
exitCode: 1,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
44
67
|
const result = await runPruneCommand({
|
|
45
68
|
runId: options.run,
|
|
69
|
+
all: wantsAll,
|
|
46
70
|
purge: Boolean(options.purge),
|
|
47
71
|
yes: Boolean(options.yes),
|
|
48
72
|
});
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { RunReport } from "../runs/records/types.js";
|
|
|
3
3
|
export interface RunCommandOptions {
|
|
4
4
|
specPath: string;
|
|
5
5
|
maxParallel?: number;
|
|
6
|
+
branch?: boolean;
|
|
6
7
|
}
|
|
7
8
|
export interface RunCommandResult {
|
|
8
9
|
report: RunReport;
|
|
@@ -10,4 +11,13 @@ export interface RunCommandResult {
|
|
|
10
11
|
exitCode?: number;
|
|
11
12
|
}
|
|
12
13
|
export declare function runRunCommand(options: RunCommandOptions): Promise<RunCommandResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Derives a branch name from a spec file path by extracting the basename without extension.
|
|
16
|
+
*
|
|
17
|
+
* Examples:
|
|
18
|
+
* - `specs/separate-eval-outcomes.md` → `separate-eval-outcomes`
|
|
19
|
+
* - `specs/foo/bar.md` → `bar`
|
|
20
|
+
* - `my-feature.md` → `my-feature`
|
|
21
|
+
*/
|
|
22
|
+
export declare function deriveBranchNameFromSpecPath(specPath: string): string;
|
|
13
23
|
export declare function createRunCommand(): Command;
|
package/dist/cli/run.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { checkPlatformSupport } from "../agents/runtime/sandbox.js";
|
|
3
4
|
import { executeRunCommand } from "../commands/run/command.js";
|
|
5
|
+
import { checkoutOrCreateBranch } from "../preflight/branch.js";
|
|
4
6
|
import { ensureCleanWorkingTree, ensureSandboxDependencies, ensureSpecPath, resolveCliContext, } from "../preflight/index.js";
|
|
5
7
|
import { createRunRenderer } from "../render/transcripts/run.js";
|
|
6
8
|
import { parsePositiveInteger } from "../utils/validators.js";
|
|
7
9
|
import { writeCommandOutput } from "./output.js";
|
|
8
10
|
export async function runRunCommand(options) {
|
|
9
|
-
const { specPath, maxParallel } = options;
|
|
11
|
+
const { specPath, maxParallel, branch } = options;
|
|
10
12
|
const { root, workspacePaths } = await resolveCliContext();
|
|
11
13
|
checkPlatformSupport();
|
|
12
14
|
ensureSandboxDependencies();
|
|
13
15
|
await ensureCleanWorkingTree(root);
|
|
14
16
|
const { absolutePath, displayPath } = await ensureSpecPath(specPath, root);
|
|
17
|
+
if (branch) {
|
|
18
|
+
const branchName = deriveBranchNameFromSpecPath(displayPath);
|
|
19
|
+
await checkoutOrCreateBranch(root, branchName);
|
|
20
|
+
}
|
|
15
21
|
const renderer = createRunRenderer();
|
|
16
22
|
const report = await executeRunCommand({
|
|
17
23
|
root,
|
|
@@ -23,11 +29,28 @@ export async function runRunCommand(options) {
|
|
|
23
29
|
});
|
|
24
30
|
const body = renderer.complete(report);
|
|
25
31
|
// Unlike other commands, `run` signals a degraded outcome via exit code 1
|
|
26
|
-
// when any agent
|
|
32
|
+
// when any agent fails. Eval failures are quality signals displayed in the output
|
|
33
|
+
// but do not affect exit code. All other CLI commands either throw on error
|
|
27
34
|
// or return a clean success with exit code 0, so keep this deviation explicit.
|
|
28
|
-
const exitCode = report.hadAgentFailure
|
|
35
|
+
const exitCode = report.hadAgentFailure ? 1 : undefined;
|
|
29
36
|
return { report, body, exitCode };
|
|
30
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Derives a branch name from a spec file path by extracting the basename without extension.
|
|
40
|
+
*
|
|
41
|
+
* Examples:
|
|
42
|
+
* - `specs/separate-eval-outcomes.md` → `separate-eval-outcomes`
|
|
43
|
+
* - `specs/foo/bar.md` → `bar`
|
|
44
|
+
* - `my-feature.md` → `my-feature`
|
|
45
|
+
*/
|
|
46
|
+
export function deriveBranchNameFromSpecPath(specPath) {
|
|
47
|
+
const base = basename(specPath);
|
|
48
|
+
const lastDotIndex = base.lastIndexOf(".");
|
|
49
|
+
if (lastDotIndex <= 0) {
|
|
50
|
+
return base;
|
|
51
|
+
}
|
|
52
|
+
return base.slice(0, lastDotIndex);
|
|
53
|
+
}
|
|
31
54
|
function parseMaxParallelOption(value) {
|
|
32
55
|
return parsePositiveInteger(value, "Expected positive integer after --max-parallel", "--max-parallel must be greater than 0");
|
|
33
56
|
}
|
|
@@ -36,11 +59,13 @@ export function createRunCommand() {
|
|
|
36
59
|
.description("Execute configured agents against a spec")
|
|
37
60
|
.requiredOption("--spec <path>", "Path to the specification to execute")
|
|
38
61
|
.option("--max-parallel <count>", "Maximum number of agents to run concurrently", parseMaxParallelOption)
|
|
62
|
+
.option("--branch", "Checkout or create a branch named after the spec file")
|
|
39
63
|
.allowExcessArguments(false)
|
|
40
64
|
.action(async (options) => {
|
|
41
65
|
const runOptions = {
|
|
42
66
|
specPath: options.spec,
|
|
43
67
|
maxParallel: options.maxParallel,
|
|
68
|
+
branch: options.branch,
|
|
44
69
|
};
|
|
45
70
|
const result = await runRunCommand(runOptions);
|
|
46
71
|
writeCommandOutput({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { buildRunRecordEnhanced } from "../../runs/records/enhanced.js";
|
|
2
3
|
import { rewriteRunRecord } from "../../runs/records/persistence.js";
|
|
3
4
|
import { toErrorMessage } from "../../utils/errors.js";
|
|
@@ -5,9 +6,9 @@ import { ensureFileExists } from "../../utils/fs.js";
|
|
|
5
6
|
import { getGitStderr, getHeadRevision, runGitCommand, } from "../../utils/git.js";
|
|
6
7
|
import { resolveDisplayPath } from "../../utils/path.js";
|
|
7
8
|
import { fetchRunSafely } from "../fetch.js";
|
|
8
|
-
import { ApplyAgentDiffMissingOnDiskError, ApplyAgentDiffNotRecordedError, ApplyAgentNotFoundError, ApplyBaseMismatchError, ApplyPatchApplicationError, ApplyRunDeletedError, } from "./errors.js";
|
|
9
|
+
import { ApplyAgentDiffMissingOnDiskError, ApplyAgentDiffNotRecordedError, ApplyAgentNotFoundError, ApplyAgentSummaryEmptyError, ApplyAgentSummaryMissingOnDiskError, ApplyAgentSummaryNotRecordedError, ApplyBaseMismatchError, ApplyGitCommitError, ApplyPatchApplicationError, ApplyRunDeletedError, } from "./errors.js";
|
|
9
10
|
export async function executeApplyCommand(input) {
|
|
10
|
-
const { root, runsFilePath, runId, agentId, ignoreBaseMismatch } = input;
|
|
11
|
+
const { root, runsFilePath, runId, agentId, ignoreBaseMismatch, commit = false, } = input;
|
|
11
12
|
const runRecord = await fetchRunSafely({
|
|
12
13
|
root,
|
|
13
14
|
runsFilePath,
|
|
@@ -30,6 +31,8 @@ export async function executeApplyCommand(input) {
|
|
|
30
31
|
}
|
|
31
32
|
const diffAbsolutePath = resolveDisplayPath(root, diffDisplayPath) ?? diffDisplayPath;
|
|
32
33
|
await ensureFileExists(diffAbsolutePath, () => new ApplyAgentDiffMissingOnDiskError(diffDisplayPath));
|
|
34
|
+
const summaryRecorded = agentRecord.artifacts?.summaryCaptured ?? false;
|
|
35
|
+
const summaryDisplayPath = enhancedAgent.assets.summaryPath;
|
|
33
36
|
const headRevision = await getHeadRevision(root);
|
|
34
37
|
const baseRevisionSha = runRecord.baseRevisionSha;
|
|
35
38
|
const baseMismatch = headRevision !== baseRevisionSha;
|
|
@@ -63,6 +66,30 @@ export async function executeApplyCommand(input) {
|
|
|
63
66
|
}
|
|
64
67
|
throw error;
|
|
65
68
|
}
|
|
69
|
+
let appliedCommitSha;
|
|
70
|
+
if (commit) {
|
|
71
|
+
try {
|
|
72
|
+
appliedCommitSha = await commitAppliedDiff({
|
|
73
|
+
root,
|
|
74
|
+
runId,
|
|
75
|
+
agentId,
|
|
76
|
+
summaryRecorded,
|
|
77
|
+
summaryDisplayPath,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
await recordApplyStatus({
|
|
82
|
+
root,
|
|
83
|
+
runsFilePath,
|
|
84
|
+
runId,
|
|
85
|
+
agentId,
|
|
86
|
+
ignoredBaseMismatch,
|
|
87
|
+
status: "failed",
|
|
88
|
+
detail: extractCommitFailureDetail(error),
|
|
89
|
+
});
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
66
93
|
await recordApplyStatus({
|
|
67
94
|
root,
|
|
68
95
|
runsFilePath,
|
|
@@ -70,6 +97,7 @@ export async function executeApplyCommand(input) {
|
|
|
70
97
|
agentId,
|
|
71
98
|
ignoredBaseMismatch,
|
|
72
99
|
status: "succeeded",
|
|
100
|
+
appliedCommitSha,
|
|
73
101
|
});
|
|
74
102
|
return {
|
|
75
103
|
runId: runRecord.runId,
|
|
@@ -81,6 +109,7 @@ export async function executeApplyCommand(input) {
|
|
|
81
109
|
agent: agentRecord,
|
|
82
110
|
diffPath: diffDisplayPath,
|
|
83
111
|
ignoredBaseMismatch,
|
|
112
|
+
...(appliedCommitSha ? { appliedCommitSha } : {}),
|
|
84
113
|
};
|
|
85
114
|
}
|
|
86
115
|
async function applyPatch(options) {
|
|
@@ -96,7 +125,7 @@ async function applyPatch(options) {
|
|
|
96
125
|
}
|
|
97
126
|
}
|
|
98
127
|
async function recordApplyStatus(options) {
|
|
99
|
-
const { root, runsFilePath, runId, agentId, status, ignoredBaseMismatch, detail, } = options;
|
|
128
|
+
const { root, runsFilePath, runId, agentId, status, ignoredBaseMismatch, appliedCommitSha, detail, } = options;
|
|
100
129
|
const appliedAt = new Date().toISOString();
|
|
101
130
|
const normalizedDetail = typeof detail === "string" && detail.length > 0
|
|
102
131
|
? truncateDetail(detail)
|
|
@@ -115,6 +144,9 @@ async function recordApplyStatus(options) {
|
|
|
115
144
|
if (normalizedDetail !== undefined) {
|
|
116
145
|
applyStatus.detail = normalizedDetail;
|
|
117
146
|
}
|
|
147
|
+
if (typeof appliedCommitSha === "string" && appliedCommitSha.length > 0) {
|
|
148
|
+
applyStatus.appliedCommitSha = appliedCommitSha;
|
|
149
|
+
}
|
|
118
150
|
return {
|
|
119
151
|
...record,
|
|
120
152
|
applyStatus,
|
|
@@ -129,7 +161,44 @@ function extractApplyFailureDetail(error) {
|
|
|
129
161
|
}
|
|
130
162
|
return error.message;
|
|
131
163
|
}
|
|
164
|
+
function extractCommitFailureDetail(error) {
|
|
165
|
+
const stderr = getGitStderr(error);
|
|
166
|
+
if (stderr && stderr.trim().length > 0) {
|
|
167
|
+
const [firstLine] = stderr.split(/\r?\n/);
|
|
168
|
+
if (firstLine && firstLine.trim().length > 0) {
|
|
169
|
+
return firstLine.trim();
|
|
170
|
+
}
|
|
171
|
+
return stderr.trim();
|
|
172
|
+
}
|
|
173
|
+
return toErrorMessage(error);
|
|
174
|
+
}
|
|
132
175
|
function truncateDetail(detail) {
|
|
133
176
|
const trimmed = detail.trim();
|
|
134
177
|
return trimmed.length > 256 ? trimmed.slice(0, 256) : trimmed;
|
|
135
178
|
}
|
|
179
|
+
async function commitAppliedDiff(options) {
|
|
180
|
+
const { root, runId, agentId, summaryRecorded, summaryDisplayPath } = options;
|
|
181
|
+
if (!summaryRecorded || !summaryDisplayPath) {
|
|
182
|
+
throw new ApplyAgentSummaryNotRecordedError(runId, agentId);
|
|
183
|
+
}
|
|
184
|
+
const summaryAbsolutePath = resolveDisplayPath(root, summaryDisplayPath) ?? summaryDisplayPath;
|
|
185
|
+
await ensureFileExists(summaryAbsolutePath, () => new ApplyAgentSummaryMissingOnDiskError(summaryDisplayPath));
|
|
186
|
+
const rawSummary = await readFile(summaryAbsolutePath, "utf8");
|
|
187
|
+
const commitSubject = normalizeCommitSubject(rawSummary);
|
|
188
|
+
if (!commitSubject) {
|
|
189
|
+
throw new ApplyAgentSummaryEmptyError(summaryDisplayPath);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await runGitCommand(["add", "-A"], { cwd: root });
|
|
193
|
+
await runGitCommand(["commit", "-m", commitSubject], { cwd: root });
|
|
194
|
+
return await runGitCommand(["rev-parse", "HEAD"], { cwd: root });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
const detail = getGitStderr(error) ?? toErrorMessage(error);
|
|
198
|
+
throw new ApplyGitCommitError(detail);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function normalizeCommitSubject(summary) {
|
|
202
|
+
const normalized = summary.trim().replace(/\s+/gu, " ");
|
|
203
|
+
return normalized.length > 0 ? normalized : "";
|
|
204
|
+
}
|
|
@@ -23,6 +23,22 @@ export declare class ApplyAgentDiffMissingOnDiskError extends ApplyError {
|
|
|
23
23
|
readonly diffPath: string;
|
|
24
24
|
constructor(diffPath: string);
|
|
25
25
|
}
|
|
26
|
+
export declare class ApplyAgentSummaryNotRecordedError extends ApplyError {
|
|
27
|
+
readonly runId: string;
|
|
28
|
+
readonly agentId: string;
|
|
29
|
+
constructor(runId: string, agentId: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class ApplyAgentSummaryMissingOnDiskError extends ApplyError {
|
|
32
|
+
readonly summaryPath: string;
|
|
33
|
+
constructor(summaryPath: string);
|
|
34
|
+
}
|
|
35
|
+
export declare class ApplyAgentSummaryEmptyError extends ApplyError {
|
|
36
|
+
readonly summaryPath: string;
|
|
37
|
+
constructor(summaryPath: string);
|
|
38
|
+
}
|
|
39
|
+
export declare class ApplyGitCommitError extends ApplyError {
|
|
40
|
+
constructor(detail: string);
|
|
41
|
+
}
|
|
26
42
|
export interface ApplyBaseMismatchOptions {
|
|
27
43
|
baseRevisionSha: string;
|
|
28
44
|
headRevision: string;
|
|
@@ -53,6 +53,42 @@ export class ApplyAgentDiffMissingOnDiskError extends ApplyError {
|
|
|
53
53
|
this.name = "ApplyAgentDiffMissingOnDiskError";
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
export class ApplyAgentSummaryNotRecordedError extends ApplyError {
|
|
57
|
+
runId;
|
|
58
|
+
agentId;
|
|
59
|
+
constructor(runId, agentId) {
|
|
60
|
+
super(`Agent ${agentId} did not record a summary for run ${runId}.`, ["A summary artifact is required for `voratiq apply --commit`."], [
|
|
61
|
+
"Re-run the spec to regenerate artifacts or apply without `--commit` and commit manually.",
|
|
62
|
+
]);
|
|
63
|
+
this.runId = runId;
|
|
64
|
+
this.agentId = agentId;
|
|
65
|
+
this.name = "ApplyAgentSummaryNotRecordedError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class ApplyAgentSummaryMissingOnDiskError extends ApplyError {
|
|
69
|
+
summaryPath;
|
|
70
|
+
constructor(summaryPath) {
|
|
71
|
+
super("Recorded summary is missing from disk.", [`Expected summary at ${summaryPath} but it was not found.`], ["Ensure the run directory still exists or re-run the agents."]);
|
|
72
|
+
this.summaryPath = summaryPath;
|
|
73
|
+
this.name = "ApplyAgentSummaryMissingOnDiskError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class ApplyAgentSummaryEmptyError extends ApplyError {
|
|
77
|
+
summaryPath;
|
|
78
|
+
constructor(summaryPath) {
|
|
79
|
+
super("Recorded summary is empty.", [`Expected summary at ${summaryPath} to contain a commit subject.`], [
|
|
80
|
+
"Re-run the spec to regenerate artifacts or apply without `--commit` and commit manually.",
|
|
81
|
+
]);
|
|
82
|
+
this.summaryPath = summaryPath;
|
|
83
|
+
this.name = "ApplyAgentSummaryEmptyError";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export class ApplyGitCommitError extends ApplyError {
|
|
87
|
+
constructor(detail) {
|
|
88
|
+
super("Failed to create git commit.", [detail], ["The diff remains applied; resolve the issue and commit manually."]);
|
|
89
|
+
this.name = "ApplyGitCommitError";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
56
92
|
export class ApplyBaseMismatchError extends ApplyError {
|
|
57
93
|
constructor(options) {
|
|
58
94
|
const { baseRevisionSha, headRevision } = options;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type { PruneCommandInput, PruneResult } from "./types.js";
|
|
1
|
+
import type { PruneAllCommandInput, PruneAllResult, PruneCommandInput, PruneResult } from "./types.js";
|
|
2
2
|
export declare function executePruneCommand(input: PruneCommandInput): Promise<PruneResult>;
|
|
3
|
+
export declare function executePruneAllCommand(input: PruneAllCommandInput): Promise<PruneAllResult>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import { NonInteractiveShellError } from "../../cli/errors.js";
|
|
3
|
-
import { buildPruneConfirmationPreface } from "../../render/transcripts/prune.js";
|
|
3
|
+
import { buildPruneAllConfirmationPreface, buildPruneConfirmationPreface, } from "../../render/transcripts/prune.js";
|
|
4
4
|
import { RunRecordNotFoundError, RunRecordParseError, } from "../../runs/records/errors.js";
|
|
5
|
-
import { rewriteRunRecord, RUN_RECORD_FILENAME, } from "../../runs/records/persistence.js";
|
|
5
|
+
import { fetchRunsSafely, rewriteRunRecord, RUN_RECORD_FILENAME, } from "../../runs/records/persistence.js";
|
|
6
6
|
import { toErrorMessage } from "../../utils/errors.js";
|
|
7
7
|
import { pathExists } from "../../utils/fs.js";
|
|
8
8
|
import { getGitStderr, runGitCommand } from "../../utils/git.js";
|
|
@@ -103,6 +103,52 @@ export async function executePruneCommand(input) {
|
|
|
103
103
|
branches: branchSummary,
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
export async function executePruneAllCommand(input) {
|
|
107
|
+
const { root, runsFilePath, confirm, clock, purge: purgeInput } = input;
|
|
108
|
+
const purge = purgeInput ?? false;
|
|
109
|
+
if (!confirm) {
|
|
110
|
+
throw new NonInteractiveShellError();
|
|
111
|
+
}
|
|
112
|
+
if (!(await pathExists(runsFilePath))) {
|
|
113
|
+
return { status: "noop", runIds: [] };
|
|
114
|
+
}
|
|
115
|
+
const { records, warnings } = await fetchRunsSafely({
|
|
116
|
+
root,
|
|
117
|
+
runsFilePath,
|
|
118
|
+
});
|
|
119
|
+
const firstWarning = warnings[0];
|
|
120
|
+
if (firstWarning) {
|
|
121
|
+
if (firstWarning.kind === "parse-error") {
|
|
122
|
+
throw new RunRecordParseError(firstWarning.displayPath, firstWarning.details);
|
|
123
|
+
}
|
|
124
|
+
throw new RunMetadataMissingError(firstWarning.runId);
|
|
125
|
+
}
|
|
126
|
+
if (records.length === 0) {
|
|
127
|
+
return { status: "noop", runIds: [] };
|
|
128
|
+
}
|
|
129
|
+
const eligibleRuns = sortRunRecordsByCreatedAt(records);
|
|
130
|
+
const runIds = eligibleRuns.map((record) => record.runId);
|
|
131
|
+
const confirmationAccepted = await confirm({
|
|
132
|
+
message: "Proceed?",
|
|
133
|
+
defaultValue: false,
|
|
134
|
+
prefaceLines: buildPruneAllConfirmationPreface({
|
|
135
|
+
records: eligibleRuns,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (!confirmationAccepted) {
|
|
139
|
+
return { status: "aborted", runIds };
|
|
140
|
+
}
|
|
141
|
+
for (const runId of runIds) {
|
|
142
|
+
await executePruneCommand({
|
|
143
|
+
...input,
|
|
144
|
+
runId,
|
|
145
|
+
confirm: () => Promise.resolve(true),
|
|
146
|
+
purge,
|
|
147
|
+
clock,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { status: "pruned", runIds };
|
|
151
|
+
}
|
|
106
152
|
function buildAgentDirectoryDisplayPaths(options) {
|
|
107
153
|
const { runRecord } = options;
|
|
108
154
|
const unique = new Set();
|
|
@@ -311,6 +357,16 @@ async function branchExists(root, branch) {
|
|
|
311
357
|
});
|
|
312
358
|
return output.length > 0;
|
|
313
359
|
}
|
|
360
|
+
function sortRunRecordsByCreatedAt(records) {
|
|
361
|
+
return Array.from(records).sort((a, b) => {
|
|
362
|
+
const aTimestamp = Date.parse(a.createdAt);
|
|
363
|
+
const bTimestamp = Date.parse(b.createdAt);
|
|
364
|
+
if (Number.isNaN(aTimestamp) || Number.isNaN(bTimestamp)) {
|
|
365
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
366
|
+
}
|
|
367
|
+
return aTimestamp - bTimestamp;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
314
370
|
async function rewriteHistory(options) {
|
|
315
371
|
const { root, runsFilePath, runId, deletedAt } = options;
|
|
316
372
|
try {
|
|
@@ -9,6 +9,14 @@ export interface PruneCommandInput {
|
|
|
9
9
|
purge?: boolean;
|
|
10
10
|
clock?: () => Date;
|
|
11
11
|
}
|
|
12
|
+
export interface PruneAllCommandInput {
|
|
13
|
+
root: string;
|
|
14
|
+
runsDir: string;
|
|
15
|
+
runsFilePath: string;
|
|
16
|
+
confirm: PruneConfirmationHandler;
|
|
17
|
+
purge?: boolean;
|
|
18
|
+
clock?: () => Date;
|
|
19
|
+
}
|
|
12
20
|
export interface PruneBranchSummary {
|
|
13
21
|
deleted: string[];
|
|
14
22
|
skipped: string[];
|
|
@@ -40,3 +48,16 @@ export interface PruneAbortedResult {
|
|
|
40
48
|
runPath: string;
|
|
41
49
|
}
|
|
42
50
|
export type PruneResult = PruneSuccessResult | PruneAbortedResult;
|
|
51
|
+
export interface PruneAllSuccessResult {
|
|
52
|
+
status: "pruned";
|
|
53
|
+
runIds: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface PruneAllNoopResult {
|
|
56
|
+
status: "noop";
|
|
57
|
+
runIds: string[];
|
|
58
|
+
}
|
|
59
|
+
export interface PruneAllAbortedResult {
|
|
60
|
+
status: "aborted";
|
|
61
|
+
runIds: string[];
|
|
62
|
+
}
|
|
63
|
+
export type PruneAllResult = PruneAllSuccessResult | PruneAllNoopResult | PruneAllAbortedResult;
|
|
@@ -83,26 +83,9 @@ export class AgentRunContext {
|
|
|
83
83
|
const evaluation = bySlug.get(fallback.slug);
|
|
84
84
|
return evaluation ?? fallback;
|
|
85
85
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const hasErrored = this.evalResults.some((evaluation) => {
|
|
90
|
-
return evaluation.status === "errored";
|
|
91
|
-
});
|
|
92
|
-
const hasFailed = this.evalResults.some((evaluation) => evaluation.status === "failed");
|
|
93
|
-
if (hasErrored) {
|
|
94
|
-
this.status = "errored";
|
|
95
|
-
if (!this.errorMessage) {
|
|
96
|
-
const erroredEval = this.evalResults.find((evaluation) => evaluation.status === "errored" && evaluation.error);
|
|
97
|
-
if (erroredEval?.error) {
|
|
98
|
-
this.errorMessage = erroredEval.error;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (hasFailed) {
|
|
104
|
-
this.status = "failed";
|
|
105
|
-
}
|
|
86
|
+
// Evals are quality signals, not execution outcomes.
|
|
87
|
+
// Agent status depends only on execution results (process exit code, etc.),
|
|
88
|
+
// not on eval failures. Eval results are tracked and exposed separately.
|
|
106
89
|
}
|
|
107
90
|
markChatArtifact(format) {
|
|
108
91
|
this.artifactState.chatCaptured = true;
|
|
@@ -90,7 +90,7 @@ export async function executeRunCommand(input) {
|
|
|
90
90
|
mutators,
|
|
91
91
|
});
|
|
92
92
|
agentRecords = executionResult.agentRecords;
|
|
93
|
-
const derivedRunStatus = executionResult.hadAgentFailure
|
|
93
|
+
const derivedRunStatus = executionResult.hadAgentFailure
|
|
94
94
|
? "failed"
|
|
95
95
|
: executionResult.agentReports.some((report) => report.status === "errored")
|
|
96
96
|
? "errored"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RunStatus } from "../../status/index.js";
|
|
2
|
+
import { TERMINABLE_RUN_STATUSES } from "../../status/index.js";
|
|
2
3
|
export declare const RUN_ABORT_WARNING = "Run aborted before agent completed.";
|
|
3
4
|
interface ActiveRunContext {
|
|
4
5
|
root: string;
|
|
@@ -11,9 +12,8 @@ interface ActiveRunAgentContext {
|
|
|
11
12
|
providerId?: string;
|
|
12
13
|
agentRoot: string;
|
|
13
14
|
}
|
|
14
|
-
declare const TERMINABLE_STATUSES: readonly ["failed", "aborted"];
|
|
15
15
|
export declare function registerActiveRun(context: ActiveRunContext): void;
|
|
16
16
|
export declare function clearActiveRun(runId: string): void;
|
|
17
17
|
export declare function getActiveTerminationStatus(runId: string): RunStatus | undefined;
|
|
18
|
-
export declare function terminateActiveRun(status: Extract<RunStatus, (typeof
|
|
18
|
+
export declare function terminateActiveRun(status: Extract<RunStatus, (typeof TERMINABLE_RUN_STATUSES)[number]>): Promise<void>;
|
|
19
19
|
export {};
|