fix(edge): Plan 구조 출력을 배열로 고정한다

This commit is contained in:
toki 2026-08-12 18:41:23 +09:00
parent f4191097a2
commit 58fdb32264
11 changed files with 122 additions and 72 deletions

View file

@ -102,9 +102,10 @@ from the operator's `execution_presets[].single_request.templates` configuration
`agent-contract/inner/edge-config-runtime-refresh.md`). These templates are
Edge-owned internal stage inputs only:
- The Plan stage requests a stage-owned strict JSON object with only `goal`, `steps`,
and `verification`, validates the bounded field grammar, and renders the frozen
Plan template inside Edge. The Review template similarly shapes the private
- The Plan stage requests a stage-owned strict JSON object with a one-line `goal`
string, 2-6 non-empty one-line `steps` strings, and 1-3 non-empty one-line
`verification` strings. Edge owns the Markdown bullet/newline formatting and
renders the frozen Plan template. The Review template similarly shapes the private
`review.md` artifact rendered from the model's `checks`, `verification`, and
`summary` fields. Provider output never controls either artifact's headings or
static template text.

View file

@ -169,7 +169,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행
| managed projection auth | `credential_plane.enabled=true` uses the fresh Control Plane projection for inbound token auth and principal route discovery. Static principal/bearer fallback is disabled. |
| managed slot route | Public model id/alias resolves to one projected route, exact slot/profile/upstream model/resource selector, and immutable revisions/generation. Unknown, cross-principal, stale, revoked, or ambiguous bindings fail closed. |
| marked preset single-request admission | An authorized fixed single-request preset compiles one service-owned admission value at request start: requested public model, canonical plan/work/review bindings resolved through managed authorization, opaque workspace capability, and absolute resource caps. Later refresh cannot mutate the admitted shape. No private binding is echoed to the caller. Compiled only after every canonical reference is verified through its catalog binding for the authenticated principal; missing, duplicate, unauthorized, dynamically selected, or option-inconsistent inputs are rejected without fallback. |
| marked single-request internal templates | The admission also freezes the operator-configured effective Plan/Review Markdown templates. They are internal artifact shapes only: the Plan stage obtains strict `goal`/`steps`/`verification` JSON fields and Edge renders `plan.md`, while the Review template shapes the private `review.md` artifact. Callers cannot supply, name, or select a template, and template paths, contents, and digests never appear in a response, error, log, or metric label. The caller-visible Messages request/response schema is unchanged and the final text stays the model's `decision.output`. |
| marked single-request internal templates | The admission also freezes the operator-configured effective Plan/Review Markdown templates. They are internal artifact shapes only: the Plan stage obtains a strict one-line `goal` plus bounded one-line `steps`/`verification` arrays, and Edge owns the bullet formatting and renders `plan.md`, while the Review template shapes the private `review.md` artifact. Callers cannot supply, name, or select a template, and template paths, contents, and digests never appear in a response, error, log, or metric label. The caller-visible Messages request/response schema is unchanged and the final text stays the model's `decision.output`. |
| marked single-request ingress | One validated and authorized Messages POST enters the separate service coordinator capability before legacy provider/caller continuation and increments `iop_anthropic_single_request_ingress_total` once. Non-streaming returns one buffered final-only message. Streaming keeps one envelope across the coordinator lifetime, exposes only fixed plan/work/review/repair text blocks plus `event: ping`, and commits one final text/error terminal. Internal reasoning/tool wire never becomes caller `tool_use`; success is acknowledged only after the complete terminal write succeeds. |
| marked single-request S11 terminal policy | The service freezes one closed `end_turn`, `length`, `error`, or `cancelled` disposition. `error` classes are provider, validation, timeout, budget, repetition, malformed, context, internal-tool, and workspace-cleanup. Buffered and SSE share one projection: `end_turn`; `max_tokens` with no private partial output; `400 invalid_request_error` for validation/context; `502 api_error` for other failures; and silent cancellation after caller disconnect. No terminal classification retries, falls back, opens a second request, or later writes success. |
| marked single-request observation evidence | A single real POST links ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, and raw-free correlation without public tool protocol. `iop_anthropic_single_request_ingress_total` is unlabeled (no request_id, stage_id, provider identity, or content). Internal tool names, raw arguments, private results, and workspace references are absent from the public terminal and from log projections. Stage-pure timing, cardinality-bounded labels, and privacy semantics are documented here. SDD S12 qualified an external Claude task on an approved IOP Node with ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, the exact expected workspace result, timing, cleanup, and redacted evidence. |
@ -262,7 +262,7 @@ sequenceDiagram
- provider-pool model group은 capacity + priority + availability 기준으로 provider candidate를 먼저 선택하고, 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 raw tunnel passthrough로 dispatch한다. Ollama/native provider가 선택되면 normalized `RunRequest` path로 dispatch한다.
- Anthropic Messages and count-tokens do not use legacy direct-route or single-target fallback. Native responses preserve provider status, allowed headers, and body/SSE bytes; bridge responses are converted between Anthropic Messages and Chat Completions shapes.
- A marked single-request Messages dispatch requires the narrow service coordinator capability and never falls back to the generic provider pool. The handler copies the immutable binding and request input and counts the accepted HTTP admission once with no labels. The service projects exactly one frozen terminal candidate through both response modes: buffered/SSE `end_turn`; buffered/SSE `max_tokens` without private partial content; `invalid_request_error` for validation/context; `api_error` for provider, timeout, budget, repetition, malformed, internal-tool, and workspace-cleanup failures; or silent cancellation after caller disconnect. The streaming path maps only fixed plan/work/review/repair summaries, serializes pings and monotonic text-block indices with one terminal owner, stops and joins liveness before terminal/return, and acknowledges completion only after `message_stop`. Arbitrary progress, reasoning, tool/provider/credential/workspace data, raw failures, and internal stage terminals stay private. No classified terminal triggers retry, fallback, partial success, a second request, or a later success terminal. Count-tokens does not enter or increment this path.
- Marked single-request Plan/Review templates are Edge-owned internal artifact shapes, not part of this input surface. The operator configures them in `execution_presets[].single_request.templates`; admission freezes the effective pair, so a config refresh reaches only requests admitted after it and an already running request keeps its pair. The Plan stage requests a closed strict JSON object containing only `goal`, `steps`, and `verification`, enforces the single-line goal, 2-6 step bullets, and 1-3 verification bullets, and lets Edge render the frozen Plan template deterministically. The Review template shapes the private `review.md` artifact rendered from the model's `checks`/`verification`/`summary` fields. No caller field, header, or metadata value can supply, name, select, or override a template, and no template path, content, or digest appears in a response, an error message, a log projection, or a metric label. Changing a template changes neither the Messages request schema nor the response schema: the buffered/SSE terminal projection is unchanged and the final caller-visible text remains the model's `decision.output`.
- Marked single-request Plan/Review templates are Edge-owned internal artifact shapes, not part of this input surface. The operator configures them in `execution_presets[].single_request.templates`; admission freezes the effective pair, so a config refresh reaches only requests admitted after it and an already running request keeps its pair. The Plan stage requests a closed strict JSON object containing a one-line `goal` string, a 2-6 item `steps` string array, and a 1-3 item `verification` string array. Each item must be non-empty and one-line; Edge adds the Markdown bullet prefixes and newlines and renders the frozen Plan template deterministically. The Review template shapes the private `review.md` artifact rendered from the model's `checks`/`verification`/`summary` fields. No caller field, header, or metadata value can supply, name, select, or override a template, and no template path, content, or digest appears in a response, an error message, a log projection, or a metric label. Changing a template changes neither the Messages request schema nor the response schema: the buffered/SSE terminal projection is unchanged and the final caller-visible text remains the model's `decision.output`.
- Marked single-request observation evidence links ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, and raw-free correlation for one real POST. `iop_anthropic_single_request_ingress_total` is strictly unlabeled: no request_id, stage_id, provider identity, content, or workspace reference appears as a metric label. Internal tool names (`workspace_read`, `workspace_write`, etc.), raw arguments, private results, and workspace references are absent from the public terminal JSON and from log projections. Stage-pure timing, cardinality-bounded labels, and privacy semantics are documented here. SDD S12 qualifies the external Claude path on an approved IOP Node with one accepted ingress, the expected stage sequence, one terminal, exact output, timing, cleanup, and redacted evidence.
- Internal workspace calls use a service-owned schema independent of caller-facing tool codecs. The five closed operation names decode into typed Node requests only after request/stage/tool identity, canonical relative path, approved operation/command/environment capability, and immutable budget checks. The loop opens once, preserves the admitted connection generation, executes one pending call at a time, accepts only correlated typed results, and returns a deep-copied raw-free result to the same executor continuation. Repeated IDs, stale responses, malformed or denied input, timeout, output/iteration exhaustion, and cancellation never become public Anthropic tool protocol or trigger a second ingress.
- Claude Code Messages requests may use adaptive thinking, `output_config.effort`, structured output, cache-control annotations, and supported beta headers, including the compatibility-only `advisor-tool-2026-03-01` marker emitted by the pinned official caller. The Chat bridge consumes rather than forwards those headers, maps supported fields, and requires callers to replay opaque `tool_use.id` values unchanged so Gemini thought signatures can be restored on tool-result turns.
@ -328,6 +328,7 @@ sequenceDiagram
- 2026-08-12: Admitted Claude Code's `advisor-tool-2026-03-01` beta as a consumed compatibility marker for both direct and marked-preset Messages ingress. It grants no internal capability and is not forwarded through the Chat bridge (`apps/edge/internal/openai/anthropic_types.go`).
- 2026-08-12: Replaced free-form PlanMD generation with a stage-owned strict `goal`/`steps`/`verification` JSON response and deterministic Edge rendering of the frozen Plan template. Internal artifact customization and all caller-visible Messages schemas remain unchanged (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 2026-08-12: Made the private Plan response provider-independent by representing steps and verification as bounded one-line string arrays; Edge now owns Markdown bullet and newline formatting (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 2026-08-09: Separated Edge-owned internal Plan/Review stage templates from caller-visible I/O. Operator-configured `execution_presets[].single_request.templates` is frozen at admission, supplies the private Plan prompt/PlanMD grammar and the private `review.md` artifact shape, and is not selectable, supplyable, or observable by callers. The Messages request/response schema, terminal projection, and final `decision.output` text are unchanged (`apps/edge/internal/openai/single_request_preset_binding.go`, `apps/edge/internal/openai/single_request_review_stage.go`, `agent-contract/outer/anthropic-compatible-api.md`).
- 2026-08-09: Qualified S12 `claude-smoke` with one supervised external Claude task on an approved IOP Node: ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, exact 42-byte workspace output, timing, cleanup, and redacted evidence.
- 2026-08-07: Implemented and documented S11 `error-cancel`: one closed service terminal disposition, request-local typed failure/no-progress classification, shared buffered/SSE `end_turn`/`max_tokens`/`invalid_request_error`/`api_error` mapping, silent disconnect, private-partial suppression, and deterministic one-ingress/one-terminal/no-second-request evidence. S12 external qualification remains pending.

View file

@ -228,7 +228,7 @@ The shared `packages/go/execution` package contains provider lifecycle, registry
| workspace tool executor | A validated `darwin|linux` Node catalog owns opened root and directory handles only when every entry platform matches the host exactly. Windows, unknown hosts, and cross-platform catalogs fail before root open; empty catalogs remain compatible. Go 1.24-compatible no-follow file primitives provide bounded read, bounded list, structured write, and non-recursive delete. Exact operator-owned command templates run through an inherited-root `fchdir`/`exec` shim with minimal allowlisted environment, shared stdout/stderr bounds, process-group timeout/cancel, and stable typed results. OS is runtime evidence rather than a caller-visible selector. |
| internal workspace tool loop | The service decodes only `workspace_read`, `workspace_list`, `workspace_write`, `workspace_delete`, and `workspace_command`, opens the admitted workspace once, dispatches one call at a time on the frozen generation, and delivers one deep-copied typed result to the emitting executor continuation. Unique request/stage/tool correlation, per-stage iteration/output/deadline limits, request wall-clock budget, and typed cancel fail closed without external continuation or reselection. |
| request-owned internal artifacts | `SingleRequestController` exposes closed plan/review read/write operations. Artifact calls and model workspace tools share one serialized lazy `WorkspaceOpen`, the exact admitted Node generation, the active stage deadline, the immutable output bound, in-flight work accounting, and one terminal cleanup. Node alone maps selectors to `plan.md` and `review.md`, and inventoried descriptor-relative reads fail closed on identity replacement. |
| Plan stage | The Plan runner validates the frozen effective template, emits the `planning` envelope, sends the immutable task through the frozen Gemini Chat binding with `reasoning_effort=high` and a stage-owned strict JSON schema for `goal`/`steps`/`verification`, validates the bounded fields, renders the template deterministically inside Edge, and writes the resulting Markdown through `SingleRequestArtifactPlan`. |
| Plan stage | The Plan runner validates the frozen effective template, emits the `planning` envelope, sends the immutable task through the frozen Plan binding with `reasoning_effort=high` and a stage-owned strict JSON schema for one-line `goal` plus bounded one-line `steps`/`verification` arrays, validates the fields, adds Markdown bullets, renders the template deterministically inside Edge, and writes the resulting Markdown through `SingleRequestArtifactPlan`. |
| single-request effective templates | `execution_presets[].single_request.templates` optionally loads `plan_file`/`review_file` as bounded Markdown relative to the directory containing `edge.yaml`; absolute and empty paths, non-regular files, oversize (`>8192` bytes), non-UTF-8, and invalid grammar fail closed at load, and each file falls back to its built-in default independently. Admission freezes the effective Plan/Review pair into the binding, so a later refresh reaches only newly admitted requests. Templates select internal stage input and internal artifact shape only; caller request/response schemas are unchanged. |
| Work stage | The `ornith-fast` Work runner reads the closed PLAN artifact, projects only the admitted workspace tools, and resumes the same frozen provider route after exactly correlated Node results. It rejects any Work `reasoning_effort`, malformed or multiple tool calls, and empty completion or verification evidence. |
| request-owned cleanup | Node creates and inventories only `.iop/job/<request_id>` internal state, cancels and waits for all active command groups, validates the exact tree without following entries, and removes matching artifacts deepest-first with non-recursive descriptor operations. Symlinks, special files, foreign devices, identity replacements, and unowned entries fail closed. User results and sibling request state are preserved. Concurrent cleanup callers receive one bounded cached typed result. |
@ -255,7 +255,7 @@ The shared `packages/go/execution` package contains provider lifecycle, registry
- The service freezes the first public terminal candidate. Legacy successful results normalize to `end_turn`; output limits produce `length`; caller disconnect produces silent `cancelled`; validation/context become `invalid_request_error`; other errors become `api_error`. Buffered and SSE projectors share that policy, emit at most one terminal, and never expose private partial stage content for `length`. This completes deterministic S11 `error-cancel` evidence without changing the Edge-Node protobuf wire. S12 external Claude qualification on an approved IOP Node is complete.
- The request-local internal tool loop is implemented between the coordinator and the dedicated workspace wire. Strict decode and capability checks happen before wire effects; Node results are accepted only for the one pending call and return only bounded typed fields to the same optional executor continuation. Repeated or stale identities, malformed/denied calls, exhausted immutable budgets, and cancellation terminate internally without selecting another Node or involving the HTTP caller.
- Request-owned plan and review artifact access is implemented between the controller and the same dedicated workspace wire. Only `SingleRequestArtifactPlan` and `SingleRequestArtifactReview` are accepted. Artifact and model-tool callers share one serialized open attempt and the same opened cleanup gate; terminal and cancellation paths wait for in-flight artifact work before issuing exactly one cleanup. Edge bounds writes before dispatch and reads before acceptance, validates the echoed kind/operation and canonical terminal, and never reselects after a generation mismatch. Node maps the closed selectors to `plan.md` and `review.md`, validates the inventoried parent/file identity with descriptor-relative no-follow reads, and never grants the public workspace tool surface access to `.iop`.
- The private Plan stage is installed in the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`). Its provider codec accepts only frozen Plan options and selected dispatch facts, uses the admitted stage deadline and exact output limit, accepts only `RESPONSE_START`, zero or more `BODY`, then `END`, and projects all provider failures to a generic internal failure. The stage owns a closed strict JSON response schema with exactly the string fields `goal`, `steps`, and `verification`; unknown, duplicate, missing, or mistyped fields fail malformed. It enforces a single-line goal, 2-6 step bullet lines, and 1-3 verification bullet lines, then Edge substitutes those normalized values into the frozen effective Plan template. Required headings remain exact standalone lines, the documented placeholder inventory is closed, and unresolved delimiters are rejected. Provider output therefore cannot vary headings or static template text, and caller request fields cannot select, supply, or override the admitted template.
- The private Plan stage is installed in the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`). Its provider codec accepts only frozen Plan options and selected dispatch facts, uses the admitted stage deadline and exact output limit, accepts only `RESPONSE_START`, zero or more `BODY`, then `END`, and projects all provider failures to a generic internal failure. The stage owns a closed strict JSON response schema with exactly a string `goal`, a string-array `steps`, and a string-array `verification`; unknown, duplicate, missing, or mistyped fields fail malformed. It enforces a single-line goal, 2-6 non-empty one-line step items, and 1-3 non-empty one-line verification items. Edge, rather than the provider, adds Markdown bullet prefixes and newlines before substituting the values into the frozen effective Plan template. Required headings remain exact standalone lines, the documented placeholder inventory is closed, and unresolved delimiters are rejected. Provider output therefore cannot vary headings, bullet formatting, or static template text, and caller request fields cannot select, supply, or override the admitted template.
- The Review stage renders its internal REVIEW artifact from the request's frozen effective Review template, substituting only the model's `checks`, `verification`, and `summary` fields into the documented placeholder inventory. The template selects the internal artifact shape only: the caller-visible final response remains the model's `decision.output`, so replacing the Review template never changes the public Messages response schema.
- The private Work stage is installed in the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`). It reads only `SingleRequestArtifactPlan`, retains only request/stage/tool identifiers while waiting for the coordinator-owned continuation, and sends no `reasoning_effort` field in an initial or resumed provider request. Its provider messages contain the immutable task, PLAN, admitted tool schemas, and bounded typed tool results; Review/repair and composite installation are active, and S12 (`claude-smoke`) qualifies the external Claude path.
- The Node-private workspace request/result wire is implemented, including catalog delivery, parser registration, optional handler behavior, stable typed failures, generation-fenced dispatch, context-cancel propagation, and request cleanup. Before ready, a non-empty catalog requires a supported `darwin|linux` host and exact entry/host matching before any root open; unsupported and cross-platform catalogs fail closed while empty catalogs remain compatible. The Node installs the workspace handler before ready and cleans active requests before closing workspace authority ahead of session/store teardown. Request authority is immutable and request-local. File operations reserve `.iop`, reject symlink/mount/replaced-parent/special-file paths before effects, process bounded list batches with deterministic truncation, and use a same-parent structured write. Command execution resolves only admitted ids to fixed templates, enters the already-opened root descriptor through `fchdir`, provides only allowlisted environment entries, shares one output cap across drained stdout/stderr, and owns the complete process group through exit, timeout, context cancel, exact request/tool cancel, or request cleanup.
@ -366,6 +366,7 @@ Heartbeat interval/wait는 protobuf field가 아닌 양쪽 transport 구현의 l
## 변경 기록
- 2026-08-12: Replaced nondeterministic free-form PlanMD generation with a stage-owned strict `goal`/`steps`/`verification` JSON response. Edge rejects unknown, duplicate, missing, mistyped, or out-of-bound fields and deterministically renders the already-frozen operator Plan template, preserving template customization and every caller-visible schema (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 2026-08-12: Replaced Markdown-bearing Plan string fields with bounded one-line string arrays and moved all bullet/newline rendering into Edge, removing the remaining provider-specific formatting dependency without changing the public Messages surface (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 2026-08-09: Replaced the Plan stage's structured JSON response contract with operator-configurable bounded Markdown templates. `execution_presets[].single_request.templates` resolves `plan_file`/`review_file` relative to the directory containing `edge.yaml` only, admission freezes the effective Plan/Review pair so a refresh reaches only newly admitted requests, the Plan stage prompts with and parses direct PlanMD against the frozen template, and the Review stage renders the internal REVIEW artifact from the frozen Review template while the caller-visible final response stays the model's `decision.output` (`packages/go/singlerequesttemplate/template.go`, `packages/go/config/load.go`, `apps/edge/internal/openai/single_request_preset_binding.go`, `apps/edge/internal/openai/single_request_plan_stage.go`, `apps/edge/internal/openai/single_request_review_stage.go`).
- 2026-08-09: Qualified S12 `claude-smoke` on an approved IOP Node. One supervised Claude task with SDK retry and session-title generation disabled produced ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, the exact 42-byte workspace result, stage/total timing, cleanup, and redacted evidence.
- 2026-08-08: Expanded workspace runtime admission to the closed `darwin|linux` implementation set with exact catalog/host matching before root open, preserved empty-catalog compatibility, and kept Windows/unknown hosts fail-closed.

View file

@ -23,10 +23,11 @@ task=m-iop-one-shot-agent-model-comparison/08+07_comparison_rerun, plan=3, tag=R
| REVIEW_REVIEW_TEST-3 lifecycle exit coherence | [x] |
| REVIEW_REVIEW_TEST-4 deterministic Plan rendering | [x] |
| REVIEW_REVIEW_TEST-5 pinned Claude beta compatibility | [x] |
| REVIEW_REVIEW_TEST-6 complete local verification | [ ] |
| REVIEW_REVIEW_TEST-7 clean release deployment | [ ] |
| REVIEW_REVIEW_TEST-8 public smokes/preflight | [ ] |
| REVIEW_REVIEW_TEST-9 scored run/report | [ ] |
| REVIEW_REVIEW_TEST-6 provider-independent Plan arrays | [x] |
| REVIEW_REVIEW_TEST-7 complete local verification | [ ] |
| REVIEW_REVIEW_TEST-8 clean release deployment | [ ] |
| REVIEW_REVIEW_TEST-9 public smokes/preflight | [ ] |
| REVIEW_REVIEW_TEST-10 scored run/report | [ ] |
## Implementation Evidence
@ -40,10 +41,12 @@ task=m-iop-one-shot-agent-model-comparison/08+07_comparison_rerun, plan=3, tag=R
- The current feature tree passes all 437 Python benchmark tests, every relevant Go package under Control Plane/Edge/Node/shared runtime (excluding only the declared `agenttask` boundary), `git diff --check`, and manifest validation. Clean release-tree repetition remains pending commit/merge.
- After deploying release `1f748bd9bd7fcc843074847a57ed523a28db75fd`, official Claude Code `2.1.228` direct smoke exposed R5 before any scored run: its nominal `result/subtype=success` carried `is_error=true` and a sanitized unsupported-beta 400 for `advisor-tool-2026-03-01`. The scored-run allowance remains unused.
- R5 adds the exact pinned-caller beta to the closed admission inventory and consumes it at the Chat bridge without forwarding it or creating capability authority. Direct Claude Code mapping, marked single-request admission, and unknown-beta rejection regressions pass.
- After the R5 release, official Claude GPT hybrid reached the configured `gpt-5.6-terra` Plan provider twice but returned a caller-visible server error. Raw-free Edge evidence classified both Plan terminals as malformed, proving routing and credential selection succeeded while the remaining R4 Markdown-in-JSON string shape failed provider-independent decoding.
- R6 changes only the private Plan response contract: `steps` and `verification` are bounded arrays of non-empty one-line strings, while Edge deterministically adds Markdown bullets and newlines. It does not change routes, credentials, retries, templates, or any caller-visible schema.
## Review-Only Checklist
- [ ] Verify every R1-R5 regression exercises the exact production lifecycle path and remains fail-closed for unknown data.
- [ ] Verify every R1-R6 regression exercises the exact production lifecycle path and remains fail-closed for unknown data.
- [ ] Verify no credential value, ambient caller config, direct provider call, manifest change, hidden retry, or provider reselection was introduced.
- [ ] Verify the prior run is immutable, exactly one plan-3 scored run exists, and all nine attempts plus web gates succeed.
- [ ] Verify clean build/deployment identities, 4/4 node and 8/8 provider health, ready=9, and report pointer consistency.

View file

@ -4,7 +4,7 @@
## For the Implementing Agent
Implement R1-R5 exactly as selected below. Do not alter benchmark inputs, route aliases, credentials, scoring, or retained run evidence. Use only the public benchmark CLI for live caller/provider execution. Run deterministic local qualification first, then clean-build and deploy the release, run non-scored direct/hybrid smokes, require ready=9, and execute exactly one fresh scored run. Fill the implementation-owned sections of `CODE_REVIEW-cloud-G10.md` with sanitized actual evidence and leave both active files in place for review.
Implement R1-R6 exactly as selected below. Do not alter benchmark inputs, route aliases, credentials, scoring, or retained run evidence. Use only the public benchmark CLI or its documented official-caller smoke path for live caller/provider execution. Run deterministic local qualification first, then clean-build and deploy the release, run non-scored direct/hybrid smokes, require ready=9, and execute exactly one fresh scored run. Fill the implementation-owned sections of `CODE_REVIEW-cloud-G10.md` with sanitized actual evidence and leave both active files in place for review.
## Background
@ -25,7 +25,8 @@ Plan 2 fixed the first live compatibility set and deployed clean release `04f7c3
| R2 | C03's structurally redacted evidence initially showed only top-level keys, but the retained raw stream and caller log prove the payload is nested and the model request first failed with Edge `400 INVALID_ARGUMENT: request body is invalid` | Edge's pinned Gemini request decoder lacks the official agy planner's structured-output request fields; the adapter then maps the caller's nested `result.status=ERROR` to `malformed_event` instead of preserving the upstream/process failure | extend the Gemini bridge's exact structured-output request contract and tests; treat a valid agy ERROR result as non-success terminal evidence without parser failure | direct-fix | Gemini bridge structured-output regression and agy error lifecycle regression pass; non-scored official agy direct smoke succeeds |
| R3 | C01 result exit `143` differs from authenticated receipt exit `null` | supervisor snapshots returncode before joining the concurrent exit watcher | join watcher/IO, refresh the authoritative child returncode, then write the one receipt; add deterministic race/recovery regression | direct-fix | timeout/recovery test proves result and receipt exit/signal equality and public resume can reconcile |
| R4 | Post-deploy raw-free metrics recorded three `plan/validation` failures among six marked single-request calls; Claude hybrid did not complete before the non-scored 180-second boundary | Plan stage asks the provider for free-form PlanMD and rejects harmless format drift after the model call | request a stage-owned strict JSON object (`goal`, `steps`, `verification`), validate the bounded fields, and render the configured PlanMD template deterministically inside Edge | direct-fix | repeated plan-stage fixtures cannot create malformed PlanMD; official Claude hybrid smoke reaches a success terminal within the scored timeout budget |
| R5 | After the R4 release deployment, pinned Claude Code `2.1.228` returned `result/subtype=success` with `is_error=true` and `API Error: 400 unsupported anthropic-beta "advisor-tool-2026-03-01"` before either direct or hybrid model execution | the exact official caller now emits a compatibility beta absent from Edge's closed admission set | admit and consume `advisor-tool-2026-03-01` without forwarding it or granting any route/tool/workspace authority; retain rejection for every unknown beta | direct-fix | direct and marked-preset header regressions pass; official Claude direct and both hybrid smokes terminate with `is_error=false` |
| R5 | After the R4 release deployment, pinned Claude Code `2.1.228` returned `result/subtype=success` with `is_error=true` and `API Error: 400 unsupported anthropic-beta "advisor-tool-2026-03-01"` before either direct or hybrid model execution | the exact official caller now emits a compatibility beta absent from Edge's closed admission set | admit and consume `advisor-tool-2026-03-01` without forwarding it or granting any route/tool/workspace authority; retain rejection for every unknown beta | direct-fix | direct and marked-preset header regressions pass; official Claude direct and Gemini hybrid smokes terminate with `is_error=false` |
| R6 | After the R5 release deployment, official Claude `gpt-hybrid` reached the configured GPT Plan provider twice but returned `is_error=true`; raw-free Edge terminal evidence classified both Plan responses as `malformed` | R4 still makes the model serialize Markdown bullet prefixes and newline layout inside two JSON string fields, leaving a provider-format dependency after structured output succeeds | make `steps` and `verification` bounded arrays of non-empty one-line strings in the closed response schema, then let Edge own bullet prefixes and newline rendering | direct-fix | array-schema/template regressions pass; official Claude GPT hybrid terminates with `is_error=false` without a retry or route change |
## Modified Files Summary
@ -40,6 +41,8 @@ Plan 2 fixed the first live compatibility set and deployed clean release `04f7c3
- `apps/edge/internal/openai/gemini_handler_test.go`
- `apps/edge/internal/openai/single_request_plan_stage.go`
- `apps/edge/internal/openai/single_request_plan_stage_test.go`
- `apps/edge/internal/openai/single_request_provider_stage.go`
- `apps/edge/internal/openai/single_request_executor_test.go`
- `packages/go/singlerequesttemplate/template.go`
- `packages/go/singlerequesttemplate/template_test.go`
- `apps/edge/internal/openai/anthropic_types.go`
@ -54,10 +57,11 @@ Plan 2 fixed the first live compatibility set and deployed clean release `04f7c3
- [x] [REVIEW_REVIEW_TEST-3] Implement R3 authoritative supervisor exit snapshot and timeout/recovery regression.
- [x] [REVIEW_REVIEW_TEST-4] Implement R4 schema-bound Plan fields and deterministic configured-template rendering.
- [x] [REVIEW_REVIEW_TEST-5] Implement R5 pinned Claude beta compatibility without forwarding or widening internal authority.
- [ ] [REVIEW_REVIEW_TEST-6] Run the complete Python benchmark suite and relevant Go/runtime regressions from a clean tree.
- [ ] [REVIEW_REVIEW_TEST-7] Commit/push, merge the clean release branch, rebuild/deploy Edge and every Node, and verify 4/4 nodes plus 8/8 healthy providers.
- [ ] [REVIEW_REVIEW_TEST-8] Run public non-scored direct and hybrid smoke coverage and a fresh ready=9 preflight.
- [ ] [REVIEW_REVIEW_TEST-9] Execute exactly one fresh scored run, require success=9 and all web gates, update existing id pointers, and generate the human-readable Markdown report.
- [x] [REVIEW_REVIEW_TEST-6] Implement R6 provider-independent array fields and Edge-owned Plan bullet rendering.
- [ ] [REVIEW_REVIEW_TEST-7] Run the complete Python benchmark suite and relevant Go/runtime regressions from a clean tree.
- [ ] [REVIEW_REVIEW_TEST-8] Commit/push, merge the clean release branch, rebuild/deploy Edge and every Node, and verify 4/4 nodes plus 8/8 healthy providers.
- [ ] [REVIEW_REVIEW_TEST-9] Run public non-scored direct and hybrid smoke coverage and a fresh ready=9 preflight.
- [ ] [REVIEW_REVIEW_TEST-10] Execute exactly one fresh scored run, require success=9 and all web gates, update existing id pointers, and generate the human-readable Markdown report.
- [ ] Fill `CODE_REVIEW-cloud-G10.md` with exact sanitized commands, release identities, deployment health, run id, terminal summary, and report path.
## Verification
@ -81,7 +85,7 @@ Plan 2 fixed the first live compatibility set and deployed clean release `04f7c3
### Outcome and Acceptance
The task is complete only when deterministic regressions prove the five boundary corrections, the clean release is deployed across the managed dev runtime, public readiness is 9/9, and one fresh immutable scored run is 9/9 with a generated Markdown report.
The task is complete only when deterministic regressions prove the six boundary corrections, the clean release is deployed across the managed dev runtime, public readiness is 9/9, and one fresh immutable scored run is 9/9 with a generated Markdown report.
### Scope and Ownership
@ -100,7 +104,7 @@ Keep one plan because the scored-run acceptance depends on one indivisible evide
## Dependencies and Execution Order
R1-R5 are independent code fixes but must all pass local regressions before one clean release is built. Deployment must finish before non-scored smokes; smokes and ready=9 must pass before the only fresh scored run.
R1-R6 are independent code fixes but must all pass local regressions before one clean release is built. Deployment must finish before non-scored smokes; smokes and ready=9 must pass before the only fresh scored run.
## Final Routing

View file

@ -95,8 +95,8 @@ func waitExecutionResult(exec edgeservice.SingleRequestExecution) (edgeservice.S
func executorPlanBody(goal, verification string) []byte {
b, _ := json.Marshal(map[string]any{
"goal": goal,
"steps": "- Step one.\n- Step two.",
"verification": "- " + verification,
"steps": []string{"Step one.", "Step two."},
"verification": []string{verification},
})
return successBodyWithThoughtSignature(string(b))
}

View file

@ -9,7 +9,7 @@ import (
"iop/packages/go/singlerequesttemplate"
)
const singleRequestPlanPrompt = "Create a concise plan for the task. Return exactly one JSON object with non-empty string fields goal, steps, and verification. Keep goal to one line. Encode steps as 2 to 6 newline-separated Markdown bullet lines, each starting with '- '. Encode verification as 1 to 3 newline-separated Markdown bullet lines, each starting with '- '. Do not include headings or template text."
const singleRequestPlanPrompt = "Create a concise plan for the task. Return exactly one JSON object with a non-empty one-line goal string, a steps array containing 2 to 6 non-empty one-line strings, and a verification array containing 1 to 3 non-empty one-line strings. Do not include Markdown bullets, headings, or template text; Edge owns all Plan document formatting."
var errSingleRequestPlanStage = errors.New("single-request plan stage: failed")
@ -32,9 +32,9 @@ type singleRequestPlanStageRequest struct {
}
type singleRequestPlanFields struct {
Goal string `json:"goal"`
Steps string `json:"steps"`
Verification string `json:"verification"`
Goal string `json:"goal"`
Steps []string `json:"steps"`
Verification []string `json:"verification"`
}
func (v *singleRequestPlanFields) UnmarshalJSON(data []byte) error {
@ -59,9 +59,13 @@ func singleRequestPlanResponseFormat() *singleRequestProviderResponseFormat {
Schema: singleRequestProviderOutputSchema{
Type: "object",
Properties: map[string]singleRequestProviderOutputProperty{
"goal": {Type: "string", MinLength: 1},
"steps": {Type: "string", MinLength: 1},
"verification": {Type: "string", MinLength: 1},
"goal": {Type: "string", MinLength: 1},
"steps": {
Type: "array", Items: &singleRequestProviderOutputProperty{Type: "string", MinLength: 1}, MinItems: 2, MaxItems: 6,
},
"verification": {
Type: "array", Items: &singleRequestProviderOutputProperty{Type: "string", MinLength: 1}, MinItems: 1, MaxItems: 3,
},
},
Required: []string{"goal", "steps", "verification"},
AdditionalProperties: false,

View file

@ -66,7 +66,7 @@ func validPlanStageRequest() singleRequestPlanStageRequest {
func TestSingleRequestPlanStageWritesArtifact(t *testing.T) {
d := matchingDispatch()
planJSON := `{"goal":"Inspect the target.","steps":"- Step one.\n- Step two.","verification":"- Run focused tests."}`
planJSON := `{"goal":"Inspect the target.","steps":["Step one.","Step two."],"verification":["Run focused tests."]}`
planMD := "# Plan\n\n## Goal\nInspect the target.\n\n## Steps\n- Step one.\n- Step two.\n\n## Verification\n- Run focused tests.\n"
tunnel := &mockTunnel{frames: framesFor(successBodyWithThoughtSignature(planJSON))}
var captured edgeservice.ProviderPoolDispatchRequest
@ -107,7 +107,7 @@ func TestSingleRequestPlanStageWritesArtifact(t *testing.T) {
func TestSingleRequestPlanStageCustomTemplate(t *testing.T) {
d := matchingDispatch()
customTmpl := "# Plan\n\nCustom Header\n\n## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"
planJSON := `{"goal":"Inspect custom target.","steps":"- Custom step 1.\n- Custom step 2.","verification":"- Custom verify."}`
planJSON := `{"goal":"Inspect custom target.","steps":["Custom step 1.","Custom step 2."],"verification":["Custom verify."]}`
planMD := "# Plan\n\nCustom Header\n\n## Goal\nInspect custom target.\n\n## Steps\n- Custom step 1.\n- Custom step 2.\n\n## Verification\n- Custom verify.\n"
binding, err := edgeservice.NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", validStageBinding(), validStageBinding(), validStageBinding(), validLimits(), edgeservice.SingleRequestTemplateBinding{
@ -139,18 +139,19 @@ func TestSingleRequestPlanStageFailsClosed(t *testing.T) {
}{
{"empty-string", ""},
{"not-json", "just plain text"},
{"unknown-field", `{"goal":"Fix bug.","steps":"- Step 1\n- Step 2","verification":"- Verify 1","extra":"no"}`},
{"duplicate-field", `{"goal":"Fix bug.","goal":"Again.","steps":"- Step 1\n- Step 2","verification":"- Verify 1"}`},
{"missing-field", `{"goal":"Fix bug.","steps":"- Step 1\n- Step 2"}`},
{"wrong-field-type", `{"goal":"Fix bug.","steps":["Step 1","Step 2"],"verification":"- Verify 1"}`},
{"step-count-1-too-few", `{"goal":"Fix bug.","steps":"- Step 1","verification":"- Verify 1"}`},
{"step-count-7-too-many", `{"goal":"Fix bug.","steps":"- S1\n- S2\n- S3\n- S4\n- S5\n- S6\n- S7","verification":"- Verify 1"}`},
{"step-not-bullet", `{"goal":"Fix bug.","steps":"Step 1\n- Step 2","verification":"- Verify 1"}`},
{"verif-count-0-too-few", `{"goal":"Fix bug.","steps":"- Step 1\n- Step 2","verification":""}`},
{"verif-count-4-too-many", `{"goal":"Fix bug.","steps":"- Step 1\n- Step 2","verification":"- V1\n- V2\n- V3\n- V4"}`},
{"verification-not-bullet", `{"goal":"Fix bug.","steps":"- Step 1\n- Step 2","verification":"Verify 1"}`},
{"multiline-goal", `{"goal":"Line 1\nLine 2","steps":"- Step 1\n- Step 2","verification":"- Verify 1"}`},
{"unresolved-token", `{"goal":"Fix {{goal}} bug.","steps":"- Step 1\n- Step 2","verification":"- Verify 1"}`},
{"unknown-field", `{"goal":"Fix bug.","steps":["Step 1","Step 2"],"verification":["Verify 1"],"extra":"no"}`},
{"duplicate-field", `{"goal":"Fix bug.","goal":"Again.","steps":["Step 1","Step 2"],"verification":["Verify 1"]}`},
{"missing-field", `{"goal":"Fix bug.","steps":["Step 1","Step 2"]}`},
{"wrong-field-type", `{"goal":"Fix bug.","steps":"Step 1 and Step 2","verification":["Verify 1"]}`},
{"step-count-1-too-few", `{"goal":"Fix bug.","steps":["Step 1"],"verification":["Verify 1"]}`},
{"step-count-7-too-many", `{"goal":"Fix bug.","steps":["S1","S2","S3","S4","S5","S6","S7"],"verification":["Verify 1"]}`},
{"step-empty-item", `{"goal":"Fix bug.","steps":["Step 1",""],"verification":["Verify 1"]}`},
{"step-multiline-item", `{"goal":"Fix bug.","steps":["Step 1\ncontinued","Step 2"],"verification":["Verify 1"]}`},
{"verif-count-0-too-few", `{"goal":"Fix bug.","steps":["Step 1","Step 2"],"verification":[]}`},
{"verif-count-4-too-many", `{"goal":"Fix bug.","steps":["Step 1","Step 2"],"verification":["V1","V2","V3","V4"]}`},
{"verification-multiline-item", `{"goal":"Fix bug.","steps":["Step 1","Step 2"],"verification":["Verify 1\ncontinued"]}`},
{"multiline-goal", `{"goal":"Line 1\nLine 2","steps":["Step 1","Step 2"],"verification":["Verify 1"]}`},
{"unresolved-token", `{"goal":"Fix {{goal}} bug.","steps":["Step 1","Step 2"],"verification":["Verify 1"]}`},
}
for _, tt := range malformedTests {
@ -222,7 +223,7 @@ func TestSingleRequestPlanStageFailsClosed(t *testing.T) {
t.Run("artifact-write-failure-rejects", func(t *testing.T) {
d := matchingDispatch()
planJSON := `{"goal":"Goal text.","steps":"- Step 1\n- Step 2","verification":"- Verify 1"}`
planJSON := `{"goal":"Goal text.","steps":["Step 1","Step 2"],"verification":["Verify 1"]}`
tunnel := &mockTunnel{frames: framesFor(successBody(planJSON))}
provider := newSingleRequestProviderStage(&mockService{submit: func(context.Context, edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error) {
return &edgeservice.ProviderPoolDispatchResult{Path: edgeservice.ProviderPoolPathTunnel, Tunnel: tunnel, DispatchInfo: d}, nil

View file

@ -185,10 +185,13 @@ type singleRequestProviderOutputSchema struct {
}
type singleRequestProviderOutputProperty struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
Enum []string `json:"enum,omitempty"`
MinLength int `json:"minLength,omitempty"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
Enum []string `json:"enum,omitempty"`
MinLength int `json:"minLength,omitempty"`
Items *singleRequestProviderOutputProperty `json:"items,omitempty"`
MinItems int `json:"minItems,omitempty"`
MaxItems int `json:"maxItems,omitempty"`
}
// buildSingleRequestChatBody owns all request authority. Stage options are

