okstra 0.71.2 → 0.73.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.
Files changed (40) hide show
  1. package/docs/kr/architecture.md +16 -2
  2. package/docs/kr/cli.md +8 -2
  3. package/docs/kr/performance-improvement-plan-v2.md +27 -7
  4. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +1290 -0
  5. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +94 -0
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/SKILL.md +2 -2
  9. package/runtime/bin/lib/okstra/cli.sh +5 -1
  10. package/runtime/bin/lib/okstra/globals.sh +1 -0
  11. package/runtime/bin/lib/okstra/usage.sh +6 -1
  12. package/runtime/bin/okstra.sh +1 -0
  13. package/runtime/prompts/profiles/error-analysis.md +1 -1
  14. package/runtime/prompts/profiles/final-verification.md +1 -1
  15. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  16. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  17. package/runtime/prompts/wizard/prompts.ko.json +9 -0
  18. package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
  19. package/runtime/python/okstra_ctl/clarification_items.py +26 -0
  20. package/runtime/python/okstra_ctl/context_cost.py +32 -5
  21. package/runtime/python/okstra_ctl/fix_cycles.py +172 -0
  22. package/runtime/python/okstra_ctl/render.py +28 -4
  23. package/runtime/python/okstra_ctl/report_views.py +63 -6
  24. package/runtime/python/okstra_ctl/run.py +97 -4
  25. package/runtime/python/okstra_ctl/run_context.py +15 -9
  26. package/runtime/python/okstra_ctl/wizard.py +64 -4
  27. package/runtime/python/okstra_token_usage/claude.py +22 -0
  28. package/runtime/python/okstra_token_usage/collect.py +46 -0
  29. package/runtime/python/okstra_token_usage/cursor.py +3 -1
  30. package/runtime/schemas/final-report-v1.0.schema.json +27 -1
  31. package/runtime/skills/okstra-brief/SKILL.md +8 -0
  32. package/runtime/skills/okstra-convergence/SKILL.md +8 -4
  33. package/runtime/skills/okstra-inspect/SKILL.md +20 -0
  34. package/runtime/skills/okstra-report-writer/SKILL.md +4 -2
  35. package/runtime/skills/okstra-run/SKILL.md +2 -1
  36. package/runtime/templates/project-docs/task-index.template.md +1 -0
  37. package/runtime/templates/reports/final-report.template.md +17 -3
  38. package/runtime/templates/reports/report.js +23 -2
  39. package/runtime/validators/validate-run.py +41 -0
  40. package/src/render-bundle.mjs +4 -1
