okstra 0.12.0 → 0.13.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.12.0",
3
- "builtAt": "2026-05-12T18:39:56.088Z",
2
+ "package": "0.13.1",
3
+ "builtAt": "2026-05-13T01:13:31.365Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -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 via the `OKSTRA_WORKTREE_SYNC_DIRS` env var: a colon-separated
58
- # list of project-root-relative paths that REPLACES this default. Use an
59
- # empty string to disable the feature entirely.
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 _resolve_sync_dirs() -> tuple[str, ...]:
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. Reads `OKSTRA_WORKTREE_SYNC_DIRS` if set (colon-separated,
178
- empty string disables); otherwise returns the built-in default.
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
- return DEFAULT_WORKTREE_SYNC_DIRS
183
- raw = raw.strip()
184
- if not raw:
185
- return ()
186
- return tuple(part for part in (p.strip() for p in raw.split(":")) if part)
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
- result = {
121
- "projectId": project_id,
122
- "projectRoot": abs_root,
123
- "createdAt": str(data.get("createdAt") or when),
124
- "updatedAt": when,
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: `"Use default workers and models, or customize?"`
151
+ Single `AskUserQuestion` first: `"기본 워커/모델로 진행할까요, 아니면 커스터마이즈할까요?"` (options: `Use defaults`, `Customize`).
152
152
 
153
153
  - `Use defaults` → all overrides remain empty.
154
- - `Customize` → ask each in turn (free text, blank = use default):
155
- - workers CSV (subset of `claude,codex,gemini,report-writer`)
156
- - `lead-model`, `claude-model`, `codex-model`, `gemini-model`, `report-writer-model`
157
- - `directive`
158
- - `related-tasks` CSV
159
- - `clarification-response` path (relevant for follow-up `requirements-discovery` / `error-analysis` runs)
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="<workers csv or empty>",
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
@@ -333,15 +333,16 @@ Empty-state placeholder, copy verbatim when nothing else applies:
333
333
  규칙:
334
334
 
335
335
  - 각 row의 `Auto-spawn?` 값이 `yes` 이면 Phase 7 의 `scripts/okstra-spawn-followups.py` 가 자동으로 후속 task 디렉터리(`tasks/<TASK_GROUP>/<New Task ID>/`)와 `task-manifest.json` (status: `todo`), stub task-brief 를 생성합니다. `no` 이면 사람이 따로 결정합니다.
336
+ - `Ticket ID` 는 본 보고서 상단 `Ticket Coverage` 규칙과 동일합니다. 후속 작업이 특정 외부 ticket(Jira/Linear/이슈 트래커 등)이나 본 보고서의 ticket 인덱스에 묶이면 그 키를 적고, 매핑이 없으면 `Task ID` 폴백 값을 prefix 없이 적습니다(예: `8852`). 어느 쪽으로도 식별 불가하면 `unknown`.
336
337
  - `New Task ID` 는 알파숫자·하이픈만 사용하는 짧은 slug 입니다 (예: `8852-followup-logs`). 같은 task-group 안에서 유일해야 합니다.
337
338
  - `Suggested task-type` 은 `requirements-discovery` / `error-analysis` / `implementation-planning` / `implementation` / `final-verification` / `release-handoff` 중 하나.
338
339
  - `Scope` 는 영향 파일·영역을 콤마 또는 한 줄로 적습니다.
339
340
  - `Reason / Why deferred` 는 본 run 에서 처리하지 못한 이유를 한 두 문장으로 적습니다. "시간이 없어서" 같은 모호한 사유는 거절됩니다.
340
341
  - 동일 follow-up 이 여러 run 에 걸쳐 등장하면 `New Task ID` 를 동일하게 유지하여 중복 spawn 을 방지합니다 (script 가 기존 디렉터리 존재 여부로 idempotent 처리).
341
342
 
342
- | ID | Origin | New Task ID | Title | Suggested task-type | Scope (files/areas) | Reason / Why deferred | Priority (P0/P1/P2) | Auto-spawn? |
343
- |----|--------|-------------|-------|---------------------|---------------------|------------------------|---------------------|-------------|
344
- | FU-001 | `<out-of-plan / verifier-concern / scope-boundary / open-question / manual>` | `<new-task-id-slug>` | <한 줄 제목> | `<task-type>` | `<files / areas>` | <한 두 문장 사유> | `P1` | `yes` |
343
+ | ID | Ticket ID | Origin | New Task ID | Title | Suggested task-type | Scope (files/areas) | Reason / Why deferred | Priority (P0/P1/P2) | Auto-spawn? |
344
+ |----|-----------|--------|-------------|-------|---------------------|---------------------|------------------------|---------------------|-------------|
345
+ | FU-001 | `<TICKET-or-fallback>` | `<out-of-plan / verifier-concern / scope-boundary / open-question / manual>` | `<new-task-id-slug>` | <한 줄 제목> | `<task-type>` | `<files / areas>` | <한 두 문장 사유> | `P1` | `yes` |
345
346
 
346
347
  빈 상태 예시 (해당 없음):
347
348