okstra 0.140.0 → 0.141.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 (37) hide show
  1. package/README.md +1 -1
  2. package/docs/architecture.md +1 -1
  3. package/docs/cli.md +1 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/agents/workers/report-writer-worker.md +4 -2
  7. package/runtime/prompts/launch.template.md +7 -5
  8. package/runtime/prompts/lead/convergence.md +22 -0
  9. package/runtime/prompts/lead/plan-body-verification.md +45 -6
  10. package/runtime/prompts/lead/report-writer.md +2 -0
  11. package/runtime/prompts/profiles/error-analysis.md +1 -0
  12. package/runtime/prompts/profiles/final-verification.md +1 -1
  13. package/runtime/prompts/profiles/implementation-planning.md +6 -4
  14. package/runtime/prompts/profiles/improvement-discovery.md +2 -1
  15. package/runtime/prompts/profiles/requirements-discovery.md +2 -1
  16. package/runtime/python/okstra_ctl/build_tools.py +95 -0
  17. package/runtime/python/okstra_ctl/incremental_scope.py +95 -0
  18. package/runtime/python/okstra_ctl/render.py +26 -11
  19. package/runtime/python/okstra_ctl/render_final_report.py +24 -1
  20. package/runtime/python/okstra_ctl/rollup.py +49 -0
  21. package/runtime/python/okstra_ctl/scope_provenance.py +98 -1
  22. package/runtime/python/okstra_ctl/stage_citations.py +47 -0
  23. package/runtime/python/okstra_ctl/workflow.py +7 -4
  24. package/runtime/schemas/final-report-v1.0.schema.json +60 -1
  25. package/runtime/skills/okstra-brief-gen/SKILL.md +38 -11
  26. package/runtime/skills/okstra-rollup/SKILL.md +3 -2
  27. package/runtime/templates/reports/brief.template.md +42 -11
  28. package/runtime/templates/reports/fan-out-unit.template.md +9 -4
  29. package/runtime/templates/reports/final-report.template.md +20 -0
  30. package/runtime/templates/reports/final-verification-input.template.md +4 -2
  31. package/runtime/templates/reports/i18n/en.json +11 -4
  32. package/runtime/templates/reports/i18n/ko.json +11 -4
  33. package/runtime/templates/reports/improvement-discovery-input.template.md +2 -2
  34. package/runtime/validators/validate-brief.py +171 -26
  35. package/runtime/validators/validate-run.py +450 -78
  36. package/runtime/validators/validate_fanout.py +19 -10
  37. package/runtime/validators/validate_improvement_report.py +17 -4
@@ -577,20 +577,28 @@ Split every such checklist across the two sections using one test:
577
577
 
578
578
  > **Can an okstra phase satisfy this by changing files in this repository?**
579
579
 
580
- - **Yes → `## Acceptance Criteria`.** Source, tests, config, and deployment
581
- *manifest files* all count editing `values-prod.yaml` is a file change.
580
+ - **Yes → the end-state sections.** A runtime behaviour goes to
581
+ `## Expected Behavior`, a behaviour that must survive unchanged to
582
+ `## Preserved Behavior`, and an artifact state the work must leave behind to
583
+ `## Expected Outcome`. Source, tests, config, and deployment *manifest files*
584
+ all count — editing `values-prod.yaml` is a file change.
582
585
  - **No → `## External Gates`.** A person's approval or sign-off, manual QA,
583
586
  validation performed in staging/production, creating a dashboard or alert,
584
587
  running a deployment, and ticket status transitions. Record who or what owns
585
588
  it (`Brice`, `release manager`, `CI`), so the reader can act on it outside
586
589
  okstra.
590
+ - **An item with no observation method is not an end state.** If nothing comes
591
+ to mind for the `— verify:` half — no command, no observation point — send it
592
+ to `## External Gates`, or raise it in `## Open Questions` as a `general:`
593
+ row. Making this call upstream is what stops each downstream phase from
594
+ reading the same prose its own way.
587
595
 
588
596
  The distinction is the *action*, not the topic: "add the RSS flag to
589
597
  `values-prod.yaml`" is a criterion; "apply that manifest to prod" and "confirm
590
598
  the new-app count on the Metabase dashboard with Brice" are gates.
591
599
 
592
600
  Never drop a gate to make the brief tidier — an omitted release step is worse
593
- than a correctly-parked one. Never promote a gate into Acceptance Criteria to
601
+ than a correctly-parked one. Never promote a gate into an end-state section to
594
602
  look thorough either: an okstra phase cannot satisfy it, so it can only surface
595
603
  later as an unmeetable requirement that blocks the run.
596
604
 
@@ -644,8 +652,12 @@ Required sections:
644
652
  - **Problem / Symptom** — current state. For bugs: repro + observed/expected;
645
653
  for greenfield: gap between current and desired.
646
654
  - **Desired Outcome** — success shape, not a solution prescription.
