@open-agent-toolkit/cli 0.1.36 → 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/docs/cli-utilities/config-and-local-state.md +4 -1
- package/assets/docs/cli-utilities/configuration.md +1 -1
- package/assets/docs/cli-utilities/workflow-gates.md +23 -6
- package/assets/docs/contributing/skills.md +7 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-project-implement/SKILL.md +2 -2
- package/assets/skills/oat-project-import-plan/SKILL.md +2 -2
- package/assets/skills/oat-project-plan/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/SKILL.md +2 -2
- 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 +120 -10
- 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
|
@@ -119,7 +119,10 @@ Available commands:
|
|
|
119
119
|
Use `oat gate review` for OAT review gates. It keeps the normal stateful
|
|
120
120
|
review-provider workflow: the review artifact, Reviews row update, and review
|
|
121
121
|
bookkeeping commits are expected, and the produced review must be received with
|
|
122
|
-
`oat-project-review-receive` before the host treats it as dispositioned.
|
|
122
|
+
`oat-project-review-receive` before the host treats it as dispositioned. Keep
|
|
123
|
+
reusable lifecycle gate commands target-neutral by omitting `--target <id>`; use
|
|
124
|
+
explicit target pins only for manual dispatch, debugging, or deliberately local
|
|
125
|
+
overrides.
|
|
123
126
|
|
|
124
127
|
Use `oat gate cross-provider-exec` for generic cross-runtime execution. It
|
|
125
128
|
avoids the current runtime by default, chooses a fresh Codex, Claude, or Cursor
|
|
@@ -270,7 +270,7 @@ Workflow preference keys live under the `workflow.*` namespace:
|
|
|
270
270
|
- `workflow.dispatchCeiling.preset` — `balanced`, `maximum`, or `cost-conscious`. Convenience preset that compiles to per-provider values at write time. Setting this key is the recommended way to configure the ceiling.
|
|
271
271
|
- `workflow.dispatchCeiling.providers.codex` — `low`, `medium`, `high`, or `xhigh`. Concrete Codex ceiling. Set automatically when a preset is selected; also settable directly for Advanced (no preset) configurations. Provider default effort is informational for base/unpinned roles and is not treated as this ceiling.
|
|
272
272
|
- `workflow.dispatchCeiling.providers.claude` — `haiku`, `sonnet`, or `opus`. Concrete Claude ceiling. Set automatically when a preset is selected; also settable directly for Advanced configurations. Claude has no separate per-dispatch effort axis, so the effort axis remains `not-applicable`.
|
|
273
|
-
- `workflow.gates.skills` / `workflow.gates.execTargets` — structured per-skill final gate commands and exec-target registry. Use `oat gate set`, `oat gate target set`, and `oat gate cross-provider-exec`; do not use `oat config set` for these objects.
|
|
273
|
+
- `workflow.gates.skills` / `workflow.gates.execTargets` — structured per-skill final gate commands and exec-target registry. Use `oat gate set`, `oat gate target set`, `oat gate review`, and `oat gate cross-provider-exec`; do not use `oat config set` for these objects.
|
|
274
274
|
|
|
275
275
|
### Auto artifact-review preferences
|
|
276
276
|
|
|
@@ -112,7 +112,8 @@ JSON argv is intentional: provider commands often contain flags such as `-p` or
|
|
|
112
112
|
Use these examples only in trusted environments where the provider process may
|
|
113
113
|
read files, run tools, and write review bookkeeping without an interactive
|
|
114
114
|
approval prompt. They are user-level target configuration, not built-in OAT
|
|
115
|
-
defaults.
|
|
115
|
+
defaults. Defining a trusted target makes it available to the dispatcher; it
|
|
116
|
+
does not mean shared lifecycle gate commands should pin that target.
|
|
116
117
|
|
|
117
118
|
Claude's default permission mode can block on
|
|
118
119
|
`Skill(oat-project-review-provide)`, `oat`, `pnpm`, and shell or tool calls. A
|
|
@@ -161,7 +162,7 @@ Set or clear a skill gate:
|
|
|
161
162
|
|
|
162
163
|
```bash
|
|
163
164
|
oat gate set oat-project-implement \
|
|
164
|
-
--command 'oat gate review --
|
|
165
|
+
--command 'oat gate review --review-type code --review-scope final "Use oat-project-review-provide code final to review the current project"' \
|
|
165
166
|
--description "Run final review in another runtime" \
|
|
166
167
|
--on-failure block \
|
|
167
168
|
--max-attempts 2 \
|
|
@@ -170,6 +171,11 @@ oat gate set oat-project-implement \
|
|
|
170
171
|
oat gate unset oat-project-implement --layer user
|
|
171
172
|
```
|
|
172
173
|
|
|
174
|
+
Lifecycle gate commands should normally omit `--target <id>`. Leaving the
|
|
175
|
+
target unset lets the dispatcher avoid the current runtime and choose the
|
|
176
|
+
highest-priority available non-host target. Pin a target only for manual
|
|
177
|
+
dispatch, debugging, or a deliberate local/user-specific override.
|
|
178
|
+
|
|
173
179
|
Set or clear an exec target:
|
|
174
180
|
|
|
175
181
|
```bash
|
|
@@ -186,7 +192,7 @@ oat gate target unset codex-high --layer user
|
|
|
186
192
|
Dispatch a review through the target registry:
|
|
187
193
|
|
|
188
194
|
```bash
|
|
189
|
-
oat gate review
|
|
195
|
+
oat gate review \
|
|
190
196
|
--review-type code \
|
|
191
197
|
--review-scope final \
|
|
192
198
|
"Use oat-project-review-provide code final to review the current project"
|
|
@@ -195,6 +201,16 @@ oat gate review --target codex-5.5-xhigh \
|
|
|
195
201
|
Leaving `--target` unset lets target priority choose the highest-priority
|
|
196
202
|
available non-host runtime.
|
|
197
203
|
|
|
204
|
+
For manual or debug dispatch, use `--target <id>` to pin one target and skip
|
|
205
|
+
detection/avoidance:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
oat gate review --target codex-5.5-xhigh \
|
|
209
|
+
--review-type code \
|
|
210
|
+
--review-scope final \
|
|
211
|
+
"Use oat-project-review-provide code final to review the current project"
|
|
212
|
+
```
|
|
213
|
+
|
|
198
214
|
Dispatch a generic prompt through the target registry:
|
|
199
215
|
|
|
200
216
|
```bash
|
|
@@ -242,6 +258,7 @@ OAT does not try another target after a failed review.
|
|
|
242
258
|
|
|
243
259
|
V1 gates avoid the current runtime, not the current model or effort setting.
|
|
244
260
|
Same-runtime but different-target dispatch, such as using Cursor again with a
|
|
245
|
-
different model slug, is future work. Until that exists,
|
|
246
|
-
|
|
247
|
-
|
|
261
|
+
different model slug, is future work. Until that exists, reusable lifecycle
|
|
262
|
+
gates should rely on default `same-runtime` avoidance for cross-runtime review;
|
|
263
|
+
manual and local overrides can pin an explicit reviewer target with
|
|
264
|
+
`--target <id>`.
|
|
@@ -95,9 +95,13 @@ The Gate Execution step should:
|
|
|
95
95
|
- `prompt` - surface the failure and ask the user.
|
|
96
96
|
- `warn` - record the failure and continue.
|
|
97
97
|
|
|
98
|
-
For
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
For OAT review gates, prefer putting `oat gate review "<prompt>"` in the
|
|
99
|
+
configured gate command rather than hard-coding a provider CLI directly in the
|
|
100
|
+
skill. Use `oat gate cross-provider-exec "<prompt>"` for generic cross-runtime
|
|
101
|
+
commands that should report only the child process status, not review findings.
|
|
102
|
+
Reusable lifecycle gate commands should normally omit `--target <id>` so the
|
|
103
|
+
dispatcher can avoid the current runtime; reserve explicit targets for
|
|
104
|
+
manual/debug commands or deliberate local/user-specific overrides. See
|
|
101
105
|
[Workflow Gates](../cli-utilities/workflow-gates.md) for the config and command
|
|
102
106
|
surface.
|
|
103
107
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-project-implement
|
|
3
|
-
version: 2.0.
|
|
3
|
+
version: 2.0.23
|
|
4
4
|
description: Use when plan.md is ready for execution. Dispatches phase-level subagents with bounded fix loops; supports plan-declared parallel phase groups with worktree-isolated execution and ordered fan-in.
|
|
5
5
|
oat_gateable: true
|
|
6
6
|
argument-hint: '[--retry-limit <N>] [--dry-run]'
|
|
@@ -1443,7 +1443,7 @@ Before reporting this skill as complete, run the configured gate as the final st
|
|
|
1443
1443
|
- `prompt`: surface the gate failure and ask the human how to proceed.
|
|
1444
1444
|
- `warn`: record the gate failure and continue.
|
|
1445
1445
|
|
|
1446
|
-
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no env var. By default, `oat gate cross-provider-exec`
|
|
1446
|
+
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no OAT runtime env var. By default, `oat gate review` and `oat gate cross-provider-exec` resolve the current host from built-in `hostDetectionCommand`s and avoid the same runtime when no exact target is supplied. Reusable lifecycle skill-gate commands should normally omit `--target <id>` so independent review stays provider-neutral. Use explicit targets only for manual/debug commands or deliberate local/user-specific overrides; do not hardcode provider/model targets in bundled skill guidance or shared lifecycle gate examples.
|
|
1447
1447
|
|
|
1448
1448
|
## Success Criteria
|
|
1449
1449
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-project-import-plan
|
|
3
|
-
version: 1.4.
|
|
3
|
+
version: 1.4.2
|
|
4
4
|
description: Use when you have an external markdown plan to execute with OAT. Preserves the source plan and normalizes it into canonical plan.md format.
|
|
5
5
|
argument-hint: '<path-to-plan.md> [--provider codex|cursor|claude] [--project <name>]'
|
|
6
6
|
oat_gateable: true
|
|
@@ -295,7 +295,7 @@ Before reporting this skill as complete, run the configured gate as the final st
|
|
|
295
295
|
- `prompt`: surface the gate failure and ask the human how to proceed.
|
|
296
296
|
- `warn`: record the gate failure and continue.
|
|
297
297
|
|
|
298
|
-
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no env var. By default, `oat gate cross-provider-exec`
|
|
298
|
+
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no OAT runtime env var. By default, `oat gate review` and `oat gate cross-provider-exec` resolve the current host from built-in `hostDetectionCommand`s and avoid the same runtime when no exact target is supplied. Reusable lifecycle skill-gate commands should normally omit `--target <id>` so independent review stays provider-neutral. Use explicit targets only for manual/debug commands or deliberate local/user-specific overrides; do not hardcode provider/model targets in bundled skill guidance or shared lifecycle gate examples.
|
|
299
299
|
|
|
300
300
|
### Step 7: Output Next Action
|
|
301
301
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-project-plan
|
|
3
|
-
version: 1.3.
|
|
3
|
+
version: 1.3.8
|
|
4
4
|
description: Use when design.md is complete and executable implementation tasks are needed. Breaks design into bite-sized TDD tasks in canonical plan.md format.
|
|
5
5
|
oat_gateable: true
|
|
6
6
|
disable-model-invocation: true
|
|
@@ -525,7 +525,7 @@ Before reporting this skill as complete, run the configured gate as the final st
|
|
|
525
525
|
- `prompt`: surface the gate failure and ask the human how to proceed.
|
|
526
526
|
- `warn`: record the gate failure and continue.
|
|
527
527
|
|
|
528
|
-
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no env var. By default, `oat gate cross-provider-exec`
|
|
528
|
+
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no OAT runtime env var. By default, `oat gate review` and `oat gate cross-provider-exec` resolve the current host from built-in `hostDetectionCommand`s and avoid the same runtime when no exact target is supplied. Reusable lifecycle skill-gate commands should normally omit `--target <id>` so independent review stays provider-neutral. Use explicit targets only for manual/debug commands or deliberate local/user-specific overrides; do not hardcode provider/model targets in bundled skill guidance or shared lifecycle gate examples.
|
|
529
529
|
|
|
530
530
|
## Success Criteria
|
|
531
531
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-project-quick-start
|
|
3
|
-
version: 2.1.
|
|
3
|
+
version: 2.1.8
|
|
4
4
|
description: Use when a task is small enough for quick mode or rapid iteration is preferred. Scaffolds a lightweight OAT project from discovery directly to a runnable plan, with optional brainstorming and lightweight design.
|
|
5
5
|
argument-hint: '<project-name> ["project description"]'
|
|
6
6
|
oat_gateable: true
|
|
@@ -615,7 +615,7 @@ Before reporting this skill as complete, run the configured gate as the final st
|
|
|
615
615
|
- `prompt`: surface the gate failure and ask the human how to proceed.
|
|
616
616
|
- `warn`: record the gate failure and continue.
|
|
617
617
|
|
|
618
|
-
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no env var. By default, `oat gate cross-provider-exec`
|
|
618
|
+
5. Runtime selection note (V1): the step runs the gate `command` as-is and reads no OAT runtime env var. By default, `oat gate review` and `oat gate cross-provider-exec` resolve the current host from built-in `hostDetectionCommand`s and avoid the same runtime when no exact target is supplied. Reusable lifecycle skill-gate commands should normally omit `--target <id>` so independent review stays provider-neutral. Use explicit targets only for manual/debug commands or deliberate local/user-specific overrides; do not hardcode provider/model targets in bundled skill guidance or shared lifecycle gate examples.
|
|
619
619
|
|
|
620
620
|
### Step 7: Output Next Action
|
|
621
621
|
|
|
@@ -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,10 +40,16 @@ 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
|
}
|
|
47
|
+
function assembleReviewGatePrompt(segments) {
|
|
48
|
+
return segments
|
|
49
|
+
.map((segment) => segment.trim())
|
|
50
|
+
.filter((segment) => segment.length > 0)
|
|
51
|
+
.join('\n\n');
|
|
52
|
+
}
|
|
47
53
|
async function runChildProcess(command, args, options) {
|
|
48
54
|
return new Promise((resolve, reject) => {
|
|
49
55
|
const child = spawn(command, args, {
|
|
@@ -571,15 +577,83 @@ function reviewBlocksAtThreshold(verdict, threshold) {
|
|
|
571
577
|
}
|
|
572
578
|
return verdict.counts.minor > 0;
|
|
573
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
|
+
}
|
|
574
604
|
function writeReviewGateResult(context, payload) {
|
|
605
|
+
const outcome = reviewGateOutcome(payload);
|
|
575
606
|
if (context.json) {
|
|
576
|
-
context.logger.json(payload);
|
|
607
|
+
context.logger.json({ outcome, ...payload });
|
|
577
608
|
return;
|
|
578
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
|
+
}
|
|
579
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
|
+
}
|
|
580
623
|
context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical}, important=${payload.counts.important}, medium=${payload.counts.medium}, minor=${payload.counts.minor})`);
|
|
581
624
|
context.logger.info(payload.handoff);
|
|
582
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
|
+
}
|
|
583
657
|
async function updateConfigLayer(context, layer, dependencies, mutate) {
|
|
584
658
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
585
659
|
const userConfigDir = join(context.home, '.oat');
|
|
@@ -697,7 +771,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
697
771
|
repoRoot,
|
|
698
772
|
projectPath,
|
|
699
773
|
});
|
|
700
|
-
const reviewPrompt = [
|
|
774
|
+
const reviewPrompt = assembleReviewGatePrompt([
|
|
701
775
|
REVIEW_GATE_CONTEXT_NOTE,
|
|
702
776
|
reviewGateProjectContext(projectPath),
|
|
703
777
|
...(options.reviewType?.trim()
|
|
@@ -706,10 +780,15 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
706
780
|
...(options.reviewScope?.trim()
|
|
707
781
|
? [`Review scope: ${options.reviewScope.trim()}.`]
|
|
708
782
|
: []),
|
|
709
|
-
|
|
710
|
-
];
|
|
711
|
-
const childExitCode = await executeTarget(selected, reviewPrompt, context, dependencies);
|
|
783
|
+
prompt.join(' '),
|
|
784
|
+
]);
|
|
785
|
+
const childExitCode = await executeTarget(selected, [reviewPrompt], context, dependencies);
|
|
712
786
|
if (childExitCode !== 0) {
|
|
787
|
+
writeReviewGateExecutionFailure(context, {
|
|
788
|
+
target: selected.id,
|
|
789
|
+
project: projectPath,
|
|
790
|
+
exitCode: childExitCode,
|
|
791
|
+
});
|
|
713
792
|
process.exitCode = childExitCode;
|
|
714
793
|
return;
|
|
715
794
|
}
|
|
@@ -719,11 +798,41 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
719
798
|
});
|
|
720
799
|
const producedArtifact = findProducedReviewArtifact(before, after);
|
|
721
800
|
if (!producedArtifact) {
|
|
722
|
-
|
|
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;
|
|
723
828
|
}
|
|
724
|
-
const verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path));
|
|
725
829
|
const blocking = reviewBlocksAtThreshold(verdict, threshold);
|
|
726
|
-
const handoff =
|
|
830
|
+
const handoff = buildReviewGateHandoff({
|
|
831
|
+
artifactPath: producedArtifact.path,
|
|
832
|
+
verdict,
|
|
833
|
+
threshold,
|
|
834
|
+
blocking,
|
|
835
|
+
});
|
|
727
836
|
writeReviewGateResult(context, {
|
|
728
837
|
status: blocking ? 'blocked' : 'ok',
|
|
729
838
|
target: selected.id,
|
|
@@ -735,6 +844,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
735
844
|
reviewType: verdict.reviewType,
|
|
736
845
|
scope: verdict.scope,
|
|
737
846
|
invocation: verdict.invocation,
|
|
847
|
+
normalization: verdict.normalization,
|
|
738
848
|
handoff,
|
|
739
849
|
});
|
|
740
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",
|