okstra 0.214.1 → 0.216.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +2 -1
  2. package/dist/cli-registry.d.mts +14 -0
  3. package/dist/cli-registry.mjs +12 -0
  4. package/dist/cli-registry.mjs.map +1 -1
  5. package/dist/commands/chat/chat.mjs +2 -56
  6. package/dist/commands/chat/chat.mjs.map +1 -1
  7. package/dist/commands/execute/render-bundle.mjs +5 -2
  8. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  9. package/dist/commands/execute/wizard.mjs +6 -0
  10. package/dist/commands/execute/wizard.mjs.map +1 -1
  11. package/dist/commands/memory/memory.mjs +57 -6
  12. package/dist/commands/memory/memory.mjs.map +1 -1
  13. package/dist/lib/file-lock.d.mts +1 -0
  14. package/dist/lib/file-lock.mjs +60 -0
  15. package/dist/lib/file-lock.mjs.map +1 -0
  16. package/dist/lib/python-command.mjs +11 -1
  17. package/dist/lib/python-command.mjs.map +1 -1
  18. package/dist/lib/skill-catalog.mjs +1 -0
  19. package/dist/lib/skill-catalog.mjs.map +1 -1
  20. package/dist/lib/types.d.mts +1 -1
  21. package/docs/architecture.md +2 -2
  22. package/docs/cli.md +36 -14
  23. package/docs/project-structure-overview.md +6 -3
  24. package/package.json +1 -1
  25. package/runtime/BUILD.json +2 -2
  26. package/runtime/bin/lib/okstra/cli.sh +9 -2
  27. package/runtime/bin/lib/okstra/globals.sh +2 -0
  28. package/runtime/bin/lib/okstra/interactive.sh +8 -2
  29. package/runtime/bin/lib/okstra/usage.sh +3 -1
  30. package/runtime/bin/okstra.sh +7 -1
  31. package/runtime/prompts/duties/discussion-worker.json +32 -0
  32. package/runtime/prompts/lead/convergence.md +58 -136
  33. package/runtime/prompts/lead/okstra-lead-contract.md +45 -12
  34. package/runtime/prompts/profiles/_common-contract.md +1 -1
  35. package/runtime/prompts/wizard/prompts.ko.json +14 -1
  36. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +4 -0
  37. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +4 -11
  38. package/runtime/python/okstra_ctl/agent/invocation.py +1 -0
  39. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +2 -2
  40. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +4 -4
  41. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +2 -2
  42. package/runtime/python/okstra_ctl/analysis_packet.py +3 -0
  43. package/runtime/python/okstra_ctl/consumers.py +8 -2
  44. package/runtime/python/okstra_ctl/convergence.py +143 -29
  45. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +57 -0
  46. package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +106 -0
  47. package/runtime/python/okstra_ctl/convergence_engine.py +140 -95
  48. package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +3 -27
  49. package/runtime/python/okstra_ctl/convergence_store.py +11 -4
  50. package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
  51. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  52. package/runtime/python/okstra_ctl/manager_cli.py +27 -1
  53. package/runtime/python/okstra_ctl/manager_launch.py +44 -4
  54. package/runtime/python/okstra_ctl/manager_sync.py +14 -1
  55. package/runtime/python/okstra_ctl/manager_view.py +53 -1
  56. package/runtime/python/okstra_ctl/next_phase.py +1 -0
  57. package/runtime/python/okstra_ctl/phases/catalog.py +2 -1
  58. package/runtime/python/okstra_ctl/phases/discussion/__init__.py +1 -0
  59. package/runtime/python/okstra_ctl/phases/discussion/boundary.json +11 -0
  60. package/runtime/python/okstra_ctl/phases/discussion/cli.py +512 -0
  61. package/runtime/python/okstra_ctl/phases/discussion/entry.py +116 -0
  62. package/runtime/python/okstra_ctl/phases/discussion/followup.py +876 -0
  63. package/runtime/python/okstra_ctl/phases/discussion/memory.py +294 -0
  64. package/runtime/python/okstra_ctl/phases/discussion/overview.py +85 -0
  65. package/runtime/python/okstra_ctl/phases/discussion/profile.json +31 -0
  66. package/runtime/python/okstra_ctl/phases/discussion/profile.md +48 -0
  67. package/runtime/python/okstra_ctl/phases/discussion/report.py +73 -0
  68. package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.html +15 -0
  69. package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.md +6 -0
  70. package/runtime/python/okstra_ctl/phases/discussion/spec.md +197 -0
  71. package/runtime/python/okstra_ctl/phases/discussion/store.py +1143 -0
  72. package/runtime/python/okstra_ctl/phases/discussion/validation.py +111 -0
  73. package/runtime/python/okstra_ctl/phases/discussion/wizard.py +90 -0
  74. package/runtime/python/okstra_ctl/phases/discussion/workflow.py +667 -0
  75. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -1
  76. package/runtime/python/okstra_ctl/phases/implementation_planning/host-rules.md +4 -1
  77. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +1 -1
  78. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +1 -1
  79. package/runtime/python/okstra_ctl/plan_run_root.py +8 -0
  80. package/runtime/python/okstra_ctl/render.py +7 -20
  81. package/runtime/python/okstra_ctl/render_final_report.py +2 -1
  82. package/runtime/python/okstra_ctl/report_assembly.py +22 -0
  83. package/runtime/python/okstra_ctl/report_contract.py +2 -0
  84. package/runtime/python/okstra_ctl/report_narrative.py +13 -4
  85. package/runtime/python/okstra_ctl/report_synthesis_packet.py +10 -0
  86. package/runtime/python/okstra_ctl/run.py +40 -7
  87. package/runtime/python/okstra_ctl/stage_close.py +46 -11
  88. package/runtime/python/okstra_ctl/stage_reopen.py +151 -0
  89. package/runtime/python/okstra_ctl/verdict_blocks.py +11 -35
  90. package/runtime/python/okstra_ctl/wizard/cli.py +8 -0
  91. package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -1
  92. package/runtime/python/okstra_ctl/wizard/engine.py +8 -0
  93. package/runtime/python/okstra_ctl/wizard/ids.py +12 -0
  94. package/runtime/python/okstra_ctl/wizard/registry.py +35 -3
  95. package/runtime/python/okstra_ctl/wizard/render.py +4 -1
  96. package/runtime/python/okstra_ctl/wizard/sources.py +2 -2
  97. package/runtime/python/okstra_ctl/wizard/state.py +11 -1
  98. package/runtime/python/okstra_ctl/wizard/steps_identity.py +4 -4
  99. package/runtime/python/okstra_ctl/worker_prompt_policy.py +24 -6
  100. package/runtime/python/okstra_token_usage/pricing.py +2 -0
  101. package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
  102. package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
  103. package/runtime/schemas/final-report-v3.0.schema.json +90 -1
  104. package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -0
  105. package/runtime/skills/_fragments/wizard-confirm-step.md +7 -0
  106. package/runtime/skills/_fragments/wizard-lead-takeover.md +8 -0
  107. package/runtime/skills/_fragments/wizard-preflight.md +75 -0
  108. package/runtime/skills/_fragments/wizard-prompt-loop.md +22 -0
  109. package/runtime/skills/_fragments/wizard-relay-protocol.md +40 -0
  110. package/runtime/skills/_fragments/wizard-render-bundle.md +74 -0
  111. package/runtime/skills/okstra-discussion/SKILL.md +347 -0
  112. package/runtime/skills/okstra-pr-gen/SKILL.md +7 -0
  113. package/runtime/skills/okstra-run/SKILL.md +22 -2
  114. package/runtime/templates/manager/view.template.html +3 -0
  115. package/runtime/templates/reports/html/i18n/en.json +5 -0
  116. package/runtime/templates/reports/html/i18n/ko.json +5 -0
  117. package/runtime/validators/validate-brief.py +3 -2
  118. package/runtime/validators/validate-run.py +4 -6
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.214.1",
3
- "builtAt": "2026-10-06T14:25:05.491Z",
2
+ "package": "0.216.0",
3
+ "builtAt": "2026-10-15T14:08:59.435Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -24,7 +24,10 @@ collect_required_arguments() {
24
24
  prompt_for_required_argument TASK_GROUP "Task Group"
