@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.
Files changed (73) hide show
  1. package/AGENT_COMPATIBILITY.md +11 -0
  2. package/DOCS_INDEX.md +1 -0
  3. package/GUIDE_ROUTER.md +26 -1
  4. package/LOOP_ENGINEERING.md +48 -0
  5. package/ORCHESTRATOR_INTEGRATION.md +38 -0
  6. package/PROTOCOL_INTEGRATION.md +62 -0
  7. package/README.md +25 -0
  8. package/benchmarks/execution-profiles/README.md +44 -0
  9. package/benchmarks/execution-profiles/api-feature.json +18 -0
  10. package/benchmarks/execution-profiles/authentication-change.json +18 -0
  11. package/benchmarks/execution-profiles/documentation-correction.json +18 -0
  12. package/benchmarks/execution-profiles/infrastructure-release.json +18 -0
  13. package/benchmarks/execution-profiles/novatask-saas-landing-page.json +36 -0
  14. package/benchmarks/execution-profiles/small-bug-fix.json +18 -0
  15. package/benchmarks/execution-profiles/static-landing-page.json +18 -0
  16. package/completions/_forgeloop +6 -4
  17. package/completions/forgeloop.bash +8 -4
  18. package/completions/forgeloop.fish +24 -1
  19. package/docs/AGENT_PROTOCOL_SUMMARY.md +45 -1
  20. package/docs/ARTIFACT_REFERENCE.md +53 -7
  21. package/docs/CLI_REFERENCE.md +83 -1
  22. package/docs/EXECUTION_PROFILE_BENCHMARKS.md +208 -0
  23. package/docs/GETTING_STARTED.md +28 -0
  24. package/docs/MCP.md +6 -0
  25. package/docs/RELEASE_CHECKLIST.md +4 -0
  26. package/docs/TROUBLESHOOTING.md +6 -0
  27. package/docs/UNIVERSAL_INTEGRATION.md +58 -0
  28. package/package.json +14 -2
  29. package/schemas/config.schema.json +1 -0
  30. package/schemas/execution-profile-benchmark-aggregate.schema.json +43 -0
  31. package/schemas/execution-profile-benchmark-run.schema.json +106 -0
  32. package/schemas/execution-profile-benchmark-scenario.schema.json +66 -0
  33. package/schemas/routing-result.schema.json +12 -0
  34. package/schemas/usage.schema.json +30 -0
  35. package/scripts/check-efficiency-regression.mjs +99 -0
  36. package/scripts/generate-agent-protocol-summary.mjs +35 -0
  37. package/scripts/lib/execution-profile-benchmark-io.mjs +67 -0
  38. package/scripts/run-execution-profile-benchmarks.mjs +265 -0
  39. package/scripts/summarize-execution-profile-benchmarks.mjs +84 -0
  40. package/scripts/validate-execution-profile-benchmarks.mjs +120 -0
  41. package/src/cli.js +16 -4
  42. package/src/commands/efficiency.js +12 -0
  43. package/src/commands/eval.js +8 -2
  44. package/src/commands/metrics.js +2 -2
  45. package/src/commands/next.js +26 -2
  46. package/src/commands/route.js +20 -2
  47. package/src/commands/task-show.js +31 -2
  48. package/src/commands/usage-record.js +61 -0
  49. package/src/core/artifact-registry.js +12 -0
  50. package/src/core/cli-command-definitions.js +27 -0
  51. package/src/core/command-executors.js +29 -5
  52. package/src/core/command-input.js +34 -0
  53. package/src/core/config.js +9 -0
  54. package/src/core/efficiency.js +197 -0
  55. package/src/core/error-codes.js +18 -0
  56. package/src/core/execution-profile-benchmarks.js +674 -0
  57. package/src/core/execution-profile-context.js +177 -0
  58. package/src/core/execution-profile.js +248 -0
  59. package/src/core/integration-invocation-policy.js +43 -0
  60. package/src/core/integration-resources.js +22 -1
  61. package/src/core/protocol-info.js +42 -0
  62. package/src/core/resumability.js +27 -1
  63. package/src/core/router.js +23 -1
  64. package/src/core/runtime-context.js +11 -0
  65. package/src/core/schema-validation.js +4 -0
  66. package/src/core/task-paths.js +2 -0
  67. package/src/core/templates.js +4 -0
  68. package/src/core/trace.js +1 -0
  69. package/src/core/trajectory-evaluation.js +2 -2
  70. package/src/core/trajectory-metrics.js +18 -2
  71. package/src/core/usage.js +137 -0
  72. package/src/integration.d.ts +84 -0
  73. 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
- renderJsonOr(options, result, formatNextActionResult);
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
- renderJsonOr(options, result, formatTaskShowResult);
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
+
@@ -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({ target, packageRoot, taskId: ctx.taskId, scenarioPath }));
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`; }
@@ -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`; }