@open-agent-toolkit/cli 0.1.35 → 0.1.36

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
2
  name: oat-reviewer
3
- version: 1.1.2
3
+ version: 1.1.3
4
4
  description: Unified reviewer for OAT projects - mode-aware verification of requirements/design alignment and code quality. Writes a review artifact to disk by default, or returns structured findings in-memory when dispatched in structured-output mode.
5
5
  tools: Read, Bash, Grep, Glob, Write
6
6
  color: yellow
@@ -54,6 +54,7 @@ You will be given a "Review Scope" block including:
54
54
  - **artifact_paths**: Paths to available artifacts (spec/design/plan/implementation/discovery/import reference)
55
55
  - **tasks_in_scope**: Task IDs being reviewed (if task/phase scope)
56
56
  - **oat_output_mode**: Optional output-sink selector. Absent (or any value other than `structured`) means **artifact mode** — write the review artifact to disk per Step 8. `structured` means **structured-output mode** — return a `StructuredFindings` object in-memory and write NO artifact file (see **Structured-Output Mode**). This key parallels the existing `oat_review_invocation` dispatch-payload naming.
57
+ - **oat_review_invocation**: Optional provenance selector for artifact-mode reviews. Use `manual`, `auto`, or `gate`; default to `manual` when absent.
57
58
  - **model_axis**: Optional model dispatch state selected by the orchestrator (`selected:<value>`, `inherited`, `not-applicable`, or `host-auto`)
58
59
  - **effort_axis**: Optional effort dispatch state selected by the orchestrator (`selected:<value>`, `provider-default`, `inherited`, `not-applicable`, or `host-auto`)
59
60
  - **dispatch_ceiling**: Optional resolved provider ceiling that capped/selected this review dispatch
@@ -264,6 +265,12 @@ Group findings by severity:
264
265
  - Missing tests for important paths
265
266
  - Stale spec/design/plan artifact that conflicts with a defensible implementation and should be aligned before closeout
266
267
 
268
+ **Medium** (default fix before pass)
269
+
270
+ - P2 requirements with meaningful behavior or quality impact
271
+ - Moderate maintainability or testability issues
272
+ - Contract gaps that can cause future regressions
273
+
267
274
  **Minor** (fix if time permits)
268
275
 
269
276
  - P2 requirements
@@ -295,6 +302,7 @@ oat_generated: true
295
302
  oat_generated_at: YYYY-MM-DD
296
303
  oat_review_scope: { scope }
297
304
  oat_review_type: { code|artifact|analysis }
305
+ oat_review_invocation: { manual|auto|gate }
298
306
  oat_project: { project-path }
299
307
  ---
300
308
 
@@ -309,6 +317,8 @@ oat_project: { project-path }
309
317
 
310
318
  {2-3 sentence summary of findings}
311
319
 
320
+ Findings: {N} critical, {N} important, {N} medium, {N} minor
321
+
312
322
  ## Findings
313
323
 
314
324
  ### Critical
@@ -324,6 +334,14 @@ oat_project: { project-path }
324
334
 
325
335
  {If none: "None"}
326
336
 
337
+ - **{Finding title}** (`{file}:{line}`)
338
+ - Issue: {description}
339
+ - Fix: {specific guidance}
340
+
341
+ ### Medium
342
+
343
+ {If none: "None"}
344
+
327
345
  - **{Finding title}** (`{file}:{line}`)
328
346
  - Issue: {description}
329
347
  - Fix: {specific guidance}
@@ -367,6 +385,8 @@ Run the `oat-project-review-receive` skill to convert findings into plan tasks.
367
385
 
368
386
  ```
369
387
 
388
+ Gate parsing contract: artifact-mode reviews, including reviews spawned by `oat gate review`, MUST include either the complete `Findings: {N} critical, {N} important, {N} medium, {N} minor` count line or the standard `## Findings` sections shown above with every severity subsection present.
389
+
370
390
 
371
391
  ### Step 9: Return Confirmation
372
392
 
@@ -380,7 +400,7 @@ Format:
380
400
  ## Review Complete
381
401
 
382
402
  **Scope:** {scope}
383
- **Findings:** {N} critical, {N} important, {N} minor
403
+ **Findings:** {N} critical, {N} important, {N} medium, {N} minor
384
404
  **Review artifact:** {path}
385
405
 
386
406
  Return to your main session and run the `oat-project-review-receive` skill.
@@ -113,13 +113,19 @@ Available commands:
113
113
  - `oat gate resolve <skill>` - inspect the resolved gate config for one skill
114
114
  - `oat gate set <skill>` / `oat gate unset <skill>` - write or clear a skill gate
115
115
  - `oat gate target set <id>` / `oat gate target unset <id>` - write or clear an exec target
116
+ - `oat gate review <prompt...>` - run a review-specific gate and map blocking review findings to exit status
116
117
  - `oat gate cross-provider-exec <prompt...>` - select an available exec target and run the prompt there
117
118
 
118
- The most common command is `oat gate cross-provider-exec`, which avoids the
119
- current runtime by default so a review gate can run in a fresh Codex, Claude, or
120
- Cursor runtime. Target commands are stored as JSON argv arrays, so configure
121
- them with `oat gate target set --base-command-json ...` rather than
122
- `oat config set`.
119
+ Use `oat gate review` for OAT review gates. It keeps the normal stateful
120
+ review-provider workflow: the review artifact, Reviews row update, and review
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.
123
+
124
+ Use `oat gate cross-provider-exec` for generic cross-runtime execution. It
125
+ avoids the current runtime by default, chooses a fresh Codex, Claude, or Cursor
126
+ target when available, and exits with the child process status. Target commands
127
+ are stored as JSON argv arrays, so configure them with
128
+ `oat gate target set --base-command-json ...` rather than `oat config set`.
123
129
 
124
130
  For schema, examples, and current V1 limits, see [Workflow Gates](workflow-gates.md).
125
131
 
@@ -7,11 +7,14 @@ description: Configure per-skill final commands and cross-runtime review dispatc
7
7
 
8
8
  Workflow gates let a gate-aware skill run a configured final command before it
9
9
  is considered done. The common use is independent review: one runtime performs
10
- the work, then `oat gate cross-provider-exec` dispatches a prompt to another
11
- runtime such as Codex or Claude.
10
+ the work, then `oat gate review` dispatches a normal OAT review to another
11
+ runtime such as Codex or Claude and maps blocking review findings to the gate
12
+ exit status.
12
13
 
13
- V1 gates are deliberately thin. A gate command passes or fails by exit code, and
14
- the skill owns what to do next.
14
+ `oat gate cross-provider-exec` remains the generic child-status executor. It
15
+ selects an exec target, runs the prompt, and exits with the child process
16
+ status. Use `oat gate review` when the command is specifically an OAT review
17
+ gate that should inspect the produced review artifact.
15
18
 
16
19
  ## Gate config
17
20
 
