okstra 0.146.1 → 0.148.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 (137) hide show
  1. package/README.md +23 -9
  2. package/docs/architecture/storage-model.md +39 -65
  3. package/docs/architecture.md +68 -60
  4. package/docs/cli.md +40 -23
  5. package/docs/for-ai/skills/okstra-run.md +13 -34
  6. package/docs/performance-improvement-plan-v2.md +2 -2
  7. package/docs/pr-template-usage.md +1 -1
  8. package/docs/project-structure-overview.md +26 -22
  9. package/docs/task-process/README.md +4 -4
  10. package/docs/task-process/common-flow.md +12 -12
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation.md +1 -1
  13. package/docs/task-process/release-handoff.md +1 -1
  14. package/package.json +2 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/agents/workers/antigravity-worker.md +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/codex-worker.md +2 -2
  19. package/runtime/agents/workers/grok-worker.md +256 -0
  20. package/runtime/agents/workers/kimi-worker.md +256 -0
  21. package/runtime/agents/workers/report-writer-worker.md +12 -12
  22. package/runtime/bin/lib/okstra/cli.sh +13 -1
  23. package/runtime/bin/lib/okstra/globals.sh +3 -0
  24. package/runtime/bin/lib/okstra/usage.sh +17 -12
  25. package/runtime/bin/okstra-grok-exec.sh +5 -0
  26. package/runtime/bin/okstra-kimi-exec.sh +5 -0
  27. package/runtime/bin/okstra-provider-exec.py +235 -0
  28. package/runtime/bin/okstra-render-final-report.py +4 -4
  29. package/runtime/bin/okstra-render-report-views.py +100 -12
  30. package/runtime/bin/okstra.sh +3 -0
  31. package/runtime/prompts/lead/adapters/antigravity.md +48 -0
  32. package/runtime/prompts/lead/adapters/claude-code.md +13 -11
  33. package/runtime/prompts/lead/adapters/codex.md +7 -7
  34. package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
  35. package/runtime/prompts/lead/report-writer.md +16 -12
  36. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  37. package/runtime/prompts/profiles/_common-contract.md +16 -10
  38. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  39. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  40. package/runtime/prompts/profiles/_implementation-executor.md +12 -12
  41. package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
  42. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  43. package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
  44. package/runtime/prompts/profiles/error-analysis.md +2 -0
  45. package/runtime/prompts/profiles/feature-analysis.md +2 -0
  46. package/runtime/prompts/profiles/final-verification.md +3 -1
  47. package/runtime/prompts/profiles/forbidden-actions.json +4 -4
  48. package/runtime/prompts/profiles/implementation-planning.md +3 -1
  49. package/runtime/prompts/profiles/implementation.md +2 -2
  50. package/runtime/prompts/profiles/improvement-discovery.md +6 -2
  51. package/runtime/prompts/profiles/project-analysis.md +2 -0
  52. package/runtime/prompts/profiles/release-handoff.md +7 -7
  53. package/runtime/prompts/profiles/requirements-discovery.md +2 -0
  54. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  55. package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
  56. package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
  57. package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
  58. package/runtime/python/okstra_ctl/lead_events.py +1 -1
  59. package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
  60. package/runtime/python/okstra_ctl/models.py +156 -8
  61. package/runtime/python/okstra_ctl/path_hints.py +9 -25
  62. package/runtime/python/okstra_ctl/paths.py +1 -1
  63. package/runtime/python/okstra_ctl/render.py +172 -74
  64. package/runtime/python/okstra_ctl/render_final_report.py +136 -28
  65. package/runtime/python/okstra_ctl/report_contract.py +124 -0
  66. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  67. package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
  68. package/runtime/python/okstra_ctl/report_html/common.py +86 -0
  69. package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
  70. package/runtime/python/okstra_ctl/report_html/models.py +59 -0
  71. package/runtime/python/okstra_ctl/report_html/render.py +76 -0
  72. package/runtime/python/okstra_ctl/report_html/router.py +40 -0
  73. package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
  74. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
  75. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
  76. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
  77. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
  78. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
  79. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
  80. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
  81. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
  82. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
  83. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
  84. package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
  85. package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
  86. package/runtime/python/okstra_ctl/report_views.py +15 -43
  87. package/runtime/python/okstra_ctl/run.py +276 -51
  88. package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
  89. package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
  90. package/runtime/python/okstra_ctl/team.py +2 -7
  91. package/runtime/python/okstra_ctl/wizard.py +194 -21
  92. package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
  93. package/runtime/python/okstra_ctl/workers.py +3 -1
  94. package/runtime/python/okstra_ctl/workflow.py +4 -2
  95. package/runtime/python/okstra_token_usage/__init__.py +1 -0
  96. package/runtime/python/okstra_token_usage/collect.py +32 -23
  97. package/runtime/python/okstra_token_usage/pricing.py +35 -3
  98. package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
  99. package/runtime/skills/okstra-run/SKILL.md +31 -42
  100. package/runtime/templates/prd/pr-body.template.md +1 -1
  101. package/runtime/templates/reports/final-report-v2.template.md +66 -0
  102. package/runtime/templates/reports/html/assets/base.css +41 -0
  103. package/runtime/templates/reports/html/assets/base.js +5 -0
  104. package/runtime/templates/reports/html/base.template.html +79 -0
  105. package/runtime/templates/reports/html/macros/forms.html +47 -0
  106. package/runtime/templates/reports/html/macros/layout.html +19 -0
  107. package/runtime/templates/reports/html/macros/visualizations.html +27 -0
  108. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
  109. package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
  110. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
  111. package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
  112. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
  113. package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
  114. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
  115. package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
  116. package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
  117. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
  118. package/runtime/templates/reports/report.js +21 -4
  119. package/runtime/templates/reports/settings.template.json +4 -0
  120. package/runtime/templates/reports/task-brief.template.md +7 -7
  121. package/runtime/validators/validate-report-views.py +86 -4
  122. package/runtime/validators/validate-run.py +73 -15
  123. package/runtime/validators/validate_improvement_report.py +55 -0
  124. package/runtime/validators/validate_session_conformance.py +2 -1
  125. package/src/cli-registry.mjs +4 -4
  126. package/src/commands/execute/codex-dispatch.mjs +7 -10
  127. package/src/commands/execute/render-bundle.mjs +3 -3
  128. package/src/commands/execute/run.mjs +17 -52
  129. package/src/commands/execute/wizard.mjs +4 -1
  130. package/src/commands/lifecycle/doctor.mjs +6 -3
  131. package/src/commands/lifecycle/install.mjs +49 -21
  132. package/src/commands/report/finalize.mjs +2 -3
  133. package/src/commands/report/render-final-report.mjs +4 -2
  134. package/src/commands/report/render-views.mjs +8 -8
  135. package/src/lib/runtime-manifest.mjs +1 -1
  136. package/src/lib/runtime-resolver.mjs +2 -2
  137. package/src/lib/worker-agent-render.mjs +50 -0
