okstra 0.189.0 → 0.189.2

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 (43) hide show
  1. package/dist/cli-registry.mjs +6 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/docs/architecture/storage-model.md +9 -0
  4. package/docs/architecture.md +2 -0
  5. package/docs/cli.md +4 -1
  6. package/docs/for-ai/skills/okstra-brief-gen.md +2 -0
  7. package/docs/project-structure-overview.md +3 -1
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/prompts/duties/direction-selection-worker.md +2 -2
  11. package/runtime/prompts/lead/report-writer.md +1 -1
  12. package/runtime/prompts/profiles/_clarification-recommendation.md +1 -1
  13. package/runtime/prompts/profiles/_common-contract.md +1 -0
  14. package/runtime/prompts/profiles/implementation-option-selection.md +2 -2
  15. package/runtime/python/okstra_ctl/analysis_packet.py +38 -0
  16. package/runtime/python/okstra_ctl/approval_decisions.py +85 -0
  17. package/runtime/python/okstra_ctl/group_context.py +223 -0
  18. package/runtime/python/okstra_ctl/implementation_options.py +35 -0
  19. package/runtime/python/okstra_ctl/model_io/renderers.py +12 -7
  20. package/runtime/python/okstra_ctl/recap.py +7 -1
  21. package/runtime/python/okstra_ctl/render.py +13 -0
  22. package/runtime/python/okstra_ctl/report_html/common.py +60 -4
  23. package/runtime/python/okstra_ctl/report_html/context_links.py +137 -0
  24. package/runtime/python/okstra_ctl/report_html/filters.py +22 -20
  25. package/runtime/python/okstra_ctl/report_html/render.py +18 -5
  26. package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +7 -0
  27. package/runtime/python/okstra_ctl/report_synthesis_packet.py +5 -0
  28. package/runtime/python/okstra_ctl/report_translation.py +8 -0
  29. package/runtime/python/okstra_ctl/run.py +28 -0
  30. package/runtime/python/okstra_ctl/scope_provenance.py +28 -6
  31. package/runtime/python/okstra_ctl/timeline_runs.py +71 -0
  32. package/runtime/python/okstra_ctl/wizard.py +2 -1
  33. package/runtime/skills/okstra-brief-gen/SKILL.md +29 -1
  34. package/runtime/skills/okstra-inspect/facets/history.md +1 -0
  35. package/runtime/skills/okstra-inspect/facets/recap.md +1 -1
  36. package/runtime/templates/reports/group-context.template.md +31 -0
  37. package/runtime/templates/reports/html/assets/base.css +10 -0
  38. package/runtime/templates/reports/html/base.template.html +10 -0
  39. package/runtime/templates/reports/html/i18n/en.json +71 -2
  40. package/runtime/templates/reports/html/i18n/ko.json +71 -2
  41. package/runtime/templates/reports/html/macros/forms.html +1 -0
  42. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +55 -11
  43. package/runtime/validators/validate-brief.py +9 -0
@@ -89,6 +89,45 @@
89
89
  "missing-information": "Missing information",
90
90
  "follow-up": "Follow-up",
91
91
  "plan-item": "Plan item"
92
+ },
93
+ "optionCriterion": {
94
+ "requirement-fit": "Requirement fit",
95
+ "architecture-fit": "Architecture fit",
96
+ "change-locality": "Change locality",
97
+ "implementation-complexity": "Implementation complexity",
98
+ "correctness-risk": "Correctness risk",
99
+ "reversibility": "Reversibility",
100
+ "verification-cost": "Verification cost",
101
+ "rollout-cost": "Rollout cost"
102
+ },
103
+ "feasibilityVerdict": {
104
+ "feasible": "Feasible",
105
+ "not-feasible": "Not feasible",
106
+ "uncertain": "Uncertain"
107
+ },
108
+ "commitmentKind": {
109
+ "user-behavior": "User behavior",
110
+ "code-change": "Code change",
111
+ "migration": "Migration",
112
+ "operational-change": "Operational change",
113
+ "preserved-behavior": "Preserved behavior",
114
+ "derived-work": "Derived work"
115
+ },
116
+ "coverageVerdict": {
117
+ "exact": "exact",
118
+ "under": "under",
119
+ "over": "over",
120
+ "contradicted": "contradicted"
121
+ },
122
+ "safetyCategory": {
123
+ "data-loss": "Data loss",
124
+ "security-violation": "Security violation",
125
+ "irreversible-migration": "Irreversible migration"
126
+ },
127
+ "briefSection": {
128
+ "Expected Behavior": "Expected behavior",
129
+ "Preserved Behavior": "Preserved behavior",
130
+ "Expected Outcome": "Expected outcome"
92
131
  }
