@h9-foundry/agentforge-cli 0.7.1 → 0.9.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/dist/index.js CHANGED
@@ -2,16 +2,17 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
2
2
  import { join } from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import yaml from "js-yaml";
5
- import { renderAuditBundleMarkdown } from "@h9-foundry/agentforge-audit";
5
+ import { buildAuditBundle, createAuditEntry, renderAuditBundleMarkdown } from "@h9-foundry/agentforge-audit";
6
6
  import { createWorkflowState, findWorkspaceRoot } from "@h9-foundry/agentforge-context-engine";
7
7
  import { createPolicyEngine, loadPolicyDocument, resolvePolicy } from "@h9-foundry/agentforge-policy-engine";
8
8
  import { runWorkflow } from "@h9-foundry/agentforge-runtime";
9
- import { agentforgeConfigSchema, auditBundleSchema, designArtifactSchema, designRequestSchema, implementationRequestSchema, incidentRequestSchema, maintenanceRequestSchema, planningArtifactSchema, planningRequestSchema, qaRequestSchema, releaseRequestSchema, securityRequestSchema, workflowDefinitionSchema } from "@h9-foundry/agentforge-schemas";
9
+ import { agentforgeConfigSchema, auditBundleSchema, benchmarkArtifactSchema, designArtifactSchema, designRequestSchema, evalArtifactSchema, evalFixtureCorpusSchema, implementationRequestSchema, incidentRequestSchema, maintenanceRequestSchema, planningArtifactSchema, planningRequestSchema, qaRequestSchema, releaseRequestSchema, schemaFixtures, securityRequestSchema, workflowDefinitionSchema } from "@h9-foundry/agentforge-schemas";
10
10
  import { createBuiltinAdapters } from "./internal/builtin-adapters.js";
11
11
  import { createBuiltinAgentRegistry } from "./internal/builtin-agents.js";
12
12
  import { LocalPluginRegistry } from "./internal/local-plugin-registry.js";
13
13
  export { checkReleaseReadiness, getReleaseGuide, renderReleaseGuide, TARGET_NPM_SCOPE, EXPECTED_PUBLIC_PACKAGES } from "./internal/release-preflight.js";
14
14
  export { verifyReleaseArtifacts } from "./internal/release-verification.js";