@@ -0,0 +1,94 @@
1
+ # Fix Cycle — 완료된 task 의 사후 버그 핫픽스 이력 설계
2
+
3
+ - 날짜: 2026-06-11
4
+ - 상태: 설계 승인 대기
5
+ - 배경: okstra 로 완료(release-handoff 까지)한 task 의 산출물에서 이후 버그가 발견되는 경우, 같은 task-id 위에서 핫픽스를 수행하고 그 수정 이력을 1급 레코드로 등록하며, 이후 brief / final-report / task 브라우징에서 해당 이력이 인지되어야 한다.
6
+
7
+ ## 결정 요약
8
+
9
+ | 결정 지점 | 선택 |
10
+ |---|---|
11
+ | lifecycle 형태 | 기존 phase 재진입 (error-analysis → planning → implementation → …) + 이력 계층만 신설. 전용 hotfix task-type 없음 |
12
+ | 진입 식별 | 자동 감지 + okstra-run wizard 확인 단계 (`fix_cycle_confirm`), CLI 는 `--fix-cycle <yes|no>` |
13
+ | 이력 SSOT | `<task_root>/history/fix-cycles.jsonl` append-only 이벤트 행 (consumers.jsonl idiom) |
14
+ | 소비처 | ① 후속 run 의 analysis-packet ② okstra-brief 인용 ③ final-report `## 5.10 Fix History` ④ task-manifest / task-index / task-catalog 요약 |
15
+
16
+ ## 1. 개념 모델
17
+
18
+ **fix cycle** 은 "완료된 task 의 기존 산출물에서 발견된 버그 1건을 고치는 재진입 run 묶음"이다.
19
+
20
+ - id: `fc-<NN>` (task 내 단조 증가, 2자리 zero-pad).
21
+ - 상태: `open` → `closed` 둘뿐. 이벤트 행으로 표현하며 별도 mutable 상태 파일은 두지 않는다.
22
+ - 대상 링크: 픽스 대상인 이전 final-report 의 project-relative 경로 (`target_report`). open 시점 task-manifest 의 `latestReportPath` 에서 채운다.
23
+ - 증상 한 줄(`symptom`): 재진입 brief 의 `Request Summary` 첫 문장에서 파생한다. 사용자 추가 입력 없음.
24
+ - 동시 open cycle 은 task 당 1개로 제한한다. 두 번째 버그는 첫 cycle 종료 후에 연다 (KISS).
25
+
26
+ ## 2. SSOT 파일 계약 — `history/fix-cycles.jsonl`
27
+
28
+ 위치: `<task_root>/history/fix-cycles.jsonl`. reader/writer 는 신규 모듈 `scripts/okstra_ctl/fix_cycles.py` 단 한 곳이다. [consumers.py](../../../scripts/okstra_ctl/consumers.py) 의 idiom 을 그대로 따른다 — append-only + mutex(`run_context` 의 mutex 헬퍼 재사용) + last-wins 읽기.
29
+
30
+ 이벤트 행 3종:
31
+
32
+ ```jsonl
33
+ {"event":"opened","cycle":"fc-01","target_report":"<상대경로>","symptom":"...","opened_at":"<ISO>"}
34
+ {"event":"run","cycle":"fc-01","task_type":"error-analysis","run_seq":3,"run_manifest":"<상대경로>"}
35
+ {"event":"closed","cycle":"fc-01","closed_by":"release-handoff","report":"<상대경로>","closed_at":"<ISO>"}
36
+ ```
37
+
38
+ - idempotency 키: `(cycle, event, run_manifest|None)` 튜플. 같은 행은 중복 append 되지 않는다.
39
+ - 경로 필드는 run-manifest 와 같은 규칙으로 프로젝트 루트 기준 상대경로만 저장한다.
40
+ - 파생 읽기 API: `open_cycle(rows) -> dict|None`, `summarize(rows) -> dict` (소비처들이 공유). `summarize` 의 `latest` 는 `{cycle, symptom, targetReport, closedAt}`.
41
+
42
+ ## 3. 진입: 자동 감지 + wizard 확인
43
+
44
+ 감지 조건 (prepare 시점):
45
+
46
+ - 대상 task 의 `workflow.lastCompletedPhase == "release-handoff"` (done 계열 상태), **그리고**
47
+ - 이번 run 의 task-type 이 분석 entry phase (`requirements-discovery` / `error-analysis` / `implementation-planning`) 중 하나일 때.
48
+ - `improvement-discovery` sidetrack 은 제외.
49
+
50
+ 확인 UI:
51
+
52
+ - okstra-run wizard 에 `fix_cycle_confirm` 단계 신설 — [wizard.py](../../../scripts/okstra_ctl/wizard.py) 의 `branch_confirm` 과 같은 step-registry 패턴 (`required` 술어 = 감지 조건, `owns` = `fix_cycle_confirmed`).
53
+ - 옵션: "버그 픽스 사이클로 기록 (추천)" / "일반 후속 작업" / "직접 입력" — okstra-run 프롬프트 3-옵션 picker 규칙 준수.
54
+ - CLI 비대화 경로(okstra.sh / Node CLI)는 `--fix-cycle <yes|no>` 플래그를 [prepare_task_bundle()](../../../scripts/okstra_ctl/run.py) 입력(`PrepareInputs`)으로 전달한다. 플래그가 주어지면 wizard 단계는 건너뛴다. 세 entry-point 모두 `prepare_task_bundle` 로 수렴하는 단일 참조 규칙을 유지하며, 환경 변수는 사용하지 않는다.
55
+
56
+ 확인되면 prepare 가 `opened` 행을 기록한다.
57
+
58
+ ## 4. run 부착과 종료
59
+
60
+ - cycle 이 `open` 인 동안 같은 task 에서 도는 **모든** run 의 prepare 가 `run` 행을 append 한다 (task-type 불문 — 핫픽스 사이클의 implementation / final-verification / release-handoff 포함).
61
+ - run-manifest 와 timeline 항목에 `fixCycleId` 필드를 추가한다 (open cycle 없으면 필드 생략).
62
+ - 종료: release-handoff 결과가 기록되어 workflow 가 `done-or-follow-up` 에 도달하는 시점에 `closed` 행을 append 한다 ([workflow.py](../../../scripts/okstra_ctl/workflow.py) 의 `DEFAULT_NEXT_PHASE`). final-verification `accepted` 만으로는 닫지 않는다 — 아직 release 가 나가지 않은 상태이기 때문.
63
+
64
+ ## 5. 소비처 4곳 — 전부 파생 뷰
65
+
66
+ 쓰기 1곳(`fix_cycles.py`), 읽기 N곳 원칙. 모든 소비처는 `fix_cycles.summarize()` 가 만든 같은 요약을 사용한다.
67
+
68
+ 1. **analysis-packet**: [analysis_packet.py](../../../scripts/okstra_ctl/analysis_packet.py) 의 `build_analysis_packet()` 이 fix-cycles.jsonl 존재 시 `## Fix History` 섹션을 packet 에 주입한다 — open cycle 상세(대상 보고서·증상·부착 run 목록) + closed cycle 한 줄 목록. lead / worker 가 분석 입력으로 받는다.
69
+ 2. **task-manifest / task-index / task-catalog**: manifest 에 파생 요약 필드 `fixCycles` (`{count, openCycleId|null, latest: {cycle, symptom, targetReport, closedAt}}`), task-index 와 `.okstra/discovery/task-catalog.json` 항목에 같은 요약 1줄. manifest/index 작성 경로([render.py](../../../scripts/okstra_ctl/render.py))에서 함께 갱신한다.
70
+ 3. **final-report**: [final-report.template.md](../../../templates/reports/final-report.template.md) 에 조건부 `## 5.10 Fix History` 섹션 신설 (5.4~5.9 사용 중 확인). fix cycle 에 부착된 run 의 보고서에 cycle id · 대상 보고서 · 증상 · cycle 내 선행 run 목록이 들어간다. `final_report_schema.py` 에 대응 필드를 추가한다.
71
+ 4. **okstra-brief**: [skills/okstra-brief/SKILL.md](../../../skills/okstra-brief/SKILL.md) Step 3 (domain alignment scan) 에 "대상 task 의 `history/fix-cycles.jsonl` 이 존재하면 `summarize` 요약을 읽어 brief 의 `Task Continuity Notes` 에 인용" 지시를 추가한다.
72
+
73
+ ## 6. 강제 지점 (선언 ≠ 강제)
74
+
75
+ | 계약 | 강제 위치 |
76
+ |---|---|
77
+ | 행 스키마 · idempotency · 단일 open cycle | `fix_cycles.py` 코드 + `tests/test_okstra_fix_cycles.py` |
78
+ | `## 5.10 Fix History` 섹션 형태 | 기존 report validator 경로 (`validators/`) 에 검사 추가 |
79
+ | wizard 단계 · `--fix-cycle` 플래그 | 기존 wizard / prepare 단위 테스트 패턴으로 커버 |
80
+ | 감지 조건 (done + entry phase) | `prepare_task_bundle` 단위 테스트 |
81
+
82
+ ## 7. 문서
83
+
84
+ - `docs/kr/architecture.md` — "Phase 간 정보 전달" 인근에 "Fix cycle (사후 버그 핫픽스 이력)" 절 추가, storage model 트리에 `history/fix-cycles.jsonl` 반영.
85
+ - `docs/kr/cli.md` — `--fix-cycle` 플래그.
86
+ - `CHANGES.md` — `사용자 영향:` 라인 포함 항목 추가.
87
+ - 전부 한국어.
88
+
89
+ ## 비목표
90
+
91
+ - 전용 hotfix task-type / fast path 신설 (phase gate 우회 없음).
92
+ - task 당 동시 다중 open cycle.
93
+ - 기존 timeline.json / run-manifest 의 구조 변경 (`fixCycleId` 필드 1개 추가 외).
94
+ - 과거 완료 task 의 이력 소급 backfill.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.71.2",
3
+ "version": "0.73.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.71.2",
3
- "builtAt": "2026-06-11T04:23:08.954Z",
2
+ "package": "0.73.0",
3
+ "builtAt": "2026-06-11T14:40:52.876Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -42,7 +42,7 @@ This SKILL.md is the operating contract and phase index. Detailed procedures liv
42
42
  | 4. Execution | Spawn analysis workers (Teams preferred) | `okstra-team-contract` |
