okstra 0.10.0 → 0.11.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 CHANGED
@@ -109,6 +109,8 @@ okstra install # 'npx -y okstra@latest install' 와 동일
109
109
 
110
110
  글로벌 설치는 Node CLI 를 PATH 에 등록할 뿐입니다. 런타임(`~/.okstra/`) 과 Claude 스킬(`~/.claude/skills/`) 은 여전히 `okstra install` 이 생성합니다 — `npm i -g` 에 포함되지 않습니다. 이후 업그레이드: `npm i -g okstra@latest && okstra install`. 글로벌 바이너리 제거: `npm uninstall -g okstra` (`~/.okstra/` 는 그대로; 그것까지 지우려면 `okstra uninstall`).
111
111
 
112
+ **글로벌 설치 시 스킬 동작.** 모든 okstra 스킬은 PATH 에 잡힌 `okstra` 를 자동 감지하여 `npx -y okstra@latest` 대신 우선 사용합니다. 즉 글로벌 설치를 해두면 매 스킬 호출(`okstra-run`, `okstra-status`, `okstra-history`, `okstra-schedule`, `okstra-report-finder`, `okstra-time-summary`, `okstra-setup` Step 2 의 Step 0) 마다 npx 가 패키지 fetch / 버전 체크하던 비용이 사라집니다. 스킬이 본인이 설치한 버전을 그대로 쓰므로 **업그레이드 타이밍은 사용자가 통제** 합니다 — 더 이상 호출마다 `@latest` 가 강제되지 않습니다. 새 릴리스를 받으려면 원하는 시점에 `npm i -g okstra@latest && okstra install` 을 실행하세요. `okstra` 가 PATH 에 없으면 스킬은 자동으로 npx fallback 으로 동작하므로 글로벌 설치가 없는 환경에서도 변경 없이 그대로 동작합니다.
113
+
112
114
  ### 3.2 프로젝트 등록 (프로젝트당 1회)
113
115
 
114
116
  CLI 에서:
package/README.md CHANGED
@@ -108,6 +108,8 @@ okstra install # same as 'npx -y okstra@latest install'
108
108
 
109
109
  The global install only registers the Node CLI on your PATH. The runtime (`~/.okstra/`) and the Claude skills (`~/.claude/skills/`) are still provisioned by `okstra install` — they are not part of `npm i -g`. To upgrade later: `npm i -g okstra@latest && okstra install`. To remove the global binary: `npm uninstall -g okstra` (leaves `~/.okstra/` untouched; remove that with `okstra uninstall`).
110
110
 
111
+ **Skill behaviour with a global install.** All okstra skills auto-detect a PATH-resolved `okstra` and prefer it over `npx -y okstra@latest`. That means a global install removes the per-call npx fetch / version-check from every skill invocation (Step 0 of `okstra-run`, `okstra-status`, `okstra-history`, `okstra-schedule`, `okstra-report-finder`, `okstra-time-summary`, `okstra-setup` Step 2). Since the skill uses your globally installed version directly, *you* control upgrade timing — `@latest` is no longer forced on each call. Run `npm i -g okstra@latest && okstra install` whenever you want to pull a new release. If `okstra` is not on PATH the skill silently falls back to npx, so machines without a global install keep working unchanged.
112
+
111
113
  ### 3.2 Register a project (once per project)
112
114
 
113
115
  From the CLI:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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.10.0",
3
- "builtAt": "2026-05-12T16:26:44.380Z",
2
+ "package": "0.11.0",
3
+ "builtAt": "2026-05-12T17:46:50.784Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -160,8 +160,9 @@ These phases are governed by [okstra-team-contract](./skills/okstra-team-contrac
160
160
 
161
161
  1. Call `TeamCreate(team_name: "okstra-<task-key>", description: "Lead-plus-worker okstra run for <task-key>")`.
162
162
  2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback.