647
- - **Acceptance Criteria** — must-pass points an okstra phase can satisfy by
648
- changing repository files. See "Splitting a source checklist" below.
655
+ - **Expected Behavior** — observable runtime behaviours the finished work must
656
+ exhibit, as `EB-NNN verify: …` bullets.
657
+ - **Preserved Behavior** — behaviours this work must NOT change, as `PB-NNN …
658
+ — verify: …` bullets. The only end-state section a codebase-scan brief carries.
659
+ - **Expected Outcome** — artifact states the finished work must leave behind, as
660
+ `EO-NNN … — verify: …` bullets. See "Splitting a source checklist" below.
649
661
  - **External Gates** — must-pass points owned by a person or by live
650
662
  infrastructure. Recorded for the reader; never a requirement for any phase.
651
663
  - **Constraints** — deadlines, compatibility, technical/operational limits.
@@ -746,7 +758,9 @@ The installed template `~/.okstra/templates/reports/brief.template.md` is the by
746
758
  | `## Problem / Symptom` | Required | Not required — omit | Required |
747
759
  | `## Context` | Required | Required | Required |
748
760
  | `## Desired Outcome` | Required | Required | Required |
749
- | `## Acceptance Criteria` | Required — `_(none)_` or one bullet per repo-satisfiable must-pass point | Required — `_(none)_` or bullets | Required — `_(none)_` or bullets |
761
+ | `## Expected Behavior` | Required — `_(none)_` or `EB-NNN verify: …` bullets | **Rejected** (scan has not run) | Required — same as reporter-input |
762
+ | `## Preserved Behavior` | Required — `_(none)_` or `PB-NNN … — verify: …` bullets | Required — `PB-NNN` bullets (`_(none)_` rejected) | Required — same as reporter-input |
763
+ | `## Expected Outcome` | Required — `_(none)_` or `EO-NNN … — verify: …` bullets | **Rejected** (scan has not run) | Required — same as reporter-input |
750
764
  | `## External Gates` | Required — `_(none)_` or one bullet per person/infra-owned gate | Required — `_(none)_` or bullets | Required — `_(none)_` or bullets |
751
765
  | `## Constraints` | Required | Required | Required |
752
766
  | `## Related Artifacts` | Required | Required | Required |
@@ -755,6 +769,14 @@ The installed template `~/.okstra/templates/reports/brief.template.md` is the by
755
769
  | `## Scan Scope` | Not applicable — omit | Required — one bullet per `scan-scope` path with a short description of what lives there | Not applicable — omit |
756
770
  | `## Priority Lenses` | Not applicable — omit | Required — one bullet per priority lens with a short rationale for why that lens applies to this scope | Not applicable — omit |
757
771
 
772
+ `## Desired Outcome` and `## Expected Outcome` are different sections. The first
773
+ is prose — the shape of success in the reporter's own words. The second is the
774
+ checkable half: `EO-NNN … — verify: …` bullets, and the only one downstream
775
+ phases map against. Prose written in the second is rejected by the brief
776
+ validator. An `EO-NNN` written in the first is **not** — that section is checked
777
+ only for a non-blank body, so the brief passes and the run then fails with no id
778
+ to map at `endStateCoverage`. Check which section you are in before writing an id.
779
+
758
780
  ### Frontmatter rules
759
781
 
760
782
  - Every brief starts on line 1 with a YAML frontmatter delimited by `---`.
@@ -880,11 +902,14 @@ labelled-handoff contract — required frontmatter keys, Augmentation labels,
880
902
  `intent-inference ↔ intent-check:` auto-mirroring, `parent-id`/`depth`
881
903
  consistency, `reporter-confirmations` markers, the variant-required sections
882
904
  from "Required sections by variant" (`Source Material` / `Problem / Symptom`
883
- off-codebase, plus `Context` / `Desired Outcome` / `Acceptance Criteria` /
905
+ off-codebase, plus `Context` / `Desired Outcome` / the end-state sections
906
+ (`Expected Behavior` / `Preserved Behavior` / `Expected Outcome`) /
884
907
  `External Gates` / `Constraints` / `Related Artifacts` / `Related Task Graph`
885
908
  / `Open Questions` everywhere — each present with a non-blank body, `_(none)_`
886
- allowed), `Related Task Graph` table shape/relation values, and the
887
- codebase-scan `scope`/`Scan Scope`/`Priority Lenses` rules.
909
+ allowed), the end-state item format (`EB-NNN` / `PB-NNN` / `EO-NNN` id plus a
910
+ non-empty `— verify:` observation method), `Related Task Graph` table
911
+ shape/relation values, and the codebase-scan `scope`/`Scan Scope`/`Priority
912
+ Lenses` rules.
888
913
 
889
914
  1. Run the validator over the briefs directory:
890
915
 
@@ -904,8 +929,10 @@ codebase-scan `scope`/`Scan Scope`/`Priority Lenses` rules.
904
929
  `depth`, `created`, `generator`, `reporter-confirmations`); every
905
930
  section the "Required sections by variant" table marks Required for the
906
931
  brief's variant is present with a non-blank body (`_(none)_` allowed) —