View file

@ -62,8 +62,8 @@ type ReviewFields struct {
type PlanFields struct {
Goal string
Steps string
Verification string
Steps []string
Verification []string
}
// exactLineOffsets returns the byte offset of the first standalone line equal to
@ -266,22 +266,22 @@ func ParsePlan(tmpl string, rawOutput string, maxOutputBytes int) ([]byte, error
vVerif = rem[:len(rem)-len(suffix)]
}
if _, err := normalizePlanFields(PlanFields{Goal: vGoal, Steps: vSteps, Verification: vVerif}); err != nil {
if _, _, _, err := normalizePlanSections(vGoal, vSteps, vVerif); err != nil {
return nil, err
}
return []byte(rawOutput), nil
}
func normalizePlanFields(fields PlanFields) (PlanFields, error) {
goal := strings.TrimSpace(fields.Goal)
steps := strings.TrimSpace(fields.Steps)
verification := strings.TrimSpace(fields.Verification)
func normalizePlanSections(goal, steps, verification string) (string, string, string, error) {
goal = strings.TrimSpace(goal)
steps = strings.TrimSpace(steps)
verification = strings.TrimSpace(verification)
if goal == "" || strings.ContainsAny(goal, "\r\n") || steps == "" || verification == "" {
return PlanFields{}, ErrMalformedPlan
return "", "", "", ErrMalformedPlan
}
if strings.Contains(goal, "{{") || strings.Contains(goal, "}}") || strings.Contains(steps, "{{") || strings.Contains(steps, "}}") || strings.Contains(verification, "{{") || strings.Contains(verification, "}}") {
return PlanFields{}, ErrMalformedPlan
return "", "", "", ErrMalformedPlan
}
normalizeBullets := func(value string, minimum, maximum int) (string, error) {
@ -301,13 +301,43 @@ func normalizePlanFields(fields PlanFields) (PlanFields, error) {
steps, err := normalizeBullets(steps, 2, 6)
if err != nil {
return PlanFields{}, err
return "", "", "", err
}
verification, err = normalizeBullets(verification, 1, 3)
if err != nil {
return PlanFields{}, err
return "", "", "", err
}
return PlanFields{Goal: goal, Steps: steps, Verification: verification}, nil
return goal, steps, verification, nil
}
func normalizePlanFields(fields PlanFields) (string, string, string, error) {
goal := strings.TrimSpace(fields.Goal)
if goal == "" || strings.ContainsAny(goal, "\r\n") || strings.Contains(goal, "{{") || strings.Contains(goal, "}}") {
return "", "", "", ErrMalformedPlan
}
normalizeItems := func(items []string, minimum, maximum int) (string, error) {
if len(items) < minimum || len(items) > maximum {
return "", ErrMalformedPlan
}
lines := make([]string, len(items))
for i, item := range items {
item = strings.TrimSpace(item)
if item == "" || strings.ContainsAny(item, "\r\n") || strings.Contains(item, "{{") || strings.Contains(item, "}}") {
return "", ErrMalformedPlan
}
lines[i] = "- " + item
}
return strings.Join(lines, "\n"), nil
}
steps, err := normalizeItems(fields.Steps, 2, 6)
if err != nil {
return "", "", "", err
}
verification, err := normalizeItems(fields.Verification, 1, 3)
if err != nil {
return "", "", "", err
}
return goal, steps, verification, nil
}
func RenderPlan(tmpl string, fields PlanFields, maxOutputBytes int) ([]byte, error) {
@ -318,13 +348,13 @@ func RenderPlan(tmpl string, fields PlanFields, maxOutputBytes int) ([]byte, err
return nil, err
}
normalized, err := normalizePlanFields(fields)
goal, steps, verification, err := normalizePlanFields(fields)
if err != nil {
return nil, err
}
res := strings.ReplaceAll(tmpl, "{{goal}}", normalized.Goal)
res = strings.ReplaceAll(res, "{{steps}}", normalized.Steps)
res = strings.ReplaceAll(res, "{{verification}}", normalized.Verification)
res := strings.ReplaceAll(tmpl, "{{goal}}", goal)
res = strings.ReplaceAll(res, "{{steps}}", steps)
res = strings.ReplaceAll(res, "{{verification}}", verification)
if strings.Contains(res, "{{") || strings.Contains(res, "}}") || len(res) > maxOutputBytes {
return nil, ErrMalformedPlan
}

View file

@ -530,8 +530,8 @@ Fix {{goal}} bug.
func TestRenderPlan(t *testing.T) {
fields := singlerequesttemplate.PlanFields{
Goal: " Inspect the target. ",
Steps: " - Step one. \n\t- Step two. ",
Verification: " - Run focused tests. ",
Steps: []string{" Step one. ", "\tStep two. "},
Verification: []string{" Run focused tests. "},
}
want := "# Plan\n\n## Goal\nInspect the target.\n\n## Steps\n- Step one.\n- Step two.\n\n## Verification\n- Run focused tests.\n"
got, err := singlerequesttemplate.RenderPlan(singlerequesttemplate.DefaultPlanTemplate, fields, 1024)
@ -554,7 +554,7 @@ func TestRenderPlan(t *testing.T) {
}
func TestRenderPlanRejectsMalformedFields(t *testing.T) {
valid := singlerequesttemplate.PlanFields{Goal: "Fix bug.", Steps: "- Step 1\n- Step 2", Verification: "- Verify 1"}
valid := singlerequesttemplate.PlanFields{Goal: "Fix bug.", Steps: []string{"Step 1", "Step 2"}, Verification: []string{"Verify 1"}}
tests := []struct {
name string
fields singlerequesttemplate.PlanFields
@ -562,11 +562,13 @@ func TestRenderPlanRejectsMalformedFields(t *testing.T) {
}{
{"empty-goal", singlerequesttemplate.PlanFields{Steps: valid.Steps, Verification: valid.Verification}, 1024},
{"multiline-goal", singlerequesttemplate.PlanFields{Goal: "Line 1\nLine 2", Steps: valid.Steps, Verification: valid.Verification}, 1024},
{"one-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: "- Step 1", Verification: valid.Verification}, 1024},
{"seven-steps", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: "- S1\n- S2\n- S3\n- S4\n- S5\n- S6\n- S7", Verification: valid.Verification}, 1024},
{"invalid-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: "Step 1\n- Step 2", Verification: valid.Verification}, 1024},
{"four-verifications", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: "- V1\n- V2\n- V3\n- V4"}, 1024},
{"invalid-verification", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: "Verify 1"}, 1024},
{"one-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"Step 1"}, Verification: valid.Verification}, 1024},
{"seven-steps", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"S1", "S2", "S3", "S4", "S5", "S6", "S7"}, Verification: valid.Verification}, 1024},
{"empty-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"Step 1", ""}, Verification: valid.Verification}, 1024},
{"multiline-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"Step 1\ncontinued", "Step 2"}, Verification: valid.Verification}, 1024},
{"four-verifications", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: []string{"V1", "V2", "V3", "V4"}}, 1024},
{"empty-verification", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: []string{""}}, 1024},
{"multiline-verification", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: []string{"Verify 1\ncontinued"}}, 1024},
{"unresolved-token", singlerequesttemplate.PlanFields{Goal: "Fix {{goal}}.", Steps: valid.Steps, Verification: valid.Verification}, 1024},
{"zero-max", valid, 0},
{"output-over-limit", valid, 10},