@h9-foundry/agentforge-cli 0.9.0 → 0.10.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, 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,58 @@ 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
+ `;
262
314
  const incidentWorkflowTemplate = `version: 1
263
315
  name: incident-handoff
264
316
  description: Validate staged incident evidence while keeping the default path local, read-only, and explicit.
@@ -328,24 +380,90 @@ function runGit(root, args) {
328
380
  return "";
329
381
  }
330
382
  }
331
- function parseGitHubRepositoryUrl(value) {
383
+ function inferScmPlatform(host) {
384
+ const normalizedHost = host.toLowerCase();
385
+ if (normalizedHost.includes("github")) {
386
+ return "github";
387
+ }
388
+ if (normalizedHost.includes("gitlab")) {
389
+ return "gitlab";
390
+ }
391
+ return "generic";
392
+ }
393
+ function parseScmRepositoryUrl(value) {
332
394
  const trimmed = value.trim();
333
- const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i);
395
+ const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\.git)?$/i);
334
396
  if (sshMatch) {
397
+ const host = sshMatch[1].toLowerCase();
398
+ const pathSegments = sshMatch[2].split("/").filter(Boolean);
399
+ if (pathSegments.length < 2) {
400
+ return undefined;
401
+ }
402
+ const repo = pathSegments[pathSegments.length - 1] ?? "";
403
+ const namespace = pathSegments.slice(0, -1).join("/");
404
+ const platform = inferScmPlatform(host);
405
+ if (platform === "github") {
406
+ return {
407
+ platform,
408
+ host,
409
+ owner: namespace,
410
+ repo
411
+ };
412
+ }
335
413
  return {
336
- host: sshMatch[1].toLowerCase(),
337
- owner: sshMatch[2],
338
- repo: sshMatch[3]
414
+ platform,
415
+ host,
416
+ namespace,
417
+ repo
339
418
  };
340
419
  }
341
- const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/)?$/i);
420
+ const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?(?:\/)?$/i);
342
421
  if (!httpsMatch) {
343
422
  return undefined;
344
423
  }
424
+ const host = httpsMatch[1].toLowerCase();
425
+ const pathSegments = httpsMatch[2].split("/").filter(Boolean);
426
+ if (pathSegments.length < 2) {
427
+ return undefined;
428
+ }
429
+ const repo = pathSegments[pathSegments.length - 1] ?? "";
430
+ const namespace = pathSegments.slice(0, -1).join("/");
431
+ const platform = inferScmPlatform(host);
432
+ if (platform === "github") {
433
+ return {
434
+ platform,
435
+ host,
436
+ owner: namespace,
437
+ repo
438
+ };
439
+ }
440
+ return {
441
+ platform,
442
+ host,
443
+ namespace,
444
+ repo
445
+ };
446
+ }
447
+ function parseGitHubRepositoryUrl(value) {
448
+ const parsed = parseScmRepositoryUrl(value);
449
+ if (!parsed || parsed.platform !== "github") {
450
+ return undefined;
451
+ }
345
452
  return {
346
- host: httpsMatch[1].toLowerCase(),
347
- owner: httpsMatch[2],
348
- repo: httpsMatch[3]
453
+ host: parsed.host,
454
+ owner: parsed.owner,
455
+ repo: parsed.repo
456
+ };
457
+ }
458
+ function parseGitLabRepositoryUrl(value) {
459
+ const parsed = parseScmRepositoryUrl(value);
460
+ if (!parsed || parsed.platform !== "gitlab") {
461
+ return undefined;
462
+ }
463
+ return {
464
+ host: parsed.host,
465
+ namespace: parsed.namespace,
466
+ repo: parsed.repo
349
467
  };
350
468
  }