907
- in particular `Desired Outcome`, `Acceptance Criteria`, `External Gates`,
908
- `Related Artifacts`, and `Related Task Graph`, the ones authors drop most;
932
+ in particular `Desired Outcome`, the three end-state sections,
933
+ `External Gates`, `Related Artifacts`, and `Related Task Graph`, the ones
934
+ authors drop most; every end-state bullet carries an `EB-NNN` / `PB-NNN` /
935
+ `EO-NNN` id and a non-empty `— verify:` observation method;
909
936
  every Augmentation entry carries one of the four labels; every
910
937
  `intent-inference` has a paired `intent-check:` row; `parent-id` is `self`
911
938
  at depth 0 and a real parent id below; and for `scope: codebase` briefs,
@@ -41,8 +41,9 @@ okstra rollup --task-group <group> --project-root <projectRoot> --json
41
41
  Omit `--task-group` for the whole project. The JSON shape (all durations are **raw milliseconds**):
42
42
 
43
43
  - `taskGroup` — the scope (`null` = whole project), `taskCount` — number of tasks.
44
- - `tasks[]` — per task: `taskKey, taskGroup, taskId, taskType, workCategory, workStatus, currentPhase, currentPhaseState, nextRecommendedPhase, latestRunStatus, updatedAt, reportPath, runCount, cpuSumMs, wallClockMs, errorCount`.
45
- - `totals` — `runs, cpuSumMs, wallClockMs, errors`, plus `byWorkStatus`, `byWorkCategory`, `byCurrentPhase`, `byTaskType` (each a `{value: count}` map).
44
+ - `tasks[]` — per task: `taskKey, taskGroup, taskId, taskType, workCategory, workStatus, currentPhase, currentPhaseState, nextRecommendedPhase, latestRunStatus, updatedAt, reportPath, runCount, cpuSumMs, wallClockMs, errorCount`, plus `criticRuns, criticGapsProposed, criticGapsMerged, criticGapsRejected, criticGapsUnverified`.
45
+ - `totals` — `runs, cpuSumMs, wallClockMs, errors`, plus `byWorkStatus`, `byWorkCategory`, `byCurrentPhase`, `byTaskType` (each a `{value: count}` map), plus `critic` (`runsWithCritic, gapsProposed, gapsMerged, gapsRejected, gapsUnverified`).
46
+ - The coverage critic is opt-in, so `totals.critic.runsWithCritic == 0` means it was never used — report that as "not used", never as a zero-yield result. When it IS non-zero, the merge rate (`gapsMerged / gapsProposed`) is what tells the user whether the pass earns its tokens; state it only when at least one run used it.
46
47
 
47
48
  `runCount` counts every timeline run; `cpuSumMs`/`wallClockMs` only reflect runs that reached Phase 7 usage, so they can be `0` while `runCount > 0`. `reportPath` is project-relative and may be empty (task not yet reported).
48
49
 
@@ -73,20 +73,51 @@ between current and desired.>
73
73
 
74
74
  <!-- Do NOT prescribe a solution — that belongs to implementation-planning. -->
75
75
 
76
- ## Acceptance Criteria
76
+ ## Expected Behavior
77
77
 
78
- <One bullet per must-pass point that okstra phases can satisfy by changing
79
- files in this repository. Use _(none)_ if none.>
78
+ <One bullet per observable runtime behaviour the finished work must exhibit.
79
+ Use _(none)_ when this work changes no runtime behaviour (a pure refactor).>
80
80
 
81
- <!-- Admissibility test a criterion belongs here only if it is satisfied by
82
- changing repository files (source, tests, config, deployment manifests).
83
- Anything that needs a person's approval, a ticket status change, or an action
84
- against live infrastructure goes to `## External Gates` instead. Downstream
85
- phases treat this section as the requirement set: `implementation-planning`
86
- builds one Requirement Coverage row per bullet, `final-verification` gates
87
- acceptance on it. -->
81
+ <!-- Format: `EB-NNN <observable condition> verify: <command or observation point>`
82
+ The `— verify:` half is mandatory and stays English in every locale, because
83
+ downstream validators parse it. An item you cannot give an observation method
84
+ belongs in `## External Gates`, not here. `## Expected Behavior` and
85
+ `## Expected Outcome` must not both be empty. -->
88
86
 
89
- - <criterion satisfied by a repository file change>
87
+ - <EB-NNN observable condition verify: command or observation point>
88
+
89
+ ## Preserved Behavior
90
+
91
+ <One bullet per behaviour this work must NOT change. Use _(none)_ if none.>
92
+
93
+ <!-- This is the upper bound a refactor or improvement cannot express through
94
+ Expected Behavior alone. Same id and `— verify:` rule, with the `PB-` prefix.
95
+ codebase-scan variant: this is the ONLY end-state section. `## Expected
96
+ Behavior` and `## Expected Outcome` are rejected there, because the scan has
97
+ not run yet. -->
98
+
99
+ - <PB-NNN behaviour that must survive unchanged — verify: regression observation point>
100
+
101
+ ## Expected Outcome
102
+
103
+ <One bullet per artifact-state the finished work must leave behind — a module
104
+ moved, a duplicate removed, a record written. Use _(none)_ if none.>
105
+
106
+ <!-- NOT `## Desired Outcome`. That section is prose: the shape of success, in
107
+ whatever words the reporter thinks in. This one is the checkable half — each
108
+ bullet is an `EO-NNN` id with an observation method, and it is what downstream
109
+ phases map against. Prose written here is rejected outright. An `EO-NNN` id
110
+ written up there is NOT: `## Desired Outcome` is checked only for a non-blank
111
+ body, so a misfiled id passes the brief and then leaves the run with nothing to
112
+ map at `endStateCoverage`. Check which section you are in before writing an id.
113
+
114
+ Same id and `— verify:` rule as the two sections above, with the `EO-` prefix.
115
+ Downstream phases treat Expected Behavior / Preserved Behavior / Expected
116
+ Outcome as the requirement set: every later phase maps each id to its own
117
+ deliverable, and `implementation-planning` builds one Requirement Coverage row
118
+ per id. -->
119
+
120
+ - <EO-NNN artifact state that must exist when the work is done — verify: command or observation point>
90
121
 