93
132
  },
94
133
  "enumHint": {
@@ -122,7 +161,11 @@
122
161
  "dismiss": "Dismiss",
123
162
  "export-downloads": "Export downloads",
124
163
  "drop-that-file-into": ". Drop that file into",
125
- "and-the-next-run-picks-your-answers-up-on-it": "and the next run picks your answers up on its own."
164
+ "and-the-next-run-picks-your-answers-up-on-it": "and the next run picks your answers up on its own.",
165
+ "brief-end-states": "End states the brief pinned",
166
+ "brief-end-states-intro": "The requirement ids this report cites (EB, PB, EO) name these brief items, quoted verbatim from the task brief.",
167
+ "brief-section": "Section",
168
+ "brief-statement": "Statement"
126
169
  },
127
170
  "runUsage": {
128
171
  "heading": "What this run cost",
@@ -342,7 +385,33 @@
342
385
  "option-id": "Option ID",
343
386
  "direction": "Direction",
344
387
  "confirmation-evidence": "Confirmation evidence",
345
- "citation": "Citation"
388
+ "citation": "Citation",
389
+ "upstream-decisions": "Upstream decisions:",
390
+ "evaluation-criteria": "Evaluation criteria",
391
+ "evaluation-criteria-intro": "Every candidate is scored 1 to 5 on each criterion. The weighted score is the weight-averaged result, so a criterion with weight 5 counts five times as much as one with weight 1.",
392
+ "criterion": "Criterion",
393
+ "weight": "Weight",
394
+ "score": "Score",
395
+ "why-it-matters": "Why it matters",
396
+ "what-changes": "What this direction changes",
397
+ "goal": "Goal:",
398
+ "goal-column": "Goal",
399
+ "change-areas": "Expected change areas",
400
+ "commitment": "Commitment",
401
+ "commitment-body": "What the direction commits to",
402
+ "requirements": "Requirements",
403
+ "how-it-works": "How it is done",
404
+ "boundaries": "Architecture boundaries it keeps",
405
+ "why-this-direction": "Why this direction",
406
+ "feasibility-votes": "Feasibility by worker",
407
+ "worker": "Worker",
408
+ "verdict": "Verdict",
409
+ "counterevidence": "Strongest counterevidence",
410
+ "safety-blockers": "Safety blockers",
411
+ "unresolved-facts": "Unresolved feasibility facts",
412
+ "expected-outcome": "Expected outcome",
413
+ "coverage-line": "Covers {covered} of {total} requirements ({verdict}).",
414
+ "planning-invariants": "Must hold in planning"
346
415
  },
347
416
  "implementation-planning": {
348
417
  "what-the-plan-is-for": "What the plan is for",
@@ -89,6 +89,45 @@
89
89
  "missing-information": "빠진 정보",
90
90
  "follow-up": "후속 작업",
91
91
  "plan-item": "계획 항목"
92
+ },
93
+ "optionCriterion": {
94
+ "requirement-fit": "요구사항 적합",
95
+ "architecture-fit": "아키텍처 적합",
96
+ "change-locality": "변경 국소성",
97
+ "implementation-complexity": "구현 복잡도",
98
+ "correctness-risk": "정확성 위험",
99
+ "reversibility": "되돌리기 쉬움",
100
+ "verification-cost": "검증 비용",
101
+ "rollout-cost": "배포 비용"
102
+ },
103
+ "feasibilityVerdict": {
104
+ "feasible": "실행 가능",
105
+ "not-feasible": "실행 불가",
106
+ "uncertain": "불확실"
107
+ },
108
+ "commitmentKind": {
109
+ "user-behavior": "사용자 동작",
110
+ "code-change": "코드 변경",
111
+ "migration": "마이그레이션",
112
+ "operational-change": "운영 변경",
113
+ "preserved-behavior": "보존 동작",
114
+ "derived-work": "파생 작업"
115
+ },
116
+ "coverageVerdict": {
117
+ "exact": "정확히 일치",
118
+ "under": "미달",
119
+ "over": "초과",
120
+ "contradicted": "모순"
121
+ },
122
+ "safetyCategory": {
123
+ "data-loss": "데이터 손실",
124
+ "security-violation": "보안 위반",
125
+ "irreversible-migration": "되돌릴 수 없는 마이그레이션"
126
+ },
127
+ "briefSection": {
128
+ "Expected Behavior": "기대 동작",
129
+ "Preserved Behavior": "보존 동작",
130
+ "Expected Outcome": "기대 산출"
92
131
  }