25
25
  prompt_for_required_argument TASK_ID "Task ID"
26
26
  prompt_for_required_argument TASK_TYPE "Task Type"
27
- if [[ "$TASK_TYPE" != "release-handoff" ]]; then
27
+ if [[ "$TASK_TYPE" == "discussion" ]]; then
28
+ # discussion 은 brief 대신 시작 입력을 받는다 — prepare 가 입력 문서를 만든다.
29
+ prompt_for_required_argument DISCUSSION_START "Discussion start (link, short description, or this task's key)"
30
+ elif [[ "$TASK_TYPE" != "release-handoff" ]]; then
28
31
  # release-handoff 는 brief 가 없다 — prepare 가 검증 보고서 인용 input 을 생성.
29
32
  prompt_for_required_argument BRIEF_PATH "Task Brief Path"
30
33
  fi
@@ -175,6 +178,10 @@ while [[ $# -gt 0 ]]; do
175
178
  BRIEF_PATH="$(require_option_value --task-brief "${2-}")"
176
179
  shift 2
177
180
  ;;
181
+ --discussion-start)
182
+ DISCUSSION_START="$(require_option_value --discussion-start "${2-}")"
183
+ shift 2
184
+ ;;
178
185
  --directive)
179
186
  DIRECTIVE="$(require_option_value --directive "${2-}")"
180
187
  shift 2