@@ -0,0 +1,39 @@
1
+ {% extends "html/base.template.html" %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
3
+ {% from "html/macros/visualizations.html" import figure %}
4
+
5
+ {% block human_content %}
6
+ <section data-report-section="verification-verdict">
7
+ <h2>검증 판정</h2>
8
+ <p><span class="status status-{{ verdict.verdictToken }}">{{ verdict.verdictToken }}</span></p>
9
+ {{ render_narrative(narrative.verdictExplanation, "finalVerification.userNarrative.verdictExplanation", evidenceIndex) }}
10
+ </section>
11
+
12
+ <section data-report-section="requirement-results" data-report-field="finalVerification.validationEvidence">
13
+ <h2>요구사항별 결과</h2>
14
+ {{ render_narrative(narrative.coverageExplanation, "finalVerification.userNarrative.coverageExplanation", evidenceIndex) }}
15
+ {{ figure(coverageFigure) }}
16
+ </section>
17
+
18
+ <section data-report-section="blockers" data-report-field="finalVerification.acceptanceBlockers">
19
+ <h2>수락 차단 항목</h2>
20
+ {{ render_narrative(narrative.blockerExplanation, "finalVerification.userNarrative.blockerExplanation", evidenceIndex) }}
21
+ <div class="summary-grid">{% for row in final.acceptanceBlockers %}{{ summary_card(row.id ~ " · " ~ row.severity, row.statement, "important") }}{% else %}<p>수락을 막는 항목이 없습니다.</p>{% endfor %}</div>
22
+ </section>
23
+
24
+ <section data-report-section="residual-risk" data-report-field="finalVerification.residualRisk">
25
+ <h2>잔여 위험</h2>
26
+ <div class="summary-grid">{% for row in final.residualRisk %}<article class="summary-card"><h3>{{ row.id }}</h3><p>{{ row.item | inline_code }}</p><p><strong>담당:</strong> {{ row.owner | inline_code }}</p><p><strong>상향 조건:</strong> {{ row.escalationTrigger | inline_code }}</p></article>{% else %}<p>기록된 잔여 위험이 없습니다.</p>{% endfor %}</div>
27
+ </section>
28
+
29
+ <section data-report-section="manual-results" data-report-field="finalVerification.manualUserTest">
30
+ <h2>수동 사용자 테스트 결과</h2>
31
+ {% if final.manualUserTest.applicable %}{% for row in final.manualUserTest.results %}<article class="summary-card tone-{{ row.result }}"><h3>{{ row.target | inline_code }} · {{ row.result }}</h3><p>{{ row.performed | inline_code }}</p><p>{{ row.observed | inline_code }}</p></article>{% endfor %}{% else %}<p>{{ final.manualUserTest.exemptionReaffirm | inline_code }}</p>{% endif %}
32
+ </section>
33
+
34
+ <section data-report-section="release-route">
35
+ <h2>릴리스 경로</h2>
36
+ {{ render_narrative(narrative.releaseReadiness, "finalVerification.userNarrative.releaseReadiness", evidenceIndex) }}
37
+ {% if releaseAllowed %}<p>{{ final.routingRecommendation | inline_code }}</p>{% else %}<p>현재 판정으로는 release-handoff를 시작할 수 없습니다. 차단 또는 조건을 먼저 해소하세요.</p>{% endif %}
38
+ </section>
39
+ {% endblock %}
@@ -0,0 +1,47 @@
1
+ {% extends "html/base.template.html" %}
2
+ {% from "html/macros/forms.html" import plan_approval %}
3
+ {% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
4
+ {% from "html/macros/visualizations.html" import figure %}
5
+
6
+ {% block human_content %}
7
+ <section data-report-section="planning-goal">
8
+ <h2>계획 목표</h2>
9
+ <p>{{ humanSummary.outcome | inline_code }}</p>
10
+ <div class="summary-grid">{% for reason in humanSummary.whyItMatters %}{{ summary_card("왜 중요한가 " ~ loop.index, reason, "important") }}{% endfor %}</div>
11
+ </section>
12
+
13
+ <section data-report-section="option-comparison">
14
+ <h2>구현 대안 비교</h2>
15
+ {{ render_narrative(narrative.optionExplanation, "implementationPlanning.userNarrative.optionExplanation", evidenceIndex) }}
16
+ <div class="summary-grid" data-report-field="implementationPlanning.optionCandidates">{% for row in planning.optionCandidates %}<article class="summary-card"><h3>{{ row.name | inline_code }}</h3><p><strong>인터페이스:</strong> {{ row.interfaces | inline_code }}</p><p><strong>영향 범위:</strong> {{ row.blastRadius | inline_code }}</p><ul>{% for file in row.fileStructure %}<li>{{ file.action }} <code>{{ file.path }}</code> — {{ file.summary | inline_code }}</li>{% endfor %}</ul></article>{% endfor %}</div>
17
+ <table data-report-field="implementationPlanning.tradeoffMatrix"><thead><tr><th>옵션</th><th>복잡도</th><th>위험</th><th>가역성</th><th>테스트 비용</th><th>배포 비용</th></tr></thead><tbody>{% for row in planning.tradeoffMatrix %}<tr><td>{{ row.option | inline_code }}</td><td>{{ row.complexity | inline_code }}</td><td>{{ row.risk | inline_code }}</td><td>{{ row.reversibility | inline_code }}</td><td>{{ row.testCoverageCost | inline_code }}</td><td>{{ row.rolloutCost | inline_code }}</td></tr>{% endfor %}</tbody></table>
18
+ </section>
19
+
20
+ <section data-report-section="recommendation" data-report-field="implementationPlanning.recommendedOption">
21
+ <h2>권장안</h2>
22
+ {{ render_narrative(narrative.recommendationExplanation, "implementationPlanning.userNarrative.recommendationExplanation", evidenceIndex) }}
23
+ <article class="summary-card tone-important"><h3>{{ planning.recommendedOption.name | inline_code }}</h3><p>{{ planning.recommendedOption.coreReason | inline_code }}</p><p>{{ planning.recommendedOption.rationale | inline_code }}</p><p><strong>선택하지 않은 안:</strong> {{ planning.recommendedOption.rejectedSummary | inline_code }}</p></article>
24
+ </section>
25
+
26
+ <section data-report-section="stage-map" data-report-field="implementationPlanning.stageMap">
27
+ <h2>단계별 실행 지도</h2>
28
+ {{ render_narrative(narrative.stageStrategy, "implementationPlanning.userNarrative.stageStrategy", evidenceIndex) }}
29
+ {{ figure(stageFigure) }}
30
+ <div class="summary-grid">{% for row in planning.stages %}<article class="summary-card"><h3>Stage {{ row.stage }} · {{ row.title | inline_code }}</h3><p>{{ row.sliceValue | inline_code }}</p><p><strong>완료 조건:</strong> {{ row.acceptance | inline_code }}</p><p><strong>검증:</strong> {{ row.stageValidation | inline_code }}</p></article>{% endfor %}</div>
31
+ </section>
32
+
33
+ <section data-report-section="validation-and-rollback">
34
+ <h2>검증과 롤백</h2>
35
+ {{ render_narrative(narrative.validationAndRollback, "implementationPlanning.userNarrative.validationAndRollback", evidenceIndex) }}
36
+ <h3>검증 체크리스트</h3><table data-report-field="implementationPlanning.validationChecklist"><thead><tr><th>ID</th><th>시점</th><th>확인</th><th>명령</th><th>기대 결과</th></tr></thead><tbody>{% for row in planning.validationChecklist %}<tr><td>{{ row.id }}</td><td>{{ row.phase }}</td><td>{{ row.check | inline_code }}</td><td><code>{{ row.commandOrObservation }}</code></td><td>{{ row.expectedOutcome | inline_code }}</td></tr>{% endfor %}</tbody></table>
37
+ <h3>롤백 전략</h3><div class="summary-grid" data-report-field="implementationPlanning.rollbackStrategy">{% for row in planning.rollbackStrategy %}{{ summary_card(row.id ~ " · " ~ (row.triggerSignal | inline_code), row.action, "important") }}{% endfor %}</div>
38
+ </section>
39
+
40
+ <section data-report-section="open-decisions">
41
+ <h2>승인 전 열린 결정</h2>
42
+ <div class="summary-grid">{% for row in openDecisions %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>승인을 막는 열린 결정이 없습니다.</p>{% endfor %}</div>
43
+ {{ action_list(humanSummary.actions) }}
44
+ </section>
45
+
46
+ {{ plan_approval(approval) }}
47
+ {% endblock %}
@@ -0,0 +1,40 @@
1
+ {% extends "html/base.template.html" %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
3
+ {% from "html/macros/visualizations.html" import figure %}
4
+
5
+ {% block human_content %}
6
+ <section data-report-section="delivered-outcome">
7
+ <h2>전달된 결과</h2>
8
+ {{ render_narrative(narrative.deliveredOutcome, "implementation.userNarrative.deliveredOutcome", evidenceIndex) }}
9
+ <p>{{ humanSummary.outcome | inline_code }}</p>
10
+ </section>
11
+
12
+ <section data-report-section="change-map" data-report-field="implementation.diffSummary">
13
+ <h2>무엇이 달라졌는가</h2>
14
+ {{ render_narrative(narrative.changeExplanation, "implementation.userNarrative.changeExplanation", evidenceIndex) }}
15
+ {{ figure(changeFigure) }}
16
+ </section>
17
+
18
+ <section data-report-section="requirement-coverage">
19
+ <h2>요구사항 충족</h2>
20
+ <table data-report-field="implementation.requirementCoverage"><thead><tr><th>ID</th><th>요구사항</th><th>구현·증거</th><th>상태</th></tr></thead><tbody>{% for row in implementation.requirementCoverage %}<tr><td>{{ row.id }}</td><td>{{ row.requirement | inline_code }}</td><td>{{ row.coveredBy | inline_code }}</td><td><span class="status status-{{ row.status }}">{{ row.status }}</span></td></tr>{% endfor %}</tbody></table>
21
+ </section>
22
+
23
+ <section data-report-section="validation" data-report-field="implementation.validationEvidence">
24
+ <h2>검증 결과</h2>
25
+ {{ render_narrative(narrative.validationExplanation, "implementation.userNarrative.validationExplanation", evidenceIndex) }}
26
+ <div class="summary-grid">{% for row in implementation.validationEvidence %}{{ summary_card(row.phase ~ " · 종료 코드 " ~ row.exitCode, row.outputTail, "important" if row.exitCode else "neutral") }}{% endfor %}</div>
27
+ <p><strong>독립 검증:</strong> {% for row in implementation.verifierResults %}<span class="status status-{{ row.verdict | lower }}">{{ row.verdict }}</span>{% if not loop.last %}, {% endif %}{% endfor %}</p>
28
+ </section>
29
+
30
+ <section data-report-section="remaining-issues">
31
+ <h2>남은 문제</h2>
32
+ {{ render_narrative(narrative.remainingIssues, "implementation.userNarrative.remainingIssues", evidenceIndex) }}
33
+ <div class="summary-grid">{% for row in implementation.outOfPlanEdits %}{{ summary_card(row.id ~ " · " ~ row.file, row.rationale, "important") }}{% else %}<p>계획 밖 수정 사항이 없습니다.</p>{% endfor %}</div>
34
+ </section>
35
+
36
+ <section data-report-section="manual-test" data-report-field="implementation.manualUserTest">
37
+ <h2>수동 사용자 테스트</h2>
38
+ {% if implementation.manualUserTest.applicable %}{% for row in implementation.manualUserTest["items"] %}<article class="summary-card"><h3>{{ row.target | inline_code }}</h3><p><strong>환경:</strong> {{ row.environmentSetup | inline_code }}</p><ol>{% for step in row.steps %}<li>{{ step | inline_code }}</li>{% endfor %}</ol><p><strong>기대 결과:</strong> {{ row.expectedResult | inline_code }}</p></article>{% endfor %}{% else %}<p>{{ implementation.manualUserTest.exemptionReason | inline_code }}</p>{% endif %}
39
+ </section>
40
+ {% endblock %}
@@ -0,0 +1,29 @@
1
+ {% extends "html/base.template.html" %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
3
+ {% from "html/macros/visualizations.html" import figure %}
4
+
5
+ {% block human_content %}
6
+ <section data-report-section="opportunities">
7
+ <h2>발견한 개선 기회</h2>
8
+ {{ render_narrative(narrative.opportunityOverview, "improvementDiscovery.userNarrative.opportunityOverview", evidenceIndex) }}
9
+ <table data-report-field="improvementDiscovery.lensCoverage"><thead><tr><th>렌즈</th><th>결과</th><th>설명</th><th>근거</th></tr></thead><tbody>{% for row in improvement.lensCoverage %}<tr><td>{{ row.lens }}</td><td>{{ row.status }}</td><td>{{ row.rationale | inline_code }}</td><td>{{ row.evidence | evidence_refs(evidenceIndex) }}</td></tr>{% endfor %}</tbody></table>
10
+ </section>
11
+
12
+ <section data-report-section="ranked-candidates">
13
+ <h2>우선순위 후보</h2>
14
+ {{ render_narrative(narrative.prioritizationExplanation, "improvementDiscovery.userNarrative.prioritizationExplanation", evidenceIndex) }}
15
+ <div class="summary-grid" data-report-field="improvementDiscovery.candidates">{% for row in improvement.candidates %}<article class="summary-card tone-{{ row.severity }}"><p class="eyebrow">{{ loop.index }}순위 · {{ row.id }} · 영향 {{ row.severity }} · 노력 {{ row.effort }}</p><h3>{{ row.title | inline_code }}</h3><p>{{ row.expectedBehaviorAfter | inline_code }}</p><p><strong>다음 단계:</strong> {{ row.recommendedNextPhase }}</p><p class="evidence-refs">근거: {{ row.evidence | evidence_refs(evidenceIndex) }}</p></article>{% else %}<p>선택할 개선 후보가 없습니다.</p>{% endfor %}</div>
16
+ </section>
17
+
18
+ <section data-report-section="impact-effort">
19
+ <h2>영향도와 노력도</h2>
20
+ {{ figure(matrixFigure) }}
21
+ </section>
22
+
23
+ <section data-report-section="selection">
24
+ <h2>선택 안내</h2>
25
+ {{ render_narrative(narrative.selectionGuidance, "improvementDiscovery.userNarrative.selectionGuidance", evidenceIndex) }}
26
+ <p><strong>선택 상한:</strong> 최대 {{ improvement.selectionLimit }}개</p>
27
+ <ul>{% for row in improvement.candidates %}<li><label><input type="checkbox" name="improvement-candidate" value="{{ row.id }}"> {{ row.id }} · {{ row.title | inline_code }}</label></li>{% endfor %}</ul>
28
+ </section>
29
+ {% endblock %}
@@ -0,0 +1,57 @@
1
+ {% extends "html/base.template.html" %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
3
+ {% from "html/macros/visualizations.html" import figure %}
4
+
5
+ {% block human_content %}
6
+ <section data-report-section="project-overview">
7
+ <h2>프로젝트 개요</h2>
8
+ {{ render_narrative(narrative.projectOverview, "projectAnalysis.userNarrative.projectOverview", evidenceIndex) }}
9
+ <div class="summary-grid">
10
+ {% for reason in humanSummary.whyItMatters %}{{ summary_card("왜 중요한가 " ~ loop.index, reason, "important") }}{% endfor %}
11
+ </div>
12
+ </section>
13
+
14
+ <section data-report-section="architecture" data-report-field="projectAnalysis.components">
15
+ <h2>아키텍처와 의존성</h2>
16
+ {{ render_narrative(narrative.architectureExplanation, "projectAnalysis.userNarrative.architectureExplanation", evidenceIndex) }}
17
+ {{ figure(architectureFigure) }}
18
+ </section>
19
+
20
+ <section data-report-section="boundaries">
21
+ <h2>시스템 경계</h2>
22
+ <div class="summary-grid" data-report-field="projectAnalysis.entryPoints">
23
+ {% for row in project.entryPoints %}{{ summary_card(row.symbol, row.role) }}{% endfor %}
24
+ </div>
25
+ <h3>데이터 저장소</h3>
26
+ <ul data-report-field="projectAnalysis.dataStores">{% for row in project.dataStores %}<li><strong>{{ row.name | inline_code }}</strong> — {{ row.readBoundary | inline_code }} / {{ row.writeBoundary | inline_code }}</li>{% endfor %}</ul>
27
+ <h3>외부 시스템</h3>
28
+ <ul data-report-field="projectAnalysis.externalSystems">{% for row in project.externalSystems %}<li><strong>{{ row.name | inline_code }}</strong> — {{ row.adapter | inline_code }} ({{ row.direction | inline_code }})</li>{% endfor %}</ul>
29
+ </section>
30
+
31
+ <section data-report-section="hotspots" data-report-field="projectAnalysis.rankedHotspots">
32
+ <h2>우선 검토할 핫스팟</h2>
33
+ {{ render_narrative(narrative.hotspotExplanation, "projectAnalysis.userNarrative.hotspotExplanation", evidenceIndex) }}
34
+ <div class="summary-grid">
35
+ {% for row in project.rankedHotspots %}
36
+ <article class="summary-card tone-{{ row.risk }}"><p class="eyebrow">{{ row.id }} · 영향 {{ row.impact }} · 위험 {{ row.risk }}</p><h3>{{ row.title | inline_code }}</h3><p>{{ row.reason | inline_code }}</p><p class="evidence-refs">근거: {{ row.evidenceRefs | evidence_refs(evidenceIndex) }}</p></article>
37
+ {% endfor %}
38
+ </div>
39
+ </section>
40
+
41
+ <section data-report-section="quality-and-tests" data-report-field="projectAnalysis.qualityCoverage">
42
+ <h2>품질과 테스트 상태</h2>
43
+ {{ render_narrative(narrative.qualityExplanation, "projectAnalysis.userNarrative.qualityExplanation", evidenceIndex) }}
44
+ <table><thead><tr><th>영역</th><th>상태</th><th>설명</th><th>근거</th></tr></thead><tbody>{% for row in project.qualityCoverage %}<tr><td>{{ row.area | inline_code }}</td><td><span class="status status-{{ row.status }}">{{ row.status }}</span></td><td>{{ row.summary | inline_code }}</td><td>{{ row.evidenceRefs | evidence_refs(evidenceIndex) }}</td></tr>{% endfor %}</tbody></table>
45
+ </section>
46
+
47
+ <section data-report-section="feature-map" data-report-field="projectAnalysis.featureIndex">
48
+ <h2>기능 지도</h2>
49
+ <div class="summary-grid">{% for row in project.featureIndex %}{{ summary_card(row.id ~ " · " ~ (row.name | inline_code), row.description) }}{% endfor %}</div>
50
+ </section>
51
+
52
+ <section data-report-section="user-actions">
53
+ <h2>다음 행동</h2>
54
+ {{ action_list(humanSummary.actions) }}
55
+ {% if humanSummary.blockers %}<h3>차단 사항</h3><ul>{% for item in humanSummary.blockers %}<li>{{ item | inline_code }}</li>{% endfor %}</ul>{% endif %}
56
+ </section>
57
+ {% endblock %}
@@ -0,0 +1,36 @@
1
+ {% extends "html/base.template.html" %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
3
+ {% from "html/macros/visualizations.html" import figure %}
4
+
5
+ {% block human_content %}
6
+ <section data-report-section="handoff-outcome">
7
+ <h2>인계 결과</h2>
8
+ {{ render_narrative(narrative.handoffOutcome, "releaseHandoff.userNarrative.handoffOutcome", evidenceIndex) }}
9
+ {{ figure(handoffFigure) }}
10
+ </section>
11
+
12
+ <section data-report-section="user-selection" data-report-field="releaseHandoff.userSelections">
13
+ <h2>사용자가 선택한 동작</h2>
14
+ <dl><dt>인계 방식</dt><dd>{{ handoff.userSelections.h1 }}</dd><dt>충돌 확인</dt><dd>{{ handoff.userSelections.h2b }}</dd><dt>PR 본문</dt><dd>{{ handoff.userSelections.h3 }}</dd></dl>
15
+ </section>
16
+
17
+ <section data-report-section="branch-and-pr" data-report-field="releaseHandoff.featureBranchState">
18
+ <h2>브랜치와 PR</h2>
19
+ {{ render_narrative(narrative.branchAndPrExplanation, "releaseHandoff.userNarrative.branchAndPrExplanation", evidenceIndex) }}
20
+ <p><strong>브랜치:</strong> {{ handoff.featureBranchState.branchName }}</p>
21
+ <p data-report-field="releaseHandoff.pullRequestOutcome"><strong>PR 결과:</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">Pull request 열기</a>{% endif %}</p>
22
+ </section>
23
+
24
+ <section data-report-section="conflicts" data-report-field="releaseHandoff.mergeConflictProbe">
25
+ <h2>충돌 판정</h2>
26
+ {{ render_narrative(narrative.conflictExplanation, "releaseHandoff.userNarrative.conflictExplanation", evidenceIndex) }}
27
+ {{ summary_card(handoff.mergeConflictProbe.kind, "기준 브랜치: " ~ (handoff.mergeConflictProbe.baseBranch | default('not-run'))) }}
28
+ {% if handoff.mergeConflictProbe.conflictingPaths %}<ul>{% for path in handoff.mergeConflictProbe.conflictingPaths %}<li>{{ path }}</li>{% endfor %}</ul>{% endif %}
29
+ </section>
30
+
31
+ <section data-report-section="next-action">
32
+ <h2>다음 행동</h2>
33
+ {{ render_narrative(narrative.nextAction, "releaseHandoff.userNarrative.nextAction", evidenceIndex) }}
34
+ <p>{{ handoff.routingRecommendation | inline_code }}</p>
35
+ </section>
36
+ {% endblock %}
@@ -0,0 +1,37 @@
1
+ {% extends "html/base.template.html" %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
3
+ {% from "html/macros/visualizations.html" import figure %}
4
+
5
+ {% block human_content %}
6
+ <section data-report-section="request">
7
+ <h2>요청 이해</h2>
8
+ {{ render_narrative(narrative.requestUnderstanding, "requirementsDiscovery.userNarrative.requestUnderstanding", evidenceIndex) }}
9
+ <h3>사용자 요청 원문</h3>
10
+ <p data-report-field="requirementsDiscovery.requestVerbatim">{{ requirements.requestVerbatim | inline_code }}</p>
11
+ <h3>시스템 해석</h3>
12
+ <p data-report-field="requirementsDiscovery.systemInterpretation">{{ requirements.systemInterpretation | inline_code }}</p>
13
+ <dl data-report-field="requirementsDiscovery.classification">{% for key, value in requirements.classification.items() %}<dt>{{ key }}</dt><dd>{{ value | inline_code }}</dd>{% endfor %}</dl>
14
+ </section>
15
+
16
+ <section data-report-section="requirements">
17
+ <h2>확정 요구사항과 미확정 요구사항</h2>
18
+ {{ render_narrative(narrative.confirmedRequirements, "requirementsDiscovery.userNarrative.confirmedRequirements", evidenceIndex) }}
19
+ <div class="summary-grid" data-report-field="requirementsDiscovery.confirmedRequirements">{% for row in requirements.confirmedRequirements %}{{ summary_card(row.id ~ " · 확정", row.statement) }}{% endfor %}</div>
20
+ {{ render_narrative(narrative.unresolvedRequirements, "requirementsDiscovery.userNarrative.unresolvedRequirements", evidenceIndex) }}
21
+ <div class="summary-grid" data-report-field="requirementsDiscovery.unresolvedRequirements">{% for row in requirements.unresolvedRequirements %}{{ summary_card(row.id ~ " · 결정 필요", row.statement, "important") }}{% else %}<p>미확정 요구사항이 없습니다.</p>{% endfor %}</div>
22
+ </section>
23
+
24
+ <section data-report-section="routing">
25
+ <h2>다음 태스크와 의존성</h2>
26
+ {{ render_narrative(narrative.routingExplanation, "requirementsDiscovery.userNarrative.routingExplanation", evidenceIndex) }}
27
+ <p><strong>다음 태스크:</strong> {{ requirements.routing.nextTaskType }} · {{ requirements.routing.rationale | inline_code }}</p>
28
+ {{ figure(routingFigure) }}
29
+ {% if not requirements.taskDependencies %}<p class="empty-state">이 요구사항 태스크는 다른 태스크에 의존하지 않는 단일 작업입니다.</p>{% endif %}
30
+ </section>
31
+
32
+ <section data-report-section="decisions">
33
+ <h2>사용자가 결정할 항목</h2>
34
+ <div class="summary-grid">{% for row in clarificationItems %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>추가 결정이 필요하지 않습니다.</p>{% endfor %}</div>
35
+ {{ action_list(humanSummary.actions) }}
36
+ </section>
37
+ {% endblock %}
@@ -249,6 +249,16 @@
249
249
  setTimeout(function () { URL.revokeObjectURL(url); }, 0);
250
250
  }
251
251
 
252
+ // The output element lives in the footer, many screens below the header
253
+ // export button, so writing text into it alone reads as "nothing happened".
254
+ function showOutput(out, text) {
255
+ if (!out) return;
256
+ out.textContent = text;
257
+ out.setAttribute("tabindex", "-1");
258
+ out.scrollIntoView({ block: "center" });
259
+ out.focus();
260
+ }
261
+
252
262
  function exportUserResponse() {
253
263
  var runMeta = readRunMeta();
254
264
  var entries = collectEntries();
@@ -258,14 +268,14 @@
258
268
  try {
259
269
  analysisReview = collectAnalysisReview();
260
270
  } catch (e) {
261
- if (out) out.textContent = e.message || String(e);
271
+ showOutput(out, e.message || String(e));
262
272
  showDismiss(true);
263
273
  return "";
264
274
  }
265
275
  var md = buildUserResponseMarkdown(
266
276
  runMeta, entries, isoNowUtc(), approval, analysisReview
267
277
  );
268
- if (out) out.textContent = md;
278
+ showOutput(out, md);
269
279
  showDismiss(true);
270
280
  // Nothing answered yet — show the empty serialisation as feedback but
271
281
  // don't download a useless file.
@@ -315,7 +325,10 @@
315
325
  if (/Reader Summary|Verdict Card|Clarification Items|Recommended Next Steps|Follow-up Tasks/.test(text)) {
316
326
  return "action";
317
327
  }
318
- if (/Evidence|Cross Verification|Execution Status|Token Usage|Plan Body Verification|Round History/.test(text)) {
328
+ // "토큰 사용량" is tokenSummary.heading from templates/reports/i18n/ko.json —
329
+ // the only audit heading the ko dictionary translates away from its English
330
+ // form. tests/report/test_report_views.py pins the two in sync.
331
+ if (/Evidence|Cross Verification|Execution Status|Token Usage|토큰 사용량|Plan Body Verification|Round History/.test(text)) {
319
332
  return "audit";
320
333
  }
321
334
  return "detail";
@@ -333,7 +346,11 @@
333
346
  current = "action";
334
347
  continue;
335
348
  }
336
- if (/^H[12]$/.test(node.tagName || "")) {
349
+ // Only h2 opens a reader section. The single h1 is the report title, and
350
+ // the run-metadata list and table of contents that trail it are document
351
+ // chrome — classifying them would hide the title in action mode, because
352
+ // the title text ("… Cross Verification Final Report") reads as audit.
353
+ if ((node.tagName || "") === "H2") {
337
354
  current = readerSectionKind(node.textContent || "");
338
355
  }
339
356
  node.setAttribute("data-reader-section", current);
@@ -25,6 +25,10 @@
25
25
  "Bash(agy:*)",
26
26
  "Bash(claude)",
27
27
  "Bash(claude:*)",
28
+ "Bash(grok)",
29
+ "Bash(grok:*)",
30
+ "Bash(kimi)",
31
+ "Bash(kimi:*)",
28
32
  "Bash(docker)",
29
33
  "Bash(docker:*)"
30
34
  ]
@@ -131,8 +131,8 @@ taskType: "{{FM_TASK_TYPE}}"
131
131
  - Should the same task key continue to be used after this run?
132
132
  - What is the likely work category for this task right now?
133
133
  - Which lifecycle phase should probably happen next after this run?
134
- - What prior decision or report should Claude compare against?
135
- - What related tasks should Claude keep in mind but not merge with this task?
134
+ - What prior decision or report should the Okstra lead compare against?
135
+ - What related tasks should the Okstra lead keep in mind but not merge with this task?
136
136
  - Which config files and deployment manifests carry the expected state for this task?
137
137
 
138
138
  ## Phase Boundary
@@ -145,27 +145,27 @@ taskType: "{{FM_TASK_TYPE}}"
145
145
 
146
146
  ## Available MCP Servers
147
147
 
148
- The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array and rendered into the Claude lead's launch prompt under `## Available MCP Servers`. They may be invoked **as needed** by Claude lead, Claude worker, and Report writer worker. The lead is responsible for forwarding the rendered list verbatim into the worker prompts (Phase 2) so workers know which tools they are allowed to call.
148
+ The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array and rendered into the Okstra lead's launch prompt under `## Available MCP Servers`. The lead is responsible for forwarding the rendered list verbatim into worker prompts (Phase 2) so each assignment can determine whether its selected runner exposes the named tools.
149
149
 
150
150
  To declare servers, add entries shaped `{ "name": "<server>", "description": "...", "tools": ["..."], "notes": "..." }` to that array. If the array is empty or absent, treat MCP as unavailable for this run.
151
151
 
152
152
  How to invoke (worker-by-worker):
153
153
 
154
- - **Claude lead / Claude worker / Report writer worker**: invoke the MCP tool **directly by its tool name** (e.g. `mcp__<server>__<tool>`) through the host's tool interface. **Do NOT call it via `Bash`** — these names are MCP tools, not shell commands; running them in a shell will always fail with `command not found` regardless of permission settings.
155
- - **Codex worker / Antigravity worker**: invoke through the external CLI's own MCP transport (e.g. `codex mcp call <server> <tool> <args>` for Codex CLI; the equivalent Antigravity CLI MCP invocation for Antigravity). If the worker's CLI has no matching MCP config, treat the server as unavailable for this run and record `MCP not available in this CLI` in `Missing Information or Assumptions` — do **not** attempt a shell fallback such as `mysql -h ...` or piping a tool name into `bash`.
154
+ - **Native-session assignments**: invoke an MCP tool directly by its tool name (e.g. `mcp__<server>__<tool>`) only when the selected host adapter exposes it. Do not call an MCP tool name through a shell.
155
+ - **CLI-wrapper assignments**: use only the MCP or tool configuration explicitly supported and forwarded by that provider wrapper. If the wrapper has no matching MCP configuration, record `MCP not available in this CLI` in `Missing Information or Assumptions`; do not attempt a shell fallback.
156
156
  - All workers: cite the exact server, tool, and SELECT (or `WHERE` filters) used in the result file. Tool-call failures must be logged in the worker's `*-errors.json` (commandKind `mcp_call`) so the lead can decide whether to retry under a different worker.
157
157
 
158
158
  Usage policy:
159
159
 
160
160
  - **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
161
161
  - **`implementation` phase**: read-only MCP queries are permitted as cross-checks; MCP MUST NOT be used as a write path even if a write tool becomes available — schema/data mutations belong in the codebase migration files reviewed by humans.
162
- - **External CLI workers (Codex, Antigravity)**: can use these MCP servers ONLY if their CLI's own MCP configuration is set up to mirror the same servers. If not configured, the worker should record `MCP not available in this CLI` in `Missing Information or Assumptions` rather than guessing.
162
+ - **CLI-wrapper workers**: can use these MCP servers only if their provider CLI configuration mirrors the same servers. If not configured, the worker should record `MCP not available in this CLI` in `Missing Information or Assumptions` rather than guessing.
163
163
  - **Forbidden**: connecting to non-listed databases, running anything that mutates state (server is read-only — flagged write attempts are a contract violation), persisting query results outside the run's own artifact directories.
164
164
  - **Limits**: `maxRows: 1000` per query (server-enforced). For larger scans, page via `WHERE` predicates and document the strategy.
165
165
 
166
166
  If MCP evidence contradicts a claim sourced from documentation, code, or prior reports, flag the conflict explicitly in the final report and prefer the live MCP evidence unless the brief says otherwise.
167
167
 
168
- ## Notes for Main Claude
168
+ ## Notes for Okstra Lead
169
169
 
170
170
  - Which workers should be used?
171
171
  - Should all workers receive the same brief or different emphasis?
@@ -23,6 +23,8 @@ per line to stderr.
23
23
  from __future__ import annotations
24
24
 
25
25
  import argparse
26
+ import hashlib
27
+ import json
26
28
  import re
27
29
  import sys
28
30
  from pathlib import Path
@@ -43,6 +45,8 @@ from okstra_ctl.report_views import ( # noqa: E402
43
45
  source_digest,
44
46
  )
45
47
  from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
48
+ from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
49
+ from okstra_ctl.report_contract import TASK_TYPE_REQUIRED_HUMAN_FIELDS # noqa: E402
46
50
 
47
51
 
48
52
  _EXTERNAL_URL_RE = re.compile(
@@ -66,11 +70,86 @@ _ANALYSIS_REVIEW_STATUSES = ["accepted", "rejected", "revision-requested"]
66
70
 
67
71
 
68
72
  def _main_body(html_text: str) -> str:
69
- start = html_text.find("<main>")
70
- end = html_text.find("</main>", start) if start >= 0 else -1
71
- if start < 0 or end < 0:
73
+ match = re.search(r"<main[^>]*>(?P<body>.*?)</main>", html_text, re.DOTALL)
74
+ if match is None:
72
75
  return html_text
73
- return html_text[start + len("<main>"): end]
76
+ return match.group("body")
77
+
78
+
79
+ def _sha256(path: Path) -> str:
80
+ return hashlib.sha256(path.read_bytes()).hexdigest()
81
+
82
+
83
+ def _digest_from_run_meta(html_text: str, key: str) -> str:
84
+ match = re.search(rf'"{re.escape(key)}"\s*:\s*"([0-9a-f]{{64}})"', html_text)
85
+ return match.group(1) if match else ""
86
+
87
+
88
+ def _load_v2_data(report_path: Path) -> tuple[Path, dict] | None:
89
+ data_path = final_report_data_path(report_path)
90
+ if not data_path.is_file():
91
+ return None
92
+ try:
93
+ data = json.loads(data_path.read_text(encoding="utf-8"))
94
+ except (OSError, json.JSONDecodeError):
95
+ return None
96
+ if isinstance(data, dict) and data.get("schemaVersion") == "2.0":
97
+ return data_path, data
98
+ return None
99
+
100
+
101
+ def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
102
+ html_path = html_view_path(report_path)
103
+ if not html_path.is_file():
104
+ return [f"missing html artifact: {html_path}"]
105
+ html_text = html_path.read_text(encoding="utf-8")
106
+ main_body = _main_body(html_text)
107
+ failures: list[str] = []
108
+
109
+ for key, path in (
110
+ ("source-data-sha256", data_path),
111
+ ("source-md-sha256", report_path),
112
+ ):
113
+ recorded = _digest_from_run_meta(html_text, key)
114
+ expected = _sha256(path)
115
+ if not recorded:
116
+ failures.append(f"html missing {key} in run-meta")
117
+ elif recorded != expected:
118
+ failures.append(f"stale html: {key} does not match current source")
119
+
120
+ task_type = data.get("header", {}).get("taskType", "")
121
+ if f'data-task-template="{task_type}"' not in html_text:
122
+ failures.append(f"v2 html task template mismatch for {task_type}")
123
+ actual_fields = set(re.findall(r'data-report-field="([^"]+)"', main_body))
124
+ for field in TASK_TYPE_REQUIRED_HUMAN_FIELDS.get(task_type, ()):
125
+ if field not in actual_fields:
126
+ failures.append(f"missing human field: {field}")
127
+
128
+ if "data-audit-field=" in main_body:
129
+ failures.append("audit field inside human main")
130
+ node_ids = set(re.findall(r'data-node-id="([^"]+)"', html_text))
131
+ fallback_ids = set(re.findall(r'data-fallback-id="([^"]+)"', html_text))
132
+ if node_ids != fallback_ids:
133
+ failures.append(
134
+ f"visualization ID mismatch: svg={sorted(node_ids)}, fallback={sorted(fallback_ids)}"
135
+ )
136
+
137
+ data_ids = []
138
+ for row in data.get("clarificationItems", []):
139
+ if not isinstance(row, dict):
140
+ continue
141
+ response_id = row.get("id")
142
+ if isinstance(response_id, str) and re.fullmatch(r"C-\d+", response_id):
143
+ data_ids.append(response_id)
144
+ data_ids.sort()
145
+ html_ids = sorted(set(_RESPONSE_ID_ATTR_RE.findall(html_text)))
146
+ if data_ids != html_ids:
147
+ failures.append(
148
+ f"Response ID mismatch: data.json has {data_ids}, HTML has {html_ids}"
149
+ )
150
+ if _EXTERNAL_URL_RE.search(html_text):
151
+ failures.append("html contains external URL in script/link/img — must be self-contained")
152
+ return failures
74
153
 
75
154
 
76
155
  def _no_form_sections(html_body: str) -> list[str]:
@@ -128,6 +207,9 @@ def validate(report_path: Path) -> list[str]:
128
207
  return [f"final-report not found: {report_path}"]
129
208
 
130
209
  md = report_path.read_text(encoding="utf-8")
210
+ v2 = _load_v2_data(report_path)
211
+ if v2 is not None:
212
+ return _validate_v2(report_path, v2[0], v2[1])
131
213
  html_path = html_view_path(report_path)
132
214
  # §1 헤딩이 있는데 파싱이 실패하면 md_ids 가 빈 []이 되어 "clarification 없음
133
215
  # → skip" 으로 흘러 HTML form parity 게이트가 조용히 열린다. fail-closed.