93
132
  },
94
133
  "enumHint": {
@@ -122,7 +161,11 @@
122
161
  "dismiss": "닫기",
123
162
  "export-downloads": "내보내면 다음 파일이 받아집니다",
124
163
  "drop-that-file-into": ". 그 파일을 여기에 두면",
125
- "and-the-next-run-picks-your-answers-up-on-it": "다음 run 이 알아서 답변을 읽어 갑니다."
164
+ "and-the-next-run-picks-your-answers-up-on-it": "다음 run 이 알아서 답변을 읽어 갑니다.",
165
+ "brief-end-states": "브리프가 고정한 최종 상태",
166
+ "brief-end-states-intro": "이 리포트가 인용하는 요구사항 아이디(EB·PB·EO)는 브리프의 다음 항목입니다. 문장은 태스크 브리프 원문 그대로입니다.",
167
+ "brief-section": "절",
168
+ "brief-statement": "내용"
126
169
  },
127
170
  "runUsage": {
128
171
  "heading": "이번 실행에 든 비용",
@@ -342,7 +385,33 @@
342
385
  "option-id": "옵션 아이디",
343
386
  "direction": "방향",
344
387
  "confirmation-evidence": "확인 근거",
345
- "citation": "인용"
388
+ "citation": "인용",
389
+ "upstream-decisions": "선행 결정:",
390
+ "evaluation-criteria": "평가 기준",
391
+ "evaluation-criteria-intro": "모든 후보를 기준마다 1~5점으로 매깁니다. 가중 점수는 가중치를 반영한 평균이라, 가중치 5인 기준은 가중치 1인 기준의 다섯 배로 반영됩니다.",
392
+ "criterion": "기준",
393
+ "weight": "가중치",
394
+ "score": "점수",
395
+ "why-it-matters": "왜 중요한가",
396
+ "what-changes": "무엇을 바꾸는가",
397
+ "goal": "목표:",
398
+ "goal-column": "목표",
399
+ "change-areas": "예상 변경 영역",
400
+ "commitment": "약속",
401
+ "commitment-body": "이 방향이 약속하는 것",
402
+ "requirements": "요구사항",
403
+ "how-it-works": "어떻게 구현하는가",
404
+ "boundaries": "지키는 아키텍처 경계",
405
+ "why-this-direction": "왜 이 방향인가",
406
+ "feasibility-votes": "작업자별 실행 가능성",
407
+ "worker": "작업자",
408
+ "verdict": "판정",
409
+ "counterevidence": "가장 강한 반대 근거",
410
+ "safety-blockers": "안전 차단 요인",
411
+ "unresolved-facts": "미확정 실행 가능성 사실",
412
+ "expected-outcome": "예상 결과",
413
+ "coverage-line": "요구사항 {total}개 중 {covered}개를 충족합니다({verdict}).",
414
+ "planning-invariants": "계획에서 지켜야 할 조건"
346
415
  },
