@h9-foundry/agentforge-cli 0.6.0 → 0.7.1
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/.tsbuildinfo +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +536 -11
- package/dist/index.js.map +1 -1
- package/dist/internal/builtin-agents.d.ts.map +1 -1
- package/dist/internal/builtin-agents.js +1866 -67
- package/dist/internal/builtin-agents.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
3
4
|
import yaml from "js-yaml";
|
|
4
5
|
import { renderAuditBundleMarkdown } from "@h9-foundry/agentforge-audit";
|
|
5
6
|
import { createWorkflowState, findWorkspaceRoot } from "@h9-foundry/agentforge-context-engine";
|
|
6
7
|
import { createPolicyEngine, loadPolicyDocument, resolvePolicy } from "@h9-foundry/agentforge-policy-engine";
|
|
7
8
|
import { runWorkflow } from "@h9-foundry/agentforge-runtime";
|
|
8
|
-
import { agentforgeConfigSchema, auditBundleSchema, designArtifactSchema, designRequestSchema, implementationRequestSchema, planningArtifactSchema, planningRequestSchema, qaRequestSchema, workflowDefinitionSchema } from "@h9-foundry/agentforge-schemas";
|
|
9
|
+
import { agentforgeConfigSchema, auditBundleSchema, designArtifactSchema, designRequestSchema, implementationRequestSchema, incidentRequestSchema, maintenanceRequestSchema, planningArtifactSchema, planningRequestSchema, qaRequestSchema, releaseRequestSchema, securityRequestSchema, workflowDefinitionSchema } from "@h9-foundry/agentforge-schemas";
|
|
9
10
|
import { createBuiltinAdapters } from "./internal/builtin-adapters.js";
|
|
10
11
|
import { createBuiltinAgentRegistry } from "./internal/builtin-agents.js";
|
|
11
12
|
import { LocalPluginRegistry } from "./internal/local-plugin-registry.js";
|
|
@@ -181,7 +182,7 @@ nodes:
|
|
|
181
182
|
`;
|
|
182
183
|
const qaWorkflowTemplate = `version: 1
|
|
183
184
|
name: qa-review
|
|
184
|
-
description: Validate a bounded QA request and
|
|
185
|
+
description: Validate a bounded QA request and synthesize a read-only QA report.
|
|
185
186
|
trigger: manual
|
|
186
187
|
catalog:
|
|
187
188
|
domain: test
|
|
@@ -193,6 +194,118 @@ nodes:
|
|
|
193
194
|
kind: deterministic
|
|
194
195
|
agent: qa-intake
|
|
195
196
|
outputs_to: agentResults.intake
|
|
197
|
+
- id: evidence
|
|
198
|
+
kind: deterministic
|
|
199
|
+
agent: qa-evidence-normalizer
|
|
200
|
+
outputs_to: agentResults.evidence
|
|
201
|
+
- id: qa
|
|
202
|
+
kind: reasoning
|
|
203
|
+
agent: qa-analyst
|
|
204
|
+
outputs_to: agentResults.qa
|
|
205
|
+
- id: report
|
|
206
|
+
kind: report
|
|
207
|
+
outputs_to: reports.final
|
|
208
|
+
`;
|
|
209
|
+
const securityWorkflowTemplate = `version: 1
|
|
210
|
+
name: security-review
|
|
211
|
+
description: Validate a bounded security request while preserving the default local security posture.
|
|
212
|
+
trigger: manual
|
|
213
|
+
catalog:
|
|
214
|
+
domain: security
|
|
215
|
+
supportLevel: official
|
|
216
|
+
maturity: mvp
|
|
217
|
+
trustScope: official-core-only
|
|
218
|
+
nodes:
|
|
219
|
+
- id: intake
|
|
220
|
+
kind: deterministic
|
|
221
|
+
agent: security-intake
|
|
222
|
+
outputs_to: agentResults.intake
|
|
223
|
+
- id: evidence
|
|
224
|
+
kind: deterministic
|
|
225
|
+
agent: security-evidence-normalizer
|
|
226
|
+
outputs_to: agentResults.evidence
|
|
227
|
+
- id: security
|
|
228
|
+
kind: reasoning
|
|
229
|
+
agent: security-analyst
|
|
230
|
+
outputs_to: agentResults.security
|
|
231
|
+
- id: report
|
|
232
|
+
kind: report
|
|
233
|
+
outputs_to: reports.final
|
|
234
|
+
`;
|
|
235
|
+
const releaseWorkflowTemplate = `version: 1
|
|
236
|
+
name: release-readiness
|
|
237
|
+
description: Validate a bounded release-readiness request while keeping trusted publish automation separate.
|
|
238
|
+
trigger: manual
|
|
239
|
+
catalog:
|
|
240
|
+
domain: release
|
|
241
|
+
supportLevel: partial
|
|
242
|
+
maturity: mvp
|
|
243
|
+
trustScope: official-core-only
|
|
244
|
+
nodes:
|
|
245
|
+
- id: intake
|
|
246
|
+
kind: deterministic
|
|
247
|
+
agent: release-intake
|
|
248
|
+
outputs_to: agentResults.intake
|
|
249
|
+
- id: evidence
|
|
250
|
+
kind: deterministic
|
|
251
|
+
agent: release-evidence-normalizer
|
|
252
|
+
outputs_to: agentResults.evidence
|
|
253
|
+
- id: release
|
|
254
|
+
kind: reasoning
|
|
255
|
+
agent: release-analyst
|
|
256
|
+
outputs_to: agentResults.release
|
|
257
|
+
- id: report
|
|
258
|
+
kind: report
|
|
259
|
+
outputs_to: reports.final
|
|
260
|
+
`;
|
|
261
|
+
const incidentWorkflowTemplate = `version: 1
|
|
262
|
+
name: incident-handoff
|
|
263
|
+
description: Validate staged incident evidence while keeping the default path local, read-only, and explicit.
|
|
264
|
+
trigger: manual
|
|
265
|
+
catalog:
|
|
266
|
+
domain: operate
|
|
267
|
+
supportLevel: partial
|
|
268
|
+
maturity: mvp
|
|
269
|
+
trustScope: official-core-only
|
|
270
|
+
nodes:
|
|
271
|
+
- id: intake
|
|
272
|
+
kind: deterministic
|
|
273
|
+
agent: incident-intake
|
|
274
|
+
outputs_to: agentResults.intake
|
|
275
|
+
- id: evidence
|
|
276
|
+
kind: deterministic
|
|
277
|
+
agent: incident-evidence-normalizer
|
|
278
|
+
outputs_to: agentResults.evidence
|
|
279
|
+
- id: incident
|
|
280
|
+
kind: reasoning
|
|
281
|
+
agent: incident-analyst
|
|
282
|
+
outputs_to: agentResults.incident
|
|
283
|
+
- id: report
|
|
284
|
+
kind: report
|
|
285
|
+
outputs_to: reports.final
|
|
286
|
+
`;
|
|
287
|
+
const maintenanceWorkflowTemplate = `version: 1
|
|
288
|
+
name: maintenance-triage
|
|
289
|
+
description: Validate a bounded maintenance request while keeping the default path local, read-only, and routing-oriented.
|
|
290
|
+
trigger: manual
|
|
291
|
+
catalog:
|
|
292
|
+
domain: maintain
|
|
293
|
+
supportLevel: partial
|
|
294
|
+
maturity: mvp
|
|
295
|
+
trustScope: official-core-only
|
|
296
|
+
nodes:
|
|
297
|
+
- id: intake
|
|
298
|
+
kind: deterministic
|
|
299
|
+
agent: maintenance-intake
|
|
300
|
+
outputs_to: agentResults.intake
|
|
301
|
+
- id: evidence
|
|
302
|
+
kind: deterministic
|
|
303
|
+
agent: maintenance-evidence-normalizer
|
|
304
|
+
outputs_to: agentResults.evidence
|
|
305
|
+
- id: maintenance
|
|
306
|
+
kind: reasoning
|
|
307
|
+
agent: maintenance-analyst
|
|
308
|
+
outputs_to: agentResults.maintenance
|
|
196
309
|
- id: report
|
|
197
310
|
kind: report
|
|
198
311
|
outputs_to: reports.final
|
|
@@ -206,6 +319,136 @@ function isRecord(value) {
|
|
|
206
319
|
function asArray(value) {
|
|
207
320
|
return Array.isArray(value) ? value : [];
|
|
208
321
|
}
|
|
322
|
+
function runGit(root, args) {
|
|
323
|
+
try {
|
|
324
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return "";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function parseGitHubRepositoryUrl(value) {
|
|
331
|
+
const trimmed = value.trim();
|
|
332
|
+
const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
333
|
+
if (sshMatch) {
|
|
334
|
+
return {
|
|
335
|
+
host: sshMatch[1].toLowerCase(),
|
|
336
|
+
owner: sshMatch[2],
|
|
337
|
+
repo: sshMatch[3]
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/)?$/i);
|
|
341
|
+
if (!httpsMatch) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
host: httpsMatch[1].toLowerCase(),
|
|
346
|
+
owner: httpsMatch[2],
|
|
347
|
+
repo: httpsMatch[3]
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function inferGitHubRepoContext(root) {
|
|
351
|
+
const packageJsonPath = join(root, "package.json");
|
|
352
|
+
if (existsSync(packageJsonPath)) {
|
|
353
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
354
|
+
if (isRecord(parsed)) {
|
|
355
|
+
const repository = parsed.repository;
|
|
356
|
+
if (typeof repository === "string") {
|
|
357
|
+
const context = parseGitHubRepositoryUrl(repository);
|
|
358
|
+
if (context) {
|
|
359
|
+
return context;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (isRecord(repository) && typeof repository.url === "string") {
|
|
363
|
+
const context = parseGitHubRepositoryUrl(repository.url);
|
|
364
|
+
if (context) {
|
|
365
|
+
return context;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const remoteUrl = runGit(root, ["config", "--get", "remote.origin.url"]);
|
|
371
|
+
return remoteUrl ? parseGitHubRepositoryUrl(remoteUrl) : undefined;
|
|
372
|
+
}
|
|
373
|
+
function normalizeGitHubReference(rawValue, repoContext) {
|
|
374
|
+
const raw = rawValue.trim();
|
|
375
|
+
if (!raw) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
const fromParts = (context, kind, number) => ({
|
|
379
|
+
platform: "github",
|
|
380
|
+
host: context.host,
|
|
381
|
+
owner: context.owner,
|
|
382
|
+
repo: context.repo,
|
|
383
|
+
kind,
|
|
384
|
+
number,
|
|
385
|
+
canonical: kind === "issue"
|
|
386
|
+
? `${context.owner}/${context.repo}#${number}`
|
|
387
|
+
: `${context.owner}/${context.repo}/pull/${number}`,
|
|
388
|
+
url: kind === "issue"
|
|
389
|
+
? `https://${context.host}/${context.owner}/${context.repo}/issues/${number}`
|
|
390
|
+
: `https://${context.host}/${context.owner}/${context.repo}/pull/${number}`,
|
|
391
|
+
source: raw
|
|
392
|
+
});
|
|
393
|
+
const urlMatch = raw.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)(?:\/)?$/i);
|
|
394
|
+
if (urlMatch) {
|
|
395
|
+
return fromParts({ host: urlMatch[1].toLowerCase(), owner: urlMatch[2], repo: urlMatch[3] }, urlMatch[4].toLowerCase() === "pull" ? "pull_request" : "issue", Number.parseInt(urlMatch[5], 10));
|
|
396
|
+
}
|
|
397
|
+
const repoIssueMatch = raw.match(/^([^/\s]+)\/([^#\s]+)#(\d+)$/);
|
|
398
|
+
if (repoIssueMatch) {
|
|
399
|
+
return fromParts({ host: repoContext?.host ?? "github.com", owner: repoIssueMatch[1], repo: repoIssueMatch[2] }, "issue", Number.parseInt(repoIssueMatch[3], 10));
|
|
400
|
+
}
|
|
401
|
+
const repoPullMatch = raw.match(/^([^/\s]+)\/([^/\s]+)\/pull\/(\d+)$/i);
|
|
402
|
+
if (repoPullMatch) {
|
|
403
|
+
return fromParts({ host: repoContext?.host ?? "github.com", owner: repoPullMatch[1], repo: repoPullMatch[2] }, "pull_request", Number.parseInt(repoPullMatch[3], 10));
|
|
404
|
+
}
|
|
405
|
+
const shortIssueMatch = raw.match(/^#(\d+)$/);
|
|
406
|
+
if (shortIssueMatch && repoContext) {
|
|
407
|
+
return fromParts(repoContext, "issue", Number.parseInt(shortIssueMatch[1], 10));
|
|
408
|
+
}
|
|
409
|
+
const shortPullMatch = raw.match(/^(?:PR|pr)\s*#(\d+)$/);
|
|
410
|
+
if (shortPullMatch && repoContext) {
|
|
411
|
+
return fromParts(repoContext, "pull_request", Number.parseInt(shortPullMatch[1], 10));
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
function normalizeGitHubReferences(rawValues, repoContext) {
|
|
416
|
+
const seen = new Set();
|
|
417
|
+
const normalized = [];
|
|
418
|
+
for (const rawValue of rawValues) {
|
|
419
|
+
const githubRef = normalizeGitHubReference(rawValue, repoContext);
|
|
420
|
+
if (!githubRef || seen.has(githubRef.canonical)) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
seen.add(githubRef.canonical);
|
|
424
|
+
normalized.push(githubRef);
|
|
425
|
+
}
|
|
426
|
+
return normalized;
|
|
427
|
+
}
|
|
428
|
+
export function mapWorkflowRunStatusToGitHubStatus(workflow, localRunStatus) {
|
|
429
|
+
if (localRunStatus === "success") {
|
|
430
|
+
return {
|
|
431
|
+
workflow,
|
|
432
|
+
localRunStatus,
|
|
433
|
+
githubStatus: "completed",
|
|
434
|
+
reason: "Successful local workflow runs map to completed GitHub handoff status."
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (localRunStatus === "partial") {
|
|
438
|
+
return {
|
|
439
|
+
workflow,
|
|
440
|
+
localRunStatus,
|
|
441
|
+
githubStatus: "blocked",
|
|
442
|
+
reason: "Partial local workflow runs map to blocked GitHub handoff status until follow-up work resolves them."
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
workflow,
|
|
447
|
+
localRunStatus,
|
|
448
|
+
githubStatus: "failed",
|
|
449
|
+
reason: "Failed local workflow runs map to failed GitHub handoff status."
|
|
450
|
+
};
|
|
451
|
+
}
|
|
209
452
|
function ensureReadablePath(policyEngine, pathValue, purpose) {
|
|
210
453
|
const decision = policyEngine.canReadPath(pathValue);
|
|
211
454
|
if (!decision.allowed) {
|
|
@@ -229,9 +472,31 @@ function validatePlanningRequestCompleteness(request) {
|
|
|
229
472
|
}
|
|
230
473
|
return request;
|
|
231
474
|
}
|
|
475
|
+
function validateIncidentRequestCompleteness(request) {
|
|
476
|
+
const evidenceSignalCount = request.evidenceSources.length + request.releaseReportRefs.length;
|
|
477
|
+
if (evidenceSignalCount === 0) {
|
|
478
|
+
throw new Error("Incident request is underspecified. Add at least one of evidenceSources or releaseReportRefs.");
|
|
479
|
+
}
|
|
480
|
+
return request;
|
|
481
|
+
}
|
|
482
|
+
function validateMaintenanceRequestCompleteness(request) {
|
|
483
|
+
const supportingSignalCount = request.dependencyAlertRefs.length +
|
|
484
|
+
request.docsTaskRefs.length +
|
|
485
|
+
request.releaseReportRefs.length +
|
|
486
|
+
request.issueRefs.length;
|
|
487
|
+
if (supportingSignalCount === 0) {
|
|
488
|
+
throw new Error("Maintenance request is underspecified. Add at least one of dependencyAlertRefs, docsTaskRefs, releaseReportRefs, or issueRefs.");
|
|
489
|
+
}
|
|
490
|
+
return request;
|
|
491
|
+
}
|
|
232
492
|
function validateWorkflowLifecyclePosture(workflow, policyEngine) {
|
|
233
493
|
const domain = workflow.catalog?.domain;
|
|
234
|
-
if (domain !== "plan" &&
|
|
494
|
+
if (domain !== "plan" &&
|
|
495
|
+
domain !== "design" &&
|
|
496
|
+
domain !== "build" &&
|
|
497
|
+
domain !== "security" &&
|
|
498
|
+
domain !== "release" &&
|
|
499
|
+
domain !== "operate") {
|
|
235
500
|
return;
|
|
236
501
|
}
|
|
237
502
|
if (policyEngine.snapshot.defaults.network !== "deny") {
|
|
@@ -265,6 +530,52 @@ function loadDesignBundleArtifact(root, designRecordRef) {
|
|
|
265
530
|
}
|
|
266
531
|
return designArtifactSchema.parse(designArtifact);
|
|
267
532
|
}
|
|
533
|
+
function ensureBundleContainsArtifactKind(root, bundleRef, artifactKind, purpose) {
|
|
534
|
+
const artifactKinds = loadLifecycleArtifactKinds(root, bundleRef);
|
|
535
|
+
if (!artifactKinds.includes(artifactKind)) {
|
|
536
|
+
throw new Error(`Referenced ${purpose} does not contain a ${artifactKind} artifact: ${bundleRef}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function validateReleaseRequestCompleteness(request) {
|
|
540
|
+
const evidenceSignalCount = request.qaReportRefs.length + request.securityReportRefs.length + request.evidenceSources.length;
|
|
541
|
+
if (evidenceSignalCount === 0) {
|
|
542
|
+
throw new Error("Release request is underspecified. Add at least one of qaReportRefs, securityReportRefs, or evidenceSources.");
|
|
543
|
+
}
|
|
544
|
+
return request;
|
|
545
|
+
}
|
|
546
|
+
function loadLifecycleArtifactKinds(root, bundleRef) {
|
|
547
|
+
const bundlePath = join(root, bundleRef);
|
|
548
|
+
if (!existsSync(bundlePath)) {
|
|
549
|
+
throw new Error(`Referenced bundle not found: ${bundleRef}`);
|
|
550
|
+
}
|
|
551
|
+
const bundle = auditBundleSchema.parse(JSON.parse(readFileSync(bundlePath, "utf8")));
|
|
552
|
+
return bundle.lifecycleArtifacts.map((artifact) => artifact.artifactKind);
|
|
553
|
+
}
|
|
554
|
+
function loadLifecycleArtifactSourceReferences(root, bundleRef) {
|
|
555
|
+
const bundlePath = join(root, bundleRef);
|
|
556
|
+
if (!existsSync(bundlePath)) {
|
|
557
|
+
throw new Error(`Referenced bundle not found: ${bundleRef}`);
|
|
558
|
+
}
|
|
559
|
+
const bundle = auditBundleSchema.parse(JSON.parse(readFileSync(bundlePath, "utf8")));
|
|
560
|
+
const repoContext = inferGitHubRepoContext(root);
|
|
561
|
+
const issueRefs = new Set();
|
|
562
|
+
const githubRefs = new Map();
|
|
563
|
+
for (const artifact of bundle.lifecycleArtifacts) {
|
|
564
|
+
for (const issueRef of artifact.source.issueRefs) {
|
|
565
|
+
issueRefs.add(issueRef);
|
|
566
|
+
}
|
|
567
|
+
for (const githubRef of artifact.source.githubRefs ?? []) {
|
|
568
|
+
githubRefs.set(githubRef.canonical, githubRef);
|
|
569
|
+
}
|
|
570
|
+
for (const githubRef of normalizeGitHubReferences(artifact.source.issueRefs, repoContext)) {
|
|
571
|
+
githubRefs.set(githubRef.canonical, githubRef);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
issueRefs: [...issueRefs],
|
|
576
|
+
githubRefs: [...githubRefs.values()]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
268
579
|
function prepareWorkflowInputs(workflow, root, policyEngine) {
|
|
269
580
|
const requestsDir = join(root, ".agentops", "requests");
|
|
270
581
|
ensureDirectory(requestsDir);
|
|
@@ -272,8 +583,10 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
|
|
|
272
583
|
const requestPath = ".agentops/requests/planning.yaml";
|
|
273
584
|
ensureReadablePath(policyEngine, requestPath, "planning request");
|
|
274
585
|
const planningRequest = validatePlanningRequestCompleteness(readYamlFile(join(root, requestPath), planningRequestSchema, "planning request"));
|
|
586
|
+
const planningGithubRefs = normalizeGitHubReferences(planningRequest.issueRefs, inferGitHubRepoContext(root));
|
|
275
587
|
return {
|
|
276
588
|
planningRequest,
|
|
589
|
+
planningGithubRefs,
|
|
277
590
|
requestFile: requestPath
|
|
278
591
|
};
|
|
279
592
|
}
|
|
@@ -306,11 +619,163 @@ function prepareWorkflowInputs(workflow, root, policyEngine) {
|
|
|
306
619
|
ensureReadablePath(policyEngine, requestPath, "QA request");
|
|
307
620
|
const qaRequest = readYamlFile(join(root, requestPath), qaRequestSchema, "QA request");
|
|
308
621
|
ensureReadablePath(policyEngine, qaRequest.targetRef, "QA target reference");
|
|
622
|
+
if (!existsSync(join(root, qaRequest.targetRef))) {
|
|
623
|
+
throw new Error(`QA target reference not found: ${qaRequest.targetRef}`);
|
|
624
|
+
}
|
|
309
625
|
for (const evidenceSource of qaRequest.evidenceSources) {
|
|
310
626
|
ensureReadablePath(policyEngine, evidenceSource, "QA evidence source");
|
|
311
627
|
}
|
|
628
|
+
const referencedSourceRefs = qaRequest.targetRef.endsWith("bundle.json")
|
|
629
|
+
? loadLifecycleArtifactSourceReferences(root, qaRequest.targetRef)
|
|
630
|
+
: { issueRefs: [], githubRefs: [] };
|
|
312
631
|
return {
|
|
313
632
|
qaRequest: qaRequest,
|
|
633
|
+
qaIssueRefs: referencedSourceRefs.issueRefs,
|
|
634
|
+
qaGithubRefs: referencedSourceRefs.githubRefs,
|
|
635
|
+
requestFile: requestPath
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (workflow.name === "security-review") {
|
|
639
|
+
const requestPath = ".agentops/requests/security.yaml";
|
|
640
|
+
ensureReadablePath(policyEngine, requestPath, "security request");
|
|
641
|
+
const securityRequest = readYamlFile(join(root, requestPath), securityRequestSchema, "security request");
|
|
642
|
+
ensureReadablePath(policyEngine, securityRequest.targetRef, "security target reference");
|
|
643
|
+
if (!existsSync(join(root, securityRequest.targetRef))) {
|
|
644
|
+
throw new Error(`Security target reference not found: ${securityRequest.targetRef}`);
|
|
645
|
+
}
|
|
646
|
+
for (const evidenceSource of securityRequest.evidenceSources) {
|
|
647
|
+
ensureReadablePath(policyEngine, evidenceSource, "security evidence source");
|
|
648
|
+
}
|
|
649
|
+
const referencedArtifactKinds = securityRequest.targetRef.endsWith("bundle.json")
|
|
650
|
+
? loadLifecycleArtifactKinds(root, securityRequest.targetRef)
|
|
651
|
+
: [];
|
|
652
|
+
const allowedSecurityTargets = new Set(["design-record", "implementation-proposal", "qa-report", "release-report"]);
|
|
653
|
+
if (securityRequest.targetRef.endsWith("bundle.json") && !referencedArtifactKinds.some((kind) => allowedSecurityTargets.has(kind))) {
|
|
654
|
+
throw new Error(`Referenced security bundle does not contain a supported lifecycle artifact: ${securityRequest.targetRef}`);
|
|
655
|
+
}
|
|
656
|
+
const referencedSourceRefs = securityRequest.targetRef.endsWith("bundle.json")
|
|
657
|
+
? loadLifecycleArtifactSourceReferences(root, securityRequest.targetRef)
|
|
658
|
+
: { issueRefs: [], githubRefs: [] };
|
|
659
|
+
return {
|
|
660
|
+
securityRequest: securityRequest,
|
|
661
|
+
securityTargetArtifactKinds: referencedArtifactKinds,
|
|
662
|
+
securityIssueRefs: referencedSourceRefs.issueRefs,
|
|
663
|
+
securityGithubRefs: referencedSourceRefs.githubRefs,
|
|
664
|
+
requestFile: requestPath
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (workflow.name === "release-readiness") {
|
|
668
|
+
const requestPath = ".agentops/requests/release.yaml";
|
|
669
|
+
ensureReadablePath(policyEngine, requestPath, "release request");
|
|
670
|
+
const releaseRequest = validateReleaseRequestCompleteness(readYamlFile(join(root, requestPath), releaseRequestSchema, "release request"));
|
|
671
|
+
const releaseIssueRefs = new Set();
|
|
672
|
+
const releaseGithubRefMap = new Map();
|
|
673
|
+
for (const qaReportRef of releaseRequest.qaReportRefs) {
|
|
674
|
+
ensureReadablePath(policyEngine, qaReportRef, "QA report reference");
|
|
675
|
+
ensureBundleContainsArtifactKind(root, qaReportRef, "qa-report", "QA report reference");
|
|
676
|
+
const refs = loadLifecycleArtifactSourceReferences(root, qaReportRef);
|
|
677
|
+
for (const issueRef of refs.issueRefs) {
|
|
678
|
+
releaseIssueRefs.add(issueRef);
|
|
679
|
+
}
|
|
680
|
+
for (const githubRef of refs.githubRefs) {
|
|
681
|
+
releaseGithubRefMap.set(githubRef.canonical, githubRef);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const securityReportRef of releaseRequest.securityReportRefs) {
|
|
685
|
+
ensureReadablePath(policyEngine, securityReportRef, "security report reference");
|
|
686
|
+
ensureBundleContainsArtifactKind(root, securityReportRef, "security-report", "security report reference");
|
|
687
|
+
const refs = loadLifecycleArtifactSourceReferences(root, securityReportRef);
|
|
688
|
+
for (const issueRef of refs.issueRefs) {
|
|
689
|
+
releaseIssueRefs.add(issueRef);
|
|
690
|
+
}
|
|
691
|
+
for (const githubRef of refs.githubRefs) {
|
|
692
|
+
releaseGithubRefMap.set(githubRef.canonical, githubRef);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
for (const evidenceSource of releaseRequest.evidenceSources) {
|
|
696
|
+
ensureReadablePath(policyEngine, evidenceSource, "release evidence source");
|
|
697
|
+
if (!existsSync(join(root, evidenceSource))) {
|
|
698
|
+
throw new Error(`Release evidence source not found: ${evidenceSource}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
releaseRequest: releaseRequest,
|
|
703
|
+
releaseIssueRefs: [...releaseIssueRefs],
|
|
704
|
+
releaseGithubRefs: [...releaseGithubRefMap.values()],
|
|
705
|
+
requestFile: requestPath
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
if (workflow.name === "incident-handoff") {
|
|
709
|
+
const requestPath = ".agentops/requests/incident.yaml";
|
|
710
|
+
ensureReadablePath(policyEngine, requestPath, "incident request");
|
|
711
|
+
const incidentRequest = validateIncidentRequestCompleteness(readYamlFile(join(root, requestPath), incidentRequestSchema, "incident request"));
|
|
712
|
+
const repoContext = inferGitHubRepoContext(root);
|
|
713
|
+
const incidentIssueRefs = new Set(incidentRequest.issueRefs);
|
|
714
|
+
const incidentGithubRefMap = new Map();
|
|
715
|
+
for (const githubRef of normalizeGitHubReferences(incidentRequest.issueRefs, repoContext)) {
|
|
716
|
+
incidentGithubRefMap.set(githubRef.canonical, githubRef);
|
|
717
|
+
}
|
|
718
|
+
for (const releaseReportRef of incidentRequest.releaseReportRefs) {
|
|
719
|
+
ensureReadablePath(policyEngine, releaseReportRef, "release report reference");
|
|
720
|
+
ensureBundleContainsArtifactKind(root, releaseReportRef, "release-report", "release report reference");
|
|
721
|
+
const refs = loadLifecycleArtifactSourceReferences(root, releaseReportRef);
|
|
722
|
+
for (const issueRef of refs.issueRefs) {
|
|
723
|
+
incidentIssueRefs.add(issueRef);
|
|
724
|
+
}
|
|
725
|
+
for (const githubRef of refs.githubRefs) {
|
|
726
|
+
incidentGithubRefMap.set(githubRef.canonical, githubRef);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
for (const evidenceSource of incidentRequest.evidenceSources) {
|
|
730
|
+
ensureReadablePath(policyEngine, evidenceSource, "incident evidence source");
|
|
731
|
+
if (!existsSync(join(root, evidenceSource))) {
|
|
732
|
+
throw new Error(`Incident evidence source not found: ${evidenceSource}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
incidentRequest: incidentRequest,
|
|
737
|
+
incidentIssueRefs: [...incidentIssueRefs],
|
|
738
|
+
incidentGithubRefs: [...incidentGithubRefMap.values()],
|
|
739
|
+
requestFile: requestPath
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (workflow.name === "maintenance-triage") {
|
|
743
|
+
const requestPath = ".agentops/requests/maintenance.yaml";
|
|
744
|
+
ensureReadablePath(policyEngine, requestPath, "maintenance request");
|
|
745
|
+
const maintenanceRequest = validateMaintenanceRequestCompleteness(readYamlFile(join(root, requestPath), maintenanceRequestSchema, "maintenance request"));
|
|
746
|
+
const repoContext = inferGitHubRepoContext(root);
|
|
747
|
+
const maintenanceIssueRefs = new Set(maintenanceRequest.issueRefs);
|
|
748
|
+
const maintenanceGithubRefMap = new Map();
|
|
749
|
+
for (const githubRef of normalizeGitHubReferences(maintenanceRequest.issueRefs, repoContext)) {
|
|
750
|
+
maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
|
|
751
|
+
}
|
|
752
|
+
for (const releaseReportRef of maintenanceRequest.releaseReportRefs) {
|
|
753
|
+
ensureReadablePath(policyEngine, releaseReportRef, "release report reference");
|
|
754
|
+
ensureBundleContainsArtifactKind(root, releaseReportRef, "release-report", "release report reference");
|
|
755
|
+
const refs = loadLifecycleArtifactSourceReferences(root, releaseReportRef);
|
|
756
|
+
for (const issueRef of refs.issueRefs) {
|
|
757
|
+
maintenanceIssueRefs.add(issueRef);
|
|
758
|
+
}
|
|
759
|
+
for (const githubRef of refs.githubRefs) {
|
|
760
|
+
maintenanceGithubRefMap.set(githubRef.canonical, githubRef);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
for (const dependencyAlertRef of maintenanceRequest.dependencyAlertRefs) {
|
|
764
|
+
ensureReadablePath(policyEngine, dependencyAlertRef, "dependency alert reference");
|
|
765
|
+
if (!existsSync(join(root, dependencyAlertRef))) {
|
|
766
|
+
throw new Error(`Dependency alert reference not found: ${dependencyAlertRef}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const docsTaskRef of maintenanceRequest.docsTaskRefs) {
|
|
770
|
+
ensureReadablePath(policyEngine, docsTaskRef, "docs task reference");
|
|
771
|
+
if (!existsSync(join(root, docsTaskRef))) {
|
|
772
|
+
throw new Error(`Docs task reference not found: ${docsTaskRef}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
maintenanceRequest: maintenanceRequest,
|
|
777
|
+
maintenanceIssueRefs: [...maintenanceIssueRefs],
|
|
778
|
+
maintenanceGithubRefs: [...maintenanceGithubRefMap.values()],
|
|
314
779
|
requestFile: requestPath
|
|
315
780
|
};
|
|
316
781
|
}
|
|
@@ -383,6 +848,51 @@ function normalizeAgentForgeConfigInput(value) {
|
|
|
383
848
|
}
|
|
384
849
|
};
|
|
385
850
|
}
|
|
851
|
+
function readLatestCompleteRunBundle(runsRoot) {
|
|
852
|
+
if (!existsSync(runsRoot)) {
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
const parseRunTimestampMs = (value) => {
|
|
856
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
const parsedDate = Date.parse(value);
|
|
860
|
+
if (!Number.isNaN(parsedDate)) {
|
|
861
|
+
return parsedDate;
|
|
862
|
+
}
|
|
863
|
+
const timestampPrefix = Number.parseInt(value.split("-")[0] ?? "", 10);
|
|
864
|
+
return Number.isNaN(timestampPrefix) ? undefined : timestampPrefix;
|
|
865
|
+
};
|
|
866
|
+
const candidates = readdirSync(runsRoot)
|
|
867
|
+
.map((entry) => {
|
|
868
|
+
const bundlePath = join(runsRoot, entry, "bundle.json");
|
|
869
|
+
if (!existsSync(bundlePath)) {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
const stats = statSync(bundlePath);
|
|
873
|
+
const bundle = JSON.parse(readFileSync(bundlePath, "utf8"));
|
|
874
|
+
const bundleRunId = typeof bundle.runId === "string" ? bundle.runId : entry;
|
|
875
|
+
const completedAtMs = parseRunTimestampMs(bundle.finishedAt) ??
|
|
876
|
+
parseRunTimestampMs(bundle.startedAt) ??
|
|
877
|
+
parseRunTimestampMs(bundleRunId) ??
|
|
878
|
+
parseRunTimestampMs(entry) ??
|
|
879
|
+
stats.mtimeMs;
|
|
880
|
+
return {
|
|
881
|
+
runDir: entry,
|
|
882
|
+
bundle,
|
|
883
|
+
bundleRunId,
|
|
884
|
+
completedAtMs
|
|
885
|
+
};
|
|
886
|
+
})
|
|
887
|
+
.filter((candidate) => Boolean(candidate))
|
|
888
|
+
.sort((left, right) => {
|
|
889
|
+
if (left.completedAtMs !== right.completedAtMs) {
|
|
890
|
+
return right.completedAtMs - left.completedAtMs;
|
|
891
|
+
}
|
|
892
|
+
return right.bundleRunId.localeCompare(left.bundleRunId);
|
|
893
|
+
});
|
|
894
|
+
return candidates[0] ? { runDir: candidates[0].runDir, bundle: candidates[0].bundle } : undefined;
|
|
895
|
+
}
|
|
386
896
|
function loadAgentForgeConfig(root) {
|
|
387
897
|
const configPath = join(root, ".agentops", "agentops.yaml");
|
|
388
898
|
if (!existsSync(configPath)) {
|
|
@@ -446,6 +956,22 @@ function ensureInitFiles(root) {
|
|
|
446
956
|
{
|
|
447
957
|
path: join(workflowsDir, "qa-review.yaml"),
|
|
448
958
|
contents: qaWorkflowTemplate
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
path: join(workflowsDir, "security-review.yaml"),
|
|
962
|
+
contents: securityWorkflowTemplate
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
path: join(workflowsDir, "release-readiness.yaml"),
|
|
966
|
+
contents: releaseWorkflowTemplate
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
path: join(workflowsDir, "incident-handoff.yaml"),
|
|
970
|
+
contents: incidentWorkflowTemplate
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
path: join(workflowsDir, "maintenance-triage.yaml"),
|
|
974
|
+
contents: maintenanceWorkflowTemplate
|
|
449
975
|
}
|
|
450
976
|
];
|
|
451
977
|
for (const file of files) {
|
|
@@ -599,18 +1125,17 @@ export function explainLastRun(cwd = process.cwd()) {
|
|
|
599
1125
|
const root = findWorkspaceRoot(cwd);
|
|
600
1126
|
const config = loadAgentForgeConfig(root);
|
|
601
1127
|
const runsRoot = join(root, config.runtime.runsPath);
|
|
602
|
-
const
|
|
603
|
-
const latest = entries.at(-1);
|
|
1128
|
+
const latest = readLatestCompleteRunBundle(runsRoot);
|
|
604
1129
|
if (!latest) {
|
|
605
|
-
throw new Error("No recorded runs found.");
|
|
1130
|
+
throw new Error("No complete recorded runs found.");
|
|
606
1131
|
}
|
|
607
|
-
const bundle =
|
|
1132
|
+
const bundle = latest.bundle;
|
|
608
1133
|
const findings = asArray(bundle.findings);
|
|
609
1134
|
const blockedPlugins = asArray(bundle.blockedPlugins);
|
|
610
1135
|
const lifecycleArtifacts = asArray(bundle.lifecycleArtifacts);
|
|
611
1136
|
const runEntries = asArray(bundle.entries);
|
|
612
1137
|
return {
|
|
613
|
-
runId: typeof bundle.runId === "string" ? bundle.runId : latest,
|
|
1138
|
+
runId: typeof bundle.runId === "string" ? bundle.runId : latest.runDir,
|
|
614
1139
|
status: typeof bundle.status === "string" ? bundle.status : "unknown",
|
|
615
1140
|
findings: findings.length,
|
|
616
1141
|
blockedActions: runEntries.reduce((total, entry) => {
|
|
@@ -620,8 +1145,8 @@ export function explainLastRun(cwd = process.cwd()) {
|
|
|
620
1145
|
return total + asArray(entry.blockedActions).length;
|
|
621
1146
|
}, 0),
|
|
622
1147
|
blockedPlugins: blockedPlugins.length,
|
|
623
|
-
jsonPath: join(runsRoot, latest, "bundle.json"),
|
|
624
|
-
markdownPath: join(runsRoot, latest, "summary.md"),
|
|
1148
|
+
jsonPath: join(runsRoot, latest.runDir, "bundle.json"),
|
|
1149
|
+
markdownPath: join(runsRoot, latest.runDir, "summary.md"),
|
|
625
1150
|
artifactCount: lifecycleArtifacts.length,
|
|
626
1151
|
artifactKinds: lifecycleArtifacts
|
|
627
1152
|
.map((artifact) => (isRecord(artifact) && typeof artifact.artifactKind === "string" ? artifact.artifactKind : undefined))
|