15
+ export const startupPresetNames = ["planning-discovery"];
15
16
  const agentforgeConfigTemplate = `version: 1
16
17
  project:
17
18
  name: REPO_NAME
@@ -856,6 +857,15 @@ function readLatestCompleteRunBundle(runsRoot) {
856
857
  if (typeof value !== "string" || value.length === 0) {
857
858
  return undefined;
858
859
  }
860
+ const compactDateTimeMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})$/);
861
+ if (compactDateTimeMatch) {
862
+ const [, year, month, day, hour, minute, second] = compactDateTimeMatch;
863
+ const isoCandidate = `${year}-${month}-${day}T${hour}:${minute}:${second}Z`;
864
+ const parsedCompactDateTime = Date.parse(isoCandidate);
865
+ if (!Number.isNaN(parsedCompactDateTime)) {
866
+ return parsedCompactDateTime;
867
+ }
868
+ }
859
869
  const parsedDate = Date.parse(value);
860
870
  if (!Number.isNaN(parsedDate)) {
861
871
  return parsedDate;
@@ -872,20 +882,24 @@ function readLatestCompleteRunBundle(runsRoot) {
872
882
  const stats = statSync(bundlePath);
873
883
  const bundle = JSON.parse(readFileSync(bundlePath, "utf8"));
874
884
  const bundleRunId = typeof bundle.runId === "string" ? bundle.runId : entry;
875
- const completedAtMs = parseRunTimestampMs(bundle.finishedAt) ??
885
+ const parsedCompletedAtMs = parseRunTimestampMs(bundle.finishedAt) ??
876
886
  parseRunTimestampMs(bundle.startedAt) ??
877
887
  parseRunTimestampMs(bundleRunId) ??
878
- parseRunTimestampMs(entry) ??
879
- stats.mtimeMs;
888
+ parseRunTimestampMs(entry);
889
+ const completedAtMs = parsedCompletedAtMs ?? stats.mtimeMs;
880
890
  return {
881
891
  runDir: entry,
882
892
  bundle,
883
893
  bundleRunId,
884
- completedAtMs
894
+ completedAtMs,
895
+ hasExplicitTimestamp: typeof parsedCompletedAtMs === "number"
885
896
  };
886
897
  })
887
898
  .filter((candidate) => Boolean(candidate))
888
899
  .sort((left, right) => {
900
+ if (left.hasExplicitTimestamp !== right.hasExplicitTimestamp) {
901
+ return left.hasExplicitTimestamp ? -1 : 1;
902
+ }
889
903
  if (left.completedAtMs !== right.completedAtMs) {
890
904
  return right.completedAtMs - left.completedAtMs;
891
905
  }
@@ -893,6 +907,29 @@ function readLatestCompleteRunBundle(runsRoot) {
893
907
  });
894
908
  return candidates[0] ? { runDir: candidates[0].runDir, bundle: candidates[0].bundle } : undefined;
895
909
  }
910
+ function readRunBundleByRef(root, runRef) {
911
+ const config = loadAgentForgeConfig(root);
912
+ const runsRoot = join(root, config.runtime.runsPath);
913
+ const bundlePath = runRef.endsWith(".json") || runRef.includes("/")
914
+ ? (runRef.startsWith("/") ? runRef : join(root, runRef))
915
+ : join(runsRoot, runRef, "bundle.json");
916
+ if (!existsSync(bundlePath)) {
917
+ throw new Error(`Run bundle not found: ${runRef}`);
918
+ }
919
+ const bundle = auditBundleSchema.parse(JSON.parse(readFileSync(bundlePath, "utf8")));
920
+ return {
921
+ runId: typeof bundle.runId === "string" ? bundle.runId : runRef,
922
+ bundlePath,
923
+ bundle
924
+ };
925
+ }
926
+ function extractEvalArtifact(bundle, runRef) {
927
+ const artifact = bundle.lifecycleArtifacts.find((candidate) => candidate.artifactKind === "eval-result");
928
+ if (!artifact) {
929
+ throw new Error(`Run ${runRef} does not contain an eval-result artifact.`);
930
+ }
931
+ return evalArtifactSchema.parse(artifact);
932
+ }
896
933
  function loadAgentForgeConfig(root) {
897
934
  const configPath = join(root, ".agentops", "agentops.yaml");
898
935
  if (!existsSync(configPath)) {
@@ -921,6 +958,425 @@ function loadAgentForgeConfig(root) {
921
958
  function ensureDirectory(pathValue) {
922
959
  mkdirSync(pathValue, { recursive: true });
923
960
  }
961
+ function writeYamlFile(filePath, value) {
962
+ writeFileSync(filePath, yaml.dump(value), "utf8");
963
+ }
964
+ function loadEvalFixtureCorpus() {
965
+ return evalFixtureCorpusSchema.parse(schemaFixtures.evalFixtureCorpus);
966
+ }
967
+ function getEvalSpec(specId) {
968
+ const corpus = loadEvalFixtureCorpus();
969
+ const spec = corpus.specs.find((candidate) => candidate.id === specId);
970
+ if (!spec) {
971
+ throw new Error(`Unknown eval spec: ${specId}`);
972
+ }
973
+ return spec;
974
+ }
975
+ function toBundleRef(run) {
976
+ return `.agentops/runs/${run.runId}/bundle.json`;
977
+ }
978
+ function toSummaryRef(run) {
979
+ return `.agentops/runs/${run.runId}/summary.md`;
980
+ }
981
+ function toSetupRun(workflow, run) {
982
+ return {
983
+ workflow,
984
+ runId: run.runId,
985
+ bundlePath: toBundleRef(run)
986
+ };
987
+ }
988
+ function createBlankEvalWorkspace(root, evalRunId, specId) {
989
+ const workspaceRoot = join(root, ".agentops", "evals", specId, evalRunId, "workspace");
990
+ ensureDirectory(workspaceRoot);
991
+ const evidenceRoot = join(workspaceRoot, ".agentops", "evidence");
992
+ ensureDirectory(evidenceRoot);
993
+ execFileSync("git", ["init"], { cwd: workspaceRoot, stdio: "ignore" });
994
+ execFileSync("git", ["config", "user.email", "eval@example.com"], { cwd: workspaceRoot, stdio: "ignore" });
995
+ execFileSync("git", ["config", "user.name", "AgentForge Eval"], { cwd: workspaceRoot, stdio: "ignore" });
996
+ writeFileSync(join(workspaceRoot, "package.json"), JSON.stringify({
997
+ name: "fixture",
998
+ repository: {
999
+ type: "git",
1000
+ url: "https://github.com/H9-Foundry/fixture.git"
1001
+ },
1002
+ scripts: {
1003
+ test: "echo test",
1004
+ lint: "echo lint",
1005
+ typecheck: "echo typecheck",
1006
+ build: "echo build"
1007
+ }
1008
+ }, null, 2), "utf8");
1009
+ writeFileSync(join(workspaceRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n", "utf8");
1010
+ writeFileSync(join(workspaceRoot, "src.ts"), "export const value = 1;\n", "utf8");
1011
+ writeFileSync(join(evidenceRoot, "dependency-alerts.json"), JSON.stringify({
1012
+ alerts: [
1013
+ {
1014
+ package: "example-dependency",
1015
+ severity: "moderate",
1016
+ summary: "Upgrade pending review for deterministic eval coverage."
1017
+ }
1018
+ ]
1019
+ }, null, 2), "utf8");
1020
+ writeFileSync(join(evidenceRoot, "docs-task.md"), "# Docs follow-up\n\n- Align workflow documentation after maintenance triage.\n", "utf8");
1021
+ execFileSync("git", ["add", "."], { cwd: workspaceRoot, stdio: "ignore" });
1022
+ execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "init"], { cwd: workspaceRoot, stdio: "ignore" });
1023
+ writeFileSync(join(workspaceRoot, "src.ts"), "export const value = 2;\n", "utf8");
1024
+ initProject(workspaceRoot);
1025
+ return workspaceRoot;
1026
+ }
1027
+ function evalRedactionCategories() {
1028
+ return ["github-token", "api-key", "aws-key", "bearer-token", "password", "private-key"];
1029
+ }
1030
+ function createEvalBundle(root, spec, evaluatedRun, workspacePath, setupRuns, deterministicChecks, modelDependentChecks) {
1031
+ const config = loadAgentForgeConfig(root);
1032
+ const policy = resolvePolicy(loadPolicyDocument(join(root, ".agentops", "policy.yaml")), process.env.CI ? "ci" : "local");
1033
+ const state = createWorkflowState({
1034
+ cwd: root,
1035
+ workflow: `eval:${spec.id}`,
1036
+ mode: "inspect",
1037
+ policy
1038
+ });
1039
+ const runsRoot = join(root, config.runtime.runsPath);
1040
+ const outputDir = join(runsRoot, state.runId);
1041
+ ensureDirectory(outputDir);
1042
+ const jsonPath = join(outputDir, "bundle.json");
1043
+ const markdownPath = join(outputDir, "summary.md");
1044
+ const failureCount = deterministicChecks.filter((check) => check.status === "failed").length;
1045
+ const passed = failureCount === 0;
1046
+ const startedAt = new Date().toISOString();
1047
+ const evalArtifact = evalArtifactSchema.parse({
1048
+ schemaVersion: state.version,
1049
+ artifactKind: "eval-result",
1050
+ lifecycleDomain: "evaluate",
1051
+ workflow: {
1052
+ name: state.workflow,
1053
+ displayName: "Eval Runner"
1054
+ },
1055
+ source: {
1056
+ sourceType: "workflow-run",
1057
+ runId: state.runId,
1058
+ inputRefs: [
1059
+ ...(evaluatedRun?.jsonPath ? [evaluatedRun.jsonPath] : []),
1060
+ ...setupRuns.map((setup) => setup.bundlePath)
1061
+ ],
1062
+ issueRefs: ["#165"],
1063
+ githubRefs: []
1064
+ },
1065
+ status: passed ? "complete" : "draft",
1066
+ generatedAt: startedAt,
1067
+ repo: {
1068
+ root: state.repo.root,
1069
+ name: state.repo.name,
1070
+ branch: state.repo.branch
1071
+ },
1072
+ provenance: {
1073
+ generatedBy: "agentforge-runtime",
1074
+ schemaVersion: state.version,
1075
+ executionEnvironment: state.context.ciExecution ? "ci" : "local",
1076
+ repoRoot: state.repo.root
1077
+ },
1078
+ redaction: {
1079
+ applied: true,
1080
+ strategyVersion: "1.0.0",
1081
+ categories: evalRedactionCategories()
1082
+ },
1083
+ auditLink: {
1084
+ bundlePath: jsonPath,
1085
+ entryIds: [`${state.runId}-eval-runner`],
1086
+ findingIds: [],
1087
+ proposedActionIds: []
1088
+ },
1089
+ summary: passed
1090
+ ? `Eval result for ${spec.id} passed ${deterministicChecks.length} deterministic check(s).`
1091
+ : `Eval result for ${spec.id} failed ${failureCount} deterministic check(s).`,
1092
+ payload: {
1093
+ specId: spec.id,
1094
+ specName: spec.name,
1095
+ workflow: spec.workflow,
1096
+ repoFixture: spec.repoFixture,
1097
+ workspacePath,
1098
+ evaluatedRunId: evaluatedRun?.runId,
1099
+ evaluatedBundlePath: evaluatedRun ? toBundleRef(evaluatedRun) : undefined,
1100
+ setupRuns,
1101
+ deterministicChecks,
1102
+ modelDependentChecks,
1103
+ passed,
1104
+ failureCount,
1105
+ warningCount: 0
1106
+ }
1107
+ });
1108
+ state.lifecycleArtifacts = [evalArtifact];
1109
+ state.auditTrail = [
1110
+ createAuditEntry({
1111
+ id: `${state.runId}-eval-runner`,
1112
+ nodeId: "eval-runner",
1113
+ nodeName: "eval-runner",
1114
+ kind: "deterministic",
1115
+ startedAt,
1116
+ completedAt: new Date().toISOString(),
1117
+ status: passed ? "success" : "failed",
1118
+ summary: evalArtifact.summary,
1119
+ toolsRequested: [],
1120
+ toolsExecuted: [],
1121
+ blockedActions: [],
1122
+ validationPassed: passed
1123
+ }),
1124
+ createAuditEntry({
1125
+ id: `${state.runId}-report`,
1126
+ nodeId: "report",
1127
+ nodeName: "final-report",
1128
+ kind: "report",
1129
+ startedAt,
1130
+ completedAt: new Date().toISOString(),
1131
+ status: "success",
1132
+ summary: "Generated eval result artifacts.",
1133
+ toolsRequested: [],
1134
+ toolsExecuted: [],
1135
+ blockedActions: [],
1136
+ validationPassed: true
1137
+ })
1138
+ ];
1139
+ const bundle = buildAuditBundle(state, {
1140
+ startedAt,
1141
+ finishedAt: new Date().toISOString(),
1142
+ status: passed ? "success" : "partial",
1143
+ jsonPath,
1144
+ markdownPath,
1145
+ provenance: {
1146
+ generatedBy: "agentforge-runtime",
1147
+ schemaVersion: state.version,
1148
+ executionEnvironment: state.context.ciExecution ? "ci" : "local",
1149
+ repoRoot: state.repo.root
1150
+ },
1151
+ redaction: {
1152
+ applied: true,
1153
+ strategyVersion: "1.0.0",
1154
+ categories: evalRedactionCategories()
1155
+ },
1156
+ components: []
1157
+ });
1158
+ writeFileSync(jsonPath, JSON.stringify(bundle, null, 2), "utf8");
1159
+ writeFileSync(markdownPath, renderAuditBundleMarkdown(bundle), "utf8");
1160
+ return { bundle, jsonPath, markdownPath, outputDir };
1161
+ }
1162
+ function compareDeterministicChecks(baselineChecks, candidateChecks) {
1163
+ const regressions = [];
1164
+ const improvements = [];
1165
+ const nonComparableFindings = [];
1166
+ let unchangedCount = 0;
1167
+ const baselineByName = new Map(baselineChecks.map((check) => [check.name, check]));
1168
+ const candidateByName = new Map(candidateChecks.map((check) => [check.name, check]));
1169
+ const checkNames = [...new Set([...baselineByName.keys(), ...candidateByName.keys()])].sort();
1170
+ for (const name of checkNames) {
1171
+ const baselineCheck = baselineByName.get(name);
1172
+ const candidateCheck = candidateByName.get(name);
1173
+ if (!baselineCheck || !candidateCheck) {
1174
+ nonComparableFindings.push(`Deterministic check \`${name}\` is missing from one of the eval results.`);
1175
+ continue;
1176
+ }
1177
+ if (baselineCheck.status === candidateCheck.status) {
1178
+ unchangedCount += 1;
1179
+ continue;
1180
+ }
1181
+ if (baselineCheck.status === "not_applicable" || candidateCheck.status === "not_applicable") {
1182
+ nonComparableFindings.push(`Deterministic check \`${name}\` changed between comparable and not_applicable states (${baselineCheck.status} -> ${candidateCheck.status}).`);
1183
+ continue;
1184
+ }
1185
+ if (baselineCheck.status === "passed" && candidateCheck.status === "failed") {
1186
+ regressions.push({
1187
+ name,
1188
+ classification: "regression",
1189
+ baselineStatus: baselineCheck.status,
1190
+ candidateStatus: candidateCheck.status,
1191
+ details: candidateCheck.details ?? baselineCheck.details
1192
+ });
1193
+ continue;
1194
+ }
1195
+ if (baselineCheck.status === "failed" && candidateCheck.status === "passed") {
1196
+ improvements.push({
1197
+ name,
1198
+ classification: "improvement",
1199
+ baselineStatus: baselineCheck.status,
1200
+ candidateStatus: candidateCheck.status,
1201
+ details: candidateCheck.details ?? baselineCheck.details
1202
+ });
1203
+ continue;
1204
+ }
1205
+ nonComparableFindings.push(`Deterministic check \`${name}\` changed in an unsupported way (${baselineCheck.status} -> ${candidateCheck.status}).`);
1206
+ }
1207
+ return { regressions, improvements, unchangedCount, nonComparableFindings };
1208
+ }
1209
+ function compareEvalArtifacts(baselineRunId, baselineBundlePath, baselineArtifact, candidateRunId, candidateBundlePath, candidateArtifact) {
1210
+ if (baselineArtifact.payload.specId !== candidateArtifact.payload.specId) {
1211
+ return {
1212
+ runId: candidateRunId,
1213
+ bundlePath: candidateBundlePath,
1214
+ specId: candidateArtifact.payload.specId,
1215
+ workflow: candidateArtifact.payload.workflow,
1216
+ comparable: false,
1217
+ passed: candidateArtifact.payload.passed,
1218
+ failureCount: candidateArtifact.payload.failureCount,
1219
+ deterministicCheckCount: candidateArtifact.payload.deterministicChecks.length,
1220
+ regressions: [],
1221
+ improvements: [],
1222
+ unchangedCount: 0,
1223
+ nonComparableFindings: [
1224
+ `Spec mismatch: baseline ${baselineArtifact.payload.specId} vs candidate ${candidateArtifact.payload.specId}.`
1225
+ ]
1226
+ };
1227
+ }
1228
+ if (baselineArtifact.payload.workflow !== candidateArtifact.payload.workflow) {
1229
+ return {
1230
+ runId: candidateRunId,
1231
+ bundlePath: candidateBundlePath,
1232
+ specId: candidateArtifact.payload.specId,
1233
+ workflow: candidateArtifact.payload.workflow,
1234
+ comparable: false,
1235
+ passed: candidateArtifact.payload.passed,
1236
+ failureCount: candidateArtifact.payload.failureCount,
1237
+ deterministicCheckCount: candidateArtifact.payload.deterministicChecks.length,
1238
+ regressions: [],
1239
+ improvements: [],
1240
+ unchangedCount: 0,
1241
+ nonComparableFindings: [
1242
+ `Workflow mismatch: baseline ${baselineArtifact.payload.workflow} vs candidate ${candidateArtifact.payload.workflow}.`
1243
+ ]
1244
+ };
1245
+ }
1246
+ const comparison = compareDeterministicChecks(baselineArtifact.payload.deterministicChecks, candidateArtifact.payload.deterministicChecks);
1247
+ return {
1248
+ runId: candidateRunId,
1249
+ bundlePath: candidateBundlePath,
1250
+ specId: candidateArtifact.payload.specId,
1251
+ workflow: candidateArtifact.payload.workflow,
1252
+ comparable: comparison.nonComparableFindings.length === 0,
1253
+ passed: candidateArtifact.payload.passed,
1254
+ failureCount: candidateArtifact.payload.failureCount,
1255
+ deterministicCheckCount: candidateArtifact.payload.deterministicChecks.length,
1256
+ regressions: comparison.regressions,
1257
+ improvements: comparison.improvements,
1258
+ unchangedCount: comparison.unchangedCount,
1259
+ nonComparableFindings: comparison.nonComparableFindings
1260
+ };
1261
+ }
1262
+ function createBenchmarkBundle(root, baselineRunId, baselineBundlePath, baselineArtifact, comparedRuns) {
1263
+ const config = loadAgentForgeConfig(root);
1264
+ const policy = resolvePolicy(loadPolicyDocument(join(root, ".agentops", "policy.yaml")), process.env.CI ? "ci" : "local");
1265
+ const state = createWorkflowState({
1266
+ cwd: root,
1267
+ workflow: "eval:compare",
1268
+ mode: "inspect",
1269
+ policy
1270
+ });
1271
+ const runsRoot = join(root, config.runtime.runsPath);
1272
+ const outputDir = join(runsRoot, state.runId);
1273
+ ensureDirectory(outputDir);
1274
+ const jsonPath = join(outputDir, "bundle.json");
1275
+ const markdownPath = join(outputDir, "summary.md");
1276
+ const regressionCount = comparedRuns.reduce((total, candidate) => total + candidate.regressions.length, 0);
1277
+ const improvementCount = comparedRuns.reduce((total, candidate) => total + candidate.improvements.length, 0);
1278
+ const unchangedCount = comparedRuns.reduce((total, candidate) => total + candidate.unchangedCount, 0);
1279
+ const nonComparableCount = comparedRuns.reduce((total, candidate) => total + candidate.nonComparableFindings.length, 0);
1280
+ const summaryConclusion = regressionCount > 0
1281
+ ? `Detected ${regressionCount} deterministic regression(s) across compared eval results.`
1282
+ : improvementCount > 0
1283
+ ? `Detected ${improvementCount} deterministic improvement(s) with no regressions.`
1284
+ : nonComparableCount > 0
1285
+ ? `Compared eval results contain ${nonComparableCount} non-comparable difference(s) and no deterministic regressions.`
1286
+ : "No deterministic regressions detected across compared eval results.";
1287
+ const benchmarkArtifact = benchmarkArtifactSchema.parse({
1288
+ schemaVersion: state.version,
1289
+ artifactKind: "benchmark-summary",
1290
+ lifecycleDomain: "evaluate",
1291
+ workflow: {
1292
+ name: state.workflow,
1293
+ displayName: "Eval Benchmark Compare"
1294
+ },
1295
+ source: {
1296
+ sourceType: "workflow-run",
1297
+ runId: state.runId,
1298
+ inputRefs: [baselineBundlePath, ...comparedRuns.map((candidate) => candidate.bundlePath)],
1299
+ issueRefs: ["#166"],
1300
+ githubRefs: []
1301
+ },
1302
+ status: "complete",
1303
+ generatedAt: new Date().toISOString(),
1304
+ repo: {
1305
+ root: state.repo.root,
1306
+ name: state.repo.name,
1307
+ branch: state.repo.branch
1308
+ },
1309
+ provenance: {
1310
+ generatedBy: "agentforge-runtime",
1311
+ schemaVersion: state.version,
1312
+ executionEnvironment: state.context.ciExecution ? "ci" : "local",
1313
+ repoRoot: state.repo.root
1314
+ },
1315
+ redaction: {
1316
+ applied: true,
1317
+ strategyVersion: "1.0.0",
1318
+ categories: evalRedactionCategories()
1319
+ },
1320
+ auditLink: {
1321
+ bundlePath: jsonPath,
1322
+ entryIds: [`${state.runId}-benchmark-compare`],
1323
+ findingIds: [],
1324
+ proposedActionIds: []
1325
+ },
1326
+ summary: summaryConclusion,
1327
+ payload: {
1328
+ baselineRunId,
1329
+ baselineBundlePath,
1330
+ baselineSpecId: baselineArtifact.payload.specId,
1331
+ baselineWorkflow: baselineArtifact.payload.workflow,
1332
+ comparedRuns,
1333
+ regressionCount,
1334
+ improvementCount,
1335
+ unchangedCount,
1336
+ nonComparableCount,
1337
+ summaryConclusion
1338
+ }
1339
+ });
1340
+ state.lifecycleArtifacts = [benchmarkArtifact];
1341
+ state.auditTrail = [
1342
+ createAuditEntry({
1343
+ id: `${state.runId}-benchmark-compare`,
1344
+ nodeId: "benchmark-compare",
1345
+ nodeName: "benchmark-compare",
1346
+ kind: "deterministic",
1347
+ startedAt: new Date().toISOString(),
1348
+ completedAt: new Date().toISOString(),
1349
+ status: regressionCount > 0 ? "failed" : "success",
1350
+ summary: benchmarkArtifact.summary,
1351
+ toolsRequested: [],
1352
+ toolsExecuted: [],
1353
+ blockedActions: [],
1354
+ validationPassed: regressionCount === 0
1355
+ })
1356
+ ];
1357
+ const bundle = buildAuditBundle(state, {
1358
+ startedAt: new Date().toISOString(),
1359
+ finishedAt: new Date().toISOString(),
1360
+ status: regressionCount > 0 || nonComparableCount > 0 ? "partial" : "success",
1361
+ jsonPath,
1362
+ markdownPath,
1363
+ provenance: {
1364
+ generatedBy: "agentforge-runtime",
1365
+ schemaVersion: state.version,
1366
+ executionEnvironment: state.context.ciExecution ? "ci" : "local",
1367
+ repoRoot: state.repo.root
1368
+ },
1369
+ redaction: {
1370
+ applied: true,
1371
+ strategyVersion: "1.0.0",
1372
+ categories: evalRedactionCategories()
1373
+ },
1374
+ components: []
1375
+ });
1376
+ writeFileSync(jsonPath, JSON.stringify(bundle, null, 2), "utf8");
1377
+ writeFileSync(markdownPath, renderAuditBundleMarkdown(bundle), "utf8");
1378
+ return { bundle, jsonPath, markdownPath, outputDir };
1379
+ }
924
1380
  function ensureInitFiles(root) {
925
1381
  const created = [];
926
1382
  const configDir = join(root, ".agentops");
@@ -1045,10 +1501,45 @@ function validateWorkflowAgents(workflow, agents, blockedPlugins) {
1045
1501
  throw new Error(`Workflow agent is not registered: ${node.agent}`);
1046
1502
  }
1047
1503
  }