347
416
  "implementation-planning": {
348
417
  "what-the-plan-is-for": "이 계획이 하려는 것",
@@ -47,6 +47,7 @@
47
47
  <fieldset><legend>{{ t('macros.forms.direction') }}</legend>
48
48
  {% for option in state.rankedOptions %}
49
49
  <input id="direction-selection-{{ option.id }}" type="radio" name="direction-selection-option" value="{{ option.id }}" data-option-name="{{ option.name }}" required><label for="direction-selection-{{ option.id }}">{{ option.id | inline_code }} · {{ option.name | inline_code }}{% if option.id == state.recommendedOptionId %} ({{ t('macros.forms.recommended') }}){% endif %}</label>
50
+ <p class="direction-goal">{{ option.goal | inline_code }}</p>
50
51
  {% endfor %}
51
52
  </fieldset>
52
53
  <input id="direction-selection-confirmed" type="checkbox" required><label for="direction-selection-confirmed">{{ t('macros.forms.confirmed') }}</label>
@@ -2,6 +2,51 @@
2
2
  {% from "html/macros/layout.html" import narrative as render_narrative, summary_card, row_key %}
3
3
  {% from "html/macros/forms.html" import direction_selection %}
4
4
 
5
+ {# One direction, read top to bottom as what it changes, how, why it ranks
6
+ where it does, and what the reader should expect afterwards. Every block
7
+ is a field the writer already fills; the card used to show the goal line
8
+ and the coverage table and left the mechanism, scores, votes, commitments
9
+ and invariants in the record only. #}
10
+ {% macro option_card(row, rank, criteria) -%}
11
+ <article class="summary-card" id="id-{{ row.id }}">
12
+ <p class="eyebrow">#{{ rank }} · {{ row.id | inline_code }} · {{ t('macros.layout.proposed-by') }} {{ row.proposedBy | inline_code }}</p>
13
+ <h3>{{ row.name | inline_code }}</h3>
14
+ <div class="option-detail">
15
+ <h4>{{ t('tasks.implementation-option-selection.what-changes') }}</h4>
16
+ <p><strong>{{ t('tasks.implementation-option-selection.goal') }}</strong> {{ row.goal | inline_code }}</p>
17
+ <p><strong>{{ t('tasks.implementation-option-selection.change-areas') }}</strong></p>
18
+ <ul>{% for area in row.expectedChangeAreas %}<li>{{ area | inline_code }}</li>{% endfor %}</ul>
19
+ <table data-report-field="implementationOptionSelection.rankedOptions.scopeCommitments"><thead><tr><th>{{ t('tasks.implementation-option-selection.commitment') }}</th><th>{{ t('tasks.implementation-option-selection.commitment-body') }}</th><th>{{ t('tasks.implementation-option-selection.requirements') }}</th></tr></thead><tbody>{% for item in row.scopeCommitments %}<tr>{{ row_key(pairs=[(t('macros.layout.id'), item.id), (t('macros.layout.kind'), item.kind | enum_label('commitmentKind'))]) }}<td>{{ item.commitment | inline_code }}</td><td>{{ item.requirementIds | join(", ") | inline_code }}</td></tr>{% endfor %}</tbody></table>
20
+ </div>
21
+ <div class="option-detail">
22
+ <h4>{{ t('tasks.implementation-option-selection.how-it-works') }}</h4>
23
+ <p>{{ row.coreMechanism | inline_code }}</p>
24
+ <p><strong>{{ t('tasks.implementation-option-selection.boundaries') }}</strong></p>
25
+ <ul>{% for boundary in row.architectureBoundaries %}<li>{{ boundary | inline_code }}</li>{% endfor %}</ul>
26
+ </div>
27
+ <div class="option-detail">
28
+ <h4>{{ t('tasks.implementation-option-selection.why-this-direction') }}</h4>
29
+ <p><strong>{{ t('tasks.implementation-option-selection.weighted-score') }}</strong> {{ row.weightedScore }} / 5</p>
30
+ <table data-report-field="implementationOptionSelection.rankedOptions.criterionScores"><thead><tr><th>{{ t('tasks.implementation-option-selection.criterion') }}</th><th class="figure">{{ t('tasks.implementation-option-selection.weight') }}</th><th class="figure">{{ t('tasks.implementation-option-selection.score') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th></tr></thead><tbody>{% for score in row.criterionScores %}<tr><td>{{ score.criterion | enum_label('optionCriterion') }}</td><td class="figure">{{ criteria.get(score.criterion, '') }}</td><td class="figure">{{ score.score }}</td><td>{{ score.rationale | inline_code }}</td></tr>{% endfor %}</tbody></table>
31
+ <p><strong>{{ t('tasks.implementation-option-selection.feasibility-votes') }}</strong></p>
32
+ <table data-report-field="implementationOptionSelection.rankedOptions.feasibilityVotes"><thead><tr><th>{{ t('tasks.implementation-option-selection.worker') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('tasks.implementation-option-selection.counterevidence') }}</th></tr></thead><tbody>{% for vote in row.feasibilityVotes %}<tr>{{ row_key(pairs=[(t('tasks.implementation-option-selection.worker'), vote.worker)], status_name=t('tasks.implementation-option-selection.verdict'), status_raw=vote.verdict, status_text=vote.verdict | enum_label('feasibilityVerdict')) }}<td>{{ vote.rationale | inline_code }}</td><td>{{ vote.counterevidence | inline_code }}</td></tr>{% endfor %}</tbody></table>
33
+ {% if row.safetyBlockers %}<p><strong>{{ t('tasks.implementation-option-selection.safety-blockers') }}</strong></p>
34
+ <ul>{% for blocker in row.safetyBlockers %}<li><span class="status status-risk">{{ blocker.category | enum_label('safetyCategory') }}</span> {{ blocker.statement | inline_code }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ blocker.evidence | evidence_refs }}</span></li>{% endfor %}</ul>{% endif %}
35
+ {% if row.unresolvedFeasibilityFacts %}<p><strong>{{ t('tasks.implementation-option-selection.unresolved-facts') }}</strong></p>
36
+ <ul>{% for fact in row.unresolvedFeasibilityFacts %}<li>{{ fact.fact | inline_code }} — {{ t('tasks.implementation-option-selection.why-it-matters') }} {{ fact.whyItMatters | inline_code }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ fact.evidence | evidence_refs }}</span></li>{% endfor %}</ul>{% endif %}
37
+ </div>
38
+ <div class="option-detail">
39
+ <h4>{{ t('tasks.implementation-option-selection.expected-outcome') }}</h4>
40
+ <div data-report-field="implementationOptionSelection.rankedOptions.coverageSummary"><p>{{ t('tasks.implementation-option-selection.coverage-line') | replace('{covered}', row.coverageSummary.coveredCount) | replace('{total}', row.coverageSummary.totalCount) | replace('{verdict}', row.coverageSummary.coverageVerdict | enum_label('coverageVerdict')) }}</p>
41
+ <p><strong>{{ t('macros.layout.coverage-percent') }}</strong> <span data-report-metric="coveragePercent">{{ row.coverageSummary.coveragePercent }}</span>% · <strong>{{ t('macros.layout.scope-precision-percent') }}</strong> <span data-report-metric="scopePrecisionPercent">{{ row.coverageSummary.scopePrecisionPercent }}</span>%</p></div>
42
+ <table data-report-field="implementationOptionSelection.rankedOptions.requirementCoverage"><thead><tr><th>{{ t('tasks.implementation-option-selection.requirement') }}</th><th>{{ t('tasks.implementation-option-selection.satisfaction') }}</th><th>{{ t('tasks.implementation-option-selection.verification') }}</th></tr></thead><tbody>{% for coverage in row.requirementCoverage %}<tr>{{ row_key(pairs=[(t('macros.layout.id'), coverage.requirementId), (t('macros.layout.status'), coverage.status)]) }}<td>{{ coverage.satisfaction | inline_code }}</td><td>{{ coverage.expectedVerification | inline_code }}</td></tr>{% endfor %}</tbody></table>
43
+ {% if row.planningInvariants %}<p><strong>{{ t('tasks.implementation-option-selection.planning-invariants') }}</strong></p>
44
+ <ul data-report-field="implementationOptionSelection.rankedOptions.planningInvariants">{% for invariant in row.planningInvariants %}<li>{{ invariant.id | inline_code }} · {{ invariant.statement | inline_code }} ({{ invariant.requirementIds | join(", ") | inline_code }})</li>{% endfor %}</ul>{% endif %}
45
+ </div>
46
+ <p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ row.codeEvidence | evidence_refs }}</p>
47
+ </article>
48
+ {%- endmacro %}
49
+
5
50
  {% block human_content %}
