@open-agent-toolkit/cli 0.1.37 → 0.1.38
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/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-review-provide/SKILL.md +2 -2
- package/assets/skills/oat-review-provide/references/review-artifact-template.md +9 -3
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +110 -6
- package/dist/commands/gate/review-verdict.d.ts +9 -1
- package/dist/commands/gate/review-verdict.d.ts.map +1 -1
- package/dist/commands/gate/review-verdict.js +153 -16
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-review-provide
|
|
3
|
-
version: 1.2.
|
|
3
|
+
version: 1.2.2
|
|
4
4
|
description: Use when you need an ad-hoc review outside an active OAT project lifecycle. Reviews code or artifacts without project phase state, unlike oat-project-review-provide.
|
|
5
5
|
argument-hint: '[unstaged|staged|base_branch=<branch>|base_sha=<sha>|<sha1>..<sha2>|--files <path1,path2,...>] [--output <path>] [--mode auto|local|tracked|inline]'
|
|
6
6
|
disable-model-invocation: true
|
|
@@ -183,7 +183,7 @@ If the file already exists (same scope reviewed twice in one day), suffix with `
|
|
|
183
183
|
|
|
184
184
|
Use the same severity model and checklist as project reviews:
|
|
185
185
|
|
|
186
|
-
- Critical / Important / Minor findings
|
|
186
|
+
- Critical / Important / Medium / Minor findings
|
|
187
187
|
- file:line references
|
|
188
188
|
- actionable fix guidance
|
|
189
189
|
- verification commands
|
|
@@ -23,19 +23,25 @@ oat_review_mode: ad_hoc
|
|
|
23
23
|
|
|
24
24
|
{2-3 sentence summary}
|
|
25
25
|
|
|
26
|
+
Findings: {N} critical, {N} important, {N} medium, {N} minor
|
|
27
|
+
|
|
26
28
|
## Findings
|
|
27
29
|
|
|
28
30
|
### Critical
|
|
29
31
|
|
|
30
|
-
{
|
|
32
|
+
{If none: "None"}
|
|
31
33
|
|
|
32
34
|
### Important
|
|
33
35
|
|
|
34
|
-
{
|
|
36
|
+
{If none: "None"}
|
|
37
|
+
|
|
38
|
+
### Medium
|
|
39
|
+
|
|
40
|
+
{If none: "None"}
|
|
35
41
|
|
|
36
42
|
### Minor
|
|
37
43
|
|
|
38
|
-
{
|
|
44
|
+
{If none: "None"}
|
|
39
45
|
|
|
40
46
|
## Verification Commands
|
|
41
47
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,EAQL,KAAK,UAAU,EAIf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,EAQL,KAAK,UAAU,EAIf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,UAAU,EAAE,CACV,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AA0CD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,EAAE,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAC;IACvD,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC7B;AAED,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,kBAAkB,GAAG,cAAc,GAAG,MAAM,CAAC;AAMlD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAyZD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,EAC9C,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,GAAG,IAAI,CAE3B;AAo6BD,wBAAgB,iBAAiB,CAC/B,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwKT"}
|
|
@@ -10,7 +10,7 @@ import { resolveEffectiveConfig, resolveExecTargets, resolveGate, } from '../../
|
|
|
10
10
|
import { dirExists, fileExists } from '../../fs/io.js';
|
|
11
11
|
import { normalizeToPosixPath, resolveProjectRoot } from '../../fs/paths.js';
|
|
12
12
|
import { Command } from 'commander';
|
|
13
|
-
import { parseReviewGateVerdict, } from './review-verdict.js';
|
|
13
|
+
import { parseReviewGateVerdict, severityDisplayName, } from './review-verdict.js';
|
|
14
14
|
const DEFAULT_DEPENDENCIES = {
|
|
15
15
|
buildCommandContext,
|
|
16
16
|
resolveProjectRoot,
|
|
@@ -40,7 +40,7 @@ const VALID_REVIEW_GATE_THRESHOLDS = [
|
|
|
40
40
|
'medium',
|
|
41
41
|
'minor',
|
|
42
42
|
];
|
|
43
|
-
const REVIEW_GATE_CONTEXT_NOTE = 'This review is gate-originated. If you run `oat-project-review-provide`, set `oat_review_invocation: gate` in the review artifact.';
|
|
43
|
+
const REVIEW_GATE_CONTEXT_NOTE = 'This review is gate-originated. If you run `oat-project-review-provide`, set `oat_review_invocation: gate` in the review artifact. Write a canonical review artifact with `### Critical`, `### Important`, `### Medium`, and `### Minor` headings in that order, using `None` for empty sections.';
|
|
44
44
|
function reviewGateProjectContext(projectPath) {
|
|
45
45
|
return `Resolved OAT project path: ${projectPath}. Run the review for this project path.`;
|
|
46
46
|
}
|
|
@@ -577,15 +577,83 @@ function reviewBlocksAtThreshold(verdict, threshold) {
|
|
|
577
577
|
}
|
|
578
578
|
return verdict.counts.minor > 0;
|
|
579
579
|
}
|
|
580
|
+
function buildReviewGateHandoff(options) {
|
|
581
|
+
const receiveInstruction = `Run oat-project-review-receive for ${options.artifactPath} before treating this gate review as consumed.`;
|
|
582
|
+
const thresholdIndex = VALID_REVIEW_GATE_THRESHOLDS.indexOf(options.threshold);
|
|
583
|
+
const nonBlockingSeverities = VALID_REVIEW_GATE_THRESHOLDS.slice(thresholdIndex + 1).filter((severity) => options.verdict.counts[severity] > 0);
|
|
584
|
+
const hasFinalNonBlockingFindings = options.verdict.scope === 'final' &&
|
|
585
|
+
!options.blocking &&
|
|
586
|
+
nonBlockingSeverities.length > 0;
|
|
587
|
+
if (!hasFinalNonBlockingFindings) {
|
|
588
|
+
return receiveInstruction;
|
|
589
|
+
}
|
|
590
|
+
const counts = nonBlockingSeverities
|
|
591
|
+
.map((severity) => `${severity}=${options.verdict.counts[severity]}`)
|
|
592
|
+
.join(', ');
|
|
593
|
+
return `Gate passed at the ${options.threshold} threshold, but the final review still contains non-blocking findings (${counts}). Run oat-project-review-receive for ${options.artifactPath} to disposition them before marking the final review row passed.`;
|
|
594
|
+
}
|
|
595
|
+
function reviewGateOutcome(payload) {
|
|
596
|
+
if (payload.blocking) {
|
|
597
|
+
return 'review_completed_blocking_findings';
|
|
598
|
+
}
|
|
599
|
+
if (payload.normalization) {
|
|
600
|
+
return 'review_completed_artifact_normalized_gate_passed';
|
|
601
|
+
}
|
|
602
|
+
return 'review_completed_gate_passed';
|
|
603
|
+
}
|
|
580
604
|
function writeReviewGateResult(context, payload) {
|
|
605
|
+
const outcome = reviewGateOutcome(payload);
|
|
581
606
|
if (context.json) {
|
|
582
|
-
context.logger.json(payload);
|
|
607
|
+
context.logger.json({ outcome, ...payload });
|
|
583
608
|
return;
|
|
584
609
|
}
|
|
610
|
+
if (outcome === 'review_completed_blocking_findings') {
|
|
611
|
+
context.logger.info('Review completed and found blocking issues.');
|
|
612
|
+
}
|
|
613
|
+
else if (outcome === 'review_completed_artifact_normalized_gate_passed') {
|
|
614
|
+
context.logger.info('Review completed, artifact was normalized, and gate passed.');
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
context.logger.info('Review completed and gate passed.');
|
|
618
|
+
}
|
|
585
619
|
context.logger.info(`Review artifact: ${payload.artifactPath}`);
|
|
620
|
+
if (payload.normalization) {
|
|
621
|
+
context.logger.info(`Artifact normalized: inserted ${payload.normalization.insertedSeverities.map((severity) => severityDisplayName(severity)).join(', ')} empty Findings section(s).`);
|
|
622
|
+
}
|
|
586
623
|
context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical}, important=${payload.counts.important}, medium=${payload.counts.medium}, minor=${payload.counts.minor})`);
|
|
587
624
|
context.logger.info(payload.handoff);
|
|
588
625
|
}
|
|
626
|
+
function writeReviewGateExecutionFailure(context, payload) {
|
|
627
|
+
const message = `Review did not complete: target ${payload.target} exited with code ${payload.exitCode}.`;
|
|
628
|
+
if (context.json) {
|
|
629
|
+
context.logger.json({
|
|
630
|
+
status: 'review_failed',
|
|
631
|
+
outcome: 'review_did_not_complete',
|
|
632
|
+
target: payload.target,
|
|
633
|
+
project: payload.project,
|
|
634
|
+
exitCode: payload.exitCode,
|
|
635
|
+
message,
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
context.logger.error(message);
|
|
640
|
+
}
|
|
641
|
+
function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
642
|
+
if (context.json) {
|
|
643
|
+
context.logger.json({
|
|
644
|
+
status: 'artifact_validation_failed',
|
|
645
|
+
outcome: 'review_completed_artifact_validation_failed',
|
|
646
|
+
target: payload.target,
|
|
647
|
+
project: payload.project,
|
|
648
|
+
artifactPath: payload.artifactPath,
|
|
649
|
+
message: payload.message,
|
|
650
|
+
recovery: payload.recovery,
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
context.logger.error(`Review completed but artifact validation failed: ${payload.message}`);
|
|
655
|
+
context.logger.error(payload.recovery);
|
|
656
|
+
}
|
|
589
657
|
async function updateConfigLayer(context, layer, dependencies, mutate) {
|
|
590
658
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
591
659
|
const userConfigDir = join(context.home, '.oat');
|
|
@@ -716,6 +784,11 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
716
784
|
]);
|
|
717
785
|
const childExitCode = await executeTarget(selected, [reviewPrompt], context, dependencies);
|
|
718
786
|
if (childExitCode !== 0) {
|
|
787
|
+
writeReviewGateExecutionFailure(context, {
|
|
788
|
+
target: selected.id,
|
|
789
|
+
project: projectPath,
|
|
790
|
+
exitCode: childExitCode,
|
|
791
|
+
});
|
|
719
792
|
process.exitCode = childExitCode;
|
|
720
793
|
return;
|
|
721
794
|
}
|
|
@@ -725,11 +798,41 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
725
798
|
});
|
|
726
799
|
const producedArtifact = findProducedReviewArtifact(before, after);
|
|
727
800
|
if (!producedArtifact) {
|
|
728
|
-
|
|
801
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
802
|
+
target: selected.id,
|
|
803
|
+
project: projectPath,
|
|
804
|
+
artifactPath: null,
|
|
805
|
+
message: `No new review artifact was detected for project ${projectPath}.`,
|
|
806
|
+
recovery: 'Ensure the review provider wrote a project review artifact. If it did, fix or attach that artifact and run oat-project-review-receive before treating the review as consumed.',
|
|
807
|
+
});
|
|
808
|
+
process.exitCode = 1;
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
let verdict;
|
|
812
|
+
try {
|
|
813
|
+
verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path), {
|
|
814
|
+
normalizeMissingEmptySeveritySections: true,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
819
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
820
|
+
target: selected.id,
|
|
821
|
+
project: projectPath,
|
|
822
|
+
artifactPath: producedArtifact.path,
|
|
823
|
+
message: detail,
|
|
824
|
+
recovery: `The review artifact was created at ${producedArtifact.path} but could not be consumed. Fix the artifact format, then run oat-project-review-receive for ${producedArtifact.path}; if the only issue is a missing zero-count severity heading, rerun the gate to normalize the same artifact instead of creating a new review version.`,
|
|
825
|
+
});
|
|
826
|
+
process.exitCode = 1;
|
|
827
|
+
return;
|
|
729
828
|
}
|
|
730
|
-
const verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path));
|
|
731
829
|
const blocking = reviewBlocksAtThreshold(verdict, threshold);
|
|
732
|
-
const handoff =
|
|
830
|
+
const handoff = buildReviewGateHandoff({
|
|
831
|
+
artifactPath: producedArtifact.path,
|
|
832
|
+
verdict,
|
|
833
|
+
threshold,
|
|
834
|
+
blocking,
|
|
835
|
+
});
|
|
733
836
|
writeReviewGateResult(context, {
|
|
734
837
|
status: blocking ? 'blocked' : 'ok',
|
|
735
838
|
target: selected.id,
|
|
@@ -741,6 +844,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
741
844
|
reviewType: verdict.reviewType,
|
|
742
845
|
scope: verdict.scope,
|
|
743
846
|
invocation: verdict.invocation,
|
|
847
|
+
normalization: verdict.normalization,
|
|
744
848
|
handoff,
|
|
745
849
|
});
|
|
746
850
|
process.exitCode = blocking ? 1 : 0;
|
|
@@ -10,6 +10,14 @@ export interface ReviewGateVerdict {
|
|
|
10
10
|
minor: number;
|
|
11
11
|
};
|
|
12
12
|
blocking: boolean;
|
|
13
|
+
normalization?: {
|
|
14
|
+
insertedSeverities: Severity[];
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export type Severity = keyof ReviewGateVerdict['counts'];
|
|
18
|
+
export interface ParseReviewGateVerdictOptions {
|
|
19
|
+
normalizeMissingEmptySeveritySections?: boolean;
|
|
13
20
|
}
|
|
14
|
-
export declare function
|
|
21
|
+
export declare function severityDisplayName(severity: Severity): string;
|
|
22
|
+
export declare function parseReviewGateVerdict(artifactPath: string, options?: ParseReviewGateVerdictOptions): Promise<ReviewGateVerdict>;
|
|
15
23
|
//# sourceMappingURL=review-verdict.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/review-verdict.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,QAAQ,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/review-verdict.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE;QACd,kBAAkB,EAAE,QAAQ,EAAE,CAAC;KAChC,CAAC;CACH;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;AAEzD,MAAM,WAAW,6BAA6B;IAC5C,qCAAqC,CAAC,EAAE,OAAO,CAAC;CACjD;AAqID,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAE9D;AAqRD,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,6BAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAsD5B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { getFrontmatterBlock } from '../shared/frontmatter.js';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
4
|
const SEVERITIES = [
|
|
@@ -89,6 +89,89 @@ function normalizeHeading(value) {
|
|
|
89
89
|
}
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
|
+
export function severityDisplayName(severity) {
|
|
93
|
+
return severity.charAt(0).toUpperCase() + severity.slice(1);
|
|
94
|
+
}
|
|
95
|
+
function markdownLines(content) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
let start = 0;
|
|
98
|
+
while (start < content.length) {
|
|
99
|
+
const newlineIndex = content.indexOf('\n', start);
|
|
100
|
+
const end = newlineIndex === -1 ? content.length : newlineIndex;
|
|
101
|
+
lines.push({
|
|
102
|
+
text: content.slice(start, end),
|
|
103
|
+
start,
|
|
104
|
+
});
|
|
105
|
+
start = newlineIndex === -1 ? content.length : newlineIndex + 1;
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
function fenceMarker(line) {
|
|
110
|
+
const match = line.match(/^\s*(`{3,}|~{3,})/);
|
|
111
|
+
if (!match?.[1]) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
marker: match[1].startsWith('`') ? '`' : '~',
|
|
116
|
+
length: match[1].length,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function linesOutsideFences(content) {
|
|
120
|
+
const outsideFenceLines = [];
|
|
121
|
+
let activeFence = null;
|
|
122
|
+
for (const line of markdownLines(content)) {
|
|
123
|
+
const marker = fenceMarker(line.text);
|
|
124
|
+
if (marker) {
|
|
125
|
+
if (!activeFence) {
|
|
126
|
+
activeFence = marker;
|
|
127
|
+
}
|
|
128
|
+
else if (marker.marker === activeFence.marker &&
|
|
129
|
+
marker.length >= activeFence.length) {
|
|
130
|
+
activeFence = null;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!activeFence) {
|
|
135
|
+
outsideFenceLines.push(line);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return outsideFenceLines;
|
|
139
|
+
}
|
|
140
|
+
function findFindingsSection(content) {
|
|
141
|
+
const lines = linesOutsideFences(content);
|
|
142
|
+
const findingsHeading = lines.find((line) => /^##\s+Findings\s*#*\s*$/i.test(line.text));
|
|
143
|
+
if (!findingsHeading) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const start = findingsHeading.start + findingsHeading.text.length;
|
|
147
|
+
const nextPeerHeading = lines.find((line) => line.start > findingsHeading.start &&
|
|
148
|
+
/^#{1,2}\s+.+?\s*#*\s*$/.test(line.text));
|
|
149
|
+
const end = nextPeerHeading?.start ?? content.length;
|
|
150
|
+
return {
|
|
151
|
+
start,
|
|
152
|
+
end,
|
|
153
|
+
content: content.slice(start, end),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function findSeverityHeadings(content) {
|
|
157
|
+
const findingsSection = findFindingsSection(content);
|
|
158
|
+
const scanContent = findingsSection?.content ?? content;
|
|
159
|
+
const offset = findingsSection?.start ?? 0;
|
|
160
|
+
return linesOutsideFences(scanContent)
|
|
161
|
+
.map((line) => {
|
|
162
|
+
const match = line.text.match(/^#{3,6}\s+(.+?)\s*#*\s*$/);
|
|
163
|
+
return {
|
|
164
|
+
severity: normalizeHeading(match?.[1] ?? ''),
|
|
165
|
+
index: offset + line.start,
|
|
166
|
+
headingLength: line.text.length,
|
|
167
|
+
};
|
|
168
|
+
})
|
|
169
|
+
.filter((heading) => heading.severity !== null);
|
|
170
|
+
}
|
|
171
|
+
function missingSeverityHeadings(severityHeadings) {
|
|
172
|
+
const seenSeverities = new Set(severityHeadings.map((heading) => heading.severity));
|
|
173
|
+
return SEVERITIES.filter((severity) => !seenSeverities.has(severity));
|
|
174
|
+
}
|
|
92
175
|
function parseFindingsSummaryCounts(content) {
|
|
93
176
|
const match = content.match(/^Findings:\s*(\d+)\s+critical,\s*(\d+)\s+important,\s*(\d+)\s+medium,\s*(\d+)\s+minor\s*$/im);
|
|
94
177
|
if (!match) {
|
|
@@ -102,19 +185,12 @@ function parseFindingsSummaryCounts(content) {
|
|
|
102
185
|
};
|
|
103
186
|
}
|
|
104
187
|
function parseFindingsSectionCounts(content, artifactPath) {
|
|
105
|
-
const
|
|
106
|
-
const severityHeadings =
|
|
107
|
-
.map((match) => ({
|
|
108
|
-
severity: normalizeHeading(match[1] ?? ''),
|
|
109
|
-
index: match.index ?? 0,
|
|
110
|
-
headingLength: match[0].length,
|
|
111
|
-
}))
|
|
112
|
-
.filter((heading) => heading.severity !== null);
|
|
188
|
+
const findingsSection = findFindingsSection(content);
|
|
189
|
+
const severityHeadings = findSeverityHeadings(content);
|
|
113
190
|
if (severityHeadings.length === 0) {
|
|
114
191
|
return null;
|
|
115
192
|
}
|
|
116
|
-
const
|
|
117
|
-
const missingSeverities = SEVERITIES.filter((severity) => !seenSeverities.has(severity));
|
|
193
|
+
const missingSeverities = missingSeverityHeadings(severityHeadings);
|
|
118
194
|
if (missingSeverities.length > 0) {
|
|
119
195
|
throw new Error(`Review artifact at ${artifactPath} has an incomplete Findings section; expected headings for Critical, Important, Medium, and Minor. Missing: ${missingSeverities.join(', ')}.`);
|
|
120
196
|
}
|
|
@@ -127,7 +203,7 @@ function parseFindingsSectionCounts(content, artifactPath) {
|
|
|
127
203
|
for (const [offset, heading] of severityHeadings.entries()) {
|
|
128
204
|
const nextHeading = severityHeadings[offset + 1];
|
|
129
205
|
const sectionStart = heading.index + heading.headingLength;
|
|
130
|
-
const sectionEnd = nextHeading?.index ?? content.length;
|
|
206
|
+
const sectionEnd = nextHeading?.index ?? findingsSection?.end ?? content.length;
|
|
131
207
|
counts[heading.severity] = countFindingsInSection(content.slice(sectionStart, sectionEnd));
|
|
132
208
|
}
|
|
133
209
|
return counts;
|
|
@@ -150,7 +226,54 @@ function parseFrontmatterObject(frontmatter, artifactPath) {
|
|
|
150
226
|
function hasBlockingFindings(counts) {
|
|
151
227
|
return counts.critical > 0 || counts.important > 0;
|
|
152
228
|
}
|
|
153
|
-
|
|
229
|
+
function insertionTextForSeverity(severity) {
|
|
230
|
+
return `\n### ${severityDisplayName(severity)}\n\nNone\n`;
|
|
231
|
+
}
|
|
232
|
+
function insertionPointForMissingSeverity(content, missingSeverity) {
|
|
233
|
+
const findingsSection = findFindingsSection(content);
|
|
234
|
+
const sectionEnd = findingsSection?.end ?? content.length;
|
|
235
|
+
const severityHeadings = findSeverityHeadings(content);
|
|
236
|
+
const missingSeverityRank = SEVERITIES.indexOf(missingSeverity);
|
|
237
|
+
const nextHeading = severityHeadings.find((heading) => SEVERITIES.indexOf(heading.severity) > missingSeverityRank);
|
|
238
|
+
return nextHeading?.index ?? sectionEnd;
|
|
239
|
+
}
|
|
240
|
+
function insertMissingSeveritySection(content, severity) {
|
|
241
|
+
const insertionPoint = insertionPointForMissingSeverity(content, severity);
|
|
242
|
+
const insertion = insertionTextForSeverity(severity);
|
|
243
|
+
const suffix = content.slice(insertionPoint);
|
|
244
|
+
const needsTrailingBlank = suffix.length > 0 ? '\n' : '';
|
|
245
|
+
return `${content.slice(0, insertionPoint)}${insertion}${needsTrailingBlank}${suffix}`;
|
|
246
|
+
}
|
|
247
|
+
async function normalizeMissingEmptySeveritySections(artifactPath, content, counts) {
|
|
248
|
+
if (!findFindingsSection(content)) {
|
|
249
|
+
throw new Error(`Review artifact at ${artifactPath} does not contain a ## Findings section, so missing severity headings cannot be safely normalized.`);
|
|
250
|
+
}
|
|
251
|
+
let normalizedContent = content;
|
|
252
|
+
const insertedSeverities = [];
|
|
253
|
+
for (const severity of missingSeverityHeadings(findSeverityHeadings(content))) {
|
|
254
|
+
if (counts[severity] !== 0) {
|
|
255
|
+
throw new Error(`Review artifact at ${artifactPath} is missing the ${severityDisplayName(severity)} Findings section, but structured counts report ${counts[severity]} ${severity} finding(s). This cannot be safely normalized.`);
|
|
256
|
+
}
|
|
257
|
+
normalizedContent = insertMissingSeveritySection(normalizedContent, severity);
|
|
258
|
+
insertedSeverities.push(severity);
|
|
259
|
+
}
|
|
260
|
+
if (insertedSeverities.length > 0) {
|
|
261
|
+
await writeFile(artifactPath, normalizedContent, 'utf8');
|
|
262
|
+
}
|
|
263
|
+
return { content: normalizedContent, insertedSeverities };
|
|
264
|
+
}
|
|
265
|
+
function resolveCounts(content, frontmatter) {
|
|
266
|
+
const frontmatterCounts = readFrontmatterCounts(frontmatter);
|
|
267
|
+
if (frontmatterCounts) {
|
|
268
|
+
return frontmatterCounts;
|
|
269
|
+
}
|
|
270
|
+
const summaryCounts = parseFindingsSummaryCounts(content);
|
|
271
|
+
if (summaryCounts) {
|
|
272
|
+
return summaryCounts;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
export async function parseReviewGateVerdict(artifactPath, options = {}) {
|
|
154
277
|
let content;
|
|
155
278
|
try {
|
|
156
279
|
content = await readFile(artifactPath, 'utf8');
|
|
@@ -163,9 +286,16 @@ export async function parseReviewGateVerdict(artifactPath) {
|
|
|
163
286
|
const frontmatter = frontmatterBlock
|
|
164
287
|
? parseFrontmatterObject(frontmatterBlock, artifactPath)
|
|
165
288
|
: {};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
289
|
+
let counts = resolveCounts(content, frontmatter);
|
|
290
|
+
let insertedSeverities = [];
|
|
291
|
+
if (counts && options.normalizeMissingEmptySeveritySections) {
|
|
292
|
+
const normalized = await normalizeMissingEmptySeveritySections(artifactPath, content, counts);
|
|
293
|
+
content = normalized.content;
|
|
294
|
+
insertedSeverities = normalized.insertedSeverities;
|
|
295
|
+
}
|
|
296
|
+
if (!counts) {
|
|
297
|
+
counts = parseFindingsSectionCounts(content, artifactPath);
|
|
298
|
+
}
|
|
169
299
|
if (!counts) {
|
|
170
300
|
throw new Error(`Review artifact at ${artifactPath} does not contain recognizable review findings or explicit verdict counts.`);
|
|
171
301
|
}
|
|
@@ -176,5 +306,12 @@ export async function parseReviewGateVerdict(artifactPath) {
|
|
|
176
306
|
invocation: stringOrNull(frontmatter['oat_review_invocation']),
|
|
177
307
|
counts,
|
|
178
308
|
blocking: hasBlockingFindings(counts),
|
|
309
|
+
...(insertedSeverities.length > 0
|
|
310
|
+
? {
|
|
311
|
+
normalization: {
|
|
312
|
+
insertedSeverities,
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
179
316
|
};
|
|
180
317
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-agent-toolkit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Open Agent Toolkit CLI",
|
|
6
6
|
"homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"ora": "^9.0.0",
|
|
35
35
|
"yaml": "2.8.2",
|
|
36
36
|
"zod": "^3.25.76",
|
|
37
|
-
"@open-agent-toolkit/control-plane": "0.1.
|
|
37
|
+
"@open-agent-toolkit/control-plane": "0.1.38"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.0",
|