1048
- export function initProject(cwd = process.cwd()) {
1504
+ function createPlanningDiscoveryPresetRequest(root) {
1505
+ const repoName = root.split("/").at(-1) ?? "this repository";
1506
+ const pathHints = ["README.md", "package.json", "src", "docs"].filter((pathHint) => existsSync(join(root, pathHint)));
1507
+ return planningRequestSchema.parse({
1508
+ problemStatement: `Plan the next safe local-first improvement for ${repoName}.`,
1509
+ goals: ["Produce one planning brief artifact", "Identify a bounded next step before opening a pull request"],
1510
+ constraints: ["Keep the default path local-first and read-only", "Prefer a small, reviewable next change"],
1511
+ pathHints,
1512
+ assumptions: ["This preset is a starter request that can be edited after initialization if the repository needs different focus."]
1513
+ });
1514
+ }
1515
+ function applyStartupPreset(root, preset) {
1516
+ const requestsRoot = join(root, ".agentops", "requests");
1517
+ ensureDirectory(requestsRoot);
1518
+ switch (preset) {
1519
+ case "planning-discovery": {
1520
+ const requestPath = join(requestsRoot, "planning.yaml");
1521
+ const created = !existsSync(requestPath);
1522
+ if (created) {
1523
+ writeYamlFile(requestPath, createPlanningDiscoveryPresetRequest(root));
1524
+ }
1525
+ return {
1526
+ preset,
1527
+ workflow: "planning-discovery",
1528
+ requestPath,
1529
+ created
1530
+ };
1531
+ }
1532
+ }
1533
+ }
1534
+ export function initProject(cwd = process.cwd(), options) {
1049
1535
  const root = findWorkspaceRoot(cwd);
1050
1536
  const created = ensureInitFiles(root);
1051
- return { root, created };
1537
+ const preset = options?.preset ? applyStartupPreset(root, options.preset) : undefined;
1538
+ return {
1539
+ root,
1540
+ created,
1541
+ ...(preset ? { preset } : {})
1542
+ };
1052
1543
  }
1053
1544
  export function scanProject(cwd = process.cwd()) {
1054
1545
  const root = findWorkspaceRoot(cwd);
@@ -1121,6 +1612,261 @@ export async function runLocalWorkflow(workflowName, cwd = process.cwd()) {
1121
1612
  artifactKinds: bundle.lifecycleArtifacts.map((artifact) => artifact.artifactKind)
1122
1613
  };
1123
1614
  }
1615
+ function checkResult(status, name, expected, actual, details) {
1616
+ return {
1617
+ name,
1618
+ status,
1619
+ expected,
1620
+ actual,
1621
+ ...(details ? { details } : {})
1622
+ };
1623
+ }
1624
+ function compareEvalSpec(spec, bundle, executionError) {
1625
+ const checks = [];
1626
+ if (!bundle) {
1627
+ checks.push(checkResult("failed", "workflow-execution", "successful workflow execution", executionError ?? "unknown failure", "The eval runner could not produce an evaluated workflow bundle."));
1628
+ return {
1629
+ deterministicChecks: checks,
1630
+ modelDependentChecks: [
1631
+ {
1632
+ name: "rubric-scoring",
1633
+ status: "not_executed",
1634
+ details: "Provider-dependent scoring is out of scope for the first local eval runner slice."
1635
+ }
1636
+ ]
1637
+ };
1638
+ }
1639
+ checks.push(checkResult(bundle.status === spec.expectedStatus ? "passed" : "failed", "run-status", spec.expectedStatus, bundle.status, "The evaluated workflow status should match the deterministic eval spec."));
1640
+ checks.push(checkResult(bundle.redaction.applied === spec.redactionExpectations.applied ? "passed" : "failed", "redaction-applied", String(spec.redactionExpectations.applied), String(bundle.redaction.applied)));
1641
+ for (const category of spec.redactionExpectations.expectedCategories) {
1642
+ checks.push(checkResult(bundle.redaction.categories.includes(category) ? "passed" : "failed", `redaction-category:${category}`, category, bundle.redaction.categories.join(", ")));
1643
+ }
1644
+ checks.push(checkResult(bundle.policy.defaults.executionMode === spec.policyExpectations.executionMode ? "passed" : "failed", "policy-execution-mode", spec.policyExpectations.executionMode, bundle.policy.defaults.executionMode));
1645
+ if (spec.policyExpectations.readOnly) {
1646
+ checks.push(checkResult(bundle.policy.defaults.writes !== "allow" ? "passed" : "failed", "policy-read-only", "writes not equal allow", bundle.policy.defaults.writes));
1647
+ }
1648
+ for (const sideEffectClass of spec.policyExpectations.sideEffectClasses) {
1649
+ checks.push(checkResult("not_applicable", `side-effect-class:${sideEffectClass}`, sideEffectClass, undefined, "The first eval runner records policy posture and workflow outputs but does not inspect adapter-level side-effect execution traces."));
1650
+ }
1651
+ for (const expectedArtifact of spec.artifactExpectations) {
1652
+ const actualArtifact = bundle.lifecycleArtifacts.find((artifact) => artifact.artifactKind === expectedArtifact.artifactKind);
1653
+ checks.push(checkResult(actualArtifact ? "passed" : "failed", `artifact-kind:${expectedArtifact.artifactKind}`, expectedArtifact.artifactKind, actualArtifact?.artifactKind));
1654
+ if (!actualArtifact || typeof actualArtifact.payload !== "object" || actualArtifact.payload === null) {
1655
+ continue;
1656
+ }
1657
+ const payload = actualArtifact.payload;
1658
+ for (const field of expectedArtifact.requiredPayloadFields) {
1659
+ checks.push(checkResult(field in payload ? "passed" : "failed", `payload-field:${expectedArtifact.artifactKind}:${field}`, field, Object.keys(payload).join(", ")));
1660
+ }
1661
+ for (const term of expectedArtifact.requiredSummaryTerms) {
1662
+ const summary = actualArtifact.summary.toLowerCase();
1663
+ checks.push(checkResult(summary.includes(term.toLowerCase()) ? "passed" : "failed", `summary-term:${expectedArtifact.artifactKind}:${term}`, term, actualArtifact.summary));
1664
+ }
1665
+ }
1666
+ if (spec.artifactExpectations.length === 0) {
1667
+ checks.push(checkResult(bundle.lifecycleArtifacts.length === 0 ? "passed" : "failed", "artifact-count", "0", String(bundle.lifecycleArtifacts.length)));
1668
+ }
1669
+ return {
1670
+ deterministicChecks: checks,
1671
+ modelDependentChecks: [
1672
+ {
1673
+ name: "rubric-scoring",
1674
+ status: "not_executed",
1675
+ details: "Provider-dependent scoring is out of scope for the first local eval runner slice."
1676
+ }
1677
+ ]
1678
+ };
1679
+ }
1680
+ async function executeEvalWorkflow(spec, workspaceRoot) {
1681
+ const setupRuns = [];
1682
+ const requestsRoot = join(workspaceRoot, ".agentops", "requests");
1683
+ ensureDirectory(requestsRoot);
1684
+ const runPlanning = async () => {
1685
+ writeYamlFile(join(requestsRoot, "planning.yaml"), schemaFixtures.planningRequest);
1686
+ return runLocalWorkflow("planning-discovery", workspaceRoot);
1687
+ };
1688
+ const runDesign = async () => {
1689
+ const planningRun = await runPlanning();
1690
+ setupRuns.push(toSetupRun("planning-discovery", planningRun));
1691
+ writeYamlFile(join(requestsRoot, "design.yaml"), {
1692
+ ...schemaFixtures.designRequest,
1693
+ planningBriefRef: toBundleRef(planningRun)
1694
+ });
1695
+ return runLocalWorkflow("architecture-design-review", workspaceRoot);
1696
+ };
1697
+ const runImplementation = async () => {
1698
+ const designRun = await runDesign();
1699
+ setupRuns.push(toSetupRun("architecture-design-review", designRun));
1700
+ writeYamlFile(join(requestsRoot, "implementation.yaml"), {
1701
+ ...schemaFixtures.implementationRequest,
1702
+ designRecordRef: toBundleRef(designRun)
1703
+ });
1704
+ return runLocalWorkflow("implementation-proposal", workspaceRoot);
1705
+ };
1706
+ const runQa = async () => {
1707
+ const implementationRun = await runImplementation();
1708
+ setupRuns.push(toSetupRun("implementation-proposal", implementationRun));
1709
+ writeYamlFile(join(requestsRoot, "qa.yaml"), {
1710
+ ...schemaFixtures.qaRequest,
1711
+ targetRef: toBundleRef(implementationRun),
1712
+ evidenceSources: [toSummaryRef(implementationRun)]
1713
+ });
1714
+ return runLocalWorkflow("qa-review", workspaceRoot);
1715
+ };
1716
+ const runSecurity = async () => {
1717
+ const qaRun = await runQa();
1718
+ setupRuns.push(toSetupRun("qa-review", qaRun));
1719
+ writeYamlFile(join(requestsRoot, "security.yaml"), {
1720
+ ...schemaFixtures.securityRequest,
1721
+ targetRef: toBundleRef(qaRun),
1722
+ evidenceSources: [toSummaryRef(qaRun)]
1723
+ });
1724
+ return runLocalWorkflow("security-review", workspaceRoot);
1725
+ };
1726
+ const runRelease = async () => {
1727
+ const securityRun = await runSecurity();
1728
+ setupRuns.push(toSetupRun("security-review", securityRun));
1729
+ const qaRun = setupRuns.find((run) => run.workflow === "qa-review");
1730
+ if (!qaRun) {
1731
+ throw new Error("QA setup run was not recorded before release eval execution.");
1732
+ }
1733
+ writeYamlFile(join(requestsRoot, "release.yaml"), {
1734
+ ...schemaFixtures.releaseRequest,
1735
+ qaReportRefs: [qaRun.bundlePath],
1736
+ securityReportRefs: [toBundleRef(securityRun)],
1737
+ evidenceSources: [toSummaryRef(securityRun)]
1738
+ });
1739
+ return runLocalWorkflow("release-readiness", workspaceRoot);
1740
+ };
1741
+ switch (spec.workflow) {
1742
+ case "pr-review":
1743
+ return { evaluatedRun: await runLocalWorkflow("pr-review", workspaceRoot), setupRuns };
1744
+ case "planning-discovery":
1745
+ writeYamlFile(join(requestsRoot, "planning.yaml"), spec.request);
1746
+ return { evaluatedRun: await runLocalWorkflow("planning-discovery", workspaceRoot), setupRuns };
1747
+ case "architecture-design-review": {
1748
+ const planningRun = await runPlanning();
1749
+ setupRuns.push(toSetupRun("planning-discovery", planningRun));
1750
+ writeYamlFile(join(requestsRoot, "design.yaml"), {
1751
+ ...spec.request,
1752
+ planningBriefRef: toBundleRef(planningRun)
1753
+ });
1754
+ return { evaluatedRun: await runLocalWorkflow("architecture-design-review", workspaceRoot), setupRuns };
1755
+ }
1756
+ case "implementation-proposal": {
1757
+ const designRun = await runDesign();
1758
+ setupRuns.push(toSetupRun("architecture-design-review", designRun));
1759
+ writeYamlFile(join(requestsRoot, "implementation.yaml"), {
1760
+ ...spec.request,
1761
+ designRecordRef: toBundleRef(designRun)
1762
+ });
1763
+ return { evaluatedRun: await runLocalWorkflow("implementation-proposal", workspaceRoot), setupRuns };
1764
+ }
1765
+ case "qa-review": {
1766
+ const implementationRun = await runImplementation();
1767
+ setupRuns.push(toSetupRun("implementation-proposal", implementationRun));
1768
+ writeYamlFile(join(requestsRoot, "qa.yaml"), {
1769
+ ...spec.request,
1770
+ targetRef: toBundleRef(implementationRun),
1771
+ evidenceSources: [toSummaryRef(implementationRun)]
1772
+ });
1773
+ return { evaluatedRun: await runLocalWorkflow("qa-review", workspaceRoot), setupRuns };
1774
+ }
1775
+ case "security-review": {
1776
+ const qaRun = await runQa();
1777
+ setupRuns.push(toSetupRun("qa-review", qaRun));
1778
+ writeYamlFile(join(requestsRoot, "security.yaml"), {
1779
+ ...spec.request,
1780
+ targetRef: toBundleRef(qaRun),
1781
+ evidenceSources: [toSummaryRef(qaRun)]
1782
+ });
1783
+ return { evaluatedRun: await runLocalWorkflow("security-review", workspaceRoot), setupRuns };
1784
+ }
1785
+ case "maintenance-triage": {
1786
+ const releaseRun = await runRelease();
1787
+ setupRuns.push(toSetupRun("release-readiness", releaseRun));
1788
+ writeYamlFile(join(requestsRoot, "maintenance.yaml"), {
1789
+ ...spec.request,
1790
+ releaseReportRefs: [toBundleRef(releaseRun)]
1791
+ });
1792
+ return { evaluatedRun: await runLocalWorkflow("maintenance-triage", workspaceRoot), setupRuns };
1793
+ }
1794
+ }
1795
+ }
1796
+ export async function runLocalEval(specId, cwd = process.cwd()) {
1797
+ const root = findWorkspaceRoot(cwd);
1798
+ ensureInitFiles(root);
1799
+ const spec = getEvalSpec(specId);
1800
+ const controlPolicy = resolvePolicy(loadPolicyDocument(join(root, ".agentops", "policy.yaml")), process.env.CI ? "ci" : "local");
1801
+ const controlState = createWorkflowState({
1802
+ cwd: root,
1803
+ workflow: `eval:${spec.id}`,
1804
+ mode: controlPolicy.defaults.executionMode,
1805
+ policy: controlPolicy
1806
+ });
1807
+ const workspaceRoot = spec.repoFixture === "agentforge-monorepo" ? root : createBlankEvalWorkspace(root, controlState.runId, spec.id);
1808
+ let evaluatedRun;
1809
+ let setupRuns = [];
1810
+ let executionError;
1811
+ try {
1812
+ const result = await executeEvalWorkflow(spec, workspaceRoot);
1813
+ evaluatedRun = result.evaluatedRun;
1814
+ setupRuns = result.setupRuns;
1815
+ }
1816
+ catch (error) {
1817
+ executionError = error instanceof Error ? error.message : String(error);
1818
+ }
1819
+ const evaluatedBundle = evaluatedRun && existsSync(evaluatedRun.jsonPath)
1820
+ ? auditBundleSchema.parse(JSON.parse(readFileSync(evaluatedRun.jsonPath, "utf8")))
1821
+ : undefined;
1822
+ const { deterministicChecks, modelDependentChecks } = compareEvalSpec(spec, evaluatedBundle, executionError);
1823
+ const { bundle, jsonPath, markdownPath, outputDir } = createEvalBundle(root, spec, evaluatedRun, workspaceRoot, setupRuns, deterministicChecks, modelDependentChecks);
1824
+ return {
1825
+ runId: bundle.runId,
1826
+ specId: spec.id,
1827
+ workflow: spec.workflow,
1828
+ outputDir,
1829
+ jsonPath,
1830
+ markdownPath,
1831
+ status: bundle.status,
1832
+ evaluatedRunId: evaluatedRun?.runId,
1833
+ evaluatedBundlePath: evaluatedRun ? toBundleRef(evaluatedRun) : undefined,
1834
+ setupRunCount: setupRuns.length,
1835
+ deterministicCheckCount: deterministicChecks.length,
1836
+ deterministicFailures: deterministicChecks.filter((check) => check.status === "failed").length,
1837
+ artifactKinds: bundle.lifecycleArtifacts.map((artifact) => artifact.artifactKind)
1838
+ };
1839
+ }
1840
+ export function compareLocalEvalRuns(baselineRunRef, candidateRunRefs, cwd = process.cwd()) {
1841
+ if (candidateRunRefs.length === 0) {
1842
+ throw new Error("Provide at least one candidate eval run to compare against the baseline.");
1843
+ }
1844
+ const root = findWorkspaceRoot(cwd);
1845
+ ensureInitFiles(root);
1846
+ const baseline = readRunBundleByRef(root, baselineRunRef);
1847
+ const baselineArtifact = extractEvalArtifact(baseline.bundle, baselineRunRef);
1848
+ const comparedRuns = candidateRunRefs.map((candidateRunRef) => {
1849
+ const candidate = readRunBundleByRef(root, candidateRunRef);
1850
+ const candidateArtifact = extractEvalArtifact(candidate.bundle, candidateRunRef);
1851
+ return compareEvalArtifacts(baseline.runId, baseline.bundlePath, baselineArtifact, candidate.runId, candidate.bundlePath, candidateArtifact);
1852
+ });
1853
+ const { bundle, jsonPath, markdownPath, outputDir } = createBenchmarkBundle(root, baseline.runId, baseline.bundlePath, baselineArtifact, comparedRuns);
1854
+ return {
1855
+ runId: bundle.runId,
1856
+ outputDir,
1857
+ jsonPath,
1858
+ markdownPath,
1859
+ status: bundle.status,
1860
+ baselineRunId: baseline.runId,
1861
+ comparedRunIds: comparedRuns.map((candidate) => candidate.runId),
1862
+ comparableRunCount: comparedRuns.filter((candidate) => candidate.comparable).length,
1863
+ regressionCount: comparedRuns.reduce((total, candidate) => total + candidate.regressions.length, 0),
1864
+ improvementCount: comparedRuns.reduce((total, candidate) => total + candidate.improvements.length, 0),
1865
+ unchangedCount: comparedRuns.reduce((total, candidate) => total + candidate.unchangedCount, 0),
1866
+ nonComparableCount: comparedRuns.reduce((total, candidate) => total + candidate.nonComparableFindings.length, 0),
1867
+ artifactKinds: bundle.lifecycleArtifacts.map((artifact) => artifact.artifactKind)
1868
+ };
1869
+ }
1124
1870
  export function explainLastRun(cwd = process.cwd()) {
1125
1871
  const root = findWorkspaceRoot(cwd);
1126
1872
  const config = loadAgentForgeConfig(root);