6
51
  <section data-report-section="selection-status">
7
52
  <h2>{{ t('tasks.implementation-option-selection.selection-status') }}</h2>
@@ -21,29 +66,28 @@
21
66
  {{ render_narrative(narrative.comparisonOverview, "implementationOptionSelection.userNarrative.comparisonOverview") }}
22
67
  <p data-report-field="implementationOptionSelection.decisionContext.originalRequirementIds"><strong>{{ t('tasks.implementation-option-selection.requirement-ids') }}</strong> {{ selection.decisionContext.originalRequirementIds | join(", ") | inline_code }}</p>
23
68
  {% if selection.decisionContext.leadingCauseRefs %}<p><strong>{{ t('tasks.implementation-option-selection.leading-causes') }}</strong> {{ selection.decisionContext.leadingCauseRefs | join(", ") | inline_code }}</p>{% endif %}
69
+ {% if selection.decisionContext.upstreamDecisionRefs %}<p><strong>{{ t('tasks.implementation-option-selection.upstream-decisions') }}</strong> {{ selection.decisionContext.upstreamDecisionRefs | join(", ") | inline_code }}</p>{% endif %}
70
+ </section>
71
+
72
+ <section data-report-section="evaluation-criteria" data-report-field="implementationOptionSelection.evaluationCriteria">
73
+ <h2>{{ t('tasks.implementation-option-selection.evaluation-criteria') }}</h2>
74
+ <p class="section-lede">{{ t('tasks.implementation-option-selection.evaluation-criteria-intro') }}</p>
75
+ <table><thead><tr><th>{{ t('tasks.implementation-option-selection.criterion') }}</th><th class="figure">{{ t('tasks.implementation-option-selection.weight') }}</th><th>{{ t('tasks.implementation-option-selection.why-it-matters') }}</th></tr></thead><tbody>{% for criterion in selection.evaluationCriteria %}<tr><td>{{ criterion.criterion | enum_label('optionCriterion') }}</td><td class="figure">{{ criterion.weight }}</td><td>{{ criterion.rationale | inline_code }}</td></tr>{% endfor %}</tbody></table>
24
76
  </section>
