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,583 +0,0 @@
1
- # run-index row 정규화 (runId SSOT화) Implementation Plan
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:** run-index row(`recent.jsonl`/`active.jsonl`/`projects/<id>/index.jsonl`)에서 runId로부터 무손실 복원 가능한 5개 파생 필드를 제거해 디스크 포맷을 slim(18→13필드)으로 만들고, read 지점 hydrate로 reader 로직을 무변경 유지한다.
6
-
7
- **Architecture:** 신규 모듈 `run_index_row.py`에 생성(`build_run_index_row`)·축약(`slim_run_row`)·복원(`hydrate_run_row`)·읽기(`read_run_index`)·일괄 변환(`reindex_slim`)을 집중한다. writer는 `build_run_index_row`로 slim row만 기록하고, reader 3곳은 `read_run_index`로 읽어 full row를 받는다. reconcile은 읽을 때 hydrate, 다시 쓸 때 slim한다.
8
-
9
- **Tech Stack:** Python 3 (4-space indent, snake_case), pytest (`tests/run/`, conftest.py가 OKSTRA_HOME 격리), `npm run build`로 runtime 동기화.
10
-
11
- ## Global Constraints
12
-
13
- - 디스크 row에서 제거하는 필드(정확히 5개): `projectId`, `taskType`, `runSeq`, `taskKey`, `invocationFile`.
14
- - 유지하는 필드(13개): `runId`, `projectRoot`, `taskGroup`, `taskId`, `status`, `startedAt`, `finishedAt`, `workers`, `leadModel`, `validation`, `finalReportRel`, `finalStatusRel`, `runDirRel`.
15
- - `taskGroup`/`taskId`는 raw 대소문자를 보존한다 (runId 세그먼트는 slug라 다를 수 있음 — 예: row `DEV-9388` vs runId `dev-9388`). 절대 runId에서 복원하지 않는다.
16
- - `runId` 포맷 `<projectId>/<taskGroup>/<taskId>/<taskType>/r<NN>`은 fs-safe 슬러그 불변식이라 변경 금지.
17
- - 모든 row 키는 camelCase. JSON 직렬화는 기존과 동일하게 `separators=(",", ":")`.
18
- - `runtime/`는 빌드 산출물 — 직접 편집 금지. 소스 수정 후 `npm run build`.
19
- - 커밋은 Conventional Commits, AI 트레일러 금지.
20
-
21
- ---
22
-
23
- ### Task 1: 코어 헬퍼 — build / slim / hydrate
24
-
25
- **Files:**
26
- - Create: `scripts/okstra_ctl/run_index_row.py`
27
- - Test: `tests/run/test_run_index_row.py`
28
-
29
- **Interfaces:**
30
- - Consumes: `parse_run_id`, `build_run_id` from [`scripts/okstra_ctl/ids.py`](../../../scripts/okstra_ctl/ids.py); `invocation_path` from [`scripts/okstra_ctl/invocation.py`](../../../scripts/okstra_ctl/invocation.py).
31
- - Produces:
32
- - `DERIVED_FIELDS: tuple = ("projectId", "taskType", "runSeq", "taskKey", "invocationFile")`
33
- - `build_run_index_row(*, project_id, project_root, task_group, task_id, task_type, run_seq, status, started_at, finished_at, workers, lead_model, validation, run_dir_rel, final_report_rel, final_status_rel) -> dict` — 13필드 slim row 반환.
34
- - `slim_run_row(row: dict) -> dict` — `DERIVED_FIELDS`를 제거한 새 dict (멱등).
35
- - `hydrate_run_row(row: dict) -> dict` — slim/full row를 받아 5개 파생 필드를 채운 full dict (멱등).
36
-
37
- - [ ] **Step 1: Write the failing test**
38
-
39
- ```python
40
- # tests/run/test_run_index_row.py
41
- from pathlib import Path
42
-
43
- from okstra_ctl.run_index_row import (
44
- DERIVED_FIELDS, build_run_index_row, slim_run_row, hydrate_run_row,
45
- )
46
-
47
-
48
- def _sample_kwargs():
49
- return dict(
50
- project_id="fontradar-v2-api",
51
- project_root="/Volumes/x/fontradar-v2-api",
52
- task_group="uploadfont",
53
- task_id="dev-6827-bug-de-redis-lupload",
54
- task_type="final-verification",
55
- run_seq=5,
56
- status="prepared",
57
- started_at="2026-06-18T20:24:00Z",
58
- finished_at="2026-06-18T20:24:00Z",
59
- workers=["claude", "codex"],
60
- lead_model="opus",
61
- validation="not-run",
62
- run_dir_rel=".okstra/tasks/uploadfont/dev-6827-bug-de-redis-lupload/runs/final-verification",
63
- final_report_rel=".okstra/tasks/uploadfont/dev-6827-bug-de-redis-lupload/runs/final-verification/reports/final-report-final-verification-001.md",
64
- final_status_rel=".okstra/tasks/uploadfont/dev-6827-bug-de-redis-lupload/runs/final-verification/status/final-final-verification-001.status",
65
- )
66
-
67
-
68
- def test_build_run_index_row_omits_derived():
69
- row = build_run_index_row(**_sample_kwargs())
70
- for f in DERIVED_FIELDS:
71
- assert f not in row, f"slim row 에 파생 필드 {f} 가 남았다"
72
- assert row["runId"] == ("fontradar-v2-api/uploadfont/"
73
- "dev-6827-bug-de-redis-lupload/final-verification/r05")
74
- assert row["taskGroup"] == "uploadfont"
75
- assert len(row) == 13
76
-
77
-
78
- def test_hydrate_restores_derived():
79
- slim = build_run_index_row(**_sample_kwargs())
80
- full = hydrate_run_row(slim)
81
- assert full["projectId"] == "fontradar-v2-api"
82
- assert full["taskType"] == "final-verification"
83
- assert full["runSeq"] == 5
84
- assert full["taskKey"] == "uploadfont/dev-6827-bug-de-redis-lupload"
85
- assert full["invocationFile"] == (
86
- "projects/fontradar-v2-api/invocations/uploadfont/"
87
- "dev-6827-bug-de-redis-lupload/final-verification/r05.json")
88
-
89
-
90
- def test_hydrate_preserves_raw_case():
91
- # row 의 raw 대소문자(DEV-9388)는 runId(slug dev-9388)에서 복원하면 안 된다
92
- kwargs = _sample_kwargs()
93
- kwargs["task_group"] = "DEV-9388"
94
- kwargs["task_id"] = "dev-9436"
95
- slim = build_run_index_row(**kwargs)
96
- full = hydrate_run_row(slim)
97
- assert full["taskGroup"] == "DEV-9388"
98
- assert full["taskKey"] == "DEV-9388/dev-9436"
99
- # invocationFile 은 slug 기반
100
- assert "/dev-9388/dev-9436/" in full["invocationFile"]
101
-
102
-
103
- def test_hydrate_idempotent_on_full():
104
- slim = build_run_index_row(**_sample_kwargs())
105
- once = hydrate_run_row(slim)
106
- twice = hydrate_run_row(once)
107
- assert once == twice
108
-
109
-
110
- def test_slim_run_row_idempotent():
111
- slim = build_run_index_row(**_sample_kwargs())
112
- full = hydrate_run_row(slim)
113
- assert slim_run_row(full) == slim
114
- assert slim_run_row(slim) == slim
115
- ```
116
-
117
- - [ ] **Step 2: Run test to verify it fails**
118
-
119
- Run: `python3 -m pytest tests/run/test_run_index_row.py -v`
120
- Expected: FAIL — `ModuleNotFoundError: No module named 'okstra_ctl.run_index_row'`
121
-
122
- - [ ] **Step 3: Write minimal implementation**
123
-
124
- ```python
125
- # scripts/okstra_ctl/run_index_row.py
126
- """run-index row 의 생성/축약/복원 단일 참조점.
127
-
128
- 디스크에는 slim(13필드)로 저장하고, reader 는 hydrate 로 full(18필드)을 받는다.
129
- 파생 5필드(projectId/taskType/runSeq/taskKey/invocationFile)는 runId 와 평면
130
- taskGroup/taskId 로부터 무손실 복원 가능하다(2026-06-20 전수 검증 근거).
131
- """
132
- from __future__ import annotations
133
-
134
- from pathlib import Path
135
-
136
- from .ids import build_run_id, parse_run_id
137
- from .invocation import invocation_path
138
-
139
- DERIVED_FIELDS = ("projectId", "taskType", "runSeq", "taskKey", "invocationFile")
140
-
141
-
142
- def build_run_index_row(*, project_id: str, project_root: str,
143
- task_group: str, task_id: str, task_type: str,
144
- run_seq: int, status: str, started_at: str,
145
- finished_at, workers: list, lead_model: str,
146
- validation: str, run_dir_rel: str,
147
- final_report_rel: str, final_status_rel: str) -> dict:
148
- """slim run-index row(13필드)를 만든다. 파생 5필드는 포함하지 않는다."""
149
- return {
150
- "runId": build_run_id(project_id, task_group, task_id,
151
- task_type, run_seq),
152
- "projectRoot": project_root,
153
- "taskGroup": task_group,
154
- "taskId": task_id,
155
- "status": status,
156
- "startedAt": started_at,
157
- "finishedAt": finished_at,
158
- "workers": workers,
159
- "leadModel": lead_model,
160
- "validation": validation,
161
- "finalReportRel": final_report_rel,
162
- "finalStatusRel": final_status_rel,
163
- "runDirRel": run_dir_rel,
164
- }
165
-
166
-
167
- def slim_run_row(row: dict) -> dict:
168
- """파생 5필드를 제거한 새 dict. 이미 slim 이면 동일(멱등)."""
169
- return {k: v for k, v in row.items() if k not in DERIVED_FIELDS}
170
-
171
-
172
- def hydrate_run_row(row: dict) -> dict:
173
- """slim/full row 를 받아 파생 5필드를 채운 full dict. 멱등.
174
- taskKey 는 runId(slug)가 아닌 평면 raw taskGroup/taskId 로 조합한다.
175
- """
176
- parsed = parse_run_id(row["runId"])
177
- out = dict(row)
178
- out["projectId"] = parsed["projectId"]
179
- out["taskType"] = parsed["taskType"]
180
- out["runSeq"] = parsed["runSeq"]
181
- out["taskKey"] = f"{row.get('taskGroup', '')}/{row.get('taskId', '')}"
182
- out["invocationFile"] = str(invocation_path(
183
- Path(""), parsed["projectId"], parsed["taskGroup"], parsed["taskId"],
184
- parsed["taskType"], parsed["runSeq"]))
185
- return out
186
- ```
187
-
188
- - [ ] **Step 4: Run test to verify it passes**
189
-
190
- Run: `python3 -m pytest tests/run/test_run_index_row.py -v`
191
- Expected: PASS (5 tests)
192
-
193
- - [ ] **Step 5: Commit**
194
-
195
- ```bash
196
- git add scripts/okstra_ctl/run_index_row.py tests/run/test_run_index_row.py
197
- git commit -m "feat(index): run-index row 생성/축약/복원 코어 헬퍼 추가"
198
- ```
199
-
200
- ---
201
-
202
- ### Task 2: read_run_index + reindex_slim 일괄 변환
203
-
204
- **Files:**
205
- - Modify: `scripts/okstra_ctl/run_index_row.py` (함수 추가)
206
- - Test: `tests/run/test_run_index_row.py` (테스트 추가)
207
-
208
- **Interfaces:**
209
- - Consumes: `read_jsonl` from [`scripts/okstra_ctl/jsonl.py`](../../../scripts/okstra_ctl/jsonl.py); `hydrate_run_row`, `slim_run_row` (Task 1).
210
- - Produces:
211
- - `read_run_index(path: Path) -> list[dict]` — jsonl을 읽어 각 row를 `hydrate_run_row`로 통과시킨 full row 목록.
212
- - `reindex_slim(home: Path) -> int` — `recent.jsonl`/`active.jsonl`/`projects/*/index.jsonl`을 slim으로 원자적 재작성. 변환한 row 총수 반환. 멱등.
213
-
214
- - [ ] **Step 1: Write the failing test**
215
-
216
- ```python
217
- # tests/run/test_run_index_row.py (추가)
218
- import json
219
-
220
- from okstra_ctl.run_index_row import read_run_index, reindex_slim
221
-
222
-
223
- def _write_full_row(path, kwargs_overrides=None):
224
- # 마이그레이션 전 full row 를 디스크에 직접 기록
225
- from okstra_ctl.run_index_row import hydrate_run_row, build_run_index_row
226
- kw = _sample_kwargs()
227
- if kwargs_overrides:
228
- kw.update(kwargs_overrides)
229
- full = hydrate_run_row(build_run_index_row(**kw))
230
- path.parent.mkdir(parents=True, exist_ok=True)
231
- with path.open("a") as f:
232
- f.write(json.dumps(full, separators=(",", ":")) + "\n")
233
- return full
234
-
235
-
236
- def test_read_run_index_hydrates(tmp_path):
237
- recent = tmp_path / "recent.jsonl"
238
- # slim row 를 디스크에 기록
239
- from okstra_ctl.run_index_row import build_run_index_row
240
- slim = build_run_index_row(**_sample_kwargs())
241
- recent.write_text(json.dumps(slim, separators=(",", ":")) + "\n")
242
- rows = read_run_index(recent)
243
- assert rows[0]["projectId"] == "fontradar-v2-api"
244
- assert rows[0]["invocationFile"].startswith("projects/")
245
-
246
-
247
- def test_reindex_slim_strips_and_is_idempotent(tmp_path):
248
- home = tmp_path
249
- recent = home / "recent.jsonl"
250
- idx = home / "projects" / "fontradar-v2-api" / "index.jsonl"
251
- full_recent = _write_full_row(recent)
252
- _write_full_row(idx)
253
- n1 = reindex_slim(home)
254
- assert n1 == 2
255
- # 디스크에서 파생 필드가 사라졌는지 확인
256
- on_disk = json.loads(recent.read_text().splitlines()[0])
257
- for f in DERIVED_FIELDS:
258
- assert f not in on_disk
259
- # 값 보존: hydrate 하면 원래 full 과 동일
260
- assert hydrate_run_row(on_disk) == full_recent
261
- # 멱등: 두 번째 실행도 같은 결과
262
- n2 = reindex_slim(home)
263
- assert json.loads(recent.read_text().splitlines()[0]) == on_disk
264
- ```
265
-
266
- - [ ] **Step 2: Run test to verify it fails**
267
-
268
- Run: `python3 -m pytest tests/run/test_run_index_row.py::test_reindex_slim_strips_and_is_idempotent -v`
269
- Expected: FAIL — `ImportError: cannot import name 'reindex_slim'`
270
-
271
- - [ ] **Step 3: Write minimal implementation**
272
-
273
- ```python
274
- # scripts/okstra_ctl/run_index_row.py (추가)
275
- import json
276
- import os
277
-
278
- from .jsonl import read_jsonl
279
-
280
-
281
- def read_run_index(path: Path) -> list:
282
- """run-index jsonl 을 읽어 각 row 를 hydrate 한 full row 목록 반환."""
283
- return [hydrate_run_row(r) for r in read_jsonl(path)]
284
-
285
-
286
- def _rewrite_slim(path: Path) -> int:
287
- rows = read_jsonl(path)
288
- if not rows:
289
- return 0
290
- tmp = path.with_suffix(path.suffix + ".tmp")
291
- with tmp.open("w") as f:
292
- for r in rows:
293
- f.write(json.dumps(slim_run_row(r), separators=(",", ":")) + "\n")
294
- os.replace(tmp, path)
295
- return len(rows)
296
-
297
-
298
- def reindex_slim(home: Path) -> int:
299
- """기존 run-index 파일들을 slim 으로 일괄 재작성. 멱등. 변환 row 총수 반환."""
300
- total = 0
301
- targets = [home / "recent.jsonl", home / "active.jsonl"]
302
- projects = home / "projects"
303
- if projects.is_dir():
304
- targets += sorted(projects.glob("*/index.jsonl"))
305
- for path in targets:
306
- if path.is_file():
307
- total += _rewrite_slim(path)
308
- return total
309
- ```
310
-
311
- - [ ] **Step 4: Run test to verify it passes**
312
-
313
- Run: `python3 -m pytest tests/run/test_run_index_row.py -v`
314
- Expected: PASS (7 tests)
315
-
316
- - [ ] **Step 5: Commit**
317
-
318
- ```bash
319
- git add scripts/okstra_ctl/run_index_row.py tests/run/test_run_index_row.py
320
- git commit -m "feat(index): read_run_index hydrate 와 reindex_slim 일괄 변환 추가"
321
- ```
322
-
323
- ---
324
-
325
- ### Task 3: writer 배선 — index.py / backfill.py
326
-
327
- **Files:**
328
- - Modify: `scripts/okstra_ctl/index.py:74-97` (record_start), `scripts/okstra_ctl/index.py:162-183` (reserve_run_in_active)
329
- - Modify: `scripts/okstra_ctl/backfill.py:219-235` (row 생성)
330
- - Test: 기존 `tests/run/` 회귀 (`test_okstra_index*.py`, `test_okstra_backfill*.py` 가 있으면 그 전체)
331
-
332
- **Interfaces:**
333
- - Consumes: `build_run_index_row` (Task 1).
334
- - Produces: writer 가 기록하는 모든 row 가 slim(13필드). runId 값과 인덱스 invariant는 불변.
335
-
336
- - [ ] **Step 1: Modify record_start to use build_run_index_row**
337
-
338
- [`scripts/okstra_ctl/index.py:74-97`](../../../scripts/okstra_ctl/index.py:74)의 `row = { ... }` 리터럴 전체를 다음으로 치환:
339
-
340
- ```python
341
- row = build_run_index_row(
342
- project_id=project_id, project_root=project_root,
343
- task_group=task_group, task_id=task_id, task_type=task_type,
344
- run_seq=run_seq, status=initial_status, started_at=when,
345
- finished_at=when if is_terminal else None,
346
- workers=workers, lead_model=lead_model, validation="not-run",
347
- run_dir_rel=run_dir_rel, final_report_rel=final_report_rel,
348
- final_status_rel=final_status_rel)
349
- ```
350
-
351
- 파일 상단 import에 추가: `from .run_index_row import build_run_index_row`
352
- (`run_id`/`inv_path_abs`/`inv_path_rel` 계산 라인 67-70, save_invocation 호출은 그대로 둔다 — `row["runId"]` 대신 기존 `run_id` 변수를 계속 사용.)
353
-
354
- - [ ] **Step 2: Modify reserve_run_in_active to use build_run_index_row**
355
-
356
- [`scripts/okstra_ctl/index.py:162-183`](../../../scripts/okstra_ctl/index.py:162)의 `row = { ... }` 리터럴 전체를 치환:
357
-
358
- ```python
359
- row = build_run_index_row(
360
- project_id=project_id, project_root=project_root,
361
- task_group=task_group, task_id=task_id, task_type=task_type,
362
- run_seq=run_seq, status="reserving", started_at=when,
363
- finished_at=None, workers=[], lead_model="", validation="not-run",
364
- run_dir_rel=run_dir_rel, final_report_rel=final_report_rel,
365
- final_status_rel=final_status_rel)
366
- ```
367
-
368
- (`inv_rel` 계산 라인 160-161은 더 이상 row에 쓰이지 않으므로 제거한다.)
369
-
370
- - [ ] **Step 3: Modify backfill row to use build_run_index_row**
371
-
372
- [`scripts/okstra_ctl/backfill.py:219-235`](../../../scripts/okstra_ctl/backfill.py:219)의 `row = { ... }` 리터럴 전체를 치환:
373
-
374
- ```python
375
- row = build_run_index_row(
376
- project_id=project_id, project_root=str(project_root),
377
- task_group=actual_group, task_id=actual_task,
378
- task_type=actual_type, run_seq=seq,
379
- status=normalized_status, started_at=started_at,
380
- finished_at=None if is_non_terminal else finished_at,
381
- workers=raw_workers, lead_model=raw_lead_model,
382
- validation=validation_status, run_dir_rel=run_dir_rel,
383
- final_report_rel=final_rel,
384
- final_status_rel=final_status_rel)
385
- ```
386
-
387
- backfill.py 상단 import에 추가: `from .run_index_row import build_run_index_row`
388
- (라인 199-201의 `inv_rel` 계산은 row에서 더 이상 안 쓰이지만 save_invocation 에는 무관 — `inv_rel` 변수가 다른 곳에서 안 쓰이면 제거. 라인 244의 save_invocation은 `actual_group/actual_task/actual_type/seq`를 직접 넘기므로 영향 없음.)
389
-
390
- - [ ] **Step 4: Run regression tests**
391
-
392
- Run: `python3 -m pytest tests/run/ -v`
393
- Expected: PASS. 만약 기존 테스트가 디스크 row에서 `projectId`/`taskType`/`runSeq`/`taskKey`/`invocationFile`을 직접 단언한다면, 그 테스트의 read를 `read_run_index`로 바꾸거나 단언을 `hydrate_run_row(row)[...]` 기준으로 수정한다(assertion을 약화하지 말 것 — 동일 값 검증 유지).
394
-
395
- - [ ] **Step 5: Commit**
396
-
397
- ```bash
398
- git add scripts/okstra_ctl/index.py scripts/okstra_ctl/backfill.py tests/run/
399
- git commit -m "refactor(index): writer 가 build_run_index_row 로 slim row 만 기록"
400
- ```
401
-
402
- ---
403
-
404
- ### Task 4: reader 배선 — sequence.py / listing.py
405
-
406
- **Files:**
407
- - Modify: `scripts/okstra_ctl/sequence.py:43-44`
408
- - Modify: `scripts/okstra_ctl/listing.py:84,88-89`
409
- - Test: 기존 `tests/run/` 회귀
410
-
411
- **Interfaces:**
412
- - Consumes: `read_run_index` (Task 2).
413
- - Produces: sequence/listing 의 row 소비 로직 무변경, 디스크 slim → 메모리 full.
414
-
415
- - [ ] **Step 1: sequence.py — read_jsonl → read_run_index**
416
-
417
- [`scripts/okstra_ctl/sequence.py:8-9`](../../../scripts/okstra_ctl/sequence.py:8) import 교체:
418
- `from .jsonl import read_jsonl` → `from .run_index_row import read_run_index`
419
-
420
- [`scripts/okstra_ctl/sequence.py:43-44`](../../../scripts/okstra_ctl/sequence.py:43):
421
- ```python
422
- for src in ("active.jsonl", "recent.jsonl"):
423
- for r in read_run_index(home / src):
424
- ```
425
- (아래 `r.get("projectId")`/`taskGroup`/`taskId`/`taskType`/`runSeq` 비교 로직은 그대로 — hydrate된 full row라 모두 존재.)
426
-
427
- - [ ] **Step 2: listing.py — list_runs 의 read 를 read_run_index 로**
428
-
429
- [`scripts/okstra_ctl/listing.py:10`](../../../scripts/okstra_ctl/listing.py:10) import 추가:
430
- `from .run_index_row import read_run_index` (기존 `from .jsonl import read_jsonl`는 다른 곳에서 안 쓰이면 제거).
431
-
432
- [`scripts/okstra_ctl/listing.py:84`](../../../scripts/okstra_ctl/listing.py:84):
433
- ```python
434
- rows = read_run_index(home / "active.jsonl") + read_run_index(home / "recent.jsonl")
435
- ```
436
-
437
- [`scripts/okstra_ctl/listing.py:88-89`](../../../scripts/okstra_ctl/listing.py:88) (archive도 hydrate):
438
- ```python
439
- for f in sorted(archive_dir.glob("*/*.jsonl")):
440
- rows.extend(read_run_index(f))
441
- ```
442
- (`format_show`/`format_runs_table`/필터의 `projectId`/`taskType`/`invocationFile` 접근은 hydrate된 row라 그대로 동작.)
443
-
444
- - [ ] **Step 3: Run regression tests**
445
-
446
- Run: `python3 -m pytest tests/run/ -v`
447
- Expected: PASS. listing/sequence 테스트가 status/show 출력을 검증한다면 동일 출력 유지(파생 필드가 hydrate로 채워짐).
448
-
449
- - [ ] **Step 4: Commit**
450
-
451
- ```bash
452
- git add scripts/okstra_ctl/sequence.py scripts/okstra_ctl/listing.py
453
- git commit -m "refactor(index): sequence/listing 이 read_run_index 로 hydrate 후 소비"
454
- ```
455
-
456
- ---
457
-
458
- ### Task 5: reconcile.py 배선 — read hydrate + write slim
459
-
460
- **Files:**
461
- - Modify: `scripts/okstra_ctl/reconcile.py` (read 4곳, write 4곳)
462
- - Test: 기존 `tests/run/test_*reconcile*.py` 회귀
463
-
464
- **Interfaces:**
465
- - Consumes: `read_run_index`, `slim_run_row` (Task 1·2).
466
- - Produces: reconcile 가 in-place 재작성 시 slim 유지. 종결 추론/promote 로직 무변경.
467
-
468
- - [ ] **Step 1: import 교체**
469
-
470
- [`scripts/okstra_ctl/reconcile.py:10`](../../../scripts/okstra_ctl/reconcile.py:10):
471
- ```python
472
- from .jsonl import append_jsonl, rotate_recent_if_needed
473
- from .run_index_row import read_run_index, slim_run_row
474
- ```
475
- (`read_jsonl` 제거 — 아래에서 모두 교체.)
476
-
477
- - [ ] **Step 2: read 지점 3곳 → read_run_index**
478
-
479
- - [`reconcile.py:89`](../../../scripts/okstra_ctl/reconcile.py:89) (`_sync_project_index`): `existing = read_jsonl(proj_index)` → `existing = read_run_index(proj_index)`
480
- - [`reconcile.py:121`](../../../scripts/okstra_ctl/reconcile.py:121): `rows = read_jsonl(active)` → `rows = read_run_index(active)`
481
- - [`reconcile.py:198`](../../../scripts/okstra_ctl/reconcile.py:198): `rows = read_jsonl(recent)` → `rows = read_run_index(recent)`
482
-
483
- (`row.get("projectId")`/`taskType`/`runSeq`/`runDirRel`/`projectRoot` 소비는 hydrate된 row라 그대로 동작.)
484
-
485
- - [ ] **Step 3: write 지점 4곳 → slim_run_row 적용**
486
-
487
- - [`reconcile.py:96-97`](../../../scripts/okstra_ctl/reconcile.py:96) (`_sync_project_index` 재작성):
488
- ```python
489
- for er in existing:
490
- f.write(json.dumps(slim_run_row(er), separators=(",", ":")) + "\n")
491
- ```
492
- - [`reconcile.py:170-171`](../../../scripts/okstra_ctl/reconcile.py:170) (active survivors 재작성):
493
- ```python
494
- for row in survivors:
495
- f.write(json.dumps(slim_run_row(row), separators=(",", ":")) + "\n")
496
- ```
497
- - [`reconcile.py:174`](../../../scripts/okstra_ctl/reconcile.py:174) (recent append):
498
- ```python
499
- append_jsonl(home / "recent.jsonl", slim_run_row(row))
500
- ```
501
- - [`reconcile.py:223-225`](../../../scripts/okstra_ctl/reconcile.py:223) (recent in-place 재작성):
502
- ```python
503
- for row in rows:
504
- f.write(json.dumps(slim_run_row(row), separators=(",", ":")) + "\n")
505
- ```
506
-
507
- (라인 175·177의 `row["projectId"]`/`row["projectRoot"]`는 hydrate된 row라 존재 — 그대로.)
508
-
509
- - [ ] **Step 4: Run regression tests**
510
-
511
- Run: `python3 -m pytest tests/run/ -v`
512
- Expected: PASS (reconcile_active promote, reconcile_recent in-place, _sync_project_index 시나리오).
513
-
514
- - [ ] **Step 5: Commit**
515
-
516
- ```bash
517
- git add scripts/okstra_ctl/reconcile.py
518
- git commit -m "refactor(index): reconcile read hydrate + write slim 로 정합 유지"
519
- ```
520
-
521
- ---
522
-
523
- ### Task 6: 일괄 마이그레이션 실행 + 빌드 + 전체 검증
524
-
525
- **Files:**
526
- - 변경 없음 (실행/검증 task)
527
-
528
- **Interfaces:**
529
- - Consumes: `reindex_slim` (Task 2).
530
-
531
- - [ ] **Step 1: 전체 단위 테스트**
532
-
533
- Run: `python3 -m pytest tests/`
534
- Expected: PASS (전 도메인).
535
-
536
- - [ ] **Step 2: 실 데이터 백업 후 마이그레이션 dry 확인**
537
-
538
- ```bash
539
- cp -R ~/.okstra/recent.jsonl ~/.okstra/active.jsonl /tmp/okstra-index-backup/ 2>/dev/null; mkdir -p /tmp/okstra-index-backup
540
- cp ~/.okstra/recent.jsonl /tmp/okstra-index-backup/recent.jsonl
541
- python3 -c "import sys; sys.path.insert(0,'scripts'); from pathlib import Path; from okstra_ctl.run_index_row import reindex_slim, read_run_index; before=read_run_index(Path.home()/'.okstra'/'recent.jsonl'); print('rows', len(before))"
542
- ```
543
- Expected: `rows 124` (또는 현재 수) — 에러 없이 hydrate 성공.
544
-
545
- - [ ] **Step 3: 마이그레이션 실행**
546
-
547
- ```bash
548
- python3 -c "import sys; sys.path.insert(0,'scripts'); from pathlib import Path; from okstra_ctl.run_index_row import reindex_slim; print('migrated', reindex_slim(Path.home()/'.okstra'))"
549
- ```
550
- Expected: `migrated <N>` (recent + active + 6 index 의 총 row 수, 약 246).
551
-
552
- - [ ] **Step 4: 마이그레이션 후 정합 검증**
553
-
554
- ```bash
555
- python3 -c "import sys,json; sys.path.insert(0,'scripts'); from pathlib import Path; from okstra_ctl.run_index_row import read_run_index; bak=[json.loads(l) for l in open('/tmp/okstra-index-backup/recent.jsonl') if l.strip()]; cur=read_run_index(Path.home()/'.okstra'/'recent.jsonl'); bak_by={r['runId']:r for r in bak}; assert all(cur_r['invocationFile']==bak_by[cur_r['runId']].get('invocationFile') and cur_r['taskKey']==bak_by[cur_r['runId']].get('taskKey') for cur_r in cur if cur_r['runId'] in bak_by), 'mismatch'; print('OK: hydrate 가 원본 파생필드 재현')"
556
- ```
557
- Expected: `OK: hydrate 가 원본 파생필드 재현` — 마이그레이션 전 full row의 파생 필드와 hydrate 결과가 일치.
558
-
559
- - [ ] **Step 5: 멱등 재실행 + CLI 스모크**
560
-
561
- ```bash
562
- python3 -c "import sys; sys.path.insert(0,'scripts'); from pathlib import Path; from okstra_ctl.run_index_row import reindex_slim; print('reidempotent', reindex_slim(Path.home()/'.okstra'))"
563
- node bin/okstra --version
564
- ```
565
- Expected: 두 번째 reindex_slim도 정상, CLI 정상.
566
-
567
- - [ ] **Step 6: runtime 빌드 + 커밋**
568
-
569
- ```bash
570
- npm run build
571
- git add -A
572
- git commit -m "chore(index): runtime 동기화 (run-index row slim 포맷)"
573
- ```
574
- Expected: `runtime/` 가 새 소스로 갱신.
575
-
576
- ---
577
-
578
- ## Self-Review
579
-
580
- - **Spec coverage:** §3.1 제거 5필드 → Task 1 `DERIVED_FIELDS`/`build_run_index_row`. §3.2 유지 13필드 → Task 1 row 구성 + 테스트. §4.1 두 헬퍼 → Task 1·2. §4.2 read hydrate(6곳 후보 → 검증으로 3곳 확정) → Task 4·5 (pane_reclaim/recap/wizard는 run-index row 비소비로 제외, 본문에 근거 명시). §5 마이그레이션(migrate.py 분리) → Task 2 `reindex_slim` + Task 6 실행. §6 테스트 5종 → Task 1·2 테스트. §7 비범위 → 계획에 포함 안 함.
581
- - **Placeholder scan:** 모든 코드 step에 실제 코드/명령/기대출력 포함. TBD 없음.
582
- - **Type consistency:** `build_run_index_row`/`slim_run_row`/`hydrate_run_row`/`read_run_index`/`reindex_slim`/`DERIVED_FIELDS` 시그니처가 Task 1→6 전반에서 일관. row 키 camelCase 일관.
583
- - **주의(실행 중 확인):** 기존 `tests/run/`의 일부 테스트가 디스크 row에서 파생 필드를 직접 단언할 수 있다. Task 3·4·5의 회귀 step에서 발견 시, read를 `read_run_index`로 바꾸거나 단언을 동일 값 기준으로 갱신한다 — assertion 약화 금지.