43
43
  | 5. Fallback | Sequential/background dispatch when Teams unavailable | `okstra-team-contract` |
44
44
  | 5.5 Convergence | Cross-verify findings across workers | `okstra-convergence` |
45
- | 5.6 Critic pass | (opt-in) reused-worker critic pass: coverage gaps (discovery/error-analysis/impl-planning) or acceptance devil's-advocate (final-verification), each verified one round | `okstra-convergence` "Coverage critic pass" / "Acceptance critic pass" |
45
+ | 5.6 Critic pass | (opt-in) reused-worker critic pass: coverage gaps (discovery/error-analysis/impl-planning) or acceptance devil's-advocate (final-verification). The critic dispatch fires concurrently with the first 5.5 reverify round (its input is fixed at Round 0); gap/blocker verification (one round) completes here | `okstra-convergence` "Coverage critic pass" / "Acceptance critic pass" |
46
46
  | 6. Synthesis | Dispatch Report writer worker, review draft. **For `implementation-planning`: then run the Phase 6 plan-body verification sub-step (see Phase 6 section below).** | `okstra-report-writer` + `okstra-convergence` (sub-step) |
47
47
  | 7. Persist | Run token-usage collector, update manifests, ask about residual tmux panes, then ask whether to disband the worker team (reconcile stale members + `TeamDelete` only on approval) | `okstra-report-writer` + `_common-contract.md` "Phase wrap-up" / "Run-end teammate teardown" |
48
48
 
@@ -94,7 +94,7 @@ Required checkpoints:
94
94
  - `PROGRESS: phase-5-poll pending=<n> done=<m>` — emitted on each wakeup while the pending set is non-empty.
95
95
  - `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified.
96
96
  - `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5).
97
- - `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — when the coverage critic pass runs (Phase 5.6, opt-in). Omitted when `convergence.critic.enabled == false`.
97
+ - `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — after the critic result is collected (Phase 5.6, opt-in; the critic dispatch itself fires concurrently with the first 5.5 reverify round). Omitted when `convergence.critic.enabled == false`.
98
98
  - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6.
99
99
  - `PROGRESS: phase-7-persist updating manifests` — at the start of Phase 7.