163
- 3. If `TeamCreate` succeeds, proceed to Phase 4 (dispatch with `team_name`).
164
- 4. If `TeamCreate` fails (tool unavailable, permission denied, environment lacks Agent Teams support), proceed to Phase 5 fallback (dispatch with `run_in_background: true` and no `team_name`).
163
+ 3. Verify `team-state.lead.sessionId` is populated. The `okstra.sh` exec path fills it automatically (`generate_claude_session_id` → `claude --session-id ...`). The render-only / in-session takeover path (`okstra-run` skill) auto-detects the live session's jsonl via `resolve_inproc_lead_session_id`, but the detector is best-effort and may return empty if `~/.claude/projects/<encoded-cwd>/` is unreadable or has no jsonl yet. If `lead.sessionId` is empty at this point, write the running session's id into team-state before proceeding — Phase 7 token-usage collection depends on it and will fail with `lead jsonl not found (sessionId=)` otherwise.
164
+ 4. If `TeamCreate` succeeds, proceed to Phase 4 (dispatch with `team_name`).
165
+ 5. If `TeamCreate` fails (tool unavailable, permission denied, environment lacks Agent Teams support), proceed to Phase 5 fallback (dispatch with `run_in_background: true` and no `team_name`).
165
166
 
166
167
  Use agent and subagent names that map cleanly to the selected worker roles. Do not create ambiguous role names that differ from `Claude worker`, `Codex worker`, `Gemini worker`, or `Report writer worker`.
167
168
 
@@ -14,6 +14,32 @@ Invoke the `okstra` skill now. Read the manifests below for all task metadata, p
14
14
  - Phase advancement requires a new okstra invocation launched with `--task-type {{WORKFLOW_NEXT_RECOMMENDED_PHASE}}` after this run's final report is written and approved. The lead must not write source code, run builds/migrations/deployments, or otherwise produce artifacts of a different phase from inside this run.
15
15
  - See `Lifecycle Phase Boundaries` in the okstra skill (`agents/SKILL.md`) for the canonical rules and the phase-transition checklist.
16
16
 
17
+ ## Team Creation Gate (BLOCKING)
18
+
19
+ Before any `Agent` dispatch for workers, you MUST perform Phase 3 of the
20
+ `okstra` skill (`agents/SKILL.md` → "Phase 3 — Team creation"). Skipping
21
+ this gate silently degrades the run to in-process background dispatch and
22
+ loses the Teams split-pane observability surface, even though worker
23
+ outputs may still appear correct on disk.
24
+
25
+ Required actions, in order, regardless of how many workers are selected
26
+ for this run (roster comes from `resultContract.requiredWorkerRoles` in
27
+ `task-manifest.json` — it may be 1, 2, 3, or more workers):
28
+
29
+ 1. Invoke the `okstra-team-contract` skill and verify the selected worker
30
+ roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.
31
+ 2. Call `TeamCreate(team_name: "okstra-{{TASK_KEY}}", description: ...)`.
32
+ 3. Record the outcome in team-state under
33
+ `teamCreate: { attempted: true, status: "ok" | "error", error?: <msg> }`
34
+ BEFORE any `Agent(...)` worker dispatch.
35
+ 4. Only after `teamCreate` is persisted may you dispatch workers — with
36
+ `team_name` on success, or with `run_in_background: true` and no
37
+ `team_name` ONLY when `teamCreate.status == "error"` was recorded.
38
+
39
+ If the Agent tool rejects a dispatch with `"team must be created first"` /
40
+ `"team을 먼저 생성하거나 team_name 없이 호출해야 합니다"`, the correct
41
+ response is to go back to step 2 — NOT to strip `team_name` and retry.
42
+
17
43
  ## Project Root
18
44
 
19
45
  - Absolute project root: `{{PROJECT_ROOT}}`
@@ -51,7 +51,11 @@ from .seeding import (
51
51
  render_runtime_settings_file,
52
52
  verify_installation,
53
53
  )
54
- from .session import generate_claude_session_id, write_claude_resume_command_file
54
+ from .session import (
55
+ generate_claude_session_id,
56
+ resolve_inproc_lead_session_id,
57
+ write_claude_resume_command_file,
58
+ )
55
59
  from .workers import normalize_workers, resolve_profile_workers
56
60
  from .workflow import compute_workflow_state
57
61
  from .worktree import provision_implementation_worktree