@@ -249,7 +256,7 @@ while [[ $# -gt 0 ]]; do
249
256
  printf ' hint: did you mean --task-id?\n' >&2
250
257
  ;;
251
258
  esac
252
- printf ' valid options: --render-only --resume-clarification --yes --role-count --role-model --lead-provider --lead-model --claude-model --codex-model --antigravity-model --worker-model --report-writer-provider --report-writer-model --lead-runtime --executor --critic --related-tasks --work-category --task-type --project-id --project-root --task-group --task-id --task-brief --directive --base-ref --fix-cycle --clarification-response --selected-direction --task-key --approved-plan --approve --implementation-option --stage --stages --qa-waiver --no-plan-verification -h|--help\n' >&2
259
+ printf ' valid options: --render-only --resume-clarification --yes --role-count --role-model --lead-provider --lead-model --claude-model --codex-model --antigravity-model --worker-model --report-writer-provider --report-writer-model --lead-runtime --executor --critic --related-tasks --work-category --task-type --project-id --project-root --task-group --task-id --task-brief --discussion-start --directive --base-ref --fix-cycle --clarification-response --selected-direction --task-key --approved-plan --approve --implementation-option --stage --stages --qa-waiver --no-plan-verification -h|--help\n' >&2
253
260
  usage
254
261
  exit 1
255
262
  ;;
@@ -39,6 +39,8 @@ BASE_REF=""
39
39
  RELATED_TASKS_RAW=""
40
40
  TASK_TYPE=""
41
41
  BRIEF_PATH=""
42
+ # discussion 전용: brief 자리에 들어가는 시작 입력(링크·짧은 설명·이 task 의 key).
43
+ DISCUSSION_START=""
42
44
  PROJECT_ID=""
43
45
  TASK_GROUP=""
44
46
  TASK_ID=""
@@ -145,7 +145,9 @@ autofill_from_manifest() {
145
145
  fi
146
146
  # release-handoff 는 brief 입력이 없다 (prepare 가 검증 보고서 인용 input 을
147
147
  # 생성) — task-type autofill 이후에 판정해야 하므로 순서가 위와 바뀌면 안 된다.
148
- if [[ -z "$BRIEF_PATH" && -n "$manifest_brief" && "$TASK_TYPE" != "release-handoff" ]]; then
148
+ # discussion 의 매니페스트 brief 는 prepare 가 만든 입력 문서라 다시 넘기지 않는다.
149
+ if [[ -z "$BRIEF_PATH" && -n "$manifest_brief" && "$TASK_TYPE" != "release-handoff" \
150
+ && "$TASK_TYPE" != "discussion" ]]; then
149
151
  BRIEF_PATH="$manifest_brief"
150
152
  printf 'autofill: brief-path from task-manifest.json: %s\n' "$BRIEF_PATH" >&2
151
153
  fi
@@ -159,7 +161,11 @@ missing_required_arguments_summary() {
159
161
  [[ -z "$TASK_ID" ]] && missing+=("<task-id>")
160
162
  [[ -z "$TASK_TYPE" ]] && missing+=("<task-type>")
161
163
  # release-handoff 는 brief 입력이 없다 — prepare 가 input 문서를 생성한다.
162
- [[ -z "$BRIEF_PATH" && "$TASK_TYPE" != "release-handoff" ]] && missing+=("<brief-path>")
164
+ if [[ "$TASK_TYPE" == "discussion" ]]; then
165
+ [[ -z "$DISCUSSION_START" ]] && missing+=("<discussion-start>")
166
+ elif [[ -z "$BRIEF_PATH" && "$TASK_TYPE" != "release-handoff" ]]; then
167
+ missing+=("<brief-path>")
168
+ fi
163
169
 
164
170
  if (( ${#missing[@]} == 0 )); then
165
171
  printf '%s' ""
@@ -3,7 +3,7 @@
3
3
  usage() {
4
4
  cat >&2 <<USAGE_EOF
5
5
  usage:
6
- $DISPLAY_COMMAND_NAME [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--role-count <role>=<N>] [--role-model <role>=<provider/model>] [--lead-provider <provider>] [--lead-model <model>] [--worker-model provider=model,...] [--report-writer-provider <provider>] [--report-writer-model <model>] [--lead-runtime <host-id-or-alias>] [--executor claude|codex|antigravity] [--critic off|claude|codex|antigravity|grok|kimi] [--related-tasks taskA,taskB] --project-id <project-id> [--project-root <path>] --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>] [--fix-cycle <yes|no>]
6
+ $DISPLAY_COMMAND_NAME [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--role-count <role>=<N>] [--role-model <role>=<provider/model>] [--lead-provider <provider>] [--lead-model <model>] [--worker-model provider=model,...] [--report-writer-provider <provider>] [--report-writer-model <model>] [--lead-runtime <host-id-or-alias>] [--executor claude|codex|antigravity] [--critic off|claude|codex|antigravity|grok|kimi] [--related-tasks taskA,taskB] --project-id <project-id> [--project-root <path>] --task-group <task-group> --task-id <task-id> (--task-brief <brief-path> | --discussion-start <start-input>) [--directive <directive>] [--fix-cycle <yes|no>]
7
7
 
8
8
  summary:
9
9
  $DISPLAY_TOOL_NAME prepares a task-keyed instruction bundle. The standalone launcher defaults to an interactive Claude session; in-host skills keep the current registered host session as the native lead.
@@ -23,6 +23,8 @@ required arguments:
23
23
  --task-id Stable task identifier inside the task group. Example: login-error-analysis
24
24
  --task-type Task type. Also selects prompts/profiles/<task-type>.md
25
25
  --task-brief Task brief file path. Relative paths are resolved against the target project root.
26
+ --discussion-start Replaces --task-brief for --task-type discussion: a ticket or project link, a short
27
+ description, or this task's key to continue its discussion in a new research cycle.
26
28
 
27
29
  optional arguments:
28
30
  --project-root Absolute path to the target project root. Resolution order when omitted:
@@ -185,6 +185,7 @@ okstra execution summary:
185
185
  task group: ${TASK_GROUP}
186
186
  task id: ${TASK_ID}
187
187
  task brief: ${BRIEF_PATH}
188
+ discussion start: ${DISCUSSION_START:-None}
188
189
  directive: ${DIRECTIVE:-None}
189
190
  clarification response: ${CLARIFICATION_RESPONSE_PATH:-None}
190
191
  selected direction: ${SELECTED_DIRECTION_PATH:-None}
@@ -216,8 +217,13 @@ PY_ARGS=(
216
217
  --task-group "$TASK_GROUP"
217
218
  --task-id "$TASK_ID"
218
219
  --task-type "$TASK_TYPE"
219
- --task-brief "$BRIEF_PATH"
220
220
  )
221
+ # discussion 은 brief 를 받지 않는다(prepare 가 --task-brief 를 거부한다) — 시작 입력이 그 자리다.
222
+ if [[ "$TASK_TYPE" == "discussion" ]]; then
223
+ PY_ARGS+=(--discussion-start "$DISCUSSION_START")
224
+ else
225
+ PY_ARGS+=(--task-brief "$BRIEF_PATH")
226
+ fi
221
227
  [[ -n "${DIRECTIVE-}" ]] && PY_ARGS+=(--directive "$DIRECTIVE")
222
228
  [[ -n "${FIX_CYCLE-}" ]] && PY_ARGS+=(--fix-cycle "$FIX_CYCLE")
223
229
  for role_count in "${ROLE_COUNTS[@]-}"; do
@@ -0,0 +1,32 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "id": "discussion-worker",
4
+ "roleId": "analyser",
5
+ "responsibilities": [
6
+ "Research the discussion subject and hand the user and the other participants the evidence, assumptions and alternative directions the decision depends on, without making the decision that belongs to the user."
7
+ ],
8
+ "requiredConduct": [
9
+ "Address the subject and the question the facilitator confirmed with the user; read the shared discussion record, including every user answer, before researching; give each claim its evidence location; state each alternative's requirements, exclusions and risks; and name the questions only the user can answer."
10
+ ],
11
+ "decisionPrinciples": [
12
+ "Separate factual claims about the code or external sources, which evidence can settle, from intent, scope, priority and acceptable risk, which only the user can settle. Resolve by inspection what the evidence answers and raise the rest as a user question. Prefer a falsifiable claim over a broad impression."
13
+ ],
14
+ "authorityAndBoundaries": [
15
+ "Research and argue; do not decide. Do not edit sources, write implementation plans or file-change specifications, or start follow-up tasks. A user decision recorded in the discussion record stands until the user changes it."
16
+ ],
17
+ "evidenceStandards": [
18
+ "Support each factual claim with evidence that bears on it directly and cite the inspected location. State when evidence is indirect, incomplete or contradicted, and what the research could not reach."
19
+ ],
20
+ "collaborationContract": [
21
+ "Reason independently and keep a minority position whose evidence is stronger. When another participant's claim is refuted or confirmed, say by which evidence. Leave cross-participant synthesis and the questions put to the user to the facilitator."
22
+ ],
23
+ "completionCriteria": [
24
+ "The confirmed question has an explicit answer or a stated reason why it has none; every factual claim carries evidence; every alternative carries its requirements, exclusions and risks; and every open user question is listed."
25
+ ],
26
+ "prohibitions": [
27
+ "Do not present an assumption as a fact, infer the user's intent from a fact check, repeat an earlier claim as new without new evidence, or drop a contested position to reach consensus."
28
+ ],
29
+ "blockedStateReporting": [
30
+ "Name the evidence or input that was unavailable, what was attempted, and which part of the question therefore has no researched answer."
31
+ ]
32
+ }
@@ -49,9 +49,8 @@ Configure this in the `convergence` block of `task-manifest.json`. If the block
49
49
  | Setting | Default | Description |
50
50
  |------|--------|------|
51
51
  | `enabled` | `true` | If `false`, skip the convergence loop and use the existing consensus/divergence method |
52
- | `maxRounds` | phase-aware: `1` for `requirements-discovery`, `2` otherwise (range 1–3) | Maximum number of re-verification rounds. Discovery's routing/missing-input outputs gain little from a second round; other phases (especially `error-analysis`) keep `2`. Lead resolves the effective value when the manifest omits the key and records it in `config.effectiveMaxRounds` of the convergence state artifact. |
53
- | `verificationMode` | `"lightweight"` | `"lightweight"` or `"full-reanalysis"` |
54
- | `adversarial` | phase-aware: `true` for `requirements-discovery` / `error-analysis` / `implementation-option-selection` / `implementation-planning` / `project-analysis` / `feature-analysis` / `change-impact-analysis`, `false` otherwise | When `true`, Phase 5.5 runs in **adversarial mode** (see §"Adversarial Verification Mode"): verifiers actively try to refute each finding, the burden of proof sits on the claim, and `verificationMode` is forced to `"full-reanalysis"` scoped to the finding's cited evidence. Resolved by `scripts/okstra_ctl/render.py` `_build_convergence_block` and recorded in `config.adversarial` of the convergence state artifact. |
52
+ | `maxRounds` | `2` (range 1–3) | Maximum number of re-verification rounds. A second round runs only for a finding whose first round produced no usable vote or a tie the critic decides (§"Adversarial classification"); with `1`, a tie is `contested`. Lead resolves the effective value when the manifest omits the key and records it in `config.effectiveMaxRounds` of the convergence state artifact. |
53
+ | `adversarial` | `true` | Always `true`: every phase runs Phase 5.5 in **adversarial mode** (see §"Adversarial Verification Mode") — verifiers actively try to refute each finding, the burden of proof sits on the claim, and `verificationMode` is `"full-reanalysis"` scoped to the finding's cited evidence. `okstra convergence prepare-groups` writes both values regardless of the task manifest, and `seed` refuses a grouping with `adversarial: false` (**Enforced:** `_parse_config` in `scripts/okstra_ctl/convergence_engine.py`). |
55
54
 
56
55
  **Auto-disable rule (BLOCKING).** Convergence requires ≥2 analyser workers to produce a meaningful consensus tally. When the run's selected roster (`resultContract.workerRoles`) holds fewer than 2 analyser workers — the user selected one analyser, or `release-handoff` (zero analyser workers, lead-only) — the lead MUST treat `convergence.enabled` as `false` for that run regardless of manifest configuration, skip Phase 5.5, and record `finalState: "converged"` with `totalRounds: 0`, `round2SkippedReason: "auto-disabled"`, an empty `roundHistory`, and an explanatory note in `config` (e.g. `"autoDisabled": "fewer-than-two-analysers"`). An `implementation-planning` run with one planner still runs the plan-body verification round; that planner's single vote settles each item (`plan-body-verification` (the absolute path in **Okstra Runtime Resources**), "A one-analyser roster"). **Enforced:** `scripts/okstra_ctl/convergence_engine.py` `seed_working_state` stops with `auto-disabled` below two analysis workers, and `validators/validate-run.py` `single_verifier_advisories` records the missing cross-model check as an advisory.
57
56
 
@@ -81,7 +80,7 @@ Nothing about the census stops a run. A missing cell, a missing result, a single
81
80
 
82
81
  ## Convergence Algorithm
83
82
 
84
- **Majority definition (BLOCKING).** "Majority" means *strictly greater than half* of the non-error votes for that finding (`verification-error` votes are excluded from both numerator and denominator). Ties — including the 1-AGREE / 1-DISAGREE case in a two-analyser roster — are NOT a majority: in intermediate rounds the finding is **carried forward**; in the final executed round the finding is classified `contested`. In adversarial mode a tie whose DISAGREE carries `counter-evidence` does not carry forward — it is classified `contested` in that round (§"Adversarial Verification Mode"). This rule applies identically to the plan-body verification round (`plan-body-verification` (the absolute path in **Okstra Runtime Resources**)) where the same verdict tokens are reused.
83
+ **Majority definition (BLOCKING).** "Majority" means *strictly greater than half* of the counted votes for that finding: the verifiers' non-error votes plus the origin worker's implicit SURVIVES (`verification-error` votes are excluded). A tie is NOT a majority: it goes to the critic when one is rostered and is `contested` otherwise (§"Adversarial classification"). The plan-body verification round (`plan-body-verification` (the absolute path in **Okstra Runtime Resources**)) keeps its own rule, which counts only verifier votes.
85
84
 
86
85
  **Enforced:** the engine owns the classifier and replays it — `scripts/okstra_ctl/convergence_engine.py` `validate_final_state` recomputes each finding's expected final classification from its recorded votes and rejects the state when the persisted value differs, and `finalize` runs that same check before it writes, so a tie scored as a consensus never reaches the artifact. `okstra convergence validate` is the same call on demand. Nothing re-derives the majority rule outside the engine; a second implementation would only drift from it.
87
86
 
@@ -129,7 +128,7 @@ Follow this protocol exactly:
129
128
  1. Run `okstra convergence seed --groups <groups> --run-manifest <current-run-manifest> --work-state <work> --final-state <final> --migration-dir <state/migrations>` for a v2 worker roster. `--run-manifest` must be the exact current manifest named by the groups document's `runManifestPath`; a previous run from the same task is not interchangeable. `seed` refuses a grouping that cites a worker whose initial attempt the mutation audit discarded (`contract-failed-unattributed`, `mutation-present-unresolved`) — the result file is still on disk, but the ledger says it was never accepted. Re-dispatch that worker as a new invocation and cite the new result, or leave it out of the grouping. **Enforced:** `discarded_worker_errors` in `scripts/okstra_ctl/convergence_provenance.py`, called by `_seed`. Omit the flag for a legacy v1 roster. A `reuse-final` action means validate the existing final and continue to Phase 6. `create-work`, `resume-work`, and `restart-round0` continue with planning.
130
129
  2. Run `okstra convergence plan-round --work-state <work> --plan <round-plan>`. This is read-only with respect to the working state. Its stdout JSON carries `round` and `inputQueueSize`; on a `dispatch` plan the round's `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` line copies those two values — never count the queue by hand. **Enforced:** `_plan_round` in `scripts/okstra_ctl/convergence.py`, `tests/run/test_convergence_cli.py`.
131
130
  3. Every plan carries `dispatchable`: `true` on an `action: "dispatch"` plan, `false` on an `action: "finalize"` plan. When the plan action is `dispatch`, create exactly one reverify prompt for each `dispatches[]` row and dispatch it through the selected runtime adapter. Its findings are exactly that row's `findingIds`.
132
- 4. Run `okstra convergence collect-results --plan <round-plan> --mode <adversarial|collaborative> --result <worker>=<path>… --run-manifest <current-run-manifest> --output convergence-round-<N>-results-<task-type>-<seq>.json`. Each planned worker's terminal outcome and `durationMs` come from the manifest's attempt ledger, not from the lead: an attempt still `started` refuses the collect — run `okstra team await` first so the attempt closes — and a worker whose attempt did not close `ok` cannot contribute a vote even if its result file exists. **Enforced:** `_dispatch_rows_from_manifest` in `scripts/okstra_ctl/convergence.py`. Then run `okstra convergence apply-round --work-state <work> --plan <round-plan> --results <round-results>`.
131
+ 4. Run `okstra convergence collect-results --plan <round-plan> --result <worker>=<path>… --run-manifest <current-run-manifest> --output convergence-round-<N>-results-<task-type>-<seq>.json`. Each planned worker's terminal outcome and `durationMs` come from the manifest's attempt ledger, not from the lead: an attempt still `started` refuses the collect — run `okstra team await` first so the attempt closes — and a worker whose attempt did not close `ok` cannot contribute a vote even if its result file exists. **Enforced:** `_dispatch_rows_from_manifest` in `scripts/okstra_ctl/convergence.py`. Then run `okstra convergence apply-round --work-state <work> --plan <round-plan> --results <round-results>`.
133
132
  5. Repeat `plan-round` and `apply-round` until the plan action is `finalize`. A `finalize` plan is **never dispatched**: it carries `dispatchable: false`, an empty `dispatches[]`, and a `note` naming the next command. Its `round` is only the `<N>` in `convergence-round-<N>-plan-<task-type>-<seq>.json` — not a round to run — so do not open reverify workers for it. **Enforced:** `okstra convergence apply-round` refuses a non-dispatch plan and returns the gate's closing reason with the remedy (`scripts/okstra_ctl/convergence_engine.py` `apply_round_results`).
134
133
  6. When the coverage critic is enabled, convert its verification batch to the canonical `dispatches[]` / `gaps[]` result shape and run `okstra convergence apply-critic-gaps --work-state <work> --results <critic-results>` exactly once. The reducer, not the lead, merges verified gaps.
135
134
  7. Run `okstra convergence finalize --work-state <work> --output <final>`.
@@ -175,17 +174,15 @@ The executable source is `scripts/okstra_ctl/convergence_engine.py`; `okstra con
175
174
 
176
175
  ## Verification Mode
177
176
 
178
- ### Lightweight (Default)
177
+ Finding convergence always uses scoped full re-analysis: the verifier re-opens the evidence each finding cites (§"Adversarial Verification Mode" → Scoped full-reanalysis).
179
178
 
180
- Decide solely on the findings and evidence other reviewers present; do not reanalyze the original code or data. Fast and cost-effective, but accuracy drops when evidence is insufficient.
179
+ ### Lightweight (plan-body verification only)
181
180
 
182
- ### Full Re-analysis (opt-in)
183
-
184
- Use each finding as a guide but reanalyze the original code/data yourself. High accuracy at 2–3× the cost and time.
181
+ Decide solely on the findings and evidence other reviewers present; do not reanalyze the original code or data. Only the plan-body verification round uses this mode.
185
182
 
186
183
  ## Adversarial Verification Mode
187
184
 
188
- Active only when `config.adversarial == true` (default for `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `project-analysis`, `feature-analysis`, and `change-impact-analysis`; see §"Configuration"); when `false`, every rule in this section is inert and the collaborative behaviour elsewhere in this contract applies unchanged. In adversarial mode the verifier's job inverts: instead of confirming a peer's finding, the verifier **tries to break it**, and the burden of proof sits on the claim — a finding survives only if refutation attempts fail.
185
+ Every phase runs in this mode. In adversarial mode the verifier's job inverts: instead of confirming a peer's finding, the verifier **tries to break it**, and the burden of proof sits on the claim — a finding survives only if refutation attempts fail.
189
186
 
190
187
  ### Read-only analysis task contract
191
188
 
@@ -219,33 +216,32 @@ Each `disagree` vote records a new field `disagreeBasis`:
219
216
 
220
217
  A `disagree` with `disagreeBasis == null` is a contract violation in adversarial mode — every refutation must state which of the two grounds it rests on. **Enforced:** `_validate_adversarial_disagree_carries_a_basis` in `validators/validate-run.py` fails such a vote when `config.adversarial` is set; non-adversarial rounds are out of scope because their verdict semantics differ. Bare "I disagree" without re-inspection is not allowed. If capability, credential, network, or service state prevents that inspection, the verdict is `UNVERIFIABLE`, persisted as `verification-error`; verifier failure is never converted to `DISAGREE` or `burden-not-met`. A live/external claim without `evidenceArtifacts` remains schema-valid, but a verifier that needs the missing artifact and cannot independently access the source MUST answer `UNVERIFIABLE` with a non-empty explanation.
221
218
 
222
- ### Adversarial classification (replaces the §"Convergence Algorithm" per-round classifier when `adversarial == true`)
219
+ ### Adversarial classification
223
220
 
224
- `verification-error` votes are excluded from numerator and denominator exactly as in the collaborative classifier. For each finding `F` in the queue at a round:
221
+ `verification-error` votes are excluded from the tally. The origin worker does not vote, but it holds its own finding, so the tally counts it as one SURVIVES. For each finding `F` in the queue at a round:
225
222
 
226
223
  ```text
227
- disagrees = [v for v in non-error votes if v.verdict == "disagree"]
228
- hard_refutes = [v for v in disagrees if v.disagreeBasis == "counter-evidence"]
229
- all_others_disagree = (every non-discoverer non-error vote is "disagree")
230
-
231
- IF len(disagrees) == 0:
232
- resolve F as "partial-consensus" if SUPPLEMENT/caveat votes are a majority of
233
- non-error votes, otherwise "full-consensus"
234
- ELIF all_others_disagree:
235
- resolve F as "worker-unique" # only the discoverer still holds it
236
- ELIF len(hard_refutes) >= 1:
237
- # an evidence-backed refute exists and the roster is split → the claim is disputed
224
+ votes = non-error votes of this round, plus the previous round's votes when F was tied there
225
+ disagrees = [v for v in votes if v.verdict == "disagree"]
226
+ survives = 1 + (len(votes) - len(disagrees)) # the 1 is the origin worker
227
+
228
+ IF votes is empty:
229
+ carry F forward; at the LAST executed round classify it "unverified"
230
+ ELIF any disagree has disagreeBasis == "counter-evidence":
238
231
  resolve F as "contested" IN THIS ROUND; F leaves the queue
239
- ELIF burden-not-met disagrees are a majority of non-error votes (per the Majority definition in the Convergence Algorithm section):
240
- carry F forward; at the LAST executed round classify it "contested"
241
- ELSE:
242
- # a lone weak (burden-not-met) doubt against an otherwise-surviving claim
243
- resolve F as "partial-consensus"
232
+ ELIF survives > len(disagrees):
233
+ resolve F as "full-consensus" when there is no disagree and SURVIVES-WITH-CAVEAT
234
+ votes are not a majority of votes; otherwise "partial-consensus"
235
+ ELIF len(disagrees) > survives:
236
+ resolve F as "worker-unique"
237
+ ELSE (tie):
238
+ critic rostered → carry F forward; the next round dispatches F to the critic only
239
+ no critic rostered → resolve F as "contested" IN THIS ROUND
244
240
  ```
245
241
 
246
- `contested` stays terminal (per §"Scope and Terminology") and is reached by two routes. A finding split on `burden-not-met` doubt alone is carried forward through intermediate rounds and labelled `contested` at the last executed round; a finding carrying a `counter-evidence` hard refute is labelled `contested` in the round that refute lands and leaves the queue there. For `requirements-discovery` (`effectiveMaxRounds = 1`) the two routes coincide — the single round IS the last round. The final-classifier block of §"Convergence Algorithm" honours this: its first branch classifies an adversarially carried-forward finding `contested` regardless of the AGREE tally, so the two sections cannot assign the same finding different labels.
242
+ The tie round's votes stay in the tally when the critic votes, so the critic's vote breaks the tie. Two analysers produce a tie whenever the one verifier refutes; three analysers produce a tie only when one verifier refutes and the other's vote is unusable. A tie whose critic round never ran (round limit) or returned no usable vote is `contested` at finalize. **Enforced:** `classify_finding_round` and `_finding_outcome` in `scripts/okstra_ctl/convergence_engine.py`; `plan_next_round` routes a tied finding to the worker named by `config.tieBreaker`, which `seed` sets to the grouping's single `critic`-audience worker or `null`.
247
243
 
248
- Design intent: one `counter-evidence` refute denies a claim consensus (it cannot rise above `contested` however many others AGREE). Because that refute is permanent, re-dispatching the finding buys nothing — the verifier gets no new information and re-casts the same refute, so the round is spent by construction. The refute therefore settles the finding where it lands, and the finding is reported under `## 6.2 Differences` exactly as a last-round `contested` finding is. A lone `burden-not-met` doubt does not sink an otherwise-surviving claim — only a majority of them does; that doubt IS resolvable by another round, so it still carries forward. When every non-discoverer refutes (all_others_disagree) the finding is worker-unique regardless of refute basis — only the discoverer still holds it. A caveat is weighed the same way a weak doubt is: with zero disagrees, SUPPLEMENT lands partial-consensus only when caveats are a **majority** of the non-error votes. A single verifier's scope note does not by itself deny a claim the rest of the roster passed cleanly — the caveat is still recorded in the dissent log either way, so the majority rule changes the label, never the record. (The collaborative classifier is more permissive still: there SUPPLEMENT counts as full agreement at any count.)
244
+ Design intent: one `counter-evidence` refute denies a claim consensus however many others SURVIVE. Because that refute is permanent, re-dispatching the finding buys nothing, so the refute settles the finding where it lands, and the finding is reported under `## 6.2 Differences`. A `burden-not-met` doubt is weighed by the majority. A tie is not sent back to the same analysers — they have no new information — but to the critic, a verifier outside the analyser panel. A caveat is weighed by majority: with zero disagrees, SURVIVES-WITH-CAVEAT lands partial-consensus only when caveats are a **majority** of the votes; the caveat is recorded in the dissent log either way.
249
245
 
250
246
  ## Re-verification Dispatch
251
247
 
@@ -266,7 +262,14 @@ materialize` with `--audience
266
262
  reverification-worker`, `--assignment-ref reverify/<workerId>`,
267
263
  `--source-role-execution-ref
268
264
  <sourceRoleExecutionRef>`, the exact `--worker-id`, `--dispatch-kind
269
- reverify-r<N>`, and the authorized prompt/result/audit paths. Do not derive the
265
+ reverify-r<N>`, and the authorized prompt/result/audit paths. A row whose
266
+ `worker` is `critic-worker` is a tie round: it takes the run's critic assignment
267
+ (`--assignment-ref critic/scope`, or `critic/acceptance` on `final-verification`)
268
+ instead of `reverify/<workerId>`, everything else unchanged, and its result is
269
+ named `critic-worker-reverify-r<N>-<task-type>-<seq>.md`. **Enforced:**
270
+ `okstra_ctl.worker_prompt_policy.is_critic_verification` admits that pair, and
271
+ `convergence_store._validate_dynamic_verifier_request` admits a critic source
272
+ only for a plan-body or finding re-verification round. Do not derive the
270
273
  source role execution from `workerId`, provider, model, or execution-label
271
274
  text. A legacy v1 run omits `--source-role-execution-ref`; the materializer
272
275
  keeps its legacy read-only identity resolution for that schema version. The
@@ -374,12 +377,9 @@ Reverify prompts MUST NOT inject the Phase 2 `[Required reading]` clause:
374
377
 
375
378
  **Enforced:** `_validate_reverify_prompt_suppresses_required_reading` in `validators/validate-run.py` fails any `*-reverify-r*.md` prompt containing the clause.
376
379
 
377
- Lightweight reverify does not require the original `analysis-packet.md`, `analysis-profile.md`, `task-brief.md`, or instruction set. Its complete input is the receiving worker's current engine-planned `findingIds` batch and the evidence embedded in those findings.
380
+ A reverify does not require the original `analysis-packet.md`, `analysis-profile.md`, `task-brief.md`, or instruction set. Its complete input is the receiving worker's current engine-planned `findingIds` batch, the evidence those findings cite, and the origin item and audit sidecar the rendered prompt names. Including the clause forces workers to re-read the entire instruction-set per round per worker (3 workers × 2 rounds × 5+ files in the worst case) for no quality gain.
378
381
 
379
- - **Lightweight mode**: the clause directly contradicts the "Do NOT re-analyze the original source materials" instruction below. Including it forces workers to re-read the entire instruction-set per round per worker (3 workers × 2 rounds × 5+ files in the worst case) for no quality gain.
380
- - **Full-reanalysis mode**: workers DO need to re-read source materials, but only the analysis-worker file list (no `final-report-template.md`). If lead chooses to inject a reading clause here, it MUST mirror the audience-scoped enumeration in [okstra-lead-contract](./okstra-lead-contract.md) Phase 2 (no template).
381
-
382
- This is the single largest avoidable cost in `requirements-discovery`, `error-analysis`, `implementation-option-selection`, and `implementation-planning` runs. Treat as mandatory.
382
+ This is the single largest avoidable cost in convergence. Treat as mandatory.
383
383
 
384
384
  ## Conditional reference reading
385
385
 
@@ -390,61 +390,17 @@ avoiding unused examples; queue and dispatch validators still enforce execution.
390
390
 
391
391
  | Current operation | Additional section to read |
392
392
  |---|---|
393
- | Diagnosing a rejected verifier prompt | The reference prompt matching the selected verification mode |
393
+ | Diagnosing a rejected verifier prompt | Adversarial Re-verification Prompt |
394
394
  | `config.critic.enabled` is true | Coverage critic pass, including its shared dispatch procedure |
395
395
  | An enabled critic uses acceptance mode for `final-verification` | Acceptance critic pass as well as the shared Coverage critic dispatch procedure |
396
396
 
397
397
  Read the matching sections before their dispatch. A disabled critic needs neither
398
- critic section. Generated prompts retain the selected mode's instructions even
399
- when the lead does not read the example text.
400
-
401
- ### Lightweight Re-verification Prompt
402
-
403
- Rendered by `okstra convergence reverify-prompt` when `config.adversarial` is false; the block below is the reference shape, and the rendered body additionally carries each finding's `**Origin item**` and `**Origin audit sidecar**` lines.
404
-
405
- ```
406
- ## Instructions
407
-
408
- **Rendered by:** okstra convergence reverify-prompt
409
-
410
- Perform re-verification for <task-key> (round <N>).
411
-
412
- Review the following findings discovered by other workers.
413
- For EACH finding, respond with exactly one verdict:
414
-
415
- - **AGREE**: The finding is valid based on the evidence presented
416
- - **DISAGREE**: The finding is incorrect or unsupported (explain briefly why)
417
- - **SUPPLEMENT**: The finding is valid AND you have additional supporting evidence or context
418
- - **UNVERIFIABLE**: Capability, credential, network, or service state prevents you
419
- from checking this finding. Explain the unavailable capability; do not substitute DISAGREE.
420
-
421
- Do NOT re-analyze the original source materials. Judge based on the evidence provided.
422
-
423
- ## Findings to verify
424
-
425
- ### F-001: <one-line summary>
426
- **Origin**: <worker role>
427
- **Evidence**: <file paths, line numbers, reasoning from origin worker>
428
-
429
- ### F-002: <one-line summary>
430
- **Origin**: <worker role>
431
- **Evidence**: <...>
432
-
433
- ## Response format
434
-
435
- For each finding, respond as:
436
-
437
- ### F-001
438
- **Verdict**: AGREE | DISAGREE | SUPPLEMENT | UNVERIFIABLE
439
- **Explanation** (required for every verdict, AGREE included): <2-3 sentences>
440
-
441
- ### F-002
442
- **Verdict**: ...
443
- ```
398
+ critic section. Generated prompts carry the adversarial instructions even when
399
+ the lead does not read the example text.
444
400
 
445
401
  ### Adversarial Re-verification Prompt
446
402
 
447
- Used instead of the lightweight/full-reanalysis prompt when `config.adversarial == true`. Rendered by `okstra convergence reverify-prompt`; the block below is the reference shape, and the rendered body additionally carries each finding's `**Origin item**` and `**Origin audit sidecar**` lines. The required anchor headers (§"Required reverify-prompt anchor headers") are identical. The `[Required reading]` clause is suppressed; only the cited-evidence paths of the items under attack are injected (see §"Adversarial Verification Mode" → Scoped full-reanalysis).
403
+ The reverify prompt for every phase. Rendered by `okstra convergence reverify-prompt`; the block below is the reference shape, and the rendered body additionally carries each finding's `**Origin item**` and `**Origin audit sidecar**` lines. The required anchor headers (§"Required reverify-prompt anchor headers") are identical. The `[Required reading]` clause is suppressed; only the cited-evidence paths of the items under attack are injected (see §"Adversarial Verification Mode" → Scoped full-reanalysis).
448
404
 
449
405
  ```
450
406
  ## Instructions
@@ -504,50 +460,6 @@ When persisting votes, map SURVIVES→`agree`, SURVIVES-WITH-CAVEAT→`supplemen
504
460
 
505
461
  UNVERIFIABLE is **not** `verification-error`. A verifier that opened the evidence and could not check it participated in the round; a `verification-error` is a verifier that failed to answer. The classifier counts only non-error votes, so folding the two shrinks the participating roster without saying so — a round where one analyser answers UNVERIFIABLE throughout would read as a two-way cross-check while reporting three voters. **Enforced:** `okstra convergence collect-results` applies this mapping (`okstra_ctl.verdict_blocks.ADVERSARIAL_VERDICTS`); do not transcribe votes by hand.
506
462
 
507
- ### Full Re-analysis Re-verification Prompt
508
-
509
- ```
510
- ## Instructions
511
-
512
- **Rendered by:** okstra convergence reverify-prompt
513
-
514
- Perform deep re-verification for <task-key> (round <N>).
515
-
516
- Independently verify the following findings by examining the original materials.
517
- Use each finding as a starting point, NOT as a confirmed conclusion.
518
- If capability, credential, network, or service state prevents access to evidence
519
- needed for a finding, you MUST answer UNVERIFIABLE with a non-empty explanation.
520
- You MUST NOT answer DISAGREE merely because access or reproduction failed;
521
- UNVERIFIABLE is persisted as `verification-error` and excluded from consensus.
522
-
523
- **Enforced:** `scripts/okstra_ctl/convergence_engine.py` `_VERDICTS` accepts the verdict as `verification-error`, and `classify_collaborative_round` drops those votes before counting consensus.
524
-
525
- ## Task bundle paths
526
- - Instruction set: <instruction-set path>
527
- - Task brief: <task-brief path>
528
- - Analysis material: <analysis-material path>
529
- - Reference expectations: <reference-expectations path>
530
-
531
- ## Findings to verify
532
-
533
- ### F-001: <one-line summary>
534
- **Origin**: <worker role>
535
- **Original evidence**: <file paths, line numbers>
536
-
537
- ### F-002: <one-line summary>
538
- **Origin**: <worker role>
539
- **Original evidence**: <...>
540
-
541
- ## Response format
542
-
543
- For each finding:
544
-
545
- ### F-001
546
- **Verdict**: AGREE | DISAGREE | SUPPLEMENT | UNVERIFIABLE
547
- **Your evidence**: <your independent evidence trail with file paths and line numbers>
548
- **Explanation**: <detailed analysis>
549
- ```
550
-
551
463
  ## Convergence State Artifact
552
464
 
553
465
  Save it to `runs/<task-type>/state/convergence-<task-type>-<seq>.json`.
@@ -558,10 +470,10 @@ Save it to `runs/<task-type>/state/convergence-<task-type>-<seq>.json`.
558
470
  "taskKey": "<task-key>",
559
471
  "config": {
560
472
  "enabled": true,
561
- "adversarial": false,
473
+ "adversarial": true,
562
474
  "maxRounds": 2,
563
475
  "effectiveMaxRounds": 2,
564
- "verificationMode": "lightweight"
476
+ "verificationMode": "full-reanalysis"
565
477
  },
566
478
  "findings": [
567
479
  {
@@ -616,9 +528,9 @@ Save it to `runs/<task-type>/state/convergence-<task-type>-<seq>.json`.
616
528
 
617
529
  Schema rules:
618
530
 
619
- - `schemaVersion`: literal string `"1.4"` for all new runs — both adversarial and collaborative. Historical readers accept `"1.0"` / `"1.1"` / `"1.2"` / `"1.3"` unchanged and never rewrite those artifacts during validation. v1.3 added the strict coverage-critic ledger and the rejection of unknown top-level fields; v1.4 adds the `unverified` classification and its `finalClassificationCounts.unverified` key. Work-state remains v1.0.
531
+ - `schemaVersion`: literal string `"1.4"` for all new runs. Historical readers accept `"1.0"` / `"1.1"` / `"1.2"` / `"1.3"` unchanged and never rewrite those artifacts during validation. v1.3 added the strict coverage-critic ledger and the rejection of unknown top-level fields; v1.4 adds the `unverified` classification and its `finalClassificationCounts.unverified` key. Work-state remains v1.0.
620
532
  - `config.adversarial`: boolean. `true` when this run used adversarial verification (default for `requirements-discovery` / `error-analysis` / `implementation-option-selection` / `implementation-planning` / `project-analysis` / `feature-analysis` / `change-impact-analysis`). When `true`, `config.verificationMode` is `"full-reanalysis"` (scoped) and every `disagree` vote carries a non-null `disagreeBasis`.
621
- - `config.effectiveMaxRounds`: the integer the lead actually used after resolving the phase-aware default (`1` for `requirements-discovery`, `2` otherwise). It may be lower than `config.maxRounds` — a phase-aware default that resolves below the manifest ceiling is the normal case — but never higher. **Enforced:** `scripts/okstra_ctl/convergence_engine.py` `_parse_config` raises `ConvergenceContractError` on `effectiveMaxRounds > maxRounds`, so a state carrying that pair cannot be loaded, and `plan_next_round` finalizes with reason `max-rounds-reached` once the executed rounds reach the effective budget rather than dispatching another one. `totalRounds` is therefore bounded by construction, not by a later re-count.
533
+ - `config.effectiveMaxRounds`: the integer the lead actually used after resolving the default (`2`). It may be lower than `config.maxRounds` — a phase-aware default that resolves below the manifest ceiling is the normal case — but never higher. **Enforced:** `scripts/okstra_ctl/convergence_engine.py` `_parse_config` raises `ConvergenceContractError` on `effectiveMaxRounds > maxRounds`, so a state carrying that pair cannot be loaded, and `plan_next_round` finalizes with reason `max-rounds-reached` once the executed rounds reach the effective budget rather than dispatching another one. `totalRounds` is therefore bounded by construction, not by a later re-count.
622
534
  - `findings[].ticketIds`: array of ticket keys from Phase 4 grouping (parsed per the Round 0 step 5 rule). It is empty when the phase does not require ticket tagging; `"unknown"` is not a ticket key and must not be synthesized.
623
535
  - `findings[].rounds[].votes.<worker>.verdict`: enum, one of `agree | disagree | supplement | verification-error`. Lower-case tokens; map upper-case AGREE/DISAGREE/SUPPLEMENT verdicts emitted by workers to their lower-case form and map the input alias `unverifiable` to persisted `verification-error`. The latter represents either a terminal non-result dispatch or a completed dispatch that could not verify a particular finding (§"Worker failure handling in reverify"). Every vote has a non-empty `explanation`.
624
536
  - `findings[].rounds[].votes.<worker>.disagreeBasis`: enum `counter-evidence | burden-not-met | null`. Non-null only when `verdict == "disagree"` AND `config.adversarial == true`; `null` (or absent, treated as null) otherwise. See §"Adversarial Verification Mode".
@@ -635,7 +547,7 @@ Schema rules:
635
547
 
636
548
  ## Coverage critic pass
637
549
 
638
- Runs when `convergence.critic.enabled == true`. Critic is opt-in on `requirements-discovery`, `error-analysis`, `implementation-planning`, and `final-verification` (role `min` 0, `recommended`/`max` 1; the user chooses whether to add the slot and picks the model). A run without a critic slot renders `enabled: false` and skips this pass. For `final-verification` the critic runs in a different mode — see §"Acceptance critic pass (final-verification)". This pass targets **scope in both directions** — findings that are missing (coverage) and work the findings propose that no requirement asked for (over-scope) — distinct from convergence, which targets **agreement quality** among the findings already raised. The pass keeps its `coverage` mode id and `gaps` vocabulary for both halves; the two are told apart by each candidate's `category`, so no schema or reducer distinguishes them. In `implementation-planning` the same critic slot also settles plan-body analyser 1-1 splits as `critic-worker`.
550
+ Runs when `convergence.critic.enabled == true`. Critic is opt-in on `requirements-discovery`, `error-analysis`, `implementation-planning`, and `final-verification` (role `min` 0, `recommended`/`max` 1; the user chooses whether to add the slot and picks the model). A run without a critic slot renders `enabled: false` and skips this pass. For `final-verification` the critic runs in a different mode — see §"Acceptance critic pass (final-verification)". This pass targets **scope in both directions** — findings that are missing (coverage) and work the findings propose that no requirement asked for (over-scope) — distinct from convergence, which targets **agreement quality** among the findings already raised. The pass keeps its `coverage` mode id and `gaps` vocabulary for both halves; the two are told apart by each candidate's `category`, so no schema or reducer distinguishes them. In `implementation-planning` the same critic slot also settles plan-body analyser 1-1 splits as `critic-worker`, and in every phase it breaks finding ties as `critic-worker` (§"Adversarial classification").
639
551
 
640
552
  ### When
641
553
 
@@ -741,14 +653,15 @@ The asymmetry is deliberate and runs the opposite way from the coverage half: a
741
653
 
742
654
  The `final-verification` phase uses the same fresh one-shot `redispatch_worker` pattern and the same dispatch timing as §"Coverage critic pass" §"When" (provider + `config.critic.modelExecutionValue` from the `convergence.critic` block; critic is opt-in — the pass is skipped when the run resolved no critic; same model-unresolved skip rule) — the delivered work the critic inspects is likewise fixed before the reverify round starts. Only the prompt, the verification semantics, and the output sink differ — final-verification's findings are defects/blockers, so the critic acts as an **acceptance devil's advocate** (find reasons NOT to accept), and its candidate blockers are NEVER dropped (that would suppress real defects).
743
655
 
744
- Before that call, write the acceptance-only task instructions and run `okstra
656
+ Before that call, write the output of `okstra convergence critic-prompt
657
+ --run-manifest <run-manifest>` verbatim as the task instructions and run `okstra
745
658
  agent-prompt materialize` with `--audience acceptance-critic`,
746
659
  `--assignment-ref critic/acceptance`, `--worker-id acceptance` (the run-input
747
660
  Worker Roster line), and `--dispatch-kind critic`. **The final-verification 96-nonblank-line prompt-body
748
661
  cap applies to this dispatch too** — `resolve_prompt_plan` exempts a critic from
749
662
  the analysis equality group, not from the cap. The anchors and the duty contract
750
663
  account for roughly 51 of those lines, so the instructions you write have a
751
- budget of about 45 nonblank lines; the §Prompt text below is 20 and fits. Adding
664
+ budget of about 45 nonblank lines; the rendered body omits the Round 0 list to fit. Adding
752
665
  your own restatement of the run's findings on top of it is what overruns the cap
753
666
  (measured 117 on 2026-09-10, dev-10642-15 final-verification 001). Verify the returned `metadataPath`, dispatch only the
754
667
  returned `promptPath`, and select `hostModelValue` for a native host or
@@ -758,6 +671,8 @@ delivery was observed.
758
671
 
759
672
  ### Prompt
760
673
 
674
+ The mandate `critic-prompt` renders for `final-verification` (it adds the delivery-mode header, the analysis packet line, the analyser result lines and the `### [AC-NNN]` candidate format):
675
+
761
676
  ```
762
677
  Challenge acceptance for <task-key>. The delivered work is about
763
678
  to be judged for acceptance. Your ONLY job is to find reasons it should NOT be
@@ -776,6 +691,13 @@ so explicitly.
776
691
  ### Verification — confirm-or-downgrade (BLOCKING)
777
692
 
778
693
  Each candidate blocker is verified by the Phase 4 analysers — all of them, on the same roster rule as §"critic gaps" above; sharing the critic's provider name does not disqualify an analyser. Do NOT use the adversarial finding classifier's "uncertain → reject" rule here.
694
+
695
+ 1. Write the candidates as `{schemaVersion, taskKey, mode: "acceptance-devils-advocate", provider, modelExecutionValue, candidates[]}` with one `{candidateId, statement, severity, originEvidence}` per candidate, `candidateId` being the id in the critic's candidate heading (`### [AC-001]` or a plain `### AC-001 — …` from a critic dispatched before the rendered format).
696
+ 2. For each analyser, render `okstra convergence critic-verify-prompt --run-manifest <run-manifest> --gaps <that batch> --worker <worker>` and materialize it as a critic-gap verification row (`--dispatch-kind critic-verify`, `--assignment-ref reverify/<worker>`).
697
+ 3. Run `okstra convergence collect-acceptance-votes --run-manifest <run-manifest> --candidates <that batch> --result <worker>=<path>… --output <acceptance-batch>`; it marks a candidate `confirmed` when any analyser answers SURVIVES or SURVIVES-WITH-CAVEAT and `downgraded` otherwise.
698
+ 4. Pass that output to `apply-acceptance-critic` (§State).
699
+
700
+ **Enforced:** `okstra_ctl.convergence_critic_verify_prompt.acceptance_verify_prompt_body` renders the instructions under the `critic-verify` signature that `validate_reverify_prompt` requires, and `convergence._collect_acceptance_votes` derives each verdict from the parsed votes.
779
701
  - Do NOT run `apply-critic-gaps` for this mode. That reducer implements coverage merge/drop semantics and rejects `acceptance-devils-advocate` input.
780
702
  - **Confirmed** (an analyser reproduces it or cites supporting evidence) → promote to a `## 5.8 Acceptance Blockers` row (keep severity + recommended follow-up phase).
781
703
  - **Not confirmed** (cannot reproduce, or evidence is weak) → **downgrade to a Residual Risk row — never drop it.** Record the escalation trigger so the user can re-judge a high-severity-but-unconfirmed candidate.