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.
- package/README.kr.md +7 -6
- package/README.md +6 -6
- package/docs/kr/architecture.md +8 -7
- package/docs/kr/cli.md +2 -2
- package/docs/kr/performance-improvement-plan-v2.md +14 -14
- package/docs/project-structure-overview.md +7 -8
- package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +504 -0
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +342 -0
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +258 -0
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +387 -0
- package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +749 -0
- package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +828 -0
- package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +490 -0
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/error-analysis.md +1 -1
- package/docs/task-process/final-verification.md +1 -1
- package/docs/task-process/implementation-planning.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/docs/task-process/requirements-discovery.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/TODO.md +2 -0
- package/runtime/agents/workers/claude-worker.md +8 -8
- package/runtime/agents/workers/codex-worker.md +8 -8
- package/runtime/agents/workers/gemini-worker.md +8 -8
- package/runtime/agents/workers/report-writer-worker.md +2 -2
- package/runtime/bin/lib/okstra/globals.sh +0 -1
- package/runtime/bin/okstra-wrapper-status.py +1 -1
- package/runtime/prompts/launch.template.md +5 -3
- package/runtime/{skills/okstra-context-loader/SKILL.md → prompts/lead/context-loader.md} +7 -14
- package/runtime/{skills/okstra-convergence/SKILL.md → prompts/lead/convergence.md} +12 -19
- package/runtime/{agents/SKILL.md → prompts/lead/okstra-lead-contract.md} +53 -59
- package/runtime/{skills/okstra-report-writer/SKILL.md → prompts/lead/report-writer.md} +12 -19
- package/runtime/{skills/okstra-team-contract/SKILL.md → prompts/lead/team-contract.md} +13 -19
- package/runtime/{skills → prompts}/okstra-coding-preflight/languages/python.md +2 -2
- package/runtime/{skills → prompts}/okstra-coding-preflight/languages/rust.md +1 -1
- package/runtime/{skills/okstra-coding-preflight/SKILL.md → prompts/okstra-coding-preflight/overview.md} +27 -38
- package/runtime/prompts/okstra-coding-preflight/scripts/preedit-check.sh +79 -0
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +2 -2
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +2 -2
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +4 -4
- package/runtime/prompts/profiles/requirements-discovery.md +2 -2
- package/runtime/python/okstra_ctl/codex_dispatch.py +12 -61
- package/runtime/python/okstra_ctl/context_cost.py +14 -11
- package/runtime/python/okstra_ctl/dispatch_core.py +36 -13
- package/runtime/python/okstra_ctl/paths.py +27 -1
- package/runtime/python/okstra_ctl/render.py +62 -8
- package/runtime/python/okstra_ctl/run.py +8 -6
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +126 -0
- package/runtime/python/okstra_token_usage/claude.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +1 -1
- package/runtime/skills/okstra-brief/SKILL.md +65 -11
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-schedule/SKILL.md +4 -4
- package/runtime/skills/okstra-setup/SKILL.md +2 -2
- package/runtime/templates/reports/settings.template.json +14 -123
- package/runtime/templates/reports/task-brief.template.md +2 -2
- package/runtime/templates/worker-prompt-preamble.md +2 -2
- package/runtime/validators/lib/validate-assets.sh +12 -4
- package/runtime/validators/validate-brief.py +99 -16
- package/runtime/validators/validate-run.py +3 -3
- package/runtime/validators/validate_session_conformance.py +11 -11
- package/src/install.mjs +95 -81
- package/src/skill-catalog.mjs +35 -0
- package/src/uninstall.mjs +5 -0
- /package/runtime/{skills/okstra-coding-preflight/architecture → prompts/okstra-coding-preflight/architectures}/hexagonal.md +0 -0
- /package/runtime/{skills → prompts}/okstra-coding-preflight/clean-code.md +0 -0
- /package/runtime/{skills/okstra-coding-preflight/languages/nodejs.md → prompts/okstra-coding-preflight/frameworks/node-server.md} +0 -0
- /package/runtime/{skills → prompts}/okstra-coding-preflight/languages/java.md +0 -0
- /package/runtime/{skills → prompts}/okstra-coding-preflight/languages/javascript-typescript.md +0 -0
- /package/runtime/{skills → prompts}/okstra-coding-preflight/languages/kotlin.md +0 -0
- /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
|
-
|
|
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 `
|
|
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
|
-
'
|
|
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.
|
|
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
|
-
"
|
|
1947
|
-
"metadata, paths, model assignments, and
|
|
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 `
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 의 단계 체크포인트 라인(
|
|
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 체크포인트(
|
|
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
|
-
|
|
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
|
-
|
|
754
|
-
|
|
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
|
-
-
|
|
870
|
-
`<PROJECT_ROOT>/.okstra
|
|
871
|
-
|
|
872
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
`~/.
|
|
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
|
|