@h9-foundry/agentforge-cli 0.9.0 → 0.11.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
@@ -6,7 +6,7 @@ import { buildAuditBundle, createAuditEntry, renderAuditBundleMarkdown } from "@
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, benchmarkArtifactSchema, designArtifactSchema, designRequestSchema, evalArtifactSchema, evalFixtureCorpusSchema, implementationRequestSchema, incidentRequestSchema, maintenanceRequestSchema, planningArtifactSchema, planningRequestSchema, qaRequestSchema, releaseRequestSchema, schemaFixtures, securityRequestSchema, workflowDefinitionSchema } from "@h9-foundry/agentforge-schemas";
9
+ import { agentforgeConfigSchema, auditBundleSchema, benchmarkArtifactSchema, designArtifactSchema, designRequestSchema, deploymentRequestSchema, evalArtifactSchema, evalFixtureCorpusSchema, implementationRequestSchema, incidentRequestSchema, maintenanceRequestSchema, pipelineRequestSchema, planningArtifactSchema, planningRequestSchema, promotionRequestSchema, 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";
@@ -239,7 +239,7 @@ description: Validate a bounded release-readiness request while keeping trusted
239
239
  trigger: manual
240
240
  catalog:
241
241
  domain: release
242
- supportLevel: partial
242
+ supportLevel: official
243
243
  maturity: mvp
244
244
  trustScope: official-core-only
245
245
  nodes:
@@ -259,6 +259,84 @@ nodes:
259
259
  kind: report
260
260
  outputs_to: reports.final
261
261
  `;
262
+ const pipelineWorkflowTemplate = `version: 1
263
+ name: pipeline-evidence-review
264
+ description: Review bounded local pipeline evidence through the shared CI model without assuming a release target.
265
+ trigger: manual
266
+ catalog:
267
+ domain: release
268
+ supportLevel: official
269
+ maturity: mvp
270
+ trustScope: official-core-only
271
+ nodes:
272
+ - id: intake
273
+ kind: deterministic
274
+ agent: pipeline-intake
275
+ outputs_to: agentResults.intake
276
+ - id: evidence
277
+ kind: deterministic
278
+ agent: pipeline-evidence-normalizer
279
+ outputs_to: agentResults.evidence
280
+ - id: pipeline
281
+ kind: reasoning
282
+ agent: pipeline-analyst
283
+ outputs_to: agentResults.pipeline
284
+ - id: report
285
+ kind: report
286
+ outputs_to: reports.final
287
+ `;
288
+ const deploymentGateWorkflowTemplate = `version: 1
289
+ name: deployment-gate-review
290
+ description: Review a bounded deployment candidate using shared CI evidence and referenced lifecycle artifacts.
291
+ trigger: manual
292
+ catalog:
293
+ domain: release
294
+ supportLevel: official
295
+ maturity: mvp
296
+ trustScope: official-core-only
297
+ nodes:
298
+ - id: intake
299
+ kind: deterministic
300
+ agent: deployment-gate-intake
301
+ outputs_to: agentResults.intake
302
+ - id: evidence
303
+ kind: deterministic
304
+ agent: deployment-gate-evidence-normalizer
305
+ outputs_to: agentResults.evidence
306
+ - id: deployment
307
+ kind: reasoning
308
+ agent: deployment-gate-analyst
309
+ outputs_to: agentResults.deployment
310
+ - id: report
311
+ kind: report
312
+ outputs_to: reports.final
313
+ `;
314
+ const promotionApprovalWorkflowTemplate = `version: 1
315
+ name: promotion-approval
316
+ description: Review promotion approval readiness using bounded CI evidence plus ready release and deployment gate artifacts.
317
+ trigger: manual
318
+ catalog:
319
+ domain: release
320
+ supportLevel: official
321
+ maturity: mvp
322
+ trustScope: official-core-only
323
+ nodes:
324
+ - id: intake
325
+ kind: deterministic
326
+ agent: promotion-approval-intake
327
+ outputs_to: agentResults.intake
328
+ - id: evidence
329
+ kind: deterministic
330
+ agent: promotion-approval-evidence-normalizer
331
+ outputs_to: agentResults.evidence
332
+ - id: promotion
333
+ kind: reasoning
334
+ agent: promotion-approval-analyst
335
+ outputs_to: agentResults.promotion
336
+ - id: report
337
+ kind: report
338
+ outputs_to: reports.final
339
+ `;
262
340
  const incidentWorkflowTemplate = `version: 1
263
341
  name: incident-handoff
264
342
  description: Validate staged incident evidence while keeping the default path local, read-only, and explicit.
@@ -328,24 +406,90 @@ function runGit(root, args) {
328
406
  return "";
329
407
  }
330
408
  }
