okstra 0.96.1 → 0.97.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 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/kr/architecture/storage-model.md +273 -0
- package/docs/kr/architecture.md +6 -277
- package/docs/kr/cli.md +1 -1
- package/docs/project-structure-overview.md +26 -22
- package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +1019 -0
- package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +583 -0
- package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +819 -0
- package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +348 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +2 -2
- package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +171 -0
- package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +112 -0
- package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +145 -0
- package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +113 -0
- package/docs/task-process/release-handoff.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +1 -1
- package/runtime/bin/lib/okstra/interactive.sh +30 -4
- package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +18 -5
- package/runtime/bin/okstra-render-final-report.py +4 -15
- package/runtime/bin/okstra.sh +5 -2
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -0
- package/runtime/prompts/profiles/forbidden-actions.json +1 -1
- package/runtime/prompts/profiles/release-handoff.md +10 -4
- package/runtime/python/okstra_ctl/__init__.py +3 -2
- package/runtime/python/okstra_ctl/backfill.py +12 -21
- package/runtime/python/okstra_ctl/codex_dispatch.py +7 -2
- package/runtime/python/okstra_ctl/conformance.py +21 -3
- package/runtime/python/okstra_ctl/consumers.py +7 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +10 -2
- package/runtime/python/okstra_ctl/doctor.py +29 -12
- package/runtime/python/okstra_ctl/error_log_core.py +72 -0
- package/runtime/python/okstra_ctl/error_report.py +8 -58
- package/runtime/python/okstra_ctl/error_zip.py +319 -0
- package/runtime/python/okstra_ctl/fanout.py +5 -2
- package/runtime/python/okstra_ctl/fix_cycles.py +14 -2
- package/runtime/python/okstra_ctl/git_reconcile.py +7 -1
- package/runtime/python/okstra_ctl/handoff.py +125 -26
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -2
- package/runtime/python/okstra_ctl/improvement_lenses.py +2 -0
- package/runtime/python/okstra_ctl/index.py +34 -91
- package/runtime/python/okstra_ctl/jsonl.py +29 -18
- package/runtime/python/okstra_ctl/listing.py +30 -5
- package/runtime/python/okstra_ctl/md_table.py +1 -1
- package/runtime/python/okstra_ctl/migrate.py +17 -7
- package/runtime/python/okstra_ctl/paths.py +1 -1
- package/runtime/python/okstra_ctl/recap.py +12 -3
- package/runtime/python/okstra_ctl/reconcile.py +13 -11
- package/runtime/python/okstra_ctl/render.py +49 -12
- package/runtime/python/okstra_ctl/render_final_report.py +4 -0
- package/runtime/python/okstra_ctl/report_views.py +47 -6
- package/runtime/python/okstra_ctl/run.py +128 -100
- package/runtime/python/okstra_ctl/run_index_row.py +118 -0
- package/runtime/python/okstra_ctl/schema_excerpt.py +8 -2
- package/runtime/python/okstra_ctl/sequence.py +2 -2
- package/runtime/python/okstra_ctl/stage_integrate.py +193 -0
- package/runtime/python/okstra_ctl/stage_targets.py +2 -8
- package/runtime/python/okstra_ctl/task_target.py +4 -1
- package/runtime/python/okstra_ctl/team_reconcile.py +6 -1
- package/runtime/python/okstra_ctl/wizard.py +27 -12
- package/runtime/python/okstra_ctl/workflow.py +9 -1
- package/runtime/python/okstra_ctl/worktree.py +93 -4
- package/runtime/python/okstra_ctl/worktree_registry.py +57 -8
- package/runtime/python/okstra_token_usage/collect.py +43 -38
- package/runtime/skills/okstra-brief/SKILL.md +43 -12
- package/runtime/skills/okstra-inspect/SKILL.md +44 -1
- package/runtime/templates/reports/final-report.template.md +2 -2
- package/runtime/templates/reports/release-handoff-input.template.md +1 -1
- package/runtime/validators/forbidden_actions.py +8 -1
- package/runtime/validators/validate-brief.py +13 -2
- package/runtime/validators/validate-run.py +27 -6
- package/runtime/validators/validate-schedule.py +1 -2
- package/runtime/validators/validate_improvement_report.py +14 -0
- package/src/cli-registry.mjs +45 -31
- package/src/{codex-dispatch.mjs → commands/execute/codex-dispatch.mjs} +3 -3
- package/src/{codex-run.mjs → commands/execute/codex-run.mjs} +3 -3
- package/src/{error-log.mjs → commands/execute/error-log.mjs} +1 -1
- package/src/{git-reconcile.mjs → commands/execute/git-reconcile.mjs} +1 -1
- package/src/{handoff.mjs → commands/execute/handoff.mjs} +3 -1
- package/src/commands/execute/integrate-stages.mjs +25 -0
- package/src/{plan-validate.mjs → commands/execute/plan-validate.mjs} +1 -1
- package/src/{render-bundle.mjs → commands/execute/render-bundle.mjs} +4 -4
- package/src/{run.mjs → commands/execute/run.mjs} +4 -4
- package/src/{spawn-followups.mjs → commands/execute/spawn-followups.mjs} +1 -1
- package/src/{team.mjs → commands/execute/team.mjs} +3 -3
- package/src/{token-usage.mjs → commands/execute/token-usage.mjs} +1 -1
- package/src/{wizard.mjs → commands/execute/wizard.mjs} +15 -6
- package/src/{worktree-lookup.mjs → commands/execute/worktree-lookup.mjs} +1 -1
- package/src/{context-cost.mjs → commands/inspect/context-cost.mjs} +2 -2
- package/src/{error-report.mjs → commands/inspect/error-report.mjs} +2 -2
- package/src/commands/inspect/error-zip.mjs +25 -0
- package/src/{recap.mjs → commands/inspect/recap.mjs} +2 -2
- package/src/{task-list.mjs → commands/inspect/task-list.mjs} +1 -1
- package/src/{task-show.mjs → commands/inspect/task-show.mjs} +1 -1
- package/src/{check-project.mjs → commands/lifecycle/check-project.mjs} +2 -2
- package/src/{config.mjs → commands/lifecycle/config.mjs} +3 -3
- package/src/{doctor.mjs → commands/lifecycle/doctor.mjs} +4 -4
- package/src/{install.mjs → commands/lifecycle/install.mjs} +18 -13
- package/src/{migrate.mjs → commands/lifecycle/migrate.mjs} +1 -1
- package/src/{paths.mjs → commands/lifecycle/paths.mjs} +1 -60
- package/src/{setup.mjs → commands/lifecycle/setup.mjs} +4 -4
- package/src/{uninstall.mjs → commands/lifecycle/uninstall.mjs} +20 -32
- package/src/{memory.mjs → commands/memory/memory.mjs} +32 -7
- package/src/{inject-report-index.mjs → commands/report/inject-report-index.mjs} +1 -1
- package/src/{render-final-report.mjs → commands/report/render-final-report.mjs} +1 -1
- package/src/{render-views.mjs → commands/report/render-views.mjs} +1 -1
- package/src/lib/paths.mjs +60 -0
- package/src/{_python-helper.mjs → lib/python-helper.mjs} +35 -13
- package/src/{version.mjs → lib/version.mjs} +2 -2
- /package/src/{okstra-dirs.mjs → lib/okstra-dirs.mjs} +0 -0
- /package/src/{_proc.mjs → lib/proc.mjs} +0 -0
- /package/src/{runtime-manifest.mjs → lib/runtime-manifest.mjs} +0 -0
- /package/src/{runtime-resolver.mjs → lib/runtime-resolver.mjs} +0 -0
- /package/src/{skill-catalog.mjs → lib/skill-catalog.mjs} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# run-index row 정규화 (runId SSOT화)
|
|
2
|
+
|
|
3
|
+
- 상태: 설계 승인 대기
|
|
4
|
+
- 작성일: 2026-06-20
|
|
5
|
+
- 범위: `~/.okstra/{recent,active}.jsonl` + `~/.okstra/projects/<id>/index.jsonl` 의 run-index row 스키마
|
|
6
|
+
|
|
7
|
+
## 1. 배경 / 문제
|
|
8
|
+
|
|
9
|
+
run-index row 는 현재 18개 필드를 가지며, 그 중 다수가 `runId` 또는 다른 평면 필드에서 파생 가능한 **중복 값**이다.
|
|
10
|
+
한 레코드는 `record_start()` 가 [`scripts/okstra_ctl/index.py:74`](../../../scripts/okstra_ctl/index.py:74) 에서 조립하고
|
|
11
|
+
`recent.jsonl`·`active.jsonl`·`projects/<id>/index.jsonl` 세 곳에 동일하게 기록된다.
|
|
12
|
+
|
|
13
|
+
중복은 두 층위로 나뉜다.
|
|
14
|
+
|
|
15
|
+
- **① 파일 간 row 복제** — 같은 row 가 recent / index / active 에 복사된다. 이는 reader 가 join 없이 한 파일만 읽도록 한 **의도된 비정규화 인덱스**이며 이번 범위에서 **건드리지 않는다**.
|
|
16
|
+
- **② 레코드 내 파생 필드** — 한 row 안에서 다른 필드로부터 계산 가능한 값들. **이번 작업의 대상**이다.
|
|
17
|
+
|
|
18
|
+
`runId` 형식은 [`scripts/okstra_ctl/ids.py:8`](../../../scripts/okstra_ctl/ids.py:8) 에 정의된 `<projectId>/<taskGroup>/<taskId>/<taskType>/r<runSeq>` 슬러그이고,
|
|
19
|
+
역파서 `parse_run_id()` 가 [`scripts/okstra_ctl/ids.py:34`](../../../scripts/okstra_ctl/ids.py:34) 에 **이미 존재**한다.
|
|
20
|
+
|
|
21
|
+
## 2. 검증 (설계의 근거)
|
|
22
|
+
|
|
23
|
+
기존 246개 row(recent 124 + 6개 프로젝트 index + active) 전체에 대해 "평면 필드 == `parse_run_id(runId)` 결과" 및 "경로 필드 == 규칙 재구성" 일치 여부를 전수 검사했다.
|
|
24
|
+
|
|
25
|
+
| 필드 | 결과 | 판정 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `projectId` | 불일치 0 | 무손실 파생 가능 |
|
|
28
|
+
| `taskType` | 불일치 0 | 무손실 파생 가능 |
|
|
29
|
+
| `runSeq` | 불일치 0 | 무손실 파생 가능 |
|
|
30
|
+
| `invocationFile` | 불일치 0 | 규칙 재구성 가능 (slug 기반) |
|
|
31
|
+
| `taskGroup` | 불일치 74 | **파생 불가** — 평면값은 raw 대소문자(`DEV-9388`), runId 세그먼트는 slug(`dev-9388`) |
|
|
32
|
+
| `taskId` | 불일치 48 | **파생 불가** — 동일 사유 |
|
|
33
|
+
| `taskKey` | 불일치 74 | runId 파싱으로는 불가, 단 평면 `taskGroup`+`taskId` 조합으로는 무손실 |
|
|
34
|
+
| `runDirRel` | 불일치 148 | **파생 불가** — 옛 데이터는 `.project-docs/okstra/tasks/...` prefix(artifact-home 변경 이력), 카운터 분리로 규칙 재구성 불가 |
|
|
35
|
+
|
|
36
|
+
**핵심 교훈**: `taskGroup`/`taskId`/`taskKey` 는 raw 대소문자를 보존하므로 runId(slug)에서 복원하면 정보가 손실된다. 따라서 raw 평면값을 유지해야 한다.
|
|
37
|
+
|
|
38
|
+
## 3. 결정
|
|
39
|
+
|
|
40
|
+
### 3.1 제거 — 5개 필드 (검증상 무손실 복원 가능)
|
|
41
|
+
|
|
42
|
+
| 제거 필드 | 복원 방법 |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `projectId` | `parse_run_id(runId)["projectId"]` |
|
|
45
|
+
| `taskType` | `parse_run_id(runId)["taskType"]` |
|
|
46
|
+
| `runSeq` | `parse_run_id(runId)["runSeq"]` |
|
|
47
|
+
| `taskKey` | `f"{row['taskGroup']}/{row['taskId']}"` (평면 raw 조합) |
|
|
48
|
+
| `invocationFile` | `projects/<pid>/invocations/<slug taskGroup>/<slug taskId>/<taskType>/r<NN>.json` — `parse_run_id` 의 slug 세그먼트 사용 |
|
|
49
|
+
|
|
50
|
+
### 3.2 유지 — 검증이 "제거 불가"로 판정
|
|
51
|
+
|
|
52
|
+
`taskGroup`, `taskId` (raw 케이스), `runDirRel`, `finalReportRel`, `finalStatusRel` (실저장값),
|
|
53
|
+
`projectRoot` (meta.json 에 SSOT 가 있으나 **과거 run 당시 경로** 보존 + meta 조회 비용 회피 위해 유지),
|
|
54
|
+
가변 필드 `status`/`startedAt`/`finishedAt`/`workers`/`leadModel`/`validation`.
|
|
55
|
+
|
|
56
|
+
결과: **18필드 → 13필드.** 가장 큰 절감은 긴 경로인 `invocationFile`.
|
|
57
|
+
|
|
58
|
+
남는 slim row 필드:
|
|
59
|
+
`runId`, `projectRoot`, `taskGroup`, `taskId`, `status`, `startedAt`, `finishedAt`, `workers`, `leadModel`, `validation`, `finalReportRel`, `finalStatusRel`, `runDirRel`.
|
|
60
|
+
|
|
61
|
+
## 4. 구조
|
|
62
|
+
|
|
63
|
+
### 4.1 단일 참조점 두 개
|
|
64
|
+
|
|
65
|
+
복원/생성 로직을 두 개의 헬퍼에 집중한다. 신규 모듈 `scripts/okstra_ctl/run_index_row.py`.
|
|
66
|
+
|
|
67
|
+
- **`hydrate_run_row(row) -> dict`** — slim row(또는 기존 full row)를 받아 제거된 5필드를 채운 full dict 반환. full row 를 받으면 기존 값을 덮어쓰지 않고 동일 결과를 보장(멱등). reader 전용.
|
|
68
|
+
- **`build_run_index_row(...) -> dict`** — slim row 를 생성하는 유일한 진입점. 현재 `index.py` 의 row dict 리터럴 두 곳([`record_start`](../../../scripts/okstra_ctl/index.py:74), [`reserve_run_in_active`](../../../scripts/okstra_ctl/index.py:162))과 reconcile/backfill 의 row 생성이 이 헬퍼를 호출하도록 치환. writer 전용.
|
|
69
|
+
|
|
70
|
+
### 4.2 read 지점 hydrate (reader 로직 무변경)
|
|
71
|
+
|
|
72
|
+
디스크는 slim, 메모리는 full 을 유지한다. run-index 전용 read 함수 `read_run_index(path) -> list[dict]` 를 도입해 각 row 를 `hydrate_run_row` 로 통과시킨다.
|
|
73
|
+
|
|
74
|
+
run-index row 소비자 6곳의 **read 진입점만** 교체하고, 그 아래 소비 로직은 그대로 둔다.
|
|
75
|
+
|
|
76
|
+
- [`scripts/okstra_ctl/sequence.py`](../../../scripts/okstra_ctl/sequence.py) — `projectId`/`taskType`/`runSeq` 로 next seq 계산
|
|
77
|
+
- [`scripts/okstra_ctl/listing.py`](../../../scripts/okstra_ctl/listing.py) — `projectId`/`taskType`/`invocationFile`/`projectRoot`
|
|
78
|
+
- [`scripts/okstra_ctl/reconcile.py`](../../../scripts/okstra_ctl/reconcile.py) — `projectId`/`taskType`/`runSeq`/`runDirRel`
|
|
79
|
+
- [`scripts/okstra_ctl/pane_reclaim.py`](../../../scripts/okstra_ctl/pane_reclaim.py) — `projectRoot`
|
|
80
|
+
- [`scripts/okstra_ctl/wizard.py`](../../../scripts/okstra_ctl/wizard.py) — `taskKey`/`taskType`
|
|
81
|
+
- [`scripts/okstra_ctl/recap.py`](../../../scripts/okstra_ctl/recap.py) — `taskType`
|
|
82
|
+
|
|
83
|
+
`reconcile.py` 는 row 를 읽어 다시 쓰는데(in-place 갱신), 쓸 때는 `build_run_index_row` 로 slim 화한다 → 자연스러운 lazy 재작성 경로가 된다.
|
|
84
|
+
|
|
85
|
+
> manifest / state / convergence 를 읽는 코드(`context_cost.py`, `render.py`, `handoff.py`, `codex_dispatch.py`, `state.py`, `error_report.py`, `task-show.mjs` 등)는 run-index row 소비자가 **아니므로** 변경 대상이 아니다.
|
|
86
|
+
|
|
87
|
+
## 5. 마이그레이션 (일괄)
|
|
88
|
+
|
|
89
|
+
`migrate.py` 는 `.project-docs/okstra/ → .okstra/` **경로 이동** 전용 one-shot 이고 project_root 레벨이라, row 스키마 변환과 책임이 다르다. **혼합하지 않는다.**
|
|
90
|
+
|
|
91
|
+
별도 일괄 변환을 둔다 — `run_index_row.py` 의 `reindex_slim(home) -> int` 함수. 동작:
|
|
92
|
+
|
|
93
|
+
1. `recent.jsonl`, `active.jsonl`, 모든 `projects/<id>/index.jsonl` 을 읽는다.
|
|
94
|
+
2. 각 row 를 `slim_run_row` 로 정규화해 slim 으로 재작성한다(원자적 tmp + os.replace).
|
|
95
|
+
3. 이미 slim 인 row 는 no-op (멱등). `runId` 없는 비-run-index row(옛 event-log)는 무변형 통과.
|
|
96
|
+
4. `archive/` 는 현재 비어 있어 대상 외. 미래 archive 는 writer 가 이미 slim 을 기록하므로 자동 정합.
|
|
97
|
+
|
|
98
|
+
이 함수는 기존 데이터 1회 정리용 **일회성 마이그레이션**이라 CLI 서브커맨드로 노출하지 않는다(YAGNI). writer 가 항상 slim 을 기록하므로 새 환경은 처음부터 slim 이고, `hydrate_run_row` 가 full row 도 수용(안전망)해 미마이그레이션 환경도 reader 가 정상 동작한다. pre-v1.0 이라 하위호환 부담도 없다.
|
|
99
|
+
|
|
100
|
+
## 6. 테스트 (`tests/run/`)
|
|
101
|
+
|
|
102
|
+
- `parse_run_id` ↔ `build_run_id` 라운드트립 (slug 세그먼트 기준).
|
|
103
|
+
- `hydrate_run_row(slim)` == 기존 full row — 실데이터 기반 회귀 스냅샷(`projectId`/`taskType`/`runSeq`/`taskKey`/`invocationFile` 5필드가 원본과 일치).
|
|
104
|
+
- `hydrate_run_row(full)` 멱등 — full row 를 넣어도 동일 결과.
|
|
105
|
+
- `reindex_slim` 멱등 — 두 번 실행해도 동일 출력.
|
|
106
|
+
- raw 케이스 보존 — `taskGroup="DEV-9388"` 인 row 가 마이그레이션 후에도 raw 유지되는지.
|
|
107
|
+
|
|
108
|
+
## 7. 비범위 (YAGNI)
|
|
109
|
+
|
|
110
|
+
- 파일 간 row 복제(① recent/index/active 통합) — 조회 경로 전면 개편이 필요하고 이번 동기와 무관.
|
|
111
|
+
- `projectRoot`/`runDirRel`/`finalReportRel`/`finalStatusRel` 제거 — 검증상 파생 불가 또는 실익 모호.
|
|
112
|
+
- `runId` 포맷 변경 — fs-safe 슬러그 불변식(충돌 방지)을 유지해야 하므로 불가.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# stage 자동 통합(머지) + worktree teardown — 설계
|
|
2
|
+
|
|
3
|
+
- 작성일: 2026-06-20
|
|
4
|
+
- 범위: `implementation` 으로 쌓인 stage 들을 task 브랜치(부모)에 **okstra 가 자동 머지**하고, 역할이 끝난 stage worktree·registry 키·stage 브랜치를 **자동 정리(teardown)** 한다. whole-task final-verification 진입 시 자동 수행하고, 동일 코어를 단독 명령으로도 노출한다.
|
|
5
|
+
- 비범위
|
|
6
|
+
- **stage-group handoff 경로 변경 없음** — release-handoff 의 stage-group(`assemble`) 모드는 그대로 둔다. 본 설계는 whole-task 통합 경로 전용이며, 두 워크플로우는 상호배타다(§2.4).
|
|
7
|
+
- **충돌 자동 해소 없음** — stage 간 머지 충돌은 `git merge --abort` 후 중단하고 사용자 수동 해소로 넘긴다(octopus/전략 머지 없음).
|
|
8
|
+
- **dirty worktree 강제 삭제 없음** — 미커밋 변경이 남은 stage worktree 는 teardown 하지 않고 경고만 남긴다.
|
|
9
|
+
- `implementation` 외 phase 의 worktree 모델 불변 — 기존 task-key worktree 1개 유지.
|
|
10
|
+
- 관계: [`2026-06-06-stage-worktree-isolation-design.md`](2026-06-06-stage-worktree-isolation-design.md) 의 stage worktree 격리 모델 위에 선다. 그 spec 의 **§2 "okstra 자동 머지 없음" 원칙을 본 설계가 의도적으로 전환**한다(§2.1). 그 spec 이 후속 과제로 남긴 **§9-#2 "stage worktree 자동 teardown"** 를 본 설계가 해소한다. [`2026-06-10-stage-group-handoff-design.md`](2026-06-10-stage-group-handoff-design.md) 의 stage-group 수집 머지와는 진입 경로가 분리되어 충돌하지 않는다(§2.4).
|
|
11
|
+
|
|
12
|
+
## 1. 동기
|
|
13
|
+
|
|
14
|
+
whole-task final-verification 게이트([`stage_targets.resolve_whole_task_target`](../../../scripts/okstra_ctl/stage_targets.py:196))는 두 가지를 강제한다.
|
|
15
|
+
|
|
16
|
+
1. **merged 게이트** — 모든 stage 의 done commit 이 task worktree HEAD 의 ancestor여야 함. 아니면 "merge stage branches then retry" 로 중단.
|
|
17
|
+
2. **dirty 게이트** — task worktree 가 깨끗해야 함([`run._is_dirty_excluding_okstra`](../../../scripts/okstra_ctl/run.py:1455)).
|
|
18
|
+
|
|
19
|
+
stage worktree 는 부모 task worktree 하위 디렉터리(`.../<task-id>/stage-<N>/`)에 위치하므로, 부모 입장에서 `git status --short` 가 `?? stage-N/` 로 잡아 **자기 산출물이 자기 dirty 게이트를 막는** 모순이 생긴다([clean gate 제외 목록](../../../scripts/okstra_ctl/worktree.py:411) 에 stage 디렉터리가 없음). 또한 stage 머지는 현재 전적으로 사용자 수동이라, 사용자가 머지를 빠뜨리면 merged 게이트에서도 막힌다.
|
|
20
|
+
|
|
21
|
+
근본 해결은 "stage done commit 을 task 브랜치에 머지하고, 역할이 끝난 stage worktree 를 정리"하는 절차를 okstra 가 수행하는 것이다. 이 절차가 끝나면 두 게이트가 모두 자연 통과한다.
|
|
22
|
+
|
|
23
|
+
## 2. 핵심 원칙
|
|
24
|
+
|
|
25
|
+
### 2.1 자동 머지 원칙 전환
|
|
26
|
+
|
|
27
|
+
[`2026-06-06 §2`](2026-06-06-stage-worktree-isolation-design.md) 는 "okstra 자동 머지 없음 — 머지 충돌 해소 책임을 okstra 에 넣지 않는다"를 못박았다. 본 설계는 이를 **whole-task 통합 경로에 한해 전환**한다: okstra 가 stage done commit 을 task 브랜치에 머지한다. 단 충돌 해소 책임은 여전히 사용자에게 남긴다 — 충돌 시 `merge --abort` 후 중단(§4)하므로, okstra 가 충돌을 해소하지 않는다는 그 spec 의 더 깊은 원칙(octopus 보류)은 유지된다. 즉 "자동 머지"는 **충돌 없는 머지의 자동화**이고, 충돌은 종전대로 사용자 몫이다.
|
|
28
|
+
|
|
29
|
+
### 2.2 2-페이즈 분리 — 머지(브랜치 기준)와 teardown(worktree 기준)
|
|
30
|
+
|
|
31
|
+
머지는 stage 브랜치 ref 와 done commit 만 있으면 되고 worktree 가 필요 없다. teardown 은 worktree 디스크 상태에 좌우된다. 둘을 분리하면 "dirty 라 teardown 은 skip 하되 머지는 이미 됨" 같은 상태를 깔끔히 표현할 수 있다.
|
|
32
|
+
|
|
33
|
+
- **Phase A (머지)**: 브랜치/커밋 기준. worktree 존재 불필요.
|
|
34
|
+
- **Phase B (teardown)**: worktree 디스크 기준. Phase A 가 머지/skip 한 stage 만 대상.
|
|
35
|
+
|
|
36
|
+
### 2.3 코어 1개 + 진입 2개 (single-reference-point)
|
|
37
|
+
|
|
38
|
+
머지+teardown 로직은 신규 모듈 [`scripts/okstra_ctl/stage_integrate.py`](../../../scripts/okstra_ctl/stage_integrate.py) 의 `integrate_stages` 하나가 SSOT다. 두 진입점이 이 코어로 수렴한다.
|
|
39
|
+
|
|
40
|
+
- **자동**: whole-task final-verification prepare 경로([`run.py:1509`](../../../scripts/okstra_ctl/run.py:1509) 의 `else` 분기)에서 `resolve_whole_task_target` 호출 **직전**에 `integrate_stages` 를 호출한다. 기존 `auto_reconcile_best_effort`([run.py:1485](../../../scripts/okstra_ctl/run.py:1485)) 와 같은 prepare 단계에 위치한다.
|
|
41
|
+
- **단독 명령**: `okstra ... integrate-stages` — 사용자가 final-verification 전에 명시 호출. CLI(`okstra.sh`/`cli.sh`)와 Node CLI 가 같은 코어로 패스스루.
|
|
42
|
+
|
|
43
|
+
### 2.4 whole-task 통합과 stage-group handoff 의 상호배타성
|
|
44
|
+
|
|
45
|
+
release-handoff 의 stage-group 모드([`handoff._merge_stages`](../../../scripts/okstra_ctl/handoff.py:173))는 `compute_branch_name` 으로 stage 브랜치를 재구성해 머지하므로 **stage 브랜치 ref 에 의존**한다. 본 설계가 stage 브랜치를 삭제(§3 Phase B)해도 이것이 문제되지 않는 이유:
|
|
46
|
+
|
|
47
|
+
- whole-task 통합을 수행하는 순간 **모든 stage 가 task 브랜치 하나로 합쳐진다.** 이 시점에 "stage 부분집합만 골라 collector 브랜치로 릴리스"하는 stage-group 은 의미를 잃는다.
|
|
48
|
+
- stage-group 을 원하는 사용자는 single-stage final-verification(`--stage N`) → stage-group handoff 경로를 타며, whole-task 통합을 호출하지 않는다.
|
|
49
|
+
- 따라서 whole-task 통합 경로의 stage 브랜치 삭제는 stage-group 워크플로우를 막지 않는다 — 두 경로는 애초에 교차하지 않는다.
|
|
50
|
+
|
|
51
|
+
### 2.5 이력 보존
|
|
52
|
+
|
|
53
|
+
Phase A 머지는 `--no-ff` 로 stage 식별 가능한 머지 커밋을 만든다. stage 원본 커밋들은 머지 커밋을 통해 부모 브랜치에서 reachable하므로, stage 브랜치 ref(포인터)를 삭제해도 커밋은 GC 되지 않고 히스토리로 추적된다. 재작업 시 자식 worktree/브랜치를 다시 발급하면 된다.
|
|
54
|
+
|
|
55
|
+
## 3. 설계
|
|
56
|
+
|
|
57
|
+
### Phase A — 위상순 머지
|
|
58
|
+
|
|
59
|
+
입력: task worktree path, task 브랜치, stage map, `consumers.jsonl` done rows.
|
|
60
|
+
|
|
61
|
+
1. stage map 을 `depends_on` 위상정렬(= `stage_number` 오름차순, plan 이 보장) 순으로 순회.
|
|
62
|
+
2. 각 stage 의 done commit(`head_commit`)이 **이미 task HEAD 의 ancestor**면(`git merge-base --is-ancestor`) **머지 skip** (no-op, `already-merged` 로 기록). 기존 `auto_reconcile` 가 patch-id 로 재배치한 경우도 ancestor 로 잡힌다.
|
|
63
|
+
3. 아니면 `git -C <task-worktree> merge --no-ff --no-edit <stage-branch>` 수행.
|
|
64
|
+
4. **충돌 시**: 해당 stage 의 `git merge --abort` 로 **그 머지만** 원상복구 → `IntegrateError`(= `PrepareError` 로 surface)로 충돌 파일 경로를 보여주고 중단. 위상순 앞서 머지 성공한 선행 stage 는 task 브랜치에 그대로 남으며(재시도 시 §3-#2 의 `already_merged` 로 skip 되어 진전이 보존된다), **Phase B(teardown)는 진입하지 않는다** — 충돌이 하나라도 있으면 어떤 worktree·브랜치도 지우지 않는다(teardown 원자성). 머지 자체는 부분 진행될 수 있으나 재진입이 idempotent 하므로 안전하다. (release-handoff `assemble` 의 exit 2 / `_merge_stages` abort 패턴과 동일)
|
|
65
|
+
|
|
66
|
+
산출: `merged: list[int]`, `already_merged: list[int]`.
|
|
67
|
+
|
|
68
|
+
### Phase B — teardown
|
|
69
|
+
|
|
70
|
+
대상: Phase A 가 `merged` 또는 `already_merged` 로 처리한 stage. (충돌로 중단되면 이 페이즈는 실행되지 않음.)
|
|
71
|
+
|
|
72
|
+
각 stage 에 대해:
|
|
73
|
+
|
|
74
|
+
1. stage worktree 가 **dirty**(`_is_dirty_excluding_okstra` 재사용)면 **skip + 경고** — worktree·브랜치 모두 보류. (done 이후 미커밋 변경 손실 방지)
|
|
75
|
+
2. clean 이면 순서대로:
|
|
76
|
+
- `git worktree remove <stage-worktree-path>`
|
|
77
|
+
- registry 의 `<task-key>#stage-<N>` 키 status 해제([`worktree_registry.release_status`](../../../scripts/okstra_ctl/worktree_registry.py) — branch 슬롯은 **건드리지 않음**, flock 보호)
|
|
78
|
+
- `git branch -d <stage-branch>` — **safe delete**. 부모에 머지 안 된 브랜치는 git 이 거부하므로, Phase A 머지 성공이 삭제의 전제로 자동 강제된다(force `-D` 미사용).
|
|
79
|
+
- branch -d 가 **성공한 뒤에만** branch 인덱스 슬롯 해제([`worktree_registry.free_branch_slot`](../../../scripts/okstra_ctl/worktree_registry.py)). 불변식: "registry 가 free 로 표시한 브랜치 이름은 항상 물리적으로 삭제돼 있다" — 동시 run 이 살아있는 브랜치 이름을 재예약하는 윈도우를 막는다. branch -d 실패 시 슬롯을 그대로 둔 채 경고만 남긴다.
|
|
80
|
+
3. per-stage best-effort: 한 stage 의 teardown 실패가 다른 stage 를 막지 않는다(경고 수집 후 계속).
|
|
81
|
+
|
|
82
|
+
산출: `torn_down: list[int]`, `teardown_skipped: list[(int, reason)]`, `warnings: list[str]`.
|
|
83
|
+
|
|
84
|
+
### 반환 — `IntegrateResult`
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
merged : list[int] # 이번에 머지한 stage
|
|
88
|
+
already_merged : list[int] # 이미 ancestor 라 skip
|
|
89
|
+
torn_down : list[int] # worktree+registry+branch 제거됨
|
|
90
|
+
teardown_skipped : list[(int, str)] # dirty 등으로 보류
|
|
91
|
+
warnings : list[str]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
final-verification 리포트의 통합 섹션과 단독 명령 출력에 그대로 노출한다.
|
|
95
|
+
|
|
96
|
+
### non-git / degraded 경로
|
|
97
|
+
|
|
98
|
+
[`2026-06-06 §2.1`](2026-06-06-stage-worktree-isolation-design.md) 의 degrade 신호(`skipped-not-git` / `skipped-in-worktree`)와 동일하게, stage worktree 가 발급된 적 없는 환경에서는 `integrate_stages` 가 no-op(빈 `IntegrateResult`)으로 graceful degrade 한다.
|
|
99
|
+
|
|
100
|
+
## 4. 에러 처리 요약
|
|
101
|
+
|
|
102
|
+
| 상황 | 처리 |
|
|
103
|
+
|---|---|
|
|
104
|
+
| stage 간 머지 충돌 | 해당 stage `merge --abort` → `IntegrateError`/`PrepareError` 충돌 경로 동봉, 중단. 선행 머지는 유지(재시도 시 skip), teardown 은 미진입(teardown 원자성) |
|
|
105
|
+
| 선행 stage 미완(done row 없음) | merged 게이트가 별도로 잡음 — 통합 전에 stage 미완으로 중단 |
|
|
106
|
+
| stage worktree dirty | 해당 stage teardown skip + 경고, 머지는 유지 |
|
|
107
|
+
| `git branch -d` 거부(미머지) | 발생 불가(Phase A 머지 성공이 전제). 발생 시 경고로 surface, worktree 는 이미 제거됨. branch 슬롯은 해제하지 않아 살아있는 브랜치 이름이 free 로 표시되지 않음(불변식 유지) |
|
|
108
|
+
| worktree remove 실패 | per-stage 경고, 다음 stage 계속 |
|
|
109
|
+
|
|
110
|
+
## 5. 변경 대상 파일 (seed only)
|
|
111
|
+
|
|
112
|
+
`feedback_okstra_fixes_target_end_users`: `runtime/`·개인 `.claude/` 가 아니라 아래 source 파일에만 가한다.
|
|
113
|
+
|
|
114
|
+
| 파일 | 변경 |
|
|
115
|
+
|---|---|
|
|
116
|
+
| [`scripts/okstra_ctl/stage_integrate.py`](../../../scripts/okstra_ctl/stage_integrate.py) (신규) | `integrate_stages` 코어 + `IntegrateResult` / `IntegrateError` |
|
|
117
|
+
| [`scripts/okstra_ctl/run.py`](../../../scripts/okstra_ctl/run.py:1509) | whole-task final-verification prepare 에서 `integrate_stages` 자동 호출; `integrate-stages` 단독 명령 dispatch |
|
|
118
|
+
| [`scripts/okstra_ctl/worktree.py`](../../../scripts/okstra_ctl/worktree.py) | registry `#stage-N` 키 해제 헬퍼(flock 보호); worktree dirty 검사 재사용 |
|
|
119
|
+
| [`scripts/okstra.sh`](../../../scripts/okstra.sh) · [`scripts/lib/okstra/cli.sh`](../../../scripts/lib/okstra/cli.sh) · Node CLI | `integrate-stages` 진입점 패스스루 |
|
|
120
|
+
| [`prompts/profiles/final-verification.md`](../../../prompts/profiles/final-verification.md) | whole-task 가 통합 머지+teardown 을 수행하는 **mutating** phase 임을 명시 |
|
|
121
|
+
| [`docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md`](2026-06-06-stage-worktree-isolation-design.md) | §2 자동 머지 원칙·§9-#2 teardown 후속 과제가 본 spec 으로 전환·해소됨을 상호 참조 |
|
|
122
|
+
| [`CHANGES.md`](../../../CHANGES.md) | `사용자 영향:` 항목(한국어) |
|
|
123
|
+
|
|
124
|
+
## 6. declaration ↔ enforcement (okstra `CLAUDE.md` rule 5)
|
|
125
|
+
|
|
126
|
+
- "충돌 시 전체 중단" 의 강제 지점은 Phase A 의 `merge --abort` + `IntegrateError` 예외다(프로파일 문구가 아니라 런타임).
|
|
127
|
+
- "머지된 stage 만 브랜치 삭제" 의 강제 지점은 `git branch -d`(safe delete)의 git 자체 거부다.
|
|
128
|
+
- "dirty teardown 금지" 의 강제 지점은 Phase B 의 `_is_dirty_excluding_okstra` 검사다.
|
|
129
|
+
- "free 로 표시한 브랜치 이름은 항상 물리 삭제돼 있다" 의 강제 지점은 `_teardown_stage` 의 호출 순서다 — `release_status`(status 만) → `git branch -d` → 성공 시에만 `free_branch_slot`. 실패하면 슬롯을 그대로 둔다. 회귀 테스트는 `tests/run/test_stage_integrate.py::test_teardown_warns_when_branch_delete_fails`.
|
|
130
|
+
|
|
131
|
+
## 7. 검증 시나리오 (테스트)
|
|
132
|
+
|
|
133
|
+
| # | 시나리오 | 기대 |
|
|
134
|
+
|---|----------|------|
|
|
135
|
+
| I1 | 미머지 stage 1·2·3, 충돌 없음 | 위상순 `--no-ff` 머지, 모두 `merged`, worktree+registry+branch 제거, dirty 게이트 통과 |
|
|
136
|
+
| I2 | 일부 stage 이미 ancestor | 해당 stage `already_merged`(머지 skip), teardown 은 수행 |
|
|
137
|
+
| I3 | stage 1 머지 성공 후 stage 2 충돌 | stage 2 `merge --abort`, stage 1 머지는 task 브랜치에 유지, `IntegrateError`, teardown 전혀 미실행. 재시도 시 stage 1 `already_merged` |
|
|
138
|
+
| I4 | stage 3 worktree dirty | stage 1·2 teardown, stage 3 는 머지 유지 + teardown skip + 경고 |
|
|
139
|
+
| I5 | `git branch -d` 대상이 부모에 머지됨 | 정상 삭제(force 아님) |
|
|
140
|
+
| I6 | non-git / stage worktree 미발급 | no-op `IntegrateResult`, 오류 없음 |
|
|
141
|
+
| I7 | 단독 명령 `integrate-stages` 와 final-verification 자동 경로 | 동일 `integrate_stages` 코어 호출(결과 동등) |
|
|
142
|
+
| I8 | 통합 후 whole-task final-verification 재진입 | merged·dirty 게이트 모두 통과, 통합 섹션이 결과 보고 |
|
|
143
|
+
| I9 | `git branch -d` 실패(인위적 미머지 브랜치) | 경고 surface, `torn_down` 제외, branch 슬롯 free 안 함, 브랜치 보존 |
|
|
144
|
+
| I10 | `git worktree remove` 실패(dirty 게이트 통과했으나 untracked 잔존) | 경고 surface, `torn_down` 제외, worktree 보존, 이후 단계 미진행 |
|
|
145
|
+
| I11 | registry 에 stage-key 없음(`get_stage_row` None) | 경고 surface, 해당 stage 머지 skip(`merged`/`already_merged` 모두 제외) |
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# release-handoff `local checkout` 액션 설계
|
|
2
|
+
|
|
3
|
+
- 작성일: 2026-06-21
|
|
4
|
+
- 상태: 설계 확정 대기(사용자 리뷰)
|
|
5
|
+
- 관련 코드: [release-handoff.md](../../../prompts/profiles/release-handoff.md), [handoff.py](../../../scripts/okstra_ctl/handoff.py), [worktree.py](../../../scripts/okstra_ctl/worktree.py), [worktree_registry.py](../../../scripts/okstra_ctl/worktree_registry.py)
|
|
6
|
+
|
|
7
|
+
## 1. 배경 / 문제
|
|
8
|
+
|
|
9
|
+
okstra 의 task 작업은 사용자 repo 의 **격리된 git worktree**(`~/.okstra/worktrees/<project-id>/<task-group>/<task-id>/`, [worktree.py:764](../../../scripts/okstra_ctl/worktree.py:764) 의 `git worktree add -b`)에서 진행되고, 결과는 task 브랜치에 커밋된다. final-verification(whole-task)이 모든 stage 를 이 task 브랜치에 머지한 뒤에도, 사용자가 그 결과를 **자기 메인 작업 디렉터리에서 로컬 테스트** 하려면 불편이 있다:
|
|
10
|
+
|
|
11
|
+
- task 브랜치는 okstra 워크트리가 점유 중이라, 메인 디렉터리에서 `git checkout <task-branch>` 가 git 규칙(한 브랜치는 한 워크트리만 체크아웃)으로 막힌다.
|
|
12
|
+
- 결국 사용자가 `~/.okstra/worktrees/...` 폴더로 직접 `cd` 해서 테스트해야 하는데, 이게 번거롭다.
|
|
13
|
+
|
|
14
|
+
release-handoff 에는 현재 `local only` / `push + PR` / `skip` 세 액션이 있지만, "검증된 브랜치를 메인 작업 디렉터리로 가져와 로컬 테스트 가능 상태로 만드는" 경로가 없다.
|
|
15
|
+
|
|
16
|
+
## 2. 목표 / 비목표
|
|
17
|
+
|
|
18
|
+
**목표**
|
|
19
|
+
- release-handoff 에 `local checkout` 액션을 추가해, 확인 후 okstra task-key 워크트리를 제거하고 task 브랜치를 **메인 워크트리에서 checkout** 해 로컬 테스트가 가능하게 한다.
|
|
20
|
+
|
|
21
|
+
**비목표 (설계상 명시적 제외)**
|
|
22
|
+
- base 브랜치(main/master/preprod 등)로의 **자동 머지는 하지 않는다.** main 통합은 PR 리뷰를 거쳐야 하므로 종전대로 `push + PR` 만 담당한다. `local checkout` 은 base 를 건드리지 않는다.
|
|
23
|
+
- stage-group 모드는 v1 범위에서 제외(§5).
|
|
24
|
+
|
|
25
|
+
## 3. 결정사항 요약 (사용자 합의)
|
|
26
|
+
|
|
27
|
+
| 항목 | 결정 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| 트리거 지점 | release-handoff 의 Action selection 에 신규 액션 추가 |
|
|
30
|
+
| 실행 범위 | okstra 가 확인받고 worktree remove + 메인 `git checkout` 까지 **대신 실행** |
|
|
31
|
+
| 안전장치 | 메인 워크트리 dirty 면 중단·경고 |
|
|
32
|
+
| 액션 구성 | `local checkout`(신규, 첫 번째) / `push + PR` / `skip`. 기존 `local only` 제거(skip 과 중복) |
|
|
33
|
+
| 모드 범위 | **whole-task 모드 전용** |
|
|
34
|
+
| 메커니즘 | [handoff.py](../../../scripts/okstra_ctl/handoff.py) 에 `okstra handoff local-checkout` 서브커맨드 추가 |
|
|
35
|
+
|
|
36
|
+
## 4. 액션 선택 재편 (release-handoff.md)
|
|
37
|
+
|
|
38
|
+
기존 "User interaction protocol → Action selection" 의 3지선다를 다음으로 교체:
|
|
39
|
+
|
|
40
|
+
1. **`local checkout`** — 검증된 task 브랜치를 메인 워크트리로 가져와 로컬 테스트 가능 상태로 만든다(push/PR 없음).
|
|
41
|
+
2. **`push + PR`** — feature 브랜치 push + PR 생성(기존 흐름 그대로).
|
|
42
|
+
3. **`skip`** — 어떤 git 명령도 없이 release-handoff 를 의도적으로 건너뛴 것으로 기록하고 final-report self-review 로 라우팅(기존 그대로).
|
|
43
|
+
|
|
44
|
+
`local only` 는 제거한다 — "git 안 건드리고 기록만 하고 끝"이라는 점에서 `skip` 과 사실상 중복이기 때문. skip 의 하위-단계 cancel 경로와 verdict 게이트는 유지되므로 잘못 진입 시 탈출 경로는 남는다.
|
|
45
|
+
|
|
46
|
+
## 5. 모드 범위 (whole-task 전용)
|
|
47
|
+
|
|
48
|
+
- `HANDOFF_MODE == whole-task` 일 때만 `local checkout` 을 제공한다. whole-task 의 deliverable 은 task 브랜치 그 자체라 바로 checkout 가능.
|
|
49
|
+
- `HANDOFF_MODE == stage-group` 에서 사용자가 `local checkout` 을 고르면, *"`local checkout` 은 whole-task 모드에서만 지원됩니다"* 안내 후 액션 선택을 다시 받는다(stage-group 은 collector 브랜치를 `assemble` 로 별도 생성해야 해 복잡도가 큼 — YAGNI).
|
|
50
|
+
|
|
51
|
+
## 6. 메커니즘 — `okstra handoff local-checkout`
|
|
52
|
+
|
|
53
|
+
[handoff.py](../../../scripts/okstra_ctl/handoff.py) 에 서브커맨드를 추가한다. release-handoff 프로필은 확인(AskUserQuestion)을 받은 뒤 이 명령 **하나만** 호출한다.
|
|
54
|
+
|
|
55
|
+
### 6.1 인자
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
okstra handoff local-checkout \
|
|
59
|
+
--project-root <dir> --project-id <id> --task-group <g> --task-id <t>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
브랜치 이름과 okstra 워크트리 경로는 인자로 받지 않고 **registry 의 task-key 행(SSOT)** 에서 해소한다(`stage_number` 없음 = task-key). 잘못된 브랜치 전달 위험을 없앤다.
|
|
63
|
+
|
|
64
|
+
### 6.2 단계 (모든 git 은 메인 워크트리 cwd 에서 실행)
|
|
65
|
+
|
|
66
|
+
self-removal 문제(release-handoff 는 okstra task-key 워크트리 안에서 실행됨, §8)를 피하려고, 헬퍼는 자기 cwd 를 **메인 워크트리**로 두고 모든 git 을 거기서 실행한다.
|
|
67
|
+
|
|
68
|
+
1. **registry 해소**: task-key 행에서 `okstra_wt`(worktree_path)·`branch` 를 읽는다. 행이 없거나 비면 exit 1 + 사유.
|
|
69
|
+
2. **메인 워크트리 해소**: [worktree.main_worktree_path(project_root)](../../../scripts/okstra_ctl/worktree.py:306).
|
|
70
|
+
3. **메인 dirty 체크**: [is_dirty_excluding_okstra(main_wt)](../../../scripts/okstra_ctl/worktree.py:465) 가 dirty 면 exit 1 + 경고(사용자 변경 보호). `.okstra` 잡음은 제외.
|
|
71
|
+
4. **okstra 워크트리 제거**: `git -C <main_wt> worktree remove --force <okstra_wt>`. **브랜치는 보존**(teardown 의 `branch -d` 와 다름 — checkout 대상이므로 절대 삭제 안 함). `--force` 는 추적되지 않은 okstra 산출물(심링크/스냅샷, 재생성 가능)만 정리한다. 실패면 exit 1 + stderr.
|
|
72
|
+
5. **registry status 해제**: [worktree_registry.release_status(...)](../../../scripts/okstra_ctl/worktree_registry.py) (task-key). **branch 슬롯은 free 하지 않는다** — 브랜치가 메인에 살아있으므로, "registry 가 free 로 표시한 브랜치는 항상 물리 삭제돼 있다"는 불변식을 지킨다([free_branch_slot](../../../scripts/okstra_ctl/worktree_registry.py) 호출하지 않음).
|
|
73
|
+
6. **메인에서 checkout**: `git -C <main_wt> checkout <branch>`. 실패면(예: 동일 이름 충돌) exit 1 + 사유. 이 시점이면 워크트리는 이미 제거됐고 브랜치는 어디에도 체크아웃돼 있지 않으므로 사용자가 수동 `git checkout` 으로 복구 가능 — 사유에 그 안내를 포함.
|
|
74
|
+
7. **결과 JSON 출력**: `{ "branch": ..., "mainWorktreePath": ..., "removedWorktree": ..., "status": "checked-out" }`.
|
|
75
|
+
|
|
76
|
+
함수는 50줄 이하로 유지(필요 시 dirty/remove/checkout 단계 헬퍼 분리).
|
|
77
|
+
|
|
78
|
+
### 6.3 exit code
|
|
79
|
+
|
|
80
|
+
- `0` 성공.
|
|
81
|
+
- `1` 전제 위반/실패(메인 dirty, registry 행 없음, worktree remove 실패, checkout 실패). 사유는 stderr.
|
|
82
|
+
|
|
83
|
+
## 7. 프로필의 `local checkout` 핸들러 흐름 (release-handoff.md)
|
|
84
|
+
|
|
85
|
+
1. whole-task 게이트(§5). stage-group 이면 안내 후 액션 재선택.
|
|
86
|
+
2. **확인 프롬프트**: 무엇을 할지 명시("okstra 워크트리 제거 + 메인에서 `<branch>` checkout, base 브랜치는 안 건드림")하고 진행/취소.
|
|
87
|
+
3. `okstra handoff local-checkout ...` 호출. exit 1 이면 사유를 보여주고 액션 선택으로 돌아감.
|
|
88
|
+
4. 성공 시, **lead 의 cwd(구 okstra 워크트리)는 사라진다** → 이후 모든 단계(final-report 작성 등)는 **메인 워크트리 절대경로 기준**으로 수행하도록 프로필에 명시.
|
|
89
|
+
5. final-report self-review 로 라우팅.
|
|
90
|
+
|
|
91
|
+
## 8. 안전장치 / 엣지 케이스
|
|
92
|
+
|
|
93
|
+
- **self-removal**: release-handoff 는 `provision_task_worktree` 로 task-key 워크트리 안에서 실행([run.py:1920](../../../scripts/okstra_ctl/run.py:1920)). 헬퍼가 메인 워크트리 cwd 에서 git 을 실행하므로 자기 자신을 제거하지 않는다. 호출 후 lead 는 메인으로 cwd 를 옮긴다(§7.4).
|
|
94
|
+
- **메인 dirty**: §6.2-3 에서 중단. 사용자 미커밋 변경을 절대 덮어쓰지 않는다.
|
|
95
|
+
- **브랜치 보존 불변식**: §6.2-5. branch 슬롯을 free 하지 않아 동시 run 이 살아있는 브랜치 이름을 재예약하지 못한다.
|
|
96
|
+
- **checkout 실패**: §6.2-6. 부분 상태(워크트리 제거됨)에 대한 수동 복구 안내 포함.
|
|
97
|
+
- **base 브랜치 보호**: `local checkout` 은 base 를 절대 건드리지 않는다(비목표). 메인이 마침 base 브랜치를 체크아웃 중이면 checkout 으로 task 브랜치로 전환될 뿐 base 브랜치 ref 는 불변.
|
|
98
|
+
|
|
99
|
+
## 9. 영향 파일
|
|
100
|
+
|
|
101
|
+
- [prompts/profiles/release-handoff.md](../../../prompts/profiles/release-handoff.md): 액션 선택 재편 + `local checkout` 핸들러 + final-report 5.6 기록 지시.
|
|
102
|
+
- [scripts/okstra_ctl/handoff.py](../../../scripts/okstra_ctl/handoff.py): `local-checkout` 서브커맨드 + argparse 등록.
|
|
103
|
+
- [tests/handoff/](../../../tests/handoff/): 헬퍼 테스트(워크트리 fixture → 실행 → 메인 checkout·워크트리 제거·브랜치 보존·dirty 중단 검증).
|
|
104
|
+
- [CHANGES.md](../../../CHANGES.md): `사용자 영향:` 항목.
|
|
105
|
+
- `runtime/` 은 빌드 산출물 — 직접 수정 금지. 작업 후 `npm run build`.
|
|
106
|
+
|
|
107
|
+
## 10. 테스트 계획
|
|
108
|
+
|
|
109
|
+
- **성공 경로**: task-key 워크트리가 있는 fixture repo 에서 헬퍼 실행 → 메인이 task 브랜치 체크아웃됨 + okstra 워크트리 디렉터리 제거됨 + 브랜치 ref 보존됨 + registry status `released`·branch 슬롯 유지.
|
|
110
|
+
- **메인 dirty 중단**: 메인에 미커밋 변경 → exit 1, 워크트리·브랜치 불변.
|
|
111
|
+
- **registry 행 없음**: exit 1.
|
|
112
|
+
- **checkout 충돌**: 메인에 동일 이름 브랜치 선점 → exit 1, 사유 출력.
|
|
113
|
+
- CLI 스모크: `node bin/okstra handoff --help` 에 `local-checkout` 노출.
|
|
@@ -97,7 +97,7 @@ lead는 사용자에게 push/PR 여부를 묻기 전에 다음을 확인한다.
|
|
|
97
97
|
stateDiagram-v2
|
|
98
98
|
[*] --> Gate: entry gate
|
|
99
99
|
Gate --> Q1: action selection
|
|
100
|
-
Q1 -->
|
|
100
|
+
Q1 --> LocalCheckout: local checkout
|
|
101
101
|
Q1 --> Skip: skip
|
|
102
102
|
Q1 --> Q2: push + PR
|
|
103
103
|
Q2 --> Probe: choose PR base
|
|
@@ -110,7 +110,7 @@ stateDiagram-v2
|
|
|
110
110
|
Q3 --> Cancel: cancel
|
|
111
111
|
Push --> ReuseOrCreate: git push feature branch
|
|
112
112
|
ReuseOrCreate --> FinalReport: gh pr list / gh pr create
|
|
113
|
-
|
|
113
|
+
LocalCheckout --> FinalReport
|
|
114
114
|
Skip --> FinalReport
|
|
115
115
|
Cancel --> FinalReport
|
|
116
116
|
FinalReport --> [*]
|
|
@@ -118,7 +118,7 @@ stateDiagram-v2
|
|
|
118
118
|
|
|
119
119
|
사용자 interaction은 정확히 세 단계다.
|
|
120
120
|
|
|
121
|
-
1. Q1 action: `local
|
|
121
|
+
1. Q1 action: `local checkout`, `push + PR`, `skip`
|
|
122
122
|
2. Q2 PR base: `staging`, `preprod`, `main`, 직접 입력 등 profile menu의 branch
|
|
123
123
|
3. Q3 PR title/body: `use as-is`, `edit then proceed`, `cancel`
|
|
124
124
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -212,7 +212,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
212
212
|
printf ' hint: did you mean --task-id?\n' >&2
|
|
213
213
|
;;
|
|
214
214
|
esac
|
|
215
|
-
printf ' valid options: --render-only --resume-clarification --yes --workers --lead-model --claude-model --codex-model --antigravity-model --report-writer-model --lead-runtime --related-tasks --task-type --project-id --project-root --task-group --task-id --task-brief --directive --fix-cycle --clarification-response --approved-plan --approve --implementation-option --stage --qa-waiver --no-plan-verification -h|--help\n' >&2
|
|
215
|
+
printf ' valid options: --render-only --resume-clarification --yes --workers --lead-model --claude-model --codex-model --antigravity-model --report-writer-model --lead-runtime --executor --critic --related-tasks --work-category --task-type --project-id --project-root --task-group --task-id --task-brief --directive --base-ref --fix-cycle --clarification-response --task-key --approved-plan --approve --implementation-option --stage --stages --qa-waiver --no-plan-verification -h|--help\n' >&2
|
|
216
216
|
usage
|
|
217
217
|
exit 1
|
|
218
218
|
;;
|
|
@@ -11,6 +11,19 @@ is_interactive_session() {
|
|
|
11
11
|
[[ -t 0 && -t 1 ]]
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
file_mtime() {
|
|
15
|
+
# Epoch mtime; portable across GNU (stat -c) and macOS/BSD (stat -f).
|
|
16
|
+
# Validate that the output is a bare integer before accepting it: GNU
|
|
17
|
+
# `stat -f %m` (a BSD-ism) does NOT fail cleanly — it succeeds with
|
|
18
|
+
# non-numeric filesystem output, so a plain `a || b` would return that
|
|
19
|
+
# garbage and later break `-gt`/`-eq` arithmetic under `set -u`.
|
|
20
|
+
local m=""
|
|
21
|
+
m="$(stat -c %Y "$1" 2>/dev/null)"
|
|
22
|
+
[[ "$m" =~ ^[0-9]+$ ]] || m="$(stat -f %m "$1" 2>/dev/null)"
|
|
23
|
+
[[ "$m" =~ ^[0-9]+$ ]] || m="0"
|
|
24
|
+
printf '%s' "$m"
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
split_task_key() {
|
|
15
28
|
local raw_key=""
|
|
16
29
|
raw_key="$(trim_whitespace "${TASK_KEY_INPUT-}")"
|
|
@@ -298,17 +311,30 @@ find_latest_final_report() {
|
|
|
298
311
|
return 1
|
|
299
312
|
fi
|
|
300
313
|
|
|
314
|
+
# Pick the chronologically newest report (mtime first), matching the Python
|
|
315
|
+
# resume path (okstra_ctl.wizard's mtime-max). A pure basename sort conflates
|
|
316
|
+
# phases: across error-analysis/ and requirements-discovery/ dirs the filename
|
|
317
|
+
# embeds the task-type, so a lexicographic max always favors one phase
|
|
318
|
+
# regardless of which run is actually newer. Ties (same-second mtime within a
|
|
319
|
+
# phase dir) fall back to the greater basename so the highest seq/timestamp
|
|
320
|
+
# wins deterministically.
|
|
301
321
|
local best_path=""
|
|
322
|
+
local best_mtime=-1
|
|
302
323
|
local best_basename=""
|
|
303
324
|
local candidate=""
|
|
325
|
+
local candidate_mtime=""
|
|
304
326
|
local candidate_base=""
|
|
305
327
|
local d=""
|
|
306
328
|
for d in "${search_dirs[@]}"; do
|
|
307
329
|
[[ -d "$d" ]] || continue
|
|
308
330
|
while IFS= read -r candidate; do
|
|
331
|
+
candidate_mtime="$(file_mtime "$candidate")"
|
|
309
332
|
candidate_base="$(basename "$candidate")"
|
|
310
|
-
if [[ -z "$best_path"
|
|
333
|
+
if [[ -z "$best_path" \
|
|
334
|
+
|| "$candidate_mtime" -gt "$best_mtime" \
|
|
335
|
+
|| ( "$candidate_mtime" -eq "$best_mtime" && "$candidate_base" > "$best_basename" ) ]]; then
|
|
311
336
|
best_path="$candidate"
|
|
337
|
+
best_mtime="$candidate_mtime"
|
|
312
338
|
best_basename="$candidate_base"
|
|
313
339
|
fi
|
|
314
340
|
done < <(find "$d" -maxdepth 1 -type f -name 'final-report-*.md' 2>/dev/null)
|
|
@@ -363,9 +389,9 @@ run_resume_clarification() {
|
|
|
363
389
|
# implementation-planning 도 §1 Clarification Items(plan-body 결정: 예 C-205
|
|
364
390
|
# tx-배관 step 누락)를 가지므로, 사용자가 결정란을 채운 뒤 같은 phase 를
|
|
365
391
|
# clarification-response 자동공급으로 재실행해 plan 을 재생성·gate 재판정할 수
|
|
366
|
-
# 있다. 자동검색(task_type_filter 없음)은
|
|
367
|
-
#
|
|
368
|
-
# implementation-planning 을 명시했을 때만 해소된다.
|
|
392
|
+
# 있다. 자동검색(task_type_filter 없음)은 phase 디렉터리 전반에서 mtime 최신
|
|
393
|
+
# 리포트를 고르므로 어느 phase 가 뽑힐지 보장되지 않는다. 따라서 이 경로는
|
|
394
|
+
# --task-type implementation-planning 을 명시했을 때만 결정적으로 해소된다.
|
|
369
395
|
case "$resolved_type" in
|
|
370
396
|
requirements-discovery|error-analysis|implementation-planning) ;;
|
|
371
397
|
*)
|
|
@@ -11,11 +11,13 @@ _okstra_ctl_tail() {
|
|
|
11
11
|
import os, sys
|
|
12
12
|
sys.path.insert(0, os.environ["OKSTRA_CTL_LIB_DIR"])
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from okstra_ctl import
|
|
14
|
+
from okstra_ctl import read_run_index, format_runs_table
|
|
15
15
|
# tail active 는 active.jsonl 자체를 보여줘야 한다 — running 외에도
|
|
16
16
|
# in-progress(backfill), reserving(rerun 예약), 그리고 어떤 비-터미널
|
|
17
17
|
# 상태든 active 에 들어있는 것은 사용자가 모니터링해야 할 대상이다.
|
|
18
|
-
|
|
18
|
+
# 디스크 row 는 slim(파생 taskType 없음)이라 read_run_index 로 hydrate 해야
|
|
19
|
+
# format_runs_table 의 TASK-TYPE 열이 채워진다.
|
|
20
|
+
rows = sorted(read_run_index(Path(os.environ["OKSTRA_HOME_RESOLVED"]) / "active.jsonl"),
|
|
19
21
|
key=lambda r: r.get("startedAt", ""), reverse=True)
|
|
20
22
|
print(format_runs_table(rows), end="")
|
|
21
23
|
PY
|
|
@@ -29,17 +31,24 @@ PY
|
|
|
29
31
|
# fallback 하고, 그래도 없으면 expected status 파일이 아직 만들어지지
|
|
30
32
|
# 않았어도 tail -F 로 wait 한다(`tail -F` 는 by-name 으로 파일 등장을
|
|
31
33
|
# 감지한다).
|
|
32
|
-
local resolved
|
|
34
|
+
local resolved resolve_rc
|
|
33
35
|
resolved="$(OKSTRA_HOME_RESOLVED="$(okstra_central_home)" \
|
|
34
36
|
OKSTRA_CTL_LIB_DIR="$SCRIPT_DIR" \
|
|
35
37
|
OK_QUERY="$1" python3 -c '
|
|
36
38
|
import os, sys
|
|
37
39
|
sys.path.insert(0, os.environ["OKSTRA_CTL_LIB_DIR"])
|
|
38
40
|
from pathlib import Path
|
|
39
|
-
from okstra_ctl import resolve_run_id, find_row_by_run_id
|
|
41
|
+
from okstra_ctl import resolve_run_id, find_row_by_run_id, ResolveError
|
|
40
42
|
home = Path(os.environ["OKSTRA_HOME_RESOLVED"])
|
|
41
|
-
|
|
43
|
+
try:
|
|
44
|
+
rid = resolve_run_id(home, os.environ["OK_QUERY"])
|
|
45
|
+
except ResolveError as e:
|
|
46
|
+
print(f"tail: {e}", file=sys.stderr)
|
|
47
|
+
for c in e.candidates: print(f" - {c}", file=sys.stderr)
|
|
48
|
+
sys.exit(2)
|
|
42
49
|
row = find_row_by_run_id(home, rid)
|
|
50
|
+
if row is None:
|
|
51
|
+
print(f"tail: row not found for {rid}", file=sys.stderr); sys.exit(2)
|
|
43
52
|
project_root = Path(row["projectRoot"])
|
|
44
53
|
status_rel = row.get("finalStatusRel") or ""
|
|
45
54
|
status_abs = str(project_root / status_rel) if status_rel else ""
|
|
@@ -48,6 +57,10 @@ print(row.get("taskType", ""))
|
|
|
48
57
|
print(int(row.get("runSeq", 0)))
|
|
49
58
|
print(status_abs)
|
|
50
59
|
')"
|
|
60
|
+
resolve_rc=$?
|
|
61
|
+
# resolve 단계가 ResolveError/누락 row 로 exit≠0 한 경우 stderr 의 깔끔한
|
|
62
|
+
# `tail: ...` 메시지를 살리고 fallback glob 으로 넘어가지 않는다.
|
|
63
|
+
if [[ $resolve_rc -ne 0 ]]; then exit "$resolve_rc"; fi
|
|
51
64
|
local rid task_type seq status_abs
|
|
52
65
|
rid="$(printf '%s\n' "$resolved" | sed -n '1p')"
|
|
53
66
|
task_type="$(printf '%s\n' "$resolved" | sed -n '2p')"
|
|
@@ -8,10 +8,10 @@ Usage:
|
|
|
8
8
|
[--template <path>]
|
|
9
9
|
|
|
10
10
|
When ``--output`` is omitted, derives the markdown sibling by stripping
|
|
11
|
-
the ``.data.json`` suffix and appending ``.md``.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
the ``.data.json`` suffix and appending ``.md``. The render is idempotent
|
|
12
|
+
under data.json mutations and intentionally overwrites any existing
|
|
13
|
+
output — the markdown is regenerated whenever the data.json changes
|
|
14
|
+
(Phase 7 token substitution, re-renders).
|
|
15
15
|
"""
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
@@ -78,20 +78,9 @@ def main(argv: list[str]) -> int:
|
|
|
78
78
|
"the renderer reads data.json.meta.reportLanguage (fallback 'en')."
|
|
79
79
|
),
|
|
80
80
|
)
|
|
81
|
-
parser.add_argument(
|
|
82
|
-
"--force",
|
|
83
|
-
action="store_true",
|
|
84
|
-
help="Allow overwriting an existing output path.",
|
|
85
|
-
)
|
|
86
81
|
args = parser.parse_args(argv)
|
|
87
82
|
|
|
88
83
|
output = args.output or _derive_output_path(args.data)
|
|
89
|
-
if output.exists() and not args.force:
|
|
90
|
-
# The data → markdown flow is meant to be idempotent: the same
|
|
91
|
-
# data.json always renders to the same markdown. Overwriting is
|
|
92
|
-
# the intended behaviour for Phase 7 token substitution and for
|
|
93
|
-
# re-renders. Require --force to make accidental clobbers loud.
|
|
94
|
-
pass # fall through to write; idempotent rewrite is intended.
|
|
95
84
|
|
|
96
85
|
try:
|
|
97
86
|
bytes_written = render_to_file(
|
package/runtime/bin/okstra.sh
CHANGED
|
@@ -137,8 +137,11 @@ if [[ "$RENDER_ONLY" == "true" ]]; then
|
|
|
137
137
|
fi
|
|
138
138
|
|
|
139
139
|
# Non-render-only: capture summary + launch JSON, then exec claude.
|
|
140
|
-
|
|
141
|
-
PY_RC
|
|
140
|
+
# `set -e` aborts on a failing command-substitution assignment, which would
|
|
141
|
+
# skip the PY_RC branch below and swallow any stdout the prepare step buffered.
|
|
142
|
+
# Guard the assignment so the explicit return-code check can surface it.
|
|
143
|
+
PY_RC=0
|
|
144
|
+
PREPARE_OUTPUT="$(okstra_py -m okstra_ctl.run "${PY_ARGS[@]}")" || PY_RC=$?
|
|
142
145
|
if [[ $PY_RC -ne 0 ]]; then
|
|
143
146
|
printf '%s\n' "$PREPARE_OUTPUT"
|
|
144
147
|
exit $PY_RC
|
|
@@ -17,6 +17,7 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
17
17
|
- This run executes `{{WORKFLOW_CURRENT_PHASE}}` only. Do not start `{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}` or any later phase inside this run, even if the user says "다음 단계 진행해" or similar.
|
|
18
18
|
{{STAGE_BATCH_DIRECTIVE}}
|
|
19
19
|
{{VERIFICATION_TARGET}}
|
|
20
|
+
{{STAGE_INTEGRATION}}
|
|
20
21
|
- Phase advancement requires a new okstra invocation launched with `--task-type {{WORKFLOW_NEXT_RECOMMENDED_PHASE}}` after this run's final report is written and approved. The lead must not write source code, run builds/migrations/deployments, or otherwise produce artifacts of a different phase from inside this run.
|
|
21
22
|
- See `Lifecycle Phase Boundaries` in the okstra lead contract (`{{OKSTRA_LEAD_CONTRACT_PATH}}`) for the canonical rules and the phase-transition checklist.
|
|
22
23
|
|