okstra 0.104.0 → 0.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.kr.md +1 -0
- package/README.md +2 -1
- package/docs/contributor-change-matrix.md +3 -0
- package/docs/for-ai/README.md +5 -1
- package/docs/for-ai/skills/okstra-manager.md +62 -0
- package/docs/for-ai/skills/okstra-rollup.md +113 -0
- package/docs/kr/architecture/storage-model.md +29 -8
- package/docs/kr/architecture.md +5 -4
- package/docs/kr/cli.md +3 -3
- package/docs/project-structure-overview.md +36 -8
- package/docs/task-process/README.md +8 -6
- package/docs/task-process/common-flow.md +12 -3
- package/docs/task-process/final-verification.md +37 -26
- package/docs/task-process/implementation.md +28 -14
- package/docs/task-process/release-handoff.md +23 -9
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/python/okstra_ctl/container_registry.py +10 -31
- package/runtime/python/okstra_ctl/json_registry.py +43 -0
- package/runtime/python/okstra_ctl/rollup.py +113 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +6 -32
- package/runtime/skills/okstra-rollup/SKILL.md +73 -0
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/rollup.mjs +27 -0
- package/src/lib/skill-catalog.mjs +1 -0
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
flowchart TD
|
|
22
22
|
Start[/okstra-run/] --> Common[공통 task identity flow]
|
|
23
23
|
Common --> Type[task-type = final-verification]
|
|
24
|
-
Type -->
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
Type --> PlanPick[approved plan pick]
|
|
25
|
+
PlanPick --> Approved[approval marker confirm]
|
|
26
|
+
Approved --> Stage[stage pick<br/>whole-task or stage number]
|
|
27
|
+
Stage --> Defaults[Use defaults / Customize]
|
|
28
28
|
Defaults -->|defaults| Workers[worker roster pick<br/>claude/codex/report-writer default]
|
|
29
29
|
Defaults -->|customize| Models[lead + worker model prompts]
|
|
30
30
|
Models --> Extras[directive, related tasks, clarification]
|
|
@@ -35,38 +35,47 @@ flowchart TD
|
|
|
35
35
|
|
|
36
36
|
`final-verification`은 analyser roster가 있는 non-implementation phase다. 따라서 `Use defaults`를 골라도 worker roster prompt는 계속 나온다. 기본 required workers는 `claude`, `codex`, `report-writer`이고, `antigravity`는 opt-in이다.
|
|
37
37
|
|
|
38
|
+
이 phase는 `base-ref`를 직접 묻지 않는다. wizard는 approved plan의 Stage Map을 기준으로 whole-task 또는 단일 stage를 선택하고, prepare가 registry/`consumers.jsonl`/git 상태에서 `VERIFICATION_TARGET`을 해소한다.
|
|
39
|
+
|
|
38
40
|
## 3. entry gate
|
|
39
41
|
|
|
40
42
|
```mermaid
|
|
41
43
|
sequenceDiagram
|
|
42
|
-
participant
|
|
43
|
-
participant
|
|
44
|
-
participant
|
|
44
|
+
participant P as prepare_task_bundle
|
|
45
|
+
participant C as consumers.jsonl
|
|
46
|
+
participant Reg as worktree registry
|
|
45
47
|
participant Git as verification worktree
|
|
48
|
+
participant Lead as Claude lead
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
P->>C: backfill carry and read done rows
|
|
51
|
+
P->>Reg: resolve task or stage worktree
|
|
52
|
+
alt whole-task
|
|
53
|
+
P->>Git: merge done stage commits into task worktree
|
|
54
|
+
P->>Git: teardown clean stage worktrees/registry keys
|
|
55
|
+
else single-stage
|
|
56
|
+
P->>Git: reuse selected stage worktree
|
|
57
|
+
end
|
|
58
|
+
P->>Git: resolve base/head/diff stat
|
|
59
|
+
P-->>Lead: inject VERIFICATION_TARGET
|
|
60
|
+
Lead->>Lead: verify against injected target only
|
|
55
61
|
```
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
prepare는 worker dispatch 전에 verification target을 고정한다. 다음 중 하나라도 실패하면 lead prompt가 시작되지 않고 `PrepareError`로 멈춘다.
|
|
64
|
+
|
|
65
|
+
- approved plan의 Stage Map을 읽을 수 없다.
|
|
66
|
+
- whole-task인데 Stage Map의 모든 stage가 `done`이 아니다.
|
|
67
|
+
- whole-task 자동 통합 중 merge conflict가 난다.
|
|
68
|
+
- single-stage인데 선택 stage의 implementation stage worktree가 없거나 dirty다.
|
|
69
|
+
- verification worktree가 `.okstra/` 밖 dirty state다.
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
- 인용된 implementation final report가 없거나 필수 section이 빠져 있다.
|
|
61
|
-
- report가 named worktree를 가리키는데 lead가 다른 checkout을 보고 있다.
|
|
62
|
-
- implementation report의 commit list / diff summary와 현재 checkout이 맞지 않는다.
|
|
63
|
-
- delivered code change commit이 없다.
|
|
71
|
+
시작된 lead는 `VERIFICATION_TARGET`을 권위로 취급한다. worktree/base/head/stage/source report를 brief에서 다시 고르지 않는다.
|
|
64
72
|
|
|
65
73
|
## 4. 검증 실행 흐름
|
|
66
74
|
|
|
67
75
|
```mermaid
|
|
68
76
|
flowchart TD
|
|
69
|
-
Gate[entry gate passed] -->
|
|
77
|
+
Gate[entry gate passed] --> Target[injected VERIFICATION_TARGET]
|
|
78
|
+
Target --> Lead[Claude lead confirms target snapshot]
|
|
70
79
|
Lead --> CW[Claude verifier<br/>read-only]
|
|
71
80
|
Lead --> XW[Codex verifier<br/>read-only]
|
|
72
81
|
Lead --> GW{Antigravity opt-in?}
|
|
@@ -110,7 +119,7 @@ flowchart TD
|
|
|
110
119
|
|
|
111
120
|
`## 7. Final Verdict`에는 `Verdict Token` field가 정확히 하나 들어가야 하며 값은 다음 셋 중 하나다.
|
|
112
121
|
|
|
113
|
-
- `accepted`: release-handoff
|
|
122
|
+
- `accepted`: whole-task이면 plain `release-handoff`, single-stage이면 `release-handoff(stage-group)` 후보가 되는 상태
|
|
114
123
|
- `conditional-accept`: 조건을 모두 명시해야 하며, 조건이 gate면 다음 phase를 막는다
|
|
115
124
|
- `blocked`: acceptance blocker가 있어 release-handoff로 갈 수 없는 상태
|
|
116
125
|
|
|
@@ -132,6 +141,7 @@ flowchart TD
|
|
|
132
141
|
final report에는 최소 다음이 필요하다.
|
|
133
142
|
|
|
134
143
|
- originating implementation final-report path
|
|
144
|
+
- verification scope(`whole-task` 또는 `single-stage`)와 injected `VERIFICATION_TARGET`
|
|
135
145
|
- inspected worktree path
|
|
136
146
|
- implementation base ref와 run start head SHA
|
|
137
147
|
- quoted commit list / diff summary
|
|
@@ -146,15 +156,16 @@ final report에는 최소 다음이 필요하다.
|
|
|
146
156
|
|
|
147
157
|
```mermaid
|
|
148
158
|
flowchart TD
|
|
149
|
-
FV[final-verification] -->
|
|
159
|
+
FV[final-verification] --> Runtime[prepare-owned whole-task integration<br/>or single-stage target resolution]
|
|
160
|
+
FV --> Allowed[lead read-only inspect/test/validate]
|
|
150
161
|
FV -. forbidden .-> Edit[source edit or config edit]
|
|
151
162
|
FV -. forbidden .-> Fix[in-run bug fix]
|
|
152
|
-
FV -. forbidden .-> Mutate[mutating command]
|
|
163
|
+
FV -. forbidden .-> Mutate[lead-owned mutating command]
|
|
153
164
|
FV -. forbidden .-> Expand[scope expansion]
|
|
154
165
|
FV -. forbidden .-> Hide[hide verifier dissent]
|
|
155
166
|
```
|
|
156
167
|
|
|
157
|
-
source edit, follow-up fix, scope expansion은 전부 금지다. 결함을 발견하면 현재 run 안에서 고치지 않고 final report에 blocker로 기록한 뒤 새 `error-analysis` 또는 `implementation-planning` 입력으로 넘긴다.
|
|
168
|
+
whole-task mode의 stage merge/teardown은 prepare가 수행하는 runtime-owned 통합 단계다. 그 이후 lead 검증은 read-only다. source edit, follow-up fix, scope expansion은 전부 금지다. 결함을 발견하면 현재 run 안에서 고치지 않고 final report에 blocker로 기록한 뒤 새 `error-analysis` 또는 `implementation-planning` 입력으로 넘긴다.
|
|
158
169
|
|
|
159
170
|
## 8. 확인한 코드
|
|
160
171
|
|
|
@@ -27,8 +27,9 @@ flowchart TD
|
|
|
27
27
|
BaseRef --> PlanPick
|
|
28
28
|
PlanPick --> Approved{APPROVED marker present?}
|
|
29
29
|
Approved -->|no| Retry[re-prompt same step]
|
|
30
|
-
Approved -->|yes| Stage[stage pick<br/>
|
|
31
|
-
Stage -->
|
|
30
|
+
Approved -->|yes| Stage[stage multi-pick<br/>ready/active markers]
|
|
31
|
+
Stage --> Chain[render-args<br/>stage + chain-stages]
|
|
32
|
+
Chain --> Executor[executor pick<br/>claude/codex/antigravity]
|
|
32
33
|
Executor --> Defaults[Use defaults / Customize]
|
|
33
34
|
Defaults -->|defaults| Confirm
|
|
34
35
|
Defaults -->|customize| Models[lead + executor model + report-writer model]
|
|
@@ -37,7 +38,9 @@ flowchart TD
|
|
|
37
38
|
Confirm --> Render[render-bundle]
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
`implementation`은 worker override 질문이 없다. wizard `render_args()`가 `workers`를 빈 문자열로 내보내고, runtime이 profile default roster를 사용한다.
|
|
41
|
+
`implementation`은 worker override 질문이 없다. wizard `render_args()`가 `workers`를 빈 문자열로 내보내고, runtime이 profile default roster를 사용한다. `stage_pick`은 완료/진행중/준비됨/대기 상태를 보여주는 multi-pick이다. 선택한 stage 집합은 의존성 closure와 위상정렬을 거쳐 `chain-stages` CSV가 되고, 각 실제 run은 그중 한 stage만 실행한다.
|
|
42
|
+
|
|
43
|
+
현재 okstra-run wizard 경로는 승인 checkbox를 직접 flip하는 `--approve`를 노출하지 않는다. plan file에는 이미 recognized approval marker가 있어야 한다.
|
|
41
44
|
|
|
42
45
|
## 3. runtime gate
|
|
43
46
|
|
|
@@ -47,16 +50,21 @@ sequenceDiagram
|
|
|
47
50
|
participant P as prepare_task_bundle
|
|
48
51
|
participant Plan as approved final-report
|
|
49
52
|
participant QA as project.json qaCommands
|
|
50
|
-
participant Stage as stage
|
|
53
|
+
participant Stage as stage target policy
|
|
54
|
+
participant Reg as worktree registry
|
|
55
|
+
participant WT as stage worktree
|
|
51
56
|
|
|
52
57
|
W->>P: task-type=implementation, approved-plan, stage, executor
|
|
53
58
|
P->>Plan: file exists?
|
|
54
59
|
P->>Plan: approval marker regex matches?
|
|
55
60
|
P->>Plan: unresolved Blocks=approval rows?
|
|
56
|
-
P->>Stage:
|
|
61
|
+
P->>Stage: parse Stage Map and recover carry/done rows
|
|
62
|
+
P->>Reg: read active stage-key reservations
|
|
63
|
+
P->>Stage: select exactly one ready stage
|
|
64
|
+
P->>WT: provision stage-N worktree + branch
|
|
57
65
|
P->>QA: validate qaCommands deny-list
|
|
58
66
|
P->>P: executor provider in resolved roster?
|
|
59
|
-
P->>P:
|
|
67
|
+
P->>P: namespace run artifacts under stage-N
|
|
60
68
|
P-->>W: prepared implementation prompt or PrepareError
|
|
61
69
|
```
|
|
62
70
|
|
|
@@ -86,16 +94,21 @@ Executor만 project file을 mutate할 수 있다. verifier는 같은 worktree에
|
|
|
86
94
|
```mermaid
|
|
87
95
|
flowchart LR
|
|
88
96
|
Plan[approved plan<br/>Stage Map] --> Parse[parse stage map]
|
|
89
|
-
Parse --> Done[read consumers.jsonl
|
|
90
|
-
Done -->
|
|
91
|
-
|
|
97
|
+
Parse --> Done[backfill carry<br/>read consumers.jsonl]
|
|
98
|
+
Done --> Reserved[read registry<br/>active stage reservations]
|
|
99
|
+
Reserved --> Resolve{stage arg}
|
|
100
|
+
Resolve -->|auto| Next[lowest ready<br/>not done/started/reserved]
|
|
92
101
|
Resolve -->|number| Forced[selected stage]
|
|
93
|
-
Next -->
|
|
94
|
-
Forced -->
|
|
95
|
-
|
|
102
|
+
Next --> Base[resolve stage base commit]
|
|
103
|
+
Forced --> Base
|
|
104
|
+
Base --> WT[create/reuse stage worktree]
|
|
105
|
+
WT --> Started[append consumer status=started]
|
|
106
|
+
Started --> Run[implementation executes one selected stage]
|
|
96
107
|
```
|
|
97
108
|
|
|
98
|
-
stage 선택은 `auto` 또는 숫자다. 다른 task-type에서 `--stage`가 오면 `PrepareError`다.
|
|
109
|
+
stage 선택은 `auto` 또는 숫자다. 다른 task-type에서 `--stage`가 오면 `PrepareError`다. runtime은 `consumers.jsonl`의 `done`/`started`와 registry의 active stage-key를 함께 보고 점유 stage를 제외한다.
|
|
110
|
+
|
|
111
|
+
stage worktree base는 dependency 형태로 정한다. 독립 stage는 첫 implementation 진입 때 고정한 task-key worktree HEAD를 anchor로 삼고, 단일 의존 stage는 선행 stage의 done `head_commit`에서 분기한다. 다중 의존 stage는 모든 선행 done commit이 task-key worktree HEAD의 ancestor인지 확인한 뒤 그 HEAD에서 분기한다.
|
|
99
112
|
|
|
100
113
|
## 6. 산출물
|
|
101
114
|
|
|
@@ -113,12 +126,14 @@ flowchart TD
|
|
|
113
126
|
final report에는 최소 다음이 필요하다.
|
|
114
127
|
|
|
115
128
|
- approved plan path와 quoted approval marker
|
|
129
|
+
- selected stage, isolated stage worktree path, run artifact path(`runs/implementation/stage-<N>/`)
|
|
116
130
|
- commit SHA, message, plan step mapping
|
|
117
131
|
- diff summary와 per-file summary
|
|
118
132
|
- out-of-plan edits block
|
|
119
133
|
- plan validation command의 실제 stdout/stderr와 exit code
|
|
120
134
|
- TDD failing-then-passing evidence
|
|
121
135
|
- verifier별 independent validation rerun result
|
|
136
|
+
- `carry/stage-<N>.json` evidence sidecar와 `consumers.jsonl` started/done row
|
|
122
137
|
- rollback verification
|
|
123
138
|
- follow-up tasks table
|
|
124
139
|
|
|
@@ -146,4 +161,3 @@ flowchart TD
|
|
|
146
161
|
- [`validators/validate-implementation-plan-stages.py`](../../validators/validate-implementation-plan-stages.py)
|
|
147
162
|
- [`scripts/okstra_ctl/qa_commands.py`](../../scripts/okstra_ctl/qa_commands.py)
|
|
148
163
|
- [`prompts/lead/okstra-lead-contract.md`](../../prompts/lead/okstra-lead-contract.md)
|
|
149
|
-
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
## 1. 목적
|
|
16
16
|
|
|
17
|
-
`release-handoff`는 `accepted` verdict가 나온 already-committed implementation
|
|
17
|
+
`release-handoff`는 `accepted` verdict가 나온 already-committed implementation 결과를 push하거나 PR로 넘기는 terminal phase다. whole-task mode는 검증된 task branch를 그대로 포장한다. stage-group mode는 선택 stage들을 collector branch에 assemble해 한 PR로 묶을 수 있으며, 이때 생성되는 merge commit은 `okstra handoff assemble`만 만든다.
|
|
18
18
|
|
|
19
19
|
이 phase는 worker dispatch가 없다. `claude`, `codex`, `report-writer` roster를 쓰지 않으며 Claude lead가 inline으로 git/gh inspection, 사용자 질문, PR draft, final report 작성을 모두 수행한다.
|
|
20
20
|
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
flowchart TD
|
|
25
25
|
Start[/okstra-run/] --> Common[공통 task identity flow]
|
|
26
26
|
Common --> Type[task-type = release-handoff]
|
|
27
|
-
Type -->
|
|
27
|
+
Type --> Plan[approved plan auto/pick]
|
|
28
|
+
Plan --> Scope[handoff stage pick<br/>whole-task or eligible stages]
|
|
29
|
+
Scope --> Worktree{active task worktree?}
|
|
28
30
|
Worktree -->|yes| Defaults[Use defaults / Customize]
|
|
29
31
|
Worktree -->|no| BaseRef[base-ref pick/text]
|
|
30
32
|
BaseRef --> Defaults
|
|
@@ -32,14 +34,14 @@ flowchart TD
|
|
|
32
34
|
Defaults -->|customize| Models[lead model prompt]
|
|
33
35
|
Models --> Extras[directive, related tasks, clarification]
|
|
34
36
|
Extras --> Template[PR template override?]
|
|
35
|
-
Template -->
|
|
36
|
-
|
|
37
|
+
Template --> TemplateScope[save template to project/global?]
|
|
38
|
+
TemplateScope --> Confirm
|
|
37
39
|
Confirm --> Render[render-bundle]
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
`release-handoff`는 worker roster prompt가 없다. wizard의 `render_args()`는 `pr-template-path`를 release-handoff일 때만 포함하고, runtime은 worker list를 강제로 empty로 만든다.
|
|
42
|
+
`release-handoff`는 worker roster prompt가 없다. wizard의 `render_args()`는 `pr-template-path`를 release-handoff일 때만 포함하고, runtime은 worker list를 강제로 empty로 만든다. scope 선택은 prepare 전에 끝난다. whole-task는 accepted whole-task verification report가 있어야 하고, stage-group은 accepted single-stage verification으로 `verified` 처리됐지만 아직 `pr`로 덮이지 않은 stage만 후보가 된다.
|
|
41
43
|
|
|
42
|
-
주의할 점은 이 phase도 task worktree provisioning 대상이라는 것이다. 정상 흐름은 같은 task-key의 implementation/final-verification
|
|
44
|
+
주의할 점은 이 phase도 task worktree provisioning 대상이라는 것이다. 정상 흐름은 같은 task-key의 implementation/final-verification 결과를 재사용한다. 새 task로 시작하면 새 branch가 만들어질 수 있고, entry gate의 "implementation commit exists" 조건에서 막힐 가능성이 높다.
|
|
43
45
|
|
|
44
46
|
## 3. prepare 단계
|
|
45
47
|
|
|
@@ -72,7 +74,10 @@ flowchart TD
|
|
|
72
74
|
Source -->|no| Block[blocked<br/>route final-verification]
|
|
73
75
|
Source -->|yes| Verdict{Verdict Token == accepted?}
|
|
74
76
|
Verdict -->|no| Block
|
|
75
|
-
Verdict -->|yes|
|
|
77
|
+
Verdict -->|yes| Mode{HANDOFF_MODE}
|
|
78
|
+
Mode -->|stage-group| Eligible[each selected stage<br/>verified and not in PR]
|
|
79
|
+
Mode -->|whole-task| Status{git status --short clean?}
|
|
80
|
+
Eligible --> Status
|
|
76
81
|
Status -->|no| Dirty[blocked<br/>dirty tree]
|
|
77
82
|
Status -->|yes| Branch{current branch is base branch?}
|
|
78
83
|
Branch -->|yes| BaseBlock[blocked<br/>never operate on base branch]
|
|
@@ -84,7 +89,8 @@ flowchart TD
|
|
|
84
89
|
lead는 사용자에게 push/PR 여부를 묻기 전에 다음을 확인한다.
|
|
85
90
|
|
|
86
91
|
- prepare 가 생성한 input 문서(`release-handoff-input.md`)의 `## Source Verification Report` 가 모드(`HANDOFF_MODE`)와 인용 보고서 표를 담고 있다. brief 는 entry phase 의 입력물이라 release-handoff 에는 없다 — 사용자의 stage 선택은 wizard `handoff_stage_pick` 또는 CLI `--stages` 로 prepare 전에 끝난다.
|
|
87
|
-
-
|
|
92
|
+
- whole-task mode에서는 인용 report가 `verificationScope=whole-task`이고 `Verdict Token = accepted`여야 한다.
|
|
93
|
+
- stage-group mode에서는 인용된 각 single-stage report가 `Verdict Token = accepted`여야 하며, prepare/`okstra handoff assemble`이 eligibility와 dependency closure를 다시 강제한다.
|
|
88
94
|
- working tree가 clean이다.
|
|
89
95
|
- 현재 branch가 `main`, `master`, `prod`, `preprod`, `staging`, `dev` 같은 base branch가 아니다.
|
|
90
96
|
- `<base>..HEAD` commit range가 비어 있지 않다.
|
|
@@ -99,7 +105,11 @@ stateDiagram-v2
|
|
|
99
105
|
Gate --> Q1: action selection
|
|
100
106
|
Q1 --> LocalCheckout: local checkout
|
|
101
107
|
Q1 --> Skip: skip
|
|
102
|
-
Q1 --> Q2: push + PR
|
|
108
|
+
Q1 --> Q2: push + PR whole-task
|
|
109
|
+
Q1 --> G2: push + PR stage-group
|
|
110
|
+
state "base select + stage confirmation / okstra handoff assemble" as Assemble
|
|
111
|
+
G2 --> Assemble
|
|
112
|
+
Assemble --> Q3: collector branch ready
|
|
103
113
|
Q2 --> Probe: choose PR base
|
|
104
114
|
Probe --> Q3: no conflict
|
|
105
115
|
Probe --> Conflict: conflict detected
|
|
@@ -124,6 +134,8 @@ stateDiagram-v2
|
|
|
124
134
|
|
|
125
135
|
`push + PR`에서만 merge-conflict probe를 한다.
|
|
126
136
|
|
|
137
|
+
stage-group mode에서는 `local checkout`을 제공하지 않는다. `push + PR`을 고른 뒤 PR base를 먼저 선택하고, 이미 고정된 `HANDOFF_STAGES`를 확인한 다음 `okstra handoff assemble`이 collector branch를 만든다. 이후 conflict probe와 PR title/body 확인은 collector branch를 head로 삼는다.
|
|
138
|
+
|
|
127
139
|
```mermaid
|
|
128
140
|
flowchart TD
|
|
129
141
|
PushPR[push + PR selected] --> Fetch[git fetch origin chosen-base]
|
|
@@ -170,11 +182,13 @@ flowchart TD
|
|
|
170
182
|
final report에는 최소 다음이 필요하다.
|
|
171
183
|
|
|
172
184
|
- originating final-verification report path와 quoted `accepted` verdict row
|
|
185
|
+
- handoff mode(`whole-task` 또는 `stage-group`)와 selected stages
|
|
173
186
|
- feature branch와 run start `git status --short`
|
|
174
187
|
- 사용자 선택 기록
|
|
175
188
|
- 실행한 모든 git/gh command와 exit code
|
|
176
189
|
- implementation commit list
|
|
177
190
|
- merge-conflict probe 결과
|
|
191
|
+
- stage-group이면 collector branch, merge commit SHA, dependency-closure 결과
|
|
178
192
|
- PR 생성, 재사용, 생략 결과
|
|
179
193
|
- routing recommendation `done`
|
|
180
194
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -4,54 +4,33 @@ docker 라벨은 컨테이너 존재 여부의 기준값(SSOT)이지만 tmux ses
|
|
|
4
4
|
findings 파일 경로 같은 운영 메타데이터는 추적하지 못한다. 이 모듈은 task root
|
|
5
5
|
하위 ``container/registry.json`` 에 그런 보조 정보를 가벼운 dict 로 보관한다.
|
|
6
6
|
|
|
7
|
-
flock + tmp+``os.replace`` 원자 쓰기 idiom 은 ``
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
flock + tmp+``os.replace`` 원자 쓰기 idiom 은 ``json_registry`` 의 공용
|
|
8
|
+
Interface 를 사용하고, lock/registry 경로만 ``paths.container_paths()`` 로
|
|
9
|
+
해소해 전역 ``~/.okstra/worktrees/registry.json`` 과 분리한다. dict 구조는
|
|
10
|
+
``{"services": {<svc>: {session_name, pane_id, findings_path}}}`` 한 가지뿐 —
|
|
11
|
+
worktree_registry 의 dataclass 는 복제하지 않는다.
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
import contextlib
|
|
16
|
-
import fcntl
|
|
17
|
-
import json
|
|
18
|
-
import os
|
|
19
15
|
from pathlib import Path
|
|
20
16
|
|
|
17
|
+
from .json_registry import load_registry_json, registry_lock, save_registry_json
|
|
21
18
|
from . import paths
|
|
22
19
|
|
|
23
20
|
|
|
24
|
-
@contextlib.contextmanager
|
|
25
21
|
def _registry_lock(lock_path: Path):
|
|
26
|
-
"""``container/registry.json.lock`` 에 대한 배타 flock.
|
|
27
|
-
|
|
28
|
-
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
if not lock_path.exists():
|
|
30
|
-
lock_path.touch()
|
|
31
|
-
f = lock_path.open("r+")
|
|
32
|
-
try:
|
|
33
|
-
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
34
|
-
yield
|
|
35
|
-
finally:
|
|
36
|
-
f.close()
|
|
22
|
+
"""``container/registry.json.lock`` 에 대한 배타 flock."""
|
|
23
|
+
return registry_lock(lock_path)
|
|
37
24
|
|
|
38
25
|
|
|
39
26
|
def _load(registry_path: Path) -> dict:
|
|
40
|
-
|
|
41
|
-
return {"services": {}}
|
|
42
|
-
try:
|
|
43
|
-
data = json.loads(registry_path.read_text())
|
|
44
|
-
except (OSError, json.JSONDecodeError):
|
|
45
|
-
return {"services": {}}
|
|
27
|
+
data = load_registry_json(registry_path, lambda: {"services": {}})
|
|
46
28
|
data.setdefault("services", {})
|
|
47
29
|
return data
|
|
48
30
|
|
|
49
31
|
|
|
50
32
|
def _save(registry_path: Path, data: dict) -> None:
|
|
51
|
-
registry_path
|
|
52
|
-
tmp = registry_path.with_suffix(".json.tmp")
|
|
53
|
-
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
54
|
-
os.replace(tmp, registry_path)
|
|
33
|
+
save_registry_json(registry_path, data)
|
|
55
34
|
|
|
56
35
|
|
|
57
36
|
def reserve(
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shared flock + atomic JSON persistence for small okstra registries."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
5
|
+
import fcntl
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@contextlib.contextmanager
|
|
13
|
+
def registry_lock(lock_path: Path) -> Iterator[None]:
|
|
14
|
+
"""Hold an exclusive flock on ``lock_path``."""
|
|
15
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
if not lock_path.exists():
|
|
17
|
+
lock_path.touch()
|
|
18
|
+
handle = lock_path.open("r+")
|
|
19
|
+
try:
|
|
20
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
21
|
+
yield
|
|
22
|
+
finally:
|
|
23
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
24
|
+
handle.close()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_registry_json(path: Path, default_factory: Callable[[], dict]) -> dict:
|
|
28
|
+
"""Load a registry dict, or a fresh default when absent/corrupt."""
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return default_factory()
|
|
31
|
+
try:
|
|
32
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
except (OSError, json.JSONDecodeError):
|
|
34
|
+
return default_factory()
|
|
35
|
+
return data if isinstance(data, dict) else default_factory()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save_registry_json(path: Path, data: dict) -> None:
|
|
39
|
+
"""Persist registry JSON via temp file + ``os.replace``."""
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
tmp = path.with_suffix(".json.tmp")
|
|
42
|
+
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
43
|
+
os.replace(tmp, path)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Read-side cross-task roll-up for a task-group (or the whole project).
|
|
2
|
+
|
|
3
|
+
단일 task 집계기(time-report / error-report / recap)는 task-root 하나만 받는다.
|
|
4
|
+
이 모듈은 그 위에서 catalog 를 task-group 단위로 fan-out 해 task 별 run 수·소요
|
|
5
|
+
시간·에러 수·report 경로를 모으고, group 차원의 합계/분포를 deterministic 하게
|
|
6
|
+
roll-up 한다. report 본문 종합(자연어 요약)은 LLM(skill)에 남긴다 — 여기서는
|
|
7
|
+
report 경로만 노출한다. time-report 와 동일하게 raw ms 를 출력하고 HH:MM:SS
|
|
8
|
+
포맷·Markdown 표 렌더는 호출자(skill)에 위임한다.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from okstra_ctl.error_log_core import glob_error_logs, parse_records
|
|
18
|
+
from okstra_ctl.time_report import _load_runs, aggregate_time
|
|
19
|
+
from okstra_project import (
|
|
20
|
+
ResolverError,
|
|
21
|
+
list_project_tasks,
|
|
22
|
+
resolve_project_root,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _collect_task(entry: dict, project_root: Path) -> dict:
|
|
27
|
+
"""list_project_tasks entry → roll-up 한 줄. catalog 상태 필드 + time/error 측정.
|
|
28
|
+
|
|
29
|
+
runCount 는 timeline 의 전체 run 수(usable durationMs 없는 run 도 포함)이고
|
|
30
|
+
cpuSumMs/wallClockMs 는 durationMs 가 있는 run 만 반영하므로 0 일 수 있다.
|
|
31
|
+
"""
|
|
32
|
+
task_root = Path(entry["_resolvedTaskRoot"])
|
|
33
|
+
runs = _load_runs(task_root)
|
|
34
|
+
time_agg = aggregate_time(runs, project_root)
|
|
35
|
+
records, _ = parse_records(glob_error_logs(task_root))
|
|
36
|
+
return {
|
|
37
|
+
"taskKey": entry.get("taskKey", ""),
|
|
38
|
+
"taskGroup": entry.get("taskGroup", ""),
|
|
39
|
+
"taskId": entry.get("taskId", ""),
|
|
40
|
+
"taskType": entry.get("taskType", ""),
|
|
41
|
+
"workCategory": entry.get("workCategory", "") or "unknown",
|
|
42
|
+
"workStatus": entry.get("workStatus", "") or "unknown",
|
|
43
|
+
"currentPhase": entry.get("currentPhase", ""),
|
|
44
|
+
"currentPhaseState": entry.get("currentPhaseState", ""),
|
|
45
|
+
"nextRecommendedPhase": entry.get("nextRecommendedPhase", ""),
|
|
46
|
+
"latestRunStatus": entry.get("latestRunStatus", ""),
|
|
47
|
+
"updatedAt": entry.get("updatedAt", ""),
|
|
48
|
+
"reportPath": entry.get("latestReportPath", ""),
|
|
49
|
+
"runCount": len(runs),
|
|
50
|
+
"cpuSumMs": time_agg["grandTotal"]["cpuSumMs"],
|
|
51
|
+
"wallClockMs": sum(r["wallClockMs"] for r in time_agg["perRunWallClock"]),
|
|
52
|
+
"errorCount": len(records),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _tally(tasks: list[dict], key: str) -> dict:
|
|
57
|
+
"""task 들의 한 필드를 값별 개수로. 빈 값은 'unknown' 으로 접는다."""
|
|
58
|
+
counts: dict[str, int] = {}
|
|
59
|
+
for task in tasks:
|
|
60
|
+
bucket = task.get(key) or "unknown"
|
|
61
|
+
counts[bucket] = counts.get(bucket, 0) + 1
|
|
62
|
+
return dict(sorted(counts.items()))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_rollup(project_root: Path, task_group: str | None) -> dict:
|
|
66
|
+
entries = list_project_tasks(project_root, task_group=task_group or None)
|
|
67
|
+
tasks = [_collect_task(e, project_root) for e in entries]
|
|
68
|
+
totals = {
|
|
69
|
+
"runs": sum(t["runCount"] for t in tasks),
|
|
70
|
+
"cpuSumMs": sum(t["cpuSumMs"] for t in tasks),
|
|
71
|
+
"wallClockMs": sum(t["wallClockMs"] for t in tasks),
|
|
72
|
+
"errors": sum(t["errorCount"] for t in tasks),
|
|
73
|
+
"byWorkStatus": _tally(tasks, "workStatus"),
|
|
74
|
+
"byWorkCategory": _tally(tasks, "workCategory"),
|
|
75
|
+
"byCurrentPhase": _tally(tasks, "currentPhase"),
|
|
76
|
+
"byTaskType": _tally(tasks, "taskType"),
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
"ok": True,
|
|
80
|
+
"projectRoot": str(project_root),
|
|
81
|
+
"taskGroup": task_group or None,
|
|
82
|
+
"taskCount": len(tasks),
|
|
83
|
+
"tasks": tasks,
|
|
84
|
+
"totals": totals,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv: list[str] | None = None) -> int:
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
prog="okstra rollup",
|
|
91
|
+
description="Roll up run results across a task-group's tasks (raw ms).")
|
|
92
|
+
parser.add_argument("--task-group", default="",
|
|
93
|
+
help="scope to this task-group (default: whole project catalog)")
|
|
94
|
+
parser.add_argument("--project-root", default="")
|
|
95
|
+
parser.add_argument("--cwd", default=".")
|
|
96
|
+
parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
|
|
97
|
+
args = parser.parse_args(argv)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
project_root = Path(
|
|
101
|
+
resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
|
|
102
|
+
).resolve()
|
|
103
|
+
except ResolverError as exc:
|
|
104
|
+
print(json.dumps({"ok": False, "stage": "resolve", "reason": str(exc)}))
|
|
105
|
+
return 2
|
|
106
|
+
|
|
107
|
+
result = build_rollup(project_root, args.task_group)
|
|
108
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -23,10 +23,6 @@ file is useful for debugging.
|
|
|
23
23
|
"""
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
-
import contextlib
|
|
27
|
-
import fcntl
|
|
28
|
-
import json
|
|
29
|
-
import os
|
|
30
26
|
import time
|
|
31
27
|
from dataclasses import dataclass
|
|
32
28
|
from pathlib import Path
|
|
@@ -34,6 +30,8 @@ from typing import Optional
|
|
|
34
30
|
|
|
35
31
|
from okstra_project.dirs import okstra_home
|
|
36
32
|
|
|
33
|
+
from .json_registry import load_registry_json, registry_lock, save_registry_json
|
|
34
|
+
|
|
37
35
|
|
|
38
36
|
REGISTRY_FILENAME = "registry.json"
|
|
39
37
|
LOCK_FILENAME = "registry.lock"
|
|
@@ -77,23 +75,9 @@ class WorktreeEntry:
|
|
|
77
75
|
stages: Optional[list] = None
|
|
78
76
|
|
|
79
77
|
|
|
80
|
-
@contextlib.contextmanager
|
|
81
78
|
def _registry_lock():
|
|
82
|
-
"""Exclusive flock on `<worktrees>/registry.lock`.
|
|
83
|
-
|
|
84
|
-
so we do not serialise unrelated central operations.
|
|
85
|
-
"""
|
|
86
|
-
root = _okstra_worktrees_dir()
|
|
87
|
-
root.mkdir(parents=True, exist_ok=True)
|
|
88
|
-
lockfile = root / LOCK_FILENAME
|
|
89
|
-
if not lockfile.exists():
|
|
90
|
-
lockfile.touch()
|
|
91
|
-
f = lockfile.open("r+")
|
|
92
|
-
try:
|
|
93
|
-
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
94
|
-
yield
|
|
95
|
-
finally:
|
|
96
|
-
f.close()
|
|
79
|
+
"""Exclusive flock on `<worktrees>/registry.lock`."""
|
|
80
|
+
return registry_lock(_okstra_worktrees_dir() / LOCK_FILENAME)
|
|
97
81
|
|
|
98
82
|
|
|
99
83
|
def _registry_path() -> Path:
|
|
@@ -101,24 +85,14 @@ def _registry_path() -> Path:
|
|
|
101
85
|
|
|
102
86
|
|
|
103
87
|
def _load() -> dict:
|
|
104
|
-
|
|
105
|
-
if not p.exists():
|
|
106
|
-
return {"tasks": {}, "branches": {}}
|
|
107
|
-
try:
|
|
108
|
-
data = json.loads(p.read_text())
|
|
109
|
-
except (OSError, json.JSONDecodeError):
|
|
110
|
-
return {"tasks": {}, "branches": {}}
|
|
88
|
+
data = load_registry_json(_registry_path(), lambda: {"tasks": {}, "branches": {}})
|
|
111
89
|
data.setdefault("tasks", {})
|
|
112
90
|
data.setdefault("branches", {})
|
|
113
91
|
return data
|
|
114
92
|
|
|
115
93
|
|
|
116
94
|
def _save(data: dict) -> None:
|
|
117
|
-
|
|
118
|
-
p.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
-
tmp = p.with_suffix(".json.tmp")
|
|
120
|
-
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
121
|
-
os.replace(tmp, p)
|
|
95
|
+
save_registry_json(_registry_path(), data)
|
|
122
96
|
|
|
123
97
|
|
|
124
98
|
def lookup(
|