91
122
  ## External Gates
92
123
 
@@ -19,13 +19,18 @@ recommended-next-phase: {{NEXT_PHASE}}
19
19
  ## Requirement Provenance
20
20
 
21
21
  <!-- Where this unit's scope came from. One bullet per source. Every bullet must be
22
- `brief:<heading>` (a heading that literally exists in the task brief) or
23
- `contract:<rule>` (an okstra-mandated artifact). A unit you cannot source this
24
- way is not a work item raise it as a clarification instead of inventing it.
22
+ `brief:EB-001` / `brief:PB-001` / `brief:EO-001` an end-state id the brief
23
+ declares — or `contract:<rule>` (an okstra-mandated artifact). Citing a heading
24
+ is rejected when the brief pins ids: every brief carries the same generic
25
+ headings, so a heading cannot say WHICH reporter line demanded this unit. Only a
26
+ brief authored before the end-state sections existed still takes the older
27
+ `brief:<heading>` form, and there the heading must literally exist in it.
28
+ A unit you cannot source this way is not a work item — raise it as a
29
+ clarification instead of inventing it.
25
30
  Unlike a planning report, `derived:<parent> — <reason>` is NOT accepted here: a
26
31
  packet is validated on its own, so the parent it names cannot be resolved. -->
27
32
 
28
- - brief:
33
+ - brief:EB-001
29
34
 
30
35
  ## Evidence
31
36
 
@@ -153,6 +153,24 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
153
153
  {% endfor %}
154
154
  {%- endif %}
155
155
 
156
+ {% if header.taskType in ('requirements-discovery', 'error-analysis', 'implementation-planning') %}
157
+
158
+ ### 2.3 End State Coverage{% if t("sectionAside.endStateCoverage") != "End State Coverage" %} ({{ t("sectionAside.endStateCoverage") }}){%- endif %}
159
+
160
+ {% if endStateCoverage | length == 0 -%}
161
+ {{ t("emptyState.endStateCoverage") }}
162
+ {%- else %}
163
+ {{ t("sectionIntro.endStateCoverage") }}
164
+
165
+ | ID | {{ t("columns.disposition") }} | {{ t("columns.coveredBy") }} | {{ t("columns.evidence") }} | {{ t("columns.rationale") }} |
166
+ |----|---------------|-----------|----------|-----------|
167
+ {% for row in endStateCoverage -%}
168
+ | {{ row.id | mdcell }} | `{{ row.disposition | mdcell }}` | {{ row.coveredBy | mdcell }} | {{ (row.evidence | join(", ")) | mdcell }} | {{ row.rationale | mdcell }} |
169
+ {% endfor %}
170
+ {%- endif %}
171
+
172
+ {% endif %}
173
+
156
174
  ## 3. Recommended Next Steps
157
175
 
158
176
  {% if recommendedNextSteps | length == 0 -%}
@@ -358,6 +376,8 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
358
376
 
359
377
  - **Round count**: `{{ implementationPlanning.planBodyVerification.roundCount }}`
360
378
  - **Gate result**: `{{ implementationPlanning.planBodyVerification.gateResult }}`
379
+ {% if implementationPlanning.planBodyVerification.participatingAnalysers %}- **Participating analysers**: `{{ implementationPlanning.planBodyVerification.participatingAnalysers.voting }}` / `{{ implementationPlanning.planBodyVerification.participatingAnalysers.rostered }}`
380
+ {% endif %}
361
381
  {% if implementationPlanning.planBodyVerification.gateBlockedBy %}- **Blocked by**: {% for cause in implementationPlanning.planBodyVerification.gateBlockedBy %}`{{ cause }}`{% if not loop.last %}, {% endif %}{% endfor %}
362
382
  {% endif %}
363
383
  > {{ t("implementationPlanning.planBodyGateLegend") }}
@@ -45,11 +45,11 @@ taskType: "{{FM_TASK_TYPE}}"
45
45
  ## Requirement Coverage Source
46
46
 
47
47
  - Approved implementation-planning report path:
48
- - Requirement source used for coverage (plan section / brief Acceptance Criteria):
48
+ - Requirement source used for coverage (plan section / brief end-state items):
49
49
  - Requirement IDs / acceptance IDs to verify:
50
50
  - Requirements intentionally excluded from this verification:
51
51
 
52
- > final-verification MUST cite an artifact in Validation Evidence for each requirement / acceptance id in the source above. If the source is empty, use the brief's `## Acceptance Criteria` as the default source.
52
+ > final-verification MUST cite an artifact in Validation Evidence for each requirement / acceptance id in the source above. If the source is empty, use the brief's `## Expected Behavior` / `## Expected Outcome` items as the default source.
53
53
 
54
54
  ## Verification Evidence
55
55
 
@@ -69,6 +69,8 @@ taskType: "{{FM_TASK_TYPE}}"
69
69
 
70
70
  ## Acceptance Criteria
71
71
 
72
+ <!-- This is the verification run's own acceptance input, not the brief's end-state sections. Seed it from the brief's `EB-NNN` / `EO-NNN` items. -->
73
+
72
74
  - Must-pass acceptance points:
73
75
  - Optional quality improvements:
74
76
  - Conditions that should block release:
@@ -18,7 +18,8 @@
18
18
  "discrepancy": "- None.",
19
19
  "lingeringRisks": "- No tracked lingering risks.",
20
20
  "noClarification": "- No additional information requested. The Section 7 verdict stands as-is.",
21
- "noFollowUp": "- No follow-up tasks. The next phase for this run is in §3 (Recommended Next Steps)."
21
+ "noFollowUp": "- No follow-up tasks. The next phase for this run is in §3 (Recommended Next Steps).",
22
+ "endStateCoverage": "No end-state coverage recorded for this phase."
22
23
  },
23
24
  "columns": {
24
25
  "recordMeta": "Record",
@@ -28,7 +29,11 @@
28
29
  "billableTokens": "Billable tokens",
29
30
  "billableTokensInputEquiv": "Billable tokens (input-equiv.)",
30
31
  "cost": "Cost (USD)",
31
- "checkMethod": "How to check"
32
+ "checkMethod": "How to check",
33
+ "disposition": "Disposition",
34
+ "coveredBy": "Covered by",
35
+ "evidence": "Evidence",
36
+ "rationale": "Rationale"
32
37
  },
33
38
  "sectionAside": {
34
39
  "crossProjectDependencies": "Cross-Project Dependencies",
@@ -40,7 +45,8 @@
40
45
  "recommendedOption": "Recommended Option",
41
46
  "optionCandidates": "Option Candidates",
42
47
  "tradeOffMatrix": "Trade-off Matrix",
43
- "stepwiseExecutionOrder": "Stepwise Execution Order"
48
+ "stepwiseExecutionOrder": "Stepwise Execution Order",
49
+ "endStateCoverage": "End State Coverage"
44
50
  },
45
51
  "sectionIntro": {
46
52
  "verdictCard": "At-a-glance verdict card — a summary that mirrors the values in `## 7. Final Verdict` and `## 3. Recommended Next Steps`.",
@@ -50,7 +56,8 @@
50
56
  "sourceItemsRule": "The `Source items` column shows which worker items each consensus row was synthesised from, as `<worker>:<item-id>` pairs.",
51
57
  "planBodyVerification": "Per-item cross-verification of each plan-body item (option, stage, step, dependency, validation, rollback, requirement coverage) for whether it is executable as written and internally consistent. Whether each option actually *solves* the problem (requirement satisfaction) is judged separately under Requirement Coverage, not here.",
52
58
  "clarificationItems": "Items that need your answer or supporting material before the next step.",
53
- "readerSummary": "The shortest reader path: what was decided, what a human must do next, and which audit details can wait until a deeper review."
59
+ "readerSummary": "The shortest reader path: what was decided, what a human must do next, and which audit details can wait until a deeper review.",
60
+ "endStateCoverage": "One row per end-state id the brief pinned, and how this phase accounted for it. A row that is not `addressed` must say why."
54
61
  },
55
62
  "tokenSummary": {
56
63
  "heading": "Token Usage Summary",
@@ -18,7 +18,8 @@
18
18
  "discrepancy": "- 없음.",
19
19
  "lingeringRisks": "- 추적 대상 잔존 위험 없음.",
20
20
  "noClarification": "- 추가 정보 요청 없음. Section 7 의 최종 판단이 그대로 유효합니다.",
21
- "noFollowUp": "- 후속 작업 없음. 본 run 의 다음 phase 는 §3 (Recommended Next Steps) 참고."
21
+ "noFollowUp": "- 후속 작업 없음. 본 run 의 다음 phase 는 §3 (Recommended Next Steps) 참고.",
22
+ "endStateCoverage": "이번 phase 에 기록된 종료 상태 처리가 없습니다."
22
23
  },
23
24
  "columns": {
24
25
  "recordMeta": "항목",
@@ -28,7 +29,11 @@
28
29
  "billableTokens": "환산 토큰",
29
30
  "billableTokensInputEquiv": "환산 토큰 (input 기준)",
30
31
  "cost": "비용 (USD)",
31
- "checkMethod": "확인 방법"
32
+ "checkMethod": "확인 방법",
33
+ "disposition": "처리",
34
+ "coveredBy": "처리 위치",
35
+ "evidence": "근거",
36
+ "rationale": "사유"
32
37
  },
