okstra 0.77.0 → 0.78.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 +1 -1
- package/README.md +1 -1
- package/bin/okstra +1 -125
- package/docs/contributor-change-matrix.md +12 -0
- package/docs/kr/cli.md +5 -4
- package/docs/project-structure-overview.md +1 -1
- package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +493 -0
- package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +134 -0
- package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +284 -0
- package/package.json +5 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/DO_NOT_EDIT.md +21 -0
- package/runtime/agents/SKILL.md +3 -0
- package/runtime/bin/okstra-trace-cleanup.sh +16 -12
- package/runtime/prompts/profiles/forbidden-actions.json +69 -0
- package/runtime/prompts/profiles/implementation.md +1 -8
- package/runtime/prompts/profiles/release-handoff.md +1 -15
- package/runtime/python/okstra_ctl/codex_dispatch.py +70 -2
- package/runtime/python/okstra_ctl/context_cost.py +1 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +897 -0
- package/runtime/python/okstra_ctl/doctor.py +292 -0
- package/runtime/python/okstra_ctl/lead_runtime.py +72 -0
- package/runtime/python/okstra_ctl/pr_template.py +4 -3
- package/runtime/python/okstra_ctl/render.py +67 -16
- package/runtime/python/okstra_ctl/run.py +20 -12
- package/runtime/python/okstra_ctl/team.py +267 -0
- package/runtime/python/okstra_ctl/tmux.py +181 -10
- package/runtime/python/okstra_ctl/workflow.py +30 -71
- package/runtime/python/okstra_ctl/wrapper_status.py +55 -0
- package/runtime/schemas/final-report-v1.0.schema.json +3 -1
- package/runtime/skills/okstra-convergence/SKILL.md +3 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +3 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +12 -0
- package/runtime/validators/forbidden_actions.py +135 -0
- package/runtime/validators/validate-run.py +65 -9
- package/runtime/validators/validate_session_conformance.py +15 -10
- package/src/cli-registry.mjs +277 -0
- package/src/doctor.mjs +93 -4
- package/src/install.mjs +6 -5
- package/src/runtime-manifest.mjs +5 -2
- package/src/team.mjs +63 -0
- package/src/uninstall.mjs +1 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# repo 유지보수 리스크 하드닝 구현 계획
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** 이 문서는 `okstra` 저장소 자체의 유지보수 리스크를 줄이기 위한 구현 계획이다. 단계별 체크박스(`- [ ]`)를 갱신하며 진행한다. `runtime/`은 build output이므로 직접 수정하지 않는다.
|
|
4
|
+
|
|
5
|
+
**Goal:** 문서 drift, generated runtime 직접 수정, profile/workflow/worker contract 불일치, 로컬/CI 검증 경로 부재로 생기는 유지보수 리스크를 낮춘다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** 구조를 크게 바꾸지 않는 P0 안정화부터 적용한다. `package.json`을 version SSOT로 두고, `tools/build.mjs`가 `runtime/DO_NOT_EDIT.md`를 생성하며, pytest contract tests가 `workflow.py` ↔ `prompts/profiles/*.md` ↔ `workers.py`의 최소 일관성을 검증한다. CI는 같은 명령을 실행해 regression을 빠르게 잡는다.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js 22+, Python 3.11+, pytest, GitHub Actions.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Scope
|
|
14
|
+
|
|
15
|
+
### P0 — 이번 구현 범위
|
|
16
|
+
|
|
17
|
+
1. 문서의 고정 package version 제거
|
|
18
|
+
2. `runtime/DO_NOT_EDIT.md` build-time 생성
|
|
19
|
+
3. repo contract pytest 추가
|
|
20
|
+
4. `package.json` check scripts 추가
|
|
21
|
+
5. GitHub Actions CI 추가
|
|
22
|
+
6. `CHANGES.md`에 사용자 영향 기록
|
|
23
|
+
|
|
24
|
+
### P1 — 후속 개선
|
|
25
|
+
|
|
26
|
+
1. [x] `okstra doctor --phase <phase>` 추가
|
|
27
|
+
2. [x] contributor change matrix 문서 추가
|
|
28
|
+
3. [x] final-report template/schema/validator contract test 강화
|
|
29
|
+
|
|
30
|
+
### P2 — 구조 개선 후보
|
|
31
|
+
|
|
32
|
+
1. [x] command metadata registry 도입 (P2-1)
|
|
33
|
+
2. [x] profile meta yaml 도입 → forbidden actions JSON SSOT로 구현 (P2-2; spec: docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md). 범위는 forbidden만(allowed/workers/Non-goals 제외).
|
|
34
|
+
3. [x] forbidden action post-run audit 자동화 (P2-3)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Non-goals
|
|
39
|
+
|
|
40
|
+
- `prepare_task_bundle()` orchestration 구조 변경 없음
|
|
41
|
+
- phase 추가/삭제 없음
|
|
42
|
+
- worker roster 정책 변경 없음
|
|
43
|
+
- `runtime/` 산출물을 git tracked file로 만들지 않음
|
|
44
|
+
- `doctor --phase`는 이번 P0 구현에 포함하지 않음
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Task 1: 문서 drift 제거
|
|
49
|
+
|
|
50
|
+
**Files:**
|
|
51
|
+
|
|
52
|
+
- Modify: `docs/project-structure-overview.md`
|
|
53
|
+
|
|
54
|
+
**Implementation:**
|
|
55
|
+
|
|
56
|
+
- [x] `package version: \`0.50.0\`` 같은 고정 semver를 제거한다.
|
|
57
|
+
- [x] 다음 중 하나로 변경한다.
|
|
58
|
+
|
|
59
|
+
```md
|
|
60
|
+
- package version: see `package.json`
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
또는 version line 자체를 제거한다.
|
|
64
|
+
|
|
65
|
+
**Rationale:**
|
|
66
|
+
|
|
67
|
+
`package.json`이 version SSOT다. 문서에 release version을 수동으로 넣으면 release마다 drift가 발생한다.
|
|
68
|
+
|
|
69
|
+
**Validation:**
|
|
70
|
+
|
|
71
|
+
- [x] 신규 `tests/test_repo_contracts.py`에서 문서 내 고정 package version을 금지한다.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Task 2: generated `runtime/` 경고 파일 생성
|
|
76
|
+
|
|
77
|
+
**Files:**
|
|
78
|
+
|
|
79
|
+
- Modify: `tools/build.mjs`
|
|
80
|
+
|
|
81
|
+
**Implementation:**
|
|
82
|
+
|
|
83
|
+
- [x] `writeDoNotEditNotice()` 함수를 추가한다.
|
|
84
|
+
- [x] build sequence 마지막에 `runtime/DO_NOT_EDIT.md`를 생성한다.
|
|
85
|
+
- [x] 파일 내용은 `runtime/`이 generated output이고 원본 수정 위치가 어디인지 명시한다.
|
|
86
|
+
|
|
87
|
+
Suggested content:
|
|
88
|
+
|
|
89
|
+
```md
|
|
90
|
+
# Do not edit runtime/
|
|
91
|
+
|
|
92
|
+
This directory is generated by `npm run build`.
|
|
93
|
+
|
|
94
|
+
Edit source files under:
|
|
95
|
+
|
|
96
|
+
- scripts/
|
|
97
|
+
- skills/
|
|
98
|
+
- agents/
|
|
99
|
+
- prompts/
|
|
100
|
+
- schemas/
|
|
101
|
+
- templates/
|
|
102
|
+
- validators/
|
|
103
|
+
|
|
104
|
+
Then run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run build
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Direct edits under runtime/ are overwritten.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Important:**
|
|
114
|
+
|
|
115
|
+
`runtime/*`은 `.gitignore`로 ignore된다. `runtime/DO_NOT_EDIT.md`를 직접 커밋하지 말고 build 산출물로만 만든다.
|
|
116
|
+
|
|
117
|
+
**Validation:**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run build
|
|
121
|
+
test -f runtime/DO_NOT_EDIT.md
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Task 3: repo contract tests 추가
|
|
127
|
+
|
|
128
|
+
**Files:**
|
|
129
|
+
|
|
130
|
+
- Add: `tests/test_repo_contracts.py`
|
|
131
|
+
|
|
132
|
+
**Implementation:**
|
|
133
|
+
|
|
134
|
+
- [x] repo root를 기준으로 `scripts/`를 `sys.path`에 추가한다.
|
|
135
|
+
- [x] `okstra_ctl.workflow`와 `okstra_ctl.workers`를 import한다.
|
|
136
|
+
- [x] 아래 contract를 검증한다.
|
|
137
|
+
|
|
138
|
+
### Contract 3.1 — workflow phase와 profile 파일 일치
|
|
139
|
+
|
|
140
|
+
- [x] `PHASE_SEQUENCE`의 모든 phase에 `prompts/profiles/<phase>.md`가 존재한다.
|
|
141
|
+
- [x] `PHASE_RULES`의 모든 non-private task type에 profile 파일이 존재한다.
|
|
142
|
+
- [x] `improvement-discovery` profile도 존재한다.
|
|
143
|
+
|
|
144
|
+
### Contract 3.2 — worker roster 일관성
|
|
145
|
+
|
|
146
|
+
- [x] `release-handoff`는 required worker가 없어야 한다.
|
|
147
|
+
- [x] `release-handoff` 외 profile은 required worker가 있어야 한다.
|
|
148
|
+
- [x] required worker는 모두 `ALLOWED_WORKERS` 안에 있어야 한다.
|
|
149
|
+
- [x] optional worker도 모두 `ALLOWED_WORKERS` 안에 있어야 한다.
|
|
150
|
+
- [x] required worker 중복이 없어야 한다.
|
|
151
|
+
- [x] optional worker 중복이 없어야 한다.
|
|
152
|
+
|
|
153
|
+
### Contract 3.3 — docs version drift 방지
|
|
154
|
+
|
|
155
|
+
- [x] `docs/project-structure-overview.md`에 `package version: 0.0.0` 형태의 고정 semver가 남지 않아야 한다.
|
|
156
|
+
|
|
157
|
+
Suggested regex:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
r"package version:\s*`?\d+\.\d+\.\d+`?"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Validation:**
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
python3 -m pytest tests/test_repo_contracts.py -q
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Task 4: package scripts 정리
|
|
172
|
+
|
|
173
|
+
**Files:**
|
|
174
|
+
|
|
175
|
+
- Modify: `package.json`
|
|
176
|
+
|
|
177
|
+
**Implementation:**
|
|
178
|
+
|
|
179
|
+
- [x] 아래 scripts를 추가한다.
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"test:py": "python3 -m pytest tests/",
|
|
184
|
+
"test:workflow": "bash validators/validate-workflow.sh",
|
|
185
|
+
"check": "npm run build && npm run test:js && npm run test:py && npm run test:workflow"
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Rationale:**
|
|
190
|
+
|
|
191
|
+
로컬과 CI가 같은 검증 명령을 사용하게 만든다.
|
|
192
|
+
|
|
193
|
+
**Validation:**
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm run check
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Task 5: CI workflow 추가
|
|
202
|
+
|
|
203
|
+
**Files:**
|
|
204
|
+
|
|
205
|
+
- Add: `.github/workflows/ci.yml`
|
|
206
|
+
|
|
207
|
+
**Implementation:**
|
|
208
|
+
|
|
209
|
+
- [x] `pull_request`와 `main` push에서 실행한다.
|
|
210
|
+
- [x] Node 22와 Python 3.11을 설정한다.
|
|
211
|
+
- [x] pytest를 설치한다.
|
|
212
|
+
- [x] `npm run check`를 실행한다.
|
|
213
|
+
|
|
214
|
+
Suggested workflow:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
name: CI
|
|
218
|
+
|
|
219
|
+
on:
|
|
220
|
+
pull_request:
|
|
221
|
+
push:
|
|
222
|
+
branches:
|
|
223
|
+
- main
|
|
224
|
+
|
|
225
|
+
permissions:
|
|
226
|
+
contents: read
|
|
227
|
+
|
|
228
|
+
jobs:
|
|
229
|
+
test:
|
|
230
|
+
runs-on: ubuntu-latest
|
|
231
|
+
steps:
|
|
232
|
+
- uses: actions/checkout@v4
|
|
233
|
+
|
|
234
|
+
- uses: actions/setup-node@v4
|
|
235
|
+
with:
|
|
236
|
+
node-version: '22'
|
|
237
|
+
|
|
238
|
+
- uses: actions/setup-python@v5
|
|
239
|
+
with:
|
|
240
|
+
python-version: '3.11'
|
|
241
|
+
|
|
242
|
+
- name: Install Python test dependencies
|
|
243
|
+
run: python -m pip install pytest
|
|
244
|
+
|
|
245
|
+
- name: Run full check
|
|
246
|
+
run: npm run check
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Note:**
|
|
250
|
+
|
|
251
|
+
`git diff --exit-code runtime/`는 이 저장소에 적합하지 않다. `runtime/*`은 gitignored build output이므로 CI는 build 성공과 테스트 통과를 검증한다.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Task 6: 변경 이력 기록
|
|
256
|
+
|
|
257
|
+
**Files:**
|
|
258
|
+
|
|
259
|
+
- Modify: `CHANGES.md`
|
|
260
|
+
|
|
261
|
+
**Implementation:**
|
|
262
|
+
|
|
263
|
+
- [x] `2026-06-13` 섹션에 아래 항목을 추가한다.
|
|
264
|
+
|
|
265
|
+
```md
|
|
266
|
+
### chore(repo): 문서 drift와 runtime 수정 실수 방지 장치 추가
|
|
267
|
+
|
|
268
|
+
- **배경**: package version 같은 수동 문서 값과 generated `runtime/` 경계가 drift / 실수 위험을 만들 수 있었다.
|
|
269
|
+
- **해결**: 문서의 고정 version을 제거하고, build 시 `runtime/DO_NOT_EDIT.md`를 생성하며, workflow/profile contract test와 CI check를 추가했다.
|
|
270
|
+
- 사용자 영향: 기여자가 잘못된 파일을 수정하거나 profile/worker 계약을 깨뜨리는 변경을 더 빨리 발견할 수 있다.
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Final validation
|
|
276
|
+
|
|
277
|
+
Run in order:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npm run build
|
|
281
|
+
python3 -m pytest tests/test_repo_contracts.py -q
|
|
282
|
+
npm run test:js
|
|
283
|
+
python3 -m pytest tests/
|
|
284
|
+
bash validators/validate-workflow.sh
|
|
285
|
+
npm run check
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Expected result: all pass.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Rollback plan
|
|
293
|
+
|
|
294
|
+
If CI/test failures are unrelated or too broad:
|
|
295
|
+
|
|
296
|
+
1. Keep Task 1 and Task 3 if they pass independently.
|
|
297
|
+
2. Revert `.github/workflows/ci.yml` first if GitHub Actions environment issues appear.
|
|
298
|
+
3. Revert `package.json` `check` script only if local command composition is problematic.
|
|
299
|
+
4. Do not modify `runtime/` manually; rerun `npm run build` after any `tools/build.mjs` revert.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Follow-up design notes
|
|
304
|
+
|
|
305
|
+
### P1. `okstra doctor --phase <phase>`
|
|
306
|
+
|
|
307
|
+
Potential checks:
|
|
308
|
+
|
|
309
|
+
| Phase | Checks |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `implementation` | git repo, worktree support, approved-plan marker, worker agents |
|
|
312
|
+
| `final-verification` | worktree lookup, validation command availability, final-report prerequisites |
|
|
313
|
+
| `release-handoff` | `gh auth status`, clean git tree, remote exists, non-base current branch |
|
|
314
|
+
| `improvement-discovery` | gemini worker installed, lens whitelist importable |
|
|
315
|
+
|
|
316
|
+
### P1. contributor change matrix
|
|
317
|
+
|
|
318
|
+
Add `docs/contributor-change-matrix.md` with rows like:
|
|
319
|
+
|
|
320
|
+
| Change | Must update | Tests |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| Add CLI flag | `src/*`, `scripts/okstra_ctl/run.py`, `docs/kr/cli.md`, wizard prompts | JS + pytest |
|
|
323
|
+
| Add phase | `workflow.py`, `prompts/profiles`, validators, tests | workflow validator |
|
|
324
|
+
| Change worker roster | profile `Required workers`, `workers.py`, tests | worker contract tests |
|
|
325
|
+
| Change report section | schema, template, renderer, validator | final-report tests |
|
|
326
|
+
|
|
327
|
+
### P2. profile meta yaml
|
|
328
|
+
|
|
329
|
+
Possible future shape:
|
|
330
|
+
|
|
331
|
+
```yaml
|
|
332
|
+
taskType: implementation
|
|
333
|
+
requiredWorkers:
|
|
334
|
+
- claude
|
|
335
|
+
- codex
|
|
336
|
+
- report-writer
|
|
337
|
+
optionalWorkers:
|
|
338
|
+
- gemini
|
|
339
|
+
executorAllowed:
|
|
340
|
+
- claude
|
|
341
|
+
- codex
|
|
342
|
+
- gemini
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
For now, markdown `Required workers:` remains the authority.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## P2 구현 계획 (P2-1 + P2-3 먼저, 이후 P2-2)
|
|
350
|
+
|
|
351
|
+
조사 근거: `agent://CommandRegistryProbe`, `agent://ForbiddenAuditProbe`, `agent://ProfileMetaProbe`.
|
|
352
|
+
P2-2(profile meta yaml)는 파서 3개 + `PHASE_RULES`↔profile 산문의 기존 divergence reconcile 이 필요해 1차에서는 보류했으나, 이후 forbidden 범위로 좁혀 JSON SSOT(docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md)로 구현 완료했다.
|
|
353
|
+
|
|
354
|
+
### P2-1: command metadata registry
|
|
355
|
+
|
|
356
|
+
**Files:**
|
|
357
|
+
- Create: `src/cli-registry.mjs` — `COMMAND_REGISTRY` 배열 + 파생 `COMMANDS`(Map) + 파생 `USAGE`(string).
|
|
358
|
+
- Modify: `bin/okstra` — 인라인 `COMMANDS`/`USAGE` 제거, `src/cli-registry.mjs` 에서 import.
|
|
359
|
+
- Test: `tests-js/cli-registry.test.mjs`.
|
|
360
|
+
|
|
361
|
+
**Registry entry shape:**
|
|
362
|
+
```js
|
|
363
|
+
// { name, module, export: exportName, category, summary }
|
|
364
|
+
// category ∈ { "admin", "introspection" }
|
|
365
|
+
// summary: string[] (1번째=요약 첫 줄, 이후=연속행. 들여쓰기 미포함)
|
|
366
|
+
```
|
|
367
|
+
- 디스플레이 순서 = 현재 USAGE 순서(admin 11개, introspection 16개)를 verbatim 보존.
|
|
368
|
+
- `export` 필드로 install 3종(`runInstall`/`runEnsureInstalled`/`runUninstall`) 흡수, 나머지는 `run`.
|
|
369
|
+
- USAGE 렌더: static preamble/footer 유지, 각 행 `" " + name.padEnd(23) + summary[0]`, 연속행 `" ".repeat(25) + line` (현재 컬럼=25 실측).
|
|
370
|
+
- `COMMANDS` = `new Map(COMMAND_REGISTRY.map(c => [c.name, () => import(c.module).then(m => m[c.export])]))`.
|
|
371
|
+
- per-command `--help`(모듈 소유)와 외부 doc table(Option D)은 범위 밖.
|
|
372
|
+
|
|
373
|
+
**TDD:**
|
|
374
|
+
1. RED `tests-js/cli-registry.test.mjs`:
|
|
375
|
+
- `buildCommandsMap()` 키 집합 == `COMMAND_REGISTRY` name 집합.
|
|
376
|
+
- 모든 registry name 이 `USAGE` 에 등장, admin/introspection 헤더 아래 개수가 category 카운트와 일치.
|
|
377
|
+
- name 중복 없음, 각 name 은 정확히 1개 category.
|
|
378
|
+
- byte-anchor: `USAGE` 에 `codex-run` / `codex-dispatch` 의 현재 요약 첫 줄 정확히 포함(기존 top-level help 테스트와 정합).
|
|
379
|
+
2. GREEN: `src/cli-registry.mjs` 구현 후 `bin/okstra` 가 import 하도록 교체.
|
|
380
|
+
3. 기존 `tests-js/*.test.mjs` (codex-run/codex-dispatch top-level help, cli-script-wrappers) 회귀 통과.
|
|
381
|
+
|
|
382
|
+
### P2-3: forbidden action post-run audit 자동화
|
|
383
|
+
|
|
384
|
+
**Files:**
|
|
385
|
+
- Create: `validators/forbidden_actions.py` — per-phase 구조화 deny-list(regex SSOT) + `scan_forbidden_actions(...)`.
|
|
386
|
+
- Modify: `validators/validate-run.py` — `_validate_session_conformance` 옆(`if task_type:` 블록, line ~2230)에 `_validate_forbidden_actions(...)` hook, 실패는 `forbidden-action: ` 접두로 fold.
|
|
387
|
+
- Test: `tests/test_validate_forbidden_actions.py`.
|
|
388
|
+
|
|
389
|
+
**Deny-list SSOT (보수적 — 맥락 무관 항상 위반만):**
|
|
390
|
+
```python
|
|
391
|
+
# 모든 phase 공통: okstra 어느 phase 도 publish/deploy/force-push 안 함
|
|
392
|
+
_UNIVERSAL = [
|
|
393
|
+
("npm publish", r"\bnpm\s+publish\b"),
|
|
394
|
+
("cargo publish", r"\bcargo\s+publish\b"),
|
|
395
|
+
("pip publish", r"\bpip\s+publish\b"),
|
|
396
|
+
("twine upload", r"\btwine\s+upload\b"),
|
|
397
|
+
("gh release", r"\bgh\s+release\s+(create|edit)\b"),
|
|
398
|
+
("docker push", r"\bdocker\s+push\b"),
|
|
399
|
+
("terraform apply", r"\bterraform\s+apply\b"),
|
|
400
|
+
("kubectl apply", r"\bkubectl\s+apply\b"),
|
|
401
|
+
("git push --force", r"\bgit\s+push\b.*--force(-with-lease)?\b"),
|
|
402
|
+
]
|
|
403
|
+
# release-handoff 는 feature-branch `git push` 허용 → bare push 는 비-handoff phase 에서만 위반
|
|
404
|
+
_NON_HANDOFF_PUSH = ("git push", r"\bgit\s+push\b")
|
|
405
|
+
```
|
|
406
|
+
- phase → 적용 패턴: 모든 phase 에 `_UNIVERSAL`; `release-handoff` 외 phase 에 `_NON_HANDOFF_PUSH` 추가.
|
|
407
|
+
|
|
408
|
+
**Scanner:**
|
|
409
|
+
- `find_claude_team_sessions(project_root, resolve_team_name(team_state), lead_sid, projects_root)` 로 lead+worker 세션 jsonl 발견(conformance seam 재사용).
|
|
410
|
+
- `resolve_run_window` 로 [since, until] 스코프. assistant 레코드 & `not isSidechain` 만, `tool_use` && `name == "Bash"` 의 `input.command` 를 패턴 매칭.
|
|
411
|
+
- 위반 1건당 `f"{label} 명령이 {agent} 세션에서 실행됨: {command[:120]}"`.
|
|
412
|
+
- codex/gemini wrapper 의 셸 명령은 claude jsonl 밖 → 문서화된 한계(스캐너는 claude lead + claude worker 세션만).
|
|
413
|
+
|
|
414
|
+
**TDD:**
|
|
415
|
+
1. RED `tests/test_validate_forbidden_actions.py` (conformance 테스트의 Fixture 패턴 차용 — `claude_projects_dir` 주입, team-needle 태그 jsonl):
|
|
416
|
+
- implementation 세션에 `git push origin x` Bash → 위반 1건.
|
|
417
|
+
- implementation 세션에 `npm publish` → 위반.
|
|
418
|
+
- release-handoff 세션에 `git push -u origin feat` → 위반 없음(허용).
|
|
419
|
+
- release-handoff 세션에 `git push --force origin feat` → 위반(force).
|
|
420
|
+
- 깨끗한 세션(`npm test`, `git status`) → 위반 없음.
|
|
421
|
+
- 패턴이 prompt/text 블록에만 있고 Bash tool_use 가 아니면 → 위반 없음(false-positive 가드).
|
|
422
|
+
2. GREEN: `validators/forbidden_actions.py` 구현 + `validate-run.py` hook.
|
|
423
|
+
3. 회귀: `tests/test_validate_session_conformance.py`, `tests/test_validate_run_*.py` 통과.
|
|
424
|
+
|
|
425
|
+
### Verification
|
|
426
|
+
```bash
|
|
427
|
+
node --test tests-js/cli-registry.test.mjs
|
|
428
|
+
python3 -m pytest tests/test_validate_forbidden_actions.py -q
|
|
429
|
+
npm run check
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Execution notes — 2026-06-13
|
|
435
|
+
|
|
436
|
+
Implemented P0 scope.
|
|
437
|
+
|
|
438
|
+
Initial validation run:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
python3 -m pytest tests/test_repo_contracts.py -q
|
|
442
|
+
npm run build
|
|
443
|
+
npm run test:js
|
|
444
|
+
bash validators/validate-workflow.sh
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Observed result: all commands above passed. `npm run check` was also started, but the local harness killed it after 600 seconds while the full pytest suite was still progressing; no test failure was observed before timeout.
|
|
448
|
+
|
|
449
|
+
Follow-up validation run:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
python3 -m pytest tests/test_okstra_ctl_show_open.py::test_open_prints_absolute_final_report_path -q
|
|
453
|
+
npm run check
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Observed result: both commands passed. The first full `npm run check` attempt exposed an ambient `EDITOR` leak in `tests/test_okstra_ctl_show_open.py`; the test helper now removes inherited `EDITOR` unless a test explicitly passes it.
|
|
457
|
+
|
|
458
|
+
P1 follow-up:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
python3 -m pytest tests/test_okstra_doctor_phase.py -q
|
|
462
|
+
node --test tests-js/doctor-runtime.test.mjs
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Observed result: both targeted checks passed after adding `okstra doctor --phase <phase>` readiness diagnostics.
|
|
466
|
+
|
|
467
|
+
P1 completion:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
python3 -m pytest tests/test_repo_contracts.py tests/test_final_report_contracts.py tests/test_schema_excerpt.py tests/test_final_report_stage_reports_render.py tests/test_validate_final_verification_consistency.py -q
|
|
471
|
+
npm run check
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Observed result: focused suite reported 51 passed; full `npm run check` completed build, JS tests, Python tests, and workflow validator successfully.
|
|
475
|
+
|
|
476
|
+
P2 completion (P2-1 + P2-3; P2-2 보류):
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
node --test tests-js/cli-registry.test.mjs
|
|
480
|
+
python3 -m pytest tests/test_validate_forbidden_actions.py -q
|
|
481
|
+
npm run check
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Observed result: registry 5 passed, forbidden-actions 8 passed; full `npm run check` = JS 59 passed, Python 1558 passed/2 skipped, workflow validator success. `okstra --help` byte-identical (registry는 단일 SSOT로 COMMANDS/USAGE 파생). Reviewer APPROVED after one CHANGES_REQUESTED round (force-push `-f` 단축 플래그 미탐 등 6건 반영).
|
|
485
|
+
|
|
486
|
+
P2-2 completion (forbidden-actions JSON SSOT):
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
python3 -m pytest tests/test_phase_forbidden_ssot.py tests/test_profile_forbidden_placeholder.py tests/test_workflow_phase_sequence.py -q
|
|
490
|
+
bash validators/validate-workflow.sh
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Observed result: SSOT/placeholder/workflow 단위 30 passed; workflow validator success(렌더된 launch 경계 forbidden이 JSON에서 주입됨 확인); implementation/release-handoff profile placeholder 치환 검증(13/11 bullets, leftover 없음). Reviewer APPROVED (P3 docstring drift 1건 반영). 단, `npm run check`의 `tests/test_context_cost.py` 2건은 **환경성 실패**(codex/gemini-worker가 빌드 생성물이라 repo static 부재 + ~/.claude 미설치) — 이 diff와 무관하며 `okstra install`로 ~/.claude agents를 복구하면 해소된다.
|
|
@@ -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 |
|