okstra 0.76.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.kr.md +5 -3
  2. package/README.md +5 -3
  3. package/bin/okstra +1 -117
  4. package/docs/contributor-change-matrix.md +12 -0
  5. package/docs/kr/architecture.md +1 -1
  6. package/docs/kr/cli.md +22 -5
  7. package/docs/pr-template-usage.md +3 -3
  8. package/docs/project-structure-overview.md +1 -1
  9. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +1 -1
  10. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +493 -0
  11. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +358 -0
  12. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +134 -0
  13. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +284 -0
  14. package/package.json +5 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/DO_NOT_EDIT.md +21 -0
  17. package/runtime/agents/SKILL.md +3 -0
  18. package/runtime/bin/lib/okstra/cli.sh +5 -1
  19. package/runtime/bin/lib/okstra/globals.sh +1 -0
  20. package/runtime/bin/lib/okstra/usage.sh +6 -4
  21. package/runtime/bin/okstra-trace-cleanup.sh +16 -12
  22. package/runtime/bin/okstra.sh +1 -0
  23. package/runtime/prompts/launch.template.md +4 -13
  24. package/runtime/prompts/profiles/error-analysis.md +6 -0
  25. package/runtime/prompts/profiles/forbidden-actions.json +69 -0
  26. package/runtime/prompts/profiles/implementation.md +1 -8
  27. package/runtime/prompts/profiles/improvement-discovery.md +5 -0
  28. package/runtime/prompts/profiles/release-handoff.md +3 -17
  29. package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
  30. package/runtime/python/okstra_ctl/codex_dispatch.py +1552 -0
  31. package/runtime/python/okstra_ctl/context_cost.py +1 -1
  32. package/runtime/python/okstra_ctl/dispatch_core.py +897 -0
  33. package/runtime/python/okstra_ctl/doctor.py +292 -0
  34. package/runtime/python/okstra_ctl/lead_events.py +129 -0
  35. package/runtime/python/okstra_ctl/lead_runtime.py +72 -0
  36. package/runtime/python/okstra_ctl/paths.py +3 -0
  37. package/runtime/python/okstra_ctl/pr_template.py +1 -1
  38. package/runtime/python/okstra_ctl/render.py +211 -29
  39. package/runtime/python/okstra_ctl/run.py +89 -18
  40. package/runtime/python/okstra_ctl/team.py +267 -0
  41. package/runtime/python/okstra_ctl/tmux.py +181 -10
  42. package/runtime/python/okstra_ctl/workflow.py +30 -71
  43. package/runtime/python/okstra_ctl/wrapper_status.py +55 -0
  44. package/runtime/python/okstra_token_usage/codex.py +12 -7
  45. package/runtime/python/okstra_token_usage/collect.py +243 -54
  46. package/runtime/python/okstra_token_usage/gemini.py +12 -7
  47. package/runtime/schemas/final-report-v1.0.schema.json +3 -1
  48. package/runtime/skills/okstra-convergence/SKILL.md +3 -0
  49. package/runtime/skills/okstra-report-writer/SKILL.md +3 -0
  50. package/runtime/skills/okstra-setup/SKILL.md +1 -1
  51. package/runtime/skills/okstra-team-contract/SKILL.md +12 -0
  52. package/runtime/validators/forbidden_actions.py +135 -0
  53. package/runtime/validators/validate-run.py +112 -22
  54. package/runtime/validators/validate_session_conformance.py +127 -5
  55. package/src/cli-registry.mjs +277 -0
  56. package/src/codex-dispatch.mjs +70 -0
  57. package/src/codex-run.mjs +68 -0
  58. package/src/doctor.mjs +130 -5
  59. package/src/install.mjs +123 -26
  60. package/src/paths.mjs +17 -3
  61. package/src/render-bundle.mjs +2 -0
  62. package/src/runtime-manifest.mjs +29 -0
  63. package/src/team.mjs +63 -0
  64. package/src/uninstall.mjs +27 -4
  65. /package/runtime/{skills/okstra-run/templates → templates/prd}/pr-body.template.md +0 -0
