okstra 0.34.1 → 0.36.1

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 (108) hide show
  1. package/README.kr.md +27 -19
  2. package/README.md +27 -19
  3. package/docs/kr/architecture.md +59 -45
  4. package/docs/kr/cli.md +61 -18
  5. package/docs/pr-template-usage.md +65 -0
  6. package/docs/project-structure-overview.md +353 -354
  7. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
  8. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
  9. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
  10. package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
  11. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
  12. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
  13. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
  14. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
  15. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
  16. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +1700 -0
  17. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
  18. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
  19. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
  20. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
  21. package/docs/task-process/README.md +74 -0
  22. package/docs/task-process/common-flow.md +166 -0
  23. package/docs/task-process/error-analysis.md +101 -0
  24. package/docs/task-process/final-verification.md +167 -0
  25. package/docs/task-process/implementation-planning.md +128 -0
  26. package/docs/task-process/implementation.md +149 -0
  27. package/docs/task-process/release-handoff.md +206 -0
  28. package/docs/task-process/requirements-discovery.md +115 -0
  29. package/package.json +1 -1
  30. package/runtime/BUILD.json +2 -2
  31. package/runtime/agents/SKILL.md +30 -7
  32. package/runtime/agents/workers/claude-worker.md +31 -6
  33. package/runtime/agents/workers/codex-worker.md +37 -10
  34. package/runtime/agents/workers/gemini-worker.md +34 -7
  35. package/runtime/agents/workers/report-writer-worker.md +19 -10
  36. package/runtime/bin/okstra-central.sh +6 -6
  37. package/runtime/bin/okstra-codex-exec.sh +49 -28
  38. package/runtime/bin/okstra-gemini-exec.sh +39 -21
  39. package/runtime/bin/okstra-render-final-report.py +13 -2
  40. package/runtime/bin/okstra-wrapper-status.py +155 -0
  41. package/runtime/bin/okstra.sh +2 -2
  42. package/runtime/prompts/launch.template.md +1 -0
  43. package/runtime/prompts/profiles/_common-contract.md +11 -6
  44. package/runtime/prompts/profiles/_implementation-deliverable.md +53 -0
  45. package/runtime/prompts/profiles/_implementation-executor.md +60 -0
  46. package/runtime/prompts/profiles/_implementation-verifier.md +76 -0
  47. package/runtime/prompts/profiles/error-analysis.md +3 -7
  48. package/runtime/prompts/profiles/implementation-planning.md +22 -21
  49. package/runtime/prompts/profiles/implementation.md +28 -118
  50. package/runtime/prompts/profiles/improvement-discovery.md +42 -0
  51. package/runtime/prompts/profiles/release-handoff.md +1 -1
  52. package/runtime/prompts/profiles/requirements-discovery.md +8 -12
  53. package/runtime/prompts/wizard/prompts.ko.json +230 -0
  54. package/runtime/python/lib/okstra/cli.sh +2 -49
  55. package/runtime/python/lib/okstra/globals.sh +21 -21
  56. package/runtime/python/lib/okstra/interactive.sh +7 -7
  57. package/runtime/python/okstra_ctl/clarification_items.py +3 -9
  58. package/runtime/python/okstra_ctl/consumers.py +53 -0
  59. package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
  60. package/runtime/python/okstra_ctl/i18n.py +73 -0
  61. package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
  62. package/runtime/python/okstra_ctl/index.py +1 -1
  63. package/runtime/python/okstra_ctl/paths.py +26 -20
  64. package/runtime/python/okstra_ctl/render.py +166 -207
  65. package/runtime/python/okstra_ctl/render_final_report.py +53 -10
  66. package/runtime/python/okstra_ctl/run.py +299 -108
  67. package/runtime/python/okstra_ctl/run_context.py +22 -0
  68. package/runtime/python/okstra_ctl/seeding.py +186 -0
  69. package/runtime/python/okstra_ctl/session.py +65 -7
  70. package/runtime/python/okstra_ctl/wizard.py +348 -127
  71. package/runtime/python/okstra_ctl/workflow.py +21 -2
  72. package/runtime/python/okstra_ctl/worktree.py +54 -1
  73. package/runtime/python/okstra_project/resolver.py +4 -3
  74. package/runtime/python/okstra_token_usage/report.py +2 -2
  75. package/runtime/schemas/final-report-v1.0.schema.json +22 -16
  76. package/runtime/skills/okstra-brief/SKILL.md +102 -218
  77. package/runtime/skills/okstra-convergence/SKILL.md +2 -3
  78. package/runtime/skills/okstra-inspect/SKILL.md +581 -0
  79. package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
  80. package/runtime/skills/okstra-run/SKILL.md +8 -7
  81. package/runtime/skills/okstra-schedule/SKILL.md +14 -157
  82. package/runtime/skills/okstra-setup/SKILL.md +28 -1
  83. package/runtime/skills/okstra-team-contract/SKILL.md +16 -107
  84. package/runtime/templates/okstra.CLAUDE.md +104 -0
  85. package/runtime/templates/reports/brief.template.md +204 -0
  86. package/runtime/templates/reports/final-report.template.md +93 -98
  87. package/runtime/templates/reports/i18n/en.json +135 -0
  88. package/runtime/templates/reports/i18n/ko.json +135 -0
  89. package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
  90. package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
  91. package/runtime/templates/reports/schedule.template.md +12 -3
  92. package/runtime/templates/reports/task-brief.template.md +2 -2
  93. package/runtime/templates/worker-prompt-preamble.md +108 -0
  94. package/runtime/validators/lib/fixtures.sh +30 -0
  95. package/runtime/validators/lib/runners.sh +1 -1
  96. package/runtime/validators/validate-implementation-plan-stages.py +211 -0
  97. package/runtime/validators/validate-run.py +121 -26
  98. package/runtime/validators/validate-workflow.sh +2 -2
  99. package/runtime/validators/validate_improvement_report.py +275 -0
  100. package/src/config.mjs +18 -0
  101. package/src/install.mjs +41 -14
  102. package/src/setup.mjs +133 -1
  103. package/src/uninstall.mjs +27 -3
  104. package/runtime/skills/okstra-history/SKILL.md +0 -165
  105. package/runtime/skills/okstra-logs/SKILL.md +0 -173
  106. package/runtime/skills/okstra-report-finder/SKILL.md +0 -111
  107. package/runtime/skills/okstra-status/SKILL.md +0 -246
  108. package/runtime/skills/okstra-time-summary/SKILL.md +0 -172
