okstra 0.82.1 → 0.84.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.
Files changed (77) hide show
  1. package/README.kr.md +7 -6
  2. package/README.md +6 -6
  3. package/docs/kr/architecture.md +8 -7
  4. package/docs/kr/cli.md +2 -2
  5. package/docs/kr/performance-improvement-plan-v2.md +14 -14
  6. package/docs/project-structure-overview.md +7 -8
  7. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +504 -0
  8. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +342 -0
  9. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +258 -0
  10. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +387 -0
  11. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +749 -0
  12. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +828 -0
  13. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +490 -0
  14. package/docs/task-process/README.md +1 -1
  15. package/docs/task-process/error-analysis.md +1 -1
  16. package/docs/task-process/final-verification.md +1 -1
  17. package/docs/task-process/implementation-planning.md +1 -1
  18. package/docs/task-process/implementation.md +1 -1
  19. package/docs/task-process/release-handoff.md +1 -1
  20. package/docs/task-process/requirements-discovery.md +2 -2
  21. package/package.json +1 -1
  22. package/runtime/BUILD.json +2 -2
  23. package/runtime/agents/TODO.md +2 -0
  24. package/runtime/agents/workers/claude-worker.md +8 -8
  25. package/runtime/agents/workers/codex-worker.md +8 -8
  26. package/runtime/agents/workers/gemini-worker.md +8 -8
  27. package/runtime/agents/workers/report-writer-worker.md +2 -2
  28. package/runtime/bin/lib/okstra/globals.sh +0 -1
  29. package/runtime/bin/okstra-wrapper-status.py +1 -1
  30. package/runtime/prompts/launch.template.md +5 -3
  31. package/runtime/{skills/okstra-context-loader/SKILL.md → prompts/lead/context-loader.md} +7 -14
  32. package/runtime/{skills/okstra-convergence/SKILL.md → prompts/lead/convergence.md} +12 -19
  33. package/runtime/{agents/SKILL.md → prompts/lead/okstra-lead-contract.md} +53 -59
  34. package/runtime/{skills/okstra-report-writer/SKILL.md → prompts/lead/report-writer.md} +12 -19
  35. package/runtime/{skills/okstra-team-contract/SKILL.md → prompts/lead/team-contract.md} +13 -19
  36. package/runtime/{skills → prompts}/okstra-coding-preflight/languages/python.md +2 -2
  37. package/runtime/{skills → prompts}/okstra-coding-preflight/languages/rust.md +1 -1
  38. package/runtime/{skills/okstra-coding-preflight/SKILL.md → prompts/okstra-coding-preflight/overview.md} +27 -38
  39. package/runtime/prompts/okstra-coding-preflight/scripts/preedit-check.sh +79 -0
  40. package/runtime/prompts/profiles/_coding-conventions-preflight.md +2 -2
  41. package/runtime/prompts/profiles/_common-contract.md +2 -2
  42. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  43. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  44. package/runtime/prompts/profiles/error-analysis.md +2 -2
  45. package/runtime/prompts/profiles/final-verification.md +1 -1
  46. package/runtime/prompts/profiles/implementation-planning.md +4 -4
  47. package/runtime/prompts/profiles/requirements-discovery.md +2 -2
  48. package/runtime/python/okstra_ctl/codex_dispatch.py +12 -61
  49. package/runtime/python/okstra_ctl/context_cost.py +14 -11
  50. package/runtime/python/okstra_ctl/dispatch_core.py +36 -13
  51. package/runtime/python/okstra_ctl/paths.py +27 -1
  52. package/runtime/python/okstra_ctl/render.py +62 -8
  53. package/runtime/python/okstra_ctl/run.py +8 -6
  54. package/runtime/python/okstra_ctl/worker_prompt_headers.py +126 -0
  55. package/runtime/python/okstra_token_usage/claude.py +1 -1
  56. package/runtime/python/okstra_token_usage/collect.py +1 -1
  57. package/runtime/skills/okstra-brief/SKILL.md +65 -11
  58. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  59. package/runtime/skills/okstra-schedule/SKILL.md +4 -4
  60. package/runtime/skills/okstra-setup/SKILL.md +2 -2
  61. package/runtime/templates/reports/settings.template.json +14 -123
  62. package/runtime/templates/reports/task-brief.template.md +2 -2
  63. package/runtime/templates/worker-prompt-preamble.md +2 -2
  64. package/runtime/validators/lib/validate-assets.sh +12 -4
  65. package/runtime/validators/validate-brief.py +99 -16
  66. package/runtime/validators/validate-run.py +3 -3
  67. package/runtime/validators/validate_session_conformance.py +11 -11
  68. package/src/install.mjs +95 -81
  69. package/src/skill-catalog.mjs +35 -0
  70. package/src/uninstall.mjs +5 -0
  71. /package/runtime/{skills/okstra-coding-preflight/architecture → prompts/okstra-coding-preflight/architectures}/hexagonal.md +0 -0
  72. /package/runtime/{skills → prompts}/okstra-coding-preflight/clean-code.md +0 -0
  73. /package/runtime/{skills/okstra-coding-preflight/languages/nodejs.md → prompts/okstra-coding-preflight/frameworks/node-server.md} +0 -0
  74. /package/runtime/{skills → prompts}/okstra-coding-preflight/languages/java.md +0 -0
  75. /package/runtime/{skills → prompts}/okstra-coding-preflight/languages/javascript-typescript.md +0 -0
  76. /package/runtime/{skills → prompts}/okstra-coding-preflight/languages/kotlin.md +0 -0
  77. /package/runtime/{skills → prompts}/okstra-coding-preflight/languages/sql.md +0 -0
