okstra 0.96.1 → 0.97.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/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 +22 -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,348 @@
|
|
|
1
|
+
# release-handoff `local checkout` 액션 구현 계획
|
|
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:** release-handoff 에 `local checkout` 액션을 추가해, 검증된 task 브랜치를 메인 워크트리로 가져와 로컬 테스트 가능 상태로 만든다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Python 헬퍼 `okstra handoff local-checkout` 가 registry 의 task-key 행에서 okstra 워크트리·브랜치를 해소하고, 메인 워크트리 cwd 에서 `git worktree remove --force`(브랜치 보존) → `release_status`(branch 슬롯 유지) → `git checkout` 을 수행한다. release-handoff 프로필이 확인을 받고 이 명령 하나만 호출한다. base 브랜치는 절대 건드리지 않는다.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (argparse, subprocess), 기존 `okstra_ctl.handoff` / `worktree` / `worktree_registry`, Node CLI shim, markdown 프로필.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- 설계 문서: [docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md](../specs/2026-06-21-release-handoff-local-checkout-design.md) — 충돌 시 spec 우선.
|
|
14
|
+
- `runtime/` 직접 수정 금지(빌드 산출물). 소스는 `scripts/` `src/` `prompts/`; 변경 후 `npm run build`.
|
|
15
|
+
- pre-1.0 — compat shim 불필요. `local only` 액션은 완전 제거.
|
|
16
|
+
- 함수 본문 50줄 이하.
|
|
17
|
+
- 동시 세션 환경 — git amend 금지, 항상 새 커밋. 내 작업 파일만 스테이징.
|
|
18
|
+
- Conventional Commits, 메시지 한국어 prose, AI 트레일러 금지.
|
|
19
|
+
- 사용자 영향 있는 변경은 [CHANGES.md](../../../CHANGES.md) 에 `사용자 영향:` 항목 추가(한국어).
|
|
20
|
+
- `local checkout` 은 whole-task 모드 전용. base 브랜치 자동 머지는 비목표.
|
|
21
|
+
|
|
22
|
+
## File Structure
|
|
23
|
+
|
|
24
|
+
- `scripts/okstra_ctl/handoff.py` — `local_checkout()` 코어 + `local-checkout` 서브커맨드/디스패치 추가.
|
|
25
|
+
- `src/commands/execute/handoff.mjs` — USAGE 에 `local-checkout` 노출(동작은 기존 shim 이 그대로 forward).
|
|
26
|
+
- `tests/handoff/test_handoff_local_checkout.py` — 신규: 성공/메인 dirty 거부/registry 행 없음 + 브랜치 보존 실증.
|
|
27
|
+
- `prompts/profiles/release-handoff.md` — 액션 선택 재편 + 핸들러 + `local only` 잔존 참조 정리 + 보고 deliverable.
|
|
28
|
+
- `CHANGES.md` — 사용자 영향 항목.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Task 1: `local_checkout()` 코어 + CLI 서브커맨드
|
|
33
|
+
|
|
34
|
+
**Files:**
|
|
35
|
+
- Modify: `scripts/okstra_ctl/handoff.py` (import 확장 + `local_checkout()` 추가 + `main()` argparse/dispatch)
|
|
36
|
+
- Modify: `src/commands/execute/handoff.mjs` (USAGE)
|
|
37
|
+
- Test: `tests/handoff/test_handoff_local_checkout.py`
|
|
38
|
+
|
|
39
|
+
**Interfaces:**
|
|
40
|
+
- Consumes: `worktree_registry.lookup(project_id, task_group, task_id) -> Optional[WorktreeEntry]` (`.branch`, `.worktree_path`, `.status`); `worktree_registry.release_status(project_id, task_group, task_id)`; `worktree.main_worktree_path(Path) -> Path`; `worktree.is_dirty_excluding_okstra(cwd) -> bool`; `handoff._run_git(args, cwd, check=True)`.
|
|
41
|
+
- Produces: `handoff.local_checkout(*, project_root, project_id, task_group, task_id) -> Dict[str, Any]` — 성공 시 `{"ok": True, "branch": str, "mainWorktreePath": str, "removedWorktree": str, "status": "checked-out"}`; 전제 위반 시 `HandoffError`(→ exit 1). CLI: `okstra handoff local-checkout --project-root <dir> --project-id <id> --task-group <g> --task-id <t>`.
|
|
42
|
+
|
|
43
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
44
|
+
|
|
45
|
+
`tests/handoff/test_handoff_local_checkout.py` 생성:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
"""handoff.local_checkout — okstra 워크트리 제거 + 메인 checkout 의 git 실증."""
|
|
49
|
+
import sys
|
|
50
|
+
import subprocess
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
from _paths import REPO_ROOT
|
|
53
|
+
|
|
54
|
+
LIB_DIR = REPO_ROOT / "scripts"
|
|
55
|
+
if str(LIB_DIR) not in sys.path:
|
|
56
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
57
|
+
|
|
58
|
+
import pytest
|
|
59
|
+
|
|
60
|
+
from okstra_ctl import handoff, worktree_registry
|
|
61
|
+
|
|
62
|
+
ARGS = dict(project_id="proj", task_group="grp", task_id="task")
|
|
63
|
+
BRANCH = "feature-development-task"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _git(repo, *args):
|
|
67
|
+
r = subprocess.run(["git", *args], cwd=str(repo),
|
|
68
|
+
capture_output=True, text=True)
|
|
69
|
+
assert r.returncode == 0, r.stderr
|
|
70
|
+
return r.stdout.strip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _main_repo(tmp_path):
|
|
74
|
+
repo = tmp_path / "main"
|
|
75
|
+
repo.mkdir()
|
|
76
|
+
_git(repo, "init", "-q", "-b", "main")
|
|
77
|
+
_git(repo, "config", "user.email", "t@t")
|
|
78
|
+
_git(repo, "config", "user.name", "t")
|
|
79
|
+
(repo / "base.txt").write_text("base\n")
|
|
80
|
+
_git(repo, "add", ".")
|
|
81
|
+
_git(repo, "commit", "-q", "-m", "base")
|
|
82
|
+
return repo
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _provision_okstra_worktree(repo, tmp_path):
|
|
86
|
+
"""main repo 에서 task 브랜치를 든 okstra 워크트리를 만들고 registry 에 등록."""
|
|
87
|
+
okstra_wt = tmp_path / "okstra-wt"
|
|
88
|
+
_git(repo, "worktree", "add", "-b", BRANCH, str(okstra_wt), "main")
|
|
89
|
+
(okstra_wt / "work.txt").write_text("task work\n")
|
|
90
|
+
_git(okstra_wt, "add", ".")
|
|
91
|
+
_git(okstra_wt, "commit", "-q", "-m", "task work")
|
|
92
|
+
worktree_registry.reserve(**ARGS, worktree_path=str(okstra_wt),
|
|
93
|
+
branch=BRANCH, base_ref="main")
|
|
94
|
+
return okstra_wt
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_local_checkout_removes_worktree_and_checks_out_in_main(tmp_path):
|
|
98
|
+
repo = _main_repo(tmp_path)
|
|
99
|
+
okstra_wt = _provision_okstra_worktree(repo, tmp_path)
|
|
100
|
+
|
|
101
|
+
out = handoff.local_checkout(project_root=repo, **ARGS)
|
|
102
|
+
|
|
103
|
+
assert out["status"] == "checked-out"
|
|
104
|
+
assert out["branch"] == BRANCH
|
|
105
|
+
assert not okstra_wt.exists() # okstra 워크트리 제거
|
|
106
|
+
assert _git(repo, "rev-parse", "--abbrev-ref", "HEAD") == BRANCH # 메인이 task 브랜치
|
|
107
|
+
assert _git(repo, "rev-parse", "--verify", BRANCH) # 브랜치 ref 보존
|
|
108
|
+
assert (repo / "work.txt").exists() # task 작업물이 메인에 보임
|
|
109
|
+
assert worktree_registry.lookup(**ARGS).status == "released"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_local_checkout_aborts_when_main_dirty(tmp_path):
|
|
113
|
+
repo = _main_repo(tmp_path)
|
|
114
|
+
okstra_wt = _provision_okstra_worktree(repo, tmp_path)
|
|
115
|
+
(repo / "uncommitted.txt").write_text("dirty\n") # 메인 dirty (.okstra 밖)
|
|
116
|
+
|
|
117
|
+
with pytest.raises(handoff.HandoffError, match="미커밋"):
|
|
118
|
+
handoff.local_checkout(project_root=repo, **ARGS)
|
|
119
|
+
|
|
120
|
+
assert okstra_wt.exists() # 보존
|
|
121
|
+
assert worktree_registry.lookup(**ARGS).status == "active" # 미변경
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_local_checkout_errors_when_no_registry_entry(tmp_path):
|
|
125
|
+
repo = _main_repo(tmp_path)
|
|
126
|
+
with pytest.raises(handoff.HandoffError, match="registry"):
|
|
127
|
+
handoff.local_checkout(project_root=repo, **ARGS)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
131
|
+
|
|
132
|
+
Run: `python3 -m pytest tests/handoff/test_handoff_local_checkout.py -v`
|
|
133
|
+
Expected: FAIL — `AttributeError: module 'okstra_ctl.handoff' has no attribute 'local_checkout'`
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 3: import 확장**
|
|
136
|
+
|
|
137
|
+
`scripts/okstra_ctl/handoff.py` 의 import 한 줄을 교체:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from .worktree import compute_branch_name, compute_worktree_path
|
|
141
|
+
```
|
|
142
|
+
→
|
|
143
|
+
```python
|
|
144
|
+
from .worktree import (compute_branch_name, compute_worktree_path,
|
|
145
|
+
main_worktree_path, is_dirty_excluding_okstra)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- [ ] **Step 4: `local_checkout()` 구현**
|
|
149
|
+
|
|
150
|
+
`scripts/okstra_ctl/handoff.py` 에 `assemble(...)` 정의 바로 다음(즉 `record_verified` 앞)에 추가:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
def local_checkout(*, project_root, project_id, task_group, task_id) -> Dict[str, Any]:
|
|
154
|
+
"""whole-task: okstra task-key 워크트리를 제거하고 task 브랜치를 메인
|
|
155
|
+
워크트리에 checkout 한다(브랜치 보존). 메인이 dirty 면 거부. base 브랜치는
|
|
156
|
+
건드리지 않는다."""
|
|
157
|
+
entry = worktree_registry.lookup(project_id, task_group, task_id)
|
|
158
|
+
if not entry or not entry.branch or not entry.worktree_path:
|
|
159
|
+
raise HandoffError(
|
|
160
|
+
"task-key worktree registry 엔트리가 없습니다 — local checkout 대상 없음")
|
|
161
|
+
main_wt = main_worktree_path(Path(project_root))
|
|
162
|
+
if is_dirty_excluding_okstra(main_wt):
|
|
163
|
+
raise HandoffError(
|
|
164
|
+
f"메인 워크트리에 미커밋 변경이 있습니다 ({main_wt}) — "
|
|
165
|
+
"커밋/스태시 후 다시 시도하세요")
|
|
166
|
+
_run_git(["worktree", "remove", "--force", entry.worktree_path], main_wt)
|
|
167
|
+
worktree_registry.release_status(project_id, task_group, task_id)
|
|
168
|
+
co = _run_git(["checkout", entry.branch], main_wt, check=False)
|
|
169
|
+
if co.returncode != 0:
|
|
170
|
+
raise HandoffError(
|
|
171
|
+
f"git checkout {entry.branch} 실패: {co.stderr.strip()} — 워크트리는 "
|
|
172
|
+
f"제거됨; 메인({main_wt})에서 수동 checkout 으로 복구하세요")
|
|
173
|
+
return {"ok": True, "branch": entry.branch, "mainWorktreePath": str(main_wt),
|
|
174
|
+
"removedWorktree": entry.worktree_path, "status": "checked-out"}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- [ ] **Step 5: argparse 서브커맨드 + 디스패치**
|
|
178
|
+
|
|
179
|
+
`scripts/okstra_ctl/handoff.py` 의 `main()` 에서 `record-pr` 서브파서 등록 다음에 추가:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
sp = sub.add_parser("local-checkout")
|
|
183
|
+
common(sp, plan=False)
|
|
184
|
+
sp.add_argument("--project-root", default=".")
|
|
185
|
+
sp.add_argument("--project-id", required=True)
|
|
186
|
+
sp.add_argument("--task-group", required=True)
|
|
187
|
+
sp.add_argument("--task-id", required=True)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
디스패치에서 `else:`(record-pr) 바로 앞에 elif 추가:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
elif a.cmd == "local-checkout":
|
|
194
|
+
out = local_checkout(
|
|
195
|
+
project_root=Path(a.project_root).resolve(),
|
|
196
|
+
project_id=a.project_id, task_group=a.task_group,
|
|
197
|
+
task_id=a.task_id)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- [ ] **Step 6: 테스트 통과 확인**
|
|
201
|
+
|
|
202
|
+
Run: `python3 -m pytest tests/handoff/test_handoff_local_checkout.py -v`
|
|
203
|
+
Expected: PASS (3 passed)
|
|
204
|
+
|
|
205
|
+
- [ ] **Step 7: handoff.mjs USAGE 갱신**
|
|
206
|
+
|
|
207
|
+
`src/commands/execute/handoff.mjs` 의 USAGE 에서 `record-pr` 줄 다음에 추가:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
okstra handoff record-pr --plan-run-root <dir> --stages 2,3 \\
|
|
211
|
+
--branch <b> --url <u>
|
|
212
|
+
okstra handoff local-checkout --project-root <dir> --project-id <id> \\
|
|
213
|
+
--task-group <g> --task-id <t>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- [ ] **Step 8: CLI 스모크**
|
|
217
|
+
|
|
218
|
+
Run: `node bin/okstra handoff --help`
|
|
219
|
+
Expected: 출력에 `local-checkout` 줄이 보임.
|
|
220
|
+
|
|
221
|
+
- [ ] **Step 9: 커밋**
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
git add scripts/okstra_ctl/handoff.py src/commands/execute/handoff.mjs tests/handoff/test_handoff_local_checkout.py
|
|
225
|
+
git commit -m "feat(handoff): okstra handoff local-checkout 서브커맨드 추가"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### Task 2: release-handoff 프로필 — 액션 재편 + `local only` 정리
|
|
231
|
+
|
|
232
|
+
**Files:**
|
|
233
|
+
- Modify: `prompts/profiles/release-handoff.md`
|
|
234
|
+
|
|
235
|
+
**Interfaces:**
|
|
236
|
+
- Consumes: Task 1 의 CLI `okstra handoff local-checkout ...`.
|
|
237
|
+
- Produces: 프로필 동작 — `local checkout`(첫째)/`push + PR`/`skip` 3지선다, whole-task 게이트, 확인 후 헬퍼 호출, 보고 deliverable.
|
|
238
|
+
|
|
239
|
+
- [ ] **Step 1: 액션 선택 블록 교체**
|
|
240
|
+
|
|
241
|
+
`prompts/profiles/release-handoff.md` 의 다음 블록(현재 22–26줄)을 교체:
|
|
242
|
+
|
|
243
|
+
old:
|
|
244
|
+
```
|
|
245
|
+
1. **Action selection** — present three choices and capture exactly one:
|
|
246
|
+
- `local only` — complete the handoff as a local delivery record: record the accepted, already-committed branch and end without push or PR.
|
|
247
|
+
- `push + PR` — push the feature branch, then open or reuse a pull request.
|
|
248
|
+
- `skip` — cancel delivery actions for now: record that release-handoff was intentionally skipped and end the run without any git command.
|
|
249
|
+
If the user picks `skip`, route directly to the final-report self-review pass.
|
|
250
|
+
```
|
|
251
|
+
new:
|
|
252
|
+
```
|
|
253
|
+
1. **Action selection** — present three choices and capture exactly one:
|
|
254
|
+
- `local checkout` (whole-task mode only) — bring the verified task branch into the MAIN worktree for local testing (no push, no PR). See step 1c. When `HANDOFF_MODE` is `stage-group`, do NOT offer this option — tell the user "`local checkout` is whole-task mode only" and re-ask.
|
|
255
|
+
- `push + PR` — push the feature branch, then open or reuse a pull request.
|
|
256
|
+
- `skip` — cancel delivery actions for now: record that release-handoff was intentionally skipped and end the run without any git command.
|
|
257
|
+
If the user picks `skip`, route directly to the final-report self-review pass.
|
|
258
|
+
1c. **Local checkout execution** (only when the user picked `local checkout`) — confirm with the user that okstra will remove the okstra task worktree and `git checkout <feature branch>` in the MAIN worktree, and that the base branch is never modified. On confirmation run:
|
|
259
|
+
`okstra handoff local-checkout --project-root <project root> --project-id <id> --task-group <g> --task-id <t>`
|
|
260
|
+
Exit 1 means a precondition failed (MAIN worktree dirty, registry entry missing, or checkout failed): show the error verbatim and re-ask the action selection. On success the okstra task worktree no longer exists — the lead's cwd is gone, so EVERY subsequent step (final-report authoring included) MUST use absolute paths or run from the MAIN worktree. Then route to the final-report self-review pass.
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- [ ] **Step 2: 잔존 `local only` 참조 정리**
|
|
264
|
+
|
|
265
|
+
다음 세 줄을 각각 교체:
|
|
266
|
+
|
|
267
|
+
68줄 old: ` - Q1 action: `local only` | `push + PR` | `skip`.`
|
|
268
|
+
68줄 new: ` - Q1 action: `local checkout` | `push + PR` | `skip`.`
|
|
269
|
+
|
|
270
|
+
75줄 old: ` - `- Not run (user picked local only or skip).``
|
|
271
|
+
75줄 new: ` - `- Not run (user picked local checkout or skip).``
|
|
272
|
+
|
|
273
|
+
79줄 old: ` - `- No PR action requested.` (user picked `local only` or `skip`)`
|
|
274
|
+
79줄 new: ` - `- No PR action requested.` (user picked `local checkout` or `skip`)`
|
|
275
|
+
|
|
276
|
+
- [ ] **Step 3: Local Checkout Outcome deliverable 추가**
|
|
277
|
+
|
|
278
|
+
`Required deliverable shape` 의 **Pull Request Outcome** 블록(현재 78–82줄) 다음에 새 deliverable 추가:
|
|
279
|
+
|
|
280
|
+
old:
|
|
281
|
+
```
|
|
282
|
+
- `- PR creation skipped: <reason>` for any user-driven cancellation
|
|
283
|
+
- **Stage Group** (stage-group mode only): selected stages, each stage's single-stage verification report path + quoted `Verdict Token` row, collector branch name, merge commit SHAs from assemble, and the dependency-closure verdict (from the assemble output / error).
|
|
284
|
+
```
|
|
285
|
+
new:
|
|
286
|
+
```
|
|
287
|
+
- `- PR creation skipped: <reason>` for any user-driven cancellation
|
|
288
|
+
- **Local Checkout Outcome**: one of
|
|
289
|
+
- `- Not run (user picked push + PR or skip).`
|
|
290
|
+
- `- Checked out <branch> into <main worktree path>; okstra task worktree <path> removed. Next: 로컬 테스트 후 PR 하려면 okstra-run 으로 release-handoff 재진입 → push + PR.`
|
|
291
|
+
- **Stage Group** (stage-group mode only): selected stages, each stage's single-stage verification report path + quoted `Verdict Token` row, collector branch name, merge commit SHAs from assemble, and the dependency-closure verdict (from the assemble output / error).
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- [ ] **Step 4: 잔존 참조 일관성 확인**
|
|
295
|
+
|
|
296
|
+
Run: `grep -n "local only" prompts/profiles/release-handoff.md`
|
|
297
|
+
Expected: 출력 없음(모든 `local only` 제거됨).
|
|
298
|
+
|
|
299
|
+
- [ ] **Step 5: 워크플로 validator**
|
|
300
|
+
|
|
301
|
+
Run: `bash validators/validate-workflow.sh`
|
|
302
|
+
Expected: PASS(또는 본 변경과 무관한 기존 상태 그대로 — 신규 실패 없음).
|
|
303
|
+
|
|
304
|
+
- [ ] **Step 6: 커밋**
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
git add prompts/profiles/release-handoff.md
|
|
308
|
+
git commit -m "feat(handoff): release-handoff 에 local checkout 액션 + local only 제거"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### Task 3: CHANGES.md + 빌드 + 최종 검증
|
|
314
|
+
|
|
315
|
+
**Files:**
|
|
316
|
+
- Modify: `CHANGES.md`
|
|
317
|
+
|
|
318
|
+
- [ ] **Step 1: CHANGES.md 항목 추가**
|
|
319
|
+
|
|
320
|
+
`CHANGES.md` 의 `## 2026-06-21` 섹션(없으면 최상단 날짜 섹션 위에 생성)에 추가:
|
|
321
|
+
|
|
322
|
+
```markdown
|
|
323
|
+
### feat(handoff): release-handoff 에 `local checkout` 액션 추가 — 검증된 브랜치를 메인 워크트리로
|
|
324
|
+
|
|
325
|
+
- **배경**: okstra task 작업은 격리된 git worktree(`~/.okstra/worktrees/...`)에서 진행돼, 검증이 끝난 뒤 사용자가 결과를 로컬에서 테스트하려면 그 폴더로 직접 `cd` 해야 했다. task 브랜치를 메인 작업 디렉터리로 가져오는 한-스텝 경로가 없었다.
|
|
326
|
+
- **해결**: release-handoff 액션 선택을 `local checkout`(신규, 첫째)/`push + PR`/`skip` 으로 재편하고(중복이던 `local only` 제거), 신규 `okstra handoff local-checkout` 헬퍼가 메인 워크트리 dirty 확인 후 okstra task 워크트리를 제거(브랜치 보존)하고 메인에서 `git checkout <task-branch>` 를 수행한다. base 브랜치는 건드리지 않는다(whole-task 모드 전용).
|
|
327
|
+
- 사용자 영향: release-handoff 에서 `local checkout` 을 고르면, 검증된 task 브랜치가 메인 작업 디렉터리에 바로 체크아웃돼 별도 `cd` 없이 로컬 테스트할 수 있다. 메인에 미커밋 변경이 있으면 중단·안내한다. 기존 `local only` 액션은 `skip` 과 중복이라 제거됐다. (설치 환경은 `okstra install` 로 런타임 갱신 후 적용.)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- [ ] **Step 2: 런타임 빌드**
|
|
331
|
+
|
|
332
|
+
Run: `npm run build`
|
|
333
|
+
Expected: 성공(소스가 `runtime/` 으로 동기화됨).
|
|
334
|
+
|
|
335
|
+
- [ ] **Step 3: 최종 검증 스윕**
|
|
336
|
+
|
|
337
|
+
Run: `python3 -m pytest tests/handoff/test_handoff_local_checkout.py tests/handoff/test_handoff_assemble.py -q`
|
|
338
|
+
Expected: PASS(신규 3 + 기존 회귀 무손상).
|
|
339
|
+
|
|
340
|
+
Run: `node bin/okstra handoff --help`
|
|
341
|
+
Expected: `local-checkout` 노출.
|
|
342
|
+
|
|
343
|
+
- [ ] **Step 4: 커밋**
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
git add CHANGES.md
|
|
347
|
+
git commit -m "docs(handoff): local checkout 액션 CHANGES 기록"
|
|
348
|
+
```
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 범위: `implementation` phase에서 stage를 **별도 git worktree로 격리**해, 사용자가 수동으로 동시에 띄운 여러 `implementation` run이 안전하게 서로 다른 stage를 진행하도록 한다. `started-exclusion`(A2)을 같은 설계에 통합한다.
|
|
5
5
|
- 비범위
|
|
6
6
|
- **자동 fan-out 없음** — okstra가 ready stage들을 여러 프로세스로 자동 분기하지 않는다. 병렬 트리거는 사용자가 stage별 run을 각각 기동하는 **수동 동시**만 지원한다.
|
|
7
|
-
- **okstra 자동 머지 없음** — stage 브랜치 합류는 사용자 수동 머지(또는 release-handoff 수집)다. (2026-06-10 개정: release-handoff stage-group 모드의 수집 머지는 예외 — [2026-06-10-stage-group-handoff-design.md](2026-06-10-stage-group-handoff-design.md))
|
|
7
|
+
- **okstra 자동 머지 없음** — stage 브랜치 합류는 사용자 수동 머지(또는 release-handoff 수집)다. (2026-06-10 개정: release-handoff stage-group 모드의 수집 머지는 예외 — [2026-06-10-stage-group-handoff-design.md](2026-06-10-stage-group-handoff-design.md)) (2026-06-20 갱신: whole-task 통합 경로에 한해 [2026-06-20-stage-auto-integrate-teardown-design.md](2026-06-20-stage-auto-integrate-teardown-design.md) 가 자동 머지+teardown 으로 전환·해소.)
|
|
8
8
|
- `implementation` 외 phase(`requirements-discovery` / `error-analysis` / `implementation-planning` / `final-verification` / `release-handoff`)의 worktree 모델은 불변 — 기존 task-key worktree 1개 유지.
|
|
9
9
|
- ADR↔gitignore(C1)는 별도 plan. 다국어/i18n.
|
|
10
10
|
- 관계: [`2026-05-20-implementation-planning-multi-stage-design.md`](2026-05-20-implementation-planning-multi-stage-design.md)의 stage 개념·carry-in 모델 위에 선다. [`2026-06-04-stage-run-batching.md`](../plans/2026-06-04-stage-run-batching.md)가 known gap으로 남긴 **started-exclusion 미구현**을 본 설계가 해소한다. [`2026-06-04-stage-splitting-cost-aware-design.md`](2026-06-04-stage-splitting-cost-aware-design.md)의 "병렬=부수효과, run batch=비용 단위" 원칙과 양립한다(본 설계는 부수효과인 병렬을 **안전하게** 만들 뿐, 분할 기준을 바꾸지 않는다).
|
|
@@ -177,4 +177,4 @@ okstra는 pre-1.0(`feedback_pre_v1_no_compat`). 기존 단일-worktree `implemen
|
|
|
177
177
|
- 하나라도 아니면 → `PrepareError`로 "선행 stage 브랜치(`-s<X>`/`-s<Y>`)를 task worktree에 머지(또는 main 머지 후 worktree 갱신) 후 재시도" 안내.
|
|
178
178
|
- 사용자 워크플로우: stage X·Y가 done → 각 stage 브랜치를 task-key worktree(또는 main)에 머지 → task worktree HEAD가 X·Y done을 ancestor로 가짐 → 다중 의존 stage가 그 위에서 자동으로 base를 잡고 시작.
|
|
179
179
|
- 옵션 B(okstra octopus 임시 머지)는 여전히 보류.
|
|
180
|
-
2. **stage worktree 자동 teardown** — 현재 수동(§6). 후속 spec 후보.
|
|
180
|
+
2. **stage worktree 자동 teardown** — 현재 수동(§6). 후속 spec 후보. (2026-06-20 갱신: whole-task 통합 경로에 한해 [2026-06-20-stage-auto-integrate-teardown-design.md](2026-06-20-stage-auto-integrate-teardown-design.md) 가 자동 머지+teardown 으로 전환·해소.)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# 에러 환류 루프 (cross-project → okstra 자가수정) — 설계 spec
|
|
2
|
+
|
|
3
|
+
- Date: 2026-06-20
|
|
4
|
+
- Status: Draft (사용자 검토 대기)
|
|
5
|
+
- Owner: Claude lead (현재 세션)
|
|
6
|
+
- Related: `scripts/okstra_ctl/index.py`, `scripts/okstra_ctl/error_report.py`, `scripts/okstra_ctl/paths.py`, `scripts/okstra-error-log.py`, `skills/okstra-inspect/SKILL.md`, `src/commands/inspect/`, `skills/okstra-brief/SKILL.md`, `prompts/profiles/error-analysis.md`
|
|
7
|
+
|
|
8
|
+
## 1. 문제
|
|
9
|
+
|
|
10
|
+
okstra 가 타겟 프로젝트에서 구동될 때, 에이전트 실행 중 발생하는 에러는 errorType 3종(`tool-failure`, `cli-failure`, `contract-violation`)으로 각 타겟의 `<target>/.okstra/tasks/<group>/<id>/runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl` 에 적재된다 (정본 writer: `scripts/okstra-error-log.py`).
|
|
11
|
+
|
|
12
|
+
이 3종 에러는 대부분 **타겟 코드의 결함이 아니라 okstra 자체의 결함 신호**다:
|
|
13
|
+
|
|
14
|
+
- `cli-failure` — codex/antigravity 래퍼의 비정상 종료·타임아웃·결과파일 누락. 래퍼 로직/폴링 cap/프롬프트 계약 문제.
|
|
15
|
+
- `contract-violation` — 워커 출력이 phase contract 를 위반. 프로파일 계약 모호성·validator 와 프로파일의 불일치.
|
|
16
|
+
- `tool-failure` — 워커 도구 호출 실패. 권한 seed 누락(`templates/reports/settings.template.json`)·도구 사용 패턴 결함 등 okstra 인프라 문제인 경우가 다수.
|
|
17
|
+
|
|
18
|
+
그러나 현재 이 에러를 **okstra 자신의 수정으로 환류시키는 경로가 없다**:
|
|
19
|
+
|
|
20
|
+
- `skills/okstra-inspect` 의 errors facet(`scripts/okstra_ctl/error_report.py`)은 **단일 task_root 안**의 JSONL 만 글롭해 markdown 으로 집계한다. 읽기 전용·단일 task·진단까지만.
|
|
21
|
+
- `improvement-discovery` sidetrack 은 코드 스캔 기반이며 **에러 로그를 입력으로 전혀 쓰지 않는다**.
|
|
22
|
+
|
|
23
|
+
결과적으로 여러 타겟에 흩어진 okstra 결함 신호는 수집·분석되지 못한 채 각 타겟의 `.okstra/` 안에 고립된다.
|
|
24
|
+
|
|
25
|
+
## 2. 목표와 비목표
|
|
26
|
+
|
|
27
|
+
### 목표
|
|
28
|
+
|
|
29
|
+
- 머신 내 **모든 타겟 프로젝트**에 흩어진 errors-*.jsonl 을 글로벌 run-index(`~/.okstra/recent.jsonl`, `active.jsonl`)로 발견·수집하는 cross-project 수집기 `okstra-error-zip` 도입.
|
|
30
|
+
- 수집 결과를 errorType·source·phase·agent·메시지패턴별로 집계하고, 익명화한 뒤 **`.zip` 아카이브**(집계 리포트 `.md` + 원본 JSONL)로 묶어 **사용자 지정 경로**에 저장.
|
|
31
|
+
- 저장 경로는 이전 입력값을 글로벌 config 에 기억하고, 재실행 시 **재사용/직접입력 3-옵션 picker** 로 제시 (마지막 옵션은 항상 직접입력).
|
|
32
|
+
- zip 을 입력으로 받아 빈발 클러스터 1개를 `error-analysis` brief 로 변환하는 `okstra-brief` 의 `error-feedback` variant 도입.
|
|
33
|
+
- 그 brief 를 **okstra 레포 자신을 타겟으로** 한 기존 `error-analysis → implementation-planning → implementation` lifecycle 에 태워 okstra 소스(`prompts/`·`agents/`·`scripts/`·`templates/`)를 실제 수정.
|
|
34
|
+
- **errorType 비결합 원칙**: 집계·클러스터링·익명화 로직은 특정 errorType 값(`tool-failure`·`cli-failure`·`contract-violation`)에 하드코딩 분기하지 않는다. errorType 은 JSONL 레코드에서 읽은 값을 그대로 그룹 키로 쓰는 **data-driven 방식**으로 다뤄, 새 errorType 이 추가돼도 error-zip 코드를 수정하지 않고 자동 집계된다. errorType 정본 목록이 필요한 지점(예: 미지 타입 경고)은 `scripts/okstra-error-log.py` 의 `ALLOWED_ERROR_TYPES`(`okstra-error-log.py:18`) 를 **단일 참조**한다 — error-zip 코드에 errorType 목록을 재정의하지 않는다.
|
|
35
|
+
|
|
36
|
+
### 비목표
|
|
37
|
+
|
|
38
|
+
- **새 task-type / phase 신설하지 않음.** 수집은 결정적 파일시스템 작업이므로 LLM phase 가 아니라 Python 코드가 맡고, 수정은 기존 error-analysis lifecycle 을 재사용한다. (검토했던 접근 B 기각 — §7)
|
|
39
|
+
- **자동 실행하지 않음.** run 종료 시 자동 적치·정기 스캔은 도입하지 않는다. 전 과정은 사용자 온디맨드 트리거.
|
|
40
|
+
- **수집기가 소스를 수정하지 않음.** `okstra-error-zip` 은 읽기·집계·저장만. 실제 수정은 별도 lifecycle run 의 책임.
|
|
41
|
+
- **타겟 프로젝트 코드를 수정하지 않음.** 환류의 수정 대상은 오직 okstra 레포 소스.
|
|
42
|
+
- **errors JSONL 정본을 삭제·이동하지 않음.** 각 타겟의 정본은 그대로 두고 읽기만 한다 (artifact-home 규칙: 타겟의 `.okstra/` 는 타겟 메모리).
|
|
43
|
+
|
|
44
|
+
## 3. 전체 데이터 흐름
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
[1 수집·집계·저장] okstra-error-zip (신규, 결정적 Python)
|
|
48
|
+
~/.okstra/{recent,active}.jsonl 순회
|
|
49
|
+
→ 각 row 의 projectRoot + runDirRel 로 run 디렉터리 복원
|
|
50
|
+
→ 그 밑 logs/errors-*.jsonl (+ stage-*/logs/errors-*.jsonl) 글롭
|
|
51
|
+
→ errorType·source·phase·agent·메시지패턴별 집계 + 빈발 클러스터
|
|
52
|
+
→ 익명화 (타겟 고유 경로/코드 excerpt 상대화·축약)
|
|
53
|
+
→ .zip 아카이브 생성 (집계 report.md + 익명화 JSONL)
|
|
54
|
+
→ 사용자 지정 경로에 저장 (이전 경로 재사용/직접입력 picker)
|
|
55
|
+
│
|
|
56
|
+
▼
|
|
57
|
+
[2 brief화] okstra-brief error-feedback variant (신규)
|
|
58
|
+
zip 입력 → 빈발 클러스터 1개(= 단일 근본원인 후보) 선택
|
|
59
|
+
→ error-analysis brief 생성 (Source Material 에 원본 JSONL 레코드 인용)
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
[3 수정] 기존 lifecycle 재사용 (신규 0)
|
|
63
|
+
okstra 레포 대상 okstra-run --task-type error-analysis
|
|
64
|
+
→ 원인분석(소스 자유 추적, scan-scope 제약 없음)
|
|
65
|
+
→ implementation-planning → implementation (실제 소스 수정)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 4. okstra-error-zip (신규)
|
|
69
|
+
|
|
70
|
+
### 4.1 진입 표면
|
|
71
|
+
|
|
72
|
+
`skills/okstra-inspect` 의 **9번째 facet `error-zip`** 으로 둔다. 근거:
|
|
73
|
+
|
|
74
|
+
- read-side okstra inspection 의 SSOT 는 `okstra-inspect` 단일 디스패처다. cross-project 에러 집계도 읽기 작업이므로 같은 디스패처에 흡수하는 것이 단일참조원칙에 맞다.
|
|
75
|
+
- 사용자가 부른 트리거 "okstra error-zip" 은 이 facet 으로 라우팅한다.
|
|
76
|
+
|
|
77
|
+
핵심 로직은 신규 `scripts/okstra_ctl/error_zip.py`. 진입 래퍼는 `src/commands/inspect/` 에 facet 핸들러로 추가.
|
|
78
|
+
|
|
79
|
+
기존 errors facet(단일 task 진단)과 error-zip facet(cross-project 환류)은 **성격이 다른 별개 facet**으로 공존한다. 단, 글롭·tally 코어는 §4.5 대로 공유한다.
|
|
80
|
+
|
|
81
|
+
### 4.2 수집 알고리즘
|
|
82
|
+
|
|
83
|
+
1. 글로벌 홈(`OKSTRA_HOME`, 기본 `~/.okstra`; `paths.py:40-42`)의 `recent.jsonl` + `active.jsonl` 의 모든 row 를 읽는다.
|
|
84
|
+
2. 각 row 에서 run 디렉터리 절대경로를 복원한다: `Path(projectRoot) / runDirRel` (`index.py:76`, `index.py:95`). `runDirRel` 은 이미 정규화된 상대경로라 가장 안전한 앵커다.
|
|
85
|
+
3. 각 run 디렉터리 밑에서 `logs/errors-*.jsonl` 과 `stage-*/logs/errors-*.jsonl` 을 글롭한다 (기존 `error_report.py:18-23` `_glob_error_logs` 의 패턴과 동일).
|
|
86
|
+
4. 각 JSONL 줄을 파싱해 레코드를 모은다. 파싱 실패 줄은 건너뛰되 카운트한다 (`error_report.py:27-45` 패턴 재사용).
|
|
87
|
+
|
|
88
|
+
> 누락 처리: row 의 `projectRoot` 경로가 더 이상 존재하지 않으면(프로젝트 삭제·이동) skip 하고 집계 리포트에 "도달 불가 run" 수를 명시한다. 침묵 누락 금지.
|
|
89
|
+
|
|
90
|
+
### 4.3 집계
|
|
91
|
+
|
|
92
|
+
errorType·source·phase·agent·메시지패턴별 tally 와 빈도순 정렬, 그리고 **빈발 클러스터**(같은 errorType + 유사 메시지패턴 + 동일 phase/agent 의 묶음)를 도출한다. 클러스터는 §6 brief 분할의 단위가 된다.
|
|
93
|
+
|
|
94
|
+
집계 결과에는 각 클러스터의 출처 프로젝트 수·발생 횟수·대표 레코드 인용이 포함된다.
|
|
95
|
+
|
|
96
|
+
**data-driven 키 수집** (§2 errorType 비결합 원칙 적용): tally 의 그룹 키(errorType 을 포함한 모든 차원)는 레코드에서 발견된 값을 그대로 동적으로 수집한다. 알려진 errorType 목록을 코드에 나열해 분기하지 않으며, 미지의 새 errorType 도 자동으로 자기 그룹으로 잡힌다. 클러스터링은 "errorType 값이 같은가"라는 **동등성 비교**만 쓰고 특정 errorType 이름을 case 분기하지 않는다. 선택적으로, 발견된 errorType 이 `ALLOWED_ERROR_TYPES` SSOT 에 없으면 집계 리포트에 "미등록 errorType" 으로 표시할 수 있으나(이때만 SSOT 참조), 집계 자체는 막지 않는다.
|
|
97
|
+
|
|
98
|
+
### 4.4 익명화 정책 (열린 디테일 — §8.1 에서 확정)
|
|
99
|
+
|
|
100
|
+
cross-project 수집이므로 타겟 코드 경로·excerpt 가 섞인다. okstra 결함 진단에 필요한 메타(`errorType`/`command`/`exitCode`/`phase`/`agent`/stderr 패턴)는 보존하되, 타겟 고유 식별 정보는 축약한다:
|
|
101
|
+
|
|
102
|
+
- 타겟 절대경로 → projectRoot 기준 상대경로로 치환.
|
|
103
|
+
- projectId / projectRoot 자체 → 안정적 짧은 토큰(예: `proj-<n>`)으로 치환하고, 매핑 표는 zip 에 포함하지 않는다(또는 별도 분리).
|
|
104
|
+
- stderr/메시지의 코드 excerpt → okstra 진단에 무관한 부분은 길이 제한·축약.
|
|
105
|
+
|
|
106
|
+
okstra 자신의 결함은 errorType·command·exit-code·phase·agent·okstra 소스 경로 패턴에서 드러나므로, 타겟 고유 정보를 축약해도 진단력은 유지된다.
|
|
107
|
+
|
|
108
|
+
### 4.5 코드 공유 (단일참조)
|
|
109
|
+
|
|
110
|
+
`error_report.py` 의 글롭(`_glob_error_logs`)과 tally 로직을 공용 모듈로 추출해 errors facet 과 error-zip facet 이 함께 쓴다. 중복 구현 금지. errors facet 은 "단일 task_root 글롭", error-zip 은 "index 순회 후 다수 run-dir 글롭"으로 **입력 경로 집합만 다르고 집계는 동일**하다.
|
|
111
|
+
|
|
112
|
+
### 4.6 출력 / 저장 UX
|
|
113
|
+
|
|
114
|
+
- 출력물: 단일 `.zip` 아카이브. 내부 구성:
|
|
115
|
+
- `report.md` — §4.3 집계 리포트 (사람이 읽는 면).
|
|
116
|
+
- `errors/` — 익명화된 원본 JSONL (lifecycle 이 근거로 인용하는 면).
|
|
117
|
+
- 저장 경로: 사용자 입력. 이전 입력 경로를 글로벌 config(`~/.okstra/` 하위, 정확한 파일명은 §8.2)에 기억.
|
|
118
|
+
- picker (메모리 규칙 "추천 1~2개 + 직접 입력 = 3-옵션" 준수):
|
|
119
|
+
1. (이전 경로가 있으면) 이전 경로 재사용 — 추천, 첫 옵션.
|
|
120
|
+
2. (없으면) 합리적 기본 경로 제안 — 추천.
|
|
121
|
+
3. 직접 입력 — 항상 마지막 옵션.
|
|
122
|
+
- artifact-home 충돌 회피: cross-project 집계물은 특정 프로젝트 소유가 아니다. 따라서 okstra 의 어느 프로젝트 `.okstra/` 에도 강제 적치하지 않고 **사용자가 지정한 경로**(예: okstra 레포 작업공간, 또는 임의 위치)에 저장한다.
|
|
123
|
+
|
|
124
|
+
## 5. okstra-brief error-feedback variant (신규)
|
|
125
|
+
|
|
126
|
+
`skills/okstra-brief/SKILL.md` 에 새 variant 를 추가한다. 입력은 §4 의 `.zip`.
|
|
127
|
+
|
|
128
|
+
- zip 을 풀어 `report.md` 의 빈발 클러스터 목록을 제시하고, 사용자가 **클러스터 1개**를 고른다.
|
|
129
|
+
- 그 클러스터를 단일 `error-analysis` brief 로 변환한다. brief 의 `Source Material` 에 해당 클러스터의 원본 JSONL 레코드(errorType·command·exitCode·phase·agent·메시지)를 인용한다.
|
|
130
|
+
- 한 brief = 한 클러스터 = 한 근본원인 후보. 이질적 에러를 한 brief 에 섞지 않는다 (§6).
|
|
131
|
+
|
|
132
|
+
## 6. 수정 lifecycle 연결 (기존 재사용)
|
|
133
|
+
|
|
134
|
+
`error-analysis` 는 brief 입력 entry phase 이며 scan-scope 제약이 없어, 워커가 okstra 레포 소스 전체를 `Read`/`Grep` 으로 자유 추적할 수 있다 (`prompts/profiles/error-analysis.md`). errorType 3종이 정확히 일치하므로 brief 의 symptom 으로 그대로 받는다.
|
|
135
|
+
|
|
136
|
+
**구조적 제약 — symptom-lock 단일성**: error-analysis 의 진단 루프는 "하나의 관측 가능한 실패조건"을 요구한다 (`error-analysis.md` Symptom lock). 따라서 cross-project 로 수집한 이질적 에러를 한 brief 에 섞으면 첫 blocker 로 "단일 실패조건 도출 불가"가 뜬다. 이 때문에 §4.3 의 **클러스터가 brief 분할의 단위**가 되어야 한다.
|
|
137
|
+
|
|
138
|
+
흐름: 원인이 evidence 로 확정되면 error-analysis 가 `implementation-planning` 으로 라우팅(`workflow.py` `DEFAULT_NEXT_PHASE["error-analysis"] = "implementation-planning"`)하고, 그 다음 `implementation` run 이 okstra 소스를 실제 수정한다. error-analysis run 자체는 소스를 고치지 않는다(프로파일 비목표).
|
|
139
|
+
|
|
140
|
+
## 7. 검토한 대안
|
|
141
|
+
|
|
142
|
+
| 접근 | 내용 | 기각/채택 사유 |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| **A (채택)** | 수집기(결정적 Python) + brief variant + 기존 lifecycle 재사용 | 신규 표면 최소. 수집=결정적/수정=LLM 책임 분리. validator·lifecycle 100% 재사용 |
|
|
145
|
+
| B (기각) | improvement-discovery 대칭으로 전용 task-type `error-feedback-discovery` 신설 | 프로파일+validator+wizard+상수+테스트 한 벌 복제(YAGNI). **파일시스템 순회를 LLM phase 에 넣으면 비결정적** — 수집의 본질과 불일치 |
|
|
146
|
+
| C (기각) | 수집기 CLI 만, brief화·수정은 사람 | "실질적으로 수정하는 프로세스"가 끊김 — 요구 미충족 |
|
|
147
|
+
|
|
148
|
+
## 8. 열린 디테일 (구현 계획에서 확정)
|
|
149
|
+
|
|
150
|
+
### 8.1 익명화 정책의 정확한 경계
|
|
151
|
+
보존 필드 화이트리스트와 축약 규칙을 확정한다. allowlist 기반(보존할 필드를 명시, 그 외는 축약)으로 설계한다.
|
|
152
|
+
|
|
153
|
+
### 8.2 저장 경로 기억 위치
|
|
154
|
+
이전 출력 경로를 기억할 글로벌 config 파일을 확정한다. 기존 `~/.okstra/` 하위 config 가 있으면 거기에 키를 추가하고, 없으면 전용 파일을 둔다. env var 대신 명시적 파일로 (메모리 규칙: env var 지양).
|
|
155
|
+
|
|
156
|
+
### 8.3 클러스터 분할 기준
|
|
157
|
+
빈발 클러스터를 묶는 정확한 기준(errorType 일치 + 메시지패턴 유사도 + phase/agent 동일)의 임계값을 확정한다.
|
|
158
|
+
|
|
159
|
+
## 9. 신규/수정 표면 요약
|
|
160
|
+
|
|
161
|
+
| 파일 | 작업 |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `scripts/okstra_ctl/error_zip.py` | 신규 — index 순회·집계·익명화·zip 생성. errorType 은 data-driven(§2 비결합 원칙), 목록 필요 시 `ALLOWED_ERROR_TYPES` SSOT 참조 |
|
|
164
|
+
| `scripts/okstra_ctl/error_report.py` | 글롭/tally 코어를 공용 모듈로 추출(리팩터) |
|
|
165
|
+
| `skills/okstra-inspect/SKILL.md` | `error-zip` facet 추가 + 저장경로 picker 문구 |
|
|
166
|
+
| `src/commands/inspect/` | error-zip facet 진입 래퍼 |
|
|
167
|
+
| `skills/okstra-brief/SKILL.md` | `error-feedback` variant 추가 |
|
|
168
|
+
| `~/.okstra/` config | 이전 출력 경로 기억 키 (§8.2) |
|
|
169
|
+
| `tests/inspect/`, `tests/report/` | 수집·집계·익명화·picker 단위 테스트 |
|
|
170
|
+
|
|
171
|
+
> `runtime/` 은 빌드 산출물이므로 직접 수정하지 않는다. 소스 수정 후 `npm run build` 로 동기화한다.
|