@@ -537,7 +541,16 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
537
541
  "EXECUTOR_WORKTREE_NOTE": worktree.note,
538
542
  })
539
543
 
540
- claude_session_id = "" if inp.render_only else generate_claude_session_id()
544
+ if inp.render_only:
545
+ # render-only entry path (e.g. okstra-run skill, in-session takeover):
546
+ # the calling Claude session itself becomes the lead, so we must NOT
547
+ # mint a fresh UUID — instead, best-effort detect the live session's
548
+ # jsonl under ~/.claude/projects/<encoded-cwd>/. Leaving this blank
549
+ # caused Phase 7 token-usage collection to fail (lead jsonl not
550
+ # found) and the validator to report `sessionId=`.
551
+ claude_session_id = resolve_inproc_lead_session_id(project_root)
552
+ else:
553
+ claude_session_id = generate_claude_session_id()
541
554
 
542
555
  # ---- material + related-tasks ----
543
556
  profile_content = _expand_profile_includes(profile_file)
@@ -15,6 +15,39 @@ def generate_claude_session_id() -> str:
15
15
  return str(uuid.uuid4())
16
16
 
17
17
 
18
+ def _claude_projects_dir_for(cwd: Path) -> Path:
19
+ """Mirror of `okstra_token_usage.paths.claude_project_dir` — kept local to
20
+ avoid a cross-package import inside the run path.
21
+ """
22
+ encoded = "-" + str(cwd).strip("/").replace("/", "-")
23
+ return Path.home() / ".claude" / "projects" / encoded
24
+
25
+
26
+ def resolve_inproc_lead_session_id(project_root: Path) -> str:
27
+ """Best-effort detection of the running Claude session's id when okstra is
28
+ invoked render-only from inside a live session (the `okstra-run` skill
29
+ path). The current session's jsonl is being actively written to under
30
+ `~/.claude/projects/<encoded-cwd>/`, so the most recently modified file
31
+ in that directory is, with very high probability, the calling session.
32
+
33
+ Returns the UUID stem on success, empty string on failure (directory
34
+ missing, no jsonl files, permission error). Callers must treat this as
35
+ best-effort — a failure does not invalidate the session, it just means
36
+ auto-detection could not confirm one.
37
+ """
38
+ try:
39
+ d = _claude_projects_dir_for(project_root)
40
+ if not d.exists():
41
+ return ""
42
+ candidates = [p for p in d.iterdir() if p.suffix == ".jsonl"]
43
+ if not candidates:
44
+ return ""
45
+ newest = max(candidates, key=lambda p: p.stat().st_mtime)
46
+ return newest.stem
47
+ except OSError:
48
+ return ""
49
+
50
+
18
51
  def write_claude_resume_command_file(
19
52
  *, resume_command_path: Path, project_root: Path, claude_session_id: str,
20
53
  ) -> None:
@@ -14,13 +14,18 @@ description: Use when the user asks to list past okstra runs, check execution hi
14
14
  ## Step 0: Verify okstra runtime + project setup
15
15
 
16
16
  ```bash
17
- npx -y okstra@latest ensure-installed >/dev/null 2>&1 || {
17
+ if command -v okstra >/dev/null 2>&1; then
18
+ OKSTRA_CMD="okstra"
19
+ else
20
+ OKSTRA_CMD="npx -y okstra@latest"
21
+ fi
22
+ $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
18
23
  echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
19
24
  exit 1
20
25
  }
21
- eval "$(npx -y okstra@latest paths --shell)"
26
+ eval "$($OKSTRA_CMD paths --shell)"
22
27
  export PYTHONPATH="$OKSTRA_PYTHONPATH"
23
- OKSTRA_PROJECT_INFO="$(npx -y okstra@latest check-project --json)" || {
28
+ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
24
29
  echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
25
30
  echo "$OKSTRA_PROJECT_INFO" >&2
26
31
  exit 1
@@ -15,13 +15,18 @@ user-invocable: false
15
15
  ## Step 0: Verify okstra runtime + project setup
16
16
 
17
17
  ```bash
