okstra 0.102.2 → 0.102.3

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 (161) hide show
  1. package/README.kr.md +5 -5
  2. package/README.md +5 -5
  3. package/docs/kr/architecture/storage-model.md +1 -1
  4. package/docs/kr/architecture.md +7 -11
  5. package/docs/kr/cli.md +4 -4
  6. package/docs/kr/container.md +1 -1
  7. package/docs/kr/performance-improvement-plan-v2.md +2 -2
  8. package/docs/project-structure-overview.md +2 -2
  9. package/package.json +7 -2
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/okstra-antigravity-exec.sh +1 -2
  12. package/runtime/bin/okstra-codex-exec.sh +1 -2
  13. package/runtime/bin/okstra-render-final-report.py +2 -9
  14. package/runtime/bin/okstra-spawn-followups.py +4 -6
  15. package/runtime/bin/okstra-trace-cleanup.sh +24 -4
  16. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  17. package/runtime/prompts/lead/report-writer.md +1 -1
  18. package/runtime/prompts/profiles/_common-contract.md +9 -11
  19. package/runtime/python/okstra_ctl/codex_dispatch.py +4 -12
  20. package/runtime/python/okstra_ctl/consumers.py +35 -0
  21. package/runtime/python/okstra_ctl/container.py +4 -6
  22. package/runtime/python/okstra_ctl/dispatch_core.py +4 -12
  23. package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
  24. package/runtime/python/okstra_ctl/handoff.py +2 -1
  25. package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
  26. package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
  27. package/runtime/python/okstra_ctl/report_views.py +7 -4
  28. package/runtime/python/okstra_ctl/run.py +3 -10
  29. package/runtime/python/okstra_ctl/stage_targets.py +18 -0
  30. package/runtime/python/okstra_ctl/wizard.py +24 -25
  31. package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
  32. package/runtime/python/okstra_token_usage/report.py +2 -5
  33. package/runtime/validators/validate-report-views.py +2 -1
  34. package/runtime/validators/validate-run.py +1 -11
  35. package/src/commands/lifecycle/install.mjs +5 -5
  36. package/src/commands/lifecycle/uninstall.mjs +4 -4
  37. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
  38. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
  39. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
  40. package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
  41. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
  42. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
  43. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
  44. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
  45. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
  46. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
  47. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
  48. package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
  49. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
  50. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
  51. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
  52. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
  53. package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
  54. package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
  55. package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
  56. package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
  57. package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
  58. package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
  59. package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
  60. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
  61. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
  62. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
  63. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
  64. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
  65. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
  66. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
  67. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
  68. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
  69. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
  70. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
  71. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
  72. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
  73. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
  74. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
  75. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
  76. package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
  77. package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
  78. package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
  79. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
  80. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
  81. package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
  82. package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
  83. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
  84. package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
  85. package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
  86. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
  87. package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
  88. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
  89. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
  90. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
  91. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
  92. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
  93. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
  94. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
  95. package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
  96. package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
  97. package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
  98. package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
  99. package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
  100. package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
  101. package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
  102. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
  103. package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
  104. package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
  105. package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
  106. package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
  107. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
  108. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
  109. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
  110. package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
  111. package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
  112. package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
  113. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
  114. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
  115. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
  116. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
  117. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
  118. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
  119. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
  120. package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
  121. package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
  122. package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
  123. package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
  124. package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
  125. package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
  126. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
  127. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
  128. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
  129. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
  130. package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
  131. package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
  132. package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
  133. package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
  134. package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
  135. package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
  136. package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
  137. package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
  138. package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
  139. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
  140. package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
  141. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
  142. package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
  143. package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
  144. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
  145. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
  146. package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
  147. package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
  148. package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
  149. package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
  150. package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
  151. package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
  152. package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
  153. package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
  154. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
  155. package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
  156. package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
  157. package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
  158. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
  159. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
  160. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
  161. package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +0 -110