331
- function parseGitHubRepositoryUrl(value) {
409
+ function inferScmPlatform(host) {
410
+ const normalizedHost = host.toLowerCase();
411
+ if (normalizedHost.includes("github")) {
412
+ return "github";
413
+ }
414
+ if (normalizedHost.includes("gitlab")) {
415
+ return "gitlab";
416
+ }
417
+ return "generic";
418
+ }
419
+ function parseScmRepositoryUrl(value) {
332
420
  const trimmed = value.trim();
333
- const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i);
421
+ const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\.git)?$/i);
334
422
  if (sshMatch) {
423
+ const host = sshMatch[1].toLowerCase();
424
+ const pathSegments = sshMatch[2].split("/").filter(Boolean);
425
+ if (pathSegments.length < 2) {
426
+ return undefined;
427
+ }
428
+ const repo = pathSegments[pathSegments.length - 1] ?? "";
429
+ const namespace = pathSegments.slice(0, -1).join("/");
430
+ const platform = inferScmPlatform(host);
431
+ if (platform === "github") {
432
+ return {
433
+ platform,
434
+ host,
435
+ owner: namespace,
436
+ repo
437
+ };
438
+ }
335
439
  return {
336
- host: sshMatch[1].toLowerCase(),
337
- owner: sshMatch[2],
338
- repo: sshMatch[3]
440
+ platform,
441
+ host,
442
+ namespace,
443
+ repo
339
444
  };
340
445
  }
341
- const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/)?$/i);
446
+ const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?(?:\/)?$/i);
342
447
  if (!httpsMatch) {
343
448
  return undefined;
344
449
  }
450
+ const host = httpsMatch[1].toLowerCase();
451
+ const pathSegments = httpsMatch[2].split("/").filter(Boolean);
452
+ if (pathSegments.length < 2) {
453
+ return undefined;
454
+ }
455
+ const repo = pathSegments[pathSegments.length - 1] ?? "";
456
+ const namespace = pathSegments.slice(0, -1).join("/");
457
+ const platform = inferScmPlatform(host);
458
+ if (platform === "github") {
459
+ return {
460
+ platform,
461
+ host,
462
+ owner: namespace,
463
+ repo
464
+ };
465
+ }
466
+ return {
467
+ platform,
468
+ host,
469
+ namespace,
470
+ repo
471
+ };
472
+ }
473
+ function parseGitHubRepositoryUrl(value) {
474
+ const parsed = parseScmRepositoryUrl(value);
475
+ if (!parsed || parsed.platform !== "github") {
476
+ return undefined;
477
+ }
478
+ return {
479
+ host: parsed.host,
480
+ owner: parsed.owner,
481
+ repo: parsed.repo
482
+ };
483
+ }
484
+ function parseGitLabRepositoryUrl(value) {
485
+ const parsed = parseScmRepositoryUrl(value);
486
+ if (!parsed || parsed.platform !== "gitlab") {
487
+ return undefined;
488
+ }
345
489
  return {
346
- host: httpsMatch[1].toLowerCase(),
347
- owner: httpsMatch[2],
348
- repo: httpsMatch[3]
490
+ host: parsed.host,
491
+ namespace: parsed.namespace,
492
+ repo: parsed.repo
349
493
  };
350
494
  }
