okstra 0.21.1 → 0.23.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 +3 -0
- package/README.md +3 -0
- package/bin/okstra +2 -0
- package/docs/kr/architecture.md +1 -0
- package/docs/project-structure-overview.md +53 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +3 -1
- package/runtime/agents/workers/codex-worker.md +3 -1
- package/runtime/agents/workers/gemini-worker.md +3 -1
- package/runtime/agents/workers/report-writer-worker.md +16 -1
- package/runtime/bin/okstra-trace-cleanup.sh +44 -11
- package/runtime/prompts/profiles/_common-contract.md +12 -0
- package/runtime/prompts/profiles/release-handoff.md +18 -1
- package/runtime/python/okstra_ctl/index.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +126 -0
- package/runtime/python/okstra_ctl/render.py +28 -2
- package/runtime/python/okstra_ctl/run.py +41 -2
- package/runtime/python/okstra_ctl/seeding.py +19 -0
- package/runtime/python/okstra_ctl/workers.py +20 -0
- package/runtime/skills/okstra-run/SKILL.md +68 -32
- package/runtime/skills/okstra-run/templates/pr-body.template.md +41 -0
- package/runtime/skills/okstra-setup/SKILL.md +37 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +46 -1
- package/runtime/templates/prd/brief.template.md +1 -0
- package/runtime/templates/project-docs/task-index.template.md +1 -0
- package/runtime/templates/reports/error-analysis-input.template.md +1 -0
- package/runtime/templates/reports/final-report.template.md +2 -0
- package/runtime/templates/reports/final-verification-input.template.md +1 -0
- package/runtime/templates/reports/implementation-input.template.md +1 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +1 -0
- package/runtime/templates/reports/quick-input.template.md +1 -0
- package/runtime/templates/reports/release-handoff-input.template.md +1 -0
- package/runtime/templates/reports/schedule.template.md +1 -0
- package/runtime/templates/reports/settings.template.json +4 -2
- package/runtime/templates/reports/task-brief.template.md +1 -0
- package/src/config.mjs +392 -0
- package/src/render-bundle.mjs +2 -1
|
@@ -49,6 +49,7 @@ from .seeding import (
|
|
|
49
49
|
SettingsLinkError,
|
|
50
50
|
cleanup_obsolete_generated_docs,
|
|
51
51
|
ensure_project_settings_symlink,
|
|
52
|
+
installed_version,
|
|
52
53
|
verify_installation,
|
|
53
54
|
)
|
|
54
55
|
from .session import (
|
|
@@ -56,7 +57,12 @@ from .session import (
|
|
|
56
57
|
resolve_inproc_lead_session_id,
|
|
57
58
|
write_claude_resume_command_file,
|
|
58
59
|
)
|
|
59
|
-
from .
|
|
60
|
+
from .pr_template import PrTemplateError, resolve_pr_template_path
|
|
61
|
+
from .workers import (
|
|
62
|
+
normalize_workers,
|
|
63
|
+
resolve_profile_workers,
|
|
64
|
+
validate_workers_against_profile,
|
|
65
|
+
)
|
|
60
66
|
from .workflow import compute_workflow_state
|
|
61
67
|
from .worktree import provision_task_worktree
|
|
62
68
|
|
|
@@ -102,6 +108,9 @@ class PrepareInputs:
|
|
|
102
108
|
base_ref: str = ""
|
|
103
109
|
approved_plan_path: str = ""
|
|
104
110
|
clarification_response_path: str = "" # absolute or empty
|
|
111
|
+
# release-handoff 전용: PR 본문 템플릿 1회성 override. 빈 문자열이면
|
|
112
|
+
# project.json → global config → 스킬 디폴트 순으로 해석된다.
|
|
113
|
+
pr_template_path: str = ""
|
|
105
114
|
render_only: bool = False
|
|
106
115
|
refresh_assets: bool = False
|
|
107
116
|
approve_plan_ack: bool = False
|
|
@@ -310,6 +319,7 @@ def _record_start(
|
|
|
310
319
|
argv=canonical_argv,
|
|
311
320
|
cwd=cwd,
|
|
312
321
|
env_overrides={},
|
|
322
|
+
okstra_version=ctx.get("OKSTRA_VERSION", ""),
|
|
313
323
|
initial_status=initial_status,
|
|
314
324
|
brief_sha256=brief_sha256,
|
|
315
325
|
)
|
|
@@ -495,12 +505,28 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
495
505
|
if inp.task_type == "release-handoff":
|
|
496
506
|
workers: list[str] = []
|
|
497
507
|
else:
|
|
498
|
-
|
|
508
|
+
profile_workers = resolve_profile_workers(profile_file)
|
|
509
|
+
profile_workers_csv = ",".join(profile_workers)
|
|
499
510
|
workers = normalize_workers(inp.workers_override or profile_workers_csv)
|
|
511
|
+
if inp.workers_override.strip():
|
|
512
|
+
validate_workers_against_profile(workers, profile_workers)
|
|
500
513
|
if not workers:
|
|
501
514
|
raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
|
|
502
515
|
selected_reviewers = ",".join(workers)
|
|
503
516
|
|
|
517
|
+
# ---- PR template resolution (release-handoff only) ----
|
|
518
|
+
pr_template_path_str = ""
|
|
519
|
+
pr_template_source = ""
|
|
520
|
+
if inp.task_type == "release-handoff":
|
|
521
|
+
try:
|
|
522
|
+
resolved_tpl = resolve_pr_template_path(
|
|
523
|
+
Path(inp.project_root), inp.pr_template_path
|
|
524
|
+
)
|
|
525
|
+
except PrTemplateError as exc:
|
|
526
|
+
raise PrepareError(f"PR template resolution failed: {exc}") from exc
|
|
527
|
+
pr_template_path_str = str(resolved_tpl.path)
|
|
528
|
+
pr_template_source = resolved_tpl.source
|
|
529
|
+
|
|
504
530
|
# ---- model assignments ----
|
|
505
531
|
lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
|
|
506
532
|
claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "sonnet")
|
|
@@ -634,6 +660,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
634
660
|
ctx.update({
|
|
635
661
|
"REVIEW_PROFILE": inp.task_type,
|
|
636
662
|
"SELECTED_REVIEWERS": selected_reviewers,
|
|
663
|
+
"PR_TEMPLATE_PATH": pr_template_path_str,
|
|
664
|
+
"PR_TEMPLATE_SOURCE": pr_template_source,
|
|
637
665
|
"CLAUDE_SESSION_ID": claude_session_id,
|
|
638
666
|
"CLARIFICATION_RESPONSE_PATH": inp.clarification_response_path,
|
|
639
667
|
"CLARIFICATION_RESPONSE_FILE": inp.clarification_response_path,
|
|
@@ -666,6 +694,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
666
694
|
"LATEST_REPORT_PATH": "",
|
|
667
695
|
"LATEST_REPORT_RELATIVE_PATH": "",
|
|
668
696
|
"RENDER_ONLY": "true" if inp.render_only else "false",
|
|
697
|
+
"OKSTRA_VERSION": installed_version(),
|
|
669
698
|
**workflow_state,
|
|
670
699
|
})
|
|
671
700
|
|
|
@@ -853,6 +882,15 @@ def main(argv: list[str]) -> int:
|
|
|
853
882
|
),
|
|
854
883
|
)
|
|
855
884
|
p.add_argument("--clarification-response", default="", dest="clarification_response_path")
|
|
885
|
+
p.add_argument(
|
|
886
|
+
"--pr-template-path",
|
|
887
|
+
default="",
|
|
888
|
+
dest="pr_template_path",
|
|
889
|
+
help=(
|
|
890
|
+
"release-handoff 전용 1회성 PR 본문 템플릿 경로. 빈 값이면 "
|
|
891
|
+
"project.json → ~/.okstra/config.json → 스킬 디폴트 순으로 해석."
|
|
892
|
+
),
|
|
893
|
+
)
|
|
856
894
|
p.add_argument("--render-only", action="store_true", dest="render_only")
|
|
857
895
|
p.add_argument("--refresh-assets", action="store_true", dest="refresh_assets")
|
|
858
896
|
p.add_argument(
|
|
@@ -917,6 +955,7 @@ def main(argv: list[str]) -> int:
|
|
|
917
955
|
base_ref=args.base_ref,
|
|
918
956
|
approved_plan_path=args.approved_plan_path,
|
|
919
957
|
clarification_response_path=clarification_abs,
|
|
958
|
+
pr_template_path=args.pr_template_path,
|
|
920
959
|
render_only=args.render_only,
|
|
921
960
|
refresh_assets=args.refresh_assets,
|
|
922
961
|
approve_plan_ack=args.approve_plan_ack,
|
|
@@ -23,6 +23,25 @@ class SettingsLinkError(Exception):
|
|
|
23
23
|
"""`<project>/.claude/settings.local.json` symlink provisioning 실패."""
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def installed_version() -> str:
|
|
27
|
+
"""Read the version stamp written by `okstra install` to `~/.okstra/version`.
|
|
28
|
+
|
|
29
|
+
Returns an empty string if the stamp is missing or unreadable. Callers use
|
|
30
|
+
the result to label generated artifacts (run manifests, final reports) so
|
|
31
|
+
that consumers can tell which okstra release produced a given run — and
|
|
32
|
+
so that report readers can distinguish behaviour drift across upgrades
|
|
33
|
+
without having to dig through git history.
|
|
34
|
+
|
|
35
|
+
The stamp lives at `_okstra_home() / "version"`. `OKSTRA_HOME` overrides
|
|
36
|
+
the home directory for tests.
|
|
37
|
+
"""
|
|
38
|
+
version_file = _okstra_home() / "version"
|
|
39
|
+
try:
|
|
40
|
+
return version_file.read_text(encoding="utf-8").strip()
|
|
41
|
+
except OSError:
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
|
|
26
45
|
def required_install_paths() -> list[Path]:
|
|
27
46
|
"""okstra install 이 채워야 하는 최소 자산 경로."""
|
|
28
47
|
okstra_home = Path.home() / ".okstra"
|
|
@@ -68,3 +68,23 @@ def normalize_workers(value: str) -> list[str]:
|
|
|
68
68
|
seen.add(v)
|
|
69
69
|
out.append(v)
|
|
70
70
|
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_workers_against_profile(
|
|
74
|
+
workers: list[str], profile_workers: list[str]
|
|
75
|
+
) -> None:
|
|
76
|
+
"""프로파일이 `Required workers:` 로 로스터를 선언했다면, 사용자
|
|
77
|
+
override 가 그 부분집합인지 검증한다.
|
|
78
|
+
|
|
79
|
+
`profile_workers` 가 비어 있으면(프로파일이 로스터를 선언하지 않은
|
|
80
|
+
구버전) 검증을 건너뛴다 — 하위 호환을 위해.
|
|
81
|
+
"""
|
|
82
|
+
if not profile_workers:
|
|
83
|
+
return
|
|
84
|
+
allowed = set(profile_workers)
|
|
85
|
+
extras = [w for w in workers if w not in allowed]
|
|
86
|
+
if extras:
|
|
87
|
+
raise WorkersError(
|
|
88
|
+
"workers not allowed by profile roster: "
|
|
89
|
+
f"{','.join(extras)} (profile allows: {','.join(profile_workers)})"
|
|
90
|
+
)
|
|
@@ -20,6 +20,16 @@ Launch an okstra task — gather inputs interactively, render the full task bund
|
|
|
20
20
|
- User wants status only — use `okstra-status`.
|
|
21
21
|
- User wants past runs — use `okstra-history`.
|
|
22
22
|
|
|
23
|
+
## Prompt convention (use the right tool for the right input shape)
|
|
24
|
+
|
|
25
|
+
`AskUserQuestion` always renders a picker UI with a forced auto-attached `Other` option. While the user types into `Other`, the picker re-renders and the experience feels out of sync. So:
|
|
26
|
+
|
|
27
|
+
- **Use `AskUserQuestion` ONLY when the answer is a fixed pick from a short option set** (2–4 distinct, mutually exclusive choices). Examples in this skill: task-type choice (Step 4), executor provider (claude/codex/gemini), model picker (default/opus/sonnet/haiku per provider), Use defaults vs Customize, Proceed vs Edit confirmation.
|
|
28
|
+
- **For pure free-text inputs** (file paths, task identifiers, CSV strings, free directives, branch names typed by the user) **do NOT use `AskUserQuestion`**. Instead, write a plain text message (e.g. `"Task group (예: backend-api, INV-1234)을 알려주세요. 빈 줄이면 취소."`) and consume the user's NEXT message as the answer. Then validate and re-prompt with another plain text message on failure.
|
|
29
|
+
- **For "menu + free-text" places** (base-ref pickers, PR base branch) — show the menu with `AskUserQuestion` listing only the canonical options + a literal option labeled `직접 입력`. When the user picks `직접 입력`, follow up with a **separate** plain text prompt and consume the next user message. Do NOT rely on `Other` auto-text inside the picker — its re-render is the root cause of the lag.
|
|
30
|
+
|
|
31
|
+
Echo each captured answer on one short line (e.g. `task-group: backend-api`) so the user sees what was registered, regardless of which prompt shape was used.
|
|
32
|
+
|
|
23
33
|
## Authority Files (disk-only — no env var caching for per-run identity)
|
|
24
34
|
|
|
25
35
|
Every step reads disk afresh. The `OKSTRA_*` env vars below identify the
|
|
@@ -82,7 +92,7 @@ okstra check-project --cwd "$(pwd)"
|
|
|
82
92
|
```
|
|
83
93
|
|
|
84
94
|
- If `ok: true`: read `projectRoot` and `projectId` from the JSON.
|
|
85
|
-
- If `ok: false`: ask the user (`AskUserQuestion
|
|
95
|
+
- If `ok: false`: ask the user with a **plain text prompt** (not `AskUserQuestion` — this is pure free text per the convention above) for an absolute project-root path; rerun with `okstra check-project --cwd <their input>`. Re-prompt with another plain text message on failure.
|
|
86
96
|
|
|
87
97
|
## Step 2: Choose task — existing vs new
|
|
88
98
|
|
|
@@ -103,12 +113,12 @@ For an existing pick, read its `task-manifest.json` to capture `taskType` and `w
|
|
|
103
113
|
|
|
104
114
|
Skip if continuing existing.
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
Use **plain text prompts** (one at a time — write the message and consume the user's next reply; do NOT use `AskUserQuestion` for these per the convention above):
|
|
107
117
|
|
|
108
|
-
1. `"Task group (
|
|
109
|
-
2. `"Task id (
|
|
118
|
+
1. `"Task group 을 알려주세요 (예: backend-api, INV-1234, refactor)"` → `task_group`
|
|
119
|
+
2. `"Task id 를 알려주세요 (예: login-error-analysis, dev-9043)"` → `task_id`
|
|
110
120
|
|
|
111
|
-
Validate that slugified `task_group` and `task_id` each contain at least one alphanumeric character.
|
|
121
|
+
Validate that slugified `task_group` and `task_id` each contain at least one alphanumeric character. On failure, re-prompt with another plain text message stating the validation failure.
|
|
112
122
|
|
|
113
123
|
## Step 4: Choose task-type
|
|
114
124
|
|
|
@@ -125,9 +135,9 @@ Validate that slugified `task_group` and `task_id` each contain at least one alp
|
|
|
125
135
|
|
|
126
136
|
For existing tasks, present `nextRecommendedPhase` as the first option (recommended default).
|
|
127
137
|
|
|
128
|
-
If `implementation` chosen, ask two more
|
|
129
|
-
-
|
|
130
|
-
-
|
|
138
|
+
If `implementation` chosen, ask two more questions in order:
|
|
139
|
+
- **Plain text prompt** (file path is pure free text): `"approved final-report.md 의 경로를 알려주세요 (APPROVED 마커가 있어야 합니다)"`. The underlying python `prepare_task_bundle` re-validates the marker, but you can pre-check with `grep`. Re-prompt with plain text on failure.
|
|
140
|
+
- **`AskUserQuestion`** with three options (`claude` / `codex` / `gemini`) — only this provider mutates project files; the other two run as read-only verifiers. Default `claude` (or `OKSTRA_DEFAULT_EXECUTOR` if set). Pass the answer through `PrepareInputs.executor`.
|
|
131
141
|
|
|
132
142
|
## Step 4.6: Base ref for the task worktree (first phase only)
|
|
133
143
|
|
|
@@ -149,16 +159,17 @@ non-null `entry` with `status: "active"` → **REUSE**.
|
|
|
149
159
|
question (the registered base is authoritative).
|
|
150
160
|
- `ASK` → this is the first phase for this task-key. Continue.
|
|
151
161
|
|
|
152
|
-
|
|
162
|
+
Use the **menu + free-text two-step pattern** (per the convention above):
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
1. `AskUserQuestion` with label `"이 task worktree 의 base branch?"` and exactly these single-select options (NO auto-Other typing — the literal `직접 입력` option is the typed-input escape hatch):
|
|
165
|
+
1. `main` (recommended)
|
|
166
|
+
2. `dev`
|
|
167
|
+
3. `staging`
|
|
168
|
+
4. `preprod`
|
|
169
|
+
5. `prod`
|
|
170
|
+
6. `직접 입력`
|
|
171
|
+
2. If the user picks `직접 입력`, follow up with a **plain text prompt**: `"base ref 를 입력해주세요 (branch, tag, 또는 short/full SHA)"`. Consume the user's next message as the chosen ref.
|
|
172
|
+
3. Otherwise the picked option label is the chosen ref directly.
|
|
162
173
|
|
|
163
174
|
Validate the chosen ref exists in the MAIN worktree before continuing:
|
|
164
175
|
|
|
@@ -168,14 +179,15 @@ git -C "$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-d
|
|
|
168
179
|
|| { echo "ref not found locally: <chosen-ref>"; exit 1; }
|
|
169
180
|
```
|
|
170
181
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
On failure, re-prompt with a plain text message (or return to step 1's
|
|
183
|
+
menu if the user wants to pick a different canonical branch). Echo the
|
|
184
|
+
resolved short SHA back to the user (`base 확정: <ref> (<short-sha>)`)
|
|
185
|
+
and capture `base_ref=<chosen-ref>` for Step 7.
|
|
174
186
|
|
|
175
187
|
## Step 5: Brief path
|
|
176
188
|
|
|
177
|
-
- New task:
|
|
178
|
-
- Existing task: default to the manifest's `taskBriefPath`. Show it; ask
|
|
189
|
+
- New task: **plain text prompt** (file path is pure free text per the convention) `"task brief markdown 의 경로를 알려주세요 (project root 기준 상대경로 또는 절대경로)"`. Consume the user's next message; verify the file exists; on failure, re-prompt with another plain text message.
|
|
190
|
+
- Existing task: default to the manifest's `taskBriefPath`. Show it; ask `AskUserQuestion` `"기존 경로를 유지할까요?"` with options `유지` / `변경`. On `변경`, follow up with a plain text prompt for the new path.
|
|
179
191
|
|
|
180
192
|
## Step 6 (optional): Directive / workers / models / related / clarification
|
|
181
193
|
|
|
@@ -201,26 +213,49 @@ In this phase the roster is fixed by the profile (executor + two verifiers + rep
|
|
|
201
213
|
1. `AskUserQuestion` `"리더(Claude lead) 모델?"` (Claude options) → `lead_model`
|
|
202
214
|
2. `AskUserQuestion` `"실행자({executor-provider}) 모델?"` with options matching the executor's provider (Claude / Codex / Gemini list above) → maps to `claude_model` / `codex_model` / `gemini_model`. The other two provider model fields stay empty (verifiers use defaults).
|
|
203
215
|
3. `AskUserQuestion` `"리포트 작성자(report-writer) 모델?"` (Claude options) → `report_writer_model`
|
|
204
|
-
4.
|
|
205
|
-
5.
|
|
216
|
+
4. **Plain text prompt** (free text) `"추가 directive 가 있으면 적어주세요 (없으면 빈 줄)"` → `directive`. Consume the user's next message verbatim; an empty line means "no directive".
|
|
217
|
+
5. **Plain text prompt** (free text) `"관련 task id 목록을 쉼표로 구분해서 적어주세요 (없으면 빈 줄)"` → `related_tasks_raw`.
|
|
206
218
|
|
|
207
219
|
Do NOT ask for `workers_override` in implementation — the profile's required roster must be preserved (verifier slots are mandatory). Leave `workers_override=""`.
|
|
208
220
|
|
|
209
221
|
### 6b. Other phases (`requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`, `release-handoff`)
|
|
210
222
|
|
|
211
|
-
|
|
223
|
+
**Before asking any worker/model question, resolve the profile's allowed roster:**
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from okstra_ctl.workers import resolve_profile_workers
|
|
227
|
+
profile_workers = resolve_profile_workers(Path("<OKSTRA_PROMPTS_PROFILES_DIR>/<task-type>.md"))
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This is the **only** set of worker IDs you may show or ask about. Never offer
|
|
231
|
+
workers outside this list. Special cases:
|
|
232
|
+
|
|
233
|
+
- If `profile_workers` is empty (e.g., `release-handoff` is lead-only with no
|
|
234
|
+
`- Required workers:` block), **skip the worker question and all
|
|
235
|
+
worker-model questions entirely** — only ask lead model, directive, related,
|
|
236
|
+
clarification. The backend forces `workers=[]` for these profiles.
|
|
237
|
+
- Otherwise, the worker question must enumerate **only** `profile_workers` —
|
|
238
|
+
do NOT show `claude, codex, gemini, report-writer` blindly.
|
|
239
|
+
|
|
240
|
+
Ask each in turn. **Model prompts use `AskUserQuestion`** with the fixed option lists above. **All other prompts use plain text messages** (do NOT wrap free-text inputs in `AskUserQuestion` — the auto-Other re-render lag is what we're avoiding). Skip any worker-model prompt whose worker is not in `profile_workers`.
|
|
212
241
|
|
|
213
|
-
1. `
|
|
242
|
+
1. (only when `profile_workers` is non-empty) **Plain text prompt** `"참여 워커 목록을 쉼표로 구분해서 적어주세요. 빈 줄이면 프로필 기본값 <profile_workers_csv> 을 그대로 씁니다. 사용 가능한 워커: <profile_workers_csv>"` → `workers_override`. Validate the answer is a subset of `profile_workers`; on failure, re-prompt with another plain text message. (Backend also rejects violations with `WorkersError`.)
|
|
214
243
|
2. `AskUserQuestion` `"리더(Claude lead) 모델?"` (Claude options) → `lead_model`
|
|
215
|
-
3. `AskUserQuestion` `"claude 워커 모델?"` (Claude options) → `claude_model`
|
|
216
|
-
4. `AskUserQuestion` `"codex 워커 모델?"` (Codex options) → `codex_model`
|
|
217
|
-
5. `AskUserQuestion` `"gemini 워커 모델?"` (Gemini options) → `gemini_model`
|
|
218
|
-
6. `AskUserQuestion` `"리포트 작성자 모델?"` (Claude options) → `report_writer_model`
|
|
244
|
+
3. (only if `claude` ∈ resolved workers) `AskUserQuestion` `"claude 워커 모델?"` (Claude options) → `claude_model`
|
|
245
|
+
4. (only if `codex` ∈ resolved workers) `AskUserQuestion` `"codex 워커 모델?"` (Codex options) → `codex_model`
|
|
246
|
+
5. (only if `gemini` ∈ resolved workers) `AskUserQuestion` `"gemini 워커 모델?"` (Gemini options) → `gemini_model`
|
|
247
|
+
6. (only if `report-writer` ∈ resolved workers) `AskUserQuestion` `"리포트 작성자 모델?"` (Claude options) → `report_writer_model`
|
|
219
248
|
7. `AskUserQuestion` `"추가 directive (선택, 빈 칸 가능)"` (free text) → `directive`
|
|
220
249
|
8. `AskUserQuestion` `"관련 task id 목록, 쉼표 구분 (선택, 빈 칸 가능)"` (free text) → `related_tasks_raw`
|
|
221
250
|
9. `AskUserQuestion` `"clarification-response 파일 경로 (follow-up 시에만, 빈 칸 가능)"` (free text) → `clarification_response_path`
|
|
251
|
+
10. (only when `task_type == "release-handoff"`) **Plain text prompt** `"PR 본문 템플릿 경로 1회성 override (빈 줄이면 project.json → ~/.okstra/config.json → 스킬 디폴트 순으로 자동 해석)"` → `pr_template_path`. The backend (`okstra_ctl.pr_template.resolve_pr_template_path`) validates the file exists and surfaces `PrTemplateError` on failure.
|
|
252
|
+
- **Persist follow-up** (only when the user typed a non-empty path AND it differs from any currently-registered project/global value): ask `AskUserQuestion` `"방금 입력한 경로를 영구 저장할까요?"` with three options:
|
|
253
|
+
1. `이번 run 만 (1회성)` — proceed with the override; do NOT touch project.json or global config.
|
|
254
|
+
2. `프로젝트에 저장 (project scope)` — run `okstra config set pr-template-path "<path>" --scope project` and use the override for this run too.
|
|
255
|
+
3. `전역에 저장 (global scope)` — run `okstra config set pr-template-path "<path>" --scope global` (must be absolute or `~/`-prefixed; if not, re-ask with a plain text prompt for an absolute version) and use the override for this run too.
|
|
256
|
+
- Skip the persist follow-up entirely when the user left the override blank, or when the typed value matches the value already stored at the scope it would land in (avoid no-op confirmations).
|
|
222
257
|
|
|
223
|
-
For prompts whose target worker is NOT in the resolved workers list (after override),
|
|
258
|
+
For prompts whose target worker is NOT in the resolved workers list (after override), present a single confirmation line such as `gemini-model 생략 (workers에 gemini 없음)` so the user can see why the question was skipped.
|
|
224
259
|
|
|
225
260
|
## Step 6.5: Confirm selections before rendering
|
|
226
261
|
|
|
@@ -267,7 +302,8 @@ okstra render-bundle \
|
|
|
267
302
|
--lead-model "..." --claude-model "..." --codex-model "..." \
|
|
268
303
|
--gemini-model "..." --report-writer-model "..." \
|
|
269
304
|
--related-tasks "..." \
|
|
270
|
-
--clarification-response "<clarification-or-empty>"
|
|
305
|
+
--clarification-response "<clarification-or-empty>" \
|
|
306
|
+
--pr-template-path "<pr-template-override-or-empty; release-handoff only>"
|
|
271
307
|
```
|
|
272
308
|
|
|
273
309
|
Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
okstra release-handoff 기본 PR 본문 템플릿.
|
|
3
|
+
|
|
4
|
+
이 파일은 사용자 정의 PR 템플릿이 없을 때 사용됩니다. 우선순위:
|
|
5
|
+
1. okstra-run Step 6 에서 입력한 per-run override 경로
|
|
6
|
+
2. <project-root>/.project-docs/okstra/project.json 의 `prTemplatePath`
|
|
7
|
+
3. ~/.okstra/config.json 의 `prTemplatePath`
|
|
8
|
+
4. 이 디폴트 파일
|
|
9
|
+
|
|
10
|
+
프로젝트 또는 전역 설정으로 자체 템플릿을 쓰려면 위 경로 중 하나에
|
|
11
|
+
`prTemplatePath` 키를 추가하세요. (절대경로 또는 project_root 기준 상대경로)
|
|
12
|
+
|
|
13
|
+
플레이스홀더는 release-handoff 의 Claude lead 가 다음 입력을 근거로
|
|
14
|
+
직접 채웁니다:
|
|
15
|
+
- run brief 의 의도/스코프
|
|
16
|
+
- 인용된 final-verification 리포트의 verdict 근거
|
|
17
|
+
- `git log --oneline <base>..HEAD` 의 commit 범위
|
|
18
|
+
- `git diff <base>..HEAD --stat` 의 변경 파일 통계
|
|
19
|
+
|
|
20
|
+
빈 섹션은 그대로 두지 말고 통째로 삭제합니다. HTML 주석은 PR 생성 전에
|
|
21
|
+
모두 제거됩니다.
|
|
22
|
+
-->
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
<!-- 1–3 bullets. 변경의 동기(WHY)와 결과(WHAT changed at a high level). -->
|
|
27
|
+
|
|
28
|
+
## Changes
|
|
29
|
+
|
|
30
|
+
<!-- 영역별로 묶은 구체적 변경 목록. 파일/모듈 단위 그룹화 권장. -->
|
|
31
|
+
|
|
32
|
+
## Test plan
|
|
33
|
+
|
|
34
|
+
<!-- 리뷰어가 따라 할 수 있는 검증 절차. 자동/수동 모두 가능. -->
|
|
35
|
+
|
|
36
|
+
- [ ] <검증 항목>
|
|
37
|
+
- [ ] <검증 항목>
|
|
38
|
+
|
|
39
|
+
## Linked issues
|
|
40
|
+
|
|
41
|
+
<!-- `Refs: TICKET-123`, `Closes #N`, 관련 PR 링크 등. 없으면 섹션 통째로 삭제. -->
|
|
@@ -209,6 +209,43 @@ To opt out (advanced): replace the symlink with a regular file. okstra
|
|
|
209
209
|
will detect that it is no longer a symlink on its next setup call and
|
|
210
210
|
back it up as `.bak.<timestamp>` rather than overwriting silently.
|
|
211
211
|
|
|
212
|
+
## Step 4.8 (optional): register a project PR body template
|
|
213
|
+
|
|
214
|
+
`release-handoff` fills the PR body from a template. By default it uses the
|
|
215
|
+
bundled skill template at
|
|
216
|
+
`~/.claude/skills/okstra-run/templates/pr-body.template.md`. Most projects
|
|
217
|
+
want their own — e.g. the repo's `.github/PULL_REQUEST_TEMPLATE.md` — to
|
|
218
|
+
keep PRs consistent with what the team already merges manually.
|
|
219
|
+
|
|
220
|
+
Ask the user with `AskUserQuestion` (fixed options, NOT free text — file
|
|
221
|
+
path entry happens in the follow-up plain text prompt per the
|
|
222
|
+
okstra-run prompt convention):
|
|
223
|
+
|
|
224
|
+
- **Question**: `"이 프로젝트에서 release-handoff 가 사용할 PR 본문 템플릿을 등록할까요?"`
|
|
225
|
+
- **Options**:
|
|
226
|
+
1. `이번 프로젝트만 (project scope)` — write to `<PROJECT_ROOT>/.project-docs/okstra/project.json` `prTemplatePath`.
|
|
227
|
+
2. `전역 (global scope)` — write to `~/.okstra/config.json` `prTemplatePath`.
|
|
228
|
+
3. `나중에` — skip.
|
|
229
|
+
|
|
230
|
+
If the user picks scope 1 or 2, follow up with a **plain text prompt**:
|
|
231
|
+
`"PR 본문 템플릿 파일 경로를 알려주세요. project 스코프는 project-root 기준 상대경로 또는 절대경로, global 스코프는 절대경로 또는 ~/ 시작 경로만 허용됩니다."` Consume the next user message, then run:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
okstra config set pr-template-path "<typed-path>" --scope <project|global>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The command validates the value (global rejects relative paths) and
|
|
238
|
+
writes atomically. Surface its stdout JSON so the user sees which file
|
|
239
|
+
was updated.
|
|
240
|
+
|
|
241
|
+
If the user chose `나중에`, tell them they can register later with one of:
|
|
242
|
+
|
|
243
|
+
- `okstra config set pr-template-path <path> --scope project|global` from a
|
|
244
|
+
terminal, or
|
|
245
|
+
- the per-run 1회성 override prompt during the next release-handoff run
|
|
246
|
+
(the run can also persist that override to project/global scope on the
|
|
247
|
+
spot — see the okstra-run skill).
|
|
248
|
+
|
|
212
249
|
## Step 5: Verify
|
|
213
250
|
|
|
214
251
|
```bash
|
|
@@ -202,7 +202,52 @@ Terminal statuses that can be recorded for a worker:
|
|
|
202
202
|
|
|
203
203
|
**Authoritative source.** If other documents (SKILL.md, worker agent definitions) disagree with this section, this section wins.
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
### Result Frontmatter (mandatory, precedes Section 0)
|
|
206
|
+
|
|
207
|
+
Every worker result file MUST begin with a YAML frontmatter block. The values are sourced from the corresponding fields of the input files' frontmatter (e.g. `analysis-material.md`, `task-brief.md`) — copy them verbatim; do NOT regenerate them. Only `workerId` and `title` are worker-specific.
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
---
|
|
211
|
+
title: OKSTRA <Worker Role> Result - <task-key>
|
|
212
|
+
id: "<task-key with ':' replaced by '-'>"
|
|
213
|
+
aliases: ["<id>-<task-type>"]
|
|
214
|
+
tags: ["obsidian", "okstra", "worker-result", "<task-type>"]
|
|
215
|
+
taskType: "<task-type>"
|
|
216
|
+
workerId: "<claude|codex|gemini|report-writer>"
|
|
217
|
+
task-id: "<task-id>"
|
|
218
|
+
task-group: "<task-group>"
|
|
219
|
+
project-id: "<project-id>"
|
|
220
|
+
date: <YYYY-MM-DD>
|
|
221
|
+
---
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Concrete example for a `claude-worker` result on task-key `fontsninja-classifier-v2:DEV-9388:DEV-9429` and task-type `implementation`:
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
---
|
|
228
|
+
title: OKSTRA Claude Worker Result - fontsninja-classifier-v2:DEV-9388:DEV-9429
|
|
229
|
+
id: "fontsninja-classifier-v2-DEV-9388-DEV-9429"
|
|
230
|
+
aliases: ["fontsninja-classifier-v2-DEV-9388-DEV-9429-implementation"]
|
|
231
|
+
tags: ["obsidian", "okstra", "worker-result", "implementation"]
|
|
232
|
+
taskType: "implementation"
|
|
233
|
+
workerId: "claude"
|
|
234
|
+
task-id: "DEV-9429"
|
|
235
|
+
task-group: "DEV-9388"
|
|
236
|
+
project-id: "fontsninja-classifier-v2"
|
|
237
|
+
date: 2026-05-15
|
|
238
|
+
---
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Rules:
|
|
242
|
+
- `id` is the run's `task-key` with `:` replaced by `-`. It is a scalar string, NOT an array.
|
|
243
|
+
- `aliases` is a YAML array containing a single value `"<id>-<task-type>"`.
|
|
244
|
+
- `taskType` mirrors the run's task type (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`).
|
|
245
|
+
- `workerId` identifies which worker produced the file. Required values: `claude` / `codex` / `gemini` / `report-writer`.
|
|
246
|
+
- Other fields (`task-id`, `task-group`, `project-id`, `date`) MUST match the input files' frontmatter exactly. If the input frontmatter is missing or unreadable, the worker MUST record a `tool-failure` and stop instead of guessing.
|
|
247
|
+
|
|
248
|
+
The same frontmatter contract applies to the `Report writer worker`'s final-report file — the report-writer copies these values from its inputs and only swaps `workerId` to `report-writer`.
|
|
249
|
+
|
|
250
|
+
A successful worker result must include the following sections in this exact order, beneath the frontmatter block:
|
|
206
251
|
|
|
207
252
|
0. **Reading Confirmation** — one short line per input file (`task-brief.md`, `analysis-profile.md`, `analysis-material.md` if present, `reference-expectations.md`, `clarification-response.md` if a carry-in was provided, `final-report-template.md`) stating that the worker read it end-to-end. Each line takes the form `- Read <file-name> end-to-end (<line-count> lines).`. If a file was skipped or only partially read, the worker MUST NOT produce sections 1–5; instead it records a `tool-failure` in the errors sidecar and stops. This section exists specifically to counteract the common failure mode where workers skim long inputs because they share structure with the file the run will eventually write into.
|
|
208
253
|
1. Findings
|
|
@@ -8,6 +8,7 @@ date: {{TASK_DATE}}
|
|
|
8
8
|
task-id: "{{TASK_ID}}"
|
|
9
9
|
task-group: "{{TASK_GROUP}}"
|
|
10
10
|
project-id: "{{PROJECT_ID}}"
|
|
11
|
+
taskType: "{{FM_TASK_TYPE}}"
|
|
11
12
|
---
|
|
12
13
|
|
|
13
14
|
# {{TASK_KEY}} - Multi-Agent Cross Verification Final Report
|
|
@@ -16,6 +17,7 @@ project-id: "{{PROJECT_ID}}"
|
|
|
16
17
|
- Task Type: {{TASK_TYPE}}
|
|
17
18
|
- Report Owner: `Claude lead`
|
|
18
19
|
- Lead Model: `{{LEAD_MODEL}}`
|
|
20
|
+
- Okstra Version: `{{OKSTRA_VERSION}}`
|
|
19
21
|
- Clarification Response Carried In: `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}`
|
|
20
22
|
|
|
21
23
|
## User Approval Request (사용자 승인 게이트)
|
|
@@ -131,11 +131,13 @@
|
|
|
131
131
|
"Bash(codex exec:*)",
|
|
132
132
|
"Bash(okstra)",
|
|
133
133
|
"Bash(okstra:*)",
|
|
134
|
-
"Bash($HOME/.okstra/bin
|
|
134
|
+
"Bash($HOME/.okstra/bin/:*)",
|
|
135
135
|
|
|
136
136
|
"Bash(gemini)",
|
|
137
137
|
"Bash(gemini:*)",
|
|
138
|
-
|
|
138
|
+
|
|
139
|
+
"Bash($HOME/.okstra/bin/okstra-trace-cleanup.sh)",
|
|
140
|
+
"Bash($HOME/.okstra/bin/okstra-trace-cleanup.sh:*)",
|
|
139
141
|
|
|
140
142
|
"Bash(claude)",
|
|
141
143
|
"Bash(claude:*)",
|