okstra 0.66.0 → 0.68.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.
- package/bin/okstra +7 -0
- package/docs/kr/architecture.md +17 -1
- package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +456 -0
- package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +1408 -0
- package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +1572 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +1 -1
- package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +107 -0
- package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +105 -0
- package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +156 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +5 -4
- package/runtime/prompts/profiles/_common-contract.md +6 -6
- package/runtime/prompts/profiles/final-verification.md +3 -2
- package/runtime/prompts/profiles/release-handoff.md +12 -5
- package/runtime/prompts/wizard/prompts.ko.json +14 -4
- package/runtime/python/okstra_ctl/consumers.py +72 -5
- package/runtime/python/okstra_ctl/git_reconcile.py +322 -0
- package/runtime/python/okstra_ctl/handoff.py +348 -0
- package/runtime/python/okstra_ctl/render.py +44 -2
- package/runtime/python/okstra_ctl/run.py +88 -27
- package/runtime/python/okstra_ctl/wizard.py +141 -36
- package/runtime/python/okstra_ctl/worktree.py +10 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +40 -9
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +1 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +2 -2
- package/runtime/skills/okstra-run/SKILL.md +45 -5
- package/runtime/skills/okstra-team-contract/SKILL.md +2 -2
- package/runtime/validators/validate-run.py +49 -9
- package/src/git-reconcile.mjs +31 -0
- package/src/handoff.mjs +30 -0
package/bin/okstra
CHANGED
|
@@ -20,6 +20,11 @@ const COMMANDS = new Map([
|
|
|
20
20
|
],
|
|
21
21
|
["config", () => import("../src/config.mjs").then((m) => m.run)],
|
|
22
22
|
["migrate", () => import("../src/migrate.mjs").then((m) => m.run)],
|
|
23
|
+
[
|
|
24
|
+
"git-reconcile",
|
|
25
|
+
() => import("../src/git-reconcile.mjs").then((m) => m.run),
|
|
26
|
+
],
|
|
27
|
+
["handoff", () => import("../src/handoff.mjs").then((m) => m.run)],
|
|
23
28
|
["task-list", () => import("../src/task-list.mjs").then((m) => m.run)],
|
|
24
29
|
["task-show", () => import("../src/task-show.mjs").then((m) => m.run)],
|
|
25
30
|
["context-cost", () => import("../src/context-cost.mjs").then((m) => m.run)],
|
|
@@ -67,6 +72,8 @@ Admin commands:
|
|
|
67
72
|
check-project Verify the current project has been registered with setup
|
|
68
73
|
config Read / write okstra settings (e.g. PR template path)
|
|
69
74
|
migrate Move legacy .project-docs/okstra/ to .okstra/ (one-shot)
|
|
75
|
+
git-reconcile Reconcile stale stage SHAs after external git history changes
|
|
76
|
+
handoff Stage-group release-handoff helpers (eligible/assemble/record)
|
|
70
77
|
paths Print runtime paths (workspace/agents/pythonpath/bin/home/version)
|
|
71
78
|
|
|
72
79
|
Introspection commands (JSON output, used by skills to avoid python heredocs):
|
package/docs/kr/architecture.md
CHANGED
|
@@ -365,6 +365,19 @@ okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결
|
|
|
365
365
|
- 답변 편집과 재실행을 한 번에 처리하려면 `--resume-clarification` 모드를 사용합니다. 자세한 동작은 `### --resume-clarification` 섹션을 참고합니다.
|
|
366
366
|
- **Stage carry-in (`implementation` → 다음 stage)**: 각 `implementation` run 은 `runs/implementation/carry/stage-<N>.json` evidence sidecar 를 남깁니다. 다음 stage 가 실행될 때 이 파일을 자동으로 carry-in 합니다. 어떤 `implementation` run 이 어떤 stage 를 소비했는지는 `runs/implementation-planning/consumers.jsonl` 에 역링크로 누적됩니다.
|
|
367
367
|
|
|
368
|
+
### release-handoff stage-group 모드
|
|
369
|
+
|
|
370
|
+
`release-handoff` 는 두 모드로 동작합니다 (설계 `docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md`).
|
|
371
|
+
|
|
372
|
+
- **whole-task (기본)**: task 전체를 1개 PR 로 내보냅니다. 기존 동작입니다.
|
|
373
|
+
- **stage-group**: 단독-stage final-verification 에서 `accepted` 를 받은 stage 들 중 일부를 골라, 수집(collector) 브랜치로 묶어 1개 PR 로 내보냅니다. task 전체가 끝나기를 기다리지 않고 검증 완료된 stage 묶음 단위로 PR 을 낼 수 있습니다.
|
|
374
|
+
|
|
375
|
+
stage-group 의 상호작용 순서: **G1 base 선택 → G2 stage multi-select → assemble(수집 브랜치 생성 + 선택 stage 머지) → 충돌 프로브 → PR 초안 → push/PR**.
|
|
376
|
+
|
|
377
|
+
- 자격 판정의 SSOT 는 `consumers.jsonl` 의 두 행입니다 — `verified`(어떤 stage 가 단독-stage final-verification 에서 accepted 됨), `pr`(어떤 stage 들이 어느 PR 로 나갔는지). `verified` 인데 아직 `pr` 에 안 들어간 stage 가 자격 후보입니다.
|
|
378
|
+
- worktree registry 는 stage-group 점유를 `<task-key>#group-<id>` 키로 예약하고, 수집 브랜치 이름은 `<work-category-prefix>-<task-id-segment>-g2-3` (예: 선택한 stage 가 2·3 이면 `-g2-3`) 형태입니다.
|
|
379
|
+
- 강제 지점은 선언이 아니라 Python 모듈 `okstra_ctl.handoff` 입니다 (`okstra handoff <subcommand>`). 서브커맨드 4종: `eligible`(자격 stage 조회) / `assemble`(수집 브랜치 생성·머지) / `record-verified`(`verified` 행 기록) / `record-pr`(`pr` 행 기록). 수집 단계의 머지는 isolation spec 의 "okstra 자동 머지 없음" 비목표에 대한 명시적 예외입니다.
|
|
380
|
+
|
|
368
381
|
### improvement-discovery (sidetrack entry-point)
|
|
369
382
|
|
|
370
383
|
````
|
|
@@ -391,7 +404,10 @@ okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결
|
|
|
391
404
|
okstra-run wizard 는 최종 confirm 직전에 `branch_confirm` 단계로 "새 브랜치/worktree 생성 vs
|
|
392
405
|
현재 worktree 재사용"을 명시 확인한다. 결정은 `worktree.preview_worktree_decision()`(부수효과 없음)
|
|
393
406
|
으로 미리보고, 같은 헬퍼를 `provision_task_worktree` 가 실행에 쓰므로 미리보기와 실제가 일치한다.
|
|
394
|
-
worktree 격리 동작 자체는 변경 없음(확인 게이트만).
|
|
407
|
+
worktree 격리 동작 자체는 변경 없음(확인 게이트만). 옵션은 항상 `진행`/`중단` 을 포함하고,
|
|
408
|
+
새 worktree 생성 케이스에만 `base-ref 다시 고르기`(edit) 가 추가된다 — 어떤 케이스든 picker 로
|
|
409
|
+
렌더링 가능한 2-옵션 이상을 보장한다. `중단` 은 터미널이다: state 가 `aborted` 로 고정되고
|
|
410
|
+
이후 `next_prompt` 는 `kind: "aborted"` 만 반환하며 `render-args` 는 `ok: false` 로 거부한다.
|
|
395
411
|
|
|
396
412
|
|
|
397
413
|
---
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# 동시-run team 가드 구현 플랜
|
|
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:** 같은 task-key 의 implementation run 이 동시에 돌 때, 후발 run 이 team 을 만들지 않고 no-team background 로 worker 를 dispatch 하게 만들어 `~/.claude/teams/` config race 를 예방한다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** `prepare_task_bundle` 의 stage 선택 단계가 이미 호출하는 `list_active_stage_numbers` 로 동시-run 을 감지한다 → `StageSelection.concurrent_stages` 에 담아 → `ctx["CONCURRENT_RUN_STAGES"]` 로 wire → render 가 "Team Creation Gate" 대신 "no-team background" 블록을 emit + `PrepareOutputs.extras["concurrent_run"]` 로 진입점에 노출. 새 lock·새 추적 없음, enforce 지점은 Python 렌더.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (`scripts/okstra_ctl/`), pytest, Markdown 프롬프트/스킬, `npm run build`(소스→`runtime/` 동기화).
|
|
10
|
+
|
|
11
|
+
**설계 출처:** [docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md](../specs/2026-06-10-concurrent-run-team-guard-design.md)
|
|
12
|
+
|
|
13
|
+
**전역 규칙:**
|
|
14
|
+
- `runtime/` 는 build output — 직접 편집 금지. 소스(`scripts/`, `prompts/`, `agents/`, `skills/`)만 고치고 Task 7 에서 `npm run build`.
|
|
15
|
+
- 커밋 trailer 금지(`Co-Authored-By` / `Generated with` 등). Conventional Commits header `type(scope): …`.
|
|
16
|
+
- 각 Task 는 소스 + 테스트를 같은 커밋에 담는다.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## File Structure
|
|
21
|
+
|
|
22
|
+
| 파일 | 책임 | Task |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| [scripts/okstra_ctl/render.py](../../../scripts/okstra_ctl/render.py) | `inject_lead_prompt_computed_tokens` 에 no-team 블록 분기 추가 | 1 |
|
|
25
|
+
| [scripts/okstra_ctl/run.py](../../../scripts/okstra_ctl/run.py) | `StageSelection.concurrent_stages` 필드 + 감지·wire + `extras` 전파 | 2, 3 |
|
|
26
|
+
| [agents/SKILL.md](../../../agents/SKILL.md), [prompts/profiles/_common-contract.md](../../../prompts/profiles/_common-contract.md) | no-team fallback 의 새 legal 사유(`status:"skipped"`, `reason:"concurrent-run"`) | 4 |
|
|
27
|
+
| [skills/okstra-run/SKILL.md](../../../skills/okstra-run/SKILL.md) | 대화형 3-옵션 picker | 5 |
|
|
28
|
+
| [tests/test_render_inject_computed_tokens.py](../../../tests/test_render_inject_computed_tokens.py), [tests/test_okstra_run_select_and_apply_implementation_stage.py](../../../tests/test_okstra_run_select_and_apply_implementation_stage.py) | 단위 테스트 | 1, 2 |
|
|
29
|
+
| `tests-e2e/scenario-<id>-concurrent-run-no-team.sh` | e2e (extras 전파 + no-team 블록) | 6 |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Task 1: render — 동시-run no-team 블록 분기
|
|
34
|
+
|
|
35
|
+
**Files:**
|
|
36
|
+
- Modify: `scripts/okstra_ctl/render.py:1645-1664` (`inject_lead_prompt_computed_tokens` 내 team-gate 분기)
|
|
37
|
+
- Test: `tests/test_render_inject_computed_tokens.py`
|
|
38
|
+
|
|
39
|
+
이 Task 가 ctx 키 계약(`CONCURRENT_RUN_STAGES`)을 먼저 정의한다. Task 2 가 이 키를 채운다.
|
|
40
|
+
|
|
41
|
+
- [ ] **Step 1: 실패 테스트 작성** — `tests/test_render_inject_computed_tokens.py` 끝에 추가
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
def test_inject_concurrent_run_emits_no_team_block():
|
|
45
|
+
"""동시-run 감지 시 Team Creation Gate 대신 no-team background 블록을 emit."""
|
|
46
|
+
ctx = _base_ctx()
|
|
47
|
+
ctx["TASK_TYPE"] = "implementation"
|
|
48
|
+
ctx["EFFECTIVE_STAGES"] = "4"
|
|
49
|
+
ctx["CONCURRENT_RUN_STAGES"] = "5"
|
|
50
|
+
inject_lead_prompt_computed_tokens(ctx)
|
|
51
|
+
gate = ctx["TEAM_CREATION_GATE"]
|
|
52
|
+
assert "Concurrent-run: no-team background" in gate
|
|
53
|
+
# 실제 team 생성 호출 형태는 없어야 한다
|
|
54
|
+
assert 'TeamCreate(team_name:' not in gate
|
|
55
|
+
assert 'reason: "concurrent-run"' in gate
|
|
56
|
+
assert "run_in_background" in gate
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_inject_no_concurrent_keeps_team_gate():
|
|
60
|
+
"""CONCURRENT_RUN_STAGES 가 없으면 기존 Team Creation Gate 그대로(회귀 방지)."""
|
|
61
|
+
ctx = _base_ctx()
|
|
62
|
+
ctx["TASK_TYPE"] = "implementation"
|
|
63
|
+
ctx["EFFECTIVE_STAGES"] = "4"
|
|
64
|
+
inject_lead_prompt_computed_tokens(ctx)
|
|
65
|
+
assert "Team Creation Gate (BLOCKING)" in ctx["TEAM_CREATION_GATE"]
|
|
66
|
+
assert 'TeamCreate(team_name: "okstra-demo/task-a-s4"' in ctx["TEAM_CREATION_GATE"]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- [ ] **Step 2: 실패 확인**
|
|
70
|
+
|
|
71
|
+
Run: `python3 -m pytest tests/test_render_inject_computed_tokens.py::test_inject_concurrent_run_emits_no_team_block -v`
|
|
72
|
+
Expected: FAIL — `"Concurrent-run: no-team background"` 가 gate 에 없음(현재는 Team Creation Gate 가 나옴).
|
|
73
|
+
|
|
74
|
+
- [ ] **Step 3: 분기 추가** — `render.py` 의 `task_type = ctx.get("TASK_TYPE", "")` 줄(현 1645) 바로 아래에 `concurrent_stages` 를 읽고, `if task_type == "release-handoff" or not selected:` 와 `else:` 사이에 `elif` 를 끼운다. 최종 형태:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
task_type = ctx.get("TASK_TYPE", "")
|
|
78
|
+
concurrent_stages = str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").strip()
|
|
79
|
+
if task_type == "release-handoff" or not selected:
|
|
80
|
+
team_creation_gate_block = (
|
|
81
|
+
"## Single-Lead Phase (no team creation)\n"
|
|
82
|
+
"\n"
|
|
83
|
+
"This run is single-lead. There is no worker roster, no\n"
|
|
84
|
+
"`TeamCreate` call, no `Agent(...)` worker dispatch, and no\n"
|
|
85
|
+
"convergence loop. The Claude lead performs every step inline\n"
|
|
86
|
+
"(reading inputs, drafting commit / PR text when applicable,\n"
|
|
87
|
+
"asking the user, running git / gh, and writing the final\n"
|
|
88
|
+
"report). Do NOT call `TeamCreate` or dispatch any sub-agent\n"
|
|
89
|
+
"from this run — that would be a contract violation."
|
|
90
|
+
)
|
|
91
|
+
elif task_type == "implementation" and concurrent_stages:
|
|
92
|
+
team_creation_gate_block = (
|
|
93
|
+
"## Concurrent-run: no-team background (BLOCKING)\n"
|
|
94
|
+
"\n"
|
|
95
|
+
"A concurrent implementation run of the same task-key holds stage(s)\n"
|
|
96
|
+
f"`{concurrent_stages}` right now. Creating a team would race the shared\n"
|
|
97
|
+
"`~/.claude/teams/` config and can make another stage's `config.json`\n"
|
|
98
|
+
"unreadable. This run therefore does NOT create a team.\n"
|
|
99
|
+
"\n"
|
|
100
|
+
"Required actions, in order:\n"
|
|
101
|
+
"\n"
|
|
102
|
+
"1. Do NOT call `TeamCreate`, and do NOT dispatch any worker with\n"
|
|
103
|
+
" `Agent(... team_name: ...)`.\n"
|
|
104
|
+
"2. Before any dispatch, record in team-state:\n"
|
|
105
|
+
' `teamCreate: { attempted: false, status: "skipped",'
|
|
106
|
+
f' reason: "concurrent-run", concurrentStages: [{concurrent_stages}] }}`.\n'
|
|
107
|
+
"3. Dispatch every worker with `run_in_background: true` and NO\n"
|
|
108
|
+
" `team_name` (the Phase 5 fallback). Worker completion is detected by\n"
|
|
109
|
+
" result-file polling, so analysis output is equivalent — only the\n"
|
|
110
|
+
" Teams split-pane view is lost."
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
team_name = f'okstra-{ctx.get("TASK_KEY", "")}'
|
|
114
|
+
stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
|
|
115
|
+
if task_type == "implementation" and stage:
|
|
116
|
+
# stage 격리 run 은 stage 별 team — 같은 task 의 다른 stage 가 남긴
|
|
117
|
+
# team 과 이름이 충돌하지 않는다(worktree branch `-s<N>` 접미사와 동형).
|
|
118
|
+
team_name = f"{team_name}-s{stage}"
|
|
119
|
+
team_creation_gate_block = (
|
|
120
|
+
# ... 기존 "## Team Creation Gate (BLOCKING)" 블록 그대로 유지 ...
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> 주의: `else:` 안의 기존 Team Creation Gate 블록 본문(현 1665-1703)은 **그대로 둔다**. `elif` 만 끼우는 변경이다.
|
|
125
|
+
|
|
126
|
+
- [ ] **Step 4: 통과 확인**
|
|
127
|
+
|
|
128
|
+
Run: `python3 -m pytest tests/test_render_inject_computed_tokens.py -v`
|
|
129
|
+
Expected: PASS (신규 2개 + 기존 전부).
|
|
130
|
+
|
|
131
|
+
- [ ] **Step 5: 커밋**
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
git add scripts/okstra_ctl/render.py tests/test_render_inject_computed_tokens.py
|
|
135
|
+
git commit -m "feat(okstra-ctl): 동시-run 시 no-team background 게이트 블록 렌더"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Task 2: run — 동시-run 감지 + ctx wire
|
|
141
|
+
|
|
142
|
+
**Files:**
|
|
143
|
+
- Modify: `scripts/okstra_ctl/run.py:1297-1313` (`StageSelection` 필드 추가)
|
|
144
|
+
- Modify: `scripts/okstra_ctl/run.py:1340-1412` (`_select_and_provision_implementation_stage` 채우기)
|
|
145
|
+
- Modify: `scripts/okstra_ctl/run.py:1426-1429` (`_apply_implementation_stage` ctx wire)
|
|
146
|
+
- Test: `tests/test_okstra_run_select_and_apply_implementation_stage.py`
|
|
147
|
+
|
|
148
|
+
- [ ] **Step 1: 실패 테스트 작성** — 파일 끝에 추가. registry 에 다른 stage 를 active 로 미리 예약해 동시-run 을 시뮬레이션한다.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
def test_select_flags_concurrent_active_stage(tmp_path, monkeypatch):
|
|
152
|
+
"""다른 run 이 stage 6 을 active 로 점유 중이면, stage 4 선택 결과의
|
|
153
|
+
concurrent_stages 에 6 이 담긴다."""
|
|
154
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
155
|
+
from okstra_ctl import worktree_registry as reg
|
|
156
|
+
from okstra_ctl.run import _select_and_provision_implementation_stage
|
|
157
|
+
|
|
158
|
+
repo = tmp_path / "repo"
|
|
159
|
+
head = _init_repo(repo)
|
|
160
|
+
|
|
161
|
+
# 다른 동시 run 이 stage 6 을 active 로 점유 중이라고 가정
|
|
162
|
+
reg.reserve(
|
|
163
|
+
project_id="proj", task_group="grp", task_id="tid",
|
|
164
|
+
worktree_path=str(tmp_path / "s6"), branch="feature-tid-s6",
|
|
165
|
+
base_ref=head, stage_number=6,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
plan = _consumers_dir(tmp_path)
|
|
169
|
+
inp = _Inp(project_root=str(repo), approved_plan_path=str(plan), stage="auto")
|
|
170
|
+
stages = [_stage(4, []), _stage(6, [])]
|
|
171
|
+
|
|
172
|
+
sel = _select_and_provision_implementation_stage(
|
|
173
|
+
inp, stages, "grp", "tid", "proj:grp:tid", "ok",
|
|
174
|
+
)
|
|
175
|
+
assert sel.stage == 4
|
|
176
|
+
assert sel.concurrent_stages == [6]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_select_no_concurrent_when_alone(tmp_path):
|
|
180
|
+
"""동시 run 이 없으면 concurrent_stages 는 빈 리스트."""
|
|
181
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
182
|
+
from okstra_ctl.run import _select_and_provision_implementation_stage
|
|
183
|
+
|
|
184
|
+
repo = tmp_path / "repo"
|
|
185
|
+
head = _init_repo(repo)
|
|
186
|
+
plan = _consumers_dir(tmp_path)
|
|
187
|
+
inp = _Inp(project_root=str(repo), approved_plan_path=str(plan), stage="auto")
|
|
188
|
+
stages = [_stage(4, [])]
|
|
189
|
+
|
|
190
|
+
sel = _select_and_provision_implementation_stage(
|
|
191
|
+
inp, stages, "grp", "tid", "proj:grp:tid", "ok",
|
|
192
|
+
)
|
|
193
|
+
assert sel.stage == 4
|
|
194
|
+
assert sel.concurrent_stages == []
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> `conftest.py` 가 `OKSTRA_HOME` 를 격리하므로 registry 는 테스트 임시 경로에 쓰인다. `_init_repo` / `_stage` / `_Inp` / `_consumers_dir` 는 파일 상단의 기존 헬퍼다.
|
|
198
|
+
|
|
199
|
+
- [ ] **Step 2: 실패 확인**
|
|
200
|
+
|
|
201
|
+
Run: `python3 -m pytest tests/test_okstra_run_select_and_apply_implementation_stage.py::test_select_flags_concurrent_active_stage -v`
|
|
202
|
+
Expected: FAIL — `AttributeError: 'StageSelection' object has no attribute 'concurrent_stages'`.
|
|
203
|
+
|
|
204
|
+
- [ ] **Step 3a: `StageSelection` 필드 추가** — `run.py:1306-1312` 의 dataclass 끝(`started_head_commit: str` 다음 줄)에 추가:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
started_head_commit: str
|
|
208
|
+
concurrent_stages: list = field(default_factory=list)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> 파일 상단에 `from dataclasses import dataclass, field` 가 이미 있는지 확인하고, `field` 가 없으면 import 에 추가한다.
|
|
212
|
+
|
|
213
|
+
- [ ] **Step 3b: 감지값 계산 + 두 반환 경로에 담기** — `_select_and_provision_implementation_stage` 안, `selected = batch[0]`(현 1350) 바로 다음 줄에:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
selected = batch[0]
|
|
217
|
+
concurrent_stages = sorted(reserved_stages - {selected})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
그리고 이 함수의 **두 `return StageSelection(...)`** (degrade 경로 현 1356, 정상 경로 현 1404) 둘 다 마지막 인자로 추가:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
started_head_commit=head, # degrade 경로
|
|
224
|
+
concurrent_stages=concurrent_stages,
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
started_head_commit=prov.base_ref, # 정상 경로
|
|
229
|
+
concurrent_stages=concurrent_stages,
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- [ ] **Step 3c: ctx wire** — `_apply_implementation_stage` 안, `ctx["EFFECTIVE_STAGES"] = csv`(현 1429) 다음 줄에:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
ctx["CONCURRENT_RUN_STAGES"] = ",".join(str(s) for s in sel.concurrent_stages)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- [ ] **Step 4: 통과 확인**
|
|
239
|
+
|
|
240
|
+
Run: `python3 -m pytest tests/test_okstra_run_select_and_apply_implementation_stage.py -v`
|
|
241
|
+
Expected: PASS (신규 2개 + 기존 전부).
|
|
242
|
+
|
|
243
|
+
- [ ] **Step 5: 커밋**
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
git add scripts/okstra_ctl/run.py tests/test_okstra_run_select_and_apply_implementation_stage.py
|
|
247
|
+
git commit -m "feat(okstra-ctl): 동시 active stage 를 StageSelection.concurrent_stages 로 감지·wire"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Task 3: run — `extras.concurrent_run` 전파
|
|
253
|
+
|
|
254
|
+
**Files:**
|
|
255
|
+
- Modify: `scripts/okstra_ctl/run.py:2005-2009` (`prepare_task_bundle` 의 `return PrepareOutputs(...)`)
|
|
256
|
+
|
|
257
|
+
진입점(스킬 / okstra.sh)이 동시-run 여부를 읽을 수 있도록 `extras` 에 싣는다. 전체 사이클 검증은 Task 6 e2e 가 담당한다.
|
|
258
|
+
|
|
259
|
+
- [ ] **Step 1: `return PrepareOutputs(...)` 수정** — 현재:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
return PrepareOutputs(
|
|
263
|
+
ctx=ctx,
|
|
264
|
+
prompt_text=prompt_text,
|
|
265
|
+
extras={"profile_content": profile_content},
|
|
266
|
+
)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
를 다음으로:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
concurrent_run: dict = {}
|
|
273
|
+
if impl_stage_selection is not None and impl_stage_selection.concurrent_stages:
|
|
274
|
+
concurrent_run = {
|
|
275
|
+
"detected": True,
|
|
276
|
+
"active_stages": impl_stage_selection.concurrent_stages,
|
|
277
|
+
}
|
|
278
|
+
return PrepareOutputs(
|
|
279
|
+
ctx=ctx,
|
|
280
|
+
prompt_text=prompt_text,
|
|
281
|
+
extras={
|
|
282
|
+
"profile_content": profile_content,
|
|
283
|
+
"concurrent_run": concurrent_run,
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
> `impl_stage_selection` 은 현 1825/1831 에서 정의돼 이 지점까지 유효하다(implementation 외 task-type 은 `None`).
|
|
289
|
+
|
|
290
|
+
- [ ] **Step 2: import 회귀 확인** (단위 테스트 없음 — 코드 단순, Task 6 e2e 가 실측)
|
|
291
|
+
|
|
292
|
+
Run: `python3 -c "import sys; sys.path.insert(0,'scripts'); from okstra_ctl.run import prepare_task_bundle; print('ok')"`
|
|
293
|
+
Expected: `ok`
|
|
294
|
+
|
|
295
|
+
- [ ] **Step 3: 커밋**
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
git add scripts/okstra_ctl/run.py
|
|
299
|
+
git commit -m "feat(okstra-ctl): PrepareOutputs.extras 로 concurrent_run 노출"
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Task 4: 프롬프트/agent 계약 — no-team 의 새 legal 사유
|
|
305
|
+
|
|
306
|
+
**Files:**
|
|
307
|
+
- Modify: `agents/SKILL.md:231`
|
|
308
|
+
- Modify: `prompts/profiles/_common-contract.md:53`
|
|
309
|
+
|
|
310
|
+
현재 no-`team_name` fallback 은 `teamCreate.status == "error"` 일 때만 legal 이다. 동시-run 사전 skip 을 legal 로 추가한다.
|
|
311
|
+
|
|
312
|
+
- [ ] **Step 1: `agents/SKILL.md:231` 수정** — 현재 문장:
|
|
313
|
+
|
|
314
|
+
> The no-`team_name` fallback (Phase 5) is only legal when team-state's `teamCreate.status` is `"error"` for this run.
|
|
315
|
+
|
|
316
|
+
를 다음으로:
|
|
317
|
+
|
|
318
|
+
> The no-`team_name` fallback (Phase 5) is legal when team-state's `teamCreate.status` is `"error"` (TeamCreate attempted and failed) OR `"skipped"` with `reason: "concurrent-run"` (the launch prompt's "Concurrent-run: no-team background" gate pre-decided no team to avoid racing a concurrent same-task run's `~/.claude/teams/` config). In both cases `teamCreate` is recorded in team-state before any dispatch.
|
|
319
|
+
|
|
320
|
+
- [ ] **Step 2: `_common-contract.md:53` 수정** — 현재 문장:
|
|
321
|
+
|
|
322
|
+
> This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallback there is no team to delete, so silent-skip.
|
|
323
|
+
|
|
324
|
+
를 다음으로:
|
|
325
|
+
|
|
326
|
+
> This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallback — whether `teamCreate.status` is `"error"` or `"skipped"` (`reason: "concurrent-run"`) — there is no team to delete, so silent-skip.
|
|
327
|
+
|
|
328
|
+
- [ ] **Step 3: grep 검증**
|
|
329
|
+
|
|
330
|
+
Run: `grep -n 'concurrent-run' agents/SKILL.md prompts/profiles/_common-contract.md`
|
|
331
|
+
Expected: 두 파일 각각 최소 1줄 매칭.
|
|
332
|
+
|
|
333
|
+
- [ ] **Step 4: 커밋**
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
git add agents/SKILL.md prompts/profiles/_common-contract.md
|
|
337
|
+
git commit -m "docs(agents): no-team fallback 에 concurrent-run skipped legal 사유 추가"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Task 5: okstra-run 스킬 — 대화형 3-옵션 picker
|
|
343
|
+
|
|
344
|
+
**Files:**
|
|
345
|
+
- Modify: `skills/okstra-run/SKILL.md` (conformance-waiver picker 섹션, 현 192 부근 근처에 동형 섹션 추가)
|
|
346
|
+
|
|
347
|
+
- [ ] **Step 1: 섹션 추가** — conformance-waiver picker 설명 블록과 같은 패턴으로, prepare 호출 결과에서 `extras.concurrent_run.detected == true` 일 때의 분기를 추가한다. 본문:
|
|
348
|
+
|
|
349
|
+
```markdown
|
|
350
|
+
### 동시-run 감지 분기 (concurrent-run)
|
|
351
|
+
|
|
352
|
+
`prepare_task_bundle` 결과의 `extras.concurrent_run.detected` 가 `true` 이면(같은
|
|
353
|
+
task-key 의 다른 implementation run 이 `extras.concurrent_run.active_stages` 를
|
|
354
|
+
점유 중), launch 프롬프트는 이미 "Concurrent-run: no-team background" 게이트로
|
|
355
|
+
렌더돼 있다. dispatch 전에 사용자에게 3-옵션 recommendation picker 를 제시한다
|
|
356
|
+
(run-prompt recommendation 규칙: 1–2 추천 + 직접 입력; 이 picker 는 스킬이
|
|
357
|
+
author 하는 것이라 wizard `options[]` 제약과 무관):
|
|
358
|
+
|
|
359
|
+
1. (추천) 이대로 no-team background 로 진행 — 이미 렌더된 bundle 을 그대로 사용한다.
|
|
360
|
+
team 을 만들지 않아 `~/.claude/teams/` race 를 회피한다(Teams split-pane 관찰성만 포기).
|
|
361
|
+
2. 대기 — 지금 dispatch 를 보류한다. stage worktree·run-context 는 보존되므로,
|
|
362
|
+
점유 중인 다른 run 종료 후 같은 stage 를 resume 으로 재개하면 그때는 정상 team
|
|
363
|
+
경로다. resume 명령(`okstra-inspect` history → resume)을 사용자에게 출력한다.
|
|
364
|
+
3. 직접 입력.
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
> 위치: conformance-waiver picker 설명([skills/okstra-run/SKILL.md](../../../skills/okstra-run/SKILL.md) 의 "Conformance waiver" 섹션) 바로 다음. 같은 "스킬이 author 하는 picker" 부류이므로 인접 배치한다.
|
|
368
|
+
|
|
369
|
+
- [ ] **Step 2: grep 검증**
|
|
370
|
+
|
|
371
|
+
Run: `grep -n 'concurrent-run\|no-team background\|동시-run' skills/okstra-run/SKILL.md`
|
|
372
|
+
Expected: 최소 1줄 매칭.
|
|
373
|
+
|
|
374
|
+
- [ ] **Step 3: 커밋**
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
git add skills/okstra-run/SKILL.md
|
|
378
|
+
git commit -m "feat(skills/okstra-run): 동시-run 시 no-team/대기 3-옵션 picker 추가"
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Task 6: e2e 시나리오 — extras 전파 + no-team 블록
|
|
384
|
+
|
|
385
|
+
**Files:**
|
|
386
|
+
- Create: `tests-e2e/scenario-<다음번호>-concurrent-run-no-team.sh`
|
|
387
|
+
|
|
388
|
+
기존 시나리오 번호를 확인해 다음 번호를 쓴다: `ls tests-e2e/ | grep -oE 'scenario-[0-9]+' | sort -u | tail -1`.
|
|
389
|
+
|
|
390
|
+
- [ ] **Step 1: 시나리오 작성** — 기존 시나리오 골격(`mktemp -d` 로 `OKSTRA_HOME`, `trap` EXIT 정리)을 따른다. 핵심 단계:
|
|
391
|
+
1. 임시 git repo + `.okstra/project.json` 셋업, implementation plan(stage 4·6) 준비.
|
|
392
|
+
2. registry 에 stage 6 을 active 로 reserve(다른 run 점유 시뮬레이션).
|
|
393
|
+
3. `prepare_task_bundle`(또는 `okstra.sh --task-type implementation --stage auto`)로 stage 4 run 을 prepare.
|
|
394
|
+
4. 검증: 렌더된 launch 프롬프트에 `"Concurrent-run: no-team background"` 포함 + `'TeamCreate(team_name:'` **미포함**.
|
|
395
|
+
5. 검증: prepare 의 `extras.concurrent_run.detected == true`, `active_stages == [6]`.
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
#!/usr/bin/env bash
|
|
399
|
+
set -euo pipefail
|
|
400
|
+
OKSTRA_HOME="$(mktemp -d)"; export OKSTRA_HOME
|
|
401
|
+
trap 'rm -rf "$OKSTRA_HOME"' EXIT
|
|
402
|
+
# ... repo/project/plan 셋업 (기존 scenario-01 패턴 참조) ...
|
|
403
|
+
# ... reg.reserve(stage_number=6) ...
|
|
404
|
+
# ... prepare 호출 후 launch 프롬프트 grep "Concurrent-run: no-team background" ...
|
|
405
|
+
echo "PASS: concurrent-run no-team scenario"
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- [ ] **Step 2: 실행**
|
|
409
|
+
|
|
410
|
+
Run: `bash tests-e2e/scenario-<번호>-concurrent-run-no-team.sh`
|
|
411
|
+
Expected: `PASS: concurrent-run no-team scenario`
|
|
412
|
+
|
|
413
|
+
- [ ] **Step 3: 커밋**
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
git add tests-e2e/scenario-<번호>-concurrent-run-no-team.sh
|
|
417
|
+
git commit -m "test(e2e): 동시-run no-team background 시나리오 추가"
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## Task 7: build + 전체 회귀
|
|
423
|
+
|
|
424
|
+
**Files:**
|
|
425
|
+
- Modify: `runtime/**` (build output — 직접 편집 아님, `npm run build` 산출)
|
|
426
|
+
|
|
427
|
+
- [ ] **Step 1: build**
|
|
428
|
+
|
|
429
|
+
Run: `npm run build`
|
|
430
|
+
Expected: 에러 없이 `scripts/`·`prompts/`·`agents/`·`skills/` 가 `runtime/` 로 동기화.
|
|
431
|
+
|
|
432
|
+
- [ ] **Step 2: 전체 단위 테스트**
|
|
433
|
+
|
|
434
|
+
Run: `python3 -m pytest tests/ -q`
|
|
435
|
+
Expected: 전부 PASS (신규 포함, 회귀 없음).
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 3: phase contract validator**
|
|
438
|
+
|
|
439
|
+
Run: `bash validators/validate-workflow.sh`
|
|
440
|
+
Expected: PASS.
|
|
441
|
+
|
|
442
|
+
- [ ] **Step 4: 커밋**
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
git add runtime/
|
|
446
|
+
git commit -m "chore(build): 동시-run team 가드 runtime 동기화"
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Self-Review (작성자 체크 완료)
|
|
452
|
+
|
|
453
|
+
- **Spec 커버리지:** §4.1 감지→Task 2, §4.2 no-team 렌더→Task 1, §4.3 대화형 picker→Task 5, §4.4 비대화형 기본값→Task 1+3(렌더가 자동 no-team, 진입점 분기 없음), §4.5 단일 수렴점→Task 3(`prepare_task_bundle` 한 곳), §4.6 legal 사유→Task 4, §7 테스트→Task 1·2·6. 누락 없음.
|
|
454
|
+
- **비대화형(§4.4)** 은 별도 코드가 아니라 "picker 를 안 띄우면 렌더된 no-team bundle 그대로 진행"으로 자동 충족됨 — 추가 Task 불필요.
|
|
455
|
+
- **타입 일관성:** `concurrent_stages` 는 `list[int]`(Task 2) → ctx 에서 csv `str`(Task 2 wire) → render 가 `str` 으로 읽음(Task 1) → `extras.active_stages` 는 `list[int]`(Task 3). 경계마다 형 변환 명시.
|
|
456
|
+
- **Placeholder:** 없음(시나리오 번호만 실행 시 확정 — Task 6 Step 0 에 조회 명령 제공).
|