33
38
  "sectionAside": {
34
39
  "crossProjectDependencies": "프로젝트 간 의존성",
@@ -40,7 +45,8 @@
40
45
  "recommendedOption": "권장 옵션",
41
46
  "optionCandidates": "옵션 후보",
42
47
  "tradeOffMatrix": "트레이드오프 매트릭스",
43
- "stepwiseExecutionOrder": "단계별 실행 순서"
48
+ "stepwiseExecutionOrder": "단계별 실행 순서",
49
+ "endStateCoverage": "종료 상태 처리"
44
50
  },
45
51
  "sectionIntro": {
46
52
  "verdictCard": "한눈에 보는 결과 카드 — `## 7. Final Verdict` 와 `## 3. Recommended Next Steps` 의 값을 그대로 옮긴 요약입니다.",
@@ -50,7 +56,8 @@
50
56
  "sourceItemsRule": "`Source items` 열은 이 합의 항목이 어느 워커의 어느 항목에서 합성됐는지를 `<worker>:<item-id>` 형식으로 표기합니다.",
51
57
  "planBodyVerification": "계획 본문의 각 항목(옵션·스테이지·스텝·의존성·검증·롤백·요구사항 커버리지)이 적힌 대로 실행 가능하고 서로 모순이 없는지 워커들이 항목별로 교차 검증한 결과입니다. 각 옵션이 '문제를 실제로 푸는지'(요구사항 충족)는 여기가 아니라 Requirement Coverage 에서 별도로 판정합니다.",
52
58
  "clarificationItems": "다음 진행 전에 사용자의 답변이나 자료 첨부가 필요한 항목입니다.",
53
- "readerSummary": "가장 짧은 읽기 경로입니다. 무엇이 결정됐는지, 사람이 다음에 무엇을 해야 하는지, 어떤 감사 세부사항은 나중에 봐도 되는지 먼저 보여줍니다."
59
+ "readerSummary": "가장 짧은 읽기 경로입니다. 무엇이 결정됐는지, 사람이 다음에 무엇을 해야 하는지, 어떤 감사 세부사항은 나중에 봐도 되는지 먼저 보여줍니다.",
60
+ "endStateCoverage": "브리프가 고정한 종료 상태 id 별로 이번 phase 가 어떻게 처리했는지 기록합니다. `addressed` 가 아닌 행은 사유가 있어야 합니다."
54
61
  },
