feat: support codex and claude reviewers
This commit is contained in:
@@ -3,11 +3,11 @@
|
|||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://nodejs.org)
|
[](https://nodejs.org)
|
||||||
|
|
||||||
**Codex-hosted autonomous implementation loop with Reasonix, Claude Code, and Agy executors.**
|
**Codex or Claude hosted autonomous implementation loop with Reasonix, Claude Code, and Agy executors.**
|
||||||
|
|
||||||
`agent-workflow` orchestrates a stateful implement-review-fix cycle where:
|
`agent-workflow` orchestrates a stateful implement-review-fix cycle where:
|
||||||
|
|
||||||
- **Codex** acts as the host planner, reviewer, and final acceptance authority
|
- **Codex or Claude** acts as the host planner, reviewer, and final acceptance authority
|
||||||
- **Reasonix, Claude Code, or Agy** executes implementation and fixes in the same session
|
- **Reasonix, Claude Code, or Agy** executes implementation and fixes in the same session
|
||||||
- The workflow maintains atomic state, VCS baseline gates, scope enforcement, and convergence limits
|
- The workflow maintains atomic state, VCS baseline gates, scope enforcement, and convergence limits
|
||||||
- Supports both **Git** and **SVN** working copies with strict validation
|
- Supports both **Git** and **SVN** working copies with strict validation
|
||||||
@@ -30,6 +30,24 @@ Or use locally in a project:
|
|||||||
npm install --save-dev agent-workflow
|
npm install --save-dev agent-workflow
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Host Skill Installation
|
||||||
|
|
||||||
|
The `agent-workflow-host` skill provides the instructions for Codex or Claude to act as the workflow host.
|
||||||
|
|
||||||
|
### Codex
|
||||||
|
Codex uses `~/.codex/skills/agent-workflow-host`.
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.codex/skills/agent-workflow-host
|
||||||
|
cp node_modules/agent-workflow/skills/agent-workflow-host/SKILL.md ~/.codex/skills/agent-workflow-host/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
Claude uses `~/.claude/skills/agent-workflow-host`.
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.claude/skills/agent-workflow-host
|
||||||
|
cp node_modules/agent-workflow/skills/agent-workflow-host/SKILL.md ~/.claude/skills/agent-workflow-host/
|
||||||
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. Create a plan (WorkflowPlanV1 JSON):
|
1. Create a plan (WorkflowPlanV1 JSON):
|
||||||
@@ -61,13 +79,13 @@ agent-workflow start --plan plan.json --executor reasonix
|
|||||||
3. Review the implementation:
|
3. Review the implementation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-workflow review --run <run-id>
|
agent-workflow review --run <run-id> --reviewer <codex|claude>
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Submit a decision (fix, accept, or needs_human):
|
4. Submit a decision (fix, accept, or needs_human):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-workflow decide --run <run-id> --input decision.json
|
agent-workflow decide --run <run-id> --input decision.json --reviewer <codex|claude>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
@@ -77,7 +95,7 @@ agent-workflow decide --run <run-id> --input decision.json
|
|||||||
Start a new workflow run. Requires a clean working tree.
|
Start a new workflow run. Requires a clean working tree.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-workflow start --plan <plan.json|-> --executor <reasonix|claude|agy> [--vcs <git|svn|none>] [--max-cycles <n>] [--new-task]
|
agent-workflow start --plan <plan.json|-> --executor <reasonix|claude|agy> [--reviewer <codex|claude>] [--vcs <git|svn|none>] [--max-cycles <n>] [--new-task]
|
||||||
```
|
```
|
||||||
|
|
||||||
- `--vcs` - Explicitly specify VCS kind (`git`, `svn`, or `none`). If omitted, auto-detects from the working directory. If neither Git nor SVN is detected, it automatically falls back to `none` (snapshot mode).
|
- `--vcs` - Explicitly specify VCS kind (`git`, `svn`, or `none`). If omitted, auto-detects from the working directory. If neither Git nor SVN is detected, it automatically falls back to `none` (snapshot mode).
|
||||||
@@ -91,30 +109,30 @@ agent-workflow start --plan <plan.json|-> --executor <reasonix|claude|agy> [--vc
|
|||||||
|
|
||||||
### `agent-workflow review`
|
### `agent-workflow review`
|
||||||
|
|
||||||
Generate a Codex review bundle after executor completion. Checks HEAD stability and scope compliance.
|
Generate a host review bundle after executor completion. Checks HEAD stability and scope compliance.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-workflow review --run <run-id>
|
agent-workflow review --run <run-id> --reviewer <codex|claude>
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the run is at or past the guidance threshold — the executor cycle about to start is `>= 3` (i.e. reviewing cycle 2, so the next fix cycle would be the third round) — the bundle sets `detailedGuidanceRequested: true` and includes a `reviewerInstructions` string. This asks Codex, if it decides `fix`, to return a concrete step-by-step implementation plan so the executor can converge quickly.
|
Once the run is at or past the guidance threshold — the executor cycle about to start is `>= 3` (i.e. reviewing cycle 2, so the next fix cycle would be the third round) — the bundle sets `detailedGuidanceRequested: true` and includes a `reviewerInstructions` string. This asks the host, if it decides `fix`, to return a concrete step-by-step implementation plan so the executor can converge quickly.
|
||||||
|
|
||||||
### `agent-workflow decide`
|
### `agent-workflow decide`
|
||||||
|
|
||||||
Submit a Codex decision (fix, accept, or needs_human). A fix decision resumes the same executor session.
|
Submit a host decision (fix, accept, or needs_human). A fix decision resumes the same executor session.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-workflow decide --run <run-id> --input <decision.json|->
|
agent-workflow decide --run <run-id> --input <decision.json|-> --reviewer <codex|claude>
|
||||||
```
|
```
|
||||||
|
|
||||||
A `fix` decision may include an optional `implementationGuidance` string (up to 20000 characters). When present, it is injected verbatim into the executor's fix prompt under a `## Detailed Implementation Guidance` section, and the frozen plan's `planMarkdown` and acceptance criteria are re-sent alongside it so the executor has full context. This is the channel Codex uses to answer the bundle's `detailedGuidanceRequested` signal, and it flows through the normal fix, `retry-execute`, and `extend` paths.
|
A `fix` decision may include an optional `implementationGuidance` string (up to 20000 characters). When present, it is injected verbatim into the executor's fix prompt under a `## Detailed Implementation Guidance` section, and the frozen plan's `planMarkdown` and acceptance criteria are re-sent alongside it so the executor has full context. This is the channel the host uses to answer the bundle's `detailedGuidanceRequested` signal, and it flows through the normal fix, `retry-execute`, and `extend` paths.
|
||||||
|
|
||||||
### `agent-workflow retry-review`
|
### `agent-workflow retry-review`
|
||||||
|
|
||||||
Retry review after the executor has removed approved out-of-scope artifacts.
|
Retry review after the executor has removed approved out-of-scope artifacts.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-workflow retry-review --run <run-id>
|
agent-workflow retry-review --run <run-id> --reviewer <codex|claude>
|
||||||
```
|
```
|
||||||
|
|
||||||
### `agent-workflow retry-execute`
|
### `agent-workflow retry-execute`
|
||||||
@@ -276,7 +294,7 @@ Any active state → completed | needs_human | blocked | budget_exhausted | abor
|
|||||||
|
|
||||||
## Scope Gates
|
## Scope Gates
|
||||||
|
|
||||||
All file modifications must stay within the approved scope. Out-of-scope changes trigger `awaiting_scope_resolution`, where Codex inspects each violation and decides whether to remove it or escalate to `needs_human`.
|
All file modifications must stay within the approved scope. Out-of-scope changes trigger `awaiting_scope_resolution`, where the host inspects each violation and decides whether to remove it or escalate to `needs_human`.
|
||||||
|
|
||||||
## VCS Support
|
## VCS Support
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
name: agent-workflow-host
|
name: agent-workflow-host
|
||||||
description: Drive an explicitly invoked agent-workflow in which Codex freezes the latest confirmed plan, delegates implementation to Reasonix, Claude Code, or Agy, independently reviews the result, submits fix decisions to the same executor session, and repeats until accepted or safely stopped. Use only when the user explicitly invokes $agent-workflow-host to execute, resume, inspect, or abort a stateful workflow after plan discussion.
|
description: Drive an explicitly invoked agent-workflow in which the host (Codex or Claude) freezes the latest confirmed plan, delegates implementation to an executor, independently reviews the result, submits fix decisions to the same executor session, and repeats until accepted or safely stopped. Use only when the user explicitly invokes $agent-workflow-host to execute, resume, inspect, or abort a stateful workflow after plan discussion.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Agent Workflow Host
|
# Agent Workflow Host
|
||||||
|
|
||||||
Act as the host planner, reviewer, and final acceptance authority. Let the selected executor implement and fix code. Use `agent-workflow`; do not replace its state machine with direct executor commands.
|
Act as the host planner, reviewer, and final acceptance authority. Let the selected executor implement and fix code. You must pass your own identity (codex or claude) as the `--reviewer` on lifecycle commands. Use `agent-workflow`; do not replace its state machine with direct executor commands. You cannot use the claude executor if you are the claude reviewer. On resume, you must inspect the `reviewer` field in the status output and refuse to continue if it belongs to a different host. When executing lifecycle commands, pass your concrete current identity (e.g. `--reviewer codex`) instead of using a literal `<codex|claude>` placeholder.
|
||||||
|
|
||||||
## Invocation Contract
|
## Invocation Contract
|
||||||
|
|
||||||
@@ -26,6 +26,8 @@ $agent-workflow-host 中止 <run-id> 原因:需求变化
|
|||||||
$agent-workflow-host 日志 <run-id> [--follow]
|
$agent-workflow-host 日志 <run-id> [--follow]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When invoked, the host (Codex or Claude) drives the workflow using its respective `--reviewer` flag.
|
||||||
|
|
||||||
- Treat an explicit `执行` invocation as authorization to execute the latest clearly bounded plan in the conversation.
|
- Treat an explicit `执行` invocation as authorization to execute the latest clearly bounded plan in the conversation.
|
||||||
- Do not start when the conversation contains multiple unresolved plan variants, missing scope, missing acceptance criteria, or missing verification commands. Ask only for the unresolved decision.
|
- Do not start when the conversation contains multiple unresolved plan variants, missing scope, missing acceptance criteria, or missing verification commands. Ask only for the unresolved decision.
|
||||||
- If invoked without an operation, show the supported operations and do nothing.
|
- If invoked without an operation, show the supported operations and do nothing.
|
||||||
@@ -37,9 +39,9 @@ $agent-workflow-host 日志 <run-id> [--follow]
|
|||||||
|
|
||||||
## Host Boundaries
|
## Host Boundaries
|
||||||
|
|
||||||
- Codex owns plan normalization, finding classification, verification, and acceptance.
|
- The host (Codex or Claude) owns plan normalization, finding classification, verification, and acceptance.
|
||||||
- The executor owns implementation, tests, and fixes. During an active workflow, Codex must not edit target repository files itself.
|
- The executor owns implementation, tests, and fixes. During an active workflow, the host must not edit target repository files itself.
|
||||||
- `agent-workflow review` only prepares evidence for Codex. Do not invoke another reviewer as an acceptance gate.
|
- `agent-workflow review` only prepares evidence for the host. Do not invoke another reviewer as an acceptance gate.
|
||||||
- Never run or authorize `git commit`, `git push`, `git reset`, `svn commit`, `svn switch`, `svn update`, history rewriting, or branch deletion.
|
- Never run or authorize `git commit`, `git push`, `git reset`, `svn commit`, `svn switch`, `svn update`, history rewriting, or branch deletion.
|
||||||
- Never broaden the approved scope or silently change requirements after start. Abort and create a new plan when requirements materially change.
|
- Never broaden the approved scope or silently change requirements after start. Abort and create a new plan when requirements materially change.
|
||||||
|
|
||||||
@@ -72,7 +74,7 @@ $agent-workflow-host 日志 <run-id> [--follow]
|
|||||||
7. Start the run and record the emitted run ID, then immediately print a logs-follow command the user can run independently:
|
7. Start the run and record the emitted run ID, then immediately print a logs-follow command the user can run independently:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-workflow start --plan <temp-plan.json> --executor <reasonix|claude|agy> [--vcs <git|svn|none>] [--new-task]
|
agent-workflow start --plan <temp-plan.json> --executor <reasonix|claude|agy> --reviewer YOUR_IDENTITY [--vcs <git|svn|none>] [--new-task]
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not pass `--new-task` for a follow-up plan, a correction, or another implementation slice of the same user goal. The CLI validates persisted task bindings before reuse and blocks stale or mismatched handles. An executor selected for the first time in the task starts lazily and is then recorded for later runs. Agy degraded `--continue` handles are intentionally not shared.
|
Do not pass `--new-task` for a follow-up plan, a correction, or another implementation slice of the same user goal. The CLI validates persisted task bindings before reuse and blocks stale or mismatched handles. An executor selected for the first time in the task starts lazily and is then recorded for later runs. Agy degraded `--continue` handles are intentionally not shared.
|
||||||
@@ -92,9 +94,9 @@ Continue until a terminal status. Do not stop after merely proposing a decision.
|
|||||||
|
|
||||||
### `awaiting_review`
|
### `awaiting_review`
|
||||||
|
|
||||||
1. Run `agent-workflow review --run <run-id>`.
|
1. Run `agent-workflow review --run <run-id> --reviewer YOUR_IDENTITY`.
|
||||||
2. Read the emitted `hostReviewFile`. Inspect its complete `diffFile`, full `executorLogFile`, plan, acceptance criteria, repository code, changed-file scope, and HEAD evidence yourself.
|
2. Read the emitted `hostReviewFile`. Inspect its complete `diffFile`, full `executorLogFile`, plan, acceptance criteria, repository code, changed-file scope, and HEAD evidence yourself.
|
||||||
3. Review for correctness, regressions, security, and missing tests. Assign stable IDs such as `codex-<cycle>-1` to concrete issues.
|
3. Review for correctness, regressions, security, and missing tests. Assign stable IDs such as `codex-<cycle>-<n>` (if you are Codex) or `claude-<cycle>-<n>` (if you are Claude) to concrete issues.
|
||||||
4. Independently run every `verificationCommands` entry from the repository root and retain concise evidence.
|
4. Independently run every `verificationCommands` entry from the repository root and retain concise evidence.
|
||||||
|
|
||||||
### While waiting for an executor
|
### While waiting for an executor
|
||||||
@@ -121,15 +123,15 @@ The `agent-workflow web` command starts a local web server that reads workflow s
|
|||||||
|
|
||||||
### `awaiting_scope_resolution`
|
### `awaiting_scope_resolution`
|
||||||
|
|
||||||
1. Confirm the Codex review bundle was not prepared and inspect every structured `scopeViolations` entry.
|
1. Confirm your review bundle was not prepared and inspect every structured `scopeViolations` entry.
|
||||||
2. Decide whether each listed path was created by the executor and can be safely removed or restored. Never broaden scope, add ignore rules, or delete automatically.
|
2. Decide whether each listed path was created by the executor and can be safely removed or restored. Never broaden scope, add ignore rules, or delete automatically.
|
||||||
3. To use `outcome: "fix"`, accept every current `scope-*` finding ID exactly once. The CLI resumes the same executor session with a prompt limited to those exact paths.
|
3. To use `outcome: "fix"`, accept every current `scope-*` finding ID exactly once. The CLI resumes the same executor session with a prompt limited to those exact paths.
|
||||||
4. Use `needs_human` when any path may contain intentional work or ownership is unclear. Never use `accept` in this state.
|
4. Use `needs_human` when any path may contain intentional work or ownership is unclear. Never use `accept` in this state.
|
||||||
5. After remediation, run `agent-workflow review` again. Codex review may proceed only after HEAD and scope gates pass.
|
5. After remediation, run `agent-workflow review --run <run-id> --reviewer YOUR_IDENTITY` again. Review may proceed only after HEAD and scope gates pass.
|
||||||
|
|
||||||
### `awaiting_host`
|
### `awaiting_host`
|
||||||
|
|
||||||
Classify every issue found by Codex:
|
Classify every issue found during your review:
|
||||||
|
|
||||||
- `accept`: real, actionable, in scope, and required for the approved acceptance criteria.
|
- `accept`: real, actionable, in scope, and required for the approved acceptance criteria.
|
||||||
- `reject`: unsupported or incorrect; include a concrete rationale.
|
- `reject`: unsupported or incorrect; include a concrete rationale.
|
||||||
@@ -140,6 +142,7 @@ Create `HostDecisionV1` in the temporary directory:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
|
"reviewer": "YOUR_IDENTITY",
|
||||||
"outcome": "fix",
|
"outcome": "fix",
|
||||||
"findingDecisions": [
|
"findingDecisions": [
|
||||||
{
|
{
|
||||||
@@ -154,9 +157,10 @@ Create `HostDecisionV1` in the temporary directory:
|
|||||||
"decidedAt": "ISO-8601 timestamp"
|
"decidedAt": "ISO-8601 timestamp"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
*(Use your respective prefix, e.g., `claude-2-1` if Claude).*
|
||||||
|
|
||||||
- Use `outcome: "fix"` when at least one accepted finding must be fixed. Every accepted Codex finding requires a non-empty `summary`; include `path` when known. Submit it with `agent-workflow decide --run <run-id> --input <temp-decision.json>`. The CLI must resume the same executor session.
|
- Use `outcome: "fix"` when at least one accepted finding must be fixed. Every accepted finding requires a non-empty `summary`; include `path` when known. Submit it with `agent-workflow decide --run <run-id> --input <temp-decision.json> --reviewer YOUR_IDENTITY`. The CLI must resume the same executor session.
|
||||||
- Use `outcome: "accept"` only when Codex has completed its own review, has no blocking issue, every verification command passes, HEAD is unchanged, and every changed file is in scope.
|
- Use `outcome: "accept"` only when you have completed your own review, have no blocking issue, every verification command passes, HEAD is unchanged, and every changed file is in scope.
|
||||||
- Use `outcome: "needs_human"` when intent, safety, architecture, or evidence cannot be resolved without the user.
|
- Use `outcome: "needs_human"` when intent, safety, architecture, or evidence cannot be resolved without the user.
|
||||||
- Never send rejected findings or follow-ups to the executor as required fixes.
|
- Never send rejected findings or follow-ups to the executor as required fixes.
|
||||||
|
|
||||||
@@ -166,7 +170,7 @@ After `fix`, repeat review and decision for the new cycle. Respect the configure
|
|||||||
|
|
||||||
- For `查看 <run-id>`, run `agent-workflow status --run <run-id> --json`, summarize it, and do not advance the workflow.
|
- For `查看 <run-id>`, run `agent-workflow status --run <run-id> --json`, summarize it, and do not advance the workflow.
|
||||||
- For `继续 <run-id>`, read status first, then continue from `awaiting_review`, `awaiting_scope_resolution`, or `awaiting_host`. Read the persisted state and referenced cycle artifacts when earlier command output is no longer in context; do not reconstruct findings from memory.
|
- For `继续 <run-id>`, read status first, then continue from `awaiting_review`, `awaiting_scope_resolution`, or `awaiting_host`. Read the persisted state and referenced cycle artifacts when earlier command output is no longer in context; do not reconstruct findings from memory.
|
||||||
- For `清理后重试 <run-id>`, run `agent-workflow retry-review --run <run-id>` after the approved scope artifacts have been removed. It must refuse when baseline drift or out-of-scope paths remain.
|
- For `清理后重试 <run-id>`, run `agent-workflow retry-review --run <run-id> --reviewer YOUR_IDENTITY` after the approved scope artifacts have been removed. It must refuse when baseline drift or out-of-scope paths remain.
|
||||||
- For `执行器重试 <run-id>`, run `agent-workflow retry-execute --run <run-id>` after the external cause of an executor failure is cleared (e.g., session conflict resolved, transient error gone, or corrected an invalid `model` configuration). It preserves failed attempt logs and reuses the same session handle. If a Reasonix handle was not captured, the CLI may recover a persisted session only when its project directory, frozen plan title, and execution time window match the run, and must audit the recovery. Initial-cycle retries continue from the original plan; later-cycle retries resume from the preceding decision. Refuses when baseline has drifted, scope violations exist, or the cycle already produced review artifacts.
|
- For `执行器重试 <run-id>`, run `agent-workflow retry-execute --run <run-id>` after the external cause of an executor failure is cleared (e.g., session conflict resolved, transient error gone, or corrected an invalid `model` configuration). It preserves failed attempt logs and reuses the same session handle. If a Reasonix handle was not captured, the CLI may recover a persisted session only when its project directory, frozen plan title, and execution time window match the run, and must audit the recovery. Initial-cycle retries continue from the original plan; later-cycle retries resume from the preceding decision. Refuses when baseline has drifted, scope violations exist, or the cycle already produced review artifacts.
|
||||||
- For `执行器重试 <run-id> 使用会话 <claude-session-id>`, run `agent-workflow retry-execute --run <run-id> --session <claude-session-id>` only when a Claude run's recorded session was never created or is unavailable and a known persisted replacement session contains the same frozen plan. The CLI must validate that the session belongs to the same repository and plan before recording the rebind in the audit log and retrying. Never edit `state.json` manually or guess a session ID.
|
- For `执行器重试 <run-id> 使用会话 <claude-session-id>`, run `agent-workflow retry-execute --run <run-id> --session <claude-session-id>` only when a Claude run's recorded session was never created or is unavailable and a known persisted replacement session contains the same frozen plan. The CLI must validate that the session belongs to the same repository and plan before recording the rebind in the audit log and retrying. Never edit `state.json` manually or guess a session ID.
|
||||||
- For `延长 <run-id> 添加 <n> 个周期`, run `agent-workflow extend --run <run-id> --additional-cycles <n>` to extend a `budget_exhausted` run. This requires: the run ended at `budget_exhausted`, has a valid session handle, the last decision was `fix`, baseline hasn't drifted, and scope is clean. Returns to `awaiting_host` state to continue the same executor session.
|
- For `延长 <run-id> 添加 <n> 个周期`, run `agent-workflow extend --run <run-id> --additional-cycles <n>` to extend a `budget_exhausted` run. This requires: the run ended at `budget_exhausted`, has a valid session handle, the last decision was `fix`, baseline hasn't drifted, and scope is clean. Returns to `awaiting_host` state to continue the same executor session.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "Agent Workflow Host"
|
display_name: "Agent Workflow Host"
|
||||||
short_description: "让 Codex 驱动已确认计划的执行、独立审查与持续修复闭环"
|
short_description: "让 Codex 或 Claude 驱动已确认计划的执行、独立审查与持续修复闭环"
|
||||||
default_prompt: "Use $agent-workflow-host to execute the latest confirmed plan with Reasonix and review until accepted."
|
default_prompt: "Use $agent-workflow-host to execute the latest confirmed plan with Reasonix and review until accepted."
|
||||||
|
|
||||||
policy:
|
policy:
|
||||||
|
|||||||
+4
-2
@@ -43,6 +43,7 @@ async function main() {
|
|||||||
case "start":
|
case "start":
|
||||||
await startWorkflow({
|
await startWorkflow({
|
||||||
planInput: args.planInput,
|
planInput: args.planInput,
|
||||||
|
reviewer: args.reviewer,
|
||||||
executor: args.executor,
|
executor: args.executor,
|
||||||
vcs: args.vcs,
|
vcs: args.vcs,
|
||||||
maxCycles: args.maxCycles,
|
maxCycles: args.maxCycles,
|
||||||
@@ -51,11 +52,11 @@ async function main() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "review":
|
case "review":
|
||||||
await reviewWorkflow({ runId: args.runId });
|
await reviewWorkflow({ runId: args.runId, reviewer: args.reviewer });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "retry-review":
|
case "retry-review":
|
||||||
retryReviewWorkflow({ runId: args.runId });
|
retryReviewWorkflow({ runId: args.runId, reviewer: args.reviewer });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "retry-execute":
|
case "retry-execute":
|
||||||
@@ -65,6 +66,7 @@ async function main() {
|
|||||||
case "decide":
|
case "decide":
|
||||||
await decideWorkflow({
|
await decideWorkflow({
|
||||||
runId: args.runId,
|
runId: args.runId,
|
||||||
|
reviewer: args.reviewer,
|
||||||
decisionInput: args.decisionInput,
|
decisionInput: args.decisionInput,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -15,6 +15,63 @@ describe("workflow start arguments", () => {
|
|||||||
vcs: undefined,
|
vcs: undefined,
|
||||||
maxCycles: undefined,
|
maxCycles: undefined,
|
||||||
newTask: true,
|
newTask: true,
|
||||||
|
reviewer: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a valid reviewer", () => {
|
||||||
|
expect(parseWorkflowArgs([
|
||||||
|
"start",
|
||||||
|
"--plan", "plan.json",
|
||||||
|
"--executor", "reasonix",
|
||||||
|
"--reviewer", "claude"
|
||||||
|
])).toEqual({
|
||||||
|
sub: "start",
|
||||||
|
planInput: "plan.json",
|
||||||
|
executor: "reasonix",
|
||||||
|
vcs: undefined,
|
||||||
|
maxCycles: undefined,
|
||||||
|
newTask: false,
|
||||||
|
reviewer: "claude",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on invalid reviewer", () => {
|
||||||
|
expect(() => parseWorkflowArgs([
|
||||||
|
"start",
|
||||||
|
"--plan", "plan.json",
|
||||||
|
"--executor", "reasonix",
|
||||||
|
"--reviewer", "invalid"
|
||||||
|
])).toThrow(/must be codex or claude/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("workflow advancing arguments", () => {
|
||||||
|
it("review requires reviewer", () => {
|
||||||
|
expect(() => parseWorkflowArgs(["review", "--run", "run-123"])).toThrow(/--reviewer is required/);
|
||||||
|
expect(parseWorkflowArgs(["review", "--run", "run-123", "--reviewer", "codex"])).toEqual({
|
||||||
|
sub: "review",
|
||||||
|
runId: "run-123",
|
||||||
|
reviewer: "codex"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decide requires reviewer", () => {
|
||||||
|
expect(() => parseWorkflowArgs(["decide", "--run", "run-123", "--input", "decision.json"])).toThrow(/--reviewer is required/);
|
||||||
|
expect(parseWorkflowArgs(["decide", "--run", "run-123", "--input", "decision.json", "--reviewer", "claude"])).toEqual({
|
||||||
|
sub: "decide",
|
||||||
|
runId: "run-123",
|
||||||
|
decisionInput: "decision.json",
|
||||||
|
reviewer: "claude"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retry-review requires reviewer", () => {
|
||||||
|
expect(() => parseWorkflowArgs(["retry-review", "--run", "run-123"])).toThrow(/--reviewer is required/);
|
||||||
|
expect(parseWorkflowArgs(["retry-review", "--run", "run-123", "--reviewer", "codex"])).toEqual({
|
||||||
|
sub: "retry-review",
|
||||||
|
runId: "run-123",
|
||||||
|
reviewer: "codex"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+44
-12
@@ -6,6 +6,7 @@ export type WorkflowSubcommand = "start" | "review" | "retry-review" | "retry-ex
|
|||||||
|
|
||||||
export interface WorkflowStartArgs {
|
export interface WorkflowStartArgs {
|
||||||
sub: "start";
|
sub: "start";
|
||||||
|
reviewer?: string;
|
||||||
executor?: string;
|
executor?: string;
|
||||||
vcs?: string;
|
vcs?: string;
|
||||||
planInput: string; // file path or "-"
|
planInput: string; // file path or "-"
|
||||||
@@ -16,11 +17,13 @@ export interface WorkflowStartArgs {
|
|||||||
export interface WorkflowReviewArgs {
|
export interface WorkflowReviewArgs {
|
||||||
sub: "review";
|
sub: "review";
|
||||||
runId: string;
|
runId: string;
|
||||||
|
reviewer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowRetryReviewArgs {
|
export interface WorkflowRetryReviewArgs {
|
||||||
sub: "retry-review";
|
sub: "retry-review";
|
||||||
runId: string;
|
runId: string;
|
||||||
|
reviewer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowRetryExecuteArgs {
|
export interface WorkflowRetryExecuteArgs {
|
||||||
@@ -32,6 +35,7 @@ export interface WorkflowRetryExecuteArgs {
|
|||||||
export interface WorkflowDecideArgs {
|
export interface WorkflowDecideArgs {
|
||||||
sub: "decide";
|
sub: "decide";
|
||||||
runId: string;
|
runId: string;
|
||||||
|
reviewer: string;
|
||||||
decisionInput: string; // file path or "-"
|
decisionInput: string; // file path or "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,11 +105,11 @@ function parsePositiveInt(flag: string, value: string): number {
|
|||||||
|
|
||||||
export const WORKFLOW_USAGE = `
|
export const WORKFLOW_USAGE = `
|
||||||
Usage:
|
Usage:
|
||||||
agent-workflow start --executor <reasonix|claude|agy> --plan <plan.json|-> [--vcs <git|svn|none>] [--max-cycles <n>] [--new-task]
|
agent-workflow start --executor <reasonix|claude|agy> --plan <plan.json|-> [--reviewer <codex|claude>] [--vcs <git|svn|none>] [--max-cycles <n>] [--new-task]
|
||||||
agent-workflow review --run <run-id>
|
agent-workflow review --run <run-id> --reviewer <codex|claude>
|
||||||
agent-workflow retry-review --run <run-id>
|
agent-workflow retry-review --run <run-id> --reviewer <codex|claude>
|
||||||
agent-workflow retry-execute --run <run-id> [--session <claude-session-id>]
|
agent-workflow retry-execute --run <run-id> [--session <claude-session-id>]
|
||||||
agent-workflow decide --run <run-id> --input <decision.json|->
|
agent-workflow decide --run <run-id> --input <decision.json|-> --reviewer <codex|claude>
|
||||||
agent-workflow extend --run <run-id> --additional-cycles <n>
|
agent-workflow extend --run <run-id> --additional-cycles <n>
|
||||||
agent-workflow status --run <run-id> [--json]
|
agent-workflow status --run <run-id> [--json]
|
||||||
agent-workflow abort --run <run-id> --reason <text>
|
agent-workflow abort --run <run-id> --reason <text>
|
||||||
@@ -119,12 +123,12 @@ Examples:
|
|||||||
agent-workflow start --plan plan.json --executor reasonix --vcs none
|
agent-workflow start --plan plan.json --executor reasonix --vcs none
|
||||||
agent-workflow start --plan plan.json --executor reasonix --vcs svn
|
agent-workflow start --plan plan.json --executor reasonix --vcs svn
|
||||||
agent-workflow start --plan - --executor claude --max-cycles 3 # read plan from stdin
|
agent-workflow start --plan - --executor claude --max-cycles 3 # read plan from stdin
|
||||||
agent-workflow review --run 2026-01-01T00-00-00_01_abc123
|
agent-workflow review --run 2026-01-01T00-00-00_01_abc123 --reviewer codex
|
||||||
agent-workflow retry-review --run 2026-01-01T00-00-00_01_abc123
|
agent-workflow retry-review --run 2026-01-01T00-00-00_01_abc123 --reviewer codex
|
||||||
agent-workflow retry-execute --run 2026-01-01T00-00-00_01_abc123
|
agent-workflow retry-execute --run 2026-01-01T00-00-00_01_abc123
|
||||||
agent-workflow retry-execute --run 2026-01-01T00-00-00_01_abc123 --session 12345678-1234-1234-1234-123456789abc
|
agent-workflow retry-execute --run 2026-01-01T00-00-00_01_abc123 --session 12345678-1234-1234-1234-123456789abc
|
||||||
agent-workflow decide --run <id> --input decision.json
|
agent-workflow decide --run <id> --input decision.json --reviewer codex
|
||||||
agent-workflow decide --run <id> --input - # read decision from stdin
|
agent-workflow decide --run <id> --input - --reviewer codex # read decision from stdin
|
||||||
agent-workflow extend --run <id> --additional-cycles 3
|
agent-workflow extend --run <id> --additional-cycles 3
|
||||||
agent-workflow status --run <id>
|
agent-workflow status --run <id>
|
||||||
agent-workflow status --run <id> --json
|
agent-workflow status --run <id> --json
|
||||||
@@ -149,6 +153,7 @@ export function parseWorkflowArgs(argv: string[]): WorkflowArgs {
|
|||||||
switch (sub) {
|
switch (sub) {
|
||||||
case "start": {
|
case "start": {
|
||||||
let executor: string | undefined;
|
let executor: string | undefined;
|
||||||
|
let reviewer: string | undefined;
|
||||||
let vcs: string | undefined;
|
let vcs: string | undefined;
|
||||||
let planInput: string | undefined;
|
let planInput: string | undefined;
|
||||||
let maxCycles: number | undefined;
|
let maxCycles: number | undefined;
|
||||||
@@ -160,6 +165,12 @@ export function parseWorkflowArgs(argv: string[]): WorkflowArgs {
|
|||||||
case "--executor":
|
case "--executor":
|
||||||
executor = requireValue(arg, rest);
|
executor = requireValue(arg, rest);
|
||||||
break;
|
break;
|
||||||
|
case "--reviewer":
|
||||||
|
reviewer = requireValue(arg, rest);
|
||||||
|
if (reviewer !== "codex" && reviewer !== "claude") {
|
||||||
|
throw new WorkflowArgsError("--reviewer must be codex or claude");
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "--vcs":
|
case "--vcs":
|
||||||
vcs = requireValue(arg, rest);
|
vcs = requireValue(arg, rest);
|
||||||
break;
|
break;
|
||||||
@@ -183,15 +194,21 @@ export function parseWorkflowArgs(argv: string[]): WorkflowArgs {
|
|||||||
|
|
||||||
if (!planInput) throw new WorkflowArgsError("--plan is required for workflow start");
|
if (!planInput) throw new WorkflowArgsError("--plan is required for workflow start");
|
||||||
|
|
||||||
return { sub: "start", executor, vcs, planInput, maxCycles, newTask };
|
return { sub: "start", reviewer, executor, vcs, planInput, maxCycles, newTask };
|
||||||
}
|
}
|
||||||
|
|
||||||
case "review": {
|
case "review": {
|
||||||
let runId: string | undefined;
|
let runId: string | undefined;
|
||||||
|
let reviewer: string | undefined;
|
||||||
while (rest.length > 0) {
|
while (rest.length > 0) {
|
||||||
const arg = rest.shift()!;
|
const arg = rest.shift()!;
|
||||||
if (arg === "--run") {
|
if (arg === "--run") {
|
||||||
runId = requireValue(arg, rest);
|
runId = requireValue(arg, rest);
|
||||||
|
} else if (arg === "--reviewer") {
|
||||||
|
reviewer = requireValue(arg, rest);
|
||||||
|
if (reviewer !== "codex" && reviewer !== "claude") {
|
||||||
|
throw new WorkflowArgsError("--reviewer must be codex or claude");
|
||||||
|
}
|
||||||
} else if (arg === "-h" || arg === "--help") {
|
} else if (arg === "-h" || arg === "--help") {
|
||||||
workflowUsage(0);
|
workflowUsage(0);
|
||||||
} else {
|
} else {
|
||||||
@@ -199,15 +216,22 @@ export function parseWorkflowArgs(argv: string[]): WorkflowArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!runId) throw new WorkflowArgsError("--run is required for workflow review");
|
if (!runId) throw new WorkflowArgsError("--run is required for workflow review");
|
||||||
return { sub: "review", runId };
|
if (!reviewer) throw new WorkflowArgsError("--reviewer is required for workflow review");
|
||||||
|
return { sub: "review", runId, reviewer };
|
||||||
}
|
}
|
||||||
|
|
||||||
case "retry-review": {
|
case "retry-review": {
|
||||||
let runId: string | undefined;
|
let runId: string | undefined;
|
||||||
|
let reviewer: string | undefined;
|
||||||
while (rest.length > 0) {
|
while (rest.length > 0) {
|
||||||
const arg = rest.shift()!;
|
const arg = rest.shift()!;
|
||||||
if (arg === "--run") {
|
if (arg === "--run") {
|
||||||
runId = requireValue(arg, rest);
|
runId = requireValue(arg, rest);
|
||||||
|
} else if (arg === "--reviewer") {
|
||||||
|
reviewer = requireValue(arg, rest);
|
||||||
|
if (reviewer !== "codex" && reviewer !== "claude") {
|
||||||
|
throw new WorkflowArgsError("--reviewer must be codex or claude");
|
||||||
|
}
|
||||||
} else if (arg === "-h" || arg === "--help") {
|
} else if (arg === "-h" || arg === "--help") {
|
||||||
workflowUsage(0);
|
workflowUsage(0);
|
||||||
} else {
|
} else {
|
||||||
@@ -215,7 +239,8 @@ export function parseWorkflowArgs(argv: string[]): WorkflowArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!runId) throw new WorkflowArgsError("--run is required for workflow retry-review");
|
if (!runId) throw new WorkflowArgsError("--run is required for workflow retry-review");
|
||||||
return { sub: "retry-review", runId };
|
if (!reviewer) throw new WorkflowArgsError("--reviewer is required for workflow retry-review");
|
||||||
|
return { sub: "retry-review", runId, reviewer };
|
||||||
}
|
}
|
||||||
|
|
||||||
case "retry-execute": {
|
case "retry-execute": {
|
||||||
@@ -239,11 +264,17 @@ export function parseWorkflowArgs(argv: string[]): WorkflowArgs {
|
|||||||
|
|
||||||
case "decide": {
|
case "decide": {
|
||||||
let runId: string | undefined;
|
let runId: string | undefined;
|
||||||
|
let reviewer: string | undefined;
|
||||||
let decisionInput: string | undefined;
|
let decisionInput: string | undefined;
|
||||||
while (rest.length > 0) {
|
while (rest.length > 0) {
|
||||||
const arg = rest.shift()!;
|
const arg = rest.shift()!;
|
||||||
if (arg === "--run") {
|
if (arg === "--run") {
|
||||||
runId = requireValue(arg, rest);
|
runId = requireValue(arg, rest);
|
||||||
|
} else if (arg === "--reviewer") {
|
||||||
|
reviewer = requireValue(arg, rest);
|
||||||
|
if (reviewer !== "codex" && reviewer !== "claude") {
|
||||||
|
throw new WorkflowArgsError("--reviewer must be codex or claude");
|
||||||
|
}
|
||||||
} else if (arg === "--input") {
|
} else if (arg === "--input") {
|
||||||
decisionInput = requireValue(arg, rest);
|
decisionInput = requireValue(arg, rest);
|
||||||
} else if (arg === "-h" || arg === "--help") {
|
} else if (arg === "-h" || arg === "--help") {
|
||||||
@@ -253,8 +284,9 @@ export function parseWorkflowArgs(argv: string[]): WorkflowArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!runId) throw new WorkflowArgsError("--run is required for workflow decide");
|
if (!runId) throw new WorkflowArgsError("--run is required for workflow decide");
|
||||||
|
if (!reviewer) throw new WorkflowArgsError("--reviewer is required for workflow decide");
|
||||||
if (!decisionInput) throw new WorkflowArgsError("--input is required for workflow decide");
|
if (!decisionInput) throw new WorkflowArgsError("--input is required for workflow decide");
|
||||||
return { sub: "decide", runId, decisionInput };
|
return { sub: "decide", runId, reviewer, decisionInput };
|
||||||
}
|
}
|
||||||
|
|
||||||
case "status": {
|
case "status": {
|
||||||
|
|||||||
+79
-30
@@ -2,9 +2,9 @@
|
|||||||
* Workflow engine — implements the workflow CLI subcommands:
|
* Workflow engine — implements the workflow CLI subcommands:
|
||||||
*
|
*
|
||||||
* start — validates plan, acquires lock, starts first execution cycle
|
* start — validates plan, acquires lock, starts first execution cycle
|
||||||
* review — generates a review bundle for the Codex host
|
* review — generates a review bundle for the host
|
||||||
* retry-review — resumes a cleaned scope-gate review
|
* retry-review — resumes a cleaned scope-gate review
|
||||||
* decide — receives Codex HostDecisionV1, drives state transitions
|
* decide — receives host HostDecisionV1, drives state transitions
|
||||||
* extend — extends a budget_exhausted run with additional cycles
|
* extend — extends a budget_exhausted run with additional cycles
|
||||||
* status — prints current run state
|
* status — prints current run state
|
||||||
* abort — terminates a run with a reason
|
* abort — terminates a run with a reason
|
||||||
@@ -65,6 +65,7 @@ import {
|
|||||||
extractBaselineHead,
|
extractBaselineHead,
|
||||||
VcsError,
|
VcsError,
|
||||||
} from "./vcs-provider.js";
|
} from "./vcs-provider.js";
|
||||||
|
import { REVIEWER_KINDS } from "./workflow-types.js";
|
||||||
import type {
|
import type {
|
||||||
CycleRecord,
|
CycleRecord,
|
||||||
ExecutorKind,
|
ExecutorKind,
|
||||||
@@ -72,6 +73,7 @@ import type {
|
|||||||
ExecutorSessionHandle,
|
ExecutorSessionHandle,
|
||||||
HostDecisionV1,
|
HostDecisionV1,
|
||||||
HostReviewBundleV1,
|
HostReviewBundleV1,
|
||||||
|
ReviewerKind,
|
||||||
ScopeViolation,
|
ScopeViolation,
|
||||||
VcsBaseline,
|
VcsBaseline,
|
||||||
VcsKind,
|
VcsKind,
|
||||||
@@ -89,7 +91,7 @@ const activeAbortControllers = new Set<AbortController>();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* When the executor cycle that is *about to start* reaches this value, the host
|
* When the executor cycle that is *about to start* reaches this value, the host
|
||||||
* review bundle asks Codex for a more detailed implementation plan so the
|
* review bundle asks the host for a more detailed implementation plan so the
|
||||||
* executor can converge faster. Compared against (cycleIndex + 1) in reviewWorkflow.
|
* executor can converge faster. Compared against (cycleIndex + 1) in reviewWorkflow.
|
||||||
*/
|
*/
|
||||||
export const DETAILED_GUIDANCE_CYCLE_THRESHOLD = 3;
|
export const DETAILED_GUIDANCE_CYCLE_THRESHOLD = 3;
|
||||||
@@ -276,6 +278,7 @@ function persistExecutorAttempt(
|
|||||||
|
|
||||||
export interface StartWorkflowOpts {
|
export interface StartWorkflowOpts {
|
||||||
planInput: string; // file path or "-" for stdin
|
planInput: string; // file path or "-" for stdin
|
||||||
|
reviewer?: string;
|
||||||
executor?: string;
|
executor?: string;
|
||||||
vcs?: string;
|
vcs?: string;
|
||||||
maxCycles?: number;
|
maxCycles?: number;
|
||||||
@@ -487,6 +490,14 @@ export async function startWorkflow(opts: StartWorkflowOpts): Promise<{ runId: s
|
|||||||
...(opts.maxCycles !== undefined ? { maxCycles: opts.maxCycles } : {}),
|
...(opts.maxCycles !== undefined ? { maxCycles: opts.maxCycles } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reviewer = (opts.reviewer ?? "codex") as ReviewerKind;
|
||||||
|
if (!REVIEWER_KINDS.includes(reviewer)) {
|
||||||
|
throw new WorkflowEngineError(`Invalid reviewer: ${opts.reviewer}. Supported: ${REVIEWER_KINDS.join(", ")}`);
|
||||||
|
}
|
||||||
|
if (reviewer === "claude" && config.executor === "claude") {
|
||||||
|
throw new WorkflowEngineError("Reviewer 'claude' cannot be used with executor 'claude'. Claude cannot self-review.", 4);
|
||||||
|
}
|
||||||
|
|
||||||
const vcsProvider = createVcsProvider(vcsKind);
|
const vcsProvider = createVcsProvider(vcsKind);
|
||||||
const codexThreadId = (opts.codexThreadId ?? process.env.CODEX_THREAD_ID)?.trim();
|
const codexThreadId = (opts.codexThreadId ?? process.env.CODEX_THREAD_ID)?.trim();
|
||||||
if (opts.newTask && !codexThreadId) {
|
if (opts.newTask && !codexThreadId) {
|
||||||
@@ -565,6 +576,7 @@ export async function startWorkflow(opts: StartWorkflowOpts): Promise<{ runId: s
|
|||||||
vcsBaseline,
|
vcsBaseline,
|
||||||
vcsKind,
|
vcsKind,
|
||||||
executor: config.executor,
|
executor: config.executor,
|
||||||
|
reviewer,
|
||||||
plan: typedPlan,
|
plan: typedPlan,
|
||||||
maxCycles: config.maxCycles,
|
maxCycles: config.maxCycles,
|
||||||
timeoutSeconds: config.timeoutSeconds,
|
timeoutSeconds: config.timeoutSeconds,
|
||||||
@@ -599,6 +611,7 @@ export async function startWorkflow(opts: StartWorkflowOpts): Promise<{ runId: s
|
|||||||
runId,
|
runId,
|
||||||
timestamp: state.createdAt,
|
timestamp: state.createdAt,
|
||||||
data: {
|
data: {
|
||||||
|
reviewer: reviewer,
|
||||||
executor: config.executor,
|
executor: config.executor,
|
||||||
maxCycles: config.maxCycles,
|
maxCycles: config.maxCycles,
|
||||||
vcsKind,
|
vcsKind,
|
||||||
@@ -611,6 +624,7 @@ export async function startWorkflow(opts: StartWorkflowOpts): Promise<{ runId: s
|
|||||||
process.stdout.write(`workflow started: ${runId}\n`);
|
process.stdout.write(`workflow started: ${runId}\n`);
|
||||||
process.stdout.write(` vcs: ${vcsKind}\n`);
|
process.stdout.write(` vcs: ${vcsKind}\n`);
|
||||||
process.stdout.write(` executor: ${config.executor}\n`);
|
process.stdout.write(` executor: ${config.executor}\n`);
|
||||||
|
process.stdout.write(` reviewer: ${reviewer}\n`);
|
||||||
process.stdout.write(` max-cycles: ${config.maxCycles}\n`);
|
process.stdout.write(` max-cycles: ${config.maxCycles}\n`);
|
||||||
if (state.taskId) {
|
if (state.taskId) {
|
||||||
process.stdout.write(` task: ${state.taskId}${state.taskSessionReused ? " (session reused)" : ""}\n`);
|
process.stdout.write(` task: ${state.taskId}${state.taskSessionReused ? " (session reused)" : ""}\n`);
|
||||||
@@ -727,11 +741,12 @@ export async function startWorkflow(opts: StartWorkflowOpts): Promise<{ runId: s
|
|||||||
state.updatedAt = nowIso();
|
state.updatedAt = nowIso();
|
||||||
writeStateSafety(dir, state);
|
writeStateSafety(dir, state);
|
||||||
|
|
||||||
|
const effectiveReviewer = state.reviewer ?? "codex";
|
||||||
process.stdout.write(`cycle ${cycleIndex} execution completed\n`);
|
process.stdout.write(`cycle ${cycleIndex} execution completed\n`);
|
||||||
process.stdout.write(`status: awaiting_review\n`);
|
process.stdout.write(`status: awaiting_review\n`);
|
||||||
process.stdout.write(`next: agent-workflow review --run ${runId}\n`);
|
process.stdout.write(`next: agent-workflow review --run ${runId} --reviewer ${effectiveReviewer}\n`);
|
||||||
|
|
||||||
emitWorkflowMeta({ runId, status: state.status, cycleIndex, executor: config.executor });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId, status: state.status, cycleIndex, executor: config.executor });
|
||||||
return { runId };
|
return { runId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,12 +756,18 @@ export async function startWorkflow(opts: StartWorkflowOpts): Promise<{ runId: s
|
|||||||
|
|
||||||
export interface ReviewWorkflowOpts {
|
export interface ReviewWorkflowOpts {
|
||||||
runId: string;
|
runId: string;
|
||||||
|
reviewer: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
||||||
const { state, dir } = requireState(opts.runId);
|
const { state, dir } = requireState(opts.runId);
|
||||||
|
|
||||||
|
const effectiveReviewer = state.reviewer ?? "codex";
|
||||||
|
if (opts.reviewer !== effectiveReviewer) {
|
||||||
|
throw new WorkflowEngineError(`Caller reviewer '${opts.reviewer}' does not match the run reviewer '${effectiveReviewer}'.`, 4);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.status !== "awaiting_review") {
|
if (state.status !== "awaiting_review") {
|
||||||
throw new WorkflowEngineError(
|
throw new WorkflowEngineError(
|
||||||
`Run '${opts.runId}' is in status '${state.status}', not awaiting_review.`,
|
`Run '${opts.runId}' is in status '${state.status}', not awaiting_review.`,
|
||||||
@@ -821,7 +842,7 @@ export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
|||||||
`Scope remediation did not converge: ${outOfScope.sort().join(", ")}`,
|
`Scope remediation did not converge: ${outOfScope.sort().join(", ")}`,
|
||||||
{ cycleIndex, scopeViolations },
|
{ cycleIndex, scopeViolations },
|
||||||
);
|
);
|
||||||
emitWorkflowMeta({ runId: state.runId, status: "needs_human", cycleIndex, scopeViolations });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId: state.runId, status: "needs_human", cycleIndex, scopeViolations });
|
||||||
throw new WorkflowEngineError("Scope remediation did not converge.", 3);
|
throw new WorkflowEngineError("Scope remediation did not converge.", 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,8 +863,9 @@ export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
|||||||
for (const violation of scopeViolations) {
|
for (const violation of scopeViolations) {
|
||||||
process.stdout.write(` ${violation.id}: ${violation.path}\n`);
|
process.stdout.write(` ${violation.id}: ${violation.path}\n`);
|
||||||
}
|
}
|
||||||
process.stdout.write(`next: agent-workflow decide --run ${opts.runId} --input <decision.json|->\n`);
|
process.stdout.write(`next: agent-workflow decide --run ${opts.runId} --input <decision.json|-> --reviewer ${effectiveReviewer}\n`);
|
||||||
emitWorkflowMeta({
|
emitWorkflowMeta({
|
||||||
|
reviewer: effectiveReviewer,
|
||||||
runId: state.runId,
|
runId: state.runId,
|
||||||
status: state.status,
|
status: state.status,
|
||||||
cycleIndex,
|
cycleIndex,
|
||||||
@@ -855,13 +877,14 @@ export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
|||||||
const generatedAt = nowIso();
|
const generatedAt = nowIso();
|
||||||
appendEvent(dir, { kind: "review_started", runId: state.runId, timestamp: generatedAt, cycleIndex });
|
appendEvent(dir, { kind: "review_started", runId: state.runId, timestamp: generatedAt, cycleIndex });
|
||||||
|
|
||||||
// 4. Persist all evidence needed by the Codex host. The CLI does not invoke
|
// 4. Persist all evidence needed by the host. The CLI does not invoke
|
||||||
// another reviewer: Codex owns review and final acceptance.
|
// another reviewer: the host owns review and final acceptance.
|
||||||
const hostReviewFile = cycleHostReviewFile(dir, cycleIndex);
|
const hostReviewFile = cycleHostReviewFile(dir, cycleIndex);
|
||||||
const nextExecutorCycle = cycleIndex + 1;
|
const nextExecutorCycle = cycleIndex + 1;
|
||||||
const detailedGuidanceRequested = nextExecutorCycle >= DETAILED_GUIDANCE_CYCLE_THRESHOLD;
|
const detailedGuidanceRequested = nextExecutorCycle >= DETAILED_GUIDANCE_CYCLE_THRESHOLD;
|
||||||
const bundle: HostReviewBundleV1 = {
|
const bundle: HostReviewBundleV1 = {
|
||||||
version: "1",
|
version: "1",
|
||||||
|
reviewer: effectiveReviewer,
|
||||||
runId: state.runId,
|
runId: state.runId,
|
||||||
cycleIndex,
|
cycleIndex,
|
||||||
repoRoot: state.repoRoot,
|
repoRoot: state.repoRoot,
|
||||||
@@ -897,7 +920,7 @@ export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
|||||||
runId: state.runId,
|
runId: state.runId,
|
||||||
timestamp: generatedAt,
|
timestamp: generatedAt,
|
||||||
cycleIndex,
|
cycleIndex,
|
||||||
data: { hostReviewFile, diffFile },
|
data: { reviewer: effectiveReviewer, hostReviewFile, diffFile },
|
||||||
});
|
});
|
||||||
|
|
||||||
state.status = "awaiting_host";
|
state.status = "awaiting_host";
|
||||||
@@ -905,11 +928,12 @@ export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
|||||||
state.updatedAt = generatedAt;
|
state.updatedAt = generatedAt;
|
||||||
writeStateSafety(dir, state);
|
writeStateSafety(dir, state);
|
||||||
|
|
||||||
process.stdout.write(`Codex review bundle ready (cycle ${cycleIndex})\n`);
|
process.stdout.write(`Review bundle ready for ${effectiveReviewer} (cycle ${cycleIndex})\n`);
|
||||||
process.stdout.write(` bundle: ${hostReviewFile}\n`);
|
process.stdout.write(` bundle: ${hostReviewFile}\n`);
|
||||||
process.stdout.write(` diff: ${diffFile}\n`);
|
process.stdout.write(` diff: ${diffFile}\n`);
|
||||||
process.stdout.write(`next: agent-workflow decide --run ${opts.runId} --input <decision.json|->\n`);
|
process.stdout.write(`next: agent-workflow decide --run ${opts.runId} --input <decision.json|-> --reviewer ${effectiveReviewer}\n`);
|
||||||
emitWorkflowMeta({
|
emitWorkflowMeta({
|
||||||
|
reviewer: effectiveReviewer,
|
||||||
runId: state.runId,
|
runId: state.runId,
|
||||||
status: state.status,
|
status: state.status,
|
||||||
cycleIndex,
|
cycleIndex,
|
||||||
@@ -924,6 +948,7 @@ export async function reviewWorkflow(opts: ReviewWorkflowOpts): Promise<void> {
|
|||||||
|
|
||||||
export interface DecideWorkflowOpts {
|
export interface DecideWorkflowOpts {
|
||||||
runId: string;
|
runId: string;
|
||||||
|
reviewer: string;
|
||||||
decisionInput: string; // file path or "-" for stdin
|
decisionInput: string; // file path or "-" for stdin
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
}
|
}
|
||||||
@@ -931,6 +956,11 @@ export interface DecideWorkflowOpts {
|
|||||||
export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
||||||
const { state, dir } = requireState(opts.runId);
|
const { state, dir } = requireState(opts.runId);
|
||||||
|
|
||||||
|
const effectiveReviewer = state.reviewer ?? "codex";
|
||||||
|
if (opts.reviewer !== effectiveReviewer) {
|
||||||
|
throw new WorkflowEngineError(`Caller reviewer '${opts.reviewer}' does not match the run reviewer '${effectiveReviewer}'.`, 4);
|
||||||
|
}
|
||||||
|
|
||||||
const resolvingScope = state.status === "awaiting_scope_resolution";
|
const resolvingScope = state.status === "awaiting_scope_resolution";
|
||||||
if (state.status !== "awaiting_host" && !resolvingScope) {
|
if (state.status !== "awaiting_host" && !resolvingScope) {
|
||||||
throw new WorkflowEngineError(
|
throw new WorkflowEngineError(
|
||||||
@@ -959,6 +989,14 @@ export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
|||||||
validateHostDecision(decisionObj);
|
validateHostDecision(decisionObj);
|
||||||
const decision = decisionObj as HostDecisionV1;
|
const decision = decisionObj as HostDecisionV1;
|
||||||
|
|
||||||
|
if (!decision.reviewer && state.reviewer === undefined) {
|
||||||
|
decision.reviewer = "codex";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision.reviewer !== effectiveReviewer) {
|
||||||
|
throw new WorkflowEngineError(`Decision reviewer '${decision.reviewer}' does not match the run reviewer '${effectiveReviewer}'.`, 4);
|
||||||
|
}
|
||||||
|
|
||||||
const cycleIndex = state.currentCycle;
|
const cycleIndex = state.currentCycle;
|
||||||
const cycleRecord = state.cycles.find((c) => c.cycleIndex === cycleIndex)!;
|
const cycleRecord = state.cycles.find((c) => c.cycleIndex === cycleIndex)!;
|
||||||
|
|
||||||
@@ -988,14 +1026,14 @@ export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
|||||||
if (!resolvingScope && decision.outcome === "fix") {
|
if (!resolvingScope && decision.outcome === "fix") {
|
||||||
const accepted = decision.findingDecisions.filter((finding) => finding.disposition === "accept");
|
const accepted = decision.findingDecisions.filter((finding) => finding.disposition === "accept");
|
||||||
if (accepted.length === 0) {
|
if (accepted.length === 0) {
|
||||||
throw new WorkflowEngineError("Fix decision must accept at least one Codex finding.", 4);
|
throw new WorkflowEngineError("Fix decision must accept at least one reviewer finding.", 4);
|
||||||
}
|
}
|
||||||
const missingSummary = accepted
|
const missingSummary = accepted
|
||||||
.filter((finding) => !finding.summary?.trim())
|
.filter((finding) => !finding.summary?.trim())
|
||||||
.map((finding) => finding.findingId);
|
.map((finding) => finding.findingId);
|
||||||
if (missingSummary.length > 0) {
|
if (missingSummary.length > 0) {
|
||||||
throw new WorkflowEngineError(
|
throw new WorkflowEngineError(
|
||||||
`Accepted Codex findings require a non-empty summary: ${missingSummary.join(", ")}.`,
|
`Accepted reviewer findings require a non-empty summary: ${missingSummary.join(", ")}.`,
|
||||||
4,
|
4,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1017,7 +1055,7 @@ export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
|||||||
runId: state.runId,
|
runId: state.runId,
|
||||||
timestamp: nowIso(),
|
timestamp: nowIso(),
|
||||||
cycleIndex,
|
cycleIndex,
|
||||||
data: { outcome: decision.outcome, reason: decision.reason },
|
data: { reviewer: effectiveReviewer, outcome: decision.outcome, reason: decision.reason },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle decision outcomes
|
// Handle decision outcomes
|
||||||
@@ -1026,12 +1064,12 @@ export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
|||||||
state,
|
state,
|
||||||
dir,
|
dir,
|
||||||
"needs_human",
|
"needs_human",
|
||||||
decision.reason ?? "Codex determined human intervention is required.",
|
decision.reason ?? "Reviewer determined human intervention is required.",
|
||||||
{ cycleIndex },
|
{ cycleIndex },
|
||||||
);
|
);
|
||||||
process.stdout.write(`workflow stopped: needs_human\n`);
|
process.stdout.write(`workflow stopped: needs_human\n`);
|
||||||
process.stdout.write(`reason: ${decision.reason ?? "human intervention required"}\n`);
|
process.stdout.write(`reason: ${decision.reason ?? "human intervention required"}\n`);
|
||||||
emitWorkflowMeta({ runId: state.runId, status: "needs_human", cycleIndex });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId: state.runId, status: "needs_human", cycleIndex });
|
||||||
process.exit(3);
|
process.exit(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,9 +1079,9 @@ export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
|||||||
if (acceptError) {
|
if (acceptError) {
|
||||||
throw new WorkflowEngineError(`Cannot accept: ${acceptError}`, 4);
|
throw new WorkflowEngineError(`Cannot accept: ${acceptError}`, 4);
|
||||||
}
|
}
|
||||||
transitionToTerminal(state, dir, "completed", "Codex accepted — all conditions met.", { cycleIndex });
|
transitionToTerminal(state, dir, "completed", "Reviewer accepted — all conditions met.", { cycleIndex });
|
||||||
process.stdout.write(`workflow completed: accepted\n`);
|
process.stdout.write(`workflow completed: accepted\n`);
|
||||||
emitWorkflowMeta({ runId: state.runId, status: "completed", cycleIndex });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId: state.runId, status: "completed", cycleIndex });
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1057,7 +1095,7 @@ export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
|||||||
`Reached maximum cycles (${state.maxCycles}). Further execution not allowed.`,
|
`Reached maximum cycles (${state.maxCycles}). Further execution not allowed.`,
|
||||||
);
|
);
|
||||||
process.stdout.write(`workflow stopped: budget_exhausted (max cycles: ${state.maxCycles})\n`);
|
process.stdout.write(`workflow stopped: budget_exhausted (max cycles: ${state.maxCycles})\n`);
|
||||||
emitWorkflowMeta({ runId: state.runId, status: "budget_exhausted", cycleIndex });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId: state.runId, status: "budget_exhausted", cycleIndex });
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1067,7 +1105,7 @@ export async function decideWorkflow(opts: DecideWorkflowOpts): Promise<void> {
|
|||||||
transitionToTerminal(state, dir, "needs_human", convergenceError, { cycleIndex });
|
transitionToTerminal(state, dir, "needs_human", convergenceError, { cycleIndex });
|
||||||
process.stdout.write(`workflow stopped: needs_human (non-convergence detected)\n`);
|
process.stdout.write(`workflow stopped: needs_human (non-convergence detected)\n`);
|
||||||
process.stdout.write(`reason: ${convergenceError}\n`);
|
process.stdout.write(`reason: ${convergenceError}\n`);
|
||||||
emitWorkflowMeta({ runId: state.runId, status: "needs_human", cycleIndex });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId: state.runId, status: "needs_human", cycleIndex });
|
||||||
process.exit(3);
|
process.exit(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1244,18 +1282,19 @@ async function resumeExecutorCycle(
|
|||||||
state.updatedAt = nowIso();
|
state.updatedAt = nowIso();
|
||||||
writeStateSafety(dir, state);
|
writeStateSafety(dir, state);
|
||||||
|
|
||||||
|
const effectiveReviewer = state.reviewer ?? "codex";
|
||||||
process.stdout.write(`cycle ${cycleIndex} execution completed\n`);
|
process.stdout.write(`cycle ${cycleIndex} execution completed\n`);
|
||||||
process.stdout.write(`status: awaiting_review\n`);
|
process.stdout.write(`status: awaiting_review\n`);
|
||||||
process.stdout.write(`next: agent-workflow review --run ${state.runId}\n`);
|
process.stdout.write(`next: agent-workflow review --run ${state.runId} --reviewer ${effectiveReviewer}\n`);
|
||||||
emitWorkflowMeta({ runId: state.runId, status: state.status, cycleIndex });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId: state.runId, status: state.status, cycleIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAcceptConditions(state: WorkflowState, dir: string, cycleIndex: number, decision: HostDecisionV1): string | undefined {
|
function validateAcceptConditions(state: WorkflowState, dir: string, cycleIndex: number, decision: HostDecisionV1): string | undefined {
|
||||||
// 1. Codex must have received a persisted review bundle for this cycle.
|
// 1. The reviewer must have received a persisted review bundle for this cycle.
|
||||||
const cycle = state.cycles.find((c) => c.cycleIndex === cycleIndex);
|
const cycle = state.cycles.find((c) => c.cycleIndex === cycleIndex);
|
||||||
if (!cycle?.hostReviewFile) return "No Codex review bundle for this cycle.";
|
if (!cycle?.hostReviewFile) return "No host review bundle for this cycle.";
|
||||||
if (!fs.existsSync(path.join(dir, cycle.hostReviewFile))) {
|
if (!fs.existsSync(path.join(dir, cycle.hostReviewFile))) {
|
||||||
return "Codex review bundle is missing from disk.";
|
return "Host review bundle is missing from disk.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Baseline must not have changed
|
// 2. Baseline must not have changed
|
||||||
@@ -1345,7 +1384,8 @@ function buildAcceptedFindingsText(
|
|||||||
): string {
|
): string {
|
||||||
const accepted = decision.findingDecisions.filter((d) => d.disposition === "accept");
|
const accepted = decision.findingDecisions.filter((d) => d.disposition === "accept");
|
||||||
|
|
||||||
const lines = ["# Codex Review Findings", "", "Fix only the following issues accepted by the Codex host:"];
|
const runReviewer = _state.reviewer ?? "codex";
|
||||||
|
const lines = [`# ${runReviewer} Review Findings`, "", `Fix only the following issues accepted by the reviewer:`];
|
||||||
for (const d of accepted) {
|
for (const d of accepted) {
|
||||||
lines.push(`\n### Finding ${d.findingId}`);
|
lines.push(`\n### Finding ${d.findingId}`);
|
||||||
if (d.path) lines.push(`- Path: ${d.path}`);
|
if (d.path) lines.push(`- Path: ${d.path}`);
|
||||||
@@ -1391,10 +1431,17 @@ function buildScopeResolutionText(violations: ScopeViolation[]): string {
|
|||||||
|
|
||||||
export interface RetryReviewWorkflowOpts {
|
export interface RetryReviewWorkflowOpts {
|
||||||
runId: string;
|
runId: string;
|
||||||
|
reviewer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function retryReviewWorkflow(opts: RetryReviewWorkflowOpts): void {
|
export function retryReviewWorkflow(opts: RetryReviewWorkflowOpts): void {
|
||||||
const { state, dir } = requireState(opts.runId);
|
const { state, dir } = requireState(opts.runId);
|
||||||
|
|
||||||
|
const effectiveReviewer = state.reviewer ?? "codex";
|
||||||
|
if (opts.reviewer !== effectiveReviewer) {
|
||||||
|
throw new WorkflowEngineError(`Caller reviewer '${opts.reviewer}' does not match the run reviewer '${effectiveReviewer}'.`, 4);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.status !== "awaiting_scope_resolution") {
|
if (state.status !== "awaiting_scope_resolution") {
|
||||||
throw new WorkflowEngineError(
|
throw new WorkflowEngineError(
|
||||||
`Run '${opts.runId}' cannot retry review from status '${state.status}'. Only scope-resolution runs are eligible.`,
|
`Run '${opts.runId}' cannot retry review from status '${state.status}'. Only scope-resolution runs are eligible.`,
|
||||||
@@ -1459,8 +1506,8 @@ export function retryReviewWorkflow(opts: RetryReviewWorkflowOpts): void {
|
|||||||
|
|
||||||
process.stdout.write(`workflow review retry enabled: ${state.runId}\n`);
|
process.stdout.write(`workflow review retry enabled: ${state.runId}\n`);
|
||||||
process.stdout.write(`status: awaiting_review\n`);
|
process.stdout.write(`status: awaiting_review\n`);
|
||||||
process.stdout.write(`next: agent-workflow review --run ${state.runId}\n`);
|
process.stdout.write(`next: agent-workflow review --run ${state.runId} --reviewer ${effectiveReviewer}\n`);
|
||||||
emitWorkflowMeta({ runId: state.runId, status: state.status, cycleIndex: state.currentCycle });
|
emitWorkflowMeta({ reviewer: effectiveReviewer, runId: state.runId, status: state.status, cycleIndex: state.currentCycle });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1867,6 +1914,7 @@ export function statusWorkflow(opts: StatusWorkflowOpts): void {
|
|||||||
const output: WorkflowStatusOutput = {
|
const output: WorkflowStatusOutput = {
|
||||||
runId: state.runId,
|
runId: state.runId,
|
||||||
status: state.status,
|
status: state.status,
|
||||||
|
reviewer: state.reviewer ?? "codex",
|
||||||
executor: state.executor,
|
executor: state.executor,
|
||||||
currentCycle: state.currentCycle,
|
currentCycle: state.currentCycle,
|
||||||
maxCycles: state.maxCycles,
|
maxCycles: state.maxCycles,
|
||||||
@@ -1909,6 +1957,7 @@ export function statusWorkflow(opts: StatusWorkflowOpts): void {
|
|||||||
"── agent-workflow ─────────────────────",
|
"── agent-workflow ─────────────────────",
|
||||||
p("Run ID", state.runId),
|
p("Run ID", state.runId),
|
||||||
p("Status", state.status.toUpperCase()),
|
p("Status", state.status.toUpperCase()),
|
||||||
|
p("Reviewer", state.reviewer ?? "codex"),
|
||||||
p("Executor", state.executor),
|
p("Executor", state.executor),
|
||||||
p("Cycle", `${state.currentCycle} / ${state.maxCycles} (${output.remainingCycles} remaining)`),
|
p("Cycle", `${state.currentCycle} / ${state.maxCycles} (${output.remainingCycles} remaining)`),
|
||||||
];
|
];
|
||||||
@@ -1986,7 +2035,7 @@ export function abortWorkflow(opts: AbortWorkflowOpts): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(`workflow aborted: ${opts.runId}\nreason: ${opts.reason}\n`);
|
process.stdout.write(`workflow aborted: ${opts.runId}\nreason: ${opts.reason}\n`);
|
||||||
emitWorkflowMeta({ runId: opts.runId, status: "aborted", reason: opts.reason });
|
emitWorkflowMeta({ reviewer: state.reviewer ?? "codex", runId: opts.runId, status: "aborted", reason: opts.reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ fi
|
|||||||
expect(state.currentCycle).toBe(1);
|
expect(state.currentCycle).toBe(1);
|
||||||
|
|
||||||
// --- STEP 2: SCOPE GATE (Cycle 1, Codex review bundle is not ready yet) ---
|
// --- STEP 2: SCOPE GATE (Cycle 1, Codex review bundle is not ready yet) ---
|
||||||
await reviewWorkflow({ runId, cwd: tmpDir });
|
await reviewWorkflow({ runId, cwd: tmpDir, reviewer: "codex" });
|
||||||
let state2 = readState(runDir(repoRoot, runId))!;
|
let state2 = readState(runDir(repoRoot, runId))!;
|
||||||
expect(state2.status).toBe("awaiting_scope_resolution");
|
expect(state2.status).toBe("awaiting_scope_resolution");
|
||||||
expect(state2.cycles[0].scopeViolations).toEqual([
|
expect(state2.cycles[0].scopeViolations).toEqual([
|
||||||
@@ -160,21 +160,27 @@ fi
|
|||||||
]);
|
]);
|
||||||
expect(state2.cycles[0].hostReviewFile).toBeUndefined();
|
expect(state2.cycles[0].hostReviewFile).toBeUndefined();
|
||||||
|
|
||||||
|
const events2 = fs.readFileSync(path.join(runDir(repoRoot, runId), "events.jsonl"), "utf8").trim().split("\n").map(l => JSON.parse(l));
|
||||||
|
const startedEvent = events2.find(e => e.kind === "workflow_started");
|
||||||
|
expect(startedEvent?.data?.reviewer).toBe("codex");
|
||||||
|
|
||||||
const incompleteDecision: HostDecisionV1 = {
|
const incompleteDecision: HostDecisionV1 = {
|
||||||
version: "1",
|
version: "1",
|
||||||
|
reviewer: "codex",
|
||||||
outcome: "fix",
|
outcome: "fix",
|
||||||
findingDecisions: [{ findingId: "scope-1", disposition: "accept" }],
|
findingDecisions: [{ findingId: "scope-1", disposition: "accept" }],
|
||||||
decidedAt: new Date().toISOString(),
|
decidedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
const incompleteDecisionPath = path.join(process.env.HOME!, "decision-incomplete.json");
|
const incompleteDecisionPath = path.join(process.env.HOME!, "decision-incomplete.json");
|
||||||
fs.writeFileSync(incompleteDecisionPath, JSON.stringify(incompleteDecision));
|
fs.writeFileSync(incompleteDecisionPath, JSON.stringify(incompleteDecision));
|
||||||
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: incompleteDecisionPath }))
|
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: incompleteDecisionPath, reviewer: "codex" }))
|
||||||
.rejects.toThrow(/accept every current violation exactly once/);
|
.rejects.toThrow(/accept every current violation exactly once/);
|
||||||
expect(readState(runDir(repoRoot, runId))?.status).toBe("awaiting_scope_resolution");
|
expect(readState(runDir(repoRoot, runId))?.status).toBe("awaiting_scope_resolution");
|
||||||
|
|
||||||
// --- STEP 3: DECIDE (Scope fix using the same Reasonix session) ---
|
// --- STEP 3: DECIDE (Scope fix using the same Reasonix session) ---
|
||||||
const scopeDecision: HostDecisionV1 = {
|
const scopeDecision: HostDecisionV1 = {
|
||||||
version: "1",
|
version: "1",
|
||||||
|
reviewer: "codex",
|
||||||
outcome: "fix",
|
outcome: "fix",
|
||||||
findingDecisions: [
|
findingDecisions: [
|
||||||
{ findingId: "scope-1", disposition: "accept" },
|
{ findingId: "scope-1", disposition: "accept" },
|
||||||
@@ -184,7 +190,7 @@ fi
|
|||||||
};
|
};
|
||||||
const scopeDecisionPath = path.join(process.env.HOME!, "decision-scope.json");
|
const scopeDecisionPath = path.join(process.env.HOME!, "decision-scope.json");
|
||||||
fs.writeFileSync(scopeDecisionPath, JSON.stringify(scopeDecision));
|
fs.writeFileSync(scopeDecisionPath, JSON.stringify(scopeDecision));
|
||||||
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: scopeDecisionPath }))
|
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: scopeDecisionPath, reviewer: "codex" }))
|
||||||
.rejects.toThrow(/Executor failed/);
|
.rejects.toThrow(/Executor failed/);
|
||||||
expect(readState(runDir(repoRoot, runId))?.status).toBe("blocked");
|
expect(readState(runDir(repoRoot, runId))?.status).toBe("blocked");
|
||||||
|
|
||||||
@@ -196,8 +202,15 @@ fi
|
|||||||
expect(fs.existsSync(path.join(tmpDir, "tmp_oc.db-shm"))).toBe(false);
|
expect(fs.existsSync(path.join(tmpDir, "tmp_oc.db-shm"))).toBe(false);
|
||||||
expect(fs.existsSync(path.join(tmpDir, "tmp_oc.db-wal"))).toBe(false);
|
expect(fs.existsSync(path.join(tmpDir, "tmp_oc.db-wal"))).toBe(false);
|
||||||
|
|
||||||
|
const events3 = fs.readFileSync(path.join(runDir(repoRoot, runId), "events.jsonl"), "utf8").trim().split("\n").map(l => JSON.parse(l));
|
||||||
|
const decisionEvent = events3.find(e => e.kind === "decision_received" && e.cycleIndex === 1);
|
||||||
|
expect(decisionEvent?.data?.reviewer).toBe("codex");
|
||||||
|
|
||||||
|
const savedDecision1 = JSON.parse(fs.readFileSync(path.join(runDir(repoRoot, runId), "cycle-1-decision.json"), "utf8"));
|
||||||
|
expect(savedDecision1.reviewer).toBe("codex");
|
||||||
|
|
||||||
// --- STEP 4: PREPARE CODEX REVIEW (Cycle 2) ---
|
// --- STEP 4: PREPARE CODEX REVIEW (Cycle 2) ---
|
||||||
await reviewWorkflow({ runId, cwd: tmpDir });
|
await reviewWorkflow({ runId, cwd: tmpDir, reviewer: "codex" });
|
||||||
let state4 = readState(runDir(repoRoot, runId))!;
|
let state4 = readState(runDir(repoRoot, runId))!;
|
||||||
expect(state4.status).toBe("awaiting_host");
|
expect(state4.status).toBe("awaiting_host");
|
||||||
expect(state4.cycles[1].hostReviewFile).toBe("cycle-2-host-review.json");
|
expect(state4.cycles[1].hostReviewFile).toBe("cycle-2-host-review.json");
|
||||||
@@ -205,6 +218,11 @@ fi
|
|||||||
path.join(runDir(repoRoot, runId), state4.cycles[1].hostReviewFile!),
|
path.join(runDir(repoRoot, runId), state4.cycles[1].hostReviewFile!),
|
||||||
"utf8",
|
"utf8",
|
||||||
));
|
));
|
||||||
|
expect(reviewBundle.reviewer).toBe("codex");
|
||||||
|
|
||||||
|
const events4 = fs.readFileSync(path.join(runDir(repoRoot, runId), "events.jsonl"), "utf8").trim().split("\n").map(l => JSON.parse(l));
|
||||||
|
const reviewCompletedEvent = events4.find(e => e.kind === "review_completed" && e.cycleIndex === 2);
|
||||||
|
expect(reviewCompletedEvent?.data?.reviewer).toBe("codex");
|
||||||
expect(reviewBundle.repoRoot).toBe(repoRoot);
|
expect(reviewBundle.repoRoot).toBe(repoRoot);
|
||||||
expect(reviewBundle.diffFile).toBe(path.join(runDir(repoRoot, runId), "cycle-2.diff"));
|
expect(reviewBundle.diffFile).toBe(path.join(runDir(repoRoot, runId), "cycle-2.diff"));
|
||||||
expect(reviewBundle.executorLogFile).toBe(path.join(runDir(repoRoot, runId), "cycle-2-exec-retry-1.log"));
|
expect(reviewBundle.executorLogFile).toBe(path.join(runDir(repoRoot, runId), "cycle-2-exec-retry-1.log"));
|
||||||
@@ -223,19 +241,21 @@ fi
|
|||||||
// --- STEP 5: CODEX DECIDES FIX ---
|
// --- STEP 5: CODEX DECIDES FIX ---
|
||||||
const incompleteFix: HostDecisionV1 = {
|
const incompleteFix: HostDecisionV1 = {
|
||||||
version: "1",
|
version: "1",
|
||||||
|
reviewer: "codex",
|
||||||
outcome: "fix",
|
outcome: "fix",
|
||||||
findingDecisions: [{ findingId: "codex-1", disposition: "accept" }],
|
findingDecisions: [{ findingId: "codex-1", disposition: "accept" }],
|
||||||
decidedAt: new Date().toISOString(),
|
decidedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
const incompleteFixPath = path.join(process.env.HOME!, "decision-fix-incomplete.json");
|
const incompleteFixPath = path.join(process.env.HOME!, "decision-fix-incomplete.json");
|
||||||
fs.writeFileSync(incompleteFixPath, JSON.stringify(incompleteFix));
|
fs.writeFileSync(incompleteFixPath, JSON.stringify(incompleteFix));
|
||||||
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: incompleteFixPath }))
|
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: incompleteFixPath, reviewer: "codex" }))
|
||||||
.rejects.toThrow(/require a non-empty summary/);
|
.rejects.toThrow(/require a non-empty summary/);
|
||||||
expect(readState(runDir(repoRoot, runId))?.status).toBe("awaiting_host");
|
expect(readState(runDir(repoRoot, runId))?.status).toBe("awaiting_host");
|
||||||
|
|
||||||
const guidanceMarkerText = "Replace `const x = 2;` in index.ts with `const x = 3;`";
|
const guidanceMarkerText = "Replace `const x = 2;` in index.ts with `const x = 3;`";
|
||||||
const fixDecision: HostDecisionV1 = {
|
const fixDecision: HostDecisionV1 = {
|
||||||
version: "1",
|
version: "1",
|
||||||
|
reviewer: "codex",
|
||||||
outcome: "fix",
|
outcome: "fix",
|
||||||
findingDecisions: [{
|
findingDecisions: [{
|
||||||
findingId: "codex-1",
|
findingId: "codex-1",
|
||||||
@@ -248,7 +268,7 @@ fi
|
|||||||
};
|
};
|
||||||
const fixDecisionPath = path.join(process.env.HOME!, "decision-fix.json");
|
const fixDecisionPath = path.join(process.env.HOME!, "decision-fix.json");
|
||||||
fs.writeFileSync(fixDecisionPath, JSON.stringify(fixDecision));
|
fs.writeFileSync(fixDecisionPath, JSON.stringify(fixDecision));
|
||||||
await decideWorkflow({ runId, cwd: tmpDir, decisionInput: fixDecisionPath });
|
await decideWorkflow({ runId, cwd: tmpDir, decisionInput: fixDecisionPath, reviewer: "codex" });
|
||||||
|
|
||||||
let state5 = readState(runDir(repoRoot, runId))!;
|
let state5 = readState(runDir(repoRoot, runId))!;
|
||||||
expect(state5.status).toBe("awaiting_review");
|
expect(state5.status).toBe("awaiting_review");
|
||||||
@@ -266,7 +286,7 @@ fi
|
|||||||
expect(cycle3Prompt).toContain("Add an index.ts file.");
|
expect(cycle3Prompt).toContain("Add an index.ts file.");
|
||||||
|
|
||||||
// --- STEP 6: PREPARE CODEX RE-REVIEW (Cycle 3) ---
|
// --- STEP 6: PREPARE CODEX RE-REVIEW (Cycle 3) ---
|
||||||
await reviewWorkflow({ runId, cwd: tmpDir });
|
await reviewWorkflow({ runId, cwd: tmpDir, reviewer: "codex" });
|
||||||
let state6 = readState(runDir(repoRoot, runId))!;
|
let state6 = readState(runDir(repoRoot, runId))!;
|
||||||
expect(state6.status).toBe("awaiting_host");
|
expect(state6.status).toBe("awaiting_host");
|
||||||
expect(state6.cycles[2].hostReviewFile).toBe("cycle-3-host-review.json");
|
expect(state6.cycles[2].hostReviewFile).toBe("cycle-3-host-review.json");
|
||||||
@@ -274,6 +294,7 @@ fi
|
|||||||
// --- STEP 7: DECIDE (Accept) ---
|
// --- STEP 7: DECIDE (Accept) ---
|
||||||
const acceptDecision: HostDecisionV1 = {
|
const acceptDecision: HostDecisionV1 = {
|
||||||
version: "1",
|
version: "1",
|
||||||
|
reviewer: "codex",
|
||||||
outcome: "accept",
|
outcome: "accept",
|
||||||
findingDecisions: [],
|
findingDecisions: [],
|
||||||
verificationEvidence: ["verified output"],
|
verificationEvidence: ["verified output"],
|
||||||
@@ -282,12 +303,110 @@ fi
|
|||||||
const acceptDecisionPath = path.join(process.env.HOME!, "decision2.json");
|
const acceptDecisionPath = path.join(process.env.HOME!, "decision2.json");
|
||||||
fs.writeFileSync(acceptDecisionPath, JSON.stringify(acceptDecision));
|
fs.writeFileSync(acceptDecisionPath, JSON.stringify(acceptDecision));
|
||||||
try {
|
try {
|
||||||
await decideWorkflow({ runId, cwd: tmpDir, decisionInput: acceptDecisionPath });
|
await decideWorkflow({ runId, cwd: tmpDir, decisionInput: acceptDecisionPath, reviewer: "codex" });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.message !== "MockExit0") throw err;
|
if (err.message !== "MockExit0") throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state7 = readState(runDir(repoRoot, runId))!;
|
let state7 = readState(runDir(repoRoot, runId))!;
|
||||||
expect(state7.status).toBe("completed");
|
expect(state7.status).toBe("completed");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
it("rejects claude executor with claude reviewer", async () => {
|
||||||
|
const executorScript = `echo "v1.0.0"
|
||||||
|
exit 0`;
|
||||||
|
const executorBin = writeFakeCli(tmpDir, "fake-executor.sh", executorScript);
|
||||||
|
fs.writeFileSync(path.join(tmpDir, "agent-workflow.json"), JSON.stringify({
|
||||||
|
version: "1",
|
||||||
|
executors: { claude: { binary: executorBin } }
|
||||||
|
}));
|
||||||
|
const planPath = path.join(tmpDir, "plan.json");
|
||||||
|
fs.writeFileSync(planPath, JSON.stringify({
|
||||||
|
version: "1",
|
||||||
|
title: "Title",
|
||||||
|
planMarkdown: "Plan",
|
||||||
|
scope: ["index.ts"],
|
||||||
|
acceptanceCriteria: ["ac"],
|
||||||
|
verificationCommands: [["cmd"]],
|
||||||
|
}));
|
||||||
|
require("child_process").execFileSync("git", ["add", "."], { cwd: tmpDir });
|
||||||
|
require("child_process").execFileSync("git", ["commit", "-m", "test1"], { cwd: tmpDir });
|
||||||
|
await expect(startWorkflow({
|
||||||
|
cwd: tmpDir,
|
||||||
|
executor: "claude",
|
||||||
|
reviewer: "claude",
|
||||||
|
planInput: planPath,
|
||||||
|
})).rejects.toThrow(/Claude cannot self-review/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces frozen reviewer identity and checks legacy fallback", async () => {
|
||||||
|
const executorScript = `
|
||||||
|
if [[ "$*" == *"--version"* ]]; then
|
||||||
|
echo "v1.0.0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "const x = 2;" > "$3/index.ts"
|
||||||
|
exit 0
|
||||||
|
`;
|
||||||
|
const executorBin = writeFakeCli(tmpDir, "fake-executor.sh", executorScript);
|
||||||
|
fs.writeFileSync(path.join(tmpDir, "agent-workflow.json"), JSON.stringify({
|
||||||
|
version: "1",
|
||||||
|
executors: { reasonix: { binary: executorBin } }
|
||||||
|
}));
|
||||||
|
const planPath = path.join(tmpDir, "plan.json");
|
||||||
|
fs.writeFileSync(planPath, JSON.stringify({
|
||||||
|
version: "1",
|
||||||
|
title: "Title",
|
||||||
|
planMarkdown: "Plan",
|
||||||
|
scope: ["index.ts"],
|
||||||
|
acceptanceCriteria: ["ac"],
|
||||||
|
verificationCommands: [["cmd"]],
|
||||||
|
}));
|
||||||
|
|
||||||
|
require("child_process").execFileSync("git", ["add", "."], { cwd: tmpDir });
|
||||||
|
require("child_process").execFileSync("git", ["commit", "-m", "test2"], { cwd: tmpDir });
|
||||||
|
// Start with claude reviewer
|
||||||
|
const { runId } = await startWorkflow({
|
||||||
|
cwd: tmpDir,
|
||||||
|
executor: "reasonix",
|
||||||
|
reviewer: "claude",
|
||||||
|
planInput: planPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mismatched reviewer
|
||||||
|
await expect(reviewWorkflow({ runId, cwd: tmpDir, reviewer: "codex" }))
|
||||||
|
.rejects.toThrow(/does not match/i);
|
||||||
|
|
||||||
|
// Correct reviewer
|
||||||
|
await reviewWorkflow({ runId, cwd: tmpDir, reviewer: "claude" });
|
||||||
|
|
||||||
|
// Test legacy fallback: overwrite state to remove reviewer
|
||||||
|
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: tmpDir, encoding: "utf8" }).trim();
|
||||||
|
const sPath = path.join(runDir(repoRoot, runId), "state.json");
|
||||||
|
const stateObj = JSON.parse(fs.readFileSync(sPath, "utf8"));
|
||||||
|
delete stateObj.reviewer;
|
||||||
|
fs.writeFileSync(sPath, JSON.stringify(stateObj));
|
||||||
|
|
||||||
|
// After removing reviewer, claude should be rejected
|
||||||
|
await expect(reviewWorkflow({ runId, cwd: tmpDir, reviewer: "claude" }))
|
||||||
|
.rejects.toThrow(/does not match/i);
|
||||||
|
|
||||||
|
// codex should be allowed (legacy fallback)
|
||||||
|
// To do this we must reset the cycle or test a decide/retry-review
|
||||||
|
const decisionPath = path.join(process.env.HOME!, "decision.json");
|
||||||
|
fs.writeFileSync(decisionPath, JSON.stringify({
|
||||||
|
version: "1",
|
||||||
|
reviewer: "codex",
|
||||||
|
outcome: "fix",
|
||||||
|
findingDecisions: [{ findingId: "codex-1", disposition: "accept", summary: "sum" }],
|
||||||
|
decidedAt: new Date().toISOString()
|
||||||
|
}));
|
||||||
|
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: decisionPath, reviewer: "claude" }))
|
||||||
|
.rejects.toThrow(/does not match/i);
|
||||||
|
|
||||||
|
// Works with codex because legacy defaults to codex
|
||||||
|
// But since it's in awaiting_host, we need a valid hostReviewFile. We skipped some mock setup so decideWorkflow might fail on executor start, but it passes the reviewer gate!
|
||||||
|
await expect(decideWorkflow({ runId, cwd: tmpDir, decisionInput: decisionPath, reviewer: "codex" }))
|
||||||
|
.rejects.toThrow(/Cannot resume: no session handle/); // Means it passed the reviewer gate!
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ describe("None VCS Provider (Snapshot Mode)", () => {
|
|||||||
fs.writeFileSync(path.join(tmpDir, "src", "code.txt"), "console.log(2)\n", "utf8");
|
fs.writeFileSync(path.join(tmpDir, "src", "code.txt"), "console.log(2)\n", "utf8");
|
||||||
|
|
||||||
// reviewWorkflow must succeed (generates diff, validates scope)
|
// reviewWorkflow must succeed (generates diff, validates scope)
|
||||||
await reviewWorkflow({ runId, cwd: tmpDir });
|
await reviewWorkflow({ runId, cwd: tmpDir, reviewer: "codex" });
|
||||||
const updatedState = readState(wDir!);
|
const updatedState = readState(wDir!);
|
||||||
expect(updatedState!.status).toBe("awaiting_host");
|
expect(updatedState!.status).toBe("awaiting_host");
|
||||||
|
|
||||||
@@ -533,7 +533,7 @@ describe("None VCS Provider (Snapshot Mode)", () => {
|
|||||||
fs.writeFileSync(path.join(wDir!, "state.json"), JSON.stringify(updatedState, null, 2), "utf8");
|
fs.writeFileSync(path.join(wDir!, "state.json"), JSON.stringify(updatedState, null, 2), "utf8");
|
||||||
|
|
||||||
// reviewWorkflow must transition run to awaiting_scope_resolution due to out-of-scope files
|
// reviewWorkflow must transition run to awaiting_scope_resolution due to out-of-scope files
|
||||||
await reviewWorkflow({ runId, cwd: tmpDir });
|
await reviewWorkflow({ runId, cwd: tmpDir, reviewer: "codex" });
|
||||||
const blockedState = readState(wDir!);
|
const blockedState = readState(wDir!);
|
||||||
expect(blockedState!.status).toBe("awaiting_scope_resolution");
|
expect(blockedState!.status).toBe("awaiting_scope_resolution");
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ export function initWorkflowState(opts: {
|
|||||||
vcsBaseline?: VcsBaseline;
|
vcsBaseline?: VcsBaseline;
|
||||||
vcsKind?: import("./workflow-types.js").VcsKind;
|
vcsKind?: import("./workflow-types.js").VcsKind;
|
||||||
executor: import("./workflow-types.js").ExecutorKind;
|
executor: import("./workflow-types.js").ExecutorKind;
|
||||||
|
reviewer: import("./workflow-types.js").ReviewerKind;
|
||||||
plan: import("./workflow-types.js").WorkflowPlanV1;
|
plan: import("./workflow-types.js").WorkflowPlanV1;
|
||||||
maxCycles: number;
|
maxCycles: number;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
@@ -325,6 +326,7 @@ export function initWorkflowState(opts: {
|
|||||||
...(opts.vcsBaseline ? { vcsBaseline: opts.vcsBaseline } : {}),
|
...(opts.vcsBaseline ? { vcsBaseline: opts.vcsBaseline } : {}),
|
||||||
...(opts.vcsKind ? { vcsKind: opts.vcsKind } : {}),
|
...(opts.vcsKind ? { vcsKind: opts.vcsKind } : {}),
|
||||||
executor: opts.executor,
|
executor: opts.executor,
|
||||||
|
reviewer: opts.reviewer,
|
||||||
plan: opts.plan,
|
plan: opts.plan,
|
||||||
status: "executing",
|
status: "executing",
|
||||||
maxCycles: opts.maxCycles,
|
maxCycles: opts.maxCycles,
|
||||||
|
|||||||
+28
-17
@@ -1,13 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Workflow types for the agent-workflow host orchestration loop.
|
* Workflow types for the agent-workflow host orchestration loop.
|
||||||
*
|
*
|
||||||
* WorkflowPlanV1 — input from Codex describing what to implement
|
* WorkflowPlanV1 — input from the reviewer describing what to implement
|
||||||
* WorkflowState — durable run state (persisted as state.json)
|
* WorkflowState — durable run state (persisted as state.json)
|
||||||
* HostDecisionV1 — Codex decision after reviewing findings
|
* HostDecisionV1 — reviewer decision after reviewing findings
|
||||||
* ExecutorKind — the three supported executor adapters
|
* ExecutorKind — the three supported executor adapters
|
||||||
* WorkflowStatus — machine-readable status summary
|
* WorkflowStatus — machine-readable status summary
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Reviewers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const REVIEWER_KINDS = ["codex", "claude"] as const;
|
||||||
|
export type ReviewerKind = (typeof REVIEWER_KINDS)[number];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Executors
|
// Executors
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -78,7 +85,7 @@ export interface ProgressSidecar {
|
|||||||
export type ProgressSidecarUpdate = Partial<Omit<ProgressSidecar, "executor" | "cycleIndex" | "attemptIndex" | "startedAt">>;
|
export type ProgressSidecarUpdate = Partial<Omit<ProgressSidecar, "executor" | "cycleIndex" | "attemptIndex" | "startedAt">>;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Plan (input from Codex)
|
// Plan (input from reviewer)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface WorkflowPlanV1 {
|
export interface WorkflowPlanV1 {
|
||||||
@@ -173,15 +180,15 @@ export interface CycleRecord {
|
|||||||
executorAttemptLogs?: string[];
|
executorAttemptLogs?: string[];
|
||||||
/** Path to the diff file generated after this cycle (relative to run dir). */
|
/** Path to the diff file generated after this cycle (relative to run dir). */
|
||||||
diffFile?: string;
|
diffFile?: string;
|
||||||
/** Path to the Codex host review bundle (relative to run dir). */
|
/** Path to the host review bundle (relative to run dir). */
|
||||||
hostReviewFile?: string;
|
hostReviewFile?: string;
|
||||||
/** Time at which the review bundle became ready for Codex. */
|
/** Time at which the review bundle became ready for the reviewer. */
|
||||||
hostReviewReadyAt?: string;
|
hostReviewReadyAt?: string;
|
||||||
/** Files that failed the frozen scope gate before Codex review. */
|
/** Files that failed the frozen scope gate before reviewer review. */
|
||||||
scopeViolations?: ScopeViolation[];
|
scopeViolations?: ScopeViolation[];
|
||||||
/** Path to the Codex decision file (relative to run dir). */
|
/** Path to the reviewer decision file (relative to run dir). */
|
||||||
decisionFile?: string;
|
decisionFile?: string;
|
||||||
/** Codex decision outcome for this cycle. */
|
/** Reviewer decision outcome for this cycle. */
|
||||||
decisionOutcome?: HostDecisionOutcome;
|
decisionOutcome?: HostDecisionOutcome;
|
||||||
/** Cycle-level stop reason when this cycle triggered termination. */
|
/** Cycle-level stop reason when this cycle triggered termination. */
|
||||||
stopReason?: WorkflowTerminalStatus;
|
stopReason?: WorkflowTerminalStatus;
|
||||||
@@ -206,6 +213,7 @@ export interface WorkflowState {
|
|||||||
vcsKind?: VcsKind;
|
vcsKind?: VcsKind;
|
||||||
/** Directory used to load agent-workflow.json; persisted for per-cycle model reloads. */
|
/** Directory used to load agent-workflow.json; persisted for per-cycle model reloads. */
|
||||||
configSourceRoot?: string;
|
configSourceRoot?: string;
|
||||||
|
reviewer?: ReviewerKind;
|
||||||
executor: ExecutorKind;
|
executor: ExecutorKind;
|
||||||
executorConfig?: ExecutorConfig;
|
executorConfig?: ExecutorConfig;
|
||||||
/** Consecutive executor inactivity allowed before the host aborts the attempt. */
|
/** Consecutive executor inactivity allowed before the host aborts the attempt. */
|
||||||
@@ -244,18 +252,18 @@ export interface ExtensionRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Host decision (from Codex)
|
// Host decision (from reviewer)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const HOST_DECISION_OUTCOMES = ["fix", "accept", "needs_human"] as const;
|
export const HOST_DECISION_OUTCOMES = ["fix", "accept", "needs_human"] as const;
|
||||||
export type HostDecisionOutcome = (typeof HOST_DECISION_OUTCOMES)[number];
|
export type HostDecisionOutcome = (typeof HOST_DECISION_OUTCOMES)[number];
|
||||||
|
|
||||||
export interface FindingDecision {
|
export interface FindingDecision {
|
||||||
/** Stable id assigned by Codex, or scope-* for a scope violation. */
|
/** Stable id assigned by the reviewer, or scope-* for a scope violation. */
|
||||||
findingId: string;
|
findingId: string;
|
||||||
/** "accept" = fix it, "reject" = acknowledged but out of scope, "followup" = record and move on. */
|
/** "accept" = fix it, "reject" = acknowledged but out of scope, "followup" = record and move on. */
|
||||||
disposition: "accept" | "reject" | "followup";
|
disposition: "accept" | "reject" | "followup";
|
||||||
/** Concrete issue description. Required for accepted Codex findings. */
|
/** Concrete issue description. Required for accepted findings. */
|
||||||
summary?: string;
|
summary?: string;
|
||||||
/** Repository-relative location when known. */
|
/** Repository-relative location when known. */
|
||||||
path?: string;
|
path?: string;
|
||||||
@@ -265,14 +273,15 @@ export interface FindingDecision {
|
|||||||
export interface HostDecisionV1 {
|
export interface HostDecisionV1 {
|
||||||
/** Schema version — always "1". */
|
/** Schema version — always "1". */
|
||||||
version: "1";
|
version: "1";
|
||||||
|
reviewer: ReviewerKind;
|
||||||
outcome: HostDecisionOutcome;
|
outcome: HostDecisionOutcome;
|
||||||
/** Findings Codex decided on. */
|
/** Findings the reviewer decided on. */
|
||||||
findingDecisions: FindingDecision[];
|
findingDecisions: FindingDecision[];
|
||||||
/** Reason for needs_human or rejection rationale. */
|
/** Reason for needs_human or rejection rationale. */
|
||||||
reason?: string;
|
reason?: string;
|
||||||
/** Follow-up items to record (not to fix now). */
|
/** Follow-up items to record (not to fix now). */
|
||||||
followUps?: string[];
|
followUps?: string[];
|
||||||
/** Codex-supplied verification evidence (command outputs, test results). */
|
/** Reviewer-supplied verification evidence (command outputs, test results). */
|
||||||
verificationEvidence?: string[];
|
verificationEvidence?: string[];
|
||||||
/**
|
/**
|
||||||
* Optional detailed, step-by-step implementation plan the host provides so the
|
* Optional detailed, step-by-step implementation plan the host provides so the
|
||||||
@@ -281,13 +290,14 @@ export interface HostDecisionV1 {
|
|||||||
* detailedGuidanceRequested (the run is approaching/at the guidance threshold).
|
* detailedGuidanceRequested (the run is approaching/at the guidance threshold).
|
||||||
*/
|
*/
|
||||||
implementationGuidance?: string;
|
implementationGuidance?: string;
|
||||||
/** ISO timestamp when Codex submitted this decision. */
|
/** ISO timestamp when the reviewer submitted this decision. */
|
||||||
decidedAt: string;
|
decidedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persisted evidence package that Codex must inspect before deciding. */
|
/** Persisted evidence package that the reviewer must inspect before deciding. */
|
||||||
export interface HostReviewBundleV1 {
|
export interface HostReviewBundleV1 {
|
||||||
version: "1";
|
version: "1";
|
||||||
|
reviewer: ReviewerKind;
|
||||||
runId: string;
|
runId: string;
|
||||||
cycleIndex: number;
|
cycleIndex: number;
|
||||||
repoRoot: string;
|
repoRoot: string;
|
||||||
@@ -302,12 +312,12 @@ export interface HostReviewBundleV1 {
|
|||||||
verificationCommands: string[][];
|
verificationCommands: string[][];
|
||||||
/**
|
/**
|
||||||
* True when the run is at or past the guidance threshold (the executor cycle
|
* True when the run is at or past the guidance threshold (the executor cycle
|
||||||
* about to start >= DETAILED_GUIDANCE_CYCLE_THRESHOLD). Signals Codex to return
|
* about to start >= DETAILED_GUIDANCE_CYCLE_THRESHOLD). Signals the reviewer to return
|
||||||
* a more detailed implementation plan if it decides "fix".
|
* a more detailed implementation plan if it decides "fix".
|
||||||
*/
|
*/
|
||||||
detailedGuidanceRequested?: boolean;
|
detailedGuidanceRequested?: boolean;
|
||||||
/**
|
/**
|
||||||
* Human-meaningful instructions telling Codex what extra detail to supply.
|
* Human-meaningful instructions telling the reviewer what extra detail to supply.
|
||||||
* Only set when detailedGuidanceRequested is true.
|
* Only set when detailedGuidanceRequested is true.
|
||||||
*/
|
*/
|
||||||
reviewerInstructions?: string;
|
reviewerInstructions?: string;
|
||||||
@@ -358,6 +368,7 @@ export interface ResolvedWorkflowConfig {
|
|||||||
export interface WorkflowStatusOutput {
|
export interface WorkflowStatusOutput {
|
||||||
runId: string;
|
runId: string;
|
||||||
status: WorkflowStatus;
|
status: WorkflowStatus;
|
||||||
|
reviewer: ReviewerKind;
|
||||||
executor: ExecutorKind;
|
executor: ExecutorKind;
|
||||||
currentCycle: number;
|
currentCycle: number;
|
||||||
maxCycles: number;
|
maxCycles: number;
|
||||||
|
|||||||
@@ -843,7 +843,11 @@ export function getAssetsHtml(): string {
|
|||||||
<div class="meta-list">
|
<div class="meta-list">
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<span class="meta-label">执行器</span>
|
<span class="meta-label">执行器</span>
|
||||||
<span class="meta-value" id="view-executor">claude</span>
|
<span class="meta-value" id="view-executor" style="text-transform: capitalize;">claude</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">审查者</span>
|
||||||
|
<span class="meta-value" id="view-reviewer" style="text-transform: capitalize;">codex</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<span class="meta-label">版本控制</span>
|
<span class="meta-label">版本控制</span>
|
||||||
@@ -1316,6 +1320,7 @@ export function getAssetsHtml(): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('view-executor').textContent = activeRun.executor;
|
document.getElementById('view-executor').textContent = activeRun.executor;
|
||||||
|
document.getElementById('view-reviewer').textContent = activeRun.reviewer || 'codex';
|
||||||
document.getElementById('view-vcs').textContent = activeRun.vcsKind || 'none';
|
document.getElementById('view-vcs').textContent = activeRun.vcsKind || 'none';
|
||||||
document.getElementById('view-max-cycles').textContent = activeRun.maxCycles;
|
document.getElementById('view-max-cycles').textContent = activeRun.maxCycles;
|
||||||
document.getElementById('view-current-cycle').textContent = activeRun.currentCycle;
|
document.getElementById('view-current-cycle').textContent = activeRun.currentCycle;
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ printf '%s\\n' '{"providers":[{"name":"test-provider","models":["test-model"]}]}
|
|||||||
const res = await makeRequest(`${baseUrl}/api/runs`);
|
const res = await makeRequest(`${baseUrl}/api/runs`);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.headers["content-type"]).toContain("application/json");
|
expect(res.headers["content-type"]).toContain("application/json");
|
||||||
|
|
||||||
const runsList = JSON.parse(res.body);
|
const runsList = JSON.parse(res.body);
|
||||||
expect(runsList).toHaveLength(1);
|
expect(runsList).toHaveLength(1);
|
||||||
expect(runsList[0].runId).toBe(runId);
|
expect(runsList[0].runId).toBe(runId);
|
||||||
|
|||||||
Reference in New Issue
Block a user