18
- npx -y okstra@latest ensure-installed >/dev/null 2>&1 || {
18
+ if command -v okstra >/dev/null 2>&1; then
19
+ OKSTRA_CMD="okstra"
20
+ else
21
+ OKSTRA_CMD="npx -y okstra@latest"
22
+ fi
23
+ $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
19
24
  echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
20
25
  exit 1
21
26
  }
22
- eval "$(npx -y okstra@latest paths --shell)"
27
+ eval "$($OKSTRA_CMD paths --shell)"
23
28
  export PYTHONPATH="$OKSTRA_PYTHONPATH"
24
- OKSTRA_PROJECT_INFO="$(npx -y okstra@latest check-project --json)" || {
29
+ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
25
30
  echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
26
31
  echo "$OKSTRA_PROJECT_INFO" >&2
27
32
  exit 1
@@ -36,18 +36,26 @@ Do NOT hard-code or guess any okstra path. Every run loads them fresh from
36
36
  the single authority — `okstra`:
37
37
 
38
38
  ```bash
39
+ # 0) Resolve runner: prefer PATH (npm-installed) over npx (avoids per-call registry lookup).
40
+ # If the user installed okstra via npm, they control upgrade timing — do not force @latest.
41
+ if command -v okstra >/dev/null 2>&1; then
42
+ OKSTRA_CMD="okstra"
43
+ else
44
+ OKSTRA_CMD="npx -y okstra@latest"
45
+ fi
46
+
39
47
  # 1) Ensure runtime is fresh (idempotent, cached when up-to-date)
40
- npx -y okstra@latest ensure-installed >/dev/null 2>&1 || {
48
+ $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
41
49
  echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
42
50
  exit 1
43
51
  }
44
52
 
45
53
  # 2) Load all runtime paths into the shell as OKSTRA_* exports
46
- eval "$(npx -y okstra@latest paths --shell)"
54
+ eval "$($OKSTRA_CMD paths --shell)"
47
55
  export PYTHONPATH="$OKSTRA_PYTHONPATH"
48
56
 
49
57
  # 3) Verify the current project has okstra metadata (project.json + projectId)
50
- OKSTRA_PROJECT_INFO="$(npx -y okstra@latest check-project --json)" || {
58
+ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
51
59
  echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
52
60
  echo "$OKSTRA_PROJECT_INFO" >&2
53
61
  exit 1
@@ -40,13 +40,18 @@ If `--title` is omitted, derive a default title from `task-group` (e.g. `uploadF
40
40
  Run before anything else in this skill:
41
41
 
42
42
  ```bash
43
- npx -y okstra@latest ensure-installed >/dev/null 2>&1 || {
43
+ if command -v okstra >/dev/null 2>&1; then
44
+ OKSTRA_CMD="okstra"
45
+ else
46
+ OKSTRA_CMD="npx -y okstra@latest"
47
+ fi
48
+ $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
44
49
  echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
45
50
  exit 1
46
51
  }
47
- eval "$(npx -y okstra@latest paths --shell)"
52
+ eval "$($OKSTRA_CMD paths --shell)"
48
53
  export PYTHONPATH="$OKSTRA_PYTHONPATH"
49
- OKSTRA_PROJECT_INFO="$(npx -y okstra@latest check-project --json)" || {
54
+ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
50
55
  echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
51
56
  echo "$OKSTRA_PROJECT_INFO" >&2
52
57
  exit 1
@@ -55,7 +55,13 @@ running the legacy `okstra-install.sh` — that path is dev-only.
55
55
  ## Step 2: Load runtime paths
56
56
 
57
57
  ```bash
58
- eval "$(npx -y okstra@latest paths --shell)"
58
+ # Prefer PATH-resolved okstra (npm-installed) over npx — avoids per-call registry lookup.
59
+ if command -v okstra >/dev/null 2>&1; then
60
+ OKSTRA_CMD="okstra"
61
+ else
62
+ OKSTRA_CMD="npx -y okstra@latest"
63
+ fi
64
+ eval "$($OKSTRA_CMD paths --shell)"
59
65
  export PYTHONPATH="$OKSTRA_PYTHONPATH"
60
66
  ```
61
67
 
@@ -113,7 +119,7 @@ PY
113
119
  ## Step 5: Verify
114
120
 
115
121
  ```bash
116
- npx -y okstra@latest doctor
122
+ $OKSTRA_CMD doctor
117
123
  ```
118
124
 
119
125
  If all checks return `OK`, the setup is complete. If any check fails, surface
@@ -17,13 +17,18 @@ Before any other step, ensure both the okstra runtime and the current
17
17
  project's okstra metadata are in place:
18
18
 
19
19
  ```bash
20
- npx -y okstra@latest ensure-installed >/dev/null 2>&1 || {
20
+ if command -v okstra >/dev/null 2>&1; then
21
+ OKSTRA_CMD="okstra"
22
+ else
23
+ OKSTRA_CMD="npx -y okstra@latest"
24
+ fi
25
+ $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
21
26
  echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
22
27
  exit 1
23
28
  }
24
- eval "$(npx -y okstra@latest paths --shell)"
29
+ eval "$($OKSTRA_CMD paths --shell)"
25
30
  export PYTHONPATH="$OKSTRA_PYTHONPATH"
26
- OKSTRA_PROJECT_INFO="$(npx -y okstra@latest check-project --json)" || {
31
+ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
27
32
  echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
28
33
  echo "$OKSTRA_PROJECT_INFO" >&2
29
34
  exit 1
@@ -30,13 +30,18 @@ If a run never reached Phase 7, its `team-state` will not have `durationMs` fill
30
30
  ## Step 0: Verify okstra runtime + project setup
31
31
 
32
32
  ```bash
33
- npx -y okstra@latest ensure-installed >/dev/null 2>&1 || {
33
+ if command -v okstra >/dev/null 2>&1; then
34
+ OKSTRA_CMD="okstra"
35
+ else
36
+ OKSTRA_CMD="npx -y okstra@latest"
37
+ fi
38
+ $OKSTRA_CMD ensure-installed >/dev/null 2>&1 || {
34
39
  echo "FAIL: okstra not installed; tell the user to run: npx okstra@latest install" >&2
35
40
  exit 1
36
41
  }
37
- eval "$(npx -y okstra@latest paths --shell)"
42
+ eval "$($OKSTRA_CMD paths --shell)"
38
43
  export PYTHONPATH="$OKSTRA_PYTHONPATH"
39
- OKSTRA_PROJECT_INFO="$(npx -y okstra@latest check-project --json)" || {
44
+ OKSTRA_PROJECT_INFO="$($OKSTRA_CMD check-project --json)" || {
40
45
  echo "FAIL: this project has no okstra setup. Tell the user to run /okstra-setup first." >&2
41
46
  echo "$OKSTRA_PROJECT_INFO" >&2
42
47
  exit 1
@@ -324,6 +324,28 @@ def validate_team_state(
324
324
  failures.append("team-state.workers must be a list")
325
325
  return
326
326
 
327
+ dispatched_statuses = {"completed", "timeout", "error", "in-progress"}
328
+ any_dispatched = any(
329
+ isinstance(w, dict) and str(w.get("status", "")).strip() in dispatched_statuses
330
+ for w in workers
331
+ )
332
+ if any_dispatched:
333
+ team_create = team_state.get("teamCreate")
334
+ if not isinstance(team_create, dict) or not team_create.get("attempted"):
335
+ failures.append(
336
+ "team-state.teamCreate.attempted must be true once any worker has "
337
+ "been dispatched (status in completed/timeout/error/in-progress). "
338
+ "Phase 3 (TeamCreate) was skipped — workers ran in-process without "
339
+ "the Teams split-pane surface. See agents/SKILL.md Phase 3."
340
+ )
341
+ else:
342
+ tc_status = str(team_create.get("status", "")).strip()
343
+ if tc_status not in {"ok", "error"}:
344
+ failures.append(
345
+ "team-state.teamCreate.status must be `ok` or `error` once "
346
+ f"workers have been dispatched (found: `{tc_status}`)."
347
+ )
348
+
327
349
  by_role: dict[str, dict] = {}
328
350
  for worker in workers:
329
351
  if not isinstance(worker, dict):