25
77
 
26
78
  <section data-report-section="ranked-options" data-report-field="implementationOptionSelection.rankedOptions">
27
79
  <h2>{{ t('tasks.implementation-option-selection.ranked-options') }}</h2>
28
80
  {{ render_narrative(narrative.rankingExplanation, "implementationOptionSelection.userNarrative.rankingExplanation") }}
29
- <div class="summary-grid">{% for row in selection.rankedOptions %}<article class="summary-card" id="id-{{ row.id }}">
30
- <p class="eyebrow">#{{ loop.index }} · {{ row.id | inline_code }}</p>
31
- <h3>{{ row.name | inline_code }}</h3>
32
- <p>{{ row.goal | inline_code }}</p>
33
- <div data-report-field="implementationOptionSelection.rankedOptions.coverageSummary"><p><strong>{{ t('macros.layout.coverage-percent') }}</strong> <span data-report-metric="coveragePercent">{{ row.coverageSummary.coveragePercent }}</span></p>
34
- <p><strong>{{ t('macros.layout.scope-precision-percent') }}</strong> <span data-report-metric="scopePrecisionPercent">{{ row.coverageSummary.scopePrecisionPercent }}</span></p></div>
35
- <p><strong>{{ t('tasks.implementation-option-selection.weighted-score') }}</strong> {{ row.weightedScore }}</p>
36
- <table data-report-field="implementationOptionSelection.rankedOptions.requirementCoverage"><thead><tr><th>{{ t('tasks.implementation-option-selection.requirement') }}</th><th>{{ t('tasks.implementation-option-selection.satisfaction') }}</th><th>{{ t('tasks.implementation-option-selection.verification') }}</th></tr></thead><tbody>{% for coverage in row.requirementCoverage %}<tr>{{ row_key(pairs=[(t('macros.layout.id'), coverage.requirementId), (t('macros.layout.status'), coverage.status)]) }}<td>{{ coverage.satisfaction | inline_code }}</td><td>{{ coverage.expectedVerification | inline_code }}</td></tr>{% endfor %}</tbody></table>
37
- </article>{% else %}<p>{{ t('tasks.implementation-option-selection.no-valid-options') }}</p>{% endfor %}</div>
81
+ <div class="summary-grid">{% for row in selection.rankedOptions %}{{ option_card(row, loop.index, criteriaWeights) }}{% else %}<p>{{ t('tasks.implementation-option-selection.no-valid-options') }}</p>{% endfor %}</div>
38
82
  </section>
39
83
 
40
84
  <section data-report-section="recommendation" data-report-field="implementationOptionSelection.recommendedOptionId">
41
85
  <h2>{{ t('tasks.implementation-option-selection.recommended-direction') }}</h2>
