@h9-foundry/agentforge-cli 0.6.0 → 0.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.
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { agentManifestSchema, agentOutputSchema, designArtifactSchema, implementationArtifactSchema, implementationInventorySchema, qaRequestSchema, planningArtifactSchema } from "@h9-foundry/agentforge-schemas";
3
+ import { agentManifestSchema, agentOutputSchema, designArtifactSchema, implementationArtifactSchema, implementationInventorySchema, qaArtifactSchema, qaEvidenceNormalizationSchema, qaRequestSchema, securityArtifactSchema, securityEvidenceNormalizationSchema, securityRequestSchema, planningArtifactSchema } from "@h9-foundry/agentforge-schemas";
4
4
  const contextCollectorAgent = {
5
5
  manifest: agentManifestSchema.parse({
6
6
  version: 1,
@@ -83,6 +83,71 @@ function buildValidationCommand(packageManager, scriptName, packageName) {
83
83
  }
84
84
  return `${packageManager} ${scriptName}`;
85
85
  }
86
+ function collectValidationCommands(repoRoot, packageManager, packageScopes) {
87
+ const discoveredValidationCommands = [];
88
+ const registerScripts = (packageJsonPath, source, packageName) => {
89
+ const scripts = parsePackageScripts(packageJsonPath);
90
+ for (const scriptName of Object.keys(scripts)) {
91
+ const command = buildValidationCommand(packageManager, scriptName, packageName);
92
+ discoveredValidationCommands.push({
93
+ command,
94
+ source,
95
+ classification: allowedValidationScriptNames.has(scriptName) ? "approval_required" : "deny",
96
+ reason: allowedValidationScriptNames.has(scriptName)
97
+ ? "Discovered from a bounded repository script; execution would still require approval."
98
+ : "Command is not in the bounded allowlist for workflow validation."
99
+ });
100
+ }
101
+ };
102
+ if (!repoRoot) {
103
+ return discoveredValidationCommands;
104
+ }
105
+ registerScripts(join(repoRoot, "package.json"), "package-script");
106
+ for (const packageScope of packageScopes) {
107
+ const packageJsonPath = join(repoRoot, packageScope, "package.json");
108
+ if (!existsSync(packageJsonPath)) {
109
+ continue;
110
+ }
111
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
112
+ const packageName = isRecord(parsed) && typeof parsed.name === "string" ? parsed.name : packageScope;
113
+ registerScripts(packageJsonPath, "workspace-script", packageName);
114
+ }
115
+ return discoveredValidationCommands;
116
+ }
117
+ function loadBundleArtifactKinds(bundlePath) {
118
+ if (!existsSync(bundlePath)) {
119
+ return [];
120
+ }
121
+ const parsed = JSON.parse(readFileSync(bundlePath, "utf8"));
122
+ if (!isRecord(parsed) || !Array.isArray(parsed.lifecycleArtifacts)) {
123
+ return [];
124
+ }
125
+ return parsed.lifecycleArtifacts
126
+ .map((artifact) => (isRecord(artifact) && typeof artifact.artifactKind === "string" ? artifact.artifactKind : undefined))
127
+ .filter((artifactKind) => Boolean(artifactKind));
128
+ }
129
+ function loadBundleArtifactPayloadPaths(bundlePath) {
130
+ if (!existsSync(bundlePath)) {
131
+ return [];
132
+ }
133
+ const parsed = JSON.parse(readFileSync(bundlePath, "utf8"));
134
+ if (!isRecord(parsed) || !Array.isArray(parsed.lifecycleArtifacts)) {
135
+ return [];
136
+ }
137
+ return parsed.lifecycleArtifacts.flatMap((artifact) => {
138
+ if (!isRecord(artifact) || !isRecord(artifact.payload)) {
139
+ return [];
140
+ }
141
+ const payload = artifact.payload;
142
+ if (Array.isArray(payload.affectedPaths)) {
143
+ return asStringArray(payload.affectedPaths);
144
+ }
145
+ if (Array.isArray(payload.evidenceSources)) {
146
+ return asStringArray(payload.evidenceSources);
147
+ }
148
+ return [];
149
+ });
150
+ }
86
151
  function derivePackageScope(pathValue) {
87
152
  const segments = pathValue.split("/").filter(Boolean);
88
153
  if (segments.length < 2) {
@@ -100,6 +165,45 @@ function getWorkflowInput(stateSlice, key) {
100
165
  }
101
166
  return stateSlice.workflowInputs[key];
102
167
  }
168
+ function buildLifecycleArtifactEnvelopeBase(state, displayName, summary, inputRefs, issueRefs = []) {
169
+ return {
170
+ schemaVersion: state.version,
171
+ workflow: {
172
+ name: state.workflow,
173
+ displayName
174
+ },
175
+ source: {
176
+ sourceType: "workflow-run",
177
+ runId: state.runId,
178
+ inputRefs: [...inputRefs],
179
+ issueRefs: [...issueRefs]
180
+ },
181
+ status: "complete",
182
+ generatedAt: new Date().toISOString(),
183
+ repo: {
184
+ root: state.repo.root,
185
+ name: state.repo.name,
186
+ branch: state.repo.branch
187
+ },
188
+ provenance: {
189
+ generatedBy: "agentforge-runtime",
190
+ schemaVersion: state.version,
191
+ executionEnvironment: state.context.ciExecution ? "ci" : "local",
192
+ repoRoot: state.repo.root
193
+ },
194
+ redaction: {
195
+ applied: true,
196
+ strategyVersion: "1.0.0",
197
+ categories: ["github-token", "api-key", "aws-key", "bearer-token", "password", "private-key"]
198
+ },
199
+ auditLink: {
200
+ entryIds: [],
201
+ findingIds: [],
202
+ proposedActionIds: []
203
+ },
204
+ summary
205
+ };
206
+ }
103
207
  function buildArtifactEnvelopeBase(state, summary, inputRefs, issueRefs) {
104
208
  return {
105
209
  schemaVersion: state.version,
@@ -593,6 +697,546 @@ const qaIntakeAgent = {
593
697
  });
594
698
  }
595
699
  };
700
+ const securityIntakeAgent = {
701
+ manifest: agentManifestSchema.parse({
702
+ version: 1,
703
+ name: "security-intake",
704
+ displayName: "Security Intake",
705
+ category: "security",
706
+ runtime: {
707
+ minVersion: "0.1.0",
708
+ kind: "deterministic"
709
+ },
710
+ permissions: {
711
+ model: false,
712
+ network: false,
713
+ tools: [],
714
+ readPaths: [".agentops/requests/**", ".agentops/runs/**"],
715
+ writePaths: []
716
+ },
717
+ inputs: ["workflowInputs", "repo"],
718
+ outputs: ["summary", "metadata"],
719
+ contextPolicy: {
720
+ sections: ["workflowInputs", "repo", "context"],
721
+ minimalContext: true
722
+ },
723
+ catalog: {
724
+ domain: "security",
725
+ supportLevel: "internal",
726
+ maturity: "mvp",
727
+ trustScope: "official-core-only"
728
+ },
729
+ trust: {
730
+ tier: "core",
731
+ source: "official",
732
+ reviewed: true
733
+ }
734
+ }),
735
+ outputSchema: agentOutputSchema,
736
+ async execute({ stateSlice }) {
737
+ const securityRequest = getWorkflowInput(stateSlice, "securityRequest");
738
+ const requestFile = getWorkflowInput(stateSlice, "requestFile");
739
+ const referencedArtifactKinds = getWorkflowInput(stateSlice, "securityTargetArtifactKinds") ?? [];
740
+ if (!securityRequest) {
741
+ throw new Error("security-review requires a validated security request before runtime execution.");
742
+ }
743
+ const targetType = securityRequest.targetRef.endsWith("bundle.json") ? "artifact-bundle" : "local-reference";
744
+ return agentOutputSchema.parse({
745
+ summary: `Loaded security request from ${requestFile ?? ".agentops/requests/security.yaml"} targeting ${securityRequest.targetRef}.`,
746
+ findings: [],
747
+ proposedActions: [],
748
+ lifecycleArtifacts: [],
749
+ requestedTools: [],
750
+ blockedActionFlags: [],
751
+ metadata: {
752
+ ...securityRequestSchema.parse({
753
+ ...securityRequest,
754
+ evidenceSources: [...new Set([securityRequest.targetRef, ...securityRequest.evidenceSources])]
755
+ }),
756
+ targetType,
757
+ referencedArtifactKinds
758
+ }
759
+ });
760
+ }
761
+ };
762
+ const securityEvidenceNormalizationAgent = {
763
+ manifest: agentManifestSchema.parse({
764
+ version: 1,
765
+ name: "security-evidence-normalizer",
766
+ displayName: "Security Evidence Normalizer",
767
+ category: "security",
768
+ runtime: {
769
+ minVersion: "0.1.0",
770
+ kind: "deterministic"
771
+ },
772
+ permissions: {
773
+ model: false,
774
+ network: false,
775
+ tools: [],
776
+ readPaths: [".agentops/requests/**", ".agentops/runs/**", "**/*.json", "**/*.md", "**/package.json"],
777
+ writePaths: []
778
+ },
779
+ inputs: ["workflowInputs", "repo", "agentResults"],
780
+ outputs: ["summary", "metadata"],
781
+ contextPolicy: {
782
+ sections: ["workflowInputs", "repo", "agentResults"],
783
+ minimalContext: true
784
+ },
785
+ catalog: {
786
+ domain: "security",
787
+ supportLevel: "internal",
788
+ maturity: "mvp",
789
+ trustScope: "official-core-only"
790
+ },
791
+ trust: {
792
+ tier: "core",
793
+ source: "official",
794
+ reviewed: true
795
+ }
796
+ }),
797
+ outputSchema: agentOutputSchema,
798
+ async execute({ stateSlice }) {
799
+ const securityRequest = getWorkflowInput(stateSlice, "securityRequest");
800
+ if (!securityRequest) {
801
+ throw new Error("security-review requires validated security request inputs before evidence normalization.");
802
+ }
803
+ const repoRoot = stateSlice.repo?.root;
804
+ const intakeMetadata = isRecord(stateSlice.agentResults?.intake?.metadata) ? stateSlice.agentResults.intake.metadata : {};
805
+ const targetType = typeof intakeMetadata.targetType === "string" && intakeMetadata.targetType === "artifact-bundle"
806
+ ? "artifact-bundle"
807
+ : "local-reference";
808
+ const targetPath = repoRoot ? join(repoRoot, securityRequest.targetRef) : securityRequest.targetRef;
809
+ if (repoRoot && !existsSync(targetPath)) {
810
+ throw new Error(`Security target reference not found: ${securityRequest.targetRef}`);
811
+ }
812
+ const referencedArtifactKinds = targetType === "artifact-bundle" ? loadBundleArtifactKinds(targetPath) : [];
813
+ const normalizedEvidenceSources = [...new Set([securityRequest.targetRef, ...securityRequest.evidenceSources])];
814
+ const missingEvidenceSources = normalizedEvidenceSources.filter((pathValue) => repoRoot && !existsSync(join(repoRoot, pathValue)));
815
+ if (missingEvidenceSources.length > 0) {
816
+ throw new Error(`Security evidence source not found: ${missingEvidenceSources[0]}`);
817
+ }
818
+ const normalizedFocusAreas = securityRequest.focusAreas.length > 0 ? [...new Set(securityRequest.focusAreas)] : ["general-review"];
819
+ const affectedPackages = targetType === "artifact-bundle"
820
+ ? [...new Set(loadBundleArtifactPayloadPaths(targetPath).map(derivePackageScope).filter((value) => Boolean(value)))]
821
+ : [];
822
+ const securitySignals = [
823
+ ...(referencedArtifactKinds.length > 0 ? [`Referenced artifact kinds: ${referencedArtifactKinds.join(", ")}`] : []),
824
+ ...(affectedPackages.length > 0 ? [`Affected packages inferred from bounded artifact payloads: ${affectedPackages.join(", ")}`] : []),
825
+ ...(normalizedFocusAreas.length > 0 ? [`Requested focus areas: ${normalizedFocusAreas.join(", ")}`] : []),
826
+ "Security evidence collection remains local, read-only, and bounded to validated references."
827
+ ];
828
+ const provenanceRefs = [
829
+ securityRequest.targetRef,
830
+ ...securityRequest.evidenceSources,
831
+ ...referencedArtifactKinds.map((artifactKind) => `${securityRequest.targetRef}#${artifactKind}`)
832
+ ];
833
+ const normalization = securityEvidenceNormalizationSchema.parse({
834
+ targetRef: securityRequest.targetRef,
835
+ targetType,
836
+ referencedArtifactKinds,
837
+ normalizedEvidenceSources,
838
+ missingEvidenceSources: [],
839
+ normalizedFocusAreas,
840
+ securitySignals,
841
+ provenanceRefs: [...new Set(provenanceRefs)],
842
+ affectedPackages
843
+ });
844
+ return agentOutputSchema.parse({
845
+ summary: `Normalized security evidence for ${securityRequest.targetRef}.`,
846
+ findings: [],
847
+ proposedActions: [],
848
+ lifecycleArtifacts: [],
849
+ requestedTools: [],
850
+ blockedActionFlags: [],
851
+ metadata: normalization
852
+ });
853
+ }
854
+ };
855
+ const securityAnalystAgent = {
856
+ manifest: agentManifestSchema.parse({
857
+ version: 1,
858
+ name: "security-analyst",
859
+ displayName: "Security Analyst",
860
+ category: "security",
861
+ runtime: {
862
+ minVersion: "0.1.0",
863
+ kind: "reasoning"
864
+ },
865
+ permissions: {
866
+ model: true,
867
+ network: false,
868
+ tools: [],
869
+ readPaths: ["**/*"],
870
+ writePaths: []
871
+ },
872
+ inputs: ["workflowInputs", "repo", "changes", "agentResults"],
873
+ outputs: ["lifecycleArtifacts"],
874
+ contextPolicy: {
875
+ sections: ["workflowInputs", "repo", "changes", "agentResults"],
876
+ minimalContext: true
877
+ },
878
+ catalog: {
879
+ domain: "security",
880
+ supportLevel: "internal",
881
+ maturity: "mvp",
882
+ trustScope: "official-core-only"
883
+ },
884
+ trust: {
885
+ tier: "core",
886
+ source: "official",
887
+ reviewed: true
888
+ }
889
+ }),
890
+ outputSchema: agentOutputSchema,
891
+ async execute({ state, stateSlice }) {
892
+ const securityRequest = getWorkflowInput(stateSlice, "securityRequest");
893
+ const requestFile = getWorkflowInput(stateSlice, "requestFile");
894
+ if (!securityRequest) {
895
+ throw new Error("security-review requires validated security inputs before security analysis.");
896
+ }
897
+ const intakeMetadata = isRecord(stateSlice.agentResults?.intake?.metadata) ? stateSlice.agentResults.intake.metadata : {};
898
+ const evidenceMetadata = securityEvidenceNormalizationSchema.safeParse(stateSlice.agentResults?.evidence?.metadata);
899
+ const normalizedEvidence = evidenceMetadata.success ? evidenceMetadata.data : undefined;
900
+ const referencedArtifactKinds = normalizedEvidence?.referencedArtifactKinds ?? asStringArray(intakeMetadata.referencedArtifactKinds);
901
+ const normalizedFocusAreas = normalizedEvidence?.normalizedFocusAreas ?? asStringArray(intakeMetadata.focusAreas);
902
+ const normalizedConstraints = asStringArray(intakeMetadata.constraints);
903
+ const evidenceSources = normalizedEvidence?.normalizedEvidenceSources && normalizedEvidence.normalizedEvidenceSources.length > 0
904
+ ? normalizedEvidence.normalizedEvidenceSources
905
+ : asStringArray(intakeMetadata.evidenceSources).length > 0
906
+ ? asStringArray(intakeMetadata.evidenceSources)
907
+ : [...new Set([securityRequest.targetRef, ...securityRequest.evidenceSources])];
908
+ const focusAreas = normalizedFocusAreas.length > 0 ? normalizedFocusAreas : securityRequest.focusAreas;
909
+ const inferredSeverity = securityRequest.releaseContext === "blocking" ? "high" : securityRequest.releaseContext === "candidate" ? "medium" : "low";
910
+ const findings = focusAreas.length > 0
911
+ ? focusAreas.map((focusArea, index) => ({
912
+ id: `security-finding-${index + 1}`,
913
+ title: `Inspect ${focusArea} evidence before promotion`,
914
+ summary: `Security review flagged ${focusArea} for bounded follow-up on ${securityRequest.targetRef}.`,
915
+ severity: inferredSeverity,
916
+ rationale: "The MVP security workflow synthesizes a structured report from validated references before deterministic evidence normalization lands.",
917
+ confidence: 0.76,
918
+ location: securityRequest.targetRef,
919
+ tags: ["security", focusArea]
920
+ }))
921
+ : [
922
+ {
923
+ id: "security-finding-1",
924
+ title: "Inspect referenced security evidence before promotion",
925
+ summary: `Security review requires bounded interpretation of the referenced evidence for ${securityRequest.targetRef}.`,
926
+ severity: inferredSeverity,
927
+ rationale: "The current security workflow is read-only and request-driven, so findings remain tied to validated local references rather than automatic scanning.",
928
+ confidence: 0.72,
929
+ location: securityRequest.targetRef,
930
+ tags: ["security", "evidence"]
931
+ }
932
+ ];
933
+ const mitigations = [
934
+ ...focusAreas.map((focusArea) => `Review ${focusArea} evidence and document the release impact before promotion.`),
935
+ ...(normalizedConstraints.length > 0 ? [`Keep security follow-up bounded by: ${normalizedConstraints.join("; ")}.`] : [])
936
+ ];
937
+ const followUpWork = [
938
+ ...(normalizedEvidence?.securitySignals ?? []),
939
+ ...(referencedArtifactKinds.length > 0
940
+ ? [`Confirm the security posture for referenced artifacts: ${referencedArtifactKinds.join(", ")}.`]
941
+ : []),
942
+ "Use deterministic security evidence normalization outputs before broadening the workflow surface."
943
+ ];
944
+ const summary = `Security report prepared for ${securityRequest.targetRef}.`;
945
+ const securityReport = securityArtifactSchema.parse({
946
+ ...buildLifecycleArtifactEnvelopeBase(state, "Security Review", summary, [
947
+ requestFile ?? ".agentops/requests/security.yaml",
948
+ ...(normalizedEvidence?.provenanceRefs ?? [securityRequest.targetRef, ...securityRequest.evidenceSources])
949
+ ]),
950
+ artifactKind: "security-report",
951
+ lifecycleDomain: "security",
952
+ redaction: {
953
+ applied: true,
954
+ strategyVersion: "1.0.0",
955
+ categories: ["github-token", "api-key", "aws-key", "bearer-token", "password", "private-key", "security-sensitive"]
956
+ },
957
+ payload: {
958
+ targetRef: securityRequest.targetRef,
959
+ evidenceSources,
960
+ findings,
961
+ severitySummary: `highest severity: ${inferredSeverity}; ${findings.length} synthesized security finding(s).`,
962
+ mitigations: mitigations.length > 0
963
+ ? mitigations
964
+ : ["Review the referenced security evidence before promoting this workflow output."],
965
+ releaseImpact: securityRequest.releaseContext === "blocking"
966
+ ? "release-blocking security findings require resolution before promotion."
967
+ : securityRequest.releaseContext === "candidate"
968
+ ? "candidate release requires explicit security review before promotion."
969
+ : "no release context was supplied; security output remains advisory.",
970
+ followUpWork
971
+ }
972
+ });
973
+ return agentOutputSchema.parse({
974
+ summary,
975
+ findings: [],
976
+ proposedActions: [],
977
+ lifecycleArtifacts: [securityReport],
978
+ requestedTools: [],
979
+ blockedActionFlags: [],
980
+ confidence: 0.76,
981
+ metadata: {
982
+ deterministicInputs: {
983
+ targetRef: securityRequest.targetRef,
984
+ evidenceSources,
985
+ focusAreas,
986
+ constraints: normalizedConstraints,
987
+ referencedArtifactKinds,
988
+ normalizedEvidence: normalizedEvidence ?? null
989
+ },
990
+ synthesizedAssessment: {
991
+ severitySummary: securityReport.payload.severitySummary,
992
+ mitigations: securityReport.payload.mitigations,
993
+ followUpWork: securityReport.payload.followUpWork
994
+ }
995
+ }
996
+ });
997
+ }
998
+ };
999
+ const qaEvidenceNormalizationAgent = {
1000
+ manifest: agentManifestSchema.parse({
1001
+ version: 1,
1002
+ name: "qa-evidence-normalizer",
1003
+ displayName: "QA Evidence Normalizer",
1004
+ category: "qa",
1005
+ runtime: {
1006
+ minVersion: "0.1.0",
1007
+ kind: "deterministic"
1008
+ },
1009
+ permissions: {
1010
+ model: false,
1011
+ network: false,
1012
+ tools: [],
1013
+ readPaths: [".agentops/requests/**", ".agentops/runs/**", "**/package.json", "**/*.json", "**/*.xml", "**/*.log", "**/*.md"],
1014
+ writePaths: []
1015
+ },
1016
+ inputs: ["workflowInputs", "repo", "agentResults"],
1017
+ outputs: ["summary", "metadata"],
1018
+ contextPolicy: {
1019
+ sections: ["workflowInputs", "repo", "agentResults"],
1020
+ minimalContext: true
1021
+ },
1022
+ catalog: {
1023
+ domain: "test",
1024
+ supportLevel: "internal",
1025
+ maturity: "mvp",
1026
+ trustScope: "official-core-only"
1027
+ },
1028
+ trust: {
1029
+ tier: "core",
1030
+ source: "official",
1031
+ reviewed: true
1032
+ }
1033
+ }),
1034
+ outputSchema: agentOutputSchema,
1035
+ async execute({ stateSlice }) {
1036
+ const qaRequest = getWorkflowInput(stateSlice, "qaRequest");
1037
+ if (!qaRequest) {
1038
+ throw new Error("qa-review requires validated QA request inputs before evidence normalization.");
1039
+ }
1040
+ const repoRoot = stateSlice.repo?.root;
1041
+ const packageManager = stateSlice.repo?.packageManager || "pnpm";
1042
+ const intakeMetadata = isRecord(stateSlice.agentResults?.intake?.metadata) ? stateSlice.agentResults.intake.metadata : {};
1043
+ const targetType = typeof intakeMetadata.targetType === "string" ? intakeMetadata.targetType : "local-reference";
1044
+ const targetPath = repoRoot ? join(repoRoot, qaRequest.targetRef) : qaRequest.targetRef;
1045
+ if (repoRoot && !existsSync(targetPath)) {
1046
+ throw new Error(`QA target reference not found: ${qaRequest.targetRef}`);
1047
+ }
1048
+ const referencedArtifactKinds = targetType === "artifact-bundle" ? loadBundleArtifactKinds(targetPath) : [];
1049
+ const normalizedEvidenceSources = [...new Set([qaRequest.targetRef, ...qaRequest.evidenceSources])];
1050
+ const missingEvidenceSources = normalizedEvidenceSources.filter((pathValue) => repoRoot && !existsSync(join(repoRoot, pathValue)));
1051
+ if (missingEvidenceSources.length > 0) {
1052
+ throw new Error(`QA evidence source not found: ${missingEvidenceSources[0]}`);
1053
+ }
1054
+ const bundleAffectedPaths = targetType === "artifact-bundle" && referencedArtifactKinds.includes("implementation-proposal") && existsSync(targetPath)
1055
+ ? asStringArray((() => {
1056
+ const parsed = JSON.parse(readFileSync(targetPath, "utf8"));
1057
+ if (!isRecord(parsed) || !Array.isArray(parsed.lifecycleArtifacts)) {
1058
+ return [];
1059
+ }
1060
+ const implementationArtifact = parsed.lifecycleArtifacts.find((artifact) => isRecord(artifact) &&
1061
+ artifact.artifactKind === "implementation-proposal" &&
1062
+ isRecord(artifact.payload) &&
1063
+ Array.isArray(artifact.payload.affectedPaths));
1064
+ return implementationArtifact?.payload?.affectedPaths ?? [];
1065
+ })())
1066
+ : [];
1067
+ const affectedPackages = [...new Set(bundleAffectedPaths.map((pathValue) => derivePackageScope(pathValue)).filter((value) => Boolean(value)))];
1068
+ const allowedValidationCommands = collectValidationCommands(repoRoot, packageManager, affectedPackages).filter((entry) => entry.classification === "approval_required");
1069
+ const allowlistedCommands = new Set(allowedValidationCommands.map((entry) => entry.command));
1070
+ const normalizedExecutedChecks = qaRequest.executedChecks.map(normalizeRequestedCommand);
1071
+ const unrecognizedExecutedChecks = normalizedExecutedChecks.filter((command) => !allowlistedCommands.has(command));
1072
+ const normalization = qaEvidenceNormalizationSchema.parse({
1073
+ targetRef: qaRequest.targetRef,
1074
+ targetType,
1075
+ referencedArtifactKinds,
1076
+ normalizedEvidenceSources,
1077
+ missingEvidenceSources: [],
1078
+ normalizedExecutedChecks,
1079
+ unrecognizedExecutedChecks,
1080
+ affectedPackages,
1081
+ allowedValidationCommands
1082
+ });
1083
+ return agentOutputSchema.parse({
1084
+ summary: `Normalized QA evidence across ${normalization.normalizedEvidenceSources.length} source(s) and ${normalization.allowedValidationCommands.length} allowlisted validation command(s).`,
1085
+ findings: [],
1086
+ proposedActions: [],
1087
+ lifecycleArtifacts: [],
1088
+ requestedTools: [],
1089
+ blockedActionFlags: [],
1090
+ metadata: normalization
1091
+ });
1092
+ }
1093
+ };
1094
+ const qaAnalystAgent = {
1095
+ manifest: agentManifestSchema.parse({
1096
+ version: 1,
1097
+ name: "qa-analyst",
1098
+ displayName: "QA Analyst",
1099
+ category: "qa",
1100
+ runtime: {
1101
+ minVersion: "0.1.0",
1102
+ kind: "reasoning"
1103
+ },
1104
+ permissions: {
1105
+ model: true,
1106
+ network: false,
1107
+ tools: [],
1108
+ readPaths: ["**/*"],
1109
+ writePaths: []
1110
+ },
1111
+ inputs: ["workflowInputs", "repo", "changes", "agentResults"],
1112
+ outputs: ["lifecycleArtifacts"],
1113
+ contextPolicy: {
1114
+ sections: ["workflowInputs", "repo", "changes", "agentResults"],
1115
+ minimalContext: true
1116
+ },
1117
+ catalog: {
1118
+ domain: "test",
1119
+ supportLevel: "internal",
1120
+ maturity: "mvp",
1121
+ trustScope: "official-core-only"
1122
+ },
1123
+ trust: {
1124
+ tier: "core",
1125
+ source: "official",
1126
+ reviewed: true
1127
+ }
1128
+ }),
1129
+ outputSchema: agentOutputSchema,
1130
+ async execute({ state, stateSlice }) {
1131
+ const qaRequest = getWorkflowInput(stateSlice, "qaRequest");
1132
+ const requestFile = getWorkflowInput(stateSlice, "requestFile");
1133
+ if (!qaRequest) {
1134
+ throw new Error("qa-review requires validated QA inputs before QA analysis.");
1135
+ }
1136
+ const intakeMetadata = isRecord(stateSlice.agentResults?.intake?.metadata) ? stateSlice.agentResults.intake.metadata : {};
1137
+ const evidenceMetadata = isRecord(stateSlice.agentResults?.evidence?.metadata) ? stateSlice.agentResults.evidence.metadata : {};
1138
+ const normalizedEvidenceSources = asStringArray(evidenceMetadata.normalizedEvidenceSources);
1139
+ const normalizedExecutedChecks = asStringArray(evidenceMetadata.normalizedExecutedChecks);
1140
+ const normalizedFocusAreas = asStringArray(intakeMetadata.focusAreas);
1141
+ const normalizedConstraints = asStringArray(intakeMetadata.constraints);
1142
+ const missingEvidenceSources = asStringArray(evidenceMetadata.missingEvidenceSources);
1143
+ const unrecognizedExecutedChecks = asStringArray(evidenceMetadata.unrecognizedExecutedChecks);
1144
+ const targetType = typeof evidenceMetadata.targetType === "string"
1145
+ ? evidenceMetadata.targetType
1146
+ : typeof intakeMetadata.targetType === "string"
1147
+ ? intakeMetadata.targetType
1148
+ : "local-reference";
1149
+ const evidenceSources = normalizedEvidenceSources.length > 0
1150
+ ? normalizedEvidenceSources
1151
+ : [...new Set([qaRequest.targetRef, ...qaRequest.evidenceSources])];
1152
+ const executedChecks = normalizedExecutedChecks.length > 0 ? normalizedExecutedChecks : qaRequest.executedChecks;
1153
+ const focusAreas = normalizedFocusAreas.length > 0 ? normalizedFocusAreas : qaRequest.focusAreas;
1154
+ const findings = focusAreas.length > 0
1155
+ ? focusAreas.map((focusArea, index) => ({
1156
+ id: `qa-finding-${index + 1}`,
1157
+ title: `Inspect ${focusArea} evidence before promotion`,
1158
+ summary: `QA review flagged ${focusArea} as an area requiring bounded interpretation for ${qaRequest.targetRef}.`,
1159
+ severity: qaRequest.releaseContext === "blocking" ? "high" : qaRequest.releaseContext === "candidate" ? "medium" : "low",
1160
+ rationale: `The MVP QA workflow remains read-only and request-driven, so ${focusArea} still depends on referenced evidence rather than automatic execution.`,
1161
+ confidence: 0.74,
1162
+ location: qaRequest.targetRef,
1163
+ tags: ["qa", focusArea]
1164
+ }))
1165
+ : [
1166
+ {
1167
+ id: "qa-finding-1",
1168
+ title: "Review referenced QA evidence before promotion",
1169
+ summary: `QA review requires bounded interpretation of the referenced evidence for ${qaRequest.targetRef}.`,
1170
+ severity: qaRequest.releaseContext === "blocking" ? "high" : "medium",
1171
+ rationale: "The current QA workflow synthesizes a report from validated references and does not execute arbitrary test commands.",
1172
+ confidence: 0.71,
1173
+ location: qaRequest.targetRef,
1174
+ tags: ["qa", "evidence"]
1175
+ }
1176
+ ];
1177
+ const coverageGaps = [
1178
+ ...(evidenceSources.length === 0 ? ["No QA evidence sources were provided beyond the target reference."] : []),
1179
+ ...missingEvidenceSources.map((pathValue) => `Referenced QA evidence is missing: ${pathValue}`),
1180
+ ...(focusAreas.includes("coverage") ? ["Coverage evidence still needs deterministic normalization before it can be promoted to an official QA signal."] : []),
1181
+ ...(executedChecks.length === 0 ? ["No executed validation checks were recorded in the request."] : []),
1182
+ ...unrecognizedExecutedChecks.map((command) => `Executed check is outside the bounded allowlist and still needs manual interpretation: ${command}`)
1183
+ ];
1184
+ const recommendedNextChecks = [
1185
+ ...executedChecks.map((command) => `Review the recorded output for \`${command}\` before promotion.`),
1186
+ ...focusAreas.map((focusArea) => `Confirm whether ${focusArea} needs additional deterministic QA evidence.`),
1187
+ ...(normalizedConstraints.length > 0 ? [`Keep QA follow-up bounded by: ${normalizedConstraints.join("; ")}.`] : [])
1188
+ ];
1189
+ const summary = `QA report prepared for ${qaRequest.targetRef}.`;
1190
+ const qaReport = qaArtifactSchema.parse({
1191
+ ...buildArtifactEnvelopeBase(state, summary, [requestFile ?? ".agentops/requests/qa.yaml", qaRequest.targetRef, ...qaRequest.evidenceSources], []),
1192
+ artifactKind: "qa-report",
1193
+ lifecycleDomain: "test",
1194
+ workflow: {
1195
+ name: state.workflow,
1196
+ displayName: "QA Review"
1197
+ },
1198
+ payload: {
1199
+ targetRef: qaRequest.targetRef,
1200
+ evidenceSources,
1201
+ executedChecks,
1202
+ findings,
1203
+ coverageGaps,
1204
+ recommendedNextChecks: recommendedNextChecks.length > 0
1205
+ ? recommendedNextChecks
1206
+ : ["Capture additional bounded QA evidence before promotion."],
1207
+ releaseImpact: qaRequest.releaseContext === "blocking"
1208
+ ? "release-blocking QA findings require resolution before promotion."
1209
+ : qaRequest.releaseContext === "candidate"
1210
+ ? "candidate release still requires explicit QA review before promotion."
1211
+ : "no release context was supplied; QA output remains advisory."
1212
+ }
1213
+ });
1214
+ return agentOutputSchema.parse({
1215
+ summary,
1216
+ findings: [],
1217
+ proposedActions: [],
1218
+ lifecycleArtifacts: [qaReport],
1219
+ requestedTools: [],
1220
+ blockedActionFlags: [],
1221
+ confidence: 0.74,
1222
+ metadata: {
1223
+ deterministicInputs: {
1224
+ targetRef: qaRequest.targetRef,
1225
+ targetType,
1226
+ evidenceSources,
1227
+ executedChecks,
1228
+ focusAreas,
1229
+ constraints: normalizedConstraints
1230
+ },
1231
+ synthesizedAssessment: {
1232
+ releaseContext: qaRequest.releaseContext,
1233
+ recommendedNextChecks: qaReport.payload.recommendedNextChecks,
1234
+ coverageGaps: qaReport.payload.coverageGaps
1235
+ }
1236
+ }
1237
+ });
1238
+ }
1239
+ };
596
1240
  const implementationInventoryAgent = {
597
1241
  manifest: agentManifestSchema.parse({
598
1242
  version: 1,
@@ -662,33 +1306,7 @@ const implementationInventoryAgent = {
662
1306
  const policySurfaces = [
663
1307
  ...new Set(resolvedAffectedPaths.filter((pathValue) => pathValue.includes("policy") || pathValue.includes(".agentops/policy.yaml")))
664
1308
  ];
665
- const discoveredValidationCommands = [];
666
- const registerScripts = (packageJsonPath, source, packageName) => {
667
- const scripts = parsePackageScripts(packageJsonPath);
668
- for (const scriptName of Object.keys(scripts)) {
669
- const command = buildValidationCommand(packageManager, scriptName, packageName);
670
- discoveredValidationCommands.push({
671
- command,
672
- source,
673
- classification: allowedValidationScriptNames.has(scriptName) ? "approval_required" : "deny",
674
- reason: allowedValidationScriptNames.has(scriptName)
675
- ? "Discovered from a bounded repository script; execution would still require approval."
676
- : "Command is not in the bounded allowlist for implementation validation."
677
- });
678
- }
679
- };
680
- if (repoRoot) {
681
- registerScripts(join(repoRoot, "package.json"), "package-script");
682
- for (const packageScope of affectedPackages) {
683
- const packageJsonPath = join(repoRoot, packageScope, "package.json");
684
- if (!existsSync(packageJsonPath)) {
685
- continue;
686
- }
687
- const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
688
- const packageName = isRecord(parsed) && typeof parsed.name === "string" ? parsed.name : packageScope;
689
- registerScripts(packageJsonPath, "workspace-script", packageName);
690
- }
691
- }
1309
+ const discoveredValidationCommands = collectValidationCommands(repoRoot, packageManager, affectedPackages);
692
1310
  const normalizedRequestedCommands = implementationRequest.validationCommands.map(normalizeRequestedCommand);
693
1311
  const allowlistedCommands = new Set(discoveredValidationCommands
694
1312
  .filter((entry) => entry.classification === "approval_required")
@@ -1218,6 +1836,11 @@ export function createBuiltinAgentRegistry() {
1218
1836
  ["design-inventory", designInventoryAgent],
1219
1837
  ["implementation-intake", implementationIntakeAgent],
1220
1838
  ["qa-intake", qaIntakeAgent],
1839
+ ["security-intake", securityIntakeAgent],
1840
+ ["security-evidence-normalizer", securityEvidenceNormalizationAgent],
1841
+ ["security-analyst", securityAnalystAgent],
1842
+ ["qa-evidence-normalizer", qaEvidenceNormalizationAgent],
1843
+ ["qa-analyst", qaAnalystAgent],
1221
1844
  ["implementation-inventory", implementationInventoryAgent],
1222
1845
  ["implementation-planner", implementationPlannerAgent],
1223
1846
  ["design-analyst", designAnalystAgent],