@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.
@@ -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 --target codex-5.5-xhigh --review-type code --review-scope final "Use oat-project-review-provide code final to review the current project"' \
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 --target codex-5.5-xhigh \
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, use `--target <id>` when
246
- you need an explicit reviewer target, or rely on the default `same-runtime`
247
- avoidance for cross-runtime review.
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 cross-runtime review gates, prefer putting
99
- `oat gate cross-provider-exec "<prompt>"` in the configured gate command rather
100
- than hard-coding a provider CLI directly in the skill. See
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
- "cli": "0.1.36",
3
- "docs-config": "0.1.36",
4
- "docs-theme": "0.1.36",
5
- "docs-transforms": "0.1.36"
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-project-implement
3
- version: 2.0.22
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` resolves the current host from built-in `hostDetectionCommand`s and avoids the same runtime with zero per-prompt input. It does not read or stamp `OAT_CURRENT_RUNTIME` or `OAT_GATE_EXEC_TARGET`. To pin a specific reviewer for this skill, set `--target <id>` once in that skill's gate `command`; this is the optional precision path and does not require per-prompt input.
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.1
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` resolves the current host from built-in `hostDetectionCommand`s and avoids the same runtime with zero per-prompt input. It does not read or stamp `OAT_CURRENT_RUNTIME` or `OAT_GATE_EXEC_TARGET`. To pin a specific reviewer for this skill, set `--target <id>` once in that skill's gate `command`; this is the optional precision path and does not require per-prompt input.
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.7
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` resolves the current host from built-in `hostDetectionCommand`s and avoids the same runtime with zero per-prompt input. It does not read or stamp `OAT_CURRENT_RUNTIME` or `OAT_GATE_EXEC_TARGET`. To pin a specific reviewer for this skill, set `--target <id>` once in that skill's gate `command`; this is the optional precision path and does not require per-prompt input.
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.7
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` resolves the current host from built-in `hostDetectionCommand`s and avoids the same runtime with zero per-prompt input. It does not read or stamp `OAT_CURRENT_RUNTIME` or `OAT_GATE_EXEC_TARGET`. To pin a specific reviewer for this skill, set `--target <id>` once in that skill's gate `command`; this is the optional precision path and does not require per-prompt input.
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.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;AAkZD,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,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
- ...prompt,
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
- 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;
723
828
  }
724
- const verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path));
725
829
  const blocking = reviewBlocksAtThreshold(verdict, threshold);
726
- 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
+ });
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 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.36",
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.36"
37
+ "@open-agent-toolkit/control-plane": "0.1.38"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",