okstra 0.102.2 → 0.102.4

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 (167) hide show
  1. package/README.kr.md +5 -5
  2. package/README.md +5 -5
  3. package/docs/kr/architecture/storage-model.md +8 -3
  4. package/docs/kr/architecture.md +12 -13
  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/lib/okstra/interactive.sh +12 -0
  12. package/runtime/bin/okstra-antigravity-exec.sh +1 -2
  13. package/runtime/bin/okstra-codex-exec.sh +1 -2
  14. package/runtime/bin/okstra-render-final-report.py +2 -9
  15. package/runtime/bin/okstra-spawn-followups.py +4 -6
  16. package/runtime/bin/okstra-trace-cleanup.sh +24 -4
  17. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  18. package/runtime/prompts/lead/report-writer.md +1 -1
  19. package/runtime/prompts/profiles/_common-contract.md +9 -11
  20. package/runtime/python/okstra_ctl/codex_dispatch.py +6 -13
  21. package/runtime/python/okstra_ctl/consumers.py +35 -0
  22. package/runtime/python/okstra_ctl/container.py +4 -6
  23. package/runtime/python/okstra_ctl/dispatch_core.py +8 -13
  24. package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
  25. package/runtime/python/okstra_ctl/handoff.py +2 -1
  26. package/runtime/python/okstra_ctl/implementation_outcome.py +271 -0
  27. package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
  28. package/runtime/python/okstra_ctl/path_hints.py +665 -0
  29. package/runtime/python/okstra_ctl/render.py +4 -3
  30. package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
  31. package/runtime/python/okstra_ctl/report_views.py +7 -4
  32. package/runtime/python/okstra_ctl/run.py +3 -10
  33. package/runtime/python/okstra_ctl/run_context.py +4 -2
  34. package/runtime/python/okstra_ctl/stage_targets.py +18 -0
  35. package/runtime/python/okstra_ctl/wizard.py +37 -26
  36. package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
  37. package/runtime/python/okstra_project/state.py +40 -2
  38. package/runtime/python/okstra_token_usage/report.py +2 -5
  39. package/runtime/validators/validate-report-views.py +2 -1
  40. package/runtime/validators/validate-run.py +1 -11
  41. package/src/commands/lifecycle/install.mjs +5 -5
  42. package/src/commands/lifecycle/uninstall.mjs +4 -4
  43. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
  44. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
  45. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
  46. package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
  47. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
  48. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
  49. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
  50. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
  51. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
  52. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
  53. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
  54. package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
  55. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
  56. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
  57. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
  58. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
  59. package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
  60. package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
  61. package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
  62. package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
  63. package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
  64. package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
  65. package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
  66. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
  67. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
  68. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
  69. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
  70. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
  71. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
  72. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
  73. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
  74. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
  75. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
  76. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
  77. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
  78. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
  79. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
  80. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
  81. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
  82. package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
  83. package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
  84. package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
  85. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
  86. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
  87. package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
  88. package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
  89. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
  90. package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
  91. package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
  92. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
  93. package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
  94. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
  95. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
  96. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
  97. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
  98. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
  99. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
  100. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
  101. package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
  102. package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
  103. package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
  104. package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
  105. package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
  106. package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
  107. package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
  108. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
  109. package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
  110. package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
  111. package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
  112. package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
  113. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
  114. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
  115. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
  116. package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
  117. package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
  118. package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
  119. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
  120. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
  121. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
  122. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
  123. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
  124. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
  125. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
  126. package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
  127. package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
  128. package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
  129. package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
  130. package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
  131. package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
  132. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
  133. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
  134. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
  135. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
  136. package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
  137. package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
  138. package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
  139. package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
  140. package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
  141. package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
  142. package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
  143. package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
  144. package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
  145. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
  146. package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
  147. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
  148. package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
  149. package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
  150. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
  151. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
  152. package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
  153. package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
  154. package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
  155. package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
  156. package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
  157. package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
  158. package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
  159. package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
  160. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
  161. package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
  162. package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
  163. package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
  164. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
  165. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
  166. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
  167. package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +0 -110