100
100
  - `PROGRESS: phase-7-teardown disbanding team` — after token-usage collection and after the pane-disposition prompt, only when the user approved worker teammate cleanup, immediately before reconciling stale members + `TeamDelete` (Teams mode only; see `_common-contract.md` "Run-end teammate teardown"). Skipped in the no-`team_name` fallback or when the user keeps the team.
@@ -142,6 +142,10 @@ while [[ $# -gt 0 ]]; do
142
142
  DIRECTIVE="$(require_option_value --directive "${2-}")"
143
143
  shift 2
144
144
  ;;
145
+ --fix-cycle)
146
+ FIX_CYCLE="$(require_option_value --fix-cycle "${2-}")"
147
+ shift 2
148
+ ;;
145
149
  --clarification-response)
146
150
  CLARIFICATION_RESPONSE_PATH="$(require_option_value --clarification-response "${2-}")"
147
151
  shift 2
@@ -204,7 +208,7 @@ while [[ $# -gt 0 ]]; do
204
208
  printf ' hint: did you mean --task-id?\n' >&2
205
209
  ;;
206
210
  esac
207
- printf ' valid options: --render-only --resume-clarification --yes --workers --lead-model --claude-model --codex-model --gemini-model --report-writer-model --related-tasks --task-type --project-id --project-root --task-group --task-id --task-brief --directive --clarification-response --approved-plan --approve --implementation-option --stage --qa-waiver --no-plan-verification -h|--help\n' >&2
211
+ printf ' valid options: --render-only --resume-clarification --yes --workers --lead-model --claude-model --codex-model --gemini-model --report-writer-model --related-tasks --task-type --project-id --project-root --task-group --task-id --task-brief --directive --fix-cycle --clarification-response --approved-plan --approve --implementation-option --stage --qa-waiver --no-plan-verification -h|--help\n' >&2
208
212
  usage
209
213
  exit 1
210
214
  ;;
@@ -37,6 +37,7 @@ TASK_KEY_INPUT=""
37
37
  TASK_KEY=""
38
38
  ANALYSIS_PROFILE=""
39
39
  DIRECTIVE=""
40
+ FIX_CYCLE=""
40
41
  CLARIFICATION_RESPONSE_PATH=""
41
42
  APPROVED_PLAN_PATH=""
42
43
  APPROVE_PLAN_ACK="false"
@@ -3,7 +3,7 @@
3
3
  usage() {
4
4
  cat >&2 <<USAGE_EOF
5
5
  usage:
6
- $DISPLAY_COMMAND_NAME [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--workers worker1,worker2] [--lead-model <model>] [--claude-model <model>] [--codex-model <model>] [--gemini-model <model>] [--report-writer-model <model>] [--executor claude|codex|gemini] [--critic off|claude|codex|gemini] [--related-tasks taskA,taskB] --project-id <project-id> [--project-root <path>] --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>]
6
+ $DISPLAY_COMMAND_NAME [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--workers worker1,worker2] [--lead-model <model>] [--claude-model <model>] [--codex-model <model>] [--gemini-model <model>] [--report-writer-model <model>] [--executor claude|codex|gemini] [--critic off|claude|codex|gemini] [--related-tasks taskA,taskB] --project-id <project-id> [--project-root <path>] --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>] [--fix-cycle <yes|no>]
7
7
 
8
8
  summary:
9
9
  $DISPLAY_TOOL_NAME prepares a task-keyed instruction bundle for Claude Code and launches an interactive Claude session by default.
@@ -32,6 +32,11 @@ optional arguments:
32
32
  inside instruction-set/analysis-material.md. Lead, workers, and skills (e.g. okstra-schedule)
33
33
  may treat this as a hard hint that overrides default heuristics. Use to express intent
34
34
  like "render a Gantt even with single XL task" or "emphasize rollout risk".
35
+ --fix-cycle <yes|no> When re-entering an entry phase (requirements-discovery / error-analysis /
36
+ implementation-planning) on a task that already completed through
37
+ release-handoff, decide whether to record this work as a bug-fix cycle.
38
+ 'yes' opens a new cycle and attaches this run to it; 'no' or omitted records
39
+ nothing.
35
40
  --clarification-response
36
41
  Low-level path argument. Carries an edited final-report.md from a prior
37
42
  requirements-discovery or error-analysis run into this run as Section 0
@@ -108,6 +108,7 @@ PY_ARGS=(
108
108
  --task-brief "$BRIEF_PATH"
109
109
  )
110
110
  [[ -n "${DIRECTIVE-}" ]] && PY_ARGS+=(--directive "$DIRECTIVE")
111
+ [[ -n "${FIX_CYCLE-}" ]] && PY_ARGS+=(--fix-cycle "$FIX_CYCLE")
111
112
  [[ -n "${WORKERS_OVERRIDE-}" ]] && PY_ARGS+=(--workers "$WORKERS_OVERRIDE")
112
113
  [[ -n "${LEAD_MODEL_OVERRIDE-}" ]] && PY_ARGS+=(--lead-model "$LEAD_MODEL_OVERRIDE")
113
114
  [[ -n "${CLAUDE_MODEL_OVERRIDE-}" ]] && PY_ARGS+=(--claude-model "$CLAUDE_MODEL_OVERRIDE")
@@ -32,7 +32,7 @@
32
32
  - **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <reporter-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only the reporter can answer this" (reporter-side data, business priority, environment they observed). A row with `none` that *could* have been answered by code or logs is a defect.
33
33
  - Cross-verification mode:
34
34
  - Phase 5.5 convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each root-cause / reproduction claim by directly re-inspecting the cited code, logs, or config; the burden of proof sits on the claim. See `skills/okstra-convergence/SKILL.md` §"Adversarial Verification Mode". A single evidence-backed refutation prevents a finding from reaching consensus.
35
- - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass runs after convergence to surface missed findings; its gaps are merged only after a 1-round adversarial reverify. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
35
+ - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass is dispatched concurrently with the first convergence reverify round to surface missed findings; its gaps are merged only after a 1-round adversarial reverify that follows convergence. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
36
36
  - Non-goals:
37
37
  - implementation details unless they are necessary to validate the cause
38
38
  - **source code edits, builds, migrations, or deployments** — this run produces evidence and cause analysis only; the fix belongs to a later `implementation-planning` run followed by an `implementation` run
@@ -48,7 +48,7 @@
48
48
  4. **Verifier dissent preserved** — if workers reach different verdicts, the disagreement is visible in section 1.2; synthesis hides nothing.
49
49
  5. **No source-mutation audit** — scan the run's session transcripts for Edit / Write or state-mutating Bash commands that touch paths OUTSIDE `<PROJECT_ROOT>/.okstra/**` and outside the assigned run-artifact paths. Writes to worker prompts, audit sidecars, team-state, the final-report `data.json`, and rendered reports under the run directory are allowed okstra artifacts. Any source/schema/deployment mutation means the run has crossed into implementation and MUST be re-routed; do NOT silently strip the evidence.
50
50
  - Cross-verification mode:
51
- - **Acceptance critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker **acceptance devil's-advocate** pass runs after convergence to surface candidate acceptance blockers the verifiers may have missed. Each candidate is verified **confirm-or-downgrade**: confirmed → an `Acceptance Blockers` row (which, since `accepted` requires zero blockers, moves the verdict to `conditional-accept` / `blocked`); unconfirmed → a `Residual Risk` row (never dropped). See `skills/okstra-convergence/SKILL.md` "Acceptance critic pass (final-verification)".
51
+ - **Acceptance critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker **acceptance devil's-advocate** pass is dispatched concurrently with the first convergence reverify round to surface candidate acceptance blockers the verifiers may have missed; candidates are verified only after convergence completes. Each candidate is verified **confirm-or-downgrade**: confirmed → an `Acceptance Blockers` row (which, since `accepted` requires zero blockers, moves the verdict to `conditional-accept` / `blocked`); unconfirmed → a `Residual Risk` row (never dropped). See `skills/okstra-convergence/SKILL.md` "Acceptance critic pass (final-verification)".
52
52
  - Non-goals:
53
53
  - proposing unrelated refactors beyond the delivered scope
54
54
  - **source code edits, follow-up bug fixes, or scope expansion** — this run renders a verdict only; defects detected here become inputs to a new `error-analysis` or `implementation-planning` run
@@ -41,7 +41,7 @@
41
41
  - Cross-verification mode:
42
42
  - Phase 5.5 finding convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker finding (requirement gap / risk / option) by re-inspecting its cited evidence; the burden of proof sits on the claim. See `skills/okstra-convergence/SKILL.md` §"Adversarial Verification Mode".
43
43
  - §5.5.9 plan-body verification runs with an **adversarial posture** (`skills/okstra-convergence/SKILL.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is unchanged — a *majority* `DISAGREE` (`majority-disagree`) is still required to block approval; a single dissent does not.
44
- - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass runs after convergence to surface missed findings; its gaps are merged only after a 1-round adversarial reverify. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
44
+ - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass is dispatched concurrently with the first convergence reverify round to surface missed findings; its gaps are merged only after a 1-round adversarial reverify that follows convergence. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
45
45
  - Non-goals:
46
46
  - code-level micro-optimization unless it changes the implementation approach
47
47
  - **source code edits of any kind** — this run produces a plan document only; Edit/Write on project source files is forbidden until the plan is approved and a separate `implementation` run starts
@@ -53,7 +53,7 @@
53
53
  - **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <human-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only a human can answer this" (reporter intent, business priority, external authority). A row with `none` that *could* have been answered by the codebase is a defect.
54
54
  - Cross-verification mode:
55
55
  - Phase 5.5 convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker's finding by directly re-inspecting the cited evidence; the burden of proof sits on the claim. See `skills/okstra-convergence/SKILL.md` §"Adversarial Verification Mode". A single evidence-backed refutation prevents a finding from reaching consensus.
56
- - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass runs after convergence to surface missed findings; its gaps are merged only after a 1-round adversarial reverify. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
56
+ - **Coverage critic (opt-in)**: when `convergence.critic.enabled=true` (chosen via the okstra-run picker or `--critic`), a reused-worker critic pass is dispatched concurrently with the first convergence reverify round to surface missed findings; its gaps are merged only after a 1-round adversarial reverify that follows convergence. See `skills/okstra-convergence/SKILL.md` "Coverage critic pass".
57
57
  - Non-goals:
58
58
  - full implementation design unless it is required to decide the next phase
59
59
  - **source code edits, plan authoring, builds, or deployments** — this run only classifies the work and routes it; deeper analysis and planning belong to subsequent phases
@@ -181,6 +181,15 @@
181
181
  "options": { "proceed": "진행", "edit": "base-ref 다시 고르기", "abort": "중단" },
182
182
  "echo_template": "branch-confirm: {value}"
183
183
  },
184
+ "fix_cycle_confirm": {
185
+ "label": "이 task 는 release-handoff 까지 완료된 task 입니다. 이번 재진입을 기존 산출물에 대한 버그 픽스 사이클로 기록할까요?",
186
+ "options": {
187
+ "yes": "버그 픽스 사이클로 기록 (추천)",
188
+ "no": "일반 후속 작업 (기록 안 함)",
189
+ "abort": "중단"
190
+ },
191
+ "echo_template": "fix-cycle: {value}"
192
+ },
184
193
  "base_ref_text": {
185
194
  "label": "base ref 를 입력해주세요 (branch, tag, 또는 short/full SHA)",
186
195
  "echo_template": "base-ref: {value}"
@@ -51,6 +51,7 @@ def build_analysis_packet(
51
51
  clarification_response_path: Path | None,
52
52
  directive: str,
53
53
  instruction_set_relative_path: str,
54
+ fix_history_text: str = "",
54
55
  ) -> str:
55
56
  """Return the primary compact input for Claude/Codex/Gemini analysers."""
56
57
  brief_text = task_brief_path.read_text(encoding="utf-8")
@@ -71,6 +72,7 @@ def build_analysis_packet(
71
72
  parts.extend(_brief_block(brief_text))
72
73
  parts.extend(_profile_block(task_type, profile_text))
73
74
  parts.extend(_reference_block(reference_text))
75
+ parts.extend(_fix_history_block(fix_history_text))
74
76
  parts.extend(_clarification_block(clarification_text))
75
77
  parts.extend(_directive_block(directive))
76
78
  return "\n".join(part.rstrip() for part in parts).rstrip() + "\n"
@@ -158,6 +160,21 @@ def _reference_block(reference_text: str) -> list[str]:
158
160
  return ["", "## Reference Expectations", "", body]
159
161
 
160
162
 
163
+ def _fix_history_block(fix_history_text: str) -> list[str]:
164
+ if not fix_history_text.strip():
165
+ return []
166
+ return [
167
+ "",
168
+ "## Fix History",
169
+ "",
170
+ "Past bug-fix cycles registered on this task. Treat the open cycle's",
171
+ "symptom as a prior-defect signal when analysing.",
172
+ "",
173
+ fix_history_text,
174
+ "",
175
+ ]
176
+
177
+
161
178
  def _clarification_block(clarification_text: str) -> list[str]:
162
179
  if not clarification_text.strip():
163
180
  return []
@@ -299,3 +299,29 @@ def section_1_present_but_unparsed(report_text: str) -> bool:
299
299
  if SECTION_HEADING_PATTERN.search(report_text):
300
300
  return False
301
301
  return bool(_LOOSE_SECTION_1_RE.search(report_text))
302
+
303
+
304
+ def clarification_response_with_sidecars(source: Path) -> str:
305
+ """clarification-response 원문 뒤에 `user-responses/` 사이드카를 덧붙인 본문.
306
+
307
+ ``source`` 가 ``runs/<task-type>/reports/final-report-*.md`` 레이아웃일 때만
308
+ 형제 ``user-responses/`` 디렉토리를 찾는다(HTML 뷰의 `Export user response`
309
+ 가 내려준 파일을 사용자가 거기 저장). 그 외 경로·사이드카 부재 시 원문 그대로 —
310
+ 호출자는 항상 이 함수를 거쳐 instruction-set 사본을 쓰면 된다.
311
+ """
312
+ text = source.read_text(encoding="utf-8")
313
+ responses_dir = source.parent.parent / "user-responses"
314
+ if source.parent.name != "reports" or not responses_dir.is_dir():
315
+ return text
316
+ sidecars = sorted(
317
+ p for p in responses_dir.glob("user-response-*.md") if p.is_file()
318
+ )
319
+ if not sidecars:
320
+ return text
321
+ parts = [text.rstrip("\n"), "\n\n---\n\n# Attached User Responses\n"]
322
+ for sidecar in sidecars:
323
+ parts.append(
324
+ f"\n## {sidecar.name}\n\n"
325
+ f"{sidecar.read_text(encoding='utf-8').strip()}\n"
326
+ )
327
+ return "".join(parts)
@@ -306,19 +306,46 @@ def _analysis_worker_metric(task_root: Path, project_root: Path) -> dict:
306
306
  }
307
307
 
308
308
 
309
+ _SEQ_RESULT_RE = re.compile(r"-(\d{3,})\.md$")
310
+
311
+
312
+ def _current_seq_worker_results(run_dir: Path) -> list[Path]:
313
+ """현재 run seq 의 분석 worker 결과만 추린다.
314
+
315
+ 같은 worker-results 디렉토리에 seq-less 레거시(`codex-worker.md`)와 이전
316
+ seq 결과가 공존한다(관측: dev-9186 에 5월 레거시 72KB 혼입 → reportWriter
317
+ 표면 과대 측정). 실 dispatch 계약은 현재 seq 파일만 열거하므로 max seq 로
318
+ 스코핑한다."""
319
+ candidates = [
320
+ path for path in (run_dir / "worker-results").glob("*.md")
321
+ if "-audit-" not in path.name and "report-writer" not in path.name
322
+ ]
323
+ by_seq: dict[str, list[Path]] = {}
324
+ for path in candidates:
325
+ match = _SEQ_RESULT_RE.search(path.name)
326
+ if match:
327
+ by_seq.setdefault(match.group(1), []).append(path)
328
+ if not by_seq:
329
+ return []
330
+ return sorted(by_seq[max(by_seq)])
331
+
332
+
309
333
  def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: Path) -> dict:
310
334
  instruction_set = task_root / "instruction-set"
311
335
  files = [instruction_set / name for name in INPUT_FILES]
336
+ # 실 dispatch 계약(report-writer prompt 의 required reading)은 packet 이
337
+ # 있으면 analysis-material 대신 analysis-packet 을 읽는다 — worker metric
338
+ # 의 packet-primary 규칙과 동일.
339
+ packet = instruction_set / "analysis-packet.md"
340
+ if packet.is_file():
341
+ files = [path for path in files if path.name != "analysis-material.md"]
342
+ files.append(packet)
312
343
  files.extend([
313
344
  instruction_set / "final-report-template.md",
314
345
  instruction_set / "final-report-schema.json",
315
346
  ])
316
347
  if run_dir is not None:
317
- worker_results = run_dir / "worker-results"
318
- files.extend(
319
- path for path in worker_results.glob("*.md")
320
- if "-audit-" not in path.name and "report-writer" not in path.name
321
- )
348
+ files.extend(_current_seq_worker_results(run_dir))
322
349
  convergence = _latest_matching_file(
323
350
  run_dir / "state", f"convergence-{run_dir.name}-*.json"
324
351
  )
@@ -0,0 +1,172 @@
1
+ """Append-only reader/writer for `<task_root>/history/fix-cycles.jsonl`.
2
+
3
+ 완료(release-handoff)된 task 에 재진입하는 버그 핫픽스 run 묶음(fix cycle)의
4
+ SSOT. consumers.jsonl 과 같은 idiom — append-only + dir flock + last-wins 읽기.
5
+ 이 모듈이 유일한 reader/writer 이며, 소비처(analysis-packet / manifest /
6
+ final-report / okstra-brief)는 모두 summarize()/packet_summary() 파생 뷰를 쓴다.
7
+
8
+ 행 3종 (event 필드로 구분):
9
+ {"event":"opened","cycle":"fc-01","target_report":...,"symptom":...,"opened_at":...}
10
+ {"event":"run","cycle":"fc-01","task_type":...,"run_seq":...,"run_manifest":...}
11
+ {"event":"closed","cycle":"fc-01","closed_by":...,"report":...,"closed_at":...}
12
+
13
+ idempotency 키: (cycle, event, run_manifest|None). open cycle 은 task 당 1개.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import re
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from .run_context import dir_flock
23
+
24
+ FIX_CYCLES_FILENAME = "fix-cycles.jsonl"
25
+ _LOCK_FILENAME = ".fix-cycles.lock"
26
+
27
+ # 완료(release-handoff) task 에 fix-cycle 로 재진입할 수 있는 entry phase 들.
28
+ # prepare(run.py) 의 게이트와 wizard 의 감지 술어가 공유하는 SSOT.
29
+ FIX_CYCLE_ENTRY_PHASES = (
30
+ "requirements-discovery", "error-analysis", "implementation-planning")
31
+
32
+
33
+ def fix_cycles_path(task_root: Path) -> Path:
34
+ return Path(task_root) / "history" / FIX_CYCLES_FILENAME
35
+
36
+
37
+ def read_rows(task_root: Path) -> List[Dict[str, Any]]:
38
+ p = fix_cycles_path(task_root)
39
+ if not p.exists():
40
+ return []
41
+ out: List[Dict[str, Any]] = []
42
+ for line in p.read_text(encoding="utf-8").splitlines():
43
+ line = line.strip()
44
+ if line:
45
+ out.append(json.loads(line))
46
+ return out
47
+
48
+
49
+ def open_cycle(rows: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
50
+ """closed 행이 없는 마지막 opened 행. open 은 동시 1개가 불변식."""
51
+ closed = {r["cycle"] for r in rows if r.get("event") == "closed"}
52
+ for r in reversed(rows):
53
+ if r.get("event") == "opened" and r["cycle"] not in closed:
54
+ return r
55
+ return None
56
+
57
+
58
+ def _next_cycle_id(rows: List[Dict[str, Any]]) -> str:
59
+ n = sum(1 for r in rows if r.get("event") == "opened")
60
+ return f"fc-{n + 1:02d}"
61
+
62
+
63
+ def append_opened(task_root: Path, *, target_report: str, symptom: str,
64
+ opened_at: str) -> str:
65
+ """새 cycle 을 열고 id 를 반환. open cycle 이 이미 있으면 그 id 반환(멱등)."""
66
+ history = fix_cycles_path(task_root).parent
67
+ with dir_flock(history, _LOCK_FILENAME):
68
+ rows = read_rows(task_root)
69
+ existing = open_cycle(rows)
70
+ if existing:
71
+ return existing["cycle"]
72
+ cycle = _next_cycle_id(rows)
73
+ _append_row(task_root, {
74
+ "event": "opened", "cycle": cycle,
75
+ "target_report": target_report, "symptom": symptom,
76
+ "opened_at": opened_at,
77
+ })
78
+ return cycle
79
+
80
+
81
+ def append_run(task_root: Path, *, cycle: str, task_type: str, run_seq: int,
82
+ run_manifest: str) -> None:
83
+ history = fix_cycles_path(task_root).parent
84
+ with dir_flock(history, _LOCK_FILENAME):
85
+ for r in read_rows(task_root):
86
+ if (r.get("event") == "run" and r.get("cycle") == cycle
87
+ and r.get("run_manifest") == run_manifest):
88
+ return
89
+ _append_row(task_root, {
90
+ "event": "run", "cycle": cycle, "task_type": task_type,
91
+ "run_seq": run_seq, "run_manifest": run_manifest,
92
+ })
93
+
94
+
95
+ def append_closed(task_root: Path, *, cycle: str, closed_by: str,
96
+ report: str, closed_at: str) -> None:
97
+ history = fix_cycles_path(task_root).parent
98
+ with dir_flock(history, _LOCK_FILENAME):
99
+ for r in read_rows(task_root):
100
+ if r.get("event") == "closed" and r.get("cycle") == cycle:
101
+ return
102
+ _append_row(task_root, {
103
+ "event": "closed", "cycle": cycle, "closed_by": closed_by,
104
+ "report": report, "closed_at": closed_at,
105
+ })
106
+
107
+
108
+ def _append_row(task_root: Path, record: Dict[str, Any]) -> None:
109
+ p = fix_cycles_path(task_root)
110
+ p.parent.mkdir(parents=True, exist_ok=True)
111
+ with p.open("a", encoding="utf-8") as f:
112
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
113
+
114
+
115
+ def summarize(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
116
+ """소비처 공용 요약: {count, openCycleId, latest:{cycle,symptom,targetReport,closedAt}}."""
117
+ opened = [r for r in rows if r.get("event") == "opened"]
118
+ if not opened:
119
+ return {"count": 0, "openCycleId": None, "latest": None}
120
+ closed_at = {r["cycle"]: r.get("closed_at")
121
+ for r in rows if r.get("event") == "closed"}
122
+ open_row = open_cycle(rows)
123
+ latest = opened[-1]
124
+ return {
125
+ "count": len(opened),
126
+ "openCycleId": open_row["cycle"] if open_row else None,
127
+ "latest": {
128
+ "cycle": latest["cycle"],
129
+ "symptom": latest.get("symptom", ""),
130
+ "targetReport": latest.get("target_report", ""),
131
+ "closedAt": closed_at.get(latest["cycle"]),
132
+ },
133
+ }
134
+
135
+
136
+ def packet_summary(rows: List[Dict[str, Any]]) -> str:
137
+ """analysis-packet 의 `## Fix History` 섹션 본문 (마크다운). 행이 없으면 ''."""
138
+ opened = [r for r in rows if r.get("event") == "opened"]
139
+ if not opened:
140
+ return ""
141
+ closed = {r["cycle"]: r for r in rows if r.get("event") == "closed"}
142
+ lines: List[str] = []
143
+ for o in opened:
144
+ state = "closed" if o["cycle"] in closed else "open"
145
+ lines.append(
146
+ f"- `{o['cycle']}` ({state}) — {o.get('symptom', '')} "
147
+ f"(target: `{o.get('target_report', '')}`)")
148
+ for r in rows:
149
+ if r.get("event") == "run" and r.get("cycle") == o["cycle"]:
150
+ lines.append(
151
+ f" - run: {r.get('task_type', '')} seq {r.get('run_seq', '')}"
152
+ f" (`{r.get('run_manifest', '')}`)")
153
+ return "\n".join(lines)
154
+
155
+
156
+ _REQUEST_SUMMARY_RE = re.compile(
157
+ r"^## Request Summary\s*$", re.MULTILINE)
158
+
159
+
160
+ def derive_symptom(brief_text: str) -> str:
161
+ """brief 의 `## Request Summary` 첫 비어있지 않은 줄 (불릿 마커 제거)."""
162
+ m = _REQUEST_SUMMARY_RE.search(brief_text)
163
+ if not m:
164
+ return ""
165
+ for line in brief_text[m.end():].splitlines():
166
+ line = line.strip()
167
+ if not line:
168
+ continue
169
+ if line.startswith("#"):
170
+ return ""
171
+ return line.lstrip("-* ").strip()
172
+ return ""