@@ -0,0 +1,358 @@
1
+ # Claude Code / Codex lead adapter split design
2
+
3
+ - 날짜: 2026-06-12
4
+ - 상태: 설계 초안
5
+ - 대상: okstra runtime, installed skills, lead orchestration contract
6
+
7
+ ## 1. 배경
8
+
9
+ okstra 의 core runtime 은 이미 비교적 플랫폼 중립적이다.
10
+ `prepare_task_bundle()` 이 task bundle, manifests, prompts, worktree,
11
+ validators 를 생성하고, Node CLI 는 이 Python core 를 호출한다.
12
+
13
+ 문제는 lead 실행 계층이 Claude Code 전용 계약을 강하게 전제한다는 점이다.
14
+
15
+ - skills 는 `~/.claude/skills/` 에 설치된다.
16
+ - worker agents 는 `~/.claude/agents/` 에 설치된다.
17
+ - in-session run 은 `AskUserQuestion` 과 Claude Code slash skill 흐름을 전제한다.
18
+ - team execution 은 `TeamCreate` / `Agent(...)` / `team_name` 을 전제한다.
19
+ - resume, token usage, team reconcile 은 Claude session jsonl 과
20
+ `~/.claude/projects`, `~/.claude/teams` 를 읽는다.
21
+
22
+ 따라서 Codex 에서 같은 저장소를 사용하려면 프로젝트를 통째로 둘로 복사하는 대신
23
+ **lead adapter** 를 나눠야 한다.
24
+
25
+ ## 2. 결정
26
+
27
+ okstra 는 하나의 공통 core 와 두 lead adapter 로 분리한다.
28
+
29
+ ```text
30
+ okstra core
31
+ - prepare_task_bundle
32
+ - task/run manifests
33
+ - worktree registry
34
+ - final-report schema/render/validators
35
+ - wizard state machine
36
+ - worker result / convergence schemas
37
+
38
+ lead adapters
39
+ - claude-code adapter
40
+ - codex adapter
41
+ ```
42
+
43
+ Claude Code adapter 는 현재 동작을 유지한다. Codex adapter 는 Claude Teams 를
44
+ 흉내내지 않고 Codex 가 제공하는 방식으로 같은 artifact contract 를 채운다.
45
+
46
+ ## 3. 비목표
47
+
48
+ - `prepare_task_bundle()` 를 Claude/Codex 별로 복제하지 않는다.
49
+ - phase profile, schema, validator 를 provider 별로 fork 하지 않는다.
50
+ - Codex adapter 가 Claude Code 의 `TeamCreate` 나 `Agent(...)` 를 동일하게
51
+ 재현한다고 주장하지 않는다.
52
+ - 기존 Claude Code slash skill UX 를 깨지 않는다.
53
+
54
+ ## 4. Adapter 경계
55
+
56
+ ### 4.1 공통 core 로 남길 것
57
+
58
+ | 영역 | 이유 |
59
+ |---|---|
60
+ | `scripts/okstra_ctl/run.py::prepare_task_bundle` | task identity 와 artifact layout 의 single authority |
61
+ | `scripts/okstra_ctl/worktree*.py` | provider 와 무관한 격리/동시성 계약 |
62
+ | `scripts/okstra_ctl/workers.py` | phase profile 이 허용하는 logical roster 검증 |
63
+ | `scripts/okstra_ctl/render*.py` | manifests, prompts, final report rendering |
64
+ | `schemas/`, `validators/`, `templates/` | output contract 의 SSOT |
65
+ | `prompts/profiles/*.md` | lifecycle phase 규칙의 SSOT |
66
+
67
+ ### 4.2 Claude Code adapter 로 남길 것
68
+
69
+ | 영역 | 현재 결합 |
70
+ |---|---|
71
+ | installed skills | `~/.claude/skills/okstra-*` |
72
+ | installed agents | `~/.claude/agents/*-worker.md` |
73
+ | interactive UI | `AskUserQuestion` |
74
+ | worker dispatch | `TeamCreate`, `Agent(...)`, `team_name` |
75
+ | resume | `claude --resume <session-id>` |
76
+ | session accounting | Claude session jsonl, `~/.claude/projects` |
77
+ | team cleanup | `~/.claude/teams`, `TeamDelete` |
78
+
79
+ ### 4.3 Codex adapter 로 새로 둘 것
80
+
81
+ Codex adapter 는 다음을 담당한다.
82
+
83
+ | 책임 | Codex 방식 |
84
+ |---|---|
85
+ | input collection | CLI wizard JSON 을 Codex 대화/질문으로 relay |
86
+ | task bundle prepare | 기존 `okstra render-bundle` / `prepare_task_bundle()` 호출 |
87
+ | lead progress logging | `PROGRESS:` line 을 assistant text + run artifact 에 기록 |
88
+ | worker fan-out | Codex sub-agent tool, or CLI wrapper process, or sequential fallback |
89
+ | worker result persistence | 기존 `worker-results/*.md` contract 에 맞춰 파일 작성 |
90
+ | convergence | 기존 `okstra-convergence` 알고리즘을 lead-local 로 실행 |
91
+ | report writing | report-writer worker 가 없으면 explicit fallback status 를 기록한 뒤 lead-authored path 사용, 또는 Codex 전용 report-writer role 을 정의 |
92
+ | resume | Codex session resume 가 아니라 run artifact checkpoint 기준 재진입 |
93
+
94
+ ## 5. Execution modes
95
+
96
+ ### 5.1 `leadRuntime=claude-code`
97
+
98
+ 현재 기본값이다.
99
+
100
+ - 설치: `okstra install` 이 `~/.claude/skills` 와 `~/.claude/agents` 를 채운다.
101
+ - 실행: slash skill 또는 `okstra.sh` 가 Claude lead 를 시작한다.
102
+ - worker dispatch: `TeamCreate` / `Agent(...)`.
103
+ - validator: 기존 session conformance 를 그대로 적용한다.
104
+
105
+ ### 5.2 `leadRuntime=codex`
106
+
107
+ 신규 모드다.
108
+
109
+ - 설치: Claude assets 는 선택 설치로 바꾸고, Codex adapter assets 를 별도 설치한다.
110
+ - 실행: `okstra codex-run` 또는 `okstra run --lead-runtime codex`.
111
+ - worker dispatch:
112
+ - 가능한 경우 Codex sub-agent fan-out 사용.
113
+ - 불가능하면 CLI wrapper 또는 sequential local analysis 로 degrade.
114
+ - team-state 는 Claude team fields 대신 `leadRuntime: "codex"` 와
115
+ `dispatchMode` 를 명시한다.
116
+ - validator 는 `leadRuntime` 별 conformance rule 을 고른다.
117
+
118
+ ## 6. Contract changes
119
+
120
+ ### 6.1 manifest
121
+
122
+ `task-manifest.json` 또는 run manifest 에 다음 필드를 추가한다.
123
+
124
+ ```json
125
+ {
126
+ "leadRuntime": "claude-code",
127
+ "leadAdapter": {
128
+ "name": "claude-code",
129
+ "dispatchMode": "team",
130
+ "sessionAccounting": "claude-jsonl"
131
+ }
132
+ }
133
+ ```
134
+
135
+ Codex run 예:
136
+
137
+ ```json
138
+ {
139
+ "leadRuntime": "codex",
140
+ "leadAdapter": {
141
+ "name": "codex",
142
+ "dispatchMode": "codex-subagents",
143
+ "sessionAccounting": "artifact-only"
144
+ }
145
+ }
146
+ ```
147
+
148
+ ### 6.2 validator
149
+
150
+ `validate_session_conformance.py` 는 현재 Claude session jsonl 을 검사한다.
151
+ 이를 다음처럼 분기한다.
152
+
153
+ | runtime | conformance source |
154
+ |---|---|
155
+ | `claude-code` | existing Claude session jsonl scan |
156
+ | `codex` | run artifact event log scan |
157
+
158
+ Codex adapter 는 `runs/<task-type>/state/lead-events-<task-type>-<seq>.jsonl`
159
+ 에 진행 이벤트를 기록한다.
160
+
161
+ Token/cost validation is runtime-aware. `claude-code` keeps the existing
162
+ fail-close `okstra token-usage --substitute-data` requirement. `codex` does
163
+ not run the Claude session-jsonl auto-recovery path. Codex runtime worker
164
+ usage is collected from `lead-events-*.jsonl` dispatch windows matched against
165
+ Codex rollout / Gemini session logs, so CLI-backed worker and report-writer
166
+ cost can be summarized without Claude wrapper jsonls. Codex lead token
167
+ accounting is still unavailable, and validator does not yet force
168
+ final-report token substitution for Codex runs.
169
+
170
+ ### 6.3 report author
171
+
172
+ 기존 규칙은 "report-writer worker 가 roster 에 있으면 lead 가 직접 쓰지 않는다"이다.
173
+ Codex adapter 에서도 이 원칙은 유지하되, report-writer role 의 구현을 runtime 별로 둔다.
174
+
175
+ ```text
176
+ logical role: report-writer
177
+ claude-code implementation: Agent(subagent_type=report-writer-worker)
178
+ codex implementation: Codex report-writer subagent or explicit recorded fallback
179
+ ```
180
+
181
+ ## 7. Install layout
182
+
183
+ 현재 설치는 Claude Code 를 전제로 한다. 설치 command 를 runtime-aware 로 분리한다.
184
+
185
+ ```bash
186
+ okstra install --runtime claude-code # default, current behavior
187
+ okstra install --runtime codex # codex adapter assets only
188
+ okstra install --runtime all # both adapters
189
+ ```
190
+
191
+ 권장 내부 구조:
192
+
193
+ ```text
194
+ adapters/
195
+ claude-code/
196
+ skills/
197
+ agents/
198
+ settings/
199
+ codex/
200
+ skills/
201
+ prompts/
202
+ adapter.md
203
+ ```
204
+
205
+ 단, 첫 구현에서는 물리 디렉터리 이동 없이 installer option 과 manifest field 만
206
+ 도입해도 된다. 대규모 파일 이동은 후속 cleanup 으로 둔다.
207
+
208
+ ## 8. 단계별 구현 계획
209
+
210
+ ### Stage 1 — runtime marker only
211
+
212
+ - `--lead-runtime <claude-code|codex>` flag 를 `render-bundle` / `okstra.sh` /
213
+ `prepare_task_bundle()` 에 추가한다.
214
+ - 기본값은 `claude-code`.
215
+ - manifests 와 team-state 에 `leadRuntime` / `leadAdapter` 를 기록한다.
216
+ - `codex` 는 dispatch adapter 가 생기기 전까지 `--render-only` 에서만 허용하고,
217
+ non-render prepare 는 명확히 fail 한다.
218
+
219
+ ### Stage 2 — Codex dry-run adapter
220
+
221
+ - `okstra codex-run` 를 추가한다.
222
+ - wrapper 가 `--render-only --lead-runtime codex` 를 소유하고, 사용자가 직접
223
+ 중복 전달하면 거부한다.
224
+ - wizard/render-bundle 로 task bundle 을 만들고, Codex lead prompt 를 출력한다.
225
+ - worker dispatch 는 하지 않는다.
226
+ - 목적: Codex 에서 task bundle intake, profile reading, phase boundary 를 안정화.
227
+
228
+ ### Stage 3 — Codex artifact event log
229
+
230
+ - `lead-events-*.jsonl` writer/reader 를 추가한다.
231
+ - `runs/<task-type>/state/lead-events-<task-type>-<seq>.jsonl` 를
232
+ team-state / run-manifest / task-manifest artifacts 에 노출한다.
233
+ - Codex render-only adapter 는 우선 `bundle-prepared` event 를 기록한다.
234
+ - validator 는 `leadRuntime=codex` 일 때 Claude session jsonl 대신 event log 를
235
+ 읽고 `progress` / `artifact-read` 이벤트를 checkpoint / implementation
236
+ sidecar-read 증거로 사용한다.
237
+ - 후속 dispatch 단계에서 `worker-dispatched`, `worker-result-collected` event 를
238
+ 기록한다.
239
+
240
+ ### Stage 4 — Codex worker dispatch
241
+
242
+ - logical worker role 을 유지하면서 Codex adapter 의 dispatch backend 를 붙인다.
243
+ - backend 우선순위:
244
+ 1. Codex sub-agent dispatch
245
+ 2. CLI wrapper dispatch
246
+ 3. sequential fallback with terminal status markers
247
+ - 모든 backend 는 기존 `worker-results/*.md`, prompt history, errors sidecar 를
248
+ 같은 path 에 써야 한다.
249
+ - 첫 구현 범위(2026-06-12): `okstra codex-run` 으로 준비된 run manifest 를
250
+ 입력으로 받는 `okstra codex-dispatch` 보조 명령을 추가한다.
251
+ - 지원 worker: 기존 CLI wrapper 계약이 있는 `codex`, `gemini`.
252
+ - `report-writer` 는 Stage 5 의 명시 opt-in 경로에서 지원한다.
253
+ - 미지원 worker: `--workers` 를 생략한 기본 dispatch 에서는 `claude` 같은
254
+ Codex-side 미지원 roster 항목을 건너뛴다. 사용자가 `--workers claude` 처럼
255
+ 명시 요청하면 Codex-native 구현 전까지 즉시 unsupported 로 실패한다.
256
+ - 실행 방식: shell 없이 wrapper 를 `subprocess.run([...])` 으로 호출하고,
257
+ team-state worker status 와 `lead-events-*.jsonl` 의
258
+ `worker-dispatched` / `worker-result-collected` / `worker-failed` 이벤트를
259
+ 갱신한다.
260
+ - prompt history 파일이 아직 없으면 run manifest / active-run-context 에서
261
+ 자동 생성한다. 기존 prompt 파일은 덮어쓰지 않는다.
262
+ - dispatch plan 과 lead-events 는 worker 별 `backend` / `dispatchMode` 를
263
+ 기록한다. 현재 값은 모두 `cli-wrapper` 이며, `_run_worker()` 는 backend router
264
+ 로 분리되어 미지원 backend 를 wrapper 실행 전에 거부한다.
265
+ - 비범위: Claude TeamCreate 대체, final-report Phase 7 token substitution,
266
+ report-writer 후처리, token/session accounting. 이후 Stage 5 에서
267
+ report-writer 후처리가 추가됐고, Stage 5.5 에서 worker CLI token accounting
268
+ 이 추가됐다.
269
+
270
+ ### Stage 5 — Codex report-writer
271
+
272
+ - report-writer logical role 을 Codex sub-agent 로 구현한다.
273
+ - 실패 시 기존 fallback 규칙처럼 dispatch attempt 와 terminal status 를 기록한다.
274
+ - final-report data.json -> renderer -> views -> token/cost placeholder 처리 순서는
275
+ Claude adapter 와 동일하게 유지한다.
276
+ - 첫 구현 범위(2026-06-12): `okstra codex-dispatch` 에 준비된
277
+ report-writer prompt 를 Codex wrapper 로 실행하는 opt-in 경로를 추가한다.
278
+ - 사용자는 `--workers report-writer --enable-codex-report-writer
279
+ --report-writer-codex-model <model>` 을 명시해야 한다.
280
+ - 이유: run manifest 의 report-writer model assignment 는 아직 Claude provider
281
+ 기준(`REPORT_WRITER_MODEL_EXECUTION_VALUE`)으로 렌더되므로, Codex CLI 에
282
+ 넘길 모델은 dispatch 단계에서 명시 override 해야 한다.
283
+ - dispatcher 는 prompt 의 `**Model:** Report writer worker, ...` header 를
284
+ override 값으로 갱신하고, wrapper 실행 성공 후 세 artifact 를 모두 확인한다:
285
+ `reports/final-report-*.data.json`, 렌더된 `reports/final-report-*.md`,
286
+ `worker-results/report-writer-worker-*.md` audit sidecar.
287
+ - artifact 확인 후 idempotent 한 후처리 네 개를 실행한다:
288
+ `okstra-token-usage.py --write --substitute-data` 로 token/cost 값을
289
+ data.json 과 markdown 에 치환하고,
290
+ `okstra-render-report-views.py` 로 HTML view 를 렌더하고,
291
+ `okstra-spawn-followups.py` 로 `followUpTasks[]` 의 auto-spawn 항목을
292
+ task stub 으로 만들고,
293
+ `validators/validate-run.py` 로 final-report schema / markdown contract /
294
+ report-view contract 를 검증한다. HTML view 가 치환 후 markdown 을 읽도록
295
+ token substitution 이 항상 render-views 보다 먼저 실행되며, validation 은
296
+ 더 이상 실패할 후처리 단계가 없는 마지막 gate 로 실행된다. 후처리가 실패하면
297
+ report-writer 는 failed 로 기록한다.
298
+ - report-writer prompt history 파일이 아직 없으면 dispatcher 가 자동 생성한다.
299
+ 이때 `Result Path` 는 `.data.json`, `Worker Result Path` 는 audit sidecar,
300
+ `Report Language` 는 project/global config 또는 task brief 추론값으로 채운다.
301
+ - 비범위: Codex lead 자체 token/cost accounting.
302
+
303
+ ### Stage 5.5 — Codex worker CLI token accounting
304
+
305
+ - Codex runtime 은 Claude Agent wrapper session 을 생성하지 않으므로,
306
+ `okstra token-usage` 가 `lead-events-*.jsonl` 의 worker dispatch window 를
307
+ session source 로 사용한다.
308
+ - `codex` worker 와 Codex opt-in `report-writer` 는 Codex rollout JSONL 을,
309
+ `gemini` worker 는 Gemini chat session JSON 을 window 안에서 매칭한다.
310
+ - `usageSummary.workerTotalTokens` 와 `estimatedCostUsd.cliWorkers` 는 CLI worker
311
+ totals 를 반영한다.
312
+ - report-writer 후처리 중에는 `worker-result-collected` 이벤트가 아직 append
313
+ 되기 전이므로, collector 는 `status=running` worker 의 active dispatch 를
314
+ 수집 시각까지의 open window 로 사용할 수 있다.
315
+ - Codex lead 자체 token/cost 는 아직 공식 수집원이 없어 `leadUsage.source =
316
+ "unavailable"` 로 남긴다.
317
+ - validator 는 Codex run 에 대해 token substitution auto-recovery 를 강제하지
318
+ 않는다. 이 단계의 목적은 collector data 를 먼저 정확히 채우는 것이다.
319
+
320
+ ### Stage 6 — install packaging cleanup
321
+
322
+ - `okstra install --runtime <claude-code|codex|all>` 을 추가한다.
323
+ - `claude-code` 기본값은 기존 동작 그대로 공유 runtime + Claude skills/agents 를
324
+ 설치한다.
325
+ - `codex` 는 공유 runtime/bin/templates/schema/validators/settings 만 설치하고
326
+ `~/.claude/skills` / `~/.claude/agents` 를 건드리지 않는다.
327
+ - `all` 은 Claude assets 포함 설치와 동일하다. 별도 Codex-only asset tree 가
328
+ 생기면 여기서 함께 설치한다.
329
+ - `okstra ensure-installed --runtime <...>` 는 같은 runtime 으로 재설치하며,
330
+ `codex` 는 Claude skill/agent drift 검사를 건너뛴다.
331
+ - `okstra doctor --runtime <...>` 는 `codex` 에서 Claude skill checks 를 제외하고
332
+ JSON 출력에 선택 runtime 을 포함한다.
333
+ - `~/.okstra/installed-runtimes.json` 을 추가해 선택 runtime 과 asset 범위를
334
+ `installed-skills.json` / `installed-agents.json` 과 분리해 기록한다.
335
+ codex-only manifest 는 uninstall 이 레거시 fallback 으로 `~/.claude` 자산을
336
+ 제거하지 않도록 막는다.
337
+ - README / docs 는 Codex-only install + doctor 경로를 노출한다.
338
+ - 남은 cleanup: Codex 전용 skill/adapter asset 이 생기면
339
+ `installed-runtimes.json` 의 Codex asset 범위를 실제 파일 manifest 로 확장한다.
340
+
341
+ ## 9. 위험과 대응
342
+
343
+ | 위험 | 대응 |
344
+ |---|---|
345
+ | validator 가 Claude session jsonl 만 신뢰 | `leadRuntime` 별 conformance source 분기 |
346
+ | worker output contract drift | logical worker role/schema 는 공유하고 dispatch backend 만 교체 |
347
+ | report-writer authorship 규칙 훼손 | runtime 별 report-writer implementation 을 두고 fallback 은 명시 기록 |
348
+ | 설치 경로 혼란 | `okstra doctor --runtime <name>` 로 runtime 별 진단 |
349
+ | 두 버전 문서 drift | phase profile/schema/validator 는 fork 금지, adapter docs 만 분리 |
350
+
351
+ ## 10. 결론
352
+
353
+ 가능하다. 단, "Claude Code 버전"과 "Codex 버전"을 제품 복사본으로 나누는 것이
354
+ 아니라, **lead runtime adapter 를 나누는 방식**이어야 한다.
355
+
356
+ 가장 작고 안전한 첫 PR 은 `leadRuntime` marker 와 Codex dry-run adapter 이다.
357
+ 이 PR 은 기존 Claude Code 사용자를 깨지 않으면서 Codex 실행 경로를 공식적으로
358
+ 받아들일 수 있는 구조를 만든다.
@@ -0,0 +1,134 @@
1
+ # Forbidden-Actions SSOT Design (P2-2)
2
+
3
+ **Goal:** Eliminate the drift between the two places per-phase *forbidden actions* are authored, by making one JSON file the single source of truth (SSOT) from which both the launch-prompt boundary and the profile body derive.
4
+
5
+ **Scope:** `forbidden` only. `allowed`, required-workers, and profile `Purpose`/`Non-goals` are explicitly out of scope (user decision; see Non-goals).
6
+
7
+ **Format:** JSON data file (no YAML — the repo deliberately ships no YAML parser; `scripts/okstra_ctl/wizard.py:128` hand-parses frontmatter "no yaml dep", CLAUDE.md "stdlib first"). User-confirmed.
8
+
9
+ ---
10
+
11
+ ## 1. Current state (verified)
12
+
13
+ Per-phase forbidden actions are authored in **two** places and injected into the lead from **two** surfaces in a single run:
14
+
15
+ - **Python literal** — [`scripts/okstra_ctl/workflow.py:36`](../../../scripts/okstra_ctl/workflow.py) `PHASE_RULES[<phase>]["forbidden"]`, a `" - item\n"`-joined string. `compute_workflow_state` ([workflow.py:233](../../../scripts/okstra_ctl/workflow.py)) emits it as `PHASE_FORBIDDEN_ACTIONS`, injected into [`prompts/launch.template.md:16`](../../../prompts/launch.template.md) "Current Phase Boundary".
16
+ - **Profile prose** — `prompts/profiles/<phase>.md`. The full profile body is rendered into `analysis-profile.md` ([run.py:1812](../../../scripts/okstra_ctl/run.py)) and read by the lead.
17
+
18
+ Crucially, only **2 of 7** profiles carry a `Forbidden actions` block that duplicates `PHASE_RULES`:
19
+
20
+ | task type | profile forbidden block? | PHASE_RULES.forbidden? | duplicated? |
21
+ |---|---|---|---|
22
+ | requirements-discovery | no (`Non-goals` only, [requirements-discovery.md:57](../../../prompts/profiles/requirements-discovery.md)) | yes | no |
23
+ | improvement-discovery | no (`Non-goals` only) | yes | no |
24
+ | error-analysis | no (`Non-goals` only) | yes | no |
25
+ | implementation-planning | no (`Non-goals` only) | yes | no |
26
+ | **implementation** | **yes** ([implementation.md:34](../../../prompts/profiles/implementation.md), 8 items) | yes (13 items) | **yes, divergent** |
27
+ | final-verification | no (`Non-goals` only) | yes | no |
28
+ | **release-handoff** | **yes** ([release-handoff.md:62](../../../prompts/profiles/release-handoff.md), 15 items) | yes (11 items) | **yes, divergent** |
29
+
30
+ `Non-goals` is a *distinct concept* (scope guidance) from `Forbidden actions` (`contract-violated` safety rules). It is **not** consolidated by this work.
31
+
32
+ So the drift-prone duplication is exactly **implementation** and **release-handoff**, where the profile block and `PHASE_RULES` have diverged (different item counts, wording, ordering) with no test guarding them.
33
+
34
+ ---
35
+
36
+ ## 2. Design
37
+
38
+ ### 2.1 SSOT file
39
+
40
+ Create `prompts/profiles/forbidden-actions.json`:
41
+
42
+ ```json
43
+ {
44
+ "requirements-discovery": ["...", "..."],
45
+ "improvement-discovery": ["..."],
46
+ "error-analysis": ["..."],
47
+ "implementation-planning": ["..."],
48
+ "implementation": ["..."],
49
+ "final-verification": ["..."],
50
+ "release-handoff": ["..."],
51
+ "unknown": ["..."]
52
+ }
53
+ ```
54
+
55
+ - Each value is the ordered list of forbidden-action items (one string per current ` - ` bullet, **without** the leading ` - `).
56
+ - `unknown` mirrors `PHASE_RULES_UNKNOWN["forbidden"]`.
57
+ - Lives under `prompts/profiles/` so it resolves the same way the profile `.md` files do (`workspace_root / "prompts" / "profiles" / ...`, [run.py:1073](../../../scripts/okstra_ctl/run.py)) and is synced into `runtime/` by `tools/build.mjs` (prompts/ is a build source).
58
+
59
+ ### 2.2 Rendering & injection
60
+
61
+ In `workflow.py`:
62
+
63
+ - `def load_phase_forbidden(workspace_root: Path) -> dict[str, str]` — explicit IO: `json.load` the file, render each phase's list to the existing `" - item\n"` string shape via a pure helper `render_forbidden(items: list[str]) -> str`. Raise `FileNotFoundError`/`PrepareError`-style if missing (fail loud, no silent stale fallback).
64
+ - `compute_workflow_state(..., forbidden_by_phase: dict[str, str])` — new **required** keyword arg; `PHASE_FORBIDDEN_ACTIONS = forbidden_by_phase.get(task_type, forbidden_by_phase["unknown"])`.
65
+ - **Delete** the `"forbidden"` key from every `PHASE_RULES` entry and from `PHASE_RULES_UNKNOWN`. `PHASE_RULES` keeps `"allowed"` only.
66
+
67
+ In `run.py` (the only two `compute_workflow_state` call sites, [run.py:2012](../../../scripts/okstra_ctl/run.py), [run.py:2232](../../../scripts/okstra_ctl/run.py)):
68
+
69
+ - Load `forbidden_by_phase = workflow.load_phase_forbidden(workspace_root)` once (where `workspace_root` is already resolved for the profile file) and pass it to both calls.
70
+ - In `_write_instruction_set_sources` ([run.py:1798](../../../scripts/okstra_ctl/run.py)), add `{{PHASE_FORBIDDEN_ACTIONS}}` to the placeholder substitution applied to `profile_rendered`, using the rendered string for `inp.task_type`.
71
+
72
+ ### 2.3 Profile body changes (only impl + release-handoff)
73
+
74
+ - [implementation.md:34-42](../../../prompts/profiles/implementation.md): keep the header line `- Forbidden actions — universal (any occurrence → terminal status \`contract-violated\`):`, replace the 8 hand-written bullets with the single token `{{PHASE_FORBIDDEN_ACTIONS}}`.
75
+ - [release-handoff.md:62-77](../../../prompts/profiles/release-handoff.md): keep the header line `- Forbidden actions (any occurrence → terminal status \`contract-violated\`):`, replace the 15 hand-written bullets (incl. the nested push-variant sub-list) with `{{PHASE_FORBIDDEN_ACTIONS}}`.
76
+ - The other 5 profiles are **untouched** (their `Non-goals` blocks stay; they have no forbidden duplication).
77
+
78
+ ### 2.4 Canonical content (reconciliation)
79
+
80
+ For the 5 non-duplicated phases, the JSON list = current `PHASE_RULES.forbidden`, **verbatim** (launch injection byte-identical, nothing else references it).
81
+
82
+ For the 2 duplicated phases the JSON list = the **union** (lose no rule from either source):
83
+
84
+ - **implementation** = `PHASE_RULES["implementation"]["forbidden"]` (13 items, the near-superset) with the profile's `git push ... --dry-run` nuance folded into the publish/push item. Every profile item maps into a PHASE_RULES item (git push, publish, DB, infra, API-write, sub-agents, Edit/Write-before-gate, verifier-read-only).
85
+ - **release-handoff** = the profile's detailed 15-item list ([release-handoff.md:62-77](../../../prompts/profiles/release-handoff.md), the superset: explicit `--force`/`--force-with-lease`/`-f`/`+refspec`, base-branch push, `--no-verify`, `gh release create`/`edit` + `twine upload` + publish set, source edits, unselected command, weaker-flag retry, `TeamCreate`/`Agent` ban, unrecognised-reply). All `PHASE_RULES` release-handoff items are already covered by it.
86
+
87
+ ### 2.5 Disclosed behavior changes
88
+
89
+ Nothing is lost; one surface becomes *more complete* for the 2 phases:
90
+
91
+ - **implementation**: profile body forbidden grows 8 → 13 items (now equals the launch boundary). Launch injection ≈ unchanged.
92
+ - **release-handoff**: launch boundary forbidden grows 11 → 15 items (now equals the detailed profile list). Profile body ≈ unchanged.
93
+
94
+ All other phases: byte-identical on both surfaces.
95
+
96
+ ---
97
+
98
+ ## 3. Tests (TDD)
99
+
100
+ - `tests/test_phase_forbidden_ssot.py`:
101
+ - `load_phase_forbidden` covers every key in `PHASE_RULES` + `unknown` (no phase missing; no extra phase).
102
+ - `render_forbidden(["a","b"]) == " - a\n - b"` (exact shape match for injection).
103
+ - `compute_workflow_state(..., forbidden_by_phase=load_phase_forbidden(repo_root))` emits `PHASE_FORBIDDEN_ACTIONS` equal to the rendered JSON for a sample phase.
104
+ - `PHASE_RULES` no longer contains a `"forbidden"` key (regression guard against re-introducing the duplicate).
105
+ - `tests/test_profile_forbidden_placeholder.py` (or extend an existing profile/prompt-metadata test):
106
+ - `implementation.md` and `release-handoff.md` contain `{{PHASE_FORBIDDEN_ACTIONS}}` and **no** hand-written bullet under the `Forbidden actions` header.
107
+ - the 5 other profiles contain neither a `Forbidden actions` block nor the placeholder (untouched).
108
+ - Existing suite must stay green, especially `tests/test_workflow_phase_sequence.py` (update its `compute_workflow_state` calls to pass `forbidden_by_phase`), `validators/validate-workflow.sh`, and the launch-template render tests.
109
+
110
+ ---
111
+
112
+ ## 4. Non-goals
113
+
114
+ - `allowed` outputs consolidation — left as-is, including the `release-handoff.md:54` "Allowed actions" block that also overlaps `PHASE_RULES` allowed. (Possible future P2-2b.)
115
+ - Required/optional workers, `Purpose`, `Non-goals` — untouched (different parsers, no forbidden drift).
116
+ - The 5 non-duplicated profiles' `Non-goals` prose — untouched.
117
+ - No new third-party dependency (JSON via stdlib).
118
+ - `runtime/` is never hand-edited; `npm run build` syncs the new JSON.
119
+
120
+ ---
121
+
122
+ ## 5. File summary
123
+
124
+ | File | Change |
125
+ |---|---|
126
+ | `prompts/profiles/forbidden-actions.json` | **create** — the SSOT (7 phases + `unknown`) |
127
+ | `scripts/okstra_ctl/workflow.py` | add `render_forbidden`, `load_phase_forbidden`; `compute_workflow_state` takes `forbidden_by_phase`; delete `PHASE_RULES[*]["forbidden"]` |
128
+ | `scripts/okstra_ctl/run.py` | load `forbidden_by_phase`, pass to both `compute_workflow_state` calls, add `{{PHASE_FORBIDDEN_ACTIONS}}` to profile rendering |
129
+ | `prompts/profiles/implementation.md` | replace forbidden bullets with placeholder |
130
+ | `prompts/profiles/release-handoff.md` | replace forbidden bullets with placeholder |
131
+ | `tests/test_phase_forbidden_ssot.py` | **create** |
132
+ | `tests/test_profile_forbidden_placeholder.py` | **create** |
133
+ | `tests/test_workflow_phase_sequence.py` | update `compute_workflow_state` calls |
134
+ | `CHANGES.md`, plan doc | record |