@@ -0,0 +1,1007 @@
1
+ # okstra-run Lead Prompt Token SOT (Phase B1) Implementation Plan
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:** `claude-execution-prompt.md` 의 `{{TOKEN}}` 들이 `prompts/launch.template.md` 단독 권위가 되도록 만든다. 렌더러의 hand-maintained mapping dict 제거 + ctx 키 = token 명 정합화. 누락 시 fail-fast.
6
+
7
+ **Architecture:**
8
+ 1. ctx producer 측 키 이름을 token 이름에 맞춰 일괄 rename (`*_FILE`/`*_DIR`/`*_SCRIPT` → `*_PATH`, `*_DISPLAY` 접미사 제거 등 — design 문서 2.2 표). 이 시점에서 옛 `render_template_file` 의 mapping dict 도 새 키를 읽도록 갱신 — 동작 동일성 유지.
9
+ 2. 기존 `render_template_file` 내부의 compute 블록(team_creation_gate 등)을 `inject_lead_prompt_computed_tokens(ctx)` 로 분리해 ctx 에 미리 머지.
10
+ 3. 신규 `render_template_with_ctx(template, out, ctx)` 가 정규식으로 토큰 추출 → `ctx[token]` 직접 lookup → 미존재 시 `RenderError`.
11
+ 4. caller 를 새 함수로 전환, 옛 `render_template_file` 제거.
12
+
13
+ **Tech Stack:** Python 3 (stdlib `re`, `pathlib`), pytest, bash (e2e scenario).
14
+
15
+ **Reference:** [docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md](../specs/2026-05-20-okstra-run-prompt-sot-design.md) (Phase B1 = 본 플랜의 범위. Phase A1 은 별도 플랜)
16
+
17
+ ---
18
+
19
+ ## File Structure
20
+
21
+ **생성**
22
+ - `scripts/okstra_ctl/render.py` 안에 두 함수 신규 추가 (Task 2/3):
23
+ - `inject_lead_prompt_computed_tokens(ctx)` — C3 / C4 (computed + default) 를 ctx 에 머지
24
+ - `render_template_with_ctx(template_path, output_path, ctx)` — pure ctx[token] lookup 렌더러
25
+ - `tests/test_render_inject_computed_tokens.py` — inject 함수 단위 테스트
26
+ - `tests/test_render_template_with_ctx.py` — 신규 렌더러 단위 테스트
27
+ - `tests/test_lead_prompt_token_resolution.py` — 합성 ctx 로 실제 `launch.template.md` 렌더 → 미치환 토큰 0 검증 (CI 게이트)
28
+
29
+ **수정**
30
+ - `scripts/okstra_ctl/render.py` — Task 1 에서 mapping dict 의 ctx lookup 키 rename; Task 5 에서 mapping dict 와 옛 `render_template_file` 함수 자체 제거 + CLI dispatcher 의 `template` subcommand 가 새 함수 호출
31
+ - `scripts/okstra_ctl/run.py` — Task 1 에서 ctx 빌드 시 새 키 이름 사용; Task 5 에서 `render_template_file(prompt_template, ...)` 호출을 새 두 함수 호출로 교체
32
+ - `scripts/okstra_ctl/paths.py` — Task 1: ctx 키 rename
33
+ - `scripts/okstra_ctl/index.py` — Task 1: ctx 키 rename
34
+ - `scripts/lib/okstra/cli.sh`, `scripts/lib/okstra/globals.sh`, `scripts/lib/okstra/interactive.sh`, `scripts/okstra.sh`, `scripts/okstra-central.sh` — Task 1: 옛 키 이름으로 ctx-render-context 를 읽는 shell 경로 rename
35
+ - `validators/validate-workflow.sh`, `validators/lib/runners.sh` — Task 1: 옛 키 참조 rename
36
+ - 영향받는 기존 테스트 (Task 1 안에서 함께 rename): `tests/test_okstra_run_context.py`, `tests/test_render_phase_blocks.py`, `tests/test_okstra_central_record_start.py`, `tests/test_okstra_ctl_frontmatter.py`, `tests/test_plan_body_verification.py`
37
+ - `tests-e2e/scenario-01-record-start-reconcile.sh` (또는 다른 시나리오 하나) — Task 6: 렌더된 `claude-execution-prompt.md` 에 `{{...}}` 잔여 없음 grep 게이트 추가
38
+ - `CHANGES.md` — Task 7: 사용자 영향 줄 추가
39
+
40
+ ---
41
+
42
+ ## Task 1: ctx 키 rename — 한 commit 으로 원자적
43
+
44
+ **Files:**
45
+ - Modify: `scripts/okstra_ctl/paths.py`
46
+ - Modify: `scripts/okstra_ctl/run.py`
47
+ - Modify: `scripts/okstra_ctl/render.py` — mapping dict 의 ctx lookup 키 + 다른 render_* 함수의 옛 키
48
+ - Modify: `scripts/okstra_ctl/index.py`
49
+ - Modify: `scripts/lib/okstra/cli.sh`, `scripts/lib/okstra/globals.sh`, `scripts/lib/okstra/interactive.sh`
50
+ - Modify: `scripts/okstra.sh`, `scripts/okstra-central.sh`
51
+ - Modify: `validators/validate-workflow.sh`, `validators/lib/runners.sh`
52
+ - Modify: `tests/test_okstra_run_context.py`, `tests/test_render_phase_blocks.py`, `tests/test_okstra_central_record_start.py`, `tests/test_okstra_ctl_frontmatter.py`, `tests/test_plan_body_verification.py`
53
+
54
+ 이 task 는 **behavior-preserving refactor** — 동작 동일성 검증은 기존 pytest 회귀가 담당. 새 테스트는 추가하지 않음.
55
+
56
+ **Rename 표** (design 문서 2.2 — 좌측 → 우측):
57
+
58
+ ```
59
+ TASK_MANIFEST_FILE → TASK_MANIFEST_PATH
60
+ TASK_INDEX_FILE → TASK_INDEX_PATH
61
+ INSTRUCTION_SET_DIR → INSTRUCTION_SET_PATH
62
+ RUN_MANIFEST_FILE → RUN_MANIFEST_PATH
63
+ TIMELINE_FILE → TIMELINE_PATH
64
+ FINAL_REPORT_FILE → FINAL_REPORT_PATH
65
+ FINAL_STATUS_FILE → FINAL_STATUS_PATH
66
+ TEAM_STATE_FILE → TEAM_STATE_PATH
67
+ WORKER_RESULTS_DIR → WORKER_RESULTS_PATH
68
+ RUN_VALIDATOR_SCRIPT → RUN_VALIDATOR_PATH
69
+ RUN_ERRORS_LOG_FILE → RUN_ERRORS_LOG_PATH
70
+ CLAUDE_WORKER_ERRORS_SIDECAR_FILE → CLAUDE_WORKER_ERRORS_SIDECAR_PATH
71
+ CODEX_WORKER_ERRORS_SIDECAR_FILE → CODEX_WORKER_ERRORS_SIDECAR_PATH
72
+ GEMINI_WORKER_ERRORS_SIDECAR_FILE → GEMINI_WORKER_ERRORS_SIDECAR_PATH
73
+ REPORT_WRITER_WORKER_ERRORS_SIDECAR_FILE → REPORT_WRITER_WORKER_ERRORS_SIDECAR_PATH
74
+ LEAD_MODEL_DISPLAY → LEAD_MODEL
75
+ CLAUDE_WORKER_MODEL_DISPLAY → CLAUDE_WORKER_MODEL
76
+ CODEX_WORKER_MODEL_DISPLAY → CODEX_WORKER_MODEL
77
+ GEMINI_WORKER_MODEL_DISPLAY → GEMINI_WORKER_MODEL
78
+ REPORT_WRITER_MODEL_DISPLAY → REPORT_WRITER_MODEL
79
+ FINAL_REPORT_TEMPLATE_FILE → FINAL_REPORT_TEMPLATE_PATH
80
+ CLARIFICATION_RESPONSE_FILE → CLARIFICATION_RESPONSE_PATH
81
+ CLAUDE_RESUME_COMMAND_FILE → CLAUDE_RESUME_COMMAND_PATH
82
+ ANALYSIS_TYPE → TASK_TYPE
83
+ REVIEW_PROFILE → ANALYSIS_PROFILE
84
+ SELECTED_REVIEWERS → RECOMMENDED_ANALYSERS
85
+ ```
86
+
87
+ 모든 매핑은 `\b` 단어 경계 기반 string replace.
88
+
89
+ - [ ] **Step 1: Pre-flight grep — 옛 키들이 다른 식별자의 prefix 인지 확인**
90
+
91
+ ```bash
92
+ grep -rnP '\b(TASK_MANIFEST_FILE|TASK_INDEX_FILE|INSTRUCTION_SET_DIR|RUN_MANIFEST_FILE|TIMELINE_FILE|FINAL_REPORT_FILE|FINAL_STATUS_FILE|TEAM_STATE_FILE|WORKER_RESULTS_DIR|RUN_VALIDATOR_SCRIPT|RUN_ERRORS_LOG_FILE|LEAD_MODEL_DISPLAY|CLAUDE_WORKER_MODEL_DISPLAY|CODEX_WORKER_MODEL_DISPLAY|GEMINI_WORKER_MODEL_DISPLAY|REPORT_WRITER_MODEL_DISPLAY|FINAL_REPORT_TEMPLATE_FILE|CLARIFICATION_RESPONSE_FILE|CLAUDE_RESUME_COMMAND_FILE|REVIEW_PROFILE|SELECTED_REVIEWERS|ANALYSIS_TYPE)[A-Z_]+\b' scripts/ tests/ validators/ 2>/dev/null
93
+ ```
94
+ Expected: prefix-extended 식별자 후보가 출력됨 (예: `ANALYSIS_TYPE_SEGMENT`). 이들은 rename **대상 아님** — 본 task 의 grep 게이트에서 자연스럽게 보존됨 (단어 경계 `\b` 가 다음 글자 `_` 도 단어로 판정해 매치 안 됨). 결과를 보고 본 plan 의 메모란에 기록만 해두고 다음 단계로.
95
+
96
+ - [ ] **Step 2: paths.py rename**
97
+
98
+ `scripts/okstra_ctl/paths.py` 에서 위 표 좌측 키들을 우측으로 일괄 치환. `Edit(replace_all=true)` 또는 IDE multi-cursor 로 한 단어씩 정확히 치환. 끝나고:
99
+
100
+ ```bash
101
+ grep -nP '\b(TASK_MANIFEST_FILE|TASK_INDEX_FILE|INSTRUCTION_SET_DIR|RUN_MANIFEST_FILE|TIMELINE_FILE|FINAL_REPORT_FILE|FINAL_STATUS_FILE|TEAM_STATE_FILE|WORKER_RESULTS_DIR|RUN_VALIDATOR_SCRIPT|RUN_ERRORS_LOG_FILE|LEAD_MODEL_DISPLAY|CLAUDE_WORKER_MODEL_DISPLAY|CODEX_WORKER_MODEL_DISPLAY|GEMINI_WORKER_MODEL_DISPLAY|REPORT_WRITER_MODEL_DISPLAY|FINAL_REPORT_TEMPLATE_FILE|CLARIFICATION_RESPONSE_FILE|CLAUDE_RESUME_COMMAND_FILE|REVIEW_PROFILE|SELECTED_REVIEWERS|ANALYSIS_TYPE)\b' scripts/okstra_ctl/paths.py
102
+ ```
103
+ Expected: 0 hits.
104
+
105
+ - [ ] **Step 3: run.py rename**
106
+
107
+ `scripts/okstra_ctl/run.py` 동일 적용. ctx 에 `TASK_TYPE` 과 `ANALYSIS_TYPE` 둘 다 채우는 alias 라인이 있다면 `TASK_TYPE` 만 남기고 제거.
108
+
109
+ ```bash
110
+ grep -nP '\b(TASK_MANIFEST_FILE|TASK_INDEX_FILE|INSTRUCTION_SET_DIR|RUN_MANIFEST_FILE|TIMELINE_FILE|FINAL_REPORT_FILE|FINAL_STATUS_FILE|TEAM_STATE_FILE|WORKER_RESULTS_DIR|RUN_VALIDATOR_SCRIPT|RUN_ERRORS_LOG_FILE|LEAD_MODEL_DISPLAY|CLAUDE_WORKER_MODEL_DISPLAY|CODEX_WORKER_MODEL_DISPLAY|GEMINI_WORKER_MODEL_DISPLAY|REPORT_WRITER_MODEL_DISPLAY|FINAL_REPORT_TEMPLATE_FILE|CLARIFICATION_RESPONSE_FILE|CLAUDE_RESUME_COMMAND_FILE|REVIEW_PROFILE|SELECTED_REVIEWERS|ANALYSIS_TYPE)\b' scripts/okstra_ctl/run.py
111
+ ```
112
+ Expected: 0 hits.
113
+
114
+ - [ ] **Step 4: render.py rename — 다른 render_* 함수 + mapping dict 모두**
115
+
116
+ `scripts/okstra_ctl/render.py` 에서 동일 적용. 이번엔 `render_template_file` 함수 안의 mapping dict 도 포함 — `"{{TASK_TYPE}}": ctx.get("ANALYSIS_TYPE", "")` 같은 라인을 `"{{TASK_TYPE}}": ctx.get("TASK_TYPE", "")` 로. mapping dict 의 LHS 토큰은 그대로, RHS ctx key 만 rename.
117
+
118
+ 또한 mapping dict 의 dead/duplicate entry (예: `{{ANALYSIS_TYPE}}` 토큰 자체가 launch.template.md 에 없으면 그 entry 자체는 그대로 두지 말고 Task 5 에서 함께 제거 — 여기서는 RHS rename 만).
119
+
120
+ ```bash
121
+ grep -nP '\b(TASK_MANIFEST_FILE|TASK_INDEX_FILE|INSTRUCTION_SET_DIR|RUN_MANIFEST_FILE|TIMELINE_FILE|FINAL_REPORT_FILE|FINAL_STATUS_FILE|TEAM_STATE_FILE|WORKER_RESULTS_DIR|RUN_VALIDATOR_SCRIPT|RUN_ERRORS_LOG_FILE|LEAD_MODEL_DISPLAY|CLAUDE_WORKER_MODEL_DISPLAY|CODEX_WORKER_MODEL_DISPLAY|GEMINI_WORKER_MODEL_DISPLAY|REPORT_WRITER_MODEL_DISPLAY|FINAL_REPORT_TEMPLATE_FILE|CLARIFICATION_RESPONSE_FILE|CLAUDE_RESUME_COMMAND_FILE|REVIEW_PROFILE|SELECTED_REVIEWERS|ANALYSIS_TYPE)\b' scripts/okstra_ctl/render.py
122
+ ```
123
+ Expected: 0 hits.
124
+
125
+ - [ ] **Step 5: index.py rename**
126
+
127
+ ```bash
128
+ grep -nP '\b(TASK_MANIFEST_FILE|...전체 패턴 동일...|ANALYSIS_TYPE)\b' scripts/okstra_ctl/index.py
129
+ ```
130
+ Expected: 0.
131
+
132
+ - [ ] **Step 6: Shell entry points rename**
133
+
134
+ `scripts/okstra.sh`, `scripts/okstra-central.sh`, `scripts/lib/okstra/{cli,globals,interactive}.sh` 에서 동일 치환. 이 파일들은 보통 `jq` 또는 환경변수로 ctx-render-context JSON 의 키를 읽으므로, `.TASK_MANIFEST_FILE` → `.TASK_MANIFEST_PATH` 형태가 됨.
135
+
136
+ ```bash
137
+ grep -nE '(TASK_MANIFEST_FILE|TASK_INDEX_FILE|INSTRUCTION_SET_DIR|RUN_MANIFEST_FILE|TIMELINE_FILE|FINAL_REPORT_FILE|FINAL_STATUS_FILE|TEAM_STATE_FILE|WORKER_RESULTS_DIR|RUN_VALIDATOR_SCRIPT|RUN_ERRORS_LOG_FILE|LEAD_MODEL_DISPLAY|CLAUDE_WORKER_MODEL_DISPLAY|CODEX_WORKER_MODEL_DISPLAY|GEMINI_WORKER_MODEL_DISPLAY|REPORT_WRITER_MODEL_DISPLAY|FINAL_REPORT_TEMPLATE_FILE|CLARIFICATION_RESPONSE_FILE|CLAUDE_RESUME_COMMAND_FILE|REVIEW_PROFILE|SELECTED_REVIEWERS)' scripts/okstra.sh scripts/okstra-central.sh scripts/lib/okstra/*.sh
138
+ ```
139
+ (쉘 파일에서는 `ANALYSIS_TYPE` 이 `ANALYSIS_TYPE_SEGMENT` 같이 등장할 수 있으므로 단어 경계는 `grep -E` 의 한계상 별도 검토 — Step 1 의 결과 활용.)
140
+ Expected: 0 hits (Step 1 의 EXCLUDE 식별자 제외).
141
+
142
+ - [ ] **Step 7: validators rename**
143
+
144
+ ```bash
145
+ grep -nE '(TASK_MANIFEST_FILE|...|SELECTED_REVIEWERS)' validators/validate-workflow.sh validators/lib/runners.sh
146
+ ```
147
+ Expected: 0.
148
+
149
+ - [ ] **Step 8: Tests rename**
150
+
151
+ 기존 단위 테스트의 fixture / assertion 을 새 키로:
152
+ - `tests/test_okstra_run_context.py`
153
+ - `tests/test_render_phase_blocks.py`
154
+ - `tests/test_okstra_central_record_start.py`
155
+ - `tests/test_okstra_ctl_frontmatter.py`
156
+ - `tests/test_plan_body_verification.py`
157
+
158
+ ```bash
159
+ grep -rnP '\b(TASK_MANIFEST_FILE|TASK_INDEX_FILE|INSTRUCTION_SET_DIR|RUN_MANIFEST_FILE|TIMELINE_FILE|FINAL_REPORT_FILE|FINAL_STATUS_FILE|TEAM_STATE_FILE|WORKER_RESULTS_DIR|RUN_VALIDATOR_SCRIPT|RUN_ERRORS_LOG_FILE|LEAD_MODEL_DISPLAY|CLAUDE_WORKER_MODEL_DISPLAY|CODEX_WORKER_MODEL_DISPLAY|GEMINI_WORKER_MODEL_DISPLAY|REPORT_WRITER_MODEL_DISPLAY|FINAL_REPORT_TEMPLATE_FILE|CLARIFICATION_RESPONSE_FILE|CLAUDE_RESUME_COMMAND_FILE|REVIEW_PROFILE|SELECTED_REVIEWERS|ANALYSIS_TYPE)\b' tests/
160
+ ```
161
+ Expected: 0.
162
+
163
+ - [ ] **Step 9: 전체 회귀 통과 + 코드베이스 leftover 게이트**
164
+
165
+ ```bash
166
+ python3 -m pytest tests/ -x
167
+ ```
168
+ Expected: PASS (옛 키 회귀 없음).
169
+
170
+ 전체 leftover 게이트:
171
+
172
+ ```bash
173
+ grep -rnP '\b(TASK_MANIFEST_FILE|TASK_INDEX_FILE|INSTRUCTION_SET_DIR|RUN_MANIFEST_FILE|TIMELINE_FILE|FINAL_REPORT_FILE|FINAL_STATUS_FILE|TEAM_STATE_FILE|WORKER_RESULTS_DIR|RUN_VALIDATOR_SCRIPT|RUN_ERRORS_LOG_FILE|LEAD_MODEL_DISPLAY|CLAUDE_WORKER_MODEL_DISPLAY|CODEX_WORKER_MODEL_DISPLAY|GEMINI_WORKER_MODEL_DISPLAY|REPORT_WRITER_MODEL_DISPLAY|FINAL_REPORT_TEMPLATE_FILE|CLARIFICATION_RESPONSE_FILE|CLAUDE_RESUME_COMMAND_FILE|REVIEW_PROFILE|SELECTED_REVIEWERS|ANALYSIS_TYPE)\b' scripts/ tests/ validators/ 2>/dev/null
174
+ ```
175
+ Expected: 0 hits.
176
+
177
+ - [ ] **Step 10: Commit**
178
+
179
+ ```bash
180
+ git add -A
181
+ git commit -m "refactor(ctx): rename lead-prompt ctx keys to match token names (no behavior change)"
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Task 2: `inject_lead_prompt_computed_tokens(ctx)` 분리 (TDD)
187
+
188
+ **Files:**
189
+ - Modify: `scripts/okstra_ctl/render.py` — 기존 `render_template_file` 함수 내부 (현재 ~1358–1467 라인의 compute 블록) 의 로직을 새 함수로 분리. 분리만, 호출은 기존 `render_template_file` 에서 계속.
190
+ - Create: `tests/test_render_inject_computed_tokens.py`
191
+
192
+ 이 task commit 후에도 외부에서 본 산출물은 동일 (compute 가 inject 안으로만 이동). mapping dict 의 9 개 compute + 4 개 default 항목은 ctx 에서 미리 머지된 값을 그대로 통과시키도록 갱신.
193
+
194
+ - [ ] **Step 1: Failing test**
195
+
196
+ `tests/test_render_inject_computed_tokens.py` 생성:
197
+
198
+ ```python
199
+ """inject_lead_prompt_computed_tokens — render.py 의 compute 블록 분리 검증."""
200
+ from __future__ import annotations
201
+
202
+ from okstra_ctl.render import inject_lead_prompt_computed_tokens
203
+
204
+
205
+ def _base_ctx() -> dict:
206
+ """렌더러가 compute 블록을 채우는 데 필요한 최소 입력 (Task 1 rename 이후 키 사용)."""
207
+ return {
208
+ "TASK_KEY": "demo/task-a",
209
+ "TASK_TYPE": "error-analysis",
210
+ "PROJECT_ROOT": "/tmp/project-x",
211
+ "LEAD_MODEL": "claude-opus-4-7",
212
+ "LEAD_MODEL_EXECUTION_VALUE": "claude-opus-4-7",
213
+ "CLAUDE_WORKER_MODEL": "claude-sonnet-4-6",
214
+ "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": "claude-sonnet-4-6",
215
+ "CODEX_WORKER_MODEL": "gpt-5",
216
+ "CODEX_WORKER_MODEL_EXECUTION_VALUE": "gpt-5",
217
+ "GEMINI_WORKER_MODEL": "gemini-2.5-pro",
218
+ "GEMINI_WORKER_MODEL_EXECUTION_VALUE": "gemini-2.5-pro",
219
+ "REPORT_WRITER_MODEL": "claude-haiku-4-5",
220
+ "REPORT_WRITER_MODEL_EXECUTION_VALUE": "claude-haiku-4-5",
221
+ "RECOMMENDED_ANALYSERS": "claude,codex,gemini",
222
+ # worker result path (relative — paths.py 결과 형태)
223
+ "CLAUDE_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/claude-worker-001.md",
224
+ "CODEX_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/codex-worker-001.md",
225
+ "GEMINI_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/gemini-worker-001.md",
226
+ "REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/report-writer-worker-001.md",
227
+ }
228
+
229
+
230
+ def test_inject_adds_team_creation_gate_for_multi_worker_phase():
231
+ ctx = _base_ctx()
232
+ inject_lead_prompt_computed_tokens(ctx)
233
+ assert "TEAM_CREATION_GATE" in ctx
234
+ assert "Team Creation Gate (BLOCKING)" in ctx["TEAM_CREATION_GATE"]
235
+ assert "okstra-demo/task-a" in ctx["TEAM_CREATION_GATE"]
236
+
237
+
238
+ def test_inject_uses_single_lead_block_for_release_handoff():
239
+ ctx = _base_ctx()
240
+ ctx["TASK_TYPE"] = "release-handoff"
241
+ ctx["RECOMMENDED_ANALYSERS"] = ""
242
+ inject_lead_prompt_computed_tokens(ctx)
243
+ assert ctx["TEAM_CREATION_GATE"].startswith("## Single-Lead Phase")
244
+
245
+
246
+ def test_inject_fills_worker_role_sentences_and_table():
247
+ ctx = _base_ctx()
248
+ inject_lead_prompt_computed_tokens(ctx)
249
+ assert ctx["REQUIRED_WORKER_ROLE_SENTENCE"].startswith("- ")
250
+ assert "Claude worker" in ctx["TEAM_ROLE_LINES"]
251
+ assert "EXECUTION_STATUS_TABLE_ROWS" in ctx
252
+ assert "Claude lead" in ctx["EXECUTION_STATUS_TABLE_ROWS"]
253
+
254
+
255
+ def test_inject_sets_default_fallbacks():
256
+ ctx = _base_ctx()
257
+ inject_lead_prompt_computed_tokens(ctx)
258
+ assert ctx["VALIDATION_STATUS"] == "not-run"
259
+ assert ctx["RELATED_TASKS_BULLETS"] == "- None recorded"
260
+ assert ctx["RELATED_TASKS_INLINE"] == "None"
261
+ assert ctx["AVAILABLE_MCP_SERVERS"] # 비어있지 않음
262
+
263
+
264
+ def test_inject_preserves_existing_overrides():
265
+ ctx = _base_ctx()
266
+ ctx["VALIDATION_STATUS"] = "ok"
267
+ ctx["RELATED_TASKS_BULLETS"] = "- foo"
268
+ inject_lead_prompt_computed_tokens(ctx)
269
+ assert ctx["VALIDATION_STATUS"] == "ok"
270
+ assert ctx["RELATED_TASKS_BULLETS"] == "- foo"
271
+ ```
272
+
273
+ - [ ] **Step 2: 테스트 실패 확인**
274
+
275
+ ```bash
276
+ python3 -m pytest tests/test_render_inject_computed_tokens.py -v
277
+ ```
278
+ Expected: `ImportError: cannot import name 'inject_lead_prompt_computed_tokens'`.
279
+
280
+ - [ ] **Step 3: `inject_lead_prompt_computed_tokens` 구현**
281
+
282
+ `scripts/okstra_ctl/render.py` 의 `render_template_file` 함수 정의 위에 다음 함수 신규 추가:
283
+
284
+ ```python
285
+ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
286
+ """Populate ctx in-place with computed lead-prompt tokens (C3) and
287
+ default fallbacks (C4). This is the part of the old `render_template_file`
288
+ mapping dict that did not have a 1:1 ctx key — it now lives in ctx itself
289
+ so the new pure-lookup renderer can resolve it.
290
+
291
+ Idempotent: existing values are preserved (setdefault for C4 defaults;
292
+ plain assignment for C3 compute blocks, which are deterministic from ctx).
293
+ """
294
+ selected = _resolve_workers(ctx)
295
+ catalog = _worker_catalog(ctx)
296
+ lead_model = ctx.get("LEAD_MODEL", "")
297
+ lead_model_execution = ctx.get("LEAD_MODEL_EXECUTION_VALUE", "")
298
+
299
+ def fmt_assignment(role: str, model: str, execution: str) -> str:
300
+ if execution and execution != model:
301
+ return f"- `{role}`: `{model}` (launch value: `{execution}`)"
302
+ return f"- `{role}`: `{model}`"
303
+
304
+ worker_result_lines: list[str] = []
305
+ team_role_lines = [f" 1. `Claude lead` (assigned model: `{lead_model}`)"]
306
+ model_assignment_lines = [
307
+ fmt_assignment("Claude lead", lead_model, lead_model_execution)
308
+ ]
309
+ worker_role_labels: list[str] = []
310
+ execution_status_entries = ["`Claude lead`"]
311
+ execution_status_table_lines = [
312
+ "| 에이전트 | 역할 | 모델 | 상태 | 핵심 발견 요약 |",
313
+ "|----------|------|------|------|----------------|",
314
+ f"| Claude Code | Claude lead | {lead_model} | completed / timeout / error / not-run | 최종 synthesis 작성 상태와 핵심 판단 |",
315
+ ]
316
+ for index, worker in enumerate(selected, start=2):
317
+ m = catalog[worker]
318
+ worker_result_lines.append(
319
+ f"- {m['role']} result path: `{m['resultPath']}` (assigned model: `{m['model']}`)"
320
+ )
321
+ team_role_lines.append(
322
+ f" {index}. `{m['role']}` (assigned model: `{m['model']}`)"
323
+ )
324
+ model_assignment_lines.append(
325
+ fmt_assignment(m["role"], m["model"], m["modelExecutionValue"])
326
+ )
327
+ worker_role_labels.append(f"`{m['role']}`")
328
+ execution_status_entries.append(f"`{m['role']}`")
329
+ execution_status_table_lines.append(
330
+ f"| {m['agentLabel']} | {m['role']} | {m['model']} | completed / timeout / error / not-run | {m['role']}의 핵심 발견 요약 |"
331
+ )
332
+
333
+ if worker_role_labels:
334
+ if len(worker_role_labels) == 1:
335
+ worker_role_sentence = f"- {worker_role_labels[0]} is the required worker role."
336
+ else:
337
+ worker_role_sentence = (
338
+ f"- {', '.join(worker_role_labels[:-1])}, "
339
+ f"and {worker_role_labels[-1]} are the required worker roles."
340
+ )
341
+ preferred_results_sentence = (
342
+ f"- Aim to collect completed results from all "
343
+ f"{len(worker_role_labels)} required workers."
344
+ )
345
+ else:
346
+ worker_role_sentence = "- No worker roles were selected for this run."
347
+ preferred_results_sentence = "- No worker results are expected for this run."
348
+ worker_attempt_sentence = (
349
+ "- `Gemini worker` is mandatory to attempt for this workflow."
350
+ if "gemini" in selected
351
+ else "- `Gemini worker` is not selected for this run, so no Gemini attempt is required."
352
+ )
353
+
354
+ task_type = ctx.get("TASK_TYPE", "")
355
+ if task_type == "release-handoff" or not selected:
356
+ team_creation_gate_block = (
357
+ "## Single-Lead Phase (no team creation)\n"
358
+ "\n"
359
+ "This run is single-lead. There is no worker roster, no\n"
360
+ "`TeamCreate` call, no `Agent(...)` worker dispatch, and no\n"
361
+ "convergence loop. The Claude lead performs every step inline\n"
362
+ "(reading inputs, drafting commit / PR text when applicable,\n"
363
+ "asking the user, running git / gh, and writing the final\n"
364
+ "report). Do NOT call `TeamCreate` or dispatch any sub-agent\n"
365
+ "from this run — that would be a contract violation."
366
+ )
367
+ else:
368
+ team_creation_gate_block = (
369
+ "## Team Creation Gate (BLOCKING)\n"
370
+ "\n"
371
+ "Before any `Agent` dispatch for workers, you MUST perform Phase 3 of the\n"
372
+ '`okstra` skill (`agents/SKILL.md` → "Phase 3 — Team creation"). Skipping\n'
373
+ "this gate silently degrades the run to in-process background dispatch and\n"
374
+ "loses the Teams split-pane observability surface, even though worker\n"
375
+ "outputs may still appear correct on disk.\n"
376
+ "\n"
377
+ "Required actions, in order, regardless of how many workers are selected\n"
378
+ "for this run (roster comes from `resultContract.requiredWorkerRoles` in\n"
379
+ "`task-manifest.json` — it may be 1, 2, 3, or more workers):\n"
380
+ "\n"
381
+ "1. Invoke the `okstra-team-contract` skill and verify the selected worker\n"
382
+ " roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.\n"
383
+ f'2. Call `TeamCreate(team_name: "okstra-{ctx.get("TASK_KEY", "")}", description: ...)`.\n'
384
+ "3. Record the outcome in team-state under\n"
385
+ ' `teamCreate: { attempted: true, status: "ok" | "error", error?: <msg> }`\n'
386
+ " BEFORE any `Agent(...)` worker dispatch.\n"
387
+ "4. Only after `teamCreate` is persisted may you dispatch workers — with\n"
388
+ " `team_name` on success, or with `run_in_background: true` and no\n"
389
+ ' `team_name` ONLY when `teamCreate.status == "error"` was recorded.\n'
390
+ "\n"
391
+ 'If the Agent tool rejects a dispatch with `"team must be created first"` /\n'
392
+ '`"team을 먼저 생성하거나 team_name 없이 호출해야 합니다"`, the correct\n'
393
+ "response is to go back to step 2 — NOT to strip `team_name` and retry."
394
+ )
395
+
396
+ # C3 — compute results (deterministic from ctx, 덮어쓰기)
397
+ ctx["TEAM_CREATION_GATE"] = team_creation_gate_block
398
+ ctx["WORKER_RESULT_PATH_LINES"] = "\n".join(worker_result_lines)
399
+ ctx["MODEL_ASSIGNMENT_LINES"] = "\n".join(model_assignment_lines)
400
+ ctx["TEAM_ROLE_LINES"] = "\n".join(team_role_lines)
401
+ ctx["REQUIRED_WORKER_ROLE_SENTENCE"] = worker_role_sentence
402
+ ctx["GEMINI_ATTEMPT_SENTENCE"] = worker_attempt_sentence
403
+ ctx["PREFERRED_WORKER_RESULTS_SENTENCE"] = preferred_results_sentence
404
+ ctx["EXECUTION_STATUS_EXACT_ENTRIES"] = ", ".join(execution_status_entries)
405
+ ctx["EXECUTION_STATUS_TABLE_ROWS"] = "\n".join(execution_status_table_lines)
406
+
407
+ # C4 — defaults (override 보존)
408
+ ctx.setdefault("VALIDATION_STATUS", "not-run")
409
+ ctx.setdefault("RELATED_TASKS_BULLETS", "- None recorded")
410
+ ctx.setdefault("RELATED_TASKS_INLINE", "None")
411
+ ctx.setdefault(
412
+ "AVAILABLE_MCP_SERVERS",
413
+ build_available_mcp_servers_block(Path(ctx.get("PROJECT_ROOT", "."))),
414
+ )
415
+ ```
416
+
417
+ - [ ] **Step 4: 기존 `render_template_file` 안의 compute 블록 제거**
418
+
419
+ `scripts/okstra_ctl/render.py` 의 `render_template_file` 함수 본문 시작 직후에 `inject_lead_prompt_computed_tokens(ctx)` 호출 추가. 그 뒤 compute 코드 (selected/catalog 선언 ~ team_creation_gate_block 정의) 와 mapping dict 의 9 개 compute + 4 개 default 항목을 ctx 직접 lookup 으로 단순화:
420
+
421
+ ```python
422
+ def render_template_file(template_path: str, output_path: str, ctx: dict) -> None:
423
+ inject_lead_prompt_computed_tokens(ctx)
424
+ template = Path(template_path).read_text(encoding="utf-8")
425
+ mapping = {
426
+ # compute + default 13 항목 — 이제 ctx 에 직접 키로 존재
427
+ "{{TEAM_CREATION_GATE}}": ctx["TEAM_CREATION_GATE"],
428
+ "{{WORKER_RESULT_PATH_LINES}}": ctx["WORKER_RESULT_PATH_LINES"],
429
+ "{{MODEL_ASSIGNMENT_LINES}}": ctx["MODEL_ASSIGNMENT_LINES"],
430
+ "{{TEAM_ROLE_LINES}}": ctx["TEAM_ROLE_LINES"],
431
+ "{{REQUIRED_WORKER_ROLE_SENTENCE}}": ctx["REQUIRED_WORKER_ROLE_SENTENCE"],
432
+ "{{GEMINI_ATTEMPT_SENTENCE}}": ctx["GEMINI_ATTEMPT_SENTENCE"],
433
+ "{{PREFERRED_WORKER_RESULTS_SENTENCE}}": ctx["PREFERRED_WORKER_RESULTS_SENTENCE"],
434
+ "{{EXECUTION_STATUS_EXACT_ENTRIES}}": ctx["EXECUTION_STATUS_EXACT_ENTRIES"],
435
+ "{{EXECUTION_STATUS_TABLE_ROWS}}": ctx["EXECUTION_STATUS_TABLE_ROWS"],
436
+ "{{VALIDATION_STATUS}}": ctx["VALIDATION_STATUS"],
437
+ "{{RELATED_TASKS_BULLETS}}": ctx["RELATED_TASKS_BULLETS"],
438
+ "{{RELATED_TASKS_INLINE}}": ctx["RELATED_TASKS_INLINE"],
439
+ "{{AVAILABLE_MCP_SERVERS}}": ctx["AVAILABLE_MCP_SERVERS"],
440
+ # 나머지 ~90 개 1:1 매핑은 그대로 (Task 5 에서 일괄 제거)
441
+ # ... (기존 mapping 의 나머지 항목 그대로) ...
442
+ }
443
+ fm_ctx = dict(ctx)
444
+ fm_ctx.setdefault("DOC_TYPE", _doc_type_from_template_path(template_path))
445
+ mapping.update(_frontmatter_mapping(fm_ctx))
446
+ rendered = template
447
+ for k, v in mapping.items():
448
+ rendered = rendered.replace(k, v)
449
+ rendered = _strip_phase_blocks(rendered, ctx.get("TASK_TYPE", ""))
450
+ _write_text(Path(output_path), rendered.rstrip() + "\n")
451
+ ```
452
+
453
+ - [ ] **Step 5: 단위 테스트 + 회귀 통과 확인**
454
+
455
+ ```bash
456
+ python3 -m pytest tests/test_render_inject_computed_tokens.py -v
457
+ python3 -m pytest tests/ -x
458
+ ```
459
+ Expected: 5 PASS + 전체 회귀 PASS.
460
+
461
+ - [ ] **Step 6: Commit**
462
+
463
+ ```bash
464
+ git add scripts/okstra_ctl/render.py tests/test_render_inject_computed_tokens.py
465
+ git commit -m "refactor(render): extract lead-prompt computed tokens into inject_lead_prompt_computed_tokens()"
466
+ ```
467
+
468
+ ---
469
+
470
+ ## Task 3: `render_template_with_ctx()` 추가 (TDD)
471
+
472
+ **Files:**
473
+ - Modify: `scripts/okstra_ctl/render.py` — 새 함수 추가, 아직 caller 없음
474
+ - Create: `tests/test_render_template_with_ctx.py`
475
+
476
+ - [ ] **Step 1: Failing test**
477
+
478
+ `tests/test_render_template_with_ctx.py` 생성:
479
+
480
+ ```python
481
+ """render_template_with_ctx — pure ctx[token] 렌더러."""
482
+ from __future__ import annotations
483
+
484
+ from pathlib import Path
485
+
486
+ import pytest
487
+
488
+ from okstra_ctl.render import RenderError, render_template_with_ctx
489
+
490
+
491
+ def test_substitutes_known_tokens(tmp_path: Path):
492
+ template = tmp_path / "tpl.md"
493
+ template.write_text("project at {{PROJECT_ROOT}}\nkey: {{TASK_KEY}}\n")
494
+ out = tmp_path / "out.md"
495
+
496
+ render_template_with_ctx(str(template), str(out), {
497
+ "PROJECT_ROOT": "/p/x",
498
+ "TASK_KEY": "demo/t1",
499
+ "TASK_TYPE": "error-analysis", # phase strip 에 필요
500
+ })
501
+
502
+ text = out.read_text()
503
+ assert "project at /p/x" in text
504
+ assert "key: demo/t1" in text
505
+ assert "{{" not in text
506
+
507
+
508
+ def test_raises_on_undefined_token(tmp_path: Path):
509
+ template = tmp_path / "tpl.md"
510
+ template.write_text("missing: {{NOT_IN_CTX}}\n")
511
+ out = tmp_path / "out.md"
512
+
513
+ with pytest.raises(RenderError) as exc_info:
514
+ render_template_with_ctx(str(template), str(out), {"TASK_TYPE": ""})
515
+ assert "NOT_IN_CTX" in str(exc_info.value)
516
+ assert "tpl.md" in str(exc_info.value)
517
+ assert not out.exists() # 실패 시 출력 파일 생성 안 함
518
+
519
+
520
+ def test_phase_strip_still_applies(tmp_path: Path):
521
+ template = tmp_path / "tpl.md"
522
+ template.write_text(
523
+ "before\n"
524
+ "<!-- phase:error-analysis -->\nEA block\n<!-- /phase:error-analysis -->\n"
525
+ "<!-- phase:implementation -->\nIMPL block\n<!-- /phase:implementation -->\n"
526
+ "after\n"
527
+ )
528
+ out = tmp_path / "out.md"
529
+
530
+ render_template_with_ctx(str(template), str(out), {"TASK_TYPE": "error-analysis"})
531
+
532
+ text = out.read_text()
533
+ assert "EA block" in text
534
+ assert "IMPL block" not in text
535
+
536
+
537
+ def test_frontmatter_overlay_applied(tmp_path: Path):
538
+ """frontmatter mapping (기존 _frontmatter_mapping) 가 그대로 적용되어야 한다."""
539
+ template = tmp_path / "tpl.md"
540
+ template.write_text(
541
+ "---\n"
542
+ "title: {{DOC_TITLE}}\n"
543
+ "---\n"
544
+ "body {{PROJECT_ROOT}}\n"
545
+ )
546
+ out = tmp_path / "out.md"
547
+
548
+ render_template_with_ctx(str(template), str(out), {
549
+ "PROJECT_ROOT": "/p",
550
+ "TASK_TYPE": "",
551
+ "DOC_TITLE": "X",
552
+ })
553
+
554
+ text = out.read_text()
555
+ assert "title: X" in text
556
+ assert "body /p" in text
557
+ ```
558
+
559
+ - [ ] **Step 2: 테스트 실패 확인**
560
+
561
+ ```bash
562
+ python3 -m pytest tests/test_render_template_with_ctx.py -v
563
+ ```
564
+ Expected: 4 FAIL (`ImportError: cannot import name 'RenderError'` 또는 `'render_template_with_ctx'`).
565
+
566
+ - [ ] **Step 3: 구현**
567
+
568
+ `scripts/okstra_ctl/render.py` 상단(다른 import / exception 정의 근처)에 `RenderError` 추가:
569
+
570
+ ```python
571
+ class RenderError(Exception):
572
+ """Raised when a template references a token not present in ctx."""
573
+ ```
574
+
575
+ `render_template_file` 정의 아래에 신규 함수 추가:
576
+
577
+ ```python
578
+ _TOKEN_RE = re.compile(r"\{\{([A-Z][A-Z0-9_]*)\}\}")
579
+
580
+
581
+ def render_template_with_ctx(template_path: str, output_path: str, ctx: dict) -> None:
582
+ """Render a `{{TOKEN}}` template with pure ctx[token] lookup.
583
+
584
+ - Tokens match regex `_TOKEN_RE` (uppercase snake).
585
+ - Each token MUST exist in ctx. Missing → `RenderError` (fail-fast).
586
+ - Phase block stripping (`<!-- phase:X --> ... -->`) is applied per `ctx['TASK_TYPE']`.
587
+ - Frontmatter mapping (`_frontmatter_mapping`) is overlaid (same as legacy renderer).
588
+
589
+ Callers that need computed tokens (team_creation_gate etc.) MUST call
590
+ `inject_lead_prompt_computed_tokens(ctx)` BEFORE invoking this function.
591
+ """
592
+ template = Path(template_path).read_text(encoding="utf-8")
593
+
594
+ fm_ctx = dict(ctx)
595
+ fm_ctx.setdefault("DOC_TYPE", _doc_type_from_template_path(template_path))
596
+ fm_overlay = _frontmatter_mapping(fm_ctx) # {"{{DOC_TITLE}}": "...", ...}
597
+
598
+ # frontmatter overlay 가 채우는 키들도 lookup 대상 — 단일 lookup 으로 통일
599
+ lookup: dict[str, str] = {}
600
+ for tok_with_braces, value in fm_overlay.items():
601
+ key = tok_with_braces[2:-2] # "{{X}}" -> "X"
602
+ lookup[key] = value
603
+
604
+ rendered = template
605
+ missing: list[str] = []
606
+ for match in _TOKEN_RE.finditer(template):
607
+ token = match.group(1)
608
+ if token in lookup:
609
+ value = lookup[token]
610
+ elif token in ctx:
611
+ value = str(ctx[token])
612
+ else:
613
+ missing.append(token)
614
+ continue
615
+ rendered = rendered.replace("{{" + token + "}}", value)
616
+
617
+ if missing:
618
+ names = ", ".join(sorted(set(missing)))
619
+ raise RenderError(
620
+ f"undefined lead-prompt token(s): {names} (template={template_path}). "
621
+ f"Add the key(s) to ctx in run.py / inject_lead_prompt_computed_tokens()."
622
+ )
623
+
624
+ rendered = _strip_phase_blocks(rendered, ctx.get("TASK_TYPE", ""))
625
+ _write_text(Path(output_path), rendered.rstrip() + "\n")
626
+ ```
627
+
628
+ - [ ] **Step 4: 테스트 통과 확인**
629
+
630
+ ```bash
631
+ python3 -m pytest tests/test_render_template_with_ctx.py -v
632
+ python3 -m pytest tests/ -x
633
+ ```
634
+ Expected: 4 PASS + 회귀 PASS.
635
+
636
+ - [ ] **Step 5: Commit**
637
+
638
+ ```bash
639
+ git add scripts/okstra_ctl/render.py tests/test_render_template_with_ctx.py
640
+ git commit -m "feat(render): add render_template_with_ctx() pure-lookup renderer"
641
+ ```
642
+
643
+ ---
644
+
645
+ ## Task 4: launch.template.md 토큰 정적 검증 게이트
646
+
647
+ **Files:**
648
+ - Create: `tests/test_lead_prompt_token_resolution.py`
649
+
650
+ 합성 ctx (모든 토큰 키 포함) 로 실제 `prompts/launch.template.md` 를 `render_template_with_ctx` 로 dry-render → 미치환 `{{...}}` 잔여 0 확인.
651
+
652
+ - [ ] **Step 1: 합성 ctx 빌더 작성 + Failing test**
653
+
654
+ `tests/test_lead_prompt_token_resolution.py` 생성:
655
+
656
+ ```python
657
+ """launch.template.md token resolution — CI 게이트.
658
+
659
+ `prompts/launch.template.md` 안의 모든 `{{TOKEN}}` 이 ctx 로부터 해석돼야 한다.
660
+ 하나라도 누락 시 `render_template_with_ctx` 가 `RenderError` 로 fail 한다.
661
+ 이 테스트가 PASS 한다는 것은 "템플릿 추가/수정 시 ctx 빌드 코드 갱신 누락이 없다"
662
+ 는 SOT 동기화 보증.
663
+ """
664
+ from __future__ import annotations
665
+
666
+ import re
667
+ from pathlib import Path
668
+
669
+ import pytest
670
+
671
+ from okstra_ctl.render import (
672
+ RenderError,
673
+ inject_lead_prompt_computed_tokens,
674
+ render_template_with_ctx,
675
+ )
676
+
677
+ REPO_ROOT = Path(__file__).resolve().parents[1]
678
+ LAUNCH_TEMPLATE = REPO_ROOT / "prompts" / "launch.template.md"
679
+ TOKEN_RE = re.compile(r"\{\{([A-Z][A-Z0-9_]*)\}\}")
680
+
681
+
682
+ def _synthetic_ctx(task_type: str = "error-analysis") -> dict:
683
+ """launch.template.md 가 참조하는 모든 token 에 placeholder 값을 채운 ctx.
684
+
685
+ 이 함수는 `prompts/launch.template.md` 와 명시적으로 동기화돼야 한다.
686
+ 템플릿에 새 토큰을 추가할 때 본 함수에도 키 한 줄을 추가하면
687
+ `inject_lead_prompt_computed_tokens()` 와 결합해 ctx 가 완전해진다.
688
+ """
689
+ return {
690
+ "TASK_KEY": "demo/t1",
691
+ "TASK_TYPE": task_type,
692
+ "PROJECT_ID": "demo-project",
693
+ "PROJECT_ROOT": "/tmp/demo",
694
+ "CLAUDE_SESSION_ID": "session-abc",
695
+ "CLARIFICATION_RESPONSE_RELATIVE_PATH": "",
696
+ "CLAUDE_RESUME_COMMAND_RELATIVE_PATH": "runs/error-analysis/sessions/resume-001.sh",
697
+ "INSTRUCTION_SET_RELATIVE_PATH": "runs/error-analysis/instruction-set-001",
698
+ "TASK_MANIFEST_RELATIVE_PATH": "task-manifest.json",
699
+ "RUN_MANIFEST_RELATIVE_PATH": "runs/error-analysis/manifests/run-manifest-001.json",
700
+ "TEAM_STATE_RELATIVE_PATH": "runs/error-analysis/state/team-state-001.json",
701
+ "FINAL_REPORT_RELATIVE_PATH": "runs/error-analysis/reports/final-report-001.md",
702
+ "FINAL_STATUS_RELATIVE_PATH": "runs/error-analysis/status/final-001.status",
703
+ "RUN_VALIDATOR_RELATIVE_PATH": "validators/validate-run.py",
704
+ "RUN_ERRORS_LOG_PATH": "/tmp/demo/runs/error-analysis/errors-log-001.jsonl",
705
+ "RUN_ERRORS_LOG_RELATIVE_PATH": "runs/error-analysis/errors-log-001.jsonl",
706
+ "CLAUDE_WORKER_ERRORS_SIDECAR_PATH": "/tmp/demo/runs/error-analysis/sidecar-claude.jsonl",
707
+ "CODEX_WORKER_ERRORS_SIDECAR_PATH": "/tmp/demo/runs/error-analysis/sidecar-codex.jsonl",
708
+ "GEMINI_WORKER_ERRORS_SIDECAR_PATH": "/tmp/demo/runs/error-analysis/sidecar-gemini.jsonl",
709
+ "REPORT_WRITER_WORKER_ERRORS_SIDECAR_PATH": "/tmp/demo/runs/error-analysis/sidecar-report-writer.jsonl",
710
+ "EXECUTOR_WORKTREE_STATUS": "skipped-not-git",
711
+ "EXECUTOR_WORKTREE_PATH": "",
712
+ "EXECUTOR_WORKTREE_BRANCH": "",
713
+ "EXECUTOR_WORKTREE_BASE_REF": "",
714
+ "EXECUTOR_WORKTREE_NOTE": "",
715
+ "WORKFLOW_CURRENT_PHASE": task_type,
716
+ "WORKFLOW_NEXT_RECOMMENDED_PHASE": "implementation-planning",
717
+ "PHASE_ALLOWED_OUTPUTS": " - final-report",
718
+ "PHASE_FORBIDDEN_ACTIONS": " - source-code-modification",
719
+ # worker result paths (inject 가 catalog 채울 때 사용)
720
+ "CLAUDE_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/claude-worker-001.md",
721
+ "CODEX_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/codex-worker-001.md",
722
+ "GEMINI_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/gemini-worker-001.md",
723
+ "REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH": "runs/error-analysis/results/report-writer-worker-001.md",
724
+ # models
725
+ "LEAD_MODEL": "claude-opus-4-7",
726
+ "LEAD_MODEL_EXECUTION_VALUE": "claude-opus-4-7",
727
+ "CLAUDE_WORKER_MODEL": "claude-sonnet-4-6",
728
+ "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": "claude-sonnet-4-6",
729
+ "CODEX_WORKER_MODEL": "gpt-5",
730
+ "CODEX_WORKER_MODEL_EXECUTION_VALUE": "gpt-5",
731
+ "GEMINI_WORKER_MODEL": "gemini-2.5-pro",
732
+ "GEMINI_WORKER_MODEL_EXECUTION_VALUE": "gemini-2.5-pro",
733
+ "REPORT_WRITER_MODEL": "claude-haiku-4-5",
734
+ "REPORT_WRITER_MODEL_EXECUTION_VALUE": "claude-haiku-4-5",
735
+ # selection
736
+ "RECOMMENDED_ANALYSERS": "claude,codex,gemini",
737
+ }
738
+
739
+
740
+ def test_launch_template_all_tokens_resolved(tmp_path: Path):
741
+ ctx = _synthetic_ctx()
742
+ inject_lead_prompt_computed_tokens(ctx)
743
+
744
+ out = tmp_path / "claude-execution-prompt.md"
745
+ render_template_with_ctx(str(LAUNCH_TEMPLATE), str(out), ctx)
746
+
747
+ text = out.read_text(encoding="utf-8")
748
+ leftovers = sorted(set(TOKEN_RE.findall(text)))
749
+ assert not leftovers, f"unresolved tokens in rendered template: {leftovers}"
750
+
751
+
752
+ def test_launch_template_release_handoff_renders(tmp_path: Path):
753
+ """release-handoff 는 single-lead — worker 없이도 렌더돼야 함."""
754
+ ctx = _synthetic_ctx(task_type="release-handoff")
755
+ ctx["RECOMMENDED_ANALYSERS"] = ""
756
+ inject_lead_prompt_computed_tokens(ctx)
757
+
758
+ out = tmp_path / "claude-execution-prompt.md"
759
+ render_template_with_ctx(str(LAUNCH_TEMPLATE), str(out), ctx)
760
+
761
+ text = out.read_text(encoding="utf-8")
762
+ assert "Single-Lead Phase" in text
763
+ leftovers = sorted(set(TOKEN_RE.findall(text)))
764
+ assert not leftovers, f"unresolved tokens: {leftovers}"
765
+
766
+
767
+ def test_launch_template_fails_with_missing_token(tmp_path: Path):
768
+ ctx = _synthetic_ctx()
769
+ inject_lead_prompt_computed_tokens(ctx)
770
+ del ctx["TASK_KEY"] # 강제 누락
771
+
772
+ out = tmp_path / "x.md"
773
+ with pytest.raises(RenderError) as exc:
774
+ render_template_with_ctx(str(LAUNCH_TEMPLATE), str(out), ctx)
775
+ assert "TASK_KEY" in str(exc.value)
776
+ ```
777
+
778
+ - [ ] **Step 2: 테스트 실행 — 일부 실패 가능**
779
+
780
+ ```bash
781
+ python3 -m pytest tests/test_lead_prompt_token_resolution.py -v
782
+ ```
783
+
784
+ Expected: `test_launch_template_fails_with_missing_token` 은 PASS. 나머지 두 테스트는 `_synthetic_ctx()` 가 템플릿 토큰을 완전히 커버하면 PASS. 만약 FAIL 시 — 에러 메시지가 짚는 토큰을 `_synthetic_ctx()` 에 추가. inject 의 compute 항목이거나 frontmatter overlay 가 채우는 항목이면 추가 불요 (이미 채워짐).
785
+
786
+ - [ ] **Step 3: synthetic_ctx 보강 후 테스트 통과 확인**
787
+
788
+ ```bash
789
+ python3 -m pytest tests/test_lead_prompt_token_resolution.py -v
790
+ ```
791
+ Expected: 3 PASS.
792
+
793
+ - [ ] **Step 4: Commit**
794
+
795
+ ```bash
796
+ git add tests/test_lead_prompt_token_resolution.py
797
+ git commit -m "test(render): static gate — launch.template.md token resolution"
798
+ ```
799
+
800
+ ---
801
+
802
+ ## Task 5: caller 전환 + 옛 `render_template_file` 제거
803
+
804
+ **Files:**
805
+ - Modify: `scripts/okstra_ctl/render.py` — 옛 `render_template_file` 제거, CLI dispatcher 의 `template` subcommand 가 새 함수 사용
806
+ - Modify: `scripts/okstra_ctl/run.py` — lead prompt 렌더 호출 교체
807
+
808
+ - [ ] **Step 1: run.py 의 lead-prompt 렌더 caller 교체**
809
+
810
+ `scripts/okstra_ctl/run.py:778` 부근:
811
+
812
+ ```python
813
+ # 변경 전
814
+ render_template_file(
815
+ str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
816
+ )
817
+
818
+ # 변경 후
819
+ inject_lead_prompt_computed_tokens(ctx)
820
+ render_template_with_ctx(
821
+ str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
822
+ )
823
+ ```
824
+
825
+ 상단 import:
826
+
827
+ ```python
828
+ from .render import (
829
+ ...,
830
+ inject_lead_prompt_computed_tokens,
831
+ render_template_with_ctx,
832
+ )
833
+ ```
834
+
835
+ `render_template_file` import 가 다른 곳에서 쓰이지 않는다면 제거. `final_report_template` 렌더 호출 (`run.py:775`) 은 그대로 — 그 템플릿은 Jinja2 `{{ var }}` (스페이스 있음) 사용이라 `_TOKEN_RE` 와 매치 안 됨. `render_template_with_ctx` 로 교체해도 동작은 동일 (regex 가 매치 안 하면 leftover 도 없음 → 통과). SOT 일관성을 위해 함께 교체 권장:
836
+
837
+ ```python
838
+ inject_lead_prompt_computed_tokens(ctx) # 위에서 이미 호출했다면 idempotent — 중복 호출 OK
839
+ render_template_with_ctx(
840
+ str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
841
+ )
842
+ ```
843
+
844
+ - [ ] **Step 2: render.py — CLI dispatcher 갱신**
845
+
846
+ `scripts/okstra_ctl/render.py` 의 `template` subcommand:
847
+
848
+ ```python
849
+ # 변경 전
850
+ elif sub == "template":
851
+ ctx_path, template_path, output_path = rest
852
+ render_template_file(template_path, output_path, _load_ctx(ctx_path))
853
+
854
+ # 변경 후
855
+ elif sub == "template":
856
+ ctx_path, template_path, output_path = rest
857
+ ctx = _load_ctx(ctx_path)
858
+ inject_lead_prompt_computed_tokens(ctx)
859
+ render_template_with_ctx(template_path, output_path, ctx)
860
+ ```
861
+
862
+ - [ ] **Step 3: 옛 `render_template_file` 제거**
863
+
864
+ `render_template_file` 함수 전체 삭제. 미사용 import 정리.
865
+
866
+ ```bash
867
+ grep -rnE '\brender_template_file\b' scripts/ tests/
868
+ ```
869
+ Expected: 0 hits.
870
+
871
+ - [ ] **Step 4: 전체 회귀 통과**
872
+
873
+ ```bash
874
+ python3 -m pytest tests/ -x
875
+ ```
876
+ Expected: PASS.
877
+
878
+ - [ ] **Step 5: smoke run**
879
+
880
+ ```bash
881
+ node bin/okstra --version
882
+ node bin/okstra doctor
883
+ ```
884
+ Expected: 성공.
885
+
886
+ ```bash
887
+ bash tests-e2e/scenario-01-record-start-reconcile.sh
888
+ ```
889
+ Expected: PASS — 시나리오가 lead prompt 렌더에 도달하면 새 함수가 통과.
890
+
891
+ - [ ] **Step 6: Commit**
892
+
893
+ ```bash
894
+ git add scripts/okstra_ctl/render.py scripts/okstra_ctl/run.py
895
+ git commit -m "refactor(render): drop legacy render_template_file; lead prompt uses render_template_with_ctx"
896
+ ```
897
+
898
+ ---
899
+
900
+ ## Task 6: E2E grep 게이트 추가
901
+
902
+ **Files:**
903
+ - Modify: `tests-e2e/scenario-01-record-start-reconcile.sh` (가장 일반적 phase 한 개에 한정)
904
+
905
+ 렌더된 `claude-execution-prompt.md` 에 미치환 `{{...}}` 가 없는지 e2e 레벨에서도 확인.
906
+
907
+ - [ ] **Step 1: 시나리오의 변수 흐름 확인**
908
+
909
+ ```bash
910
+ grep -nE 'INSTRUCTION_SET|claude-execution-prompt' tests-e2e/scenario-01-record-start-reconcile.sh
911
+ ```
912
+ 이 결과로 시나리오가 어떤 변수에 instruction-set 경로를 담는지 (예: `INSTRUCTION_SET_DIR`, `TASK_ROOT`) 확인.
913
+
914
+ - [ ] **Step 2: scenario 끝부분에 grep 게이트 추가**
915
+
916
+ `tests-e2e/scenario-01-record-start-reconcile.sh` 의 task bundle 생성 직후 (예: `okstra render-bundle ... --render-only` 호출 직후) 다음 블록 추가 (`<INSTRUCTION_SET_VAR>` 는 Step 1 에서 식별된 시나리오 변수로 대체):
917
+
918
+ ```bash
919
+ # Lead prompt token resolution gate — `{{TOKEN}}` 잔여 0 검증
920
+ LEAD_PROMPT=$(find "$<INSTRUCTION_SET_VAR>" -name 'claude-execution-prompt.md' -type f | head -1)
921
+ if [ -z "$LEAD_PROMPT" ]; then
922
+ echo "FAIL: lead prompt not rendered" >&2
923
+ exit 1
924
+ fi
925
+ if grep -nE '\{\{[A-Z_]+\}\}' "$LEAD_PROMPT"; then
926
+ echo "FAIL: unresolved tokens in $LEAD_PROMPT" >&2
927
+ exit 1
928
+ fi
929
+ ```
930
+
931
+ - [ ] **Step 3: 시나리오 실행**
932
+
933
+ ```bash
934
+ bash tests-e2e/scenario-01-record-start-reconcile.sh
935
+ ```
936
+ Expected: PASS — 게이트가 통과.
937
+
938
+ - [ ] **Step 4: Commit**
939
+
940
+ ```bash
941
+ git add tests-e2e/scenario-01-record-start-reconcile.sh
942
+ git commit -m "test(e2e): assert lead prompt has no unresolved tokens"
943
+ ```
944
+
945
+ ---
946
+
947
+ ## Task 7: CHANGES.md + npm build 확인
948
+
949
+ **Files:**
950
+ - Modify: `CHANGES.md`
951
+
952
+ - [ ] **Step 1: CHANGES.md 항목 추가**
953
+
954
+ `CHANGES.md` 의 최상단 (또는 진행 중 섹션) 에:
955
+
956
+ ```markdown
957
+ ### Lead prompt 토큰 SOT 단일화 (2026-05-20)
958
+
959
+ - `claude-execution-prompt.md` 의 `{{TOKEN}}` 들이 `prompts/launch.template.md` 단독 권위가 되도록 변경.
960
+ - `scripts/okstra_ctl/render.py` 의 hand-maintained mapping dict 제거. ctx 키 = 토큰 명 정합화 (`*_FILE`/`*_DIR`/`*_SCRIPT` → `*_PATH`, `*_DISPLAY` 접미사 제거, `ANALYSIS_TYPE`/`REVIEW_PROFILE`/`SELECTED_REVIEWERS` 통일).
961
+ - 누락 시 `RenderError` 로 fail-fast — silent drift 차단.
962
+ - 사용자 영향: 없음 (산출물 동일). 컨트리뷰터 영향: 새 토큰 추가 시 `prompts/launch.template.md` + ctx 빌드 함수 (`paths.py` / `run.py` / `render.py` `inject_lead_prompt_computed_tokens`) 만 손대면 됨 (이전엔 mapping dict 도 같이).
963
+ ```
964
+
965
+ - [ ] **Step 2: npm build smoke**
966
+
967
+ ```bash
968
+ npm run build
969
+ ```
970
+ Expected: `runtime/` 가 최신 source 와 동기화됨. 에러 없음.
971
+
972
+ - [ ] **Step 3: 최종 회귀**
973
+
974
+ ```bash
975
+ python3 -m pytest tests/ -v
976
+ bash validators/validate-workflow.sh
977
+ bash tests-e2e/scenario-01-record-start-reconcile.sh
978
+ ```
979
+ Expected: 전부 PASS.
980
+
981
+ - [ ] **Step 4: Commit**
982
+
983
+ ```bash
984
+ git add CHANGES.md
985
+ git commit -m "docs(changes): note lead-prompt token SOT unification"
986
+ ```
987
+
988
+ ---
989
+
990
+ ## Self-Review 체크리스트
991
+
992
+ - [x] **Spec coverage**: design 문서 §2 (Phase B1) 의 모든 항목이 task 로 포함 — 2.1 토큰 카테고리 (Task 2: C3/C4 분리), 2.2 ctx 키 rename (Task 1), 2.3 데이터 흐름 (Task 2+3+5), 2.4 변경 파일 (Task 2·3·5), 2.5 검증 (Task 2·3·4·6), 2.6 마이그레이션 순서 (1→2→3→4→5→6→7), 2.7 실패모드 (Task 3 의 RenderError 테스트), 2.8 롤백 (각 task 가 별 commit — atomic revert).
993
+ - [x] **Placeholder scan**: TBD/TODO 없음. 모든 step 에 실제 명령·코드·grep 패턴 명시. Task 6 의 `<INSTRUCTION_SET_VAR>` 은 시나리오 스크립트 grep 결과로 채우는 명시적 슬롯 (placeholder 가 아니라 lookup 지시).
994
+ - [x] **Type consistency**: `inject_lead_prompt_computed_tokens(ctx) -> None` / `render_template_with_ctx(template, output, ctx) -> None` / `RenderError(Exception)` — 모든 task 에서 동일 시그니처 사용.
995
+ - [x] **Atomic rename**: Task 1 한 commit (Step 2~10) 로 모든 caller 동시 변경. caller 가 새 키로 ctx 쓰기 시작하므로 옛 키 잔존 위험 없음. Task 2 이후의 inject() 함수는 처음부터 새 키를 읽음.
996
+ - [x] **Task 순서 정합성**: rename 먼저 → inject 분리 → 새 렌더러 추가 → 정적 게이트 → caller 전환 → e2e → docs. 각 task 의 작업 commit 후에도 시스템 동작 동일성 유지 (Task 5 까지) 또는 산출물 동일 (전 task).
997
+
998
+ ---
999
+
1000
+ ## 실행 옵션
1001
+
1002
+ Plan complete and saved to [docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md](docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md). 두 가지 실행 방식:
1003
+
1004
+ 1. **Subagent-Driven (recommended)** — task 마다 새로운 subagent 가 들어가 작업, task 사이에서 review, 빠른 iteration
1005
+ 2. **Inline Execution** — 본 세션 안에서 executing-plans 로 batch 실행, checkpoint 마다 review
1006
+
1007
+ 어떤 방식으로 진행할까요?