@@ -33,9 +33,15 @@ __all__ = [
33
33
  "next_run_seq",
34
34
  "task_dir",
35
35
  "task_runs_dir",
36
+ "okstra_home",
36
37
  ]
37
38
 
38
39
 
40
+ def okstra_home() -> Path:
41
+ raw = os.environ.get("OKSTRA_HOME", "").strip()
42
+ return Path(raw).expanduser() if raw else Path.home() / ".okstra"
43
+
44
+
39
45
  def task_dir(project_root: Path, task_group: str, task_id: str) -> Path:
40
46
  """task root 경로: ``<project>/.okstra/tasks/<group-seg>/<id-seg>``.
41
47
 
@@ -188,6 +194,8 @@ def compute_run_paths(
188
194
  report_writer_worker_result = worker_results / f"report-writer-worker{suffixes['worker_results']}.md"
189
195
 
190
196
  run_errors_log = run_logs / f"errors-{task_type_segment}-{seqs['state']}.jsonl"
197
+ runtime_home = okstra_home()
198
+
191
199
  claude_worker_errors_sidecar = worker_results / f"claude-worker-errors{suffixes['worker_results']}.json"
192
200
  codex_worker_errors_sidecar = worker_results / f"codex-worker-errors{suffixes['worker_results']}.json"
193
201
  gemini_worker_errors_sidecar = worker_results / f"gemini-worker-errors{suffixes['worker_results']}.json"
@@ -209,7 +217,25 @@ def compute_run_paths(
209
217
  "OKSTRA_ROOT": str(okstra_root),
210
218
  "OKSTRA_TASKS_ROOT": str(tasks_root),
211
219
  "WORKER_PROMPT_PREAMBLE_PATH": str(
212
- Path.home() / ".okstra" / "templates" / "worker-prompt-preamble.md"
220
+ runtime_home / "templates" / "worker-prompt-preamble.md"
221
+ ),
222
+ "OKSTRA_LEAD_CONTRACT_PATH": str(
223
+ runtime_home / "prompts" / "lead" / "okstra-lead-contract.md"
224
+ ),
225
+ "OKSTRA_CONTEXT_LOADER_PATH": str(
226
+ runtime_home / "prompts" / "lead" / "context-loader.md"
227
+ ),
228
+ "OKSTRA_TEAM_CONTRACT_PATH": str(
229
+ runtime_home / "prompts" / "lead" / "team-contract.md"
230
+ ),
231
+ "OKSTRA_CONVERGENCE_PATH": str(
232
+ runtime_home / "prompts" / "lead" / "convergence.md"
233
+ ),
234
+ "OKSTRA_REPORT_WRITER_PATH": str(
235
+ runtime_home / "prompts" / "lead" / "report-writer.md"
236
+ ),
237
+ "OKSTRA_CODING_PREFLIGHT_DIR": str(
238
+ runtime_home / "prompts" / "coding-preflight"
213
239
  ),
214
240
  "OKSTRA_DISCOVERY_DIR": str(discovery_dir),
215
241
  "TASK_ROOT": str(task_root),
@@ -28,6 +28,7 @@ from okstra_project.dirs import OKSTRA_DIR_NAME, project_json_path
28
28
  # render_task_manifest 가 동일한 리스트/딕셔너리를 로컬에 중복 정의했는데,
29
29
  # 이는 silent drift 위험이 있어 SSOT import 로 통합한다.
30
30
  from . import fix_cycles
31
+ from .paths import okstra_home
31
32
  from .lead_runtime import lead_runtime_info
32
33
  from .workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE
33
34
 
@@ -369,6 +370,14 @@ def _active_error_logs(ctx: dict) -> dict:
369
370
  }
370
371
 
371
372
 
373
+ def _active_runtime_resources(ctx: dict) -> dict:
374
+ runtime_home = okstra_home()
375
+ return {
376
+ "codingPreflightDir": ctx.get("OKSTRA_CODING_PREFLIGHT_DIR")
377
+ or str(runtime_home / "prompts" / "coding-preflight"),
378
+ }
379
+
380
+
372
381
  def _active_executor_worktree(ctx: dict) -> dict:
373
382
  return {
374
383
  "status": ctx.get("EXECUTOR_WORKTREE_STATUS", ""),
@@ -419,6 +428,7 @@ def render_active_run_context(active_context_path: str, ctx: dict) -> None:
419
428
  "instructionSet": _active_instruction_set(ctx),
420
429
  "workers": _active_workers(ctx),
421
430
  "errorLogs": _active_error_logs(ctx),
431
+ "runtimeResources": _active_runtime_resources(ctx),
422
432
  "executorWorktree": _active_executor_worktree(ctx),
423
433
  "sourceArtifacts": _active_source_artifacts(ctx),
424
434
  "lazyReadPlan": _active_lazy_read_plan(),
@@ -1104,7 +1114,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1104
1114
  def _build_convergence_block(ctx: dict) -> dict:
1105
1115
  """Resolve the `convergence` sub-tree written into task-manifest.json.