351
495
  function inferGitHubRepoContext(root) {
@@ -371,6 +515,60 @@ function inferGitHubRepoContext(root) {
371
515
  const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
372
516
  return remoteUrl ? parseGitHubRepositoryUrl(remoteUrl) : undefined;
373
517
  }
518
+ function inferGitLabRepoContext(root) {
519
+ const packageJsonPath = join(root, "package.json");
520
+ if (existsSync(packageJsonPath)) {
521
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
522
+ if (isRecord(parsed)) {
523
+ const repository = parsed.repository;
524
+ if (typeof repository === "string") {
525
+ const context = parseGitLabRepositoryUrl(repository);
526
+ if (context) {
527
+ return context;
528
+ }
529
+ }
530
+ if (isRecord(repository) && typeof repository.url === "string") {
531
+ const context = parseGitLabRepositoryUrl(repository.url);
532
+ if (context) {
533
+ return context;
534
+ }
535
+ }
536
+ }
537
+ }
538
+ const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
539
+ return remoteUrl ? parseGitLabRepositoryUrl(remoteUrl) : undefined;
540
+ }
541
+ function inferScmRepoContext(root) {
542
+ const gitHubContext = inferGitHubRepoContext(root);
543
+ if (gitHubContext) {
544
+ return { platform: "github", ...gitHubContext };
545
+ }
546
+ const gitLabContext = inferGitLabRepoContext(root);
547
+ if (gitLabContext) {
548
+ return { platform: "gitlab", ...gitLabContext };
549
+ }
550
+ const packageJsonPath = join(root, "package.json");
551
+ if (existsSync(packageJsonPath)) {
552
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
553
+ if (isRecord(parsed)) {
554
+ const repository = parsed.repository;
555
+ if (typeof repository === "string") {
556
+ const context = parseScmRepositoryUrl(repository);
557
+ if (context) {
558
+ return context;
559
+ }
560
+ }
561
+ if (isRecord(repository) && typeof repository.url === "string") {
562
+ const context = parseScmRepositoryUrl(repository.url);
563
+ if (context) {
564
+ return context;
565
+ }
566
+ }
567
+ }
568
+ }
569
+ const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
570
+ return remoteUrl ? parseScmRepositoryUrl(remoteUrl) : undefined;
571
+ }
374
572
  function normalizeGitHubReference(rawValue, repoContext) {
375
573
  const raw = rawValue.trim();
376
574
  if (!raw) {
@@ -426,6 +624,86 @@ function normalizeGitHubReferences(rawValues, repoContext) {
426
624
  }
427
625
  return normalized;
428
626
  }
627
+ function normalizeGitLabReference(rawValue, repoContext) {
628
+ const raw = rawValue.trim();
629
+ if (!raw) {
630
+ return undefined;
631
+ }
632
+ const fromParts = (context, kind, number) => ({
633
+ platform: "gitlab",
634
+ host: context.host,
635
+ namespace: context.namespace,
636
+ repo: context.repo,
637
+ kind,
638
+ identifier: `${number}`,
639
+ number,
640
+ canonical: kind === "issue"
641
+ ? `${context.host}/${context.namespace}/${context.repo}#${number}`
642
+ : `${context.host}/${context.namespace}/${context.repo}!${number}`,
643
+ url: kind === "issue"
644
+ ? `https://${context.host}/${context.namespace}/${context.repo}/-/issues/${number}`
645
+ : `https://${context.host}/${context.namespace}/${context.repo}/-/merge_requests/${number}`,
646
+ source: raw
647
+ });
648
+ const urlMatch = raw.match(/^https?:\/\/([^/]+)\/(.+?)\/-\/(issues|merge_requests)\/(\d+)(?:\/)?$/i);
649
+ if (urlMatch) {
650
+ const pathSegments = urlMatch[2].split("/").filter(Boolean);
651
+ if (pathSegments.length < 2) {
652
+ return undefined;
653
+ }
654
+ const repo = pathSegments[pathSegments.length - 1] ?? "";
655
+ const namespace = pathSegments.slice(0, -1).join("/");
656
+ return fromParts({ host: urlMatch[1].toLowerCase(), namespace, repo }, urlMatch[3].toLowerCase() === "merge_requests" ? "merge_request" : "issue", Number.parseInt(urlMatch[4], 10));
657
+ }
658
+ const shortIssueMatch = raw.match(/^#(\d+)$/);
659
+ if (shortIssueMatch && repoContext) {
660
+ return fromParts(repoContext, "issue", Number.parseInt(shortIssueMatch[1], 10));
661
+ }
662
+ const shortMergeRequestMatch = raw.match(/^!(\d+)$/);
663
+ if (shortMergeRequestMatch && repoContext) {
664
+ return fromParts(repoContext, "merge_request", Number.parseInt(shortMergeRequestMatch[1], 10));
665
+ }
666
+ return undefined;
667
+ }
668
+ function normalizeScmReference(rawValue, repoContext) {
669
+ const raw = rawValue.trim();
670
+ if (!raw) {
671
+ return undefined;
672
+ }
673
+ const gitHubRef = normalizeGitHubReference(raw, repoContext?.platform === "github"
674
+ ? { host: repoContext.host, owner: repoContext.owner, repo: repoContext.repo }
675
+ : undefined);
676
+ if (gitHubRef) {
677
+ return {
678
+ platform: "github",
679
+ host: gitHubRef.host,
680
+ namespace: gitHubRef.owner,
681
+ repo: gitHubRef.repo,
682
+ kind: gitHubRef.kind,
683
+ identifier: `${gitHubRef.number}`,
684
+ number: gitHubRef.number,
685
+ canonical: `${gitHubRef.host}/${gitHubRef.owner}/${gitHubRef.repo}${gitHubRef.kind === "issue" ? `#${gitHubRef.number}` : `/pull/${gitHubRef.number}`}`,
686
+ url: gitHubRef.url,
687
+ source: gitHubRef.source
688
+ };
689
+ }
690
+ return normalizeGitLabReference(raw, repoContext?.platform === "gitlab"
691
+ ? { host: repoContext.host, namespace: repoContext.namespace, repo: repoContext.repo }
692
+ : undefined);
693
+ }
694
+ function normalizeScmReferences(rawValues, repoContext) {
695
+ const seen = new Set();
696
+ const normalized = [];
697
+ for (const rawValue of rawValues) {
698
+ const scmRef = normalizeScmReference(rawValue, repoContext);
699
+ if (!scmRef || seen.has(scmRef.canonical)) {
700
+ continue;
701
+ }
702
+ seen.add(scmRef.canonical);
703
+ normalized.push(scmRef);
704
+ }
705
+ return normalized;
706
+ }
429
707
  export function mapWorkflowRunStatusToGitHubStatus(workflow, localRunStatus) {
430
708
  if (localRunStatus === "success") {
431
709
  return {
@@ -544,6 +822,49 @@ function validateReleaseRequestCompleteness(request) {
544
822
  }
545
823
  return request;
546
824
  }
825
+ function validatePipelineRequestCompleteness(request) {
826
+ const evidenceSignalCount = request.evidenceSources.length + request.qaReportRefs.length + request.securityReportRefs.length + request.releaseReportRefs.length;
827
+ if (evidenceSignalCount === 0) {
828
+ throw new Error("Pipeline request is underspecified. Add at least one of evidenceSources, qaReportRefs, securityReportRefs, or releaseReportRefs.");
829
+ }
830
+ return request;
831
+ }
832
+ function validateDeploymentRequestCompleteness(request) {
833
+ const evidenceSignalCount = request.evidenceSources.length +
834
+ request.qaReportRefs.length +
835
+ request.securityReportRefs.length +
836
+ request.releaseReportRefs.length +
837
+ request.pipelineReportRefs.length;
838
+ if (evidenceSignalCount === 0) {
839
+ throw new Error("Deployment request is underspecified. Add at least one of evidenceSources, qaReportRefs, securityReportRefs, releaseReportRefs, or pipelineReportRefs.");
840
+ }
841
+ return request;
842
+ }
843
+ function validatePromotionRequestCompleteness(request) {
844
+ const evidenceSignalCount = request.evidenceSources.length +
845
+ request.qaReportRefs.length +
846
+ request.securityReportRefs.length +
847
+ request.releaseReportRefs.length +
848
+ request.deploymentGateReportRefs.length;
849
+ if (evidenceSignalCount === 0) {
850
+ throw new Error("Promotion request is underspecified. Add at least one of evidenceSources, qaReportRefs, securityReportRefs, releaseReportRefs, or deploymentGateReportRefs.");
851
+ }
852
+ if (request.releaseReportRefs.length === 0 || request.deploymentGateReportRefs.length === 0) {
853
+ throw new Error("Promotion request is underspecified. Add at least one releaseReportRef and one deploymentGateReportRef.");
854
+ }
855
+ return request;
856
+ }
857
+ function addSourceReferences(refs, incoming) {
858
+ for (const issueRef of incoming.issueRefs) {
859
+ refs.issueRefs.add(issueRef);
860
+ }
861
+ for (const scmRef of incoming.scmRefs) {
862
+ refs.scmRefMap.set(scmRef.canonical, scmRef);
863
+ }
864
+ for (const githubRef of incoming.githubRefs) {
865
+ refs.githubRefMap.set(githubRef.canonical, githubRef);
866
+ }
867
+ }
547
868
  function loadLifecycleArtifactKinds(root, bundleRef) {
548
869
  const bundlePath = join(root, bundleRef);
549
870
  if (!existsSync(bundlePath)) {
@@ -558,22 +879,31 @@ function loadLifecycleArtifactSourceReferences(root, bundleRef) {
558
879
  throw new Error(`Referenced bundle not found: ${bundleRef}`);
559
880
  }
560
881
  const bundle = auditBundleSchema.parse(JSON.parse(readFileSync(bundlePath, "utf8")));
561
- const repoContext = inferGitHubRepoContext(root);
882
+ const scmRepoContext = inferScmRepoContext(root);
883
+ const gitHubRepoContext = inferGitHubRepoContext(root);
562
884
  const issueRefs = new Set();
885
+ const scmRefs = new Map();
563
886
  const githubRefs = new Map();
564
887
  for (const artifact of bundle.lifecycleArtifacts) {
565
888
  for (const issueRef of artifact.source.issueRefs) {
566
889
  issueRefs.add(issueRef);
567
890
  }
891
+ for (const scmRef of artifact.source.scmRefs ?? []) {
892
+ scmRefs.set(scmRef.canonical, scmRef);
893
+ }
568
894
  for (const githubRef of artifact.source.githubRefs ?? []) {
569
895
  githubRefs.set(githubRef.canonical, githubRef);
570
896
  }
571
- for (const githubRef of normalizeGitHubReferences(artifact.source.issueRefs, repoContext)) {
897
+ for (const scmRef of normalizeScmReferences(artifact.source.issueRefs, scmRepoContext)) {
898
+ scmRefs.set(scmRef.canonical, scmRef);
899
+ }
900
+ for (const githubRef of normalizeGitHubReferences(artifact.source.issueRefs, gitHubRepoContext)) {
572
901
  githubRefs.set(githubRef.canonical, githubRef);
573
902
  }
574
903
  }
575
904
  return {
576
905
  issueRefs: [...issueRefs],
906
+ scmRefs: [...scmRefs.values()],
577
907
  githubRefs: [...githubRefs.values()]
578
908
  };
579
909
  }
@@ -584,9 +914,11 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
584
914
  const requestPath = ".agentops/requests/planning.yaml";
585
915
  ensureReadablePath(policyEngine, requestPath, "planning request");
586
916
  const planningRequest = validatePlanningRequestCompleteness(readYamlFile(join(root, requestPath), planningRequestSchema, "planning request"));
917
+ const planningScmRefs = normalizeScmReferences(planningRequest.issueRefs, inferScmRepoContext(root));
587
918
  const planningGithubRefs = normalizeGitHubReferences(planningRequest.issueRefs, inferGitHubRepoContext(root));
588
919
  return {
589
920
  planningRequest,
921
+ planningScmRefs,
590
922
  planningGithubRefs,
591
923
  requestFile: requestPath
592
924
  };
@@ -628,10 +960,11 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
628
960
  }
629
961
  const referencedSourceRefs = qaRequest.targetRef.endsWith("bundle.json")
630
962
  ? loadLifecycleArtifactSourceReferences(root, qaRequest.targetRef)
631
- : { issueRefs: [], githubRefs: [] };
963
+ : { issueRefs: [], scmRefs: [], githubRefs: [] };
632
964
  return {
633
965
  qaRequest: qaRequest,
634
966
  qaIssueRefs: referencedSourceRefs.issueRefs,
967
+ qaScmRefs: referencedSourceRefs.scmRefs,
635
968
  qaGithubRefs: referencedSourceRefs.githubRefs,
636
969
  requestFile: requestPath
637
970
  };
@@ -656,20 +989,68 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
656
989
  }
657
990
  const referencedSourceRefs = securityRequest.targetRef.endsWith("bundle.json")
658
991
  ? loadLifecycleArtifactSourceReferences(root, securityRequest.targetRef)
659
- : { issueRefs: [], githubRefs: [] };
992
+ : { issueRefs: [], scmRefs: [], githubRefs: [] };
660
993
  return {
661
994
  securityRequest: securityRequest,
662
995
  securityTargetArtifactKinds: referencedArtifactKinds,
663
996
  securityIssueRefs: referencedSourceRefs.issueRefs,
997
+ securityScmRefs: referencedSourceRefs.scmRefs,
664
998
  securityGithubRefs: referencedSourceRefs.githubRefs,
665
999
  requestFile: requestPath
666
1000
  };
667
1001
  }
1002
+ if (workflow.name === "pipeline-evidence-review") {
1003
+ const requestPath = ".agentops/requests/pipeline.yaml";
1004
+ ensureReadablePath(policyEngine, requestPath, "pipeline request");
1005
+ const pipelineRequest = validatePipelineRequestCompleteness(readYamlFile(join(root, requestPath), pipelineRequestSchema, "pipeline request"));
1006
+ const scmRepoContext = inferScmRepoContext(root);
1007
+ const gitHubRepoContext = inferGitHubRepoContext(root);
1008
+ const pipelineRefs = {
1009
+ issueRefs: new Set(pipelineRequest.issueRefs),
1010
+ scmRefMap: new Map(),
1011
+ githubRefMap: new Map()
1012
+ };
1013
+ for (const scmRef of normalizeScmReferences(pipelineRequest.issueRefs, scmRepoContext)) {
1014
+ pipelineRefs.scmRefMap.set(scmRef.canonical, scmRef);
1015
+ }
1016
+ for (const githubRef of normalizeGitHubReferences(pipelineRequest.issueRefs, gitHubRepoContext)) {
1017
+ pipelineRefs.githubRefMap.set(githubRef.canonical, githubRef);
1018
+ }
1019
+ for (const qaReportRef of pipelineRequest.qaReportRefs) {
1020
+ ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
1021
+ ensureBundleContainsArtifactKind(root, qaReportRef, "qa-report", "QA report reference");
1022
+ addSourceReferences(pipelineRefs, loadLifecycleArtifactSourceReferences(root, qaReportRef));
1023
+ }
1024
+ for (const securityReportRef of pipelineRequest.securityReportRefs) {
1025
+ ensureReadablePath(policyEngine, securityReportRef, "security report reference");
1026
+ ensureBundleContainsArtifactKind(root, securityReportRef, "security-report", "security report reference");
1027
+ addSourceReferences(pipelineRefs, loadLifecycleArtifactSourceReferences(root, securityReportRef));
1028
+ }
1029
+ for (const releaseReportRef of pipelineRequest.releaseReportRefs) {
1030
+ ensureReadablePath(policyEngine, releaseReportRef, "release report reference");
1031
+ ensureBundleContainsArtifactKind(root, releaseReportRef, "release-report", "release report reference");
1032
+ addSourceReferences(pipelineRefs, loadLifecycleArtifactSourceReferences(root, releaseReportRef));
1033
+ }
1034
+ for (const evidenceSource of pipelineRequest.evidenceSources) {
1035
+ ensureReadablePath(policyEngine, evidenceSource, "pipeline evidence source");
1036
+ if (!existsSync(join(root, evidenceSource))) {
1037
+ throw new Error(`Pipeline evidence source not found: ${evidenceSource}`);
1038
+ }
1039
+ }
1040
+ return {
1041
+ pipelineRequest: pipelineRequest,
1042
+ pipelineIssueRefs: [...pipelineRefs.issueRefs],
1043
+ pipelineScmRefs: [...pipelineRefs.scmRefMap.values()],
1044
+ pipelineGithubRefs: [...pipelineRefs.githubRefMap.values()],
1045
+ requestFile: requestPath
1046
+ };
1047
+ }
668
1048
  if (workflow.name === "release-readiness") {
669
1049
  const requestPath = ".agentops/requests/release.yaml";
670
1050
  ensureReadablePath(policyEngine, requestPath, "release request");
671
1051
  const releaseRequest = validateReleaseRequestCompleteness(readYamlFile(join(root, requestPath), releaseRequestSchema, "release request"));
672
1052
  const releaseIssueRefs = new Set();
1053
+ const releaseScmRefMap = new Map();
673
1054
  const releaseGithubRefMap = new Map();
674
1055
  for (const qaReportRef of releaseRequest.qaReportRefs) {
675
1056
  ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
@@ -678,6 +1059,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
678
1059
  for (const issueRef of refs.issueRefs) {
679
1060
  releaseIssueRefs.add(issueRef);
680
1061
  }
1062
+ for (const scmRef of refs.scmRefs) {
1063
+ releaseScmRefMap.set(scmRef.canonical, scmRef);
1064
+ }
681
1065
  for (const githubRef of refs.githubRefs) {
682
1066
  releaseGithubRefMap.set(githubRef.canonical, githubRef);
683
1067
  }
@@ -689,6 +1073,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
689
1073
  for (const issueRef of refs.issueRefs) {
690
1074
  releaseIssueRefs.add(issueRef);
691
1075
  }
1076
+ for (const scmRef of refs.scmRefs) {
1077
+ releaseScmRefMap.set(scmRef.canonical, scmRef);
1078
+ }
692
1079
  for (const githubRef of refs.githubRefs) {
693
1080
  releaseGithubRefMap.set(githubRef.canonical, githubRef);
694
1081
  }
@@ -702,18 +1089,126 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
702
1089
  return {
703
1090
  releaseRequest: releaseRequest,
704
1091
  releaseIssueRefs: [...releaseIssueRefs],
1092
+ releaseScmRefs: [...releaseScmRefMap.values()],
705
1093
  releaseGithubRefs: [...releaseGithubRefMap.values()],
706
1094
  requestFile: requestPath
707
1095
  };
708
1096
  }
1097
+ if (workflow.name === "deployment-gate-review") {
1098
+ const requestPath = ".agentops/requests/deployment.yaml";
1099
+ ensureReadablePath(policyEngine, requestPath, "deployment request");
1100
+ const deploymentRequest = validateDeploymentRequestCompleteness(readYamlFile(join(root, requestPath), deploymentRequestSchema, "deployment request"));
1101
+ const scmRepoContext = inferScmRepoContext(root);
1102
+ const gitHubRepoContext = inferGitHubRepoContext(root);
1103
+ const deploymentRefs = {
1104
+ issueRefs: new Set(deploymentRequest.issueRefs),
1105
+ scmRefMap: new Map(),
1106
+ githubRefMap: new Map()
1107
+ };
1108
+ for (const scmRef of normalizeScmReferences(deploymentRequest.issueRefs, scmRepoContext)) {
1109
+ deploymentRefs.scmRefMap.set(scmRef.canonical, scmRef);
1110
+ }
1111
+ for (const githubRef of normalizeGitHubReferences(deploymentRequest.issueRefs, gitHubRepoContext)) {
1112
+ deploymentRefs.githubRefMap.set(githubRef.canonical, githubRef);
1113
+ }
1114
+ for (const qaReportRef of deploymentRequest.qaReportRefs) {
1115
+ ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
1116
+ ensureBundleContainsArtifactKind(root, qaReportRef, "qa-report", "QA report reference");
1117
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, qaReportRef));
1118
+ }
1119
+ for (const securityReportRef of deploymentRequest.securityReportRefs) {
1120
+ ensureReadablePath(policyEngine, securityReportRef, "security report reference");
1121
+ ensureBundleContainsArtifactKind(root, securityReportRef, "security-report", "security report reference");
1122
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, securityReportRef));
1123
+ }
1124
+ for (const releaseReportRef of deploymentRequest.releaseReportRefs) {
1125
+ ensureReadablePath(policyEngine, releaseReportRef, "release report reference");
1126
+ ensureBundleContainsArtifactKind(root, releaseReportRef, "release-report", "release report reference");
1127
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, releaseReportRef));
1128
+ }
1129
+ for (const pipelineReportRef of deploymentRequest.pipelineReportRefs) {
1130
+ ensureReadablePath(policyEngine, pipelineReportRef, "pipeline report reference");
1131
+ ensureBundleContainsArtifactKind(root, pipelineReportRef, "pipeline-report", "pipeline report reference");
1132
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, pipelineReportRef));
1133
+ }
1134
+ for (const evidenceSource of deploymentRequest.evidenceSources) {
1135
+ ensureReadablePath(policyEngine, evidenceSource, "deployment evidence source");
1136
+ if (!existsSync(join(root, evidenceSource))) {
1137
+ throw new Error(`Deployment evidence source not found: ${evidenceSource}`);
1138
+ }
1139
+ }
1140
+ return {
1141
+ deploymentRequest: deploymentRequest,
1142
+ deploymentIssueRefs: [...deploymentRefs.issueRefs],
1143
+ deploymentScmRefs: [...deploymentRefs.scmRefMap.values()],
1144
+ deploymentGithubRefs: [...deploymentRefs.githubRefMap.values()],
1145
+ requestFile: requestPath
1146
+ };
1147
+ }
1148
+ if (workflow.name === "promotion-approval") {
1149
+ const requestPath = ".agentops/requests/promotion.yaml";
1150
+ ensureReadablePath(policyEngine, requestPath, "promotion request");
1151
+ const promotionRequest = validatePromotionRequestCompleteness(readYamlFile(join(root, requestPath), promotionRequestSchema, "promotion request"));
1152
+ const scmRepoContext = inferScmRepoContext(root);
1153
+ const gitHubRepoContext = inferGitHubRepoContext(root);
1154
+ const promotionRefs = {
1155
+ issueRefs: new Set(promotionRequest.issueRefs),
1156
+ scmRefMap: new Map(),
1157
+ githubRefMap: new Map()
1158
+ };
1159
+ for (const scmRef of normalizeScmReferences(promotionRequest.issueRefs, scmRepoContext)) {
1160
+ promotionRefs.scmRefMap.set(scmRef.canonical, scmRef);
1161
+ }
1162
+ for (const githubRef of normalizeGitHubReferences(promotionRequest.issueRefs, gitHubRepoContext)) {
1163
+ promotionRefs.githubRefMap.set(githubRef.canonical, githubRef);
1164
+ }
1165
+ for (const qaReportRef of promotionRequest.qaReportRefs) {
1166
+ ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
1167
+ ensureBundleContainsArtifactKind(root, qaReportRef, "qa-report", "QA report reference");
1168
+ addSourceReferences(promotionRefs, loadLifecycleArtifactSourceReferences(root, qaReportRef));
1169
+ }
1170
+ for (const securityReportRef of promotionRequest.securityReportRefs) {
1171
+ ensureReadablePath(policyEngine, securityReportRef, "security report reference");
1172
+ ensureBundleContainsArtifactKind(root, securityReportRef, "security-report", "security report reference");
1173
+ addSourceReferences(promotionRefs, loadLifecycleArtifactSourceReferences(root, securityReportRef));
1174
+ }
1175
+ for (const releaseReportRef of promotionRequest.releaseReportRefs) {
1176
+ ensureReadablePath(policyEngine, releaseReportRef, "release report reference");
1177
+ ensureBundleContainsArtifactKind(root, releaseReportRef, "release-report", "release report reference");
1178
+ addSourceReferences(promotionRefs, loadLifecycleArtifactSourceReferences(root, releaseReportRef));
1179
+ }
1180
+ for (const deploymentGateReportRef of promotionRequest.deploymentGateReportRefs) {
1181
+ ensureReadablePath(policyEngine, deploymentGateReportRef, "deployment gate report reference");
1182
+ ensureBundleContainsArtifactKind(root, deploymentGateReportRef, "deployment-gate-report", "deployment gate report reference");
1183
+ addSourceReferences(promotionRefs, loadLifecycleArtifactSourceReferences(root, deploymentGateReportRef));
1184
+ }
1185
+ for (const evidenceSource of promotionRequest.evidenceSources) {
1186
+ ensureReadablePath(policyEngine, evidenceSource, "promotion evidence source");
1187
+ if (!existsSync(join(root, evidenceSource))) {
1188
+ throw new Error(`Promotion evidence source not found: ${evidenceSource}`);
1189
+ }
1190
+ }
1191
+ return {
1192
+ promotionRequest: promotionRequest,
1193
+ promotionIssueRefs: [...promotionRefs.issueRefs],
1194
+ promotionScmRefs: [...promotionRefs.scmRefMap.values()],
1195
+ promotionGithubRefs: [...promotionRefs.githubRefMap.values()],
1196
+ requestFile: requestPath
1197
+ };
1198
+ }
709
1199
  if (workflow.name === "incident-handoff") {
710
1200
  const requestPath = ".agentops/requests/incident.yaml";
711
1201
  ensureReadablePath(policyEngine, requestPath, "incident request");
712
1202
  const incidentRequest = validateIncidentRequestCompleteness(readYamlFile(join(root, requestPath), incidentRequestSchema, "incident request"));
713
- const repoContext = inferGitHubRepoContext(root);
1203
+ const scmRepoContext = inferScmRepoContext(root);
1204
+ const gitHubRepoContext = inferGitHubRepoContext(root);
714
1205
  const incidentIssueRefs = new Set(incidentRequest.issueRefs);
1206
+ const incidentScmRefMap = new Map();
715
1207
  const incidentGithubRefMap = new Map();
716
- for (const githubRef of normalizeGitHubReferences(incidentRequest.issueRefs, repoContext)) {
1208
+ for (const scmRef of normalizeScmReferences(incidentRequest.issueRefs, scmRepoContext)) {
1209
+ incidentScmRefMap.set(scmRef.canonical, scmRef);
1210
+ }
1211
+ for (const githubRef of normalizeGitHubReferences(incidentRequest.issueRefs, gitHubRepoContext)) {
717
1212
  incidentGithubRefMap.set(githubRef.canonical, githubRef);
718
1213
  }
719
1214
  for (const releaseReportRef of incidentRequest.releaseReportRefs) {
@@ -723,6 +1218,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
723
1218
  for (const issueRef of refs.issueRefs) {
724
1219
  incidentIssueRefs.add(issueRef);
725
1220
  }
1221
+ for (const scmRef of refs.scmRefs) {
1222
+ incidentScmRefMap.set(scmRef.canonical, scmRef);
1223
+ }
726
1224
  for (const githubRef of refs.githubRefs) {
727
1225
  incidentGithubRefMap.set(githubRef.canonical, githubRef);
728
1226
  }
@@ -736,6 +1234,7 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
736
1234
  return {
737
1235
  incidentRequest: incidentRequest,
738
1236
  incidentIssueRefs: [...incidentIssueRefs],
1237
+ incidentScmRefs: [...incidentScmRefMap.values()],
739
1238
  incidentGithubRefs: [...incidentGithubRefMap.values()],
740
1239
  requestFile: requestPath
741
1240
  };
@@ -744,10 +1243,15 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
744
1243
  const requestPath = ".agentops/requests/maintenance.yaml";
745
1244
  ensureReadablePath(policyEngine, requestPath, "maintenance request");
746
1245
  const maintenanceRequest = validateMaintenanceRequestCompleteness(readYamlFile(join(root, requestPath), maintenanceRequestSchema, "maintenance request"));
747
- const repoContext = inferGitHubRepoContext(root);
1246
+ const scmRepoContext = inferScmRepoContext(root);
1247
+ const gitHubRepoContext = inferGitHubRepoContext(root);
748
1248
  const maintenanceIssueRefs = new Set(maintenanceRequest.issueRefs);
1249
+ const maintenanceScmRefMap = new Map();
749
1250
  const maintenanceGithubRefMap = new Map();
750
- for (const githubRef of normalizeGitHubReferences(maintenanceRequest.issueRefs, repoContext)) {
1251
+ for (const scmRef of normalizeScmReferences(maintenanceRequest.issueRefs, scmRepoContext)) {
1252
+ maintenanceScmRefMap.set(scmRef.canonical, scmRef);
1253
+ }
1254
+ for (const githubRef of normalizeGitHubReferences(maintenanceRequest.issueRefs, gitHubRepoContext)) {
751
1255
  maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
752
1256
  }
753
1257
  for (const releaseReportRef of maintenanceRequest.releaseReportRefs) {
@@ -757,6 +1261,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
757
1261
  for (const issueRef of refs.issueRefs) {
758
1262
  maintenanceIssueRefs.add(issueRef);
759
1263
  }
1264
+ for (const scmRef of refs.scmRefs) {
1265
+ maintenanceScmRefMap.set(scmRef.canonical, scmRef);
1266
+ }
760
1267
  for (const githubRef of refs.githubRefs) {
761
1268
  maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
762
1269
  }
@@ -776,6 +1283,7 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
776
1283
  return {
777
1284
  maintenanceRequest: maintenanceRequest,
778
1285
  maintenanceIssueRefs: [...maintenanceIssueRefs],
1286
+ maintenanceScmRefs: [...maintenanceScmRefMap.values()],
779
1287
  maintenanceGithubRefs: [...maintenanceGithubRefMap.values()],
780
1288
  requestFile: requestPath
781
1289
  };
@@ -1421,6 +1929,18 @@ function ensureInitFiles(root) {
1421
1929
  path: join(workflowsDir, "release-readiness.yaml"),
1422
1930
  contents: releaseWorkflowTemplate
1423
1931
  },
1932
+ {
1933
+ path: join(workflowsDir, "pipeline-evidence-review.yaml"),
1934
+ contents: pipelineWorkflowTemplate
1935
+ },
1936
+ {
1937
+ path: join(workflowsDir, "deployment-gate-review.yaml"),
1938
+ contents: deploymentGateWorkflowTemplate
1939
+ },
1940
+ {
1941
+ path: join(workflowsDir, "promotion-approval.yaml"),
1942
+ contents: promotionApprovalWorkflowTemplate
1943
+ },
1424
1944
  {
1425
1945
  path: join(workflowsDir, "incident-handoff.yaml"),
1426
1946
  contents: incidentWorkflowTemplate