55
62
  "tokenSummary": {
56
63
  "heading": "토큰 사용량 요약",
@@ -62,8 +62,8 @@ Workers MUST read the resolved values from the log the lead points to via the
62
62
 
63
63
  ## Improvement Candidates (workers populate this)
64
64
 
65
- | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
66
- |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|
65
+ | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Expected behavior after | Evidence |
66
+ |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|-------------------------|----------|
67
67
 
68
68
  ## Questions for Analysers
69
69
 
@@ -37,6 +37,11 @@ Checks performed per brief file:
37
37
  columns and relation/direction values from the okstra-brief-gen contract.
38
38
  13. The requirement/objective section `## Desired Outcome` (required in every
39
39
  brief variant) exists and its body is not blank. `_(none)_` stays valid.
40
+ 14. The end-state sections `## Expected Behavior` (EB-NNN) / `## Preserved
41
+ Behavior` (PB-NNN) / `## Expected Outcome` (EO-NNN): each item carries a
42
+ 3-digit id and an `— verify: <how>` observation method, EB/EO are rejected
43
+ in the codebase-scan variant, and `## External Gates` — the destination for
44
+ an item with no observation method — is required in every variant.
40
45
 
41
46
  Exit code 0 on PASS, 1 on FAIL.
42
47
  """
@@ -426,42 +431,182 @@ def check_requirement_section(text: str, errors: list[str]) -> None:
426
431
  )
427
432
 
428
433
 
429
- # Splitting the source checklist across these two sections is what keeps a
430
- # person-owned gate ("validated in staging by <name>") out of the requirement
431
- # set. Both must exist even when empty, because a missing heading is
432
- # indistinguishable from "the reporter listed no gates" and the silent
433
- # version of that mistake is what puts an unmeetable requirement into
434
- # Requirement Coverage.
435
- SCOPE_SPLIT_SECTIONS = ("Acceptance Criteria", "External Gates")
434
+ # The brief pins the end state once; every later phase maps to it by id rather
435
+ # than restating a goal of its own. Splitting behavior from outcome is what lets
436
+ # a refactor say "nothing observable changes" without leaving the upper bound
437
+ # blank `Preserved Behavior` carries the bound that `Expected Behavior` can't.
438
+ _END_STATE_SECTIONS = (
439
+ ("Expected Behavior", "EB"),
440
+ ("Preserved Behavior", "PB"),
441
+ ("Expected Outcome", "EO"),
442
+ )
443
+ # The destination for a must-pass point nobody can hand an observation method.
444
+ # It is enforced here rather than in ALWAYS_REQUIRED_SECTIONS so the failure can
445
+ # still say WHY the section exists: a generic "required section missing" gives
446
+ # the reporter no way to tell a gate apart from an end state.
447
+ _GATE_SECTION = "External Gates"
448
+ _END_STATE_ID_RE = re.compile(r"^(?P<id>(?:EB|PB|EO)-\d{3})\b")
449
+ # Mirrors the `[—-]` tolerance in scripts/okstra_ctl/scope_provenance.py so a
450
+ # reporter who cannot type an em dash is not silently rejected.
451
+ _VERIFY_RE = re.compile(r"[—-]{1,2}\s*verify:\s*(?P<how>\S.*)$")
452
+ _SCAN_ONLY_PREFIXES = frozenset({"EB", "EO"})
453
+
454
+
455
+ def _has_bullet(body: str) -> bool:
456
+ return any(line.strip().startswith("- ") for line in body.splitlines())
457
+
458
+
459
+ def _declares_nothing(body: str) -> bool:
460
+ """True when the body is the contract's explicit empty marker and nothing else.
461
+
462
+ `_(none)_` is written bare in every other brief section — `is_placeholder`
463
+ accepts both the bare and the `- ` form, and the template tells the reporter
464
+ to "use _(none)_ if none". Demanding a dash here alone would reject the house
465
+ form, and the message that fires would tell the author to write the very
466
+ thing they wrote.
467
+ """
468
+ lines = [line.strip() for line in body.splitlines() if line.strip()]
469
+ return bool(lines) and all(is_placeholder(line) for line in lines)
436
470
 
437
471
 
438
- def check_scope_split_sections(text: str, errors: list[str]) -> None:
439
- """Both halves of the requirement/gate split must exist with a body.
472
+ def _check_gate_section(text: str, errors: list[str]) -> None:
473
+ if not has_section_heading(text, _GATE_SECTION):
474
+ errors.append(
475
+ f"required section '## {_GATE_SECTION}' is missing — it is where a "
476
+ "must-pass point that a person or live infrastructure owns goes. "
477
+ "Without it, an item okstra cannot satisfy has nowhere to land but "
478
+ "the end-state sections, and the plan is then forced to build a "
479
+ "stage for work no phase can do"
480
+ )
481
+ return
482
+ if not section_body(text, _GATE_SECTION).strip():
483
+ errors.append(
484
+ f"required section '## {_GATE_SECTION}' has an empty body "
485
+ "(use _(none)_ only for a deliberately-empty section)"
486
+ )
487
+
440
488
 
441
- `## Acceptance Criteria` holds must-pass points an okstra phase can satisfy
442
- by changing repository files; `## External Gates` holds the ones a person or
443
- live infrastructure owns (okstra-brief-gen SKILL.md §"Splitting a source
444
- checklist"). `_(none)_` stays valid — it is the contract's explicit
445
- placeholder for a deliberately-empty section.
446
- """
447
- for heading in SCOPE_SPLIT_SECTIONS:
448
- if not has_section_heading(text, heading):
489
+ def _check_end_state_items(
490
+ text: str, heading: str, prefix: str, seen: set[str], errors: list[str]
491
+ ) -> None:
492
+ for bullet in meaningful_bullets(text, heading):
493
+ match = _END_STATE_ID_RE.match(bullet)
494
+ if match is None or not match.group("id").startswith(f"{prefix}-"):
449
495
  errors.append(
450
- f"required section '## {heading}' is missing (a brief must split "
451
- "must-pass points into repo-satisfiable criteria and "
452
- "person/infrastructure-owned gates)"
496
+ f"'## {heading}' item {bullet!r} must start with a `{prefix}-NNN` id "
497
+ "(3 digits) later phases map to the brief by id, not by heading"
453
498
  )
454
499
  continue
455
- if not section_body(text, heading).strip():
500
+ item_id = match.group("id")
501
+ if item_id in seen:
502
+ errors.append(f"duplicate end-state id {item_id!r}")
503
+ seen.add(item_id)
504
+ if not _VERIFY_RE.search(bullet):
456
505
  errors.append(
457
- f"required section '## {heading}' has an empty body "
458
- "(use _(none)_ only for a deliberately-empty section)"
506
+ f"end-state item {item_id!r} has no observation method — append "
507
+ "`— verify: <command or observation point>`. An item nobody can "
508
+ "observe is not an end state. It has two destinations, both of "
509
+ f"which keep it in the brief: '## {_GATE_SECTION}' when a person "
510
+ "or live infrastructure owns it, or '## Open Questions' as a "
511
+ "`general:` row when the observation method is what is still "
512
+ "unknown. Deleting it is not one of them."
513
+ )
514
+
515
+
516
+ def _check_one_end_state_section(
517
+ text: str, heading: str, prefix: str, scope: str, seen: set[str], errors: list[str]
518
+ ) -> None:
519
+ """One end-state section: whether it may exist here, its body shape, its items.
520
+
521
+ A codebase-scan brief is authored before anything has been found, so it can
522
+ only pin what must NOT change. Declaring an expected behavior there would be
523
+ an invented requirement, which is why EB/EO are rejected rather than treated
524
+ as optional — and why `_(none)_` is rejected for the one section that
525
+ survives there: it is the whole upper bound of that variant.
526
+ """
527
+ if scope == "codebase" and prefix in _SCAN_ONLY_PREFIXES:
528
+ if has_section_heading(text, heading):
529
+ errors.append(
530
+ f"section '## {heading}' is not allowed in a codebase-scan brief "
531
+ "(the scan has not run yet, so its outcome cannot be declared) — "
532
+ "use '## Preserved Behavior' for what must not change"
533
+ )
534
+ return
535
+ if not has_section_heading(text, heading):
536
+ errors.append(
537
+ f"required section '## {heading}' is missing (the brief pins the "
538
+ "end state that every later phase maps to)"
539
+ )
540
+ return
541
+ body = section_body(text, heading)
542
+ if not body.strip():
543
+ errors.append(
544
+ f"required section '## {heading}' has an empty body "
545
+ "(use _(none)_ only for a deliberately-empty section)"
546
+ )
547
+ return
548
+ if _declares_nothing(body):
549
+ # In a scan this is the only section left, so an empty marker here is
550
+ # not "deliberately empty" — it is a brief with no upper bound at all,
551
+ # which is what improvement-discovery reads as its bound.
552
+ if scope == "codebase":
553
+ errors.append(
554
+ f"'## {heading}' is _(none)_ in a codebase-scan brief. It is the "
555
+ "only end-state section this variant carries, and "
556
+ "improvement-discovery treats it as the bound on what a "
557
+ "candidate may change — an empty one bounds nothing. State at "
558
+ "least one behaviour no candidate from this scan may break"
459
559
  )
560
+ return
561
+ # Only `- ` lines are read as items, so prose here is silently dropped
562
+ # rather than rejected — the reporter believes they pinned an end state
563
+ # and every downstream phase sees a section that declared nothing.
564
+ if not _has_bullet(body):
565
+ errors.append(
566
+ f"'## {heading}' has a body but no `- ` item — prose in this "
567
+ "section is dropped, not read. Every end state is a "
568
+ f"`- {prefix}-NNN <condition> — verify: <how>` bullet; write "
569
+ "_(none)_ on its own line if the section is deliberately empty"
570
+ )
571
+ return
572
+ _check_end_state_items(text, heading, prefix, seen, errors)
573
+
574
+
575
+ def _check_end_state_declared(text: str, errors: list[str]) -> None:
576
+ """A reporter-input brief declares at least one observable end state."""
577
+ if meaningful_bullets(text, "Expected Behavior") or meaningful_bullets(
578
+ text, "Expected Outcome"
579
+ ):
580
+ return
581
+ errors.append(
582
+ "'## Expected Behavior' and '## Expected Outcome' are both empty — a brief "
583
+ "must declare at least one observable end state. A pure bugfix may leave "
584
+ "Expected Outcome as _(none)_ and a pure refactor may leave Expected "
585
+ "Behavior as _(none)_, but not both. If every must-pass point you have is "
586
+ f"owned by a person ('## {_GATE_SECTION}') or still lacks an observation "
587
+ "method ('## Open Questions'), this brief is not ready to start a run — "
588
+ "answer the open question first rather than emptying the end state"
589
+ )
590
+
591
+
592
+ def check_end_state_sections(text: str, scope: str, errors: list[str]) -> None:
593
+ """The brief's end state: expected behavior, preserved behavior, expected outcome.
594
+
595
+ `## External Gates` is checked here too, in every variant: it is where an
596
+ item with no observation method goes, so the admissible set and its overflow
597
+ have to be required by the same check or the overflow silently disappears.
598
+ """
599
+ _check_gate_section(text, errors)
600
+ seen: set[str] = set()
601
+ for heading, prefix in _END_STATE_SECTIONS:
602
+ _check_one_end_state_section(text, heading, prefix, scope, seen, errors)
603
+ if scope != "codebase":
604
+ _check_end_state_declared(text, errors)
460
605
 
461
606
 
462
607
  # The variant-independent rows of the skill's "Required sections by variant"
463
- # table (okstra-brief-gen SKILL.md). Desired Outcome and the Acceptance
464
- # Criteria / External Gates split have dedicated checks with richer messages;
608
+ # table (okstra-brief-gen SKILL.md). Desired Outcome and the end-state
609
+ # sections / External Gates have dedicated checks with richer messages;
465
610
  # Scan Scope / Priority Lenses are codebase-only (check_codebase_scope);
466
611
  # Source Material / Problem / Symptom are omitted by the codebase variant, so
467
612
  # they are enforced only off it.
@@ -563,7 +708,7 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
563
708
  check_related_task_graph(text, errors)
564
709
 
565
710
  check_requirement_section(text, errors)
566
- check_scope_split_sections(text, errors)
711
+ check_end_state_sections(text, scope, errors)
567
712
  check_variant_required_sections(text, scope, errors)
568
713
 
569
714
  # 2. brief-id matches filename stem