okstra 0.29.0 → 0.30.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 +4 -0
- package/README.md +4 -0
- package/docs/kr/architecture.md +74 -13
- package/docs/kr/cli.md +6 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_common-contract.md +9 -0
- package/runtime/python/okstra_ctl/wizard.py +220 -14
- package/runtime/python/okstra_ctl/worktree.py +32 -0
- package/runtime/skills/okstra-brief/SKILL.md +73 -35
package/README.kr.md
CHANGED
|
@@ -185,6 +185,9 @@ Claude Code 세션 밖에서 task 를 시작하려면:
|
|
|
185
185
|
- **PR 본문 템플릿 설정** (release-handoff) — PR 본문은 마크다운 템플릿에서 채워집니다. 해석 우선순위: 1회성 override (`--pr-template-path` 또는 okstra-run Step 6 prompt) → `<project_root>/.project-docs/okstra/project.json` 의 `prTemplatePath` → `~/.okstra/config.json` 의 `prTemplatePath` → 스킬 디폴트 `~/.claude/skills/okstra-run/templates/pr-body.template.md`. 템플릿 등록 명령: `okstra config set pr-template-path <path> [--scope project|global]` (project 스코프는 project root 기준 상대경로 허용, global 스코프는 절대경로 또는 `~/` 시작 경로만 허용). 현재 설정 확인: `okstra config get pr-template-path --scope all` 은 각 스코프 값 + 실제로 우승하는 경로(effective) 까지 보여줍니다. 디폴트 템플릿은 `## Summary` / `## Changes` / `## Test plan` / `## Linked issues` 4 섹션 + HTML 주석으로 lead 작성 가이드를 포함하며, PR 생성 직전에 lead 가 주석을 제거합니다.
|
|
186
186
|
- **프로파일 워커 로스터 검증** — `--workers <csv>` 와 okstra-run Step 6 의 워커 prompt 는 해당 프로파일의 `Required workers:` 블록에 선언된 워커 ID 만 허용합니다. 프로파일에 없는 워커 (예: `release-handoff` 에서 `codex` / `gemini`) 를 요청하면 명확한 에러로 거절되고, 인터랙티브 prompt 도 프로파일이 실제로 받는 워커만 보여줍니다.
|
|
187
187
|
- **Phase 6 plan-body verification (implementation-planning 전용)** — Report writer worker 가 final-report draft 를 작성한 직후, User Approval gate 직전에 lead 가 1 라운드의 사후 검증을 추가로 돌립니다. 합성된 `## 4.5` plan 본문에서 `P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` plan-item 을 추출해 모든 analyser 워커에게 `AGREE` / `DISAGREE(a-e)` / `SUPPLEMENT` 평결을 요청합니다. 집계된 gate 결과는 `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result` 중 하나. 앞 둘은 final-report 상단의 `- [ ] Approved` 마커를 렌더하고, 뒤 둘은 `majority-disagree` 항목을 `## 5. Clarification Items` 의 `Blocks=approval` row 로 변환합니다. 빠른 반복용 opt-out: `--no-plan-verification` (기본값: 활성). 자세한 라운드 프로토콜은 [`skills/okstra-convergence/SKILL.md`](skills/okstra-convergence/SKILL.md) 의 "Plan-body verification mode" 섹션과 [`docs/kr/cli.md#--no-plan-verification`](docs/kr/cli.md#--no-plan-verification).
|
|
188
|
+
- **Brief = translation layer + Step 6.5 reporter batch confirmation** — `okstra-brief` 가 외부 입력 (이슈 ticket, 요구사항 문서, 사용자 메시지) 을 verbatim 으로 옮기되 okstra 가 추가한 부분은 labelled augmentation 으로 구분하는 translation layer 가 됐습니다. Step 6.5 가 brief 가 옮기는 과정에서 의미 변화가 발생했는지 사용자에게 일괄 확인받아 `Reporter Confirmations` 섹션에 기록하고, 모든 분석 profile 은 이 섹션의 존재를 phase 분석 진입 precondition 으로 강제합니다 (validator: `validators/validate-brief.py`).
|
|
189
|
+
- **Artifact-home rule (`.project-docs/okstra/`)** — okstra 가 사용자 프로젝트에 쓰는 모든 파일은 `<project>/.project-docs/okstra/` subtree 안에만 위치합니다. 외부 경로 (`CONTEXT.md`, `docs/adr/`, `.scratch/`, 소스 코드) 는 read-only reference 이며 부재해도 정상 상태입니다. okstra-internal 등가물: 용어집 `glossary.md`, 결정 기록 `decisions/<NNNN>-<slug>.md` (`implementation-planning` phase 에서 평가).
|
|
190
|
+
- **Dual-format final-report views** — Phase 7 가 `final-report-<task-type>-<seq>.md` 를 쓰면 `okstra render-views` 가 같은 `reports/` 폴더에 두 view 를 자동 생성합니다: AI 다음-phase 입력용 슬림 markdown, 사람 reviewer 용 self-contained HTML (CSS/JS 인라인, 외부 URL 0). HTML 의 `Export user response` 버튼은 `## 5. Clarification Items` 입력을 `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` 사이드카로 직렬화해 다음 phase 가 소비합니다. 원본 MD 는 어떤 경우에도 view 생성으로 인해 수정되지 않습니다.
|
|
188
191
|
|
|
189
192
|
### 3.5 운영 명령
|
|
190
193
|
|
|
@@ -196,6 +199,7 @@ Claude Code 세션 밖에서 task 를 시작하려면:
|
|
|
196
199
|
| `npx -y okstra@latest setup --project-id <id>` | 현재 프로젝트를 등록 (`.project-docs/okstra/project.json`) |
|
|
197
200
|
| `npx -y okstra@latest check-project` | 현재 프로젝트가 `setup` 으로 등록됐는지 검증 |
|
|
198
201
|
| `npx -y okstra@latest config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | okstra 설정 읽기/쓰기. 현재 지원 키: `pr-template-path` (project.json 또는 `~/.okstra/config.json` 의 `prTemplatePath` 갱신) |
|
|
202
|
+
| `npx -y okstra@latest render-views <final-report.md>` | final-report MD 한 본을 입력으로 슬림 MD + HTML 두 view 를 (재)생성 (Phase 7 step 1.5; 멱등) |
|
|
199
203
|
| `npx -y okstra@latest uninstall` | 런타임 + 스킬 제거; 사용자 데이터(`recent.jsonl`, `projects/`, …)는 보존 |
|
|
200
204
|
| `npx -y okstra@latest uninstall --purge -y` | 사용자 데이터까지 모두 제거 |
|
|
201
205
|
|
package/README.md
CHANGED
|
@@ -184,6 +184,9 @@ Recent workflow additions (post-0.8.0, on `main`):
|
|
|
184
184
|
- **Configurable PR body template** (release-handoff) — the PR body is filled from a markdown template chosen in priority order: per-run override (`--pr-template-path` or the okstra-run Step 6 prompt) → `<project_root>/.project-docs/okstra/project.json` `prTemplatePath` → `~/.okstra/config.json` `prTemplatePath` → bundled skill default at `~/.claude/skills/okstra-run/templates/pr-body.template.md`. Register a template with `okstra config set pr-template-path <path> [--scope project|global]` (project scope accepts paths relative to the project root; global scope requires absolute or `~/`-prefixed). `okstra config get pr-template-path --scope all` reports every scope plus the effective winner. The bundled default ships `## Summary` / `## Changes` / `## Test plan` / `## Linked issues` with HTML comment guidance that the lead strips before opening the PR.
|
|
185
185
|
- **Profile-roster worker validation** — `--workers <csv>` (and the okstra-run Step 6 worker prompt) are now restricted to the worker IDs declared by the chosen profile's `Required workers:` block. Asking for `codex` / `gemini` on a profile that does not list them (e.g. `release-handoff`) is rejected with a clear error, and the interactive prompt only offers workers the profile actually accepts.
|
|
186
186
|
- **Phase 6 plan-body verification (implementation-planning only)** — after the Report writer worker authors the final-report draft and before the User Approval gate, the lead now runs one additional verification round: it extracts `P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` items from the consolidated `## 4.5` plan body and dispatches them to every analyser worker as `AGREE` / `DISAGREE(a-e)` / `SUPPLEMENT`. The aggregated gate result is one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`; only the first two render the top-of-report `- [ ] Approved` marker, the other two convert `majority-disagree` items into `## 5. Clarification Items` rows with `Blocks=approval`. Disable for fast iteration with `--no-plan-verification` (default: enabled). Details: [`skills/okstra-convergence/SKILL.md`](skills/okstra-convergence/SKILL.md) "Plan-body verification mode" and [`docs/kr/cli.md#--no-plan-verification`](docs/kr/cli.md#--no-plan-verification).
|
|
187
|
+
- **Brief as translation layer + reporter batch confirmation (Step 6.5)** — `okstra-brief` now produces the brief as a translation layer that preserves external inputs (issue ticket, requirements doc, user message) verbatim while labelling okstra augmentations. A new Step 6.5 walks the user through a single batch confirmation of any rewordings, recorded in a `Reporter Confirmations` section. Every analysis profile blocks phase entry until this section exists (validator: `validators/validate-brief.py`).
|
|
188
|
+
- **Artifact-home rule (`.project-docs/okstra/`)** — okstra writes only inside `<project>/.project-docs/okstra/`. External paths (`CONTEXT.md`, `docs/adr/`, `.scratch/`, source code) are read-only references; their absence is normal. okstra-internal equivalents: `glossary.md` for terminology and `decisions/<NNNN>-<slug>.md` for decision records (evaluated in `implementation-planning`).
|
|
189
|
+
- **Dual-format final-report views** — after Phase 7 writes `final-report-<task-type>-<seq>.md`, `okstra render-views` automatically emits two sibling views in the same `reports/` directory: a slim Markdown for downstream AI input and a self-contained HTML (inline CSS/JS, no external URLs) for human review. The HTML lets a reviewer fill in `## 5. Clarification Items` decisions and export them to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`, which the next phase consumes as input. The original MD is never modified by view generation.
|
|
187
190
|
|
|
188
191
|
### 3.5 Ops commands
|
|
189
192
|
|
|
@@ -195,6 +198,7 @@ Recent workflow additions (post-0.8.0, on `main`):
|
|
|
195
198
|
| `npx -y okstra@latest setup --project-id <id>` | Register the current project (`.project-docs/okstra/project.json`) |
|
|
196
199
|
| `npx -y okstra@latest check-project` | Verify the current project has been registered with `setup` |
|
|
197
200
|
| `npx -y okstra@latest config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | Read / write okstra settings; initial key `pr-template-path` (writes `prTemplatePath` to project.json or `~/.okstra/config.json`) |
|
|
201
|
+
| `npx -y okstra@latest render-views <final-report.md>` | Regenerate the slim-MD + HTML sibling views from a final-report MD (Phase 7 step 1.5; idempotent) |
|
|
198
202
|
| `npx -y okstra@latest uninstall` | Remove runtime + skills; preserves user data (`recent.jsonl`, `projects/`, …) |
|
|
199
203
|
| `npx -y okstra@latest uninstall --purge -y` | Remove everything including user data |
|
|
200
204
|
|
package/docs/kr/architecture.md
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
- [Required team contract](#required-team-contract)
|
|
36
36
|
- [Stable task identity](#stable-task-identity)
|
|
37
37
|
- [Project self-registration](#project-self-registration)
|
|
38
|
+
- [Artifact-home rule](#artifact-home-rule)
|
|
38
39
|
- [Task type](#task-type)
|
|
39
40
|
- [표준 task type](#표준-task-type)
|
|
40
41
|
- [Phase 간 정보 전달](#phase-간-정보-전달)
|
|
@@ -62,9 +63,11 @@
|
|
|
62
63
|
- [8. 구현 후 최종 검토](#8-구현-후-최종-검토)
|
|
63
64
|
- [9. 같은 task 재개](#9-같은-task-재개)
|
|
64
65
|
- [Lifecycle status and resume](#lifecycle-status-and-resume)
|
|
65
|
-
- [
|
|
66
|
+
- [Final report structure](#final-report-structure)
|
|
67
|
+
- [Final report views (slim MD + HTML)](#final-report-views-slim-md--html)
|
|
66
68
|
- [Worker error collection (optional sidecar)](#worker-error-collection-optional-sidecar)
|
|
67
69
|
- [Token usage and cost accounting](#token-usage-and-cost-accounting)
|
|
70
|
+
- [Validators](#validators)
|
|
68
71
|
- [Practical notes](#practical-notes)
|
|
69
72
|
- [Related documents](#related-documents)
|
|
70
73
|
|
|
@@ -307,6 +310,19 @@ Claude launch prompt 본문은 항상 `prompts/launch.template.md` 템플릿에
|
|
|
307
310
|
|
|
308
311
|
`okstra-ctl` 의 reindex/backfill 도 신규 모델에서 권위 소스를 변경했습니다. 과거에는 `examples/projects/*.conf.sh` 를 source 했지만, 지금은 `~/.okstra/projects/<projectId>/meta.json` (record_start 가 위 project.json 정보를 mirror 한 결과) 을 스캔하여 (projectId, projectRoot) 매핑을 복원합니다. `OKSTRA_PROJECT_DEFINITION_DIR_OVERRIDE` 환경변수도 함께 폐기되었습니다.
|
|
309
312
|
|
|
313
|
+
## Artifact-home rule
|
|
314
|
+
|
|
315
|
+
okstra 가 사용자 프로젝트에 생성·수정·삭제하는 모든 파일은 `<PROJECT_ROOT>/.project-docs/okstra/` subtree 안에만 위치합니다. 외부 경로 (예: `<PROJECT_ROOT>/CONTEXT.md`, `<PROJECT_ROOT>/docs/adr/`, `<PROJECT_ROOT>/.scratch/`, 소스 코드) 는 **read-only reference** 입니다. okstra phase 는 그 파일들이 존재하면 읽을 수 있지만 부재해도 정상 상태로 간주하며, 자체 판단으로 외부 경로에 쓰지 않습니다.
|
|
316
|
+
|
|
317
|
+
유일한 예외: brief 의 `Source Material` 또는 `Reporter Confirmations` 섹션에서 사용자가 **verbatim** 으로 특정 외부 파일 편집을 요청한 경우. 해당 편집을 수행하는 phase 는 자신의 final-report 에 사용자 원문 인용을 함께 남겨야 합니다.
|
|
318
|
+
|
|
319
|
+
okstra 는 자기 subtree 안에 자체 institutional memory 를 유지합니다.
|
|
320
|
+
|
|
321
|
+
- `<PROJECT_ROOT>/.project-docs/okstra/glossary.md` — run 을 가로지르며 누적되는 okstra 용어집. 외부 `CONTEXT.md` 류 skill 의 기능을 흡수합니다.
|
|
322
|
+
- `<PROJECT_ROOT>/.project-docs/okstra/decisions/<NNNN>-<slug>.md` — okstra 의 결정 기록. 외부 ADR 시스템의 기능을 흡수합니다. 평가 시점은 `implementation-planning` phase 이며 `okstra-brief` 단계에서는 후보만 표시합니다.
|
|
323
|
+
|
|
324
|
+
okstra phase 는 PRD / issue file 을 직접 쓰지 않습니다. 동등한 결정 산출물은 `requirements-discovery` 와 `implementation-planning` 이 `.project-docs/okstra/` 내부에 만듭니다.
|
|
325
|
+
|
|
310
326
|
## Task type
|
|
311
327
|
|
|
312
328
|
`task-type`은 이번 run의 목적과 profile 선택, 그리고 lifecycle phase 라우팅을 동시에 결정합니다.
|
|
@@ -417,9 +433,14 @@ task manifest, task index, instruction-set, runs, history가 이 루트 아래
|
|
|
417
433
|
그리고 `--render-only`가 아니면 handoff된 Claude session이 보통 아래 결과 파일을 현재 run에 추가합니다.
|
|
418
434
|
- `sessions/claude-resume-<task-type>-<seq>.sh`
|
|
419
435
|
- `reports/final-report-<task-type>-<seq>.md`
|
|
436
|
+
- `reports/final-report-<task-type>-<seq>.slim.md` *(Phase 7 결정론적 후처리: AI 다음-phase 입력용 슬림 markdown)*
|
|
437
|
+
- `reports/final-report-<task-type>-<seq>.html` *(Phase 7 결정론적 후처리: 사람 reviewer 용 self-contained HTML, CSS/JS 인라인)*
|
|
438
|
+
- `user-responses/user-response-<task-type>-<seq>.md` *(HTML 의 `Export user response` 버튼이 생성하는 사이드카; 사용자가 채워 저장하면 다음 phase 가 입력으로 소비)*
|
|
439
|
+
- `worker-results/<worker>-audit-<task-type>-<seq>.md` *(워커별 Reading Confirmation 사이드카; 본문이 아니라 audit 용)*
|
|
420
440
|
- `status/final-<task-type>-<seq>.status`
|
|
421
|
-
최종 결과 파일
|
|
441
|
+
최종 결과 파일 (`final-report` MD / status) 은 `okstra`가 stdout을 저장해서 만드는 파일이 아닙니다.
|
|
422
442
|
`okstra`가 준비한 task bundle을 바탕으로 Claude가 현재 run 안에 직접 작성하는 결과물입니다.
|
|
443
|
+
slim MD / HTML 두 view 는 `okstra render-views <final-report.md>` (Phase 7 step 1.5) 가 final-report MD 한 본을 입력으로 결정론적으로 생성합니다. 원본 MD 는 view 생성으로 인해 수정되지 않습니다.
|
|
423
444
|
반면 `sessions/claude-resume-<task-type>-<seq>.sh`는 `okstra`가 Claude launch 전에 미리 생성하는 interruption recovery helper입니다.
|
|
424
445
|
|
|
425
446
|
run directory는 task-type 단위로 task 실행 이력을 모으고, 내부를 `manifests/`, `state/`, `prompts/`, `reports/`, `status/`, `sessions/`, `worker-results/`처럼 유형별 하위 폴더로 나눈 뒤 각 run-level artifact와 result 파일을 `-<task-type>-<seq>` suffix(per-category 3-digit zero-padded counter, 예: `001`, `002`)로 구분합니다.
|
|
@@ -611,6 +632,8 @@ canonical metadata는 항상 `task-manifest.json`을 기준으로 확인합니
|
|
|
611
632
|
|
|
612
633
|
`okstra`는 brief-first 구조입니다. brief 가 분석의 정본 입력이며, 워커가 필요로 할 추가 자료(보고서, 코드 스니펫, 로그 등)는 brief 내부의 `Evidence and Source Materials` 섹션에 inline 또는 path 로 모두 포함시킵니다.
|
|
613
634
|
|
|
635
|
+
brief 는 **translation layer** 입니다 — 외부 입력 (이슈 트래커 ticket, 요구사항 문서, 사용자 메시지) 을 okstra-readable 형식으로 옮기되, 원문은 verbatim 으로 보존하고 okstra 가 추가한 부분은 labelled augmentation 으로 명확히 구분합니다. okstra-brief skill 의 산출이 정식 SSOT 이며, 분석 워커들이 phase 시작 전에 일률적으로 읽어들이는 단일 입력입니다.
|
|
636
|
+
|
|
614
637
|
brief에는 보통 아래를 포함합니다.
|
|
615
638
|
|
|
616
639
|
- 문제 설명
|
|
@@ -622,6 +645,10 @@ brief에는 보통 아래를 포함합니다.
|
|
|
622
645
|
- worker에게 줄 질문
|
|
623
646
|
- 기대 출력
|
|
624
647
|
- 이전 run 또는 연관 task 정보
|
|
648
|
+
- (선택) glossary 추가 후보 — okstra-brief Step 4.5 가 `<PROJECT_ROOT>/.project-docs/okstra/glossary.md` 에 직접 기록
|
|
649
|
+
- (선택) decisions 후보 — `implementation-planning` phase 에서 평가 후 `.project-docs/okstra/decisions/<NNNN>-<slug>.md` 로 승격
|
|
650
|
+
|
|
651
|
+
okstra-brief 의 Step 6.5 는 **reporter batch confirmation** 입니다. brief 가 외부 입력을 옮기는 도중 의미 변화가 발생했는지를 사용자에게 일괄 확인받고 그 결과를 `Reporter Confirmations` 섹션에 기록합니다. 모든 분석 profile 은 이 섹션의 존재를 phase 분석 진입의 precondition 으로 강제합니다 (validator: `validators/validate-brief.py`).
|
|
625
652
|
|
|
626
653
|
기본 템플릿:
|
|
627
654
|
|
|
@@ -796,22 +823,42 @@ resume 판단 기준:
|
|
|
796
823
|
- 다음 phase 시작: `workflow.nextRecommendedPhase`가 구체적인 phase면 그 값으로 다음 `okstra.sh` 실행을 준비합니다.
|
|
797
824
|
- 추가 자료 필요: `routingStatus=pending` 또는 `nextRecommendedPhase=pending-routing-decision`이면 brief 보강이 먼저입니다.
|
|
798
825
|
|
|
799
|
-
##
|
|
826
|
+
## Final report structure
|
|
800
827
|
|
|
801
828
|
기본 최종 보고서 템플릿은 `templates/reports/final-report.template.md`입니다.
|
|
802
|
-
Claude가 작성하는 최종 보고서는 brief
|
|
829
|
+
Claude가 작성하는 최종 보고서는 아래 구조를 우선 사용합니다 (brief 의 augmentation 이 더 구체적인 형식을 요구할 때만 그것을 따릅니다).
|
|
803
830
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
831
|
+
- `## Verdict Card` — **최상단 의무 섹션**. Final Conclusion / Verdict Token / Direction / Approval Required? / Next Step 5 행. Verdict Token / Direction / Next Step 셀은 본문 §2 (실행 현황) 와 §6 (다음 단계) 의 권위 셀과 byte-match 해야 합니다.
|
|
832
|
+
- (선택) `## 0. Clarification Response Carried In From Previous Run` — 직전 run 에서 응답이 carry-in 된 경우에만 렌더링. 빈 carry-in 일 때는 헤딩 자체를 출력하지 않습니다.
|
|
833
|
+
- `## 1. 문제 또는 검증 대상 요약` — §1.1 Consensus / §1.2 Differences 표 각각 `Source items (worker:item)` 컬럼 보존 (cross-worker traceability).
|
|
834
|
+
- `## 2. 에이전트별 실행 현황`
|
|
835
|
+
- `## 3. Cross Verification 결과` — §3.1 Primary Evidence 에 `Source items (worker:item)` + `Source (path:line / log)` 컬럼.
|
|
836
|
+
- `## 4. 최종 판단` — `implementation-planning` 의 §4.5.9 Plan Body Verification 은 두 표 (`Verdict summary` 4-열, `Verdict details` 5-열, plan item × worker) 로 분할 emit.
|
|
837
|
+
- `## 5. Clarification Items` — 통합 8-열 표 한 곳. 기존 §5.1 / §5.2 / §4.5.8 / §4.5.9 Open Questions 는 deprecated 되어 validator 가 등장 시 fail.
|
|
838
|
+
- `## 6. 권장 다음 단계`
|
|
839
|
+
- `## Token Usage Summary` — sentinel (`pending` / `N/A` / `--` / `?` / 빈 셀) 또는 zero (`0` / `$0.00`) 박제 시 validator 가 출고를 차단합니다. `Codex/Gemini CLI 추가 비용` 행만 "CLI 미사용" 의미로 `$0.00` 허용.
|
|
840
|
+
|
|
841
|
+
워커 출력의 `## 0. Reading Confirmation` 블록은 본문에 두지 않고 `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md` 사이드카에 작성합니다 (validator 강제).
|
|
811
842
|
|
|
812
843
|
차이점이 실질적으로 없으면 억지로 대비를 만들지 말고, 차이가 없음을 명시합니다.
|
|
813
844
|
저장 실패나 세션 제한에 대한 메타 설명 대신 실제 Markdown 보고서 본문을 파일에 작성해야 합니다.
|
|
814
845
|
|
|
846
|
+
## Final report views (slim MD + HTML)
|
|
847
|
+
|
|
848
|
+
Phase 7 step 1.5 가 final-report MD 한 본을 입력으로 두 view 를 결정론적으로 자동 생성합니다.
|
|
849
|
+
|
|
850
|
+
- `reports/final-report-<task-type>-<seq>.slim.md` — AI 다음-phase 입력용. 장식·캡션·sentinel 셀을 제거한 토큰-경제 버전. `validate-run.py` 의 phase substring 검사를 byte-identical 로 통과합니다.
|
|
851
|
+
- `reports/final-report-<task-type>-<seq>.html` — 사람 reviewer 용 self-contained HTML. CSS / JS 인라인 임베드 (외부 URL 0), system color 다크모드, sticky header, 인쇄 대응. §5 `C-*` 행의 의사결정 입력 (체크박스 / 셀렉트 / textarea) 을 화면에서 채우고 `Export user response` 버튼으로 사이드카 markdown 을 생성합니다.
|
|
852
|
+
|
|
853
|
+
진입점:
|
|
854
|
+
|
|
855
|
+
- Python 단일 reference: `scripts/okstra_ctl/report_views.py` (`slim_markdown(...)`, `render_html(..., css, js)`, `serialize_user_response(...)`). HTML 내 JS `buildUserResponseMarkdown` 은 Python `serialize_user_response` 와 **byte-identical** (Node `vm.runInThisContext` 단위 테스트로 자동 검증).
|
|
856
|
+
- CLI: `scripts/okstra-render-report-views.py <final-report.md>` 또는 Node 위임 wrapper `bin/okstra render-views <md>`.
|
|
857
|
+
- 검증: `validators/validate-report-views.py` — slim 의 phase substring 보존, HTML 내 form control 위치, 외부 URL 부재, Response ID parity (`C-*` ↔ HTML), 두 모듈 substring 상수 drift 모두 검사.
|
|
858
|
+
- 사용자 응답 사이드카 스키마 SSOT: `templates/reports/user-response.template.md`.
|
|
859
|
+
|
|
860
|
+
원본 final-report MD 는 어떤 경우에도 view 생성으로 인해 수정되지 않습니다.
|
|
861
|
+
|
|
815
862
|
## Worker error collection (optional sidecar)
|
|
816
863
|
|
|
817
864
|
워커(Claude/Codex/Gemini worker, Report writer, Claude lead) 실행 중 발생한 에러를 단일 시계열 로그로 수집해 사후 회고에 사용할 수 있습니다.
|
|
@@ -854,8 +901,19 @@ Claude가 작성하는 최종 보고서는 brief에 더 구체적인 형식이
|
|
|
854
901
|
- Claude lead/workers: `~/.claude/projects/<cwd-as-dashes>/<sessionId>.jsonl`의 per-message `message.usage`
|
|
855
902
|
- Codex CLI: `~/.codex/sessions/Y/M/D/rollout-*.jsonl`의 마지막 `total_token_usage.total_tokens`
|
|
856
903
|
- Gemini CLI: `~/.gemini/tmp/*/chats/session-*.json`의 per-message `tokens.total`
|
|
857
|
-
- billable-equivalent token math와 USD cost estimation을 함께 기록합니다. Anthropic billing ratio(`
|
|
858
|
-
- 가격표는
|
|
904
|
+
- billable-equivalent token math와 USD cost estimation을 함께 기록합니다. Anthropic billing ratio(`cache_creation_5m=1.25x`, `cache_creation_1h=2.0x`, `cache_read=0.1x`, `output=5x`)를 반영합니다. transcript 의 `usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens` 분해가 있으면 분리 집계합니다.
|
|
905
|
+
- 가격표는 `scripts/okstra_token_usage/pricing.py` 에서 중앙 관리합니다. 모델 가격이 바뀌면 거기서 갱신합니다. 가격 매칭에 실패한 모델 id 는 `usageSummary.unmatchedModels` 필드로 사용자에게 노출됩니다 (silent zero 사고 방지).
|
|
906
|
+
|
|
907
|
+
## Validators
|
|
908
|
+
|
|
909
|
+
phase 산출물의 출고 가능 여부를 강제하는 진입점:
|
|
910
|
+
|
|
911
|
+
- `validators/validate-workflow.sh` — phase contract 통합 검증.
|
|
912
|
+
- `validators/validate-run.py` — run-level final-report 본문 contract (Verdict Card 존재, deprecated §5.1/§5.2/§4.5.8/§4.5.9 Open Questions 부재, Plan Body Verification gate × Approval 마커 cross-check, Token Usage sentinel/zero 차단, 워커-결과 audit 사이드카 존재).
|
|
913
|
+
- `validators/validate-report-views.py` — slim MD / HTML view 의 phase substring 보존 및 form-control 영역 검사.
|
|
914
|
+
- `validators/validate-brief.py` — brief schema (front-matter, `Reporter Confirmations` 섹션 존재, root parent-id self 규칙, slug 컨벤션 등) 강제. `bash validators/validate-brief.sh <brief.md>` 가 thin wrapper.
|
|
915
|
+
|
|
916
|
+
각 validator 는 contract 위반 시 `contract-violated` exit code 로 phase 를 차단합니다. 위반은 다음 phase 실행 시점에 적용되므로 이전 산출물은 그대로 둡니다.
|
|
859
917
|
|
|
860
918
|
## Practical notes
|
|
861
919
|
|
|
@@ -883,6 +941,9 @@ Claude가 작성하는 최종 보고서는 brief에 더 구체적인 형식이
|
|
|
883
941
|
- 워커 에러는 옵션 sidecar `runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl`로 수집되며 lead가 단독 writer입니다. 진입점 helper는 `scripts/okstra-error-log.py`입니다.
|
|
884
942
|
- 토큰 사용 및 비용 집계는 `scripts/okstra-token-usage.py`가 담당하며.
|
|
885
943
|
- `okstra.sh`는 worker CLI 호출 anchoring을 위해 절대 projectRoot를 강제합니다.
|
|
944
|
+
- `okstra wizard step` 은 `--answer <val>` 을 **필수** 로 받습니다. 응답을 줄 차례가 아니라 다음 prompt 만 미리 보고 싶다면 `--no-submit` 으로 peek 합니다.
|
|
945
|
+
- `okstra history` 는 manifest fallback / 페이지네이션 / 필터를 지원하며, `--base-ref` 는 워크트리 registry 에서 해석합니다.
|
|
946
|
+
- 사용자 프로젝트에 대한 모든 쓰기는 `<PROJECT_ROOT>/.project-docs/okstra/` 안에만 발생합니다 (Artifact-home rule 참조).
|
|
886
947
|
|
|
887
948
|
## Related documents
|
|
888
949
|
|
package/docs/kr/cli.md
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
- [`--work-category`](#--work-category)
|
|
33
33
|
- [`--related-tasks`](#--related-tasks)
|
|
34
34
|
- [`--render-only`](#--render-only)
|
|
35
|
+
- [`--no-plan-verification`](#--no-plan-verification)
|
|
35
36
|
- [Interactive input flow](#interactive-input-flow)
|
|
36
37
|
- [Confirmation flow](#confirmation-flow)
|
|
37
38
|
- [okstra Control Center — 설치 / 자주 쓰는 명령](#okstra-control-center--설치--자주-쓰는-명령)
|
|
@@ -517,6 +518,7 @@ chmod +x ~/.local/bin/okstra-ctl
|
|
|
517
518
|
| 진행 중 run 보기 | `okstra-ctl tail active` |
|
|
518
519
|
| 단일 run 결과 메타 | `okstra-ctl show <runId-or-prefix>` |
|
|
519
520
|
| 결과 보고서 경로 | `okstra-ctl open <runId-or-prefix>` |
|
|
521
|
+
| final-report 두 view 재생성 | `okstra render-views <final-report.md>` |
|
|
520
522
|
| 단일 재실행 | `okstra-ctl rerun <runId-or-prefix> --yes` |
|
|
521
523
|
| 다중 재실행 | `okstra-ctl rerun --filter --project X --status failed --yes` |
|
|
522
524
|
| 가장 최근 재실행 | `okstra-ctl rerun last --project X --task-group Y --yes` |
|
|
@@ -535,7 +537,10 @@ chmod +x ~/.local/bin/okstra-ctl
|
|
|
535
537
|
| `okstra worktree-lookup <task-key>` | `worktree_registry.lookup` 결과 (예약된 path / branch / base ref / 현재 상태) |
|
|
536
538
|
| `okstra plan-validate <plan-path>` | `_validate_approved_plan` — approval marker 인식 결과와 sanitization 후 diff |
|
|
537
539
|
| `okstra render-bundle <args…>` | `prepare_task_bundle(render_only=True)` 의 thin shim — `python3 -m okstra_ctl.run --render-only` 와 동일 시그니처 |
|
|
538
|
-
| `okstra
|
|
540
|
+
| `okstra render-views <final-report.md>` | Phase 7 step 1.5 — 토큰 치환된 final-report MD 한 본을 입력으로 sibling `*.slim.md` (AI 입력용) + `*.html` (사람용 self-contained) 두 view 를 결정론적으로 생성. 원본 MD 는 수정하지 않음. Node 위임 wrapper는 `scripts/okstra-render-report-views.py` 를 호출. `validators/validate-report-views.py` 가 substring 보존 / form-control 위치 / Response ID parity 를 검사 |
|
|
541
|
+
| `okstra wizard <init\|step\|render-args\|confirmation> --state-file <path>` | okstra-run 인터랙티브 입력 상태머신 (`okstra_ctl.wizard`). `init` 으로 state file 을 시드한 뒤 skill 이 `step --answer <val>` 을 반복 호출하면 다음 `Prompt` JSON 을 받음. `--answer` 는 **필수**. 응답을 주지 않고 다음 prompt 만 미리 보고 싶다면 `--no-submit` 으로 peek. `render-args` 는 최종 `render-bundle` 인자 맵, `confirmation` 은 사용자 echo 블록을 반환 |
|
|
542
|
+
| `okstra history [--limit N] [--offset M] [--project <id>] [--status <enum>]` | run history 페이지네이션 / 필터 조회. 중앙 인덱스가 비어 있으면 프로젝트별 task-manifest 들을 스캔해 fallback 으로 채움. `--base-ref` 는 워크트리 registry 에서 해석 |
|
|
543
|
+
| `okstra config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | 영구 설정 관리 (예: `pr-template-path`). 원자적 JSON 쓰기. global scope 의 상대경로는 거절 |
|
|
539
544
|
|
|
540
545
|
> 모든 subcommand 는 `bin/okstra` 가 spawn 하는 python 헬퍼 (`src/_python-helper.mjs`) 가 `PYTHONPATH` 와 `~/.okstra/lib/python` 을 wire 합니다. 직접 `python3 -m okstra_ctl.*` 으로 호출하면 `PYTHONPATH` 를 사용자가 직접 셋업해야 합니다.
|
|
541
546
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -77,3 +77,12 @@ profile document.
|
|
|
77
77
|
- Audit sidecar (shared — applies to every analysis-worker output and every final-report):
|
|
78
78
|
- Reading Confirmation lines (one short line per input file confirming end-to-end reading) live in the **worker audit sidecar** at `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md`, NOT in the worker's main worker-results file. The worker-results body starts at section 1 (Findings). The validator fails worker-results files that contain a `## 0. Reading Confirmation` heading.
|
|
79
79
|
- The audit sidecar carries any other meta the worker wants to log (tool-call counts, MCP query summaries, timing notes). The lead's final-report does NOT duplicate this content — it is consumed by the validator and by post-run audit tooling, not by end-user readers.
|
|
80
|
+
|
|
81
|
+
- Markdown authoring (shared — applies to every markdown document produced by the lead or any worker, including final-reports, worker-results, briefs, and ad-hoc notes):
|
|
82
|
+
- every document must begin with an `Index` section.
|
|
83
|
+
- include only information necessary to fulfill the user's stated purpose and directly related requirements.
|
|
84
|
+
- follow only the sections, format, tone, and scope specified by the user, plus the required `Index` section.
|
|
85
|
+
- when writing task instructions or work orders, define the scope of work clearly and specifically, including deliverables, acceptance criteria, and verification steps when relevant.
|
|
86
|
+
- define scope positively by stating what work is included. Work outside the defined scope must not be performed.
|
|
87
|
+
- before adding any structure or content not explicitly specified, ask the user for confirmation, except for the required `Index` section.
|
|
88
|
+
- before completion, verify that the document exactly matches the requested scope and contains no unrelated material.
|
|
@@ -73,12 +73,93 @@ GEMINI_MODEL_OPTIONS = ["default", "gemini-3-pro-preview", "gemini-3-flash-previ
|
|
|
73
73
|
# special pick value: start a brand-new task
|
|
74
74
|
TASK_PICK_NEW_TOKEN = "__new__"
|
|
75
75
|
|
|
76
|
+
# Pick-vs-free-text tokens shared by suggestion-aware prompts.
|
|
77
|
+
PICK_USE_SUGGESTED = "__use_suggested__"
|
|
78
|
+
PICK_TYPE_CUSTOM = "__free_input__"
|
|
79
|
+
|
|
80
|
+
# Lines of `key: value` we pull from a brief markdown frontmatter. The
|
|
81
|
+
# parser is intentionally lightweight (no yaml dep) and tolerant — a
|
|
82
|
+
# malformed brief returns an empty dict.
|
|
83
|
+
_BRIEF_FRONTMATTER_LINE_RE = re.compile(r"^([a-zA-Z0-9_\-]+)\s*:\s*(.*)$")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_brief_frontmatter(path: Path) -> dict[str, str]:
|
|
87
|
+
"""Read the YAML-style frontmatter at the top of a brief markdown file
|
|
88
|
+
and return a flat ``{key: value}`` map.
|
|
89
|
+
|
|
90
|
+
Returns ``{}`` if the file is unreadable, has no frontmatter, or the
|
|
91
|
+
frontmatter is malformed. Comments (``# ...``) and quoted values are
|
|
92
|
+
stripped. Placeholder values like ``<task-group>`` are kept verbatim;
|
|
93
|
+
callers decide whether to treat them as a real suggestion.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
text = path.read_text(encoding="utf-8")
|
|
97
|
+
except OSError:
|
|
98
|
+
return {}
|
|
99
|
+
if not text.startswith("---"):
|
|
100
|
+
return {}
|
|
101
|
+
lines = text.splitlines()
|
|
102
|
+
if not lines or lines[0].strip() != "---":
|
|
103
|
+
return {}
|
|
104
|
+
out: dict[str, str] = {}
|
|
105
|
+
for line in lines[1:]:
|
|
106
|
+
if line.strip() == "---":
|
|
107
|
+
break
|
|
108
|
+
# strip trailing inline comment
|
|
109
|
+
comment_idx = line.find("#")
|
|
110
|
+
if comment_idx >= 0:
|
|
111
|
+
line = line[:comment_idx]
|
|
112
|
+
m = _BRIEF_FRONTMATTER_LINE_RE.match(line.strip())
|
|
113
|
+
if not m:
|
|
114
|
+
continue
|
|
115
|
+
key, val = m.group(1), m.group(2).strip()
|
|
116
|
+
# strip matching quotes
|
|
117
|
+
if (len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"')):
|
|
118
|
+
val = val[1:-1]
|
|
119
|
+
out[key] = val
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _looks_like_template_placeholder(value: str) -> bool:
|
|
124
|
+
"""Treat ``<task-group>``, ``<...>``, empty strings, and ``self`` as
|
|
125
|
+
non-suggestions. Anything else (a real slug-like value) is honored."""
|
|
126
|
+
v = (value or "").strip()
|
|
127
|
+
if not v:
|
|
128
|
+
return True
|
|
129
|
+
if v.startswith("<") and v.endswith(">"):
|
|
130
|
+
return True
|
|
131
|
+
if v.lower() in ("self", "tbd", "n/a", "na", "none"):
|
|
132
|
+
return True
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _brief_suggestions(path: Path) -> tuple[str, str]:
|
|
137
|
+
"""Return ``(task_group_suggestion, task_id_suggestion)`` extracted from
|
|
138
|
+
the brief's frontmatter, or empty strings when no usable value exists.
|
|
139
|
+
|
|
140
|
+
- ``task_group`` ← frontmatter ``task-group``.
|
|
141
|
+
- ``task_id`` ← frontmatter ``brief-id`` (which matches the
|
|
142
|
+
filename stem in okstra-brief output and is the
|
|
143
|
+
strongest single identifier of the task).
|
|
144
|
+
|
|
145
|
+
A brief without frontmatter, or with placeholder values, yields two
|
|
146
|
+
empty strings — callers fall back to plain-text input.
|
|
147
|
+
"""
|
|
148
|
+
fm = _parse_brief_frontmatter(path)
|
|
149
|
+
tg_raw = fm.get("task-group", "")
|
|
150
|
+
bid_raw = fm.get("brief-id", "")
|
|
151
|
+
tg = "" if _looks_like_template_placeholder(tg_raw) else tg_raw
|
|
152
|
+
tid = "" if _looks_like_template_placeholder(bid_raw) else bid_raw
|
|
153
|
+
return tg, tid
|
|
154
|
+
|
|
76
155
|
|
|
77
156
|
# ---- Step IDs ------------------------------------------------------------
|
|
78
157
|
|
|
79
158
|
S_TASK_PICK = "task_pick"
|
|
80
159
|
S_TASK_GROUP = "task_group"
|
|
160
|
+
S_TASK_GROUP_TEXT = "task_group_text"
|
|
81
161
|
S_TASK_ID = "task_id"
|
|
162
|
+
S_TASK_ID_TEXT = "task_id_text"
|
|
82
163
|
S_TASK_TYPE = "task_type"
|
|
83
164
|
S_BRIEF_KEEP = "brief_keep"
|
|
84
165
|
S_BRIEF_PATH = "brief_path"
|
|
@@ -123,6 +204,13 @@ class WizardState:
|
|
|
123
204
|
task_group: str = ""
|
|
124
205
|
task_id: str = ""
|
|
125
206
|
existing_brief_path: str = ""
|
|
207
|
+
# brief-derived suggestions (new-task flow only; set when brief is
|
|
208
|
+
# accepted, cleared if the user picks "type custom" so the next
|
|
209
|
+
# `_build_*` falls back to plain text input)
|
|
210
|
+
task_group_suggestion: str = ""
|
|
211
|
+
task_id_suggestion: str = ""
|
|
212
|
+
task_group_pending_text: bool = False
|
|
213
|
+
task_id_pending_text: bool = False
|
|
126
214
|
|
|
127
215
|
# task-type + dependents
|
|
128
216
|
task_type: str = ""
|
|
@@ -421,24 +509,98 @@ def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
|
|
|
421
509
|
|
|
422
510
|
|
|
423
511
|
def _build_task_group(state: WizardState) -> Prompt:
|
|
512
|
+
sugg = state.task_group_suggestion
|
|
513
|
+
if sugg:
|
|
514
|
+
return Prompt(
|
|
515
|
+
step=S_TASK_GROUP, kind="pick",
|
|
516
|
+
label=f"Task group? (brief 추천: {sugg})",
|
|
517
|
+
options=[
|
|
518
|
+
_opt(PICK_USE_SUGGESTED, f"brief 값 사용: {sugg}"),
|
|
519
|
+
_opt(PICK_TYPE_CUSTOM, "다른 값 입력"),
|
|
520
|
+
],
|
|
521
|
+
echo_template="task-group: {value}",
|
|
522
|
+
)
|
|
424
523
|
return Prompt(step=S_TASK_GROUP, kind="text",
|
|
425
524
|
label="Task group 을 알려주세요 (예: backend-api, INV-1234, refactor)",
|
|
426
525
|
echo_template="task-group: {value}")
|
|
427
526
|
|
|
428
527
|
|
|
429
528
|
def _submit_task_group(state: WizardState, value: str) -> Optional[str]:
|
|
529
|
+
if state.task_group_suggestion:
|
|
530
|
+
if value == PICK_USE_SUGGESTED:
|
|
531
|
+
state.task_group = _slug_or_die(
|
|
532
|
+
state.task_group_suggestion, "task_group"
|
|
533
|
+
)
|
|
534
|
+
state.task_group_pending_text = False
|
|
535
|
+
return f"task-group: {state.task_group} (brief)"
|
|
536
|
+
if value == PICK_TYPE_CUSTOM:
|
|
537
|
+
state.task_group_pending_text = True
|
|
538
|
+
return f"task-group: (직접 입력)"
|
|
539
|
+
raise WizardError(
|
|
540
|
+
f"expected {PICK_USE_SUGGESTED!r} or {PICK_TYPE_CUSTOM!r}, "
|
|
541
|
+
f"got: {value!r}"
|
|
542
|
+
)
|
|
543
|
+
state.task_group = _slug_or_die(value, "task_group")
|
|
544
|
+
state.task_group_pending_text = False
|
|
545
|
+
return f"task-group: {state.task_group}"
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _build_task_group_text(state: WizardState) -> Prompt:
|
|
549
|
+
return Prompt(step=S_TASK_GROUP_TEXT, kind="text",
|
|
550
|
+
label="Task group 을 입력해주세요 (예: backend-api, INV-1234, refactor)",
|
|
551
|
+
echo_template="task-group: {value}")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _submit_task_group_text(state: WizardState, value: str) -> Optional[str]:
|
|
430
555
|
state.task_group = _slug_or_die(value, "task_group")
|
|
556
|
+
state.task_group_pending_text = False
|
|
431
557
|
return f"task-group: {state.task_group}"
|
|
432
558
|
|
|
433
559
|
|
|
434
560
|
def _build_task_id(state: WizardState) -> Prompt:
|
|
561
|
+
sugg = state.task_id_suggestion
|
|
562
|
+
if sugg:
|
|
563
|
+
return Prompt(
|
|
564
|
+
step=S_TASK_ID, kind="pick",
|
|
565
|
+
label=f"Task id? (brief 추천: {sugg})",
|
|
566
|
+
options=[
|
|
567
|
+
_opt(PICK_USE_SUGGESTED, f"brief 값 사용: {sugg}"),
|
|
568
|
+
_opt(PICK_TYPE_CUSTOM, "다른 값 입력"),
|
|
569
|
+
],
|
|
570
|
+
echo_template="task-id: {value}",
|
|
571
|
+
)
|
|
435
572
|
return Prompt(step=S_TASK_ID, kind="text",
|
|
436
573
|
label="Task id 를 알려주세요 (예: login-error-analysis, dev-9043)",
|
|
437
574
|
echo_template="task-id: {value}")
|
|
438
575
|
|
|
439
576
|
|
|
440
577
|
def _submit_task_id(state: WizardState, value: str) -> Optional[str]:
|
|
578
|
+
if state.task_id_suggestion:
|
|
579
|
+
if value == PICK_USE_SUGGESTED:
|
|
580
|
+
state.task_id = _slug_or_die(state.task_id_suggestion, "task_id")
|
|
581
|
+
state.task_id_pending_text = False
|
|
582
|
+
return f"task-id: {state.task_id} (brief)"
|
|
583
|
+
if value == PICK_TYPE_CUSTOM:
|
|
584
|
+
state.task_id_pending_text = True
|
|
585
|
+
return f"task-id: (직접 입력)"
|
|
586
|
+
raise WizardError(
|
|
587
|
+
f"expected {PICK_USE_SUGGESTED!r} or {PICK_TYPE_CUSTOM!r}, "
|
|
588
|
+
f"got: {value!r}"
|
|
589
|
+
)
|
|
441
590
|
state.task_id = _slug_or_die(value, "task_id")
|
|
591
|
+
state.task_id_pending_text = False
|
|
592
|
+
return f"task-id: {state.task_id}"
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _build_task_id_text(state: WizardState) -> Prompt:
|
|
596
|
+
return Prompt(step=S_TASK_ID_TEXT, kind="text",
|
|
597
|
+
label="Task id 를 입력해주세요 (예: login-error-analysis, dev-9043)",
|
|
598
|
+
echo_template="task-id: {value}")
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _submit_task_id_text(state: WizardState, value: str) -> Optional[str]:
|
|
602
|
+
state.task_id = _slug_or_die(value, "task_id")
|
|
603
|
+
state.task_id_pending_text = False
|
|
442
604
|
return f"task-id: {state.task_id}"
|
|
443
605
|
|
|
444
606
|
|
|
@@ -502,6 +664,14 @@ def _build_brief_path(state: WizardState) -> Prompt:
|
|
|
502
664
|
def _submit_brief_path(state: WizardState, value: str) -> Optional[str]:
|
|
503
665
|
p = _require_file(value, Path(state.project_root), "task brief")
|
|
504
666
|
state.brief_path = str(p)
|
|
667
|
+
# When the user is starting a brand-new task, pull task-group /
|
|
668
|
+
# task-id candidates from the brief frontmatter so the next two
|
|
669
|
+
# prompts can offer them as a one-click pick instead of forcing a
|
|
670
|
+
# free-text retype of what the brief already declares.
|
|
671
|
+
if state.is_new_task and not state.task_group and not state.task_id:
|
|
672
|
+
tg, tid = _brief_suggestions(p)
|
|
673
|
+
state.task_group_suggestion = tg
|
|
674
|
+
state.task_id_suggestion = tid
|
|
505
675
|
return f"brief: {p}"
|
|
506
676
|
|
|
507
677
|
|
|
@@ -1002,15 +1172,57 @@ STEPS: list[Step] = [
|
|
|
1002
1172
|
applies=lambda s: s.is_new_task is None,
|
|
1003
1173
|
build=_build_task_pick, submit=_submit_task_pick,
|
|
1004
1174
|
owns=("is_new_task", "task_group", "task_id", "task_type",
|
|
1005
|
-
"existing_brief_path", "profile_workers"
|
|
1175
|
+
"existing_brief_path", "profile_workers",
|
|
1176
|
+
"task_group_suggestion", "task_id_suggestion",
|
|
1177
|
+
"task_group_pending_text", "task_id_pending_text")),
|
|
1178
|
+
Step(S_BRIEF_PATH,
|
|
1179
|
+
applies=lambda s: (
|
|
1180
|
+
not s.brief_path
|
|
1181
|
+
and (
|
|
1182
|
+
# new-task flow: collect brief FIRST so task-group /
|
|
1183
|
+
# task-id can be offered as one-click picks from the
|
|
1184
|
+
# brief's frontmatter.
|
|
1185
|
+
(s.is_new_task is True)
|
|
1186
|
+
# existing-task flow: brief comes after task-type and
|
|
1187
|
+
# the optional brief-keep step (unchanged behavior).
|
|
1188
|
+
or (s.is_new_task is False
|
|
1189
|
+
and S_TASK_TYPE in s.answered
|
|
1190
|
+
and (s.keep_existing_brief is False
|
|
1191
|
+
or not s.existing_brief_path))
|
|
1192
|
+
)
|
|
1193
|
+
),
|
|
1194
|
+
build=_build_brief_path, submit=_submit_brief_path,
|
|
1195
|
+
owns=("brief_path", "task_group_suggestion", "task_id_suggestion")),
|
|
1006
1196
|
Step(S_TASK_GROUP,
|
|
1007
|
-
applies=lambda s: bool(s.is_new_task)
|
|
1197
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
1198
|
+
and bool(s.brief_path)
|
|
1199
|
+
and not s.task_group
|
|
1200
|
+
and not s.task_group_pending_text),
|
|
1008
1201
|
build=_build_task_group, submit=_submit_task_group,
|
|
1009
|
-
owns=("task_group",)),
|
|
1202
|
+
owns=("task_group", "task_group_pending_text")),
|
|
1203
|
+
Step(S_TASK_GROUP_TEXT,
|
|
1204
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
1205
|
+
and bool(s.brief_path)
|
|
1206
|
+
and not s.task_group
|
|
1207
|
+
and s.task_group_pending_text),
|
|
1208
|
+
build=_build_task_group_text, submit=_submit_task_group_text,
|
|
1209
|
+
owns=("task_group", "task_group_pending_text")),
|
|
1010
1210
|
Step(S_TASK_ID,
|
|
1011
|
-
applies=lambda s: bool(s.is_new_task)
|
|
1211
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
1212
|
+
and bool(s.brief_path)
|
|
1213
|
+
and bool(s.task_group)
|
|
1214
|
+
and not s.task_id
|
|
1215
|
+
and not s.task_id_pending_text),
|
|
1012
1216
|
build=_build_task_id, submit=_submit_task_id,
|
|
1013
|
-
owns=("task_id",)),
|
|
1217
|
+
owns=("task_id", "task_id_pending_text")),
|
|
1218
|
+
Step(S_TASK_ID_TEXT,
|
|
1219
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
1220
|
+
and bool(s.brief_path)
|
|
1221
|
+
and bool(s.task_group)
|
|
1222
|
+
and not s.task_id
|
|
1223
|
+
and s.task_id_pending_text),
|
|
1224
|
+
build=_build_task_id_text, submit=_submit_task_id_text,
|
|
1225
|
+
owns=("task_id", "task_id_pending_text")),
|
|
1014
1226
|
Step(S_TASK_TYPE,
|
|
1015
1227
|
applies=lambda s: (s.is_new_task is not None
|
|
1016
1228
|
and (s.is_new_task is False or bool(s.task_id))
|
|
@@ -1023,15 +1235,7 @@ STEPS: list[Step] = [
|
|
|
1023
1235
|
and s.keep_existing_brief is None
|
|
1024
1236
|
and S_TASK_TYPE in s.answered),
|
|
1025
1237
|
build=_build_brief_keep, submit=_submit_brief_keep,
|
|
1026
|
-
owns=("keep_existing_brief",
|
|
1027
|
-
Step(S_BRIEF_PATH,
|
|
1028
|
-
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
1029
|
-
and not s.brief_path
|
|
1030
|
-
and (s.is_new_task
|
|
1031
|
-
or s.keep_existing_brief is False
|
|
1032
|
-
or (not s.is_new_task and not s.existing_brief_path))),
|
|
1033
|
-
build=_build_brief_path, submit=_submit_brief_path,
|
|
1034
|
-
owns=("brief_path",)),
|
|
1238
|
+
owns=("keep_existing_brief",)),
|
|
1035
1239
|
Step(S_BASE_REF_PICK,
|
|
1036
1240
|
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
1037
1241
|
and s.reuse_worktree is False
|
|
@@ -1240,6 +1444,8 @@ def _reset_from(state: WizardState, target_step: str) -> None:
|
|
|
1240
1444
|
_FIELD_DEFAULTS: dict[str, Any] = {
|
|
1241
1445
|
"is_new_task": None, "task_group": "", "task_id": "",
|
|
1242
1446
|
"existing_brief_path": "", "task_type": "",
|
|
1447
|
+
"task_group_suggestion": "", "task_id_suggestion": "",
|
|
1448
|
+
"task_group_pending_text": False, "task_id_pending_text": False,
|
|
1243
1449
|
"profile_workers": [], "keep_existing_brief": None,
|
|
1244
1450
|
"brief_path": "", "reuse_worktree": None, "base_ref": "",
|
|
1245
1451
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
@@ -43,6 +43,7 @@ from typing import Optional
|
|
|
43
43
|
|
|
44
44
|
from .ids import _safe_fs_segment
|
|
45
45
|
from . import worktree_registry
|
|
46
|
+
from .seeding import SettingsLinkError, ensure_project_settings_symlink
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
# Project-root directories that hold okstra task state, ignored by git, or
|
|
@@ -361,6 +362,34 @@ def _link_sync_files(source_root: Path, worktree_path: Path) -> list[str]:
|
|
|
361
362
|
return notes
|
|
362
363
|
|
|
363
364
|
|
|
365
|
+
def _seed_worktree_settings_symlink(worktree_path: Path) -> None:
|
|
366
|
+
"""Seed `.claude/settings.local.json` in the worker worktree so dispatched
|
|
367
|
+
Claude / codex / gemini sessions inherit the okstra read-only / write
|
|
368
|
+
allowlist. Mirrors the main-project seeding done in `run.py` — needed
|
|
369
|
+
because `_link_sync_dirs` skips `.claude/` whenever `git worktree add`
|
|
370
|
+
already materialised the directory (e.g. tracked `.claude/handoff-*.md`).
|
|
371
|
+
Failures degrade to stderr warning so worktree provisioning still
|
|
372
|
+
succeeds.
|
|
373
|
+
"""
|
|
374
|
+
try:
|
|
375
|
+
link = ensure_project_settings_symlink(project_root=worktree_path)
|
|
376
|
+
except SettingsLinkError as exc:
|
|
377
|
+
print(
|
|
378
|
+
f"okstra-settings: failed to seed worker worktree symlink at "
|
|
379
|
+
f"{worktree_path / '.claude/settings.local.json'} — worker dispatch "
|
|
380
|
+
f"may be blocked by Claude Code permissions. ({exc})",
|
|
381
|
+
file=__import__("sys").stderr,
|
|
382
|
+
)
|
|
383
|
+
return
|
|
384
|
+
if link is None:
|
|
385
|
+
print(
|
|
386
|
+
"okstra-settings: ~/.okstra/templates/settings.local.json missing — "
|
|
387
|
+
"re-run 'npx okstra@latest install' (0.14.0+) to provision the "
|
|
388
|
+
"symlink target.",
|
|
389
|
+
file=__import__("sys").stderr,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
364
393
|
def _copy_snapshot_files(source_root: Path, worktree_path: Path) -> list[str]:
|
|
365
394
|
"""Copy fixture files from MAIN → task worktree as read-only snapshots
|
|
366
395
|
(FU-V3).
|
|
@@ -485,6 +514,7 @@ def provision_task_worktree(
|
|
|
485
514
|
existing = worktree_registry.lookup(safe_project, safe_group, safe_task)
|
|
486
515
|
if existing is not None and existing.status == "active":
|
|
487
516
|
worktree_registry.touch_phase(safe_project, safe_group, safe_task, task_type)
|
|
517
|
+
_seed_worktree_settings_symlink(Path(existing.worktree_path))
|
|
488
518
|
return WorktreeProvision(
|
|
489
519
|
status="reused",
|
|
490
520
|
path=existing.worktree_path,
|
|
@@ -586,6 +616,8 @@ def provision_task_worktree(
|
|
|
586
616
|
_git(main_root, "branch", "-D", branch)
|
|
587
617
|
raise
|
|
588
618
|
|
|
619
|
+
_seed_worktree_settings_symlink(worktree_path)
|
|
620
|
+
|
|
589
621
|
base_label = (
|
|
590
622
|
f"{base_origin} @ {resolved_base_ref[:12]}"
|
|
591
623
|
if base_origin != "HEAD"
|
|
@@ -648,11 +648,15 @@ reporter-confirmations: <complete | partial | pending | skipped> # set by Step
|
|
|
648
648
|
> Recommended next phase: <requirements-discovery | error-analysis> ← from Step 6
|
|
649
649
|
> Handoff contract: see `prompts/profiles/_common-contract.md` § "Brief handoff contract"
|
|
650
650
|
|
|
651
|
-
## Source Material
|
|
651
|
+
## Source Material
|
|
652
652
|
|
|
653
|
+
<!-- author guidance — strip out at fill-in time:
|
|
653
654
|
Paste each source separately and as-is. No paraphrasing, summarizing, or
|
|
654
655
|
restructuring. Format conversion (e.g. Jira ADF → Markdown) is allowed and
|
|
655
|
-
must be annotated in the header meta.
|
|
656
|
+
must be annotated in the header meta. Heading was originally
|
|
657
|
+
"Source Material (verbatim — do not modify)" — the parenthetical is a
|
|
658
|
+
reviewer note, not body text.
|
|
659
|
+
-->
|
|
656
660
|
|
|
657
661
|
### Source 1 — <type: file | linear | jira | github | notion | url | user-input>
|
|
658
662
|
|
|
@@ -665,30 +669,31 @@ must be annotated in the header meta.
|
|
|
665
669
|
<Paste the raw source here without changing a single character.>
|
|
666
670
|
```
|
|
667
671
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
(Repeat as needed.)
|
|
672
|
+
<!-- Repeat `### Source N — …` blocks as needed. -->
|
|
671
673
|
|
|
672
674
|
## Context
|
|
673
675
|
|
|
674
676
|
<Background / scope / why now. If self-evident from Source Material, quote
|
|
675
677
|
it briefly and stop. Use the blockquote below when augmentation is needed.>
|
|
676
678
|
|
|
677
|
-
> augmented: <label> — <Interpretation added by the skill or user
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
679
|
+
> augmented: <label> — <Interpretation added by the skill or user.>
|
|
680
|
+
|
|
681
|
+
<!-- label MUST be one of: `evidence-link` / `format-conversion` /
|
|
682
|
+
`terminology-mapping` / `intent-inference`. Do NOT add any extra
|
|
683
|
+
interpretation outside the `> augmented:` blockquote. -->
|
|
681
684
|
|
|
682
685
|
## Problem / Symptom
|
|
683
686
|
|
|
684
687
|
<Current state. For bugs: repro / observed / expected. For greenfield: gap
|
|
685
|
-
between current and desired
|
|
686
|
-
|
|
688
|
+
between current and desired.>
|
|
689
|
+
|
|
690
|
+
<!-- Same source-quote + `> augmented:` rule as the Context section. -->
|
|
687
691
|
|
|
688
692
|
## Desired Outcome
|
|
689
693
|
|
|
690
|
-
<Shape of success
|
|
691
|
-
|
|
694
|
+
<Shape of success.>
|
|
695
|
+
|
|
696
|
+
<!-- Do NOT prescribe a solution — that belongs to implementation-planning. -->
|
|
692
697
|
|
|
693
698
|
## Constraints
|
|
694
699
|
|
|
@@ -701,14 +706,18 @@ none.>
|
|
|
701
706
|
|
|
702
707
|
## Open Questions
|
|
703
708
|
|
|
709
|
+
<!-- author guidance — strip out at fill-in time:
|
|
704
710
|
Prefix every row with one of these signals so the next phase knows how to
|
|
705
711
|
handle it. Free-form rows are allowed only as `general:`.
|
|
706
712
|
|
|
713
|
+
Allowed signals:
|
|
707
714
|
- `general: <unresolved question the user flagged>`
|
|
708
|
-
- `terminology: <reporter word> — needs canonical resolution against
|
|
715
|
+
- `terminology: <reporter word> — needs canonical resolution against
|
|
716
|
+
<PROJECT_ROOT>/.project-docs/okstra/glossary.md`
|
|
709
717
|
- `intent-check: <restated inference> — confirm with reporter`
|
|
710
718
|
(auto-paired with every `intent-inference` augmentation)
|
|
711
|
-
- `conversion-block: <reporter statement> — could not be mapped to project
|
|
719
|
+
- `conversion-block: <reporter statement> — could not be mapped to project
|
|
720
|
+
vocabulary; reporter query required`
|
|
712
721
|
- `adr-candidate: <topic>` — signal only; `implementation-planning`
|
|
713
722
|
evaluates and, if accepted, drafts a decision file at
|
|
714
723
|
`<PROJECT_ROOT>/.project-docs/okstra/decisions/<NNNN>-<slug>.md`.
|
|
@@ -718,11 +727,14 @@ Use `_(none)_` only if every signal is empty. `intent-check:` and
|
|
|
718
727
|
from this list — they receive a `[CONFIRMED <YYYY-MM-DD> → RC-N]`
|
|
719
728
|
marker that links to the corresponding entry under
|
|
720
729
|
`## Reporter Confirmations`.
|
|
730
|
+
-->
|
|
731
|
+
|
|
732
|
+
- <fill in one row per signal, or replace with `_(none)_`>
|
|
721
733
|
|
|
722
734
|
## Reporter Confirmations
|
|
723
735
|
|
|
724
|
-
Populated by Step 6.5. Each subsection records one reporter answer
|
|
725
|
-
verbatim, with a link back to the originating `Open Questions` row.
|
|
736
|
+
<!-- Populated by Step 6.5. Each subsection records one reporter answer
|
|
737
|
+
verbatim, with a link back to the originating `Open Questions` row. -->
|
|
726
738
|
|
|
727
739
|
_(none — pending or skipped)_
|
|
728
740
|
|
|
@@ -737,6 +749,7 @@ _(none — pending or skipped)_
|
|
|
737
749
|
|
|
738
750
|
## Augmentation
|
|
739
751
|
|
|
752
|
+
<!-- author guidance — strip out at fill-in time:
|
|
740
753
|
Cross-references / interpretation / context added by the user or skill that
|
|
741
754
|
is not in the original source. May be empty. Keep this section visually
|
|
742
755
|
separated from Source Material — never inline it inside Source Material.
|
|
@@ -744,44 +757,59 @@ separated from Source Material — never inline it inside Source Material.
|
|
|
744
757
|
Every entry below must start with one of the four labels:
|
|
745
758
|
`evidence-link` / `format-conversion` / `terminology-mapping` /
|
|
746
759
|
`intent-inference`. Unlabelled entries are forbidden.
|
|
760
|
+
-->
|
|
747
761
|
|
|
748
762
|
### Domain alignment
|
|
749
763
|
|
|
750
|
-
|
|
751
|
-
|
|
764
|
+
<!-- author guidance — strip out at fill-in time:
|
|
765
|
+
Observations from Step 3b and the outcome of Step 4.5 (glossary applied
|
|
766
|
+
vs. skipped). The actual glossary edits live in
|
|
752
767
|
`<PROJECT_ROOT>/.project-docs/okstra/glossary.md` when applied; this
|
|
753
|
-
section records what happened. Decision candidates are NOT recorded
|
|
754
|
-
|
|
768
|
+
section records what happened. Decision candidates are NOT recorded here —
|
|
769
|
+
they flow through `Open Questions` as `adr-candidate:` rows for
|
|
755
770
|
`implementation-planning` to evaluate (and, if accepted, draft into
|
|
756
771
|
`<PROJECT_ROOT>/.project-docs/okstra/decisions/`).
|
|
757
772
|
|
|
773
|
+
Allowed entry shapes:
|
|
758
774
|
- `terminology-mapping: <reporter word> → <okstra glossary canonical>` —
|
|
759
775
|
routine glossary alignment, paired with `terminology:` in Open Questions
|
|
760
776
|
when unresolved.
|
|
761
|
-
- `terminology-mapping: applied glossary: <term> → <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
|
|
762
|
-
|
|
763
|
-
4.5 outcomes.
|
|
764
|
-
|
|
777
|
+
- `terminology-mapping: applied glossary: <term> → <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
|
|
778
|
+
- `terminology-mapping: skipped glossary: <term> = <definition>` —
|
|
779
|
+
Step 4.5 outcomes.
|
|
780
|
+
Use `_(none)_` if every alignment entry is empty.
|
|
781
|
+
-->
|
|
782
|
+
|
|
783
|
+
- <fill in one entry per alignment, or replace with `_(none)_`>
|
|
765
784
|
|
|
766
785
|
### Evidence links (file / symbol resolution)
|
|
767
786
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
-
|
|
787
|
+
<!-- Allowed entry shapes:
|
|
788
|
+
`evidence-link: <reporter phrase> → <relative path>:<line>` or
|
|
789
|
+
`evidence-link: <reporter phrase> → <symbol> in <relative path>`.
|
|
790
|
+
Use `_(none)_` if none. -->
|
|
791
|
+
|
|
792
|
+
- <fill in one entry per link, or replace with `_(none)_`>
|
|
771
793
|
|
|
772
794
|
### Intent inferences
|
|
773
795
|
|
|
774
|
-
Every entry here is an unverified hypothesis. Each one MUST have a
|
|
775
|
-
`intent-check:` row under Open Questions.
|
|
796
|
+
<!-- Every entry here is an unverified hypothesis. Each one MUST have a
|
|
797
|
+
paired `intent-check:` row under Open Questions.
|
|
798
|
+
|
|
799
|
+
Allowed entry shape:
|
|
800
|
+
`intent-inference: <reporter phrase> → <qualitative restatement>`
|
|
801
|
+
(qualitative only — never invent numeric thresholds).
|
|
802
|
+
Use `_(none)_` if none. -->
|
|
776
803
|
|
|
777
|
-
-
|
|
778
|
-
(qualitative only — never invent numeric thresholds)
|
|
779
|
-
- `_(none)_` if none.
|
|
804
|
+
- <fill in one entry per inference, or replace with `_(none)_`>
|
|
780
805
|
|
|
781
806
|
### Format conversions
|
|
782
807
|
|
|
783
|
-
|
|
784
|
-
-
|
|
808
|
+
<!-- Allowed entry shape:
|
|
809
|
+
`format-conversion: <ref> — <e.g. Jira ADF → Markdown, semantics preserved>`.
|
|
810
|
+
Use `_(none)_` if none. -->
|
|
811
|
+
|
|
812
|
+
- <fill in one entry per conversion, or replace with `_(none)_`>
|
|
785
813
|
````
|
|
786
814
|
|
|
787
815
|
### Frontmatter rules
|
|
@@ -966,6 +994,16 @@ started.
|
|
|
966
994
|
is allowed, and the conversion must be annotated in the `format:` meta.
|
|
967
995
|
Augmentation / interpretation goes only into the `Augmentation` section
|
|
968
996
|
or a `> augmented:` blockquote inside the required section.
|
|
997
|
+
- **No template author-guidance in the artifact body.** The Step 5
|
|
998
|
+
template above carries author guidance in HTML comments
|
|
999
|
+
(`<!-- ... -->`); preserve those comments when writing the brief but do
|
|
1000
|
+
NOT promote them to body prose. Section headings stay clean — never copy
|
|
1001
|
+
parenthetical reviewer notes onto the heading itself (e.g. emit
|
|
1002
|
+
`## Source Material`, not `## Source Material (verbatim — do not modify)`).
|
|
1003
|
+
Placeholder prose inside angle brackets (`<Background / scope / why
|
|
1004
|
+
now…>`) is intentional and is meant to be replaced with the reporter's
|
|
1005
|
+
actual content; do not strip the angle-bracket placeholders, replace
|
|
1006
|
+
them.
|
|
969
1007
|
- If the tracker MCP is not connected, do not guess — ask the user to paste
|
|
970
1008
|
the body or skip the source.
|
|
971
1009
|
- If a URL fetch fails or hits an auth wall, do not guess — ask for a
|