1106
1116
 
1107
- Defaults follow `skills/okstra-convergence/SKILL.md`:
1117
+ Defaults follow `prompts/lead/convergence.md`:
1108
1118
  - `enabled` default True
1109
1119
  - `maxRounds` default 1 for `requirements-discovery`, 2 otherwise
1110
1120
  - `verificationMode` default "lightweight"
@@ -1667,6 +1677,35 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1667
1677
  lead_agent_label = _lead_agent_label(ctx)
1668
1678
  lead_model = ctx.get("LEAD_MODEL", "")
1669
1679
  lead_model_execution = ctx.get("LEAD_MODEL_EXECUTION_VALUE", "")
1680
+ home_prompts = okstra_home() / "prompts"
1681
+ lead_contract_path = ctx.get("OKSTRA_LEAD_CONTRACT_PATH") or str(
1682
+ home_prompts / "lead" / "okstra-lead-contract.md"
1683
+ )
1684
+ context_loader_path = ctx.get("OKSTRA_CONTEXT_LOADER_PATH") or str(
1685
+ home_prompts / "lead" / "context-loader.md"
1686
+ )
1687
+ team_contract_path = ctx.get("OKSTRA_TEAM_CONTRACT_PATH") or str(
1688
+ home_prompts / "lead" / "team-contract.md"
1689
+ )
1690
+ convergence_path = ctx.get("OKSTRA_CONVERGENCE_PATH") or str(
1691
+ home_prompts / "lead" / "convergence.md"
1692
+ )
1693
+ report_writer_path = ctx.get("OKSTRA_REPORT_WRITER_PATH") or str(
1694
+ home_prompts / "lead" / "report-writer.md"
1695
+ )
1696
+ coding_preflight_dir = ctx.get("OKSTRA_CODING_PREFLIGHT_DIR") or str(
1697
+ home_prompts / "coding-preflight"
1698
+ )
1699
+ okstra_runtime_resources_block = (
1700
+ "## Okstra Runtime Resources\n"
1701
+ "\n"
1702
+ f"- Lead contract: `{lead_contract_path}`\n"
1703
+ f"- Context loader: `{context_loader_path}`\n"
1704
+ f"- Team contract: `{team_contract_path}`\n"
1705
+ f"- Convergence contract: `{convergence_path}`\n"
1706
+ f"- Report writer contract: `{report_writer_path}`\n"
1707
+ f"- Coding preflight pack: `{coding_preflight_dir}`"
1708
+ )
1670
1709
 
1671
1710
  def fmt_assignment(role: str, model: str, execution: str) -> str:
1672
1711
  if execution and execution != model:
@@ -1852,7 +1891,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1852
1891
  "## Team Creation Gate (BLOCKING)\n"
1853
1892
  "\n"
1854
1893
  "Before any `Agent` dispatch for workers, you MUST perform Phase 3 of the\n"
1855
- '`okstra` skill (`agents/SKILL.md` "Phase 3 Team creation"). Skipping\n'
1894
+ f'okstra lead contract (`{lead_contract_path}` \u2192 "Phase 3 \u2014 Team creation"). Skipping\n'
1856
1895
  "this gate silently degrades the run to in-process background dispatch and\n"
1857
1896
  "loses the Teams split-pane observability surface, even though worker\n"
1858
1897
  "outputs may still appear correct on disk.\n"
@@ -1861,7 +1900,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1861
1900
  "for this run (roster comes from `resultContract.requiredWorkerRoles` in\n"
1862
1901
  "`task-manifest.json` — it may be 1, 2, 3, or more workers):\n"
1863
1902
  "\n"
1864
- "1. Invoke the `okstra-team-contract` skill and verify the selected worker\n"
1903
+ f"1. Read the team-contract resource (`{team_contract_path}`) and verify the selected worker\n"
1865
1904
  " roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.\n"
1866
1905
  f'2. Call `TeamCreate(team_name: "{team_name}", description: ...)`.\n'
1867
1906
  "3. Record the outcome in team-state under\n"
@@ -1943,8 +1982,10 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1943
1982
  else:
1944
1983
  lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
1945
1984
  lead_bootstrap_instruction = (
1946
- "Invoke the `okstra` skill now. Read the manifests below for all task "
1947
- "metadata, paths, model assignments, and worker roster."
1985
+ f"Read the Okstra lead contract at `{lead_contract_path}` now, then read "
1986
+ "the manifests below for all task metadata, paths, model assignments, and "
1987
+ "worker roster. Follow the contract's lazy-read rules for support resources; "
1988
+ "do not invoke hidden/internal skills."
1948
1989
  )
