okstra 0.101.0 → 0.101.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/docs/kr/architecture.md +1 -1
- package/docs/project-structure-overview.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/python/okstra_ctl/report_views.py +39 -3
- package/runtime/templates/reports/report.css +10 -0
- package/runtime/templates/reports/report.js +16 -0
package/docs/kr/architecture.md
CHANGED
|
@@ -347,7 +347,7 @@ okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결
|
|
|
347
347
|
|
|
348
348
|
- `implementation`을 제외한 모든 phase는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다(`final-verification`은 read-only 테스트 명령만 허용). `implementation`은 승인된 plan의 파일 목록 안에서만 edit/commit이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API는 여전히 금지됩니다.
|
|
349
349
|
- **모든 task-type 격리 worktree (BLOCKING)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 자동으로 task-key 단위 `git worktree` 를 생성하고, 같은 task-key 의 이후 phase (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) 는 동일한 worktree·브랜치를 재사용합니다. 위치는 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화) 이고, 브랜치 이름은 `<work-category-prefix>-<task-id-segment>` (예: `feat-dev-9436`, `fix-dev-7311`) 입니다. base ref 는 첫 phase prepare 시점의 main worktree `HEAD`. `~/.okstra/worktrees/registry.json` (flock-guarded) 가 task-key → path/branch 매핑을 전역 관리해 동시 실행 시 path·branch 충돌을 방지합니다. configured sync dirs 는 main worktree 에서 symlink 로 연결되어 task checkout 사이의 filesystem continuity 를 제공합니다 (sync 대상 목록은 `project.json` 의 `worktreeSyncDirs` 또는 `OKSTRA_WORKTREE_SYNC_DIRS` 환경변수로 override 가능; 빈 배열이면 sync 비활성화). 이 sync 는 okstra context/write boundary 를 확장하지 않습니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 executor 는 project_root 에서 그대로 작업합니다. worktree 는 run 종료 후 자동 삭제되지 않으며 후속 phase·PR 작성·rollback 검증의 권위 artefact 입니다. 수동 cleanup: `git -C <main-worktree> worktree remove <path>` → `git -C <main-worktree> branch -D <branch>` + registry 항목 삭제. 자세한 동작은 `prompts/profiles/implementation.md` 의 *Task worktree* 블록과 `prompts/lead/okstra-lead-contract.md` 의 *Task worktree (BLOCKING for every task-type)* 섹션 참고.
|
|
350
|
-
- **implementation stage 격리 worktree (동시 병렬)**: 위 task-key 단위 worktree 는 `requirements-discovery`~`implementation-planning` 의 모델입니다. `implementation` task 는 **stage 격리** 로 동작합니다 (spec `docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md`) — **한 run = 한 stage**, 각 run 이 `.../<task-id-segment>/stage-<N>/` (브랜치 `<prefix>-<task-id-segment>-s<N>`) 격리 worktree 를 발급받습니다. registry 가 task-key 와 **stage-key** (`<task-key>#stage-<N>`) 를 함께 flock 예약하고, `_resolve_effective_stages` 가 `consumers.jsonl` 의 `started` + registry 예약 stage 를 ready 집합에서 제외하므로(점유 SSOT = registry), 사용자가 두 `implementation` run 을 동시에 띄우면 서로 다른 독립 stage 를 충돌 없이 진행합니다. base 결정: 독립 = 공통 anchor(첫 stage 진입 HEAD 고정), 단일 의존 = 선행 done commit, 다중 의존 = 선행이 모두 ancestor 인 task worktree HEAD(`git merge-base --is-ancestor`; 미머지 시 `PrepareError`). cost-aware-design 의 ready-set batch 는 stage 마다 격리 branch 가 필요해 의미를 잃으므로(같은 branch 에 두 stage-key reserve 시 branch-uniqueness 충돌) 폐기되었고, 순차 진행은 stage done 후 다음 run, 동시 진행은 별도 run 으로 — cost 등가. `--stage <auto|N>` 또는 wizard `stage_pick` 으로 stage 를 선택합니다. worktree 뿐 아니라 **run 산출물(report·state·worker-results·manifest)도 `runs/implementation/stage-<N>/` 로 stage 별 격리**되므로 동시 실행하는 두 stage 의 보고서·상태가 섞이지 않습니다. 반면 `consumers.jsonl` 과 worktree registry 는 stage 간 공유되는 조율 SSOT 라 task-type 루트(`runs/implementation/`)에 그대로 둡니다.
|
|
350
|
+
- **implementation stage 격리 worktree (동시 병렬)**: 위 task-key 단위 worktree 는 `requirements-discovery`~`implementation-planning` 의 모델입니다. `implementation` task 는 **stage 격리** 로 동작합니다 (spec `docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md`) — **한 run = 한 stage**, 각 run 이 `.../<task-id-segment>/stage-<N>/` (브랜치 `<prefix>-<task-id-segment>-s<N>`) 격리 worktree 를 발급받습니다. registry 가 task-key 와 **stage-key** (`<task-key>#stage-<N>`) 를 함께 flock 예약하고, `_resolve_effective_stages` 가 `consumers.jsonl` 의 `started` + registry 예약 stage 를 ready 집합에서 제외하므로(점유 SSOT = registry), 사용자가 두 `implementation` run 을 동시에 띄우면 서로 다른 독립 stage 를 충돌 없이 진행합니다. base 결정: 독립 = 공통 anchor(첫 stage 진입 HEAD 고정), 단일 의존 = 선행 done commit, 다중 의존 = 선행이 모두 ancestor 인 task worktree HEAD(`git merge-base --is-ancestor`; 미머지 시 `PrepareError`). cost-aware-design 의 ready-set batch 는 stage 마다 격리 branch 가 필요해 의미를 잃으므로(같은 branch 에 두 stage-key reserve 시 branch-uniqueness 충돌) 폐기되었고, 순차 진행은 stage done 후 다음 run, 동시 진행은 별도 run 으로 — cost 등가. `--stage <auto|N>` 또는 wizard `stage_pick` 으로 stage 를 선택합니다. wizard `stage_pick` 은 각 stage 의 상태(`[완료]`/`[진행중]`/`[준비됨]`/`[대기]`)를 라벨에 표시하는 멀티선택이며, 선택분의 의존성 closure 를 Kahn 위상정렬(`stage_targets.order_stage_closure`)해 render-args 의 `chain-stages` CSV 로 내보냅니다. `okstra-run` SKILL 이 이 큐를 받아 의존성 순서대로 단일-stage run 을 N 회 순차 실행(무인 연쇄)하되, 각 stage 의 Phase 6 `done` 행을 확인한 뒤 다음으로 넘어갑니다. 이는 오케스트레이션 계층일 뿐이며 wizard·prepare 의 **한 run = 한 stage** 격리 불변은 그대로입니다. worktree 뿐 아니라 **run 산출물(report·state·worker-results·manifest)도 `runs/implementation/stage-<N>/` 로 stage 별 격리**되므로 동시 실행하는 두 stage 의 보고서·상태가 섞이지 않습니다. 반면 `consumers.jsonl` 과 worktree registry 는 stage 간 공유되는 조율 SSOT 라 task-type 루트(`runs/implementation/`)에 그대로 둡니다.
|
|
351
351
|
- **단일-stage final-verification 의 run 산출물 격리 (동시 병렬)**: 단독-stage `final-verification`(`--stage <N>`)도 implementation 과 동일하게 run 산출물을 `runs/final-verification/stage-<N>/` 하위에 격리하고(seq 도 stage 별 독립), 팀 이름에 `-fv-s<N>` 접미사를 붙입니다 — `-fv-` 구분자로 같은 stage 의 implementation 팀(`-s<N>`)과도, 전체-task 검증의 기본 이름과도 충돌하지 않습니다. 따라서 여러 stage 의 final-verification 을 동시에 띄워도 state·worker-results·보고서·팀이 섞이지 않습니다. worktree 는 새로 만들지 않고 해당 implementation stage worktree 를 registry 에서 read-only 로 재사용하며, 그래서 registry stage-key 예약도 하지 않습니다. `teamName` 라벨에 `-fv-s<N>` 접미사를 붙이는 것은 audit/표시용 구분일 뿐이며, 실제 팀은 세션별 implicit team(`session-<leadSid>`)이라 v2.1.178 이전의 `TeamCreate` 이름 충돌 hard-fail 은 더 이상 발생하지 않습니다. 전체-task 검증(stage 빈 값)은 기존 평면 `runs/final-verification/` 구조를 유지합니다.
|
|
352
352
|
- `implementation` 과 `release-handoff` 를 제외한 모든 phase 는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다 (`final-verification` 은 read-only 테스트 명령만 허용). `implementation` 은 승인된 plan 의 파일 목록 안에서만 edit/commit 이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API 는 여전히 금지됩니다. `release-handoff` 는 source code 자체는 수정하지 않고, 사용자가 메뉴로 선택한 commit / push / PR 명령만 실행합니다 (force push, base 브랜치 직접 push, hook bypass, release publish 는 여전히 금지).
|
|
353
353
|
- 사용자가 "다음 단계 진행해" 같은 표현을 보내도, 그 발화만으로 다음 phase가 자동 시작되지 않습니다. 다음 phase는 새 `okstra.sh` 실행으로만 시작합니다.
|
|
@@ -194,7 +194,7 @@ Important modules:
|
|
|
194
194
|
|---|---|
|
|
195
195
|
| `run.py` | `prepare_task_bundle()` single authority and CLI parser |
|
|
196
196
|
| `implementation_stage.py` | `implementation` 단일-stage run 오케스트레이션 — consumer 상태 복구 → 가용 Stage Map entry 선택 → 격리 stage worktree provision → 선택 stage 를 run context 로 발행 (`run.py` 에서 추출) |
|
|
197
|
-
| `stage_targets.py` | Stage readiness/verification 정책 SSOT — 어떤 stage 가 실행 가능한지, 어느 commit 에서 분기하는지, final-verification 이 무엇을 검사하는지 결정 (`consumers.jsonl` 행과 git ancestry 검사를 단일 인터페이스 뒤로 캡슐화) |
|
|
197
|
+
| `stage_targets.py` | Stage readiness/verification 정책 SSOT — 어떤 stage 가 실행 가능한지, 어느 commit 에서 분기하는지, final-verification 이 무엇을 검사하는지 결정 (`consumers.jsonl` 행과 git ancestry 검사를 단일 인터페이스 뒤로 캡슐화). `order_stage_closure` 는 wizard 멀티선택 stage 집합의 의존성 closure 를 Kahn 위상정렬해 `chain-stages` 무인 연쇄 순서를 산출 |
|
|
198
198
|
| `stage_reconcile.py` | stage prepare 흐름 공용 best-effort git reconciliation (`git_reconcile.auto_reconcile` 위임; advisory — 실패는 stderr 보고만, dependency gate 가 권위 판정 유지) |
|
|
199
199
|
| `run_context.py` | Per-task mutex, run context and run-input persistence; `consumers_mutex` helper for atomic `consumers.jsonl` writes |
|
|
200
200
|
| `consumers.py` | Append-only `consumers.jsonl` writer + reader — records which `implementation` runs consumed which `implementation-planning` stage |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -148,6 +148,7 @@ def render_html(
|
|
|
148
148
|
f"<code>{sidecar_dir}</code> 에 저장하면 resume-clarification 이 자동으로 첨부합니다.</p>\n"
|
|
149
149
|
f" <pre id=\"user-response-output\" aria-live=\"polite\"></pre>\n"
|
|
150
150
|
f" <button type=\"button\" data-action=\"copy-user-response\">Copy</button>\n"
|
|
151
|
+
f" <button type=\"button\" data-action=\"dismiss-user-response\" class=\"user-response-dismiss\" aria-label=\"출력 닫기\" title=\"닫기\" hidden>×</button>\n"
|
|
151
152
|
f"</footer>\n"
|
|
152
153
|
f"<script id=\"run-meta\" type=\"application/json\">"
|
|
153
154
|
f"{{\"task-key\":\"{html.escape(run_meta.task_key)}\","
|
|
@@ -533,6 +534,36 @@ def _parse_expected_form_options(expected_form: str) -> list[tuple[str, str]]:
|
|
|
533
534
|
return options
|
|
534
535
|
|
|
535
536
|
|
|
537
|
+
_OPTION_LABEL_PREFIX = re.compile(r"^(추천:\s*|\([0-9a-z]\)\s*)")
|
|
538
|
+
# 옵션 본문이 이보다 짧으면 prefix 매칭을 건너뛰고 정확 일치만 인정 —
|
|
539
|
+
# "혼합" 같은 짧은 라벨이 더 긴 다른 응답의 접두어로 오매칭되는 것을 막는다.
|
|
540
|
+
_MIN_OPTION_PREFIX_LEN = 6
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _match_option_value(
|
|
544
|
+
current_value: str, opts: list[tuple[str, str]]
|
|
545
|
+
) -> str | None:
|
|
546
|
+
"""Re-select the option a user previously picked when a carried-in
|
|
547
|
+
``User input`` cell is rendered read-only. Compares ``current_value``
|
|
548
|
+
against each option's label body (``추천:`` / ``(a)`` prefix stripped).
|
|
549
|
+
Exact match wins over a prefix match; returns ``None`` when nothing
|
|
550
|
+
matches so the caller falls back to the ``기타`` branch."""
|
|
551
|
+
cv = (current_value or "").strip()
|
|
552
|
+
if not cv:
|
|
553
|
+
return None
|
|
554
|
+
bodies = [
|
|
555
|
+
(value, _OPTION_LABEL_PREFIX.sub("", label).strip())
|
|
556
|
+
for value, label in opts
|
|
557
|
+
]
|
|
558
|
+
for value, body in bodies:
|
|
559
|
+
if body and (cv == body or cv == value):
|
|
560
|
+
return value
|
|
561
|
+
for value, body in bodies:
|
|
562
|
+
if len(body) >= _MIN_OPTION_PREFIX_LEN and cv.startswith(body):
|
|
563
|
+
return value
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
|
|
536
567
|
def _form_control(
|
|
537
568
|
response_id: str,
|
|
538
569
|
kind: str,
|
|
@@ -562,22 +593,27 @@ def _form_control(
|
|
|
562
593
|
for letter, text in _parse_enum_options(statement)
|
|
563
594
|
]
|
|
564
595
|
if opts:
|
|
596
|
+
matched = _match_option_value(current_value, opts)
|
|
597
|
+
use_other = matched is None and bool((current_value or "").strip())
|
|
565
598
|
select_opts = "".join(
|
|
566
|
-
f'<option value="{html.escape(value)}"
|
|
599
|
+
f'<option value="{html.escape(value)}"'
|
|
600
|
+
f'{" selected" if value == matched else ""}>'
|
|
567
601
|
f"{_inline(label)}</option>"
|
|
568
602
|
for value, label in opts
|
|
569
603
|
)
|
|
604
|
+
other_attr = " selected" if use_other else ""
|
|
605
|
+
other_hidden = "" if use_other else " hidden"
|
|
570
606
|
select_html = (
|
|
571
607
|
f'<select name="{rid}" data-response-id="{rid}" '
|
|
572
608
|
f'data-kind="{html.escape(kind)}"{disabled}>'
|
|
573
609
|
'<option value="">(선택)</option>'
|
|
574
610
|
f"{select_opts}"
|
|
575
|
-
'<option value="__other__">기타 (직접 입력)</option>'
|
|
611
|
+
f'<option value="__other__"{other_attr}>기타 (직접 입력)</option>'
|
|
576
612
|
"</select>"
|
|
577
613
|
)
|
|
578
614
|
other_html = (
|
|
579
615
|
f' <textarea data-other-for="{rid}" '
|
|
580
|
-
f'placeholder="기타 응답" rows="2"{disabled}
|
|
616
|
+
f'placeholder="기타 응답" rows="2"{disabled}{other_hidden}>'
|
|
581
617
|
f"{safe_value}</textarea>"
|
|
582
618
|
)
|
|
583
619
|
return select_html + other_html
|
|
@@ -197,6 +197,16 @@ button[data-action]:hover { background: color-mix(in srgb, Highlight 20%, Button
|
|
|
197
197
|
}
|
|
198
198
|
#user-response-output:empty { display: none; }
|
|
199
199
|
|
|
200
|
+
.user-response-dismiss {
|
|
201
|
+
position: absolute;
|
|
202
|
+
top: 0.35em;
|
|
203
|
+
right: 0.5em;
|
|
204
|
+
padding: 0 0.45em;
|
|
205
|
+
line-height: 1.5;
|
|
206
|
+
font-size: 1.05rem;
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
}
|
|
209
|
+
|
|
200
210
|
#plan-approval {
|
|
201
211
|
max-width: 120ch;
|
|
202
212
|
margin: 1.5rem auto;
|
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
var md = buildUserResponseMarkdown(runMeta, entries, isoNowUtc(), approval);
|
|
188
188
|
var out = document.getElementById("user-response-output");
|
|
189
189
|
if (out) out.textContent = md;
|
|
190
|
+
showDismiss(true);
|
|
190
191
|
// Nothing answered yet — show the empty serialisation as feedback but
|
|
191
192
|
// don't download a useless file.
|
|
192
193
|
if (entries.length > 0 || approval) downloadUserResponse(md, runMeta);
|
|
@@ -196,6 +197,7 @@
|
|
|
196
197
|
function copyUserResponse() {
|
|
197
198
|
var out = document.getElementById("user-response-output");
|
|
198
199
|
if (!out || !out.textContent) return;
|
|
200
|
+
showDismiss(true);
|
|
199
201
|
var text = out.textContent;
|
|
200
202
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
201
203
|
navigator.clipboard.writeText(text).catch(function () {
|
|
@@ -217,6 +219,17 @@
|
|
|
217
219
|
document.body.removeChild(ta);
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
function showDismiss(show) {
|
|
223
|
+
var btn = document.querySelector('button[data-action="dismiss-user-response"]');
|
|
224
|
+
if (btn) btn.hidden = !show;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function dismissUserResponse() {
|
|
228
|
+
var out = document.getElementById("user-response-output");
|
|
229
|
+
if (out) out.textContent = "";
|
|
230
|
+
showDismiss(false);
|
|
231
|
+
}
|
|
232
|
+
|
|
220
233
|
function bind() {
|
|
221
234
|
var clickables = document.querySelectorAll("button[data-action]");
|
|
222
235
|
for (var i = 0; i < clickables.length; i++) {
|
|
@@ -226,7 +239,10 @@
|
|
|
226
239
|
btn.addEventListener("click", exportUserResponse);
|
|
227
240
|
} else if (action === "copy-user-response") {
|
|
228
241
|
btn.addEventListener("click", copyUserResponse);
|
|
242
|
+
} else if (action === "dismiss-user-response") {
|
|
243
|
+
btn.addEventListener("click", dismissUserResponse);
|
|
229
244
|
}
|
|
245
|
+
|
|
230
246
|
}
|
|
231
247
|
bindOtherInputToggle();
|
|
232
248
|
}
|