42
- {% if recommendedOption %}{{ summary_card(recommendedOption.id ~ " · " ~ recommendedOption.name, recommendedOption.coreMechanism, "important") }}{% else %}<p>{{ t('tasks.implementation-option-selection.no-recommendation') }}</p>{% endif %}
86
+ {% if recommendedOption %}{{ summary_card(recommendedOption.id ~ " · " ~ recommendedOption.name, recommendedOption.goal ~ " " ~ recommendedOption.coreMechanism, "important") }}{% else %}<p>{{ t('tasks.implementation-option-selection.no-recommendation') }}</p>{% endif %}
43
87
  </section>
44
88
 
45
89
  <section data-report-section="candidate-audit" data-report-field="implementationOptionSelection.candidateAudit">
46
90
  <h2>{{ t('tasks.implementation-option-selection.candidate-audit') }}</h2>
47
- <table><thead><tr><th>{{ t('tasks.implementation-option-selection.candidate') }}</th><th>{{ t('tasks.implementation-option-selection.disposition') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('macros.layout.coverage-percent') }}</th><th>{{ t('macros.layout.scope-precision-percent') }}</th></tr></thead><tbody>{% for row in selection.candidateAudit %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id), (t('macros.layout.proposed-by'), row.proposedBy)]) }}<td>{{ row.disposition }}</td><td>{{ row.reason | inline_code }}</td><td>{{ row.coverageSummary.coveragePercent }}</td><td>{{ row.coverageSummary.scopePrecisionPercent }}</td></tr>{% else %}<tr><td colspan="6">{{ t('tasks.implementation-option-selection.no-audit-entries') }}</td></tr>{% endfor %}</tbody></table>
91
+ <table><thead><tr><th>{{ t('tasks.implementation-option-selection.candidate') }}</th><th>{{ t('tasks.implementation-option-selection.goal-column') }}</th><th>{{ t('tasks.implementation-option-selection.disposition') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('macros.layout.coverage-percent') }}</th><th>{{ t('macros.layout.scope-precision-percent') }}</th></tr></thead><tbody>{% for row in selection.candidateAudit %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id), (t('macros.layout.proposed-by'), row.proposedBy)]) }}<td>{{ row.goal | inline_code }}</td><td>{{ row.disposition }}{% if row.mergedInto %} → {{ row.mergedInto | inline_code }}{% endif %}</td><td>{{ row.reason | inline_code }}</td><td>{{ row.coverageSummary.coveragePercent }}</td><td>{{ row.coverageSummary.scopePrecisionPercent }}</td></tr>{% else %}<tr><td colspan="6">{{ t('tasks.implementation-option-selection.no-audit-entries') }}</td></tr>{% endfor %}</tbody></table>
48
92
  </section>
49
93
  {% endblock %}
@@ -42,6 +42,11 @@ Checks performed per brief file:
42
42
  3-digit id and an `— verify: <how>` observation method, EB/EO are rejected
43
43
  in the codebase-scan variant, and `## External Gates` — the destination for
44
44
  an item with no observation method — is required in every variant.
45
+ 15. A file whose frontmatter `type` is `group-context` is the task-group's
46
+ context document (`<briefs>/<task-group>/group-context.md`), not a brief:
47
+ it is checked by `okstra_ctl.group_context.validate_group_context` (four
48
+ required sections, no template placeholder left, directory slug matches
49
+ the frontmatter `task-group`). Any other `type` is a failure.
45
50
 
46
51
  Exit code 0 on PASS, 1 on FAIL.
47
52
  """
@@ -66,6 +71,7 @@ from okstra_ctl.improvement_lenses import (
66
71
  MAX_PRIORITY_LENSES,
67
72
  MIN_PRIORITY_LENSES,
68
73
  )
74
+ from okstra_ctl.group_context import GROUP_CONTEXT_TYPE, validate_group_context
69
75
  from okstra_ctl.ids import slugify_task_segment
70
76
 
71
77
  REQUIRED_FRONTMATTER_KEYS = {
@@ -677,6 +683,9 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
677
683
  except ValueError as exc:
678
684
  return [f"frontmatter: {exc}"]
679
685
 
686
+ if fm.get("type") == GROUP_CONTEXT_TYPE:
687
+ return validate_group_context(path, briefs_root)
688
+
680
689
  missing = REQUIRED_FRONTMATTER_KEYS - fm.keys()
681
690
  if missing:
682
691
  errors.append(f"frontmatter missing keys: {sorted(missing)}")