@cassiomc1/forgeloop 1.6.4 → 1.7.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/AGENT_COMPATIBILITY.md +11 -0
- package/DOCS_INDEX.md +1 -0
- package/GUIDE_ROUTER.md +26 -1
- package/LOOP_ENGINEERING.md +48 -0
- package/ORCHESTRATOR_INTEGRATION.md +38 -0
- package/PROTOCOL_INTEGRATION.md +62 -0
- package/README.md +25 -0
- package/benchmarks/execution-profiles/README.md +44 -0
- package/benchmarks/execution-profiles/api-feature.json +18 -0
- package/benchmarks/execution-profiles/authentication-change.json +18 -0
- package/benchmarks/execution-profiles/documentation-correction.json +18 -0
- package/benchmarks/execution-profiles/infrastructure-release.json +18 -0
- package/benchmarks/execution-profiles/novatask-saas-landing-page.json +36 -0
- package/benchmarks/execution-profiles/small-bug-fix.json +18 -0
- package/benchmarks/execution-profiles/static-landing-page.json +18 -0
- package/completions/_forgeloop +6 -4
- package/completions/forgeloop.bash +8 -4
- package/completions/forgeloop.fish +24 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +45 -1
- package/docs/ARTIFACT_REFERENCE.md +53 -7
- package/docs/CLI_REFERENCE.md +83 -1
- package/docs/EXECUTION_PROFILE_BENCHMARKS.md +208 -0
- package/docs/GETTING_STARTED.md +28 -0
- package/docs/MCP.md +6 -0
- package/docs/RELEASE_CHECKLIST.md +4 -0
- package/docs/TROUBLESHOOTING.md +6 -0
- package/docs/UNIVERSAL_INTEGRATION.md +58 -0
- package/package.json +14 -2
- package/schemas/config.schema.json +1 -0
- package/schemas/execution-profile-benchmark-aggregate.schema.json +43 -0
- package/schemas/execution-profile-benchmark-run.schema.json +106 -0
- package/schemas/execution-profile-benchmark-scenario.schema.json +66 -0
- package/schemas/routing-result.schema.json +12 -0
- package/schemas/usage.schema.json +30 -0
- package/scripts/check-efficiency-regression.mjs +99 -0
- package/scripts/generate-agent-protocol-summary.mjs +35 -0
- package/scripts/lib/execution-profile-benchmark-io.mjs +67 -0
- package/scripts/run-execution-profile-benchmarks.mjs +265 -0
- package/scripts/summarize-execution-profile-benchmarks.mjs +84 -0
- package/scripts/validate-execution-profile-benchmarks.mjs +120 -0
- package/src/cli.js +16 -4
- package/src/commands/efficiency.js +12 -0
- package/src/commands/eval.js +8 -2
- package/src/commands/metrics.js +2 -2
- package/src/commands/next.js +26 -2
- package/src/commands/route.js +20 -2
- package/src/commands/task-show.js +31 -2
- package/src/commands/usage-record.js +61 -0
- package/src/core/artifact-registry.js +12 -0
- package/src/core/cli-command-definitions.js +27 -0
- package/src/core/command-executors.js +29 -5
- package/src/core/command-input.js +34 -0
- package/src/core/config.js +9 -0
- package/src/core/efficiency.js +197 -0
- package/src/core/error-codes.js +18 -0
- package/src/core/execution-profile-benchmarks.js +674 -0
- package/src/core/execution-profile-context.js +177 -0
- package/src/core/execution-profile.js +248 -0
- package/src/core/integration-invocation-policy.js +43 -0
- package/src/core/integration-resources.js +22 -1
- package/src/core/protocol-info.js +42 -0
- package/src/core/resumability.js +27 -1
- package/src/core/router.js +23 -1
- package/src/core/runtime-context.js +11 -0
- package/src/core/schema-validation.js +4 -0
- package/src/core/task-paths.js +2 -0
- package/src/core/templates.js +4 -0
- package/src/core/trace.js +1 -0
- package/src/core/trajectory-evaluation.js +2 -2
- package/src/core/trajectory-metrics.js +18 -2
- package/src/core/usage.js +137 -0
- package/src/integration.d.ts +84 -0
- package/src/integration.js +14 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { assertSchema, readSchema } from "../../src/core/schema-validation.js";
|
|
5
|
+
import { getPackageRoot } from "../../src/core/templates.js";
|
|
6
|
+
import {
|
|
7
|
+
assertBenchmarkScenario,
|
|
8
|
+
assertBenchmarkRun,
|
|
9
|
+
assertRequiredBenchmarkScenarios,
|
|
10
|
+
} from "../../src/core/execution-profile-benchmarks.js";
|
|
11
|
+
|
|
12
|
+
export async function readBenchmarkScenarios(repositoryRoot) {
|
|
13
|
+
const directory = path.join(repositoryRoot, "benchmarks", "execution-profiles");
|
|
14
|
+
const schema = await readSchema("execution-profile-benchmark-scenario", getPackageRoot());
|
|
15
|
+
const names = (await readdir(directory)).filter((name) => /^[a-z0-9-]+\.json$/u.test(name)).sort();
|
|
16
|
+
const scenarios = [];
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const scenario = JSON.parse(await readFile(path.join(directory, name), "utf8"));
|
|
19
|
+
assertSchema(scenario, schema, name);
|
|
20
|
+
scenarios.push(assertBenchmarkScenario(scenario));
|
|
21
|
+
}
|
|
22
|
+
return assertRequiredBenchmarkScenarios(scenarios);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readJsonFiles(directory) {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code === "ENOENT") return [];
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const files = [];
|
|
34
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
35
|
+
const entryPath = path.join(directory, entry.name);
|
|
36
|
+
if (entry.isDirectory()) files.push(...await readJsonFiles(entryPath));
|
|
37
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) files.push(entryPath);
|
|
38
|
+
}
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readBenchmarkRunSets(resultsDirectory, runSetId = null) {
|
|
43
|
+
const rawDirectory = path.join(resultsDirectory, "raw");
|
|
44
|
+
let names;
|
|
45
|
+
try {
|
|
46
|
+
names = (await readdir(rawDirectory, { withFileTypes: true }))
|
|
47
|
+
.filter((entry) => entry.isDirectory() && (runSetId === null || entry.name === runSetId))
|
|
48
|
+
.map((entry) => entry.name)
|
|
49
|
+
.sort();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === "ENOENT") return [];
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const schema = await readSchema("execution-profile-benchmark-run", getPackageRoot());
|
|
55
|
+
const runSets = [];
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
const files = await readJsonFiles(path.join(rawDirectory, name));
|
|
58
|
+
const runs = [];
|
|
59
|
+
for (const filename of files) {
|
|
60
|
+
const value = JSON.parse(await readFile(filename, "utf8"));
|
|
61
|
+
assertSchema(value, schema, filename);
|
|
62
|
+
runs.push(assertBenchmarkRun(value));
|
|
63
|
+
}
|
|
64
|
+
runSets.push({ runSetId: name, runs });
|
|
65
|
+
}
|
|
66
|
+
return runSets;
|
|
67
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { performance } from "node:perf_hooks";
|
|
4
|
+
import { access, readFile, readdir, mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
import { evaluateRoute } from "../src/core/router.js";
|
|
9
|
+
import { currentRepositoryFingerprint } from "../src/core/repository.js";
|
|
10
|
+
import { getPackageRoot } from "../src/core/templates.js";
|
|
11
|
+
import { assertSchema, readSchema } from "../src/core/schema-validation.js";
|
|
12
|
+
import {
|
|
13
|
+
aggregateBenchmarkRuns,
|
|
14
|
+
BENCHMARK_MODES,
|
|
15
|
+
BENCHMARK_VERSION,
|
|
16
|
+
assertBenchmarkScenario,
|
|
17
|
+
assertRequiredBenchmarkScenarios,
|
|
18
|
+
createBenchmarkRun,
|
|
19
|
+
} from "../src/core/execution-profile-benchmarks.js";
|
|
20
|
+
|
|
21
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
const scenarioDirectory = path.join(repositoryRoot, "benchmarks", "execution-profiles");
|
|
23
|
+
const defaultOutputDirectory = path.join(scenarioDirectory, "results");
|
|
24
|
+
|
|
25
|
+
function usageError(message) {
|
|
26
|
+
const error = new Error(message);
|
|
27
|
+
error.code = "E_BENCHMARK_USAGE";
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const options = {
|
|
33
|
+
target: process.cwd(),
|
|
34
|
+
adapter: null,
|
|
35
|
+
runs: 5,
|
|
36
|
+
runSetId: null,
|
|
37
|
+
output: defaultOutputDirectory,
|
|
38
|
+
json: false,
|
|
39
|
+
};
|
|
40
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
41
|
+
const argument = argv[index];
|
|
42
|
+
const value = () => {
|
|
43
|
+
const next = argv[++index];
|
|
44
|
+
if (!next || next.startsWith("--")) throw usageError(`${argument} requires a value`);
|
|
45
|
+
return next;
|
|
46
|
+
};
|
|
47
|
+
if (argument === "--target") options.target = value();
|
|
48
|
+
else if (argument === "--adapter") options.adapter = value();
|
|
49
|
+
else if (argument === "--runs") options.runs = Number(value());
|
|
50
|
+
else if (argument === "--run-set") options.runSetId = value();
|
|
51
|
+
else if (argument === "--output") options.output = value();
|
|
52
|
+
else if (argument === "--json") options.json = true;
|
|
53
|
+
else if (argument === "--help" || argument === "-h") {
|
|
54
|
+
options.help = true;
|
|
55
|
+
} else throw usageError(`unknown option: ${argument}`);
|
|
56
|
+
}
|
|
57
|
+
if (options.help) return options;
|
|
58
|
+
if (!options.adapter) throw usageError("--adapter is required; ForgeLoop never invents provider or host measurements");
|
|
59
|
+
if (!Number.isInteger(options.runs) || options.runs < 1 || options.runs > 100) {
|
|
60
|
+
throw usageError("--runs must be an integer from 1 through 100");
|
|
61
|
+
}
|
|
62
|
+
if (options.runSetId !== null && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(options.runSetId)) {
|
|
63
|
+
throw usageError("--run-set must contain only portable identifier characters");
|
|
64
|
+
}
|
|
65
|
+
return options;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function helpText() {
|
|
69
|
+
return [
|
|
70
|
+
"Usage: npm run benchmark:profiles -- --adapter <module> [options]",
|
|
71
|
+
"",
|
|
72
|
+
"The adapter must execute each scenario and return actual provider/host usage, verification, comparable steps, and optional host-reported contextUsage.",
|
|
73
|
+
"Options: --target <path> --runs <1..100> --run-set <id> --output <directory> --json",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function loadScenarios() {
|
|
78
|
+
const scenarioSchema = await readSchema("execution-profile-benchmark-scenario", getPackageRoot());
|
|
79
|
+
const names = (await readdir(scenarioDirectory)).filter((name) => /^[a-z0-9-]+\.json$/u.test(name)).sort();
|
|
80
|
+
const scenarios = [];
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
const scenario = JSON.parse(await readFile(path.join(scenarioDirectory, name), "utf8"));
|
|
83
|
+
assertSchema(scenario, scenarioSchema, name);
|
|
84
|
+
scenarios.push(assertBenchmarkScenario(scenario));
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return assertRequiredBenchmarkScenarios(scenarios);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw usageError(error.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadAdapter(adapterSpecifier) {
|
|
94
|
+
const adapterPath = path.resolve(process.cwd(), adapterSpecifier);
|
|
95
|
+
const adapterModule = await import(pathToFileURL(adapterPath).href);
|
|
96
|
+
const adapter = adapterModule.runBenchmark ?? adapterModule.default?.runBenchmark ?? adapterModule.default;
|
|
97
|
+
if (typeof adapter !== "function") {
|
|
98
|
+
throw usageError("benchmark adapter must export runBenchmark(input) or be a function default export");
|
|
99
|
+
}
|
|
100
|
+
return adapter;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function generatedRunSetId() {
|
|
104
|
+
return new Date().toISOString().replace(/[-:.]/gu, "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function environmentMetadata(adapterMetadata = {}) {
|
|
108
|
+
return {
|
|
109
|
+
environmentClass: adapterMetadata.environmentClass ?? `${process.platform}-node${process.versions.node.split(".")[0]}`,
|
|
110
|
+
nodeVersion: adapterMetadata.nodeVersion ?? process.versions.node,
|
|
111
|
+
os: adapterMetadata.os ?? process.platform,
|
|
112
|
+
arch: adapterMetadata.arch ?? process.arch,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function profileForMode(scenario, mode) {
|
|
117
|
+
if (mode === "direct") return { requestedProfile: null, resolvedProfile: null };
|
|
118
|
+
const requestedProfile = mode === "forgeloopBalanced" ? "balanced" : "auto";
|
|
119
|
+
const route = evaluateRoute(scenario.input, { requestedProfile });
|
|
120
|
+
return { requestedProfile, resolvedProfile: route.executionProfile.resolved };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizedAdapterMetadata(response, target, repository, scenario, mode) {
|
|
124
|
+
const adapterMetadata = response.metadata && typeof response.metadata === "object"
|
|
125
|
+
? response.metadata
|
|
126
|
+
: {};
|
|
127
|
+
const profile = profileForMode(scenario, mode);
|
|
128
|
+
const environment = environmentMetadata(adapterMetadata);
|
|
129
|
+
return {
|
|
130
|
+
...environment,
|
|
131
|
+
model: response.usage?.model ?? adapterMetadata.model ?? null,
|
|
132
|
+
provider: response.usage?.provider ?? adapterMetadata.provider ?? null,
|
|
133
|
+
promptSpecFingerprint: response.promptSpecFingerprint ?? adapterMetadata.promptSpecFingerprint ?? null,
|
|
134
|
+
// A Git revision is derived from the target rather than trusted from the adapter.
|
|
135
|
+
projectRevision: repository.head ?? adapterMetadata.projectRevision ?? null,
|
|
136
|
+
requestedProfile: profile.requestedProfile,
|
|
137
|
+
resolvedProfile: profile.resolvedProfile,
|
|
138
|
+
targetPath: target,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function executeRuns({ adapter, scenarios, target, runSetId, runs }) {
|
|
143
|
+
const repository = await currentRepositoryFingerprint(target);
|
|
144
|
+
const allRuns = [];
|
|
145
|
+
for (const scenario of scenarios) {
|
|
146
|
+
for (const mode of BENCHMARK_MODES) {
|
|
147
|
+
for (let runIndex = 1; runIndex <= runs; runIndex += 1) {
|
|
148
|
+
const started = performance.now();
|
|
149
|
+
const response = await adapter({
|
|
150
|
+
benchmarkVersion: BENCHMARK_VERSION,
|
|
151
|
+
scenario: structuredClone(scenario),
|
|
152
|
+
mode,
|
|
153
|
+
runIndex,
|
|
154
|
+
target,
|
|
155
|
+
projectRevision: repository.head ?? null,
|
|
156
|
+
// The adapter owns execution; the runner owns elapsed-time measurement.
|
|
157
|
+
});
|
|
158
|
+
const wallClockMs = Number((performance.now() - started).toFixed(4));
|
|
159
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
160
|
+
throw usageError(`${scenario.scenarioId}/${mode}/${runIndex}: adapter must return an object`);
|
|
161
|
+
}
|
|
162
|
+
const run = createBenchmarkRun({
|
|
163
|
+
runSetId,
|
|
164
|
+
runId: `run-${scenario.scenarioId}-${mode}-${String(runIndex).padStart(3, "0")}`,
|
|
165
|
+
runIndex,
|
|
166
|
+
scenario,
|
|
167
|
+
mode,
|
|
168
|
+
usage: response.usage ?? {},
|
|
169
|
+
wallClockMs,
|
|
170
|
+
verification: response.verification ?? "NOT_AVAILABLE",
|
|
171
|
+
verificationCycles: response.verificationCycles ?? null,
|
|
172
|
+
comparableSteps: response.comparableSteps ?? null,
|
|
173
|
+
contextUsage: response.contextUsage,
|
|
174
|
+
quality: response.quality,
|
|
175
|
+
metadata: normalizedAdapterMetadata(response, target, repository, scenario, mode),
|
|
176
|
+
});
|
|
177
|
+
allRuns.push(run);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return allRuns;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function writeResults({ outputDirectory, runSetId, scenarios, runs }) {
|
|
185
|
+
const rawDirectory = path.join(outputDirectory, "raw", runSetId);
|
|
186
|
+
const aggregateDirectory = path.join(outputDirectory, "aggregate", runSetId);
|
|
187
|
+
try {
|
|
188
|
+
await access(rawDirectory);
|
|
189
|
+
throw usageError(`run set already exists and will not be overwritten: ${runSetId}`);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error.code !== "ENOENT") throw error;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await access(aggregateDirectory);
|
|
195
|
+
throw usageError(`aggregate run set already exists and will not be overwritten: ${runSetId}`);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error.code !== "ENOENT") throw error;
|
|
198
|
+
}
|
|
199
|
+
await mkdir(rawDirectory, { recursive: true });
|
|
200
|
+
await mkdir(aggregateDirectory, { recursive: true });
|
|
201
|
+
const aggregates = [];
|
|
202
|
+
for (const scenario of scenarios) {
|
|
203
|
+
const scenarioRuns = runs.filter((run) => run.scenarioId === scenario.scenarioId);
|
|
204
|
+
const aggregate = aggregateBenchmarkRuns({ scenario, runs: scenarioRuns });
|
|
205
|
+
aggregates.push(aggregate);
|
|
206
|
+
for (const run of scenarioRuns) {
|
|
207
|
+
const directory = path.join(rawDirectory, scenario.scenarioId, run.mode);
|
|
208
|
+
await mkdir(directory, { recursive: true });
|
|
209
|
+
await writeFile(path.join(directory, `run-${String(run.runIndex).padStart(3, "0")}.json`), `${JSON.stringify(run, null, 2)}\n`, "utf8");
|
|
210
|
+
}
|
|
211
|
+
await writeFile(path.join(aggregateDirectory, `${scenario.scenarioId}.json`), `${JSON.stringify(aggregate, null, 2)}\n`, "utf8");
|
|
212
|
+
}
|
|
213
|
+
const summary = {
|
|
214
|
+
schemaVersion: 1,
|
|
215
|
+
benchmarkVersion: BENCHMARK_VERSION,
|
|
216
|
+
runSetId,
|
|
217
|
+
scenarioCount: scenarios.length,
|
|
218
|
+
runCount: runs.length,
|
|
219
|
+
claimsAllowed: aggregates.some((aggregate) => aggregate.claimsAllowed),
|
|
220
|
+
scenarios: aggregates.map((aggregate) => ({
|
|
221
|
+
scenarioId: aggregate.scenarioId,
|
|
222
|
+
expectedProfile: aggregate.expectedProfile,
|
|
223
|
+
claimsAllowed: aggregate.claimsAllowed,
|
|
224
|
+
lightObjectives: aggregate.lightObjectives,
|
|
225
|
+
contextInflation: aggregate.contextInflation ?? null,
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
await writeFile(path.join(aggregateDirectory, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
|
229
|
+
return { rawDirectory, aggregateDirectory, summary };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function main() {
|
|
233
|
+
const options = parseArgs(process.argv.slice(2));
|
|
234
|
+
if (options.help) {
|
|
235
|
+
console.log(helpText());
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const target = path.resolve(options.target);
|
|
239
|
+
const outputDirectory = path.resolve(options.output);
|
|
240
|
+
const runSetId = options.runSetId ?? generatedRunSetId();
|
|
241
|
+
const adapter = await loadAdapter(options.adapter);
|
|
242
|
+
const scenarios = await loadScenarios();
|
|
243
|
+
const runs = await executeRuns({ adapter, scenarios, target, runSetId, runs: options.runs });
|
|
244
|
+
const result = await writeResults({ outputDirectory, runSetId, scenarios, runs });
|
|
245
|
+
const output = {
|
|
246
|
+
status: "MEASURED",
|
|
247
|
+
runSetId,
|
|
248
|
+
scenarioCount: scenarios.length,
|
|
249
|
+
runCount: runs.length,
|
|
250
|
+
outputDirectory,
|
|
251
|
+
claimsAllowed: result.summary.claimsAllowed,
|
|
252
|
+
};
|
|
253
|
+
console.log(options.json ? JSON.stringify(output) : [
|
|
254
|
+
`Benchmark run set: ${runSetId}`,
|
|
255
|
+
`Scenarios: ${scenarios.length}`,
|
|
256
|
+
`Runs: ${runs.length}`,
|
|
257
|
+
`Claims allowed: ${result.summary.claimsAllowed ? "yes (observational only)" : "no"}`,
|
|
258
|
+
`Results: ${outputDirectory}`,
|
|
259
|
+
].join("\n"));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
main().catch((error) => {
|
|
263
|
+
console.error(`ForgeLoop benchmark runner: ${error.message}`);
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { aggregateBenchmarkRuns, BENCHMARK_VERSION } from "../src/core/execution-profile-benchmarks.js";
|
|
7
|
+
import { readBenchmarkRunSets, readBenchmarkScenarios } from "./lib/execution-profile-benchmark-io.mjs";
|
|
8
|
+
|
|
9
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const defaultResultsDirectory = path.join(repositoryRoot, "benchmarks", "execution-profiles", "results");
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const options = { results: defaultResultsDirectory, runSetId: null, json: false };
|
|
14
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
15
|
+
const argument = argv[index];
|
|
16
|
+
if (argument === "--json") options.json = true;
|
|
17
|
+
else if (["--results", "--run-set"].includes(argument)) {
|
|
18
|
+
const value = argv[++index];
|
|
19
|
+
if (!value || value.startsWith("--")) throw new Error(`${argument} requires a value`);
|
|
20
|
+
if (argument === "--results") options.results = value;
|
|
21
|
+
else options.runSetId = value;
|
|
22
|
+
} else if (argument === "--help" || argument === "-h") options.help = true;
|
|
23
|
+
else throw new Error(`unknown option: ${argument}`);
|
|
24
|
+
}
|
|
25
|
+
return options;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function helpText() {
|
|
29
|
+
return "Usage: npm run benchmark:profiles:summary -- [--results <directory>] [--run-set <id>] [--json]";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
const options = parseArgs(process.argv.slice(2));
|
|
34
|
+
if (options.help) {
|
|
35
|
+
console.log(helpText());
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const scenarios = await readBenchmarkScenarios(repositoryRoot);
|
|
39
|
+
const runSets = await readBenchmarkRunSets(path.resolve(options.results), options.runSetId);
|
|
40
|
+
const summaries = [];
|
|
41
|
+
for (const runSet of runSets) {
|
|
42
|
+
const aggregates = scenarios
|
|
43
|
+
.map((scenario) => {
|
|
44
|
+
const runs = runSet.runs.filter((run) => run.scenarioId === scenario.scenarioId);
|
|
45
|
+
return runs.length > 0 ? aggregateBenchmarkRuns({ scenario, runs }) : null;
|
|
46
|
+
});
|
|
47
|
+
summaries.push({
|
|
48
|
+
runSetId: runSet.runSetId,
|
|
49
|
+
runCount: runSet.runs.length,
|
|
50
|
+
scenarioCount: aggregates.filter(Boolean).length,
|
|
51
|
+
claimsAllowed: aggregates.some((aggregate) => aggregate?.claimsAllowed === true),
|
|
52
|
+
scenarios: aggregates.filter(Boolean).map((aggregate) => ({
|
|
53
|
+
scenarioId: aggregate.scenarioId,
|
|
54
|
+
expectedProfile: aggregate.expectedProfile,
|
|
55
|
+
claimsAllowed: aggregate.claimsAllowed,
|
|
56
|
+
comparisons: aggregate.comparisons,
|
|
57
|
+
lightObjectives: aggregate.lightObjectives,
|
|
58
|
+
modeAggregates: aggregate.modeAggregates,
|
|
59
|
+
contextInflation: aggregate.contextInflation ?? null,
|
|
60
|
+
})),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const output = {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
benchmarkVersion: BENCHMARK_VERSION,
|
|
66
|
+
status: summaries.length === 0 ? "NOT_MEASURED" : "MEASURED",
|
|
67
|
+
claimsAllowed: summaries.some((summary) => summary.claimsAllowed),
|
|
68
|
+
scenarioCount: scenarios.length,
|
|
69
|
+
runSets: summaries,
|
|
70
|
+
claimPolicy: "No efficiency claim is allowed when trusted measurements are unavailable or non-comparable.",
|
|
71
|
+
};
|
|
72
|
+
if (options.json) console.log(JSON.stringify(output));
|
|
73
|
+
else {
|
|
74
|
+
console.log(`Benchmark status: ${output.status}`);
|
|
75
|
+
console.log(`Run sets: ${summaries.length}`);
|
|
76
|
+
console.log(`Claims allowed: ${output.claimsAllowed ? "yes (observational only)" : "no"}`);
|
|
77
|
+
if (summaries.length === 0) console.log("No provider or host benchmark measurements are present.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch((error) => {
|
|
82
|
+
console.error(`ForgeLoop benchmark summary: ${error.message}`);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { assertSchema, readSchema } from "../src/core/schema-validation.js";
|
|
8
|
+
import { getPackageRoot } from "../src/core/templates.js";
|
|
9
|
+
import { aggregateBenchmarkRuns, BENCHMARK_MODES } from "../src/core/execution-profile-benchmarks.js";
|
|
10
|
+
import { readBenchmarkRunSets, readBenchmarkScenarios } from "./lib/execution-profile-benchmark-io.mjs";
|
|
11
|
+
|
|
12
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const defaultResultsDirectory = path.join(repositoryRoot, "benchmarks", "execution-profiles", "results");
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const options = { results: defaultResultsDirectory, json: false };
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
const argument = argv[index];
|
|
19
|
+
if (argument === "--json") options.json = true;
|
|
20
|
+
else if (argument === "--results") {
|
|
21
|
+
const value = argv[++index];
|
|
22
|
+
if (!value || value.startsWith("--")) throw new Error("--results requires a value");
|
|
23
|
+
options.results = value;
|
|
24
|
+
} else if (argument === "--help" || argument === "-h") options.help = true;
|
|
25
|
+
else throw new Error(`unknown option: ${argument}`);
|
|
26
|
+
}
|
|
27
|
+
return options;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertCompleteRunSet(scenario, runs) {
|
|
31
|
+
if (runs.length === 0) throw new Error(`${scenario.scenarioId}: no raw runs found`);
|
|
32
|
+
const byMode = Object.fromEntries(BENCHMARK_MODES.map((mode) => [mode, runs.filter((run) => run.mode === mode)]));
|
|
33
|
+
const counts = new Set(BENCHMARK_MODES.map((mode) => byMode[mode].length));
|
|
34
|
+
if (counts.size !== 1 || counts.has(0)) throw new Error(`${scenario.scenarioId}: every mode must have the same non-zero run count`);
|
|
35
|
+
const expectedIndexes = byMode.direct.map((run) => run.runIndex).sort((a, b) => a - b);
|
|
36
|
+
for (const mode of BENCHMARK_MODES) {
|
|
37
|
+
const indexes = byMode[mode].map((run) => run.runIndex).sort((a, b) => a - b);
|
|
38
|
+
if (JSON.stringify(indexes) !== JSON.stringify(expectedIndexes)) {
|
|
39
|
+
throw new Error(`${scenario.scenarioId}: ${mode} run indexes do not match direct baseline`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return aggregateBenchmarkRuns({ scenario, runs });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function validateStoredAggregates(resultsDirectory, runSetId, scenarios, expectedAggregates) {
|
|
46
|
+
const schema = await readSchema("execution-profile-benchmark-aggregate", getPackageRoot());
|
|
47
|
+
const aggregateDirectory = path.join(resultsDirectory, "aggregate", runSetId);
|
|
48
|
+
const aggregateEntries = await readdir(aggregateDirectory, { withFileTypes: true });
|
|
49
|
+
const expectedNames = new Set(scenarios.map((scenario) => `${scenario.scenarioId}.json`));
|
|
50
|
+
for (const entry of aggregateEntries) {
|
|
51
|
+
if (entry.isFile() && entry.name.endsWith(".json") && entry.name !== "summary.json" && !expectedNames.has(entry.name)) {
|
|
52
|
+
throw new Error(`${runSetId}: aggregate has an unexpected scenario file: ${entry.name}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const aggregate of expectedAggregates) {
|
|
56
|
+
const filename = path.join(resultsDirectory, "aggregate", runSetId, `${aggregate.scenarioId}.json`);
|
|
57
|
+
const stored = JSON.parse(await readFile(filename, "utf8"));
|
|
58
|
+
assertSchema(stored, schema, filename);
|
|
59
|
+
if (JSON.stringify(stored) !== JSON.stringify(aggregate)) {
|
|
60
|
+
throw new Error(`${aggregate.scenarioId}: stored aggregate does not reproduce raw measurements`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const summaryPath = path.join(resultsDirectory, "aggregate", runSetId, "summary.json");
|
|
64
|
+
const summary = JSON.parse(await readFile(summaryPath, "utf8"));
|
|
65
|
+
if (summary.runSetId !== runSetId
|
|
66
|
+
|| summary.scenarioCount !== scenarios.length
|
|
67
|
+
|| summary.runCount !== expectedAggregates.reduce((sum, aggregate) => sum + aggregate.generatedFromRunCount, 0)
|
|
68
|
+
|| summary.claimsAllowed !== expectedAggregates.some((aggregate) => aggregate.claimsAllowed)) {
|
|
69
|
+
throw new Error(`${runSetId}: aggregate summary identity is inconsistent`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readAggregateRunSetNames(resultsDirectory) {
|
|
74
|
+
try {
|
|
75
|
+
return (await readdir(path.join(resultsDirectory, "aggregate"), { withFileTypes: true }))
|
|
76
|
+
.filter((entry) => entry.isDirectory())
|
|
77
|
+
.map((entry) => entry.name)
|
|
78
|
+
.sort();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error.code === "ENOENT") return [];
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
const options = parseArgs(process.argv.slice(2));
|
|
87
|
+
if (options.help) {
|
|
88
|
+
console.log("Usage: npm run benchmark:profiles:check -- [--results <directory>] [--json]");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const scenarios = await readBenchmarkScenarios(repositoryRoot);
|
|
92
|
+
const resultsDirectory = path.resolve(options.results);
|
|
93
|
+
const runSets = await readBenchmarkRunSets(resultsDirectory);
|
|
94
|
+
const aggregateRunSetNames = await readAggregateRunSetNames(resultsDirectory);
|
|
95
|
+
const rawRunSetNames = new Set(runSets.map((runSet) => runSet.runSetId));
|
|
96
|
+
for (const name of aggregateRunSetNames) {
|
|
97
|
+
if (!rawRunSetNames.has(name)) throw new Error(`${name}: aggregate exists without raw benchmark history`);
|
|
98
|
+
}
|
|
99
|
+
if (runSets.length === 0) {
|
|
100
|
+
const output = { status: "VALID", benchmarkStatus: "NOT_MEASURED", scenarioCount: scenarios.length, runSets: 0 };
|
|
101
|
+
console.log(options.json ? JSON.stringify(output) : "Benchmark schemas valid; no measured run sets are present.");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const runSet of runSets) {
|
|
106
|
+
const aggregates = scenarios.map((scenario) => assertCompleteRunSet(
|
|
107
|
+
scenario,
|
|
108
|
+
runSet.runs.filter((run) => run.scenarioId === scenario.scenarioId),
|
|
109
|
+
));
|
|
110
|
+
await validateStoredAggregates(resultsDirectory, runSet.runSetId, scenarios, aggregates);
|
|
111
|
+
results.push({ runSetId: runSet.runSetId, runCount: runSet.runs.length, claimsAllowed: aggregates.some((aggregate) => aggregate.claimsAllowed) });
|
|
112
|
+
}
|
|
113
|
+
const output = { status: "VALID", benchmarkStatus: "MEASURED", scenarioCount: scenarios.length, runSets: results };
|
|
114
|
+
console.log(options.json ? JSON.stringify(output) : `Benchmark schemas and aggregates valid for ${results.length} run set(s).`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main().catch((error) => {
|
|
118
|
+
console.error(`ForgeLoop benchmark validation: ${error.message}`);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
});
|
package/src/cli.js
CHANGED
|
@@ -35,6 +35,8 @@ import { formatActionVerifyResult } from "./commands/action-verify.js";
|
|
|
35
35
|
import { formatActionShowResult } from "./commands/action-show.js";
|
|
36
36
|
import { formatActionReconcileResult } from "./commands/action-reconcile.js";
|
|
37
37
|
import { formatMetricsResult } from "./commands/metrics.js";
|
|
38
|
+
import { formatUsageRecordResult } from "./commands/usage-record.js";
|
|
39
|
+
import { formatEfficiencyResult } from "./commands/efficiency.js";
|
|
38
40
|
import { formatEvalResult } from "./commands/eval.js";
|
|
39
41
|
import { formatApprovalRequestResult } from "./commands/approval-request.js";
|
|
40
42
|
import { formatApprovalResolveResult } from "./commands/approval-resolve.js";
|
|
@@ -48,14 +50,14 @@ import { formatTraceResult } from "./commands/trace.js";
|
|
|
48
50
|
import { formatReflectResult } from "./commands/reflect.js";
|
|
49
51
|
import { formatProgressResult } from "./commands/progress.js";
|
|
50
52
|
import { formatRecordDecisionCriterionResult } from "./commands/record-decision-criterion.js";
|
|
51
|
-
import { formatNextActionResult } from "./commands/next.js";
|
|
53
|
+
import { formatNextActionResult, formatCompactNextActionResult } from "./commands/next.js";
|
|
52
54
|
import { formatContinuityResult } from "./commands/continuity.js";
|
|
53
55
|
import { formatRecordContinuityResult } from "./commands/record-continuity.js";
|
|
54
56
|
import { formatReconcileContinuityResult } from "./commands/reconcile-continuity.js";
|
|
55
57
|
import { formatClearContinuityResult } from "./commands/clear-continuity.js";
|
|
56
58
|
import { formatTaskCreateResult } from "./commands/task-create.js";
|
|
57
59
|
import { formatTaskListResult } from "./commands/task-list.js";
|
|
58
|
-
import { formatTaskShowResult } from "./commands/task-show.js";
|
|
60
|
+
import { formatTaskShowResult, formatCompactTaskShowResult } from "./commands/task-show.js";
|
|
59
61
|
import { formatTaskScopeResult } from "./commands/task-scope.js";
|
|
60
62
|
import { formatTaskMigrateResult } from "./commands/task-migrate.js";
|
|
61
63
|
import { formatMigrateProtocolResult } from "./commands/migrate-protocol.js";
|
|
@@ -384,7 +386,8 @@ export const COMMAND_HANDLERS = Object.freeze({
|
|
|
384
386
|
},
|
|
385
387
|
next: async ({ target, packageRoot, options }) => {
|
|
386
388
|
const { result } = await COMMAND_EXECUTORS.next({ target, packageRoot, options });
|
|
387
|
-
|
|
389
|
+
if (options.compact) console.log(formatCompactNextActionResult(result));
|
|
390
|
+
else renderJsonOr(options, result, formatNextActionResult);
|
|
388
391
|
return 0;
|
|
389
392
|
},
|
|
390
393
|
continuity: async ({ target, packageRoot, options }) => {
|
|
@@ -510,6 +513,14 @@ export const COMMAND_HANDLERS = Object.freeze({
|
|
|
510
513
|
const { result } = await COMMAND_EXECUTORS.metrics({ target, packageRoot, options });
|
|
511
514
|
renderJsonOr(options, result, formatMetricsResult); return 0;
|
|
512
515
|
},
|
|
516
|
+
"usage-record": async ({ target, packageRoot, options }) => {
|
|
517
|
+
const { result } = await COMMAND_EXECUTORS["usage-record"]({ target, packageRoot, options });
|
|
518
|
+
renderJsonOr(options, result, formatUsageRecordResult); return 0;
|
|
519
|
+
},
|
|
520
|
+
efficiency: async ({ target, packageRoot, options }) => {
|
|
521
|
+
const { result } = await COMMAND_EXECUTORS.efficiency({ target, packageRoot, options });
|
|
522
|
+
renderJsonOr(options, result, formatEfficiencyResult); return 0;
|
|
523
|
+
},
|
|
513
524
|
eval: async ({ target, packageRoot, options }) => {
|
|
514
525
|
const { result, exitCode } = await COMMAND_EXECUTORS.eval({ target, packageRoot, options });
|
|
515
526
|
renderJsonOr(options, result, formatEvalResult); return exitCode;
|
|
@@ -677,7 +688,8 @@ export const COMMAND_HANDLERS = Object.freeze({
|
|
|
677
688
|
},
|
|
678
689
|
"task-show": async ({ target, packageRoot, options }) => {
|
|
679
690
|
const { result } = await COMMAND_EXECUTORS["task-show"]({ target, packageRoot, options });
|
|
680
|
-
|
|
691
|
+
if (options.compact) console.log(formatCompactTaskShowResult(result));
|
|
692
|
+
else renderJsonOr(options, result, formatTaskShowResult);
|
|
681
693
|
return 0;
|
|
682
694
|
},
|
|
683
695
|
"task-lock-status": async ({ target, packageRoot, options }) => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildEfficiencyReport } from "../core/efficiency.js";
|
|
2
|
+
import { withResolvedTask } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
export async function runEfficiency({ target, packageRoot, taskId, baselinePath = null, runtimeContext = null }) {
|
|
5
|
+
return withResolvedTask(target, { taskId, packageRoot, explicitRequired: true }, (ctx) =>
|
|
6
|
+
buildEfficiencyReport({ target, packageRoot, taskId: ctx.taskId, baselinePath, runtimeContext }));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatEfficiencyResult(result) {
|
|
10
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
11
|
+
}
|
|
12
|
+
|
package/src/commands/eval.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { evaluateTrajectory } from "../core/trajectory-evaluation.js";
|
|
2
2
|
import { withResolvedTask } from "../core/task-command.js";
|
|
3
|
-
export async function runEval({ target, packageRoot, taskId, scenarioPath }) {
|
|
4
|
-
return withResolvedTask(target, { taskId, packageRoot }, (ctx) => evaluateTrajectory({
|
|
3
|
+
export async function runEval({ target, packageRoot, taskId, scenarioPath, runtimeContext = null }) {
|
|
4
|
+
return withResolvedTask(target, { taskId, packageRoot }, (ctx) => evaluateTrajectory({
|
|
5
|
+
target,
|
|
6
|
+
packageRoot,
|
|
7
|
+
taskId: ctx.taskId,
|
|
8
|
+
scenarioPath,
|
|
9
|
+
runtimeContext,
|
|
10
|
+
}));
|
|
5
11
|
}
|
|
6
12
|
export function formatEvalResult(result) { return `${JSON.stringify(result, null, 2)}\n`; }
|
package/src/commands/metrics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildTrajectoryMetrics } from "../core/trajectory-metrics.js";
|
|
2
2
|
import { withResolvedTask } from "../core/task-command.js";
|
|
3
|
-
export async function runMetrics({ target, packageRoot, taskId }) {
|
|
3
|
+
export async function runMetrics({ target, packageRoot, taskId, runtimeContext = null }) {
|
|
4
4
|
return withResolvedTask(target, { taskId, packageRoot }, (ctx) =>
|
|
5
|
-
buildTrajectoryMetrics({ target, packageRoot, taskId: ctx.taskId }));
|
|
5
|
+
buildTrajectoryMetrics({ target, packageRoot, taskId: ctx.taskId, runtimeContext }));
|
|
6
6
|
}
|
|
7
7
|
export function formatMetricsResult(result) { return `${JSON.stringify(result, null, 2)}\n`; }
|