okstra 0.102.2 → 0.102.3
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.
- package/README.kr.md +5 -5
- package/README.md +5 -5
- package/docs/kr/architecture/storage-model.md +1 -1
- package/docs/kr/architecture.md +7 -11
- package/docs/kr/cli.md +4 -4
- package/docs/kr/container.md +1 -1
- package/docs/kr/performance-improvement-plan-v2.md +2 -2
- package/docs/project-structure-overview.md +2 -2
- package/package.json +7 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-antigravity-exec.sh +1 -2
- package/runtime/bin/okstra-codex-exec.sh +1 -2
- package/runtime/bin/okstra-render-final-report.py +2 -9
- package/runtime/bin/okstra-spawn-followups.py +4 -6
- package/runtime/bin/okstra-trace-cleanup.sh +24 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +9 -11
- package/runtime/python/okstra_ctl/codex_dispatch.py +4 -12
- package/runtime/python/okstra_ctl/consumers.py +35 -0
- package/runtime/python/okstra_ctl/container.py +4 -6
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -12
- package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
- package/runtime/python/okstra_ctl/handoff.py +2 -1
- package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
- package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +7 -4
- package/runtime/python/okstra_ctl/run.py +3 -10
- package/runtime/python/okstra_ctl/stage_targets.py +18 -0
- package/runtime/python/okstra_ctl/wizard.py +24 -25
- package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
- package/runtime/python/okstra_token_usage/report.py +2 -5
- package/runtime/validators/validate-report-views.py +2 -1
- package/runtime/validators/validate-run.py +1 -11
- package/src/commands/lifecycle/install.mjs +5 -5
- package/src/commands/lifecycle/uninstall.mjs +4 -4
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
- package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
- package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
- package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
- package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
- package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
- package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
- package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
- package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
- package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
- package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
- package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
- package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
- package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
- package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
- package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
- package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
- package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
- package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
- package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
- package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
- package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
- package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
- package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
- package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
- package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
- package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
- package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
- package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
- package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
- package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
- package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
- package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
- package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
- package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
- package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
- package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
- package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
- package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
- package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
- package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
- package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
- package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
- package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
- package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
- package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
- package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
- package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
- package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
- package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
- package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
- package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
- package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
- package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
- package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
- package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
- package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
- package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
- package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
- package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
- package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
- package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
- package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
- package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
- package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
- package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
- package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
- package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
- package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
- package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
- package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
- package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
- package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
- package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
- package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
- package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
- package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
- package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
- package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
- package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
- package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
- package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
- package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
- package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
- package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
- package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
- package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
- package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
- package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
- package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
- package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
- package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
- package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
- package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
- package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
- package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +0 -110
|
@@ -1,1000 +0,0 @@
|
|
|
1
|
-
# final-report HTML 승인·옵션 선택 구현 계획
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** implementation-planning final-report 의 HTML 뷰에 승인 체크 + 구현 옵션 선택 위젯을 추가하고, Export sidecar 의 `## APPROVAL` 블록을 implementation 마법사가 감지·확인 후 기존 승인 경로로 적용한다.
|
|
6
|
-
|
|
7
|
-
**Architecture:** HTML 은 원본 md/data.json 을 절대 수정하지 않는다 — 위젯 값은 기존 user-response sidecar 에 `## APPROVAL` 블록으로 직렬화되고, 마법사 `approve_plan_confirm` 단계가 sidecar 를 감지해 기존 `_approve_plan_in_place`(data.json 플립) + `_apply_cli_implementation_option`(frontmatter 치환)을 재사용해 적용한다. 스펙: `docs/superpowers/specs/2026-06-12-html-plan-approval-design.md`.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Python 3.11 (scripts/okstra_ctl), vanilla JS (templates/reports/report.js), pytest (+ Node byte-identity 하니스).
|
|
10
|
-
|
|
11
|
-
**전제 (이미 코드베이스에 있음):**
|
|
12
|
-
- `scan_approval_gate` — `scripts/okstra_ctl/clarification_items.py` (§1 Blocks=approval 스캔, fail-closed).
|
|
13
|
-
- `_approve_plan_in_place` / `_classify_approved_plan` — `scripts/okstra_ctl/wizard.py:538` / `:486`.
|
|
14
|
-
- `_apply_cli_implementation_option` — `scripts/okstra_ctl/run.py:744`.
|
|
15
|
-
- 마법사 prompt 텍스트 SOT — `prompts/wizard/prompts.ko.json` → `steps.approve_plan_confirm`.
|
|
16
|
-
- 동시 세션 주의: `scripts/okstra_ctl/md_table.py`(escape_pipes/split_pipe_row)는 다른 세션이 작업 중인 미커밋 변경 — 그 파일을 수정하지도 커밋하지도 말 것. 커밋은 항상 이 계획의 파일만 `git add <paths>` 로 명시.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
### Task 1: Python serializer — `## APPROVAL` 블록
|
|
21
|
-
|
|
22
|
-
**Files:**
|
|
23
|
-
- Modify: `scripts/okstra_ctl/report_views.py` (serialize_user_response, ~:726)
|
|
24
|
-
- Test: `tests/test_report_views.py`
|
|
25
|
-
|
|
26
|
-
- [ ] **Step 1: 실패 테스트 작성** — `tests/test_report_views.py` 의 serializer 섹션(`test_python_and_js_serialisers_byte_identical_nonempty` 위)에 추가:
|
|
27
|
-
|
|
28
|
-
```python
|
|
29
|
-
def test_serialize_user_response_appends_approval_block():
|
|
30
|
-
from okstra_ctl.report_views import UserResponseApproval
|
|
31
|
-
|
|
32
|
-
meta = _meta("implementation-planning")
|
|
33
|
-
out = serialize_user_response(
|
|
34
|
-
run_meta=meta, entries=[], created_at="2026-06-12T00:00:00Z",
|
|
35
|
-
approval=UserResponseApproval(approved=True, implementation_option="Option B — y"),
|
|
36
|
-
)
|
|
37
|
-
assert out.endswith(
|
|
38
|
-
"# User Response\n\n## APPROVAL\n- Approved: true\n- Implementation-Option: Option B — y\n"
|
|
39
|
-
)
|
|
40
|
-
# 승인이 있으면 빈-응답 플레이스홀더는 나오지 않는다.
|
|
41
|
-
assert "_(No user responses recorded.)_" not in out
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_serialize_user_response_approval_without_option_omits_line():
|
|
45
|
-
from okstra_ctl.report_views import UserResponseApproval
|
|
46
|
-
|
|
47
|
-
meta = _meta("implementation-planning")
|
|
48
|
-
out = serialize_user_response(
|
|
49
|
-
run_meta=meta,
|
|
50
|
-
entries=[UserResponseEntry("C-001", "decision", "(a)", None)],
|
|
51
|
-
created_at="2026-06-12T00:00:00Z",
|
|
52
|
-
approval=UserResponseApproval(approved=True),
|
|
53
|
-
)
|
|
54
|
-
assert "\n## APPROVAL\n- Approved: true\n" in out
|
|
55
|
-
assert "Implementation-Option" not in out
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def test_serialize_user_response_without_approval_is_unchanged():
|
|
59
|
-
meta = _meta("implementation-planning")
|
|
60
|
-
out = serialize_user_response(run_meta=meta, entries=[], created_at="2026-06-12T00:00:00Z")
|
|
61
|
-
assert out.endswith("_(No user responses recorded.)_\n")
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
- [ ] **Step 2: 실패 확인** — Run: `python3 -m pytest tests/test_report_views.py -k approval_block -q`
|
|
65
|
-
Expected: FAIL (`cannot import name 'UserResponseApproval'` 또는 TypeError unexpected keyword `approval`)
|
|
66
|
-
|
|
67
|
-
- [ ] **Step 3: 구현** — `report_views.py` 의 `UserResponseEntry` dataclass 아래에 추가하고 `serialize_user_response` 를 다음으로 교체:
|
|
68
|
-
|
|
69
|
-
```python
|
|
70
|
-
@dataclass(frozen=True)
|
|
71
|
-
class UserResponseApproval:
|
|
72
|
-
"""HTML Plan Approval 위젯의 Export 결과. ``implementation_option`` 이 빈
|
|
73
|
-
문자열이면 라인을 생략한다 (소비 측은 Recommended Option 폴백)."""
|
|
74
|
-
approved: bool
|
|
75
|
-
implementation_option: str = ""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def serialize_user_response(
|
|
79
|
-
*,
|
|
80
|
-
run_meta: RunMeta,
|
|
81
|
-
entries: list[UserResponseEntry],
|
|
82
|
-
created_at: str,
|
|
83
|
-
approval: UserResponseApproval | None = None,
|
|
84
|
-
) -> str:
|
|
85
|
-
"""Return the canonical markdown text the HTML 'Export user
|
|
86
|
-
response' button must produce. Used by validators to confirm that
|
|
87
|
-
pasted sidecar files conform to the schema and that the JS
|
|
88
|
-
serialiser stayed in sync.
|
|
89
|
-
"""
|
|
90
|
-
head = (
|
|
91
|
-
"---\n"
|
|
92
|
-
f"task-key: {run_meta.task_key}\n"
|
|
93
|
-
f"task-type: {run_meta.task_type}\n"
|
|
94
|
-
f"seq: {run_meta.seq}\n"
|
|
95
|
-
f"source-report: {run_meta.source_report}\n"
|
|
96
|
-
"created-by: user\n"
|
|
97
|
-
f"created-at: {created_at}\n"
|
|
98
|
-
"---\n"
|
|
99
|
-
"\n"
|
|
100
|
-
"# User Response\n"
|
|
101
|
-
)
|
|
102
|
-
has_approval = approval is not None and approval.approved
|
|
103
|
-
body_chunks: list[str] = []
|
|
104
|
-
for e in entries:
|
|
105
|
-
chunk = (
|
|
106
|
-
f"\n## {e.response_id}\n"
|
|
107
|
-
f"- Kind: {e.kind}\n"
|
|
108
|
-
"- Value:\n"
|
|
109
|
-
f" > {e.value.strip()}\n"
|
|
110
|
-
)
|
|
111
|
-
if e.rationale:
|
|
112
|
-
chunk += f"- Rationale: {e.rationale.strip()}\n"
|
|
113
|
-
body_chunks.append(chunk)
|
|
114
|
-
if not entries and not has_approval:
|
|
115
|
-
body_chunks.append("\n_(No user responses recorded.)_\n")
|
|
116
|
-
if has_approval:
|
|
117
|
-
chunk = "\n## APPROVAL\n- Approved: true\n"
|
|
118
|
-
if approval.implementation_option:
|
|
119
|
-
chunk += f"- Implementation-Option: {approval.implementation_option.strip()}\n"
|
|
120
|
-
body_chunks.append(chunk)
|
|
121
|
-
return head + "".join(body_chunks)
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
- [ ] **Step 4: 통과 확인** — Run: `python3 -m pytest tests/test_report_views.py -q` → 전체 PASS
|
|
125
|
-
- [ ] **Step 5: Commit** — `git add scripts/okstra_ctl/report_views.py tests/test_report_views.py && git commit -m "feat(okstra-ctl/report-views): user-response sidecar 에 APPROVAL 블록 직렬화 추가"`
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
### Task 2: JS serializer + byte-identity
|
|
130
|
-
|
|
131
|
-
**Files:**
|
|
132
|
-
- Modify: `templates/reports/report.js` (buildUserResponseMarkdown :116, exportUserResponse :164)
|
|
133
|
-
- Test: `tests/test_report_views.py` (`_js_serialise` :484)
|
|
134
|
-
|
|
135
|
-
- [ ] **Step 1: 실패 테스트** — `_js_serialise` 를 4번째 인자를 받도록 교체하고 parity 테스트 추가:
|
|
136
|
-
|
|
137
|
-
```python
|
|
138
|
-
def _js_serialise(
|
|
139
|
-
meta: dict, entries: list[dict], created_at: str, approval: dict | None = None
|
|
140
|
-
) -> str:
|
|
141
|
-
"""Run templates/reports/report.js via Node + vm to get the JS
|
|
142
|
-
serialiser's bytes."""
|
|
143
|
-
assert NODE is not None
|
|
144
|
-
script = (
|
|
145
|
-
"const fs=require('fs');const vm=require('vm');"
|
|
146
|
-
f"vm.runInThisContext(fs.readFileSync({str((REPO_ROOT / 'templates' / 'reports' / 'report.js'))!r},'utf8'));"
|
|
147
|
-
"const m=globalThis.__okstraReportViewExports__;"
|
|
148
|
-
"const meta=JSON.parse(process.argv[1]);"
|
|
149
|
-
"const entries=JSON.parse(process.argv[2]);"
|
|
150
|
-
"const approval=JSON.parse(process.argv[4]);"
|
|
151
|
-
"process.stdout.write(m.buildUserResponseMarkdown(meta, entries, process.argv[3], approval));"
|
|
152
|
-
)
|
|
153
|
-
import json as _json
|
|
154
|
-
|
|
155
|
-
result = subprocess.run(
|
|
156
|
-
[NODE, "-e", script, _json.dumps(meta), _json.dumps(entries), created_at,
|
|
157
|
-
_json.dumps(approval)],
|
|
158
|
-
check=True,
|
|
159
|
-
capture_output=True,
|
|
160
|
-
text=True,
|
|
161
|
-
)
|
|
162
|
-
return result.stdout
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@pytest.mark.skipif(NODE is None, reason="node not available")
|
|
166
|
-
def test_python_and_js_serialisers_byte_identical_with_approval():
|
|
167
|
-
from okstra_ctl.report_views import UserResponseApproval
|
|
168
|
-
|
|
169
|
-
meta = _meta("implementation-planning")
|
|
170
|
-
py_out = serialize_user_response(
|
|
171
|
-
run_meta=meta, entries=[], created_at="2026-06-12T00:00:00Z",
|
|
172
|
-
approval=UserResponseApproval(approved=True, implementation_option="Option B — y"),
|
|
173
|
-
)
|
|
174
|
-
js_out = _js_serialise(
|
|
175
|
-
{"task-key": meta.task_key, "task-type": meta.task_type,
|
|
176
|
-
"seq": meta.seq, "source-report": meta.source_report},
|
|
177
|
-
[], "2026-06-12T00:00:00Z",
|
|
178
|
-
{"approved": True, "implementationOption": "Option B — y"},
|
|
179
|
-
)
|
|
180
|
-
assert py_out == js_out
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
- [ ] **Step 2: 실패 확인** — Run: `python3 -m pytest tests/test_report_views.py -k byte_identical -q`
|
|
184
|
-
Expected: with_approval FAIL (JS 가 4번째 인자를 무시 → empty 플레이스홀더 출력), 기존 2종은 PASS 유지
|
|
185
|
-
- [ ] **Step 3: 구현** — `report.js` 의 `buildUserResponseMarkdown` 을 교체:
|
|
186
|
-
|
|
187
|
-
```js
|
|
188
|
-
function buildUserResponseMarkdown(runMeta, entries, createdAt, approval) {
|
|
189
|
-
var head =
|
|
190
|
-
"---\n" +
|
|
191
|
-
"task-key: " + (runMeta["task-key"] || "") + "\n" +
|
|
192
|
-
"task-type: " + (runMeta["task-type"] || "") + "\n" +
|
|
193
|
-
"seq: " + (runMeta["seq"] || "") + "\n" +
|
|
194
|
-
"source-report: " + (runMeta["source-report"] || "") + "\n" +
|
|
195
|
-
"created-by: user\n" +
|
|
196
|
-
"created-at: " + createdAt + "\n" +
|
|
197
|
-
"---\n" +
|
|
198
|
-
"\n" +
|
|
199
|
-
"# User Response\n";
|
|
200
|
-
|
|
201
|
-
entries = entries || [];
|
|
202
|
-
var hasApproval = !!(approval && approval.approved);
|
|
203
|
-
var chunks = "";
|
|
204
|
-
for (var i = 0; i < entries.length; i++) {
|
|
205
|
-
var e = entries[i];
|
|
206
|
-
var chunk =
|
|
207
|
-
"\n## " + e.responseId + "\n" +
|
|
208
|
-
"- Kind: " + e.kind + "\n" +
|
|
209
|
-
"- Value:\n" +
|
|
210
|
-
" > " + trimMultiline(e.value) + "\n";
|
|
211
|
-
if (e.rationale) {
|
|
212
|
-
chunk += "- Rationale: " + trimMultiline(e.rationale) + "\n";
|
|
213
|
-
}
|
|
214
|
-
chunks += chunk;
|
|
215
|
-
}
|
|
216
|
-
if (entries.length === 0 && !hasApproval) {
|
|
217
|
-
chunks += "\n_(No user responses recorded.)_\n";
|
|
218
|
-
}
|
|
219
|
-
if (hasApproval) {
|
|
220
|
-
chunks += "\n## APPROVAL\n- Approved: true\n";
|
|
221
|
-
if (approval.implementationOption) {
|
|
222
|
-
chunks +=
|
|
223
|
-
"- Implementation-Option: " + trimMultiline(approval.implementationOption) + "\n";
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return head + chunks;
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
같은 파일의 `collectEntries` 아래에 위젯 수집 함수를 추가하고 `exportUserResponse` 를 교체:
|
|
231
|
-
|
|
232
|
-
```js
|
|
233
|
-
// Plan Approval 위젯 (implementation-planning 보고서에만 존재). 체크박스가
|
|
234
|
-
// disabled(§1 차단 미해소)이거나 미체크면 null. 옵션이 추천(data-recommended)
|
|
235
|
-
// 그대로면 implementationOption 을 비워 보낸다 → 소비 측 Recommended 폴백.
|
|
236
|
-
function collectApproval() {
|
|
237
|
-
var box = document.getElementById("approval-checkbox");
|
|
238
|
-
if (!box || box.disabled || !box.checked) return null;
|
|
239
|
-
var sel = document.getElementById("approval-option");
|
|
240
|
-
var option = "";
|
|
241
|
-
if (sel && sel.selectedIndex >= 0) {
|
|
242
|
-
var opt = sel.options[sel.selectedIndex];
|
|
243
|
-
if (opt && opt.getAttribute("data-recommended") !== "true") {
|
|
244
|
-
option = opt.value;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return { approved: true, implementationOption: option };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function exportUserResponse() {
|
|
251
|
-
var runMeta = readRunMeta();
|
|
252
|
-
var entries = collectEntries();
|
|
253
|
-
var approval = collectApproval();
|
|
254
|
-
var md = buildUserResponseMarkdown(runMeta, entries, isoNowUtc(), approval);
|
|
255
|
-
var out = document.getElementById("user-response-output");
|
|
256
|
-
if (out) out.textContent = md;
|
|
257
|
-
// Nothing answered yet — show the empty serialisation as feedback but
|
|
258
|
-
// don't download a useless file.
|
|
259
|
-
if (entries.length > 0 || approval) downloadUserResponse(md, runMeta);
|
|
260
|
-
return md;
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
`window.okstraReportView` export 객체에 `collectApproval: collectApproval,` 한 줄 추가.
|
|
265
|
-
|
|
266
|
-
- [ ] **Step 4: 통과 확인** — Run: `python3 -m pytest tests/test_report_views.py -q` → 전체 PASS
|
|
267
|
-
- [ ] **Step 5: Commit** — `git add templates/reports/report.js tests/test_report_views.py && git commit -m "feat(reports/report.js): Plan Approval 위젯 수집 + APPROVAL 블록 직렬화 (Python 과 byte-identical)"`
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
### Task 3: HTML 위젯 렌더 + 생성 조건 확장
|
|
272
|
-
|
|
273
|
-
**Files:**
|
|
274
|
-
- Modify: `scripts/okstra_ctl/report_views.py` (render_html :94, render_html_view :783)
|
|
275
|
-
- Modify: `templates/reports/report.css`
|
|
276
|
-
- Modify: `validators/validate-report-views.py` (:101 분기)
|
|
277
|
-
- Test: `tests/test_report_views.py`
|
|
278
|
-
|
|
279
|
-
- [ ] **Step 1: 실패 테스트 작성**:
|
|
280
|
-
|
|
281
|
-
```python
|
|
282
|
-
def _planning_md(*, c_row: bool = True) -> str:
|
|
283
|
-
clar = (
|
|
284
|
-
"| 항목 | Statement | User input |\n|---|---|---|\n"
|
|
285
|
-
"| **C-001**<br>Ticket: `T-1`<br>Kind: `decision`<br>Blocks: `none`<br>Status: open | 질문 | |\n"
|
|
286
|
-
if c_row else "- 추가 정보 요청 없음.\n"
|
|
287
|
-
)
|
|
288
|
-
return (
|
|
289
|
-
"---\napproved: false\nimplementation-option:\n---\n"
|
|
290
|
-
"# demo/T-1 - Multi-Agent Cross Verification Final Report\n\n"
|
|
291
|
-
"## 1. Clarification Items\n\n" + clar + "\nbody\n"
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def _planning_data_json() -> str:
|
|
296
|
-
import json as _json
|
|
297
|
-
return _json.dumps({
|
|
298
|
-
"frontmatter": {"approved": False},
|
|
299
|
-
"implementationPlanning": {
|
|
300
|
-
"optionCandidates": [{"name": "Option A — x"}, {"name": "Option B — y"}],
|
|
301
|
-
"recommendedOption": {"name": "Option A — x"},
|
|
302
|
-
},
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def _render_planning_view(tmp_path, md_text: str, *, with_data: bool = True):
|
|
307
|
-
from okstra_ctl.report_views import render_html_view
|
|
308
|
-
|
|
309
|
-
reports = tmp_path / "runs" / "implementation-planning" / "reports"
|
|
310
|
-
reports.mkdir(parents=True)
|
|
311
|
-
md = reports / "final-report-implementation-planning-002.md"
|
|
312
|
-
md.write_text(md_text, encoding="utf-8")
|
|
313
|
-
if with_data:
|
|
314
|
-
md.with_name(md.stem + ".data.json").write_text(_planning_data_json(), encoding="utf-8")
|
|
315
|
-
return md, render_html_view(md, run_meta=_meta("implementation-planning"), css=CSS, js=JS)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def test_html_plan_approval_widget_renders_with_recommended_default(tmp_path):
|
|
319
|
-
md, html_path = _render_planning_view(tmp_path, _planning_md())
|
|
320
|
-
body = html_path.read_text(encoding="utf-8")
|
|
321
|
-
assert '<section id="plan-approval">' in body
|
|
322
|
-
assert 'data-recommended="true" selected>Option A — x (권장)</option>' in body
|
|
323
|
-
assert '<option value="Option B — y">Option B — y</option>' in body
|
|
324
|
-
assert '<input type="checkbox" id="approval-checkbox">' in body # disabled 아님
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def test_html_plan_approval_disabled_when_blockers_open(tmp_path):
|
|
328
|
-
blocked = _planning_md().replace("Blocks: `none`", "Blocks: `approval`")
|
|
329
|
-
md, html_path = _render_planning_view(tmp_path, blocked)
|
|
330
|
-
body = html_path.read_text(encoding="utf-8")
|
|
331
|
-
assert '<section id="plan-approval">' in body
|
|
332
|
-
assert 'id="approval-checkbox" disabled' in body
|
|
333
|
-
assert "승인 차단 항목 1건" in body
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def test_html_plan_approval_absent_without_data_json(tmp_path):
|
|
337
|
-
md, html_path = _render_planning_view(tmp_path, _planning_md(), with_data=False)
|
|
338
|
-
assert 'id="plan-approval"' not in html_path.read_text(encoding="utf-8")
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def test_html_view_generated_for_approval_only_report(tmp_path):
|
|
342
|
-
# C-행이 없어도 승인 위젯이 적용되면 html 을 생성한다 (생성 조건 확장).
|
|
343
|
-
md, html_path = _render_planning_view(tmp_path, _planning_md(c_row=False))
|
|
344
|
-
assert html_path is not None
|
|
345
|
-
assert 'id="plan-approval"' in html_path.read_text(encoding="utf-8")
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
- [ ] **Step 2: 실패 확인** — Run: `python3 -m pytest tests/test_report_views.py -k plan_approval -q` → 4건 FAIL
|
|
349
|
-
- [ ] **Step 3: 구현** — `report_views.py` 상단 import 에 `import json` 이 없으면 추가하고, `report_has_clarification_items` 아래에 추가:
|
|
350
|
-
|
|
351
|
-
```python
|
|
352
|
-
@dataclass(frozen=True)
|
|
353
|
-
class PlanApprovalContext:
|
|
354
|
-
"""Plan Approval 위젯 렌더 입력. ``disabled_reason`` 이 비어 있지 않으면
|
|
355
|
-
위젯은 disabled 로 렌더된다 (run-prep 승인 게이트와 동일한 fail-closed 기준)."""
|
|
356
|
-
option_names: tuple[str, ...]
|
|
357
|
-
recommended_option: str
|
|
358
|
-
disabled_reason: str
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def plan_approval_context(
|
|
362
|
-
src_md_path: Path, src_text: str
|
|
363
|
-
) -> PlanApprovalContext | None:
|
|
364
|
-
"""implementation-planning 보고서 + sibling data.json 의 optionCandidates 가
|
|
365
|
-
있을 때만 컨텍스트를 만든다. planning 여부는 task-type 문자열이 아니라
|
|
366
|
-
data.json 의 ``implementationPlanning`` 키(SSOT)로 판정한다 — renderer 와
|
|
367
|
-
validator 가 같은 판정을 공유한다."""
|
|
368
|
-
data_path = src_md_path.with_name(src_md_path.stem + ".data.json")
|
|
369
|
-
if not data_path.is_file():
|
|
370
|
-
return None
|
|
371
|
-
try:
|
|
372
|
-
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
373
|
-
except (OSError, json.JSONDecodeError):
|
|
374
|
-
return None
|
|
375
|
-
planning = data.get("implementationPlanning")
|
|
376
|
-
if not isinstance(planning, dict):
|
|
377
|
-
return None
|
|
378
|
-
names = tuple(
|
|
379
|
-
c.get("name", "")
|
|
380
|
-
for c in (planning.get("optionCandidates") or [])
|
|
381
|
-
if isinstance(c, dict) and c.get("name")
|
|
382
|
-
)
|
|
383
|
-
if not names:
|
|
384
|
-
return None
|
|
385
|
-
recommended = ""
|
|
386
|
-
rec = planning.get("recommendedOption")
|
|
387
|
-
if isinstance(rec, dict):
|
|
388
|
-
recommended = rec.get("name") or ""
|
|
389
|
-
scan = scan_approval_gate(src_text)
|
|
390
|
-
if scan.unreadable_reason:
|
|
391
|
-
reason = "§1 승인 게이트를 읽을 수 없어 승인이 비활성화되었습니다 — 보고서를 재렌더하세요."
|
|
392
|
-
elif scan.blockers:
|
|
393
|
-
reason = (
|
|
394
|
-
f"§1 승인 차단 항목 {len(scan.blockers)}건 미해소 — 답변을 채워 Export 한 뒤 "
|
|
395
|
-
"resume-clarification 으로 해소한 다음 보고서에서 승인하세요."
|
|
396
|
-
)
|
|
397
|
-
else:
|
|
398
|
-
reason = ""
|
|
399
|
-
return PlanApprovalContext(
|
|
400
|
-
option_names=names, recommended_option=recommended, disabled_reason=reason
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def _plan_approval_section(ctx: PlanApprovalContext) -> str:
|
|
405
|
-
disabled = " disabled" if ctx.disabled_reason else ""
|
|
406
|
-
opts: list[str] = []
|
|
407
|
-
for name in ctx.option_names:
|
|
408
|
-
is_rec = name == ctx.recommended_option
|
|
409
|
-
label = f"{name} (권장)" if is_rec else name
|
|
410
|
-
attrs = ' data-recommended="true" selected' if is_rec else ""
|
|
411
|
-
opts.append(f'<option value="{html.escape(name)}"{attrs}>{html.escape(label)}</option>')
|
|
412
|
-
reason_html = (
|
|
413
|
-
f'\n <p class="approval-disabled-reason">{html.escape(ctx.disabled_reason)}</p>'
|
|
414
|
-
if ctx.disabled_reason else ""
|
|
415
|
-
)
|
|
416
|
-
return (
|
|
417
|
-
'<section id="plan-approval">\n'
|
|
418
|
-
" <h2>Plan Approval</h2>\n"
|
|
419
|
-
f' <label>구현 옵션: <select id="approval-option"{disabled}>{"".join(opts)}</select></label>\n'
|
|
420
|
-
f' <label><input type="checkbox" id="approval-checkbox"{disabled}> '
|
|
421
|
-
"이 plan 을 승인합니다 — Export 시 sidecar 에 APPROVAL 블록으로 기록되고, "
|
|
422
|
-
"implementation 시작 마법사가 확인 후 적용합니다.</label>"
|
|
423
|
-
f"{reason_html}\n"
|
|
424
|
-
"</section>\n"
|
|
425
|
-
)
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
`scan_approval_gate` 가 report_views 에 이미 import 되어 있는지 확인하고 없으면 `from okstra_ctl.clarification_items import scan_approval_gate` 형태로 기존 import 블록에 추가 (기존 import 스타일을 그대로 따른다).
|
|
429
|
-
|
|
430
|
-
`render_html` 시그니처에 `approval_ctx: PlanApprovalContext | None = None` 키워드를 추가하고, 본문 f-string 의 `f"<main>{body_html}</main>\n"` 다음 줄에 삽입:
|
|
431
|
-
|
|
432
|
-
```python
|
|
433
|
-
f"<main>{body_html}</main>\n"
|
|
434
|
-
f"{_plan_approval_section(approval_ctx) if approval_ctx else ''}"
|
|
435
|
-
f"<footer class=\"report-footer\">\n"
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
`render_html_view` 를 다음으로 교체 (생성 조건 확장 + ctx 전달):
|
|
439
|
-
|
|
440
|
-
```python
|
|
441
|
-
def render_html_view(
|
|
442
|
-
src_md_path: Path,
|
|
443
|
-
*,
|
|
444
|
-
run_meta: RunMeta,
|
|
445
|
-
css: str,
|
|
446
|
-
js: str,
|
|
447
|
-
) -> Path | None:
|
|
448
|
-
"""Write ``<stem>.html`` next to ``src_md_path`` and return its path,
|
|
449
|
-
or return ``None`` when the report needs no interactive view — §1
|
|
450
|
-
clarification rows 도 없고 Plan Approval 위젯 대상도 아닐 때
|
|
451
|
-
(``report_has_clarification_items`` / ``plan_approval_context``).
|
|
452
|
-
Idempotent — overwrites an existing html sibling, and removes a stale
|
|
453
|
-
one when the report no longer needs a view."""
|
|
454
|
-
src_text = src_md_path.read_text(encoding="utf-8")
|
|
455
|
-
html_path = src_md_path.with_name(src_md_path.stem + ".html")
|
|
456
|
-
approval_ctx = plan_approval_context(src_md_path, src_text)
|
|
457
|
-
if not report_has_clarification_items(src_text) and approval_ctx is None:
|
|
458
|
-
if html_path.is_file():
|
|
459
|
-
html_path.unlink()
|
|
460
|
-
return None
|
|
461
|
-
html_text = render_html(
|
|
462
|
-
src_text, run_meta=run_meta, css=css, js=js, approval_ctx=approval_ctx
|
|
463
|
-
)
|
|
464
|
-
html_path.write_text(html_text, encoding="utf-8")
|
|
465
|
-
return html_path
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
`templates/reports/report.css` 끝부분 `@media print` 블록 **앞**에 추가:
|
|
469
|
-
|
|
470
|
-
```css
|
|
471
|
-
#plan-approval {
|
|
472
|
-
max-width: 120ch;
|
|
473
|
-
margin: 1.5rem auto;
|
|
474
|
-
padding: 0.8em 1rem;
|
|
475
|
-
border: 1px solid color-mix(in srgb, GrayText 40%, transparent);
|
|
476
|
-
border-radius: 4px;
|
|
477
|
-
}
|
|
478
|
-
#plan-approval h2 { margin-top: 0; }
|
|
479
|
-
#plan-approval label { display: block; margin: 0.4em 0; }
|
|
480
|
-
#plan-approval select { font: inherit; max-width: 100%; }
|
|
481
|
-
.approval-disabled-reason { color: GrayText; margin: 0.4em 0 0; }
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
그리고 `@media print` 블록 안에 `#plan-approval { display: none; }` 추가.
|
|
485
|
-
|
|
486
|
-
`validators/validate-report-views.py` 의 html 부재 분기(:101)를 교체 — import 블록의 `from okstra_ctl.report_views import (...)` 에 `plan_approval_context` 를 추가하고:
|
|
487
|
-
|
|
488
|
-
```python
|
|
489
|
-
if not html_path.is_file():
|
|
490
|
-
if not md_ids and plan_approval_context(report_path, md) is None:
|
|
491
|
-
return []
|
|
492
|
-
return [f"missing html artifact: {html_path}"]
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
- [ ] **Step 4: 통과 확인** — Run: `python3 -m pytest tests/test_report_views.py -q` → 전체 PASS. 이어서 `python3 -m pytest tests/ -q` 로 validator 회귀 확인.
|
|
496
|
-
- [ ] **Step 5: Commit** — `git add scripts/okstra_ctl/report_views.py templates/reports/report.css validators/validate-report-views.py tests/test_report_views.py && git commit -m "feat(okstra-ctl/report-views): Plan Approval 위젯 렌더 + 위젯 대상 보고서로 html 생성 조건 확장"`
|
|
497
|
-
|
|
498
|
-
---
|
|
499
|
-
|
|
500
|
-
### Task 4: user-responses/ 디렉토리 선생성
|
|
501
|
-
|
|
502
|
-
**Files:**
|
|
503
|
-
- Modify: `scripts/okstra_ctl/report_views.py` (render_html_view)
|
|
504
|
-
- Test: `tests/test_report_views.py`
|
|
505
|
-
|
|
506
|
-
- [ ] **Step 1: 실패 테스트**:
|
|
507
|
-
|
|
508
|
-
```python
|
|
509
|
-
def test_html_view_precreates_user_responses_dir(tmp_path):
|
|
510
|
-
md, html_path = _render_planning_view(tmp_path, _planning_md())
|
|
511
|
-
assert (md.parent.parent / "user-responses").is_dir()
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
def test_no_user_responses_dir_when_view_skipped(tmp_path):
|
|
515
|
-
from okstra_ctl.report_views import render_html_view
|
|
516
|
-
|
|
517
|
-
reports = tmp_path / "runs" / "implementation" / "reports"
|
|
518
|
-
reports.mkdir(parents=True)
|
|
519
|
-
md = reports / "final-report-implementation-001.md"
|
|
520
|
-
md.write_text("# demo/T-1 - Final Report\n\nbody\n", encoding="utf-8")
|
|
521
|
-
assert render_html_view(md, run_meta=_meta("implementation"), css=CSS, js=JS) is None
|
|
522
|
-
assert not (md.parent.parent / "user-responses").exists()
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
- [ ] **Step 2: 실패 확인** — Run: `python3 -m pytest tests/test_report_views.py -k user_responses_dir -q` → precreates FAIL
|
|
526
|
-
- [ ] **Step 3: 구현** — `render_html_view` 의 `html_path.write_text(...)` 바로 다음에:
|
|
527
|
-
|
|
528
|
-
```python
|
|
529
|
-
# 사용자 확인(폼/승인)이 필요한 보고서 — Export 파일의 저장 위치를 미리
|
|
530
|
-
# 만들어 사용자가 디렉토리를 만들 필요가 없게 한다 (reports/ 의 sibling).
|
|
531
|
-
(src_md_path.parent.parent / "user-responses").mkdir(parents=True, exist_ok=True)
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
- [ ] **Step 4: 통과 확인** — Run: `python3 -m pytest tests/test_report_views.py -q` → PASS
|
|
535
|
-
- [ ] **Step 5: Commit** — `git add scripts/okstra_ctl/report_views.py tests/test_report_views.py && git commit -m "feat(okstra-ctl/report-views): 인터랙티브 html 렌더 시 user-responses/ 디렉토리 선생성"`
|
|
536
|
-
|
|
537
|
-
---
|
|
538
|
-
|
|
539
|
-
### Task 5: sidecar APPROVAL 파서
|
|
540
|
-
|
|
541
|
-
**Files:**
|
|
542
|
-
- Modify: `scripts/okstra_ctl/clarification_items.py` (파일 끝에 추가)
|
|
543
|
-
- Test: `tests/test_clarification_sidecar_approval.py` (Create)
|
|
544
|
-
|
|
545
|
-
- [ ] **Step 1: 실패 테스트** — 새 파일 `tests/test_clarification_sidecar_approval.py`:
|
|
546
|
-
|
|
547
|
-
```python
|
|
548
|
-
import sys
|
|
549
|
-
from pathlib import Path
|
|
550
|
-
|
|
551
|
-
REPO = Path(__file__).resolve().parents[1]
|
|
552
|
-
if str(REPO / "scripts") not in sys.path:
|
|
553
|
-
sys.path.insert(0, str(REPO / "scripts"))
|
|
554
|
-
|
|
555
|
-
from okstra_ctl.clarification_items import parse_user_response_approval # noqa: E402
|
|
556
|
-
|
|
557
|
-
_SIDE = (
|
|
558
|
-
"---\n"
|
|
559
|
-
"task-key: demo/T-1\n"
|
|
560
|
-
"task-type: implementation-planning\n"
|
|
561
|
-
"seq: 002\n"
|
|
562
|
-
"source-report: runs/implementation-planning/reports/final-report-implementation-planning-002.md\n"
|
|
563
|
-
"created-by: user\n"
|
|
564
|
-
"created-at: 2026-06-12T00:00:00Z\n"
|
|
565
|
-
"---\n\n# User Response\n"
|
|
566
|
-
)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
def test_parses_approval_with_option():
|
|
570
|
-
rec = parse_user_response_approval(
|
|
571
|
-
_SIDE + "\n## APPROVAL\n- Approved: true\n- Implementation-Option: Option B — y\n"
|
|
572
|
-
)
|
|
573
|
-
assert rec is not None
|
|
574
|
-
assert rec.approved is True
|
|
575
|
-
assert rec.implementation_option == "Option B — y"
|
|
576
|
-
assert rec.seq == "002"
|
|
577
|
-
assert rec.source_report.endswith("final-report-implementation-planning-002.md")
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
def test_parses_approval_without_option():
|
|
581
|
-
rec = parse_user_response_approval(_SIDE + "\n## APPROVAL\n- Approved: true\n")
|
|
582
|
-
assert rec is not None and rec.implementation_option == ""
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def test_returns_none_without_block_or_when_not_true():
|
|
586
|
-
assert parse_user_response_approval(_SIDE + "\n## C-001\n- Kind: decision\n- Value:\n > x\n") is None
|
|
587
|
-
assert parse_user_response_approval(_SIDE + "\n## APPROVAL\n- Approved: false\n") is None
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def test_option_scoped_to_approval_block_only():
|
|
591
|
-
text = (
|
|
592
|
-
_SIDE
|
|
593
|
-
+ "\n## APPROVAL\n- Approved: true\n"
|
|
594
|
-
+ "\n## C-009\n- Kind: decision\n- Value:\n > Implementation-Option: 가짜\n"
|
|
595
|
-
)
|
|
596
|
-
rec = parse_user_response_approval(text)
|
|
597
|
-
assert rec is not None and rec.implementation_option == ""
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
- [ ] **Step 2: 실패 확인** — Run: `python3 -m pytest tests/test_clarification_sidecar_approval.py -q` → ImportError FAIL
|
|
601
|
-
- [ ] **Step 3: 구현** — `clarification_items.py` 파일 끝에 추가:
|
|
602
|
-
|
|
603
|
-
```python
|
|
604
|
-
# --------------------------------------------------------------------------- #
|
|
605
|
-
# User-response sidecar APPROVAL block (HTML Plan Approval 위젯의 Export 산출)
|
|
606
|
-
# --------------------------------------------------------------------------- #
|
|
607
|
-
|
|
608
|
-
_APPROVAL_HEADING_RE = re.compile(r"^## APPROVAL\s*$", re.MULTILINE)
|
|
609
|
-
_NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
@dataclass(frozen=True)
|
|
613
|
-
class UserResponseApprovalRecord:
|
|
614
|
-
"""sidecar 의 ``## APPROVAL`` 블록 + 매칭에 필요한 frontmatter 필드."""
|
|
615
|
-
approved: bool
|
|
616
|
-
implementation_option: str
|
|
617
|
-
source_report: str
|
|
618
|
-
seq: str
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
def parse_user_response_approval(
|
|
622
|
-
sidecar_text: str,
|
|
623
|
-
) -> Optional[UserResponseApprovalRecord]:
|
|
624
|
-
"""``## APPROVAL`` 블록이 ``- Approved: true`` 를 가질 때만 record 를
|
|
625
|
-
반환한다. 옵션 라인은 블록 내부에서만 읽는다 (다른 응답 본문의 우연한
|
|
626
|
-
동일 문구를 옵션으로 오인하지 않도록)."""
|
|
627
|
-
m = _APPROVAL_HEADING_RE.search(sidecar_text)
|
|
628
|
-
if not m:
|
|
629
|
-
return None
|
|
630
|
-
block = sidecar_text[m.end():]
|
|
631
|
-
nxt = _NEXT_RESPONSE_HEADING_RE.search(block)
|
|
632
|
-
if nxt:
|
|
633
|
-
block = block[: nxt.start()]
|
|
634
|
-
if re.search(r"^- Approved:\s*true\s*$", block, re.MULTILINE) is None:
|
|
635
|
-
return None
|
|
636
|
-
om = re.search(r"^- Implementation-Option:\s*(\S.*?)\s*$", block, re.MULTILINE)
|
|
637
|
-
sm = re.search(r"^seq:\s*(\S+)\s*$", sidecar_text, re.MULTILINE)
|
|
638
|
-
rm = re.search(r"^source-report:\s*(\S.*?)\s*$", sidecar_text, re.MULTILINE)
|
|
639
|
-
return UserResponseApprovalRecord(
|
|
640
|
-
approved=True,
|
|
641
|
-
implementation_option=om.group(1) if om else "",
|
|
642
|
-
source_report=rm.group(1) if rm else "",
|
|
643
|
-
seq=sm.group(1) if sm else "",
|
|
644
|
-
)
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
- [ ] **Step 4: 통과 확인** — Run: `python3 -m pytest tests/test_clarification_sidecar_approval.py tests/test_okstra_clarification_items.py -q` (후자가 없으면 `tests/ -k clarification`) → PASS
|
|
648
|
-
- [ ] **Step 5: Commit** — `git add scripts/okstra_ctl/clarification_items.py tests/test_clarification_sidecar_approval.py && git commit -m "feat(okstra-ctl/clarification-items): user-response sidecar APPROVAL 블록 파서 추가"`
|
|
649
|
-
|
|
650
|
-
---
|
|
651
|
-
|
|
652
|
-
### Task 6: 마법사 — sidecar 감지·확인·적용
|
|
653
|
-
|
|
654
|
-
**Files:**
|
|
655
|
-
- Modify: `scripts/okstra_ctl/wizard.py`
|
|
656
|
-
- Modify: `prompts/wizard/prompts.ko.json` (steps.approve_plan_confirm)
|
|
657
|
-
- Test: `tests/test_wizard_approve_plan_confirm.py`
|
|
658
|
-
|
|
659
|
-
- [ ] **Step 1: 실패 테스트** — `tests/test_wizard_approve_plan_confirm.py` 끝에 추가 (기존 `_load_wizard`/`_state`/`_GATE_CLEAR_BODY` 재사용):
|
|
660
|
-
|
|
661
|
-
```python
|
|
662
|
-
def _planning_run(tmp_path, *, approved: bool, option_in_sidecar: str):
|
|
663
|
-
"""runs/implementation-planning/{reports,user-responses} 구조 + APPROVAL sidecar."""
|
|
664
|
-
reports = tmp_path / "runs" / "implementation-planning" / "reports"
|
|
665
|
-
reports.mkdir(parents=True)
|
|
666
|
-
plan = reports / "final-report-implementation-planning-002.md"
|
|
667
|
-
flag = "true" if approved else "false"
|
|
668
|
-
plan.write_text(
|
|
669
|
-
f"---\napproved: {flag}\nimplementation-option:\n---\n{_GATE_CLEAR_BODY}",
|
|
670
|
-
encoding="utf-8",
|
|
671
|
-
)
|
|
672
|
-
plan.with_name(plan.name[:-3] + ".data.json").write_text(json.dumps({
|
|
673
|
-
"frontmatter": {"approved": approved},
|
|
674
|
-
"implementationPlanning": {
|
|
675
|
-
"optionCandidates": [{"name": "Option A — x"}, {"name": "Option B — y"}],
|
|
676
|
-
"recommendedOption": {"name": "Option A — x"},
|
|
677
|
-
},
|
|
678
|
-
}), encoding="utf-8")
|
|
679
|
-
ur = reports.parent / "user-responses"
|
|
680
|
-
ur.mkdir()
|
|
681
|
-
option_line = (
|
|
682
|
-
f"- Implementation-Option: {option_in_sidecar}\n" if option_in_sidecar else ""
|
|
683
|
-
)
|
|
684
|
-
(ur / "user-response-implementation-planning-002.md").write_text(
|
|
685
|
-
"---\ntask-key: demo/T-1\ntask-type: implementation-planning\nseq: 002\n"
|
|
686
|
-
f"source-report: runs/implementation-planning/reports/{plan.name}\n"
|
|
687
|
-
"created-by: user\ncreated-at: 2026-06-12T00:00:00Z\n---\n\n"
|
|
688
|
-
f"# User Response\n\n## APPROVAL\n- Approved: true\n{option_line}",
|
|
689
|
-
encoding="utf-8",
|
|
690
|
-
)
|
|
691
|
-
return plan
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
def test_plan_selection_detects_html_approval_sidecar(tmp_path):
|
|
695
|
-
wizard = _load_wizard()
|
|
696
|
-
state = _state(wizard, tmp_path)
|
|
697
|
-
plan = _planning_run(tmp_path, approved=True, option_in_sidecar="Option B — y")
|
|
698
|
-
|
|
699
|
-
wizard._submit_approved_plan(state, str(plan))
|
|
700
|
-
|
|
701
|
-
assert state.approve_plan_candidate == str(plan)
|
|
702
|
-
assert state.html_approval_sidecar.endswith("user-response-implementation-planning-002.md")
|
|
703
|
-
assert state.html_approval_option == "Option B — y"
|
|
704
|
-
prompt = wizard.STEP_BY_ID[wizard.S_APPROVE_PLAN_CONFIRM].build(state)
|
|
705
|
-
assert {o.value for o in prompt.options} == {"yes_apply", "yes", "no"}
|
|
706
|
-
assert "HTML 승인 기록" in prompt.label
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
def test_confirm_yes_apply_sets_implementation_option(tmp_path):
|
|
710
|
-
wizard = _load_wizard()
|
|
711
|
-
state = _state(wizard, tmp_path)
|
|
712
|
-
plan = _planning_run(tmp_path, approved=True, option_in_sidecar="Option B — y")
|
|
713
|
-
wizard._submit_approved_plan(state, str(plan))
|
|
714
|
-
|
|
715
|
-
echo = wizard._submit_approve_plan_confirm(state, "yes_apply")
|
|
716
|
-
|
|
717
|
-
body = plan.read_text(encoding="utf-8")
|
|
718
|
-
assert "implementation-option:" in body and "Option B — y" in body
|
|
719
|
-
assert state.approved_plan_path == str(plan)
|
|
720
|
-
assert state.html_approval_sidecar == "" and state.html_approval_option == ""
|
|
721
|
-
assert "Option B — y" in echo
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
def test_confirm_yes_ignores_sidecar_option(tmp_path):
|
|
725
|
-
wizard = _load_wizard()
|
|
726
|
-
state = _state(wizard, tmp_path)
|
|
727
|
-
plan = _planning_run(tmp_path, approved=True, option_in_sidecar="Option B — y")
|
|
728
|
-
wizard._submit_approved_plan(state, str(plan))
|
|
729
|
-
|
|
730
|
-
wizard._submit_approve_plan_confirm(state, "yes")
|
|
731
|
-
|
|
732
|
-
body = plan.read_text(encoding="utf-8")
|
|
733
|
-
assert "Option B — y" not in body # 기록 무시 → frontmatter 무변경
|
|
734
|
-
assert state.approved_plan_path == str(plan)
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def test_yes_apply_rejects_unknown_option(tmp_path):
|
|
738
|
-
wizard = _load_wizard()
|
|
739
|
-
state = _state(wizard, tmp_path)
|
|
740
|
-
plan = _planning_run(tmp_path, approved=True, option_in_sidecar="Option Z — 없음")
|
|
741
|
-
wizard._submit_approved_plan(state, str(plan))
|
|
742
|
-
|
|
743
|
-
with pytest.raises(wizard.WizardError) as excinfo:
|
|
744
|
-
wizard._submit_approve_plan_confirm(state, "yes_apply")
|
|
745
|
-
msg = str(excinfo.value)
|
|
746
|
-
assert "Option Z — 없음" in msg and "Option A — x" in msg
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
def test_seq_mismatch_sidecar_ignored(tmp_path):
|
|
750
|
-
wizard = _load_wizard()
|
|
751
|
-
state = _state(wizard, tmp_path)
|
|
752
|
-
plan = _planning_run(tmp_path, approved=True, option_in_sidecar="Option B — y")
|
|
753
|
-
ur = plan.parent.parent / "user-responses"
|
|
754
|
-
old = ur / "user-response-implementation-planning-002.md"
|
|
755
|
-
old.rename(ur / "user-response-implementation-planning-001.md")
|
|
756
|
-
text = (ur / "user-response-implementation-planning-001.md").read_text(encoding="utf-8")
|
|
757
|
-
(ur / "user-response-implementation-planning-001.md").write_text(
|
|
758
|
-
text.replace("seq: 002", "seq: 001").replace("-002.md", "-001.md"),
|
|
759
|
-
encoding="utf-8",
|
|
760
|
-
)
|
|
761
|
-
|
|
762
|
-
wizard._submit_approved_plan(state, str(plan))
|
|
763
|
-
|
|
764
|
-
assert state.html_approval_sidecar == ""
|
|
765
|
-
prompt = wizard.STEP_BY_ID[wizard.S_APPROVE_PLAN_CONFIRM].build(state)
|
|
766
|
-
assert {o.value for o in prompt.options} == {"yes", "no"}
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
- [ ] **Step 2: 실패 확인** — Run: `python3 -m pytest tests/test_wizard_approve_plan_confirm.py -q` → 신규 5건 FAIL (AttributeError `html_approval_sidecar` 등), 기존 9건 PASS 유지
|
|
770
|
-
- [ ] **Step 3: 구현** — `wizard.py`:
|
|
771
|
-
|
|
772
|
-
(a) import 블록(:41 부근)의 `from .run import (...)` 에 `_apply_cli_implementation_option,` 추가. `from .clarification_items import` 블록에 `parse_user_response_approval,` 추가 (기존 import 스타일 확인 후 동일하게).
|
|
773
|
-
|
|
774
|
-
(b) `WizardState` (:339 부근) 에 필드 추가:
|
|
775
|
-
|
|
776
|
-
```python
|
|
777
|
-
# HTML Plan Approval 위젯이 남긴 sidecar 감지 결과. 선택된 plan 과
|
|
778
|
-
# source-report·seq 가 일치하는 APPROVAL 블록이 있을 때만 채워지고,
|
|
779
|
-
# approve-confirm 단계가 3-옵션(yes_apply/yes/no)으로 확장된다.
|
|
780
|
-
html_approval_sidecar: str = ""
|
|
781
|
-
html_approval_option: str = ""
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
그리고 wizard.py:3007 부근의 state 초기값 dict 에 `"html_approval_sidecar": "", "html_approval_option": "",` 추가.
|
|
785
|
-
|
|
786
|
-
(c) `_stage_plan_for_confirmation` (:556) 의 `state.approve_plan_candidate = str(p)` 다음에:
|
|
787
|
-
|
|
788
|
-
```python
|
|
789
|
-
state.html_approval_sidecar = ""
|
|
790
|
-
state.html_approval_option = ""
|
|
791
|
-
found = _find_html_approval_sidecar(p)
|
|
792
|
-
if found is not None:
|
|
793
|
-
sidecar_path, record = found
|
|
794
|
-
state.html_approval_sidecar = str(sidecar_path)
|
|
795
|
-
state.html_approval_option = record.implementation_option
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
같은 파일 `_approve_plan_in_place` 아래에 추가:
|
|
799
|
-
|
|
800
|
-
```python
|
|
801
|
-
def _find_html_approval_sidecar(
|
|
802
|
-
plan_path: Path,
|
|
803
|
-
) -> Optional[tuple[Path, "UserResponseApprovalRecord"]]:
|
|
804
|
-
"""plan 의 run 디렉토리 sibling ``user-responses/`` 에서 APPROVAL 블록을
|
|
805
|
-
가진 sidecar 를 찾는다. source-report 파일명과 seq 가 plan 과 일치해야
|
|
806
|
-
하며, 복수면 mtime 최신을 택한다."""
|
|
807
|
-
responses_dir = plan_path.parent.parent / "user-responses"
|
|
808
|
-
if not responses_dir.is_dir():
|
|
809
|
-
return None
|
|
810
|
-
m = re.search(r"-(\d+)\.md$", plan_path.name)
|
|
811
|
-
plan_seq = m.group(1) if m else ""
|
|
812
|
-
best: Optional[tuple[float, Path, "UserResponseApprovalRecord"]] = None
|
|
813
|
-
for f in sorted(responses_dir.glob("user-response-*.md")):
|
|
814
|
-
try:
|
|
815
|
-
text = f.read_text(encoding="utf-8", errors="replace")
|
|
816
|
-
except OSError:
|
|
817
|
-
continue
|
|
818
|
-
rec = parse_user_response_approval(text)
|
|
819
|
-
if rec is None or rec.seq != plan_seq:
|
|
820
|
-
continue
|
|
821
|
-
if Path(rec.source_report).name != plan_path.name:
|
|
822
|
-
continue
|
|
823
|
-
mtime = f.stat().st_mtime
|
|
824
|
-
if best is None or mtime > best[0]:
|
|
825
|
-
best = (mtime, f, rec)
|
|
826
|
-
return (best[1], best[2]) if best else None
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
def _validate_sidecar_option(plan_path: Path, option_name: str, errors_t: dict) -> None:
|
|
830
|
-
"""sidecar 의 옵션 이름이 plan data.json 의 optionCandidates 에 있는지
|
|
831
|
-
검증한다. 없으면 유효 후보를 나열하며 거부한다 (fail-closed)."""
|
|
832
|
-
data_path = plan_path.with_name(plan_path.name[: -len(".md")] + ".data.json")
|
|
833
|
-
candidates: list[str] = []
|
|
834
|
-
if data_path.is_file():
|
|
835
|
-
try:
|
|
836
|
-
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
837
|
-
planning = data.get("implementationPlanning") or {}
|
|
838
|
-
candidates = [
|
|
839
|
-
c.get("name", "") for c in planning.get("optionCandidates") or []
|
|
840
|
-
if isinstance(c, dict) and c.get("name")
|
|
841
|
-
]
|
|
842
|
-
except (OSError, json.JSONDecodeError):
|
|
843
|
-
candidates = []
|
|
844
|
-
if option_name not in candidates:
|
|
845
|
-
raise WizardError(
|
|
846
|
-
errors_t["unknown_option"].format(
|
|
847
|
-
option=option_name,
|
|
848
|
-
candidates=", ".join(candidates) if candidates else "(없음)",
|
|
849
|
-
)
|
|
850
|
-
)
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
(`json` / `re` 는 wizard.py 에 이미 import 되어 있는지 확인; 없으면 추가. `UserResponseApprovalRecord` 는 타입 주석용 — import 에 포함.)
|
|
854
|
-
|
|
855
|
-
(d) `_build_approve_plan_confirm` (:1605) 교체:
|
|
856
|
-
|
|
857
|
-
```python
|
|
858
|
-
def _build_approve_plan_confirm(state: WizardState) -> Prompt:
|
|
859
|
-
t = _p(state.workspace_root, "approve_plan_confirm",
|
|
860
|
-
path=state.approve_plan_candidate)
|
|
861
|
-
label = t["label"]
|
|
862
|
-
options_map = t["options"]
|
|
863
|
-
if state.html_approval_sidecar:
|
|
864
|
-
label += t["html_approval_note"].format(
|
|
865
|
-
sidecar=state.html_approval_sidecar,
|
|
866
|
-
option=state.html_approval_option or "(추천 옵션 그대로)",
|
|
867
|
-
)
|
|
868
|
-
options_map = t["options_html_approval"]
|
|
869
|
-
return Prompt(
|
|
870
|
-
step=S_APPROVE_PLAN_CONFIRM, kind="pick",
|
|
871
|
-
label=label,
|
|
872
|
-
options=[_opt(k, v) for k, v in options_map.items()],
|
|
873
|
-
echo_template=t["echo_template"],
|
|
874
|
-
)
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
(e) `_submit_approve_plan_confirm` (:1616) 교체 — `yes_apply` 분기 추가:
|
|
878
|
-
|
|
879
|
-
```python
|
|
880
|
-
def _submit_approve_plan_confirm(state: WizardState, value: str) -> Optional[str]:
|
|
881
|
-
allowed = ("yes_apply", "yes", "no") if state.html_approval_sidecar else ("yes", "no")
|
|
882
|
-
if value not in allowed:
|
|
883
|
-
raise WizardError(f"expected one of {allowed}, got: {value!r}")
|
|
884
|
-
candidate = state.approve_plan_candidate
|
|
885
|
-
if not candidate:
|
|
886
|
-
raise WizardError("approve-plan: no candidate plan to approve")
|
|
887
|
-
t = _p(state.workspace_root, "approve_plan_confirm", path=candidate)
|
|
888
|
-
if value == "no":
|
|
889
|
-
# Declining leaves the candidate set so the confirm step re-prompts;
|
|
890
|
-
# implementation cannot proceed without choosing to proceed.
|
|
891
|
-
raise WizardError(t["errors"]["declined"])
|
|
892
|
-
apply_option = value == "yes_apply" and bool(state.html_approval_option)
|
|
893
|
-
p = Path(candidate)
|
|
894
|
-
if apply_option:
|
|
895
|
-
# 승인 플립 전에 옵션 유효성부터 검증한다 — 무효 옵션으로 plan 이
|
|
896
|
-
# 절반만(승인만) 적용되는 상태를 만들지 않는다.
|
|
897
|
-
_validate_sidecar_option(p, state.html_approval_option, t["errors"])
|
|
898
|
-
resolved, fully_approved = _classify_approved_plan(
|
|
899
|
-
str(p), Path(state.project_root))
|
|
900
|
-
if not fully_approved:
|
|
901
|
-
# Not yet approved → flip data.json (SSOT) + re-render, then re-verify.
|
|
902
|
-
_approve_plan_in_place(p)
|
|
903
|
-
resolved, fully_approved = _classify_approved_plan(
|
|
904
|
-
str(p), Path(state.project_root))
|
|
905
|
-
if not fully_approved:
|
|
906
|
-
raise WizardError(t["errors"]["still_unapproved"].format(path=resolved))
|
|
907
|
-
echo = t["echo_variants"]["approved"].format(path=resolved)
|
|
908
|
-
if apply_option:
|
|
909
|
-
_apply_cli_implementation_option(str(resolved), state.html_approval_option)
|
|
910
|
-
echo = t["echo_variants"]["approved_with_option"].format(
|
|
911
|
-
path=resolved, option=state.html_approval_option)
|
|
912
|
-
state.approved_plan_path = str(resolved)
|
|
913
|
-
state.approve_plan_candidate = ""
|
|
914
|
-
state.html_approval_sidecar = ""
|
|
915
|
-
state.html_approval_option = ""
|
|
916
|
-
return echo
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
주의: `_apply_cli_implementation_option` 은 `PrepareError` 를 던진다 — `_approve_plan_in_place` 의 기존 전파 방식과 동일하게 그대로 전파시킨다 (별도 try 불요).
|
|
920
|
-
|
|
921
|
-
(f) `prompts/wizard/prompts.ko.json` 의 `steps.approve_plan_confirm` 에 키 추가 (기존 키는 무변경):
|
|
922
|
-
|
|
923
|
-
```json
|
|
924
|
-
"html_approval_note": "\nHTML 승인 기록 발견: {sidecar}\n 선택 옵션: {option}\n· 예(기록 적용) — 승인과 함께 위 옵션을 frontmatter 에 기록합니다.",
|
|
925
|
-
"options_html_approval": {
|
|
926
|
-
"yes_apply": "예 — HTML 기록대로 승인 + 옵션 적용 (추천)",
|
|
927
|
-
"yes": "예 — 승인만 (HTML 기록 무시)",
|
|
928
|
-
"no": "아니오 — 진행하지 않음"
|
|
929
|
-
},
|
|
930
|
-
"echo_variants": {
|
|
931
|
-
"selected": "(기존 값 유지)",
|
|
932
|
-
"approved": "(기존 값 유지)",
|
|
933
|
-
"approved_with_option": "approved-plan: {path} (승인 + 옵션 `{option}` 적용됨)"
|
|
934
|
-
},
|
|
935
|
-
"errors": {
|
|
936
|
-
"declined": "(기존 값 유지)",
|
|
937
|
-
"still_unapproved": "(기존 값 유지)",
|
|
938
|
-
"unknown_option": "HTML 승인 기록의 옵션 `{option}` 이 plan 의 Option Candidates 에 없습니다. 유효 후보: {candidates}. 보고서 HTML 에서 다시 Export 하거나 '예 — 승인만' 을 선택하세요."
|
|
939
|
-
}
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
("(기존 값 유지)" 는 실제 JSON 에 그 문구를 쓰라는 뜻이 아니라 **기존 값을 건드리지 말라**는 지시 — `echo_variants` / `errors` 객체에 새 키만 추가한다.)
|
|
943
|
-
|
|
944
|
-
- [ ] **Step 4: 통과 확인** — Run: `python3 -m pytest tests/test_wizard_approve_plan_confirm.py tests/test_wizard_prompts.py tests/test_okstra_ctl_wizard.py -q` → PASS (prompts 스키마 테스트가 새 키를 거부하면 해당 테스트의 허용 키 목록에 추가)
|
|
945
|
-
- [ ] **Step 5: Commit** — `git add scripts/okstra_ctl/wizard.py prompts/wizard/prompts.ko.json tests/test_wizard_approve_plan_confirm.py && git commit -m "feat(okstra-ctl/wizard): HTML APPROVAL sidecar 감지 — 확인 후 승인+옵션을 기존 경로로 적용"`
|
|
946
|
-
|
|
947
|
-
---
|
|
948
|
-
|
|
949
|
-
### Task 7: 문서·빌드·전체 검증
|
|
950
|
-
|
|
951
|
-
**Files:**
|
|
952
|
-
- Modify: `templates/reports/user-response.template.md`
|
|
953
|
-
- Modify: `skills/okstra-report-writer/SKILL.md` (:108 HTML 뷰 설명)
|
|
954
|
-
- Modify: `skills/okstra-run/SKILL.md` (:126 implementation sub-flow)
|
|
955
|
-
- Modify: `CHANGES.md`
|
|
956
|
-
|
|
957
|
-
- [ ] **Step 1: sidecar 템플릿 문서** — `user-response.template.md` 의 "## 호환성 규칙" 앞에 섹션 추가:
|
|
958
|
-
|
|
959
|
-
```markdown
|
|
960
|
-
## APPROVAL 블록 (implementation-planning 전용)
|
|
961
|
-
|
|
962
|
-
Plan Approval 위젯에서 승인을 체크하고 Export 하면 본문 끝에 다음 블록이 추가됩니다:
|
|
963
|
-
|
|
964
|
-
```markdown
|
|
965
|
-
## APPROVAL
|
|
966
|
-
- Approved: true
|
|
967
|
-
- Implementation-Option: <Option Candidates 의 name 그대로>
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
- 옵션을 추천(권장) 그대로 두면 `Implementation-Option:` 라인은 생략됩니다 — implementation 은 plan 의 Recommended Option 으로 폴백합니다.
|
|
971
|
-
- 이 블록의 소비자는 implementation 시작 마법사의 approve-confirm 단계입니다 (`scripts/okstra_ctl/wizard.py`). 마법사가 사용자 확인 후 기존 `--approve` / `--implementation-option` 경로로 적용하며, sidecar 자체는 승인 게이트 검증을 우회하지 않습니다.
|
|
972
|
-
- `--resume-clarification` 은 sidecar 를 verbatim 첨부하므로 APPROVAL 블록도 그대로 실립니다 — planning 재실행 입력에서는 무해한 잔류 정보입니다.
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
(코드펜스 안의 ``` 는 실제 파일에서는 일반 ``` 로.)
|
|
976
|
-
|
|
977
|
-
- [ ] **Step 2: 스킬 문서 2곳** — `okstra-report-writer/SKILL.md` :108 의 HTML 뷰 설명 문장에 생성 조건 확장과 위젯을 반영: "**generated only when the report has at least one §5 `C-*` clarification row**" 를 "**generated when the report has at least one §1 `C-*` clarification row OR an implementation-planning Plan Approval widget target (sibling data.json 의 optionCandidates)**" 로 교체하고, 같은 항목에 한 문장 추가: "implementation-planning 보고서에는 Plan Approval 섹션(옵션 select + 승인 체크)이 렌더되며, Export sidecar 의 `## APPROVAL` 블록은 implementation 마법사가 확인 후 적용한다." `okstra-run/SKILL.md` :126 의 implementation sub-flow 항목에 추가: "approved-plan 선택 시 user-responses/ 의 HTML APPROVAL sidecar 를 감지하면 confirm 단계가 3-옵션(yes_apply 추천/yes/no)으로 확장된다."
|
|
978
|
-
- [ ] **Step 3: CHANGES.md** — `## 2026-06-12` 섹션에 항목 추가 (기존 항목 위):
|
|
979
|
-
|
|
980
|
-
```markdown
|
|
981
|
-
### feat(okstra-ctl/report-views, wizard): final-report HTML 에서 plan 승인·구현 옵션 선택 지원
|
|
982
|
-
|
|
983
|
-
- **배경**: planning 보고서 승인은 frontmatter 수동 편집 또는 `--approve` 플래그, 옵션 선택은 `--implementation-option` 플래그뿐이라 보고서를 검토하는 HTML 화면에서 곧바로 의사를 기록할 수 없었다.
|
|
984
|
-
- **해결**: implementation-planning HTML 에 Plan Approval 섹션(옵션 select — 권장 기본값, 승인 체크; §1 Blocks=approval 미해소 시 disabled) 추가. Export sidecar 에 `## APPROVAL` 블록으로 직렬화하고, implementation 마법사 approve-confirm 단계가 감지해 사용자 확인 후 기존 `--approve`/`--implementation-option` 경로로 적용. 인터랙티브 html 렌더 시 `user-responses/` 디렉토리를 선생성. 강제 지점은 report-views/wizard 단위 테스트와 기존 승인 게이트(`_validate_approved_plan`) 무변경 유지.
|
|
985
|
-
- 사용자 영향: 다음 release 후 planning 보고서 HTML 에서 승인·옵션을 체크해 Export 하고, implementation 시작 마법사에서 "예 — HTML 기록대로" 한 번으로 승인+옵션이 적용된다.
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
- [ ] **Step 4: 빌드 + 전체 테스트** — Run: `npm run build && python3 -m pytest tests/ -q` → 전체 PASS, `bash validators/validate-workflow.sh` 도 PASS
|
|
989
|
-
- [ ] **Step 5: 실물 검증** — dev-9186 planning 보고서로 재렌더해 위젯 확인:
|
|
990
|
-
|
|
991
|
-
```bash
|
|
992
|
-
python3 scripts/okstra-render-report-views.py \
|
|
993
|
-
"/Volumes/Workspaces/workspace/projects/FontsNinja/app/fontradar-v2-api/.okstra/tasks/calcule-des-prix-1-1/dev-9186/runs/implementation-planning/reports/final-report-implementation-planning-002.md"
|
|
994
|
-
grep -c 'id="plan-approval"' .../final-report-implementation-planning-002.html # expect 1
|
|
995
|
-
ls .../runs/implementation-planning/user-responses/ # 디렉토리 존재
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
(해당 보고서는 §1 에 open Blocks=approval 행이 있으므로 위젯이 **disabled + 사유 표시** 로 렌더되는 것까지 확인.)
|
|
999
|
-
|
|
1000
|
-
- [ ] **Step 6: Commit** — `git add templates/reports/user-response.template.md skills/okstra-report-writer/SKILL.md skills/okstra-run/SKILL.md CHANGES.md && git commit -m "docs(reports,skills): HTML Plan Approval 흐름 문서화 + CHANGES 기록"`
|