@@ -1,1019 +0,0 @@
1
- # 에러 환류 루프 (cross-project → okstra 자가수정) 구현 계획
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** 머신 내 모든 타겟 프로젝트에 흩어진 okstra 실행 에러를 cross-project 로 수집·집계·익명화해 `.zip` 으로 묶는 `okstra error-zip` facet 과, 그 zip 을 okstra 자신을 고치는 error-analysis brief 로 변환하는 `okstra-brief` variant 를 추가한다.
6
-
7
- **Architecture:** 수집·집계·익명화·zip 생성은 결정적 Python(`scripts/okstra_ctl/error_zip.py`)이 맡고, 기존 `error_report.py` 의 글롭/파싱/집계 코어를 공용 모듈로 추출해 공유한다. Node 래퍼(`src/commands/inspect/error-zip.mjs`)는 python 모듈을 spawn 하는 얇은 어댑터이며, `okstra-inspect` SKILL 에 facet 으로 노출한다. 수정은 신규 phase 없이 기존 `error-analysis → implementation-planning → implementation` lifecycle 을 재사용한다.
8
-
9
- **Tech Stack:** Python 3 (stdlib `json`/`pathlib`/`zipfile`/`argparse`), Node ES modules, pytest, okstra 빌드(`npm run build`).
10
-
11
- ## Global Constraints
12
-
13
- - **runtime/ 직접 수정 금지** — `runtime/python/`, `runtime/` 전체는 빌드 산출물. 소스는 `scripts/`·`src/`·`skills/` 만 고치고, 마지막에 `npm run build` 로 `runtime/` 동기화. (CLAUDE.md "runtime/ 은 build output")
14
- - **errorType 비결합(data-driven)** — 집계·클러스터링·익명화는 특정 errorType 값(`tool-failure`/`cli-failure`/`contract-violation`)에 case 분기하지 않는다. errorType 은 레코드에서 읽은 값을 그대로 그룹 키로 쓴다. errorType 목록이 필요한 유일한 지점은 `scripts/okstra-error-log.py:18` 의 `ALLOWED_ERROR_TYPES` 를 import 해 참조한다 — error_zip 코드에 목록을 재정의하지 않는다.
15
- - **익명화는 allowlist 기반** — 출력 레코드에 포함할 필드를 명시 화이트리스트로 정하고, 그 외 키는 드롭한다. (denylist 금지)
16
- - **수집기는 읽기 전용** — 타겟의 `.okstra/` 정본 JSONL 을 삭제·이동·수정하지 않는다.
17
- - **문서/주석 한국어** — SKILL 본문, 코드 주석의 산문은 한국어. 코드 식별자·경로·CLI 인자는 영어.
18
- - **단일참조** — recent/active 읽기는 `okstra_ctl.jsonl.read_jsonl` / `okstra_ctl.paths.okstra_home` 재사용. 글롭/집계는 신규 `error_log_core` 공용 모듈 1곳.
19
- - **함수 50줄 상한** — 신규/대폭수정 함수는 유효 50줄 이하. 초과 시 분할.
20
-
21
- ---
22
-
23
- ### Task 1: 글롭/파싱/집계 코어를 공용 모듈로 추출
24
-
25
- `error_report.py` 의 순수·읽기전용 함수(`_glob_error_logs`, `_parse_records`, `_tally`, `_rows`, `_aggregate`)를 신규 `error_log_core.py` 로 옮기고, `error_report.py` 는 거기서 import 한다. 동작 변화 없는 리팩터 — 기존 테스트가 green 으로 남아야 한다.
26
-
27
- **Files:**
28
- - Create: `scripts/okstra_ctl/error_log_core.py`
29
- - Modify: `scripts/okstra_ctl/error_report.py:18-70` (함수 정의 제거 후 import 로 대체)
30
- - Test: `tests/inspect/test_error_log_core.py` (신규), `tests/inspect/test_okstra_error_report.py` (기존, 회귀)
31
-
32
- **Interfaces:**
33
- - Produces:
34
- - `glob_error_logs(task_root: Path) -> list[Path]`
35
- - `parse_records(paths: list[Path]) -> tuple[list[dict], int]` — 각 dict 에 `_sourceLog`(str 절대경로) 주입
36
- - `tally(records: list[dict], key: str) -> dict[str, int]`
37
- - `rows(counts: dict[str, int], label: str) -> list[dict]`
38
- - `aggregate(records: list[dict]) -> dict` — `{errorCount, runCount, byErrorType, bySource, byPhase, byAgent}`
39
-
40
- - [ ] **Step 1: 코어 단위 테스트 작성 (실패하도록)**
41
-
42
- Create `tests/inspect/test_error_log_core.py`:
43
-
44
- ```python
45
- import json
46
- import sys
47
- from pathlib import Path
48
- from _paths import REPO_ROOT
49
-
50
- LIB_DIR = REPO_ROOT / "scripts"
51
- if str(LIB_DIR) not in sys.path:
52
- sys.path.insert(0, str(LIB_DIR))
53
-
54
- from okstra_ctl import error_log_core as core
55
-
56
-
57
- def _write_log(task_root: Path, run_seg: str, seq: str, records: list[dict]) -> Path:
58
- log_dir = task_root / "runs" / run_seg / "logs"
59
- log_dir.mkdir(parents=True, exist_ok=True)
60
- path = log_dir / f"errors-{run_seg.split('/')[0]}-{seq}.jsonl"
61
- with path.open("w", encoding="utf-8") as fh:
62
- for rec in records:
63
- fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
64
- return path
65
-
66
-
67
- def test_glob_finds_flat_and_staged(tmp_path):
68
- _write_log(tmp_path, "implementation", "001", [{"errorType": "x"}])
69
- _write_log(tmp_path, "implementation/stage-1", "001", [{"errorType": "y"}])
70
- found = core.glob_error_logs(tmp_path)
71
- assert len(found) == 2
72
-
73
-
74
- def test_parse_injects_source_log_and_counts_skips(tmp_path):
75
- p = _write_log(tmp_path, "implementation", "001", [{"errorType": "a"}])
76
- p.open("a", encoding="utf-8").write("not-json\n")
77
- records, skipped = core.parse_records([p])
78
- assert len(records) == 1
79
- assert skipped == 1
80
- assert records[0]["_sourceLog"] == str(p)
81
-
82
-
83
- def test_aggregate_is_errortype_agnostic(tmp_path):
84
- records = [
85
- {"errorType": "brand-new-type", "source": "s", "phase": "p", "agent": "a", "_sourceLog": "L1"},
86
- {"errorType": "brand-new-type", "source": "s", "phase": "p", "agent": "a", "_sourceLog": "L1"},
87
- ]
88
- agg = core.aggregate(records)
89
- assert agg["errorCount"] == 2
90
- assert agg["byErrorType"] == {"brand-new-type": 2}
91
- assert agg["runCount"] == 1
92
- ```
93
-
94
- - [ ] **Step 2: 실패 확인**
95
-
96
- Run: `python3 -m pytest tests/inspect/test_error_log_core.py -v`
97
- Expected: FAIL — `ModuleNotFoundError: No module named 'okstra_ctl.error_log_core'`
98
-
99
- - [ ] **Step 3: 코어 모듈 작성**
100
-
101
- Create `scripts/okstra_ctl/error_log_core.py` (기존 `error_report.py:18-70` 의 본문을 public 이름으로 그대로 이전):
102
-
103
- ```python
104
- """errors-*.jsonl 글롭·파싱·집계 공용 코어 (read-only).
105
-
106
- error_report(단일 task)와 error_zip(cross-project)이 공유한다.
107
- errorType 값에 분기하지 않는 data-driven 집계.
108
- """
109
- from __future__ import annotations
110
-
111
- import json
112
- from pathlib import Path
113
-
114
-
115
- def glob_error_logs(task_root: Path) -> list[Path]:
116
- runs = task_root / "runs"
117
- if not runs.exists():
118
- return []
119
- flat = runs.glob("*/logs/errors-*.jsonl")
120
- staged = runs.glob("*/stage-*/logs/errors-*.jsonl")
121
- return sorted(set(flat) | set(staged))
122
-
123
-
124
- def parse_records(paths: list[Path]) -> tuple[list[dict], int]:
125
- records: list[dict] = []
126
- skipped = 0
127
- for path in paths:
128
- for line in path.read_text(encoding="utf-8").splitlines():
129
- line = line.strip()
130
- if not line:
131
- continue
132
- try:
133
- rec = json.loads(line)
134
- except Exception:
135
- skipped += 1
136
- continue
137
- if isinstance(rec, dict):
138
- rec["_sourceLog"] = str(path)
139
- records.append(rec)
140
- else:
141
- skipped += 1
142
- return records, skipped
143
-
144
-
145
- def tally(records: list[dict], key: str) -> dict:
146
- out: dict[str, int] = {}
147
- for rec in records:
148
- name = str(rec.get(key, ""))
149
- out[name] = out.get(name, 0) + 1
150
- return out
151
-
152
-
153
- def rows(counts: dict, label: str) -> list[dict]:
154
- return [
155
- {label: name, "count": n}
156
- for name, n in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
157
- ]
158
-
159
-
160
- def aggregate(records: list[dict]) -> dict:
161
- return {
162
- "errorCount": len(records),
163
- "runCount": len({rec.get("_sourceLog", "") for rec in records}),
164
- "byErrorType": tally(records, "errorType"),
165
- "bySource": tally(records, "source"),
166
- "byPhase": rows(tally(records, "phase"), "phase"),
167
- "byAgent": rows(tally(records, "agent"), "agent"),
168
- }
169
- ```
170
-
171
- - [ ] **Step 4: error_report.py 를 코어 import 로 전환**
172
-
173
- In `scripts/okstra_ctl/error_report.py`, 기존 `_glob_error_logs`/`_parse_records`/`_tally`/`_rows`/`_aggregate` 정의를 삭제하고 상단 import 부에 추가:
174
-
175
- ```python
176
- from okstra_ctl.error_log_core import (
177
- glob_error_logs,
178
- parse_records,
179
- aggregate,
180
- )
181
- ```
182
-
183
- 그리고 `build_and_write` 내부 호출부를 새 이름으로 교체:
184
- - `_glob_error_logs(task_root)` → `glob_error_logs(task_root)`
185
- - `_parse_records(paths)` → `parse_records(paths)`
186
- - `_aggregate(records)` → `aggregate(records)`
187
-
188
- (`render_markdown`, `_md_table` 등 렌더 함수는 error_report 에 그대로 둔다 — 코어가 아님.)
189
-
190
- - [ ] **Step 5: 코어 + 회귀 테스트 통과 확인**
191
-
192
- Run: `python3 -m pytest tests/inspect/test_error_log_core.py tests/inspect/test_okstra_error_report.py -v`
193
- Expected: PASS (신규 3개 + 기존 error_report 테스트 전부)
194
-
195
- - [ ] **Step 6: 커밋**
196
-
197
- ```bash
198
- git add scripts/okstra_ctl/error_log_core.py scripts/okstra_ctl/error_report.py tests/inspect/test_error_log_core.py
199
- git commit -m "refactor(inspect): errors 로그 글롭/집계 코어를 error_log_core 로 추출"
200
- ```
201
-
202
- ---
203
-
204
- ### Task 2: cross-project run-디렉터리 수집 reader
205
-
206
- 글로벌 run-index(`recent.jsonl`+`active.jsonl`)를 읽어 모든 타겟의 run 디렉터리를 복원하고, 그 밑 errors 로그를 모아 레코드로 파싱한다. 도달 불가(프로젝트 삭제) run 은 skip + 카운트.
207
-
208
- **Files:**
209
- - Create: `scripts/okstra_ctl/error_zip.py`
210
- - Test: `tests/inspect/test_okstra_error_zip.py` (신규)
211
-
212
- **Interfaces:**
213
- - Consumes: `error_log_core.parse_records` (Task 1), `okstra_ctl.jsonl.read_jsonl`, `okstra_ctl.paths.okstra_home`
214
- - Produces:
215
- - `collect_records(home: Path) -> tuple[list[dict], dict]` — `(records, stats)` 에서 `stats = {"runCount": int, "unreachableRuns": int, "projectRoots": list[str]}`. 각 record 는 `parse_records` 의 `_sourceLog` + 신규 `_projectRoot`(str) 키를 가짐.
216
-
217
- - [ ] **Step 1: 수집 테스트 작성 (실패하도록)**
218
-
219
- Create `tests/inspect/test_okstra_error_zip.py`:
220
-
221
- ```python
222
- import json
223
- import sys
224
- from pathlib import Path
225
- from _paths import REPO_ROOT
226
-
227
- LIB_DIR = REPO_ROOT / "scripts"
228
- if str(LIB_DIR) not in sys.path:
229
- sys.path.insert(0, str(LIB_DIR))
230
-
231
- from okstra_ctl import error_zip
232
- from okstra_ctl.jsonl import append_jsonl
233
- from okstra_ctl.paths import okstra_home
234
-
235
-
236
- def _rec(**over) -> dict:
237
- base = {
238
- "ts": "2026-06-17T01:00:00+00:00",
239
- "taskKey": "projA:g:DEV-1",
240
- "phase": "implementation",
241
- "agent": "claude-worker",
242
- "agentRole": "worker",
243
- "source": "worker-reported",
244
- "errorType": "cli-failure",
245
- "command": "pytest",
246
- "exitCode": 1,
247
- "message": "boom",
248
- "stderrExcerpt": "trace...",
249
- }
250
- base.update(over)
251
- return base
252
-
253
-
254
- def _seed_project(project_root: Path, run_dir_rel: str, records: list[dict]) -> None:
255
- log_dir = project_root / run_dir_rel / "logs"
256
- log_dir.mkdir(parents=True, exist_ok=True)
257
- seq_name = Path(run_dir_rel).name
258
- path = log_dir / f"errors-{seq_name}-001.jsonl"
259
- with path.open("w", encoding="utf-8") as fh:
260
- for rec in records:
261
- fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
262
-
263
-
264
- def _index_row(project_root: Path, run_dir_rel: str) -> dict:
265
- return {
266
- "runId": "r1", "projectId": "projA", "projectRoot": str(project_root),
267
- "taskGroup": "g", "taskId": "DEV-1", "taskKey": "g/DEV-1",
268
- "taskType": "implementation", "runSeq": 1, "status": "done",
269
- "startedAt": "2026-06-17T01:00:00+00:00", "finishedAt": None,
270
- "runDirRel": run_dir_rel,
271
- }
272
-
273
-
274
- def test_collect_records_across_two_projects(tmp_path):
275
- home = okstra_home()
276
- p1 = tmp_path / "proj1"
277
- p2 = tmp_path / "proj2"
278
- rel1 = ".okstra/tasks/g/DEV-1/runs/implementation"
279
- rel2 = ".okstra/tasks/g/DEV-2/runs/implementation"
280
- _seed_project(p1, rel1, [_rec(), _rec(errorType="tool-failure")])
281
- _seed_project(p2, rel2, [_rec(taskKey="projB:g:DEV-2")])
282
- append_jsonl(home / "recent.jsonl", _index_row(p1, rel1))
283
- append_jsonl(home / "recent.jsonl", _index_row(p2, rel2))
284
-
285
- records, stats = error_zip.collect_records(home)
286
- assert len(records) == 3
287
- assert stats["runCount"] == 2
288
- assert stats["unreachableRuns"] == 0
289
- assert set(stats["projectRoots"]) == {str(p1), str(p2)}
290
-
291
-
292
- def test_collect_skips_unreachable_project(tmp_path):
293
- home = okstra_home()
294
- gone = tmp_path / "deleted-project"
295
- rel = ".okstra/tasks/g/DEV-1/runs/implementation"
296
- append_jsonl(home / "recent.jsonl", _index_row(gone, rel))
297
- records, stats = error_zip.collect_records(home)
298
- assert records == []
299
- assert stats["unreachableRuns"] == 1
300
- ```
301
-
302
- (`append_jsonl` 시그니처 확인: `okstra_ctl.jsonl.append_jsonl(path: Path, row: dict)`. 다르면 Step 3 에서 맞춘다.)
303
-
304
- - [ ] **Step 2: 실패 확인**
305
-
306
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py -v`
307
- Expected: FAIL — `ModuleNotFoundError: No module named 'okstra_ctl.error_zip'`
308
-
309
- - [ ] **Step 3: 수집 reader 작성**
310
-
311
- Create `scripts/okstra_ctl/error_zip.py`:
312
-
313
- ```python
314
- """cross-project errors-*.jsonl 수집·집계·익명화·zip 생성 (read-only).
315
-
316
- 글로벌 run-index(recent/active)를 순회해 모든 타겟의 에러 로그를 모은다.
317
- errorType 값에 분기하지 않는 data-driven 처리.
318
- """
319
- from __future__ import annotations
320
-
321
- from pathlib import Path
322
-
323
- from okstra_ctl.error_log_core import parse_records
324
- from okstra_ctl.jsonl import read_jsonl
325
-
326
-
327
- def _run_dirs(home: Path) -> list[tuple[str, Path]]:
328
- rows = read_jsonl(home / "recent.jsonl") + read_jsonl(home / "active.jsonl")
329
- seen: set[tuple[str, str]] = set()
330
- out: list[tuple[str, Path]] = []
331
- for row in rows:
332
- project_root = str(row.get("projectRoot", ""))
333
- run_dir_rel = str(row.get("runDirRel", ""))
334
- if not project_root or not run_dir_rel:
335
- continue
336
- key = (project_root, run_dir_rel)
337
- if key in seen:
338
- continue
339
- seen.add(key)
340
- out.append((project_root, Path(project_root) / run_dir_rel))
341
- return out
342
-
343
-
344
- def _logs_in_run_dir(run_dir: Path) -> list[Path]:
345
- flat = run_dir.glob("logs/errors-*.jsonl")
346
- staged = run_dir.glob("stage-*/logs/errors-*.jsonl")
347
- return sorted(set(flat) | set(staged))
348
-
349
-
350
- def collect_records(home: Path) -> tuple[list[dict], dict]:
351
- records: list[dict] = []
352
- project_roots: set[str] = set()
353
- reachable = 0
354
- unreachable = 0
355
- for project_root, run_dir in _run_dirs(home):
356
- if not run_dir.exists():
357
- unreachable += 1
358
- continue
359
- logs = _logs_in_run_dir(run_dir)
360
- if not logs:
361
- continue
362
- reachable += 1
363
- recs, _ = parse_records(logs)
364
- for rec in recs:
365
- rec["_projectRoot"] = project_root
366
- records.append(rec)
367
- project_roots.add(project_root)
368
- stats = {
369
- "runCount": reachable,
370
- "unreachableRuns": unreachable,
371
- "projectRoots": sorted(project_roots),
372
- }
373
- return records, stats
374
- ```
375
-
376
- - [ ] **Step 4: 통과 확인**
377
-
378
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py -v`
379
- Expected: PASS (2개). `append_jsonl` 시그니처가 다르면 테스트의 호출을 실제 시그니처에 맞춘 뒤 재실행.
380
-
381
- - [ ] **Step 5: 커밋**
382
-
383
- ```bash
384
- git add scripts/okstra_ctl/error_zip.py tests/inspect/test_okstra_error_zip.py
385
- git commit -m "feat(inspect): error-zip cross-project run-디렉터리 수집 reader"
386
- ```
387
-
388
- ---
389
-
390
- ### Task 3: allowlist 기반 익명화
391
-
392
- cross-project 레코드의 타겟 고유 정보를 축약한다. projectRoot 목록을 안정 토큰(`proj-1`, `proj-2`...)으로 매핑하고, 절대경로 문자열을 토큰으로 치환하며, 출력 필드를 화이트리스트로 제한한다.
393
-
394
- **Files:**
395
- - Modify: `scripts/okstra_ctl/error_zip.py`
396
- - Test: `tests/inspect/test_okstra_error_zip.py`
397
-
398
- **Interfaces:**
399
- - Consumes: `collect_records` 출력 (Task 2)
400
- - Produces:
401
- - `KEEP_FIELDS: tuple[str, ...]` — 보존 필드 화이트리스트
402
- - `anonymize(records: list[dict]) -> tuple[list[dict], dict]` — `(clean_records, project_token_map)`. project_token_map = `{projectRoot: token}`.
403
-
404
- - [ ] **Step 1: 익명화 테스트 작성 (실패하도록)**
405
-
406
- Append to `tests/inspect/test_okstra_error_zip.py`:
407
-
408
- ```python
409
- def test_anonymize_tokenizes_paths_and_keeps_allowlist():
410
- records = [
411
- {
412
- "ts": "2026-06-17T01:00:00+00:00",
413
- "taskKey": "projA:g:DEV-1",
414
- "phase": "implementation", "agent": "claude-worker",
415
- "agentRole": "worker", "source": "worker-reported",
416
- "errorType": "cli-failure", "command": "pytest",
417
- "exitCode": 1, "message": "fail at /Users/me/proj1/src/a.py",
418
- "stderrExcerpt": "trace /Users/me/proj1/x",
419
- "_sourceLog": "/Users/me/proj1/.okstra/tasks/g/DEV-1/runs/implementation/logs/errors-implementation-001.jsonl",
420
- "_projectRoot": "/Users/me/proj1",
421
- "secretInternalField": "DROP-ME",
422
- }
423
- ]
424
- clean, token_map = error_zip.anonymize(records)
425
- rec = clean[0]
426
- assert token_map == {"/Users/me/proj1": "proj-1"}
427
- assert "secretInternalField" not in rec
428
- assert "/Users/me/proj1" not in rec["message"]
429
- assert "proj-1" in rec["message"]
430
- assert rec["errorType"] == "cli-failure" # data-driven 값 보존
431
- assert rec["sourceProject"] == "proj-1"
432
- assert rec["taskKey"] == "proj-1:g:DEV-1" # projectId 세그 치환
433
- ```
434
-
435
- - [ ] **Step 2: 실패 확인**
436
-
437
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py::test_anonymize_tokenizes_paths_and_keeps_allowlist -v`
438
- Expected: FAIL — `AttributeError: module 'okstra_ctl.error_zip' has no attribute 'anonymize'`
439
-
440
- - [ ] **Step 3: 익명화 구현**
441
-
442
- Append to `scripts/okstra_ctl/error_zip.py`:
443
-
444
- ```python
445
- KEEP_FIELDS = (
446
- "ts", "taskKey", "phase", "agent", "agentRole", "source",
447
- "errorType", "command", "exitCode", "message", "stderrExcerpt",
448
- )
449
-
450
-
451
- def _token_map(records: list[dict]) -> dict:
452
- roots = sorted({str(r.get("_projectRoot", "")) for r in records if r.get("_projectRoot")})
453
- return {root: f"proj-{i}" for i, root in enumerate(roots, start=1)}
454
-
455
-
456
- def _scrub(value: object, token_map: dict) -> object:
457
- if not isinstance(value, str):
458
- return value
459
- out = value
460
- for root, token in token_map.items():
461
- if root:
462
- out = out.replace(root, token)
463
- return out
464
-
465
-
466
- def anonymize(records: list[dict]) -> tuple[list[dict], dict]:
467
- token_map = _token_map(records)
468
- clean: list[dict] = []
469
- for rec in records:
470
- root = str(rec.get("_projectRoot", ""))
471
- token = token_map.get(root, "proj-?")
472
- item = {k: _scrub(rec.get(k), token_map) for k in KEEP_FIELDS if k in rec}
473
- task_key = str(rec.get("taskKey", ""))
474
- if ":" in task_key:
475
- item["taskKey"] = token + task_key[task_key.index(":"):]
476
- item["sourceProject"] = token
477
- clean.append(item)
478
- return clean, token_map
479
- ```
480
-
481
- - [ ] **Step 4: 통과 확인**
482
-
483
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py -v`
484
- Expected: PASS (수집 2개 + 익명화 1개)
485
-
486
- - [ ] **Step 5: 커밋**
487
-
488
- ```bash
489
- git add scripts/okstra_ctl/error_zip.py tests/inspect/test_okstra_error_zip.py
490
- git commit -m "feat(inspect): error-zip allowlist 기반 레코드 익명화"
491
- ```
492
-
493
- ---
494
-
495
- ### Task 4: 클러스터 집계 + markdown 리포트 렌더
496
-
497
- 익명화된 레코드를 (errorType + phase + agent + 정규화된 message) 동일성으로 클러스터링하고, 빈도순 집계 markdown 을 만든다. errorType 은 값 비교만 — case 분기 없음.
498
-
499
- **Files:**
500
- - Modify: `scripts/okstra_ctl/error_zip.py`
501
- - Test: `tests/inspect/test_okstra_error_zip.py`
502
-
503
- **Interfaces:**
504
- - Consumes: `anonymize` 출력 (Task 3), `error_log_core.aggregate` (Task 1)
505
- - Produces:
506
- - `cluster_key(rec: dict) -> str` — message 의 숫자/경로 토큰을 정규화한 묶음 키
507
- - `build_clusters(records: list[dict]) -> list[dict]` — `[{key, errorType, phase, agent, count, projects, sample}]` count 내림차순
508
- - `render_report(*, records: list[dict], clusters: list[dict], stats: dict, generated_at: str) -> str`
509
-
510
- - [ ] **Step 1: 클러스터 테스트 작성 (실패하도록)**
511
-
512
- Append to `tests/inspect/test_okstra_error_zip.py`:
513
-
514
- ```python
515
- def test_clusters_group_by_normalized_message():
516
- records = [
517
- {"errorType": "cli-failure", "phase": "impl", "agent": "codex-worker",
518
- "message": "timeout after 124s", "sourceProject": "proj-1"},
519
- {"errorType": "cli-failure", "phase": "impl", "agent": "codex-worker",
520
- "message": "timeout after 999s", "sourceProject": "proj-2"},
521
- {"errorType": "tool-failure", "phase": "impl", "agent": "claude-worker",
522
- "message": "Edit denied", "sourceProject": "proj-1"},
523
- ]
524
- clusters = error_zip.build_clusters(records)
525
- assert clusters[0]["count"] == 2 # 두 timeout 이 한 클러스터
526
- assert clusters[0]["errorType"] == "cli-failure"
527
- assert set(clusters[0]["projects"]) == {"proj-1", "proj-2"}
528
- assert any(c["errorType"] == "tool-failure" for c in clusters)
529
-
530
-
531
- def test_render_report_lists_clusters_and_unreachable():
532
- records = [{"errorType": "cli-failure", "phase": "impl", "agent": "codex-worker",
533
- "message": "timeout", "sourceProject": "proj-1", "source": "worker-reported"}]
534
- clusters = error_zip.build_clusters(records)
535
- md = error_zip.render_report(
536
- records=records, clusters=clusters,
537
- stats={"runCount": 1, "unreachableRuns": 2, "projectRoots": ["/x"]},
538
- generated_at="2026-06-20T00:00:00+00:00",
539
- )
540
- assert "cli-failure" in md
541
- assert "도달 불가" in md # unreachableRuns 표면화
542
- ```
543
-
544
- - [ ] **Step 2: 실패 확인**
545
-
546
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py::test_clusters_group_by_normalized_message -v`
547
- Expected: FAIL — `AttributeError: ... has no attribute 'build_clusters'`
548
-
549
- - [ ] **Step 3: 클러스터 + 렌더 구현**
550
-
551
- Append to `scripts/okstra_ctl/error_zip.py` (상단 import 에 `import re`, `import datetime as dt` 추가):
552
-
553
- ```python
554
- _NUM = re.compile(r"\d+")
555
- _PATHISH = re.compile(r"[/\\][^\s]+")
556
-
557
-
558
- def cluster_key(rec: dict) -> str:
559
- msg = str(rec.get("message", ""))
560
- msg = _PATHISH.sub("<path>", msg)
561
- msg = _NUM.sub("<n>", msg)
562
- msg = msg.strip()[:80]
563
- return "|".join([
564
- str(rec.get("errorType", "")),
565
- str(rec.get("phase", "")),
566
- str(rec.get("agent", "")),
567
- msg,
568
- ])
569
-
570
-
571
- def build_clusters(records: list[dict]) -> list[dict]:
572
- buckets: dict[str, dict] = {}
573
- for rec in records:
574
- key = cluster_key(rec)
575
- b = buckets.setdefault(key, {
576
- "key": key, "errorType": str(rec.get("errorType", "")),
577
- "phase": str(rec.get("phase", "")), "agent": str(rec.get("agent", "")),
578
- "count": 0, "projects": set(), "sample": str(rec.get("message", "")),
579
- })
580
- b["count"] += 1
581
- b["projects"].add(str(rec.get("sourceProject", "")))
582
- clusters = []
583
- for b in buckets.values():
584
- b["projects"] = sorted(p for p in b["projects"] if p)
585
- clusters.append(b)
586
- clusters.sort(key=lambda c: (-c["count"], c["key"]))
587
- return clusters
588
-
589
-
590
- def render_report(*, records, clusters, stats, generated_at) -> str:
591
- agg = aggregate(records)
592
- lines = [
593
- "# okstra cross-project 에러 환류 리포트",
594
- "",
595
- f"- 생성 시각: {generated_at}",
596
- f"- 총 에러: {agg['errorCount']} (로그 {stats['runCount']} run)",
597
- f"- 도달 불가 run: {stats['unreachableRuns']}",
598
- f"- errorType 분포: {agg['byErrorType']}",
599
- "",
600
- "## 빈발 클러스터 (brief 분할 단위)",
601
- "",
602
- "| # | errorType | phase | agent | count | projects | 대표 메시지 |",
603
- "|---|---|---|---|---:|---|---|",
604
- ]
605
- for i, c in enumerate(clusters, start=1):
606
- sample = c["sample"].replace("|", "\\|").replace("\n", " ")[:60]
607
- lines.append(
608
- f"| {i} | {c['errorType']} | {c['phase']} | {c['agent']} "
609
- f"| {c['count']} | {len(c['projects'])} | {sample} |"
610
- )
611
- return "\n".join(lines) + "\n"
612
- ```
613
-
614
- (`aggregate` 는 Task 1 코어에서 import — `from okstra_ctl.error_log_core import parse_records, aggregate` 로 import 줄 확장.)
615
-
616
- - [ ] **Step 4: 통과 확인**
617
-
618
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py -v`
619
- Expected: PASS (전체)
620
-
621
- - [ ] **Step 5: 커밋**
622
-
623
- ```bash
624
- git add scripts/okstra_ctl/error_zip.py tests/inspect/test_okstra_error_zip.py
625
- git commit -m "feat(inspect): error-zip 클러스터 집계 + 리포트 렌더(data-driven)"
626
- ```
627
-
628
- ---
629
-
630
- ### Task 5: zip 생성 + main(argparse --out) + 출력 경로 기억
631
-
632
- 수집→익명화→집계→`.zip`(report.md + errors/anonymized.jsonl) 생성을 묶고, CLI 진입점을 만든다. 성공 시 `--out` 경로를 `~/.okstra/error-zip.json` 의 `lastOutputPath` 에 기억한다.
633
-
634
- **Files:**
635
- - Modify: `scripts/okstra_ctl/error_zip.py`
636
- - Test: `tests/inspect/test_okstra_error_zip.py`
637
-
638
- **Interfaces:**
639
- - Consumes: Task 2–4 함수, `okstra_ctl.paths.okstra_home`
640
- - Produces:
641
- - `build_zip(home: Path, out_path: Path, now: dt.datetime) -> dict` — `{outPath, errorCount, runCount, unreachableRuns, clusterCount, projectCount}`
642
- - `remember_output_path(home: Path, out_path: Path) -> None`
643
- - `last_output_path(home: Path) -> str`
644
- - `main(argv: list[str] | None = None) -> int`
645
-
646
- - [ ] **Step 1: zip + main 테스트 작성 (실패하도록)**
647
-
648
- Append to `tests/inspect/test_okstra_error_zip.py` (상단에 `import zipfile`, `import datetime as dt` 추가):
649
-
650
- ```python
651
- def test_main_writes_zip_and_remembers_path(tmp_path, capsys):
652
- home = okstra_home()
653
- p1 = tmp_path / "proj1"
654
- rel = ".okstra/tasks/g/DEV-1/runs/implementation"
655
- _seed_project(p1, rel, [_rec(), _rec(errorType="tool-failure")])
656
- append_jsonl(home / "recent.jsonl", _index_row(p1, rel))
657
- out = tmp_path / "bundle.zip"
658
-
659
- rc = error_zip.main(["--out", str(out)])
660
- assert rc == 0
661
- summary = json.loads(capsys.readouterr().out)
662
- assert summary["errorCount"] == 2
663
- assert out.is_file()
664
- with zipfile.ZipFile(out) as zf:
665
- names = zf.namelist()
666
- assert "report.md" in names
667
- assert any(n.startswith("errors/") for n in names)
668
- report = zf.read("report.md").decode("utf-8")
669
- assert "/proj1" not in report # 익명화 확인
670
- assert error_zip.last_output_path(home) == str(out)
671
-
672
-
673
- def test_main_no_errors_still_writes_zip(tmp_path, capsys):
674
- out = tmp_path / "empty.zip"
675
- rc = error_zip.main(["--out", str(out)])
676
- assert rc == 0
677
- summary = json.loads(capsys.readouterr().out)
678
- assert summary["errorCount"] == 0
679
- assert out.is_file()
680
- ```
681
-
682
- - [ ] **Step 2: 실패 확인**
683
-
684
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py::test_main_writes_zip_and_remembers_path -v`
685
- Expected: FAIL — `AttributeError: ... has no attribute 'main'`
686
-
687
- - [ ] **Step 3: zip + main + config 구현**
688
-
689
- Append to `scripts/okstra_ctl/error_zip.py` (상단 import 에 `import argparse`, `import json`, `import sys`, `import zipfile` 추가; `from okstra_ctl.paths import okstra_home` 추가):
690
-
691
- ```python
692
- _CONFIG_NAME = "error-zip.json"
693
-
694
-
695
- def remember_output_path(home: Path, out_path: Path) -> None:
696
- cfg = home / _CONFIG_NAME
697
- cfg.write_text(
698
- json.dumps({"lastOutputPath": str(out_path)}, ensure_ascii=False, indent=2),
699
- encoding="utf-8",
700
- )
701
-
702
-
703
- def last_output_path(home: Path) -> str:
704
- cfg = home / _CONFIG_NAME
705
- if not cfg.is_file():
706
- return ""
707
- try:
708
- return str(json.loads(cfg.read_text(encoding="utf-8")).get("lastOutputPath", ""))
709
- except Exception:
710
- return ""
711
-
712
-
713
- def build_zip(home: Path, out_path: Path, now: dt.datetime) -> dict:
714
- records, stats = collect_records(home)
715
- clean, _ = anonymize(records)
716
- clusters = build_clusters(clean)
717
- report = render_report(
718
- records=clean, clusters=clusters, stats=stats,
719
- generated_at=now.isoformat(),
720
- )
721
- out_path.parent.mkdir(parents=True, exist_ok=True)
722
- with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zf:
723
- zf.writestr("report.md", report)
724
- payload = "\n".join(json.dumps(r, ensure_ascii=False) for r in clean)
725
- zf.writestr("errors/anonymized.jsonl", payload + ("\n" if payload else ""))
726
- remember_output_path(home, out_path)
727
- return {
728
- "outPath": str(out_path),
729
- "errorCount": len(clean),
730
- "runCount": stats["runCount"],
731
- "unreachableRuns": stats["unreachableRuns"],
732
- "clusterCount": len(clusters),
733
- "projectCount": len(stats["projectRoots"]),
734
- }
735
-
736
-
737
- def main(argv: list[str] | None = None) -> int:
738
- parser = argparse.ArgumentParser(
739
- prog="okstra error-zip",
740
- description="cross-project okstra 에러를 수집·익명화해 zip 으로 묶는다.",
741
- )
742
- parser.add_argument("--out", required=True, help="출력 .zip 절대/상대 경로")
743
- args = parser.parse_args(argv)
744
- home = okstra_home()
745
- result = build_zip(home, Path(args.out), dt.datetime.now(dt.timezone.utc))
746
- print(json.dumps(result, ensure_ascii=False, indent=2))
747
- return 0
748
-
749
-
750
- if __name__ == "__main__":
751
- raise SystemExit(main(sys.argv[1:]))
752
- ```
753
-
754
- - [ ] **Step 4: 통과 확인 + 모듈 직접 실행 스모크**
755
-
756
- Run: `python3 -m pytest tests/inspect/test_okstra_error_zip.py -v`
757
- Expected: PASS (전체)
758
-
759
- Run: `OKSTRA_HOME=$(mktemp -d) python3 -m okstra_ctl.error_zip --out /tmp/ez-smoke.zip`
760
- Expected: stdout 에 `"errorCount": 0` JSON, `/tmp/ez-smoke.zip` 생성 (PYTHONPATH 에 `scripts/` 필요: `PYTHONPATH=scripts`)
761
-
762
- - [ ] **Step 5: 커밋**
763
-
764
- ```bash
765
- git add scripts/okstra_ctl/error_zip.py tests/inspect/test_okstra_error_zip.py
766
- git commit -m "feat(inspect): error-zip zip 생성·CLI·출력경로 기억"
767
- ```
768
-
769
- ---
770
-
771
- ### Task 6: Node 래퍼 + cli-registry 등록
772
-
773
- `okstra error-zip --out <path>` 를 노출한다. error-report.mjs 를 복제해 module/USAGE 만 바꾼다.
774
-
775
- **Files:**
776
- - Create: `src/commands/inspect/error-zip.mjs`
777
- - Modify: `src/cli-registry.mjs` (error-report 객체 `:106-112` 바로 뒤)
778
-
779
- **Interfaces:**
780
- - Consumes: `okstra_ctl.error_zip` (Task 5), `src/lib/python-helper.mjs` 의 `runPythonModule`
781
- - Produces: `run(args) -> Promise<number>`
782
-
783
- - [ ] **Step 1: Node 래퍼 작성**
784
-
785
- Create `src/commands/inspect/error-zip.mjs`:
786
-
787
- ```js
788
- import { runPythonModule } from "../../lib/python-helper.mjs";
789
-
790
- const USAGE = `okstra error-zip — bundle cross-project okstra error logs into a zip
791
-
792
- Usage:
793
- okstra error-zip --out <path>
794
-
795
- Walks the global run-index (~/.okstra/recent.jsonl + active.jsonl), collects
796
- every target project's runs/*/logs/errors-*.jsonl, anonymizes records, and
797
- writes a .zip (report.md + errors/anonymized.jsonl) to <path>. Read-only
798
- against every target's .okstra/. Prints a JSON summary to stdout.
799
- `;
800
-
801
- export async function run(args) {
802
- if (args.includes("--help") || args.includes("-h")) {
803
- process.stdout.write(USAGE);
804
- return 0;
805
- }
806
- const result = await runPythonModule({
807
- module: "okstra_ctl.error_zip",
808
- args,
809
- stdio: "inherit-stdout",
810
- });
811
- return result.code ?? 0;
812
- }
813
- ```
814
-
815
- - [ ] **Step 2: cli-registry 등록**
816
-
817
- In `src/cli-registry.mjs`, `error-report` 객체 바로 뒤에 추가:
818
-
819
- ```js
820
- {
821
- name: "error-zip",
822
- module: "./commands/inspect/error-zip.mjs",
823
- export: "run",
824
- category: "introspection",
825
- summary: ["Bundle cross-project okstra error logs into a zip"],
826
- },
827
- ```
828
-
829
- - [ ] **Step 3: CLI 스모크 테스트**
830
-
831
- Run: `node bin/okstra error-zip --help`
832
- Expected: USAGE 텍스트 출력, 종료코드 0
833
-
834
- Run: `OKSTRA_HOME=$(mktemp -d) node bin/okstra error-zip --out /tmp/ez-node.zip`
835
- Expected: JSON 요약(`"errorCount": 0`), `/tmp/ez-node.zip` 생성
836
-
837
- - [ ] **Step 4: 커밋**
838
-
839
- ```bash
840
- git add src/commands/inspect/error-zip.mjs src/cli-registry.mjs
841
- git commit -m "feat(cli): okstra error-zip facet Node 래퍼 + 레지스트리 등록"
842
- ```
843
-
844
- ---
845
-
846
- ### Task 7: okstra-inspect SKILL 에 error-zip facet + 저장경로 picker
847
-
848
- 읽기측 facet 으로 노출하고, 출력 경로를 이전값 재사용/직접입력 3-옵션 picker 로 받는다.
849
-
850
- **Files:**
851
- - Modify: `skills/okstra-inspect/SKILL.md` (3곳: frontmatter description `:4`, sub-command 테이블 `:19` 뒤, 본문 `## recap` `:764` 앞)
852
-
853
- **Interfaces:** 없음 (마크다운). CLI 호출은 `okstra error-zip --out <chosen>`.
854
-
855
- - [ ] **Step 1: 트리거 + 테이블 행 추가**
856
-
857
- In `skills/okstra-inspect/SKILL.md` frontmatter `description`(`:4`), 트리거 추가: `"okstra error-zip", "에러 zip", "에러 환류", "에러 번들", "cross-project 에러"`. facet 개수 문장(`:9` 부근의 `eight facets` / `status...recap` 나열)을 9개로 갱신.
858
-
859
- Sub-command 테이블(`:19` `errors` 행 뒤)에 추가:
860
-
861
- ```
862
- | `error-zip` | Collect cross-project okstra error logs into an anonymized zip (report + raw) and summarize clusters. |
863
- ```
864
-
865
- - [ ] **Step 2: `## error-zip` 본문 섹션 추가**
866
-
867
- `## recap`(`:764`) 직전에 `---` 구분선과 함께 삽입:
868
-
869
- ````markdown
870
- ## error-zip
871
-
872
- Trigger phrases: "okstra error-zip", "에러 zip", "에러 환류", "에러 번들", "cross-project 에러".
873
-
874
- 머신 내 모든 타겟 프로젝트의 okstra 실행 에러(`runs/*/logs/errors-*.jsonl`)를 글로벌 run-index 로 수집·익명화해 `.zip`(집계 리포트 + 익명화 JSONL)으로 묶는다. 모든 타겟의 `.okstra/` 에 대해 read-only.
875
-
876
- ### error-zip.1 — 출력 경로 결정 (picker)
877
-
878
- 이전 출력 경로를 `~/.okstra/error-zip.json` 의 `lastOutputPath` 에서 읽는다. 그 값이 있으면 3-옵션 picker 로 제시:
879
-
880
- 1. (이전 경로 있음) `<lastOutputPath>` 재사용 — 추천, 첫 옵션.
881
- (이전 경로 없음) 기본 경로 `~/okstra-error-feedback-<YYYY-MM-DD>.zip` 제안 — 추천.
882
- 2. 직접 입력 (항상 마지막 옵션).
883
-
884
- `~/.okstra/error-zip.json` 읽기는 Read 로 직접 수행한다(없으면 이전 경로 없음으로 간주).
885
-
886
- ### error-zip.2 — 렌더 실행
887
-
888
- ```bash
889
- okstra error-zip --out <결정된 경로>
890
- ```
891
-
892
- ### error-zip.3 — 요약 보고
893
-
894
- stdout JSON 을 파싱해 보고:
895
-
896
- | 필드 | 출처 |
897
- |---|---|
898
- | 출력 zip | `outPath` |
899
- | 총 에러 | `errorCount` |
900
- | 로그(run) | `runCount` |
901
- | 도달 불가 run | `unreachableRuns` |
902
- | 클러스터 수 | `clusterCount` |
903
- | 프로젝트 수 | `projectCount` |
904
-
905
- - `unreachableRuns > 0` 이면 표면화한다(침묵 누락 금지).
906
- - 끝에 다음 단계를 안내한다: "이 zip 으로 okstra 자신을 고치려면 `/okstra-brief` 의 error-feedback variant 로 brief 를 만든 뒤 `okstra-run --task-type error-analysis` 를 okstra 레포에서 실행하세요."
907
- ````
908
-
909
- - [ ] **Step 3: 일관성 grep**
910
-
911
- Run: `grep -n "error-zip" skills/okstra-inspect/SKILL.md`
912
- Expected: description / 테이블 행 / 본문 섹션 3곳에 등장. facet 개수 문장이 9로 갱신됐는지 육안 확인.
913
-
914
- - [ ] **Step 4: 커밋**
915
-
916
- ```bash
917
- git add skills/okstra-inspect/SKILL.md
918
- git commit -m "feat(inspect): okstra-inspect 에 error-zip facet + 저장경로 picker 추가"
919
- ```
920
-
921
- ---
922
-
923
- ### Task 8: okstra-brief 에 error-feedback variant 추가
924
-
925
- zip 을 입력받아 빈발 클러스터 1개를 error-analysis brief 로 변환하는 variant 를 추가한다.
926
-
927
- **Files:**
928
- - Modify: `skills/okstra-brief/SKILL.md`
929
-
930
- **Interfaces:** 없음 (마크다운). 출력은 error-analysis brief 파일.
931
-
932
- - [ ] **Step 1: 기존 variant 등록 지점 확인**
933
-
934
- Run: `grep -n "variant\|Codebase scan\|codebase-scan\|Step 1" skills/okstra-brief/SKILL.md | head -40`
935
- Expected: variant 선택 메뉴(Step 1)와 codebase-scan variant 정의 위치 파악. error-feedback variant 를 같은 메뉴/패턴에 추가.
936
-
937
- - [ ] **Step 2: variant 메뉴에 error-feedback 추가**
938
-
939
- variant 선택 Step(Codebase scan 이 등록된 곳과 같은 목록)에 `error-feedback` 옵션을 추가하고, 별도 variant 섹션을 작성:
940
-
941
- ````markdown
942
- ### variant: error-feedback (okstra 자가수정용)
943
-
944
- 입력: `okstra error-zip` 이 만든 `.zip`. 사용자에게 zip 경로를 묻는다(이전 경로 picker 가 있으면 재사용 제안).
945
-
946
- 1. zip 의 `report.md` 를 읽어 빈발 클러스터 표를 사용자에게 보여주고, **클러스터 1개**를 고르게 한다. (3-옵션: top-1 클러스터 추천 / top-2 / 직접 지정)
947
- 2. 고른 클러스터의 익명화 레코드(`errors/anonymized.jsonl` 에서 해당 errorType+phase+agent 일치분)를 brief 의 `Source Material` 에 인용한다 — errorType·command·exitCode·phase·agent·대표 메시지.
948
- 3. brief frontmatter 의 task-type 은 `error-analysis`, 수정 대상은 **okstra 레포 자신**임을 본문에 명시한다("이 에러는 okstra 런타임/프롬프트/래퍼/권한 seed 결함 신호이며, 수정 대상은 okstra 소스(`prompts/`·`agents/`·`scripts/`·`templates/`)").
949
- 4. **단일 클러스터 = 단일 실패조건** 원칙을 지킨다 — 이질적 errorType 을 한 brief 에 섞지 않는다(error-analysis 의 symptom-lock 요건).
950
- 5. 생성된 brief 끝에 다음 단계를 인라인으로 적는다: "okstra 레포에서 `okstra-run --task-type error-analysis` 로 이 brief 를 실행하세요."
951
- ````
952
-
953
- - [ ] **Step 3: 일관성 grep**
954
-
955
- Run: `grep -n "error-feedback\|error-zip" skills/okstra-brief/SKILL.md`
956
- Expected: variant 메뉴 + variant 섹션에 등장.
957
-
958
- - [ ] **Step 4: 커밋**
959
-
960
- ```bash
961
- git add skills/okstra-brief/SKILL.md
962
- git commit -m "feat(brief): okstra-brief 에 error-feedback variant(zip→error-analysis brief) 추가"
963
- ```
964
-
965
- ---
966
-
967
- ### Task 9: 빌드 동기화 + 전체 검증
968
-
969
- 소스 변경을 `runtime/` 으로 동기화하고 전체 스위트를 돌린다.
970
-
971
- **Files:** 없음 (빌드/검증)
972
-
973
- - [ ] **Step 1: 빌드**
974
-
975
- Run: `npm run build`
976
- Expected: 성공. `runtime/python/okstra_ctl/error_zip.py`, `error_log_core.py` 가 생성되고 skills 가 동기화됨.
977
-
978
- - [ ] **Step 2: 빌드 산출물 동기화 확인**
979
-
980
- Run: `diff -q scripts/okstra_ctl/error_zip.py runtime/python/okstra_ctl/error_zip.py && diff -q scripts/okstra_ctl/error_log_core.py runtime/python/okstra_ctl/error_log_core.py`
981
- Expected: 차이 없음(빌드가 정확히 복사). 차이 있으면 빌드 설정에 신규 모듈 누락 — `tools/build.mjs` 확인.
982
-
983
- - [ ] **Step 3: 전체 unit suite**
984
-
985
- Run: `python3 -m pytest tests/`
986
- Expected: PASS (기존 + 신규 전부)
987
-
988
- - [ ] **Step 4: facet 디스패치 스모크**
989
-
990
- Run: `node bin/okstra doctor`
991
- Expected: 런타임/스킬 진단 통과.
992
-
993
- Run: `node bin/okstra --help | grep error-zip`
994
- Expected: error-zip 이 introspection 카테고리에 노출.
995
-
996
- - [ ] **Step 5: CHANGES.md 항목 추가**
997
-
998
- `CHANGES.md` 에 사용자 영향 항목 추가(한국어):
999
-
1000
- ```markdown
1001
- - `okstra error-zip` facet 추가: 머신 내 모든 타겟 프로젝트의 okstra 실행 에러를 cross-project 로 수집·익명화해 zip 으로 묶고, `okstra-brief` 의 error-feedback variant 로 okstra 자신을 고치는 error-analysis brief 를 만들 수 있습니다.
1002
- - 사용자 영향: 여러 프로젝트에 흩어진 okstra 결함 신호를 한데 모아 okstra 자가수정 lifecycle 로 환류할 수 있습니다.
1003
- ```
1004
-
1005
- - [ ] **Step 6: 최종 커밋**
1006
-
1007
- ```bash
1008
- git add runtime/ CHANGES.md
1009
- git commit -m "build: error-zip/error-feedback 런타임 동기화 + CHANGES 기록"
1010
- ```
1011
-
1012
- ---
1013
-
1014
- ## Self-Review 메모
1015
-
1016
- - **Spec coverage**: spec §4(okstra-error-zip)→Task 2–7, §4.4 익명화→Task 3, §4.5 코어 공유→Task 1, §4.6 저장 UX/경로 기억→Task 5·7, §5(brief variant)→Task 8, §6(lifecycle 재사용)→신규 구현 없음(브리프가 error-analysis 로 라우팅), §2 errorType 비결합→Task 1·3·4 전반, §8.1–8.3 열린 디테일→Task 3(allowlist)·Task 5(config 파일명 `error-zip.json`)·Task 4(클러스터 기준).
1017
- - **수정 lifecycle(error-analysis→implementation)** 은 기존 기능이라 plan 에 구현 task 없음 — Task 8 의 brief 가 진입점을 만든다.
1018
- - **`append_jsonl` 시그니처**는 Task 2 Step 4 에서 실제값으로 검증 후 맞춘다(가정: `append_jsonl(path, row)`).
1019
- - **빌드가 신규 모듈을 자동 포함하는지**는 Task 9 Step 2 에서 검증 — 누락 시 `tools/build.mjs` 수정 필요.