@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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli": "0.1.37",
3
- "docs-config": "0.1.37",
4
- "docs-theme": "0.1.37",
5
- "docs-transforms": "0.1.37"
2
+ "cli": "0.1.38",
3
+ "docs-config": "0.1.38",
4
+ "docs-theme": "0.1.38",
5
+ "docs-transforms": "0.1.38"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-review-provide
3
- version: 1.2.1
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
- {None or list}
32
+ {If none: "None"}
31
33
 
32
34
  ### Important
33
35
 
34
- {None or list}
36
+ {If none: "None"}
37
+
38
+ ### Medium
39
+
40
+ {If none: "None"}
35
41
 
36
42
  ### Minor
37
43
 
38
- {None or list}
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;AAOpC,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;AA+wBD,wBAAgB,iBAAiB,CAC/B,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwKT"}
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
- throw new Error(`No new review artifact was detected for project ${projectPath}. Ensure the review provider wrote a project review artifact before the gate exits.`);
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 = `Run oat-project-review-receive for ${producedArtifact.path} before treating this gate review as consumed.`;
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 parseReviewGateVerdict(artifactPath: string): Promise<ReviewGateVerdict>;
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;CACnB;AA2ND,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAmC5B"}
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 headingMatches = [...content.matchAll(/^#{3,6}\s+(.+?)\s*#*\s*$/gm)];
106
- const severityHeadings = headingMatches
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 seenSeverities = new Set(severityHeadings.map((heading) => heading.severity));
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
- export async function parseReviewGateVerdict(artifactPath) {
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
- const counts = readFrontmatterCounts(frontmatter) ??
167
- parseFindingsSummaryCounts(content) ??
168
- parseFindingsSectionCounts(content, artifactPath);
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.37",
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"
37
+ "@open-agent-toolkit/control-plane": "0.1.38"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",