1949
1990
  lead_session_block = (
1950
1991
  "## Session\n"
@@ -1957,9 +1998,9 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1957
1998
  "\n"
1958
1999
  "- 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.\n"
1959
2000
  "- 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.\n"
1960
- "- 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.\n"
2001
+ "- Before dispatching any required worker, **you (the lead) construct the worker prompt and persist it to the assigned absolute path using `Write`** (per `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.\n"
1961
2002
  "- 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.\n"
1962
- f"- **Worker Preamble Path (single anchor — replaces inlined `[Required reading]` and `[Error reporting]` blocks).** Every worker prompt you construct MUST include the anchor header `**Worker Preamble Path:** {ctx.get('WORKER_PROMPT_PREAMBLE_PATH', str(Path.home() / '.okstra' / 'templates' / 'worker-prompt-preamble.md'))}`. The file at that path is the canonical SSOT for Required Reading + Error Reporting + Output sections; workers Read it end-to-end before producing output. Do NOT re-inline those blocks into worker prompt bodies — that is the legacy ~80-line dispatch boilerplate that this anchor is designed to replace."
2003
+ f"- **Worker Preamble Path (single anchor — replaces inlined `[Required reading]` and `[Error reporting]` blocks).** Every worker prompt you construct MUST include the anchor header `**Worker Preamble Path:** {ctx.get('WORKER_PROMPT_PREAMBLE_PATH', str(okstra_home() / 'templates' / 'worker-prompt-preamble.md'))}`. The file at that path is the canonical SSOT for Required Reading + Error Reporting + Output sections; workers Read it end-to-end before producing output. Do NOT re-inline those blocks into worker prompt bodies — that is the legacy ~80-line dispatch boilerplate that this anchor is designed to replace."
1963
2004
  )
1964
2005
 
1965
2006
  # Compute results (deterministic from ctx, 덮어쓰기)
@@ -1968,6 +2009,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1968
2009
  ctx["LEAD_SESSION_BLOCK"] = lead_session_block
1969
2010
  ctx["TEAM_CREATION_GATE"] = team_creation_gate_block
1970
2011
  ctx["WORKER_DISPATCH_GUIDANCE"] = worker_dispatch_guidance
2012
+ ctx["OKSTRA_RUNTIME_RESOURCES"] = okstra_runtime_resources_block
1971
2013
  ctx["WORKER_RESULT_PATH_LINES"] = "\n".join(worker_result_lines)
1972
2014
  ctx["MODEL_ASSIGNMENT_LINES"] = "\n".join(model_assignment_lines)
1973
2015
  ctx["TEAM_ROLE_LINES"] = "\n".join(team_role_lines)
@@ -2001,9 +2043,21 @@ def apply_lead_prompt_defaults(ctx: dict) -> None:
2001
2043
  # Empty for non-final-verification runs; the final-verification prepare
2002
2044
  # path overwrites it with the resolved verification target block.
2003
2045
  ctx.setdefault("VERIFICATION_TARGET", "")
2046
+ runtime_home = okstra_home()
2004
2047
  ctx.setdefault(
2005
2048
  "WORKER_PROMPT_PREAMBLE_PATH",
2006
- str(Path.home() / ".okstra" / "templates" / "worker-prompt-preamble.md"),
2049
+ str(runtime_home / "templates" / "worker-prompt-preamble.md"),
2050
+ )
2051
+ # Lead resource paths the launch template references directly. paths.py
2052
+ # seeds the production values; setdefault backfills callers that render the
2053
+ # template without the full path ctx (mirrors WORKER_PROMPT_PREAMBLE_PATH).
2054
+ ctx.setdefault(
2055
+ "OKSTRA_LEAD_CONTRACT_PATH",
2056
+ str(runtime_home / "prompts" / "lead" / "okstra-lead-contract.md"),
2057
+ )
2058
+ ctx.setdefault(
2059
+ "OKSTRA_TEAM_CONTRACT_PATH",
2060
+ str(runtime_home / "prompts" / "lead" / "team-contract.md"),
2007
2061
  )
2008
2062
  if "AVAILABLE_MCP_SERVERS" not in ctx:
2009
2063
  ctx["AVAILABLE_MCP_SERVERS"] = build_available_mcp_servers_block(
@@ -79,6 +79,7 @@ from .session import (
79
79
  from .pr_template import PrTemplateError, resolve_pr_template_path
80
80
  from .workers import (
81
81
  normalize_workers,
82
+ resolve_optional_workers,
82
83
  resolve_profile_workers,
83
84
  validate_workers_against_profile,
84
85
  )
@@ -1067,7 +1068,7 @@ class _ResolvedAssets:
1067
1068
  prompt_template: Path
1068
1069
  task_index_template: Path
1069
1070
  final_report_template: Path
1070
- source_skill: Path
1071
+ lead_contract: Path
1071
1072
  run_validator: Path
1072
1073
 
1073
1074
 
@@ -1090,19 +1091,19 @@ def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _Resolv
1090
1091
  final_report_template = (
1091
1092
  workspace_root / "templates" / "reports" / "final-report.template.md"
1092
1093
  )
1093
- source_skill = workspace_root / "agents" / "SKILL.md"
1094
+ lead_contract = workspace_root / "prompts" / "lead" / "okstra-lead-contract.md"
1094
1095
  run_validator = workspace_root / "validators" / "validate-run.py"
1095
- for required in (task_index_template, final_report_template, run_validator, source_skill):
1096
+ for required in (task_index_template, final_report_template, run_validator, lead_contract):
1096
1097
  if not required.is_file():
1097
1098
  raise PrepareError(
1098
- f"required okstra template or source skill missing: {required}.{_INSTALL_HINT}"
1099
+ f"required okstra template or lead contract missing: {required}.{_INSTALL_HINT}"
1099
1100
  )
1100
1101
  return _ResolvedAssets(
1101
1102
  profile_file=profile_file,
1102
1103
  prompt_template=prompt_template,
1103
1104
  task_index_template=task_index_template,
1104
1105
  final_report_template=final_report_template,
1105
- source_skill=source_skill,
1106
+ lead_contract=lead_contract,
1106
1107
  run_validator=run_validator,
1107
1108
  )
1108
1109
 
@@ -1402,10 +1403,11 @@ def _resolve_roster(inp: PrepareInputs, profile_file: Path) -> tuple[list[str],
1402
1403
  if inp.task_type == "release-handoff":
1403
1404
  return [], ""
1404
1405
  profile_workers = resolve_profile_workers(profile_file)
1406
+ optional_workers = resolve_optional_workers(profile_file)
1405
1407
  profile_workers_csv = ",".join(profile_workers)
1406
1408
  workers = normalize_workers(inp.workers_override or profile_workers_csv)
1407
1409
  if inp.workers_override.strip():
1408
- validate_workers_against_profile(workers, profile_workers)
1410
+ validate_workers_against_profile(workers, profile_workers, optional_workers)
1409
1411
  if not workers:
1410
1412
  raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
1411
1413
  return workers, ",".join(workers)
@@ -0,0 +1,126 @@
1
+ """Shared worker prompt anchor header rendering."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any, Mapping
6
+
7
+ from .paths import okstra_home
8
+
9
+
10
+ class WorkerPromptHeaderError(Exception):
11
+ """Raised when worker prompt anchor headers cannot be rendered."""
12
+
13
+
14
+ def worker_prompt_headers(
15
+ *,
16
+ project_root: Path,
17
+ prompt_rel: str,
18
+ result_rel: str,
19
+ worker_id: str,
20
+ manifest: Mapping[str, Any],
21
+ active_context: Mapping[str, Any],
22
+ ) -> list[str]:
23
+ """Render the team-contract worker prompt anchor headers."""
24
+ prompt_path = _resolve_project_path(project_root, prompt_rel)
25
+ return [
26
+ f"**Project Root:** {project_root}",
27
+ f"**Prompt History Path:** {prompt_rel}",
28
+ f"**Result Path:** {result_rel}",
29
+ f"Assigned worker prompt history path: {prompt_path}",
30
+ f"**Worker Preamble Path:** {_worker_preamble_path()}",
31
+ f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}",
32
+ f"**Errors log path:** {_errors_log_path(project_root, manifest, active_context)}",
33
+ (
34
+ f"**Errors sidecar path:** "
35
+ f"{_worker_errors_sidecar_path(project_root, manifest, active_context, worker_id)}"
36
+ ),
37
+ ]
38
+
39
+
40
+ def _worker_preamble_path() -> Path:
41
+ return okstra_home() / "templates" / "worker-prompt-preamble.md"
42
+
43
+
44
+ def _coding_preflight_pack_path(active_context: Mapping[str, Any]) -> Path:
45
+ runtime_resources = active_context.get("runtimeResources")
46
+ if isinstance(runtime_resources, Mapping):
47
+ value = _string_value(runtime_resources.get("codingPreflightDir"))
48
+ if value:
49
+ return Path(value)
50
+ return okstra_home() / "prompts" / "coding-preflight"
51
+
52
+
53
+ def _errors_log_path(
54
+ project_root: Path,
55
+ manifest: Mapping[str, Any],
56
+ active_context: Mapping[str, Any],
57
+ ) -> Path:
58
+ error_logs = active_context.get("errorLogs")
59
+ if isinstance(error_logs, Mapping):
60
+ value = _string_value(error_logs.get("runErrorsLogPath"))
61
+ if value:
62
+ return _resolve_project_path(project_root, value)
63
+ run_dir = _run_directory_path(project_root, manifest)
64
+ task_type = _require_string(manifest, "taskType")
65
+ seq = _sequence(manifest, "state")
66
+ return run_dir / "logs" / f"errors-{task_type}-{seq}.jsonl"
67
+
68
+
69
+ def _worker_errors_sidecar_path(
70
+ project_root: Path,
71
+ manifest: Mapping[str, Any],
72
+ active_context: Mapping[str, Any],
73
+ worker_id: str,
74
+ ) -> Path:
75
+ error_logs = active_context.get("errorLogs")
76
+ if isinstance(error_logs, Mapping):
77
+ sidecars = error_logs.get("sidecarsByWorkerId")
78
+ if isinstance(sidecars, Mapping):
79
+ value = _string_value(sidecars.get(worker_id))
80
+ if value:
81
+ return _resolve_project_path(project_root, value)
82
+ run_dir = _run_directory_path(project_root, manifest)
83
+ task_type = _require_string(manifest, "taskType")
84
+ seq = _sequence(manifest, "workerResults")
85
+ return run_dir / "worker-results" / f"{worker_id}-worker-errors-{task_type}-{seq}.json"
86
+
87
+
88
+ def _run_directory_path(project_root: Path, manifest: Mapping[str, Any]) -> Path:
89
+ value = _string_value(manifest.get("runDirectoryPath"))
90
+ if value:
91
+ return _resolve_project_path(project_root, value)
92
+ team_state_path = _resolve_project_path(
93
+ project_root,
94
+ _require_string(manifest, "teamStatePath"),
95
+ )
96
+ return team_state_path.parent.parent
97
+
98
+
99
+ def _sequence(manifest: Mapping[str, Any], key: str) -> str:
100
+ seqs = manifest.get("runSequencesByCategory")
101
+ if isinstance(seqs, Mapping):
102
+ return _string_value(seqs.get(key)) or _run_seq(manifest)
103
+ return _run_seq(manifest)
104
+
105
+
106
+ def _run_seq(manifest: Mapping[str, Any]) -> str:
107
+ seqs = manifest.get("runSequencesByCategory")
108
+ if not isinstance(seqs, Mapping):
109
+ raise WorkerPromptHeaderError("run manifest has no runSequencesByCategory object")
110
+ return _require_string(seqs, "manifests")
111
+
112
+
113
+ def _resolve_project_path(project_root: Path, value: str) -> Path:
114
+ path = Path(value)
115
+ return path if path.is_absolute() else project_root / path
116
+
117
+
118
+ def _require_string(payload: Mapping[str, Any], key: str) -> str:
119
+ value = payload.get(key)
120
+ if not isinstance(value, str) or not value.strip():
121
+ raise WorkerPromptHeaderError(f"missing required string field: {key}")
122
+ return value
123
+
124
+
125
+ def _string_value(value: Any) -> str:
126
+ return value.strip() if isinstance(value, str) else ""
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  from .cursor import MAX_NEEDLES, fresh_cache, load_cache, save_cache
10
10
  from .paths import claude_project_dir, ts_in_window
11
11
 
12
- # lead 의 단계 체크포인트 라인(agents/SKILL.md "Progress reporting") — phase id 가
12
+ # lead 의 단계 체크포인트 라인(prompts/lead/okstra-lead-contract.md "Progress reporting") — phase id 가
13
13
  # `phase-<digit>` 로 시작하는 라인만 마커로 인정해 일반 대화의 오탐을 줄인다.
14
14
  _PROGRESS_RE = re.compile(r"^PROGRESS:\s+(phase-\d\S*(?:[ \t].*)?)$", re.MULTILINE)
15
15
  _PROGRESS_MARKER_MAX_CHARS = 160
@@ -159,7 +159,7 @@ def _wall_ms(start_iso: str | None, end_iso: str | None) -> int | None:
159
159
 
160
160
 
161
161
  def phase_timeline(markers: list[dict]) -> dict:
162
- """lead 의 PROGRESS 체크포인트(agents/SKILL.md "Progress reporting")로
162
+ """lead 의 PROGRESS 체크포인트(prompts/lead/okstra-lead-contract.md "Progress reporting")로
163
163
  run 내부 단계 경계 wall-clock 을 복원한다 (perf plan v2 P0 계측).
164
164
 
165
165
  phase id = marker 의 첫 토큰(`phase-5.5-convergence` 등). 같은 phase 의
@@ -145,7 +145,7 @@ If the user picked `Reporter input`, proceed to Step 1.A's sub-flow below — th
145
145
  - **Label**: "Source of this brief?"
146
146
  - **Options** (single-select):
147
147
  1. `File` — path to an existing markdown / txt / issue-export file
148
- 2. `Issue tracker ticket` — Linear / Jira / GitHub Issue / Notion key or URL
148
+ 2. `Issue tracker ticket` — Linear URL/ Jira URL/ GitHub Issue URL/ Notion key or URL
149
149
  3. `Link URL` — general web page, blog, spec doc, design doc, etc.
150
150
  4. `User input` — synthesize from the current conversation context, or
151
151
  accept a short free-text from the user
@@ -678,7 +678,7 @@ Use the same template per brief file. In tracker mode producing a parent
678
678
  plus N children, you write **N+1 files** (each carries only its own Source
679
679
  Material).
680
680
 
681
- [`templates/reports/brief.template.md`](../../templates/reports/brief.template.md) is the byte-for-byte SSOT for the brief shape — frontmatter keys, top-header blockquote, section ordering, and inline `<!-- author guidance -->` HTML comments. Render that file with the per-brief values substituted; leave any section's body as `_(none)_` rather than fabricating content. Preserve the template's HTML comments verbatim (they are part of the contract) but do NOT promote them to body prose.
681
+ The installed template `~/.okstra/templates/reports/brief.template.md` is the byte-for-byte SSOT for the brief shape — frontmatter keys, top-header blockquote, section ordering, and inline `<!-- author guidance -->` HTML comments. Render that file with the per-brief values substituted; leave any section's body as `_(none)_` rather than fabricating content. Preserve the template's HTML comments verbatim (they are part of the contract) but do NOT promote them to body prose.
682
682
 
683
683
  **Variant note:** The template file is the canonical reporter-input shape. For the codebase-scan variant (`scope: codebase` in frontmatter), OMIT the `## Source Material` and `## Problem / Symptom` headings entirely — do NOT include them with `_(none)_` bodies. Instead, KEEP the `## Scan Scope` and `## Priority Lenses` sections (already present in the template file, marked with HTML comments). The remaining sections (Context / Desired Outcome / Constraints / Related Artifacts / Open Questions / Reporter Confirmations / Augmentation) apply to both variants.
684
684
 
@@ -710,6 +710,22 @@ Material).
710
710
  frontmatter.
711
711
  - `ticket-id` is populated only for tracker sources; leave it as an empty
712
712
  string otherwise.
713
+ - `source-type` records the input origin (`file | linear | jira | github |
714
+ notion | url | user-input`).
715
+ - `task-group` echoes the Step 2a value.
716
+ - `created` is the brief's creation date (`YYYY-MM-DD`).
717
+ - `generator: okstra-brief` is a fixed value.
718
+ - `reporter-confirmations` is set by Step 6.5 (`complete | partial | pending |
719
+ skipped`).
720
+ - `scope` is `codebase` for the codebase-scan variant; omit it (⇒
721
+ `reporter-input`) for the reporter-input variant.
722
+
723
+ The required-key set (`type`, `brief-id`, `parent-id`, `ticket-id`,
724
+ `source-type`, `task-group`, `depth`, `created`, `generator`,
725
+ `reporter-confirmations`) and every consistency rule above are enforced by
726
+ `~/.okstra/lib/validators/validate-brief.py` — run in
727
+ Step 6.6. The byte-for-byte field shape remains the job of
728
+ `~/.okstra/templates/reports/brief.template.md`.
713
729
 
714
730
  Echo the file path back on one line. Show the rendered brief to the user
715
731
  inline and ask:
@@ -750,8 +766,17 @@ cannot resolve:
750
766
  - `intent-check:` (auto-mirrored from `intent-inference` augmentations)
751
767
  - `conversion-block:` (translation failed in Step 3c)
752
768
 
753
- If the resulting list is empty, set `reporter-confirmations: complete` in
754
- the brief's frontmatter and skip the rest of this step.
769
+ **Re-run reseed (rebuild from disk).** A prior run may have left this brief at
770
+ `reporter-confirmations: partial` with some rows already answered. Before
771
+ building the pending list, exclude every `intent-check:` / `conversion-block:`
772
+ row that already carries a `[CONFIRMED <date> → RC-N]` marker — those were
773
+ resolved in an earlier run and their answers already live under `## Reporter
774
+ Confirmations`. Only the unmarked rows are pending this run. This mirrors the
775
+ visited-set rebuild rule in Step 1b: confirmation state lives on disk, not in
776
+ memory.
777
+
778
+ If the resulting pending list is empty, set `reporter-confirmations: complete`
779
+ in the brief's frontmatter and skip the rest of this step.
755
780
 
756
781
  ### 6.5b. Ask once: collect now or later
757
782
 
@@ -839,6 +864,37 @@ rows (`complete` / `partial` with remainder in the same file) or an
839
864
  explicit `skipped` signal that downstream phases consume per the
840
865
  "Brief consumption" addendum in `_common-contract.md`.
841
866
 
867
+ ## Step 6.6: Self-validate before hand-off
868
+
869
+ After every brief is written (Step 5) and its `reporter-confirmations` state
870
+ is set (Step 6.5), and **before** printing the Step 7 hand-off message, you
871
+ MUST validate every brief produced this run. The validator enforces the
872
+ labelled-handoff contract — required frontmatter keys, Augmentation labels,
873
+ `intent-inference ↔ intent-check:` auto-mirroring, `parent-id`/`depth`
874
+ consistency, `reporter-confirmations` markers, and the codebase-scan
875
+ `scope`/`Scan Scope`/`Priority Lenses` rules.
876
+
877
+ 1. Run the validator over the briefs directory:
878
+
879
+ ```bash
880
+ ~/.okstra/lib/validators/validate-brief.sh "<PROJECT_ROOT>/.okstra/briefs" --briefs-root "<PROJECT_ROOT>/.okstra/briefs"
881
+ ```
882
+
883
+ If you are working from a repo checkout instead of an install, the dev
884
+ copy is at `validators/validate-brief.sh`.
885
+
886
+ 2. If it exits non-zero, **fix the offending brief(s) and re-run**. Do NOT
887
+ print the Step 7 message until the validator passes.
888
+
889
+ 3. If neither validator copy is present, fall back to a manual checklist:
890
+ confirm each brief has all ten required frontmatter keys (`type`,
891
+ `brief-id`, `parent-id`, `ticket-id`, `source-type`, `task-group`,
892
+ `depth`, `created`, `generator`, `reporter-confirmations`); every
893
+ Augmentation entry carries one of the four labels; every `intent-inference`
894
+ has a paired `intent-check:` row; `parent-id` is `self` at depth 0 and a
895
+ real parent id below; and for `scope: codebase` briefs, `Scan Scope` is
896
+ non-empty and `Priority Lenses` lists 1–4 whitelist lenses.
897
+
842
898
  ## Step 7: Hand off
843
899
 
844
900
  Single brief:
@@ -866,13 +922,10 @@ started.
866
922
 
867
923
  ## Output Rules
868
924
 
869
- - All okstra-generated artifacts live under
870
- `<PROJECT_ROOT>/.okstra/`. This skill's brief files go
871
- under `.okstra/briefs/`; its glossary writes go to
872
- `.okstra/glossary.md` (Step 4.5 approval flow).
873
- - Paths outside `<PROJECT_ROOT>/.okstra/**` are read-only
874
- source material only when explicitly cited by the reporter. This skill
875
- never writes outside okstra's subtree.
925
+ - **Artifact-home** (see the "Artifact-home rule" section above): every write
926
+ stays under `<PROJECT_ROOT>/.okstra/` briefs in `.okstra/briefs/`, glossary
927
+ in `.okstra/glossary.md` (Step 4.5). Paths outside that subtree are read-only
928
+ source material, and only when the reporter cited them.
876
929
  - **Verbatim source**: never paraphrase, summarize, restructure, or reorder
877
930
  the Source Material section. Only format conversion (ADF → MD, HTML → MD)
878
931
  is allowed, and the conversion must be annotated in the `format:` meta.
@@ -910,3 +963,4 @@ started.
910
963
  | URL fetch fails or 401 / login wall | intranet, paywall, JS-rendered page | Ask the user to paste the core body. Never guess. |
911
964
  | Tracker body is in non-MD format (ADF / HTML) | Jira description, Notion blocks | Convert format only, preserve semantics. Annotate the conversion in the `format:` meta. |
912
965
  | Brief comes out skeletal after synthesis | source too thin AND user picked `User input` with little context | Switch to inline input mode and ask one or two targeted questions. |
966
+ | `validate-brief` exits non-zero in Step 6.6 | missing frontmatter key, unlabelled augmentation, broken auto-mirroring, or codebase-scan lens out of whitelist | Read the validator's per-row message, fix the cited brief, re-run. Never print the Step 7 hand-off until it passes. |
@@ -360,7 +360,7 @@ If a run never reached Phase 7, its `team-state` lacks `durationMs`. Mark such r
360
360
  3. Multiple matches → list candidates (`taskKey`, `taskType`, `updatedAt`) and ask the user to pick.
361
361
  4. Read `historyTimelinePath` from the chosen entry.
362
362
 
363
- If `task-catalog.json` is missing: `No okstra history found. Run scripts/okstra.sh first.`
363
+ If `task-catalog.json` is missing: `No okstra history found. Run /okstra-run first.`
364
364
 
365
365
  ### time.2 — Walk runs and collect durations
366
366
 
@@ -236,7 +236,7 @@ The validator rejects any of these patterns.
236
236
 
237
237
  ## Section Contract (required)
238
238
 
239
- The canonical template lives at `templates/reports/schedule.template.md`. **Read it before writing the schedule** and follow it byte-for-byte for headings.
239
+ The canonical template lives at `~/.okstra/templates/reports/schedule.template.md`. **Read it before writing the schedule** and follow it byte-for-byte for headings.
240
240
 
241
241
  ### MUST — exact ordered heading list
242
242
 
@@ -441,14 +441,14 @@ After writing the file and before printing the completion message in Step 7, you
441
441
  1. **Re-read the file** you just wrote.
442
442
  2. **Run the validator**:
443
443
  ```bash
444
- python3 validators/validate-schedule.py <output-path>
444
+ python3 ~/.okstra/lib/validators/validate-schedule.py <output-path>
445
445
  ```
446
446
  3. If the validator exits non-zero, **fix the file and re-validate**. Do not print the completion message until the validator passes.
447
- 4. If `validators/validate-schedule.py` is not present in the repo, fall back to a manual checklist: confirm each of the 11 mandatory headings from `### MUST — exact ordered heading list` (above) appears in the file with that exact spelling, the title ends with `— Work Schedule`, and the metadata `> Generated:` block is present.
447
+ 4. If `~/.okstra/lib/validators/validate-schedule.py` is not present, fall back to a manual checklist: confirm each of the 11 mandatory headings from `### MUST — exact ordered heading list` (above) appears in the file with that exact spelling, the title ends with `— Work Schedule`, and the metadata `> Generated:` block is present.
448
448
 
449
449
  ## Schedule Document Template
450
450
 
451
- [`templates/reports/schedule.template.md`](../../templates/reports/schedule.template.md) is the byte-for-byte SSOT for section ordering, heading spelling, table columns, and per-section HTML-comment guidance (Dependency Graph Shape A/B, Gantt rules, optional Glossary gate). Do not omit sections; if data is unavailable, render the section with `_없음_` rather than skipping it.
451
+ The installed template `~/.okstra/templates/reports/schedule.template.md` is the byte-for-byte SSOT for section ordering, heading spelling, table columns, and per-section HTML-comment guidance (Dependency Graph Shape A/B, Gantt rules, optional Glossary gate). Do not omit sections; if data is unavailable, render the section with `_없음_` rather than skipping it.
452
452
 
453
453
  The two rules below complement the template — they encode COMPUTATION that the template scaffold cannot carry inline.
454
454
 
@@ -250,8 +250,8 @@ back it up as `.bak.<timestamp>` rather than overwriting silently.
250
250
  ## Step 4.8 (optional, opt-in): register a project PR body template
251
251
 
252
252
  `release-handoff` fills the PR body from a template. By default it uses the
253
- bundled skill template at
254
- `~/.claude/skills/templates/prd/pr-body.template.md`. Most projects
253
+ installed template at
254
+ `~/.okstra/templates/prd/pr-body.template.md`. Most projects
255
255
  want their own — e.g. the repo's `.github/PULL_REQUEST_TEMPLATE.md` — to
256
256
  keep PRs consistent with what the team already merges manually.
257
257