351
469
  function inferGitHubRepoContext(root) {
@@ -371,6 +489,60 @@ function inferGitHubRepoContext(root) {
371
489
  const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
372
490
  return remoteUrl ? parseGitHubRepositoryUrl(remoteUrl) : undefined;
373
491
  }
492
+ function inferGitLabRepoContext(root) {
493
+ const packageJsonPath = join(root, "package.json");
494
+ if (existsSync(packageJsonPath)) {
495
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
496
+ if (isRecord(parsed)) {
497
+ const repository = parsed.repository;
498
+ if (typeof repository === "string") {
499
+ const context = parseGitLabRepositoryUrl(repository);
500
+ if (context) {
501
+ return context;
502
+ }
503
+ }
504
+ if (isRecord(repository) && typeof repository.url === "string") {
505
+ const context = parseGitLabRepositoryUrl(repository.url);
506
+ if (context) {
507
+ return context;
508
+ }
509
+ }
510
+ }
511
+ }
512
+ const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
513
+ return remoteUrl ? parseGitLabRepositoryUrl(remoteUrl) : undefined;
514
+ }
515
+ function inferScmRepoContext(root) {
516
+ const gitHubContext = inferGitHubRepoContext(root);
517
+ if (gitHubContext) {
518
+ return { platform: "github", ...gitHubContext };
519
+ }
520
+ const gitLabContext = inferGitLabRepoContext(root);
521
+ if (gitLabContext) {
522
+ return { platform: "gitlab", ...gitLabContext };
523
+ }
524
+ const packageJsonPath = join(root, "package.json");
525
+ if (existsSync(packageJsonPath)) {
526
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
527
+ if (isRecord(parsed)) {
528
+ const repository = parsed.repository;
529
+ if (typeof repository === "string") {
530
+ const context = parseScmRepositoryUrl(repository);
531
+ if (context) {
532
+ return context;
533
+ }
534
+ }
535
+ if (isRecord(repository) && typeof repository.url === "string") {
536
+ const context = parseScmRepositoryUrl(repository.url);
537
+ if (context) {
538
+ return context;
539
+ }
540
+ }
541
+ }
542
+ }
543
+ const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
544
+ return remoteUrl ? parseScmRepositoryUrl(remoteUrl) : undefined;
545
+ }
374
546
  function normalizeGitHubReference(rawValue, repoContext) {
375
547
  const raw = rawValue.trim();
376
548
  if (!raw) {
@@ -426,6 +598,86 @@ function normalizeGitHubReferences(rawValues, repoContext) {
426
598
  }
427
599
  return normalized;
428
600
  }
601
+ function normalizeGitLabReference(rawValue, repoContext) {
602
+ const raw = rawValue.trim();
603
+ if (!raw) {
604
+ return undefined;
605
+ }
606
+ const fromParts = (context, kind, number) => ({
607
+ platform: "gitlab",
608
+ host: context.host,
609
+ namespace: context.namespace,
610
+ repo: context.repo,
611
+ kind,
612
+ identifier: `${number}`,
613
+ number,
614
+ canonical: kind === "issue"
615
+ ? `${context.host}/${context.namespace}/${context.repo}#${number}`
616
+ : `${context.host}/${context.namespace}/${context.repo}!${number}`,
617
+ url: kind === "issue"
618
+ ? `https://${context.host}/${context.namespace}/${context.repo}/-/issues/${number}`
619
+ : `https://${context.host}/${context.namespace}/${context.repo}/-/merge_requests/${number}`,
620
+ source: raw
621
+ });
622
+ const urlMatch = raw.match(/^https?:\/\/([^/]+)\/(.+?)\/-\/(issues|merge_requests)\/(\d+)(?:\/)?$/i);
623
+ if (urlMatch) {
624
+ const pathSegments = urlMatch[2].split("/").filter(Boolean);
625
+ if (pathSegments.length < 2) {
626
+ return undefined;
627
+ }
628
+ const repo = pathSegments[pathSegments.length - 1] ?? "";
629
+ const namespace = pathSegments.slice(0, -1).join("/");
630
+ return fromParts({ host: urlMatch[1].toLowerCase(), namespace, repo }, urlMatch[3].toLowerCase() === "merge_requests" ? "merge_request" : "issue", Number.parseInt(urlMatch[4], 10));
631
+ }
632
+ const shortIssueMatch = raw.match(/^#(\d+)$/);
633
+ if (shortIssueMatch && repoContext) {
634
+ return fromParts(repoContext, "issue", Number.parseInt(shortIssueMatch[1], 10));
635
+ }
636
+ const shortMergeRequestMatch = raw.match(/^!(\d+)$/);
637
+ if (shortMergeRequestMatch && repoContext) {
638
+ return fromParts(repoContext, "merge_request", Number.parseInt(shortMergeRequestMatch[1], 10));
639
+ }
640
+ return undefined;
641
+ }
642
+ function normalizeScmReference(rawValue, repoContext) {
643
+ const raw = rawValue.trim();
644
+ if (!raw) {
645
+ return undefined;
646
+ }
647
+ const gitHubRef = normalizeGitHubReference(raw, repoContext?.platform === "github"
648
+ ? { host: repoContext.host, owner: repoContext.owner, repo: repoContext.repo }
649
+ : undefined);
650
+ if (gitHubRef) {
651
+ return {
652
+ platform: "github",
653
+ host: gitHubRef.host,
654
+ namespace: gitHubRef.owner,
655
+ repo: gitHubRef.repo,
656
+ kind: gitHubRef.kind,
657
+ identifier: `${gitHubRef.number}`,
658
+ number: gitHubRef.number,
659
+ canonical: `${gitHubRef.host}/${gitHubRef.owner}/${gitHubRef.repo}${gitHubRef.kind === "issue" ? `#${gitHubRef.number}` : `/pull/${gitHubRef.number}`}`,
660
+ url: gitHubRef.url,
661
+ source: gitHubRef.source
662
+ };
663
+ }
664
+ return normalizeGitLabReference(raw, repoContext?.platform === "gitlab"
665
+ ? { host: repoContext.host, namespace: repoContext.namespace, repo: repoContext.repo }
666
+ : undefined);
667
+ }
668
+ function normalizeScmReferences(rawValues, repoContext) {
669
+ const seen = new Set();
670
+ const normalized = [];
671
+ for (const rawValue of rawValues) {
672
+ const scmRef = normalizeScmReference(rawValue, repoContext);
673
+ if (!scmRef || seen.has(scmRef.canonical)) {
674
+ continue;
675
+ }
676
+ seen.add(scmRef.canonical);
677
+ normalized.push(scmRef);
678
+ }
679
+ return normalized;
680
+ }
429
681
  export function mapWorkflowRunStatusToGitHubStatus(workflow, localRunStatus) {
430
682
  if (localRunStatus === "success") {
431
683
  return {
@@ -544,6 +796,35 @@ function validateReleaseRequestCompleteness(request) {
544
796
  }
545
797
  return request;
546
798
  }
799
+ function validatePipelineRequestCompleteness(request) {
800
+ const evidenceSignalCount = request.evidenceSources.length + request.qaReportRefs.length + request.securityReportRefs.length + request.releaseReportRefs.length;
801
+ if (evidenceSignalCount === 0) {
802
+ throw new Error("Pipeline request is underspecified. Add at least one of evidenceSources, qaReportRefs, securityReportRefs, or releaseReportRefs.");
803
+ }
804
+ return request;
805
+ }
806
+ function validateDeploymentRequestCompleteness(request) {
807
+ const evidenceSignalCount = request.evidenceSources.length +
808
+ request.qaReportRefs.length +
809
+ request.securityReportRefs.length +
810
+ request.releaseReportRefs.length +
811
+ request.pipelineReportRefs.length;
812
+ if (evidenceSignalCount === 0) {
813
+ throw new Error("Deployment request is underspecified. Add at least one of evidenceSources, qaReportRefs, securityReportRefs, releaseReportRefs, or pipelineReportRefs.");
814
+ }
815
+ return request;
816
+ }
817
+ function addSourceReferences(refs, incoming) {
818
+ for (const issueRef of incoming.issueRefs) {
819
+ refs.issueRefs.add(issueRef);
820
+ }
821
+ for (const scmRef of incoming.scmRefs) {
822
+ refs.scmRefMap.set(scmRef.canonical, scmRef);
823
+ }
824
+ for (const githubRef of incoming.githubRefs) {
825
+ refs.githubRefMap.set(githubRef.canonical, githubRef);
826
+ }
827
+ }
547
828
  function loadLifecycleArtifactKinds(root, bundleRef) {
548
829
  const bundlePath = join(root, bundleRef);
549
830
  if (!existsSync(bundlePath)) {
@@ -558,22 +839,31 @@ function loadLifecycleArtifactSourceReferences(root, bundleRef) {
558
839
  throw new Error(`Referenced bundle not found: ${bundleRef}`);
559
840
  }
560
841
  const bundle = auditBundleSchema.parse(JSON.parse(readFileSync(bundlePath, "utf8")));
561
- const repoContext = inferGitHubRepoContext(root);
842
+ const scmRepoContext = inferScmRepoContext(root);
843
+ const gitHubRepoContext = inferGitHubRepoContext(root);
562
844
  const issueRefs = new Set();
845
+ const scmRefs = new Map();
563
846
  const githubRefs = new Map();
564
847
  for (const artifact of bundle.lifecycleArtifacts) {
565
848
  for (const issueRef of artifact.source.issueRefs) {
566
849
  issueRefs.add(issueRef);
567
850
  }
851
+ for (const scmRef of artifact.source.scmRefs ?? []) {
852
+ scmRefs.set(scmRef.canonical, scmRef);
853
+ }
568
854
  for (const githubRef of artifact.source.githubRefs ?? []) {
569
855
  githubRefs.set(githubRef.canonical, githubRef);
570
856
  }
571
- for (const githubRef of normalizeGitHubReferences(artifact.source.issueRefs, repoContext)) {
857
+ for (const scmRef of normalizeScmReferences(artifact.source.issueRefs, scmRepoContext)) {
858
+ scmRefs.set(scmRef.canonical, scmRef);
859
+ }
860
+ for (const githubRef of normalizeGitHubReferences(artifact.source.issueRefs, gitHubRepoContext)) {
572
861
  githubRefs.set(githubRef.canonical, githubRef);
573
862
  }
574
863
  }
575
864
  return {
576
865
  issueRefs: [...issueRefs],
866
+ scmRefs: [...scmRefs.values()],
577
867
  githubRefs: [...githubRefs.values()]
578
868
  };
579
869
  }
@@ -584,9 +874,11 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
584
874
  const requestPath = ".agentops/requests/planning.yaml";
585
875
  ensureReadablePath(policyEngine, requestPath, "planning request");
586
876
  const planningRequest = validatePlanningRequestCompleteness(readYamlFile(join(root, requestPath), planningRequestSchema, "planning request"));
877
+ const planningScmRefs = normalizeScmReferences(planningRequest.issueRefs, inferScmRepoContext(root));
587
878
  const planningGithubRefs = normalizeGitHubReferences(planningRequest.issueRefs, inferGitHubRepoContext(root));
588
879
  return {
589
880
  planningRequest,
881
+ planningScmRefs,
590
882
  planningGithubRefs,
591
883
  requestFile: requestPath
592
884
  };
@@ -628,10 +920,11 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
628
920
  }
629
921
  const referencedSourceRefs = qaRequest.targetRef.endsWith("bundle.json")
630
922
  ? loadLifecycleArtifactSourceReferences(root, qaRequest.targetRef)
631
- : { issueRefs: [], githubRefs: [] };
923
+ : { issueRefs: [], scmRefs: [], githubRefs: [] };
632
924
  return {
633
925
  qaRequest: qaRequest,
634
926
  qaIssueRefs: referencedSourceRefs.issueRefs,
927
+ qaScmRefs: referencedSourceRefs.scmRefs,
635
928
  qaGithubRefs: referencedSourceRefs.githubRefs,
636
929
  requestFile: requestPath
637
930
  };
@@ -656,20 +949,68 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
656
949
  }
657
950
  const referencedSourceRefs = securityRequest.targetRef.endsWith("bundle.json")
658
951
  ? loadLifecycleArtifactSourceReferences(root, securityRequest.targetRef)
659
- : { issueRefs: [], githubRefs: [] };
952
+ : { issueRefs: [], scmRefs: [], githubRefs: [] };
660
953
  return {
661
954
  securityRequest: securityRequest,
662
955
  securityTargetArtifactKinds: referencedArtifactKinds,
663
956
  securityIssueRefs: referencedSourceRefs.issueRefs,
957
+ securityScmRefs: referencedSourceRefs.scmRefs,
664
958
  securityGithubRefs: referencedSourceRefs.githubRefs,
665
959
  requestFile: requestPath
666
960
  };
667
961
  }
962
+ if (workflow.name === "pipeline-evidence-review") {
963
+ const requestPath = ".agentops/requests/pipeline.yaml";
964
+ ensureReadablePath(policyEngine, requestPath, "pipeline request");
965
+ const pipelineRequest = validatePipelineRequestCompleteness(readYamlFile(join(root, requestPath), pipelineRequestSchema, "pipeline request"));
966
+ const scmRepoContext = inferScmRepoContext(root);
967
+ const gitHubRepoContext = inferGitHubRepoContext(root);
968
+ const pipelineRefs = {
969
+ issueRefs: new Set(pipelineRequest.issueRefs),
970
+ scmRefMap: new Map(),
971
+ githubRefMap: new Map()
972
+ };
973
+ for (const scmRef of normalizeScmReferences(pipelineRequest.issueRefs, scmRepoContext)) {
974
+ pipelineRefs.scmRefMap.set(scmRef.canonical, scmRef);
975
+ }
976
+ for (const githubRef of normalizeGitHubReferences(pipelineRequest.issueRefs, gitHubRepoContext)) {
977
+ pipelineRefs.githubRefMap.set(githubRef.canonical, githubRef);
978
+ }
979
+ for (const qaReportRef of pipelineRequest.qaReportRefs) {
980
+ ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
981
+ ensureBundleContainsArtifactKind(root, qaReportRef, "qa-report", "QA report reference");
982
+ addSourceReferences(pipelineRefs, loadLifecycleArtifactSourceReferences(root, qaReportRef));
983
+ }
984
+ for (const securityReportRef of pipelineRequest.securityReportRefs) {
985
+ ensureReadablePath(policyEngine, securityReportRef, "security report reference");
986
+ ensureBundleContainsArtifactKind(root, securityReportRef, "security-report", "security report reference");
987
+ addSourceReferences(pipelineRefs, loadLifecycleArtifactSourceReferences(root, securityReportRef));
988
+ }
989
+ for (const releaseReportRef of pipelineRequest.releaseReportRefs) {
990
+ ensureReadablePath(policyEngine, releaseReportRef, "release report reference");
991
+ ensureBundleContainsArtifactKind(root, releaseReportRef, "release-report", "release report reference");
992
+ addSourceReferences(pipelineRefs, loadLifecycleArtifactSourceReferences(root, releaseReportRef));
993
+ }
994
+ for (const evidenceSource of pipelineRequest.evidenceSources) {
995
+ ensureReadablePath(policyEngine, evidenceSource, "pipeline evidence source");
996
+ if (!existsSync(join(root, evidenceSource))) {
997
+ throw new Error(`Pipeline evidence source not found: ${evidenceSource}`);
998
+ }
999
+ }
1000
+ return {
1001
+ pipelineRequest: pipelineRequest,
1002
+ pipelineIssueRefs: [...pipelineRefs.issueRefs],
1003
+ pipelineScmRefs: [...pipelineRefs.scmRefMap.values()],
1004
+ pipelineGithubRefs: [...pipelineRefs.githubRefMap.values()],
1005
+ requestFile: requestPath
1006
+ };
1007
+ }
668
1008
  if (workflow.name === "release-readiness") {
669
1009
  const requestPath = ".agentops/requests/release.yaml";
670
1010
  ensureReadablePath(policyEngine, requestPath, "release request");
671
1011
  const releaseRequest = validateReleaseRequestCompleteness(readYamlFile(join(root, requestPath), releaseRequestSchema, "release request"));
672
1012
  const releaseIssueRefs = new Set();
1013
+ const releaseScmRefMap = new Map();
673
1014
  const releaseGithubRefMap = new Map();
674
1015
  for (const qaReportRef of releaseRequest.qaReportRefs) {
675
1016
  ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
@@ -678,6 +1019,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
678
1019
  for (const issueRef of refs.issueRefs) {
679
1020
  releaseIssueRefs.add(issueRef);
680
1021
  }
1022
+ for (const scmRef of refs.scmRefs) {
1023
+ releaseScmRefMap.set(scmRef.canonical, scmRef);
1024
+ }
681
1025
  for (const githubRef of refs.githubRefs) {
682
1026
  releaseGithubRefMap.set(githubRef.canonical, githubRef);
683
1027
  }
@@ -689,6 +1033,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
689
1033
  for (const issueRef of refs.issueRefs) {
690
1034
  releaseIssueRefs.add(issueRef);
691
1035
  }
1036
+ for (const scmRef of refs.scmRefs) {
1037
+ releaseScmRefMap.set(scmRef.canonical, scmRef);
1038
+ }
692
1039
  for (const githubRef of refs.githubRefs) {
693
1040
  releaseGithubRefMap.set(githubRef.canonical, githubRef);
694
1041
  }
@@ -702,18 +1049,75 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
702
1049
  return {
703
1050
  releaseRequest: releaseRequest,
704
1051
  releaseIssueRefs: [...releaseIssueRefs],
1052
+ releaseScmRefs: [...releaseScmRefMap.values()],
705
1053
  releaseGithubRefs: [...releaseGithubRefMap.values()],
706
1054
  requestFile: requestPath
707
1055
  };
708
1056
  }
1057
+ if (workflow.name === "deployment-gate-review") {
1058
+ const requestPath = ".agentops/requests/deployment.yaml";
1059
+ ensureReadablePath(policyEngine, requestPath, "deployment request");
1060
+ const deploymentRequest = validateDeploymentRequestCompleteness(readYamlFile(join(root, requestPath), deploymentRequestSchema, "deployment request"));
1061
+ const scmRepoContext = inferScmRepoContext(root);
1062
+ const gitHubRepoContext = inferGitHubRepoContext(root);
1063
+ const deploymentRefs = {
1064
+ issueRefs: new Set(deploymentRequest.issueRefs),
1065
+ scmRefMap: new Map(),
1066
+ githubRefMap: new Map()
1067
+ };
1068
+ for (const scmRef of normalizeScmReferences(deploymentRequest.issueRefs, scmRepoContext)) {
1069
+ deploymentRefs.scmRefMap.set(scmRef.canonical, scmRef);
1070
+ }
1071
+ for (const githubRef of normalizeGitHubReferences(deploymentRequest.issueRefs, gitHubRepoContext)) {
1072
+ deploymentRefs.githubRefMap.set(githubRef.canonical, githubRef);
1073
+ }
1074
+ for (const qaReportRef of deploymentRequest.qaReportRefs) {
1075
+ ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
1076
+ ensureBundleContainsArtifactKind(root, qaReportRef, "qa-report", "QA report reference");
1077
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, qaReportRef));
1078
+ }
1079
+ for (const securityReportRef of deploymentRequest.securityReportRefs) {
1080
+ ensureReadablePath(policyEngine, securityReportRef, "security report reference");
1081
+ ensureBundleContainsArtifactKind(root, securityReportRef, "security-report", "security report reference");
1082
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, securityReportRef));
1083
+ }
1084
+ for (const releaseReportRef of deploymentRequest.releaseReportRefs) {
1085
+ ensureReadablePath(policyEngine, releaseReportRef, "release report reference");
1086
+ ensureBundleContainsArtifactKind(root, releaseReportRef, "release-report", "release report reference");
1087
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, releaseReportRef));
1088
+ }
1089
+ for (const pipelineReportRef of deploymentRequest.pipelineReportRefs) {
1090
+ ensureReadablePath(policyEngine, pipelineReportRef, "pipeline report reference");
1091
+ ensureBundleContainsArtifactKind(root, pipelineReportRef, "pipeline-report", "pipeline report reference");
1092
+ addSourceReferences(deploymentRefs, loadLifecycleArtifactSourceReferences(root, pipelineReportRef));
1093
+ }
1094
+ for (const evidenceSource of deploymentRequest.evidenceSources) {
1095
+ ensureReadablePath(policyEngine, evidenceSource, "deployment evidence source");
1096
+ if (!existsSync(join(root, evidenceSource))) {
1097
+ throw new Error(`Deployment evidence source not found: ${evidenceSource}`);
1098
+ }
1099
+ }
1100
+ return {
1101
+ deploymentRequest: deploymentRequest,
1102
+ deploymentIssueRefs: [...deploymentRefs.issueRefs],
1103
+ deploymentScmRefs: [...deploymentRefs.scmRefMap.values()],
1104
+ deploymentGithubRefs: [...deploymentRefs.githubRefMap.values()],
1105
+ requestFile: requestPath
1106
+ };
1107
+ }
709
1108
  if (workflow.name === "incident-handoff") {
710
1109
  const requestPath = ".agentops/requests/incident.yaml";
711
1110
  ensureReadablePath(policyEngine, requestPath, "incident request");
712
1111
  const incidentRequest = validateIncidentRequestCompleteness(readYamlFile(join(root, requestPath), incidentRequestSchema, "incident request"));
713
- const repoContext = inferGitHubRepoContext(root);
1112
+ const scmRepoContext = inferScmRepoContext(root);
1113
+ const gitHubRepoContext = inferGitHubRepoContext(root);
714
1114
  const incidentIssueRefs = new Set(incidentRequest.issueRefs);
1115
+ const incidentScmRefMap = new Map();
715
1116
  const incidentGithubRefMap = new Map();
716
- for (const githubRef of normalizeGitHubReferences(incidentRequest.issueRefs, repoContext)) {
1117
+ for (const scmRef of normalizeScmReferences(incidentRequest.issueRefs, scmRepoContext)) {
1118
+ incidentScmRefMap.set(scmRef.canonical, scmRef);
1119
+ }
1120
+ for (const githubRef of normalizeGitHubReferences(incidentRequest.issueRefs, gitHubRepoContext)) {
717
1121
  incidentGithubRefMap.set(githubRef.canonical, githubRef);
718
1122
  }
719
1123
  for (const releaseReportRef of incidentRequest.releaseReportRefs) {
@@ -723,6 +1127,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
723
1127
  for (const issueRef of refs.issueRefs) {
724
1128
  incidentIssueRefs.add(issueRef);
725
1129
  }
1130
+ for (const scmRef of refs.scmRefs) {
1131
+ incidentScmRefMap.set(scmRef.canonical, scmRef);
1132
+ }
726
1133
  for (const githubRef of refs.githubRefs) {
727
1134
  incidentGithubRefMap.set(githubRef.canonical, githubRef);
728
1135
  }
@@ -736,6 +1143,7 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
736
1143
  return {
737
1144
  incidentRequest: incidentRequest,
738
1145
  incidentIssueRefs: [...incidentIssueRefs],
1146
+ incidentScmRefs: [...incidentScmRefMap.values()],
739
1147
  incidentGithubRefs: [...incidentGithubRefMap.values()],
740
1148
  requestFile: requestPath
741
1149
  };
@@ -744,10 +1152,15 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
744
1152
  const requestPath = ".agentops/requests/maintenance.yaml";
745
1153
  ensureReadablePath(policyEngine, requestPath, "maintenance request");
746
1154
  const maintenanceRequest = validateMaintenanceRequestCompleteness(readYamlFile(join(root, requestPath), maintenanceRequestSchema, "maintenance request"));
747
- const repoContext = inferGitHubRepoContext(root);
1155
+ const scmRepoContext = inferScmRepoContext(root);
1156
+ const gitHubRepoContext = inferGitHubRepoContext(root);
748
1157
  const maintenanceIssueRefs = new Set(maintenanceRequest.issueRefs);
1158
+ const maintenanceScmRefMap = new Map();
749
1159
  const maintenanceGithubRefMap = new Map();
750
- for (const githubRef of normalizeGitHubReferences(maintenanceRequest.issueRefs, repoContext)) {
1160
+ for (const scmRef of normalizeScmReferences(maintenanceRequest.issueRefs, scmRepoContext)) {
1161
+ maintenanceScmRefMap.set(scmRef.canonical, scmRef);
1162
+ }
1163
+ for (const githubRef of normalizeGitHubReferences(maintenanceRequest.issueRefs, gitHubRepoContext)) {
751
1164
  maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
752
1165
  }
753
1166
  for (const releaseReportRef of maintenanceRequest.releaseReportRefs) {
@@ -757,6 +1170,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
757
1170
  for (const issueRef of refs.issueRefs) {
758
1171
  maintenanceIssueRefs.add(issueRef);
759
1172
  }
1173
+ for (const scmRef of refs.scmRefs) {
1174
+ maintenanceScmRefMap.set(scmRef.canonical, scmRef);
1175
+ }
760
1176
  for (const githubRef of refs.githubRefs) {
761
1177
  maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
762
1178
  }
@@ -776,6 +1192,7 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
776
1192
  return {
777
1193
  maintenanceRequest: maintenanceRequest,
778
1194
  maintenanceIssueRefs: [...maintenanceIssueRefs],
1195
+ maintenanceScmRefs: [...maintenanceScmRefMap.values()],
779
1196
  maintenanceGithubRefs: [...maintenanceGithubRefMap.values()],
780
1197
  requestFile: requestPath
781
1198
  };
@@ -1421,6 +1838,14 @@ function ensureInitFiles(root) {
1421
1838
  path: join(workflowsDir, "release-readiness.yaml"),
1422
1839
  contents: releaseWorkflowTemplate
1423
1840
  },
1841
+ {
1842
+ path: join(workflowsDir, "pipeline-evidence-review.yaml"),
1843
+ contents: pipelineWorkflowTemplate
1844
+ },
1845
+ {
1846
+ path: join(workflowsDir, "deployment-gate-review.yaml"),
1847
+ contents: deploymentGateWorkflowTemplate
1848
+ },
1424
1849
  {
1425
1850
  path: join(workflowsDir, "incident-handoff.yaml"),
1426
1851
  contents: incidentWorkflowTemplate