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.
- package/README.kr.md +27 -19
- package/README.md +27 -19
- package/docs/kr/architecture.md +59 -45
- package/docs/kr/cli.md +61 -18
- package/docs/pr-template-usage.md +65 -0
- package/docs/project-structure-overview.md +353 -354
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
- package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +1700 -0
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
- package/docs/task-process/README.md +74 -0
- package/docs/task-process/common-flow.md +166 -0
- package/docs/task-process/error-analysis.md +101 -0
- package/docs/task-process/final-verification.md +167 -0
- package/docs/task-process/implementation-planning.md +128 -0
- package/docs/task-process/implementation.md +149 -0
- package/docs/task-process/release-handoff.md +206 -0
- package/docs/task-process/requirements-discovery.md +115 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +30 -7
- package/runtime/agents/workers/claude-worker.md +31 -6
- package/runtime/agents/workers/codex-worker.md +37 -10
- package/runtime/agents/workers/gemini-worker.md +34 -7
- package/runtime/agents/workers/report-writer-worker.md +19 -10
- package/runtime/bin/okstra-central.sh +6 -6
- package/runtime/bin/okstra-codex-exec.sh +49 -28
- package/runtime/bin/okstra-gemini-exec.sh +39 -21
- package/runtime/bin/okstra-render-final-report.py +13 -2
- package/runtime/bin/okstra-wrapper-status.py +155 -0
- package/runtime/bin/okstra.sh +2 -2
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/profiles/_common-contract.md +11 -6
- package/runtime/prompts/profiles/_implementation-deliverable.md +53 -0
- package/runtime/prompts/profiles/_implementation-executor.md +60 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +76 -0
- package/runtime/prompts/profiles/error-analysis.md +3 -7
- package/runtime/prompts/profiles/implementation-planning.md +22 -21
- package/runtime/prompts/profiles/implementation.md +28 -118
- package/runtime/prompts/profiles/improvement-discovery.md +42 -0
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -12
- package/runtime/prompts/wizard/prompts.ko.json +230 -0
- package/runtime/python/lib/okstra/cli.sh +2 -49
- package/runtime/python/lib/okstra/globals.sh +21 -21
- package/runtime/python/lib/okstra/interactive.sh +7 -7
- package/runtime/python/okstra_ctl/clarification_items.py +3 -9
- package/runtime/python/okstra_ctl/consumers.py +53 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
- package/runtime/python/okstra_ctl/i18n.py +73 -0
- package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
- package/runtime/python/okstra_ctl/index.py +1 -1
- package/runtime/python/okstra_ctl/paths.py +26 -20
- package/runtime/python/okstra_ctl/render.py +166 -207
- package/runtime/python/okstra_ctl/render_final_report.py +53 -10
- package/runtime/python/okstra_ctl/run.py +299 -108
- package/runtime/python/okstra_ctl/run_context.py +22 -0
- package/runtime/python/okstra_ctl/seeding.py +186 -0
- package/runtime/python/okstra_ctl/session.py +65 -7
- package/runtime/python/okstra_ctl/wizard.py +348 -127
- package/runtime/python/okstra_ctl/workflow.py +21 -2
- package/runtime/python/okstra_ctl/worktree.py +54 -1
- package/runtime/python/okstra_project/resolver.py +4 -3
- package/runtime/python/okstra_token_usage/report.py +2 -2
- package/runtime/schemas/final-report-v1.0.schema.json +22 -16
- package/runtime/skills/okstra-brief/SKILL.md +102 -218
- package/runtime/skills/okstra-convergence/SKILL.md +2 -3
- package/runtime/skills/okstra-inspect/SKILL.md +581 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
- package/runtime/skills/okstra-run/SKILL.md +8 -7
- package/runtime/skills/okstra-schedule/SKILL.md +14 -157
- package/runtime/skills/okstra-setup/SKILL.md +28 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +16 -107
- package/runtime/templates/okstra.CLAUDE.md +104 -0
- package/runtime/templates/reports/brief.template.md +204 -0
- package/runtime/templates/reports/final-report.template.md +93 -98
- package/runtime/templates/reports/i18n/en.json +135 -0
- package/runtime/templates/reports/i18n/ko.json +135 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
- package/runtime/templates/reports/schedule.template.md +12 -3
- package/runtime/templates/reports/task-brief.template.md +2 -2
- package/runtime/templates/worker-prompt-preamble.md +108 -0
- package/runtime/validators/lib/fixtures.sh +30 -0
- package/runtime/validators/lib/runners.sh +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +211 -0
- package/runtime/validators/validate-run.py +121 -26
- package/runtime/validators/validate-workflow.sh +2 -2
- package/runtime/validators/validate_improvement_report.py +275 -0
- package/src/config.mjs +18 -0
- package/src/install.mjs +41 -14
- package/src/setup.mjs +133 -1
- package/src/uninstall.mjs +27 -3
- package/runtime/skills/okstra-history/SKILL.md +0 -165
- package/runtime/skills/okstra-logs/SKILL.md +0 -173
- package/runtime/skills/okstra-report-finder/SKILL.md +0 -111
- package/runtime/skills/okstra-status/SKILL.md +0 -246
- 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
|
+
어떤 방식으로 진행할까요?
|