@@ -23,7 +26,7 @@ Gate config lives under `workflow.gates.skills` and is keyed by skill name.
23
26
  "gates": {
24
27
  "skills": {
25
28
  "oat-project-implement": {
26
- "command": "oat gate cross-provider-exec \"Use oat-project-review-provide to review the current project\"",
29
+ "command": "oat gate review \"Use oat-project-review-provide code final to review the current project\"",
27
30
  "description": "Run a fresh-runtime final review before implementation is considered done.",
28
31
  "onFailure": "block",
29
32
  "maxAttempts": 2
@@ -41,11 +44,33 @@ Fields:
41
44
  - `description` - optional context for the agent running the gate.
42
45
  - `maxAttempts` - retry bound for `block`; defaults to `2`.
43
46
 
44
- `oat-project-plan` and `oat-project-implement` are currently gate-aware. Gate
45
- awareness is declared in skill frontmatter with `oat_gateable: true`, and
47
+ `oat-project-plan`, `oat-project-implement`, `oat-project-quick-start`, and
48
+ `oat-project-import-plan` are currently gate-aware. Gate awareness is declared
49
+ in skill frontmatter with `oat_gateable: true`, and
46
50
  `oat internal validate-oat-skills` warns when config targets a missing or
47
51
  non-gateable skill.
48
52
 
53
+ ## Review gates
54
+
55
+ `oat gate review` is intentionally stateful. It is equivalent to running
56
+ `oat-project-review-provide` in another terminal or provider, so the normal
57
+ review side effects are expected:
58
+
59
+ - a review artifact is written under the project reviews directory
60
+ - the plan Reviews row is updated
61
+ - review bookkeeping commits are expected when the review workflow changes
62
+ tracked state
63
+
64
+ Gate-produced review artifacts use `oat_review_invocation: gate` in
65
+ frontmatter. After a gate review reports a produced artifact, the host must run
66
+ or hand off to `oat-project-review-receive` before treating the review as
67
+ dispositioned. Until receive runs, the artifact is only produced, not consumed.
68
+
69
+ `oat gate review` parses the produced artifact and returns a blocking exit
70
+ status when the configured threshold is met. `cross-provider-exec` does not do
71
+ that interpretation; for generic prompts it still returns only the child process
72
+ status.
73
+
49
74
  ## Exec targets
50
75
 
51
76
  `oat gate cross-provider-exec` chooses from `workflow.gates.execTargets`.
@@ -55,11 +80,16 @@ id.
55
80
 
56
81
  Built-in targets:
57
82
 
58
- | Target id | Runtime | Base command | Current-host detector |
59
- | ---------------- | -------- | ----------------------------------- | --------------------------------------- |
60
- | `codex-default` | `codex` | `["codex", "exec"]` | `CODEX_THREAD_ID` or `CODEX_SESSION_ID` |
61
- | `claude-default` | `claude` | `["claude", "-p"]` | `CLAUDECODE` |
62
- | `cursor-default` | `cursor` | `["cursor-agent", "-p", "--force"]` | `CURSOR_AGENT` |
83
+ | Target id | Runtime | Base command | Current-host detector |
84
+ | ---------------- | -------- | ------------------------ | --------------------------------------- |
85
+ | `codex-default` | `codex` | `["codex", "exec"]` | `CODEX_THREAD_ID` or `CODEX_SESSION_ID` |
86
+ | `claude-default` | `claude` | `["claude", "-p"]` | `CLAUDECODE` |
87
+ | `cursor-default` | `cursor` | `["cursor-agent", "-p"]` | `CURSOR_AGENT` |
88
+
89
+ Built-in targets are conservative defaults. Trusted noninteractive gates that
90
+ need to run tools without hanging on provider approval prompts should be
91
+ configured deliberately in `workflow.gates.execTargets`; OAT should not bake
92
+ dangerous provider permission flags into built-ins.
63
93
 
64
94
  Config layers can partially override a target or disable it with `null`. Use the
65
95
  dedicated target writer instead of `oat config set` because target commands are
@@ -71,12 +101,54 @@ oat gate target set claude-opus \
71
101
  --base-command-json '["claude","-p","--model","opus"]' \
72
102
  --availability-json '["claude","--version"]' \
73
103
  --priority 90 \
74
- --user
104
+ --layer user
75
105
  ```
76
106
 
77
107
  JSON argv is intentional: provider commands often contain flags such as `-p` or
78
108
  `--model`, which would be ambiguous as variadic CLI options.
79
109
 
110
+ ### Trusted target examples
111
+
112
+ Use these examples only in trusted environments where the provider process may
113
+ read files, run tools, and write review bookkeeping without an interactive
114
+ approval prompt. They are user-level target configuration, not built-in OAT
115
+ defaults.
116
+
117
+ Claude's default permission mode can block on
118
+ `Skill(oat-project-review-provide)`, `oat`, `pnpm`, and shell or tool calls. A
119
+ trusted user can opt into `--dangerously-skip-permissions` or the equivalent
120
+ `--permission-mode bypassPermissions` behavior in their target command.
121
+
122
+ Codex trusted gate automation should make sandbox and approval bypass explicit
123
+ with `--dangerously-bypass-approvals-and-sandbox`, even when the user's current
124
+ default profile already permits the needed commands.
125
+
126
+ Cursor trusted automation should use `--force`; `--yolo` is the documented alias
127
+ for the same behavior.
128
+
129
+ ```bash
130
+ oat gate target set codex-5.5-xhigh \
131
+ --runtime codex \
132
+ --base-command-json '["codex","exec","--model","gpt-5.5","-c","model_reasoning_effort=\"xhigh\"","--dangerously-bypass-approvals-and-sandbox"]' \
133
+ --availability-json '["codex","--version"]' \
134
+ --priority 120 \
135
+ --layer user
136
+
137
+ oat gate target set claude-opus-skip-permissions \
138
+ --runtime claude \
139
+ --base-command-json '["claude","-p","--model","opus","--dangerously-skip-permissions"]' \
140
+ --availability-json '["claude","--version"]' \
141
+ --priority 115 \
142
+ --layer user
143
+
144
+ oat gate target set cursor-force \
145
+ --runtime cursor \
146
+ --base-command-json '["cursor-agent","-p","--force"]' \
147
+ --availability-json '["cursor-agent","--version"]' \
148
+ --priority 90 \
149
+ --layer user
150
+ ```
151
+
80
152
  ## Command surface
81
153
 
82
154
  Inspect a skill gate:
@@ -89,13 +161,13 @@ Set or clear a skill gate:
89
161
 
90
162
  ```bash
91
163
  oat gate set oat-project-implement \
92
- --command 'oat gate cross-provider-exec "Use oat-project-review-provide to review the current project"' \
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"' \
93
165
  --description "Run final review in another runtime" \
94
166
  --on-failure block \
95
167
  --max-attempts 2 \
96
- --user
168
+ --layer user
97
169
 
98
- oat gate unset oat-project-implement --user
170
+ oat gate unset oat-project-implement --layer user
99
171
  ```
100
172
 
101
173
  Set or clear an exec target:
@@ -106,12 +178,24 @@ oat gate target set codex-high \
106
178
  --base-command-json '["codex","exec","--model","gpt-5.5"]' \
107
179
  --availability-json '["codex","--version"]' \
108
180
  --priority 90 \
109
- --user
181
+ --layer user
182
+
183
+ oat gate target unset codex-high --layer user
184
+ ```
110
185
 
111
- oat gate target unset codex-high --user
186
+ Dispatch a review through the target registry:
187
+
188
+ ```bash
189
+ oat gate review --target codex-5.5-xhigh \
190
+ --review-type code \
191
+ --review-scope final \
192
+ "Use oat-project-review-provide code final to review the current project"
112
193
  ```
113
194
 
114
- Dispatch a prompt through the target registry:
195
+ Leaving `--target` unset lets target priority choose the highest-priority
196
+ available non-host runtime.
197
+
198
+ Dispatch a generic prompt through the target registry:
115
199
 
116
200
  ```bash
117
201
  oat gate cross-provider-exec "Use oat-project-review-provide to review the current project"
@@ -14,7 +14,7 @@ The CLI is also a standalone value path. You can use `oat init`, `oat sync`, `oa
14
14
  - [CLI Bootstrap](../cli-utilities/bootstrap.md) - Bootstrap a repo with `oat init`, guided setup, and initial provider adoption.
15
15
  - [Tool Packs](../cli-utilities/tool-packs.md) - Install, update, inspect, and remove bundled OAT skills and agents.
16
16
  - [Config and Local State](../cli-utilities/config-and-local-state.md) - Config, backlog, local paths, diagnostics, and related utility commands.
17
- - [Workflow Gates](../cli-utilities/workflow-gates.md) - Per-skill final commands and cross-runtime review dispatch.
17
+ - [Workflow Gates](../cli-utilities/workflow-gates.md) - Per-skill final commands, review gates, and cross-runtime prompt dispatch.
18
18
  - [Docs Tooling Commands](../docs-tooling/commands.md) - Docs app scaffolding, migration, index generation, and nav sync.
19
19
  - [Provider Sync](../provider-sync/index.md) - Sync behavior, provider capabilities, config, and drift management.
20
20
  - [Agentic Workflows](../workflows/index.md) - Tracked project execution, skills, ideas, and workflow routing.
@@ -46,7 +46,7 @@ The first practical expansion path is to keep improving the existing owners: [Do
46
46
  | `oat decision ...` | Create, index, and migrate file-per-record repo decisions under `reference/decisions/` (`init`, `new`, `regenerate-index`, `migrate`). | [Config and Local State](../cli-utilities/config-and-local-state.md#oat-decision-) |
47
47
  | `oat backlog ...` / `oat local ...` | File-backed backlog helpers, local path sync, and local-only operational support. | [Config and Local State](../cli-utilities/config-and-local-state.md) |
48
48
  | `oat config ...` / `oat instructions ...` | Config discovery, source-aware config dumps, supported mutations, and instruction-integrity helpers. | [Config and Local State](../cli-utilities/config-and-local-state.md) |
49
- | `oat gate ...` | Per-skill final gate config, exec-target registry writes, and cross-runtime prompt dispatch. | [Workflow Gates](../cli-utilities/workflow-gates.md) |
49
+ | `oat gate ...` | Per-skill final gate config, review-specific gate execution, exec-target registry writes, and cross-runtime prompt dispatch. | [Workflow Gates](../cli-utilities/workflow-gates.md) |
50
50
  | `oat state ...` / `oat index ...` / `internal` | Repo dashboard refresh, repo indexing, validation helpers, and diagnostics. | [Config and Local State](../cli-utilities/config-and-local-state.md) |
51
51
  | `oat docs ...` | Docs app bootstrap, migration, index generation, nav sync, and docs workflow entrypoints. | [Docs Tooling Commands](../docs-tooling/commands.md) |
52
52
  | `oat status` / `oat sync` / `oat providers ...` | Provider sync, drift inspection, provider configuration, and adoption behavior. | [Provider Sync](../provider-sync/index.md) |
@@ -68,7 +68,8 @@ Notable commands introduced in the current CLI surface:
68
68
  - `oat repo archive sync [project-name]` - hydrate archived project snapshots from the configured repo-scoped S3 archive into `.oat/projects/archived/`. The old `oat project archive sync` path remains as a deprecated shim.
69
69
  - `oat project validate-plan --project-path <path>` - validates `oat_plan_parallel_groups` metadata in `plan.md`; exits non-zero on invalid. See [Implementation Execution](../workflows/projects/implementation-execution.md#validating-plan-metadata).
70
70
  - `oat project set-mode` — deprecated no-op. Execution mode is no longer user-selectable; emits a deprecation warning and preserves the `--json` contract.
71
- - `oat gate cross-provider-exec <prompt...>` - choose an available exec target while avoiding the current runtime by default, then run the prompt with the chosen target's configured base command.
71
+ - `oat gate review <prompt...>` - run a stateful OAT review through the target registry, parse the produced review artifact, and exit nonzero for configured blocking findings; produced gate reviews use `oat_review_invocation: gate` and still require `oat-project-review-receive` handoff before they are dispositioned.
72
+ - `oat gate cross-provider-exec <prompt...>` - choose an available exec target while avoiding the current runtime by default, then run the prompt with the chosen target's configured base command and exit with the child status.
72
73
 
73
74
  ## `oat config` surface flags
74
75
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli": "0.1.35",
3
- "docs-config": "0.1.35",
4
- "docs-theme": "0.1.35",
5
- "docs-transforms": "0.1.35"
2
+ "cli": "0.1.36",
3
+ "docs-config": "0.1.36",
4
+ "docs-theme": "0.1.36",
5
+ "docs-transforms": "0.1.36"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-project-implement
3
- version: 2.0.21
3
+ version: 2.0.22
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]'
@@ -1434,12 +1434,16 @@ Before reporting this skill as complete, run the configured gate as the final st
1434
1434
 
1435
1435
  2. If a gate config is returned, run its `command` exactly as configured. Capture stdout, stderr, and the exit code. A zero exit code means the gate passed and the skill is complete.
1436
1436
 
1437
- 3. If the command exits nonzero, use `description` to orient the next steps and handle `onFailure`:
1437
+ 3. Review-artifact handoff:
1438
+ - If the gate reports a produced review artifact, the host must run `oat-project-review-receive` to receive and disposition that artifact before treating the review as consumed.
1439
+ - This applies to `oat gate review ...` outputs regardless of whether the gate ultimately exits zero or nonzero; the command output owns the exact artifact path, and receive-review owns disposition and archival.
1440
+
1441
+ 4. If the command exits nonzero, use `description` to orient the next steps and handle `onFailure`:
1438
1442
  - `block`: read gate feedback, remediate, and re-run the gate up to `maxAttempts` attempts (default `2`). If attempts are exhausted, escalate to the human with accumulated feedback and append that feedback to `implementation.md`. Treat a launch failure, missing CLI, or no eligible runtime as escalation-biased and do not spend it as a remediation attempt.
1439
1443
  - `prompt`: surface the gate failure and ask the human how to proceed.
1440
1444
  - `warn`: record the gate failure and continue.
1441
1445
 
1442
- 4. 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 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.
1443
1447
 
1444
1448
  ## Success Criteria
1445
1449
 
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  name: oat-project-import-plan
3
- version: 1.4.0
3
+ version: 1.4.1
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
+ oat_gateable: true
6
7
  disable-model-invocation: true
7
8
  user-invocable: true
8
9
  allowed-tools: Read, Write, Bash, Glob, Grep, AskUserQuestion
@@ -56,14 +57,15 @@ When executing this skill, provide lightweight progress feedback so the user can
56
57
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57
58
 
58
59
  - Before multi-step work, print step indicators, e.g.:
59
- - `[0/7] Checking inherited git state...`
60
- - `[1/7] Resolving project + source plan…`
61
- - `[2/7] Preserving imported source…`
62
- - `[3/7] Normalizing plan to OAT task structure…`
63
- - `[4/7] Updating plan metadata…`
64
- - `[5/7] Running import-aware plan review…`
65
- - `[6/7] Updating project state + dashboard…`
66
- - `[7/7] Ensuring implementation tracker…`
60
+ - `[0/8] Checking inherited git state...`
61
+ - `[1/8] Resolving project + source plan…`
62
+ - `[2/8] Preserving imported source…`
63
+ - `[3/8] Normalizing plan to OAT task structure…`
64
+ - `[4/8] Updating plan metadata…`
65
+ - `[5/8] Running import-aware plan review…`
66
+ - `[6/8] Updating project state + dashboard…`
67
+ - `[7/8] Ensuring implementation tracker + committing…`
68
+ - `[8/8] Running configured gate…`
67
69
 
68
70
  ## Process
69
71
 
@@ -255,6 +257,46 @@ If missing, scaffold from template:
255
257
 
256
258
  Initialize pointer to first plan task ID.
257
259
 
260
+ ### Step 6.5: Commit Import Artifacts
261
+
262
+ After the import-aware plan artifact review, project state sync, dashboard refresh, and implementation tracker setup, stage and commit the changed import artifacts before handing off to implementation or stopping.
263
+
264
+ ```bash
265
+ git add "$PROJECT_PATH/references/"
266
+ for path in \
267
+ "$PROJECT_PATH/plan.md" \
268
+ "$PROJECT_PATH/implementation.md" \
269
+ "$PROJECT_PATH/state.md"; do
270
+ [ -e "$path" ] && git add "$path"
271
+ done
272
+ git diff --cached --quiet || git commit -m "chore(oat): update imported plan artifacts for {project-name}"
273
+ ```
274
+
275
+ ### Gate Execution
276
+
277
+ Before reporting this skill as complete, run the configured gate as the final step after artifact review, state sync, dashboard refresh, and the import artifact commit:
278
+
279
+ 1. Resolve the gate for this skill:
280
+
281
+ ```bash
282
+ oat gate resolve <this-skill> --json
283
+ ```
284
+
285
+ If the command returns JSON `null`, no gate is configured; the skill is complete.
286
+
287
+ 2. If a gate config is returned, run its `command` exactly as configured. Capture stdout, stderr, and the exit code. A zero exit code means the gate passed and the skill is complete.
288
+
289
+ 3. Review-artifact handoff:
290
+ - If the gate reports a produced review artifact, the host must run `oat-project-review-receive` to receive and disposition that artifact before treating the review as consumed.
291
+ - This applies to `oat gate review ...` outputs regardless of whether the gate ultimately exits zero or nonzero; the command output owns the exact artifact path, and receive-review owns disposition and archival.
292
+
293
+ 4. If the command exits nonzero, use `description` to orient the next steps and handle `onFailure`:
294
+ - `block`: read gate feedback, remediate, and re-run the gate up to `maxAttempts` attempts (default `2`). If attempts are exhausted, escalate to the human with accumulated feedback and append that feedback to `implementation.md`. Treat a launch failure, missing CLI, or no eligible runtime as escalation-biased and do not spend it as a remediation attempt.
295
+ - `prompt`: surface the gate failure and ask the human how to proceed.
296
+ - `warn`: record the gate failure and continue.
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.
299
+
258
300
  ### Step 7: Output Next Action
259
301
 
260
302
  Report:
@@ -280,3 +322,5 @@ Report:
280
322
  - ✅ `## Planning Checklist` items left unchecked (HiLL configuration deferred to implementation).
281
323
  - ✅ `activeProject` in `.oat/config.local.json` points to the imported project.
282
324
  - ✅ `.oat/state.md` has been refreshed locally after pointer update; it is not staged or committed.
325
+ - ✅ Changed import artifacts are committed before handoff or pause.
326
+ - ✅ Configured gate has run, and any produced review artifact has been handed off to `oat-project-review-receive` before it is treated as consumed.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-project-plan
3
- version: 1.3.6
3
+ version: 1.3.7
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
@@ -516,12 +516,16 @@ Before reporting this skill as complete, run the configured gate as the final st
516
516
 
517
517
  2. If a gate config is returned, run its `command` exactly as configured. Capture stdout, stderr, and the exit code. A zero exit code means the gate passed and the skill is complete.
518
518
 
519
- 3. If the command exits nonzero, use `description` to orient the next steps and handle `onFailure`:
519
+ 3. Review-artifact handoff:
520
+ - If the gate reports a produced review artifact, the host must run `oat-project-review-receive` to receive and disposition that artifact before treating the review as consumed.
521
+ - This applies to `oat gate review ...` outputs regardless of whether the gate ultimately exits zero or nonzero; the command output owns the exact artifact path, and receive-review owns disposition and archival.
522
+
523
+ 4. If the command exits nonzero, use `description` to orient the next steps and handle `onFailure`:
520
524
  - `block`: read gate feedback, remediate, and re-run the gate up to `maxAttempts` attempts (default `2`). If attempts are exhausted, escalate to the human with accumulated feedback and append that feedback to `implementation.md`. Treat a launch failure, missing CLI, or no eligible runtime as escalation-biased and do not spend it as a remediation attempt.
521
525
  - `prompt`: surface the gate failure and ask the human how to proceed.
522
526
  - `warn`: record the gate failure and continue.
523
527
 
524
- 4. 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 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.
525
529
 
526
530
  ## Success Criteria
527
531
 
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  name: oat-project-quick-start
3
- version: 2.1.6
3
+ version: 2.1.7
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
+ oat_gateable: true
6
7
  disable-model-invocation: true
7
8
  user-invocable: true
8
9
  allowed-tools: Read, Write, Bash, Glob, Grep, AskUserQuestion
@@ -57,14 +58,15 @@ When executing this skill, provide lightweight progress feedback so the user can
57
58
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58
59
 
59
60
  - Before multi-step work, print step indicators, e.g.:
60
- - `[0/7] Checking inherited git state...`
61
- - `[1/7] Scaffolding quick-mode project…`
62
- - `[2/7] Exploring solution space + capturing discovery…`
63
- - `[3/7] Decision point: design depth…`
64
- - `[4/7] Generating execution plan…`
65
- - `[5/7] Running plan artifact review…`
66
- - `[6/7] Initializing implementation tracker…`
67
- - `[7/7] Refreshing dashboard…`
61
+ - `[0/8] Checking inherited git state...`
62
+ - `[1/8] Scaffolding quick-mode project…`
63
+ - `[2/8] Exploring solution space + capturing discovery…`
64
+ - `[3/8] Decision point: design depth…`
65
+ - `[4/8] Generating execution plan…`
66
+ - `[5/8] Running plan artifact review…`
67
+ - `[6/8] Initializing implementation tracker…`
68
+ - `[7/8] Refreshing dashboard + committing…`
69
+ - `[8/8] Running configured gate…`
68
70
  - _(If lightweight design is chosen, insert design steps between 3 and 4)_
69
71
 
70
72
  ## Artifact Persistence (Required)
@@ -590,6 +592,31 @@ done
590
592
  git diff --cached --quiet || git commit -m "chore(oat): update quick-start artifacts for {project-name}"
591
593
  ```
592
594
 
595
+ ### Gate Execution
596
+
597
+ Before reporting this skill as complete, run the configured gate as the final step after artifact review, state sync, dashboard refresh, and the quick-start artifact commit:
598
+
599
+ 1. Resolve the gate for this skill:
600
+
601
+ ```bash
602
+ oat gate resolve <this-skill> --json
603
+ ```
604
+
605
+ If the command returns JSON `null`, no gate is configured; the skill is complete.
606
+
607
+ 2. If a gate config is returned, run its `command` exactly as configured. Capture stdout, stderr, and the exit code. A zero exit code means the gate passed and the skill is complete.
608
+
609
+ 3. Review-artifact handoff:
610
+ - If the gate reports a produced review artifact, the host must run `oat-project-review-receive` to receive and disposition that artifact before treating the review as consumed.
611
+ - This applies to `oat gate review ...` outputs regardless of whether the gate ultimately exits zero or nonzero; the command output owns the exact artifact path, and receive-review owns disposition and archival.
612
+
613
+ 4. If the command exits nonzero, use `description` to orient the next steps and handle `onFailure`:
614
+ - `block`: read gate feedback, remediate, and re-run the gate up to `maxAttempts` attempts (default `2`). If attempts are exhausted, escalate to the human with accumulated feedback and append that feedback to `implementation.md`. Treat a launch failure, missing CLI, or no eligible runtime as escalation-biased and do not spend it as a remediation attempt.
615
+ - `prompt`: surface the gate failure and ask the human how to proceed.
616
+ - `warn`: record the gate failure and continue.
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.
619
+
593
620
  ### Step 7: Output Next Action
594
621
 
595
622
  Report:
@@ -611,3 +638,4 @@ Report:
611
638
  - ✅ `plan.md` records the plan artifact review row unless `workflow.autoArtifactReview.plan` was explicitly disabled.
612
639
  - ✅ `implementation.md` is initialized for resumable execution.
613
640
  - ✅ Changed quick-start artifacts are committed before handoff or pause; `.oat/state.md` is refreshed locally when available.
641
+ - ✅ Configured gate has run, and any produced review artifact has been handed off to `oat-project-review-receive` before it is treated as consumed.
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: oat-project-review-provide
3
- version: 1.3.7
3
+ version: 1.3.8
4
4
  description: Use when the user explicitly asks to review an OAT project — e.g. "review project", "review the project", "run project review", or confirms a previously offered review. Do NOT auto-invoke on completed work alone. Resolves a project review scope and offers before running.
5
5
  disable-model-invocation: false
6
6
  user-invocable: true
7
- allowed-tools: Read, Glob, Grep, Bash(git:*), AskUserQuestion
7
+ allowed-tools: Read, Glob, Grep, Write, Edit, Bash(git:*), Bash(oat:*), Bash(pnpm:*), Bash(mkdir:*), Bash(date:*), Bash(realpath:*), Bash(awk:*), AskUserQuestion
8
8
  ---
9
9
 
10
10
  # Request Review
@@ -639,7 +639,7 @@ oat_generated: true
639
639
  oat_generated_at: { today }
640
640
  oat_review_scope: { scope }
641
641
  oat_review_type: { code|artifact }
642
- oat_review_invocation: { manual|auto }
642
+ oat_review_invocation: { manual|auto|gate }
643
643
  oat_project: { PROJECT_PATH }
644
644
  ---
645
645
 
@@ -655,13 +655,21 @@ oat_project: { PROJECT_PATH }
655
655
 
656
656
  - `manual` (default): Review was manually triggered by the user. `oat-project-review-receive` uses standard disposition behavior (user prompts for triage, minors auto-deferred for non-final scopes).
657
657
  - `auto`: Review was spawned by the auto-review checkpoint trigger in `oat-project-implement`. `oat-project-review-receive` uses relaxed disposition: minors are auto-converted to fix tasks (not deferred), no user prompts for disposition decisions.
658
+ - `gate`: Review was spawned by `oat gate review`. Gate-originated reviews use normal stateful review-provide behavior: write the review artifact, update the `## Reviews` row, and commit review bookkeeping. `oat-project-review-receive` treats gate reviews with the same standard disposition behavior as manual reviews unless a future implementation explicitly designs autonomous receive.
658
659
 
659
- When `oat-project-implement` spawns this skill for auto-review at checkpoints, it passes context indicating auto invocation. Set `oat_review_invocation: auto` in the artifact frontmatter. For all other invocations (user-triggered, fresh session), use `manual`.
660
+ When `oat-project-implement` spawns this skill for auto-review at checkpoints, it passes context indicating auto invocation. Set `oat_review_invocation: auto` in the artifact frontmatter. When `oat gate review` spawns this skill, set `oat_review_invocation: gate`. For all other invocations (user-triggered, fresh session), use `manual`.
661
+
662
+ Gate parsing contract:
663
+
664
+ - Include either the `Findings: {N} critical, {N} important, {N} medium, {N} minor` summary line or the standard `## Findings` section with `### Critical`, `### Important`, `### Medium`, and `### Minor` subsections populated with findings or `None`.
665
+ - Do not omit severity headings merely because a severity has zero findings; `oat gate review` depends on counts or standard Findings sections to determine whether the review blocks.
660
666
 
661
667
  ## Summary
662
668
 
663
669
  {2-3 sentence summary}
664
670
 
671
+ Findings: {N} critical, {N} important, {N} medium, {N} minor
672
+
665
673
  ## Findings
666
674
 
667
675
  ### Critical
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-project-review-receive
3
- version: 1.5.3
3
+ version: 1.5.4
4
4
  description: Use when the user explicitly asks to receive review findings for an OAT project — e.g. "receive review", "process review", "process the project review", or confirms a previously offered review-receive step. Do NOT auto-invoke merely because a review file exists. Resolves the latest review and offers before acting.
5
5
  disable-model-invocation: false
6
6
  user-invocable: true
@@ -274,6 +274,8 @@ Read `oat_review_type` and `oat_review_invocation` from review artifact frontmat
274
274
  - **No user prompts for disposition decisions.** The auto-review path runs fully autonomously.
275
275
  - Genuinely ambiguous findings (e.g., a medium the agent disagrees with) are deferred with a note explaining why, rather than pausing for interactive resolution.
276
276
  - Follow the task-conversion flow in Steps 3-10 with these adjusted defaults.
277
+ - If `oat_review_type == code` AND `oat_review_invocation == gate`:
278
+ - **Gate review mode.** This review was spawned by `oat gate review`, but receive remains standard disposition behavior. Treat `gate` the same as manual for prompts, finding conversion, artifact archival, and bookkeeping unless a future implementation explicitly designs an autonomous receive path.
277
279
  - If `oat_review_type == code` (manual or `oat_review_invocation` absent):
278
280
  - Follow the existing task-conversion flow in Steps 3-10 with standard disposition behavior.
279
281
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,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;AAEzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,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;AAmCD,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;AAKlD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAmWD,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;AA2SD,wBAAgB,iBAAiB,CAC/B,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAoIT"}
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,11 +1,16 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { join } from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { readdir, readFile } from 'node:fs/promises';
4
+ import { basename, isAbsolute, join, relative } from 'node:path';
3
5
  import { buildCommandContext, } from '../../app/command-context.js';
6
+ import { getFrontmatterBlock, getFrontmatterField, } from '../shared/frontmatter.js';
4
7
  import { readGlobalOptions } from '../shared/shared.utils.js';
5
8
  import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
6
9
  import { resolveEffectiveConfig, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
7
- import { resolveProjectRoot } from '../../fs/paths.js';
10
+ import { dirExists, fileExists } from '../../fs/io.js';
11
+ import { normalizeToPosixPath, resolveProjectRoot } from '../../fs/paths.js';
8
12
  import { Command } from 'commander';
13
+ import { parseReviewGateVerdict, } from './review-verdict.js';
9
14
  const DEFAULT_DEPENDENCIES = {
10
15
  buildCommandContext,
11
16
  resolveProjectRoot,
@@ -29,6 +34,16 @@ const VALID_CROSS_PROVIDER_AVOIDS = [
29
34
  'same-runtime',
30
35
  'none',
31
36
  ];
37
+ const VALID_REVIEW_GATE_THRESHOLDS = [
38
+ 'critical',
39
+ 'important',
40
+ 'medium',
41
+ 'minor',
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.';
44
+ function reviewGateProjectContext(projectPath) {
45
+ return `Resolved OAT project path: ${projectPath}. Run the review for this project path.`;
46
+ }
32
47
  async function runChildProcess(command, args, options) {
33
48
  return new Promise((resolve, reject) => {
34
49
  const child = spawn(command, args, {
@@ -90,6 +105,13 @@ function parseCrossProviderAvoid(value) {
90
105
  }
91
106
  return avoid;
92
107
  }
108
+ function parseReviewGateThreshold(value) {
109
+ const threshold = value?.trim() || 'important';
110
+ if (!VALID_REVIEW_GATE_THRESHOLDS.includes(threshold)) {
111
+ throw new Error('--exit-nonzero-on must be one of critical | important | medium | minor.');
112
+ }
113
+ return threshold;
114
+ }
93
115
  function parseOnFailure(value) {
94
116
  if (!value || !VALID_ON_FAILURE.includes(value)) {
95
117
  throw new Error('--on-failure must be one of block | prompt | warn.');
@@ -149,6 +171,15 @@ function parseGateConfig(options) {
149
171
  : {}),
150
172
  };
151
173
  }
174
+ function detectDevBuildGateCommandWarnings(command) {
175
+ const normalized = command.trim();
176
+ if (!/^node\s+(?:"[^"]*\/packages\/cli\/dist\/index\.js"|'[^']*\/packages\/cli\/dist\/index\.js'|\S*\/packages\/cli\/dist\/index\.js)\s+gate(?:\s|$)/.test(normalized)) {
177
+ return [];
178
+ }
179
+ return [
180
+ 'Durable docs/config should reference `oat gate ...`; absolute dev-build paths are reserved for local development of unmerged behavior.',
181
+ ];
182
+ }
152
183
  function parseExecTargetConfig(options) {
153
184
  if (options.disable) {
154
185
  return null;
@@ -299,6 +330,23 @@ async function selectAvailableExecTarget(registry, currentRuntime, avoid, contex
299
330
  }
300
331
  return null;
301
332
  }
333
+ async function resolveSelectedExecTarget(targets, options, context, dependencies) {
334
+ const explicitTarget = options.target?.trim();
335
+ if (explicitTarget) {
336
+ const target = targets[explicitTarget];
337
+ if (!target) {
338
+ throw new Error(`Unknown exec target "${explicitTarget}".`);
339
+ }
340
+ return { id: explicitTarget, target: cloneExecTarget(target) };
341
+ }
342
+ const avoid = parseCrossProviderAvoid(options.avoid);
343
+ const currentRuntime = await resolveCurrentRuntime(options.currentRuntime, context, dependencies);
344
+ const selected = await selectAvailableExecTarget(targets, currentRuntime, avoid, context, dependencies);
345
+ if (!selected) {
346
+ throw new Error(noEligibleTargetMessage(currentRuntime, avoid));
347
+ }
348
+ return selected;
349
+ }
302
350
  async function executeTarget(selected, prompt, context, dependencies) {
303
351
  const [command, baseArgs] = argvHead(selected.target.baseCommand);
304
352
  if (!command) {
@@ -321,6 +369,217 @@ async function executeTarget(selected, prompt, context, dependencies) {
321
369
  function noEligibleTargetMessage(currentRuntime, avoid) {
322
370
  return `No eligible gate exec target found for current runtime "${currentRuntime}" with --avoid ${avoid}. Install or configure an alternate runtime, rerun with --avoid none, or pin a target with --target <id>.`;
323
371
  }
372
+ function normalizeRepoRelativeProjectPath(repoRoot, pathValue) {
373
+ const trimmed = pathValue.trim().replace(/\/+$/, '');
374
+ const withoutState = trimmed.endsWith('/state.md')
375
+ ? trimmed.slice(0, -'/state.md'.length)
376
+ : trimmed;
377
+ const repoRelative = isAbsolute(withoutState)
378
+ ? relative(repoRoot, withoutState)
379
+ : withoutState;
380
+ const normalized = normalizeToPosixPath(repoRelative).replace(/^\.\//, '');
381
+ if (normalized === '..' || normalized.startsWith('../')) {
382
+ throw new Error(`Project path must resolve inside the current repository: ${pathValue}`);
383
+ }
384
+ return normalized;
385
+ }
386
+ async function listProjectCandidates(repoRoot, projectsRoot) {
387
+ const normalizedProjectsRoot = normalizeRepoRelativeProjectPath(repoRoot, projectsRoot);
388
+ const absoluteProjectsRoot = join(repoRoot, normalizedProjectsRoot);
389
+ let scopeEntries;
390
+ try {
391
+ scopeEntries = await readdir(absoluteProjectsRoot, { withFileTypes: true });
392
+ }
393
+ catch (error) {
394
+ if (typeof error === 'object' &&
395
+ error !== null &&
396
+ 'code' in error &&
397
+ error.code === 'ENOENT') {
398
+ return [];
399
+ }
400
+ throw error;
401
+ }
402
+ const candidates = (await Promise.all(scopeEntries
403
+ .filter((entry) => entry.isDirectory())
404
+ .map(async (scopeEntry) => {
405
+ const directProjectPath = normalizeToPosixPath(join(normalizedProjectsRoot, scopeEntry.name));
406
+ if (await fileExists(join(repoRoot, directProjectPath, 'state.md'))) {
407
+ return [directProjectPath];
408
+ }
409
+ const scopePath = join(absoluteProjectsRoot, scopeEntry.name);
410
+ const projectEntries = await readdir(scopePath, {
411
+ withFileTypes: true,
412
+ });
413
+ return Promise.all(projectEntries
414
+ .filter((entry) => entry.isDirectory())
415
+ .map(async (projectEntry) => {
416
+ const projectPath = normalizeToPosixPath(join(normalizedProjectsRoot, scopeEntry.name, projectEntry.name));
417
+ return (await fileExists(join(repoRoot, projectPath, 'state.md')))
418
+ ? projectPath
419
+ : null;
420
+ }));
421
+ })))
422
+ .flat()
423
+ .filter((candidate) => candidate !== null)
424
+ .sort();
425
+ return candidates;
426
+ }
427
+ async function assertProjectPath(repoRoot, projectPath, source) {
428
+ const normalizedPath = normalizeRepoRelativeProjectPath(repoRoot, projectPath);
429
+ const absolutePath = join(repoRoot, normalizedPath);
430
+ if (!(await dirExists(absolutePath)) ||
431
+ !(await fileExists(join(absolutePath, 'state.md')))) {
432
+ throw new Error(`${source} project "${projectPath}" does not resolve to a project directory containing state.md.`);
433
+ }
434
+ return normalizedPath;
435
+ }
436
+ async function resolveExplicitReviewProject(repoRoot, projectsRoot, projectValue) {
437
+ const trimmed = projectValue.trim();
438
+ if (!trimmed) {
439
+ throw new Error('--project must be a non-empty project path or name.');
440
+ }
441
+ const looksLikePath = trimmed.includes('/') || trimmed.startsWith('.') || isAbsolute(trimmed);
442
+ if (looksLikePath) {
443
+ return assertProjectPath(repoRoot, trimmed, '--project');
444
+ }
445
+ const candidates = await listProjectCandidates(repoRoot, projectsRoot);
446
+ const matches = candidates.filter((candidate) => basename(candidate) === trimmed);
447
+ if (matches.length === 1) {
448
+ return matches[0];
449
+ }
450
+ if (matches.length > 1) {
451
+ throw new Error(`Multiple OAT projects match --project "${trimmed}": ${matches.join(', ')}. Pass a full project path instead.`);
452
+ }
453
+ return assertProjectPath(repoRoot, `${projectsRoot}/${trimmed}`, '--project');
454
+ }
455
+ async function resolveReviewProject(options) {
456
+ const projectsRoot = String(options.effective.resolved['projects.root']?.value ??
457
+ options.effective.shared.projects?.root ??
458
+ '.oat/projects/shared');
459
+ if (options.project !== undefined) {
460
+ return resolveExplicitReviewProject(options.repoRoot, projectsRoot, options.project);
461
+ }
462
+ const activeProject = options.effective.local.activeProject?.trim();
463
+ if (activeProject) {
464
+ return assertProjectPath(options.repoRoot, activeProject, 'Active');
465
+ }
466
+ const candidates = await listProjectCandidates(options.repoRoot, projectsRoot);
467
+ if (candidates.length === 0) {
468
+ throw new Error('No OAT project could be resolved for gate review. Set an active project or pass --project <path-or-name>.');
469
+ }
470
+ if (candidates.length > 1) {
471
+ throw new Error(`Multiple OAT projects could be resolved for gate review: ${candidates.join(', ')}. Pass --project <path-or-name>.`);
472
+ }
473
+ return candidates[0];
474
+ }
475
+ function reviewGateLifecycleRank(scope) {
476
+ const normalizedScope = scope.trim().toLowerCase();
477
+ if (normalizedScope === 'final') {
478
+ return Number.MAX_SAFE_INTEGER;
479
+ }
480
+ const phases = [...normalizedScope.matchAll(/p(\d+)/g)].map((match) => Number.parseInt(match[1] ?? '0', 10));
481
+ if (phases.length === 0) {
482
+ return 0;
483
+ }
484
+ const tasks = [...normalizedScope.matchAll(/(?:^|[-_])t(\d+)/g)].map((match) => Number.parseInt(match[1] ?? '0', 10));
485
+ const latestPhase = Math.max(...phases);
486
+ const latestTask = tasks.length > 0 ? Math.max(...tasks) : 9999;
487
+ return latestPhase * 10_000 + latestTask;
488
+ }
489
+ async function readReviewGateArtifactCandidate(repoRoot, relativePath) {
490
+ const content = await readFile(join(repoRoot, relativePath), 'utf8');
491
+ const frontmatter = getFrontmatterBlock(content);
492
+ if (!frontmatter) {
493
+ return null;
494
+ }
495
+ const generatedAt = getFrontmatterField(frontmatter, 'oat_generated_at');
496
+ if (!generatedAt) {
497
+ return null;
498
+ }
499
+ const generatedTime = Date.parse(generatedAt);
500
+ if (Number.isNaN(generatedTime)) {
501
+ return null;
502
+ }
503
+ const scope = getFrontmatterField(frontmatter, 'oat_review_scope') ?? '';
504
+ return {
505
+ path: relativePath,
506
+ scope,
507
+ generatedAt,
508
+ kind: 'project',
509
+ archived: false,
510
+ actionable: true,
511
+ generatedTime,
512
+ lifecycleRank: reviewGateLifecycleRank(scope),
513
+ signature: createHash('sha256').update(content).digest('hex'),
514
+ };
515
+ }
516
+ function sortReviewGateArtifacts(left, right) {
517
+ if (left.generatedTime !== right.generatedTime) {
518
+ return right.generatedTime - left.generatedTime;
519
+ }
520
+ if (left.lifecycleRank !== right.lifecycleRank) {
521
+ return right.lifecycleRank - left.lifecycleRank;
522
+ }
523
+ return left.path.localeCompare(right.path);
524
+ }
525
+ async function listActiveProjectReviewCandidates(options) {
526
+ const projectPath = normalizeRepoRelativeProjectPath(options.repoRoot, options.projectPath);
527
+ const reviewsDir = `${projectPath}/reviews`;
528
+ let entries;
529
+ try {
530
+ entries = await readdir(join(options.repoRoot, reviewsDir), {
531
+ withFileTypes: true,
532
+ });
533
+ }
534
+ catch (error) {
535
+ if (typeof error === 'object' &&
536
+ error !== null &&
537
+ 'code' in error &&
538
+ error.code === 'ENOENT') {
539
+ return [];
540
+ }
541
+ throw error;
542
+ }
543
+ return (await Promise.all(entries
544
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
545
+ .map((entry) => readReviewGateArtifactCandidate(options.repoRoot, normalizeToPosixPath(join(reviewsDir, entry.name))))))
546
+ .filter((candidate) => candidate !== null)
547
+ .sort(sortReviewGateArtifacts);
548
+ }
549
+ function findProducedReviewArtifact(before, after) {
550
+ const beforeSignatures = new Map(before.map((candidate) => [candidate.path, candidate.signature]));
551
+ return (after.find((candidate) => beforeSignatures.get(candidate.path) !== candidate.signature) ?? null);
552
+ }
553
+ function reviewBlocksAtThreshold(verdict, threshold) {
554
+ if (verdict.counts.critical > 0) {
555
+ return true;
556
+ }
557
+ if (threshold === 'critical') {
558
+ return false;
559
+ }
560
+ if (verdict.counts.important > 0) {
561
+ return true;
562
+ }
563
+ if (threshold === 'important') {
564
+ return false;
565
+ }
566
+ if (verdict.counts.medium > 0) {
567
+ return true;
568
+ }
569
+ if (threshold === 'medium') {
570
+ return false;
571
+ }
572
+ return verdict.counts.minor > 0;
573
+ }
574
+ function writeReviewGateResult(context, payload) {
575
+ if (context.json) {
576
+ context.logger.json(payload);
577
+ return;
578
+ }
579
+ context.logger.info(`Review artifact: ${payload.artifactPath}`);
580
+ context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical}, important=${payload.counts.important}, medium=${payload.counts.medium}, minor=${payload.counts.minor})`);
581
+ context.logger.info(payload.handoff);
582
+ }
324
583
  async function updateConfigLayer(context, layer, dependencies, mutate) {
325
584
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
326
585
  const userConfigDir = join(context.home, '.oat');
@@ -352,8 +611,21 @@ async function runGateSet(skillName, options, context, dependencies) {
352
611
  const layer = parseLayer(options.layer);
353
612
  const normalizedSkill = trimRequired(skillName, '<skill>');
354
613
  const gate = parseGateConfig(options);
614
+ const warnings = gate
615
+ ? detectDevBuildGateCommandWarnings(gate.command)
616
+ : [];
355
617
  await updateConfigLayer(context, layer, dependencies, (config) => setSkillGate(config, normalizedSkill, gate));
356
- writeSuccess(context, { layer, skill: normalizedSkill, gate });
618
+ if (!context.json) {
619
+ for (const warning of warnings) {
620
+ context.logger.warn(warning);
621
+ }
622
+ }
623
+ writeSuccess(context, {
624
+ layer,
625
+ skill: normalizedSkill,
626
+ gate,
627
+ ...(warnings.length > 0 ? { warnings } : {}),
628
+ });
357
629
  process.exitCode = 0;
358
630
  }
359
631
  catch (error) {
@@ -401,22 +673,71 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
401
673
  try {
402
674
  const effective = await readEffectiveConfig(context, dependencies);
403
675
  const targets = resolveExecTargets(effective);
404
- const explicitTarget = options.target?.trim();
405
- if (explicitTarget) {
406
- const target = targets[explicitTarget];
407
- if (!target) {
408
- throw new Error(`Unknown exec target "${explicitTarget}".`);
409
- }
410
- process.exitCode = await executeTarget({ id: explicitTarget, target: cloneExecTarget(target) }, prompt, context, dependencies);
676
+ const selected = await resolveSelectedExecTarget(targets, options, context, dependencies);
677
+ process.exitCode = await executeTarget(selected, prompt, context, dependencies);
678
+ }
679
+ catch (error) {
680
+ writeError(context, error);
681
+ }
682
+ }
683
+ async function runReviewGate(prompt, options, context, dependencies) {
684
+ try {
685
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
686
+ const userConfigDir = join(context.home, '.oat');
687
+ const effective = await dependencies.resolveEffectiveConfig(repoRoot, userConfigDir, dependencies.processEnv);
688
+ const projectPath = await resolveReviewProject({
689
+ repoRoot,
690
+ effective,
691
+ project: options.project,
692
+ });
693
+ const targets = resolveExecTargets(effective);
694
+ const selected = await resolveSelectedExecTarget(targets, options, context, dependencies);
695
+ const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
696
+ const before = await listActiveProjectReviewCandidates({
697
+ repoRoot,
698
+ projectPath,
699
+ });
700
+ const reviewPrompt = [
701
+ REVIEW_GATE_CONTEXT_NOTE,
702
+ reviewGateProjectContext(projectPath),
703
+ ...(options.reviewType?.trim()
704
+ ? [`Review type: ${options.reviewType.trim()}.`]
705
+ : []),
706
+ ...(options.reviewScope?.trim()
707
+ ? [`Review scope: ${options.reviewScope.trim()}.`]
708
+ : []),
709
+ ...prompt,
710
+ ];
711
+ const childExitCode = await executeTarget(selected, reviewPrompt, context, dependencies);
712
+ if (childExitCode !== 0) {
713
+ process.exitCode = childExitCode;
411
714
  return;
412
715
  }
413
- const avoid = parseCrossProviderAvoid(options.avoid);
414
- const currentRuntime = await resolveCurrentRuntime(options.currentRuntime, context, dependencies);
415
- const selected = await selectAvailableExecTarget(targets, currentRuntime, avoid, context, dependencies);
416
- if (!selected) {
417
- throw new Error(noEligibleTargetMessage(currentRuntime, avoid));
716
+ const after = await listActiveProjectReviewCandidates({
717
+ repoRoot,
718
+ projectPath,
719
+ });
720
+ const producedArtifact = findProducedReviewArtifact(before, after);
721
+ 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.`);
418
723
  }
419
- process.exitCode = await executeTarget(selected, prompt, context, dependencies);
724
+ const verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path));
725
+ const blocking = reviewBlocksAtThreshold(verdict, threshold);
726
+ const handoff = `Run oat-project-review-receive for ${producedArtifact.path} before treating this gate review as consumed.`;
727
+ writeReviewGateResult(context, {
728
+ status: blocking ? 'blocked' : 'ok',
729
+ target: selected.id,
730
+ project: projectPath,
731
+ artifactPath: producedArtifact.path,
732
+ threshold,
733
+ blocking,
734
+ counts: verdict.counts,
735
+ reviewType: verdict.reviewType,
736
+ scope: verdict.scope,
737
+ invocation: verdict.invocation,
738
+ handoff,
739
+ });
740
+ process.exitCode = blocking ? 1 : 0;
420
741
  }
421
742
  catch (error) {
422
743
  writeError(context, error);
@@ -470,6 +791,21 @@ export function createGateCommand(overrides = {}) {
470
791
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
471
792
  await runCrossProviderExec(prompt, options, context, dependencies);
472
793
  });
794
+ cmd
795
+ .command('review')
796
+ .description('Run a review gate and map review findings to exit status')
797
+ .option('--target <id>', 'Run this exact exec target')
798
+ .option('--avoid <mode>', 'Avoidance mode: same-runtime or none')
799
+ .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
800
+ .option('--project <path-or-name>', 'Project path or name to review; defaults to the active project')
801
+ .option('--review-scope <scope>', 'Review scope hint for the provider')
802
+ .option('--review-type <type>', 'Review type hint for the provider')
803
+ .option('--exit-nonzero-on <severity>', 'Lowest severity that exits nonzero: critical, important, medium, or minor')
804
+ .argument('<prompt...>', 'Review prompt arguments appended to the target command')
805
+ .action(async (prompt, options, command) => {
806
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
807
+ await runReviewGate(prompt, options, context, dependencies);
808
+ });
473
809
  const target = new Command('target').description('Manage gate execution targets');
474
810
  target
475
811
  .command('set')
@@ -0,0 +1,15 @@
1
+ export interface ReviewGateVerdict {
2
+ artifactPath: string;
3
+ reviewType: 'code' | 'artifact' | 'unknown';
4
+ scope: string | null;
5
+ invocation: string | null;
6
+ counts: {
7
+ critical: number;
8
+ important: number;
9
+ medium: number;
10
+ minor: number;
11
+ };
12
+ blocking: boolean;
13
+ }
14
+ export declare function parseReviewGateVerdict(artifactPath: string): Promise<ReviewGateVerdict>;
15
+ //# sourceMappingURL=review-verdict.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,180 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { getFrontmatterBlock } from '../shared/frontmatter.js';
3
+ import YAML from 'yaml';
4
+ const SEVERITIES = [
5
+ 'critical',
6
+ 'important',
7
+ 'medium',
8
+ 'minor',
9
+ ];
10
+ const FRONTMATTER_COUNT_KEYS = {
11
+ critical: ['oat_review_critical_count', 'critical'],
12
+ important: ['oat_review_important_count', 'important'],
13
+ medium: ['oat_review_medium_count', 'medium'],
14
+ minor: ['oat_review_minor_count', 'minor'],
15
+ };
16
+ function normalizeReviewType(value) {
17
+ return value === 'code' || value === 'artifact' ? value : 'unknown';
18
+ }
19
+ function stringOrNull(value) {
20
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
21
+ }
22
+ function parseCountValue(value) {
23
+ const numberValue = typeof value === 'number'
24
+ ? value
25
+ : typeof value === 'string'
26
+ ? Number(value.trim())
27
+ : Number.NaN;
28
+ if (!Number.isInteger(numberValue) || numberValue < 0) {
29
+ return null;
30
+ }
31
+ return numberValue;
32
+ }
33
+ function readFrontmatterCounts(frontmatter) {
34
+ const nestedCounts = typeof frontmatter['oat_review_counts'] === 'object' &&
35
+ frontmatter['oat_review_counts'] !== null &&
36
+ !Array.isArray(frontmatter['oat_review_counts'])
37
+ ? frontmatter['oat_review_counts']
38
+ : null;
39
+ const counts = {
40
+ critical: 0,
41
+ important: 0,
42
+ medium: 0,
43
+ minor: 0,
44
+ };
45
+ for (const severity of SEVERITIES) {
46
+ const candidateValues = [
47
+ ...FRONTMATTER_COUNT_KEYS[severity].map((key) => frontmatter[key]),
48
+ ...(nestedCounts
49
+ ? FRONTMATTER_COUNT_KEYS[severity].map((key) => nestedCounts[key])
50
+ : []),
51
+ ].filter((value) => value !== undefined && value !== null);
52
+ if (candidateValues.length === 0) {
53
+ return null;
54
+ }
55
+ const parsedCount = candidateValues.reduce((parsed, value) => {
56
+ if (parsed !== null) {
57
+ return parsed;
58
+ }
59
+ return parseCountValue(value);
60
+ }, null);
61
+ if (parsedCount === null) {
62
+ return null;
63
+ }
64
+ counts[severity] = parsedCount;
65
+ }
66
+ return counts;
67
+ }
68
+ function sectionContentIsEmpty(content) {
69
+ const cleaned = content
70
+ .split('\n')
71
+ .map((line) => line.trim())
72
+ .filter((line) => line.length > 0 && !/^<!--.*-->$/.test(line))
73
+ .join('\n')
74
+ .trim();
75
+ return cleaned.length === 0 || /^none\.?$/i.test(cleaned);
76
+ }
77
+ function countFindingsInSection(content) {
78
+ if (sectionContentIsEmpty(content)) {
79
+ return 0;
80
+ }
81
+ return content
82
+ .split('\n')
83
+ .filter((line) => /^([-*+]\s+\S|\d+\.\s+\S)/.test(line)).length;
84
+ }
85
+ function normalizeHeading(value) {
86
+ const normalized = value.trim().toLowerCase();
87
+ if (SEVERITIES.includes(normalized)) {
88
+ return normalized;
89
+ }
90
+ return null;
91
+ }
92
+ function parseFindingsSummaryCounts(content) {
93
+ const match = content.match(/^Findings:\s*(\d+)\s+critical,\s*(\d+)\s+important,\s*(\d+)\s+medium,\s*(\d+)\s+minor\s*$/im);
94
+ if (!match) {
95
+ return null;
96
+ }
97
+ return {
98
+ critical: Number.parseInt(match[1] ?? '0', 10),
99
+ important: Number.parseInt(match[2] ?? '0', 10),
100
+ medium: Number.parseInt(match[3] ?? '0', 10),
101
+ minor: Number.parseInt(match[4] ?? '0', 10),
102
+ };
103
+ }
104
+ 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);
113
+ if (severityHeadings.length === 0) {
114
+ return null;
115
+ }
116
+ const seenSeverities = new Set(severityHeadings.map((heading) => heading.severity));
117
+ const missingSeverities = SEVERITIES.filter((severity) => !seenSeverities.has(severity));
118
+ if (missingSeverities.length > 0) {
119
+ throw new Error(`Review artifact at ${artifactPath} has an incomplete Findings section; expected headings for Critical, Important, Medium, and Minor. Missing: ${missingSeverities.join(', ')}.`);
120
+ }
121
+ const counts = {
122
+ critical: 0,
123
+ important: 0,
124
+ medium: 0,
125
+ minor: 0,
126
+ };
127
+ for (const [offset, heading] of severityHeadings.entries()) {
128
+ const nextHeading = severityHeadings[offset + 1];
129
+ const sectionStart = heading.index + heading.headingLength;
130
+ const sectionEnd = nextHeading?.index ?? content.length;
131
+ counts[heading.severity] = countFindingsInSection(content.slice(sectionStart, sectionEnd));
132
+ }
133
+ return counts;
134
+ }
135
+ function parseFrontmatterObject(frontmatter, artifactPath) {
136
+ try {
137
+ const parsed = YAML.parse(frontmatter);
138
+ if (typeof parsed === 'object' &&
139
+ parsed !== null &&
140
+ !Array.isArray(parsed)) {
141
+ return parsed;
142
+ }
143
+ throw new Error('frontmatter must be a YAML object');
144
+ }
145
+ catch (error) {
146
+ const detail = error instanceof Error ? error.message : String(error);
147
+ throw new Error(`Unable to parse review artifact frontmatter at ${artifactPath}: ${detail}`, { cause: error });
148
+ }
149
+ }
150
+ function hasBlockingFindings(counts) {
151
+ return counts.critical > 0 || counts.important > 0;
152
+ }
153
+ export async function parseReviewGateVerdict(artifactPath) {
154
+ let content;
155
+ try {
156
+ content = await readFile(artifactPath, 'utf8');
157
+ }
158
+ catch (error) {
159
+ const detail = error instanceof Error ? error.message : String(error);
160
+ throw new Error(`Unable to read review artifact at ${artifactPath}: ${detail}`, { cause: error });
161
+ }
162
+ const frontmatterBlock = getFrontmatterBlock(content);
163
+ const frontmatter = frontmatterBlock
164
+ ? parseFrontmatterObject(frontmatterBlock, artifactPath)
165
+ : {};
166
+ const counts = readFrontmatterCounts(frontmatter) ??
167
+ parseFindingsSummaryCounts(content) ??
168
+ parseFindingsSectionCounts(content, artifactPath);
169
+ if (!counts) {
170
+ throw new Error(`Review artifact at ${artifactPath} does not contain recognizable review findings or explicit verdict counts.`);
171
+ }
172
+ return {
173
+ artifactPath,
174
+ reviewType: normalizeReviewType(frontmatter['oat_review_type']),
175
+ scope: stringOrNull(frontmatter['oat_review_scope']),
176
+ invocation: stringOrNull(frontmatter['oat_review_invocation']),
177
+ counts,
178
+ blocking: hasBlockingFindings(counts),
179
+ };
180
+ }
@@ -45,7 +45,7 @@ export const BUILTIN_EXEC_TARGETS = {
45
45
  },
46
46
  'cursor-default': {
47
47
  runtime: 'cursor',
48
- baseCommand: ['cursor-agent', '-p', '--force'],
48
+ baseCommand: ['cursor-agent', '-p'],
49
49
  hostDetectionCommand: ['sh', '-c', 'test -n "$CURSOR_AGENT"'],
50
50
  availabilityCommand: ['cursor-agent', '--version'],
51
51
  priority: 70,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
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.35"
37
+ "@open-agent-toolkit/control-plane": "0.1.36"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",