@@ -1,647 +0,0 @@
1
- # okstra-inspect `recap` facet 구현 계획
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-inspect 에 task-id 단위 전/후 요약 + 자유 Q&A 를 제공하는 8번째 facet `recap` 을 추가한다.
6
-
7
- **Architecture:** 결정론적 부분(timeline run 간 phase 전이 계산 = `recap assemble`, recap-log append = `recap record`)은 python `okstra_ctl/recap.py` 에 두고 단위 테스트로 검증한다. 요약 서술과 자유 Q&A 는 skill-markdown(okstra-inspect)이 그 산출물 위에서 수행한다. 경로는 SSOT 인 `compute_run_paths` 에만 추가한다.
8
-
9
- **Tech Stack:** Python 3 (argparse, 4-space indent, snake_case) · Node ES modules (`src/*.mjs`, 2-space indent) · skill markdown · pytest.
10
-
11
- ## Global Constraints
12
-
13
- - `runtime/` 는 빌드 산출물 — 절대 손으로 수정하지 않는다. 소스는 `scripts/`, `skills/`, `src/` 등에서만 고치고 `npm run build` 로 동기화한다.
14
- - 모든 okstra 산출물은 `<PROJECT_ROOT>/.okstra/` 안에만 쓴다. recap 은 `<task-root>/recap/recap-log.jsonl` 만 append 하고 다른 task 산출물(`task-manifest.json`, catalog, timeline)은 mutate 하지 않는다.
15
- - read-side python 모듈은 task-key/ task-root 입력 계약을 `okstra_ctl.task_target.resolve_task_root(target, project_root, cwd)` 로 공유한다 (context-cost / error-report 와 동일).
16
- - 사용자-노출 문서·주석 산문은 한국어, 코드 식별자/경로/CLI 인자는 영어.
17
- - 새 함수는 유효 코드 50줄 이하로 유지한다.
18
- - 타임스탬프는 `datetime.datetime.now(datetime.timezone.utc).isoformat()` (error_report 와 동일 패턴).
19
-
20
- ---
21
-
22
- ### Task 1: `compute_run_paths` 에 recap 경로 추가
23
-
24
- **Files:**
25
- - Modify: `scripts/okstra_ctl/paths.py` (task-root 레벨 경로 블록 `paths.py:122-130`, `abs_paths` 딕셔너리 `paths.py:206-302`, `rel_pairs` 리스트 `paths.py:303-350`)
26
- - Test: `tests/test_okstra_ctl_paths.py`
27
-
28
- **Interfaces:**
29
- - Consumes: 기존 `compute_run_paths(...)` 시그니처와 `task_root` 지역변수.
30
- - Produces: 컨텍스트 딕셔너리에 새 키 `RECAP_DIR`, `RECAP_LOG_PATH`(절대), `RECAP_DIR_RELATIVE_PATH`, `RECAP_LOG_RELATIVE_PATH`(project-root 상대). 경로 규약: `<task-root>/recap/` 디렉터리와 `<task-root>/recap/recap-log.jsonl` 파일.
31
-
32
- - [ ] **Step 1: Write the failing test**
33
-
34
- `tests/test_okstra_ctl_paths.py` 끝에 추가:
35
-
36
- ```python
37
- def test_recap_paths_present():
38
- ctx = compute_run_paths(**_common())
39
- assert ctx["RECAP_DIR"].endswith("/.okstra/tasks/g/t/recap")
40
- assert ctx["RECAP_LOG_PATH"].endswith("/.okstra/tasks/g/t/recap/recap-log.jsonl")
41
- assert ctx["RECAP_DIR_RELATIVE_PATH"] == ".okstra/tasks/g/t/recap"
42
- assert ctx["RECAP_LOG_RELATIVE_PATH"] == ".okstra/tasks/g/t/recap/recap-log.jsonl"
43
- ```
44
-
45
- - [ ] **Step 2: Run test to verify it fails**
46
-
47
- Run: `python3 -m pytest tests/test_okstra_ctl_paths.py::test_recap_paths_present -v`
48
- Expected: FAIL with `KeyError: 'RECAP_DIR'`.
49
-
50
- - [ ] **Step 3: Add the path locals**
51
-
52
- `scripts/okstra_ctl/paths.py` 의 `timeline_file = history_dir / "timeline.json"` (`paths.py:130`) 바로 다음 줄에 추가:
53
-
54
- ```python
55
- recap_dir = task_root / "recap"
56
- recap_log = recap_dir / "recap-log.jsonl"
57
- ```
58
-
59
- - [ ] **Step 4: Register absolute keys**
60
-
61
- `abs_paths` 딕셔너리에서 `"TIMELINE_PATH": str(timeline_file),` (`paths.py:249`) 다음 줄에 추가:
62
-
63
- ```python
64
- "RECAP_DIR": str(recap_dir),
65
- "RECAP_LOG_PATH": str(recap_log),
66
- ```
67
-
68
- - [ ] **Step 5: Register relative keys**
69
-
70
- `rel_pairs` 리스트에서 `("TIMELINE_RELATIVE_PATH", timeline_file),` (`paths.py:314`) 다음 줄에 추가:
71
-
72
- ```python
73
- ("RECAP_DIR_RELATIVE_PATH", recap_dir),
74
- ("RECAP_LOG_RELATIVE_PATH", recap_log),
75
- ```
76
-
77
- - [ ] **Step 6: Run test to verify it passes**
78
-
79
- Run: `python3 -m pytest tests/test_okstra_ctl_paths.py -v`
80
- Expected: PASS (신규 + 기존 테스트 모두).
81
-
82
- - [ ] **Step 7: Commit**
83
-
84
- ```bash
85
- git add scripts/okstra_ctl/paths.py tests/test_okstra_ctl_paths.py
86
- git commit -m "feat(paths): compute_run_paths 에 recap 디렉터리/로그 경로 추가"
87
- ```
88
-
89
- ---
90
-
91
- ### Task 2: `recap assemble` — timeline run 간 phase 전이 조립
92
-
93
- **Files:**
94
- - Create: `scripts/okstra_ctl/recap.py`
95
- - Test: `tests/test_okstra_recap.py`
96
-
97
- **Interfaces:**
98
- - Consumes: `okstra_ctl.task_target.resolve_task_root`, `project_rel`. task-root 의 `history/timeline.json`(`{"runs": [ {runTimestamp, taskType, status, reportPath, workflowSnapshot:{currentPhase, lastCompletedPhase, nextRecommendedPhase, phaseStates}} ... ]}`).
99
- - Produces: `assemble_recap(task_root: Path, project_root: Path) -> dict` 반환:
100
- ```
101
- {
102
- "taskKey": str,
103
- "runCount": int,
104
- "transitions": [ {"index": int, "runTimestamp": str, "taskType": str,
105
- "status": str, "fromPhase": str, "toPhase": str,
106
- "lastCompletedPhase": str, "nextRecommendedPhase": str,
107
- "reportPath": str} ... ],
108
- "latestPhaseStates": dict
109
- }
110
- ```
111
- `fromPhase` = 직전 run 의 `currentPhase`(첫 run 은 `""`), `toPhase` = 해당 run 의 `currentPhase`.
112
-
113
- - [ ] **Step 1: Write the failing test**
114
-
115
- `tests/test_okstra_recap.py` 생성:
116
-
117
- ```python
118
- import json
119
- import sys
120
- from pathlib import Path
121
-
122
- LIB_DIR = Path(__file__).resolve().parents[1] / "scripts"
123
- if str(LIB_DIR) not in sys.path:
124
- sys.path.insert(0, str(LIB_DIR))
125
-
126
- from okstra_ctl import recap
127
-
128
-
129
- def _run(ts, current, last="", nxt="", task_type="implementation",
130
- status="done", report=""):
131
- return {
132
- "runTimestamp": ts, "taskType": task_type, "status": status,
133
- "reportPath": report,
134
- "workflowSnapshot": {
135
- "currentPhase": current, "lastCompletedPhase": last,
136
- "nextRecommendedPhase": nxt, "phaseStates": {current: "done"},
137
- },
138
- }
139
-
140
-
141
- def _write_timeline(task_root: Path, runs: list[dict]) -> None:
142
- hist = task_root / "history"
143
- hist.mkdir(parents=True, exist_ok=True)
144
- (hist / "timeline.json").write_text(
145
- json.dumps({"runs": runs}, ensure_ascii=False), encoding="utf-8")
146
- (task_root / "task-manifest.json").write_text(
147
- json.dumps({"taskKey": "p:g:DEV-1"}, ensure_ascii=False), encoding="utf-8")
148
-
149
-
150
- def test_assemble_computes_adjacent_transitions(tmp_path):
151
- _write_timeline(tmp_path, [
152
- _run("2026-06-01T00:00:00+00:00", "requirements-discovery",
153
- nxt="error-analysis"),
154
- _run("2026-06-02T00:00:00+00:00", "error-analysis",
155
- last="requirements-discovery", nxt="implementation-planning"),
156
- ])
157
- result = recap.assemble_recap(tmp_path, tmp_path)
158
- assert result["taskKey"] == "p:g:DEV-1"
159
- assert result["runCount"] == 2
160
- assert result["transitions"][0]["fromPhase"] == ""
161
- assert result["transitions"][0]["toPhase"] == "requirements-discovery"
162
- assert result["transitions"][1]["fromPhase"] == "requirements-discovery"
163
- assert result["transitions"][1]["toPhase"] == "error-analysis"
164
- assert result["latestPhaseStates"] == {"error-analysis": "done"}
165
-
166
-
167
- def test_assemble_empty_timeline(tmp_path):
168
- (tmp_path / "history").mkdir(parents=True)
169
- (tmp_path / "history" / "timeline.json").write_text(
170
- json.dumps({"runs": []}), encoding="utf-8")
171
- (tmp_path / "task-manifest.json").write_text(
172
- json.dumps({"taskKey": "p:g:DEV-1"}), encoding="utf-8")
173
- result = recap.assemble_recap(tmp_path, tmp_path)
174
- assert result["runCount"] == 0
175
- assert result["transitions"] == []
176
- assert result["latestPhaseStates"] == {}
177
- ```
178
-
179
- - [ ] **Step 2: Run test to verify it fails**
180
-
181
- Run: `python3 -m pytest tests/test_okstra_recap.py -v`
182
- Expected: FAIL with `ModuleNotFoundError: No module named 'okstra_ctl.recap'`.
183
-
184
- - [ ] **Step 3: Write the module with assemble**
185
-
186
- `scripts/okstra_ctl/recap.py` 생성:
187
-
188
- ```python
189
- """Read-side recap for a task: assemble run-to-run phase transitions and
190
- append a recap Q&A log. Mutates only <task-root>/recap/recap-log.jsonl;
191
- never touches task-manifest / catalog / timeline.
192
- """
193
- from __future__ import annotations
194
-
195
- import argparse
196
- import datetime as dt
197
- import json
198
- import sys
199
- from pathlib import Path
200
-
201
- from okstra_ctl.task_target import resolve_task_root, project_rel
202
-
203
-
204
- def _load_timeline(task_root: Path) -> list[dict]:
205
- path = task_root / "history" / "timeline.json"
206
- if not path.is_file():
207
- return []
208
- try:
209
- data = json.loads(path.read_text(encoding="utf-8"))
210
- except Exception:
211
- return []
212
- runs = data.get("runs", [])
213
- return runs if isinstance(runs, list) else []
214
-
215
-
216
- def _task_key(task_root: Path) -> str:
217
- path = task_root / "task-manifest.json"
218
- if not path.is_file():
219
- return ""
220
- try:
221
- return json.loads(path.read_text(encoding="utf-8")).get("taskKey", "")
222
- except Exception:
223
- return ""
224
-
225
-
226
- def assemble_recap(task_root: Path, project_root: Path) -> dict:
227
- runs = _load_timeline(task_root)
228
- transitions = []
229
- prev_phase = ""
230
- latest_states: dict = {}
231
- for idx, run in enumerate(runs):
232
- snap = run.get("workflowSnapshot", {}) if isinstance(run, dict) else {}
233
- cur = snap.get("currentPhase", "")
234
- transitions.append({
235
- "index": idx,
236
- "runTimestamp": run.get("runTimestamp", ""),
237
- "taskType": run.get("taskType", ""),
238
- "status": run.get("status", ""),
239
- "fromPhase": prev_phase,
240
- "toPhase": cur,
241
- "lastCompletedPhase": snap.get("lastCompletedPhase", ""),
242
- "nextRecommendedPhase": snap.get("nextRecommendedPhase", ""),
243
- "reportPath": run.get("reportPath", ""),
244
- })
245
- prev_phase = cur
246
- latest_states = snap.get("phaseStates", {}) or latest_states
247
- return {
248
- "taskKey": _task_key(task_root),
249
- "runCount": len(runs),
250
- "transitions": transitions,
251
- "latestPhaseStates": latest_states,
252
- }
253
- ```
254
-
255
- - [ ] **Step 4: Run test to verify it passes**
256
-
257
- Run: `python3 -m pytest tests/test_okstra_recap.py -v`
258
- Expected: PASS (2 tests).
259
-
260
- - [ ] **Step 5: Commit**
261
-
262
- ```bash
263
- git add scripts/okstra_ctl/recap.py tests/test_okstra_recap.py
264
- git commit -m "feat(recap): timeline run 간 phase 전이 조립(assemble_recap)"
265
- ```
266
-
267
- ---
268
-
269
- ### Task 3: `recap record` — recap-log.jsonl append + CLI 진입점
270
-
271
- **Files:**
272
- - Modify: `scripts/okstra_ctl/recap.py`
273
- - Test: `tests/test_okstra_recap.py`
274
-
275
- **Interfaces:**
276
- - Consumes: Task 2 의 `assemble_recap`, `resolve_task_root`, `project_rel`.
277
- - Produces:
278
- - `append_recap_entry(task_root, project_root, *, kind, mode, question, answer_summary, citations, now) -> dict` — `<task-root>/recap/recap-log.jsonl` 에 한 줄 append, `{"logPath": <project-rel>, "entry": <written dict>}` 반환. entry 스키마: `{ts, kind, mode, question, answerSummary, citations}`.
279
- - `main(argv)` — `okstra recap` 의 두 서브커맨드(`assemble`, `record`)를 argparse 로 디스패치하고 결과 JSON 을 stdout 에 출력.
280
-
281
- - [ ] **Step 1: Write the failing test**
282
-
283
- `tests/test_okstra_recap.py` 에 추가:
284
-
285
- ```python
286
- def test_append_recap_entry_appends_one_line(tmp_path):
287
- (tmp_path / "task-manifest.json").write_text(
288
- json.dumps({"taskKey": "p:g:DEV-1"}), encoding="utf-8")
289
- now = dt.datetime(2026, 6, 19, tzinfo=dt.timezone.utc)
290
- first = recap.append_recap_entry(
291
- tmp_path, tmp_path, kind="qa", mode="artifact",
292
- question="무엇을 했나?", answer_summary="phase 2 까지 진행",
293
- citations=["history/timeline.json:1"], now=now)
294
- second = recap.append_recap_entry(
295
- tmp_path, tmp_path, kind="summary", mode="code",
296
- question="", answer_summary="요약", citations=[], now=now)
297
- log = tmp_path / "recap" / "recap-log.jsonl"
298
- lines = [l for l in log.read_text(encoding="utf-8").splitlines() if l.strip()]
299
- assert len(lines) == 2
300
- rec0 = json.loads(lines[0])
301
- assert rec0["kind"] == "qa"
302
- assert rec0["mode"] == "artifact"
303
- assert rec0["question"] == "무엇을 했나?"
304
- assert rec0["answerSummary"] == "phase 2 까지 진행"
305
- assert rec0["citations"] == ["history/timeline.json:1"]
306
- assert rec0["ts"] == "2026-06-19T00:00:00+00:00"
307
- assert first["logPath"].endswith("recap/recap-log.jsonl")
308
- assert second["entry"]["kind"] == "summary"
309
-
310
-
311
- def test_main_assemble_prints_json(tmp_path, capsys):
312
- _write_timeline(tmp_path, [
313
- _run("2026-06-01T00:00:00+00:00", "implementation"),
314
- ])
315
- rc = recap.main(["assemble", str(tmp_path)])
316
- out = json.loads(capsys.readouterr().out)
317
- assert rc == 0
318
- assert out["runCount"] == 1
319
-
320
-
321
- def test_main_record_appends_and_prints(tmp_path, capsys):
322
- (tmp_path / "task-manifest.json").write_text(
323
- json.dumps({"taskKey": "p:g:DEV-1"}), encoding="utf-8")
324
- rc = recap.main([
325
- "record", str(tmp_path), "--kind", "qa", "--mode", "artifact",
326
- "--question", "q?", "--answer", "a.",
327
- "--citation", "x:1", "--citation", "y:2",
328
- ])
329
- out = json.loads(capsys.readouterr().out)
330
- assert rc == 0
331
- assert out["entry"]["citations"] == ["x:1", "y:2"]
332
- log = tmp_path / "recap" / "recap-log.jsonl"
333
- assert len([l for l in log.read_text().splitlines() if l.strip()]) == 1
334
- ```
335
-
336
- - [ ] **Step 2: Run test to verify it fails**
337
-
338
- Run: `python3 -m pytest tests/test_okstra_recap.py -v`
339
- Expected: FAIL with `AttributeError: module 'okstra_ctl.recap' has no attribute 'append_recap_entry'`.
340
-
341
- - [ ] **Step 3: Add append + main**
342
-
343
- `scripts/okstra_ctl/recap.py` 끝(`assemble_recap` 다음)에 추가:
344
-
345
- ```python
346
- def append_recap_entry(task_root, project_root, *, kind, mode, question,
347
- answer_summary, citations, now) -> dict:
348
- recap_dir = task_root / "recap"
349
- recap_dir.mkdir(parents=True, exist_ok=True)
350
- log_path = recap_dir / "recap-log.jsonl"
351
- entry = {
352
- "ts": now.isoformat(),
353
- "kind": kind,
354
- "mode": mode,
355
- "question": question,
356
- "answerSummary": answer_summary,
357
- "citations": list(citations),
358
- }
359
- with log_path.open("a", encoding="utf-8") as fh:
360
- fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
361
- return {"logPath": project_rel(log_path, project_root), "entry": entry}
362
-
363
-
364
- def main(argv: list[str] | None = None) -> int:
365
- parser = argparse.ArgumentParser(prog="okstra recap")
366
- parser.add_argument("--project-root", default="")
367
- parser.add_argument("--cwd", default=".")
368
- sub = parser.add_subparsers(dest="command", required=True)
369
-
370
- p_asm = sub.add_parser("assemble", help="assemble run-to-run phase transitions")
371
- p_asm.add_argument("target", help="task root path or task-key")
372
-
373
- p_rec = sub.add_parser("record", help="append a recap Q&A/summary log entry")
374
- p_rec.add_argument("target", help="task root path or task-key")
375
- p_rec.add_argument("--kind", choices=["summary", "qa"], required=True)
376
- p_rec.add_argument("--mode", choices=["artifact", "code"], required=True)
377
- p_rec.add_argument("--question", default="")
378
- p_rec.add_argument("--answer", required=True)
379
- p_rec.add_argument("--citation", action="append", default=[])
380
-
381
- args = parser.parse_args(argv)
382
- task_root, project_root = resolve_task_root(
383
- args.target, args.project_root, args.cwd)
384
-
385
- if args.command == "assemble":
386
- result = assemble_recap(task_root, project_root)
387
- else:
388
- result = append_recap_entry(
389
- task_root, project_root, kind=args.kind, mode=args.mode,
390
- question=args.question, answer_summary=args.answer,
391
- citations=args.citation,
392
- now=dt.datetime.now(dt.timezone.utc))
393
- print(json.dumps(result, ensure_ascii=False, indent=2))
394
- return 0
395
-
396
-
397
- if __name__ == "__main__":
398
- raise SystemExit(main(sys.argv[1:]))
399
- ```
400
-
401
- - [ ] **Step 4: Run test to verify it passes**
402
-
403
- Run: `python3 -m pytest tests/test_okstra_recap.py -v`
404
- Expected: PASS (5 tests).
405
-
406
- - [ ] **Step 5: Commit**
407
-
408
- ```bash
409
- git add scripts/okstra_ctl/recap.py tests/test_okstra_recap.py
410
- git commit -m "feat(recap): recap-log.jsonl append + assemble/record CLI 디스패치"
411
- ```
412
-
413
- ---
414
-
415
- ### Task 4: Node CLI 래퍼 `okstra recap`
416
-
417
- **Files:**
418
- - Create: `src/recap.mjs`
419
- - Modify: `src/cli-registry.mjs` (introspection 카테고리, `error-report` 항목 `cli-registry.mjs:106-112` 바로 다음)
420
-
421
- **Interfaces:**
422
- - Consumes: `./_python-helper.mjs` 의 `runPythonModule({module, args, stdio})` (error-report.mjs 와 동일).
423
- - Produces: `okstra recap <assemble|record> <target> ...` 가 `okstra_ctl.recap` 파이썬 모듈로 인자를 그대로 전달하고 stdout 을 그대로 흘려보낸다.
424
-
425
- - [ ] **Step 1: Create the wrapper**
426
-
427
- `src/recap.mjs` 생성:
428
-
429
- ```javascript
430
- import { runPythonModule } from "./_python-helper.mjs";
431
-
432
- const USAGE = `okstra recap — assemble a task's run-to-run phase recap, or append a recap log entry
433
-
434
- Usage:
435
- okstra recap assemble <task-root|task-key> [--project-root <dir>] [--cwd <dir>]
436
- okstra recap record <task-root|task-key> --kind <summary|qa> --mode <artifact|code> \\
437
- [--question <text>] --answer <text> [--citation <path:line> ...]
438
-
439
- \`assemble\` is read-only and prints a JSON summary of phase transitions across runs.
440
- \`record\` appends one line to <task-root>/recap/recap-log.jsonl; it never mutates
441
- other task artifacts.
442
- `;
443
-
444
- export async function run(args) {
445
- if (args.includes("--help") || args.includes("-h")) {
446
- process.stdout.write(USAGE);
447
- return 0;
448
- }
449
-
450
- const result = await runPythonModule({
451
- module: "okstra_ctl.recap",
452
- args,
453
- stdio: "inherit-stdout",
454
- });
455
- return result.code ?? 0;
456
- }
457
- ```
458
-
459
- - [ ] **Step 2: Register in cli-registry**
460
-
461
- `src/cli-registry.mjs` 의 `error-report` 객체(`cli-registry.mjs:106-112`) 닫는 `},` 다음에 추가:
462
-
463
- ```javascript
464
- {
465
- name: "recap",
466
- module: "./recap.mjs",
467
- export: "run",
468
- category: "introspection",
469
- summary: ["Assemble a task's run-to-run phase recap or append a recap log entry"],
470
- },
471
- ```
472
-
473
- - [ ] **Step 3: Smoke-test the CLI wiring**
474
-
475
- Run: `node bin/okstra recap --help`
476
- Expected: USAGE 텍스트 출력, exit 0.
477
-
478
- - [ ] **Step 4: Smoke-test the python bridge**
479
-
480
- Run: `node bin/okstra recap assemble /nonexistent-task-root`
481
- Expected: `no task root for task key:` 류의 메시지로 비정상 종료(파이썬 브리지가 연결됐다는 증거). 경로/스택 추적 없이 SystemExit 메시지면 정상.
482
-
483
- - [ ] **Step 5: Commit**
484
-
485
- ```bash
486
- git add src/recap.mjs src/cli-registry.mjs
487
- git commit -m "feat(cli): okstra recap Node 래퍼 + cli-registry 등록"
488
- ```
489
-
490
- ---
491
-
492
- ### Task 5: okstra-inspect 에 `recap` facet 추가
493
-
494
- **Files:**
495
- - Modify: `skills/okstra-inspect/SKILL.md` (frontmatter description `SKILL.md:3-4`, facet 테이블 `SKILL.md:11-19`, facet 본문 — `errors` 섹션 `SKILL.md:695` 다음 끝에 추가)
496
-
497
- **Interfaces:**
498
- - Consumes: Task 4 의 `okstra recap assemble` / `okstra recap record` CLI. Step 0 의 공유 부트스트랩(`okstra ensure-installed`, `okstra check-project --json`)과 target 해석 규약(task-key / task-id catalog 매칭 / task-root path — `cost`·`errors` 와 동일).
499
- - Produces: 사용자에게 노출되는 8번째 sub-command `recap`.
500
-
501
- - [ ] **Step 1: Extend the frontmatter description**
502
-
503
- `SKILL.md:3-4` 의 `description` 에서 `dispatches by sub-command to seven facets — status, history, report, time, logs, cost, errors.` 를 `dispatches by sub-command to eight facets — status, history, report, time, logs, cost, errors, recap.` 로 바꾸고, 트리거 문자열 목록 끝(`"실패 로그 정리".` 직전)에 다음을 추가:
504
-
505
- ```
506
- "okstra recap", "recap", "작업 요약", "이 task 요약", "전후 요약", "이 작업 설명해줘", "task 질문",
507
- ```
508
-
509
- - [ ] **Step 2: Add the facet table row**
510
-
511
- `SKILL.md:19` 의 `errors` 행 바로 다음에 추가:
512
-
513
- ```
514
- | `recap` | Summarize a task's run-to-run phase transitions, then answer free-form questions over its `.okstra` artifacts. Appends each summary/Q&A to `recap/recap-log.jsonl`. |
515
- ```
516
-
517
- 그리고 `SKILL.md:9` 문장의 "Sub-commands:" 앞에 있는 facet 나열 설명이 7개를 가정하면 8개로 맞춘다(해당 문장에 숫자가 박혀 있지 않으면 변경 불필요 — 확인 후 진행).
518
-
519
- - [ ] **Step 3: Add the `recap` facet body**
520
-
521
- `errors — Output template` 블록(`SKILL.md:757` 부근, 파일 끝) 다음에 추가:
522
-
523
- ````markdown
524
- ---
525
-
526
- ## recap
527
-
528
- Trigger phrases: "okstra recap", "recap", "작업 요약", "이 task 요약", "전후 요약", "이 작업 설명해줘", "task 질문".
529
-
530
- task-id 하나에 쌓인 `.okstra` 산출물 위에서 (a) 처리 전/후 요약을 내고, (b) 그 작업에 대한 자유 질문에 답한다. 기본은 `.okstra/` 서브트리만 읽는다(artifact 모드). 사용자가 명시적으로 코드 변경까지 봐 달라고 요청할 때만 code 모드로 확장한다. 이 sub-command 는 `recap/recap-log.jsonl` 만 append 하고 `task-manifest.json` / catalog / timeline 은 절대 mutate 하지 않는다.
531
-
532
- ### recap.1 — Resolve target
533
-
534
- `cost` / `errors` 와 동일한 3형식: ① full task-key, ② task-id 만(→ `.okstra/discovery/task-catalog.json` 의 `taskId` 대소문자 무시 매칭; 복수면 나열·질문, 없으면 not-found), ③ task-root path.
535
-
536
- ### recap.2 — Assemble the before/after summary
537
-
538
- CLI 출력을 source of truth 로 쓴다:
539
-
540
- ```bash
541
- okstra recap assemble <resolved-target> --project-root <projectRoot>
542
- ```
543
-
544
- stdout JSON(`{taskKey, runCount, transitions[], latestPhaseStates}`)을 파싱해 run 단위 전/후 전이를 서술한다. 각 `transitions[]` 항목의 `fromPhase → toPhase`(직전 run currentPhase → 이번 run currentPhase), `status`, `lastCompletedPhase`, `nextRecommendedPhase`, `reportPath` 를 시간순으로 보여준다. `runCount == 0` 이면 "이 task 에는 기록된 run 이 없습니다." 라고만 답하고 파일을 읽었다고 주장하지 않는다.
545
-
546
- `reportPath` 가 있는 run 은 사용자가 더 깊은 요약을 원할 때만 해당 final-report 를 읽어 보강한다(자동으로 모두 읽지 않는다).
547
-
548
- ### recap.3 — Free-form Q&A loop
549
-
550
- 요약 출력 뒤 사용자의 임의 질문을 받는다.
551
-
552
- - **artifact 모드(기본)**: `.okstra/` 산출물(timeline, task-manifest, 해당 run 의 final-report)만 근거로 답한다. 모든 사실 주장은 `.okstra` 파일 `path:line` 인용을 동반한다.
553
- - **code 모드(opt-in)**: 사용자가 "코드 변경까지 / diff 까지 봐줘" 류로 **명시적으로 요청**할 때만, 해당 task 의 worktree/브랜치에서 `git diff` · `git log` 를 추가로 읽어 코드 레벨로 답한다. recap 진입만으로는 절대 code 모드로 넘어가지 않는다.
554
-
555
- ### recap.4 — Persist each turn
556
-
557
- 요약 1건과 각 Q&A 답변을 로그에 남긴다(append-only):
558
-
559
- ```bash
560
- okstra recap record <resolved-target> --project-root <projectRoot> \
561
- --kind <summary|qa> --mode <artifact|code> \
562
- --question "<질문 또는 생략>" --answer "<한두 줄 요약>" \
563
- --citation "<path:line>" --citation "<path:line>"
564
- ```
565
-
566
- `--answer` 에는 전체 답변 전사가 아니라 한두 줄 요약을 넣는다. 인용이 여러 개면 `--citation` 을 반복한다. 기록 실패(비정상 종료)는 조용히 넘기지 말고 사용자에게 알린다.
567
-
568
- ### recap — Output template
569
-
570
- ```markdown
571
- ## okstra Recap — <task-key>
572
-
573
- 전/후 요약 (runs: <N>):
574
-
575
- | # | when | task-type | from → to | status | next |
576
- |---|---|---|---|---|---|
577
- | 1 | <ts> | requirements-discovery | (시작) → requirements-discovery | done | error-analysis |
578
-
579
- <이후 자유 질문을 받습니다. 코드 변경까지 보려면 "diff 까지 봐줘" 라고 요청하세요.>
580
- ```
581
- ````
582
-
583
- - [ ] **Step 4: Verify the facet count is consistent**
584
-
585
- Run: `grep -n "seven facets\|eight facets\|seven\b" skills/okstra-inspect/SKILL.md`
586
- Expected: `seven` 잔존 없음, `eight facets` 만 존재. 잔존하면 그 자리를 8개 표현으로 고친다.
587
-
588
- - [ ] **Step 5: Commit**
589
-
590
- ```bash
591
- git add skills/okstra-inspect/SKILL.md
592
- git commit -m "feat(inspect): recap facet — task-id 전/후 요약 + 자유 Q&A"
593
- ```
594
-
595
- ---
596
-
597
- ### Task 6: 아키텍처 문서 갱신 + 빌드 동기화 + 전체 검증
598
-
599
- **Files:**
600
- - Modify: `docs/kr/architecture.md` (task-root 트리, `architecture.md:443-478` 부근)
601
- - (빌드) `npm run build` → `runtime/` 동기화
602
-
603
- **Interfaces:**
604
- - Consumes: Task 1–5 의 결과. 새 정보 없음(문서·빌드만).
605
-
606
- - [ ] **Step 1: Document the recap directory**
607
-
608
- `docs/kr/architecture.md` 의 task-root 트리(`history/`·`timeline.json` 가 그려진 블록, `architecture.md:443-478`)에 `recap/` 노드를 추가하고, 한 줄 설명을 단다:
609
-
610
- ```
611
- └── recap/
612
- └── recap-log.jsonl # recap facet 의 전/후 요약·Q&A append-only 로그 (다른 산출물 불변)
613
- ```
614
-
615
- - [ ] **Step 2: Sync runtime**
616
-
617
- Run: `npm run build`
618
- Expected: 에러 없이 완료. `recap.py` / `recap.mjs` / 갱신된 `SKILL.md` 가 `runtime/` 에 복사됨.
619
-
620
- - [ ] **Step 3: Run the full python suite**
621
-
622
- Run: `python3 -m pytest tests/ -q`
623
- Expected: 전부 PASS (신규 `tests/test_okstra_recap.py` 포함, 회귀 없음).
624
-
625
- - [ ] **Step 4: CLI smoke**
626
-
627
- Run: `node bin/okstra recap --help`
628
- Expected: USAGE 출력, exit 0.
629
-
630
- Run: `node bin/okstra --help`
631
- Expected: introspection 카테고리에 `recap` 이 나열됨.
632
-
633
- - [ ] **Step 5: Commit**
634
-
635
- ```bash
636
- git add docs/kr/architecture.md runtime/
637
- git commit -m "docs(architecture): task-root 트리에 recap/ 추가 + runtime 동기화"
638
- ```
639
-
640
- ---
641
-
642
- ## 검증 (계획 전체)
643
-
644
- - 결정론적 로직(`compute_run_paths` recap 경로 / `assemble_recap` 전이 계산 / `append_recap_entry` append)은 모두 단위 테스트로 커버(Task 1·2·3).
645
- - facet 동작(요약 서술·Q&A·2-모드)은 skill-markdown 주도라 자동 단위 테스트 대상이 아님 → Task 4·6 의 CLI 스모크 + 수동 `okstra recap assemble <task-id>` 로 확인.
646
- - "요약 품질"은 휴리스틱이라 자동 검증 범위 밖(spec §5 와 동일 입장).
647
- - 단일 참조점: 경로는 `compute_run_paths` 한 곳, target 해석은 `task_target.resolve_task_root` 한 곳, Node↔python 브리지는 `runPythonModule` 한 곳으로만 흐른다.