okstra 0.12.0 → 0.13.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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +7 -0
- package/runtime/python/okstra_ctl/worktree.py +52 -13
- package/runtime/python/okstra_project/resolver.py +9 -6
- package/runtime/skills/okstra-run/SKILL.md +52 -8
- package/runtime/skills/okstra-setup/SKILL.md +26 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -22,6 +22,13 @@ Invoke the `okstra` skill now. Read the manifests below for all task metadata, p
|
|
|
22
22
|
- All other paths in this prompt are relative to this root unless they begin with `/`.
|
|
23
23
|
- When dispatching workers, you MUST include this absolute root in every worker prompt header so that workers do not depend on inherited cwd to resolve relative paths.
|
|
24
24
|
|
|
25
|
+
## Worker Prompt Files (not pre-rendered)
|
|
26
|
+
|
|
27
|
+
- The `runs/<phase>/prompts/<worker>-worker-prompt-*.md` files referenced in `task-manifest.json → artifacts.workerPromptPathByWorkerId` are **NOT** rendered by the okstra runtime. Those paths are the *assigned* locations where the prompt history MUST be written at dispatch time.
|
|
28
|
+
- Therefore: at the start of every phase the prompts/ directory is normally empty (or contains only previously-dispatched workers' files). This is expected. Do NOT narrate it as "missing", "누락", or "not yet rendered" — it just means dispatch has not happened yet.
|
|
29
|
+
- Before dispatching any required worker, **you (the lead) construct the worker prompt and persist it to the assigned absolute path using `Write`** (per `okstra-team-contract` rule 6). Only after persisting do you call the worker subagent (Agent tool / Codex / Gemini wrapper). The wrapper subagents will also re-write the same file on their end; the double-write is intentional and idempotent.
|
|
30
|
+
- Do not "check if the file exists and skip dispatch" — file presence is not a signal to skip. Worker selection and skipping rules come from team-state, never from prompts/ directory contents.
|
|
31
|
+
|
|
25
32
|
## Manifests
|
|
26
33
|
|
|
27
34
|
- Task manifest: `{{TASK_MANIFEST_RELATIVE_PATH}}`
|
|
@@ -32,6 +32,7 @@ Side effects:
|
|
|
32
32
|
"""
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
|
+
import json
|
|
35
36
|
import os
|
|
36
37
|
import subprocess
|
|
37
38
|
from dataclasses import dataclass
|
|
@@ -54,13 +55,19 @@ OKSTRA_WORKTREES_RELATIVE = Path(".okstra/worktrees")
|
|
|
54
55
|
# acceptable because okstra only writes inside its own task-scoped
|
|
55
56
|
# subdirectory (e.g. `.project-docs/okstra/tasks/<task-id>/runs/...`).
|
|
56
57
|
#
|
|
57
|
-
# Override
|
|
58
|
-
#
|
|
59
|
-
#
|
|
58
|
+
# Override precedence (most-specific first):
|
|
59
|
+
# 1. `OKSTRA_WORKTREE_SYNC_DIRS` env var — colon-separated list, REPLACES
|
|
60
|
+
# defaults. Empty string disables the feature entirely. One-off
|
|
61
|
+
# operator override.
|
|
62
|
+
# 2. `worktreeSyncDirs` array in `.project-docs/okstra/project.json` —
|
|
63
|
+
# project-level config, persists across runs. Same semantics: array
|
|
64
|
+
# REPLACES defaults, empty array disables.
|
|
65
|
+
# 3. The built-in `DEFAULT_WORKTREE_SYNC_DIRS` below.
|
|
60
66
|
DEFAULT_WORKTREE_SYNC_DIRS: tuple[str, ...] = (
|
|
61
67
|
".project-docs",
|
|
62
68
|
".scratch",
|
|
63
69
|
"graphify-out",
|
|
70
|
+
".claude",
|
|
64
71
|
)
|
|
65
72
|
|
|
66
73
|
|
|
@@ -172,18 +179,50 @@ def _main_worktree_path(project_root: Path) -> Path:
|
|
|
172
179
|
return project_root
|
|
173
180
|
|
|
174
181
|
|
|
175
|
-
def
|
|
182
|
+
def _read_project_json_sync_dirs(project_root: Path) -> Optional[tuple[str, ...]]:
|
|
183
|
+
"""Read `worktreeSyncDirs` from `.project-docs/okstra/project.json`.
|
|
184
|
+
|
|
185
|
+
Returns None if the field is absent or the file cannot be parsed (so
|
|
186
|
+
the caller falls back to defaults). Returns an empty tuple if the
|
|
187
|
+
field is explicitly an empty array (caller treats this as "disable").
|
|
188
|
+
A non-list value is treated as missing — we do not raise here because
|
|
189
|
+
sync-dir resolution must never block worktree provisioning.
|
|
190
|
+
"""
|
|
191
|
+
target = project_root / ".project-docs" / "okstra" / "project.json"
|
|
192
|
+
if not target.is_file():
|
|
193
|
+
return None
|
|
194
|
+
try:
|
|
195
|
+
data = json.loads(target.read_text(encoding="utf-8"))
|
|
196
|
+
except (OSError, json.JSONDecodeError):
|
|
197
|
+
return None
|
|
198
|
+
if not isinstance(data, dict):
|
|
199
|
+
return None
|
|
200
|
+
value = data.get("worktreeSyncDirs")
|
|
201
|
+
if not isinstance(value, list):
|
|
202
|
+
return None
|
|
203
|
+
cleaned = tuple(
|
|
204
|
+
item.strip() for item in value
|
|
205
|
+
if isinstance(item, str) and item.strip()
|
|
206
|
+
)
|
|
207
|
+
return cleaned
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _resolve_sync_dirs(project_root: Optional[Path] = None) -> tuple[str, ...]:
|
|
176
211
|
"""Return the list of project-root-relative dirs to symlink into the
|
|
177
|
-
new worktree.
|
|
178
|
-
|
|
212
|
+
new worktree. Precedence: env var → project.json → built-in default.
|
|
213
|
+
See the comment above `DEFAULT_WORKTREE_SYNC_DIRS` for full semantics.
|
|
179
214
|
"""
|
|
180
215
|
raw = os.environ.get("OKSTRA_WORKTREE_SYNC_DIRS")
|
|
181
|
-
if raw is None:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return ()
|
|
186
|
-
|
|
216
|
+
if raw is not None:
|
|
217
|
+
raw = raw.strip()
|
|
218
|
+
if not raw:
|
|
219
|
+
return ()
|
|
220
|
+
return tuple(part for part in (p.strip() for p in raw.split(":")) if part)
|
|
221
|
+
if project_root is not None:
|
|
222
|
+
from_project = _read_project_json_sync_dirs(project_root)
|
|
223
|
+
if from_project is not None:
|
|
224
|
+
return from_project
|
|
225
|
+
return DEFAULT_WORKTREE_SYNC_DIRS
|
|
187
226
|
|
|
188
227
|
|
|
189
228
|
def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
|
|
@@ -201,7 +240,7 @@ def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
|
|
|
201
240
|
caller can include them in the provisioning note.
|
|
202
241
|
"""
|
|
203
242
|
notes: list[str] = []
|
|
204
|
-
for rel in _resolve_sync_dirs():
|
|
243
|
+
for rel in _resolve_sync_dirs(source_root):
|
|
205
244
|
src = (source_root / rel).resolve()
|
|
206
245
|
if not src.exists():
|
|
207
246
|
continue
|
|
@@ -117,12 +117,15 @@ def upsert_project_json(project_root: Path, project_id: str, *,
|
|
|
117
117
|
f"or manually delete {target} if you intend to re-register this directory "
|
|
118
118
|
f"under a different id. "
|
|
119
119
|
f"(projectId 불일치: 한 PROJECT_ROOT 에는 하나의 projectId 만 허용됩니다.)")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
# Preserve any user-managed fields (e.g. `worktreeSyncDirs`,
|
|
121
|
+
# `mcpServers`) so manual edits to project.json are not wiped
|
|
122
|
+
# by the per-run self-registration upsert. Only the canonical
|
|
123
|
+
# identity/timestamp fields below are owned by this function.
|
|
124
|
+
result = dict(data) if isinstance(data, dict) else {}
|
|
125
|
+
result["projectId"] = project_id
|
|
126
|
+
result["projectRoot"] = abs_root
|
|
127
|
+
result["createdAt"] = str(data.get("createdAt") or when)
|
|
128
|
+
result["updatedAt"] = when
|
|
126
129
|
else:
|
|
127
130
|
result = {
|
|
128
131
|
"projectId": project_id,
|
|
@@ -148,15 +148,59 @@ If `implementation` chosen, ask two more `AskUserQuestion` in order:
|
|
|
148
148
|
|
|
149
149
|
## Step 6 (optional): Directive / workers / models / related / clarification
|
|
150
150
|
|
|
151
|
-
Single `AskUserQuestion` first: `"
|
|
151
|
+
Single `AskUserQuestion` first: `"기본 워커/모델로 진행할까요, 아니면 커스터마이즈할까요?"` (options: `Use defaults`, `Customize`).
|
|
152
152
|
|
|
153
153
|
- `Use defaults` → all overrides remain empty.
|
|
154
|
-
- `Customize` → ask
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
- `Customize` → the prompts you ask depend on the `task_type` chosen in Step 4. Blank answer always means "use default". Never call the prompt label "worker CSV" — use plain Korean labels as shown below.
|
|
155
|
+
|
|
156
|
+
### 6a. `implementation` phase (executor-driven)
|
|
157
|
+
|
|
158
|
+
In this phase the roster is fixed by the profile (executor + two verifiers + report-writer). The Step 4 `executor` answer already determines who mutates code; verifier models use phase-specific defaults (`Claude verifier`=sonnet, `Codex verifier`=gpt-5.5, `Gemini verifier`=auto). So ask **only three model prompts**, plus directive/related/clarification:
|
|
159
|
+
|
|
160
|
+
1. `"리더(Claude lead) 모델? (빈 칸 = 기본값)"` → `lead_model`
|
|
161
|
+
2. `"실행자({executor-provider}) 모델? (빈 칸 = 기본값)"` → maps to `claude_model` / `codex_model` / `gemini_model` based on the Step 4 executor choice. The other two provider model fields stay empty (verifiers use defaults).
|
|
162
|
+
3. `"리포트 작성자(report-writer) 모델? (빈 칸 = 기본값)"` → `report_writer_model`
|
|
163
|
+
4. `"추가 directive (선택, 빈 칸 가능)"` → `directive`
|
|
164
|
+
5. `"관련 task id 목록, 쉼표 구분 (선택, 빈 칸 가능)"` → `related_tasks_raw`
|
|
165
|
+
|
|
166
|
+
Do NOT ask for `workers_override` in implementation — the profile's required roster must be preserved (verifier slots are mandatory). Leave `workers_override=""`.
|
|
167
|
+
|
|
168
|
+
### 6b. Other phases (`requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`, `release-handoff`)
|
|
169
|
+
|
|
170
|
+
Ask each in turn (free text, blank = default):
|
|
171
|
+
|
|
172
|
+
1. `"참여 워커 목록 (쉼표 구분, 빈 칸 = 프로필 기본값). 선택지: claude, codex, gemini, report-writer"` → `workers_override`
|
|
173
|
+
2. `"리더(Claude lead) 모델? (빈 칸 = 기본값)"` → `lead_model`
|
|
174
|
+
3. `"claude 워커 모델? (빈 칸 = 기본값)"` → `claude_model`
|
|
175
|
+
4. `"codex 워커 모델? (빈 칸 = 기본값)"` → `codex_model`
|
|
176
|
+
5. `"gemini 워커 모델? (빈 칸 = 기본값)"` → `gemini_model`
|
|
177
|
+
6. `"리포트 작성자 모델? (빈 칸 = 기본값)"` → `report_writer_model`
|
|
178
|
+
7. `"추가 directive (선택, 빈 칸 가능)"` → `directive`
|
|
179
|
+
8. `"관련 task id 목록, 쉼표 구분 (선택, 빈 칸 가능)"` → `related_tasks_raw`
|
|
180
|
+
9. `"clarification-response 파일 경로 (follow-up 시에만, 빈 칸 가능)"` → `clarification_response_path`
|
|
181
|
+
|
|
182
|
+
For prompts whose target worker is NOT in the resolved workers list (after override), skip the prompt and present a single line such as `gemini-model 생략 (workers에 gemini 없음)`.
|
|
183
|
+
|
|
184
|
+
## Step 6.5: Confirm selections before rendering
|
|
185
|
+
|
|
186
|
+
Before calling `prepare_task_bundle`, echo the resolved selections back to the user in a compact block so they can verify what will be passed. Show the **effective** values, not the raw input — i.e. when the user left a field blank, display `default` (and where known, the actual default such as `opus` / `sonnet`). Example for an `implementation` run:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
선택 확인:
|
|
190
|
+
task-type : implementation
|
|
191
|
+
task-key : <group>/<id>
|
|
192
|
+
executor : codex
|
|
193
|
+
workers : (프로필 기본 — executor + verifier 2 + report-writer)
|
|
194
|
+
lead-model : default (opus)
|
|
195
|
+
codex-model : gpt-5.5-high ← executor model
|
|
196
|
+
claude-model : default (sonnet) ← verifier
|
|
197
|
+
gemini-model : default (auto) ← verifier
|
|
198
|
+
report-writer : default (opus)
|
|
199
|
+
directive : (none)
|
|
200
|
+
approved-plan : <abs path>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Then `AskUserQuestion`: `"이대로 진행할까요?"` with options `Proceed` / `Edit`. On `Edit`, return to the relevant Step 6 sub-prompt.
|
|
160
204
|
|
|
161
205
|
## Step 7: Call `prepare_task_bundle` directly
|
|
162
206
|
|
|
@@ -182,7 +226,7 @@ out = prepare_task_bundle(PrepareInputs(
|
|
|
182
226
|
task_type="<task-type>",
|
|
183
227
|
brief_path=brief_abs,
|
|
184
228
|
directive="<directive or empty>",
|
|
185
|
-
workers_override="<
|
|
229
|
+
workers_override="<comma-separated worker list, or empty for profile default; MUST be empty for implementation>",
|
|
186
230
|
lead_model="...", claude_model="...", codex_model="...",
|
|
187
231
|
gemini_model="...", report_writer_model="...",
|
|
188
232
|
related_tasks_raw="...",
|
|
@@ -116,6 +116,32 @@ print(result)
|
|
|
116
116
|
PY
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## Step 4.5 (optional): customise worktree sync dirs
|
|
120
|
+
|
|
121
|
+
Each okstra run provisions a task-scoped git worktree under
|
|
122
|
+
`~/.okstra/worktrees/.../`. A small set of project-root-relative
|
|
123
|
+
directories is symlinked from the main checkout into that worktree so
|
|
124
|
+
every task sees the shared state. The built-in default is
|
|
125
|
+
`.project-docs`, `.scratch`, `graphify-out`, `.claude`.
|
|
126
|
+
|
|
127
|
+
To override per-project, add a `worktreeSyncDirs` array to
|
|
128
|
+
`project.json`. Empty array disables the feature; the field is
|
|
129
|
+
preserved across the runtime's auto-upserts (only `projectId`,
|
|
130
|
+
`projectRoot`, `createdAt`, `updatedAt` are runtime-owned).
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"projectId": "...",
|
|
135
|
+
"projectRoot": "...",
|
|
136
|
+
"worktreeSyncDirs": [".project-docs", ".scratch", ".claude", "my-custom-dir"]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Resolution precedence: `OKSTRA_WORKTREE_SYNC_DIRS` env var → this
|
|
141
|
+
field → built-in default. Only edit when defaults don't cover the
|
|
142
|
+
project's working files (e.g. additional cache or local-config dirs
|
|
143
|
+
that must follow the executor into the worktree).
|
|
144
|
+
|
|
119
145
|
## Step 5: Verify
|
|
120
146
|
|
|
121
147
|
```bash
|