@h9-foundry/agentforge-cli 0.8.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,12 +6,13 @@ 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";
13
13
  export { checkReleaseReadiness, getReleaseGuide, renderReleaseGuide, TARGET_NPM_SCOPE, EXPECTED_PUBLIC_PACKAGES } from "./internal/release-preflight.js";
14
14
  export { verifyReleaseArtifacts } from "./internal/release-verification.js";
15
+ export const startupPresetNames = ["planning-discovery"];
15
16
  const agentforgeConfigTemplate = `version: 1
16
17
  project:
17
18
  name: REPO_NAME
@@ -238,7 +239,7 @@ description: Validate a bounded release-readiness request while keeping trusted
238
239
  trigger: manual
239
240
  catalog:
240
241
  domain: release
241
- supportLevel: partial
242
+ supportLevel: official
242
243
  maturity: mvp
243
244
  trustScope: official-core-only
244
245
  nodes:
@@ -258,6 +259,58 @@ nodes:
258
259
  kind: report
259
260
  outputs_to: reports.final
260
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
+ `;
261
314
  const incidentWorkflowTemplate = `version: 1
262
315
  name: incident-handoff
263
316
  description: Validate staged incident evidence while keeping the default path local, read-only, and explicit.
@@ -327,24 +380,90 @@ function runGit(root, args) {
327
380
  return "";
328
381
  }
329
382
  }
330
- 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) {
331
394
  const trimmed = value.trim();
332
- const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i);
395
+ const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\.git)?$/i);
333
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
+ }
334
413
  return {
335
- host: sshMatch[1].toLowerCase(),
336
- owner: sshMatch[2],
337
- repo: sshMatch[3]
414
+ platform,
415
+ host,
416
+ namespace,
417
+ repo
338
418
  };
339
419
  }
340
- const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/)?$/i);
420
+ const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?(?:\/)?$/i);
341
421
  if (!httpsMatch) {
342
422
  return undefined;
343
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
+ }
452
+ return {
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
+ }
344
463
  return {
345
- host: httpsMatch[1].toLowerCase(),
346
- owner: httpsMatch[2],
347
- repo: httpsMatch[3]
464
+ host: parsed.host,
465
+ namespace: parsed.namespace,
466
+ repo: parsed.repo
348
467
  };
349
468
  }
350
469
  function inferGitHubRepoContext(root) {
@@ -370,6 +489,60 @@ function inferGitHubRepoContext(root) {
370
489
  const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
371
490
  return remoteUrl ? parseGitHubRepositoryUrl(remoteUrl) : undefined;
372
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
+ }
373
546
  function normalizeGitHubReference(rawValue, repoContext) {
374
547
  const raw = rawValue.trim();
375
548
  if (!raw) {
@@ -425,6 +598,86 @@ function normalizeGitHubReferences(rawValues, repoContext) {
425
598
  }
426
599
  return normalized;
427
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
+ }
428
681
  export function mapWorkflowRunStatusToGitHubStatus(workflow, localRunStatus) {
429
682
  if (localRunStatus === "success") {
430
683
  return {
@@ -543,6 +796,35 @@ function validateReleaseRequestCompleteness(request) {
543
796
  }
544
797
  return request;
545
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
+ }
546
828
  function loadLifecycleArtifactKinds(root, bundleRef) {
547
829
  const bundlePath = join(root, bundleRef);
548
830
  if (!existsSync(bundlePath)) {
@@ -557,22 +839,31 @@ function loadLifecycleArtifactSourceReferences(root, bundleRef) {
557
839
  throw new Error(`Referenced bundle not found: ${bundleRef}`);
558
840
  }
559
841
  const bundle = auditBundleSchema.parse(JSON.parse(readFileSync(bundlePath, "utf8")));
560
- const repoContext = inferGitHubRepoContext(root);
842
+ const scmRepoContext = inferScmRepoContext(root);
843
+ const gitHubRepoContext = inferGitHubRepoContext(root);
561
844
  const issueRefs = new Set();
845
+ const scmRefs = new Map();
562
846
  const githubRefs = new Map();
563
847
  for (const artifact of bundle.lifecycleArtifacts) {
564
848
  for (const issueRef of artifact.source.issueRefs) {
565
849
  issueRefs.add(issueRef);
566
850
  }
851
+ for (const scmRef of artifact.source.scmRefs ?? []) {
852
+ scmRefs.set(scmRef.canonical, scmRef);
853
+ }
567
854
  for (const githubRef of artifact.source.githubRefs ?? []) {
568
855
  githubRefs.set(githubRef.canonical, githubRef);
569
856
  }
570
- 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)) {
571
861
  githubRefs.set(githubRef.canonical, githubRef);
572
862
  }
573
863
  }
574
864
  return {
575
865
  issueRefs: [...issueRefs],
866
+ scmRefs: [...scmRefs.values()],
576
867
  githubRefs: [...githubRefs.values()]
577
868
  };
578
869
  }
@@ -583,9 +874,11 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
583
874
  const requestPath = ".agentops/requests/planning.yaml";
584
875
  ensureReadablePath(policyEngine, requestPath, "planning request");
585
876
  const planningRequest = validatePlanningRequestCompleteness(readYamlFile(join(root, requestPath), planningRequestSchema, "planning request"));
877
+ const planningScmRefs = normalizeScmReferences(planningRequest.issueRefs, inferScmRepoContext(root));
586
878
  const planningGithubRefs = normalizeGitHubReferences(planningRequest.issueRefs, inferGitHubRepoContext(root));
587
879
  return {
588
880
  planningRequest,
881
+ planningScmRefs,
589
882
  planningGithubRefs,
590
883
  requestFile: requestPath
591
884
  };
@@ -627,10 +920,11 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
627
920
  }
628
921
  const referencedSourceRefs = qaRequest.targetRef.endsWith("bundle.json")
629
922
  ? loadLifecycleArtifactSourceReferences(root, qaRequest.targetRef)
630
- : { issueRefs: [], githubRefs: [] };
923
+ : { issueRefs: [], scmRefs: [], githubRefs: [] };
631
924
  return {
632
925
  qaRequest: qaRequest,
633
926
  qaIssueRefs: referencedSourceRefs.issueRefs,
927
+ qaScmRefs: referencedSourceRefs.scmRefs,
634
928
  qaGithubRefs: referencedSourceRefs.githubRefs,
635
929
  requestFile: requestPath
636
930
  };
@@ -655,20 +949,68 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
655
949
  }
656
950
  const referencedSourceRefs = securityRequest.targetRef.endsWith("bundle.json")
657
951
  ? loadLifecycleArtifactSourceReferences(root, securityRequest.targetRef)
658
- : { issueRefs: [], githubRefs: [] };
952
+ : { issueRefs: [], scmRefs: [], githubRefs: [] };
659
953
  return {
660
954
  securityRequest: securityRequest,
661
955
  securityTargetArtifactKinds: referencedArtifactKinds,
662
956
  securityIssueRefs: referencedSourceRefs.issueRefs,
957
+ securityScmRefs: referencedSourceRefs.scmRefs,
663
958
  securityGithubRefs: referencedSourceRefs.githubRefs,
664
959
  requestFile: requestPath
665
960
  };
666
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
+ }
667
1008
  if (workflow.name === "release-readiness") {
668
1009
  const requestPath = ".agentops/requests/release.yaml";
669
1010
  ensureReadablePath(policyEngine, requestPath, "release request");
670
1011
  const releaseRequest = validateReleaseRequestCompleteness(readYamlFile(join(root, requestPath), releaseRequestSchema, "release request"));
671
1012
  const releaseIssueRefs = new Set();
1013
+ const releaseScmRefMap = new Map();
672
1014
  const releaseGithubRefMap = new Map();
673
1015
  for (const qaReportRef of releaseRequest.qaReportRefs) {
674
1016
  ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
@@ -677,6 +1019,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
677
1019
  for (const issueRef of refs.issueRefs) {
678
1020
  releaseIssueRefs.add(issueRef);
679
1021
  }
1022
+ for (const scmRef of refs.scmRefs) {
1023
+ releaseScmRefMap.set(scmRef.canonical, scmRef);
1024
+ }
680
1025
  for (const githubRef of refs.githubRefs) {
681
1026
  releaseGithubRefMap.set(githubRef.canonical, githubRef);
682
1027
  }
@@ -688,6 +1033,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
688
1033
  for (const issueRef of refs.issueRefs) {
689
1034
  releaseIssueRefs.add(issueRef);
690
1035
  }
1036
+ for (const scmRef of refs.scmRefs) {
1037
+ releaseScmRefMap.set(scmRef.canonical, scmRef);
1038
+ }
691
1039
  for (const githubRef of refs.githubRefs) {
692
1040
  releaseGithubRefMap.set(githubRef.canonical, githubRef);
693
1041
  }
@@ -701,18 +1049,75 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
701
1049
  return {
702
1050
  releaseRequest: releaseRequest,
703
1051
  releaseIssueRefs: [...releaseIssueRefs],
1052
+ releaseScmRefs: [...releaseScmRefMap.values()],
704
1053
  releaseGithubRefs: [...releaseGithubRefMap.values()],
705
1054
  requestFile: requestPath
706
1055
  };
707
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
+ }
708
1108
  if (workflow.name === "incident-handoff") {
709
1109
  const requestPath = ".agentops/requests/incident.yaml";
710
1110
  ensureReadablePath(policyEngine, requestPath, "incident request");
711
1111
  const incidentRequest = validateIncidentRequestCompleteness(readYamlFile(join(root, requestPath), incidentRequestSchema, "incident request"));
712
- const repoContext = inferGitHubRepoContext(root);
1112
+ const scmRepoContext = inferScmRepoContext(root);
1113
+ const gitHubRepoContext = inferGitHubRepoContext(root);
713
1114
  const incidentIssueRefs = new Set(incidentRequest.issueRefs);
1115
+ const incidentScmRefMap = new Map();
714
1116
  const incidentGithubRefMap = new Map();
715
- 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)) {
716
1121
  incidentGithubRefMap.set(githubRef.canonical, githubRef);
717
1122
  }
718
1123
  for (const releaseReportRef of incidentRequest.releaseReportRefs) {
@@ -722,6 +1127,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
722
1127
  for (const issueRef of refs.issueRefs) {
723
1128
  incidentIssueRefs.add(issueRef);
724
1129
  }
1130
+ for (const scmRef of refs.scmRefs) {
1131
+ incidentScmRefMap.set(scmRef.canonical, scmRef);
1132
+ }
725
1133
  for (const githubRef of refs.githubRefs) {
726
1134
  incidentGithubRefMap.set(githubRef.canonical, githubRef);
727
1135
  }
@@ -735,6 +1143,7 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
735
1143
  return {
736
1144
  incidentRequest: incidentRequest,
737
1145
  incidentIssueRefs: [...incidentIssueRefs],
1146
+ incidentScmRefs: [...incidentScmRefMap.values()],
738
1147
  incidentGithubRefs: [...incidentGithubRefMap.values()],
739
1148
  requestFile: requestPath
740
1149
  };
@@ -743,10 +1152,15 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
743
1152
  const requestPath = ".agentops/requests/maintenance.yaml";
744
1153
  ensureReadablePath(policyEngine, requestPath, "maintenance request");
745
1154
  const maintenanceRequest = validateMaintenanceRequestCompleteness(readYamlFile(join(root, requestPath), maintenanceRequestSchema, "maintenance request"));
746
- const repoContext = inferGitHubRepoContext(root);
1155
+ const scmRepoContext = inferScmRepoContext(root);
1156
+ const gitHubRepoContext = inferGitHubRepoContext(root);
747
1157
  const maintenanceIssueRefs = new Set(maintenanceRequest.issueRefs);
1158
+ const maintenanceScmRefMap = new Map();
748
1159
  const maintenanceGithubRefMap = new Map();
749
- 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)) {
750
1164
  maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
751
1165
  }
752
1166
  for (const releaseReportRef of maintenanceRequest.releaseReportRefs) {
@@ -756,6 +1170,9 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
756
1170
  for (const issueRef of refs.issueRefs) {
757
1171
  maintenanceIssueRefs.add(issueRef);
758
1172
  }
1173
+ for (const scmRef of refs.scmRefs) {
1174
+ maintenanceScmRefMap.set(scmRef.canonical, scmRef);
1175
+ }
759
1176
  for (const githubRef of refs.githubRefs) {
760
1177
  maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
761
1178
  }
@@ -775,6 +1192,7 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
775
1192
  return {
776
1193
  maintenanceRequest: maintenanceRequest,
777
1194
  maintenanceIssueRefs: [...maintenanceIssueRefs],
1195
+ maintenanceScmRefs: [...maintenanceScmRefMap.values()],
778
1196
  maintenanceGithubRefs: [...maintenanceGithubRefMap.values()],
779
1197
  requestFile: requestPath
780
1198
  };
@@ -1420,6 +1838,14 @@ function ensureInitFiles(root) {
1420
1838
  path: join(workflowsDir, "release-readiness.yaml"),
1421
1839
  contents: releaseWorkflowTemplate
1422
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
+ },
1423
1849
  {
1424
1850
  path: join(workflowsDir, "incident-handoff.yaml"),
1425
1851
  contents: incidentWorkflowTemplate
@@ -1500,10 +1926,45 @@ function validateWorkflowAgents(workflow, agents, blockedPlugins) {
1500
1926
  throw new Error(`Workflow agent is not registered: ${node.agent}`);
1501
1927
  }
1502
1928
  }
1503
- export function initProject(cwd = process.cwd()) {
1929
+ function createPlanningDiscoveryPresetRequest(root) {
1930
+ const repoName = root.split("/").at(-1) ?? "this repository";
1931
+ const pathHints = ["README.md", "package.json", "src", "docs"].filter((pathHint) => existsSync(join(root, pathHint)));
1932
+ return planningRequestSchema.parse({
1933
+ problemStatement: `Plan the next safe local-first improvement for ${repoName}.`,
1934
+ goals: ["Produce one planning brief artifact", "Identify a bounded next step before opening a pull request"],
1935
+ constraints: ["Keep the default path local-first and read-only", "Prefer a small, reviewable next change"],
1936
+ pathHints,
1937
+ assumptions: ["This preset is a starter request that can be edited after initialization if the repository needs different focus."]
1938
+ });
1939
+ }
1940
+ function applyStartupPreset(root, preset) {
1941
+ const requestsRoot = join(root, ".agentops", "requests");
1942
+ ensureDirectory(requestsRoot);
1943
+ switch (preset) {
1944
+ case "planning-discovery": {
1945
+ const requestPath = join(requestsRoot, "planning.yaml");
1946
+ const created = !existsSync(requestPath);
1947
+ if (created) {
1948
+ writeYamlFile(requestPath, createPlanningDiscoveryPresetRequest(root));
1949
+ }
1950
+ return {
1951
+ preset,
1952
+ workflow: "planning-discovery",
1953
+ requestPath,
1954
+ created
1955
+ };
1956
+ }
1957
+ }
1958
+ }
1959
+ export function initProject(cwd = process.cwd(), options) {
1504
1960
  const root = findWorkspaceRoot(cwd);
1505
1961
  const created = ensureInitFiles(root);
1506
- return { root, created };
1962
+ const preset = options?.preset ? applyStartupPreset(root, options.preset) : undefined;
1963
+ return {
1964
+ root,
1965
+ created,
1966
+ ...(preset ? { preset } : {})
1967
+ };
1507
1968
  }
1508
1969
  export function scanProject(cwd = process.cwd()) {
1509
1970
  const root = findWorkspaceRoot(cwd);